commit - 2bd90b642f5b0f2eee6ef497269e1e983b8966c5
commit + da1136e4275efd1152585f31dab183ab5089370d
blob - 56d10fc49bb84b6df9a0a994afdcfd4b8338a295
blob + 50638422e9585c4f4108abfb0fa57a2695ca3baa
--- README.md
+++ README.md
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
## 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`).
+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.
**imapd requires OpenBSD -current, and will not build on 7.9.**. Building on the most recent stable release is a goal for 1.0.
## 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
# 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.
#
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() {
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
.\"
.\" 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
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
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
.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
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
.\" 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
.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
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
.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
.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
.%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
.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
.\" 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
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
.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
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
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 <imapd_users>
+
+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
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
#include <imsg.h>
#include <stdint.h>
-#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
/* RFC 7162 SS7 */
#define INDEX_MODSEQ_MAX INT64_MAX
+#define CRLF_BLOCK 65536
+
int
index_field_valid(const char *field)
{
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;
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);
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
}
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
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;
/* 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)",
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') {
}
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
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);
}
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;
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;
}
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;
(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
#include <sys/types.h>
#include <sys/queue.h>
+#include <sys/stat.h>
#include <errno.h>
#include <event.h>
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 (;;) {
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 */