Skip to main content

Mail Server Setup - Technical Documentation

Table of Contents

  1. Overview
  2. System Configuration
  3. Package Installation
  4. Postfix Mail Transfer Agent
  5. SSL/TLS Configuration
  6. Anti-Spam Measures
  7. SASL Authentication
  8. Submission Port (587)
  9. Dovecot IMAP/POP3 Server
  10. OpenDKIM Email Signing
  11. Email Aliases
  12. User Creation
  13. Firewall Configuration
  14. Service Startup
  15. Helper Scripts
  16. DNS Configuration Timer
  17. SSL Certificate Acquisition

Overview

This script sets up a complete, production-ready mail server with the following components:

  • Postfix: Mail Transfer Agent (MTA) for sending/receiving email
  • Dovecot: IMAP/POP3 server for email retrieval
  • OpenDKIM: Email authentication and signing
  • Let's Encrypt: Free SSL/TLS certificates
  • Anti-spam: RBL checks, rate limiting, and authentication requirements
  • Security: Encrypted connections, authentication, firewall rules

The setup achieves a 9/10 mail-tester.com score and ensures high deliverability.


System Configuration

Package Management Configuration

sed -i "s/#\$nrconf{restart} = 'i';/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf

Purpose: Configures automatic service restarts during package updates.

  • needrestart normally prompts interactively when services need restarting
  • Setting to 'a' (automatic) allows unattended installation
  • Critical for automated deployments via StackScript
echo 'Dpkg::Options {"--force-confold";}' > /etc/apt/apt.conf.d/99-force-confold

Purpose: Preserves existing configuration files during package upgrades.

  • When packages are upgraded, dpkg may ask about config file changes
  • --force-confold keeps the currently installed version
  • Prevents installation from hanging on configuration prompts

System Updates

apt-get update -qq
apt-get upgrade -y -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"

Purpose: Updates package lists and upgrades all installed packages.

  • -qq: Quiet mode, minimal output
  • -y: Automatic yes to prompts
  • --force-confdef: Use default for new config files
  • --force-confold: Keep old config files when unchanged
  • Ensures system has latest security patches

Hostname Configuration

hostnamectl set-hostname "$HOSTNAME"

Purpose: Sets the system hostname (e.g., mx.itisajoke.net).

  • Used by Postfix to identify itself in SMTP communications
  • Appears in email headers
  • Must match your PTR (reverse DNS) record
grep -q "$HOSTNAME" /etc/hosts || echo "127.0.1.1 $HOSTNAME mail" >> /etc/hosts

Purpose: Adds hostname to /etc/hosts for local resolution.

  • 127.0.1.1 is the standard loopback for hostname
  • Ensures hostname resolves locally without DNS lookup
  • mail alias provides additional local name
  • grep -q checks if entry exists to avoid duplicates

Package Installation

Pre-configuration

debconf-set-selections <<< "postfix postfix/mailname string $DOMAIN"

Purpose: Pre-configures Postfix's mail domain name.

  • Prevents interactive prompts during installation
  • Sets the domain appended to local user emails
  • Example: user becomes user@itisajoke.net
debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'"

Purpose: Configures Postfix for direct internet mail delivery.

  • Internet Site: Sends and receives mail directly
  • Alternative would be "Satellite" (relay through another server)
  • Required for running an independent mail server
debconf-set-selections <<< "dovecot-core dovecot-core/create-ssl-cert boolean true"
debconf-set-selections <<< "dovecot-core dovecot-core/ssl-cert-name string $HOSTNAME"

Purpose: Pre-configures Dovecot's SSL certificate settings.

  • Creates initial self-signed certificate during installation
  • Sets certificate CN (Common Name) to hostname
  • Later replaced with Let's Encrypt certificate

Package Installation

apt-get install -y -qq postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
certbot mailutils dnsutils opendkim opendkim-tools

Packages installed:

  • postfix: SMTP server for sending/receiving mail
  • dovecot-core: Base IMAP/POP3 server
  • dovecot-imapd: IMAP protocol support (port 143/993)
  • dovecot-pop3d: POP3 protocol support (port 110/995)
  • dovecot-lmtpd: Local Mail Transfer Protocol (Postfix → Dovecot delivery)
  • certbot: Let's Encrypt SSL certificate manager
  • mailutils: Command-line mail utilities (mail, mailx commands)
  • dnsutils: DNS lookup tools (dig, nslookup)
  • opendkim: DKIM signing daemon
  • opendkim-tools: DKIM key generation and testing utilities

Stop Services

systemctl stop postfix dovecot opendkim 2>/dev/null || true

Purpose: Stops services before configuration.

  • Prevents services from reading partially-written config files
  • 2\>/dev/null suppresses errors if services aren't running
  • || true prevents script failure if stop fails

Postfix Mail Transfer Agent

Basic Configuration

postconf -e "myhostname = $HOSTNAME"

Purpose: Sets the mail server's hostname.

  • Used in SMTP HELO/EHLO commands
  • Appears in Received: headers
  • Must match PTR record for best deliverability
  • Example: mx.itisajoke.net
postconf -e "mydomain = $DOMAIN"

Purpose: Sets the base domain for the mail server.

  • Used for email addresses on this server
  • Example: itisajoke.net
  • Local usernames become user@itisajoke.net
postconf -e 'myorigin = $mydomain'

Purpose: Sets the domain that appears in outgoing mail FROM addresses.

  • When local users send mail, this domain is appended
  • root sends as root@itisajoke.net (not root@mx.itisajoke.net)
  • Uses the $mydomain variable defined above
postconf -e 'inet_interfaces = all'

Purpose: Listens on all network interfaces.

  • all: Accepts connections from any interface
  • localhost: Would only accept local connections
  • Required for receiving mail from the internet
postconf -e 'inet_protocols = ipv4'

Purpose: Restricts to IPv4 only.

  • Simplifies configuration for IPv4-only setups
  • Change to all to support both IPv4 and IPv6
  • Many providers don't provide IPv6 or it requires additional setup
postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain'

Purpose: Defines which domains this server accepts mail for (final destination).

  • $myhostname: mx.itisajoke.net (mail to server itself)
  • localhost.$mydomain: localhost.itisajoke.net
  • localhost: Local machine
  • $mydomain: itisajoke.net (main domain)
  • Mail to any of these domains is delivered locally, not relayed
postconf -e 'mynetworks = 127.0.0.0/8'

Purpose: Defines trusted networks that can send mail without authentication.

  • 127.0.0.0/8: Local machine only
  • Prevents open relay (would allow anyone to send mail through your server)
  • External users MUST authenticate to send mail
postconf -e 'home_mailbox = Maildir/'

