Sending mail
Submit mail on port 587 or with the JSON API, how headers are added, how the outbound queue retries, and how mail is delivered.
Mail clients and your own apps send mail in one of two ways:
- SMTP on port 587, with authentication.
- HTTP, with the JSON send API.
Both end in the same pipeline, so Bcc handling, headers, DKIM signing and the queue behave the same.
Connect a client or app
| Setting | Value |
|---|---|
| Server | Your OUTGOING_MAIL_HOST, for example mail.example.com |
| Port | 587 |
| Authentication | Required. LOGIN is supported. XOAUTH2 is rejected. |
The pipeline
When a client submits mail on port 587:
- AUTH. LOGIN authentication is required.
- MAIL FROM. Validates the sender and checks for relay.
- RCPT TO. Checks each recipient against
outgoing.maxRecipients. - DATA. Processes the message:
- Recipients come only from the SMTP envelope (the
RCPT TOcommands), so Bcc works and is never shown to other recipients. - Any
Bcc:header is removed. The rest of the message is left unchanged. - The server adds headers, signs the message with DKIM, writes it to the outbound queue, and replies
250 Message queued as <id>.
- Recipients come only from the SMTP envelope (the
The JSON send API builds the MIME message first, then runs the same steps.
Outgoing headers
The server adds these to every message before signing it:
| Header | When | Setting |
|---|---|---|
X-AE-Queue-Id: <id> | Always | none |
X-AE-Bounce-Address: bounce+<id>@BOUNCE_DOMAIN | When BOUNCE_DOMAIN is set | outgoing.bounceHeader.enabled, outgoing.bounceHeader.name |
| Your own headers | When outgoing.customHeaders.enabled is true | outgoing.customHeaders.headers |
Set your own headers in server/config/mail.config.json. Values can use {queueId} and {sender}:
A header set here replaces a header with the same name sent by the client. Header names are checked when the file loads, and a bad name keeps the previous settings. The bounce header name is set in the same way, in outgoing.bounceHeader.name.
The headers are added before DKIM signing, so they are covered by the signature.
The outbound queue
Queued mail is kept in MAIL_SPOOL_DIR, so it survives a restart. Each recipient is tracked on its own:
| Result | What happens |
|---|---|
| Delivered | Removed from the queue. |
Permanent failure (5xx) | Removed and logged. The sender gets a bounce report. |
Temporary failure (4xx, timeout, connection refused) | Retried on the queue.retryDelaysMinutes schedule, by default after 1, 5, 15, 30, 60 and 120 minutes, then every 240 minutes, until queue.maxAgeHours (default 48) has passed. |
| Address on the suppression list | Fails straight away without a delivery attempt. |
queue.maxConcurrent (default 10) sets how many deliveries run at the same time. All queue settings are in Configuration files.
Outbox and Sent
When the IMAP server is connected (IMAP_LMTP_HOST), mail a user sends on port 587 shows up in their mailbox:
- When the message is queued, a copy goes into the user's Outbox folder.
- When the queue is done and at least one recipient got it, the copy moves to Sent.
- If no recipient got it, the copy stays in Outbox and the user gets the usual bounce report.
Mail clients such as Thunderbird also save their own copy in Sent. The server sees that copy (by its Message-ID) and removes the Outbox copy instead of moving it, so Sent never has the message twice. The move waits at least 5 seconds after the message was queued, which gives the client time to save its copy first. A message that arrives without a Message-ID gets one, so the copies can be matched.
The copy goes to the account that logged in. While the login on 587 is still the placeholder (a user name that is not an email address), it goes to the envelope sender instead. Senders without an IMAP mailbox are skipped. Mail sent through the JSON API is not copied.
Turn it off with outgoing.imapOutbox.enabled: false in mail.config.json. With the relay or KumoMTA drivers, "sent" means the message was handed to that MTA.
How mail is delivered
OUTBOUND_DRIVER decides where the queue sends each message:
directdelivers to the recipient's MX server from this machine. This is the default.gosends through the Go MTA service.relaysubmits to another SMTP server, such as KumoMTA.kumo-httpposts to KumoMTA's HTTP API.
See Outbound drivers for the settings of each, and for what KumoMTA needs.
MTA-STS
Direct delivery follows the recipient domain's MTA-STS policy. In enforce mode the server requires TLS with a valid certificate and only uses an MX host that the policy allows. Relay connections verify the certificate by default, and OUTBOUND_RELAY_TLS changes that.
Programmatic use
The direct delivery engine is the NodeMailerMTA class. See the NodeMailerMTA API for sendMail, sendViaRelay and the result format.
Components involved
| Component | File | Purpose |
|---|---|---|
OutgoingMailHandler | server/src/services/OutgoingMailHandler.ts | Authentication, parsing and queueing for port 587. |
submitOutgoing | server/src/services/OutgoingPipeline.ts | Strips Bcc, adds headers, signs with DKIM and queues. Shared by port 587 and the JSON API. |
OutboundQueue | server/src/server/mta/OutboundQueue.ts | The disk queue, retries and per-recipient results. |
| Drivers | server/src/server/mta/drivers/index.ts | direct, go, relay and kumo-http. |
NodeMailerMTA | server/src/server/mta/node-mta.ts | Builds the message, signs with DKIM, resolves MX records and delivers. |
MailComposer | server/src/server/config/MailComposer.ts | RFC 5322 headers and MIME. |