-
Notifications
You must be signed in to change notification settings - Fork 3
How Email Works
This page began as a chapter of the 6.2.10 manual and has been corrected for 6.2.24, then rewritten against the 6.2.28 source on 8 September 2026. The Control Panel's pages are grouped differently now, so the paths below use today's groups; the TLS ports 465, 993 and 995 exist only after you create them on the TCP/IP ports page (a fresh install seeds 25, 587, 110 and 143); and everything added since 6.2.10 is in Changes-Since-6210. Where a value here disagrees with the Settings Reference, which is generated from the code, the reference is right.
Skip this chapter if you already know it. If you do not, the rest of this wiki will make far more sense with it.
You send mail to alice@example.com. Here is what happens:
sequenceDiagram
autonumber
participant MC as Your mail client
participant YS as Your hMailServer
participant DNS as DNS
participant RS as The recipient mail server
participant A as Alice
MC->>YS: SMTP submission on port 587 - you log in first
YS-->>MC: 250 Queued
YS->>DNS: Who receives mail for example.com?
DNS-->>YS: MX record - mail.example.com, priority 10
YS->>DNS: A and AAAA for mail.example.com, plus TLSA and MTA-STS policy
YS->>RS: SMTP delivery on port 25 - no login, TLS if offered
RS-->>YS: 250 OK
Note over RS: Stored in the recipient mailbox
A->>RS: IMAP on port 993
RS-->>A: The message
Two facts do most of the explaining:
- Port 25 has no password. Any server on the internet may connect to your port 25 and offer you mail for your own domains. That is how email works — it has to be open, or nobody could write to you. This is also why spam exists, and why DNS for Your Domain, Stopping Spam and Security Hardening matter.
- Your users use a different door. Port 587 (submission) requires a login. Never let an unauthenticated stranger send mail through you to the outside world — that is an open relay, and it will get your server blacklisted within hours. hMailServer refuses this by default.
This is a real session on port 587 against a STARTTLS required listener, with the checks
hMailServer runs at each step. Drawn from Server/SMTP/SMTPConnection.cpp.
sequenceDiagram
autonumber
participant C as Mail client
participant S as hMailServer
C->>S: TCP connect
Note right of S: A PTR lookup for the client's IP starts on a worker thread now,<br/>so the Received header does not wait for it later
S-->>C: 220 MAIL01 ESMTP
C->>S: EHLO laptop.example.net
S-->>C: 250-SIZE 20971520 … 250-STARTTLS … 250 HELP
Note right of S: No AUTH mechanism is offered yet if the port or the IP range requires TLS
C->>S: STARTTLS
S-->>C: 220 Ready to start TLS
C-->>S: TLS handshake
Note right of S: Session resets - HELO host, credentials and any transaction are discarded.<br/>The client must EHLO again, per RFC 3207 section 4.2
C->>S: EHLO laptop.example.net
S-->>C: 250-AUTH LOGIN SCRAM-SHA-256 SCRAM-SHA-256-PLUS …
C->>S: AUTH PLAIN then the base64 credential
Note right of S: Account looked up, lockout checked before the password,<br/>stored hash verified, then app passwords if the account is enrolled in TOTP
S-->>C: 235 2.7.0 authenticated.
C->>S: MAIL FROM: you@yourcompany.com SIZE=48210
Note right of S: Disk space, sender syntax, Send-As permission,<br/>per-IP submission rate, SIZE against the domain limit, sending quota
S-->>C: 250 2.1.0 OK
C->>S: RCPT TO: alice@example.com
Note right of S: Deliverability, relay permission for this IP range,<br/>authentication requirement, greylisting, recipient expansion
S-->>C: 250 2.1.5 OK
C->>S: DATA
Note right of S: OnSMTPData script event fires here — it can still refuse
S-->>C: 354 OK, send.
C->>S: headers, body, then CRLF dot CRLF
Note right of S: Accept and save run on a worker thread -<br/>post-transmission spam tests, spam headers, signature,<br/>List headers, Authentication-Results, archive, save
S-->>C: 250 2.0.0 Queued (0.412 seconds)
C->>S: QUIT
S-->>C: 221 goodbye
The (0.412 seconds) in the final reply is real: the server times the accept-and-save stage
and reports it. A number that climbs into whole seconds is the first sign of a slow spam test,
a stalled virus scanner or a saturated work queue — see Diagnosing Stalled Mail.
The order matters, because it decides when a sender is told no — before the message body is transferred, or after.
| # | Stage | What is checked | Reply when it fails |
|---|---|---|---|
| 1 | Any command | On a STARTTLS required port, everything but NOOP, EHLO, STARTTLS and QUIT
|
530 Must issue STARTTLS first. |
| 2 | MAIL FROM |
Free disk space — MinimumFreeDiskSpaceMB, default 100
|
452 4.3.1 Insufficient system storage — temporary, so the sender holds the message |
| 3 | MAIL FROM |
Syntax; a blank sender is allowed only if Allow empty sender address is on |
550 The address is not valid. / 550 Sender address must be specified.
|
| 4 | MAIL FROM |
Send-As: the authenticated account owns or was granted this address — only when SmtpAuthenticatedSenderCheck=1 (default 0) |
550 5.7.1 Sender address rejected |
| 5 | MAIL FROM |
Per-IP rate: MaxSubmissionsPerIPPerMinute (default 0, unlimited) |
421 Too many messages from your IP address |
| 6 | MAIL FROM |
ESMTP parameters: SIZE, AUTH, BODY, SMTPUTF8, RET, ENVID. Anything else is refused rather than ignored |
501 for a malformed value, 550 Unsupported ESMTP extension for an unknown one |
| 7 | MAIL FROM |
Pre-transmission spam tests, when DNSBLChecksAfterMailFrom=1 (the default) |
550 5.7.1 with the failing test's message |
| 8 | MAIL FROM |
Declared SIZE against the domain's maximum message size (default 20480 KB) |
552 5.3.4 Message size exceeds fixed maximum message size |
| 9 | RCPT TO |
Is the address deliverable at all? A database that cannot answer is told apart from an address that does not exist |
550 Unknown user, or 451 4.3.2 Unable to verify the recipient at the moment
|
| 10 | RCPT TO |
Mailbox full |
452 4.2.2 — temporary, so the sending server holds the mail rather than bouncing it |
| 11 | RCPT TO |
Relay permission for this IP range (see the next section) | 550 Delivery is not allowed to this address. |
| 12 | RCPT TO |
Authentication required for this combination and not supplied | 530 SMTP authentication is required. |
| 13 | RCPT TO |
Greylisting, when it is on (default off) | 451 Please try again later. |
| 14 | DATA |
The OnSMTPData script event |
554 5.7.1 Rejected, or 453 4.7.0 for a temporary refusal |
| 15 | end of data | Actual size, bare LF check, post-transmission spam tests |
552 5.3.4, 554 5.6.0 Rejected - Message containing bare LF's, 554 5.7.1
|
| 16 | end of data | The OnAcceptMessage script event, then the database write |
554 5.7.1, or 451 4.3.0 … could not be saved. Please retry later.
|
Two conventions in that table. An ESMTP session — one where the client greeted with EHLO
rather than HELO — has an RFC 3463 enhanced status code inserted after the numeric one, so
530 Must issue STARTTLS first. is really 530 5.7.0 Must issue STARTTLS first., and a plain
550 becomes 550 5.7.1. And a 4xx reply always means "come back later" while a 5xx
means "never": the difference is what decides whether a legitimate sender's message waits in
their queue or is destroyed, which is why a full mailbox is 452 and not 550.
Everything numbered 2 to 13 happens before a single byte of the message crosses the wire. That is deliberate: refusing early costs the sender nothing and costs you nothing, and it generates no bounce to an address the spammer only claimed to be.
This is the single decision that separates a mail server from an open relay. hMailServer makes it per IP range (Access & abuse protection → IP ranges), from a two-by-two grid.
flowchart TD
A["RCPT TO accepted so far"] --> B{"Is the sender address<br/>one of your own domains,<br/>or a route address?"}
B -->|yes| C{"Is the recipient local?"}
B -->|no| D{"Is the recipient local?"}
C -->|yes| E["Local to local"]
C -->|no| F["Local to remote"]
D -->|yes| G["Remote to local"]
D -->|no| H["Remote to remote"]
E --> I{"Allowed for this IP range?"}
F --> I
G --> I
H --> I
I -->|no| J["550 Delivery is not allowed to this address."]
I -->|yes| K{"Does the range require<br/>SMTP authentication<br/>for this combination?"}
K -->|yes, and not authenticated| L["530 SMTP authentication is required."]
K -->|otherwise| M["250 2.1.5 OK"]
- Remote to local is inbound mail from the internet. It must be allowed, and must not require authentication, or nobody can write to you.
- Remote to remote is the open-relay box. On the default Internet range it is off. Leave it off.
- Local to remote is your own users sending outward. Allowed, but with authentication required — which is why port 587 needs a login.

