ServicesAboutGet Started
PostfixMicrosoft GraphAmazon SESLinux

Centralized Postfix API Mail Relay

Build a single internal relay that accepts standard SMTP from every server and appliance on your network, then delivers it via Microsoft Graph API or Amazon SES over HTTPS — all from one system, with one set of credentials.

Why a centralized relay?

Our previous guides — Proxmox Email via Graph/SES and DNF5 Automatic Email via Graph/SES — show how to configure individual servers to send notifications directly through a cloud API. That works well when you only have a few systems to manage.

But on a site with dozens of servers, switches, UPS devices, storage appliances, and other infrastructure — each needing to send alerts — manually deploying and maintaining API credentials on every system becomes impractical. Credentials expire, and you would need to rotate them everywhere.

The solution is a centralized API relay. Stand up a single dedicated VM or container running Postfix. Point every internal system at it using standard SMTP (the default for nearly every appliance and daemon). This relay accepts unauthenticated mail from your LAN, then delivers it to the outside world exclusively through secure HTTPS API calls — never over traditional SMTP ports that may be blocked by your ISP or firewall.

text
Internal systems (standard SMTP)
  → Centralized Postfix relay (your LAN)
  → Python script (this guide)
  → Microsoft Graph API  or  Amazon SES  (HTTPS, port 443)
  → Recipient mailbox
💡

This relay handles outbound notification mail only — it is not a full mail server. It does not receive inbound internet email, host mailboxes, or handle DKIM/SPF for arbitrary domains. It is a one-way bridge from internal SMTP to cloud API.


Architecture overview

The relay uses three files to separate concerns:

FileLocationPurpose
postfix-api-relay/opt/postfix-relay/The Python script — logic and API integration
relay-config.json/opt/postfix-relay/Routing rules — approved senders, admin email, notification settings
api-secrets.json/etc/postfix/API credentials — locked to 0600 permissions

This separation means routing changes (adding an approved sender, changing the admin address) do not require touching the secrets file, and credential rotation does not require editing the configuration.


1. Prerequisites

This guide assumes you have a Linux system (RHEL/Fedora, Debian/Ubuntu, or similar) with Postfix installed at its default settings. Most minimal server installations include Postfix, or it can be installed with your package manager.

You also need credentials for your chosen cloud provider. If you have not set those up yet, see our Microsoft Graph App Registration guide.

Install Python dependencies

The Graph API provider uses only Python 3 standard library modules — no additional packages are needed. The SES provider requires boto3:

bash
# RHEL / Fedora
dnf install -y python3-boto3

# Debian / Ubuntu
apt install -y python3-boto3

2. Create the secrets file

The secrets file stores your API credentials in a secure location with strict permissions. Both RHEL and Debian families use /etc/postfix/ for Postfix configuration files. The field names are generic — comments above each value indicate what each means for your specific provider.

Microsoft Graph API

bash
cat > /etc/postfix/api-secrets.json <<'EOF'
{
    "API_PROVIDER": "graph",

    "_comment_tenant": "Microsoft Entra (Azure AD) Tenant ID",
    "API_TENANT_OR_REGION": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",

    "_comment_id": "App Registration Client ID",
    "API_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",

    "_comment_secret": "App Registration Client Secret",
    "API_SECRET": "your-client-secret-here",

    "_comment_expires": "Expiration date of the client secret (YYYY-MM-DD)",
    "API_EXPIRATION_DATE": "2028-04-05"
}
EOF

Amazon SES

bash
cat > /etc/postfix/api-secrets.json <<'EOF'
{
    "API_PROVIDER": "ses",

    "_comment_region": "AWS Region for SES (e.g. us-east-1)",
    "API_TENANT_OR_REGION": "us-east-1",

    "_comment_id": "IAM Access Key ID",
    "API_ID": "AKIAIOSFODNN7EXAMPLE",

    "_comment_secret": "IAM Secret Access Key",
    "API_SECRET": "your-secret-key-here",

    "_comment_expires": "Expiration date of the IAM access key (YYYY-MM-DD)",
    "API_EXPIRATION_DATE": "2028-04-05"
}
EOF

