CentOS 7 - Mail System - Postfix Dovecot

Emails on CentOS 7 (Postfix & Dovecot)

Note: This howto works with Potsfix 2.10 and Dovecot 2.2 (standard on CentOS 7.x)

Sending out emails from the server

Make sure port connection to port 25 is not allowed from outside: firewall-cmd --zone=public --list-all

# yum -y install postfix mailx mutt

# vim /etc/postfix/main.cf

myhostname = mail.example.com
# this could be the same as $mydomain if you want: myhostname = example.com

mydomain = example.com

myorigin = $mydomain

#make sure both ipv4 ip and ipv6 ip have DNS PTR record (check with 'ip addr' and 'dig -x <ip_address>')
#otherwise, e.g. if only ipv4 has PTR, you might need to change inet_protocols to:
#inet_protocols = ipv4
#servers like gmail check PTR and refuse to accept any email from IP that does not have PTR
#make sure that DNS name PTR points to is not hosting default, but real FQDN (some email servers don't accept emails from IP's that have "hosting default PTR records), e.g. email.com
#btw, some hostings allow to change DNS PTR dynamically, e.g. cloudsigma: https://www.cloudsigma.com/how-to-dynamically-update-and-manage-ptr-records-in-your-cloudsigma-infrastructure/

#this is if you your server is also MX (and you want to collect incoming emails on this server)
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
#if your MX is different (gmail, protonmail, ...) and you just want the server to be able to send out notification emails, but not collecting emails itself, use following:
#mydestination = localhost.$mydomain, localhost

mynetworks = 127.0.0.0/8, [::1]/128
# adjust if your server is in different (or multiple) private networks and you want to acepting sending email from other machines in your private network also, for example:
# mynetworks = 127.0.0.0/8, [::1]/128, 10.0.0.0/24, ipv6_version_of_network_range, ....

home_mailbox = Maildir/

smtpd_banner = $myhostname ESMTP

# systemctl restart postfix 
# systemctl enable postfix 

Make sure your server hostname and/or IP is in SPF record:
@   3600    IN  TXT "v=spf1 ...original record ... ip4:our_ip_address a:our_server_hostname ~all"

Try to send test email via commandline (connecting via TCP port 25 works but sending emails outside is not possible because relaying is disabled)

$ mail -s "Test subject" your@email.com
test test test
.

Test sending emails over TCP/IP:

# yum install -y telnet

$ telnet localhost 25
EHLO mail.example.com<Enter>
250-STARTTLS
250-AUTH PLAIN LOGIN
250-AUTH=PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

MAIL FROM: <web@example.com><Enter>
250 2.1.0 Ok

RCPT TO: <outside_email@gmail.com><Enter>
250 2.1.0 Ok

DATA<Enter>
SUBJECT: Test Subject<Enter><Enter>

some random text, end with <Enter>.<Enter>
250 2.0.0 Ok: queued as EDA473680D5

QUIT<Enter>
221 2.0.0 Bye

# vim /var/log/maillog
The email needs to be sent fine in maillog

You can also use website https://www.mail-tester.com/ and send email to  temporary testing email address to see the SPAM level of your server

Relay through sendgrid

Taken from: https://sendgrid.com/docs/for-developers/sending-email/postfix/

# yum -y install cyrus-sasl-plain
# vim /etc/postfix/sasl_passwd
[smtp.sendgrid.net]:587 <sendgrid_account_username>:<sendgrid_account_password>

# vim /etc/postfix/main.cf

...
# at the end of file: SendGrid config
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
smtp_tls_security_level = encrypt
header_size_limit = 4096000
relayhost = [smtp.sendgrid.net]:587

# chmod 600 /etc/postfix/sasl_passwd
# postmap /etc/postfix/sasl_passwd
# systemctl restart postfix

Test by sending email and checking /var/log/maillog (& sendgrid activity tab)

Enabling receiving emails from outside for our domain + authentication

Make sure dig mx example.com returns your server.

Install dovecot:

# yum -y install dovecot

