Install Postfix + Dovecot e-mail server

Go to End


  • 1 Feb 2023: Correct typos.
  • 10 Oct 2022: Add DKIM setting.
  • 18 Jun 2021: Adjust TLS setting.
  • 5 Jan 2021: Increase imap-login process limit. List Dovecot full custom settings.
  • 9 Dec 2020: Define cron job to delete filtered mails.
  • 20 Sep 2020: Stop using mail-stack-delivery.
  • 8 May 2020: Correct typo errors.
  • 30 May 2019: Add anti-virus and spam mail filtering.
  • 17 May 2019: Add copying emails to external accounts.
  • 7 May 2019: Change "gksudo gedit" to "sudo gedit" as Ubuntu 18.04 dropped "gksudo". Add header_checks. Add auto creation of Trash folders. Add webmaster.
  • 29 Sep 2018: Add "body_checks" for spam control.
  • 20 Sep 2018: Increase message_size_limit to 20 times the default.
  • 5 Apr 2018: Increase message_size_limit to 10 times the default.
  • 25 Dec 2018: Publish on web.
  • 12 Apr 2014: Specify maximal_queue_lifetime to notify unsuccessful delivery immediately.
  • 2 Apr 2014: Specify fully qualified domain name.


Postfix is a mail transfer agent (MTA) responsible for sending out and receiving emails between servers.

Dovecot is a mail delivery agent (MDA) responsible for sending out and receiving emails between a server and its users.

Mail-stack-delivery was a combined package containing both Postfix and Dovecot. It is now no longer supported.

Re-direct to server ports​

Set the internet router to re-direct the following connections to server ports:

  • SMTP = port 25 (for receiving or sending emails)
  • secure SMTP = port 465 (for receiving or sending emails securely)
  • IMAP = port 143 (for retrieving emails)
  • secure IMAP = port 993 (for retrieving emails securely)
  • POP3 = port 110 (for retrieving emails)

Install both Postfix and Dovecot​

Ubuntu 20.04 does not support the combined package "mail-stack-delivery" anymore.

Install Dovecot and Postfix individually:

$ sudo apt install dovecot-imapd dovecot-pop3d
$ sudo apt install postfix

However, the previous config file "/etc/dovecot/conf.d/99-mail-stack-delivery.conf" is still retained for use, because "99" represents the last and overriding config file. This eliminates the need to change the individual config files.

(revised to install individually, 20 Sep 2020)

Reconfigure Postfix


$ sudo dpkg-reconfigure postfix

Use Tab key to change selection.

Select "Internet Site".

Enter the following information:

System mail name: <fully qualified domain name, such as "">
Root and postmaster mail recipient: <such as "kctang">
Other destinations to accept mail: <fully qualified domain name, such as "">, <server name such as "server1">, localhost.localdomain, localhost
Force synchronous updates on mail queue: No
Local networks: [::ffff:]/104 [::1]/128
Use procmail for local delivery: No
Mailbox size limit (bytes): 0
Local address extension character: +
Internet protocols to use: all

Activate the changes:

$ sudo systemctl reload postfix
$ sudo service postfix reload

Edit "" settings:

$ sudo gedit /etc/postfix/


myhostname =
# last line changed from server name to fully qualified domain name,
# otherwise some servers would not accept e-mails sent without fully qualified domain name, 2/4/2014

message_size_limit = 204800000
# last line added to increase the default 10 times, 5/4/2014
# increased to 20 times, 20/9/2018

maximal_queue_lifetime = 0
# last line added to report unsuccessful delivery immediately instead of after the default of 5 days, 12/4/2018

bounce_queue_lifetime = 0
# last line added, this should not be bigger than maximal_queue_lifetime, KCTang 20/5/2019

# smtpd_tls_mandatory_protocols = SSLv3, TLSv1 # commented, replaced below, KCTang 18/6/2021
smtpd_tls_received_header = yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache

# next 2 lines added to enhance security, KCTang 1/7/2021
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

# spf configuration, added but disabled, KCTang 1/6/2019
#check_policy_service unix:private/policyd-spf
#policyd-spf_time_limit = 3600s

# Milter configuration, added but disabled, KCTang 2/6/2019, enabled again to use dkim, KCTang 19/2/2022 
milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:/opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters
# end

#body_checks = regexp:/etc/postfix/body_checks
# last line added to refer to another file to check contents of email bodies, KCTang 29/9/2018, disabled after using amavis 1/6/2019

#header_checks = regexp:/etc/postfix/header_checks
# last line added to check headers, KCTang 30/9/2018, disabled after using amavis 1/6/2019

# virtual_alias_domains =
# last line added on 17/5/2019, but disabled since same domain name already defined as destination above, KCTang 20/5/2019

