Tree
- Tree:
99e3bcbd7c8563240f932bb9ec2897772ff2a193- Date:
- Message:
- subscriptions, large messages, lock timeout, login grace, logging, IDLE SUBSCRIBE and UNSUBSCRIBE are implemented, and LIST takes the SUBSCRIBED selection option of RFC 9051 section 6.3.9.1. Both commands used to reply NO, and LSUB returned every mailbox the account had, because no subscription state was kept at all: the deprecated command worked and its replacement was missing. Subscriptions now live in one file per account, imapd.subscriptions, at the maildir root beside imapd.uidvalidity, one mailbox name per line. An absent file means every mailbox is subscribed. Only UNSUBSCRIBE creates one, and the first UNSUBSCRIBE writes out every other mailbox on its way past, so upgrading from 0.1.4 cannot leave an account subscribed to nothing, and a client that hides unsubscribed mailboxes cannot come back to an empty tree. A file that cannot be read is treated the same way, so damage shows too many mailboxes rather than too few. INBOX is never named in the file: it always exists and cannot be deleted, so it is permanently subscribed and UNSUBSCRIBE INBOX replies NO. A name outlives its mailbox, which RFC 9051 section 6.3.8 requires. LIST (SUBSCRIBED) reports such a name \Subscribed \NonExistent, and LSUB reports it \Noselect, which is RFC 3501 section 6.3.9's spelling of the same fact. A plain LIST reports no attributes at all and never opens the subscription file, so the request a client makes on every connection costs what it did before any of this existed. Neither CREATE nor RENAME changes the file; what is subscribed is left to the client to say, and the clients tested say it. Unsupported LIST selection options, REMOTE and RECURSIVEMATCH, are refused rather than ignored, since accepting one silently would misreport which names the response contains. A mailbox name the server refuses now draws NO rather than BAD. RFC 9051 section 7.1.3 reserves a tagged BAD for a protocol-level error in the client's command, and these are not errors of that kind: SELECT "" parses, since an empty quoted string is well-formed, and so does a name carrying the hierarchy delimiter. Each of SELECT, EXAMINE, CREATE, RENAME, SUBSCRIBE and COPY carries an explicit "NO - ... that name" in its own result table. Five sites changed: NO [NONEXISTENT] for SELECT's empty name, which is what the store's own no-such-mailbox reply already said, and NO [CANNOT] for the rest, which is what the non-UTF-8 refusal one line above four of them already said. BAD is untouched wherever a command genuinely fails to parse. Apple Mail sends SELECT "" before renaming a selected mailbox, which is how this surfaced. imapd now logs to LOG_MAIL rather than LOG_DAEMON. Upgrading from 0.1.4, this is the change that needs attention: lines that landed in /var/log/messages now land in /var/log/maillog under the rule set syslog.conf(5) documents, so anyone routing daemon.* somewhere particular will need to adjust. The syslog tag is now the daemon name rather than the per-process role, it read "listener[29527]:" before, naming no daemon at all, with the role moved into the message text, where smtpd and ntpd keep theirs. imapd.8 states the facility. Every session now gets one connect line and one close line at LOG_INFO, where a default-verbosity daemon previously said nothing about any session it served. The close line carries the peer address, the authenticated user where there was one, the duration, whether the session ended up TLS-protected, and why it closed: one of logout, client-closed, io-error, tls-error, protocol-error, limit-exceeded, login-grace or shutdown. The connect line cannot report the first of those, since it describes how the connection was accepted and a port-143 session may issue STARTTLS afterwards. ps(1) now shows one line per session, "imapd: session 42 [authenticated]" beside "session 42 auth", "session 42 search" and "session 42 store", since the daemon forks a worker group per connection. Titles carry the session id and lifecycle stage only, never the username or peer address: a process title is readable by any local user, and unlike an ssh login an IMAP login is not otherwise visible locally. Identity is recovered by joining the session id to the log, which is not world-readable. The parent is deliberately left untitled, since rc.d finds a daemon by matching the command line setproctitle(3) overwrites, and a titled parent could not be stopped by rcctl. rcctl stop now stops the daemon. The parent used to signal every child and exit without waiting, so it returned before they were gone and every open session lost its record at the moment an operator would most want one. It now closes each listener worker's imsg channel, which that worker takes as the cue to tear its own session down: the store child is asked to exit through a message it already understood rather than shot, the client gets its TLS close_notify, and the session gets a close line with reason=shutdown. The parent then waits for the last child, bounded at five seconds, after which anything still there is killed and counted; a second SIGTERM skips the wait. keymgr is asked to exit rather than signalled, so a bare EOF still means the parent died, and it frees the loaded TLS key on the way out. Starting as a non-root user now says so, and names the uid. It previously failed reading the root-only imapd.conf and reported that instead, naming the wrong problem entirely. Under -d, a log line is now written with one write(2), so concurrent sessions no longer interleave mid-line. The store child no longer addresses mailboxes by changing its working directory. It chdir(2)s once at startup, to the account's maildir root, and afterwards names every mailbox by an open descriptor: one for the root, one for the SELECTed mailbox, and a temporary one for any command that visits a second mailbox. The commands that used to save the working directory, switch away and switch back do none of those things now, and the thirteen places that switched back are gone. Eleven of those thirteen only logged a failure to switch back, and nothing else re-established the directory. A session whose switch back failed therefore kept serving with the client's selection naming one mailbox and the process standing in another, so the next FETCH read the wrong mailbox and the next EXPUNGE deleted from it, both under a tagged OK. Triggering it needs no bug, only a second session renaming or deleting the mailbox between the switch away and the switch back, which two ordinary IMAP commands arrange; done that way it destroyed every message in a mailbox the client had never named. A regression test does exactly that and is in the suite. A COPY into a mailbox another session renames mid-command now succeeds, where before it failed and left the session working in the destination. MOVE loses the error path that reported messages duplicated in both mailboxes because it could not get back to the source, since there is no longer anything to get back to. The can't-happen truncation recovery in the old switching function is gone with the function, having been unable to do what it claimed: it walked into the truncated name it had just written. unveil(2) covers a lookup relative to a descriptor exactly as it covers one relative to the working directory, and every *at call this uses is already permitted by the store's pledge. A FETCH that cannot return an item it was asked for now ends in NO rather than OK. A message whose body could not be read came back as a bare "* 1 FETCH (UID 42)" under a tagged OK, which a client can only take to mean the message has no body. RFC 9051 section 6.4.5 gives FETCH the result "NO - fetch error: can't fetch that data". The untagged responses for whatever could be produced are still sent, and a section-part the message does not have is still an empty item rather than a failure. A corrupt index can no longer widen a UID sequence set. The function that finds the highest UID, which resolves "*" for UID FETCH, STORE, EXPUNGE, COPY, MOVE and SEARCH, parsed the index's last line with its own code and no sign check, so a last line of "-1:name::1" made "*" 4294967295 and "1:*" the whole UID space; for STORE, EXPUNGE, COPY and MOVE that decides which messages are changed. It now uses the index's one line parser. That parser, and the header's UIDVALIDITY and UIDNEXT, now refuse a value above 4294967295 instead of truncating it: a UIDNEXT of 4294967297 had become 1, so the next APPEND would have reused UIDs already given out, which RFC 9051 section 2.3.1.1 does not allow. The index lives in the user's own maildir, which is why it is checked at all. A connection that never authenticates is now closed, after 60 seconds by default, set by a new imapd.conf directive, login grace, from 1 to 3600 seconds or 0 to disable. Every accepted connection costs three processes and nothing closed one that completed TCP and then sent nothing, so about a hundred silent connections reached the startups full limit and every later client was refused. The timer covers a connection on the implicit-TLS port that never starts its handshake as well as one on port 143 that never sends a command, and is cancelled the moment a session authenticates. RFC 9051 section 5.4 permits exactly this: servers "are allowed to use a shortened pre-authentication timer to protect themselves from Denial-of-Service attacks". It is not an autologout for authenticated sessions, which imapd does not have. Such a connection closes with reason=login-grace. A STORE or EXPUNGE that answers NO now changes nothing. System flags live in the maildir filename and keywords and modseq in the index, and both commands used to change files message by message and save the index once at the end, reporting each message to the client as they went. A STORE whose merged keyword set did not fit on a later message was refused with earlier messages already renamed: their system flags changed on disk while their keywords and modseq were discarded, so the client was shown flags that were never stored, and a client syncing by CONDSTORE modseq never learned of the change that did stick. STORE now works out every message before renaming any, undoes its renames if a later rename or the index save fails, and reports only after the save. EXPUNGE told the client each message was gone before saving the index, and RFC 9051 section 7.5.1 has the client renumber on each such response. If the save then failed, client and server numbered messages differently for the rest of the session, so a later STORE and EXPUNGE by sequence number could delete a message the user never chose, and the index kept entries for files already unlinked, which nothing ever removed and which made any COPY including them fail. EXPUNGE now saves the index first, then removes the files, then reports. Neither command reports a HIGHESTMODSEQ the index does not hold, and a failed command no longer moves the value a session later reports on ENABLE CONDSTORE. Finding a message's file no longer rescans cur/. Every message access looked for new/<name> and then read cur/ until it matched, and APPEND and STORE both leave messages in cur/, so FETCH 1:* (FLAGS) over 10,000 messages did 10,000 scans of 10,000 entries and took 19.9 s. The store now reads cur/ once per command into a sorted array; a miss still falls back to a real scan, so the snapshot can cost a lookup but never change an answer. The same FETCH takes 0.87 s. A large FETCH now starts answering at once. The store used to compose the reply for every matching message before sending any of it, so FETCH 1:* (BODY.PEEK[]) over 10,000 messages waited 1.46 s for its first line and grew the store child by 92 MB. It now pauses after composing 1 MB, resumes when that has drained, and answers the first line in 0.06 s with the store growing 3.7 MB. A message larger than 12000 octets can now be uploaded and downloaded. APPEND used to refuse one with NO [LIMIT] and FETCH returned no BODY[] for one, so a client could neither save nor read an ordinary message with an attachment. The cap was there because a message travelled between the store child and the listener inside one imsg, which libutil bounds at 16384 octets. APPEND now streams. The listener tells the store to open the tmp/ file when the literal is announced, passes the literal on in pieces as it arrives, and tells the store to commit once the command's closing CRLF is in, so neither process holds the whole message. A failure part way through is reported when the command ends, and a session that dies mid-upload leaves no tmp/ file behind. A new imapd.conf directive, append max, bounds what APPEND accepts, 35 MiB by default, the same as the max-message-size default of smtpd.conf(5). A larger literal draws NO [LIMIT] stating the limit as a number; the old reply had an unmatched parenthesis and named a C macro. imapd refuses a configuration whose append max exceeds attachment max, since a message larger than that could be stored but its BODYSTRUCTURE never fetched. FETCH of BODY[], BODY[TEXT] and BODY[<part>] no longer copies the message into an imsg. The store finds the octet range, refusing a message that contains NUL, which a literal cannot carry (RFC 9051 section 9, CHAR8), and passes the listener a read-only descriptor on the message file with an offset and length; the listener reads and writes and parses nothing. The store's pledge gains sendfd, and the listener already held recvfd. A FETCH walk also pauses after passing 16 descriptors, each of which holds a slot in the system-wide file table until it is sent, and a store that runs out of descriptors with some of its own still queued waits and retries the message rather than failing it. On premio, a 10 W Celeron J1900, FETCH 1:* (BODY.PEEK[]) over 1,000 messages of 64 KB answered in 1.97 s, with the store growing 260 KiB and holding at most 13 descriptors in flight. The partial-fetch clamp at 12000 octets is gone with the cap. BODY[]<0.1000000> of a large message used to report the text as 12000 octets long, which RFC 9051 section 6.4.5 does not allow: it truncates a partial fetch at the end of the text, not at a server limit. Nothing in src/ exceeds 80 columns; 750 lines did, across 29 of 33 files. IDLE now reports flag changes, and a refresh costs what the change cost rather than what the mailbox holds. A client in IDLE heard about new mail and about expunges, but never about a flag another session set, so a message marked read on a phone stayed unread on a desktop until that desktop asked. RFC 9051 section 6.3.13 names flag changes among the things IDLE exists to report. The server now sends an untagged FETCH carrying the UID and the flags for a message whose flags changed elsewhere, and the mod-sequence as well once the session has issued a CONDSTORE enabling command, which RFC 7162 section 3.1 requires. A message that arrived since the last poll is still announced by EXISTS alone, since its flags are news to nobody. The refresh behind IDLE also stopped sending the whole mailbox. The store child used to send the listener one imsg per message on any change, and the listener compared that list against the previous one by rescanning it from the start for each message, which is quadratic. The store child now keeps the list it last reported, compares in a single walk of two ascending lists, and sends only what the listener is to print: the EXPUNGE sequence numbers, the new count, and the flag changes. Nothing about the mailbox is kept in the listener any more. On premio, a 10 W Celeron J1900, with a mailbox of 100,000 messages and a session idling on it, the work behind one push fell from between 6.33 and 7.33 seconds to under 0.52 seconds. At 10,000 messages it was already under 0.12 seconds. What remains at 100,000 is reading and parsing the index, not comparing it. testing/imap_idle_flags_test.py is new and joins the regression set: two sessions on one scratch mailbox, one idling, with the flag push asserted and an APPEND and an EXPUNGE as controls, and a third session that selects with the CONDSTORE parameter to assert the mod-sequence is there for it and absent for the session that did not ask. testing/idle_diff_test.c, the standalone test for the sequence-number diff, now carries both the old rescan and the new walk and checks every case through both, including RFC 9051 section 6.4.3's EXPUNGE example and section 6.3.13's IDLE transcript. An IDLE refresh that gives up no longer loses the change it was about to report. The cheap probe in front of the refresh records the mailbox directory's inode and modification time whether or not its caller then succeeds, so a refresh that failed after the probe had said "something moved" had already consumed that change, and the next poll found nothing to report. The client was never told. The refresh now clears the probe on any reply it does not mark ok, so the following poll looks properly. Reachable today only through an index read or an allocation failing, which is why nothing exercises it. SELECT no longer moves this session's state before it knows the selection succeeded. It closed the old mailbox descriptor, installed the new one and recorded the new name, and only then took the lock and read the index, so a failure in either left the store child's descriptor and recorded name pointing at a mailbox whose SELECT had failed. The gate that guards the commands using that descriptor was never at risk: store_dispatch() clears mailbox_selected before dispatching a SELECT, so FETCH, STORE, EXPUNGE, SEARCH, COPY, MOVE and the IDLE refresh are all refused after one fails. What was left was the recorded name, which DELETE and RENAME read without consulting that gate, so either could act on bookkeeping for a mailbox this session never selected. The descriptor and the name now move with the gate, after the index has been read, and every failure before that point leaves all three alone. Not exercised by a test. Reaching the interesting failures needs the index read or the lock to fail, which a client cannot arrange, and the one failure a client can arrange returns before any of this. Waiting for another session's mailbox index lock is now bounded. The lock was taken with a blocking flock(2) and nothing anywhere gave up, so a session waited as long as the holder took. Measured three times on a 100,000 message mailbox: an ordinary STORE 1:* holds the lock for about 46 seconds, and a second session of the same user issuing a one-message STORE was blocked for about 45 of those, with no error and nothing to show for it. Two clients on one account is ordinary, so this is the common case rather than a corner. index_lock_acquire() grows a third answer for a caller that adds LOCK_NB: another process holds it. That is an ordinary outcome rather than a failure, so it is not logged, and blocking callers cannot see it because flock(2) does not report EWOULDBLOCK without LOCK_NB. A command whose handler cannot take the lock is kept by the store child and run again from the top on a timer, backing off from 50ms to a cap of 250ms, until it can or until the new "lock timeout" passes. That cap is also the worst case delay between the lock coming free and the command noticing; at it, a command waiting behind a 46 second STORE costs about 190 wakeups, four a second. One slot is enough, since the listener holds a session's next command while one is in flight. The child never blocks, so it keeps serving its listener while it waits. Re-running rather than resuming means a handler that defers must not have touched the mailbox or the session first, which is checked per handler. At the deadline the command answers NO with the RFC 9051 section 7.1 INUSE response code, whose description is this case exactly: someone else may be holding an exclusive lock needed for this operation, and the operation may succeed if the client tries again later. The default is deliberately generous at 120 seconds, because it is a safety net for a holder that is stuck rather than a cure for one that is slow: an ordinary command over a large mailbox legitimately runs for the better part of a minute, and a deadline under that would refuse ordinary concurrent use. A client that does not retry INUSE would discard the user's change with nothing on screen, which is worse than waiting. Setting it to 0 restores the unbounded wait. Every command that takes a mailbox's index lock is served this way: STORE, EXPUNGE, CLOSE, FETCH, SEARCH, SELECT, STATUS, COPY, MOVE and APPEND, each answered with its own reply type so that the client sees NO [INUSE] rather than a generic failure or "no such mailbox". What each handler has to clean up before returning busy differs by command: SELECT and STATUS close the directory they opened, FETCH takes the lock before resetting its walk, and APPEND skips the fsync(2) an earlier try already did and removes its tmp/ file when it gives up. COPY and MOVE still take their two locks in name order, but when the second is busy they now release the first as well and start again from nothing, since the command is run again from the top. Holding the first while waiting would keep every other session off that mailbox for as long as the deadline, and the ordering already rules out the deadlock that releasing might otherwise risk. A CLOSE that fails now leaves the mailbox selected. It removes nothing unless its index save succeeds, so a NO always meant the mailbox was untouched, yet the session was deselected anyway, and a client retrying the CLOSE as the INUSE text invites got BAD. The listener and the store child now both deselect only on success. The IDLE refresh skips a poll when the lock is busy, and the next poll tries again. The first refresh of an IDLE cannot skip: it records the mailbox as the client now knows it, and without it the next poll would compare against an older record and resend EXPUNGE responses the client has already processed, renumbering its view wrongly (RFC 9051 section 7.5.1). So a busy first refresh reads the committed index without the lock, which is safe because index_save() only ever replaces it whole by rename(2), and the change it found the lock held for is reported once the holder saves. One window remains: a holder that has saved but not yet released has committed its change, and a first refresh in that window adopts it unreported, as the blocking refresh it replaces did for every change made under the lock. A busy skip is logged at debug rather than as a failed refresh. Every call to index_lock_acquire() now passes LOCK_NB. testing/imap_lock_timeout_test.py covers eleven commands, choosing the waiter from STORE, EXPUNGE, FETCH, SEARCH, CLOSE, SELECT, STATUS, COPY, MOVE, APPEND and IDLE. For the first ten it asserts the bound, that it does not fire early, the response code, and that the same command succeeds afterwards, which for CLOSE is the check that the refused one left the mailbox selected. For IDLE it asserts that the idling client is told of every message the holder changed. It needs a lock timeout far below the default and so is not in run-all-tests.sh, as the login grace test is not.
| README.md | commits | blame |
| contrib/ | |
| src/ | |
README.md
# OpenIMAPD
A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library.
**Status:** Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
## What it is
- **Privilege-separated**, `smtpd`-style, across six processes: a root *parent* reads configuration and binds the listening sockets; unprivileged *listener* and *auth* children handle the network and credential checks; *keymgr* holds the TLS private key and performs every private-key operation on request, so the process terminating TLS never has the key in its address space; a per-connection *search-oracle* parses the `SEARCH` grammar, the largest attacker-reachable parser in the daemon — in a process with no descriptors and no filesystem; and a *store* child is forked per authenticated session, chroots into the mail spool, and drops privileges to that session's own user before ever touching a message. `pledge(2)`, `unveil(2)`, and `chroot(2)` enforce these boundaries, not just convention: *parent* is the only process that can pass a file descriptor at all, and *search-oracle* runs on bare `stdio`.
- **Storage**: stock maildir format (`tmp/`/`new/`/`cur/`, atomic delivery via `rename(2)`), readable with `ls` and `grep`, and natively understood by `smtpd(8)`'s own `maildir` delivery action. IMAP's extra bookkeeping (UIDs, UIDVALIDITY, per-message mod-sequences, keywords) lives in a small, `flock(2)`-guarded, line-oriented index file per mailbox, plain colon-delimited text, not a database.
- **Transport**: STARTTLS on port 143 and implicit TLS on port 993 ([RFC 8314](https://www.rfc-editor.org/rfc/rfc8314)), via `libtls`. `AUTH=PLAIN` only, refused before TLS is established.
## Protocol coverage
`CAPABILITY`, `STARTTLS`, `AUTHENTICATE`, `ID`, `ENABLE`, `SELECT`, `EXAMINE`, `CREATE`, `DELETE`, `RENAME`, `LIST`, `LSUB`, `NAMESPACE`, `STATUS`, `FETCH` (including `ENVELOPE`, `BODYSTRUCTURE`, and MIME-part-addressed `BODY[<part>]`/`BODY.PEEK[<part>]`), `STORE`, `SEARCH`, `APPEND`, `COPY`, `MOVE`, `EXPUNGE`, `UNSELECT`, `CLOSE`, the `UID`-prefixed form of every command that supports it, `IDLE` (with the cross-session caveat noted below), and the [RFC 7162](https://www.rfc-editor.org/rfc/rfc7162) `CONDSTORE`/`QRESYNC` extensions.
`SUBSCRIBE`, `UNSUBSCRIBE`, and ACL/shared-mailbox support are deliberately left out.
**`IDLE` is a poll, not a kernel-driven push.** An `IDLE`ing session rechecks its selected mailbox every `idle poll` seconds (default 5, see `imapd.conf`), so new mail, whether delivered by an external MTA or by another IMAP session, is reported within one interval rather than instantly. Most polls are two `stat(2)` calls and no lock: the store child only re-reads the index and streams UIDs when the mailbox directory or `new/` has actually been touched. Setting `idle poll 0` disables polling entirely, which restores the earlier behaviour where an `IDLE`ing session saw nothing until it sent `DONE`.
## Requirements
OpenBSD only. This depends on `<imsg.h>`, `pledge(2)`, `unveil(2)`, and libutil's `imsgbuf_*` API, none of which exist outside OpenBSD. Links against libevent, libtls/libssl/libcrypto, and libutil, all base-system libraries (see `src/Makefile`).
**imapd requires OpenBSD -current, and will not build on 7.9.**. Building on the most recent stable release is a goal for 1.0.
`keymgr`, the process that isolates the TLS private key from `listener`, additionally depends on `tls_config_use_fake_private_key()` and undocumented ex_data-tagging behavior inside `tls_keypair_load()`, both unexported libtls/LibreSSL internals with no compatibility promise, and neither declared in libtls's public, installed `tls.h`.
## Building and installing
```
cd src
make
doas make install
```
Installs the daemon to `/usr/local/sbin/imapd`, man pages to `/usr/local/man/man8`, the `imapduser` account-provisioning tool alongside the daemon, and a sample config to `/usr/local/share/examples/imapd/imapd.conf`.
The `rc.d(8)` script is not installed automatically. `install(1)`, not `cp(1)`, so the installed copy is executable regardless of the source tree's own permission bits:
```
doas install -o root -g wheel -m 555 src/rc.d/imapd /etc/rc.d/imapd
```
## Configuring
Copy the sample config into place with restrictive permissions. imapd refuses to start against a config that's group- or world-writable, *or* world-readable:
```
doas install -o root -g wheel -m 600 \
/usr/local/share/examples/imapd/imapd.conf /etc/imapd.conf
```
Every directive is documented inline in the sample file; the full reference is in `imapd(8)`'s FILES section.
## Creating an account
imapd's users aren't real system accounts, `imapduser(8)` manages a bespoke credentials file (`username:passwordhash:uid:gid:maildir`, bcrypt via `crypt_checkpass(3)`) and the matching maildir ownership together, since no combination of `useradd(8)`/`userdel(8)` can safely keep both in sync:
```
doas imapduser -a someuser
```
See `imapduser(8)` for `-d` (revoke login without touching mail) and the `-c`/`-s`/`-u`/`-g` overrides.
## Running
```
doas rcctl enable imapd
doas rcctl start imapd
```
## Known limitations
Beyond the deliberate protocol-scope decisions covered in `imapd(8)`'s CAVEATS:
- If the listener or auth process exits unexpectedly after startup, it is not automatically restarted. Recovery is `rcctl restart imapd`. See `imapd(8)`.
`SIGHUP` reloads `spool`, `attachment max`, `idle poll`, `startups`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd(8)`.
IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see `imapd(8)`'s `listen on` directive.
## Getting the source
The repository is hosted with [Game of Trees](https://gameoftrees.org/) (`got`), read-only anonymous access over SSH:
```
got clone ssh://anonymous@got.openimapd.dev/imapd
```
The repository is also git-compatible; a plain `git clone` against the same URL works too:
```
git clone ssh://anonymous@got.openimapd.dev/imapd
```
## Security
Report security issues to security@openimapd.dev. General questions or feedback: feedback@openimapd.dev.
## License
ISC. See the copyright header in each source file.
## More
`imapd(8)` and `imapduser(8)` are the authoritative technical reference.
