This document lists, protocol by protocol, what ShardPost implements:
the standards it follows, the extensions it advertises, the commands
and methods it answers, and where it deliberately or knowingly deviates.
It describes ShardPost 0.52.0 as of 2026-10-03 and was written from
the code, not from earlier documentation.
How to read the tables:
Mark
Meaning
✅
Implemented and advertised
◐
Implemented with a documented restriction or deviation (see the note)
✗
Not implemented
⚙
Only present when the named setting is configured
An RFC listed here means that ShardPost implements the parts named in
the row, not that every SHOULD of that RFC has been verified. Test
evidence is in Quality and the versioned
conformance results.
The server does not accept internet SMTP and has no submission
listener (587/465): an upstream MTA delivers through LMTP, and outgoing
mail generated by the server goes to a configured relay
(Outbound SMTP).
Every mail and JMAP listener accepts the HAProxy PROXY protocol from
trusted networks; see Transport.
IMAP
Parser, dispatcher and encoder are ShardPost's own code
(internal/imapproto). The vendored go-imap v2 is used only for MIME
section extraction, BODYSTRUCTURE and LIST pattern matching.
Capabilities
The capability list depends on the session state. Before login the
server advertises the first group and the authentication capabilities;
after login it adds the rest. The list is sent in the greeting and in
the tagged OK of LOGIN/AUTHENTICATE.
Capability
RFC
Status
Notes
IMAP4rev2
RFC 9051
✅
Advertised together with IMAP4rev1. After ENABLE IMAP4rev2: UTF-8 mailbox names; no \Recent flag, RECENT responses, STATUS RECENT or [UNSEEN]; SEARCH answers with ESEARCH; SELECT/EXAMINE return the mailbox's LIST line with its attributes (\Subscribed, children, special-use) and OLDNAME for a normalized name; STORE and NOTIFY FETCH responses carry the UID. The IMAP4rev1 commands and items RFC 9051 deprecates — LSUB, CHECK, the RFC822* items and the search keys NEW, OLD, RECENT — stay available.
IMAP4rev1
RFC 3501
✅
Base protocol.
LITERAL+
RFC 7888
✅
Non-synchronising literals up to the literal limit. LITERAL- is not advertised.
SASL-IR
RFC 4959
✅
Initial response on AUTHENTICATE; = is an empty response.
ID
RFC 2971
◐
Returns name, vendor and support-url. The client's ID is ignored and not passed to the authentication backend.
ENABLE
RFC 5161
✅
CONDSTORE, QRESYNC, UTF8=ACCEPT, IMAP4rev2; unknown names are ignored.
UTF8=ACCEPT
RFC 6855
◐
After ENABLE UTF8=ACCEPT: mailbox names in UTF-8 both ways; APPEND, CATENATE and REPLACE take UTF8 (~{n}) message data; SEARCH CHARSET other than UTF-8 or US-ASCII is BAD. New names are put into Normalization Form C (announced with LIST … OLDNAME); names with control characters are refused. Not done: APPEND of a message with 8-bit header text from a session that did not enable UTF-8 is accepted (RFC 6855 §4 asks for NO); no downgrade (RFC 6857, RFC 6858); no UTF8=ONLY.
COMPRESS=DEFLATE
RFC 4978
✅
Any state; a second COMPRESS gets [COMPRESSIONACTIVE]. A patched inflater (internal/inflate) handles Z_PARTIAL_FLUSH clients such as iOS Mail. No longer advertised once active.
STARTTLS
RFC 9051 §6.2.1
⚙
Only on a plain connection with a TLS certificate configured.
AUTH=PLAIN
RFC 4616
✅
authzid different from authcid is refused (no proxy authorisation).
AUTH=XOAUTH2
Google XOAUTH2
✅
Bearer token, checked through Nauthilus. Failure uses the same JSON continuation as OAUTHBEARER.
AUTH=OAUTHBEARER
RFC 7628
✅
Failure is reported with the RFC 7628 JSON error challenge.
LOGINDISABLED
RFC 9051
✅
Instead of the AUTH= list when the connection is not encrypted and server.insecure_auth is off (the default).
IMAPSIEVE=<url>
RFC 6785
⚙
Only with imapsieve.enabled and imapsieve.user_scripts. See IMAPSieve.
NAMESPACE
RFC 2342
✅
Personal "", other users Shared/, public Public/ (prefixes configurable). A namespace with an empty prefix is switched off and shown as NIL. Prefixes are sent in modified UTF-7 to sessions without UTF-8 names. No namespace response extensions.
UNSELECT
RFC 3691
✅
IDLE
RFC 2177
✅
Also in the authenticated state. Changes come from a RADOS watch on the mailbox, shared by every session of the pod that follows it, plus a 60 s poll. IDLE itself has no timeout.
UIDPLUS
RFC 4315
✅
[APPENDUID], [COPYUID], UID EXPUNGE.
MOVE
RFC 6851
✅
Untagged OK [COPYUID] before the expunges.
ESEARCH
RFC 4731
✅
RETURN (MIN MAX ALL COUNT SAVE); an unknown return option is BAD.
SEARCHRES
RFC 5182
✅
RETURN (SAVE) on SEARCH and SORT, and $ wherever a set is taken: FETCH, STORE, COPY, MOVE, REPLACE, UID EXPUNGE and the SEARCH keys. SAVE alone sends no untagged response; with only MIN and/or MAX the saved result is those messages, otherwise every match.
LIST-EXTENDED
RFC 5258
✅
Selection SUBSCRIBED, RECURSIVEMATCH, SPECIAL-USE (REMOTE accepted and ignored); return SUBSCRIBED, CHILDREN, SPECIAL-USE, MYRIGHTS, STATUS; several patterns.
LIST-STATUS
RFC 5819
✅
LIST-MYRIGHTS
RFC 8440
✅
STATUS=SIZE
RFC 8438
✅
SPECIAL-USE
RFC 6154, RFC 8457
✅
\All \Archive \Drafts \Flagged \Important \Junk \Sent \Trash (\Important from RFC 8457). Attributes appear only with RETURN (SPECIAL-USE) or the SPECIAL-USE selection option, never on shared or public folders.
CREATE-SPECIAL-USE
RFC 6154
◐
CREATE name (USE (…)). Unknown attributes are stored as given; there is no USEATTR error.
CHILDREN
RFC 3348
✅
\HasChildren / \HasNoChildren on every LIST entry.
BINARY
RFC 3516
✅
BINARY[], BINARY.PEEK[], BINARY.SIZE[], ~{n} literals. A part in an unknown transfer encoding fails BINARY and BINARY.SIZE with NO [UNKNOWN-CTE]; a non-PEEK BINARY FETCH is checked before any \Seen is set (a single message is read once, several are read twice).
MULTIAPPEND
RFC 3502
✅
All-or-nothing: a failed command stores nothing.
CONDSTORE
RFC 7162
✅
HIGHESTMODSEQ, MODSEQ fetch and search, CHANGEDSINCE, UNCHANGEDSINCE with [MODIFIED]. Enabled implicitly by the first CONDSTORE command. No NOMODSEQ mailboxes.
QRESYNC
RFC 7162
◐
SELECT (QRESYNC …) with VANISHED (EARLIER), VANISHED instead of EXPUNGE, [CLOSED], UID FETCH … (VANISHED). The known-UID set is honoured, the optional sequence-match data is ignored; when the change log is too short and no known UIDs were given, a superset is reported.
QUOTA
RFC 9208
✅
One quota root "". GETQUOTA, GETQUOTAROOT; SETQUOTA only with server.quota_self_service (default off), and QUOTASET is advertised exactly then. STATUS items DELETED and DELETED-STORAGE (KiB, rounded up).
QUOTA=RES-STORAGE
RFC 9208
✅
In KiB.
QUOTA=RES-MESSAGE
RFC 9208
✅
SORT
RFC 5256
✅
ARRIVAL CC DATE FROM REVERSE SIZE SUBJECT TO, plus DISPLAYFROM/DISPLAYTO with SORT=DISPLAY. An unknown sort key is BAD. The charset of SORT and THREAD converts the search strings like SEARCH CHARSET; an unknown one is NO [BADCHARSET]. A MODSEQ key appends (MODSEQ n) (RFC 7162). Without RETURN the answer stays * SORT, also after ENABLE IMAP4rev2.
ESORT
RFC 5267
✅
SORT/UID SORT RETURN (MIN MAX ALL COUNT SAVE) answer with ESEARCH: MIN/MAX are the first/last message in sort order, ALL lists the matches in sort order (only ascending runs become ranges), RETURN () means ALL. CONTEXT=SEARCH/CONTEXT=SORT from the same RFC are not offered.
SORT=DISPLAY
RFC 5957
✅
DISPLAYFROM, DISPLAYTO: the decoded display name of the first address, else mailbox@host, else the mailbox; ASCII case-folding (i;ascii-casemap).
THREAD=REFERENCES
RFC 5256
✅
Simplified JWZ algorithm.
THREAD=ORDEREDSUBJECT
RFC 5256
✅
OBJECTID
RFC 8474
✅
EMAILID, THREADID (fetch and search), MAILBOXID (SELECT and STATUS); CREATE returns [MAILBOXID].
PREVIEW
RFC 8970
✅
Stored preview text, computed at delivery, so PREVIEW (LAZY) answers like PREVIEW; a message without text has "", never NIL.
SAVEDATE
RFC 8514
✅
FETCH SAVEDATE; search keys SAVEDBEFORE, SAVEDON, SAVEDSINCE and SAVEDATESUPPORTED (matches every message). A message stored without a save date uses its internal date.
Mailbox and server entries, MAXSIZE, DEPTH. No entry-count limit (TOOMANY) and no unsolicited METADATA responses.
CATENATE
RFC 4469
✅
TEXT, URL and UTF8 (RFC 6855) parts; URLs are local IMAP URLs (RFC 5092). A failing URL gets [BADURL <url>] with that URL (cut after 1024 bytes, ] percent-encoded). No URLAUTH. An oversized result is [TOOBIG], in APPEND and REPLACE alike.
Before login, any command other than the not-authenticated ones gets
NO Please authenticate first; a UID prefix on a command that has
no UID form is BAD. Pending changes are polled before every command
in the selected state; FETCH, STORE and SEARCH without
UID hold back expunge responses as RFC 9051 §7.5.1 requires.
FETCH
Item
Status
Notes
UID, FLAGS, INTERNALDATE, RFC822.SIZE, ENVELOPE
✅
ENVELOPE reports obsolete source routes as the at-domain-list; messages stored before 0.36.0 keep their cached envelope without routes.
BODY, BODYSTRUCTURE
✅
The MD5 field is always NIL.
RFC822, RFC822.HEADER, RFC822.TEXT
✅
BODY[section]<partial>, BODY.PEEK[…]
✅
HEADER, TEXT, MIME, HEADER.FIELDS, HEADER.FIELDS.NOT, part numbers.
BINARY[…], BINARY.PEEK[…], BINARY.SIZE[…]
✅
RFC 3516; [UNKNOWN-CTE] for an undecodable part.
MODSEQ
✅
RFC 7162.
EMAILID, THREADID
✅
RFC 8474; the email ID is the message GUID, the thread ID comes from the root of the References chain.
PREVIEW
✅
RFC 8970; also PREVIEW (LAZY).
SAVEDATE
✅
RFC 8514; always a date, never NIL.
Macros ALL, FAST, FULL
✅
Modifiers CHANGEDSINCE, VANISHED
✅
VANISHED needs UID FETCH and QRESYNC enabled.
A non-PEEK body fetch sets \Seen if the mailbox was opened read-write
(never after EXAMINE) and the session holds the s right, and returns
FLAGS unasked. At most 64 items per FETCH.
SEARCH, SORT and THREAD
Area
Supported
Flag keys
ALL ANSWERED DELETED DRAFT FLAGGED SEEN and their UN… forms, NEW RECENT OLD, KEYWORD, UNKEYWORD
Date keys
BEFORE ON SINCE (internal date), SENTBEFORE SENTON SENTSINCE, SAVEDBEFORE SAVEDON SAVEDSINCE SAVEDATESUPPORTED (RFC 8514), YOUNGER OLDER (RFC 5032)
Size keys
LARGER SMALLER
Set keys
sequence sets, UID <set>, $ (RFC 5182)
Object keys
MODSEQ (RFC 7162), EMAILID, THREADID (RFC 8474)
Text keys
FROM TO CC BCC SUBJECT BODY TEXT HEADER
Logic
NOT, OR, parentheses
Charset
UTF-8, US-ASCII and every charset in the WHATWG list; unknown charsets get [BADCHARSET (UTF-8)]. After ENABLE UTF8=ACCEPT only UTF-8 and US-ASCII (anything else is BAD).
Result forms
classic SEARCH, ESEARCH with RETURN (MIN MAX ALL COUNT SAVE); always ESEARCH after ENABLE IMAP4rev2. SORT answers * SORT, or ESEARCH in sort order with RETURN (ESORT)
SORT keys
ARRIVAL CC DATE FROM REVERSE SIZE SUBJECT TO DISPLAYFROM DISPLAYTO
THREAD
REFERENCES, ORDEREDSUBJECT
Text keys match substrings, without regard to case, as RFC 3501 and
RFC 9051 specify, against the decoded message: header names and values
(encoded words decoded, raw 8-bit text in fts.fallback_charset) and
the body as the text of its text/* parts converted to UTF-8 (HTML
reduced to its visible text), the file names of all parts and the
header and text of attached messages. TEXT matches the header or the
body, HEADER <name> <value> the field value. The full-text index
(FTS) supplies candidates — never fewer than the
messages that match — for the header and the body of every key,
wherever it is in the tree (AND intersects, OR unites, NOT does
not narrow). Each candidate is checked: headers against the decoded
header the index keeps per message, the body by reading the message
only where the body candidates include it, fts.verify_parallel
(default 8) messages at a time under the memory budget. A value that
may begin inside a header word (TEXT, HEADER with names other than
Subject and the address fields) checks the stored header of every
message of the mailbox; a key with no word the index can narrow by (one
or two letters inside a word) reads every message. A message expunged
during the search is no match; if a message cannot be read for another
reason, the command fails with NO [SERVERBUG] instead of returning a
partial result. A search whose client closes the connection stops
checking messages, and the session ends without an answer. With
fts.imap_search: word (default substring) the index answers
SUBJECT, BODY, TEXT and HEADER Subject alone, by word (all words in any order, stem, part of a word), without reading
messages. An account whose index lacks the version marker (indexed
before this release, or a rebuild that has not completed) does not use
the index and checks every message until fts_rebuild has run.
Two settings, both off by default, change what BODY and the body part
of TEXT find (0.50.0; storage):
fts.attached_headers: nosig leaves the signature and trace fields
of attached messages (DKIM-Signature, ARC-*,
Authentication-Results, Received, Received-SPF, X-*-Signature,
also in a text/rfc822-headers part) out of the body text — of the
index and of the text each candidate is checked against alike. A
search for a word that occurs only there finds nothing. The header
of the message itself (HEADER, TEXT, FROM, …) is not affected.
fts.dict_filter: ids gives a body word with letters and digits of
at least fts.dict_id_min_runes (default 25) characters — a tracking
token, a hash, a base64 run — no trigram keys in the index of an
account whose index generation was built with it. Such a word is
found as a whole word and by its start (and a word of more than 40
characters also by the start of each of its 40-character windows:
offsets 0, 20, 40, … and the last one), not as a
part from its inside: a message is missing from a BODY or TEXT
result exactly when a word of the search occurs in its body only as
an inner or end piece of such identifiers. SUBJECT, FROM, TO,
CC, BCC and HEADER keep substring matching. RFC 9051 6.4.4
leaves the matching of BODY and TEXT to the server ("only
performing full word matching", "full-word matching"), so this is
within IMAP4rev2; RFC 3501 has no such exception, and for IMAP4rev1
sessions the setting is a documented deviation, as
fts.imap_search: word is. The index is shared by every session, so
there is no switch per session.
While the accounts are rebuilt after fts.attached_headers: nosig
was set (with online rebuilds since 0.51.0, account by account in
the reshard window), an account not rebuilt yet still has an index
with the signature fields: IMAP BODY and TEXT already check the
messages without them, so a word that occurs only there is not
found — the new semantics, only earlier. JMAP finds it until the
account is rebuilt.
Up to limits.keywords_per_mailbox (default 1000) distinct keywords per mailbox; a STORE, APPEND, COPY or MOVE that would add more is NO [LIMIT] (see account limits). Stored lower-case. System flags and $Forwarded $MDNSent $Junk $NotJunk $Phishing are returned in their canonical spelling, other keywords lower-case.
PERMANENTFLAGS
✅
Follows the session's rights; includes \* with the w right.
\Recent
✅
Session-scoped; SELECT claims it, EXAMINE does not. Not shown after ENABLE IMAP4rev2.
Each reader has their own \Seen; other flags are shared.
Mailboxes
Feature
Status
Notes
Hierarchy delimiter
✅
/
INBOX
✅
Case-insensitive, also as INBOX/… prefix; cannot be deleted. RENAME INBOX moves its messages into the new mailbox (RFC 9051).
Mailbox names
✅
Stored in UTF-8. Sessions that enabled UTF8=ACCEPT or IMAP4rev2 use UTF-8 both ways; all others use modified UTF-7 (RFC 3501 §5.1.3) both ways, in its canonical form only (anything else is NO [CANNOT], except in a LIST/LSUB reference or pattern, which is then taken as it stands and lists nothing); raw UTF-8 from such a session is understood, octets that are not UTF-8 are BAD. CATENATE URLs carry UTF-8 names. New names are put into Normalization Form C: CREATE and RENAME answer with LIST … ("OLDNAME" …), to IMAP4rev1 sessions as LIST-EXTENDED data as well. LIST/LSUB references and patterns and NOTIFY mailbox names in another normalization form also match.
LIST attributes
◐
\HasChildren, \HasNoChildren, \Noselect, \NonExistent, \Subscribed, CHILDINFO, and the special-use attributes when asked for. Not \Marked, \Unmarked, \NoInferiors, \Remote.
LSUB
✅
Still served.
STATUS items
✅
MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN DELETED DELETED-STORAGE SIZE HIGHESTMODSEQ MAILBOXID APPENDLIMIT; no RECENT after ENABLE IMAP4rev2.
SELECT responses
✅
EXISTS, RECENT, [UNSEEN], [UIDVALIDITY], [UIDNEXT], FLAGS, [PERMANENTFLAGS], [HIGHESTMODSEQ], [MAILBOXID], [READ-WRITE]/[READ-ONLY]. After ENABLE IMAP4rev2 a LIST response replaces RECENT and [UNSEEN].
COPY/MOVE to a missing mailbox
✅
[TRYCREATE] in the own namespace, [NONEXISTENT] in shared ones.
Mailbox being deleted
✅
While a mailbox_delete job with the mailbox lock (since 0.52.0) holds a mailbox, SELECT, EXAMINE, STATUS, APPEND, COPY/MOVE into it, STORE/EXPUNGE in it, CREATE under it, RENAME of it or into it and DELETE answer NO [UNAVAILABLE] Mailbox is being deleted; LIST shows it until it is gone. A session that has it selected is closed with BYE [UNAVAILABLE] Selected mailbox is being deleted — at once in IDLE, otherwise at its next command — and a session whose selected mailbox another session deleted with BYE [UNAVAILABLE] Selected mailbox was deleted (RFC 2180 §3.3), only after a registry read confirmed it, never on a transport error. No other session is touched.
Auto-created folders
✅
Drafts, Sent, Trash, Junk, Archive with special-use, created (and subscribed) at IMAP login and on the first JMAP session when missing; the list is configurable with server.autocreate (name, special_use, subscribe).
Other users' folders
✅
Shared/<account>/<mailbox> per ACL; the account part is case-insensitive.
Public folders
✅
Public/…, stored in the ~public account with its own quota.
APPEND and literals
Feature
Status
Notes
APPEND with flags and date
✅
Flags the session has no right for are dropped.
MULTIAPPEND
✅
RFC 3502, atomic.
CATENATE
✅
RFC 4469; see the capability table.
REPLACE
◐
RFC 8508; see the capability table.
literal8 / binary content
✅
A single-part binary message is re-encoded to base64.
UTF8 (literal8)
✅
RFC 6855 §4, after ENABLE UTF8=ACCEPT; also as a CATENATE part and in REPLACE.
Message size limit
✅
256 MiB for all literals of one command together (APPENDLIMIT, not configurable), at most 256 literals per command, bounded by the process memory budget; oversized literals or an exhausted budget get [TOOBIG].
Command line limit
✅
64 KiB, lists nested at most 16 deep.
ACL
RFC 4314 with the rights l r s w i p k x t e a. The obsolete c maps
to k and d to x t e.
Feature
Status
Notes
SETACL with +/-, DELETEACL
✅
Need a. Both record a share notification for the grantee (RFC 9670 §3; not for anyone). A grant to an account whose purge already deleted its data answers NO [UNAVAILABLE] and changes nothing; removing rights still works.
GETACL, LISTRIGHTS
✅
Need a.
MYRIGHTS
✅
Identifier anyone
✅
Effective rights are the user's entry plus anyone.
Identifiers authenticated, anonymous
✗
Negative rights (-identifier)
✗
Owner rights
✅
Always all rights; cannot be changed ([CANNOT]).
Where each right is checked: l listing and (un)subscribing shared
folders, r select, status, metadata reads and CATENATE URLs,
s/w/t store (s also the implicit \Seen of FETCH), w shared
metadata entries, i append, copy, move and replace target, k create
and rename in a shared parent, x delete and rename, e expunge and
close, t and e move and replace source, a ACL commands and the
IMAPSieve script entry.
METADATA
Feature
Status
Notes
/shared/…, /private/… entries
✅
Printable ASCII names; values up to 64 KiB ([METADATA MAXSIZE 65536]).
Server entries (mailbox "")
✅
Shared server entries are read-only for users; private ones can be written.
JMAP identities and vacation; read-only over IMAP.
[METADATA TOOMANY]
✅
A SETMETADATA that would take the account beyond limits.metadata_entries (default 1000) entries.
Unsolicited METADATA responses
✗
NOTIFY and IDLE
Feature
Status
Notes
NOTIFY SET [STATUS], NOTIFY NONE
✅
RFC 5465.
Filters
◐
SELECTED, SELECTED-DELAYED, INBOXES (INBOX only), PERSONAL, SUBSCRIBED, SUBTREE, MAILBOXES; own namespace only, at most 64 mailboxes ([LIMIT]).
Events
◐
MessageNew (with fetch attributes), MessageExpunge, FlagChange. Every other event (MailboxName, SubscriptionChange, MailboxMetadataChange, ServerMetadataChange, extension events) gets NO [BADEVENT (MessageNew MessageExpunge FlagChange)] (RFC 5465 §3.1). MessageNew and MessageExpunge must be given together; FlagChange needs both.
Other mailboxes
✅
Reported as STATUS (MESSAGES UIDNEXT UIDVALIDITY, plus HIGHESTMODSEQ with CONDSTORE or FlagChange).
IDLE
✅
Authenticated and selected state.
Change detection
✅
NOTIFY follows the account's change log through one watch on the account object per pod (not one per mailbox and session); a selected shared or public mailbox through its owner's. The mailboxes the new entries name get their STATUS or poll; when the log no longer reaches back far enough, every notified mailbox is looked at. Every 60 s the notifier also reads the log without a notify: a change whose log entry is still announced (recorded after its notify, at most 60 s later when its writer died) or that fell into a watch the pod was setting up again is reported then at the latest, where before the mailbox watch reported it at once. After a SELECT under NOTIFY the selected mailbox is polled once. Without an account watch (an account whose change log was never written) NOTIFY falls back to one watch per mailbox. A connection follows at most server.imap_max_watches watches (default 8, at least 2), one of them always kept for IDLE; when the fallback watches do not fit, NOTIFY SET answers NO [LIMIT] and the earlier NOTIFY state stays as it was. The responses a client gets are the same as before.
Connection handling
Feature
Value
Session idle timeout
30 min (fixed; suspended during IDLE)
Write timeout
2 min (fixed)
Per-IP connection limit
server.max_conns_per_ip, refused with * BYE [UNAVAILABLE]; default unlimited
Per-user connection limit
server.max_conns_per_user, NO [LIMIT] at login; default unlimited
Shutdown
* BYE [UNAVAILABLE] Server shutting down, please reconnect to every session after a short readiness pause; the server then waits up to server.drain_seconds (default 20) for clients to leave
Maintenance lock
* BYE [UNAVAILABLE] Mailbox maintenance… while an import or repair holds the account. A reshard or rebuild of the full-text index that runs online (since 0.51.0, with the cluster mark fts/online) takes no lock: sessions, logins and deliveries go on, a search reads the old index until the switch and starts over in the new one once; at most a delivery or APPEND waits up to fts.build_hold_ms (2 s) before the switch of an account with a steady stream of deliveries. Offline runs (without the mark, or with an older server in the cluster) lock the account as before
Protocol trace
Per account and time-limited, via shardpostctl trace
Conformance
The full Dovecot imaptest run against 0.37.0 on 2026-10-01 covered 72
test groups: none failed (base protocol 0 of 460 commands, extensions 0
of 272), 8 groups were skipped for capabilities the server does not
offer. Earlier runs and their assessment are in the
conformance results.
POP3
internal/pop3proto. Serves the INBOX only.
Standard / capability
RFC
Status
Notes
Base protocol
RFC 1939
◐
USER PASS STAT LIST UIDL RETR TOP DELE RSET NOOP QUIT. APOP and LAST are not implemented.
CAPA
RFC 2449
✅
TOP, UIDL
RFC 1939 / 2449
✅
RESP-CODES
RFC 2449
✅
[IN-USE], [AUTH], [SYS/TEMP], [SYS/PERM].
AUTH-RESP-CODE
RFC 3206
✅
PIPELINING
RFC 2449
✅
STLS
RFC 2595
⚙
On a plain connection with a certificate configured.
SASL PLAIN XOAUTH2 OAUTHBEARER
RFC 5034, 4616, 7628
✅
Initial response optional; * cancels; RFC 7628 error continuation. Advertised only with TLS or server.insecure_auth. AUTH without argument is not supported.
USER
RFC 2449
✅
Advertised only with TLS or server.insecure_auth.
UTF8
RFC 6856
◐
The command is accepted; LANG is not implemented.
IMPLEMENTATION
RFC 2449
✅
ShardPost.
EXPIRE, LOGIN-DELAY
RFC 2449
✗
Behaviour:
Feature
Notes
Locking
Exclusive RADOS lock per maildrop (10 min, renewed); a second session gets [IN-USE]. A lost lock discards pending deletions.
Deletion
DELE marks, QUIT expunges, RSET clears.
\Seen
RETR sets \Seen when server.pop3_mark_seen is on (default); TOP never does.
UIDL
The imported POP3 UIDL if a migration provided one, otherwise the message GUID.
Limits
Line 4096 bytes; idle timeout 10 min; no per-IP or per-user connection limit.
LMTP
internal/lmtpd, built on the vendored emersion go-smtp.
Feature
RFC
Status
Notes
LMTP
RFC 2033
✅
Per-recipient status after DATA and BDAT.
PIPELINING
RFC 2920
✅
8BITMIME
RFC 6152
✅
ENHANCEDSTATUSCODES
RFC 2034, RFC 3463
✅
CHUNKING (BDAT)
RFC 3030
✅
SMTPUTF8
RFC 6531
✅
SIZE
RFC 1870
✅
256 MiB, fixed (not configurable).
LIMITS RCPTMAX=100
RFC 9422
✅
STARTTLS
RFC 3207
⚙
With a certificate configured.
AUTH PLAIN
RFC 4954
⚙
Optional service credentials (server.lmtp_auth_user); advertised only with TLS or server.insecure_auth. When configured, MAIL or RCPT before AUTH gets 530 5.7.0 (RFC 4954 §6).
BINARYMIME, DSN, REQUIRETLS, DELIVERBY, XCLIENT
—
✗
Delivery:
Feature
Notes
Recipient lookup
Each RCPT is resolved through Nauthilus (recipient_lookup); unknown: 550 5.1.1, backend failure: 451 4.3.0. Alias and +detail mapping is up to Nauthilus.
Sieve
Before scripts, the user's active script, after scripts, then the JMAP vacation response. See Sieve language.
Quota
Checked when the message is stored, not at RCPT. Over quota: 552 5.2.2.
Per-recipient replies
Sieve ereject550 5.7.1; Sieve reject250 and a rejection MDN to the sender, falling back to 550 5.7.1 without a relay, when the relay refuses the MDN, with sieve_reject_mode=protocol, or for automatic and list mail (see Sieve language); too large 552 5.3.4, memory budget 452 4.3.1, redirect failed 451 4.4.0, account locked 451 4.2.1, other 451 4.3.0.
Headers added
Return-Path: <sender> (<> for the null sender), Delivered-To: <rcpt>, then Received: from … by <server.lmtp_hostname> with LMTP for <rcpt>. Return-Path fields the message brought along are removed. server.lmtp_return_path (default on) and server.lmtp_delivered_to (final, the default, or none) switch the first two.
Atomicity
fileinto to several mailboxes is all-or-nothing; redirect, vacation and notify run only after the message is durable.
Local delivery (LDA)
POST /admin/v1/deliveries, or shardpostctl deliver (see
shardpostctl-deliver(1)). Uses the
same delivery pipeline as LMTP.
Feature
Notes
Recipient
-d address or account
Envelope sender
-f; empty means the null sender
Target mailbox
-m skips Sieve and the INBOX fallback; public folders need the p right; another account's mailbox is refused. The public namespace itself is not a recipient.
Sieve
--no-sieve skips it
Quota
--no-enforce-quota, needs the deliver:noenforce role
Header
Return-Path, Delivered-To (same settings as LMTP), Received: from … by <server.lmtp_hostname> with LDA
Result
disposition: stored, discarded or rejected (a Sieve reject that sent a rejection MDN); a protocol-level Sieve rejection is HTTP 422
Exit codes
sysexits: 0, 64, 67, 69, 70, 73, 75, 77
ManageSieve
internal/sieve/managesieve.go, RFC 5804.
Command
Status
Notes
CAPABILITY, NOOP, LOGOUT
✅
NOOP "tag" answers OK (TAG "tag") "Done" (RFC 5804 §2.13).
STARTTLS
⚙
With a certificate configured.
AUTHENTICATE
✅
PLAIN, XOAUTH2, OAUTHBEARER; initial response optional. Without TLS (and without server.insecure_auth) the mechanism list is empty and the reply is NO (ENCRYPT-NEEDED). A temporarily unavailable backend gives NO (TRYLATER).
LISTSCRIPTS, GETSCRIPT, PUTSCRIPT, CHECKSCRIPT
✅
Scripts are compiled with the user's include resolver.
SETACTIVE
✅
SETACTIVE "" deactivates. Scripts that need imapsieve cannot be activated for delivery.
DELETESCRIPT, RENAMESCRIPT
✅
DELETESCRIPT answers (ACTIVE) for the active script; RENAMESCRIPT answers (ALREADYEXISTS) and (NONEXISTENT); GETSCRIPT answers (NONEXISTENT).
HAVESPACE
◐
Checks the size only ((QUOTA/MAXSIZE)), not a script count.
UNAUTHENTICATE
✅
RFC 5804 §2.14.1; advertised in the capability list. Returns to the non-authenticated state, TLS stays; with arguments, or when not authenticated, NO.
PLAIN XOAUTH2 OAUTHBEARER (empty without TLS unless server.insecure_auth)
STARTTLS
⚙
MAXREDIRECTS
4
VERSION
1.0
NOTIFY
mailto (the one enotify method)
UNAUTHENTICATE
advertised
OWNER, LANGUAGE
not advertised
Limits: script 256 KiB, script name 512 bytes without control
characters, command line 8 KiB, idle timeout 10 min, no limit on the
number of scripts. The reserved script
jmap-vacation is listed and readable but cannot be changed. There is
no implicit-TLS ManageSieve listener.
Sieve language
The interpreter is a fork of foxcpp/go-sieve in third_party/go-sieve.
The SIEVE capability lists the extensions below; the ⚙ ones only when
configured.
Extension
RFC
Status
Notes
Base language (keep, discard, redirect, stop, if, address, header, exists, size, …)
RFC 5228
✅
fileinto
RFC 5228
✅
With :copy, :flags, :create, :specialuse. A target a mailbox_delete job is about to remove (since 0.52.0) takes the message like any other failed fileinto: it goes to INBOX (implicit keep), once — a keep beside it does not store a second copy, in either order — with a delivery intent that names INBOX, so a rollback after a crash finds it there.
envelope
RFC 5228
✅
from, to, auth.
encoded-character
RFC 5228
✅
comparator-i;octet, comparator-i;ascii-casemap
RFC 4790
✅
Built in.
comparator-i;ascii-numeric
RFC 4790
✅
comparator-i;unicode-casemap
RFC 5051
✅
reject, ereject
RFC 5429
✅
ereject refuses the message in the protocol (LMTP 550 5.7.1, local delivery API 422). reject sends a rejection message — an MDN (RFC 8098) from the null sender, a global MDN (RFC 6533) for non-ASCII recipients or headers — and the message counts as delivered. Mail from the null sender is discarded silently; for automatic and list mail (RFC 3834 rules) the rejection is made in the protocol (550), as it is without a relay or when the relay refuses the MDN; an MDN whose acceptance the relay only failed to confirm in time counts as sent. Settings sieve_reject_mode (mdn, the default, or protocol), rejection_subject, rejection_reason, postmaster_address.
imap4flags
RFC 5232
✅
variables
RFC 5229
✅
Including :encodeurl (RFC 5435).
relational
RFC 5231
✅
copy
RFC 3894
✅
subaddress
RFC 5233
✅
environment
RFC 5183
◐
During delivery only empty imap.* items exist; domain, host, name, version, location, phase are unknown. In IMAPSieve: location, phase and the imap.* items.
body
RFC 5173
✅
vacation
RFC 5230
✅
:days, :subject, :from, :addresses, :mime, :handle, :fcc (RFC 8580). The period is clamped to sieve_vacation_min_period (default 1 day) … sieve_vacation_max_period (default 60 days, 0 = no maximum); without :days it is sieve_vacation_default_period (default 7 days).
vacation-seconds
RFC 6131
✅
Clamped like :days; :seconds 0 replies to every message when sieve_vacation_min_period is 0.
Protected headers configurable (default Received, Auto-Submitted, Return-Path, Delivered-To); edits of a protected header are silently ignored.
mailbox
RFC 5490
✅
mailboxexists, fileinto :create.
mboxmetadata, servermetadata
RFC 5490
✅
ihave
RFC 5463
✅
include
RFC 6609
✅
:personal, :global, :once, :optional, global, return; by default at most 255 includes and depth 10 (sieve_include_max_includes, sieve_include_max_nesting_depth).
regex
draft-ietf-sieve-regex
✅
spamtest, spamtestplus, virustest
RFC 5235
✅
Values taken from configurable headers.
special-use
RFC 8579
✅
specialuse_exists, fileinto :specialuse.
enotify
RFC 5435
◐
Only the mailto method (RFC 5436); :fcc (RFC 8580); by default at most 10 notifications per script (sieve_notify_max_notifications, 0 forbids notify).
mime, foreverypart, extracttext
RFC 5703
◐
replace and enclose are not implemented.
fcc
RFC 8580
✅
:fcc with :create, :flags, :specialuse on vacation and notify. The copy is stored after the message was sent, under the user's quota; in IMAPSieve only into existing mailboxes.
sieve.before_dir scripts, the user's active script, sieve.after_dir scripts (Pigeonhole sieve_before/sieve_after semantics), then the JMAP VacationResponse when no script replied and the message is kept. The next script runs only while the message is still kept. An explicit keep is sticky across scripts: a later fileinto or discard cancels only the implicit keep. A reject after an earlier script's explicit keep, fileinto :copy, redirect, pipe or vacation fails with an action conflict and the message stays kept; policy rejects belong in sieve.before_dir.
Global includes
sieve.global_dir serves include :global.
Runtime limits
5 s wall-clock per script (sieve_max_cpu_time), 32 actions (sieve_max_actions; 8 in IMAPSieve), MIME tree at most 1000 parts and depth 20, foreverypart nesting 4, 5000 tokens, block nesting 15, 128 variables of at most 4000 bytes.
Redirect
At most 4, duplicates collapsed, a redirect to the recipient itself is dropped; envelope sender kept; loop protection by X-Sieve-Redirected-From, Auto-Submitted and more than 25 Received headers. Without an SMTP relay the message is kept instead. In IMAPSieve the redirect is sent from the mailbox owner.
Vacation
Reply tracking per sender and handle in the user's store; RFC 5230 §4.6 rules (no reply to the null sender, to automatic mail, to lists, or when the user is not a recipient). Envelope sender null by default. Period from the sieve_vacation_*_period settings; a :fcc copy of the reply is stored after it was sent.
Notify
mailto over the relay with Auto-Submitted: auto-notified; never for automatic mail; the sender without :from is postmaster_address; envelope sender chosen by sieve_notify_mailto_envelope_from, null for null-sender mail.
Settings
sieve.settings holds key=value pairs separated by ;; \; is a literal semicolon and \\ a backslash inside a value.
Failure
A failing script is skipped as if it had kept the message; no active script means INBOX.
The Pigeonhole test suite runs 139 of 148 files successfully with 9
excluded and none failing; see the
Sieve conformance report.
keep, flags, fileinto (existing mailboxes only; the original is marked \Deleted without keep), redirect, notify (with :fcc into existing mailboxes) and external programs for the mailbox owner. reject, vacation and envelope are not available.
Limits
✅
imapsieve.timeout_ms (default 500 ms), imapsieve.max_parallel (default 4); one follow-up FLAG run after APPEND/COPY.
Learning queue
⚙
Operator rules for COPY/MOVE between special-use folders feed a durable queue with retries; the classifier (Rspamd, spamc, …) is an external program.
JMAP
internal/jmap; details in JMAP and
JMAP by example. JMAP runs only when jmap.addr is
set.
Capabilities
Capability URN
RFC
Status
Notes
urn:ietf:params:jmap:core
RFC 8620
✅
See the limits below.
urn:ietf:params:jmap:mail
RFC 8621
✅
maxSizeMailboxName 255, maxSizeAttachmentsPerEmail = upload limit, no mailbox-per-email or depth limit.
urn:ietf:params:jmap:submission
RFC 8621
⚙
With jmap.submission.relay_addr. Delayed sending (FUTURERELEASE, RFC 4865: HOLDFOR/HOLDUNTIL) with jmap.submission.delayed.max_delay_seconds (0.43.0); otherwise maxDelayedSend 0 and no submission extensions.
urn:ietf:params:jmap:vacationresponse
RFC 8621
⚙
With an SMTP relay (smtp.addr).
urn:ietf:params:jmap:quota
RFC 9425
✅
urn:ietf:params:jmap:blob
RFC 9404
✅
Digests sha-256, sha; types Email, Mailbox, Thread, SieveScript; maxSizeBlobSet = upload limit, maxDataSources at least 64.
urn:ietf:params:jmap:sieve
RFC 9661
✅
Same extension list as ManageSieve; maxSizeScript 256 KiB, maxSizeScriptName 512, maxNumberRedirects 4, no script-count limit; notification method mailto.
urn:ietf:params:jmap:principals
RFC 9670
✅
urn:ietf:params:jmap:principals:owner
RFC 9670
✅
On the personal and on other users' accounts, not on ~public.
urn:ietf:params:jmap:mail:share
draft-ietf-jmap-mail-sharing
✅
shareWith, mayShare.
urn:ietf:params:jmap:webpush-vapid
RFC 9749
⚙
With a VAPID key.
urn:ietf:params:jmap:websocket
RFC 8887
✗
urn:ietf:params:jmap:mdn
RFC 9007
✗
Contacts, calendars
RFC 9610, draft-ietf-jmap-calendars
✗
Core limits (configurable under jmap.*): maxSizeUpload 50 MiB,
maxSizeRequest 10 MiB, maxConcurrentUpload 4,
maxConcurrentRequests 8, maxCallsInRequest 16, maxObjectsInGet
500, maxObjectsInSet 500; collations i;ascii-casemap and i;octet.
Not advertised: downloads (jmap.max_concurrent_downloads, 8) and event
sources (jmap.max_event_sources, 4) per account. A request at a full
limit queues first come, first served for up to jmap.limit_wait
(2 s, at most twice the limit waiting), then gets 429 with
Retry-After (jmap.retry_after, 1 s): the
urn:ietf:params:jmap:error:limit problem for an advertised limit, a
plain about:blank problem for the others; an event source gets 429 at
once. An optional bound per client address before the authentication:
jmap.max_concurrent_per_ip (default off; open event sources do not
count against it). Blob
limits: max_size_blob_set, max_data_sources, max_size_blob_get
(10 MiB). Search limits: jmap.search.snippet_read_bytes (default 256 KiB),
jmap.search.max_attachment_reads (default 1000).
Methods
Data type
Methods
Notes
Core
Core/echo
Mailbox
get, changes, query, queryChanges, set
queryChanges always cannotCalculateChanges; query supports sortAsTree and filterAsTree.
queryChanges only for the default sort, without collapseThreads and without thread-keyword conditions.
SearchSnippet
get
Subject and a body preview (at most 255 octets) with the matched words in <mark>; the body is read up to jmap.search.snippet_read_bytes (default 256 KiB), a match beyond that gives no preview. Also on shared accounts.
Identity
get, changes, set
changes only from the current state.
EmailSubmission
get, changes, query, queryChanges, set
onSuccessUpdateEmail, onSuccessDestroyEmail; sent through the relay (see Outbound SMTP). Delayed sending with undoStatuspending and canceling to canceled when configured; sent at most once (an interrupted sending is final with deliveredunknown). query filters by identityIds, emailIds, threadIds, undoStatus, before, after with AND/OR/NOT; anything else is unsupportedFilter. queryChanges always cannotCalculateChanges.
VacationResponse
get, set
Stored as metadata and turned into a Sieve vacation at delivery; the user's own vacation wins.
Quota
get, changes, query, queryChanges
queryChanges always cannotCalculateChanges.
Blob
upload, get, lookup
Blob/copy is not implemented.
SieveScript
get, set, query, queryChanges, validate
queryChanges always cannotCalculateChanges.
Principal
get, changes, query, queryChanges, set
Read-only: set is always forbidden.
ShareNotification
get, changes, set, query, queryChanges
set can only destroy.
PushSubscription
get, set
⚙ With jmap.push.vapid_key_file (subscriptions are kept in the admin store); unknownMethod otherwise.
Only Mailbox, Email, Thread and SearchSnippet methods work on shared accounts.
All */changes methods are answered from the account change log,
which keeps server.account_log_retention entries (default 10000). A
state older than the oldest entry gets cannotCalculateChanges, and
the client resynchronises. The same log serves IMAP QRESYNC.
Email specifics
Feature
Supported
Email/query filters
inMailbox, inMailboxOtherThan, before, after, minSize, maxSize, allInThreadHaveKeyword, someInThreadHaveKeyword, noneInThreadHaveKeyword, hasKeyword, notKeyword, hasAttachment, text, from, to, cc, bcc, subject, body, header; AND, OR, NOT. hasAttachment comes from the index record; a message stored by an earlier version is read once, at most jmap.search.max_attachment_reads (default 1000) per query, after which a background fill starts and the query answers serverUnavailable until it is done (job attachment_backfill fills an account ahead of time).
All RFC 8621 properties, headers, bodyStructure, header:<name> with the forms asRaw, asText, asAddresses, asGroupedAddresses, asMessageIds, asDate, asURLs and :all.
Session URLs use jmap.public_base_url, the address clients reach
behind a proxy; when it is empty they are built from the request. With
jmap.shard_attribute and jmap.shard the server serves only the
accounts the authentication backend assigns to this shard.
Separate shardpost push worker; aes128gcm (RFC 8188), VAPID, https on port 443 only; internal endpoint addresses are refused unless jmap.push.allow_internal_endpoints.
WebSocket
RFC 8887
✗
Authentication
RFC 6750, RFC 7617
✅
Bearer (OAUTHBEARER through Nauthilus, RFC 7662 introspection) and Basic; refused without TLS unless jmap.insecure_auth (default off). Token introspection (RFC 7662) happens in the Nauthilus backend.
Errors
RFC 7807
✅
Request-level errors as problem details.
Sharing
Other users' folders appear as separate accounts named after the owner,
public folders as the account ~public. JMAP rights map to IMAP ACL
rights (mayReadItems = l r, mayAddItems = i, mayRemoveItems
= t e, maySetSeen = s, maySetKeywords = w,
mayRename/mayDelete = x, mayCreateChild = k, mayShare =
a). Shared accounts are listed whether or not the user subscribed to
them, a deviation from RFC 9670 §1.4. Bounds: jmap.max_shared_accounts
(default 64), jmap.max_shared_mailboxes (default 1000),
jmap.max_principals (default 1000), at most
jmap.max_principal_lookups (default 5) backend lookups per request and
jmap.principal_miss_per_minute (default 30) misses per account.
Outbound SMTP
The server sends mail only through a configured relay (smtp.*), never
directly to the internet.
smtp.addr. Without it, redirects keep the message, vacation and notify are skipped, and reject becomes a protocol-level rejection.
IMAPSieve redirect and notify
⚙
Same relay.
JMAP EmailSubmission
⚙
Separate jmap.submission.* relay. By default it names the user with Postfix XCLIENT LOGIN=… (jmap.submission.xclient; xclient_addr adds ADDR=) instead of AUTH. With xclient: false it logs in with jmap.submission.user/password, and the sender check (check_sender) is mandatory.
STARTTLS (RFC 3207) / implicit TLS
✅
STARTTLS is used whenever the relay offers it; smtp.starttls (default on) makes it mandatory. smtp.tls for implicit TLS (port 465). TLS 1.2 minimum.
AUTH PLAIN (RFC 4954) to the relay
✅
smtp.user, smtp.password[_file]; only sent over TLS or to localhost.
8BITMIME, SMTPUTF8 on MAIL FROM
✅
When the relay offers them.
DSN parameters
✗
RFC 3461 NOTIFY/RET/ENVID are not sent.
Authentication and SASL
Mechanism
IMAP
POP3
ManageSieve
LMTP
JMAP
Plain password (LOGIN / USER+PASS)
✅
✅
—
—
—
PLAIN (RFC 4616)
✅
✅
✅
⚙ service only
Basic
XOAUTH2
✅
✅
✅
✗
—
OAUTHBEARER (RFC 7628)
✅
✅
✅
✗
Bearer
LOGIN, CRAM-MD5, SCRAM-*, EXTERNAL
✗
✗
✗
✗
—
Feature
Notes
Backend
server.auth_backend: nauthilus (the operational profile) or static (code default, a test user list from server.users). Nauthilus is reached over gRPC AuthService (nauthilus.transport, default grpc) or its HTTP JSON API.
Bearer tokens
Passed to Nauthilus as a password, or checked by OAuth 2.0 token introspection (RFC 7662) with optional audience and scope checks (nauthilus.oidc_audience, nauthilus.oidc_scope).
Backchannel credentials
HTTP Basic, static bearer token or OAuth 2.0 client credentials (RFC 6749 §4.4); gRPC with TLS 1.2+ and optional mTLS, or plaintext with nauthilus.grpc_insecure.
✗ PLAIN with an authzid different from the authcid is refused. XOAUTH2 user= and OAUTHBEARER a= name the login; with token introspection they must resolve to the token's account.
Brute-force data
Client IP/port, local IP/port, protocol, mechanism, TLS state (ssl is on with TLS and empty without, as Dovecot and nginx send it; Nauthilus takes any value for TLS) and, for IMAP, POP3 and ManageSieve, the attempt ordinal of the connection (auth_login_attempt = 1 + the refused logins before it on that connection) are sent to Nauthilus. JMAP and LMTP send no ordinal.
Auth cache
auth.cache in front of Nauthilus (never the static backend): a successful login is answered from memory for ttl (30 s), a bearer token's entry ends at its exp when that comes first; lookups (LMTP recipients, the target of a master-user login, JMAP identities) for lookup_ttl (30 s), unknown names for lookup_negative_ttl (5 s). Identical concurrent requests share one backend call. Refusals are cached only with negative_ttl (default off) and only for identical credentials; temporary failures never. The master password is never cached. shardpostctl auth flush drops entries on every server; ttl: 0 switches the cache off. See Configuration.
Timeouts
One request to Nauthilus, the introspection or the token endpoint: nauthilus.timeout (10 s); a login as a whole: 15 s for IMAP, POP3 and ManageSieve. A refused backchannel token is renewed and the request retried once, only when the time left allows it.
Temporary failure
Reported as unavailable ([UNAVAILABLE], [SYS/TEMP], TRYLATER, 451, HTTP 503 with Retry-After), never as a wrong password.
Plaintext authentication
Allowed without TLS only with server.insecure_auth for IMAP, POP3, ManageSieve and LMTP (default off), and with jmap.insecure_auth for JMAP (default off).
Transport: TLS, PROXY protocol, listeners
Feature
Notes
TLS version
1.2 minimum, server and client side.
Certificate reload
The pair is re-checked on the first handshake after tls.reload_seconds (default 60; 0 loads once) and reloaded when the files changed; a pair that does not load keeps the old one.
Implicit TLS
IMAPS and POP3S (RFC 8314), only when a certificate is configured. Not for LMTP and ManageSieve.
PROXY protocol
HAProxy v1 and v2 on IMAP, IMAPS, POP3, POP3S, LMTP, ManageSieve and JMAP, accepted only from server.proxy_protocol_trusted_cidrs; 5 s header timeout; applied before TLS. Not on the metrics or admin listeners.
Graceful shutdown
Readiness goes red; after a short pause IMAP clients get BYE, POP3 clients get -ERR [SYS/TEMP] at the end, and sessions have up to server.drain_seconds (default 20) to finish. LMTP and ManageSieve listeners close at once.
Memory budget
server.memory_budget_bytes (default 256 MiB) bounds message buffers across all protocols.
Unix socket (only the server's own UID is admitted, with the configured socket roles) or TCP with mutual TLS (client certificate identity mapped to roles).
ETags with If-Match on mailbox, ACL and Sieve writes; Idempotency-Key on POST /jobs.
Audit
One audit log line per request; request metrics.
Endpoint groups
Server version and status; accounts, mailboxes, ACL (including explain), quota, shared folders; messages (list, show, export, flags, copy, move, and search bounded by admin.search_scan_limit, default 50000, continued with a cursor); Sieve (including a check dry run that sees the Return-Path and Delivered-To a delivery adds and reports the vacation period in days and seconds); learning queue; deliveries; plans and jobs (including cancel); export; import (session lock, mailboxes, messages, changes, ACL, public-folder readers, metadata, Sieve, finalize, full-text reshard); protocol trace; account directory (entry, repair); full-text layout (layout 3 mark and server registrations, accounts with layout 3, layout record of an account and its clean-up).
Jobs
index_check (role diagnose, runs directly); scrub, quota_recalc, acl_reconcile, fts_rebuild, fts_dict_convert (legacy: layout 2 indexes written before 0.35.0), fts_layout (converts the full-text index between layout 2 and layout 3 or reshards it), mailbox_name_convert (renames mailboxes stored under modified UTF-7 names, after an upgrade from 0.35.2 or earlier), attachment_backfill (fills in hasAttachment for messages stored by earlier versions) (role repair) and mailbox_delete, expunge, purge (role delete): plan, then apply. Any server with jobs.enabled (default on) claims queued jobs; a plan can be applied for jobs.plan_ttl_seconds (default 3600); finished jobs are kept jobs.retention_days (default 14).
shardpostctl groups
version, server, account (including quota and shared), mailbox, acl, message, sieve, job (plan, apply, check), learning, export (archive verify, restore), deliver, migrate, trace, auth, submission, fts (layout 3 mark, accounts with layout 3, layout record of an account).
Server process
Subcommand
Purpose
shardpost serve
Runs all configured listeners (the default).
shardpost config check
Validates the configuration and the secret files and compiles the before/after Sieve scripts, then exits; does not connect to Ceph or Nauthilus.
shardpost scrub
Walks all accounts (or scrub.users) and repairs what a crash left behind; scrub.grace_seconds (default 3600), scrub.dry_run only reports.
/healthz (process), /readyz (store reachable, not draining; 503 otherwise); JMAP also serves /jmap/healthz.
Tracing
OpenTelemetry OTLP (gRPC or HTTP/protobuf), W3C Trace Context and Baggage
Spans per IMAP, POP3 and ManageSieve command, JMAP request and method call (not the event source), LMTP delivery, admin operation and job, with store operations below them; personal attributes (account and mailbox names, client addresses, error texts) only with telemetry.user_attributes (default off); off without an endpoint; sampled with telemetry.sample_ratio (default 1.0).
Logs
JSON (slog)
Session ID and the client address from the PROXY header.
Protocol trace
—
IMAP only, per account and time-limited; literals by size, never credentials.
Ceph RADOS directly: an index pool (omap) and a data pool (may be erasure coded); one RADOS namespace per user.
Messages
Immutable blobs in chunks of ceph.chunk_size (default 4 MiB), shared by COPY; reference counts. No compression or encryption at rest.
Consistency
Compare-and-swap on object versions, delivery intents, repair jobs; see RADOS storage.
Credentials
ceph.key, or without it the Rook client secret (rook.namespace, rook.client_secret); the key is re-read every server.key_poll_seconds (default 60), and a rotated key is picked up without a restart.
Several replicas
Sessions are process-local; all durable state is in RADOS.
Full-text search
Feature
Notes
Fields
Subject, From/Sender/Reply-To, To/Cc/Bcc (every value), all header names and values (RFC 2047 in any WHATWG charset, raw 8-bit text in fts.fallback_charset), text/* parts converted to UTF-8 (the <meta> charset for HTML; UTF-8 under a single-byte label is kept as UTF-8), the visible text of HTML parts (fts.html: text, the default, raw or skip), the file names of all parts (RFC 2231, RFC 2047), header and text of attached messages (fts.attached_headers: nosig without their signature and trace fields, since 0.50.0); up to 4 MiB of text per message.
Words
Unicode letters and digits, 2–40 characters, lower-cased; longer words as their first 40 characters and overlapping 40-character windows (with marker terms beyond).
Stemming
Snowball English and German, always both.
Substrings
Trigram dictionary; with fts.dict_filter: ids (since 0.50.0, off by default) a body identifier of at least fts.dict_id_min_runes characters has one start entry instead of its trigrams and is found as a whole word and by its start only (see SEARCH).
Header store
The decoded header text of every message (DEFLATE, up to 64 KiB), so that header keys are checked without reading messages.
IMAP matching
Substrings (RFC 9051); the index is a candidate pre-filter per key and part, candidates are verified in parallel (fts.verify_parallel); fts.imap_search: word for word semantics. The index version marker is set by fts_rebuild (and on a fresh index); without it the index is not used. See SEARCH.
Not supported
Attachment content, CJK segmentation, other languages, SEARCH=FUZZY, MIME part header fields other than file names.
Quota
Feature
Notes
Resources
Storage bytes and message count.
Limit source
IMAP SETQUOTA (with server.quota_self_service), the user-database attribute server.quota_attribute (default rnsMSQuota, storage only), or the defaults server.quota_bytes/server.quota_msgs.
Rules
Grace, per-mailbox rules (ignore, extra allowance by name or special-use).
Warnings
External programs per resource and percent threshold, in the direction over or under. An over-quota flag in the user database is checked at login, after delivery and when usage crosses the limit.
Recount
The job quota_recalc, under the account lock or (since 0.52.0, cluster mark quota/online) online beside the writers, exact and unnoticed by users; see Quota: recounting the usage.
Shared/<account>/… by ACL (IMAP), separate accounts (JMAP).
Public folders
Public/… / ~public, own quota, private \Seen.
Share notifications
RFC 9670 §3 records on ACL changes; at most 50 per user by default (server.share_notifications_max, at most 200), kept 30 days (server.share_notifications_retention_seconds).
Account limits
Configurable upper bounds per account (section limits, see
Configuration; 0 switches
one off). What each protocol answers when a change would exceed one:
Limit (default)
IMAP
JMAP (RFC 8620 §5.3)
Admin API
Delivery (LMTP, LDA, admin deliver)
limits.mailboxes (10000)
CREATE: NO [LIMIT]
Mailbox/set create: overQuota
409 limit_exceeded
A Sieve fileinto that would create the mailbox goes to INBOX. INBOX is never refused.
POP3 creates nothing but INBOX and adds only \Seen, so it never meets
a limit. An account import is held to the mailbox limits but not to the
keyword limit.
Import, export and migration
Feature
Notes
Account import
shardpostctl migrate from an IMAP server (master-user LOGIN, default format {user}*{master}; optional PROXY header); keeps UIDs, UIDVALIDITY and MODSEQ, uses CONDSTORE, X-GUID and SAVEDATE when offered, copies Sieve scripts over ManageSieve, renders Dovecot POP3 UIDLs.
Import lock
An open import session holds the account's lock: IMAP, POP3, ManageSieve, LMTP and local delivery refuse the account temporarily until the lock is released or its TTL expires.
Export
tar archive with a SHA-256 manifest; archive verify, restore through the import API. The FTS index is not exported.