DEV Community

Cover image for Let's build a Local Mail Server from Scratch with Postfix and Dovecot
khripkooleg
khripkooleg

Posted on

Let's build a Local Mail Server from Scratch with Postfix and Dovecot

My boss at work decided it was time to test my knowledge, patience, and DevOps engineering skills, so he gave me a task that seemed simple at first glance but was actually guite challenging.

We decided to build our own local mail server from scratch for use among our colleagues. Instead of using ready-made Docker images, I created a mail server using Postfix and Dovecot, implemented virtual users, enabled LMTP delivery, and secured everything.


Before we begin...

Before we start creating our own mail server, we need to decide on the technologies we will use.

Without a doubt, the core of our project are Postfix and Dovecot, which serve a single purpose - to help us create a mail server.
I'll quickly explain what they do.

Postfix
Postfix is responsible for handling SMTP traffic. It:

  • Receives incoming email
  • Sends outgoing email
  • Decides whether mail should be relayed or delivered locally
  • Enforces relay and security rules

Dovecot
Dovecot acts as the mail access server. It:

  • Stores emails ( Maildir format )
  • Provides IMAP/POP3 access
  • Handles user authentication
  • Accepts local delivery via LMTP

Mail Flow Design

Before configuring the services, let's quickly understand how email would flow through the system.

A mail server is essentially a pipeline. To debug more precisely we need to know how messages flow between components.

In my setup, the mail flow looks like this:

Client -> SMTP -> LMTP -> Dovecot -> Maildir
Client -> IMAP -> Dovecot -> Maildir

Sending an Email
When user sends an email:

  • The client connects to Postfix using SMTP ( port 587 ).
  • Postfix validated authentication and checks relay restrictions.
  • If the recipient is a local virtual user, Postfix forwards the message via LMTP to Dovecot.
  • Dovecot stores the message in the user's Maildir directory.

Instead of using the Postfix's built-in virtual delivery system, I chose LMTP because it allows Dovecot to handle the final delivery process directly.

Reading an Email
When a user checks their mailbox:

  • The client connects to Dovecot via IMAP.
  • Dovecot authenticates the user using passdb.
  • After successful authentication, Dovecot retrieves emails from the Maildir storage.
  • The client receives the message list.

Mail storage format Maildir
Maildir is a mail storage format provided by Dovecot to store messages on the server.
In Maildir, each message is stored as an individual file inside a structured directory hierarchy ( tmp/, new/, and cur/ ).
This design eliminated file locking issues common in older mailbox format and allows multiple processes to work safely in parallel.

SSL/TLS Encryption: Secure Communication
SSL (Secure Sockets Layer) and TLS (Transport Layer Security) are protocols that encrypt the communication between email servers and clients, protecting your emails and login credentials during transmission over the internet.


Installing the Core Components

Once the architecture is clear, we can start our project. First, let's install the necessary components.

sudo apt update
sudo apt install postfix dovecot-imapd dovecot-lmtpd
Enter fullscreen mode Exit fullscreen mode

You might ask: "Why virtual users?". Creating Linux system users for each email account would tightly couple mail accounts with operating system accounts.

This approach has several drawbacks:

  • Every email user = real system user.
  • Potential shell access risks.
  • Harder to scale and manage.
  • Poor separation of concers.

Postfix configuration

Now that we have installed all the main components, we can move on to configuring Postfix.

/etc/postfix/main.cf

Basic Identity

myhostname = hostname.domain
mydomain = domain
myorigin = $mydomain
Enter fullscreen mode Exit fullscreen mode

These parameters define how the server identifies itself when sending and receiving mail.

Network Configuration

inet_interfaces = all
inet_protocols = all
mynetworks = 127.0.0.0/8 ...
mydestination = localhost, localhost.$mydomain
relayhost = 
Enter fullscreen mode Exit fullscreen mode
  • inet_interfaces - ensures Postfix listens on all interfaces
  • inet_protocols - IPv4 + IPv6
  • mynetworks - defines which subnets are trusted and allowed to relay mail.
  • mydestination - defines the domains that are trusted.
  • relayhost ( empty ) - the server won't send any messages through another SMTP server.

Mailbox Settings

mailbox_size_limit = 0
Enter fullscreen mode Exit fullscreen mode

Define that there is no restrictions on mailbox size.

Relay Restrictions && Recipient

smtpd_recipient_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_unauth_destination
Enter fullscreen mode Exit fullscreen mode

