--- name: mail-server-setup description: "Set up Postfix+Dovecot+DKIM mail server on Ubuntu." version: 1.0.0 author: Hermes Agent license: MIT metadata: hermes: tags: [mail, postfix, dovecot, dkim, smtp, imap, letsencrypt, email] --- # Mail Server Setup Complete mail server on Ubuntu: Postfix (SMTP/SMTPS/Submission), Dovecot (IMAPS), OpenDKIM signing, and Let's Encrypt TLS certificate. ## Prerequisites - Ubuntu 22.04+ with root/sudo access - DNS managed externally (Aliyun DNS, Cloudflare, etc.) - Port 25 may need manual unblock from cloud provider (Aliyun requires ticket) - A domain with MX record pointing to the server ## DNS Records Required Before starting, ensure these DNS records exist: | Type | Host | Value | Priority | |------|------|-------|----------| | A | mail.domain.com | | — | | MX | @ | mail.domain.com | 1 | | TXT | @ | v=spf1 mx ~all | — | | TXT | _dmarc | v=DMARC1; p=none; rua=mailto:admin@domain.com | — | | TXT | default._domainkey | (generated below) | — | ## Cloud Firewall Ports Open these TCP ports in the cloud security group: | Port | Purpose | |------|---------| | 25 | SMTP (requires manual unblock ticket on Aliyun) | | 465 | SMTPS (SSL-encrypted submission) | | 587 | Submission (STARTTLS) | | 993 | IMAPS (SSL-encrypted retrieval) | | 80 | HTTP (certbot renewal) | No local firewall needed if UFW is inactive (default on many Ubuntu images). ## Install Packages ```bash sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ postfix dovecot-core dovecot-imapd opendkim opendkim-tools ``` Debconf preseed avoids interactive postfix configuration prompts. ## Configure Postfix ```bash postconf -e myhostname=mail.domain.com postconf -e mydomain=domain.com postconf -e myorigin=/etc/mailname postconf -e mydestination=localhost postconf -e mynetworks=127.0.0.0/8 [::1]/128 postconf -e home_mailbox=Maildir/ postconf -e virtual_alias_maps=hash:/etc/postfix/virtual postconf -e smtpd_tls_security_level=may postconf -e smtp_tls_security_level=may postconf -e smtpd_sasl_type=dovecot postconf -e smtpd_sasl_path=private/auth postconf -e smtpd_sasl_auth_enable=yes postconf -e smtpd_tls_auth_only=yes postconf -e smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination ``` CRITICAL: use `key=value` syntax (no spaces around `=`) to avoid shell quoting issues. Enable submission (587) and smtps (465) in master.cf: ```bash sed -i 's/^#submission/submission/' /etc/postfix/master.cf sed -i 's/^#smtps/smtps/' /etc/postfix/master.cf # Also uncomment the -o lines under each ``` Set up virtual alias and Maildir: ```bash echo "domain.com" > /etc/mailname echo "admin@domain.com username" > /etc/postfix/virtual postmap /etc/postfix/virtual mkdir -p ~/Maildir/{cur,new,tmp} && chmod -R 700 ~/Maildir ``` ## Configure Dovecot Mail location → Maildir: ```bash sed -i 's|^mail_location = .*|mail_location = maildir:~/Maildir|' /etc/dovecot/conf.d/10-mail.conf ``` Auth config (SASL for Postfix): ```bash cat > /etc/dovecot/conf.d/10-auth.conf << 'EOF' disable_plaintext_auth = yes auth_mechanisms = plain login !include auth-system.conf.ext EOF ``` Master config (IMAPS only, SASL socket): ```bash cat > /etc/dovecot/conf.d/10-master.conf << 'EOF' service auth { unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } unix_listener auth-userdb { mode = 0600 user = USERNAME } } service imap-login { inet_listener imap { port = 0 } inet_listener imaps { port = 993; ssl = yes } } service pop3-login { inet_listener pop3 { port = 0 } inet_listener pop3s { port = 0 } } EOF ``` SSL (initially snakeoil): ```bash cat > /etc/dovecot/conf.d/10-ssl.conf << 'EOF' ssl = yes ssl_cert = /etc/dovecot/conf.d/10-ssl.conf << EOF ssl = yes ssl_cert = <$CERT ssl_key = <$KEY ssl_min_protocol = TLSv1.2 ssl_prefer_server_ciphers = yes EOF ``` ## Configure DKIM (OpenDKIM) Generate keys: ```bash mkdir -p /etc/opendkim/keys/domain.com && cd /etc/opendkim/keys/domain.com opendkim-genkey -s default -d domain.com chown root:root default.private && chmod 600 default.private ``` OpenDKIM config: ```bash cat > /etc/opendkim.conf << 'EOF' Syslog yes UMask 002 Domain domain.com KeyFile /etc/opendkim/keys/domain.com/default.private Selector default Mode sv Canonicalization relaxed/simple Socket inet:8891@localhost EOF ``` Add milter to Postfix and restart: ```bash postconf -e milter_default_action=accept postconf -e milter_protocol=6 postconf -e smtpd_milters=inet:localhost:8891 postconf -e non_smtpd_milters=inet:localhost:8891 systemctl restart opendkim postfix dovecot ``` Get the DKIM DNS record: ```bash cat /etc/opendkim/keys/domain.com/default.txt ``` ## Verification ```bash # Check ports sudo ss -tlnp | grep -E '25|465|587|993' # Test TLS (should show "Verify return code: 0 (ok)") echo "QUIT" | openssl s_client -starttls smtp -connect localhost:25 2>&1 | grep "Verify return code" echo "Q" | openssl s_client -connect localhost:993 2>&1 | grep "Verify return code" # Test local delivery echo "TEST" | mail -s "Test" username@localhost ls ~/Maildir/new/ ``` ## Pitfalls 1. **SSH quoting breaks config commands**: When running through SSH, use base64 encoding to transmit complex scripts. See `socks-proxy-download` skill → shell-quoting-workaround.md. 2. **postconf whitespace**: `postconf -e "key = value"` fails (shell consumes `=`). Always use `postconf -e key=value` (no spaces). 3. **opendkim key ownership**: Must match the running uid. If no `UserID` in config, opendkim runs as root → key must be owned by root. Error: "not owned by executing uid". 4. **Aliyun blocks port 25**: Requires manual ticket to unblock. 5. **DNS propagation delay**: Let's Encrypt validation may fail for newly added DNS records. Use `--dry-run` first. Retry after propagation. 6. **Dovecot SASL socket**: Must be at `/var/spool/postfix/private/auth` with mode 0660, group postfix — otherwise postfix can't authenticate users.