commit da1136e4275efd1152585f31dab183ab5089370d from: David Williams date: Sun Oct 4 04:46:05 2026 UTC smtpd delivery docs; oversize refusal; CRLF mail; child and login races Change 1 of 6: Document how smtpd(8) delivers into an imapd account imapd only reads maildirs. smtpd(8) writes them, and nothing told an operator how to configure it. imapd accounts are not system users, so smtpd needs a userbase table giving each account the uid, gid and maildir of its credentials line. Without one, smtpd delivers as the getpwnam(3) user of the same name, if there is one. On the test host that user's uid was not the account's, the maildir is mode 0700 for the account's uid, and every delivery failed with "mail.maildir: Permission denied" and stayed queued. imapd.conf.5: a new Mail delivery subsection. It gives the userbase line and says its uid and gid must match the credentials line, since smtpd writes each message mode 0600 as that user. It shows an example table, action and match rule using maildir "%{user.directory}", and explains why a bare maildir action is wrong: it delivers to ~/Maildir, which imapd shows as a mailbox. It covers rule order, that a listener declared with "auth" refuses mail from other hosts, smtpctl update table, the .Junk mailbox, and subaddress mail, which mail.maildir files in a mailbox named with a leading dot only if that mailbox exists. SEE ALSO gains table(5), smtpctl(8) and smtpd(8). imapduser: -a prints the account's userbase line on standard output, built from the same values as its credentials line, so "imapduser -a joe >> table" works. -d reminds the operator to remove that line. imapduser.8 says both. Change 2 of 6: Refuse a message over "attachment max" without reading it read_body_from_fd() (BODYSTRUCTURE, BODY[]) and SEARCH's read_header() for BODY and TEXT refused a message over "attachment max" only after reading attachment max + 1 octets of it, 40 MiB by default, on every FETCH or SEARCH that reached it, while the account's other sessions waited on the parser. Both now refuse on the size fstat(2) reports, before reading anything. A message at the cap, and SEARCH's header-only read, are read as before. What a client sees is unchanged, and so are the log lines. imapd.conf.5: "attachment max" says what a FETCH answers for a larger message; that mail delivered by smtpd(8) is not bound by "append max"; that smtpd's max-message-size should stay below it, with room for the headers smtpd adds and does not count; and how to list the larger messages a maildir brought from elsewhere already holds. Change 3 of 6: Store delivered mail with CRLF line endings smtpd's mail.maildir ends each line with LF alone, and imapd served those bytes as they were, so BODY[] was not the RFC 5322 form of the message that RFC 9051 SS6.4.5 defines, and RFC822.SIZE was not its RFC 5322 size (SS2.3.4). When the store first indexes a file in new/, new_to_crlf() now copies it through tmp/ with each bare LF made CRLF, fsync(2)s it and renames it over the original, before the message has a UID; new/ is fsync(2)ed once after the scan. RFC 9051 SS2.3.1.1 lets no UID's text change, so messages already indexed are left as they are. A lone CR, NUL and every other byte are kept. A file with no bare LF is not rewritten. If the copy fails, a warning is logged and the message is indexed unconverted, so delivered mail is never hidden. A file that is not regular, or a symlink, is not opened for reading. APPEND still stores what the client sends. imapd.8 says delivered mail is rewritten with CRLF line endings. imapd.conf.5's advice on smtpd's max-message-size now leaves room for the added carriage returns. Change 4 of 6: A broken channel to one child no longer ends imapd The parent fatal()ed whenever a write to, or a read from, a listener, auth worker, parser or keymgr failed. Those children exit on their own schedule: an auth worker exits once its listener closes, so a client that logged in and out at once could close the listener before the parent's queued IMSG_AUTH_EXIT went out. The write then failed with EPIPE and imapd exited, ending every session. On the test host six clients logging in at once took imapd down this way. A message from a child with a bad length did the same, so a compromised listener or parser could stop the service. parent_dispatch_child() now drops that child instead, as the store's channel already did and as smtpd's mproc.c does for a closed pipe: it stops watching the channel and sends SIGKILL, and reap_child() frees the child on SIGCHLD. A failed write is logged at debug level, naming the child's process type and pid; a failed read is logged as a warning. keymgr is treated the same way, so losing it is reported as reap_child() already reported it. Change 5 of 6: A login answered OK no longer refuses what follows A granted login reaches the listener as two messages on two channels: the auth worker's IMSG_AUTH_RESULT, and the parent's IMSG_SETUP_PEER once the account's store is wired, which marks the session authenticated and sends the OK. Nothing orders the two. When the store's message came first, the late result put the session back in the state before the store was wired, and every command after the OK was answered "BAD Command not permitted in this state". On the test host five clients logging in at once saw it on 154 of 1000 logins. The listener now ignores a granted result that arrives after the store, and logs it at debug level. The auth worker died with "fatal: imsgbuf_write: Broken pipe" when its listener had already gone: after a session that had finished before the result went out, or a client that hung up while its password was checked. It now takes EPIPE on that channel as the listener closing, as httpd's and relayd's proc.c do, and exits quietly; other write errors stay fatal. Change 6 of 6: Trim imapd.8 to the shape of smtpd.8 imapd.8 had grown to 478 lines, against 167 for smtpd.8. It now has smtpd.8's sections and a short CAVEATS, in 178 lines. The credentials file's format and rules move to imapd.conf.5, as a Credentials file subsection beside the Mail delivery one; the credentials keyword points to it, and imapd.conf.5 lists the default credentials file under FILES. imapd.8 and imapduser.8 point to imapd.conf.5 for both. imapd.8 no longer lists the commands, describes the processes, or lists the protocol details a client meets (LIST options, FETCH BINARY, RENAME INBOX, subscriptions, mailbox name rules); CAVEATS says in one sentence that what is not supported is refused with BAD or NO, and keeps the notes an operator needs. STANDARDS keeps RFC 9051 and RFC 7162. commit - 2bd90b642f5b0f2eee6ef497269e1e983b8966c5 commit + da1136e4275efd1152585f31dab183ab5089370d blob - 56d10fc49bb84b6df9a0a994afdcfd4b8338a295 blob + 50638422e9585c4f4108abfb0fa57a2695ca3baa --- README.md +++ README.md @@ -2,7 +2,7 @@ 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:** 0.2.0 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository. +**Status:** 0.2.1 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository. ## What it is @@ -20,7 +20,7 @@ ACL/shared-mailbox support is deliberately left out. ## Requirements -OpenBSD only. This depends on ``, `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`). +OpenBSD only. This depends on ``, `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. **imapd requires OpenBSD -current, and will not build on 7.9.**. Building on the most recent stable release is a goal for 1.0. @@ -84,8 +84,6 @@ doas rcctl start imapd ## Known limitations -Beyond the deliberate protocol-scope decisions covered in `imapd(8)`'s CAVEATS: - - If the keymgr process, which holds the TLS private key, exits unexpectedly, it is not restarted and new TLS handshakes fail. Recovery is `rcctl restart imapd`. See `imapd(8)`. `SIGHUP` reloads `spool`, `append max`, `attachment max`, `account sessions`, `connections max`, `idle poll`, `lock timeout`, `login grace`, `startups`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd.conf(5)`. blob - b2ac6c85f01332165cfe0472ac57af93f72ca3de blob + 7802c5fd432765b12ff918d2c1566e1fdcd5a7db --- contrib/imapduser +++ contrib/imapduser @@ -22,7 +22,7 @@ # imapduser -a [-c credentials-file] [-s spool-root] [-u uid] [-g gid] username # imapduser -d [-c credentials-file] username # -# -a adds a new mailbox account. +# -a adds a new mailbox account and prints its smtpd(8) userbase line. # # -d removes a mailbox account's credentials-file line ONLY. # @@ -167,6 +167,8 @@ do_add() { echo "${USERNAME}:${HASH}:${NEWUID}:${NEWGID}:${MAILDIR}" >> "$CRED_FILE" echo "${0##*/}: added $USERNAME (uid $NEWUID, gid $NEWGID, maildir $MAILDIR_PATH) to $CRED_FILE" 1>&2 + echo "${0##*/}: its smtpd(8) userbase line follows; see imapd.conf(5)" 1>&2 + printf '%s\t%s:%s:%s\n' "$USERNAME" "$NEWUID" "$NEWGID" "$MAILDIR_PATH" } do_delete() { @@ -194,6 +196,8 @@ do_delete() { echo "${0##*/}: removed $USERNAME from $CRED_FILE ($SPOOL_ROOT/$MAILDIR_FIELD" \ "was left untouched -- remove it by hand if you also want the" \ "mail data gone)" 1>&2 + echo "${0##*/}: remove $USERNAME's line from smtpd(8)'s userbase" \ + "table too, or mail for it is still delivered" 1>&2 } case "$MODE" in blob - cba2e724253c971c0d0706a3c356dd098acd931c blob + 2d6a88c2791ae4c79b6b30ed63f9c474427d2574 --- contrib/imapduser.8 +++ contrib/imapduser.8 @@ -2,7 +2,7 @@ .\" .\" Written for the OpenIMAPD project. Public domain / no rights reserved. .\" -.Dd $Mdocdate: September 30 2026 $ +.Dd $Mdocdate: October 3 2026 $ .Dt IMAPDUSER 8 .Os .Sh NAME @@ -78,6 +78,10 @@ mode 0700, and appends a line to the credentials file, prompting for a password via .Xr encrypt 1 . +It then prints the account's line for a +.Xr smtpd 8 +userbase table on standard output; see +.Xr imapd.conf 5 . If .Fl u and @@ -102,6 +106,10 @@ and does not conflate them. The maildir's path is printed on success so it can be removed by hand if that is actually what is wanted. +The account's line in a +.Xr smtpd 8 +userbase table must also be removed by hand; until it is, mail for the +account is still delivered. .Fl u and .Fl g @@ -181,7 +189,9 @@ Default spool root; see .Xr encrypt 1 , .Xr crypt_checkpass 3 , .Xr imapd.conf 5 , -.Xr imapd 8 +.Xr smtpd.conf 5 , +.Xr imapd 8 , +.Xr smtpd 8 .Sh HISTORY .Nm was written for the OpenIMAPD project. blob - c89f711d6103856cce71d763e5eb1631cb7d9d37 blob + 21da97eeae47457e4c695fb195895f59f99d0393 --- src/auth.c +++ src/auth.c @@ -190,8 +190,14 @@ auth_dispatch(int fd, short event, void *arg) ssize_t n; if (event & EV_WRITE) { - if (imsgbuf_write(&iev->ibuf) == -1) + if (imsgbuf_write(&iev->ibuf) == -1) { + if (errno == EPIPE) { + log_debug("auth-worker: listener closed " + "channel, exiting"); + exit(0); + } fatal("imsgbuf_write"); + } } if (event & EV_READ) { blob - 8bd071198b174dfe4b1d2aaa8c6426cdfbae4646 blob + 4e44803fb3a69f2c0741294274aac08ab9941e6a --- src/imapd.8 +++ src/imapd.8 @@ -3,7 +3,7 @@ .\" Written for the OpenIMAPD project. Public domain / no rights reserved, .\" matching the project's ports-oriented, OpenBSD-base-inclusion goal. .\" -.Dd $Mdocdate: September 30 2026 $ +.Dd $Mdocdate: October 3 2026 $ .Dt IMAPD 8 .Os .Sh NAME @@ -20,7 +20,12 @@ is an Internet Message Access Protocol .Pq IMAP daemon implementing a subset of IMAP4rev2, as defined by RFC 9051, and the RFC 7162 CONDSTORE and QRESYNC extensions. -It serves mail stored in maildir format. +It serves mail stored in maildir format, delivered by +.Xr smtpd 8 +as described in +.Xr imapd.conf 5 , +and rewrites each new message with the CRLF line endings RFC 5322 +requires before any client sees it. .Pp By default, .Nm @@ -29,90 +34,30 @@ listens on port 143, where clients may upgrade to TLS and on port 993 with implicit TLS, as recommended by RFC 8314. Authentication is .Li AUTH=PLAIN -only, and is refused until TLS is established; -.Li LOGIN -is always refused. -The listening addresses and ports are set in +only, and is refused until TLS is established. +Users are not system accounts; they are listed in a credentials file, +described in .Xr imapd.conf 5 . .Pp Each user's mailboxes form a single flat namespace: .Li INBOX -and zero or more sibling mailboxes, with no hierarchy and no shared -mailboxes. -.Li INBOX -is the account's maildir itself, and each other mailbox is a maildir -in a subdirectory of it with the mailbox's name. -The commands implemented are -.Li CAPABILITY , -.Li NOOP , -.Li LOGOUT , -.Li STARTTLS , -.Li AUTHENTICATE , -.Li ID , -.Li ENABLE , -.Li SELECT , -.Li EXAMINE , -.Li CREATE , -.Li DELETE , -.Li RENAME , -.Li SUBSCRIBE , -.Li UNSUBSCRIBE , -.Li LIST , -.Li LSUB , -.Li NAMESPACE , -.Li STATUS , -.Li FETCH , -.Li STORE , -.Li SEARCH , -.Li APPEND , -.Li COPY , -.Li MOVE , -.Li EXPUNGE , -.Li UNSELECT , -.Li CLOSE , -.Li UID -and -.Li IDLE . +is the account's maildir, and each other mailbox is a maildir in a +subdirectory of it with the mailbox's name. .Pp .Nm -is privilege separated. -A root parent process reads the configuration, binds the listening -sockets and accepts connections. -Each connection is handled by unprivileged processes that are -.Xr chroot 2 Ns ed -and restricted with -.Xr pledge 2 . -The TLS private key is held by a separate process and never enters -the process that speaks to the network. -Mailbox access for each logged-in account is performed by one process, -shared by all of that account's sessions, which is -.Xr chroot 2 Ns ed -into the mail spool and runs as the account's own user. -.Nm requires the .Sy _imapd , .Sy _imapauth and .Sy _imapkey users to exist. -.Pp -.Nm -does not detach from its controlling terminal and is expected to be -started by -.Xr rc.d 8 . -It logs to +It does not detach from its controlling terminal, logs to .Xr syslogd 8 with the .Dv LOG_MAIL -facility. +facility, and rereads its configuration file on +.Dv SIGHUP . .Pp -.Nm -rereads its configuration file when it receives -.Dv SIGHUP ; -see -.Xr imapd.conf 5 -for the settings that require a restart instead. -.Pp The options are as follows: .Bl -tag -width Ds .It Fl D Ar macro Ns = Ns Ar value @@ -139,62 +84,6 @@ Print the version and exit. .It Fl v Produce more verbose logging. .El -.Sh AUTHENTICATION -Users of -.Nm -are not system accounts. -They are listed in a credentials file, by default -.Pa /etc/imapd/credentials , -one per line in the form: -.Bd -literal -offset indent -username:passwordhash:uid:gid:maildir -.Ed -.Pp -The password hash is a bcrypt hash as accepted by -.Xr crypt_checkpass 3 . -.Ar uid -and -.Ar gid -are the user and group the account's mailbox process runs as, and -.Ar maildir -is the account's maildir, relative to the spool set in -.Xr imapd.conf 5 . -Entries that share all three name the same account. -.Pp -Empty lines and lines beginning with -.Sq # -are ignored, and malformed lines are skipped. -A line is also skipped if its hash is not a bcrypt hash or its -.Ar uid -or -.Ar gid -is 0. -The first valid line naming a user is the one used. -A login is refused if the -.Ar maildir -is empty, is an absolute path, or has a -.Pa \&. -or -.Pa .. -component. -A line of 1023 or more characters causes every login to be refused. -.Xr imapduser 8 -adds and removes entries. -.Pp -The file is read by a process running as -.Sy _imapauth , -so it must be readable by that user. -It must be owned by root or -.Sy _imapauth -and must not be group writable or executable, or accessible by -others; otherwise every login is refused. -.Xr imapduser 8 -creates it owned by root, group -.Sy _imapauth , -mode 0640. -A failed login for a name with no entry costs one hash at the highest -cost in the file, so an entry hashed at a lower cost can be told from a -missing one by timing; rehash such entries at the file's cost. .Sh FILES .Bl -tag -width "/etc/ssl/private/imapd.keyXX" -compact .It Pa /etc/imapd.conf @@ -214,27 +103,20 @@ and .Pa imapd.index.tmp beside it. .It Pa imapd.subscriptions -Subscribed mailboxes, one per line, at the root of each maildir, with +Subscribed mailboxes, at the root of each maildir, with .Pa imapd.subscriptions.lock and .Pa imapd.subscriptions.tmp beside it. -Created by the first -.Li UNSUBSCRIBE ; -while it is absent, every mailbox is subscribed. +While it is absent, every mailbox is subscribed. .It Pa imapd.uidvalidity Highest .Li UIDVALIDITY issued, at the root of each maildir. -Created on demand. -.It Pa /usr/local/share/examples/imapd/imapd.conf -Example configuration file. .El .Sh SEE ALSO -.Xr crypt_checkpass 3 , .Xr imapd.conf 5 , .Xr imapduser 8 , -.Xr rc.d 8 , .Xr smtpd 8 , .Xr syslogd 8 .Sh STANDARDS @@ -254,36 +136,6 @@ Example configuration file. .%R RFC 7162 .%T IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC) .Re -.Pp -.Rs -.%A F. Yergeau -.%D November 2003 -.%R RFC 3629 -.%T UTF-8, a transformation format of ISO 10646 -.Re -.Pp -.Rs -.%A K. Zeilenga -.%D August 2006 -.%R RFC 4616 -.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism -.Re -.Pp -.Rs -.%A J. Klensin -.%A M. Padlipsky -.%D March 2008 -.%R RFC 5198 -.%T Unicode Format for Network Interchange -.Re -.Pp -.Rs -.%A K. Moore -.%A C. Newman -.%D January 2018 -.%R RFC 8314 -.%T Cleartext Considered Obsolete: Use of Transport Layer Security (TLS) for Email Submission and Access -.Re .Sh HISTORY .Nm is a from-scratch IMAP server written for the OpenIMAPD project, in @@ -293,103 +145,34 @@ privilege-separation tradition of .Xr smtpd 8 . .Sh CAVEATS This implementation is under active development. +Commands, options and +.Li FETCH +items it does not support are refused with +.Li BAD +or +.Li NO . .Pp .Li IDLE -is served by polling the selected mailbox, so new mail is reported -within one +polls the selected mailbox, so new mail is reported within one .Ic idle poll -interval rather than at once. +interval. .Pp -All sessions of one account are served by one process, one command -at a time. -.Li FETCH -and -.Li SEARCH -give way to the account's other sessions as they go; +One process serves all sessions of an account, so a .Li STORE , .Li EXPUNGE , .Li COPY -and +or .Li MOVE -do not, so one of these over a very large mailbox delays every other -session of the same account until it completes. +over a very large mailbox delays the account's other sessions. .Pp -If the process holding the TLS private key exits, it is not restarted, -and new TLS handshakes fail until +If the process holding the TLS private key exits, new TLS handshakes +fail until .Nm is restarted. .Pp -.Li INBOX -cannot be unsubscribed: -.Li UNSUBSCRIBE INBOX -replies -.Li NO . -A subscription is kept when its mailbox is deleted, as RFC 9051 -section 6.3.7 recommends, and is not moved when its mailbox is renamed, -as section 6.3.6 describes. -.Pp -.Li RENAME INBOX -is refused, which RFC 9051 section 6.3.6 notes some servers do. -.Pp -.Li LIST -selection options other than -.Li SUBSCRIBED , -return options, and more than one mailbox pattern are refused. -.Pp -.Li FETCH -does not support -.Li BINARY , -.Li BINARY.PEEK -or -.Li BINARY.SIZE , -which are answered -.Li BAD . -It does not return a section that combines a part number with -.Li HEADER , -.Li HEADER.FIELDS , -.Li TEXT -or -.Li MIME , -nor -.Li RFC822 , -.Li RFC822.HEADER -or -.Li RFC822.TEXT ; -it returns the other items asked for and answers -.Li NO . -.Pp -A session that deletes its selected mailbox is returned to the -authenticated state, as if by -.Li CLOSE . -.Pp -Mailbox names must be well-formed UTF-8 per RFC 3629. -Of the Net-Unicode -.Pq RFC 5198 -requirements, -.Nm -refuses the C1 controls and refuses U+FEFF anywhere in a name, which is -stricter than RFC 5198. -It does not normalize names to NFC, so two canonically equivalent -spellings are two distinct mailboxes, and it does not check names -against a Unicode version. -.Li CREATE , -.Li RENAME , -.Li SUBSCRIBE , -.Li UNSUBSCRIBE , -.Li COPY -and -.Li MOVE -refuse a name that fails these checks with -.Li NO [CANNOT] , -and other commands report it as nonexistent. -An existing directory with such a name is not listed and cannot be -selected. -.Pp -The names .Pa tmp , -.Pa new -and -.Pa cur , -and the names of the files listed under +.Pa new , +.Pa cur +and the maildir files under .Sx FILES -that are kept in a maildir, cannot be used as mailbox names. +cannot be used as mailbox names. blob - f46a691dfe5110714c6b3d4809ba96583710a330 blob + 7cbe25a59044bfe29bab71adac0297c25f905621 --- src/imapd.conf.5 +++ src/imapd.conf.5 @@ -3,7 +3,7 @@ .\" Written for the OpenIMAPD project. Public domain / no rights reserved, .\" matching the project's ports-oriented, OpenBSD-base-inclusion goal. .\" -.Dd $Mdocdate: September 30 2026 $ +.Dd $Mdocdate: October 3 2026 $ .Dt IMAPD.CONF 5 .Os .Sh NAME @@ -110,6 +110,10 @@ will read to produce its or one of its MIME parts. A larger message is treated as one whose structure cannot be produced; it is not truncated. +A +.Li FETCH +of either leaves that item out of its response and ends with +.Li NO . The same limit bounds what .Li SEARCH reads of a message, so a search on @@ -118,9 +122,35 @@ or .Li TEXT that reaches a larger message fails with .Li NO . +.Pp +Mail delivered by +.Xr smtpd 8 +is not bound by +.Ic append max . +Keep the +.Ic smtp max-message-size +of +.Xr smtpd.conf 5 +below +.Ic attachment max , +with room for the headers +.Xr smtpd 8 +adds to each message and for the carriage return +.Xr imapd 8 +adds before each line feed, neither of which its limit counts. +A maildir brought from elsewhere may already hold larger messages. +To list them, for the default +.Ic spool +and +.Ic attachment max : +.Pp +.Dl # find /var/mail/imapd -type f -size +41943040c +.Pp Must be between 12000 and 1073741824 and may not be less than .Ic append max . -The default is 41943040 (40 MiB). +The default is 41943040 (40 MiB), which leaves room above the +.Ic smtp max-message-size +default of 35M. .It Ic connections max Ar count The most connections open at once, logged in or not. Past it, a new connection on the cleartext port is answered with a @@ -148,7 +178,7 @@ The default is 256, which fits the defaults. .It Ic credentials Ar path The credentials file; see -.Xr imapd 8 . +.Sx Credentials file . The default is .Pa /etc/imapd/credentials . .It Ic idle poll Ar seconds @@ -282,11 +312,143 @@ A key that fails this check is not loaded, and a warni The default is .Pa /etc/ssl/private/imapd.key . .El +.Ss Credentials file +Users of +.Xr imapd 8 +are not system accounts. +They are listed in the file named by +.Ic credentials , +one per line in the form: +.Bd -literal -offset indent +username:passwordhash:uid:gid:maildir +.Ed +.Pp +The password hash is a bcrypt hash as accepted by +.Xr crypt_checkpass 3 . +.Ar uid +and +.Ar gid +are the user and group the account's mailbox process runs as, and +.Ar maildir +is the account's maildir, relative to +.Ic spool . +Entries that share all three name the same account. +.Pp +Empty lines and lines beginning with +.Sq # +are ignored, and malformed lines are skipped. +A line is also skipped if its hash is not a bcrypt hash or its +.Ar uid +or +.Ar gid +is 0. +The first valid line naming a user is the one used. +A login is refused if the +.Ar maildir +is empty, is an absolute path, or has a +.Pa \&. +or +.Pa .. +component. +A line of 1023 or more characters causes every login to be refused. +.Xr imapduser 8 +adds and removes entries. +.Pp +The file is read by a process running as +.Sy _imapauth , +so it must be readable by that user. +It must be owned by root or +.Sy _imapauth +and must not be group writable or executable, or accessible by +others; otherwise every login is refused. +.Xr imapduser 8 +creates it owned by root, group +.Sy _imapauth , +mode 0640. +A failed login for a name with no entry costs one hash at the highest +cost in the file, so an entry hashed at a lower cost can be told from a +missing one by timing; rehash such entries at the file's cost. +.Ss Mail delivery +.Xr imapd 8 +does not deliver mail. +.Xr smtpd 8 +delivers it with its +.Cm maildir +method, configured in +.Xr smtpd.conf 5 . +Accounts are not system users, so the action needs a +.Cm userbase +table, in the format of +.Xr table 5 , +mapping each username to the +.Ar uid , +.Ar gid +and maildir of its credentials line, the maildir as an absolute path: +.Bd -literal -offset indent +joe 2000:2000:/var/mail/imapd/joe +.Ed +.Pp +.Xr imapduser 8 +prints this line for each account it adds. +The +.Ar uid +and +.Ar gid +must be those of the credentials line: +.Xr smtpd 8 +writes each message as that user, mode 0600, and +.Xr imapd 8 +cannot read a message written as any other user. +.Pp +A configuration that delivers mail for one domain to the accounts in +the table: +.Bd -literal -offset indent +table imapd_users file:/etc/mail/imapd_users + +action "imapd" maildir "%{user.directory}" userbase + +match from any for domain "example.org" action "imapd" +.Ed +.Pp +The +.Cm maildir +path must be given. +Without it, mail goes to +.Pa ~/Maildir , +a subdirectory of the account's maildir, which +.Xr imapd 8 +shows as a mailbox named +.Dq Maildir . +The +.Ic match +rule must come before any other rule that matches the same recipients. +Mail from other hosts must arrive on a listener without the +.Cm auth +option, or with +.Cm auth-optional +instead. +After editing the table, run: +.Pp +.Dl # smtpctl update table imapd_users +.Pp +With the +.Cm junk +option, spam is delivered to a mailbox named +.Dq \&.Junk . +.Pp +Mail for a subaddress, such as +.Ql joe+lists , +is delivered to the mailbox named +.Dq \&.lists +if it exists, and to INBOX otherwise. +To have such mail filed, create the mailbox with the leading dot. .Sh FILES .Bl -tag -width "/usr/local/share/examples/imapd/imapd.conf" -compact .It Pa /etc/imapd.conf .Xr imapd 8 configuration file. +.It Pa /etc/imapd/credentials +Default credentials file. .It Pa /usr/local/share/examples/imapd/imapd.conf Example configuration file. .El @@ -305,10 +467,15 @@ tls key "/etc/ssl/private/mail.example.com.key" account sessions 16 .Ed .Sh SEE ALSO +.Xr crypt_checkpass 3 , .Xr login.conf 5 , .Xr smtpd.conf 5 , .Xr sshd_config 5 , +.Xr table 5 , .Xr imapd 8 , +.Xr imapduser 8 , +.Xr smtpctl 8 , +.Xr smtpd 8 , .Xr sysctl 8 .Sh HISTORY .Nm blob - 1399571babfb3228b192fa93d49b66a9bbe29e86 blob + 178a56bd0c968d14e88ac309af53431fb69dbae2 --- src/imapd.h +++ src/imapd.h @@ -28,7 +28,7 @@ #include #include -#define IMAPD_VERSION "0.2.0" +#define IMAPD_VERSION "0.2.1" #define IMAPD_USER "_imapd" #define IMAPD_AUTH_USER "_imapauth" blob - 2a1b2706eaad3081c7d655ba21ff1fcba647e5ee blob + 2c32ea030166b5b383da1b7d1c3561933e8c9407 --- src/index.c +++ src/index.c @@ -41,6 +41,8 @@ /* RFC 7162 SS7 */ #define INDEX_MODSEQ_MAX INT64_MAX +#define CRLF_BLOCK 65536 + int index_field_valid(const char *field) { @@ -788,12 +790,100 @@ uidvalidity_next(void) return (val); } +/* RFC 5322 SS2.3: CR and LF only as CRLF; mail.maildir(8) writes LF alone */ static int +new_to_crlf(int dfd, const char *name) +{ + struct stat st; + char src[PATH_MAX], tmp[PATH_MAX]; + char *in = NULL, *out; + size_t i, o, w; + ssize_t n; + int ifd = -1, ofd = -1, made = 0, cr = 0, changed = 0; + int rc = -1; + + if (snprintf(src, sizeof(src), "new/%s", name) >= (int)sizeof(src) || + snprintf(tmp, sizeof(tmp), "tmp/%s.crlf", name) >= + (int)sizeof(tmp)) { + log_warnx("session %u: new/%s: name too long for CRLF, " + "indexed as it is", session_id, name); + return (-1); + } + ifd = openat(dfd, src, O_RDONLY | O_NOFOLLOW | O_NONBLOCK); + if (ifd == -1 || fstat(ifd, &st) == -1) + goto fail; + if (!S_ISREG(st.st_mode)) { + log_warnx("session %u: %s: not a regular file, indexed as " + "it is", session_id, src); + goto done; + } + if ((in = malloc(3 * CRLF_BLOCK)) == NULL) + goto fail; + out = in + CRLF_BLOCK; + ofd = openat(dfd, tmp, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (ofd == -1 && errno == EEXIST && unlinkat(dfd, tmp, 0) == 0) + ofd = openat(dfd, tmp, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (ofd == -1) + goto fail; + made = 1; + + for (;;) { + if ((n = read(ifd, in, CRLF_BLOCK)) == -1) { + if (errno == EINTR) + continue; + goto fail; + } + if (n == 0) + break; + for (i = 0, o = 0; i < (size_t)n; i++) { + if (in[i] == '\n' && !cr) { + out[o++] = '\r'; + changed = 1; + } + cr = in[i] == '\r'; + out[o++] = in[i]; + } + for (w = 0; w < o; w += (size_t)n) { + while ((n = write(ofd, out + w, o - w)) == -1 && + errno == EINTR) + ; + if (n == -1) + goto fail; + } + } + + if (changed) { + if (fsync(ofd) == -1) + goto fail; + n = close(ofd); + ofd = -1; + if (n == -1 || renameat(dfd, tmp, dfd, src) == -1) + goto fail; + made = 0; + log_debug("session %u: %s: bare LF made CRLF", session_id, src); + } + rc = changed; + goto done; + +fail: + log_warn("session %u: %s to CRLF, indexed as it is", session_id, src); +done: + if (ifd != -1) + close(ifd); + if (ofd != -1) + close(ofd); + if (made) + unlinkat(dfd, tmp, 0); + free(in); + return (rc); +} + +static int index_scan_new(int dfd, struct mbox_index *idx, int mutate) { DIR *dp; struct dirent *de; - int added = 0; + int added = 0, converted = 0; { int newfd; @@ -830,6 +920,9 @@ index_scan_new(int dfd, struct mbox_index *idx, int mu closedir(dp); return (1); } + /* RFC 9051 SS2.3.1.1: convert before the UID is given */ + if (new_to_crlf(dfd, de->d_name) == 1) + converted = 1; if (index_append(idx, idx->uidnext, de->d_name) == -1) { closedir(dp); index_free(idx); @@ -838,6 +931,8 @@ index_scan_new(int dfd, struct mbox_index *idx, int mu idx->uidnext++; added = 1; } + if (converted && fsync(dirfd(dp)) == -1) + log_warn("session %u: fsync new", session_id); closedir(dp); return (added); } blob - 0741e76249dd96e3781a2f87114e35397b1d0fcd blob + 72cd56b418ec77a0a000346f1b2af0a4aa3ff452 --- src/listener.c +++ src/listener.c @@ -1691,8 +1691,13 @@ listener_dispatch_auth(int fd, short event, void *arg) } auth_granted = 1; - s->state = SESSION_STORE_PENDING; setproctitle("session %u [authenticated]", s->id); + if (s->state != SESSION_AUTHENTICATING) { + log_debug("session %u: auth result after the " + "store peer", s->id); + break; + } + s->state = SESSION_STORE_PENDING; break; } default: blob - a7f71c2b4615291f9471cd67ab552d19672ed753 blob + b31e1c0eec8e826c1f842dc5fa52f2781378025e --- src/mime.c +++ src/mime.c @@ -352,7 +352,7 @@ read_body_from_fd(int fd, const char *basename, int te size_t maxlen, const char *label, char **buf_out, uint32_t *len_out) { - char *readbuf; + char *readbuf = NULL; size_t readbuf_size; struct stat st; ssize_t n, total = 0; @@ -363,9 +363,11 @@ read_body_from_fd(int fd, const char *basename, int te /* sized to the message, not the configured ceiling */ readbuf_size = maxlen + 1; - if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size >= 0 && - (uint64_t)st.st_size < (uint64_t)maxlen) + if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size >= 0) { + if ((uint64_t)st.st_size > (uint64_t)maxlen) + goto toolarge; readbuf_size = (size_t)st.st_size + 1; + } if ((readbuf = malloc(readbuf_size)) == NULL) { log_warn("session %u: malloc message body readbuf (%s)", @@ -389,12 +391,8 @@ read_body_from_fd(int fd, const char *basename, int te total += n; } - if (total > (ssize_t)maxlen) { - log_warnx("session %u: message %s exceeds %zu bytes, " - "%s skipped", session_id, basename, maxlen, label); - free(readbuf); - return (-1); - } + if (total > (ssize_t)maxlen) + goto toolarge; for (i = 0; i < (size_t)total; i++) { if (readbuf[i] == '\0') { @@ -431,6 +429,12 @@ read_body_from_fd(int fd, const char *basename, int te } free(readbuf); return (0); + +toolarge: + log_warnx("session %u: message %s exceeds %zu bytes, %s skipped", + session_id, basename, maxlen, label); + free(readbuf); + return (-1); } /* RFC 9051 SS6.4.5.1: ASCII case-insensitive */ blob - e6e6dcf7bba24a7986c91c5d690b1721c12eeb23 blob + e6ffc62566f5ef03659e82ca925bbb36182680d5 --- src/parent.c +++ src/parent.c @@ -143,6 +143,7 @@ static int setup_peer_send(struct imsgev *, struct im const char *, uint32_t); static void setup_done_send(struct imsgev *); static void parent_dispatch_child(int, short, void *); +static void child_drop(struct child *); static struct open_session *open_session_find(uint32_t); static unsigned int count_startups(void); static int startups_should_drop(unsigned int); @@ -361,7 +362,7 @@ fork_child_nonfatal(enum openimap_proc_type type, stru } c->pid = pid; c->type = type; - imsgev_init(&c->iev, sp[0], handler, NULL); + imsgev_init(&c->iev, sp[0], handler, c); TAILQ_INSERT_TAIL(&children, c, entry); *ievp = &c->iev; @@ -432,21 +433,31 @@ setup_done_send(struct imsgev *iev) imsg_free(&imsg); } +/* a broken channel ends that child, not imapd */ static void parent_dispatch_child(int fd, short event, void *arg) { - struct imsgev *iev = arg; + struct child *c = arg; + struct imsgev *iev = &c->iev; struct imsg imsg; ssize_t n; if (event & EV_WRITE) { - if (imsgbuf_write(&iev->ibuf) == -1) - fatal("imsgbuf_write"); + if (imsgbuf_write(&iev->ibuf) == -1) { + log_debug("%s[%d]: channel closed: %s", + log_procname(c->type), c->pid, strerror(errno)); + child_drop(c); + return; + } } if (event & EV_READ) { - if ((n = imsgbuf_read(&iev->ibuf)) == -1) - fatal("imsgbuf_read"); + if ((n = imsgbuf_read(&iev->ibuf)) == -1) { + log_warn("%s[%d]: imsgbuf_read, killing it", + log_procname(c->type), c->pid); + child_drop(c); + return; + } if (n == 0) { event_del(&iev->ev); return; @@ -454,8 +465,12 @@ parent_dispatch_child(int fd, short event, void *arg) } for (;;) { - if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1) - fatal("imsgbuf_get"); + if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1) { + log_warn("%s[%d]: imsgbuf_get, killing it", + log_procname(c->type), c->pid); + child_drop(c); + return; + } if (n == 0) break; @@ -518,6 +533,14 @@ parent_dispatch_child(int fd, short event, void *arg) (void)fd; } +/* reap_child() frees c once SIGCHLD reports the exit */ +static void +child_drop(struct child *c) +{ + event_del(&c->iev.ev); + kill(c->pid, SIGKILL); +} + static struct open_session * open_session_find(uint32_t session_id) { blob - 0fc76f71c80271d4856ee9038dcac60a2f275f4f blob + 39a322115575c507c1a25286fcf680d5f615b106 --- src/search_match.c +++ src/search_match.c @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -346,10 +347,14 @@ static int read_header(int fd, const char *basename, int whole, char **buf_out, size_t *len_out) { - char *buf = NULL, *nbuf; - size_t len = 0, size = 0, hdrend, limit; - ssize_t n; + struct stat st; + char *buf = NULL, *nbuf; + size_t len = 0, size = 0, hdrend, limit; + ssize_t n; + if (whole && fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && + st.st_size > (off_t)bodystructure_read_max) + goto toolarge; /* one octet past the cap tells a header at it from one over it */ limit = (size_t)bodystructure_read_max + 1; for (;;) { @@ -383,17 +388,18 @@ read_header(int fd, const char *basename, int whole, c break; } } - if (len > bodystructure_read_max) { - log_warnx("session %u: message %s %s %u bytes, SEARCH " - "cannot check it", session_id, basename, whole ? - "is over" : "has no header end within", - bodystructure_read_max); - free(buf); - return (-1); - } + if (len > bodystructure_read_max) + goto toolarge; *buf_out = buf; *len_out = len; return (0); + +toolarge: + log_warnx("session %u: message %s %s %u bytes, SEARCH cannot " + "check it", session_id, basename, whole ? "is over" : + "has no header end within", bodystructure_read_max); + free(buf); + return (-1); } /* RFC 9051 SS6.4.4: addresses as ENVELOPE shows them, not group names */