Exim4 on Ubuntu
A self-hosted mail server sends email without depending on a third-party relay like SendGrid, SES, or Mailgun. Exim4 in direct-delivery mode connects to recipient MX servers itself โ no middleman, no relay costs, and full control over headers, DKIM signing, sender enforcement, and rate limiting.
This is useful when:
- Services generate automated mail (monitoring alerts, cron output, account notifications)
- A fixed sender address must be enforced per authenticated account so one credential cannot spoof another
- DKIM-signed mail is needed under a specific domain without routing through a third party
- Per-message pricing at scale needs to be avoided
The tradeoff is that the server's IP reputation must be managed. Deliverability depends on correct DNS records (SPF, DKIM, PTR), a clean IP, and port 25 being unblocked by the hosting provider.
This setup is for outbound sending only. It does not cover inbound mail.
Oracle Cloud permanently blocks outbound port 25 at the network level and cannot be unblocked. Direct mail delivery is not supported on Oracle Cloud.
For AWS EC2 instances, read the exim4-ubuntu section at the end before starting โ port 25 and reverse DNS require AWS approval upfront and can take time.
Step 1: Prerequisites
- Static public IP
- Reverse DNS (PTR) for the mail host pointing to
mail.example.com - Outbound port
25open to the internet - Inbound ports
587and465open if clients submit mail remotely - A hostname such as
mail.example.com
Step 2: DNS setup
Create these records before starting. SPF and DKIM records are required for deliverability โ without them most receiving servers will reject or junk the mail.
# A record: resolves the mail hostname to the server IP
mail.example.com. IN A a.a.a.a
# SPF: authorises mail.example.com to send on behalf of example.com
example.com. IN TXT "v=spf1 a:mail.example.com -all"
# DKIM: public key used by receiving servers to verify the DKIM signature
# Populate the p= value after generating the key pair below
mail._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=PASTE_PUBLIC_KEY_HERE"Step 3: TLS with certbot
This guide assumes a Let's Encrypt certificate already exists at:
/etc/letsencrypt/live/mail.example.com/fullchain.pem
/etc/letsencrypt/live/mail.example.com/privkey.pemThe certificate hostname must match what clients connect to (mail.example.com). Exim presents this certificate on ports 587 (STARTTLS) and 465 (implicit TLS).
Refer certbot-docker to run certbot service on docker.
Step 4: DKIM keys
Generate the private key, lock down its permissions so only Exim can read it, then extract the public key to publish in DNS:
# Create the DKIM directory โ owned by root, readable by the Debian-exim group
sudo install -d -o root -g Debian-exim -m 0750 /etc/exim4/dkim
# Generate a 2048-bit RSA private key and restrict access to Exim only
sudo openssl genrsa -out /etc/exim4/dkim/example.com.private 2048
sudo chown root:Debian-exim /etc/exim4/dkim/example.com.private
sudo chmod 0640 /etc/exim4/dkim/example.com.private
# Extract the public key from the private key
sudo openssl rsa -in /etc/exim4/dkim/example.com.private -pubout -out /etc/exim4/dkim/example.com.public.pem
sudo chmod 0644 /etc/exim4/dkim/example.com.public.pem
# Print the public key โ copy this into the DNS DKIM record
sudo cat /etc/exim4/dkim/example.com.public.pemStrip the -----BEGIN PUBLIC KEY----- header and footer from the output, join the base64 lines into one string, and paste it into the p= field of the DKIM DNS record.
Step 5: Install and configure
Install the packages, then generate password hashes for each SMTP auth account:
sudo apt update && sudo apt install -y exim4 openssl
exim4 passwd app
exim4 passwd alertsFile: /etc/mailname
example.comFile: /etc/exim4/update-exim4.conf.conf
# Direct delivery: Exim connects to recipient MX servers itself, no relay
dc_eximconfig_configtype='internet'
# Hostnames this server accepts as locally delivered (not relayed)
dc_other_hostnames='mail.example.com'
# Listen on all interfaces for inbound SMTP submission
# Change to '127.0.0.1 ; ::1' to accept only local connections
dc_local_interfaces='0.0.0.0 ; ::0'
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets=''
# No smarthost โ this server delivers directly to recipient MX
dc_smarthost=''
CFILEMODE='644'
# Required to use the /etc/exim4/conf.d/ split config layout
dc_use_split_config='true'
dc_hide_mailname='false'
dc_mailname_in_oh='true'
dc_localdelivery='mail_spool'File: /etc/exim4/passwd
# SMTP auth credentials โ one account per line, format: username:hashed_password
# Generate hashes with: exim4 passwd <username>
app:$6$rounds=656000$REPLACE_WITH_REAL_HASH
alerts:$6$rounds=656000$REPLACE_WITH_REAL_HASHFile: /etc/exim4/sender_map
# Maps each SMTP auth username to the one sender address it is allowed to use
# The ACL checks this: if the From address does not match, the message is rejected
app:app@example.com
alerts:alerts@example.comFile: /etc/exim4/sender_blocklist
# Addresses permanently blocked from submitting mail through this server
blocked@example.com
bounce-disabled@example.comSplit config files
File: /etc/exim4/conf.d/main/00_local_macros
# Domain used in DKIM signatures on outbound mail
DKIM_DOMAIN = example.com
# Selector matches the DNS record: mail._domainkey.example.com
DKIM_SELECTOR = mail
# Private key used to sign outbound messages
DKIM_PRIVATE_KEY = /etc/exim4/dkim/example.com.private
# Maximum messages per authenticated account per hour before rate limiting kicks in
EXIM_RATE_LIMIT = 120File: /etc/exim4/conf.d/main/03_exim4-config_tlsoptions
# Enable TLS for inbound SMTP connections
MAIN_TLS_ENABLE = yes
# Advertise STARTTLS to all connecting hosts
tls_advertise_hosts = *
# Let's Encrypt certificate for mail.example.com
tls_certificate = /etc/letsencrypt/live/mail.example.com/fullchain.pem
tls_privatekey = /etc/letsencrypt/live/mail.example.com/privkey.pem
# Port 465 uses implicit TLS (TLS wraps the entire connection from the start)
tls_on_connect_ports = 465
.ifndef MAIN_TLS_MIN_PROTOCOL
MAIN_TLS_MIN_PROTOCOL = TLSv1.2
.endifFile: /etc/exim4/conf.d/main/04_local_options
# Accept mail submission on 587 (STARTTLS) and 465 (implicit TLS)
# Port 25 is handled separately by the OS for MX-to-MX delivery
daemon_smtp_ports = 587 : 465
# Only advertise AUTH when TLS is active โ prevents plaintext credential exposure
auth_advertise_hosts = ${if eq{$tls_in_cipher}{}{}{*}}
# Include TLS and protocol errors in the mail log for debugging
log_selector = +smtp_protocol_error +smtp_syntax_error +tls_peerdn
# Concurrency limits to protect the server under load
smtp_accept_max = 50
smtp_accept_max_per_host = 10
queue_run_max = 5
remote_max_parallel = 10File: /etc/exim4/conf.d/auth/30_local_smtp_auth
# PLAIN auth: client sends username and password in one shot
# Looks up the username in /etc/exim4/passwd and compares the hashed password
plain_server:
driver = plaintext
public_name = PLAIN
server_condition = ${if crypteq{$auth3}{${extract{1}{:}{${lookup{$auth2}lsearch{/etc/exim4/passwd}{$value}{*:*}}}}}{yes}{no}}
server_set_id = $auth2
# LOGIN auth: client sends username and password as separate prompts
# Same lookup as PLAIN but uses $auth1 (username) and $auth2 (password)
login_server:
driver = plaintext
public_name = LOGIN
server_prompts = "Username:: : Password::"
server_condition = ${if crypteq{$auth2}{${extract{1}{:}{${lookup{$auth1}lsearch{/etc/exim4/passwd}{$value}{*:*}}}}}{yes}{no}}
server_set_id = $auth1File: /etc/exim4/conf.d/acl/30_exim4-config_check_rcpt
acl_check_rcpt:
# Allow mail injected locally (e.g. from cron or the system itself)
accept hosts = :
# Reject if TLS is not active โ prevents credentials being sent in cleartext
deny message = TLS required
condition = ${if eq{$tls_in_cipher}{}{yes}{no}}
# Reject unauthenticated senders โ this server is submission-only, not an open relay
deny message = Authentication required
!authenticated = *
# Reject if the From address is on the permanent blocklist
deny message = Sender address blocked
senders = /etc/exim4/sender_blocklist
# Reject if the sender address does not match the account's entry in sender_map
# Prevents one account from spoofing another account's address
deny message = Sender address not allowed for this login
condition = ${if !eq{$sender_address}{${lookup{$authenticated_id}lsearch{/etc/exim4/sender_map}{$value}{}}}{yes}{no}}
# Reject if the account has exceeded its hourly message quota
deny message = Rate limit exceeded for $authenticated_id
ratelimit = EXIM_RATE_LIMIT / 1h / strict / $authenticated_id
accept authenticated = *
deny message = relay not permittedFile: /etc/exim4/conf.d/transport/30_exim4-config_remote_smtp
# Outbound SMTP transport with DKIM signing
# Macros are defined in /etc/exim4/conf.d/main/00_local_macros
remote_smtp:
driver = smtp
dkim_domain = DKIM_DOMAIN
dkim_selector = DKIM_SELECTOR
dkim_private_key = DKIM_PRIVATE_KEYBuild config and restart
sudo update-exim4.conf
sudo systemctl restart exim4
sudo systemctl status exim4
# Verify the generated config and check for errors
sudo exim4 -bV
sudo exim4 -bP
sudo ls -l /var/lib/exim4/config.autogeneratedAWS EC2
If this mail server runs on AWS EC2, do this before setting up Exim. AWS blocks outbound port 25 and does not set reverse DNS by default โ both must be requested and approved before mail can be delivered.
Before submitting the request, create the DNS A record for mail.example.com. AWS validates that the hostname resolves before approving the PTR record.
Submit the request at: https://support.console.aws.amazon.com/support/contacts#/rdns-limits
Request both:
- Reverse DNS (PTR) for the instance public IP โ
mail.example.com - Removal of the outbound port
25restriction
Recommended values for the form:
EC2 instance ID : i-0123456789abcdef0
Elastic IP : a.a.a.a
PTR record : mail.example.com
Use case : transactional and system outbound mail from example.comDo not continue with Exim setup until AWS has approved both the PTR record and port 25 removal. Approval typically takes one business day.
References:
- exim4-docker - Exim4 docker deployment
- certbot-docker - Running certbot service in docker