This prevents the server from becoming an open relay.

  • reject_unauth_destination - is key option. It prohibits delivery to domain for which the server is not responsible.

SASL Authentication

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
Enter fullscreen mode Exit fullscreen mode

It simply tells Postfix to not save the users and let the Dovecot do the job.

TLS

smtpd_tls_cert_file = /etc/postfix/certs/server.crt
smtpd_tls_key_file = /etc/postfix/certs/server.key
smtpd_tls_security_level = may
smtp_tls_security_level = may
Enter fullscreen mode Exit fullscreen mode

We tell the server:

  • That we support TLS.
  • TLS is supported but not required, and for production, encrypt is better.

For the local environments is perfect.

Virtual Domain && LMTP

virtual_mailbox_domains = domain
virtual_transport = lmtp:unix:private/dovecot-lmtp
Enter fullscreen mode Exit fullscreen mode
  • virtual_mailbox_domains - we tell Postfix which domain it should handle as virtual mailboxes. In other words, Postfix will accept mails for this domain and deliver them to virtual users.
  • virtual_transport = lmtp:unix:private/dovecot-lmtp - we tell Postfix how to deliver mail for those virtual mailboxes. lmtp is the Local Mail Transfer Protocol, which is used to hand mails over to Dovecot for delivery. unix:private/dovecot-lmtp specifies the socket (a kind of local connection) that Dovecot is listening on to receive these emails.

Once Postfix is fully configured... wait! We are not done yet.

Postfix Ports

After setting up Postfix, we need to make sure the mail server can actually receive connections. For that, we need to open the necessary ports in our firewall (UFW) and configure master.cf.

Port Protocol Purpose
25 TCP SMTP for receiving mail from other mail servers
587 TCP SMTP submission for sending mail from email clients (authenticated)
465 TCP SMTP over SSL (optional, secure submission)
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 465/tcp
sudo ufw reload
sudo ufw status
Enter fullscreen mode Exit fullscreen mode

The master.cf file defines how Postfix listens and handles connections.

Submissions defines the outgoing from clients.

/etc/postfix/master.cf

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
Enter fullscreen mode Exit fullscreen mode

Each -o option overrides default Postfix behavior for this service only, like forcing TLS and enabling authentication.


Now that we are finished configuring the Postfix, we can move on to configuring Dovecot.

Dovecot Configuration

After Postfix is configured to handle sending and receiving mails, Dovecot takes care of user authentication and mail storage access.

Create the vmail User

All virtual mailboxes will be stored under a dedicated system user called vmail. This avoids creating a system account for each email user, improving security and manageability.

sudo groupadd -g 5000 vmail
sudo useradd -g 5000 -u 5000 vmail -s /bin/bash -m /home/vmail
sudo passwd vmail
Enter fullscreen mode Exit fullscreen mode
  • -g 5000 - sets the primary group to vmail
  • -u 5000 - assigns a UID

Create a Virtual User Password File

Dovecot needs a lsit of virtual users and their passwords. This can be a simple text file for local testing.

/etc/dovecot/passwd

sender@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/sender
receiver@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/receiver
Enter fullscreen mode Exit fullscreen mode

In Dovecot, virtual users are not real Linux users, but they still need file system access to read/write their mailboxes. Virtual usera are mapped to vmail's UID/GID so Dovecot processes access the files as vmail.

Field Meaning
sender@domain Virtual email address (login name).
{PLAIN}passwd Password for this user ({PLAIN} = plain text for testing).
5000 UID of the system user Dovecot should use (vmail).
5000 GID of the system group (vmail).
(empty) Typically used for extra info like quota; empty here.
/home/vmail/domain/sender Home directory / Maildir path for this user. Dovecot stores the mail here.

Configure Password and User Databases

/etc/dovecot/conf.d/auth-passwdfile.conf.ext

passdb passwd-file {
        driver = passwd-file
        auth_username_format = %{user}
        passwd_file_path = /etc/dovecot/passwd
}

userdb static-users {
        driver = static
        fields {
                uid = 5000
                gid = 5000
                home = /home/vmail/domain/%{user | username}
                mail = maildir:~/Maildir
        }
}
Enter fullscreen mode Exit fullscreen mode

