📬 Postfix: Install and Configure

Postfix install and configuration guide / survival guide

Video: Postfix (sysadmin video)
Prefer to watch? This video runs the guide's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube. It is part of the Services, Storage and Security playlist.
Postfix: Installing and Configuring a Mail Server (Send-only, Relay and Full Server)
postconf -n

Postfix is the most widely used mail transfer agent (MTA) on Linux. It receives mail, decides where it should go, queues it, and delivers it, either to a local
mailbox or to another server on the internet. This guide shows how to install and configure it for the three jobs sysadmins meet most often:
  1. A SEND-ONLY server that just needs to mail alerts and cron output (usually through a relay/smarthost)          <- the most common case
  2. A RELAY through a mail service (Azure Communication Services, SES, SendGrid, Microsoft 365, your ISP)
  3. A FULL internet mail server that receives mail for your domain, with TLS, authenticated sending (submission), and anti-spam basics
It ends with DNS records (SPF, DKIM, DMARC, PTR), queue management, logs, testing, security and troubleshooting. Steps are for Debian/Ubuntu and the RHEL family.
Running a public mail server is real work: deliverability, spam and abuse handling never stop. Consider a hosted service unless you need the control.

How mail flows (the vocabulary)

MUA   Mail User Agent      Your mail program (Thunderbird, Outlook, mutt, a phone)
MSA   Mail Submission      Where users SEND mail from their client: port 587 (STARTTLS) or 465 (implicit TLS), with authentication
MTA   Mail Transfer Agent  Moves mail between servers over SMTP port 25. This is Postfix
MDA   Mail Delivery Agent  Puts mail into the user's mailbox (Postfix local delivery, or Dovecot LMTP)
MX    Mail Exchanger       A DNS record that says which server accepts mail for a domain
IMAP  Mailbox access       Dovecot serves mailboxes to clients on 143 (STARTTLS) or 993 (TLS)
Postfix is a set of small cooperating programs (smtpd, qmgr, smtp, local, cleanup, pickup...) controlled by two files: main.cf (settings) and master.cf (which services run on which ports).
Path of an outgoing message:  program > sendmail command > pickup/cleanup > queue (/var/spool/postfix) > qmgr > smtp client > the recipient's MX server on port 25.
Path of an incoming message:  a sender's server > port 25 > smtpd (checks) > cleanup > queue > qmgr > local/virtual/LMTP delivery > mailbox.

What you need first (prerequisites)

[ ] A server with a static public IP address (for a full mail server), and the ability to open ports
[ ] A domain name and DNS control: an A record for the mail host, an MX record, and REVERSE DNS (PTR) for the IP that matches the host name. The PTR is set by whoever owns the IP (your ISP or cloud provider)
[ ] A fully qualified hostname on the server (hostnamectl set-hostname mail.example.com) that resolves to its IP
[ ] Outbound port 25 allowed. MANY clouds and ISPs BLOCK it: AWS blocks port 25 outbound by default (request removal or use SES), Azure blocks it on many subscription types, Google Cloud blocks it.
    If yours is blocked, send through a relay on port 587 instead (see the relay section)
