Repository navigation
v2.4.1
yarilo 2.4.1
The first stable release. Until now the project has been a beta: the configuration keys, the on-disk format and the meaning of the version number could all move without notice. From this release they cannot.
yarilo is a mail server written in Go — IMAP, POP3, LMTP delivery, Submission, Sieve and ManageSieve, with JMAP alongside them. It runs as a set of single-purpose processes rather than one binary with modes, and every deployment shape is a configuration, not a build.
Upgrading from 2.3.x is not a drop-in. Read Upgrading from 2.3.x before you run helm upgrade: several defaults changed, and some configurations that loaded before now refuse to start.
What is stable, and what is merely present
Stable means the promises below hold. It does not mean everything is finished:
| IMAP4rev2 / IMAP4rev1 | complete, with the extension set listed in the README |
| POP3, LMTP, Submission | complete |
| Sieve, ManageSieve | complete, including the vendor extensions |
| JMAP | partial |
| Other Users namespace | not implemented |
JMAP reads, synchronises and writes keywords: the session resource, Mailbox/{get,query,changes}, Email/{get,query,set,changes}, Thread/{get,changes}, SearchSnippet/get, Quota/{get,changes,query,queryChanges} (read-only), blob download and back-references, with real state strings. Email/set updates keywords only; create and destroy are refused with an answer naming what to use instead. Mailbox/set, blob upload and push are not implemented. Email/queryChanges and Mailbox/queryChanges answer cannotCalculateChanges by design, and canCalculateChanges is advertised as false.
Shared mailboxes work through owner-templated shared namespaces (user/%u/) and public namespaces. A namespace of type: other (other_users is accepted as an alias) is answered with NO and a reason; it needs cross-pod routing that is not built yet.
That is a statement about absence, not about breakage. What is served is under the same promises as everything else.
The three contracts
Configuration keys. A key does not change meaning. A key that is removed is answered rather than ignored: either it logs a warning at start saying what took its place, or, where silently ignoring it would change what the deployment does, the configuration refuses to start and the message says what to use instead. A rename keeps the old spelling working as an alias; both spellings set to different values refuse startup, both set to the same value are accepted. One pair is stricter: auth_allow_cleartext and disable_plaintext_auth are one setting with opposite senses, so carrying both refuses startup even when they agree.
On-disk format. Within a major version the format does not change, and the previous minor can read what a newer one wrote. That is what makes a rollback available during an incident; see Rolling back.
The version number.
- a patch changes behaviour only where the old behaviour was a defect;
- a minor adds a feature or changes a default, and a changed default is named in these notes under Upgrading. Keys keep their meaning, and two neighbouring minors read each other's data;
- a major is anything a rollback could not read, or a key that changed meaning.
Upgrading from 2.3.x
Breaking changes
- Internal TLS is on by default. One switch,
internalTLS.enabled: true; per-component flags are refused. The chart makes a CA and one certificate per role unless you give itinternalTLS.ca.secretNameor cert-manager. Each internal listener accepts only the roles meant to talk to it.- Upgrading a release that ran with internal TLS off is a short outage: while the rollout runs, new pods speak TLS and old ones do not, and calls between them fail until the last pod has restarted. Either plan it as a window, or set
internalTLS.enabled: falsefor this upgrade and switch it on later in a window of your choosing. - GitOps renderers (Argo CD,
helm template) cannot see existing Secrets: setinternalTLS.ca.secretNameto a CA made once outside the chart, or use cert-manager, or every render makes a new CA.
- Upgrading a release that ran with internal TLS off is a short outage: while the rollout runs, new pods speak TLS and old ones do not, and calls between them fail until the last pod has restarted. Either plan it as a window, or set
- Certificates without a role are accepted in this release only. A peer certificate whose SAN names no role is accepted and logged once per listener and certificate, so a rolling upgrade from one shared certificate keeps working. The next release refuses it.
- NetworkPolicy is on by default. The chart renders one ingress policy per internal listener, admitting only the roles that talk to it. It needs a CNI that enforces policies (Calico, Cilium, …); on one that does not, the policies are silently inert. Operator tools outside the chart's pods (a smoketest,
yarilo-migrate) neednetworkPolicy.adminFrom; the MTA-facing and telemetry ports takenetworkPolicy.mtaFromandnetworkPolicy.telemetryFrom(empty: open to everyone). - The AUTH protocol changed on the wire, so all components must run the same version. The initial response is now base64, as the protocol always documented. During a rolling upgrade, a login proxy and yarilo-auth on different versions refuse each other's logins until both are on 2.4.1. Plan the upgrade as a window, or keep it short.
- An empty admin API token stops the service at start.
backend_apiand the director's admin API refuse to start without a token; running without one takes an explicitauth_disabled: true(local development only), and setting both is refused. The chart always sets a token, so a Helm deployment is unaffected; a hand-writtenyarilo.yamlmay not be. - A shared or public namespace with ACL off refuses to start. With
acl.enabled: falseevery authenticated user could reach every mailbox of such a namespace. The message names the namespace and the key. - sasl-login listens on 12345, not 12325. Point your MTA's
smtpd_sasl_pathat the new port in the same upgrade.auth_service.sasl_listenis gone; it refuses to load, and the message points atsasl_login.listen. lmtp_user_concurrency_limit: 0refuses to start. The default is 10 concurrent deliveries per user, counted across the cluster through yarilo-warden and enforced by yarilo-lmtp-login atRCPT TO(451 4.3.0past it). Unlimited is-1. With warden unreachable a delivery is accepted without the limit and a warning is logged.auth_allow_cleartext: falsenow refuses, where it used to only hide mechanisms. On a connection that is neither TLS, nor behind a trusted HAProxy, nor local, PLAIN and LOGIN are refused until STARTTLS. The default (true) is unchanged.- Removed keys that now refuse to load:
director_service.lmtp_listen,director_service.lmtp_backend_port,auth_service.sasl_listen,services.<listener>.connection_limit,telemetry.telemetry_pprof_heap_enabled. Each message names what replaced it.
Redis with a password, in this order
The chart now passes a Redis password to every process that opens Redis, from redis.passwordSecret (name, key) into YARILO_REDIS_PASSWORD; a Redis dict of your own takes password: "${YARILO_REDIS_PASSWORD}" in its settings. Do not put the password in a Redis URL: the URL is rendered into the ConfigMap as written.
To turn a password on without a window:
- create the Secret and set
redis.passwordSecret, then upgrade. The clients now authenticate; a Redis that has no password yet accepts that (AUTH default <password>against anopassdefault user); - then set the password on Redis itself.
The other order locks every component out of Redis until the upgrade lands.
Behaviour that changed without a key
- A message delivered without a
Message-IDgets one (protocol.lmtp.lmtp_add_message_id, defaulttrue). - A deployment that set only
protocol.submission.hostnamewas giving that name to the LMTP banner and toReceivedas well; those now take the installation'shostname, which defaults to each process's own host name until you set it. Sethostnameon a cluster.
New configuration in this release
Every key helm/values.yaml gained since 2.3.266, by section, with its default. The documentation site has the full reference for each.
Top level and chart
| Key | Default | What it does |
|---|---|---|
hostname |
the host's own name | What this installation calls itself: the domain of a synthesised Message-ID, the LMTP banner, Received. Set it on a cluster. |
config_schema_version, chart_version |
written by the chart | Chart-managed; do not set. A process warns when its ConfigMap was rendered by an older chart, and names the settings that are being defaulted. |
internalTLS.enabled |
true |
Internal mTLS with per-role certificates (above). |
internalTLS.ca.secretName |
— | A CA Secret made outside the chart; without it and without cert-manager the chart makes one. |
internalTLS.certManager.enabled, .issuerRef.kind, .issuerRef.name |
false, Issuer, — |
One cert-manager Certificate per role instead of chart-made Secrets. |
internalTLS.certGeneration |
0 |
Raise to re-issue every chart-made certificate. |
networkPolicy.enabled |
true |
Ingress policies per internal listener (above). |
networkPolicy.adminFrom, .mtaFrom, .telemetryFrom |
[] |
Peers outside the chart for the admin, MTA-facing and telemetry ports; empty mtaFrom/telemetryFrom admit everyone. |
redis.passwordSecret.name, .key |
—, password |
The Redis password, from a Secret (above). |
virtual_definitions |
{} |
One file per mailbox of a shared virtual namespace, mounted read-only; a namespace must point at them. |
namespaces[].mailboxes |
— | Mailboxes a namespace creates or subscribes on its own (auto: create / subscribe). |
Storage
| Key | Default | What it does |
|---|---|---|
storage.mail_fsync |
optimized |
What reaches the disk before a delivery is answered: never, optimized (the body) or always (plus index, list and directory). |
storage.storage_lock_method |
flock |
How a write to a shared file excludes another writer: flock, fcntl (NFS with NLM) or dotlock. |
storage.storage_lock_stale_timeout |
180 |
Seconds an unchanged dotlock is left before a waiter takes it over; -1 never. |
storage.mail_cache_purge_min_size |
32k |
A cache file smaller than this is never purged on its own; 0 purges any size. |
storage.mail_cache_purge_delete_percentage |
20 |
Expunged records against live ones, in percent, that purges the cache. |
storage.mail_cache_purge_continued_percentage |
200 |
Continued records against live ones, in percent, that purges the cache. |
dicts.metadata.settings.lock_method |
— (flock) |
Write lock for the annotations file; match storage_lock_method. |
Protocols
| Key | Default | What it does |
|---|---|---|
protocol.lmtp.add_message_id |
true |
Synthesise a Message-ID for a message that arrives without one; an existing header is never rewritten. |
protocol.lmtp.lda_mailbox_autocreate |
false |
A delivery to a folder that does not exist makes it. |
protocol.lmtp.lda_mailbox_autosubscribe |
false |
Subscribes a folder lda_mailbox_autocreate made; refused without it. |
protocol.lmtp.quota_full_tempfail |
false |
A full mailbox answers 452 4.2.2 (retry) instead of 552 5.2.2 (bounce). |
protocol.lmtp.lmtp_max_recipients |
0 (unlimited) |
Recipients per LMTP transaction, advertised as LIMITS RCPTMAX. |
protocol.lmtp.proxy.lmtp_proxy_timeout |
125 |
lmtp-login's cap in seconds on reaching a backend and finishing one delivery; past it the MTA gets 451. |
protocol.managesieve.managesieve_max_line_length |
65536 |
Bytes in one ManageSieve command line or literal, a script's body aside (that is sieve_max_script_size); a larger literal is refused before it is read. |
login.login_proxy_timeout |
30 |
Seconds a login proxy may spend reaching a backend and bringing the session up; a userdb proxy_timeout overrides it per user. |
Components
| Key | Default | What it does |
|---|---|---|
components.auth.auth_max_attempts |
3 |
Failed authentications allowed on one connection before it is closed. |
components.auth.wait_pool_size |
64 |
Connections kept for waiting lock acquires. |
components.director.director_domain_expire |
900 |
Seconds a domain keeps its backend with no session on it (domain assignment policy). |
components.director.director_domain_rebalance_percent, director_domain_rebalance_interval, director_domain_rebalance_cooldown |
20, 60, 600 |
When a domain moves from the busiest backend to the quietest, how often that is judged, and how long a moved domain is left alone; 0 percent never moves one. |
components.quotaStatus.quota_status_success, quota_status_toolarge, quota_status_overquota |
OK, — (the over-quota action), 554 5.2.2 %{error} |
quota-status answers for a recipient that fits, a message that is too large, and a full mailbox. |
components.warden.service.event_queue_size |
4096 |
Warden's queue of session events; when full, the oldest are dropped and counted. |
components.dict.enabled, replicas, listen, max_conns |
true, 1, :9107, 8 |
The dict service and the connections a session keeps to one named dict. |
components.dict.internalTLS.secretName, resources, affinity, nodeSelector, tolerations |
chart-managed / empty | Pod settings of the dict service. |
Rolling back
Two routes, and they are not the same:
helm rollbackmoves the chart and the image together;--set image.tag=<older>moves the image alone, leaving the newer chart in place, so an older binary receives keys it does not know.
A rollback to 2.3.x is verified after this release, not before it, and this section will carry the result: what the candidate wrote, what 2.3.266 read back, and the digests before and after. Until then, treat a rollback across this upgrade as untested. Two things are already known to stand in the way of 2.3.266:
- with
redis.passwordSecretset and a password on Redis, 2.3.266 cannot authenticate to Redis; - IMAP METADATA annotations now live in a file in each user's home (
%h/yarilo-metadata.json, themetadatadict); 2.3.266 reads its old shared location and will not see them. Nothing is deleted.
Changes since 2.3.266
Every commit: v2.3.266...v2.4.1.