Skip to main content
A mailing-list platform (MailWizz, Listmonk, Mautic…) sending through your OxiMail needs the envelope sender to differ from the visible From:
  • From: stays the human-readable address recipients see and reply to.
  • Return-Path: (the envelope MAIL FROM) is a dedicated technical mailbox that collects delivery status notifications, so bounce processing never wades through a person’s ordinary mail.
This is the standard bulk arrangement — every commercial sender works this way (Amazon SES calls it a custom MAIL FROM domain, Postmark a custom Return-Path). Out of the box OxiMail refuses it, correctly: an authenticated account may only use its own identities as MAIL FROM, and a bounce mailbox is deliberately not one of the sender’s identities. Envelope-sender domain grants open exactly this arrangement, and nothing more. What a platform sending at scale needs beyond the return path (an identity of its own, its own address, a queue that keeps people’s mail first, the providers’ requirements) is further down.
A grant widens the envelope only. The From: header keeps its own gate — it must always be an identity of the authenticated account. And a grant is not an alias: it implies no delivery and creates no identity. It is pure authorisation.

How it works

A grant authorises any local-part of one domain as MAIL FROM. Domain granularity is deliberate: bounce processing normally stamps a different envelope per recipient (VERP — bounce+alice=example.com@bounces.example.org), so per-address rules could never keep up. One granted domain covers every VERP variant. The proof belongs to the tenant’s own domain, like every sender-domain proof: once it holds, any ordinary account of the tenant may use the domain as MAIL FROM, and the account you name at add is the one whose identities the same-organization rule below is checked against. Two safety rails are enforced when the grant is created, not discovered later:
  1. The domain’s mail must come back to this server. The grant authorises nothing until the domain’s MX records all name the server’s host name ([server] hostname), or the domain has no MX and is that host name (the implicit MX of RFC 5321 §5.1, which needs no DNS change). A return path is where bounces and complaints arrive, so the proof that matters is that they arrive here; it replaces the _oximail-envelope TXT record that grants used to ask for. An MX anywhere else, or a null MX, suspends the grant fail-closed; a transient DNS error never does. The proof is re-checked two minutes after the server starts, then every six hours, and one no check has confirmed for 72 hours (name servers that stopped answering) stops authorizing as well. Either way sends under it are deferred 451 4.7.1, not bounced: the platform’s queue retries while the proof comes back. The oximail_envelope_domain_suspended_total metric counts the suspensions, and oximail doctor checks every proof under sender_domain_proofs.
  2. The same-organization rule. The granted domain must share its organizational domain with one of the account’s identities. bounces.example.org can be granted to sender@example.org; bounces.other.net cannot. This is the Amazon SES rule, and it is what keeps SPF aligned under DMARC by construction — a cross-organization Return-Path silently breaks DMARC at some receivers weeks later, so OxiMail makes it unrepresentable instead of documenting it as a caveat.
Grants are operator-provisioned (CLI). An account cannot grant itself the exemption that gates it.
A grant made when the proof was a TXT record is judged by the MX from its next check on, and suspended if no MX names the server. Delegate the domain (below) before you rely on an older grant.

Setting it up

Say the platform authenticates as sender@example.org and you want bounces collected in a dedicated mailbox.
Point the platform’s bounce processing at bounce@’s mailbox (IMAP), and set its Return-Path to an address under the granted domain. Done: the platform sends as sender@, receivers see sender@, and every DSN lands in bounce@. A report OxiMail writes itself carries the original Message-ID in its third part (the original headers by default, the Message-ID alone for a message that expired in the queue), which is what a platform like MailWizz attributes a bounce by; a submission that had no Message-ID got one from the server at submission.

VERP without any extra server work

