I am trying to setup self hosted email without exposing any ports. I am doing this by using cloudflare email forwarding and a cloudflare mail worker to send the mail over HTTPS to my server. Cloudflare workers cannot send over SMTP
This is possible by using a local script that takes a HTTP POST from the worker and then sends the mail to Stalwart using SMTP. I think it might be possible to avoid this and send the mail directly to stalwart over HTTP via it’s API.
From what I can tell, the JMAP API allows importing mail into the mailbox but it is unclear whether it will be treated as incoming mail and go through the MTA pipeline and filters. The MTA docs page says:
Messages submitted over JMAP follow the same path. They have no network connection, so they enter at the MAIL FROM stage of step 2.
This seems to suggest that you can get mail to go through MTA but I am unsure what endpoint I am mean to use. The JMAP EmailSubmission/set seems to be for outbound mail and I am unsure if the docs are referring to Email/import.
Does anyone know if there is an API endpoint for my use case?
Thanks for any help!
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
I couldn’t find an HTTP endpoint in the docs that injects incoming mail into the MTA pipeline.
The sentence you quoted is about outbound mail. It describes EmailSubmission/set, where a user sends a message through JMAP. That message has no SMTP connection, so it skips the connect, EHLO and AUTH stages and enters at MAIL FROM. It is then queued for delivery like any other submission, as the MTA overview describes. It isn’t a way to hand over mail that arrived from outside. It also runs as an authenticated user, so it would be treated as submission, not as MX traffic. SPF, DKIM and DMARC checks on the sender wouldn’t be done the way they are for incoming mail.
Email/import doesn’t use the pipeline either. The event reference lists a separate message-ingest.jmap-append event for messages appended via JMAP, next to message-ingest.imap-append. These are direct mailbox writes, much like an IMAP APPEND. The spam filter, the MTA stages, milters and MTA hooks don’t run on them. I didn’t find source that confirms exactly what happens on import, so treat that as the documented behaviour, not something I verified in the code.
For your setup, I’d keep the small receiver you described. It accepts the POST from the worker and hands the raw message to Stalwart over SMTP or LMTP. It can connect to a listener bound only to localhost or a private interface, so nothing is exposed publicly. Use the real envelope sender and recipient from the Cloudflare worker, message.from and message.to, in MAIL FROM and RCPT TO. Stalwart can then run its normal checks and filters. Be aware that after Cloudflare forwards the mail, SPF will be evaluated against the connecting IP of your relay, not Cloudflare’s. You may need to adjust the MX-stage checks on that listener.
There’s no HTTP endpoint for incoming mail, so keep the small bridge that turns the worker’s POST into an SMTP delivery to Stalwart. It can talk to port 25 on localhost or your private network, so nothing has to be exposed, and the message then goes through the whole inbound pipeline: spam filter, Sieve, milters, MTA hooks and so on.
That sentence in the MTA docs is about outgoing mail. “Submitted over JMAP” means EmailSubmission/set, which sends a message that’s already in the account as that account: the envelope sender has to be the address of the identity you pick, so it can’t carry the original sender of a forwarded message. Email/import goes the other way. It stores the message straight into a mailbox and skips the MTA entirely, so no spam filter and no Sieve.
On the bridge, Stalwart’s SPF check will see the bridge’s IP, not the original sender’s server, so SPF will fail or be meaningless for every message. DKIM still verifies as long as the worker passes the raw message through unchanged. If you’d like a native HTTP ingest endpoint, open a topic in Feature Ideas.