Installation and configuration of mail server with Postfix ands devcot

Date: May 2026 Version:

Begin with: https://neptunet.fr/messagerie-debian/
=> but use postfix-admin web service to configure postfix

After: https://www.digitalocean.com/community/tutorials/how-to-configure-a-mail-server-using-postfix-dovecot-mysql-and-spamassassin
=> here all in command line

For the next:

Package installation

mariadb-server: fork de MySql

Open ports used by mailserver:

Configuration of SGBD:

Database and users:

CREATE DATABASE postfix;

CREATE USER 'postfix'@'localhost' IDENTIFIED BY '<password>';

GRANT ALL PRIVILEGES ON `postfix` . * TO 'postfix'@'localhost';

CREATE USER 'mailuser'@'localhost' IDENTIFIED BY '<password>';

GRANT SELECT ON `postfix`.* TO 'mailuser'@'localhost';

FLUSH PRIVILEGES;

QUIT ;

Create 3 tables:

 CREATE TABLE `virtual_domains` (
     `id`  INT NOT NULL AUTO_INCREMENT,
     `name` VARCHAR(50) NOT NULL,
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `virtual_users` (
       `id` INT NOT NULL AUTO_INCREMENT,
       `domain_id` INT NOT NULL,
       `use_id` VARCHAR(120) NOT NULL,
       `password` VARCHAR(106) NOT NULL,
       `email` VARCHAR(120) NOT NULL,
       `home` VARCHAR(255) NOT NULL,
       `uid` INTEGER NOT NULL,
       `gid` INTEGER NOT NULL,
       PRIMARY KEY (`id`),
       UNIQUE KEY `email` (`email`),
       FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `virtual_aliases` (
     `id` INT NOT NULL AUTO_INCREMENT,
     `domain_id` INT NOT NULL,
     `source` varchar(100) NOT NULL,
     `destination` varchar(100) NOT NULL,
     PRIMARY KEY (`id`),
     FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 INSERT INTO `postfix`.`virtual_domains`
     (`id` ,`name`)
     VALUES
     ('1', 'peanutsworld.org');

=> for instance manage only ‘peanutsworld.org’ not ‘micro-cosme.com’ because it seems to be a mess ton manage ssl certificat for multi domains

 INSERT INTO `postfix`.`virtual_users` (`id`, `domain_id`, `usernameid`, `password` , `email`, `home`, `uid`, `gid`) VALUES
    ('1', '1', 'snoopy', ENCRYPT('<password>', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'snoopy@peanutsworld.org', '/var/mail/vhosts/peanutsworld.org/snoopy', 5000, 5000),
    ('2', '1', 'charlie', ENCRYPT('<password>', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'charlie@peanutsworld.org', '/var/mail/vhosts/peanutsworld.org/charlie', 5000, 5000);

 INSERT INTO `postfix`.`virtual_aliases` (`id`, `domain_id`, `source`, `destination`) VALUES
    ('1', '1', 'dog@peanutsworld.org', 'snoopy@peanutsworld.org'),
    ('2', '1', 'flyingace@peanutsworld.org', 'snoopy@peanutsworld.org'),
    ('3', '1', 'boy@peanutsworld.org', 'charlie@peanutsworld.org'),
    ('4', '1', 'master@peanutsworld.org', 'charlie@peanutsworld.org'),

Postfix configuration

main.cf

 smtpd_sasl_type = dovecot
 smtpd_sasl_path = private/auth
 smtpd_sasl_auth_enable = yes
 smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
 virtual_transport = lmtp:unix:private/dovecot-lmtp
 virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
 virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
 virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf
 smtpd_tls_key_file = /etc/ssl/private/ssl-cert-snakeoil.key
 smtpd_tls_cert_file = /etc/ssl/certs/ssl-cert-snakeoil.pem
 smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
 smtpd_tls_security_level = none
 smtp_tls_security_level = none
 mydestination = localhost

Domain(s): mysql-virtual-mailbox-domains.cf

 user = mailuser
 password = <password>
 hosts = 127.0.0.1
 dbname = postfix
 query = SELECT 1 FROM virtual_domains WHERE name='%s'

Mails/users: postfix/mysql-virtual-mailbox-maps.cf

 user = mailuser
 password = <password>
 hosts = 127.0.0.1
 dbname = postfix
 query = SELECT 1 FROM virtual_users WHERE email='%s'

SMTP Relay:

Cf: https://superuser.com/questions/280165/how-do-i-change-postfix-port-from-25-to-587

Alias : /etc/postfix/mysql-virtual-alias-maps.cf

 user = mailuser
 password = Fr3dL!k3M4ri4DB
 hosts = 127.0.0.1
 dbname = postfix
 query = SELECT destination FROM virtual_aliases WHERE source='%s'

Dovecot configuration

/etc/dovecot/dovecot.conf

 !include_try /usr/share/dovecot/protocols.d/*.protocol
 protocols = imap lmtp

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

 mail_driver = maildir
 mail_path = /var/mail/vhosts/%{user | domain}/%{user | username}
 mailbox_list_layout = fs
 mail_inbox_path = /var/mail/vhosts/%{user | domain}/%{user | username}/INBOX
 mailbox_directory_name = DataMail

/var/mail

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

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

 sql_driver = mysql

 mysql localhost {
  user = mailuser
  password = Fr3dL!k3M4ri4DB
  dbname = postfix
  #ssl = yes                  
  #ssl_client_ca_dir = /etc/ssl/certs                                                                                                                                                         
 }

 passdb sql {
       passdb_default_password_scheme = SHA512-CRYPT

       query = SELECT  user_id as username, \
                       password, \
                       (SELECT name FROM virtual_domains WHERE id = domain_id) as domaine_name \
               FROM  virtual_users \
               WHERE user_id = '%{user | username}' AND \
                     domain_id = (SELECT id FROM virtual_domains WHERE name =  '%{user | domain}');
 }

 userdb sql {
    query = SELECT home, uid, gid \
                FROM virtual_users \
            WHERE user_id= '%{user | username}' AND \
                  domain_id = (SELECT id FROM virtual_domains WHERE name =  '%{user | domain}');;
 # For using doveadm -A:                                                                                                                                                                       
  iterate_query = SELECT user_id AS username FROM virtual_users;
 }

/etc/dovecot ownership

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

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

 service lmtp {
   unix_listener /var/spool/postfix/private/dovecot-lmtp {
       mode = 0600
       user = postfix
       group = postfix
   }
  #inet_listener lmtp {
    # Avoid making LMTP visible for the entire internet
    #address =
    #port =
  #}
 } 

 service auth {
  unix_listener /var/spool/postfix/private/auth {
   mode = 0666
   user = postfix
   group = postfix
  }

  unix_listener auth-userdb {
   mode = 0600
   user = vmail
   group = vmail
  }

  #unix_listener /var/spool/postfix/private/auth {
  # mode = 0666
  #}

  user = dovecot
 }

 service auth-worker {
  # Auth worker process is run as root by default, so that it can access 
  # /etc/shadow. If this isn't necessary, the user should be changed to 
  # $SET:default_internal_user. 
  user = vmail
 }

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

 protocol lmtp {
  auth_username_format = %{user}
 }

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

Mailserver secure

To secure mailserver you need to have a certificat for your domain associated with email: eg: snnopy@peanutsworld.org -> a certificat for ‘peanutsworld.org’

DNS entry domain.name <-> ip of mailserver

CERTBOT.EFF process (see below) work well, but you need to have a DNS entry associating ‘domain’ and the machine (in this case mailserver) from you make the certificat request.

Get a SSL certificat:

 Saving debug log to /var/log/letsencrypt/letsencrypt.log
 Please enter the domain name(s) you would like on your certificate (comma and/or space separated) (Enter 'c' to cancel): peanutsworld.org peanuts.world
 Requesting a certificate for peanutsworld.org and peanuts.world

 Successfully received certificate.
 Certificate is saved at: /etc/letsencrypt/live/<domain/fullchain.pem
 Key is saved at:         /etc/letsencrypt/live/<domaon/privkey.pem
 This certificate expires on year-month-day.
 These files will be updated when the certificate renews.
 
 NEXT STEPS:
 - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps
   to enable that functionality . See https://certbot.org/renewal-setup for instructions.

SSL Certificat renewal

Cf : https://eff-certbot.readthedocs.io/en/latest/using.html#setting-up-automated-renewal

#> SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

#> sh -c 'printf "#!/bin/sh\nservice postfix stop\nservice dovecot stop\n" > /etc/letsencrypt/renewal-hooks/pre/mailserver.sh'
#> sh -c 'printf "#!/bin/sh\nservice postfix start\nservice dovecot start\n" > /etc/letsencrypt/renewal-hooks/post/mailserver.sh'
#> chmod 755 /etc/letsencrypt/renewal-hooks/pre/mailserver.sh
#> chmod 755 /etc/letsencrypt/renewal-hooks/post/mailserver.sh

Postfix: /etc/postfix/main.cf

 smtpd_tls_key_file = /etc/letsencrypt/live/peanutsworld.org/privkey.pem
 smtpd_tls_cert_file = /etc/letsencrypt/live/peanutsworld.org/fullchain.pem
 smtpd_tls_security_level = encrypt

Postfix: /etc/postfix/master.cf

 submission inet  n       -       y       -       -       smtpd

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

 ssl = yes
 ssl_server_cert_file = /etc/letsencrypt/live/peanutsworld.org/fullchain.pem
 ssl_server_key_file = /etc/letsencrypt/live/peanutsworld.org/privkey.pem

Port 993, 587

#> ufw allow 993
#> ufw allow 587

Test SSL configuration:

To be sure all is well configured before to try with your email client (sometimes its sucks, like thunderbird, because other thinks that ssl ..):

IMAP:

#> openssl s_client -crlf -connect <mail_server_ip>:993 :

All information about ssl certificat are displaye and:

 OK [CAPABILITY IMAP4rev1 LOGIN-REFERRALS ID ENABLE IDLE SASL-IR LITERAL+ AUTH=PLAIN AUTH=LOGIN] Dovecot ready.
 01 login <user> <password> :
 01 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS [...] NOTIFY LITERAL+] Logged in

SMTP :

All information about ssl certificat are displayed