Ranges are matched by priority, highest first, so a narrow range for your own network can sit in front of the broad Internet range without editing it.
There is one more consequence buried here: if the client authenticated, or if it sends as one of your own domains from a range that does not require authentication to do so, this server counts itself the submission server for the message (RFC 6409) and adds the headers a submission server adds. For everyone else it is a relay, and a relay adds trace fields only.
Acceptance and delivery are separate. The 250 means the message is on disk and in the
database; delivery happens on the delivery threads afterwards.
flowchart TD
Q["Message in the delivery queue"] --> P1["OnDeliveryStart script event"]
P1 -->|cancelled| X["Deleted, and logged as rejected"]
P1 --> P2["Virus scanning"]
P2 -->|infected| X2["Handled per your anti-virus action"]
P2 -->|unscannable, and AVFailAction is 1| HOLD["Held, retried later"]
P2 --> P3["Global rules"]
P3 --> P4["OnDeliverMessage script event"]
P4 --> P5["DKIM signing - first attempt only"]
P5 --> P6["Mirroring - first attempt only"]
P6 --> LD["Local delivery - account rules, Sieve, forwarding,<br/>distribution lists, public folders"]
LD --> RM{"Any recipients left<br/>outside this server?"}
RM -->|no| DONE["Delivered"]
RM -->|yes| ED["External delivery - routes or MX lookup,<br/>MTA-STS, DANE, TLS, then SMTP"]
ED --> OK{"Accepted by the<br/>receiving server?"}
OK -->|yes| DONE
OK -->|temporary failure| RETRY["Rescheduled - 4 tries,<br/>60 minutes apart by default"]
RETRY --> ED
OK -->|permanent failure, or retries exhausted| DSN["RFC 3464 delivery status<br/>notification to the sender"]
DKIM signing and mirroring run only on the first attempt (retry count = 0), so a message is
never signed twice or mirrored twice. The retry count and interval are on
Mail flow & delivery → Delivery of e-mail (Number of delivery retries, Minutes between
retries); a route may override them for its own domains.
Anti-spam runs in two phases, and which phase a test belongs to is fixed by the test, not by configuration. The pipeline stops early once the score has already passed the higher of your two thresholds, so a message condemned by a cheap local test never pays for an expensive remote one.
| Order | Test | Phase | Score when it fails | Default on a fresh install |
|---|---|---|---|---|
| 1 | Blocked senders — first, because it is the cheapest verdict there is | pre-transmission | the entry's own score | list is empty |
| 2 | DNS blacklists (DNSBL) | pre-transmission | per list, seeded 3 |
zen.spamhaus.org and bl.spamcop.net are seeded but inactive
|
| 3 | HELO host | pre-transmission | 2 | off (ascheckhostinhelo 0) |
| 4 | PTR / reverse DNS | pre-transmission | 1 | off (ascheckptr 0) |
| 5 | MX records for the sender domain | pre-transmission | 2 | off (usemxchecks 0) |
| 6 | SPF | pre-transmission | 3 | off (usespf 0) |
| 7 | SURBL | post-transmission | per list, seeded 3 |
multi.surbl.org is seeded but inactive
|
| 8 | DKIM verification | post-transmission | 5 | off (ASDKIMVerificationEnabled 0) |
| 9 | ARC — before DMARC, because its only output offsets the DMARC score | post-transmission | a negative offset | off (ASArcFilteringEnabled 0) |
| 10 | DMARC | post-transmission | 5 |
on (ASDMARCEnabled 1) |
| 11 | SpamAssassin | post-transmission | as scored | off (spamassassinenabled 0); spamassassinport seeded 783
|
| 12 | Filter hook — last, because it is the most expensive | post-transmission | as returned, refusing at FilterHookRejectScore (100) |
off (FilterHookUrl empty) |
DMARC being on while SPF and DKIM verification are off is not a contradiction: the DMARC test evaluates SPF and DKIM itself, for its own purposes, whether or not either is enabled as a scored test of its own. So a default install does judge inbound mail against the sender's published DMARC policy — it simply does not add a separate SPF or DKIM score on top.
Greylisting is not a scored test: it is a separate 451 at RCPT TO, off by default. Two
thresholds decide what a score means — mark (default 5) adds spam headers and sets the spam
flag, delete (default 20) refuses the message, or quarantines it when the quarantine is on
and there is a message to hold. A quarantined message is answered 250, because a false
positive that the sender believes was delivered is recoverable without backscatter; a refused
one is answered 550 or 554.
Anti-spam does not run at all for an authenticated session, for a whitelisted sender, or for an IP range with spam protection switched off. See Stopping Spam and Stopping Viruses.
IMAP is a state machine, and almost every "the client says the folder is empty" problem is a
client sitting in the wrong state. This is hMailServer's, drawn from
Server/IMAP/IMAPConnection.cpp and the command handlers beside it.
stateDiagram-v2
state "Not authenticated" as NA
state "Authenticated" as AU
state "Selected" as SE
state "Idling" as IDL
[*] --> NA : TCP connect, then the OK greeting
NA --> NA : STARTTLS, then re-read CAPABILITY
NA --> NA : CAPABILITY, NOOP, ID
NA --> AU : LOGIN or AUTHENTICATE succeeds
NA --> NA : LOGIN fails - tarpit, then NO
NA --> [*] : 10 failures on one connection - BYE
AU --> AU : LIST, LSUB, CREATE, DELETE, RENAME, SUBSCRIBE, STATUS, APPEND, GETQUOTA, SETACL
AU --> SE : SELECT or EXAMINE
SE --> AU : CLOSE or UNSELECT
SE --> SE : FETCH, STORE, SEARCH, SORT, THREAD, COPY, MOVE, EXPUNGE, REPLACE
SE --> IDL : IDLE
IDL --> SE : DONE, or any command
AU --> NA : UNAUTHENTICATE - user discarded, TLS and ENABLEd extensions kept
SE --> NA : UNAUTHENTICATE
AU --> [*] : LOGOUT
SE --> [*] : LOGOUT
COMPRESS DEFLATE (new in 6.2.28) is a self-transition available in any state while
IMAPCompressionEnabled=1 and the connection is not already compressed. A second COMPRESS
is refused with [COMPRESSIONACTIVE], and STARTTLS is refused once compression is active.
SELECT is where the numbers come from. hMailServer answers it with EXISTS, RECENT,
FLAGS, [UIDVALIDITY], [UNSEEN], [UIDNEXT] and [MAILBOXID], plus [HIGHESTMODSEQ]
once the session has enabled CONDSTORE. A session that has enabled IMAP4rev2 gets no RECENT
and no [UNSEEN], because RFC 9051 removed them.
- IMAP keeps mail on the server; every device sees the same mailbox, folders and read/unread state, and the server pushes changes to an idle client. Use IMAP.
-
POP3 downloads mail to one device and (usually) deletes it from the server. It has no
folders, no shared flags and no push — a client that wants to look responsive must poll, and
each poll costs a TCP connection, a TLS handshake and a password verification. hMailServer
advertises
EXPIRE NEVERon POP3, which is the honest answer: nothing anywhere deletes a delivered message because of its age, so "leave mail on server" means for good.
| IMAP | POP3 | |
|---|---|---|
| Mail lives | on the server | on one device |
| Folders | yes, and shared/public folders too | INBOX only |
| Read/unread, flags | shared across devices | per device |
| New-mail notification |
IDLE pushes it |
polling only, with an optional LOGIN-DELAY
|
| Server-side search | yes | no |
| Concurrent sessions | many | one at a time — a second gets -ERR [IN-USE]
|
| Default ports here | 143, and 993 once you create it | 110, and 995 once you create it |
A message you send is judged before it is delivered. Three DNS records decide whether it lands in the inbox or the spam folder — or is rejected outright:
- SPF — a list of the servers allowed to send mail for your domain.
- DKIM — a cryptographic signature proving the message really came from you and was not altered.
- DMARC — a policy saying what to do when SPF and DKIM disagree, and where to send reports.
DNS for Your Domain sets all three up. Without them your mail will go to spam. This is the single most common reason a new mail server "doesn't work". Note the asymmetry: hMailServer signs your outgoing mail with DKIM as soon as you configure a key, but checking SPF on incoming mail is off by default — the record you publish is for other people's servers, and the check you enable is for yours.
hMailServer 6.3.3 · AGPL-3.0-or-later · Repository · Report a documentation error
Hmail Server — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- The Control Panel
- Your First Domain and Mailbox
- Connecting a Mail Client
- DNS for Your Domain
2. Secure it
3. Operate it
- Monitoring and Health
- Backup and Restore
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Routes and Relays
- The COM API and Scripting
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey