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

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

Source: Dev.to

Before we begin... ## Mail Flow Design ## Installing the Core Components ## Postfix configuration ## Postfix Ports ## Dovecot Configuration ## DNS Zones Set Up ## Logging Mail Server ## Testing Local Mail Server ## Conclusion 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 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: Dovecot Dovecot acts as the mail access server. It: 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: 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: 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. Once the architecture is clear, we can start our project. First, let's install the necessary components. 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: Now that we have installed all the main components, we can move on to configuring Postfix. These parameters define how the server identifies itself when sending and receiving mail. Network Configuration Define that there is no restrictions on mailbox size. Relay Restrictions && Recipient This prevents the server from becoming an open relay. It simply tells Postfix to not save the users and let the Dovecot do the job. For the local environments is perfect. Virtual Domain && LMTP Once Postfix is fully configured... wait! We are not done yet. 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. The master.cf file defines how Postfix listens and handles connections. Submissions defines the outgoing from clients. /etc/postfix/master.cf 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. 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. 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. 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. Configure Password and User Databases /etc/dovecot/conf.d/auth-passwdfile.conf.ext 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 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 This is where we tell Dovecot where and how to store messages for each virtual user. Authentication Settings /etc/dovecot/conf.d/10-auth.conf This file controls how users log in and which authentication backends Dovecot uses. Master Process && Socket Configuration /etc/dovecot/conf.d/10-master.conf This file controls how Dovecot runs its services ( IMAP, LMTP, authentication ) and how they communicate with other processes. LMTP Protocol Settings /etc/dovecot/conf.d/20-lmtp.conf This file controls how Dovecot handles mail delivery via LMTP from Postfix. Also, we should enable LMTP in Dovecot config file. /etc/dovecot/dovecot.conf /etc/dovecot/conf.d/10-ssl.conf These settings make sure that all client connections to Dovecot are encrypted, keeping passwords and emails secure. ! 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. Let's allow 143 port for IMAP / POP3 protocols. 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. Forward Zone maps domain names to IP addresses. 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 Monitoring logs is crucial for: Postfix logs are usually written to syslog. sudo tail -f /var/log/mail.log 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. To check if ports are listening: Now that Postfix and Dovecot are configured, it's crucial to verify that: We can do this easily with Telnet ( or OpenSSL ). First thing first, restart your services. Test SMTP ( Port 587 / 25 ) You should see a response like Now send a test email manually: Test IMAP ( Port 143 ) 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! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: sudo apt update sudo apt install postfix dovecot-imapd dovecot-lmtpd Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: sudo apt update sudo apt install postfix dovecot-imapd dovecot-lmtpd COMMAND_BLOCK: sudo apt update sudo apt install postfix dovecot-imapd dovecot-lmtpd CODE_BLOCK: myhostname = hostname.domain mydomain = domain myorigin = $mydomain Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: myhostname = hostname.domain mydomain = domain myorigin = $mydomain CODE_BLOCK: myhostname = hostname.domain mydomain = domain myorigin = $mydomain CODE_BLOCK: inet_interfaces = all inet_protocols = all mynetworks = 127.0.0.0/8 ... mydestination = localhost, localhost.$mydomain relayhost = Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: inet_interfaces = all inet_protocols = all mynetworks = 127.0.0.0/8 ... mydestination = localhost, localhost.$mydomain relayhost = CODE_BLOCK: inet_interfaces = all inet_protocols = all mynetworks = 127.0.0.0/8 ... mydestination = localhost, localhost.$mydomain relayhost = CODE_BLOCK: mailbox_size_limit = 0 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: mailbox_size_limit = 0 CODE_BLOCK: mailbox_size_limit = 0 CODE_BLOCK: smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination CODE_BLOCK: smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination CODE_BLOCK: smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_auth_enable = yes Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_auth_enable = yes CODE_BLOCK: smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_auth_enable = yes CODE_BLOCK: 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 CODE_BLOCK: 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 CODE_BLOCK: 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 CODE_BLOCK: virtual_mailbox_domains = domain virtual_transport = lmtp:unix:private/dovecot-lmtp Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: virtual_mailbox_domains = domain virtual_transport = lmtp:unix:private/dovecot-lmtp CODE_BLOCK: virtual_mailbox_domains = domain virtual_transport = lmtp:unix:private/dovecot-lmtp COMMAND_BLOCK: 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 COMMAND_BLOCK: sudo ufw allow 25/tcp sudo ufw allow 587/tcp sudo ufw allow 465/tcp sudo ufw reload sudo ufw status COMMAND_BLOCK: sudo ufw allow 25/tcp sudo ufw allow 587/tcp sudo ufw allow 465/tcp sudo ufw reload sudo ufw status CODE_BLOCK: 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 CODE_BLOCK: submission inet n - y - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes CODE_BLOCK: submission inet n - y - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes COMMAND_BLOCK: 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 COMMAND_BLOCK: sudo groupadd -g 5000 vmail sudo useradd -g 5000 -u 5000 vmail -s /bin/bash -m /home/vmail sudo passwd vmail COMMAND_BLOCK: sudo groupadd -g 5000 vmail sudo useradd -g 5000 -u 5000 vmail -s /bin/bash -m /home/vmail sudo passwd vmail CODE_BLOCK: 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 CODE_BLOCK: sender@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/sender receiver@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/receiver CODE_BLOCK: sender@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/sender receiver@domain:{PLAIN}passwd:5000:5000::/home/vmail/domain/receiver CODE_BLOCK: 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 CODE_BLOCK: 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 } } CODE_BLOCK: 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 } } CODE_BLOCK: mail_driver = maildir mail_home = /home/vmail/domain/%{user | username} mail_path = ~/Maildir Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: mail_driver = maildir mail_home = /home/vmail/domain/%{user | username} mail_path = ~/Maildir CODE_BLOCK: mail_driver = maildir mail_home = /home/vmail/domain/%{user | username} mail_path = ~/Maildir CODE_BLOCK: auth_mechanisms = plain login auth_allow_cleartext = no #!include auth-system.conf.ext !include auth-passwdfile.conf.ext Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: auth_mechanisms = plain login auth_allow_cleartext = no #!include auth-system.conf.ext !include auth-passwdfile.conf.ext CODE_BLOCK: auth_mechanisms = plain login auth_allow_cleartext = no #!include auth-system.conf.ext !include auth-passwdfile.conf.ext CODE_BLOCK: 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 CODE_BLOCK: 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 } } CODE_BLOCK: 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 } } CODE_BLOCK: protocol lmtp { auth_username_format = %{user} } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: protocol lmtp { auth_username_format = %{user} } CODE_BLOCK: protocol lmtp { auth_username_format = %{user} } CODE_BLOCK: protocols = imap lmtp Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: protocols = imap lmtp CODE_BLOCK: protocols = imap lmtp CODE_BLOCK: 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 CODE_BLOCK: ssl = required ssl_server_cert_file = /etc/postfix/certs/server.crt ssl_server_key_file = /etc/postfix/certs/server.key CODE_BLOCK: ssl = required ssl_server_cert_file = /etc/postfix/certs/server.crt ssl_server_key_file = /etc/postfix/certs/server.key COMMAND_BLOCK: sudo ufw allow 143/tcp sudo ufw reload Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: sudo ufw allow 143/tcp sudo ufw reload COMMAND_BLOCK: sudo ufw allow 143/tcp sudo ufw reload CODE_BLOCK: auth_debug_passwords = yes log_debug=category=mail mail_plugins { notify = yes mail_log = yes } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: auth_debug_passwords = yes log_debug=category=mail mail_plugins { notify = yes mail_log = yes } CODE_BLOCK: auth_debug_passwords = yes log_debug=category=mail mail_plugins { notify = yes mail_log = yes } COMMAND_BLOCK: sudo ss -tulpn | grep -E '25|587|456|143' Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: sudo ss -tulpn | grep -E '25|587|456|143' COMMAND_BLOCK: sudo ss -tulpn | grep -E '25|587|456|143' COMMAND_BLOCK: sudo systemctl restart postfix sudo systemctl restart dovecot Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: sudo systemctl restart postfix sudo systemctl restart dovecot COMMAND_BLOCK: sudo systemctl restart postfix sudo systemctl restart dovecot CODE_BLOCK: telnet domain 587 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: telnet domain 587 CODE_BLOCK: telnet domain 587 CODE_BLOCK: 220 domain ESMTP Postfix Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 220 domain ESMTP Postfix CODE_BLOCK: 220 domain ESMTP Postfix CODE_BLOCK: 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 CODE_BLOCK: EHLO domain.com AUTH LOGIN MAIL FROM:<sender@domain> RCPT TO:<receiver@domain> DATA Subject: Test Email Hello! This is a test. . QUIT CODE_BLOCK: EHLO domain.com AUTH LOGIN MAIL FROM:<sender@domain> RCPT TO:<receiver@domain> DATA Subject: Test Email Hello! This is a test. . QUIT CODE_BLOCK: telnet domain 143 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: telnet domain 143 CODE_BLOCK: telnet domain 143 CODE_BLOCK: * OK Dovecot ready. Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: * OK Dovecot ready. CODE_BLOCK: * OK Dovecot ready. CODE_BLOCK: a login [email protected] passwd b select inbox Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: a login [email protected] passwd b select inbox CODE_BLOCK: a login [email protected] passwd b select inbox - Receives incoming email - Sends outgoing email - Decides whether mail should be relayed or delivered locally - Enforces relay and security rules - Stores emails ( Maildir format ) - Provides IMAP/POP3 access - Handles user authentication - Accepts local delivery via LMTP - 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. - 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. - Every email user = real system user. - Potential shell access risks. - Harder to scale and manage. - Poor separation of concers. - 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. - reject_unauth_destination - is key option. It prohibits delivery to domain for which the server is not responsible. - That we support TLS. - TLS is supported but not required, and for production, encrypt is better. - 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. - -g 5000 - sets the primary group to vmail - -u 5000 - assigns a UID - 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_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. - 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. - 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. - 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. - 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. - 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. - 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. - 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. - Checking mail delivery process. - Detecting authentication or TLS issues. - Debugging error during SMTP, LMTP, or IMAP operations. - 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. - SMTP works. - LMTP delivery to Maildir works. - IMAP works.