Purpose: Specifies mailbox format and location.

  • Maildir/: One-message-per-file format (recommended)
  • Alternative: mbox (single file, prone to corruption)
  • Stored in user's home: /home/username/Maildir/
  • Trailing / is important - indicates Maildir format
postconf -e 'smtpd_banner = $myhostname ESMTP'

Purpose: Sets the greeting banner when clients connect.

  • ESMTP: Extended SMTP (supports modern features)
  • Hides Postfix version number (security through obscurity)
  • Default would show: $myhostname ESMTP Postfix (Ubuntu)
postconf -e 'message_size_limit = 10485760'

Purpose: Maximum message size in bytes.

  • 10485760 bytes: 10 MB
  • Prevents abuse and disk filling
  • Rejects messages larger than this limit
  • Includes headers + body + attachments
postconf -e 'mailbox_size_limit = 0'

Purpose: Maximum mailbox size per user.

  • 0: Unlimited
  • Would set a quota in bytes if non-zero
  • Users can accumulate unlimited email storage
  • Consider setting a limit for production (e.g., 1GB = 1073741824)

SSL/TLS Configuration

Initial Self-Signed Certificate

CERT_DIR="/etc/letsencrypt/live/$HOSTNAME"
mkdir -p "$CERT_DIR"

Purpose: Creates directory for SSL certificates.

  • Uses Let's Encrypt's standard directory structure
  • Even though initially self-signed, uses same path
  • Let's Encrypt will replace these files later
openssl req -new -x509 -days 365 -nodes \
-out "$CERT_DIR/fullchain.pem" \
-keyout "$CERT_DIR/privkey.pem" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=$HOSTNAME"

Purpose: Generates temporary self-signed SSL certificate.

  • -x509: Self-signed certificate (not a CSR)
  • -days 365: Valid for one year
  • -nodes: No password protection on private key
  • fullchain.pem: Certificate file
  • privkey.pem: Private key file
  • CN=$HOSTNAME: Certificate matches hostname
  • Allows encrypted connections immediately
  • Replaced with Let's Encrypt certificate after DNS setup

Postfix TLS Configuration

postconf -e "smtpd_tls_cert_file = $CERT_DIR/fullchain.pem"
postconf -e "smtpd_tls_key_file = $CERT_DIR/privkey.pem"

Purpose: Tells Postfix where SSL certificate and key are located.

  • smtpd_tls_cert_file: Public certificate
  • smtpd_tls_key_file: Private key
  • Used for STARTTLS on port 25 and 587
postconf -e 'smtpd_tls_security_level = may'

Purpose: Enables opportunistic TLS encryption.

  • may: Offers TLS but doesn't require it
  • encrypt: Would require TLS (breaks mail from servers without TLS)
  • none: Would disable TLS
  • Best balance between security and compatibility
postconf -e 'smtpd_tls_auth_only = yes'

Purpose: Requires TLS encryption for SASL authentication.

  • Authentication credentials sent only over encrypted connections
  • Prevents password sniffing
  • Combined with smtpd_tls_security_level = may:
    • TLS is optional for receiving mail
    • TLS is required for authenticated sending
postconf -e 'smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'

Purpose: Disables insecure TLS versions.

  • !: Negation (disable)
  • SSLv2, SSLv3: Ancient, completely broken
  • TLSv1, TLSv1.1: Deprecated, have known vulnerabilities
  • Only allows TLSv1.2 and TLSv1.3 (secure modern protocols)
postconf -e 'smtpd_tls_ciphers = high'

Purpose: Requires strong encryption ciphers.

  • high: Strong ciphers only (AES, etc.)
  • medium: Would allow weaker ciphers
  • export: Would allow very weak export-grade ciphers
  • Prevents downgrade attacks
postconf -e 'smtp_tls_security_level = may'

Purpose: Enables TLS for outgoing mail (when possible).

  • smtp (not smtpd): Outgoing connections
  • may: Use TLS if recipient server supports it
  • Encrypts mail in transit when possible
  • Doesn't fail if recipient doesn't support TLS
postconf -e 'smtpd_tls_loglevel = 1'

Purpose: Logs TLS connection information.

  • 0: No TLS logging
  • 1: Log TLS handshake summary
  • 2+: Very verbose (debug only)
  • Helps troubleshoot TLS connection issues

Anti-Spam Measures

HELO Restrictions

postconf -e 'smtpd_helo_required = yes'

Purpose: Requires HELO/EHLO command from connecting clients.

  • HELO: SMTP greeting command
  • Spammers often skip HELO to save time
  • Legitimate mail servers always send HELO
  • Rejects connections without proper SMTP greeting
postconf -e 'smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname, permit'

Purpose: Validates HELO/EHLO hostname.

Rules applied in order:

  1. permit_mynetworks: Allow local connections (127.0.0.0/8)
  2. permit_sasl_authenticated: Allow authenticated users
  3. reject_invalid_helo_hostname: Reject malformed hostnames
  4. reject_non_fqdn_helo_hostname: Reject non-FQDN (e.g., "localhost")
  5. reject_unknown_helo_hostname: Reject if HELO hostname has no DNS record
  6. permit: Allow everything else

Blocks spammers using fake or invalid HELO hostnames.

Sender Restrictions

postconf -e 'smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, reject_unknown_sender_domain, permit'

Purpose: Validates sender (FROM) address.

Rules:

  1. permit_mynetworks: Allow local senders
  2. permit_sasl_authenticated: Allow authenticated users
  3. reject_non_fqdn_sender: Reject if sender isn't fully qualified (e.g., "user" instead of "user@domain.com")
  4. reject_unknown_sender_domain: Reject if sender domain has no MX/A record
  5. permit: Allow everything else

Prevents spoofed sender addresses from non-existent domains.

Recipient Restrictions

postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net, permit'

Purpose: Most important anti-spam layer - validates recipients and checks blacklists.

Rules:

  1. permit_mynetworks: Allow local delivery
  2. permit_sasl_authenticated: Allow authenticated users to send anywhere
  3. reject_non_fqdn_recipient: Reject malformed recipient addresses
  4. reject_unknown_recipient_domain: Reject if recipient domain doesn't exist
  5. reject_unauth_destination: CRITICAL - Prevents open relay (only accept mail for domains in mydestination)
  6. reject_rbl_client zen.spamhaus.org: Check Spamhaus blacklist
  7. reject_rbl_client bl.spamcop.net: Check SpamCop blacklist
  8. permit: Allow if all checks pass

Without reject_unauth_destination, server would be an open relay (anyone could send mail through it).

Relay Restrictions

postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'

Purpose: Controls who can relay mail through this server.

Rules:

  1. permit_mynetworks: Local network can relay
  2. permit_sasl_authenticated: Authenticated users can relay
  3. reject_unauth_destination: Reject all other relay attempts

Simplified version of recipient_restrictions focused solely on relay control.

Rate Limiting