If the platform stamps per-recipient envelopes (VERP), have it use a plus-tag of the bounce mailbox: bounce+<subscriber>@bounces.example.org. The grant authorises every variant (any local-part of the domain passes), and OxiMail’s plus-addressing delivers all of them into bounce@’s mailbox natively. No catch-all needed, no server-side configuration. The division of labour is deliberate, and it is how the whole industry works: the mail server authorises and keeps DMARC aligned; the list platform stamps envelopes and parses bounces. OxiMail does not generate VERP addresses and does not manage sending cadence — your platform already does both, and the receiver-side bulk rules (one-click unsubscribe, complaint-rate ceilings) are its obligations too: a bulk flow checks that the one-click headers are there, it never writes them.
The outbound budget applies to the platform’s account, and it is set per account. [rate_limit] smtp_outbound_rcpt_per_hour (150 recipients per hour by default) is the instance value every account inherits; beyond it a submission is deferred 452 4.7.1 and the platform retries, so a 5 000-recipient campaign would drain at 150 an hour. Give the sending account its own budget instead of raising the whole box (mail & SMTP):
The same budget is readable and settable from an administration console as outboundRcptPerHour on Principal/getAdmin / Principal/setAdmin (null = the instance value, 0 = no limit). Every deferral is counted per account in oximail_smtp_outbound_budget_deferred_total{account} and named in the journal, so a throttled platform is visible before its customer says so; a message whose recipient list alone exceeds the budget is refused with an error that says it can never fit as is.

When a send is refused

A MAIL FROM that matches no identity and no grant ever verified is rejected at end-of-DATA:
Check the journal on the server: if a grant for that domain exists but was never verified, the log says so explicitly (“one step from done”) instead of looking like a forgery attempt — run envelope-domain verify and re-check the domain’s MX. A grant that was verified and has since been suspended, or not re-verified for 72 hours, answers 451 4.7.1 instead (“Envelope-sender domain proof is not current”): a reversible state, so a transient reply the platform retries within its own give-up window. envelope-domain list shows it as suspended or stale; restore the MX or the domain’s DNS, then run envelope-domain verify.

Removing a grant

Removal is immediate: sends under the domain start being rejected with the 550 above. --yes is required, as for every destructive verb.

Sending at scale

A platform that sends thousands of messages is not a person, and the same box carries both. Four mechanisms keep them apart. The route, the class, the signature and the headers described here are served today; the admission (the decision to accept or refuse a submission by flow) is not yet: see the callout below.

Sending flows

A flow is the unit that sends. It belongs to one account, and the credential that opens the SMTP session chooses it: an app password bound to the flow, or the account’s own password for the one flow created with --account-login. A flow has a role, exclusive on one account:
  • origination: an application that emits. Its mail is the server’s to complete and sign (with a key of the operator or a delegated key, and a cosignature if its pool has one). Once the admission is enforced, its From: is judged against the tenant’s proven domains rather than the account’s identities.
  • transit: a leaf that signs itself, forwarded byte for byte as a relay account’s mail is. Today that behaviour is decided by the account’s service role.
Every mutating verb is a dry run until --apply (--yes for suspend and delete); list and show --flow <id or name> take --json. The name is unique on the account (A-Za-z0-9._-, 1 to 64 characters). --bulk declares bulk sending. --retention kept|ephemeral (default kept) is stored on the flow; what makes a relay account’s mail ephemeral is still its service role (retention), not this field. suspend --flow … --reason … --yes pauses a flow, resume --apply lifts the pause, delete --yes is refused while an app password is bound to the flow. An account that carries flows, reached by a credential that names none, is never taken for a person. What a flow changes today:
  • its class in the queue (below): bulk if the flow is declared bulk, transactional if it is not, and a submission without a flow stays a person’s;
  • its address: the egress pool named by set-pool, for the mail enqueued from then on (a DSN or a relay report inherits the route of the row it answers; a JMAP submission carries no flow and leaves from the default address);
  • its headers: an origination flow’s message leaves with X-OxiMail-Flow: <flow id> (how a complaint finds its flow) and, when the client set none, a Feedback-ID: <flow>:<tenant>:<bulk|tx>:<relay host name> (opaque identifiers, no personal data). A X-OxiMail-Flow written by a client is removed on every ordinary submission: a copied value would charge another flow’s complaints;
  • its counters: remote recipients delivered, and complaints, per flow (below).
