Skip to main content

Outbound deliverability

The Halon platform is used by many as an outbound anti-spam. Successfully delivering email (measured as "deliverability") to external parties (email servers on the Internet) is important in many cases, such as outbound email services, forwarding and even VPS (cloud) providers. Halon's scripting language HSL lets you design and tailor the logic to handle compromised accounts and abusive users; in order to avoid blacklisting without bothering legitimate users.

Anti-spam and rate limits

With the right tools, outbound anti-spam can be extremely effective because you know who the sender is; their SASL username if authenticated directly with the Halon system, sender address (if enforced by the email server), sender IP (in the case of a VPS provider), a header in the message (enforced by PHP), etc. Therefore, we recommend that you create a deferring, rate-limit based script such as this:

EOD context

EOD context
$transactionid = $transaction["id"];

// Identify the sender as accurately as possible
$customer = $transaction["senderaddress"]["domain"];
$script = $arguments["mail"]->getHeader("X-PHP-Originating-Script");
if ($script)
$customer = $script;
if ($connection["auth"]["username"])
$customer = $connection["auth"]["username"];

// Defer high volumes of bulk and spam
if ((ScanRPD(["outbound" => true]) === 50 or ScanSA() > 4) and rate("outbound-bulk", $customer, 300, 28800) === false)
Defer("$customer is only allowed to send 300 bulk messages per 8 hours, try again later (".$transaction["id"].")");
if ((ScanRPD(["outbound" => true]) === 100 or ScanSA() > 6) and rate("outbound-spam", $customer, 100, 28800) === false)
Defer("$customer is only allowed to send 100 spam messages per 8 hours, try again later (".$transaction["id"].")");

// Flag mail as spam
if (ScanRPD(["outbound" => true]) >= 50 or ScanRPD(["outbound" => true]) === 10 or ScanSA() > 6)
$metadata["spam"] = "yes";

// Anti-virus checks
if (ScanRPD(["outbound" => true, "extended_result" => true])["virus_score"])
Reject("Rejected by virus filter ($transactionid)");
if (ScanKAV())
Reject("Rejected by virus filter ($transactionid)");
if (ScanCLAM())
Reject("Rejected by virus filter ($transactionid)");

which should be accompanied with an upper limit for the total number of messages allowed during a set interval in the RCPT TO script.

RCPT TO context

RCPT TO context
// Max messages per hour
if (rate("outbound", $transaction["sender"], 250, 3600) === false)
Defer($transaction["sender"]." can only send 250 messages per hour, try again later (".$transaction["id"].")");

DKIM signing

While not necessarily a deliverability feature, many customers has DKIM signing as part of their outbound deliverability plan. The exceptionally lightweight and heavily optimised DKIM signer in our scripting language is based on our libdkim++ project, and enables you to deploy per-domain DKIM signing for all your outbound traffic with very little overhead. See this article for how per-domain DKIM signing can be implemented.

Source address

We recommend having multiple (warmed up) IPs per cluster node in order to do

  • Source hashing based on customer ID
  • Sending suspect spam out though a bulk IP

Since each node in a cluster have different IPs, it's convenient to address the IPs using their configuration ID. The example Pre-delivery script below uses uses IP 1-3 for normal traffic (source hashing based on sending domain) and IP 4 for suspect spam. The metadata "spam" variable should be set in the EOD script based on the result from the anti-spam engines (see previous section).

Pre-delivery context

Pre-delivery context
$metadata = GetMetaData();
$transportid = $message["transportid"];
$senderdomain = $message["senderaddress"]["domain"];
$options = [];

// Outbound traffic
if ($transportid === "outbound") {
// Set source IP
if ($metadata["spam"] === "yes") {
$options["sourceip"] = ["4"];
} else {
// Source hash
$addrs = ["1", "2", "3"];
$sourcehash = number("0x".md5($senderdomain)[0:6]);
$options["sourceip"] = [$addrs[$sourcehash % length($addrs)]];
}
}

Try($options);
note

It's important that you configure the hostname that should be used in the HELO message for outbound connections when adding the IP-address to the configuration. Otherwise you would face issues with failing FcRDNS checks.

Act on deliverability

Reacting to automatically measured deliverability is a powerful way to avoid blacklisting. You can for example count the delivery failure rate in the Post-delivery script:

Post-delivery context

Post-delivery context
$transportid = $message["transportid"];
$senderdomain = $message["senderaddress"]["domain"];
$errorcode = $arguments["attempt"]["result"]["code"];

// Delivery failures
if ($transportid === "outbound" and is_number($errorcode) and $errorcode >= 400)
rate("delivery-failures", $senderdomain, 1000, 3600);

And then check it in the MAIL FROM script.

MAIL FROM context

MAIL FROM context
$transactionid = $transaction["id"];
$senderdomain = $arguments["address"]["domain"];

// Delivery failures
if (rate("delivery-failures", $senderdomain, 0, 3600) >= 1000)
Defer("$senderdomain has had more than 1000 failed deliveries during the last hour ($transactionid)");

Connection rate and concurrency

We recommend having sane rate and concurrency limits for outbound connections that is based on for example the destination server or recipient domain. This is even more important for common (high volume) recipient domains such as gmail.com. See this section in our manual for how you can configure queue pickup policies that implements this.

Feedback loops

Receiving Feedback Loop Reports (FBLs) is an important step in monitoring your deliverability rate. By analysing the complaints you receive for emails sent through your system you can identify common causes and take action. Below is a list of some of the biggest FBLs but it's by no means exhaustive.

Reporting

The scriptable reporting enables you to design just the right metrics for you. For example, the Post-delivery script below creates both a line chart (over time) and a pie chart for the delivery failures.

Post-delivery context

Post-delivery context
$errorcode = $arguments["attempt"]["result"]["code"];
if ($errorcode >= 400 and $errorcode < 500)
stat("delivery-failures", ["500" => 0, "400" => 1]);
if ($errorcode >= 500)
stat("delivery-failures", ["500" => 1, "400" => 0]);

You can also send a notification to for example Slack when a rate limit such as delivery-failures has been exceeded, see this article for more information about that.

Forwards

If you deliver (inbound) email directly to your email storage server using for example LMTP, it's common to also do forwarding and auto responses in the inbound script. Forwards should typically be treated in the same way as outbound email in terms of outbound anti-spam filtering and rate limiting. We also recommend using SRS.