Lock down permissions

bash
chown root:root /etc/postfix/api-secrets.json
chmod 600 /etc/postfix/api-secrets.json

The script enforces 0600 permissions and will refuse to run if the secrets file is accessible to group or world.


3. Create the configuration file

The configuration file controls routing behavior — which sender addresses are approved, who receives admin alerts, and where to log errors. This file does not contain secrets and is safe to manage openly.

bash
mkdir -p /opt/postfix-relay

cat > /opt/postfix-relay/relay-config.json <<'EOF'
{
    "ADMIN_NOTIFICATION_EMAIL": "admin@yourdomain.com",

    "NOTIFY_ON_OVERRIDE": true,

    "LOG_FILE_PATH": "/opt/postfix-relay/postfix-api-relay.log",

    "VALID_FROM_ADDRESSES": [
        "alerts@yourdomain.com",
        "network@yourdomain.com"
    ],

    "DEFAULT_FROM_ADDRESS": "alerts@yourdomain.com"
}
EOF
FieldDescription
ADMIN_NOTIFICATION_EMAIL(Required) The admin who receives expiry warnings, delivery failure alerts, and sender override notices.
NOTIFY_ON_OVERRIDEIf true, the admin receives an email whenever a message arrives from an unapproved sender address and is automatically rerouted. Set to false to silently override without notification.
LOG_FILE_PATHLocal log file for errors. If the API fails and email cannot be sent, the error is always written here as a fallback.
VALID_FROM_ADDRESSESAn array of approved sender addresses. If an incoming message matches one of these, it is sent as-is. Otherwise it is sent using the default address with the original sender noted in the subject line.
DEFAULT_FROM_ADDRESS(Required) The fallback sender address used when the original sender is not in the approved list. Must be a valid mailbox in your Graph tenant or a verified identity in SES.

4. Install the relay script

Download the postfix-api-relay Python script into /opt/postfix-relay/:

bash
wget -O /opt/postfix-relay/postfix-api-relay https://meikakuconsulting.com/guides/postfix-relay/postfix-api-relay
chmod 755 /opt/postfix-relay/postfix-api-relay
chown root:root /opt/postfix-relay/postfix-api-relay

Verify the script compiles

bash
python3 -m py_compile /opt/postfix-relay/postfix-api-relay

If this command produces no output, the script is syntactically valid.


5. Configure Postfix

Two Postfix files need to be modified: main.cf (general settings) and master.cf (service definitions). These changes tell Postfix to accept mail from your LAN and route all outbound mail through the relay script instead of trying to deliver via traditional SMTP.

main.cf — Accept LAN traffic and set the default transport

Replace 192.168.1.0/24 with your actual internal network CIDR. You can list multiple networks separated by commas.

bash
# Allow your internal network to relay through this server
postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"

# Route ALL outbound mail through our API relay transport
postconf -e "default_transport = apirelay"

# Bind Postfix to all interfaces so LAN clients can connect
postconf -e "inet_interfaces = all"
⚠️

Security: Because this Postfix instance accepts unauthenticated mail from mynetworks, it must only be reachable by trusted systems. Use firewall rules or place it on a dedicated management VLAN. Do not expose it to the public internet.

master.cf — Define the API relay transport

Add the following line to the end of /etc/postfix/master.cf. This defines a Postfix service named apirelay that pipes every message through the Python script, passing the envelope sender and recipient as arguments:

bash
cat >> /etc/postfix/master.cf <<'EOF'

# API mail relay transport — routes outbound mail through Graph or SES API
apirelay  unix  -       n       n       -       -       pipe
  flags=Rq user=root argv=/opt/postfix-relay/postfix-api-relay ${sender} ${recipient}
EOF
💡