The admission runs in shadow; it is not enforced yet. Every submission, SMTP or JMAP, is also judged by one admission object (disabled account, header counts, From:, envelope sender and its proofs, a paused flow, the bulk guard), and the server’s own historical decision still decides. oximail doctor counts the days the two agree under admission_shadow (seven days without a divergence is the criterion to switch; a divergence is a warning, and only the reason code of the day’s last one is kept, never an address). Until the switch, account flow suspend, a pause by complaint rate and the bulk guard are recorded and observed, not applied: the submission still gets its 250. The switch itself is planned, not served.

Egress pools

By default every message leaves from the box’s default address and announces the box’s HELO name. A campaign should not share the address people send from: reputation is kept per address. An egress pool is an address (or one per family) and its HELO, declared in the configuration because it is a fact of the host:
An unknown key refuses the file. Every local address is bound once at boot; one that is on no interface of the machine refuses to start. A pool is added with a restart, and set-pool warns when it names one the loaded configuration does not hold. Never a fallback. A row whose pool is absent from the configuration, or has no address in the family the box is pinned to, waits: no attempt is counted and the expiry does not move. A bulk message never leaves from the address people send from. The pool enters every key the worker shares (the reused session, the per-host slots, the host circuit breaker), so a campaign never rides a session opened from the default address, and the breaker does not flap between two addresses. A pool with both an IPv4 and an IPv6 address reaches a destination that only has IPv6 from its IPv6 address, and everything else from its IPv4: see which IP family mail leaves over. The pools’ public addresses count as this box for loop detection, and their public IPv4 addresses join the reputation monitoring. A dedicated pool for bulk mail is recommended, not required: a bulk flow without one shares the default address with people, and the classes below then protect latency, not reputation. oximail doctor checks each pool as egress_pools: each local address is on an interface, and with --reachability each public address has forward-confirmed reverse DNS on the HELO and the HELO’s SPF passes for it.

Queue classes

Outgoing mail waits in three classes, served in this order: human, transactional, bulk. The class is decided once, when the message is enqueued, and stored on the queue row. A delivery report (DSN) or a relay report inherits the class of the row that produced it. An account without a flow that sends to more than 200 recipients in a sliding hour is demoted to transactional for what follows, never refused. The dispatcher runs 16 delivery slots ([smtp.dispatcher] global_slots, bounded by the database pool size minus 4; it used to run 4 because it read a stale default). Bulk never holds more than 12 of them, and the last two are for people alone (transactional mail does not take them either); bulk leaves a quarter of the in-flight byte budget free and transactional an eighth, but only while something else holds bytes; and of the two connection slots toward each remote host, one is kept for people. On a measured bench, one person’s message toward a host that a 5 000-message campaign was filling left in 107 ms, with 4 989 bulk messages still waiting. oximail doctor reports queue_classes: pending, sending and the age of the oldest never-attempted row per class, and it fails when a person’s message has waited over 60 seconds without an attempt while bulk is being sent, which means the budgets are not holding. The same figures are oximail_smtp_queue_class_depth{class}, oximail_smtp_queue_class_in_flight{class}, oximail_smtp_queue_class_oldest_unattempted_seconds{class} and the starvation gauge oximail_smtp_queue_human_starved. queue_depth thresholds people’s and transactional mail only: a campaign is one row per recipient, so bulk depth is shown, never judged.

What the large providers require