# vim /etc/dovecot/dovecot.conf

# line 24: uncomment
protocols = imap pop3 lmtp

# line 30: uncomment and change ( if not use IPv6 )
listen = *

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

# line 10: uncomment and change ( allow plain text auth )
disable_plaintext_auth = no

# line 100: add
auth_mechanisms = plain login

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

# line 30: uncomment and add
mail_location = maildir:~/Maildir

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

# line 96-98: uncomment and add like follows
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix 
    group = postfix 
}

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

# line 8: change (not require SSL)
ssl = no

# systemctl start dovecot 
# systemctl enable dovecot 

Update Postfix config and restart postfix:

# vim /etc/postfix/main.cf

inet_interfaces = all

# add follows to the end
# limit an email size for 10M
message_size_limit = 10485760
# limit a mailbox for 1G
mailbox_size_limit = 1073741824

# for SMTP-Auth
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
smtpd_recipient_restrictions = permit_mynetworks,permit_auth_destination,permit_sasl_authenticated,reject

Restart postfix:

# systemctl restart postfix

Allow connections from outside

# firewall-cmd --add-service=smtp --permanent 
# firewall-cmd --add-service={pop3,imap} --permanent 
# firewall-cmd --reload

Make sure postfix correctly listens on all interfaces (not just localhost):

# netstat -tunlp | grep 25

Make sure postfix correctly receives email:

1. send test email from any gmail account to "root@example.com"

2. check log & check correctly received email in maildir with mutt:

# tail /var/log/maillog

# mutt -f ~/Maildir
y

Adding SSL/TLS

