Raja's Exocortex

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:

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

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.pem

The 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.pem

Strip 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 alerts

File: /etc/mailname

example.com

File: /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_HASH

File: /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.com

File: /etc/exim4/sender_blocklist

# Addresses permanently blocked from submitting mail through this server
blocked@example.com
bounce-disabled@example.com

Split 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 = 120

File: /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
.endif

File: /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 = 10

File: /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 = $auth1

File: /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 permitted

File: /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_KEY

Build 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.autogenerated

AWS 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:

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.com

Do not continue with Exim setup until AWS has approved both the PTR record and port 25 removal. Approval typically takes one business day.

References:

  1. exim4-docker - Exim4 docker deployment
  2. certbot-docker - Running certbot service in docker