Tools: Stop using Postfix for transactional email - Full Analysis

Tools: Stop using Postfix for transactional email - Full Analysis

What an "SMTP server" actually does in 2026

The five things you stop maintaining

Real workloads, real one-liners

Build alerts from CI

Cron job reports

Application errors

When you actually do need Postfix

The mental model shift

Migration in five minutes

Next steps Postfix has shipped 12 security advisories since 2020 (source). Each one needs a patch, a daemon restart, and a smoke test to confirm mail still flows. Twelve interruptions to ship, for a sub-system that exists to do one thing: hand a string to a smarter mail provider 50ms later. If your only outbound need is "send a templated email from a script", you do not need an SMTP daemon. You need a function call. For most workloads, the local Postfix instance: Steps 1, 2, 4 are infrastructure. Step 3 is the work. Removing 1, 2, 4 means you trade five files in /etc/postfix and a queue daemon for a single command: That is it. No main.cf, no SASL, no relayhost tuning. The CLI handles the relay handshake. Switching off the local Postfix kills five operational concerns: Every one of those is non-revenue work. No "install Postfix on your runner" step. brew install nylas/nylas-cli/nylas (or curl install.sh | bash on Linux) is the entire prerequisite. The Python program does not need an SMTP library. It calls a CLI. If you are not in one of those three categories, the Postfix install is technical debt. Treat outbound mail like you treat outbound HTTP: a function call to a managed service. Your script does not run apache2 to make an HTTP request. It calls curl or a library. Same logic for mail. nylas email send is the curl of email. The last command will feel like cheating. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

nylas email send \ --to [email protected] \ --subject "Build #4129 failed" \ --body "See https://ci.example.com/4129 for details." nylas email send \ --to [email protected] \ --subject "Build #4129 failed" \ --body "See https://ci.example.com/4129 for details." nylas email send \ --to [email protected] \ --subject "Build #4129 failed" \ --body "See https://ci.example.com/4129 for details." # In .github/workflows/notify.yml - run: | nylas email send \ --to [email protected] \ --subject "🚨 main is red" \ --body "Run: ${{ github.run_id }}\nCommit: ${{ github.sha }}" # In .github/workflows/notify.yml - run: | nylas email send \ --to [email protected] \ --subject "🚨 main is red" \ --body "Run: ${{ github.run_id }}\nCommit: ${{ github.sha }}" # In .github/workflows/notify.yml - run: | nylas email send \ --to [email protected] \ --subject "🚨 main is red" \ --body "Run: ${{ github.run_id }}\nCommit: ${{ github.sha }}" # /etc/cron.daily/db-backup #!/usr/bin/env bash if pg_dump --quiet ourdb | gzip > /backups/$(date +%F).sql.gz; then nylas email send --to [email protected] \ --subject "DB backup OK" \ --body "$(date): backup complete. Size: $(du -h /backups/$(date +%F).sql.gz)" else nylas email send --to [email protected] \ --subject "🔴 DB backup FAILED" \ --body "Check /var/log/db-backup.log on $(hostname)" fi # /etc/cron.daily/db-backup #!/usr/bin/env bash if pg_dump --quiet ourdb | gzip > /backups/$(date +%F).sql.gz; then nylas email send --to [email protected] \ --subject "DB backup OK" \ --body "$(date): backup complete. Size: $(du -h /backups/$(date +%F).sql.gz)" else nylas email send --to [email protected] \ --subject "🔴 DB backup FAILED" \ --body "Check /var/log/db-backup.log on $(hostname)" fi # /etc/cron.daily/db-backup #!/usr/bin/env bash if pg_dump --quiet ourdb | gzip > /backups/$(date +%F).sql.gz; then nylas email send --to [email protected] \ --subject "DB backup OK" \ --body "$(date): backup complete. Size: $(du -h /backups/$(date +%F).sql.gz)" else nylas email send --to [email protected] \ --subject "🔴 DB backup FAILED" \ --body "Check /var/log/db-backup.log on $(hostname)" fi import subprocess try: process_payments() except CriticalError as e: subprocess.run([ 'nylas', 'email', 'send', '--to', '[email protected]', '--subject', f'Payment processor down: {e.code}', '--body', str(e) ], check=True) import subprocess try: process_payments() except CriticalError as e: subprocess.run([ 'nylas', 'email', 'send', '--to', '[email protected]', '--subject', f'Payment processor down: {e.code}', '--body', str(e) ], check=True) import subprocess try: process_payments() except CriticalError as e: subprocess.run([ 'nylas', 'email', 'send', '--to', '[email protected]', '--subject', f'Payment processor down: {e.code}', '--body', str(e) ], check=True) # 1. Install brew install nylas/nylas-cli/nylas # or: curl -fsSL https://cli.nylas.com/install.sh | bash # 2. Auth (paste your API key) nylas auth config --api-key YOUR_KEY # 3. Send a test nylas email send --to [email protected] --subject "test" --body "hi from the new world" # 4. Replace every "sendmail -t" call with "nylas email send" grep -r 'sendmail\|mailx' /etc /home /opt /var/scripts | wc -l # That number is your migration scope. # 5. systemctl disable postfix && systemctl stop postfix # 1. Install brew install nylas/nylas-cli/nylas # or: curl -fsSL https://cli.nylas.com/install.sh | bash # 2. Auth (paste your API key) nylas auth config --api-key YOUR_KEY # 3. Send a test nylas email send --to [email protected] --subject "test" --body "hi from the new world" # 4. Replace every "sendmail -t" call with "nylas email send" grep -r 'sendmail\|mailx' /etc /home /opt /var/scripts | wc -l # That number is your migration scope. # 5. systemctl disable postfix && systemctl stop postfix # 1. Install brew install nylas/nylas-cli/nylas # or: curl -fsSL https://cli.nylas.com/install.sh | bash # 2. Auth (paste your API key) nylas auth config --api-key YOUR_KEY # 3. Send a test nylas email send --to [email protected] --subject "test" --body "hi from the new world" # 4. Replace every "sendmail -t" call with "nylas email send" grep -r 'sendmail\|mailx' /etc /home /opt /var/scripts | wc -l # That number is your migration scope. # 5. systemctl disable postfix && systemctl stop postfix - Accepts mail from sendmail -t or mail - Hands it to a smart relay (SES, SendGrid, Mailgun, or your provider of choice) - Logs the result - Inbound mail to a custom domain — though even this is now better solved with a managed agent account: nylas agent account create [email protected], then poll with nylas email list or webhook with nylas webhook create --triggers message.created. - Air-gapped networks with no outbound HTTPS — Postfix to a DMZ relay still wins. - Compliance regimes that forbid third-party mail relays — a niche, but real. - Send email from the terminal — full reference for outbound CLI mail - Best CLI email tools compared — mutt, mailx, msmtp head-to-head - Receive email without an SMTP server — the inbound counterpart - Full command reference