postconf -e 'smtpd_client_connection_count_limit = 10'

Purpose: Maximum simultaneous connections per client IP.

  • 10: One IP can have 10 concurrent SMTP connections
  • Prevents connection flooding
  • Legitimate servers rarely need more than 2-3
  • Slows down spam zombies
postconf -e 'smtpd_client_connection_rate_limit = 30'

Purpose: Maximum new connections per time unit per client.

  • 30: One IP can make 30 new connections per minute
  • Blocks rapid-fire connection attempts
  • Legitimate use rarely exceeds this
  • Stops brute force attacks
postconf -e 'smtpd_client_message_rate_limit = 100'

Purpose: Maximum messages per time unit per client.

  • 100: One client can send 100 messages per minute
  • Prevents spam floods
  • Normal users send 1-10 messages per minute
  • Allows bulk sending but prevents abuse

SASL Authentication

Dovecot SASL Integration

postconf -e 'smtpd_sasl_type = dovecot'

Purpose: Use Dovecot for SMTP authentication instead of Cyrus SASL.

  • dovecot: Modern, integrated with Dovecot
  • cyrus: Older, separate daemon
  • Single authentication system for IMAP and SMTP
  • Simpler configuration
postconf -e 'smtpd_sasl_path = private/auth'

Purpose: Unix socket path for Dovecot authentication.

  • private/auth: Relative to Postfix chroot /var/spool/postfix/
  • Full path: /var/spool/postfix/private/auth
  • Dovecot creates this socket
  • Postfix connects to it for authentication
postconf -e 'smtpd_sasl_auth_enable = yes'

Purpose: Enables SASL authentication.

  • Allows clients to authenticate with username/password
  • Required for users to send mail through server
  • Without this, only local users can send
postconf -e 'smtpd_sasl_security_options = noanonymous'

Purpose: Disables anonymous authentication.

  • noanonymous: Requires username/password
  • noplaintext: Would disable plain passwords (too strict for most)
  • Prevents unauthenticated sending
postconf -e 'broken_sasl_auth_clients = yes'

Purpose: Compatibility for older email clients.

  • Some old clients (Outlook Express, old Thunderbird) use non-standard AUTH
  • Enables compatibility modes
  • Harmless to enable, helps ancient clients

Submission Port (587)

Modern email clients should use port 587 (submission) instead of port 25 (SMTP) for sending mail.

postconf -M submission/inet="submission inet n - y - - smtpd"

Purpose: Enables submission service on port 587.

  • submission: Service name
  • inet: Network service
  • n: No privileged mode
  • -: No chroot (uses default)
  • y: Unpriv (can run as non-root)
  • -: No wakeup time
  • -: No process limit
  • smtpd: Use smtpd daemon
postconf -P "submission/inet/syslog_name=postfix/submission"

Purpose: Separate log prefix for submission port.

  • Logs show postfix/submission instead of postfix/smtp
  • Easier to distinguish port 25 vs 587 traffic in logs
postconf -P "submission/inet/smtpd_tls_security_level=encrypt"

Purpose: Requires TLS encryption on port 587.

  • encrypt: Mandatory TLS (vs "may" which is optional)
  • Port 587 is submission port - should always use TLS
  • Protects authentication credentials
postconf -P "submission/inet/smtpd_sasl_auth_enable=yes"

Purpose: Enables authentication on submission port.

  • Users must authenticate to send mail
  • Prevents unauthorized relay
postconf -P "submission/inet/smtpd_tls_auth_only=yes"

Purpose: Requires TLS before allowing authentication.

  • Prevents sending passwords in cleartext
  • Combined with smtpd_tls_security_level=encrypt
  • Double protection for credentials
postconf -P "submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"

Purpose: Only authenticated users can send mail.

  • permit_sasl_authenticated: If authenticated, allow
  • reject: Reject everything else
  • Simpler than main recipient_restrictions
  • Port 587 is only for authenticated sending
postconf -P "submission/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject"

Purpose: Relay control for submission port.

  • Mirrors recipient_restrictions
  • Authenticated users can relay
  • Non-authenticated cannot
postconf -P "submission/inet/milter_macro_daemon_name=ORIGINATING"

Purpose: Marks mail from port 587 as originating (not relayed).

  • ORIGINATING: This mail is from our users
  • Affects DKIM signing behavior
  • Some filters treat originating mail differently
  • Helps reputation systems

Dovecot IMAP/POP3 Server

Authentication Settings

sed -i 's/^#disable_plaintext_auth = yes/disable_plaintext_auth = yes/' /etc/dovecot/conf.d/10-auth.conf

