How to block outbound email address

Your question

I don’t really know how and where to block an outbound email address. This email address to the external recipient must be blocked urgently.

I use the latest version of binary stalwart 0.16.15

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.

on

I am very tired but i will tell you how you can do that.

I already do that and more

Block domain out

Block domain in

Block spammer promotion

And more

Domain out (emailaddress of the external recipient) is the most important part for me.

If you would do that. I’d appreciate that. Thanks in advance.

I will, if i do not in 24h ping me. I’ve only slept 50 hours in three weeks—I need to get some rest.

And who did force you to do that? (To whom should we talk with?)

If it is you @halaklium just take rest ( stop talking that you need rest )

And who did force you to do that? (To whom should we talk with?) Sorry I don’t understand this part :sad_but_relieved_face:

Yes it is me :slight_smile:

just take rest ( stop talking that you need rest )

Ho I see, thank for your attention. I’m just a hard worker :slight_smile:

I will

This will be a long reply. I’m working on my web UI right now, and I’ll take the time to get back to you. This will also give me a chance to organize my documentation, since I’d like to have a well-organized set of materials on these topics.

Take your time and thank you so much in advance…

Okay, I’m available. I’ll try to keep it as simple as possible. If you want to go into more detail, you’ll need to read the documentation and/or ask an AI for help.

You can implement this with System Sieve scripts.

Create each script under:

Settings → Sieve → System Scripts

Then attach it to the correct SMTP stage under:

Settings → MTA → Session

The stage matters because an SMTP transaction happens in this order:

MAIL FROM → RCPT TO → DATA

Official overview:

Block outbound email to a specific recipient

Example: prevent authenticated users from sending to:

[email protected]

Create a System Script named block-outbound-recipient:

require ["envelope", "reject"];

if envelope :is :comparator "i;ascii-casemap"
    "to" "[email protected]"
{
    reject "550 5.7.1 Sending to this recipient is prohibited.";
    stop;
}

Attach it to:

Settings → MTA → Session → RCPT TO Stage → Run Script

Configure:

IF      !is_empty(authenticated_as)
THEN    'block-outbound-recipient'
ELSE    false

Why use the RCPT TO stage?

The recipient does not exist yet during MAIL FROM. It becomes available when the client sends:

RCPT TO:<[email protected]>

Rejecting at this stage stops the message before Stalwart receives its body or adds it to the outbound queue.

The authenticated_as condition limits the rule to authenticated outbound users and applications. Without that condition, the rule could also affect inbound email.

Official documentation:

You can run a test locally on your server using GitHub - jetmore/swaks: Swaks - Swiss Army Knife for SMTP · GitHub

swaks \
--server ``mail.example.com`` \
--port 587 \
--tls \
--auth LOGIN \
--auth-user '[email protected]' \
--auth-password 'password'\
--from '[email protected]' \
--to '[email protected]' \
--header 'Subject: Outbound recipient blocking test' \
--body 'This message should be rejected.'

I can’t even remember why I set this up for the email address I’ve hidden, but it worked. I have the documentation on how to block an entire domain, but I’m checking to make sure everything is correct—I have some doubts.

To block outbound email to block-domain.com, create a trusted System Sieve script and run it during the recipient-processing stage.

For a single domain:

require ["envelope", "reject"];

if envelope :domain :is :comparator "i;ascii-casemap"
    "to" "block-domain.com"
{
    reject "550 5.7.1 Sending email to this domain is prohibited.";
    stop;
}

If you already maintain a list of blocked domains in a Stalwart search list, the script can read that list instead of hard-coding each domain. This makes it easier to manage the policy, since you can add or remove domains without having to modify the script.

The example above assumes that you agree to receive emails from this domain but do not want your users—or certain users (you can adapt this so the script isn’t global, I believe)—to receive them.

Update : Correction made based on the answer below

Stalwart extends Sieve with a set of built-in functions that can be called from expressions inside eval, let, and while instructions. The functions operate on Sieve values (strings, integers, floats, arrays) and on the message being processed: headers, MIME parts, envelope, and environment.

More information : Reference | Stalwart

Conceptually:

require [
    "variables",
    "envelope",
    "reject",
    "vnd.stalwart.expressions"
];

let "recipient_domain" "to_lowercase(email_part(envelope.to, 'domain'))";

if eval "key_exists('blocked-domain', recipient_domain)" {
    reject "550 5.7.1 Sending email to this domain is prohibited.";
    stop;
}

envelope.to, ‘domain’: Only the “domain” part of the email address string is taken

So we have recipient_domain = “block-domain.com

So it will read the InMemoryKey, which is automatically filled in when you add a domain to the blocked domains list

If you need separate policies—one script for specific email addresses and another for domains—use a main trusted script with include, because a stage’s script expression selects one script name.

require ["include"];

include "block-outbound-addresses";
include "block-outbound-domains";

The main script then calls the individual policy scripts.

Trusted System Sieve scripts, lookup access, SMTP-stage invocation, and include are documented here:

@halaklium’s answer is correct, a trusted system script on the RCPT stage, gated on authenticated_as so it only applies to outbound submissions. Both of the hard-coded scripts work as written, and the 550 5.7.1 text is passed through verbatim rather than being replaced with a generic code. One useful thing to add: this also covers webmail and JMAP clients, not just SMTP submission, because a JMAP send runs the same RCPT stage internally.

One line to change, in the list-based version. set "recipient_domain" "${email_part(envelope.to, 'domain')}"; will not do what you expect, because ${} is plain variable substitution and does not evaluate function calls. Use let instead, without the braces:

let "recipient_domain" "to_lowercase(email_part(envelope.to, 'domain'))";

if eval "key_exists('blocked-domain', recipient_domain)" {
    reject "550 5.7.1 Sending email to this domain is prohibited.";
}

Everything after that line is correct, including the include approach for splitting the address and domain policies into separate scripts. One design note: blocked-domain is the spam filter’s inbound blocked-domains list, so reusing it for outbound recipients works but ties two policies together. A dedicated lookup namespace is cleaner if they should ever need to differ.

Thanks for the confirmation and the correction. I’m still having trouble figuring out on my own which STAGE the script needs to be added to (I should be able to know that by heart). Also, while going through the tutorial, I learned that the Include function exists, and I’ll probably need to use it in the future.

PS : I also wanted to set up a response to block spammers, but I’ve been busy with my personal projects, so I’ll do it later. It’s very useful when a spammer sends 5 messages from one address and 5 more from another.

All the suggestions here work perfectly so far. Thanks very much for it. If you have more things to release, let us know. Thanks

I’ll admit I was a bit stumped on how to block a spammer who keeps changing their address, but it comes down to a combination of keywords in the body of the email. They wouldn’t stop sending me five emails every day from one address, followed by five more emails on the same day with the exact same body text, so I had to find a solution.

You really need to take a close look at the Sieve syntax and the syntax supported by Stalwart to sometimes fine-tune things.

You’r welcome