Postfix MX-Based Relay Host Configuration: Route Emails by Destination MX Records


2 views

When dealing with email delivery through Postfix, you might encounter situations where specific destination mail servers (identified by their MX records) require special routing. Common scenarios include:

  • Bypassing corporate firewalls for Microsoft 365/O365 traffic
  • Using alternative delivery paths for high-volume email providers
  • Implementing custom routing for compliance requirements

To implement MX-based relay routing, we'll need to leverage Postfix's transport_maps mechanism combined with a custom policy script. Unlike sender_dependent_relayhost_maps which operates on sender addresses, this solution acts on resolved MX records.

Here's the complete workflow we'll implement:

1. Postfix performs DNS MX lookup for recipient domain
2. Custom policy script checks MX record against our rules
3. Based on MX match, returns appropriate transport mapping
4. Postfix routes message through designated relay host

1. Create the Transport Map File

First, create a transport map file (e.g., /etc/postfix/mx_transport):

# Format: mx_pattern transport:relay_host
outlook.com smtp:[some_smtp.example.com]
.protection.outlook.com smtp:[alternative-relay.example.com]
* local:

2. Create the Policy Script

Save this as /usr/libexec/postfix/mx_policy:

#!/bin/sh

# Read the MX record from STDIN
read mx_record

# Match against our patterns
case "$mx_record" in
    *outlook.com*)
        echo "smtp:[some_smtp.example.com]"
        ;;
    *protection.outlook.com*)
        echo "smtp:[alternative-relay.example.com]"
        ;;
    *)
        echo "local:"
        ;;
esac

exit 0

Make it executable:

chmod +x /usr/libexec/postfix/mx_policy

3. Configure Postfix

Add these to your main.cf:

# Enable transport maps
transport_maps = pcre:/etc/postfix/mx_transport

# Custom MX resolution policy
smtpd_policy_service_default_action = DUNNO
smtpd_policy_service_time_limit = 3600s
policy-spf_time_limit = 3600s
smtpd_recipient_restrictions = 
    check_policy_service unix:private/mx_policy
    ... [other restrictions] ...

4. Create the Master Service Entry

Add to master.cf:

mx_policy unix - n n - - spawn
    user=nobody argv=/usr/libexec/postfix/mx_policy

Verify your configuration:

postmap -q "microsoft-com.mail.protection.outlook.com" pcre:/etc/postfix/mx_transport
echo "microsoft-com.mail.protection.outlook.com" | /usr/libexec/postfix/mx_policy

For more complex routing, you can use PCRE patterns in your transport file:

# Case-insensitive matching for various Outlook MX patterns
/^(.*\.)?outlook\.com$/i smtp:[outlook-relay.example.com]
/^.*\.protection\.outlook\.com$/i smtp:[o365-relay.example.com]
/^.*\.google(mail)?\.com$/i smtp:[gmail-relay.example.com]
  • Cache MX lookups using Postfix's address_verify_cache
  • Consider implementing a daemon policy server for high-volume environments
  • Monitor your mail logs for "mx_policy" timeout warnings

Check these logs when debugging:

grep 'mx_policy' /var/log/maillog
postconf -n | grep -E 'transport|policy'
postqueue -p

When dealing with complex email routing scenarios in Postfix, we often encounter situations where certain destination domains require special handling. The specific case here involves routing emails through different SMTP relays based on the recipient domain's MX records, particularly for domains like Outlook.com that might have specific delivery requirements.

The default Postfix relayhost parameter applies globally to all outgoing mail, while sender_dependent_relayhost_maps only considers the sender address. Neither solution addresses our need for destination-based routing.

Postfix's transport maps provide the perfect mechanism for destination-based routing. Here's how to implement it:

# main.cf additions
transport_maps = hash:/etc/postfix/transport
default_transport = smtp

First, create /etc/postfix/transport with content like:

# Handle Outlook.com domains
.outlook.com   relay:[some_smtp.example.com]
.protection.outlook.com   relay:[some_smtp.example.com]

# Default route (catch-all)
*              smtp:

For true MX-based matching, we need to combine transport maps with policy services. Here's a more advanced solution:

# main.cf
smtpd_recipient_restrictions =
    check_recipient_mx_access hash:/etc/postfix/mx_access

# mx_access file
microsoft-com.mail.protection.outlook.com RELAY=[some_smtp.example.com]

For maximum flexibility, consider a policy script that performs real-time MX lookups:

# policy script example (Python)
import socket

def mx_lookup(domain):
    try:
        return sorted(socket.getaddrinfo(domain, None, socket.AF_INET))
    except socket.gaierror:
        return []

def policy_filter(recipient):
    domain = recipient.split('@')[-1]
    mx_hosts = mx_lookup(domain)
    for mx in mx_hosts:
        if 'outlook.com' in mx[4][0]:
            return "RELAY=[some_smtp.example.com]"
    return "DUNNO"

Always test your configuration with:

postmap /etc/postfix/transport
postfix reload
postfix flush

Verify routing decisions with:

postmap -q "microsoft-com.mail.protection.outlook.com" hash:/etc/postfix/transport

When implementing MX-based routing:

  • Cache MX lookups to avoid DNS overhead
  • Set proper TTL values in your transport maps
  • Consider using memcache for large-scale deployments

If emails aren't routing correctly:

  1. Check Postfix logs (/var/log/mail.log)
  2. Verify transport map syntax with postmap
  3. Test MX lookups manually with dig MX example.com