[ ] An IP with a clean reputation (check it against blocklists, for example with MXToolbox, before you use it)
[ ] A TLS certificate for the mail hostname (Let's Encrypt is fine)
[ ] Time synchronized (chrony), so DKIM timestamps and logs make sense
Firewall ports:  25/tcp (server to server, and inbound mail), 587/tcp (submission), 465/tcp (smtps, optional), 143/tcp and 993/tcp (IMAP), 110 and 995 (POP3, only if used).
For a send-only server, no inbound ports are needed at all.
sudo ufw allow 25/tcp ; sudo ufw allow 587/tcp ; sudo ufw allow 993/tcp          # Ubuntu (firewalld:  sudo firewall-cmd --permanent --add-service=smtp --add-service=smtp-submission --add-service=imaps ; sudo firewall-cmd --reload)

Install Postfix

Debian/Ubuntu:
sudo apt update
sudo apt install postfix mailutils libsasl2-modules             # mailutils gives you the "mail" command. libsasl2-modules lets Postfix log in to a relay
   The installer asks for a "General type of mail configuration":
      Internet Site                  full server that sends and receives directly (also fine for many send-only setups)
      Internet with smarthost        receives directly, sends through a relay
      Satellite system               sends everything through a relay, no local domain (common for servers that only send alerts)
      Local only                     only delivers to local users
   and for the "System mail name": your domain, for example example.com (it is saved in /etc/mailname)
   Run the wizard again any time:  sudo dpkg-reconfigure postfix
RHEL / Rocky / Alma / Fedora:
sudo dnf install postfix s-nail cyrus-sasl-plain                # s-nail provides "mail"
sudo systemctl disable --now sendmail 2>/dev/null               # only one MTA can own port 25 (also stop exim if present)
sudo alternatives --set mta /usr/sbin/sendmail.postfix          # make Postfix the system "sendmail"
sudo systemctl enable --now postfix
Check it runs:
systemctl status postfix      postfix status      sudo ss -tlnp | grep ':25'        # is something listening on port 25?
postconf mail_version                                          # the Postfix version
If the port is already taken (exim4, sendmail): remove or stop that MTA first (sudo apt purge exim4-daemon-light).

The two files and the commands you will use

/etc/postfix/main.cf         Main settings (edit this the most)
/etc/postfix/master.cf       Which daemons run and on which ports (submission, smtps...)
/etc/aliases                 Local aliases (run newaliases after editing)          /etc/mailname    (Debian) the mail domain
postconf -n                  Show ONLY the settings that differ from the defaults (paste this when asking for help)
postconf -d                  Show all defaults          postconf parameter    show one value         postconf -e 'parameter = value'    set a value in main.cf without an editor
postconf -M                  Show master.cf services      postconf -P    master.cf parameters       postconf -x    show expanded values
postfix check                Look for configuration and permission problems
sudo postfix reload          Apply changes to main.cf and master.cf without stopping     sudo systemctl restart postfix    a full restart
sudo postfix set-permissions   Repair file permissions and ownership under /etc/postfix and /var/spool/postfix
Edit main.cf, run  postfix check , then  postfix reload . Watch the log while you test:  sudo tail -f /var/log/mail.log   (RHEL: /var/log/maillog, or  journalctl -u postfix -f )
Syntax: one "name = value" per line. Reference other settings with $name (for example $myhostname). Indent continuation lines. Comments start with #. Never put a comment on the same line as a value.

Key settings explained (main.cf)

myhostname = mail.example.com          The fully qualified name of THIS server. It is what Postfix says in "HELO" and must match the PTR record
mydomain = example.com                 Your domain (default: myhostname without the first part)
myorigin = $mydomain                   The domain added to locally sent mail (for "root" it becomes root@example.com). Or /etc/mailname
mydestination = ...                    Domains Postfix delivers to LOCALLY (real system users). Full server: $myhostname, localhost.$mydomain, localhost, $mydomain
                                       Send-only or virtual-domain server: keep it minimal, do NOT list your virtual domains here
inet_interfaces = all                  Where to listen. "loopback-only" for a send-only server (no outside connections at all), "all" for a real server
inet_protocols = ipv4                  ipv4 or all. Use ipv4 if the server has no working IPv6 (avoids mysterious delays and deferrals)
mynetworks = 127.0.0.0/8 [::1]/128     Networks allowed to RELAY through you without logging in. Keep it TINY. A wrong value makes you an open relay
relayhost = [smtp.example.net]:587     Send ALL outgoing mail through this server (a smarthost). The brackets skip MX lookups. Empty = deliver directly
home_mailbox = Maildir/                Deliver into ~/Maildir (one file per message) instead of /var/mail/user (mbox). Needed with Dovecot Maildir
alias_maps = hash:/etc/aliases         Local alias file (root: you@example.com)
message_size_limit = 26214400          Largest message in bytes (25 MB). mailbox_size_limit = 0 means no limit
smtpd_banner = $myhostname ESMTP       What the server says on connect. Do not advertise the software version
biff = no    append_dot_mydomain = no  Old, unneeded features
compatibility_level                    Behavior version of the defaults. Set by the installer or postconf. Leave it alone, and only change it when you upgrade Postfix and have read the notes
maximal_queue_lifetime = 5d            How long to keep retrying before bouncing (default 5 days)      bounce_queue_lifetime = 5d
recipient_delimiter = +                Allows user+tag@example.com to reach user
disable_vrfy_command = yes             Stops others from probing which accounts exist
smtpd_helo_required = yes              Insist on a HELO greeting (stops many bots)

Scenario 1: a send-only server (cron mail, alerts, application mail)

Goal: local programs can send mail; nothing from the outside can connect. Send directly if port 25 is open, or through a relay (see Scenario 2).
sudo postconf -e 'myhostname = server01.example.com'
sudo postconf -e 'myorigin = example.com'
sudo postconf -e 'inet_interfaces = loopback-only'
sudo postconf -e 'inet_protocols = ipv4'
sudo postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost'
sudo postconf -e 'mynetworks = 127.0.0.0/8'
sudo postconf -e 'smtp_tls_security_level = may'                # use TLS when the other side offers it
sudo postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
echo 'root: alerts@example.com' | sudo tee -a /etc/aliases ; sudo newaliases      # mail for root goes to a real mailbox
sudo postfix check && sudo systemctl restart postfix
Test:  echo "Test body" | mail -s "Test from $(hostname)" you@example.com       then  sudo tail -f /var/log/mail.log
Direct delivery from a plain server often lands in spam (no reputation, no PTR, no SPF/DKIM). For anything that must arrive, use a relay.

Scenario 2: send through a relay (smarthost) with a login

Works with Azure Communication Services email, Amazon SES, SendGrid, Mailgun, Microsoft 365 or Gmail SMTP relay, or your own provider. Replace the host, port, user and password:
1. Store the credentials in a protected file:
     sudo nano /etc/postfix/sasl_passwd
        [smtp.example-relay.com]:587    relay-user:relay-password
     sudo chmod 600 /etc/postfix/sasl_passwd
     sudo postmap /etc/postfix/sasl_passwd                       # creates sasl_passwd.db  (run again after every edit)
     sudo chmod 600 /etc/postfix/sasl_passwd.db
2. Add these settings:
     sudo postconf -e 'relayhost = [smtp.example-relay.com]:587'
     sudo postconf -e 'smtp_sasl_auth_enable = yes'
     sudo postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
     sudo postconf -e 'smtp_sasl_security_options = noanonymous'
     sudo postconf -e 'smtp_sasl_tls_security_options = noanonymous'
     sudo postconf -e 'smtp_tls_security_level = encrypt'               # REQUIRE TLS to the relay (use "may" only if it lacks TLS)
     sudo postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
     sudo postconf -e 'smtp_tls_note_starttls_offer = yes'
3. Make the From address acceptable to the relay. Many relays reject any sender that is not a verified address or domain:
     sudo postconf -e 'sender_canonical_maps = static:noreply@example.com'      # rewrite EVERY sender (simple, and good for alert-only servers)
   or map specific users:  sender_canonical_maps = hash:/etc/postfix/sender_canonical   (root@server01.example.com   noreply@example.com)  then  sudo postmap /etc/postfix/sender_canonical
4. Restart and test:
     sudo postfix check && sudo systemctl restart postfix
     echo "Relay test" | mail -s "Relay test" -r noreply@example.com you@example.com
     sudo tail -f /var/log/mail.log            # look for  status=sent (250 ...)
For port 465 (implicit TLS) relays add:  smtp_tls_wrappermode = yes  and  smtp_tls_security_level = encrypt , and use [host]:465 in relayhost.
See the "Azure SMTP Relay" guide for the Azure Communication Services values (username format and secret).
Only relay some domains through a service (transport maps):
     sudo postconf -e 'transport_maps = hash:/etc/postfix/transport'
     /etc/postfix/transport:   example.org   smtp:[relay.example.net]:587      then  sudo postmap /etc/postfix/transport

Scenario 3: a full mail server (receives mail for your domain)

Step 1: DNS (published BEFORE you start receiving mail)
example.com.              IN MX   10 mail.example.com.
mail.example.com.         IN A    203.0.113.10
mail.example.com.         IN AAAA 2001:db8::10                     (only if IPv6 works end to end. Otherwise leave it out)
10.113.0.203.in-addr.arpa. IN PTR  mail.example.com.               (reverse DNS: ask your ISP or cloud provider to set it to the mail hostname)
example.com.              IN TXT  "v=spf1 mx -all"                 (SPF: only the MX host may send for this domain. Add other senders, for example include:spf.relay.example)
mail._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIIBIj..."  (DKIM public key, from the DKIM step below)
_dmarc.example.com.       IN TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"     (DMARC: start with p=none, later quarantine, then reject)
Check with:  dig +short MX example.com   dig +short TXT example.com   dig +short -x 203.0.113.10   (the PTR must equal the mail hostname, and that name must resolve back to the same IP)
Step 2: hostname and certificate
sudo hostnamectl set-hostname mail.example.com ; echo "mail.example.com" | sudo tee /etc/mailname
sudo apt install certbot                                                                    # RHEL: sudo dnf install certbot
sudo certbot certonly --standalone -d mail.example.com                                       # port 80 must be open. Certificates land in /etc/letsencrypt/live/mail.example.com/
sudo systemctl enable certbot.timer                                                          # automatic renewals. Add a deploy hook to reload Postfix and Dovecot:
sudo sh -c 'printf "#!/bin/sh\nsystemctl reload postfix dovecot\n" > /etc/letsencrypt/renewal-hooks/deploy/mail-reload.sh' ; sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail-reload.sh
Step 3: core settings (main.cf)
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
inet_interfaces = all
inet_protocols = ipv4
mynetworks = 127.0.0.0/8 [::1]/128
home_mailbox = Maildir/
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
message_size_limit = 26214400
smtpd_banner = $myhostname ESMTP
disable_vrfy_command = yes
smtpd_helo_required = yes
Step 4: TLS (encrypt mail in transit)
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may                     # offer STARTTLS to other servers (do NOT force it on port 25: many servers cannot)
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_loglevel = 1                             # log the TLS version and cipher of connections
smtp_tls_security_level = may                      # use TLS when sending, if the receiver offers it
smtp_tls_protocols = >=TLSv1.2
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_loglevel = 1
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
Optional strict TLS to specific partners: smtp_tls_policy_maps = hash:/etc/postfix/tls_policy  (example.org  encrypt). Modern DANE and MTA-STS are advanced ways to protect server-to-server TLS.
Step 5: users log in to send mail (submission on port 587) using Dovecot for authentication
Install Dovecot:   sudo apt install dovecot-imapd dovecot-lmtpd          (RHEL: sudo dnf install dovecot)
/etc/dovecot/conf.d/10-mail.conf:       mail_location = maildir:~/Maildir
/etc/dovecot/conf.d/10-auth.conf:       disable_plaintext_auth = yes      auth_mechanisms = plain login
/etc/dovecot/conf.d/10-ssl.conf:        ssl = required     ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem     ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
/etc/dovecot/conf.d/10-master.conf:     share the login service with Postfix through a socket:
    service auth {
      unix_listener /var/spool/postfix/private/auth {
        mode = 0660
        user = postfix
        group = postfix
      }
    }
sudo systemctl restart dovecot
Tell Postfix to use it (main.cf):
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = no                        # keep it OFF on port 25, and turn it on only for the submission service below
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
Then enable the submission service in /etc/postfix/master.cf (uncomment or add):
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_tls_auth_only=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
Optional implicit-TLS port 465 (some clients prefer it):
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
Users log in with their system account name and password (or your Dovecot user database). Client settings: incoming IMAP mail.example.com port 993 SSL/TLS, outgoing SMTP mail.example.com port 587 STARTTLS, username and password.
Step 6: restrictions (the anti-abuse rules): put these in main.cf
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination
smtpd_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname
smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain
smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_client_connection_count_limit = 20
smtpd_client_message_rate_limit = 60               # per client per minute
smtpd_error_sleep_time = 1s
smtpd_soft_error_limit = 10
smtpd_hard_error_limit = 20
The most important lines are  defer_unauth_destination  and  reject_unauth_destination : they stop the server relaying mail for strangers (an OPEN RELAY). Never remove them.
Optional extras: reject_rbl_client zen.spamhaus.org (use a Spamhaus DQS account for busier servers), postscreen (a front-line filter for port 25: see postconf -d | grep postscreen), greylisting (postgrey), and a content filter (rspamd or SpamAssassin, see below).
Step 7: apply and test
sudo postfix check && sudo systemctl restart postfix
sudo ss -tlnp | grep -E ':(25|587|465|993)'                # the ports are listening
Test sending, receiving and login (see the Testing section).

Aliases and forwarding

/etc/aliases (then run  sudo newaliases ):
    postmaster: root
    root: you@example.com                       # forward system mail to a real address
    webmaster: alice
    sales: alice, bob                           # a list
    devnull: /dev/null
    "|/usr/local/bin/handler": ...              # pipes to programs are risky: avoid unless needed
sudo newaliases
sudo postmap -q root hash:/etc/aliases          # check one alias
Per-user forwarding: a ~/.forward file with an address on one line (user@example.com), or  \user, other@example.com  to keep a local copy.
Catch-all and address rewriting for local domains: virtual_alias_maps (see below).

Virtual domains and mailboxes (many domains, mailboxes that are not Linux users)
Use this when mail users should NOT be system accounts. The mail domain must NOT be in mydestination.
sudo groupadd -g 5000 vmail ; sudo useradd -g vmail -u 5000 -d /var/vmail -m -s /usr/sbin/nologin vmail
main.cf:
virtual_mailbox_domains = example.com, example.org
virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = hash:/etc/postfix/vmailbox
virtual_alias_maps = hash:/etc/postfix/virtual
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
/etc/postfix/vmailbox (address, then mailbox path relative to virtual_mailbox_base; a trailing / means Maildir):
    alice@example.com     example.com/alice/
    bob@example.org       example.org/bob/
/etc/postfix/virtual (aliases and forwards):
    info@example.com      alice@example.com
    @example.org          bob@example.org              # a catch-all (everything not otherwise matched)
sudo postmap /etc/postfix/vmailbox /etc/postfix/virtual ; sudo postfix reload
Better with Dovecot handing the mail over (so Dovecot can apply quotas and filters):
    virtual_transport = lmtp:unix:private/dovecot-lmtp          (and enable the lmtp service in Dovecot with a unix_listener at /var/spool/postfix/private/dovecot-lmtp)
Dovecot needs a user database for the virtual users (a passwd-file or SQL). Follow the Dovecot documentation for "virtual users": passdb passwd-file / userdb static with uid=5000 gid=5000 home=/var/vmail/%d/%n.
Check the maps:  sudo postmap -q alice@example.com hash:/etc/postfix/vmailbox     sudo postmap -q info@example.com hash:/etc/postfix/virtual
Hash maps need  postmap  after every edit. Alternatives: lmdb:, texthash: (no postmap needed), regexp:, pcre:, mysql:, pgsql:, ldap:.

SPF, DKIM and DMARC (so your mail is trusted, not spam-foldered)

SPF   A DNS record listing which servers may send for your domain (already added above). Only ONE SPF record per domain. "-all" fails everything else, "~all" is a softer version while you test
DKIM  Your server signs each message with a private key, and receivers check the signature with the public key in DNS
DMARC Tells receivers what to do when SPF and DKIM fail for the From domain, and where to send reports
Set up DKIM with OpenDKIM:
sudo apt install opendkim opendkim-tools                                             # RHEL: sudo dnf install opendkim opendkim-tools
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s mail -v          # creates mail.private (secret) and mail.txt (DNS record)
sudo chown -R opendkim:opendkim /etc/opendkim ; sudo chmod 600 /etc/opendkim/keys/example.com/mail.private
/etc/opendkim.conf (a working minimum):
    Syslog                  yes
    UMask                   002
    Mode                    sv
    Canonicalization        relaxed/simple
    Socket                  inet:8891@localhost
    KeyTable                /etc/opendkim/key.table
    SigningTable            refile:/etc/opendkim/signing.table
    InternalHosts           /etc/opendkim/trusted.hosts
/etc/opendkim/key.table:        mail._domainkey.example.com   example.com:mail:/etc/opendkim/keys/example.com/mail.private
/etc/opendkim/signing.table:    *@example.com   mail._domainkey.example.com
/etc/opendkim/trusted.hosts:    127.0.0.1
                                localhost
                                *.example.com
Publish the public key: sudo cat /etc/opendkim/keys/example.com/mail.txt   (copy the p=... value into a TXT record named mail._domainkey.example.com; join long strings)
Connect it to Postfix:
    sudo postconf -e 'milter_default_action = accept'
    sudo postconf -e 'milter_protocol = 6'
    sudo postconf -e 'smtpd_milters = inet:localhost:8891'
    sudo postconf -e 'non_smtpd_milters = $smtpd_milters'
sudo systemctl enable --now opendkim ; sudo systemctl restart postfix
Verify:  sudo opendkim-testkey -d example.com -s mail -vvv     then send a message to a Gmail address and choose "Show original": look for  dkim=pass  spf=pass  dmarc=pass
DMARC rollout: start with  p=none  and a reporting address, read the reports for a couple of weeks, then move to  p=quarantine  and finally  p=reject . Do this only when every legitimate sender is covered by SPF or DKIM.
Optional: checking SPF and DMARC on INCOMING mail (postfix-policyd-spf-python, opendmarc, or rspamd, which does SPF, DKIM, DMARC and spam scoring in one). Rspamd is the modern all-in-one choice.

Spam and virus filtering (short)

rspamd            Modern, fast, one tool for spam scoring, DKIM, SPF, DMARC and greylisting. Connects as a milter (smtpd_milters = inet:localhost:11332)
SpamAssassin      The classic content filter (spamd, with spamass-milter or amavis)
ClamAV            Virus scanning (clamav-milter or amavis)
postscreen        Built into Postfix: blocks zombie bots on port 25 before smtpd starts (uses DNSBL scores, pregreet and greylist tests)
postgrey          Greylisting: temporarily refuse first contact from unknown senders (delays some legitimate mail by minutes)
fail2ban          Ban IPs that fail authentication (enable the "postfix" and "postfix-sasl" jails: sudo apt install fail2ban)
Mail filtering per user: Sieve rules in Dovecot (dovecot-sieve, dovecot-managesieved).

Sending mail from programs and scripts

echo "body" | mail -s "Subject" you@example.com                               # mailutils/mailx
echo "body" | mail -s "Subject" -a "From: alerts@example.com" you@example.com   # choose the From address (mailutils uses -a, mailx uses -r)
printf 'Subject: Test\nFrom: alerts@example.com\nTo: you@example.com\n\nBody text\n' | sendmail -t          # a raw message with sendmail
echo "body" | mail -s "Report" -A /path/report.pdf you@example.com                # attachment (mailutils: -A)
Cron mail: set  MAILTO=you@example.com  at the top of the crontab. Output from cron jobs is mailed to that address (or to root, and then via /etc/aliases).
PHP/Python/Node applications: point them at localhost:25 (send-only server) or at the submission port 587 with the credentials, instead of embedding their own SMTP details.
Set a proper Return-Path and From on your messages, use your own domain, and avoid sending from a hostname that has no DNS records.

The queue: see, flush and clean it

postqueue -p           (or mailq)             list the queue: ID, size, time, sender, recipient, and the reason a message is waiting (in parentheses)
postqueue -f                                   try to deliver everything now (flush)
postqueue -i ID                                try one message now
sudo postsuper -d ID                           delete one message          sudo postsuper -d ALL    delete EVERYTHING (careful)         sudo postsuper -d ALL deferred    only deferred mail
sudo postsuper -h ID                           put a message on hold       sudo postsuper -H ID     release from hold         sudo postsuper -r ID    requeue (re-evaluate) a message
sudo postcat -q ID                             read a queued message (headers and body)
postqueue -p | grep -c '^[0-9A-F]'             number of queued messages
sudo qshape deferred                           a summary table of the deferred queue by domain and age (from postfix-doc/qshape)
Queues:  incoming (new)   active (being delivered)   deferred (delivery failed for now, retry later)   hold (parked by you)   corrupt
Common queue reasons:  "Connection timed out" (port 25 blocked or the destination is down)   "Host or domain name not found"   "Connection refused"   "451 4.7.1 Greylisted, try again"   "550 5.7.1 blocked by policy" (reputation)
Delete mail to one recipient:  mailq | awk 'BEGIN{RS=""} /baduser@example.com/ {print $1}' | tr -d '*!' | sudo postsuper -d -
Empty a huge deferred queue safely: fix the cause first (DNS, port 25, relay login), then  postqueue -f .

Logs: read them and find messages

Debian/Ubuntu: /var/log/mail.log          RHEL family: /var/log/maillog          Any: journalctl -u postfix  (and  journalctl -t postfix/smtp )        Errors only: /var/log/mail.err
A message leaves a trail. Follow it by its queue ID:
    Sep 26 10:15:01 mail postfix/pickup[1234]: A1B2C3D4E5: uid=1000 from=<alice>
    Sep 26 10:15:01 mail postfix/cleanup[1240]: A1B2C3D4E5: message-id=<20260926101501.A1B2C3D4E5@mail.example.com>
    Sep 26 10:15:01 mail postfix/qmgr[1100]: A1B2C3D4E5: from=<alice@example.com>, size=512, nrcpt=1 (queue active)
    Sep 26 10:15:02 mail postfix/smtp[1250]: A1B2C3D4E5: to=<bob@example.org>, relay=mx.example.org[198.51.100.5]:25, delay=1.2, delays=0.1/0/0.6/0.5, dsn=2.0.0, status=sent (250 2.0.0 OK queued as XYZ)
    Sep 26 10:15:02 mail postfix/qmgr[1100]: A1B2C3D4E5: removed
sudo grep A1B2C3D4E5 /var/log/mail.log                                   # everything about one message
sudo grep 'status=' /var/log/mail.log | awk -F'status=' '{print $2}' | cut -d' ' -f1 | sort | uniq -c        # counts of sent, deferred, bounced
sudo grep 'status=deferred' /var/log/mail.log | tail                      # why things are stuck
sudo grep 'status=bounced' /var/log/mail.log | tail
sudo grep -E 'reject|NOQUEUE' /var/log/mail.log | tail                   # what your restrictions are blocking
sudo grep 'sasl_username' /var/log/mail.log | tail                        # who authenticated to send
sudo pflogsumm /var/log/mail.log | less                                   # a daily summary report (sudo apt install postfix-pflogsumm)
Status words:  sent (delivered)   deferred (will retry: 4xx or a temporary problem)   bounced (permanent failure: 5xx, a bounce message goes to the sender)   expired (gave up after maximal_queue_lifetime)
SMTP replies: 2xx success, 4xx temporary failure (retry later), 5xx permanent failure. Enhanced codes like 5.1.1 (bad mailbox), 5.7.1 (policy or relay denied), 4.2.2 (mailbox full).
Turn up detail for one problem:  postconf -e 'debug_peer_list = 203.0.113.50' ; postconf -e 'debug_peer_level = 3' ; postfix reload   (remove afterwards).  Or run  postfix -v  to verbose one command.

Testing (do all of these before you call it done)

1. Configuration:        postfix check ; postconf -n | less ; sudo postfix reload
2. Local delivery:       echo test | mail -s test alice     then    sudo tail /var/log/mail.log    and check ~/Maildir/new/
3. Address resolution:   sendmail -bv alice@example.com     # shows how Postfix would route an address without sending it (-bv = verify)
4. Talk SMTP by hand:    telnet localhost 25       or       nc localhost 25
      EHLO test.example.com
      MAIL FROM:<you@example.com>
      RCPT TO:<alice@example.com>
      DATA
      Subject: hand test
      (a blank line, some text, then a single line with just a dot)
      .
      QUIT
5. TLS and login on 587: openssl s_client -starttls smtp -connect mail.example.com:587 -crlf      then    EHLO test    (look for  250-AUTH PLAIN LOGIN )
      swaks --to you@gmail.com --from alice@example.com --server mail.example.com --port 587 --tls --auth LOGIN --auth-user alice --auth-password 'PASSWORD'          (sudo apt install swaks)
6. Certificate check:    openssl s_client -connect mail.example.com:993 -showcerts </dev/null | openssl x509 -noout -dates -subject
7. DNS and reputation:   dig +short MX example.com ; dig +short -x YOUR_IP ; dig +short TXT example.com ; dig +short TXT mail._domainkey.example.com
      Send a message to a Gmail address: open it > "Show original" and confirm SPF: PASS, DKIM: PASS, DMARC: PASS. Online tools (mail-tester.com, MXToolbox) score your setup and check blocklists
8. Receiving from outside: send from a Gmail account to your server and watch the log
9. OPEN RELAY TEST (essential): from a machine OUTSIDE your network:
      swaks --server mail.example.com --to someone@gmail.com --from other@notyourdomain.com
   You MUST see a rejection such as  554 5.7.1 <someone@gmail.com>: Relay access denied . If the message is accepted, fix mynetworks and the restrictions immediately.

Security checklist

[ ] NOT an open relay (test 9 above). mynetworks contains only 127.0.0.0/8 (and truly trusted networks), never 0.0.0.0/0
[ ] TLS certificate valid and renewing; TLS 1.2 or newer; authentication ONLY over TLS (smtpd_tls_auth_only = yes on submission)
[ ] Port 25 offers STARTTLS but does not require it; submission (587) requires TLS and login
[ ] Strong passwords for mail users; fail2ban watching the postfix and dovecot logs; rate limits set (smtpd_client_message_rate_limit)
[ ] disable_vrfy_command = yes; no software version in the banner; keep Postfix, Dovecot and the OS patched
[ ] SPF, DKIM and DMARC published; PTR record matches the mail hostname
[ ] Firewall opens only the needed ports; a send-only server listens on loopback only
[ ] Files protected: /etc/postfix/sasl_passwd and its .db are mode 600, root-owned; DKIM private keys are readable only by opendkim
[ ] Monitor the queue length and the log; alert on a growing queue (it can mean an outbreak of spam from a compromised account)
[ ] Backups of /etc/postfix, /etc/dovecot, DKIM keys and /var/vmail or the users' Maildirs
[ ] Do not run a "backup MX" (a secondary that accepts and forwards mail) unless you configure it carefully. It becomes a spam magnet. Rate limit and validate recipients if you do
Compromised accounts are the usual cause of your IP being blocklisted. Watch for sudden spikes of outgoing mail (sudo grep sasl_username /var/log/mail.log | awk ... | sort | uniq -c), and change the password immediately.

Common problems

"Relay access denied" (554 5.7.1)                        The sender is not allowed to relay: they must authenticate on 587, or the destination domain is not one you accept mail for (mydestination / virtual_mailbox_domains)
"Mail for example.com loops back to myself"              mydomain/mydestination mismatch, or the MX points to this server but the domain is not in mydestination or virtual_mailbox_domains. Or relayhost points at yourself
"connect to mx.example.org[203.0.113.5]:25: Connection timed out"   Outbound port 25 is blocked (ISP or cloud). Use a relay on 587, or ask the provider to unblock it
"Host or domain name not found. Name service error for name=..."     DNS lookup failed. Check /etc/resolv.conf, and that resolv.conf is copied into the chroot (or set chroot=n in master.cf)
"Network is unreachable" for IPv6 addresses              The server has no IPv6 route. Set  inet_protocols = ipv4
"status=deferred (Connection refused)"                   The destination is not listening, or the relay port/host is wrong (check the brackets and the port)
"SASL authentication failed" (as a client to a relay)    Wrong username or password in sasl_passwd (run postmap again), or the relay needs a different port/security. Check for special characters
"SASL authentication failed" (users failing to log in)   Wrong password, wrong auth mechanism, the Dovecot socket permissions (mode 0660, user postfix), or Postfix cannot reach /var/spool/postfix/private/auth
"warning: SASL: Connect to private/auth failed: No such file or directory"   Dovecot is not running or its auth socket is not at /var/spool/postfix/private/auth
"Server certificate not verified" / "certificate verify failed"   Missing CA file (smtp_tls_CAfile), a self-signed certificate, or a name mismatch. Fix the certificate. Do not disable verification for the internet
"warning: database /etc/aliases.db is older than source file"     Run  sudo newaliases
"warning: hash:/etc/postfix/sasl_passwd.db is missing" / changes ignored   Run  sudo postmap /etc/postfix/sasl_passwd  (and again after each change)
"fatal: bind 0.0.0.0 port 25: Address already in use"    Another MTA (exim4, sendmail) or a second Postfix owns the port: sudo ss -tlnp | grep :25, stop or remove the other one
"postfix/postfix-script: fatal: the Postfix mail system is already running"   It is already up: use  postfix reload
"User unknown in local recipient table" (550 5.1.1)     The mailbox does not exist. Add the user, the alias (newaliases), or the virtual mailbox entry (postmap)
"Recipient address rejected: Domain not found"          The recipient's domain has no MX or A record (a typo, or their DNS is broken)
"Message size exceeds fixed limit"                       Raise message_size_limit (and the mailbox/attachment limits on the other side)
"queue file write error" / "No space left on device"     The disk is full (/var/spool/postfix or /var). Free space, then postqueue -f
Mail arrives but goes to spam                            Check PTR, SPF, DKIM and DMARC (test 7), the IP's reputation (blocklists), and message content. Send test mails to mail-tester.com. Warm up a new IP slowly
"550 5.7.1 ... blocked using zen.spamhaus.org" (bounces)   Your IP is on a blocklist: find the reason (an open relay, a compromised account, a residential IP range), fix it, then request delisting
Greylisting: "450 4.7.1 Greylisted"                      Normal. The sending server retries after a few minutes. If it never retries, the sender is misconfigured
"lost connection after CONNECT" / "timeout after ..."   Scanners or clients that hang up, a firewall, or the smtpd_tls settings breaking STARTTLS. Check smtpd logs with a test client
Users cannot send from their phone                       They must use port 587 (STARTTLS) or 465 with authentication, the username as the full address or as configured, and a valid certificate
Changes have no effect                                   Run postfix reload. Some settings (master.cf changes, inet_interfaces) need a restart. Check postconf -n to see what is really in effect
A "mail" program is not found                            Install mailutils (Debian) or s-nail/mailx (RHEL) and check  which sendmail  points to Postfix
"chroot" oddities (files, DNS or sockets not found)      Some master.cf services run chrooted in /var/spool/postfix. Copy needed files (resolv.conf, ssl certs) there, or set the chroot column to n for that service

Tips

    Start with the simplest thing that works: a send-only server or a relay. Build a full mail server only when you truly need to receive mail yourself.

    Before anything else, check outbound port 25. If your cloud provider blocks it, plan a relay on port 587 from the start.

    postconf -n shows only what you changed. It is the first thing to look at, and the first thing to paste when asking for help.

    Always run postfix check and postfix reload after edits, and watch the log with tail -f while you test.

    Test that you are NOT an open relay from an outside network. Never remove reject_unauth_destination or defer_unauth_destination.

    Set up PTR, SPF, DKIM and DMARC before you send real mail. Without them, even a perfect server lands in spam.

    Use TLS everywhere you can: STARTTLS on port 25 (optional), required TLS and login on 587, and TLS 1.2 or newer.

    Read the log by queue ID: grep the ID to see the whole story of one message. Learn sent, deferred and bounced.

    Watch the queue and outgoing volume. A sudden spike usually means a compromised account, and it will get your IP blocklisted.

    Keep Postfix and Dovecot patched, back up /etc/postfix and your DKIM keys, and document your setup.