Start here

Protocols, features and RFCs

Per-protocol reference: capabilities, commands, methods, limits, deviations, RFC index.

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.

Contents

  1. Overview
  2. IMAP
  3. POP3
  4. LMTP
  5. Local delivery (LDA)
  6. ManageSieve
  7. Sieve language
  8. IMAPSieve
  9. JMAP
  10. Outbound SMTP
  11. Authentication and SASL
  12. Transport: TLS, PROXY protocol, listeners
  13. Admin API and shardpostctl
  14. Observability
  15. Mail store features
  16. Not supported at a glance
  17. RFC index

Overview

Protocol Role Listener (config key) Container default Service port TLS Main standards
IMAP Mailbox access server.imap_addr :1143 143 STARTTLS RFC 9051, RFC 3501
IMAPS Mailbox access server.imaps_addr :1993 993 implicit ⚙ certificate RFC 8314
POP3 Mailbox access (INBOX) server.pop3_addr :1110 110 STLS RFC 1939
POP3S Mailbox access (INBOX) server.pop3s_addr :1995 995 implicit ⚙ certificate RFC 8314
LMTP Delivery from the MTA server.lmtp_addr :2424 24 STARTTLS RFC 2033
ManageSieve Sieve script management sieve.addr :14190 4190 STARTTLS RFC 5804
JMAP Mailbox access over HTTP jmap.addr off 9443 (overlay) HTTPS RFC 8620, RFC 8621
Admin API Operations admin.socket / admin.addr off 8443 (component) Unix socket / mTLS OpenAPI 3.0.3
HTTP Metrics and health server.http_addr :8080 — none Prometheus

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.
ACL RFC 4314 ✅ See ACL.
RIGHTS=kxte RFC 4314 ✅
METADATA RFC 5464 ◐ 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.
NOTIFY RFC 5465 ◐ See NOTIFY.
WITHIN RFC 5032 ✅ YOUNGER, OLDER against the internal date.
REPLACE RFC 8508 ◐ Also UID REPLACE and with CATENATE. Not atomic (new message first, then the old one is expunged) and fires no IMAPSieve event.
UNAUTHENTICATE RFC 8437 ✅ Clears selection, NOTIFY and ENABLE state; TLS and compression stay.
APPENDLIMIT=<n> RFC 7889 ✅ 256 MiB, fixed (not configurable); also as a STATUS item.

Response codes from RFC 5530 (AUTHENTICATIONFAILED, UNAVAILABLE, PRIVACYREQUIRED, LIMIT, NOPERM, CANNOT, NONEXISTENT, TOOBIG, OVERQUOTA, SERVERBUG, …) are used throughout.

Commands

State Commands
Any CAPABILITY, NOOP, CHECK (same as NOOP), LOGOUT, ID, COMPRESS
Not authenticated STARTTLS, LOGIN, AUTHENTICATE
Authenticated and selected ENABLE, NAMESPACE, LIST, LSUB, CREATE, DELETE, RENAME, SUBSCRIBE, UNSUBSCRIBE, STATUS, APPEND, SELECT, EXAMINE, IDLE, NOTIFY, UNAUTHENTICATE, GETACL, SETACL, DELETEACL, LISTRIGHTS, MYRIGHTS, GETMETADATA, SETMETADATA, GETQUOTA, GETQUOTAROOT, SETQUOTA
Selected CLOSE, UNSELECT, EXPUNGE, FETCH, STORE, SEARCH, SORT, THREAD, COPY, MOVE, REPLACE
With UID prefix FETCH, STORE, SEARCH, SORT, THREAD, COPY, MOVE, REPLACE, EXPUNGE

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.

Not supported: FUZZY (RFC 6203), CONTEXT=SEARCH/CONTEXT=SORT (RFC 5267), PARTIAL (RFC 9394), MULTISEARCH (RFC 7377).

STORE, flags and keywords

Feature Status Notes
FLAGS, +FLAGS, -FLAGS, .SILENT ✅
UNCHANGEDSINCE ✅ Failed messages are listed in [MODIFIED].
Arbitrary keywords ◐ 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.
Rights per flag ✅ \Seen needs s, \Deleted needs t, everything else w (RFC 4314).
Private \Seen in public folders ✅ 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.
GETMETADATA MAXSIZE, DEPTH 0/1/infinity ✅ [METADATA LONGENTRIES n].
/shared/imapsieve/script ✅ Needs the a right; see IMAPSieve.
/private/vendor/shardpost/jmap/… ✅ 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 ereject 550 5.7.1; Sieve reject 250 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.
Capability Value
IMPLEMENTATION ShardPost
SIEVE the extension list from Sieve language
SASL 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.
date, index RFC 5260 ✅ date, currentdate, :index, :last.
duplicate RFC 7352 ✅ :handle, :header, :uniqueid, :seconds, :last; default 7 days.
editheader RFC 5293 ✅ 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.
imapsieve RFC 6785 ⚙ Only for IMAPSieve scripts.
vnd.dovecot.pipe, vnd.dovecot.filter, vnd.dovecot.execute Pigeonhole extprograms ⚙ Advertised when external programs are configured; only allowlisted programs, no shell.
vnd.dovecot.debug Pigeonhole ✅ Accepted, not advertised.
redirect-dsn, redirect-deliverby, convert, extlists, vnd.dovecot.environment, vnd.dovecot.report RFC 6009, 6558, 6134 ✗

