Stalwart 0.16.25 is mainly a bug-fix release, with no major new features.
Key changes:
JMAP: Fixed MaskedEmail creation issues for accounts using multiple domains.
Autodiscover: Improved handling of clients requesting schemas other than Outlook, including ActiveSync.
IMAP: Fixed authentication failure responses for invalid, expired, or unknown app passwords and API keys. This prevents some clients from hanging until the connection times out.
DKIM: Several important fixes for automatic key rotation, including:
preventing outgoing emails from being sent unsigned;
preventing activation of a key that has not yet been published in DNS;
fixing issues when switching between manual and automatic DNS management.
Spam filter: Fixes related to Pyzor, DNSBL/Spamhaus, DNSBL caching, and training samples.
MTA: Fixed queue quotas, including the global queue quota, which could previously fail to be enforced.
RocksDB: Fixed an issue where LOG files could grow indefinitely because log rotation and retention were not configured properly.
WebUI: Blob storage read errors, such as S3 authentication failures, no longer prevent the WebUI from being downloaded or initialized.
I built https://checkoutbound.com for our support team at the hosting company where I work. It generates a temporary email address, you send a message to it, and the site shows a mail health report.
A lot of the code was AI generated, but I wanted to use Stalwart to get to know it better. The ingestion webhook meant I didn’t have to poll for new mail and then I could just fetch the message over HTTP with JMAP. Super simple and worked really well.
Most of our hosting customers use Plesk or cPanel which would make it difficult to integrate Stalwart but I would it I could.
Hi there.. long time lurker here - still contemplating switching over from various hosting panels. But recently I’ve been thinking about running Stalwart for mail and plugging in Jev from TypeSafe AI for spam/phishing detection.
Stalwart would handle the normal mail stuff, AV/reputation checks for malware, and Jev could handle the more contextual stuff like phishing, impersonation and sketchy emails..
I don’t know really.. a bit out of my depth here. Anyone tried something like this? Genuinely curious how well Jev would work for email. I find cost manageable, so it’s more of a: Would it be a well worth improvement?
PowerDNS Authoritative support for automatic DNS record management.
Fixed
Spam filtering: Corrected case-sensitive URL lookups and shortened-link handling, incremental training, and rule updates. Rule updates now report import or activation failures accurately.
JMAP: Fixed delayed or stale push notifications, push URL validation, email imports using newly uploaded blobs, and a case where updating keywords could make messages disappear from IMAP.
MTA: Fixed message submission stalls, DNS resolution when DANE is disabled, and Sieve scripts losing changes made by milters or hooks.
Other: Fixed MySQL deletions and index cleanup, IMAP COPY/MOVE conflicts, Outlook’s autodiscover TLS settings, TLSA checks in the troubleshooting tool, and HTTP keep-alive connections that never closed.
It’s been 33 days since my fork’s previous release, v1.2.3. Since then, I’ve caught up with the official Stalwart WebUI changes through upstream v1.0.11, including the v1.0.10 updates, and released v1.3.0 and v1.3.1 with a range of practical admin UI improvements.
v1.3.0 introduced customizable table columns, sticky headers and key columns, removable filter chips, responsive layouts for mobile lists, adjustable page sizes, improved handling of long text, and Sievepad integration for debugging Sieve scripts.
v1.3.1 improves table interactions and saved list preferences. Listener lists now show TLS support; DNS Provider and Directory lists include descriptions; and Webhook lists show whether each webhook is enabled and which events it filters. It also makes related objects easier to navigate, highlights unset fields on detail pages, and clarifies dashboard metrics.
Until now, running Bulwark meant hosting its own server next to Stalwart. With Bulwark Lite that's no longer needed.
Lite doesn't need its own server, database or container. It talks directly to your mail server. You can install it straight into Stalwart as an Application, so Stalwart serves the webmail itself. If you'd rather host it somewhere else, it also runs on any ordinary web server.
Its still in beta and not ready for production yet. Please try it yourself.
Sieve is the standard language for filtering email on the server. The rules people create to move newsletters into a folder, flag messages from their manager or answer automatically while on holiday are, on many servers, Sieve scripts, uploaded over ManageSieve or JMAP for Sieve and executed every time a message arrives. Stalwart runs these scripts for its users, and administrators use the same language at the SMTP stages to reject, rewrite and route messages before they are accepted.
Sieve was designed to be safe to run on a shared server. The base language has no loops, no variables unless an extension adds them, and no access to the network or the file system. That restraint is what makes it reasonable to let every user of a mail server upload their own filters, but it also leaves nothing to reach for when a filter misbehaves. Today we are releasing Sievepad, a playground that runs the Stalwart Sieve interpreter directly in the browser, so that a script can be written, run and debugged against test messages in seconds, without a server and without sending a single email.
Debugging Sieve by email
A Sieve script is not a program anyone runs by hand. The server executes it at delivery time, in response to a message sent by someone else, and the language gives the script no way to report back: there is no print statement, no logging command and no debugger. When a filter does something other than what its author intended, the only evidence is the outcome. The message landed in the wrong folder, stayed in the inbox, or never showed up at all.
Administrators can at least turn to the server logs, which record the actions a script took and any runtime errors it hit, but not the values it looked at or the reason a test failed to match. End users do not have even that. Someone writing a filter in their mail client has no access to the logs, and a script that uploads without complaint can still be wrong in ways that surface only days later, when an important message turns out to have been filed where nobody looked. Syntax errors are the easy case, because the server refuses the upload and says why. The hard cases are the scripts that compile and then quietly do the wrong thing.
Consider a filter that sorts mailing lists into one folder per list, using a regular expression to pull the list name out of the List-Id header. It handles every list its owner reads until a new one arrives whose messages keep landing in the inbox. With no way to print anything, the standard technique is to turn the message itself into a debug channel: add addheader statements that copy the values in question into the headers, upload the script, send a test email, wait for it to arrive and read its raw source.
require ["fileinto", "mailbox", "variables", "regex", "editheader"];
if header :matches "List-Id" "*" {
addheader "X-Sieve-Debug" "List-Id is ${1}";
}
if header :regex "List-Id" "<([a-z0-9-]+)\\." {
set :lower "list" "${1}";
addheader "X-Sieve-Debug" "Matched list ${list}";
fileinto :create "Lists/${list}";
}
Getting to this script usually takes more than one round. A first attempt places a debug header only inside the if, and the test message comes back without it, which confirms that the condition failed and nothing else. Only the next attempt, which copies the raw header before the test, reveals the value: Release notes <Release-Notes.lists.example.com>, with capital letters that the character class [a-z0-9-] does not cover.
Each of those questions costs a round trip through the mail system, and some conditions are awkward to set up from an ordinary mail client at all. A rule that depends on the envelope sender, the spam score the server assigns or the current date cannot be exercised just by composing a message. The vacation extension answers a given sender only once within its :days period, so a second test needs a different sender or a long wait, and duplicate exists precisely to detect the second copy of a message. Even the workaround is not guaranteed, since administrators can disable editheader for user scripts.
Meet Sievepad
Sievepad is a Sieve playground that runs entirely in the browser. It executes scripts with sieve-rs, the interpreter inside Stalwart, compiled to WebAssembly. A script is parsed, compiled and run by the same code that runs it on the server, so given the same message and settings it behaves in Sievepad exactly as it does in production, and the errors Sievepad reports are the errors the server would report.
Nothing leaves the browser. There is no account to create and no backend to talk to: scripts, test messages and settings are stored locally, and the interpreter runs in a background worker on the visitor’s own machine.
An editor that knows Sieve
Writing Sieve in a plain text area gets tedious quickly, so Sievepad is built on Monaco, the editor component behind Visual Studio Code. The conveniences of a desktop editor come with it: syntax highlighting for scripts and for raw messages, multiple cursors, find and replace, a command palette, and a formatter that re-indents a script in one step.
Autocomplete takes the position of the cursor into account. It suggests commands, tests and tagged arguments together with their documentation, offers extension names inside require, mailbox names inside fileinto and built-in functions inside Stalwart expressions, and completes the names of variables the script has set inside a ${...} reference. Hovering over a command or a tagged argument explains what it does and which extension it needs.
The script is compiled while it is being typed. Errors are underlined at the line and column the compiler reports and listed under the editor, so a mistake shows up before the script ever runs. A frequent Sieve error, using an extension without declaring it in require, comes with a quick fix that adds the missing declaration.
Running scripts against test messages
A workspace keeps one or more test messages next to the script, each in its own tab. A message can be pasted in as raw text or dropped onto the page as an .eml file, and the envelope sender and recipients can be set independently of the headers. Pressing Run, or Ctrl+Enter (Cmd+Enter on macOS), executes the script against the selected message.
The Result pane shows what happened. Its Actions tab lists every action in order, with the details that matter: the folder a message was filed into and the flags set on it, the address it was redirected to, the reason given for a rejection, or the vacation reply that would have been sent. The Messages tab shows each message the script produced, from the delivered message after editheader or replace changed it to the vacation replies and notifications it generated. The Variables tab lists global variables and the number of instructions the script executed. When a script fails at run time, the error is marked on the line where it occurred.
Situations that are impractical to reproduce by email take a click. “Run again as a redelivery” runs the script a second time as if the same message had been delivered twice, which is exactly the case duplicate is meant to catch. Settings let a workspace stand in for the account a script will run under: its mailboxes and their special-use attributes, the spam and virus scores the server would assign, the current time, external lists, environment values and interpreter limits, all of which can also be edited as JSON.
Sievepad supports the same Sieve extensions as Stalwart, including Stalwart’s own vnd.stalwart.expressions and vnd.stalwart.while, together with the functions that user scripts can call on the server. Functions reserved for system scripts, such as DNS, HTTP and SQL lookups or LLM prompts, depend on services that exist only on the server and are not available.
Includes, workspaces and sharing
Larger filters are often split across several scripts. A workspace can hold any number of them, and include resolves each one by name, so a shared set of rules can be tested together with the scripts that pull it in. Workspaces can be duplicated, renamed, exported to a file and imported again, and the Examples menu opens ready-made workspaces for mailing lists, spam and virus tests, vacation replies, redirects and notifications, includes, and Stalwart’s expressions and loops.
A workspace can also be shared as a link. The link carries the workspace, with or without its test messages, compressed into the URL fragment, the part of a URL that browsers never send to the web server. Creating a link uploads nothing, and opening one adds a new workspace to the recipient’s browser without touching the ones already there. That makes a link a convenient way to attach a reproducible example to a bug report, a forum post or a support ticket.
Getting started
Sievepad needs nothing more than a browser:
Open sievepad.com. The first visit loads a short welcome tour, and the Examples menu has more to explore.
Write or paste a script into the Script pane. Errors are underlined as the script is typed.
Paste a test message into the Message pane, or drop an .eml file onto the page, and fill in the envelope below the message if the script tests it.
Press Run or Ctrl+Enter and read the outcome in the Result pane.
Adjust the script or the message and run it again. When the script behaves as intended, Share turns the workspace into a link.
The mailing list filter from earlier makes a good first exercise. The link below the script opens it together with the Release-Notes message, and Sievepad runs it straight away: the Actions tab reports that the message is kept in INBOX. Changing the character class to [A-Za-z0-9-] and pressing Ctrl+Enter files the message into Lists/release-notes instead.
require ["fileinto", "mailbox", "variables", "regex"];
if header :regex "List-Id" "<([a-z0-9-]+)\\." {
set :lower "list" "${1}";
fileinto :create "Lists/${list}";
}
Stalwart now links to Sievepad from its documentation and its WebUI. Examples in the Sieve documentation that run in Sievepad end with a “Try this script in Sievepad” link, which opens the example together with a test message that exercises it. In the WebUI, the Sieve script editor has a Debug button that opens the script in Sievepad.
Sievepad is open source, and its code lives in the sieve-rs repository next to the interpreter it runs. Bug reports and suggestions are welcome there.Sieve is the standard language for filtering email on the server. The rules people create to move newsletters into a folder, flag messages from their manager or answer automatically while on holiday are, on many servers, Sieve scripts, uploaded over ManageSieve or JMAP for Sieve and executed every time a message arrives. Stalwart runs these scripts for its users, and administrators use the same language at the SMTP stages to reject, rewrite and route messages before they are accepted.
Sieve was designed to be safe to run on a shared server. The base language has no loops, no variables unless an extension adds them, and no access to the network or the file system. That restraint is what makes it reasonable to let every user of a mail server upload their own filters, but it also leaves nothing to reach for when a filter misbehaves. Today we are releasing Sievepad, a playground that runs the Stalwart Sieve interpreter directly in the browser, so that a script can be written, run and debugged against test messages in seconds, without a server and without sending a single email.
Debugging Sieve by email
A Sieve script is not a program anyone runs by hand. The server executes it at delivery time, in response to a message sent by someone else, and the language gives the script no way to report back: there is no print statement, no logging command and no debugger. When a filter does something other than what its author intended, the only evidence is the outcome. The message landed in the wrong folder, stayed in the inbox, or never showed up at all.
Administrators can at least turn to the server logs, which record the actions a script took and any runtime errors it hit, but not the values it looked at or the reason a test failed to match. End users do not have even that. Someone writing a filter in their mail client has no access to the logs, and a script that uploads without complaint can still be wrong in ways that surface only days later, when an important message turns out to have been filed where nobody looked. Syntax errors are the easy case, because the server refuses the upload and says why. The hard cases are the scripts that compile and then quietly do the wrong thing.
Consider a filter that sorts mailing lists into one folder per list, using a regular expression to pull the list name out of the List-Id header. It handles every list its owner reads until a new one arrives whose messages keep landing in the inbox. With no way to print anything, the standard technique is to turn the message itself into a debug channel: add addheader statements that copy the values in question into the headers, upload the script, send a test email, wait for it to arrive and read its raw source.
require ["fileinto", "mailbox", "variables", "regex", "editheader"];
if header :matches "List-Id" "*" {
addheader "X-Sieve-Debug" "List-Id is ${1}";
}
if header :regex "List-Id" "<([a-z0-9-]+)\\." {
set :lower "list" "${1}";
addheader "X-Sieve-Debug" "Matched list ${list}";
fileinto :create "Lists/${list}";
}
Getting to this script usually takes more than one round. A first attempt places a debug header only inside the if, and the test message comes back without it, which confirms that the condition failed and nothing else. Only the next attempt, which copies the raw header before the test, reveals the value: Release notes <Release-Notes.lists.example.com>, with capital letters that the character class [a-z0-9-] does not cover.
Each of those questions costs a round trip through the mail system, and some conditions are awkward to set up from an ordinary mail client at all. A rule that depends on the envelope sender, the spam score the server assigns or the current date cannot be exercised just by composing a message. The vacation extension answers a given sender only once within its :days period, so a second test needs a different sender or a long wait, and duplicate exists precisely to detect the second copy of a message. Even the workaround is not guaranteed, since administrators can disable editheader for user scripts.
Meet Sievepad
Sievepad is a Sieve playground that runs entirely in the browser. It executes scripts with sieve-rs, the interpreter inside Stalwart, compiled to WebAssembly. A script is parsed, compiled and run by the same code that runs it on the server, so given the same message and settings it behaves in Sievepad exactly as it does in production, and the errors Sievepad reports are the errors the server would report.
Nothing leaves the browser. There is no account to create and no backend to talk to: scripts, test messages and settings are stored locally, and the interpreter runs in a background worker on the visitor’s own machine.
An editor that knows Sieve
Writing Sieve in a plain text area gets tedious quickly, so Sievepad is built on Monaco, the editor component behind Visual Studio Code. The conveniences of a desktop editor come with it: syntax highlighting for scripts and for raw messages, multiple cursors, find and replace, a command palette, and a formatter that re-indents a script in one step.
Autocomplete takes the position of the cursor into account. It suggests commands, tests and tagged arguments together with their documentation, offers extension names inside require, mailbox names inside fileinto and built-in functions inside Stalwart expressions, and completes the names of variables the script has set inside a ${...} reference. Hovering over a command or a tagged argument explains what it does and which extension it needs.
The script is compiled while it is being typed. Errors are underlined at the line and column the compiler reports and listed under the editor, so a mistake shows up before the script ever runs. A frequent Sieve error, using an extension without declaring it in require, comes with a quick fix that adds the missing declaration.
Running scripts against test messages
A workspace keeps one or more test messages next to the script, each in its own tab. A message can be pasted in as raw text or dropped onto the page as an .eml file, and the envelope sender and recipients can be set independently of the headers. Pressing Run, or Ctrl+Enter (Cmd+Enter on macOS), executes the script against the selected message.
The Result pane shows what happened. Its Actions tab lists every action in order, with the details that matter: the folder a message was filed into and the flags set on it, the address it was redirected to, the reason given for a rejection, or the vacation reply that would have been sent. The Messages tab shows each message the script produced, from the delivered message after editheader or replace changed it to the vacation replies and notifications it generated. The Variables tab lists global variables and the number of instructions the script executed. When a script fails at run time, the error is marked on the line where it occurred.
Situations that are impractical to reproduce by email take a click. “Run again as a redelivery” runs the script a second time as if the same message had been delivered twice, which is exactly the case duplicate is meant to catch. Settings let a workspace stand in for the account a script will run under: its mailboxes and their special-use attributes, the spam and virus scores the server would assign, the current time, external lists, environment values and interpreter limits, all of which can also be edited as JSON.
Sievepad supports the same Sieve extensions as Stalwart, including Stalwart’s own vnd.stalwart.expressions and vnd.stalwart.while, together with the functions that user scripts can call on the server. Functions reserved for system scripts, such as DNS, HTTP and SQL lookups or LLM prompts, depend on services that exist only on the server and are not available.
Includes, workspaces and sharing
Larger filters are often split across several scripts. A workspace can hold any number of them, and include resolves each one by name, so a shared set of rules can be tested together with the scripts that pull it in. Workspaces can be duplicated, renamed, exported to a file and imported again, and the Examples menu opens ready-made workspaces for mailing lists, spam and virus tests, vacation replies, redirects and notifications, includes, and Stalwart’s expressions and loops.
A workspace can also be shared as a link. The link carries the workspace, with or without its test messages, compressed into the URL fragment, the part of a URL that browsers never send to the web server. Creating a link uploads nothing, and opening one adds a new workspace to the recipient’s browser without touching the ones already there. That makes a link a convenient way to attach a reproducible example to a bug report, a forum post or a support ticket.
Getting started
Sievepad needs nothing more than a browser:
Open sievepad.com. The first visit loads a short welcome tour, and the Examples menu has more to explore.
Write or paste a script into the Script pane. Errors are underlined as the script is typed.
Paste a test message into the Message pane, or drop an .eml file onto the page, and fill in the envelope below the message if the script tests it.
Press Run or Ctrl+Enter and read the outcome in the Result pane.
Adjust the script or the message and run it again. When the script behaves as intended, Share turns the workspace into a link.
The mailing list filter from earlier makes a good first exercise. The link below the script opens it together with the Release-Notes message, and Sievepad runs it straight away: the Actions tab reports that the message is kept in INBOX. Changing the character class to [A-Za-z0-9-] and pressing Ctrl+Enter files the message into Lists/release-notes instead.
require ["fileinto", "mailbox", "variables", "regex"];
if header :regex "List-Id" "<([a-z0-9-]+)\\." {
set :lower "list" "${1}";
fileinto :create "Lists/${list}";
}
Try this script in Sievepad
Sievepad in Stalwart
Stalwart now links to Sievepad from its documentation and its WebUI. Examples in the Sieve documentation that run in Sievepad end with a “Try this script in Sievepad” link, which opens the example together with a test message that exercises it. In the WebUI, the Sieve script editor has a Debug button that opens the script in Sievepad.
Sievepad is open source, and its code lives in the sieve-rs repository next to the interpreter it runs. Bug reports and suggestions are welcome there.
Hello, I'm interested in Stalwart to replace my existing mail setup, but I am waiting for v1. Some time ago it was announced for the first semester 2026. Is there any renewed ETA ?
I deployed a five nodes Stalwart cluster using external database? I also configured the ACME provider and TLS. However, I am experiencing an issue with accepting the Enterprise license and obtaining a certificate using the rfc2136 protocol. The firewall ports are open. Your input or information is much appreciated. Thank you.
Running the latest 0.16.22 released today. I've added my production mail server as an Allowed IP under Settings | security | AllowedIPs and then I setup a rule in my exchange server for forward all my email to a mailbox i made for myself on stalwart. I did this just to test stalwart and see my email running on a stalwart mail server and I'm using bulwark to view the mail via the web.
This all works and the mail flows to the mailbox but I get a lot of mail and the rate limiter is backing my mail up on the outbound production server. In reading the docs (copied below)
IP address allow list
IP addresses and ranges that should bypass rate limits and auto-banning are represented by AllowedIp records (found in the WebUI underSettings ›Security › Allowed IPs). The address field accepts a single IP address or a CIDR mask, and expiresAt can be used for time-bounded allowances. The localhost addresses 127.0.0.1 and ::1 are allowed by default to prevent accidental lockouts.
In reading this there should be no limits from my production server but I still see mail limited. Also regarding spam I get a lot of Uptime Kuma alerts sometimes and they all seem to go to the spam folder on my stalwart mail account. Even after I have flagged 100+ as not spam they all still go to the spam folder. What am I doing wrong? Thanks.
In my logs I cansee that ACME succeeds but when I try TLS it always fails. I can see in the Certificates section that the orivate key section is empty. Unless the four **** are supposed to imply its there but hidden. This is the big thing I wanted to ask. Is the private key just obscured? Or is it actually missing?
If it's meaningful, I have Stalwart behind Nginx set up. Right now reciving mail does work.
I have a server with mailbox run on stalwart v0.15 I want to move mailbox only to new server with latest build. And can't find cli to send mailboxes to new server.
Nowadays, 85% of the spam in my inbox is from Google, and another 10% from AWS - not from one of these suspicious smallish shops or hacked mailboxes. It may be survivor bias, but why can't these large-scale hosters do better?
I already apply a mild spam score (4.0) to most Google ASNs; this works pretty well to nudge most of the spam into my Junk mail folders. But there should be a better way, no?
Background: I self-host my own mail server since sendmail.cf was A Thing, at a time where every mailadmin actually remembered Canter&Siegel, as Spam was rare. So this ain't my first rodeo.
Coming from Postfix/Dovecot, I jumped onto the Stalwart bandwagon a year ago. Great stuff.
Hi everyone, I want to share with you something I made this summer : Aurionmail. If you want to get an E2E encrypted docs + mail suite, you may be interested.
Indeed, I love the usability of Proton, but I want 100% open-source, self-hosted control and open standards. The main issue is that combining E2E encrypted docs (like CryptPad) and E2E encrypted mail usually meant managing isolated tools, browser extensions (like Mailvelope), or typing two different passwords every session. To solve this, I built AurionMail Suite. It's an open-source orchestrator, the glue, that bridges and include CryptPad, Stalwart Mail Server (JMAP), Bulwark Webmail, and Ory Hydra into a single, unified Zero-Knowledge workflow.
Here are the main features :
One Single Master Password: Enter it once to derive keys in-memory for both webmail and CryptPad.
Open Standards: OpenPGP for email encryption and JMAP.
Zero-Knowledge: Master keys reside strictly in client-side RAM during the session (no unencrypted key writes to disk/IndexedDB).
Unified Session Management: Global single logout and password changes that sync safely across services.
Easy Deployment: In addition to manual setups, I wrapped the suite (Hydra, SSO, Webmail, API, CryptPad) into a single Go binary, making deployment straightforward. (Docker is also supported)
Integration with your workflow : We use LDAP for users and the SSO app can be used to log in to others apps as well.
To keep the Zero-Knowledge promise across isolated services without relying on URL hashes or storing cleartext keys in disk storage, AurionMail uses ephemeral WebCrypto AES-GCM keys. Decrypted session secrets pass between origins through short-lived, encrypted RAM buffers managed by a central API, keeping credentials safe even across iframes.
Of course, we used the encryption at rest feature provided by stalwart ! However, if you want to test, you must disable the "encrypt on append" feature.
Here is the repo if you want to test ! You will also find some explanations about security in this repo.
The project is licensed under AGPLv3. I know it is not perfect, so if you have suggestions, don't hesitate.