The large mailbox providers ask bulk senders for a signed one-click unsubscribe, a domain with DMARC, and a low complaint rate. The relay measures each one and, once the admission is enforced, applies it. Complaints. A provider’s complaint (an ARF report, RFC 5965) sent to the relay’s feedback-loop address is counted against the flow that sent the reported message. The address is yours to register with the providers’ feedback loops, then to name:
The address must exist on the server like any report address, and the report is absorbed unless forward is on. A complaint is believed only when the report passed DMARC for a listed provider, the reported original carries exactly one X-OxiMail-Flow that names an existing origination flow, and its From: domain is proven by that flow’s tenant. Anything else is logged with its reason and never counted, because a complaint can pause a flow and a forged one is an attack on a client. What is kept is the flow, the provider, the Feedback-Type and the date: never the reported message or its recipient. Gmail sends no complaint reports: Postmaster Tools reads the Feedback-ID instead, which the signature covers. Complaint rate. Remote recipients delivered are counted per flow when the delivery ends. Every hour the relay judges the last seven days, from 1 000 delivered recipients and 3 complaints: 0.1 % raises an alert and 0.3 % pauses the flow (the pause reason names the rate). oximail doctor shows each flow’s rate under complaint_rates, warns from the alert threshold and names a paused flow with the verb that lifts it, oximail account flow resume --email … --flow … --apply, which logs what it lifted. Counters are kept 30 days. Bulk guard. For a flow declared bulk, a message must carry a List-Unsubscribe with a List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058), and its From: domain must publish DMARC (its own _dmarc record, else its organizational domain’s). Missing headers or a domain without DMARC are refusals (5xx). The DMARC answer is read from a cache and never queried while the message is received: a domain not known yet is deferred 451, and the relay resolves it within a minute, refreshes it daily, and fetches ahead every domain a bulk flow may use (a dkim or return-path proof that is verified and not suspended), so the first message of a newly proven domain does not wait. Signature. It covers the unsubscribe pair, Feedback-ID and X-OxiMail-Flow, as RFC 8058 §4 asks (who signs what). The complaint rate is counted and the guard evaluated today. Both act on a submission only when the admission is enforced (see the warning above).

When a recipient provider pushes back

The relay reads what each recipient site answers, flow by flow, and can hold one flow toward one site while its other sites keep sending. This is decided at delivery and comes in two halves, each in shadow by default (decided and recorded, nothing held) or enforced:
Pauses. A flow is paused toward a site (the organizational domain of the next hop, such as google.com, outlook.com or yahoodns.net) for one hour, doubled when it happens again within a day, six hours at most. Three things start a pause:
  • one of the rate-limiting replies that Gmail, Microsoft and Yahoo publish: the first one is enough;
  • 20 permanent refusals that are not about an address, within ten minutes;
  • 3 reputation refusals within an hour (Gmail’s “likely unsolicited” and “very low reputation”). This is the one a slow flow can reach: a single such refusal pauses nothing, since one message can earn it by its content alone.
A held message spends no attempt. oximail account flow show lists a flow’s pauses, and oximail account flow resume --site <site> lifts one. Daily allowance. A pool has a daily cap, of which 20 % is kept for transactional mail. Each signing domain, and each sending address, has a ramp toward each site: 500 recipients a day at first, doubled every two days while refusals stay under 2 %. What is over waits for the next day, and a bulk message held past its life is abandoned, the abandon naming the allowance that held it. On a server that has no history for a domain, read and set its ramps before the first large campaign: oximail account relay ramp list and oximail account relay ramp set. Enforce pauses first: it only holds what a site already refuses. Enforce daily once a campaign has gone through in shadow and oximail doctor has shown, under relay_allowance, which ramps it would have spent.

Checking it

A 250 your platform receives after DATA means the message survives a power loss (mail & SMTP).

Relation to relay accounts

If you run a smarthost, note the boundary: a role="service" relay account has its own envelope authorisation (the proven declared domains of ADR-116) and cannot receive envelope-sender grants — one mechanism per account kind. Grants are for normal accounts on a normal Primary, sending their own organisation’s list mail. Both kinds of proof are rows of the tenant’s own domain: a tenant cannot hold a proof on another tenant’s domain.