The flags=Rq tell Postfix to pass the raw envelope sender (R) and quote special characters (q). The user=root is required so the script can read the 0600-permissioned secrets file. On hardened systems, you can create a dedicated service account and grant it read access to the secrets file instead.

Restart Postfix

bash
systemctl restart postfix

6. Test

Send a test email through the relay from the local system:

bash
echo "Test message from the API relay" | sendmail -f alerts@yourdomain.com admin@yourdomain.com

Check the Postfix mail log for confirmation:

bash
# RHEL / Fedora
journalctl -u postfix -n 50 --no-pager

# Debian / Ubuntu
tail -50 /var/log/mail.log

Test from another system on your LAN

From any other server or appliance on your network, configure it to send SMTP mail to the relay system's IP address on port 25. For a quick test from another Linux box:

bash
echo "Test from a remote system" | sendmail -S relay-hostname:25 -f alerts@yourdomain.com admin@yourdomain.com

Test sender override

Send from an address that is not in your VALID_FROM_ADDRESSES list to verify the override logic:

bash
echo "This should trigger an override" | sendmail -f unknown-device@local admin@yourdomain.com

The email should arrive at your admin address with the subject prefixed by [Sent by unknown-device@local]. If NOTIFY_ON_OVERRIDE is enabled, a separate notification email will also be delivered to the admin.


7. Point your internal systems at the relay

On each server, switch, UPS, or appliance, configure SMTP to point at this relay. The exact setting varies by device, but the values are always:

SettingValue
SMTP Server / Relay HostIP address or hostname of this relay system
SMTP Port25
AuthenticationNone
TLS / SSLNone (internal LAN only)
💡

For example, on a Proxmox host you would set the relay host under Datacenter → Notifications → SMTP. On a network switch, it is usually under the SNMP/email alert settings. The key point is that every system simply sends standard SMTP to this one relay — no API keys, no OAuth tokens, no expiring credentials on any of them.


8. Error handling and logging

The relay script includes robust error handling to ensure you are aware of delivery problems:

ScenarioBehavior
API call fails (network error, invalid credentials, rejected sender)Error is logged to the local log file. A delivery failure alert is sent to the admin using the default address.
Admin alert also failsThe secondary failure is appended to the local log file and the script exits. No retry loop is created.
Credentials approaching expiration (within 30 days)A warning banner is prepended to the delivered message and a dedicated expiry alert is sent to the admin.
Unapproved sender addressMessage is delivered using the default address. If NOTIFY_ON_OVERRIDE is enabled, the admin is notified separately.

The log file location is configured in relay-config.json. The default is /opt/postfix-relay/postfix-api-relay.log. Check it if you suspect delivery issues.


Troubleshooting

SymptomLikely Cause
Config file not foundrelay-config.json does not exist in /opt/postfix-relay/. Create it per step 3.
Secrets file not found/etc/postfix/api-secrets.json does not exist. Create it per step 2.
permissions are 0o644, expected 0o600Run chmod 600 /etc/postfix/api-secrets.json to fix.
API_PROVIDER must be 'graph' or 'ses'The API_PROVIDER field is missing or has a typo in the secrets file.
ADMIN_NOTIFICATION_EMAIL is requiredAdd the ADMIN_NOTIFICATION_EMAIL field to relay-config.json. This field is mandatory.
Failed to obtain Graph tokenCheck API_TENANT_OR_REGION, API_ID, and API_SECRET. Verify the app has Mail.Send permission with admin consent.
Graph sendMail failedThe DEFAULT_FROM_ADDRESS mailbox may not exist or the app is not authorized to send as that mailbox.
boto3 is required for the SES providerRun dnf install python3-boto3 or apt install python3-boto3.
SES send_email failedVerify IAM credentials, region, and that the sender address is verified in SES.
Remote system cannot connect to the relayCheck that inet_interfaces = all is set in main.cf and that your firewall allows port 25 from the internal network.
Mail queues up but never deliversRun postqueue -p to view the queue. Check the relay log file for errors. Verify the apirelay transport is correctly defined in master.cf.