This file tells Dovecot how to find virtual users and which system account should own their mail files. There are two main sections: passdb and userdb

  • passdb passwd-file - Password Database. Dovecot checks this when a user logs in.
  • driver passwd-file - Dovecot reads users/passwords from a plain text file.
  • auth_username_format - This ensured that Dovecot uses the exact username entered by the client ( before @ or with full mail depending on the option ).
  • passwd_file_path - Location of the file with virtual users and passwords.

  • userdb static-users - This is a user database named static-users. It tells Dovecot who owns the mailbox.

  • driver = static - Same system UID/GID for all virtual users.

  • uid = 5000 - The system user ID used to acces mailbox files.

  • gid = 5000 - The system group ID used to access mailbox files.

  • home - The home path of a virtual user

  • mail - Location of the user's Maildir inside the home directory.

Mail Storage Configuration

/etc/dovecot/conf.d/10-mail.conf

mail_driver = maildir
mail_home = /home/vmail/domain/%{user | username}
mail_path = ~/Maildir
Enter fullscreen mode Exit fullscreen mode

This is where we tell Dovecot where and how to store messages for each virtual user.

  • mail_driver = maildir - Dovecot will use the Maildir format for storing emails.
  • mail_home - This is the home directory for the virtual user's mailbox. %{user | username} takes the local part of the email ( everything before @ )
  • mail_path - This is where Dovecot actually reads and writes the user's emails.

Authentication Settings

/etc/dovecot/conf.d/10-auth.conf

auth_mechanisms = plain login
auth_allow_cleartext = no

#!include auth-system.conf.ext
!include auth-passwdfile.conf.ext
Enter fullscreen mode Exit fullscreen mode

This file controls how users log in and which authentication backends Dovecot uses.

  • auth_mechanisms - Specifies which authentication methods are allowed. plain - username and password are sent as plain text. login - a common method used by email clients.
  • auth_allow_cleartext - Prevents sending passwords in plain text over unecrypted connections.
  • #!include auth-system.conf.ext - We comment out his line so Dovecot will not use system Linux users for authentication, because we only want virtual users.
  • !include auth-passwdfile.conf.ext - Includes the configuration file we created for virtual users.

Master Process && Socket Configuration

/etc/dovecot/conf.d/10-master.conf

service imap-login {
  inet_listener imap {
    port = 143
  }
}

service auth {
        unix_listener /var/spool/postfix/private/auth {
                mode = 0660
                user = postfix
                group = postfix
        }
        unix_listener auth-userdb {
                mode = 0660
                user = vmail
                group = vmail
        }
}

service lmtp {
        unix_listener /var/spool/postfix/private/dovecot-lmtp {
                mode = 0600
                user = postfix
                group = postfix
        }
}
Enter fullscreen mode Exit fullscreen mode

This file controls how Dovecot runs its services ( IMAP, LMTP, authentication ) and how they communicate with other processes.

  • service imap-login - Handles client logins via IMAP by setting up a TCP socket for standard IMAP port 143.
  • service auth - Handles all user authentication requests by creating a UNIX socket for Postfix.
  • service lmtp - Handles mail delivery from Postfix by creating a UNIX socket.
Service Purpose Socket/Port User/Group Notes
imap-login IMAP client connections TCP 143 - Allows clients to read mail
auth User authentication /var/spool/postfix/private/auth postfix:postfix Postfix can check credentials
auth-userdb User info query internal vmail:vmail Dovecot internal use
lmtp Mail delivery from Postfix /var/spool/postfix/private/dovecot-lmtp postfix:postfix Delivers mail to Maildir

LMTP Protocol Settings

/etc/dovecot/conf.d/20-lmtp.conf

protocol lmtp {
  auth_username_format = %{user}
}
Enter fullscreen mode Exit fullscreen mode

This file controls how Dovecot handles mail delivery via LMTP from Postfix.

  • protocol lmtp - Tells Dovecot we are configuring the LMTP protocol.
  • auth_username_format - Ensures Dovecot uses the exact username sent by Postfix when delivering mail.

Also, we should enable LMTP in Dovecot config file.

/etc/dovecot/dovecot.conf

protocols = imap lmtp
Enter fullscreen mode Exit fullscreen mode

SSL/TLS Settings

/etc/dovecot/conf.d/10-ssl.conf

ssl = required
ssl_server_cert_file = /etc/postfix/certs/server.crt
ssl_server_key_file = /etc/postfix/certs/server.key
Enter fullscreen mode Exit fullscreen mode