virtual_alias_maps = hash:/etc/postfix/virtual
# last line added to forward emails to another server, KCTang 17/5/2019

content_filter = smtp-amavis:[]:10024
# last line added for amavis, KCTang 1/6/2019

Edit "" settings:

$ sudo gedit /etc/postfix/


smtp      inet  n       -       y       -       -       smtpd
smtps     inet  n       -       y       -       -       smtpd
# last line uncommented
   -o smtpd_sasl_auth_enable=yes
# last line added to enable STARTTLS authentication
   -o smtpd_client_restrictions=permit_sasl_authenticated,reject
# last line added to reject if not authenticated, no space after ","
   -o smtpd_tls_wrappermode=yes
# last line added to force use of TLS
   -o milter_macro_daemon_name=ORIGINATING
# last line added

Create "body_checks" file, if required:

$ sudo gedit /etc/postfix/body_checks

Specify​ one or more lines of texts within //:

/unique text contained in email you do not want to receive/ DISCARD

"DISCARD" means delete from the server.

Create "header_checks" file, if required:

$ sudo gedit /etc/postfix/header_checks

Specify similarly.

(header checks added 7 May 2019)

Change Dovecot settings

Edit config file:

$ sudo gedit /etc/dovecot/conf.d/99-mail-stack-delivery.conf


disable_plaintext_auth = yes

mail_location = maildir:~/Maildir:LAYOUT=fs
# LAYOUT=fs added to last line, to use "/" instead of "." to denote sub-folders

auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@