First generate keys (adapted from https://www.server-world.info/en/note?os=CentOS_7&p=ssl)

# cd /etc/pki/tls/certs
# openssl genrsa -out example.com.key 2048
# openssl req -new -sha256 -key example.com.key -out example.com.csr
# openssl req -x509 -sha256 -days 365 -key example.com.key -in example.com.csr -out example.com.crt

Configure postfix & dovecot (taken from https://www.server-world.info/en/note?os=CentOS_7&p=mail&f=4):

# vim /etc/postfix/main.cf

# add to the end (replace certificates to your own one)
smtpd_use_tls = yes
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_cert_file = /etc/pki/tls/certs/host.example.com.crt
smtpd_tls_key_file = /etc/pki/tls/certs/host.example.com.key
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
#outgoing emails from server should also use TLS
smtp_use_tls = yes

# vim /etc/postfix/master.cf

#adjust
submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
#  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes

#adjust
smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes

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

ssl = yes

ssl_cert = </etc/pki/tls/certs/host.example.com.crt
ssl_key = </etc/pki/tls/certs/host.example.com.key

ssl_protocols = !SSLv2 !SSLv3

# systemctl restart postfix dovecot

# firewall-cmd --add-service={smtp-submission,smtps,pop3s,imaps} --permanent
# firewall-cmd --reload

Check imap and submission with email client (providing we have created OS user example)

Thunderbird:

Edit -> Account Settings -> Account Actions -> Add Mail Account ...
Your name: example
Email address: example@example.com
Password: <ssh_password>
Manual config
IMAP - host.example.com - 143 - STARTTLS - Normal password
SMTP - host.example.com - 587 - STARTTLS - Normal password
Username: Incoming: example Outgoing: example

Go to https://www.mail-tester.com/ and check sending outgoing mail from example@example.com

DKIM

(Adapted from http://www.linuxtechi.com/configure-domainkeys-with-postfix-on-centos-7/)
Install openDKIM and configure it:

# yum install -y opendkim
# opendkim-default-keygen
# cd /etc/opendkim/keys/
# ll

# vim /etc/opendkim.conf

Mode    sv
Socket  inet:8891@127.0.0.1
Canonicalization    relaxed/simple
Domain example.com
File /etc/opendkim/keys/default.private
KeyTable refile:/etc/opendkim/KeyTable
SigningTable    refile:/etc/opendkim/SigningTable
ExternalIgnoreList  refile:/etc/opendkim/TrustedHosts
InternalHosts   refile:/etc/opendkim/TrustedHosts

# vim /etc/opendkim/KeyTable

default._domainkey.example.com example.com:default:/etc/opendkim/keys/default.private

# vim /etc/opendkim/SigningTable

*@example.com default._domainkey.example.com

# vim /etc/opendkim/TrustedHosts 

host.example.com
example.com

Configure postfix:

# vim /etc/postfix/main.cf

smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters
milter_default_action = accept

Start opendkim and restart postfix:

# hash -r
# systemctl start opendkim ; systemctl enable opendkim ; systemctl restart postfix

Put publick key into DNS TXT entry:

# cat /etc/opendkim/keys/default.txt

Send an email and check if email is properly signed.

SPF

SPF: https://antipaucity.com/2016/04/25/turn-on-spf-filtering-with-postfix-and-centos-7/

Virtual mailboxes in MariaDB

Taken from https://www.linode.com/docs/email/postfix/email-with-postfix-dovecot-and-mariadb-on-centos-7

# yum install -y dovecot-mysql

# sudo mysql -u root -p

CREATE DATABASE mailserver;
GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1' IDENTIFIED BY 'mailuserpass';
FLUSH PRIVILEGES;
USE mailserver;

CREATE TABLE `virtual_domains` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `virtual_users` (
  `id` int(11) NOT NULL auto_increment,
  `domain_id` int(11) NOT NULL,
  `password` varchar(106) NOT NULL,
  `email` varchar(100) 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(11) NOT NULL auto_increment,
  `domain_id` int(11) 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 `mailserver`.`virtual_domains`
  (`id` ,`name`)
VALUES
  ('1', 'example.com');

NSERT INTO `mailserver`.`virtual_users`
  (`id`, `domain_id`, `password` , `email`)
VALUES
  ('1', '1', ENCRYPT('password', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'email1@example.com');

INSERT INTO `mailserver`.`virtual_aliases`
  (`id`, `domain_id`, `source`, `destination`)
VALUES
  ('1', '1', 'webmaster@example.com', 'email1@example.com'),
  ('2', '1', 'postmaster@example.com', 'email1@example.com'),
  ('3', '1', 'abuse@example.com', 'email1@example.com'),
  ('4', '1', 'admin@example.com', 'email1@example.com'),
  ('5', '1', 'info@example.com', 'email1@example.com');

Check:

# mysql -u root -p mailserver

SELECT * FROM virtual_domains;
SELECT * FROM virtual_users;
SELECT * FROM virtual_aliases;

Update postfix configuration:

# vim /etc/postfix/main.cf

# make sure $mydomain is not in mydestination configuration option if this domain is in virtual_mailbox_domains
mydestination = $myhostname, localhost.$mydomain, localhost

# virtual mailboxes in mysql
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,mysql:/etc/postfix/mysql-virtual-email2email.cf

# vim /etc/postfix/mysql-virtual-mailbox-domains.cf

user = mailuser
password = mailuserpass
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s'

# vim /etc/postfix/mysql-virtual-mailbox-maps.cf

user = mailuser
password = mailuserpass
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s'

# vim /etc/postfix/mysql-virtual-alias-maps.cf

user = mailuser
password = mailuserpass
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s'

# vim /etc/postfix/mysql-virtual-email2email.cf

user = mailuser
password = mailuserpass
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s'

# systemctl restart postfix

# postmap -q example.com mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
1

# postmap -q support@example.com mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
1

# postmap -q support@example.com mysql:/etc/postfix/mysql-virtual-email2email.cf
support@example.com

# postmap -q abuse@example.com mysql:/etc/postfix/mysql-virtual-alias-maps.cf
support@example.com

Configure dovecot:

# mkdir -p /var/mail/vhosts/example.com
# groupadd -g 5000 vmail
# useradd -g vmail -u 5000 vmail -d /var/mail/
# chown -R vmail:vmail /var/mail/
# chown -R vmail:vmail /var/mail/vhosts

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

disable_plaintext_auth = yes

auth_mechanisms = plain login

!include auth-system.conf.ext
!include auth-sql.conf.ext

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

passdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
}

userdb {
  driver = static
  args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
}

# vim /etc/dovecot/dovecot-sql.conf.ext

driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=mailuserpass
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';

# chown -R vmail:dovecot /etc/dovecot
# chmod -R o-rwx /etc/dovecot

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

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    #mode = 0666i
    mode = 0600
    user = postfix
    group = postfix
  }
}

service auth {

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

  user = dovecot
}

service auth-worker {
  user = vmail
}

# systemctl restart dovecot

Test:

 

Other

Final main.cf:

smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination
# When changing helo_client_exceptions, sender_checks or rbl_client_exceptions, db file needs to be refreshed: postmap <file>
smtpd_recipient_restrictions =
   check_client_access hash:/etc/postfix/helo_client_exceptions
   check_sender_access hash:/etc/postfix/sender_checks,
   reject_invalid_hostname,
# Following can cause issues with Auth SMTP, be careful
   reject_non_fqdn_hostname,
   reject_non_fqdn_sender,
   reject_non_fqdn_recipient,
   reject_unknown_sender_domain,
   reject_unknown_recipient_domain,
   permit_mynetworks,
   permit_sasl_authenticated,
   reject_unauth_destination,
   check_client_access hash:/etc/postfix/rbl_client_exceptions,
   reject_rbl_client cbl.abuseat.org,
   reject_rbl_client sbl-xbl.spamhaus.org,
   reject_rbl_client bl.spamcop.net,
   reject_rhsbl_sender dsn.rfc-ignorant.org,
# here might go greylisting line (check_policy_service inet:127.0.0.1:60000)
   permit

Debugging problems

# vim /etc/postfix/main.cf
debug_peer_level = 2
debug_peer_list = 127.0.0.1,localhost

# postfix reload

Sources

Postfix: https://www.server-world.info/en/note?os=CentOS_7&p=mail&f=1
Dovecot: https://www.server-world.info/en/note?os=CentOS_7&p=mail&f=2

Test 3rd party email service with telnet

$ telnet mail.provider.com 587
ehlo my.server.com<enter>
250-relay03.plus.net Hello relay [80.229.xxx.xxx]
250-SIZE 104857600
250-PIPELINING
250-AUTH PLAIN LOGIN
250 HELP

auth login<enter>
334 VXNlcm5hbWU6

enter base64encoded username (http://www.webpan.com/Customers/Email/base64_conversion.htm):
dXNlcm5hbWU=<enter>
334 UGFzc3dvcmQ6

enter base64encoded password (http://www.webpan.com/Customers/Email/base64_conversion.htm):
dXNlcm5hbWU=<enter>
235 Authentication succeeded

mail from: test_user@my.server.com<enter>
250 2.1.0 Sender OK

rcpt to: reliable-email-address@gmail.com<enter>
250 2.1.5 Recipient OK

data<enter>
354 Start mail input; end with .
Subject: This line is optional
This is test body of the email
.

250 2.6.0 <ae80543d-cb8a-5c45-ad80-23b1985df753@mail.woshub.com> [InternalId=6334284] Queued mail for delivery

quit<enter>

 

Inspecting and deleting emails from postfix queue

Display a list of queued mail, both deferred and pending (postfix has 2 queues)

# mailq

or

# postqueue -p

View message (contents, header and body) in Postfix queue

Assuming the message has the ID XXXXXXX (you can see the ID form the QUEUE)

# postcat -vq XXXXXXXXXX

Process queue now

# postqueue -f

Delete all queued mail

# postsuper -d ALL

Deleting all emails from queue which recipient is specific email address

# mailq | tail -n +2 | grep -v '^ *(' | awk  'BEGIN { RS = "" } { if ($8 == "email@address.com" && $9 == "") print $1 } ' | tr -d '*!' | postsuper -d -