Settings in detail
Microsoft 365
| Setting | Meaning |
|---|---|
| Tenant ID | GUID of your Azure tenant — see Entra admin center. |
| Client ID | GUID of your app registration. |
| Client secret | Stored DPAPI-encrypted. Leave empty = no change. |
| Sender address | Default/override MAIL FROM. Must be a valid Microsoft 365 mailbox. |
| Always override sender | When active, the client's MAIL FROM is ignored and replaced by the sender address. Useful when devices send incorrect FROMs. |
| Put original sender in the subject | Since version 1.8.12, off by default, available only together with “Always override sender”. Applies only when the From line was replaced: the subject then starts with [old.address@example.com] , so mailbox rules can sort mail by the original sender. The change touches only that one line; body and attachments stay byte for byte identical. A header of its own is not possible, because Microsoft Graph discards any custom X-header on send. An Exchange rule should therefore filter on the subject (“subject contains”). The subject is visible to all recipients, so they will see the original sender address. Per tenant, in all editions. |
| Allowed domains | Only mails with FROM @thisdomain.tld are permitted. Empty = only the sender address's domain. |
SMTP listener
| Setting | Meaning |
|---|---|
| Port | 25 for internal legacy devices, 587 for officially compliant submission. Freely choosable. Since 1.8.8, the assistant, the settings and the self-test check whether another application already uses the port and name it. The self-test and the assistant also connect to the listener via the host name and each of its addresses and report if one of them is not allowed. That is the typical case of an application on the server being rejected over IPv6. |
| Bind address | 127.0.0.1 or ::1 (local only), localhost (both loopbacks), 0.0.0.0 or :: (all interfaces, both address families), or a specific LAN address. See IPv4 and IPv6. |
| Max message size | Default 25 MB. Larger mails are rejected with SMTP 552. Graph accepts up to 150 MB. |
| Allowed source IPs | Whitelist of single IPs or CIDR ranges. 192.168.1.0/24 allows the entire /24 network, for example. Since 1.8.8, the addresses of this server are listed below it; a click adds one to the list. This helps applications on the server that address it by host name and therefore often over IPv6. An update does not change the list. |
| Blocked source IPs | Block list, since 1.8.8, off by default. Checked before the allow list and also applies in open mode. See Block lists and allowing. |
| Open mode | Whitelist disabled, any IP can relay. Only use in trusted networks. |
Transport encryption
Whether a listener port runs in plaintext, with STARTTLS or with implicit TLS is set on the listener itself. The certificate behind it has a section of its own, because it applies to every service on this machine: see the Certificates chapter for the rest.
Mail log privacy
| Setting | Meaning |
|---|---|
| Store subject | When off, subjects in the mail tracker are replaced by ***. |
| Store addresses | When off, sender/recipient are replaced by ***. |
| Log rejected mails | When off, IP-blocked or oversized mails do not appear in the tracker. The system log still records them. |
| Masking | None = plain text. Partial = m**l@e*****e.com and subject truncated to 15 characters + "…". |
Retention
Log files and mail tracker DB have separate retention periods. Both fields use the same sentinel scheme:
-1= do not store (completely disabled)0= unlimited (never deleted automatically)N > 0= N days, older entries are deleted automatically
Predefined options: 7 / 14 / 30 / 60 / 90 / 180 / 365 days. Defaults are 7 days (logs) and 14 days (mail tracker).
Delivery to Microsoft 365
If handing a mail over to Microsoft 365 fails, SMTPly retries. Under How long to keep retrying (minutes) you set for how long — only after that is the mail considered undeliverable. The default of 55 minutes bridges a short internet outage; raise it if your line tends to be down for longer. The setting applies to all tenants and therefore sits under Microsoft 365 rather than with the SMTP listener: what is retried is the handover to Microsoft, not the SMTP reception.
The waits between attempts are fixed (10 seconds, 30 seconds, then every two minutes). Only certain values are therefore reachable; an intermediate value is rounded to the nearest one, with a deviation of at most one minute. Throttling by Microsoft (HTTP 429) and an open circuit breaker keep retrying independently — they do not count against this window.
Backup: passphrase and instant test
The backup passphrase is entered twice. That is not red tape: a typo would otherwise surface only when the backup is needed — and at that point the file cannot be opened any more, because nobody remembers what was actually typed.
Create backup now writes a backup to the configured folder immediately, without waiting for the scheduled run. The file produced is identical to the scheduled export. Email delivery is not exercised, because that runs in the service.
Window size and keyboard use
SMTPly remembers the size and position of its window across restarts. If the stored position no longer falls on an existing screen — after unplugging a second monitor, or in an RDP session with a different resolution — it is discarded and the window opens centred again.
The interface can be operated without a mouse: Ctrl + 1 to 8 switch between dashboard, Maillog, system log, Mailflow, settings, licence, about and Mailflow with the rule tester, Ctrl + , opens the settings, and F6 moves the focus between the navigation pane and the content. In Mailflow the arrow keys select a rule, Enter opens it for editing, Ctrl + S saves noted changes (also in the settings), and Enter in a rule tester field runs the test.
Administrator rights and “Always start as administrator”
SMTPly reads and changes the configuration, which is accessible to administrators only. Without administrator rights the interface starts in read-only mode: the Maillog with the running figures, licence, help and about stay visible, while dashboard, system log, Mailflow and settings are hidden. Under Settings → General you can switch on Always start as administrator. SMTPly then opens with full rights on every start, also when you double-click the shortcut or use the Start menu; Windows asks for confirmation each time. The setting applies from the next start and only to the signed-in Windows user, and it is not part of the backup. It uses the Windows compatibility setting “Run this program as an administrator” and can also be switched off in the properties of the shortcut. The checkbox is only reachable in administrator mode, so the first time start SMTPly once via right-click and “Run as administrator”.
Certificates
Since version 1.8.0 the certificate has a section of its own in the settings, for a reason that makes setup simpler: one certificate serves every service on this machine. The SMTP listeners take it from there, and so do the monitoring endpoint and the REST interface. It is the same machine under the same name; a second certificate setup would be duplicate work for the same purpose. Changes take effect without restarting the service.
When you actually need one
Transport encryption is off by default, and for legacy devices on a trusted LAN that is usually the right setting. You need a certificate as soon as one of these applies:
- A device requires STARTTLS: the connection starts in plaintext and is upgraded to TLS by a command. This is the recommended mode and fits most modern SMTP clients.
- A device requires implicit TLS/SSL: the connection is encrypted from the very first byte, with no plaintext phase. Typical for older industrial controllers whose interface simply offers "SSL". Without that mode they fail with errors such as
SSL23_GET_SERVER_HELLO:unknown protocol. - You put the monitoring endpoint or REST interface on HTTPS because they listen beyond the loopback.
- You use SMTP authentication with plaintext passwords. SMTPly refuses those on unencrypted connections unless you explicitly enable the exception.
From the Business edition on you can run several listener ports in parallel, each with its own TLS mode. Old "SSL" devices then point at one port, newer STARTTLS devices at another, and you do not have to reconfigure the devices one by one.
Where the certificate comes from
Four sources are available, all in the Certificate section:
| Source | When it fits |
|---|---|
| Self-signed | SMTPly generates a certificate valid for ten years and also places it under Trusted Root Certification Authorities of the local machine. The quickest route for a relay whose traffic never leaves the machine. |
| PEM files | Two files, such as fullchain.pem and privkey.pem from an ACME client. |
| PFX / PKCS#12 | A ready-made container with a password, as exported by Windows tooling. |
| Windows certificate store | A certificate already present there, from your internal CA or from an ACME client such as win-acme or Certify The Web. The source that makes automatic renewal possible: see below. |
Inspect and validate the certificate
Next to the certificate source you will find Show current certificate. The button opens the certificate the service actually uses, whichever of the four sources it came from, in the familiar Windows dialog with thumbprint, validity and subject alternative names.
If transport encryption is switched on without a usable certificate, SMTPly says so when saving and offers to generate a self-signed one straight away. If you decline, encryption is saved as switched off, because otherwise the service would fail to start.
The self-test covers the certificate as well: validity, fit with the listener, and for implicit TLS a real handshake against your own port.
Renewing certificates automatically
SMTPly does not issue certificates itself and does not handle ACME enrollment. That is deliberate: a tool that only manages certificates does it more thoroughly, and SMTPly needs no provider integrations to maintain. SMTPly's job ends with picking up a renewed certificate without any action, and that it can do.
The trick fits in one sentence: let the renewal tool place the certificate into the Windows store, and point SMTPly's certificate source at the subject rather than the thumbprint. Every renewal changes the thumbprint but keeps the subject. SMTPly then looks every six hours for the newest valid certificate with that subject and picks it up without a service restart. Without this, you would have to enter the new thumbprint by hand after every renewal.
Internal certificate authority (the most common case)
If your devices reach the relay under an internal name such as smtp.company.local or an IP, an internal CA is the right way, not a public one. Active Directory Certificate Services (ADCS) issues a server certificate by auto-enrollment and renews it on its own in the Windows store. Your devices trust the internal CA anyway. Point SMTPly at that certificate's subject and renewal runs unnoticed forever.
Public certificate with win-acme (Let's Encrypt)
Only makes sense if the relay is reachable under a publicly resolvable name and can satisfy an ACME challenge. No public authority issues for a purely internal name or an IP, with any method. If that applies to you, the path with win-acme is short:
- Download win-acme and start it as administrator.
- Request a certificate for the server's public name. Depending on reachability the challenge runs over HTTP (port 80 must be reachable from the internet) or over DNS (a TXT record set by a script).
- Choose Windows Certificate Store as the store target (not just a PEM file). win-acme creates a scheduled task and renews the certificate on its own from then on.
- In SMTPly under Certificate choose the source Windows certificate store and enter the subject, not the thumbprint.
Mind the trust: a Let's Encrypt certificate requires the device to know the root authority. Older printers and line-of-business devices often do not. In such cases a self-signed certificate you place into the devices once, or one from the internal PKI, is the more reliable choice.
Keeping an eye on expiry
Whatever the source, SMTPly warns by email when a certificate nears expiry; the switch for it sits with the notifications. The monitoring endpoint reports the remaining lifetime as certificateDays and smtply_tls_certificate_days_remaining, and the REST interface carries it in /api/v1/status. Negative values mean "expired N days ago", which is exactly what you want to alert on. So if a renewal ever fails to happen, you learn about it before delivery stops.
When a device does not trust the certificate
The most common picture after switching TLS on: the device sends in plaintext without complaint and fails with encryption without a useful message. The cause is almost always the self-signed certificate, whose issuer the device does not know. The error arises on the device side, which is why SMTPly's system log often shows nothing at all. The steps to fix it are under troubleshooting.
Renew the client secret
Client secrets in Microsoft Entra have a limited lifetime — once the secret expires, mail delivery stops. SMTPly warns from 14 days before expiry in the interface and by e-mail to your notification address. There are two ways to renew: by hand or automatically Business.
Renew by hand
- Open Settings → Microsoft 365. Next to the expiry date is the Renew secret … button.
- Click Renew secret …. The Microsoft sign-in window opens.
- Sign in with an administrator account of your tenant. SMTPly adds an additional secret to the existing app registration — app, service principal and permissions stay untouched.
- SMTPly applies the new secret and its expiry automatically. Then Save and restart the service.


The previous secret is not removed. It stays valid until its regular expiry, so there is no gap between renewal and restart. With multiple tenants Business every tenant card has its own button — sign in to the tenant whose secret you want to renew.
Renew automatically Business
Set up once, the service renews the secret itself from 21 days before expiry — no more diary entry.
- In Settings → Microsoft 365 (or in a tenant card) tick Renew the client secret automatically before it expires.
- A short dialog explains what is required and offers the choice between Let SMTPly set it up and Do it yourself in the Entra admin center. For the first route, click Set up now.
- Sign in with an administrator account of your tenant in the Microsoft sign-in window. SMTPly grants the app registration
Application.ReadWrite.OwnedByonce and makes it the owner of its own registration. - Done. From now on the service renews the secret itself once less than 21 days remain. Optionally an e-mail reports each run (on by default).

What you trade for it. The app registration is granted Application.ReadWrite.OwnedBy once and becomes the owner of its own registration. That lets it change registrations it owns itself — and only those; Application.ReadWrite.All is deliberately not requested.
That is more than plain mail delivery needs. So the feature is explicitly optional and off by default: if you would rather not grant it, keep renewing by hand and you lose nothing. Here too the old secret stays valid until it expires normally.
What happens during sign-in. Step 3 signs you in through Microsoft's own "Microsoft Graph Command Line Tools" application. Microsoft requires consent on behalf of the organisation for this — as a result, that Microsoft application permanently receives the rights Application.ReadWrite.All and AppRoleAssignment.ReadWrite.All in your tenant. This is a necessary prerequisite to then be able to grant the SMTPly app its own, tightly scoped OwnedBy permission — Entra has no way to grant that without this intermediate step.
These rights remain in place after the run. You can remove them again at any time in the Entra admin center under Enterprise applications → Microsoft Graph Command Line Tools → Permissions, without affecting the OwnedBy permission already granted to the SMTPly app.
Without a sign-in: grant both permissions by hand
If you would rather verify the Entra changes yourself, you can set up the same two things directly in the Entra admin center — without a sign-in window:
- App registrations → your SMTPly app → API permissions → Add a permission → Microsoft Graph → Application permissions, select
Application.ReadWrite.OwnedByand add it. Then click "Grant admin consent for [tenant]" (requires the Global Administrator role). - The app also needs to become the owner of its own registration. The Entra portal UI does not support this directly — the simplest way is the Graph Explorer with your admin account:
POSTtohttps://graph.microsoft.com/v1.0/applications/{app object ID}/owners/$refwith body{"@odata.id": "https://graph.microsoft.com/v1.0/servicePrincipals/{service principal object ID}"}. Both object IDs are shown on the app registration's overview page and its enterprise application entry. - Only then tick Renew client secret automatically before it expires in SMTPly. In the dialog that follows, choose "Do it myself in the Entra admin center" and confirm the two steps — this opens no sign-in window.
Up to version 1.7.31 this worked differently: the dialog and the sign-in window opened even when the permission had long since been granted by hand, merely reporting "Permission was already granted". Since 1.7.32 the manual route is a route of its own rather than an intermediate step.
Mail.Send appears under "Configured permissions"; Application.ReadWrite.OwnedBy — whether granted by SMTPly or by hand — shows up under "Other permissions granted". Both with status "Granted".
Set up webhooks
As of version 1.7.16, SMTPly can send an HTTP POST to an address of your choice when operational events occur (Business/Enterprise). That puts incidents where you are already looking instead of burying them in a mailbox: a message in a Teams or Slack channel, a ticket in your PSA system, or a trigger in n8n, Node-RED or Power Automate.
Setup
In Settings under Webhooks: tick the box, enter the target URL, optionally set a signing secret, and pick which events to send. Save, then verify with "Send test event" — the button uses exactly the same path the service uses for real events, so a successful test is meaningful.
https is accepted everywhere. Plain http only for targets inside your own network (loopback, 10.x, 172.16–31.x, 192.168.x, 169.254.x, CGNAT), because the payload contains sender, recipients and subject, which has no business travelling unencrypted across the internet.
Events
| Event | When |
|---|---|
delivery.failed | A message could not be delivered after all retry attempts |
relay.paused | Microsoft Graph is unreachable, sending is paused |
relay.resumed | Sending is working again |
certificate.expiring | The STARTTLS certificate expires in 30 days or less |
secret.expiring | The Azure client secret is about to expire |
license.invalid | The license has become invalid, for example after the trial or the 30-day offline grace period ended. Since 1.8.5, all editions. Since 1.8.9 with relayPaused and relayPausesAt (delivery paused, or from when) |
license.revalidation_due | The last successful online license check is approaching the 30-day limit (14, 7, 3 and 1 day before). Since 1.8.7 |
test | Triggered manually from the test button |
Request format
POST with Content-Type: application/json, plus the headers X-Smtply-Event (event name) and, if a secret is configured, X-Smtply-Signature.
{
"event": "delivery.failed",
"timestamp": "2026-07-31T08:45:13+02:00",
"server": "SRV-RELAY01",
"version": "1.7.16",
"data": {
"from": "printer@example.com",
"to": "accounting@example.com",
"subject": "Scan 2026-07-31",
"error": "HTTP 403 Forbidden",
"tenant": "Primary tenant",
"attempts": 5
}
}
The fields inside data follow your privacy settings: with masking enabled, addresses and subjects arrive already masked.
Verifying the signature
With a secret configured, X-Smtply-Signature carries an HMAC-SHA256 over the exact request body in the form sha256=<hex>. The receiver recomputes it and thereby knows the call genuinely came from this installation and was not altered in transit:
# PowerShell example for the receiving side
$secret = "YourSigningSecret"
$body = $request.Body # exactly as received, do not reformat
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($secret))
$hash = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($body))
$expected = "sha256=" + ([BitConverter]::ToString($hash) -replace '-','').ToLower()
if ($expected -ne $request.Headers["X-Smtply-Signature"]) { throw "Invalid signature" }
Important: compute over the unmodified body. Parsing the JSON and re-serialising it produces a different string and therefore a different signature.
For Teams and Slack webhooks you can leave the secret empty — there the unguessable URL is the protection.
Delivery behaviour
Up to three attempts, 2 and 6 seconds apart. If the receiver answers with a 4xx error (other than 408 and 429), there is no retry, because that is a configuration problem and repeating it changes nothing. A webhook is a notification, not guaranteed delivery — the mail tracker remains the reliable record. Sending runs concurrently: a slow or unreachable receiver never slows down mail relaying. The outcome of the last attempt is shown in Settings under "Last delivery attempt".
Hook up monitoring
As of version 1.7.13 SMTPly ships an HTTP monitoring endpoint (Business/Enterprise). Enable it in Settings under Monitoring (checkbox + port, default 8025), then restart the service. The endpoint is disabled by default and answers only on 127.0.0.1 — responses contain nothing but counters and status values, no addresses, subjects or mail content.
GET /health — for HTTP sensors
Returns HTTP 200 while the relay is operational and HTTP 503 when sending is paused (Microsoft Graph unreachable, circuit breaker open). A plain HTTP sensor is enough: 200 = green, anything else = alert.
{
"status": "ok", // "ok" or "degraded"
"version": "1.7.13",
"uptimeSeconds": 86400, // seconds since service start
"queueLength": 0, // mails queued / retrying
"circuitBreaker": "Closed", // Closed | Open | HalfOpen
"today": { "sent": 312, "failed": 1, "pending": 0 }
}
The daily counters use the server's local calendar day — the same values as the daily tiles in the Maillog.
Since version 1.8.0 /health also reports the state of sandbox mode and the remaining lifetime of the client secret and the listener certificate:
"sandbox": { "configured": false, "activeNow": false },
"expiry": {
"certificateDays": 42, // null when TLS is off
"clientSecretDaysMin": 17, // smallest value across all tenants
"clientSecrets": [ { "tenant": "Firma 1", "days": 17 } ]
}
/metrics carries the same values as smtply_sandbox_active, smtply_tls_certificate_days_remaining and smtply_client_secret_days_remaining (labelled per tenant) plus …_min. Negative values mean "expired N days ago" — exactly the state worth alerting on.
The HTTP status code is unaffected: 503 means "not working right now", not "will stop working in three weeks".
TLS. Also since 1.8.0 the endpoint can be switched to HTTPS — the Use TLS (HTTPS) checkbox in the settings. It uses the same certificate as the SMTP listener. Less pressing here than for the REST interface, since only counters go over the wire; some monitoring systems refuse unencrypted targets outright, though.
GET /metrics — Prometheus text format
smtply_up 1
smtply_uptime_seconds 86400
smtply_queue_length 0
smtply_circuit_breaker_open 0
smtply_mails_sent_today 312
smtply_mails_failed_today 1
smtply_mails_pending_today 0
Useful alerts: smtply_up missing (service down), smtply_circuit_breaker_open == 1 (sending paused), smtply_queue_length growing steadily, smtply_mails_failed_today spiking.
Hooking up common systems
PRTG: an "HTTP" or "HTTP Advanced" sensor on http://<server>:8025/health — status code 200 as the OK condition is all it takes. For values as channels, use "EXE/Script Advanced" with the PowerShell script below. Zabbix: HTTP agent item on /health with JSONPath preprocessing (e.g. $.queueLength), or scrape /metrics natively via the Prometheus support. CheckMK: active HTTP check on /health or a local check based on the script below. Prometheus/Grafana: scrape job straight on /metrics.
RMM platforms: in N-able N-sight, deploy the script below as a "Script Check" (non-zero exit code = alert); in N-able N-central, as an automation policy (AMP) or custom service check. Octoja and comparable RMM solutions with script sensors hook SMTPly up through exactly the same pattern — and since the agent runs locally on the server, the default bind on 127.0.0.1 is sufficient, no firewall opening needed.
PowerShell example
Universal health check — as a Task Scheduler probe, Zabbix UserParameter or PRTG "EXE/Script Advanced" (exit code 0 = OK, 1 = warning, 2 = critical):
# smtply-healthcheck.ps1 — example, adjust the port if needed
$url = "http://127.0.0.1:8025/health"
try {
$h = Invoke-RestMethod -Uri $url -TimeoutSec 5
if ($h.circuitBreaker -eq "Open") {
Write-Output "CRITICAL: sending paused (circuit breaker open), queue=$($h.queueLength)"
exit 2
}
if ($h.queueLength -gt 50) {
Write-Output "WARNING: queue backlog ($($h.queueLength) mails)"
exit 1
}
Write-Output "OK: v$($h.version), today $($h.today.sent) sent, $($h.today.failed) failed, queue=$($h.queueLength)"
exit 0
}
catch {
Write-Output "CRITICAL: SMTPly endpoint unreachable ($($_.Exception.Message))"
exit 2
}
Polling from the LAN: by default the endpoint answers locally only. For remote polling, set BindAddress in the Monitoring section of %ProgramData%\Smtply\appsettings.json to the LAN IP or 0.0.0.0, restart the service, and open the port in Windows Firewall for your monitoring server specifically. The endpoint deliberately has no authentication — the firewall rule is the access control.
Backup & restore
Yes — the entire file is encrypted, for the manual export just as for the scheduled one. Protection is your backup passphrase via PBKDF2-SHA256 (600,000 iterations) and AES-256-GCM. All that stays readable is a short envelope with a format marker and creation date, so you can tell what a file is even without the passphrase:
{
"format": "smtply-encrypted-backup",
"version": 1,
"createdAt": "2026-07-30T23:51:45+02:00",
"payload": "portable:v1:600000:…"
}
Everything else — tenant and client IDs, client secrets, sender addresses, domain and IP allowlists, ports, SMTP users and paths — sits inside the encrypted block. On top of that, the credentials within it are individually encrypted again, so they are not exposed even after unpacking. The backup passphrase itself is never included.
AES-GCM detects any later modification: a corrupted or tampered backup is rejected on import rather than half-applied. Without the passphrase the file cannot be restored — store it separately from the backups, otherwise the best backup is worthless when you need it.
Permissions on UNC/SMB paths. The SMTPly service runs as LocalSystem by default. That account is not your signed-in user: when accessing a network share, the server authenticates with its computer account, i.e. DOMAIN\SERVERNAME$. A path you can happily write to in Explorer may therefore still be denied to the service.
For the scheduled export to work, grant the computer account write access — in both places:
- Share permissions of the SMB share:
DOMAIN\SERVERNAME$→ Change - NTFS permissions of the target folder:
DOMAIN\SERVERNAME$→ Modify
You can verify this upfront from an administrative PowerShell on the SMTPly server:
# Test in the computer account's context (requires PsExec from Sysinternals)
psexec -s -accepteula powershell -Command "New-Item '\\nas\backup\smtply\test.txt' -ItemType File; Remove-Item '\\nas\backup\smtply\test.txt'"
Alternatively, run the service under a domain user account or a group Managed Service Account (gMSA) — then its permissions apply. Change this in the Services console under Properties → Log On.
If access fails, the reason appears in the system log and in Settings under "Last backup", including a pointer to the computer account. An unreachable share does not block email delivery: both destinations are attempted independently.
Restoring: applying only parts
Before applying, SMTPly shows what would change: section, field, current value, imported value. Changed rows are highlighted, secrets never appear in clear text but only as "set", "changed" or "unchanged".
Since version 1.8.0 every row can be ticked individually in that preview. Only what stays ticked is applied; everything else keeps the value currently on the server. Two buttons select all or none.
The reason is an everyday situation: an older backup holds the right listener settings but a client secret that has since been replaced. Import used to be all or nothing, so restoring the listener dragged the old credentials along. Now you tick port, bind address and allow list and leave Microsoft 365 untouched.
Two things belong together and therefore share a row: the syslog target (server, port and transport) and the bind address plus port of the REST and MCP interfaces. Applied separately they would produce half a configuration. The list of additional tenants has a row of its own and is applied like any other field: only when ticked. Mixing it field by field would not make sense, so it is carried as a whole.
Backing up: include only parts
The same works in the other direction. Next to Create backup there is Export selection…: you see the same list, tick what should go into the file, and everything else is stored at the value a fresh installation would have. Useful when a backup goes to support or moves to a second server without taking the credentials along.
A file created this way remembers which fields it contains (encrypted like the rest of its content). On restore the preview offers only those; the remaining rows appear as "(not in this file)" and cannot be ticked. That matters more than it sounds: without this note a backup holding three fields would look as if it wanted to reset everything else to defaults, and one click on Select all would have done exactly that. Older backups without the note stay readable and behave as before.
Dashboard: numbers and trend
Since version 1.9.0 the interface starts with an evaluation. It shows at a glance what has passed through the relay in the last hours or days, and answers the questions that would otherwise be filtering work in the mail table. The table itself is called Maillog since this version.
What the dashboard shows
- Tiles: delivered, in delivery, failed, rejected, delivered volume (the size of the delivered mails) and the share of mails received encrypted (STARTTLS or implicit TLS versus plain text). Mails from before version 1.8.0, whose transport was not recorded, and rejections are not part of that share.
- Trend: mails over time, split into delivered and failed or rejected. The default is 7 days. “Today” shows every hour, “7 days” and “30 days” show every day, in the server's local time.
- Rankings: the most frequent senders, recipients, source IPs and SMTP accounts, and the reasons mails were rejected. Each list shows the five most frequent entries.
Data and limits
All numbers come from the mail tracker, that is, from the same metadata as in the Maillog. Without an Enterprise licence the view reaches back 14 days, with retention beyond 14 days further. At most the newest 50,000 mails of the chosen period are evaluated; if the period is larger, a note appears below.
Anonymisation: if partial masking is switched on in the privacy section of the settings, addresses, IPs and accounts are masked in the dashboard as well, at display time. Older entries that were written in plain text before masking was switched on are protected too. Grouping uses the masked form, so the rankings become a little coarser.
Reset: Reset statistics sets the numbers to zero and counts anew from that moment. It only affects the display of this Windows user; the mail history is kept.
Administrator rights required: the database is readable by administrators only. Without them the dashboard is not in the navigation, and the interface starts in the Maillog with the live numbers.
Maillog (mail tracker)
Up to version 1.8 this view was called Dashboard. Since 1.9.0 it is the second entry in the navigation and is named Maillog; it works unchanged.
The mail tracker lists every submitted message with time, sender, recipients, subject, source IP, user, transport, size and status. Only this metadata is stored — no message content. Sender, recipients, subject, source IP and error message are individually encrypted in the database.
Choosing columns
Columns above the table lets you switch any column off and on again. The choice is per Windows user and survives restarts. A column added by a later version appears with its default — an older selection does not become invalid because of it.
Filtering
A filter row sits above the table: free text across all visible columns, plus status and period. A right-click on a cell offers Filter by this value — the fastest route from "this one address stands out" to "what else did it send".
Detail view
Clicking the triangle at the start of a row expands the entry and shows what does not fit in the table: duration, attempt count, tenant, Graph message ID and the full error message. The last two sit in selectable fields because they get copied into support tickets. Empty fields hide their row. A second click collapses it again.
Since 1.8.8, sender, recipient and source IP in the detail view are clickable. A click opens a menu showing the current state (allowed, blocked, not allowed) and the matching actions: allow, lift block, block the address or the entire domain. Blocked values are shown in red, source IPs that are not allowed in yellow, in the detail view and in the table. Details under Block lists and allowing.
Colour coding
Rows that were not successful are highlighted: red for permanently failed, red for rejected (by the listener, before the message reached the queue), yellow for "retrying", grey for "still waiting". Successful rows stay unmarked — the colour is meant to mean something.
Daily counters and status bar
Above the table are today's figures: sent, pending, errors. Reset them by right-clicking the tiles or under Settings → General → Reset today's counters. This only affects the display; the mail history is kept.
In the bottom right, the status bar shows the state of the service on every page: a green dot while it runs, yellow during a transition or when the live connection to the window is missing, red when it is stopped. Clicking it opens a menu to start, stop and restart the service. In the settings, Save and, when a change requires it, Restart service now sit on the left of the same bar.
Mailflow and rule tester
The Mailflow view (since 1.9.0, Ctrl+4) shows which rules from the settings currently act on a mail, in the order SMTPly checks them. It is a second view of the same settings: nothing is stored twice, and every setting stays where it was. The view needs administrator rights because it reads the configuration.
| Rule | Conditions | Action | State | Counter | |
|---|---|---|---|---|---|
| Reception4 | |||||
| Requirements (all must be met) | 4.318 | ||||
| 6 | Allowed source IPs | Source in: 192.168.10.0/24 ✎ | Required | active | 12 |
| 11 | Message size | at most 25 MB ✎ | Required | active | 0 |
| 12 | Everything else | – | Reject (550) | always | |
| Assignment3 | |||||
| 13 | Tenant example.com | Sender in: @example.com ✎ | Assign: example.com | active | 4.318 |
| 15 | No tenant matches | – | Reject (550) | always | 7 |
| Delivery1 | |||||
| 23 | All assigned mails | – | Hand over to Microsoft 365 | always | 4.316 |
Layout
At the top is the path of a mail: device, then the three stages inside SMTPly, then Microsoft 365. Clicking a stage jumps to its rules; hovering over it highlights them. Below is the ruleset with the columns Rule, Conditions, Action, State and Counter. Conditions holds all criteria of a rule, labelled with source, sender, recipient or subject, for example “Source in: 192.168.10.0/24”. It is organised by stage:
- Reception: first the blocks (blocked source IPs, senders and recipients, which reject when they apply), then the requirements: allowed source IPs, recipient restriction, allow lists, rate limit and message size. All requirements must be met together for a mail to be accepted. At the end stands “Everything else: Reject”. The counter at a requirement counts the mails that failed at it.
- Assignment: AUTH bindings, tenants by sender domain and outbound rules. The final rule “No tenant matches” rejects the mail.
- Delivery: replace sender, subject label, sandbox mode and journal BCC, ending with “Hand over to Microsoft 365”.
As in a firewall ruleset, the order counts from top to bottom. The numbers are positions in the ruleset; while switched-off rules are hidden (the default), the numbering has gaps. Rules the licence does not cover stay visible and are marked yellow because they are a warning. Each group folds with its arrow, “+ Add rule” is a button at the head of each group. In Delivery it opens the functions replace sender, subject label, sandbox mode and journal BCC to switch them on; what the licence does not cover is greyed out with the reason. Columns can be resized and rearranged.
Editing
A small pencil stands behind the conditions of every rule, including switched-off rules. It opens a window with exactly the fields of that rule. A double click on a rule opens the matching place in the settings instead. Outbound rules also have arrows to change their order, because only there does the order change the result. By keyboard: the arrow keys select a rule, Enter edits it, Alt + arrow up or down moves an outbound rule, Insert opens “Add rule” of the stage.
The view never writes to the configuration on its own: changes take the same route as in the settings, with the same checks and licence limits. Changes are only noted: a hint appears above the ruleset and “Save” appears in the bar at the bottom (also Ctrl + S), followed by “Restart service now”. This way several changes can be applied in one go, and the service never restarts unasked. The table shows the change at once; it is only saved once you click “Save”. Some things stay with the settings, such as AUTH bindings, a new tenant or the sandbox time window; the double click leads there directly.
Rule tester
Below the ruleset is the rule tester, open when the view opens (also Ctrl+8). It answers “What happens to this mail?” without sending anything. You enter source IP, optionally an AUTH user, sender, recipients, subject and size; the tester runs the same evaluation as the service and shows the result (accepted with tenant and sender address, or rejected with reason and SMTP code). The stages and rules involved are marked in the diagram and the table, and “All steps” shows the check step by step.
Assume edition lets you play through what another licence tier would change. Not simulated are the rate limit and the session limits: they depend on the traffic of the moment, not on the settings.
Rule counters
Since 1.9.0 SMTPly counts how often each rule has applied and shows it as the Counter column in Mailflow. It shows whether a rule does anything in daily use at all, and which rule caused an unexpected rejection.
What is counted
- Acceptance rules count the acceptance attempts they rejected. Devices often retry a rejected mail, so the number can be higher than the number of mails.
- Assignment (tenants, AUTH bindings, outbound rules) and the final rule of reception count accepted mails.
- Delivery rules count delivered mails, once after successful sending, not per attempt. The envelope Bcc is not counted.
The counter always sits at exactly the rule that applied: per list for block lists, per rule that covers the source IP for the recipient restriction. If you delete a rule, its counter disappears at the next service start; a new rule starts at zero.
Reset and storage
Reset counters asks first and sets all counters to zero; next to it you see since when counting runs. The interface cannot control the service directly, so it leaves a marker that the service picks up within seconds. The numbers live in a file in the protected configuration folder and survive restarts. They contain only step names, identifiers and numbers, no addresses. A hard crash costs at most the numbers of the last seconds. The counters are operating data and not a setting, so they are not part of the backup.
For monitoring and support
- Monitoring (Business and Enterprise):
/metricscarries the seriessmtply_rule_counter_total{rule="…"}. The label names the step and, where needed,primaryortenant-Nfor tenants,rule-Nfor outbound rules andrestriction-Nfor recipient restrictions. Names and addresses never appear because the endpoint has no login. - REST interface (Enterprise): the status contains the block
ruleCounterswith the same labels and the time counting started. - Diagnostics package:
configuration-summary.txtlists the counters under “Trefferzähler der Regeln”.
Journal BCC and conditional journalling
Business and Enterprise feature. Journal BCC places a blind copy of every relayed message into an archive mailbox. Technically a Bcc: line is inserted into the raw message (or an existing one extended); Microsoft picks the recipients up from it and strips the line before delivery — ordinary BCC semantics. Nothing else about the message changes.
Conditional journalling (from version 1.8.0) narrows this to certain senders: under Settings → Microsoft 365 → Journal only for you enter addresses or whole domains, one per line. An entry without @ counts as a domain, example.com and @example.com mean the same thing. Case does not matter.
Leave the field empty and everything is journalled — the behaviour before 1.8.0. The field only appears once an archive mailbox is configured; a filter without a target would be a setting without effect.
In sandbox mode journal BCC is switched off. Outbound rules can force or suppress journalling per message and take precedence over the filter.
Retention with proof
Enterprise feature, from version 1.8.0. It answers a question that comes up in audits again and again: how do you show that nothing was changed in your delivery history afterwards?
How it works
Every mail tracker entry gets a checksum (SHA-256) as it is written, computed over its own fields and over the checksum of the preceding entry. That forms a chain: change or delete a row later and its checksum changes — and from that point on none of the following ones match any more.
The checksum covers timestamp, sender, recipients, subject, size, status, tenant, attempt count and the Graph message ID. The chain is built while writing, not afterwards — so there is no moment at which a row could be slipped in unnoticed.
Verifying the chain
Settings → Retention → Verify chain. SMTPly reads the history once and either reports that all links match, or names the timestamp and id of the first entry where the chain breaks. From there on the series can no longer be vouched for — before it, it can.
Rows written before 1.8.0 carry no checksum. They do not break the chain, they restart it; the report states from which point the proof applies.
Signed export
Settings → Retention → Export proof. Produces two files: a CSV with the entries and their checksums, and a manifest holding the period covered, the row count, the checksum of the CSV file itself and the last link of the chain. The manifest is deliberately short and in plain text so an auditor can read it without SMTPly.
What the proof shows — and what it does not
It shows that the exported series is internally consistent and unchanged since it was written. Delete the database entirely and start over, and you are left with a shorter but equally consistent chain — the proof does not by itself reveal that something is missing. That is why the start of the chain is in the manifest: a series that begins suspiciously late is worth asking about.
This is not a GoBD certification and it does not replace audit-proof archiving. It vouches for the integrity of SMTPly's delivery history — the messages themselves live in Microsoft 365, not here. For archiving the messages, journal BCC is the tool.
Masking individual entries afterwards
Sometimes the history holds something that should not stay there: a subject line with a diagnosis, an employee's private address, an erasure request under Article 17 GDPR. Since version 1.8.0 a single entry can be masked afterwards, without clearing the whole history.
Maillog → right-click the row → Mask this entry. Sender, recipients and subject are replaced by their masked version, the same one privacy masking produces while writing. Everything else stays: time, status, size, duration, source IP, tenant. The entry does not disappear, it merely becomes impersonal.
It is final. The values are overwritten in the database, not just hidden from view. There is no way back, not even with administrator rights. That is why a confirmation is asked first.
The time of masking is recorded and shown in the entry's detail view: Masked on …
What this does to the checksum chain. It breaks at that row, and that is intended. The chain exists to make later changes visible; recomputing it after a masking would be exactly the cover-up it was built against.
So that this does not raise a false suspicion, the check report and the audit export tell the two cases apart: if the chain breaks at a masked row, it reads "explained by a later masking" along with the number of masked entries, rather than the warning about tampering. An auditor can see that someone acted deliberately and ask about the case.
Retention period
Up to 14 days works in every edition. Beyond that — 30, 60, 90, 180, 365 days or unlimited — Enterprise is required; the longer entries stay visible in the list but cannot be selected without it. If an installation with a longer period falls back to a smaller licence, entries older than 14 days are no longer shown and no longer exported, but they are not deleted either. Deletion still follows the configured period. The service records the restriction once in the system log.
The difference matters: if the licence lapses for a while, say because nobody pressed "check online" for thirty days, your history stays complete and is back once the licence is reactivated. Losing data must never be a side effect of a licence state.
Sandbox and migration mode
Enterprise feature, from version 1.8.0. Meant for the day hundreds of devices are switched over to SMTPly and nobody knows what they actually send.
While the mode is active, every message goes to a single test mailbox instead of the real recipients. The subject gets a prefix ([SANDBOX] by default), followed by a tag naming the original recipients, for example [SANDBOX] [orig: customer@example.com, accounts@example.com +1] Invoice 4711 (up to three addresses, then a count). A header of its own is not possible, because Microsoft Graph discards any custom X-header on send. Journal BCC is switched off in this mode — otherwise something would still reach another mailbox.
Time window. Start and end can be set separately and both are optional. With neither, the mode applies from switching it on until switching it off; a start delays it, an end releases delivery again on its own. Server local time applies and the end is excluded — a window ending at 08:00 lets a message submitted at 08:00:00 run normally.
Because a mode that is switched on but not currently in effect is the most confusing state of all, both go into the system log: at service start, whether it redirects or not, and for every message outside the window a line stating why. The diagnostics package carries it on the first page.
Without an Enterprise licence, an enabled sandbox mode delivers nothing at all. That sounds harsh and is the only defensible answer: whoever switches the mode on explicitly wants nothing to reach the real recipients. If the redirect simply did not happen for lack of a licence, the mail would go exactly there while the interface reports "active". Affected messages stay in the queue and the reason is in the system log. Remedy: switch the mode off or activate the licence.
If the redirect fails because of an unusable address, the message is not sent. In migration mode, delivery to the real recipients would be the worst possible outcome.
Outbound rules
Enterprise feature, from version 1.8.0. Rules control the delivery path of a message: which tenant it goes through, under which sender address, and whether it is journalled. They never change the content of the message — no boilerplate, no signature, no disclaimer. That is not an omission but a core rule of the product: what the listener accepts goes to Microsoft byte for byte.
A rule consists of conditions (sender address, sender domain, recipient domain, source IP, AUTH user) and actions (tenant, sender address, force or suppress journalling). For recipients, one matching recipient is enough for the rule to apply.
The first matching rule wins; later rules never override a decision already made. The order can be rearranged in the settings. If no rule matches, normal routing by sender domain or AUTH user binding applies.
REST interface
Enterprise feature, from version 1.8.0. Hands out operating state and message history as JSON so you can build your own tools around SMTPly — a dashboard across several servers, a nightly script collecting failures, or a link into a ticket system.
Read-only. No endpoint sends mail or changes the configuration. Whoever gets hold of the access token sees traffic data; they cannot do anything with it.
Set it up under Settings → REST interface: switch it on, choose port and bind address, generate an access token. The token is shown once in clear text and kept encrypted afterwards — write it down right away. Without a token the interface does not start.
| Endpoint | Returns |
|---|---|
GET /api/v1/status | version, edition, uptime, queue, circuit breaker, sandbox state, counters for the current day |
GET /api/v1/messages | mail tracker entries; filters from, to (ISO 8601), status, tenant, limit, offset |
GET /api/v1/messages/{id} | a single entry |
GET /api/v1/stats?days=7 | counters per calendar day |
GET /api/v1/tenants | configured tenants — without secrets |
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8026/api/v1/status
By default the interface listens on 127.0.0.1 only. Exposed to the network, switch TLS on — the Use TLS (HTTPS) checkbox sits right below. It uses the same certificate as the SMTP listener; there is deliberately no second certificate setup, it is the same machine with the same name. Without a usable certificate the interface does not start, and the reason is in the system log.
If TLS stays off while the interface is not restricted to loopback, the service writes a warning at start: sender, recipients and subjects would go over the wire in the clear, with only the token guarding access.
Addresses and subjects can be masked in the responses, by the same rules as the mail tracker. Sensible where the evaluation leaves the house; off by default, because whoever switches the interface on usually wants to evaluate.
The first request per caller appears in the system log, followed by an hourly summary. Failed authentications are always logged individually as warnings.
What data comes out
First things first: there are no mail contents. SMTPly stores neither body nor attachments — the message passes through and is not kept. What the interface hands out is traffic data from the mail tracker.
An entry from /api/v1/messages carries these fields:
| Field | Content |
|---|---|
id, timestamp | Entry ID and time of submission (UTC) |
from, to, subject | Sender, recipients, subject — masked if masking is switched on |
sizeBytes | Message size |
remoteIp, authUser, transport | Source IP of the submitting device, authenticated SMTP user, and transport (plain, STARTTLS, implicit TLS) |
status, attemptCount, durationMs | Delivery state, number of attempts, duration |
errorMessage, graphMessageId | Error message in plain text and the message ID from Microsoft Graph |
tenantId, tenantName | Which tenant the mail went through |
/api/v1/status returns version, edition, start time and uptime, queue length, circuit breaker state, the state of sandbox mode including its time window, the remaining validity of the certificate and of each tenant's client secret, plus today's counters. /api/v1/stats gives the same counters per calendar day along with the volume transferred. /api/v1/tenants lists tenant name, directory ID, application ID, sender address and allowed sender domains.
What never comes out: mail contents and attachments, client secrets, access tokens, SMTP user passwords and the contents of the configuration file. The application ID appears in the tenant data; the matching secret does not.
Examples
Yesterday's failed deliveries, for a nightly script:
$head = @{ Authorization = "Bearer <secret>" }
$from = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
$to = (Get-Date).ToString("yyyy-MM-dd")
$answer = Invoke-RestMethod -Headers $head `
-Uri "https://mailrelay01:8026/api/v1/messages?from=$from&to=$to&status=Failed&limit=200"
$answer.items | Select-Object timestamp, from, to, errorMessage | Format-Table
A traffic light for a dashboard across several servers — including the remaining lifetime of the secrets, since that is what takes an installation out of service silently:
$s = Invoke-RestMethod -Headers $head -Uri "https://mailrelay01:8026/api/v1/status"
if ($s.status -ne "ok") { "RED: delivery paused ($($s.circuitBreaker))" }
elseif ($s.sandbox.activeNow) { "YELLOW: sandbox active — nothing reaches real recipients" }
elseif ($s.expiry.clientSecretDaysMin -lt 14) { "YELLOW: secret expires in $($s.expiry.clientSecretDaysMin) days" }
else { "GREEN: $($s.today.sent) messages today" }
Volume over the last 30 days as CSV, for billing perhaps:
$v = Invoke-RestMethod -Headers $head -Uri "https://mailrelay01:8026/api/v1/stats?days=30"
$v.items | Export-Csv -Path volume.csv -NoTypeInformation -Encoding UTF8
And the shortest way to see whether it works at all:
curl -H "Authorization: Bearer <secret>" https://127.0.0.1:8026/api/v1/status
MCP server
Enterprise, from version 1.8.0. Offers the same read-only queries as the REST interface as tools an AI assistant can call — Claude Desktop, Claude Code and anything else that speaks MCP.
The point is troubleshooting in conversation. "Why did the invoice from the printer not go out yesterday?" answers itself in one sentence instead of someone filtering period, sender and status by hand.
What to weigh before switching it on
What a tool returns goes to a language model — depending on the setup, into the cloud. That means sender, recipients and subjects, never message content: SMTPly does not store any. They are still your organisation's traffic data.
So the MCP server is off by default, masking is on by default — the other way round from the REST interface — and switching it off is recorded as a warning in the system log. Whether that is acceptable for you is your call; SMTPly does not make it for you.
Setting it up
Settings → MCP server: switch on, choose port and bind address, generate an access secret. The secret is shown once in clear text and kept encrypted afterwards. Without a secret the server does not start.
Below it sits the ready-made entry to copy. For Claude Desktop it belongs in claude_desktop_config.json, for Claude Code in .mcp.json in the project folder:
{
"mcpServers": {
"smtply": {
"type": "http",
"url": "https://127.0.0.1:8027/mcp",
"headers": {
"Authorization": "Bearer <secret>"
}
}
}
}
The tools
| Tool | Returns |
|---|---|
smtply_status | version, edition, uptime, queue, circuit breaker, sandbox state, remaining lifetime of secret and certificate, daily counters |
smtply_messages | message history entries; filters by period, status and tenant |
smtply_message | a single entry with the full error message and Graph id |
smtply_stats | counters per calendar day |
smtply_tenants | configured tenants — without secrets |
All five are read-only. There is no tool that sends mail or changes a setting.
What the assistant gets to see
The same data as through the REST interface, in the same fields: time, sender, recipients, subject, size, source IP, authenticated user, transport, status, attempts, duration, error message, Graph ID and tenant. Plus state and daily counters from smtply_status.
Mail contents and attachments are not included — SMTPly does not store them. Neither are client secrets, access tokens or passwords.
With masking switched on — the default — the assistant sees m**l@e*****e.com instead of the address, and the first 15 characters of the subject. Troubleshooting still works: time, status, error message and device stay readable as they are.
Example questions
The point is questions that would otherwise mean filtering work in the dashboard:
- "Which mails failed yesterday, and why?"
- "Is anything stuck in the queue right now, and since when?"
- "Which device on the network sends the most mail — and over which transport?"
- "Show me all deliveries for the Contoso tenant from last week with status Failed."
- "Did the printer in accounting (192.168.10.44) send anything today?"
- "How many mails went out over the last 30 days, and how does that spread across weekdays?"
- "Is a client secret or the certificate about to expire in the next few weeks?"
- "Is sandbox mode active right now?"
- "Compare this week's failure rate with last week's."
Also useful, because it would otherwise be two queries: "Summarise what went wrong today and name the submitting device for each failure."
Trying it yourself
The protocol is JSON-RPC 2.0 over HTTP POST — you can check it without an assistant:
# open the session
curl -s -X POST https://127.0.0.1:8027/mcp \
-H "Authorization: Bearer <secret>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"curl","version":"1"}}}'
# list the tools
curl -s -X POST https://127.0.0.1:8027/mcp \
-H "Authorization: Bearer <secret>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# query the state
curl -s -X POST https://127.0.0.1:8027/mcp \
-H "Authorization: Bearer <secret>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"smtply_status","arguments":{}}}'
On Windows PowerShell the line continuation character is the backtick, not the backslash shown here.
The first call per caller appears in the system log, followed by an hourly summary. Failed authentications are always logged individually as warnings.
Syslog and SIEM forwarding
Enterprise feature, from version 1.8.0. Sends the same content that appears in the system log to a syslog receiver as well — Graylog, Splunk, Wazuh, rsyslog and anything else that speaks RFC 5424. Deliberately no second, separate event list that would drift apart over time.
You can set receiver and port, the transport (UDP, TCP or TCP with TLS), the facility, the application name and the minimum level. A test button sends a single message — needed above all with UDP, where nobody reports back that the messages vanish into nothing.
If forwarding fails, that is not written to the system log. That sounds wrong but is deliberate: an error while forwarding would produce a message whose forwarding fails again — the loop would have no end. Whether the messages arrive is checked at the receiver.
What a message looks like
RFC 5424, as it goes over the wire:
<14>1 2026-08-23T14:07:11.482+02:00 mailrelay01 SMTPly 4812 - - Mail a1f3… delivered to m**l@e*****e.com (tenant "Firma 1", 412 ms)
First the priority from facility and level, then the timestamp with time zone, host name, application name, process id. The application name is configurable — useful when several relays write into the same SIEM.
Setting up the receiver
rsyslog — one file per sender, so SMTPly does not drown in the general syslog:
# /etc/rsyslog.d/10-smtply.conf
module(load="imudp")
input(type="imudp" port="514")
if ($programname == "SMTPly") then {
action(type="omfile" file="/var/log/smtply.log")
stop
}
Graylog: System → Inputs → Syslog UDP, port 514, nothing else needed. The RFC 5424 fields are parsed; search with application_name:SMTPly.
Splunk: create a UDP input on port 514 with sourcetype = syslog. Search:
index=* sourcetype=syslog "SMTPly" | search "rejected" OR "failed"
Wazuh: the manager reads syslog through <remote> with connection=syslog; SMTPly needs nothing special for it.
What to expect
With UDP nobody reports back that the messages vanish into nothing — hence the test button in the settings. It sends a single message; whether it arrives is only visible at the receiver.
If forwarding fails, SMTPly does not write that to the system log. That sounds wrong and is deliberate: an error while forwarding would produce a message whose forwarding fails again — the loop would have no end.
The privacy settings still apply: what is masked in the system log leaves masked.
Creating SMTP users from CSV
Business and Enterprise feature, from version 1.8.0. Nobody clicks through hundreds of device accounts one at a time.
Settings → SMTP authentication → Import users. A sample template sits right next to it ("Save template"). Comma, semicolon and tab are recognised as separators, as are German and English column headings. Before anything is applied, SMTPly shows what it read and names every unusable row — with line number and reason.
Automatic updates
From version 1.8.2, all editions, off by default. Once enabled, the service installs a new version on its own as soon as the next check falls inside your maintenance window.
The service rather than the interface, because unattended means nobody is logged in. The one-click update in the interface is still there; anyone who uses it does not need this feature.
Why this only exists now
A service that downloads files from the network and runs them behaves like malware, regardless of how well it means it. Two things made the approach defensible: all binaries have been signed since version 1.7.18, and the updater verifies that signature since 1.8.2.
Both checks run, and both before anything is executed:
- the SHA-256 checksum against the manifest on smtply.app
- the Authenticode signature against the publisher
IT-Beratung - Andreas Hähnel
Neither check is sufficient on its own. The checksum sits on the same site as the link to the file: whoever controls the site supplies both to match. The signature in turn proves the publisher, not the version, so on its own it would allow an older but genuinely signed build to be slipped in. If either check fails, the file is deleted and the reason is written to the system log.
The maintenance window
Start and end in local server time, optionally restricted to certain weekdays. No weekday ticked means every day.
- If the end is earlier than the start, the window spans midnight.
- The weekday is the day the window starts on: "Sunday, 22:00 to 04:00" is the night from Sunday to Monday, not two stumps on two different Sundays.
- The end itself no longer counts. A window until 04:00 ends with the last second before it.
The service checks every ten minutes. Anything finer is pointless, a maintenance window usually lasts hours.
What happens during the install
The installer stops the service, replaces the files and starts it again. During that time the SMTP listener accepts nothing; devices trying to deliver get a connection error and usually retry by themselves. Messages already queued are not lost, they continue after the restart.
Only switch this on if a short interruption inside that window is acceptable. If you deliver around the clock, leave it off and update by hand.
A version that could not be installed is not downloaded again in every window. Only a newer one triggers another attempt. Without that brake, a server with a persistent problem would pull around a hundred megabytes every night and fail the same way every night.
Where to look
- Settings → Updates: time and result of the last automatic attempt
- System log: every attempt with its reason, failures as errors
- Monitoring endpoint and REST API: under
autoUpdate, plus the metricssmtply_auto_update_enabledandsmtply_auto_update_in_window. An interruption at three in the morning explains itself instead of looking like an outage. - Diagnostics bundle: under Updates
Two cases where nothing happens
Both look the same in operation, namely like "nothing is going on". That is why both report themselves, in the system log and in the diagnostics bundle:
- The maintenance window cannot be parsed, after a typo for instance. The warning appears once, not again every ten minutes.
- Automatic installation is on but "check for updates automatically" is off. The service then never learns about a new version.
Waiting period before installing
Since version 1.8.7, all editions. Under Settings → Updates you can have the service install a new version only once 7 or 14 days have passed since its release (default: immediately). The count starts at the release time on GitHub, not when this server first sees the version. That gives other customers time to report a problem before it reaches your server.
If the release time cannot be determined, for example because the server cannot reach GitHub, the service does not install rather than skip the waiting period. The reason is written to the system log.
When an update is available, the navigation shows a green “1” next to About.
Recipient restrictions per source IP
Since version 1.8.7, in all editions, off by default. Defines which recipients a particular device may send to. Meant for devices that by their nature never need arbitrary recipients, such as an alarm panel that only notifies your own company. If such a device is ever compromised or misconfigured, it cannot become an open relay to the outside world.
Settings → Senders and recipients, per SMTP listener. A rule consists of:
- Source IP or CIDR network, for example
192.168.1.5or192.168.1.0/24(IPv4 and IPv6) - Allowed recipients, one per line: a full address such as
alarm@example.comor@example.comfor the whole domain
How it is checked:
- Both the envelope recipients (
RCPT TO) and the recipients in theTo:,Cc:andBcc:headers are checked. Microsoft 365 delivers to the header recipients; up to version 1.8.8 only the envelope was checked. A device that writes different recipients into the headers than into the envelope is rejected since 1.8.9. - A source IP without a rule stays unrestricted. The feature only narrows down the devices you create a rule for.
- If even one recipient matches none of the allowed entries, SMTPly rejects the whole message with SMTP 550. Individual recipients are not stripped, because SMTPly never alters messages.
- The rejection is recorded in the system log and the mail tracker. The self-test warns if a rule is enabled but has no allowed recipients.
Deliberately not tied to an edition: this is security hardening, and installations with many unmaintained legacy devices need it most.
Allow and block lists
In all editions and all switched off as shipped: the three block lists since version 1.8.8, the allow lists for senders and recipients since 1.8.9. Existing installations behave exactly as before after the update. Allowing already existed: allowed source IPs, allowed sender domains per tenant and the recipient restriction. The block lists exclude individual devices, senders or recipients inside an allowed range; the allow lists let only specific addresses through.
| List | Where | What is checked |
|---|---|---|
| Blocked source IPs | Settings → SMTP listener, directly below the allowed source IPs | The address the connection comes from. Single addresses and networks, IPv4 and IPv6. Always wins, against the allow list and in open mode. |
| Blocked senders | Settings → Senders and recipients, its own collapsible group | Envelope (MAIL FROM) and the From: line. |
| Blocked recipients | same place | Envelope recipients plus To:, Cc: and Bcc:, because Microsoft 365 delivers to the header recipients. |
| Allowed senders | same place | Envelope and From: line must both match. An empty envelope sender is not counted. |
| Allowed recipients | same place | Every recipient from the envelope, To:, Cc: and Bcc: must match. |
Enter senders and recipients as a full address (mail@example.com) or as a domain (@example.com). A domain matches exactly that domain, not its subdomains. If an entry matches, SMTPly rejects the whole mail with SMTP 550 (source IP blocked, sender blocked or recipient blocked). Individual recipients are not removed, because SMTPly does not alter messages.
Allow lists for senders and recipients
The allowed sender domains of a tenant cover a whole domain. To accept only specific senders, for example only scanner@example.com and @erp.example.com, switch on the sender allow list under Settings → Senders and recipients. Likewise, the recipient allow list only lets mail through to the listed addresses or domains, for example only to your own company, from any source IP. For individual devices, the recipient restriction per source IP remains available as well.
- The allow lists apply in addition to the allowed sender domains of the tenants, and also to devices with SMTP sign-in.
- The block lists are checked first: a block always wins, even against an allow entry.
- Mail that is not allowed is rejected with SMTP 550 (sender not allowed or recipient not allowed); for recipients, the whole mail as soon as one does not match.
- An allow list that is switched on but empty lets no mail through at all. SMTPly therefore asks before saving, and the self-test reports it as an error.
- The test mail from the settings also goes through the listener. Add its sender and recipient address, otherwise it is rejected too.
A rejected mail is neither accepted nor stored nor passed on to Microsoft 365. The sending device learns about the rejection directly in the SMTP session, with the reason. Whether that becomes a non-delivery report or just an entry in the device log is up to the device. In the Maillog the mail counts under errors, in the usage report as rejected.
Notification by mail (optional)
Since 1.8.8, Settings → Email notifications has two further events, both off by default:
- Warning mail when a mail is rejected to the notification address, with reason, source IP, sender and recipient. At most one mail per 15 minutes, further cases are counted.
- Inform the sender about a rejection: a short mail with the reason to the sender. Deliberately narrow, because sender addresses can be forged and a reply to outside addresses would go out in your name: only for senders whose domain is allowed for a tenant, never for rejections because of the source IP, never for blocked or non-allowed senders, never for the recipient restriction per source IP, never for unreadable mail, never for a temporary rate limit, at most once per hour per sender and at most 20 notices per hour in total. The notice states the reason and time, but neither the subject nor the recipients of the rejected mail. Since 1.8.10 it is always in English, because SMTPly does not know the sender's language, and the "once per hour" limit survives a service restart.
Since 1.8.10 all notification mails to the administrator (warnings, expiry notices, license, status report, backup) use the interface language set in SMTPly; with "Automatic", the Windows language of the server.
Straight from the mail tracker
In the detail view of an entry, sender, recipient and source IP are clickable. The menu shows the state at the top and below it only what fits:
- Block: for sender and recipient either the address or the entire domain, for several recipients per recipient. The matching block list is switched on. Addresses of the server itself cannot be blocked, as that would hit the test mail, the self-test and local applications.
- Lift block: for a blocked value; all entries matching it are removed.
- Allow: for a source IP that is not allowed (it is added to the allow list), for a sender whose domain is not assigned to any tenant (the domain is allowed for the primary tenant), and, with the sender or recipient allow list switched on, either the address or the entire domain. The mail tracker never switches on an allow list that is off, as that would lock out every other sender at once.
Blocked values are red and values that are not allowed are yellow, in future entries and in the table as well. Allowed values stay uncoloured, as that would be almost every row.
Every action asks first, saves the settings and restarts the service so it takes effect immediately. The rejected mail itself is not delivered afterwards; the device has to send it again. If the settings still contain unsaved changes, SMTPly only adds the value and asks you to save, instead of saving other changes unasked. The links require administrator rights.
Where it shows
Every rejection appears in the system log and in the mail tracker. When the listener starts, the log states which lists are active, and an empty allow list as a warning. The diagnostics package lists all of them, and the self-test warns about a block list that is switched on but empty, about an IP block that hits the server itself, and about a sender allow list without the primary tenant’s sender address. Backup and restore carry the lists like any other setting.
IPv4 and IPv6
Both address families are equal, on the SMTP listener as well as on the HTTP interfaces. Whatever you enter as the bind address is interpreted the same way everywhere:
| Input | Result |
|---|---|
0.0.0.0, ::, *, + | all interfaces, both address families |
localhost or empty | both loopbacks, that is 127.0.0.1 and ::1 |
127.0.0.1 | IPv4 only |
::1, 2001:db8::5 | IPv6 only |
[2001:db8::5], fe80::1%12 | brackets and zone identifiers are stripped |
For "all interfaces" the SMTP listener opens one socket per address family on the same port. That is not a detour but a necessity: a socket on 0.0.0.0 accepts IPv4 only, one on :: IPv6 only.
The allow-list understands IPv6 too, as a single address and as a CIDR network, mixed with IPv4 rules in the same list. A rule with a typo is reported straight away instead of being skipped silently. Rate limiting counts IPv6 on the /64, not per address: a host is usually given an entire /64 there and rotates its address inside it on its own, so a per-address limit would achieve nothing.
Where to check which families are actually being served:
- Self-test: line IPv4 / IPv6, with a warning if the machine has IPv6 but the listener serves IPv4 only
- Diagnostics bundle: under SMTP listener, including a note when the two do not match
- System log: every accepted mail records the family next to the source IP
- Maillog: column IP version (off by default) and a marker in the detail view, both searchable and filterable
An IPv4-mapped IPv6 address (::ffff:…) counts as IPv4, because the peer spoke IPv4.
Limits
| Limit | Value | Source |
|---|---|---|
| Max message size | 25 MB (SMTPly default) — configurable up to 150 MB | Graph /sendMail |
| Max recipients per mail | 500 (Exchange Online) | Microsoft |
| Mails per day / mailbox | 10,000 (Exchange Online) | Microsoft |
| API rate limit | 10,000 requests / 10 min per app / tenant | Graph throttling |
| Licensed tenants | 1 (Starter), up to 5 Business, up to 25 Enterprise | SMTPly license |
SMTPly honors all Graph throttling hints — when Microsoft sends a Retry-After header, the queue automatically respects it and waits accordingly.
Licensing
SMTPly is offered as a one-time purchase with a per-server bound license. Activation takes place online via the Polar license system.
- 14-day trial — the full Enterprise feature set, no limits, starts automatically on first launch. Whoever evaluates should be able to see everything there is to buy.
- Activation — paste license key into GUI → "License" and confirm.
- Hardware binding — fingerprint from several independent characteristics of the machine, chiefly the SMBIOS UUID. On virtual machines that UUID belongs to the VM itself and travels with it during live migration: moving between failover cluster nodes, a replaced network card or a restore from backup do not invalidate the licence. Moving to a different server still requires prior deactivation.
- Offline grace — after successful activation, SMTPly runs up to 30 days without an internet check.
- Automatic online check — since version 1.8.6 the service checks the license online once a day by itself, whether or not anyone opens the GUI. If outbound network access is restricted, that call needs to be allowed, or the 30-day offline grace kicks in.
- Deactivation — GUI → "License" → "Deactivate". Afterwards the key can be activated elsewhere.
- Early warning (since 1.8.7, Business and Enterprise): when the last successful online check approaches the 30-day limit, SMTPly warns 14, 7, 3 and 1 day before, by email and webhook (
license.revalidation_due). Enable it under Settings → Email notifications. - Without a valid license (since 1.8.9) delivery pauses: devices receive a temporary rejection (
451) and retry later, queued mail stays in the queue and goes out after activation. SMTPly's own notices to the administrator keep going out. This applies immediately after the trial has ended and when Polar has revoked a license. If a paid license just cannot be confirmed at the moment (30 days without an online check, hardware not recognised after a migration), SMTPly keeps running for another 14 days with all features of the purchased edition (Business stays Business, Enterprise stays Enterprise); during that time SMTPly warns daily by email, in the system log, in the self-test and in the app. Before the trial ends, SMTPly sends a reminder three days and one day before. - Recovery (since 1.8.7): if the license is only unconfirmed because no online check succeeded for 30 days, the License page shows a Re-verify online button. One successful check fully restores the license, without activating again. Next to it is a link to the Polar customer portal.
Activation, deactivation, and every online check (manual or automatic) talk to exactly one endpoint:
| Host | api.polar.sh |
| Port | 443 (HTTPS) |
| Protocol | TLS/HTTPS only, no plaintext HTTP |
| Direction | outbound from the server running SMTPly |
No other host is needed for the license check. The one-click update mechanism additionally checks smtply.app (also port 443), but that is a separate process, independent of licensing.
Concurrent session limits
Since version 1.8.2 SMTPly limits the number of concurrently open SMTP sessions, in total and per source IP. This guards against a single client that opens a connection and then keeps pushing data endlessly without ending the message. Such a client binds memory in the service, and without a cap that adds up across many connections.
| Concurrent sessions total | 50 (default), 0 = unlimited |
| Concurrent sessions per source IP | 5 (default), 0 = unlimited |
| Maximum session duration | 5 minutes (was 10) |
Normal operation never hits these: printers, ERP systems and monitoring deliver serially over one or a few connections, not over dozens in parallel. When a further session is rejected it is recorded in the system log; surplus connections are closed, existing ones continue unaffected. The three values are special-case settings without their own UI and can be changed by hand in the configuration if needed. The state also appears in the diagnostics bundle under SMTP listener.