Tree


README.mdcommits | 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:** 0.2.0 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 *parser* child, spun up by a *store* child on demand and retired after a spell idle, does every parse of attacker-reachable content — `SEARCH`'s grammar and `FETCH`'s `ENVELOPE`/`BODYSTRUCTURE`/header-field parsing alike — confined to `pledge(2)` `"stdio recvfd"`: it opens nothing itself, reading each message over a descriptor the store child passes it already open; and a *store* child is forked per authenticated account, shared by all of that account's sessions, chroots into the mail spool, and drops privileges to that account'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.
- **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` (including `RETURN (SAVE)` and the `$` saved-result-set marker, [RFC 5182](https://www.rfc-editor.org/rfc/rfc5182), folded into RFC 9051), `APPEND`, `COPY`, `MOVE`, `EXPUNGE`, `UNSELECT`, `CLOSE`, `SUBSCRIBE`, `UNSUBSCRIBE` (including LIST's [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051) section 6.3.9.1 `SUBSCRIBED` selection option), 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.

ACL/shared-mailbox support is 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/man5` and `/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 `imapd.conf(5)`.

## Creating the daemon accounts

imapd drops privileges to three accounts of its own and will not start until all three exist. Create them once, before the first mailbox account, since `imapduser(8)` gives the credentials file to the `_imapauth` group:

```
doas useradd -c "IMAP Daemon" -d /var/empty -s /sbin/nologin _imapd
doas useradd -c "IMAP Auth" -d /var/empty -s /sbin/nologin _imapauth
doas useradd -c "IMAP Key Manager" -d /var/empty -s /sbin/nologin _imapkey
```

Each runs in a group of its own name, which `useradd(8)` creates by default unless `/etc/usermgmt.conf` sets `group` otherwise.

## 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 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)`.

IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see the `listen on` directive in `imapd.conf(5)`.

## 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)`, `imapd.conf(5)` and `imapduser(8)` are the authoritative technical reference.