Purpose: Disables plaintext authentication over unencrypted connections.

  • disable_plaintext_auth = yes: Requires TLS for authentication
  • Prevents password sniffing
  • Uncomments the setting (removes #)
sed -i 's/^auth_mechanisms = plain$/auth_mechanisms = plain login/' /etc/dovecot/conf.d/10-auth.conf

Purpose: Enables both PLAIN and LOGIN authentication mechanisms.

  • plain: Standard SASL PLAIN (username + password in one command)
  • login: Older mechanism (username and password separate)
  • LOGIN needed for old email clients (Outlook, old Android)

SSL/TLS Configuration

sed -i 's|^ssl = yes|ssl = required|' /etc/dovecot/conf.d/10-ssl.conf

Purpose: Requires SSL/TLS for all connections.

  • required: No plaintext connections allowed
  • yes: Would allow plaintext connections too
  • Forces clients to use IMAPS (993) or POP3S (995)
sed -i "s|^ssl_cert = <.*|ssl_cert = <$CERT_DIR/fullchain.pem|" /etc/dovecot/conf.d/10-ssl.conf
sed -i "s|^ssl_key = <.*|ssl_key = <$CERT_DIR/privkey.pem|" /etc/dovecot/conf.d/10-ssl.conf

Purpose: Points Dovecot to SSL certificate files.

  • ssl_cert: Public certificate
  • ssl_key: Private key
  • <: Indicates file content (Dovecot syntax)
  • Uses same certificate as Postfix

Mailbox Location

sed -i 's|^mail_location = .*|mail_location = maildir:~/Maildir|' /etc/dovecot/conf.d/10-mail.conf

Purpose: Specifies mailbox format and location.

  • maildir: Maildir format (one file per message)
  • ~/Maildir: In user's home directory
  • Must match Postfix's home_mailbox setting
  • Alternative would be mbox:~/mail for mbox format

Postfix Integration

cat >> /etc/dovecot/conf.d/10-master.conf << 'EOF'

service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
EOF

Purpose: Creates authentication socket for Postfix.

  • unix_listener: Creates Unix domain socket
  • /var/spool/postfix/private/auth: Same path Postfix expects
  • mode = 0660: Read/write for owner and group
  • user = postfix, group = postfix: Postfix can access socket
  • Allows Postfix to authenticate SMTP users through Dovecot

Certificate Directory Permissions

chmod 755 /etc/letsencrypt/{live,archive}

Purpose: Allows Dovecot to read Let's Encrypt certificates.

  • 755: rwxr-xr-x (owner full, others read+execute)
  • By default, Let's Encrypt directories are 700 (owner only)
  • Dovecot runs as different user, needs read access
  • Only affects directories, not private keys (which stay 600)

OpenDKIM Email Signing

DKIM (DomainKeys Identified Mail) cryptographically signs outgoing emails to prove they came from your server.

Directory Setup

mkdir -p /etc/opendkim/keys/$DOMAIN
chown -R opendkim:opendkim /etc/opendkim
chmod 700 /etc/opendkim/keys

Purpose: Creates directories for DKIM configuration and keys.

  • /etc/opendkim/keys/$DOMAIN: Stores private signing keys per domain
  • opendkim:opendkim: Owned by opendkim daemon
  • 700: Only opendkim user can access (private keys are sensitive)

Main Configuration File

cat > /etc/opendkim.conf << 'EOF'
Syslog yes
SyslogSuccess yes
Canonicalization relaxed/simple
Mode sv
SubDomains no
AutoRestart yes
SignatureAlgorithm rsa-sha256
UserID opendkim
Socket local:/var/spool/postfix/opendkim/opendkim.sock
PidFile /run/opendkim/opendkim.pid
KeyTable refile:/etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
ExternalIgnoreList /etc/opendkim/trusted.hosts
InternalHosts /etc/opendkim/trusted.hosts
EOF

Configuration explained:

  • Syslog yes: Log to syslog
  • SyslogSuccess yes: Log successful signatures (helps debugging)
  • Canonicalization relaxed/simple:
    • relaxed: Allows whitespace changes in headers
    • simple: Body must be exact
    • Balance between compatibility and security
  • Mode sv:
    • s: Sign outgoing mail
    • v: Verify incoming mail signatures
  • SubDomains no: Don't sign for subdomains
  • AutoRestart yes: Restart on failure
  • SignatureAlgorithm rsa-sha256: Modern, secure algorithm
  • UserID opendkim: Run as opendkim user
  • Socket: Unix socket for Postfix communication
  • PidFile: Process ID file location
  • KeyTable: Maps selectors to key files
  • SigningTable: Maps email addresses to selectors
  • ExternalIgnoreList / InternalHosts: Trusted hosts that bypass checks

Key Table

echo "$PREFIX._domainkey.$DOMAIN $DOMAIN:$PREFIX:/etc/opendkim/keys/$DOMAIN/$PREFIX.private" > /etc/opendkim/key.table

Purpose: Maps DKIM selectors to private key files.

Format: selector domain:selector:keyfile

Example: mx._domainkey.itisajoke.net itisajoke.net:mx:/etc/opendkim/keys/itisajoke.net/mx.private

  • mx._domainkey.itisajoke.net: DNS record name
  • itisajoke.net:mx: Domain and selector
  • keyfile: Path to private key

Signing Table

echo "*@$DOMAIN $PREFIX._domainkey.$DOMAIN" > /etc/opendkim/signing.table

Purpose: Maps sender addresses to DKIM keys.

Format: pattern selector

Example: \1*\2itisajoke.net mx._domainkey.itisajoke.net`

  • \1*\2itisajoke.net: All addresses from this domain
  • mx._domainkey.itisajoke.net: Use this key to sign

Trusted Hosts

cat > /etc/opendkim/trusted.hosts << EOF
127.0.0.1
localhost
$SERVER_IP
$HOSTNAME
$DOMAIN
*.${DOMAIN}
EOF

Purpose: Lists hosts allowed to send mail without DKIM requirements.

  • 127.0.0.1, localhost: Local machine
  • $SERVER_IP: This server's IP
  • $HOSTNAME: This server's hostname
  • $DOMAIN: Main domain
  • \1*\2${DOMAIN}: All subdomains
  • Mail from these sources is signed but verification isn't enforced

DKIM Key Generation

opendkim-genkey -b 2048 -d "$DOMAIN" -D "/etc/opendkim/keys/$DOMAIN" -s $PREFIX -v

Purpose: Generates RSA key pair for DKIM signing.

  • -b 2048: 2048-bit key (secure, widely supported)
  • -d "$DOMAIN": Domain name
  • -D "path": Output directory
  • -s $PREFIX: Selector (e.g., "mx")
  • -v: Verbose output

Creates two files:

  • mx.private: Private key (keep secret!)
  • mx.txt: Public key for DNS
chown -R opendkim:opendkim /etc/opendkim
chmod 600 /etc/opendkim/keys/$DOMAIN/$PREFIX.private

Purpose: Secures private key.

  • opendkim:opendkim: Only opendkim can access
  • 600: Owner can read/write, nobody else can access
  • Private key must be kept secret

DKIM Public Key Extraction

DKIM_KEY_FILE="/etc/opendkim/keys/$DOMAIN/$PREFIX.txt"
DKIM_PUBLIC_KEY=$(grep '"' "$DKIM_KEY_FILE" | tr -d '\n\t "()' | sed -n 's/.*p=\([A-Za-z0-9+\/=]*\).*/\1/p')
echo "v=DKIM1; k=rsa; p=$DKIM_PUBLIC_KEY" > /root/dkim-public-key.txt

Purpose: Extracts public key for DNS record.

  • grep '"': Gets lines with quotes (the key data)
  • tr -d '\n\t "()': Removes formatting
  • sed: Extracts base64 key after p=
  • v=DKIM1: DKIM version
  • k=rsa: RSA key type
  • p=: Public key data

Output file /root/dkim-public-key.txt contains exactly what goes in DNS TXT record.

Postfix Milter Integration

mkdir -p /var/spool/postfix/opendkim
chown opendkim:postfix /var/spool/postfix/opendkim
chmod 750 /var/spool/postfix/opendkim

Purpose: Creates directory for OpenDKIM socket inside Postfix chroot.

  • opendkim:postfix: opendkim creates, postfix reads
  • 750: rwxr-x--- (owner full, group read+execute)
  • Inside Postfix chroot so Postfix can access it

OpenDKIM Service Configuration

mkdir -p /etc/systemd/system/opendkim.service.d
cat > /etc/systemd/system/opendkim.service.d/override.conf << 'EOF'
[Service]
ExecStartPre=/bin/mkdir -p /var/spool/postfix/opendkim
ExecStartPre=/bin/chown opendkim:postfix /var/spool/postfix/opendkim
ExecStartPre=/bin/chmod 750 /var/spool/postfix/opendkim
EOF

Purpose: Ensures socket directory exists before OpenDKIM starts.

  • ExecStartPre: Runs before service starts
  • Creates directory if missing (after reboot)
  • Sets correct permissions
  • systemd override: Doesn't modify main service file

Postfix Milter Configuration

postconf -e 'milter_default_action = accept'

Purpose: What to do if milter fails.

  • accept: Deliver mail even if DKIM signing fails
  • reject: Would reject mail if signing fails (too strict)
  • tempfail: Would defer mail (retry later)
  • Prevents mail loss if OpenDKIM crashes
postconf -e 'milter_protocol = 6'

Purpose: Milter protocol version.

  • 6: Modern protocol version
  • Required for OpenDKIM
  • Older versions (2, 3) lack features
postconf -e 'smtpd_milters = local:opendkim/opendkim.sock'

Purpose: Milters for incoming mail (SMTP daemon).

  • local:: Unix socket (not network)
  • opendkim/opendkim.sock: Relative to Postfix chroot
  • Full path: /var/spool/postfix/opendkim/opendkim.sock
  • Processes all incoming SMTP connections
postconf -e 'non_smtpd_milters = $smtpd_milters'

Purpose: Milters for mail injected locally (not via SMTP).

  • $smtpd_milters: Use same milters as SMTP
  • Covers mail from sendmail command
  • Ensures all outgoing mail is signed

Email Aliases

grep -q "^postmaster:" /etc/aliases || echo "postmaster: root" >> /etc/aliases
grep -q "^abuse:" /etc/aliases || echo "abuse: root" >> /etc/aliases

Purpose: Creates required email aliases.

  • postmaster@domain: Required by RFC 5321 (mail admin contact)
  • abuse@domain: Spam/abuse reports
  • Both redirect to root account
  • grep -q: Only add if not already present
sed -i '/^root:/d' /etc/aliases
echo "root: $SSL_EMAIL" >> /etc/aliases

Purpose: Redirects root's mail to real email address.

  • sed -i '/^root:/d': Removes any existing root alias
  • echo: Adds new root alias
  • System messages (cron, security) sent to real address
  • Ensures you get server notifications
newaliases

Purpose: Rebuilds alias database.

  • Converts /etc/aliases text file to binary database
  • Postfix reads binary format for performance
  • Must run after any alias changes

User Creation

if ! id "$TEST_USERNAME" &>/dev/null; then
useradd -m -s /bin/bash "$TEST_USERNAME"
echo "$TEST_USERNAME:$TEST_PASSWORD" | chpasswd
mkdir -p /home/$TEST_USERNAME/Maildir/{new,cur,tmp}
mkdir -p /home/$TEST_USERNAME/Maildir/.{Sent,Trash,Drafts}/{new,cur,tmp}
chown -R $TEST_USERNAME:$TEST_USERNAME /home/$TEST_USERNAME/Maildir
chmod -R 700 /home/$TEST_USERNAME/Maildir
fi

Purpose: Creates initial mail user with Maildir structure.

Steps:

  1. id "$TEST_USERNAME": Check if user exists
  2. useradd -m -s /bin/bash: Create user with home directory and bash shell
  3. chpasswd: Set password
  4. mkdir Maildir: Create Maildir structure
    • new/: New unread messages
    • cur/: Current (read) messages
    • tmp/: Temporary storage during delivery
  5. Subdirectories: Special folders (Sent, Trash, Drafts)
    • .Sent: Period prefix indicates subfolder
    • Each has new/cur/tmp structure
  6. chown: Make user own maildir
  7. chmod 700: Only user can access their mail

Firewall Configuration

if command -v ufw &> /dev/null; then
ufw allow 22/tcp comment 'SSH'
ufw allow 25/tcp comment 'SMTP'
ufw allow 80/tcp comment 'HTTP'
ufw allow 587/tcp comment 'Submission'
ufw allow 993/tcp comment 'IMAPS'
ufw allow 995/tcp comment 'POP3S'
ufw --force enable
fi

Purpose: Opens required firewall ports.

Ports:

  • 22/tcp: SSH (remote administration)
  • 25/tcp: SMTP (incoming mail from other servers)
  • 80/tcp: HTTP (Let's Encrypt certificate validation)
  • 587/tcp: Submission (authenticated mail sending)
  • 993/tcp: IMAPS (IMAP over SSL/TLS)
  • 995/tcp: POP3S (POP3 over SSL/TLS)

Note: Unencrypted ports (110, 143) are NOT opened because ssl = required in Dovecot.

--force enable: Enables firewall without prompting (automated install)


Service Startup

systemctl daemon-reload

Purpose: Reload systemd configuration.

  • Reads new/modified service files
  • Picks up OpenDKIM override configuration
  • Required after creating override.conf
systemctl restart opendkim && systemctl enable opendkim
sleep 3

Purpose: Start OpenDKIM and enable on boot.

  • restart: Start (or restart if running)
  • enable: Start automatically on server boot
  • sleep 3: Wait for socket creation
chown opendkim:postfix /var/spool/postfix/opendkim/opendkim.sock
chmod 660 /var/spool/postfix/opendkim/opendkim.sock

Purpose: Fix socket permissions (critical for DKIM signing).

  • Default: opendkim:opendkim with 755
  • Need: opendkim:postfix with 660
  • 660: rw-rw---- (owner and group can read/write)
  • postfix group: Allows Postfix to write to socket
  • Without this, emails won't be DKIM signed
systemctl restart postfix && systemctl enable postfix
systemctl restart dovecot && systemctl enable dovecot

Purpose: Start mail services and enable on boot.

  • All configuration loaded
  • Services will auto-start on reboot
sleep 3

Purpose: Wait for services to fully start.

  • Services may take a moment to bind ports
  • Ensures they're ready before script continues

Helper Scripts

Add Mail User Script

cat > /root/add-mail-user.sh << 'EOFSCRIPT'
#!/bin/bash
[ $# -ne 2 ] && { echo "Usage: $0 <username> <password>"; exit 1; }
USERNAME=$1; PASSWORD=$2; DOMAIN=$(postconf -h mydomain)
id "$USERNAME" &>/dev/null && { echo "User exists"; exit 1; }
useradd -m -s /bin/bash "$USERNAME"
echo "$USERNAME:$PASSWORD" | chpasswd
mkdir -p /home/$USERNAME/Maildir/{new,cur,tmp}
mkdir -p /home/$USERNAME/Maildir/.{Sent,Trash,Drafts}/{new,cur,tmp}
chown -R $USERNAME:$USERNAME /home/$USERNAME/Maildir
chmod -R 700 /home/$USERNAME/Maildir
echo "Created: $USERNAME@$DOMAIN"
EOFSCRIPT

Purpose: Simplifies adding new mail users.

Usage: /root/add-mail-user.sh john SecurePassword123

Steps:

  1. Check for 2 arguments (username, password)
  2. Get domain from Postfix config
  3. Check user doesn't already exist
  4. Create system user
  5. Set password
  6. Create Maildir structure
  7. Set ownership and permissions
  8. Confirm creation

Test Mail Script

cat > /root/test-mail.sh << 'EOFSCRIPT'
#!/bin/bash
DOMAIN=$(postconf -h mydomain)
HOSTNAME=$(postconf -h myhostname)
echo "Mail Server: $HOSTNAME"
systemctl is-active postfix && echo " [OK] Postfix" || echo " [FAIL] Postfix"
systemctl is-active dovecot && echo " [OK] Dovecot" || echo " [FAIL] Dovecot"
systemctl is-active opendkim && echo " [OK] OpenDKIM" || echo " [FAIL] OpenDKIM"
[ $# -eq 1 ] && {
echo "Sending test to $1..."
echo "Test from $HOSTNAME at $(date)" | mail -s "Mail Test $(date +%s)" $1
echo "Check: tail -f /var/log/mail.log"
} || echo "Usage: $0 <recipient@example.com>"
EOFSCRIPT

Purpose: Tests mail server functionality.

Usage: /root/test-mail.sh test@mail-tester.com

Features:

  1. Shows server hostname
  2. Checks service status (Postfix, Dovecot, OpenDKIM)
  3. Sends test email if recipient provided
  4. Suggests checking mail log

DNS Verification Script

cat > /root/verify-dns.sh << 'EOFSCRIPT'
#!/bin/bash
DOMAIN=$(postconf -h mydomain)
HOSTNAME=$(postconf -h myhostname)
SERVER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -n1)

# Extract prefix from hostname (e.g., "mx" from "mx.itisajoke.net")
PREFIX="${HOSTNAME%%.*}"

echo "DNS VERIFICATION"
echo "=================="
A_RECORD=$(dig @1.1.1.1 +short "$HOSTNAME" | head -n1)
[ "$A_RECORD" == "$SERVER_IP" ] && echo "[OK] A: $HOSTNAME -> $A_RECORD" || echo "[FAIL] A record missing"
MX_RECORD=$(dig @1.1.1.1 +short MX "$DOMAIN" | sort -n | head -n1 | awk '{print $2}')
[[ "$MX_RECORD" == *"$PREFIX.$DOMAIN"* ]] && echo "[OK] MX: $MX_RECORD" || echo "[FAIL] MX missing"
SPF_RECORD=$(dig @1.1.1.1 +short TXT "$DOMAIN" | grep "v=spf1")
[ -n "$SPF_RECORD" ] && echo "[OK] SPF found" || echo "[FAIL] SPF missing"
DKIM_RECORD=$(dig @1.1.1.1 +short TXT "$PREFIX._domainkey.$DOMAIN" | grep "v=DKIM1")
[ -n "$DKIM_RECORD" ] && echo "[OK] DKIM found" || echo "[FAIL] DKIM missing"
DMARC_RECORD=$(dig @1.1.1.1 +short TXT "_dmarc.$DOMAIN" | grep "v=DMARC1")
[ -n "$DMARC_RECORD" ] && echo "[OK] DMARC found" || echo "[FAIL] DMARC missing"
PTR_RECORD=$(dig @1.1.1.1 +short -x "$SERVER_IP" | head -n1)
if [ "$PTR_RECORD" == "$HOSTNAME." ]; then
echo "[OK] PTR: $SERVER_IP -> $PTR_RECORD"
else
echo "[FAIL] PTR: $SERVER_IP -> $PTR_RECORD (should be $HOSTNAME.)"
fi
EOFSCRIPT

Purpose: Verifies all required DNS records are configured.

Usage: /root/verify-dns.sh

Checks:

  1. A Record: Hostname resolves to server IP
  2. MX Record: Domain has mail server configured
  3. SPF Record: Sender Policy Framework configured
  4. DKIM Record: Public key published
  5. DMARC Record: Email authentication policy configured
  6. PTR Record: Reverse DNS configured

Uses @1.1.1.1 (Cloudflare DNS) to avoid local caching issues.

Make Scripts Executable

chmod +x /root/{add-mail-user.sh,test-mail.sh,verify-dns.sh}

Purpose: Makes helper scripts executable.

  • chmod +x: Add execute permission
  • Allows running scripts directly: /root/test-mail.sh
  • Without this, would need: bash /root/test-mail.sh

DNS Configuration Timer

echo ""
echo "DKIM key:"
cat /root/dkim-public-key.txt
echo ""
echo "Update DNS, then waiting 10 minutes..."
echo ""

# 10-minute countdown
for i in {600..1}; do
printf "\rTime remaining: %02d:%02d" $((i/60)) $((i%60))
sleep 1
done

Purpose: Displays DKIM key and waits for DNS propagation.

Countdown timer:

  • {600..1}: Counts from 600 to 1 (10 minutes)
  • printf \r: Overwrites same line (live countdown)
  • $((i/60)): Minutes remaining
  • $((i%60)): Seconds remaining
  • User sees live countdown while DNS propagates

What to do during wait: Update DNS with:

  1. A record for mail server hostname
  2. MX record pointing to mail server
  3. SPF TXT record
  4. DKIM TXT record (shown in output)
  5. DMARC TXT record
  6. PTR record (in Linode dashboard)

SSL Certificate Acquisition

CERTBOT_CMD="certbot certonly --standalone --non-interactive --agree-tos --email $SSL_EMAIL -d $HOSTNAME --preferred-challenges http --force-renewal"

$CERTBOT_CMD >> "$LOGFILE" 2>&1 && {
log "SSL certificate obtained"
systemctl restart postfix dovecot
} || {
log "SSL certificate failed - check $LOGFILE for details"
}

Purpose: Obtains Let's Encrypt SSL certificate after DNS is ready.

Certbot options:

  • certonly: Only obtain certificate (don't install)
  • --standalone: Use built-in web server for validation
  • --non-interactive: No prompts
  • --agree-tos: Accept Let's Encrypt terms
  • --email $SSL_EMAIL: Contact email for expiration notices
  • -d $HOSTNAME: Domain to get certificate for
  • --preferred-challenges http: Use HTTP-01 validation (port 80)
  • --force-renewal: Overwrite any existing certificate

HTTP-01 Challenge:

  1. Certbot starts temporary web server on port 80
  2. Let's Encrypt connects to http://mx.itisajoke.net/.well-known/acme-challenge/xxx
  3. Server responds with validation token
  4. Let's Encrypt verifies domain ownership
  5. Certificate issued

Why it might fail:

  • DNS not propagated yet (A record missing)
  • Port 80 blocked by firewall
  • Hostname doesn't resolve
  • Rate limit (5 certs per domain per week)

On success:

  • Certificate: /etc/letsencrypt/live/$HOSTNAME/fullchain.pem
  • Private key: /etc/letsencrypt/live/$HOSTNAME/privkey.pem
  • Postfix and Dovecot restarted to use new certificate
  • Auto-renewal configured by certbot

On failure:

  • Server keeps using self-signed certificate
  • Mail works but clients see certificate warning
  • Can manually run certbot later

Final Verification

/root/verify-dns.sh

Purpose: Automatically verifies DNS configuration after certificate acquisition.

  • Runs DNS checks
  • Shows which records are configured correctly
  • Helps identify any missing DNS records
  • Output visible in script logs and console

Post-Deployment

Email Client Configuration

Mozilla Thunderbird

Step 1: Add New Account

  1. Open Thunderbird
  2. Click (menu) → NewExisting Mail Account

Step 2: Enter Account Details

Your full name:     Tom Vitkovic
Email address: tomislav.vitkovic@itisajoke.net
Password: [your password from stackscript_data]

Click Continue

Step 3: Manual Configuration Thunderbird will try auto-detection. Click Manual config button.

Incoming Server (IMAP):

Protocol:           IMAP
Hostname: mx.itisajoke.net
Port: 993
Connection security: SSL/TLS
Authentication: Normal password
Username: tomislav.vitkovic

Outgoing Server (SMTP):

Hostname:           mx.itisajoke.net
Port: 587
Connection security: STARTTLS
Authentication: Normal password
Username: tomislav.vitkovic

Step 4: Advanced Settings (Optional)

After account creation, you can adjust:

  • Right-click account → Settings
  • Server Settings → Check mail every X minutes
  • Copies & Folders → Where to save sent mail
  • Composition & Addressing → Default From address

Common Issues:

  • If you get certificate warning, click Confirm Security Exception (Let's Encrypt certificates are valid)
  • If authentication fails, verify username is just tomislav.vitkovic (not full email address)

Apple Mail (macOS)

Step 1: Add Account

  1. Open Mail app
  2. Click MailAdd Account
  3. Select Other Mail AccountContinue

Step 2: Account Information

Name:           Tom Vitkovic
Email Address: tomislav.vitkovic@itisajoke.net
Password: [your password from stackscript_data]

Click Sign In

Step 3: Manual Setup

If auto-detection fails, you'll see incoming/outgoing server fields:

Incoming Mail Server (IMAP):

IMAP Hostname:      mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]

Click Sign In, then configure outgoing:

Outgoing Mail Server (SMTP):

SMTP Hostname:      mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]

Step 4: Verify Settings

After setup, verify configuration:

  1. MailPreferencesAccounts
  2. Select your account
  3. Click Server Settings

Incoming (IMAP) should show:

Host Name:          mx.itisajoke.net
Port: 993
TLS/SSL: ON
Authentication: Password

Outgoing (SMTP) should show:

Host Name:          mx.itisajoke.net
Port: 587
TLS/SSL: ON
Authentication: Password

Step 5: Advanced Options

Click Advanced tab:

  • Enable this account: Checked
  • Include when automatically checking for new messages: Checked
  • Remove copy from server after retrieving: Optional (uncheck to keep mail on server)

iOS Mail (iPhone/iPad)

Step 1: Add Account

  1. Open Settings
  2. Scroll to Mail
  3. Tap AccountsAdd Account
  4. Select Other
  5. Tap Add Mail Account

Step 2: Enter Information

Name:       Tom Vitkovic
Email: tomislav.vitkovic@itisajoke.net
Password: [your password]
Description: My Mail Server

Tap Next

Step 3: Configure Servers

Incoming Mail Server:

Host Name:      mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]

Outgoing Mail Server:

Host Name:      mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]

Tap Next, then Save

iOS will automatically detect:

  • IMAP port 993 with SSL
  • SMTP port 587 with STARTTLS

Using Helper Scripts

The setup script creates three utility scripts in /root/ for server management.

1. Adding New Mail Users

Script: /root/add-mail-user.sh

Purpose: Creates new email accounts on the server

Usage:

/root/add-mail-user.sh <username> <password>

Example:

/root/add-mail-user.sh john MySecurePass123

Output:

Created: john@itisajoke.net

What it does:

  1. Validates you provided both username and password
  2. Checks if username already exists (prevents duplicates)
  3. Creates Linux system user with home directory
  4. Sets the password
  5. Creates complete Maildir structure:
    • /home/john/Maildir/new/ - New unread messages
    • /home/john/Maildir/cur/ - Current (read) messages
    • /home/john/Maildir/tmp/ - Temporary delivery storage
    • /home/john/Maildir/.Sent/ - Sent folder
    • /home/john/Maildir/.Trash/ - Deleted messages
    • /home/john/Maildir/.Drafts/ - Draft messages
  6. Sets correct ownership (user owns their maildir)
  7. Sets permissions (700 - only user can access)

User can now:

  • Log in via IMAP: john@itisajoke.net
  • Send mail via SMTP: john@itisajoke.net
  • Use password: MySecurePass123

Password Requirements:

  • Use strong passwords in production
  • Minimum 12 characters recommended
  • Mix of letters, numbers, symbols
  • Avoid dictionary words

Viewing All Mail Users:

ls -la /home/

Any directory except the test user is a mail account.

Deleting a Mail User:

userdel -r username

This removes the user and their entire maildir (cannot be undone).


2. Testing Mail Functionality

Script: /root/test-mail.sh

Purpose: Verifies mail server is working and tests deliverability

Usage:

/root/test-mail.sh [recipient-email]

Example 1: Check Service Status

/root/test-mail.sh

Output:

Mail Server: mx.itisajoke.net
[OK] Postfix
[OK] Dovecot
[OK] OpenDKIM
Usage: /root/test-mail.sh <recipient@example.com>

Shows which mail services are running:

  • Postfix: SMTP server (sending/receiving)
  • Dovecot: IMAP/POP3 server (mailbox access)
  • OpenDKIM: Email signing (authentication)

[OK] = Service running correctly [FAIL] = Service stopped or crashed

Example 2: Send Test Email

/root/test-mail.sh test@mail-tester.com

Output:

Mail Server: mx.itisajoke.net
[OK] Postfix
[OK] Dovecot
[OK] OpenDKIM
Sending test to test@mail-tester.com...
Check: tail -f /var/log/mail.log

What it does:

  1. Checks service status
  2. Sends test email with:
    • From: Current user (root or actual user)
    • To: Address you specified
    • Subject: "Mail Test [timestamp]"
    • Body: "Test from mx.itisajoke.net at [date/time]"
  3. Email is DKIM signed automatically
  4. Shows log command to watch delivery

Testing with mail-tester.com:

/root/test-mail.sh test@mail-tester.com

Then visit: https://www.mail-tester.com

You'll get a score out of 10:

  • 10/10: Perfect configuration
  • 9/10: Excellent (minor issue, likely blacklist)
  • 8/10: Good (needs tuning)
  • <7/10: Problems need fixing

Common Issues:

  • Score drops if sent from root (use actual user: su - tomislav.vitkovic)
  • New servers may be on minor blacklists (auto-resolve in 1-2 weeks)
  • Empty message body reduces score (include real content)

Watching Email Delivery:

tail -f /var/log/mail.log

Look for:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=itisajoke.net
status=sent (250 2.0.0 OK)

DKIM-Signature = Email was signed status=sent = Successfully delivered


3. Verifying DNS Configuration

Script: /root/verify-dns.sh

Purpose: Checks all required DNS records are configured correctly

Usage:

/root/verify-dns.sh

Example Output:

DNS VERIFICATION
==================
[OK] A: mx.itisajoke.net -> 172.104.132.225
[OK] MX: mx.itisajoke.net.
[OK] SPF found
[OK] DKIM found
[OK] DMARC found
[OK] PTR: 172.104.132.225 -> mx.itisajoke.net.

What Each Check Means:

1. A Record:

[OK] A: mx.itisajoke.net -> 172.104.132.225
  • Verifies hostname resolves to server IP
  • Required for: Let's Encrypt, email delivery
  • Checked against: Public DNS (1.1.1.1 - Cloudflare)
  • [FAIL] means: DNS not configured or not propagated yet

2. MX Record:

[OK] MX: mx.itisajoke.net.
  • Verifies domain has mail server configured
  • Other servers use this to find where to send mail
  • Should point to your mail server hostname
  • [FAIL] means: Mail to @itisajoke.net won't be delivered

3. SPF Record:

[OK] SPF found
  • Sender Policy Framework - authorizes your IP to send mail
  • Format: v=spf1 ip4:172.104.132.225 a:mx.itisajoke.net -all
  • Prevents email spoofing
  • [FAIL] means: Your emails may go to spam

4. DKIM Record:

[OK] DKIM found
  • Public key for email signature verification
  • Format: v=DKIM1; k=rsa; p=MIIBIjAN...
  • Proves emails actually came from your server
  • [FAIL] means: Email authentication fails, likely spam

5. DMARC Record:

[OK] DMARC found
  • Email authentication policy
  • Format: v=DMARC1; p=quarantine; rua=mailto:postmaster@itisajoke.net
  • Tells receivers what to do with failed authentication
  • [FAIL] means: No policy, receivers don't know how to handle failures

6. PTR Record (Reverse DNS):

[OK] PTR: 172.104.132.225 -> mx.itisajoke.net.
  • IP address points back to hostname
  • Most important for deliverability
  • Set in Linode dashboard (not DNS provider)
  • [FAIL] means: 90% of your emails go to spam

If You See [FAIL]:

A Record Failed:

[FAIL] A record missing

Check your DNS provider has:

Type: A
Name: mx
Value: 172.104.132.225
TTL: 300 (or default)

Wait 5-10 minutes for propagation.

DKIM Failed:

[FAIL] DKIM missing

Check DNS has complete DKIM key:

cat /root/dkim-public-key.txt

Copy entire output to DNS TXT record:

Type: TXT
Name: mx._domainkey
Value: v=DKIM1; k=rsa; p=MIIBIjAN... [full key]

PTR Failed:

[FAIL] PTR: 172.104.132.225 -> 172-104-132-225.ip.linodeusercontent.com. (should be mx.itisajoke.net.)

This means PTR not set in Linode:

  1. Go to Linode Cloud Manager
  2. Click your instance
  3. Network tab
  4. Find your IP: 172.104.132.225
  5. Click Edit RDNS
  6. Enter: mx.itisajoke.net
  7. Save

Using with Monitoring:

Create a cron job to check DNS daily:

crontab -e

Add:

0 9 * * * /root/verify-dns.sh | mail -s "Daily DNS Check" admin@itisajoke.net

This emails you DNS status every morning at 9 AM.


Quick Reference

Check Service Status:

systemctl status postfix dovecot opendkim

View Mail Logs:

tail -f /var/log/mail.log

Test DKIM Key:

opendkim-testkey -d itisajoke.net -s mx -vvv

Manual Email Send (Command Line):

echo "Test message" | mail -s "Test Subject" recipient@example.com

Check Mail Queue:

mailq

Flush Mail Queue:

postfix flush

View User's Mailbox:

ls -la /home/username/Maildir/new/

Monitor Active Connections:

netstat -tulpn | grep -E ':(25|587|993|995)'


Security Considerations

Password Security:

  • Never use test passwords in production
  • Minimum 12 characters with mixed case, numbers, symbols
  • Change passwords regularly
  • Different password for each user

System Updates:

apt-get update && apt-get upgrade -y

Run weekly to get security patches.

Firewall:

  • Already configured via UFW
  • Only necessary ports open (22, 25, 80, 587, 993, 995)
  • Port 22 (SSH) should be restricted to known IPs in production

Monitoring:

# Watch for authentication failures
grep "authentication failed" /var/log/mail.log

# Watch for unusual activity
tail -f /var/log/mail.log

Fail2ban (Optional but Recommended):

apt-get install fail2ban -y

Automatically blocks IPs after repeated failed login attempts.

Backups:

  • Regular backups of /home (user mailboxes)
  • Regular backups of /etc/postfix, /etc/dovecot, /etc/opendkim
  • Store backups off-server
  • Test restoration procedure

Maintenance

Certificate Renewal

Automatic renewal configured by certbot:

# Check renewal timer
systemctl status certbot.timer

# Test renewal (dry-run)
certbot renew --dry-run

# Manual renewal
certbot renew
systemctl restart postfix dovecot

Backup Strategy

# Backup mail and configuration
tar -czf /root/mail-backup-$(date +%Y%m%d).tar.gz \
/home \
/etc/postfix \
/etc/dovecot \
/etc/opendkim \
/etc/letsencrypt

# Restore from backup
tar -xzf /root/mail-backup-YYYYMMDD.tar.gz -C /
systemctl restart postfix dovecot opendkim

Log Rotation

Automatic log rotation configured by default:

  • /var/log/mail.log - Main mail log
  • /var/log/mail.err - Error log
  • Rotated weekly
  • Kept for 4 weeks
  • Compressed after rotation

Performance Tuning

For high-volume servers:

# Increase connection limits
postconf -e 'default_process_limit = 200'
postconf -e 'smtpd_client_connection_count_limit = 50'

# Increase worker processes
postconf -e 'smtp_connection_cache_destinations = 1000'

# Optimize Dovecot
# Edit /etc/dovecot/conf.d/10-mail.conf
# mail_fsync = never # Faster but less safe

# Restart services
systemctl restart postfix dovecot