Connection settings
- Host
- your-kumo-hostname.example.com
- Port
- 587
- Security
- STARTTLS
KumoMTA is a high-performance, Lua-configured MTA for large-scale sending. There is no fixed SMTP hostname: you define your own ESMTP listeners with kumo.start_esmtp_listener, and the operator owns every layer: listener, TLS certificate, auth hook, relay policy. A protocol-level test matters because logs only tell you what happened after the fact, while a live transcript shows what the server advertises and accepts before anything reaches the queue.
Recommended settings
- Host: your server's public hostname or IP (must match the TLS certificate subject/SAN)
- Port: 587 (STARTTLS) for authenticated submission, or 25 for trusted relay
- Security: STARTTLS (KumoMTA requires encryption before AUTH)
- Auth method: PLAIN (no AUTH LOGIN support)
- Username/password: whatever your
smtp_server_auth_plainhandler validates
New to protocol testing? What an SMTP test is explains the stages you will see in the transcript.
Choosing listener ports: 25 vs 587
Run two listeners with distinct roles. Port 25 is for relay from trusted infrastructure: application servers, other MTAs, internal networks. Port 587 is for authenticated submission: users, outbound gateways, anything outside your trusted CIDRs.
-- Trusted relay: no AUTH, IP-based permission only
kumo.start_esmtp_listener {
listen = '0.0.0.0:25',
hostname = 'mail.example.com',
relay_hosts = { '10.0.0.0/8', '192.168.1.0/24' },
}
-- Authenticated submission: AUTH required, STARTTLS enforced
kumo.start_esmtp_listener {
listen = '0.0.0.0:587',
hostname = 'mail.example.com',
tls_certificate = '/etc/kumomta/certs/mail.example.com.pem',
tls_private_key = '/etc/kumomta/certs/mail.example.com.key',
}
The separation pays off twice: you can enforce TLS before AUTH on 587 without breaking legacy internal senders on 25, and a failed test immediately tells you which permission model is in play (IP allowlist on 25, credentials plus relay hook on 587). Never expose 587 to the internet with an empty or permissive smtp_server_auth_plain; the listener will accept mail from anyone.
In the 587 block, hostname appears in the EHLO greeting and should match your DNS and certificate. A common mistake is binding to 127.0.0.1 on a box that also runs your application, then wondering why external tests get connection refused; bind 0.0.0.0 (or a specific public interface) and control access with firewall rules and relay policy.
Implementing AUTH PLAIN
KumoMTA authenticates clients through the smtp_server_auth_plain event, a Lua function that receives the username and password from the AUTH PLAIN exchange and returns true or false:
kumo.on('smtp_server_auth_plain', function(authz, authc, password)
-- authz is usually empty; authc is the username
if authc == 'injector@example.com' and password == 'correct-secret' then
return true
end
return false
end)
For production, validate against a secrets store (Vault, environment variables, or a password file) rather than hardcoding credentials. KumoMTA's documentation covers Vault-backed authentication patterns.
Two key points:
- AUTH is only accepted after STARTTLS succeeds; clients that try AUTH on plaintext get a
530 Must issue a STARTTLS command firstresponse. - Only AUTH PLAIN is supported (AUTH LOGIN will fail). Set your client, or SMTP Tester, to PLAIN explicitly if Auto-detect does not work.
Verifying what your listener advertises
Run SMTP Tester in handshake-only mode (no message) and read the EHLO response in the transcript; a correctly configured submission listener advertises:
250-mail.example.com Hello [203.0.113.7]
250-STARTTLS
250-AUTH PLAIN
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250 HELP
Check three things:
STARTTLSis present on port 587. If missing, yourtls_certificate/tls_private_keypaths are wrong or unreadable by the kumomta process.AUTH PLAINappears only after STARTTLS completes; on the pre-TLS EHLO you should see STARTTLS but no AUTH.SIZEmatches your intended max message size; if missing or too small, larger mail bounces with a 552.
If you see AUTH LOGIN advertised, a proxy in front of KumoMTA is answering EHLO; credentials will still fail because the handler only implements PLAIN.
Reading the transcript stage by stage
The transcript is a chronological record of the client and server dialogue. Each stage isolates one layer of your configuration, so a failure at a specific line points at a specific knob in init.lua:
| Stage | Lines to look for | What it proves |
|---|---|---|
| Connect | 220 mail.example.com ... |
Listener reachable, hostname matches config |
| EHLO | 250- capability list |
Pre-TLS: STARTTLS present, no AUTH |
| STARTTLS | 220 Ready to start TLS + handshake |
Certificate loads, TLS negotiates |
| EHLO (after TLS) | 250-AUTH PLAIN |
Auth hook registered |
| AUTH PLAIN | 334, then 235 2.7.0 Accepted |
smtp_server_auth_plain returned true |
| MAIL FROM | 250 2.1.0 OK |
Envelope sender accepted |
| RCPT TO | 250 2.1.5 OK or 550/554 |
Relay permission granted or denied |
| DATA | 354, then 250 ... queued as ... |
Message entered the spool |
Two habits are worth building. Read the last server line before any error; that code is the authoritative answer. And compare the two EHLO responses side by side: most misconfigurations show up as a missing capability in one of those blocks rather than as an explicit error elsewhere.
Testing with SMTP Tester
- Set host to your KumoMTA server's public hostname or IP.
- Port 587, security STARTTLS.
- Auth method: PLAIN (Auto may also work if the server advertises only PLAIN).
- Enter the credentials your
smtp_server_auth_plainhandler expects. - From address: whatever your relay policy allows.
- Click Run. The transcript shows EHLO → STARTTLS → AUTH PLAIN → 235.
A 235 confirms authentication; a 250 with a queue ID means the message entered the spool.
Handshake-only testing against production
Full tests inject a real message into the queue, rarely what you want on a production MTA during a change or incident. Handshake-only mode stops after AUTH (or after EHLO on unauthenticated listeners): it proves TLS, capability advertising, and credentials without queuing anything. Use it after upgrades or init.lua changes, certificate rotations, and firewall or load-balancer changes that may break the client-visible path while the daemon is healthy. Run it from an external network so you test the same path your applications take.
Testing relay permissions: external vs localhost
Relay policy is where most KumoMTA surprises live: the same test can succeed from your jump host and fail from a laptop on a home connection. Test both paths:
| Test path | Expected result on port 25 | Expected result on port 587 |
|---|---|---|
From a relay_hosts CIDR |
250, no AUTH exchange | 250 after AUTH |
| From an external network | 550 relay denied (or 554) on RCPT TO |
250 after AUTH, if the relay hook grants authenticated sessions |
| AUTH with bad credentials | n/a | 535 authentication failed |
| AUTH on plaintext session | n/a | 530 Must issue a STARTTLS command first |
Many deployments accept unauthenticated injection from trusted servers on port 25 via relay_hosts: connections from listed CIDRs relay without AUTH. Test from an allowed network with security set to "none" (or STARTTLS if certs are configured) and empty credentials; the transcript should show a clean 250 on RCPT TO with no AUTH exchange. If external authenticated users get relay denied despite a 235, your relay hook is not checking the authenticated identity and must allow sessions that completed AUTH:
-- In a relay_message_generated or similar hook
if msg:get_meta('authc') then
-- allow
end
Internal-but-not-external relay after auth is the signature of a missing authc check.
Using the transcript to confirm DKIM and header behavior
Once a message is accepted, KumoMTA signs it according to the domain, selector, and key configuration in your policy script, with the signing identity usually taken from the envelope sender. The transcript cannot show the signature (it is added after injection), but it confirms the two things that determine whether signing and attribution work:
- Envelope sender: the
MAIL FROMaddress. The DKIM domain and selector are typically chosen from this, so a wrong envelope domain means mail signs as the wrong identity. Check the exactMAIL FROM:<...>line. - Received headers: KumoMTA adds a
Receivedheader reflecting the connecting IP and the listenerhostname; a wrong greeting hostname gives every message a misleadingReceivedchain, which hurts deliverability with Gmail and Microsoft.
A practical verification loop: send one test message to a Gmail or Microsoft 365 inbox you control, then check the delivered message for the DKIM d= domain and the Received header; if either is wrong, fix the envelope sender or the listener hostname and re-run. The Gmail SMTP test and Outlook 365 SMTP test guides cover the receiving side.
Comparing with swaks and telnet
If you already use swaks or telnet, SMTP Tester covers the same ground without remembering flags or typing base64 by hand:
| Task | swaks / telnet | SMTP Tester |
|---|---|---|
| Check advertised AUTH | swaks --server host:587, read EHLO |
Handshake-only run; capabilities in transcript |
| AUTH PLAIN test | swaks --auth PLAIN --auth-user u --auth-password p |
Select PLAIN, enter credentials |
| STARTTLS check | openssl s_client -starttls smtp -connect host:587 |
TLS panel: protocol, cipher, cert subject and expiry |
telnet handles quick loopback checks (telnet localhost 25, type EHLO) but cannot do STARTTLS, and typing AUTH PLAIN base64 into a terminal leaves the credential in your shell history. Hosted providers like Amazon SES remove most of this surface; on a self-hosted MTA the protocol is exposed to you directly.
Common operator errors
535 "Authentication failed"
Credentials do not match what your Lua handler expects. Check case sensitivity, trailing whitespace, and which listener you reached (multiple listeners can have their own auth logic). If your client negotiated AUTH LOGIN against a proxy in front of KumoMTA, force PLAIN.
550 "Relaying denied" / 554 after successful auth
After authentication, the server still checks relay policy. A relay error despite 235 AUTH OK means the configuration is not granting relay rights to authenticated sessions; add the authc check shown above. A 554 on RCPT TO with an authenticated session usually means the same.
Connection refused / connection timeout
Verify the listener is bound to an externally reachable address (not 127.0.0.1), that firewall rules allow 587 and 25 inbound, and that KumoMTA is running. A refused connection with the daemon up usually means the port is not in the listen set; ss -tlnp | grep -E ':(25|587)' settles it.
TLS handshake failures
- Certificate or key paths wrong, or the key unreadable by the kumomta user. The daemon log has the exact error.
- Expired certificate on 587: renew with certbot or acme.sh, repoint
tls_certificate/tls_private_keyat the fullchain and key files, and restart KumoMTA. The TLS panel shows protocol, cipher, cert subject, issuer, and expiry, so you can confirm the cert withoutopenssl s_client. - Hostname mismatch: the test client connected to a name not in the cert's subject/SAN. Use the CN or a listed SAN as the test host.
AUTH not advertised at all
If neither EHLO response contains an AUTH line, the smtp_server_auth_plain hook is not registered for the listener you reached. Confirm the kumo.on('smtp_server_auth_plain', ...) call executes in your policy script, that you are testing the right port, and that the script loaded without errors; a Lua exception during load can leave hooks unregistered while the daemon appears healthy.
421 and transient failures
A 421 Service not available on connect or EHLO usually means the listener is throttling, out of resources, or shutting down. Check the daemon log and connection limits before assuming a config bug; a single handshake-only run keeps the probe footprint minimal.
Security notes
- Do not expose port 587 to the internet without implementing
smtp_server_auth_plain; without it, any client can inject mail. - Use strong, unique passwords for SMTP injection credentials, and rotate them if compromised.
- SMTP Tester redacts all credentials from the transcript, so the output is safe to share in a ticket or team chat.
- Rate-limit authenticated injection using KumoMTA's traffic shaping, and keep port 25 relay restricted to tight CIDRs; an open relay will be found and abused within hours of exposure.
Related guides
Fixing SMTP authentication error 535 covers the 535 error above in more depth, and the PowerMTA SMTP test guide covers the other common self-hosted MTA. For port selection see SMTP port 587 vs 465, and for alternative tools see SMTP Tester vs smtper.net.
Frequently asked questions
Why does the transcript show no AUTH in the EHLO response before STARTTLS?
KumoMTA hides AUTH capabilities on unencrypted sessions to avoid leaking that authentication exists and to force clients onto TLS. Complete STARTTLS first, then issue EHLO again; the second response advertises AUTH PLAIN.
How do I test relay permissions without sending real mail?
Use handshake-only mode. It walks through connection, EHLO, STARTTLS, and AUTH but stops before MAIL FROM and RCPT TO, so nothing is queued. For relay policy you need the RCPT stage, so run one full test to a mailbox you control and read the exact response line.
What does a 250 with a queue ID actually confirm?
That KumoMTA accepted the message into its spool: listener, TLS, auth, and relay policy all passed. Delivery is a separate matter; check the queue and logs.
Can a test damage my production queue?
Handshake-only mode queues nothing. A full test queues exactly one message to the recipient you specify, which is negligible for a production MTA, but point it at a mailbox you control. During an incident, avoid repeated full tests: the extra queue entries make triage harder.
My applications connect from inside the datacenter. Why test from outside?
External tests exercise the full client-visible path: firewall, any load balancer or proxy, and the listener binding. A config that works over loopback can still be unreachable from the internet, and the failure mode tells you which layer diverges.
How often should I run a test against production KumoMTA?
Run handshake-only after every config change, certificate renewal, upgrade, or network change, and a full end-to-end test to a controlled mailbox on a regular schedule (weekly is a reasonable baseline for production).