Execution:

Feature Notes
Script sequence 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.

IMAPSieve

RFC 6785; see also Sieve and learning.

Feature Status Notes
Events ✅ APPEND (including MULTIAPPEND and CATENATE), COPY, MOVE (reported as cause COPY), FLAG (STORE, and FETCH setting \Seen). REPLACE fires nothing.
Script selection ⚙ With imapsieve.user_scripts (default on): mailbox entry /shared/imapsieve/script, otherwise the server entry; empty or missing script means no run.
/private/imapsieve/script ✗
Environment ✅ location, phase, imap.cause, imap.mailbox, imap.user, imap.email, imap.changedflags.
Actions ◐ 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.
Thread get, changes
Email get, changes, query, queryChanges, set, copy, import, parse 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 undoStatus pending and canceling to canceled when configured; sent at most once (an interrupted sending is final with delivered unknown). 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).
Email/query sort receivedAt, size, from, to, subject, sentAt, hasKeyword, allInThreadHaveKeyword, someInThreadHaveKeyword; collapseThreads; anchor, position, limit, calculateTotal.
Email/get properties All RFC 8621 properties, headers, bodyStructure, header:<name> with the forms asRaw, asText, asAddresses, asGroupedAddresses, asMessageIds, asDate, asURLs and :all.
Body values fetchTextBodyValues, fetchHTMLBodyValues, fetchAllBodyValues, maxBodyValueBytes, bodyProperties.

Endpoints and push

Endpoint Purpose
GET /.well-known/jmap Session resource
POST /jmap/api/ Method calls, result references (#), creation IDs
POST /jmap/upload/{accountId}/ Blob upload
GET /jmap/download/{accountId}/{blobId}/{name}?accept={type} Blob download with HTTP ranges, also from shared accounts (uploads go to the personal account only)
GET /jmap/eventsource/ Server-sent events (RFC 8620 §7.3); types required, closeafter, ping (clamped to 15–900 s), Last-Event-ID
GET /jmap/healthz Health check

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.

Feature RFC Status Notes
EventSource push RFC 8620 §7.3 ✅ Mailbox, Email, Thread, EmailDelivery, ShareNotification.
Web Push RFC 8030, RFC 8291, RFC 8292 ⚙ 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.

Feature Status Notes
Sieve redirect, vacation, enotify mailto, reject MDN ⚙ 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.
Master user Dovecot style user*master (separator configurable), password only, compared in constant time.
Proxy authorisation ✗ 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.

Admin API and shardpostctl

An HTTP/JSON API described by OpenAPI 3.0.3 under /admin/v1, and the command-line client shardpostctl; see Admin API and LDA and the manual pages.

Feature Notes
Access 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).
Roles diagnose, deliver, deliver:noenforce, import, trace, provision, repair, delete, content, support, export; optional account patterns per client.
Concurrency 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.
shardpost push The JMAP Web Push worker (see JMAP).

Observability

Feature Standard Notes
Metrics Prometheus exposition GET /metrics on server.http_addr.
Health — /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.

See Observability.

Mail store features

Storage

Feature Notes
Backend 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.
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.

See Quota.

Namespaces and sharing

Feature Notes
Other users' folders 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.
limits.mailbox_names (50000) CREATE under a name never used: NO [LIMIT] overQuota 409 limit_exceeded as above
limits.keywords_per_mailbox (1000) STORE, APPEND, COPY, MOVE: NO [LIMIT] Email/set (create, update), Email/import, Email/copy: tooManyKeywords (RFC 8621 §4.6, §4.8) 409 limit_exceeded The keywords are dropped, the message is stored.
limits.metadata_entries (1000) SETMETADATA: NO [METADATA TOOMANY] — (server-managed entries are never refused) 409 limit_exceeded (metadata import) —
limits.shared_pointers (10000) SETACL granting a new grantee: NO [LIMIT] Mailbox/set shareWith: overQuota 409 limit_exceeded —

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.

Message parsing

Feature Standard
Message format RFC 5322, MIME RFC 2045–2049
Encoded words RFC 2047, WHATWG charset list
Parameter value encoding (file names) RFC 2231
Preview text RFC 8970
Base subject for SORT/THREAD RFC 5256
List header forms in JMAP RFC 2369
mailto: URIs (Sieve notify) RFC 6068
Download file names (Content-Disposition) RFC 6266, RFC 5987
JSON pointers in JMAP result references RFC 6901
DEFLATE (IMAP COMPRESS) RFC 1951

Not supported at a glance

Area Not supported
IMAP CONTEXT=SEARCH/SORT, PARTIAL, SEARCH=FUZZY, MULTISEARCH, URLAUTH, LANGUAGE, I18NLEVEL, LIST-METADATA, UTF8=ONLY, UTF-8 downgrade (RFC 6857, RFC 6858), LITERAL-, INPROGRESS, UIDONLY, JMAPACCESS, ANNOTATE, CONVERT, X-GM-EXT, XLIST, ACL identifiers authenticated/anonymous and negative rights, METADATA TOOMANY and unsolicited METADATA responses, NOTIFY MailboxName/SubscriptionChange/metadata events, LIST attributes \Marked/\Unmarked/\NoInferiors/\Remote
POP3 APOP, LAST, LANG, EXPIRE, LOGIN-DELAY, AUTH without argument
LMTP DSN, BINARYMIME, REQUIRETLS, DELIVERBY, XCLIENT, implicit TLS, quota check at RCPT
ManageSieve OWNER/LANGUAGE capabilities, script-count limit, implicit TLS
Sieve redirect-dsn, redirect-deliverby, convert, extlists, vnd.dovecot.environment, vnd.dovecot.report, mime replace/enclose, notification methods other than mailto
JMAP WebSocket, MDN, contacts, calendars, Blob/copy
SASL LOGIN, CRAM-MD5, SCRAM, EXTERNAL, proxy authorisation, OAuth mechanisms on LMTP
Other Internet SMTP and a submission listener, DSN on outbound mail, encryption at rest, full-text search over attachment content and CJK text

RFC index

The index lists every RFC this document names, with the protocol it concerns; ◐ marks a partial implementation and ✗ an RFC that is not implemented.

RFC Title (short) Where
RFC 1870 SMTP SIZE LMTP
RFC 1939 POP3 POP3 ◐
RFC 1951 DEFLATE IMAP COMPRESS
RFC 2033 LMTP LMTP
RFC 2034 SMTP enhanced status codes LMTP
RFC 2045–2049 MIME message parsing
RFC 2047 Encoded words message parsing, FTS
RFC 2231 MIME parameter value encoding FTS (file names)
RFC 2177 IMAP IDLE IMAP
RFC 2342 IMAP NAMESPACE IMAP
RFC 2369 List header fields JMAP
RFC 2449 POP3 extensions (CAPA) POP3
RFC 2595 TLS for IMAP/POP3 (STLS) POP3
RFC 2920 SMTP PIPELINING LMTP
RFC 2971 IMAP ID IMAP ◐
RFC 3030 SMTP CHUNKING LMTP
RFC 3206 POP3 SYS and AUTH response codes POP3
RFC 3207 SMTP STARTTLS LMTP, outbound
RFC 3348 IMAP CHILDREN IMAP
RFC 3461 SMTP DSN outbound ✗
RFC 3463 Enhanced status codes LMTP
RFC 3501 IMAP4rev1 IMAP
RFC 3502 IMAP MULTIAPPEND IMAP
RFC 3516 IMAP BINARY IMAP
RFC 3691 IMAP UNSELECT IMAP
RFC 3834 Automatic responses Sieve vacation, reject
RFC 3894 Sieve copy Sieve
RFC 4314 IMAP ACL IMAP
RFC 4315 IMAP UIDPLUS IMAP
RFC 4469 IMAP CATENATE IMAP
RFC 4616 SASL PLAIN IMAP, POP3, ManageSieve, LMTP
RFC 4731 IMAP ESEARCH IMAP
RFC 4790 Collation registry (comparators) Sieve
RFC 4865 SMTP Future Message Release (FUTURERELEASE) JMAP (delayed sending)
RFC 4954 SMTP AUTH LMTP, outbound
RFC 4959 IMAP SASL-IR IMAP
RFC 4978 IMAP COMPRESS IMAP
RFC 5032 IMAP WITHIN IMAP
RFC 5034 POP3 SASL POP3
RFC 5051 i;unicode-casemap Sieve
RFC 5092 IMAP URL IMAP (CATENATE)
RFC 5161 IMAP ENABLE IMAP
RFC 5173 Sieve body Sieve
RFC 5182 IMAP SEARCHRES IMAP
RFC 5183 Sieve environment Sieve ◐
RFC 5228 Sieve Sieve
RFC 5229 Sieve variables Sieve
RFC 5230 Sieve vacation Sieve
RFC 5231 Sieve relational Sieve
RFC 5232 Sieve imap4flags Sieve
RFC 5233 Sieve subaddress Sieve
RFC 5235 Sieve spamtest, virustest Sieve
RFC 5256 IMAP SORT and THREAD IMAP
RFC 5258 IMAP LIST-EXTENDED IMAP
RFC 5260 Sieve date and index Sieve
RFC 5267 IMAP CONTEXT=SEARCH/SORT, ESORT IMAP ◐ (ESORT implemented, CONTEXT not)
RFC 5293 Sieve editheader Sieve
RFC 5322 Internet message format message parsing
RFC 5429 Sieve reject, ereject Sieve
RFC 5435 Sieve enotify Sieve ◐
RFC 5436 Sieve mailto notifications Sieve
RFC 5463 Sieve ihave Sieve
RFC 5464 IMAP METADATA IMAP ◐
RFC 5465 IMAP NOTIFY IMAP ◐
RFC 5490 Sieve mailbox and metadata Sieve
RFC 5530 IMAP response codes IMAP
RFC 5703 Sieve MIME parts Sieve ◐
RFC 5804 ManageSieve ManageSieve ◐
RFC 5819 IMAP LIST-STATUS IMAP
RFC 5957 IMAP SORT=DISPLAY IMAP
RFC 5987 Header parameter encoding JMAP downloads
RFC 6009 Sieve redirect-dsn, redirect-deliverby Sieve ✗
RFC 6068 mailto URI Sieve enotify
RFC 6131 Sieve vacation-seconds Sieve
RFC 6134 Sieve extlists Sieve ✗
RFC 6152 SMTP 8BITMIME LMTP
RFC 6154 IMAP SPECIAL-USE, CREATE-SPECIAL-USE IMAP ◐
RFC 6203 IMAP SEARCH=FUZZY IMAP ✗
RFC 6266 Content-Disposition JMAP downloads
RFC 6531 SMTPUTF8 LMTP
RFC 6533 Internationalized delivery reports Sieve reject MDN
RFC 6558 Sieve convert Sieve ✗
RFC 6609 Sieve include Sieve
RFC 6749 OAuth 2.0 client credentials authentication backchannel
RFC 6750 Bearer tokens over HTTP JMAP
RFC 6785 IMAPSieve IMAP, Sieve
RFC 6851 IMAP MOVE IMAP
RFC 6855 IMAP UTF-8 IMAP ◐
RFC 6856 POP3 UTF-8 POP3 ◐
RFC 6857 IMAP UTF-8 downgrade IMAP ✗
RFC 6858 Simplified IMAP UTF-8 downgrade IMAP ✗
RFC 6901 JSON Pointer JMAP
RFC 7162 IMAP CONDSTORE and QRESYNC IMAP ◐
RFC 7352 Sieve duplicate Sieve
RFC 7377 IMAP MULTISEARCH IMAP ✗
RFC 7617 HTTP Basic JMAP
RFC 7628 SASL OAUTHBEARER IMAP, POP3, ManageSieve, JMAP
RFC 7662 OAuth token introspection authentication
RFC 7807 HTTP problem details JMAP
RFC 7888 IMAP LITERAL+ IMAP
RFC 7889 IMAP APPENDLIMIT IMAP
RFC 8030 Web Push JMAP push
RFC 8098 Message disposition notification Sieve reject
RFC 8188 aes128gcm content encoding JMAP push
RFC 8291 Web Push message encryption JMAP push
RFC 8292 VAPID JMAP push
RFC 8314 Implicit TLS for mail access IMAPS, POP3S
RFC 8437 IMAP UNAUTHENTICATE IMAP
RFC 8438 IMAP STATUS=SIZE IMAP
RFC 8440 IMAP LIST-MYRIGHTS IMAP
RFC 8457 IMAP \Important special-use IMAP
RFC 8474 IMAP OBJECTID IMAP
RFC 8508 IMAP REPLACE IMAP ◐
RFC 8514 IMAP SAVEDATE IMAP
RFC 8579 Sieve special-use Sieve
RFC 8580 Sieve fcc Sieve, IMAPSieve
RFC 8620 JMAP core JMAP
RFC 8621 JMAP mail JMAP
RFC 8887 JMAP WebSocket JMAP ✗
RFC 8970 IMAP PREVIEW IMAP
RFC 9007 JMAP MDN JMAP ✗
RFC 9051 IMAP4rev2 IMAP
RFC 9208 IMAP QUOTA IMAP
RFC 9394 IMAP PARTIAL IMAP ✗
RFC 9404 JMAP blob JMAP
RFC 9422 SMTP LIMITS LMTP
RFC 9425 JMAP quota JMAP
RFC 9610 JMAP contacts JMAP ✗
RFC 9661 JMAP Sieve JMAP
RFC 9670 JMAP sharing JMAP
RFC 9749 JMAP Web Push VAPID JMAP push

Rendered from docs/protocols.md in the ShardPost repository.