# IMAP configuration
protocol imap {
        mail_max_userip_connections = 1000
        # 10 in last line increased to 1000
        imap_client_workarounds = delay-newmail

# POP3 configuration
protocol pop3 {
        mail_max_userip_connections = 50
        # 10 in last line increased to 50
        pop3_client_workarounds = outlook-no-nuls oe-ns-eoh

# LDA configuration
protocol lda {
        postmaster_address = postmaster
        mail_plugins = sieve
        quota_full_tempfail = yes
        deliver_log_format = msgid=%m: %$
        rejection_reason = Your message to <%t> was automatically rejected:%n%r

# Plugins configuration
plugin {

# Authentication configuration
auth_mechanisms = plain login

service auth {
  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/dovecot-auth {
    mode = 0660
    user = postfix
    group = postfix

# The following are additional to those in 15-mailboxes.conf.
# They are to auto create Trash folders.
# Trash folders would not be backed up with back-in-time,
# and therefore would need to be re-created after email recovery is done from back-in-time.
namespace inbox {
  mailbox Trash {
    auto = subscribe

# process_limit increased
service imap-login {
  process_limit = 200

(trash folder setting added 7 May 2019)

(full custom settings listed 5 Jan 2021)

Activate the changes:

$ sudo systmctl reload postfix
$ sudo service postfix reload
$ sudo systemctl restart dovecot
$ sudo service dovecot reload

Verify success

See whether the Postfix server is running:

$ telnet localhost 25

should display:

220 ESMTP Postfix (Ubuntu)

ehlo localhost

should display the following:


250-SIZE 102400000






250 DSN


to exit to "telnet >" prompt.


to exit telnet.

Try also:

$ telnet localhost 993

("993", not "995", 1 Feb 2023)


$ telnet localhost 465

should display either one:

Connected to localhost

Connected to


 to exit to "telnet >" prompt.


to exit telnet.

Specify internal email forwarding

Edit "aliases" file:

$ sudo gedit /etc/aliases


postmaster: kctang
webmaster: kctang
kctcl: kctcl, kctclpop


  • forwarding e-mails sent to postmaster@localhost and webmaster@localhost to kctang@localhost, no email will be left at postmaster or webmaster

(webmaster added 7 May 2019)

  • forwarding e-mails sent to kctcl@localhost to kctcl@localhost (itself) and to kctclpop@localhst, i.e. making a copy

"localhost" means "" in our case. For the email user name before "@", there is no need to create a  file system user account for it if all emails addressed to it are forwarded elsewhere. The name serves as an alias only of the email account forwarded to.

(added 17 May 2019)


Activate setting everytime the "aliases" file has been changed:

$ sudo newaliases


Copy to external email accounts

(section added, 17 May 2019)


$ sudo gedit /etc/postfix/

Specify​ at the end of the file:

virtual_alias_domains =
virtual_alias_maps = hash:/etc/postfix/virtual


$ sudo gedit /etc/postfix/virtual

Specify​ to make a copy to itself and a copy to the external email account:

# from               to one or more addresses, separated by a space

Omit making a copy to itself if only email forwarding is required.

Save and exit.

Execute after the "virtual" file is created or changed:

$ sudo postmap /etc/postfix/virtual


$ sudo systemctl restart postfix

Test by sending emails.

Install anti-virus Clamav-Daemon

(section added, 30 May 2019)

Install clamav-daemon:

$ sudo apt update
$ sudo apt install clamav-daemon            (clamav-freshclam also automatically installed)
$ sudo dpkg-reconfigure clamav-daemon   (must be done, generally accept all defaults, set yes to scan emails)
$ sudo systemctl start clamav-freshclam
$ sudo systemctl start clamav-daemon
$ sudo systemctl status clamav-freshclam    (check if running)
$ sudo systemctl status clamav-daemon       (check if running, OK if reported "/bin/mkdir /run/clamav (code=exited, status=1/FAILURE" because directory already created)
$ tail -f /var/log/clamav/clamav.log        (see running progress, Ctrl-Z to exit)

(typo corrected, 8 May 2020)

Filter spam mails

(section added, 30 May 2019)

Execute to install various software:

$ sudo apt update
$ sudo apt install amavisd-new spamassassin
$ sudo apt install postfix-policyd-spf-python                                 (optionally required for spf) 
$ sudo apt install opendkim                                                   (optionally required for opendkim)
$ sudo apt install pyzor razor                                                (optional extras)
$ sudo apt install arj cabextract cpio lhasa nomarch pax rar unrar unzip zip  ("lhasa", not "lha")

Configure ClamAV:

$ sudo adduser clamav amavis
$ sudo adduser amavis clamav

Configure SpamAssassin:

$ sudo gedit /etc/default/spamassassin

 Change "ENABLED=0" to:


(no longer set here, 8 May 2020)

Start the service:

$ sudo systemctl start spamassassin.service

Configure Amavisd-new:

$ sudo gedit /etc/amavis/conf.d/15-content_filter_mode


use strict;

# Uncomment the two lines below to enable antivirus checking mode

@bypass_virus_checks_maps = (
   \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);

# Uncomment the two lines below to enable SPAM checking mode

@bypass_spam_checks_maps = (
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);

1;  # insure a defined return


$ sudo gedit /etc/amavis/conf.d/20-debian_defaults

Change "D_PASS" to:

$final_spam_destiny = D_DISCARD

(deleted,  8 May 2020, reinstated, 26 Jul 2021)

Adjust the following values only if desired to flag more messages as spam:

$sa_tag_level_deflt = 2.0; # add spam info headers if at, or above that level
$sa_tag2_level_deflt = 6.31; # add 'spam detected' headers at that level
$sa_kill_level_deflt = 6.31; # triggers spam evasive actions
$sa_dsn_cutoff_level = 10; # spam level beyond which a DSN is not sent


$ sudo gedit /etc/amavis/conf.d/50-user


$myhostname = '';

Re-start the service:

$ sudo systemctl restart amavis.service

Edit the following file to specify domains to be whitelisted if necessary:

$ sudo gedit /etc/amavis/conf.d/40-policy_banks

Test that the Amavisd-new SMTP is listening:

telnet localhost 10024
Connected to
Escape character is '^]'.
220 [] ESMTP amavisd-new service ready

Press Ctrl-]  and enter "quit" to exit.

Configure Postfix

$ sudo gedit /etc/postfix/

Add the following to the end of the file:

smtp-amavis     unix    -       -       -       -       2       smtp
    -o smtp_data_done_timeout=1200
    -o smtp_send_xforward_command=yes
    -o disable_dns_lookups=yes
    -o max_use=20 inet    n       -       -       -       -       smtpd
    -o content_filter=
    -o local_recipient_maps=
    -o relay_recipient_maps=
    -o smtpd_restriction_classes=
    -o smtpd_delay_reject=no
    -o smtpd_client_restrictions=permit_mynetworks,reject
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=reject_unauth_pipelining
    -o smtpd_end_of_data_restrictions=
    -o mynetworks=
    -o smtpd_error_sleep_time=0
    -o smtpd_soft_error_limit=1001
    -o smtpd_hard_error_limit=1000
    -o smtpd_client_connection_count_limit=0
    -o smtpd_client_connection_rate_limit=0
    -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters

Add the following immediately below the "pickup" transport service:

     -o content_filter=
     -o receive_override_options=no_header_body_checks

Configure Postfix

$ sudo gedit /etc/postfix/

Add the following to the end of the file:

content_filter = smtp-amavis:[]:10024

Comment it with "#" at the start of the line if desired to stop using amavis.

Restart service:

$ sudo systemctl restart postfix.service

Check the hidden header of email received after the above to see the presence of one or more of the following:

X-Virus-Scanned: Debian amavisd-new at

If present, the spam filter is working.

See for a full explanation of the above.

Delete filtered mails

(section added, 9 Dec 2020)

Filtered files are stored in /var/lib/amavis/virusmails and should be deleted regularly.

Edit to define a scheduled job to delete filtered files:

$ sudo crontab -e -u amavis 


-e = edit

-u amavis = user amavis

Define as follows:

0 0 * * * find /var/lib/amavis/virusmails/ -type f -mtime +15 -delete

which means at

0 = 0 minute 

0 = 0 hour

* = every day of month

* = every month

* = every day of week

find /var/lib/amavis/virusmails = find in that directory

-type f = regular files

-mtime +15 = file modified more than 15 days ago

-delete = delete the file

Set up DKIM

(implemented, 19 Feb 2022

section added, 10 Oct 2022)

Set up DomainKeys Identified Mail (DKIM) to prevent email phishing by others by putting a digital key in the outgoing email headers so that the receiving email server can verify the digital key by looking up the public key in the DNS record of the sender domain.

Install OpenDKIM:

$ sudo apt install opendkim opendkim-tools

Add postfix user to opendkim group:

$ sudo gpasswd -a postfix opendkim


$ sudo nano /etc/opendkim.conf


# keep to log in /var/log/mail.log
Syslog             yes
SyslogSuccess      yes
# change to "yes" if desired to log more details
Logwhy             no
# keep
Canonicalization   relaxed/simple
# uncomment
Mode               sv
SubDomains         no
# keep
OversignHeaders    from
# add
AutoRestart         yes
AutoRestartRate     10/1M
Background          yes
DNSTimeout          5
SignatureAlgorithm  rsa-sha256
# keep
UserID              opendkim
UMask               007
# comment
#Socket             local:/run/opendkim/opendkim.sock
# add
Socket              local:/var/spool/postfix/opendkim/opendkim.sock
# keep
PidFile             /run/opendkim/
TrustAnchorFile     /usr/share/dns/root.key
# keep
KeyTable            refile:/etc/opendkim/key.table
SigningTable        refile:/etc/opendkim/signing.table
ExternalIgnoreList  /etc/opendkim/trusted.hosts
InternalHosts       /etc/opendkim/trusted.hosts

Create a directory structure:

$ sudo mkdir /etc/opendkim
$ sudo mkdir /etc/opendkim/keys
$ sudo chown -R opendkim:opendkim /etc/opendkim
$ sudo chmod go-rw /etc/opendkim/keys

Create the signing table:

$ sudo nano /etc/opendkim/signing.table


# if sub-domain used

Create the key table:

$ sudo nano /etc/opendkim/key.table


Create the trusted hosts file:

$ sudo nano /etc/opendkim/trusted.hosts

Specify to sign but not to verify emails from localhost or the following domain:

("*" added, 1 Feb 2023)

Create separate folder for the domain and generate Private/Public Keypair:

$ sudo mkdir /etc/opendkim/keys/
$ sudo opendkim-genkey -b 1024 -d -D /etc/opendkim/keys/ -s default -v

Use -b 2048 instead of -b 1024 for bits of key if the domain name server permits.
-d for domain name

-D for directory to store the keys

-s for selector

The private key will be written to default.private file and the public key will be written to default.txt file.

Change owner and permission:

$ sudo chown opendkim:opendkim /etc/opendkim/keys/
$ sudo chmod 600 /etc/opendkim/keys/

Display the public key:

$ sudo cat /etc/opendkim/keys/

The string after the p parameter is the public key:

default._domainkey      IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; "
          "p=..." )  ; ----- DKIM key default for

Define in the Domain Name Record on the domain name host:

Name = default._domainkey

Address or value = the text in ( ) above, but deleting double quotes and spaces

Type = SPF (txt)

Test DKIM Key:

$ sudo opendkim-testkey -d -s default -vvv

Successful if an opendkim-testkey output is key OK.

No problem if an opendkim-testkey output is key not secure because DNSSEC may not have been enabled on the domain name.

The DKIM record may take up to 24 hours to propagate to the Internet.

Can also go to, enter "" as the domain name and "default" as the selector to check DKIM record propagation.

In case of the query timed out error, comment out the following line in /etc/opendkim.conf file and restart opendkim.service.

TrustAnchorFile       /usr/share/dns/root.key

Create a directory to hold the OpenDKIM socket file, and allow only opendkim user and postfix group to access it:

$ sudo mkdir /var/spool/postfix/opendkim
$ sudo chown opendkim:postfix /var/spool/postfix/opendkim


$ sudo nano /etc/default/opendkim 

Comment out:

# SOCKET=local:$RUNDIR/opendkim.sock



Edit Postfix to configure Milter as shown above.

Restart opendkim and postfix service:

$ sudo systemctl restart opendkim postfix


End of Page