These settings make sure that all client connections to Dovecot are encrypted, keeping passwords and emails secure.

  • ssl = required - Forces all connections to use SSL/TLS.
  • ssl_server_cert/key_file - Specifies the certificate and private key Dovecot uses for TLS encryption.

! Since we are creating a local mail server, we won't need this, as we won't be trying to send messages to external domains such as gmail.

Firewall Settings

Let's allow 143 port for IMAP / POP3 protocols.

sudo ufw allow 143/tcp
sudo ufw reload
Enter fullscreen mode Exit fullscreen mode

Even though our mail server is local, I'm still creating is inside my corporate network and email clients and Postfix still rely on DNS to resolve domain names.

  • Postfix needs to know where to send mail ( MX records ) even if it's just internal.
  • Clients need to resolve the domain to my mail server's IP.
  • Forward zones help our DNS server map our domain to our mail server.

DNS Zones Set Up

Forward Zone maps domain names to IP addresses.

  • Zone file: Contains all the records for your domain.
  • Forwarding: Internal clients query your DNS for domain.com. If the DNS knows about the zone, it replies; otherwise, it can forward requests to external DNS.
  • Records: The forward zone contains different types of records.

  • A RECORD - Maps a hostname to an IP address.

  • ANAME - Some DNS server support aliases as a CNAME replacement at the root domain.

  • CNAME - Creates an alias from one hostname to another.

  • MX Record - Directs email to the mail server for our domain.

Putting It All Together

  • A Record → points mail.domain.com → 192.168.x.x.
  • CNAME (optional) → smtp.domain.com → mail.domain.com.
  • ANAME / ALIAS (optional) → domain.com → mail.domain.com.
  • MX Record → domain.com → mail.domain.com with priority 10.

Monitoring logs is crucial for:

  • Checking mail delivery process.
  • Detecting authentication or TLS issues.
  • Debugging error during SMTP, LMTP, or IMAP operations.

Logging Mail Server

Postfix logs are usually written to syslog.

sudo tail -f /var/log/mail.log

What to look for:

Log Entry Meaning
status=sent Email was successfully delivered.
status=bounced Delivery failed (check recipient or MX).
reject_unauth_destination Someone tried to relay through your server — Postfix blocked it.
sasl_method=PLAIN Authentication attempt logged.

To see Dovecot logs authentication and mail access we can use journalctl -u dovecot. In order to extend the logs we should edit the /etc/dovecot/conf.d/10-logging.conf.

auth_debug_passwords = yes
log_debug=category=mail
mail_plugins {
        notify = yes
        mail_log = yes
}
Enter fullscreen mode Exit fullscreen mode
  • auth_debug_password - Shows logs in case of password mismatches.
  • log_debug=category=mail - Shows mail process debugging, this helps to figure out why Dovecot isn't finding mails.
  • mail_plugins - Provides more event logging for mail processes.

To check if ports are listening:

sudo ss -tulpn | grep -E '25|587|456|143'
Enter fullscreen mode Exit fullscreen mode

Testing Local Mail Server

Now that Postfix and Dovecot are configured, it's crucial to verify that:

  • SMTP works.
  • LMTP delivery to Maildir works.
  • IMAP works.

We can do this easily with Telnet ( or OpenSSL ).

First thing first, restart your services.

sudo systemctl restart postfix
sudo systemctl restart dovecot
Enter fullscreen mode Exit fullscreen mode

Test SMTP ( Port 587 / 25 )

telnet domain 587
Enter fullscreen mode Exit fullscreen mode

You should see a response like

220 domain ESMTP Postfix
Enter fullscreen mode Exit fullscreen mode

Now send a test email manually:

EHLO domain.com
AUTH LOGIN
MAIL FROM:<sender@domain>
RCPT TO:<receiver@domain>
DATA
Subject: Test Email
Hello! This is a test.
.
QUIT
Enter fullscreen mode Exit fullscreen mode

Test IMAP ( Port 143 )

telnet domain 143
Enter fullscreen mode Exit fullscreen mode

You should see:

* OK Dovecot ready.
Enter fullscreen mode Exit fullscreen mode

Now check an inbox.

a login sender@domain.com passwd
b select inbox
Enter fullscreen mode Exit fullscreen mode

Conclusion

You now have a fully functional local mail server that is capable of handling virtual users, sending and receiving emails, and securely storing messages in Maildir format!

If you have any questions or suggestions, I will be happy to hear them! Criticism is also welcome!

Top comments (0)