Mail Server with Postfix, Dovecot and Virtual Users with PostgreSQL

Author: Paul Sueno
Created: 4/26/2020 // Updated: 8/4/2020


Set up a mail server with virtual users. The backbone will be Postfix. Access to mail by Dovecot. Virtual users through PostgreSQL DB.

This is an old version. To read the most recent version, click on the mail tutorial blog for Ubuntu 20.04.

A mail server allows you to send and receive emails. It requires an MTA, and we will use Postfix for this. To access the mail easily, an IMAP service will be set up. Dovecot will serve this role for us. Basic email servers on Linux deliver mail for local users only. But what we want instead is a custom set of email addresses for our registered domain. This is where virtual users comes into play. A PostgreSQL database will be used to set up the database of email addresses (or usernames) and passwords.

This tutorial will show you how to install the Postfix MTA server, Dovecot IMAP server and create PostgreSQL virtual user database on Ubuntu 18.04. We will be organizing/saving our emails on the server by Maildir format. You will need TLS certificates set up and PostgreSQL installed on your server already. If you've followed along with my other tutorials, then proceed.

If you haven't already:

Adjust DNS and firewall settings

The world wide web needs to know where to forward emails sent to your domain. You do this by setting your domain registrar mail mx records. Log into your domain registrar and create the following record type, host, value and priority (if needed). Be sure to replace with your own. For this tutorial, I will not be using ipv6 for my email server. I will still set up the AAAA record for other functionality.

  • A Record, mail, [your ipv4]
  • AAAA Record, mail, [your ipv6]
  • MX Record, @,, 10
  • MX Record,,, 10

You also need to create a reverse DNS pointer record (PTR). This is assigned through your cloud hosting service. Make sure your ipv4 PTR is set to, where you replace with your own. I use Linode. Here's their howto for this.

While you go through the rest of this tutorial, the DNS changes should propagate throughout the inter web.

For the firewall, make sure to allow traffic for SMTP (25,465), IMAP (993) and POP3 (995). I opened both unencrypted and encrypted traffic for SMTP (receive/send email); and only encrypted traffic for IMAP/POP3 (read email). If you have the formatted iptables file, add these lines below the http/https ports via sudo nano /etc/iptables/rules.v4. Otherwise, run sudo iptables -L -v -n --line-numbers to find the line number of the http/https ports, and instead of append do insert -I INPUT X where X is the line number.

-A INPUT -p tcp --dport 25 -j ACCEPT -m comment --comment "smtp"
-A INPUT -p tcp --dport 465 -j ACCEPT -m comment --comment "smtps"
-A INPUT -p tcp --dport 993 -j ACCEPT -m comment --comment "imaps"
-A INPUT -p tcp --dport 995 -j ACCEPT -m comment --comment "pop3s"

Afterwards, back up your firewall via sudo iptables-save > /etc/iptables/rules.v4. If you are using ipv6 for email, then do the same for ipv6.

Create virtual email database

The basic email servers for Linux only recognize local users. Local users are the users that are allowed to log into your Ubuntu server. This is not the type of email server we want. Instead, we want to set up the mail server for a bunch of users (email addresses, e.g.,, who do not have access to our Ubuntu server. This is where virtual comes in. They are email users (virtual) but are not true Ubuntu users on our server. This assumes you have your own domain. Throughout this tutorial, you must replace with the one you have registered.

Create the PostgreSQL user and database for the virtual users. The database and username will be the same name. Use your own password. I recommend sticking with letters and numbers. The password will be passed to other services on your server. Oftentimes the scripting language used between the services will require escaping symbols. To make things easier, just don't use symbols. I recommend saving a local file only accesible to you that stores the password, and then just use a long random string. Here's a simple command to generate a random string with 20 characters: cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1. Be sure to actually type in the single-quote ' around your password in the command below. Please don't use password as your password.

Go ahead and log into phpPgAdmin as a super user and click on SQL in the upper right corner. Type in the following commands in the pop-up window. Under server, keep it as PostgreSQL. Make sure schema is public. OK to leave Database as --. You must Execute each line separately.

CREATE USER virtualmail WITH PASSWORD 'password';
CREATE DATABASE virtualmail;
GRANT ALL ON DATABASE virtualmail TO virtualmail;

To see if you set it up correctly, go to the main phpPgAdmin page and look for your database on the left panel. Click on it. On the main panel, click on Privileges and see that the user (or Role) you created is granted access to this database.

We want PostgreSQL to be able to create hashed and encrypted entries. If you would rather execute SQL commands by Ubuntu bash command line, then use this format. You can also execute the SQL command inside the quotes using the phpPgAdmin method. From here on out, I'll just put in SQL commands, and I'll let you decide which method to use.

sudo -i -u postgres psql -c "CREATE EXTENSION pgcrypto;"

Create tables. Every SQL database is a collection of tables. Before doing this, let's log out of phpPgAdmin super user; and log in again as virtualmail user/role instead. The first table we'll create is the email domain our server will be used for. We'll insert values into our tables later on. Make sure the phpPgAdmin SQL pop-up window has virtualmail as the Database.

CREATE TABLE virtual_domains(
  name varchar(50) NOT NULL

Create the virtual users database. This is the main database that stores the email addresses and the hashed passwords. You can set whatever default quota storage you want. Here, I set it as 500M. This is based on Dovecot's quota documentation, recommending use of b, k, M, G, T or % suffixes.

CREATE TABLE virtual_users(
  domain_id int NOT NULL,
  email varchar(100) NOT NULL UNIQUE,
  password varchar(250) NOT NULL,
  quota varchar(20) DEFAULT '500M',
  FOREIGN KEY (domain_id) REFERENCES public.virtual_domains (id)

Let's add a column comment, so you see how this is done in PostgreSQL.

COMMENT ON COLUMN virtual_users.password IS 'This is hashed';

Aliases are important to point specific email addresses to one account. For example, let's assume you create your personal email address as You can point the source emails (e.g.,, and to the destination

CREATE TABLE virtual_aliases(
  domain_id int NOT NULL,
  source varchar(100) NOT NULL,
  destination varchar(100) NOT NULL,
  FOREIGN KEY (domain_id) REFERENCES public.virtual_domains (id)

Now let's insert some values into our tables. We'll start with the domain.

INSERT INTO public.virtual_domains (name) VALUES ('');

For the email addresses, we will need to submit a password and then store the one-way hashed password. We will use a PHP script to output the hashed password. For password hashing, we will utilize SHA512 with a 16 character salt. The prefix $6$ signifies SHA512. We'll store the script in your home directory: nano ~/virtualemailpassword.php. Be sure to use your own password, and make sure it is strong. Obviously, do not share any of the file contents with anybody. Feel free to delete 'password' or the file entirely afterwards.

$pass = 'password';
$salt = substr(hash('sha1',rand(10000,999999)),-16);
$passHash = crypt($pass, '$6$'.$salt);
echo $passHash;

Now let's get our hashed password. Run php -f ~/virtualemailpassword.php. Copy the output, and replace [password-hash] with this value when we insert our first virtual email in the database.

INSERT INTO public.virtual_users 

Insert some aliases into the database.

INSERT INTO public.virtual_aliases
  ('1', '', ''),
  ('1', '', ''),
  ('1', '', ''),
  ('1', '', '');

Now that the virtual email database is set up, let's install the mail server.

Install Postfix

To send and receive email, we need to install and set up a message transfer agent (or MTA). We will use Postfix.

Update package repositories and install postfix package. We'll include two additional packages. These will allow Postfix to interface with our PostgreSQL database and use Perl compatible regular expressions (or PCRE) to help prevent spam mail.

sudo apt update
sudo apt install postfix postfix-pgsql postfix-pcre

When the installer configuration pops up, select Internet Site as our mail configuration. Your mail name should be your domain, as in

Back up Postfix configuration files and create our own configuration files.

sudo cp /etc/postfix/ /etc/postfix/
sudo cp /etc/postfix/ /etc/postfix/
sudo touch /etc/postfix/
sudo touch /etc/postfix/
sudo touch /etc/postfix/
sudo touch /etc/postfix/

Now it's time to customize Postfix.

Postfix - configuration file

The Postfix configuration file is where we set all the options about our email server. We'll do it in chunks, so that it'll easier to follow along.

We want to use our Certbot Let's Encrypt TLS certificate to make sure our emails are sent encrypted. Unfortunately, we can only ensure connections to our server are encrypted. We cannot ensure other MTA's would also use TLS encrypted connections. Run sudo nano /etc/postfix/ Look for # TLS parameters and replace the lines in that paragraph with the lines below. Be sure to substitute the names and locations of your own certificates!!

# TLS paramters
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_tls_auth_only = yes
smtp_tls_security_level = may
smtpd_tls_security_level = may
smtpd_tls_loglevel = 2
smtp_tls_loglevel = 2
smtpd_tls_received_header = yes
smtp_tls_CApath = /etc/ssl/certs
smtpd_tls_CApath = /etc/ssl/certs

Immediately following the above lines, I set which encryption versions and ciphers I allow and disable. I based the list on the blog at

# TLS cipher suites
smtpd_tls_protocols = TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3
smtp_tls_protocols = TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3
smtp_tls_ciphers = high
smtpd_tls_ciphers = high
smtpd_tls_mandatory_protocols = TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3
smtp_tls_mandatory_protocols = TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3
smtp_tls_mandatory_ciphers = high
smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtpd_tls_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtp_tls_mandatory_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtp_tls_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
tls_preempt_cipherlist = yes

We will completely replace the last block of lines in configuration file, e.g., all the lines after # See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for. Use the Postfix Configuration Parameters site for details on each line. Be sure to replace your own settings in the example below. This is especially the case for and [my ipv4 address] (which should be your public ip address in x.x.x.x given to you by your cloud hosting company). I do not use ipv6 for my email server.

# Basic configuration
myhostname =
myorigin = $mydomain
mydestination = localhost
mynetworks = [::1]/128
relay_domains =
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = [your ipv4 address],
inet_protocols = ipv4

Mail handling settings will tell Postfix which senders to accept/reject (e.g., emails from), which recipients to accept/reject (e.g., emails to) and other accept/reject parameters. Header checks is a file of regular expression lines that I use to reject server name patterns that send me spam. reject_rbl_client lines tell Postfix to look up the sender in blacklists set up by the servers named. The other settings help prevent sloppy spammers from getting to you (won't prvent more savvy ones); and ensure only people on your network and in your database are allowed to use your MTA.

If you allow everybody access to your MTA, your cloud service provider may kick you off and your mail server IP will likely get blacklisted.

# Mail handling
header_checks = pcre:/etc/postfix/pcre_headers
smtpd_client_restrictions =
  check_client_access hash:/etc/postfix/client_access
  check_sender_access hash:/etc/postfix/sender_email_access
smtpd_helo_restrictions =
smtpd_sender_restrictions =
smtpd_recipient_restrictions =
smtpd_relay_restrictions =
smtpd_data_restrictions =

Create some of the look up files we noted in our configuration file. We will modify these once we start getting spam emails. Hopefully you don't.

sudo touch /etc/postfix/pcre_headers
sudo touch /etc/postfix/client_access
sudo touch /etc/postfix/sender_email_access
sudo postmap /etc/postfix/client_access
sudo postmap /etc/postfix/sender_email_access

The new Postfix settings have to be reloaded.

sudo service postfix reload

Let's test sending an email. You can use the mail command, but will need to install it first. Run sudo apt install mailutils. Now let's test it out. Be sure to replace your own email address you currently use.

echo "Test email being sent." | mail -s "Test Email Subject"

Next step is to tell Postfix to look up email addresses in our PostgreSQL database.

Postfix - virtual users

The following files tell Postfix to lookup specific query results in our PostgreSQL database. Be sure to replace your own PostgreSQL virtualmail user password.

Run sudo nano /etc/postfix/

user = virtualmail
password = [password]
hosts =
dbname = virtualmail
query = SELECT 1 FROM virtual_domains WHERE name='%s'

Run sudo nano /etc/postfix/

user = virtualmail
password = [password]
hosts =
dbname = virtualmail
query = SELECT 1 FROM virtual_users WHERE email='%s'

Run sudo nano /etc/postfix/

user = virtualmail
password = [password]
hosts =
dbname = virtualmail
query = SELECT destination FROM virtual_aliases WHERE source='%s'

Run sudo nano /etc/postfix/

user = virtualmail
password = [password]
hosts =
dbname = virtualmail
query = SELECT email FROM virtual_users WHERE email='%s'

Modify Postfix configuration file by running sudo nano /etc/postfix/ Add these lines at the bottom of the file.

# Virtual domains, users and aliases
virtual_mailbox_domains = pgsql:/etc/postfix/
virtual_mailbox_maps = pgsql:/etc/postfix/
virtual_alias_maps = 

Postfix now knows which virtual email users to serve.

Postfix - master process

The Postfix daemon processes control how Postfix is called and when. We'll modify the master process configuration file. Run sudo nano /etc/postfix/

Uncomment the following lines and make sure they read as the following. The lines should be 17-20.

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

Do the same for this line, should be line 23.

  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

Do the same for the following lines, should be lines 28-32.

  -o milter_macro_daemon_name=ORIGINATING
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes

Do the same for this line, should be line 34.

  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

Do the same for this line, should be line 39.

  -o milter_macro_daemon_name=ORIGINATING

Reload the Postfix configuration settings by running sudo service postfix reload.

Dovecot - IMAP service

IMAP is a way for us to access our emails remotely. It is separate from MTA. We will use Dovecot as our IMAP service.

Install Dovecot package and other related packages.

sudo apt install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-pgsql

Back up the files we will be editing.

sudo cp /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.bak
sudo cp /etc/dovecot/conf.d/10-mail.conf /etc/dovecot/conf.d/10-mail.conf.bak
sudo cp /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.bak
sudo cp /etc/dovecot/conf.d/auth-sql.conf.ext /etc/dovecot/conf.d/auth-sql.conf.ext.bak
sudo cp /etc/dovecot/dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext.bak
sudo cp /etc/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf.bak
sudo cp /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.bak
sudo cp /etc/dovecot/conf.d/15-lda.conf /etc/dovecot/conf.d/15-lda.conf.bak
sudo cp /etc/dovecot/conf.d/20-imap.conf /etc/dovecot/conf.d/20-imap.conf.bak

I wanted to ensure my users know I would not be snooping into their mail. Thus, I encrypt their email by using Dovecot's encryption module and creating a key.

sudo -i
cd /root
openssl ecparam -name prime256v1 -genkey | openssl pkey -out dovecotpriv.pem
mv dovecotpriv.pem /etc/dovecot/private/
openssl pkey -in /etc/dovecot/private/dovecotpriv.pem -pubout -out /etc/dovecot/private/dovecotpub.pem
chmod -R 660 /etc/dovecot/private/

Modify the Dovecot conifguration file by running sudo nano /etc/dovecot/dovecot.conf. Add these lines at the bottom. These allow the encyrption and quota modules.

plugin {
  mail_crypt_global_private_key = </etc/dovecot/private/dovecotpriv.pem
  mail_crypt_global_public_key = </etc/dovecot/private/dovecotpub.pem
  mail_crypt_save_version = 2
  quota = maildir:User quota
  quota_rule = *:storage=10M
  quota_rule2 = Trash:storage=+10%%
  quota_warning = storage=95%% quota-warning 95 %u
  quota_warning2 = storage=80%% quota-warning 80 %u
  quota_warning3 = -storage=100%% quota-warning below %u # user is no longer over quota
service quota-warning {
  executable = script /usr/local/bin/
  # use some unprivileged user for executing the quota warnings
  user = vmail
  unix_listener quota-warning {

Create the quota warning script by running sudo nano /usr/local/bin/

cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
Subject: quota warning

Your mailbox is now $PERCENT% full.

Make the file executable by running sudo chmod 775 /usr/local/bin/

Run sudo nano /etc/dovecot/conf.d/10-mail.conf.

[Change line, should be 30] mail_location = maildir:/var/mail/vhosts/%d/%n
[Change line, should be 215] mail_plugins = $mail_plugins quota mail_crypt

Let's create the Maildir directories and create the Ubuntu user and group vmail.

sudo mkdir -p /var/mail/vhosts/
sudo groupadd -g 5000 vmail
sudo useradd -g vmail -u 5000 vmail -d /var/mail
sudo chown -R vmail:vmail /var/mail

Run sudo nano /etc/dovecot/conf.d/10-auth.conf.

[uncomment line, should be 10] disable_plaintext_auth = yes
[change line, should be 100] auth_mechanisms = plain login
[comment out line, should be 122] #!include auth-system.conf.ext
[uncomment line, should be 123] !include auth-sql.conf.ext

Run sudo nano /etc/dovecot/conf.d/auth-sql.conf.ext. When deleting contents of a file, be sure to read them first. The comments are very helpful for guidance and troubleshooting. You can always read the back ups we created above. Delete all the lines in this file, and replace them with the following.

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

Run sudo nano /etc/dovecot/dovecot-sql.conf.ext. Delete the lines in the file, and replace them with below. Be mindful of your own settings.

driver = pgsql
connect = host= dbname=virtualmail user=virtualmail password=[password no quotes or brackets]
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email AS user, password, \
  'vmail' AS userdb_uid, 'vmail' AS userdb_gid, \
  CONCAT('/var/mail/vhosts/',SPLIT_PART(email,'@',1)) AS userdb_home, \
  CONCAT('*:storage=', quota) AS userdb_quota_rule \
  FROM virtual_users WHERE email='%u';
user_query = \
  SELECT CONCAT('/var/mail/vhosts/',SPLIT_PART(email,'@',1)) AS home, \
  'vmail' AS uid, 'vmail' AS gid, CONCAT('*:storage=', quota) AS quota_rule \
  FROM virtual_users WHERE email='%u';
iterate_query = SELECT SPLIT_PART(email,'@',1) AS username, \
  SPLIT_PART(email,'@',2) AS domain \
  FROM virtual_users;

Run sudo nano /etc/dovecot/conf.d/20-imap.conf.

[change line, should be 87] mail_plugins = $mail_plugins imap_quota

Run sudo nano /etc/dovecot/conf.d/10-master.conf. This tells Dovecot how to listen for connections and what to do with these connections. I will only use dovecot over encrypted connections. Read the file and know that the back up exists. Delete all the lines, and replace it with the following.

service imap-login {
  inet_listener imap {
    port = 0
  inet_listener imaps {
    port = 993
    ssl = yes
service pop3-login {
  inet_listener pop3 {
    port = 0
  inet_listener pop3s {
    port = 995
    ssl = yes
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
service imap {
service pop3 {
service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  unix_listener auth-userdb {
    mode = 0600
    user = vmail
  user = dovecot
service auth-worker {
  user = vmail
service dict {
  unix_listener dict {

Run sudo nano /etc/dovecot/conf.d/10-ssl.conf. Be sure to replace it with your own settings.

[change line, should be 6] ssl = required
[change line, should be 12] ssl_cert = </etc/letsencrypt/live/
[change line, should be 13] ssl_key = </etc/letsencrypt/live/
[change line, should be 46] ssl_dh_parameters_length = 2048
[change line, should be 49] ssl_protocols = !SSLv3 !TLSv1
[change line, should be 52] ssl_cipher_list = HIGH:!LOW:!SSLv2:!EXP:!aNULL:!MEDIUM

Run sudo nano /etc/dovecot/conf.d/15-lda.conf. Be sure to replace with your own.

change line 7: postmaster_address =

We need to tell Postfix to use Dovecot. Let's go back to the configuration file by running sudo nano /etc/postfix/ Add these lines to the end of the file.

# Dovecot stuff
smtpd_sasl_type = dovecot 
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_local_domain =  
smtpd_sasl_security_options = noanonymous
broken_sasl_auth_clients = yes

# Handing off local delivery to Dovecot's LMTP, and telling it where to store mail
virtual_transport = lmtp:unix:private/dovecot-lmtp

Change some ownership and permission settings. Then restart Postfix and Dovecot.

sudo chown -R vmail:dovecot /etc/dovecot
sudo chmod 770 /etc/dovecot
sudo service dovecot restart
sudo service postfix restart

That's it!! You now have your own email server. You can use any email client program like Thunderbird.

Up next - apps and verification

Now that we have an email server and, more importantly, a method to authenticate users, our server can host web-based software as a service (SaaS) apps. We can create a webmail app and show you how to tell the world your email system is verified by SPF, DKIM and DMARC.

Our user authentication method by IMAP can also be used to set up a private document server built on Nextcloud web-based SaaS. The tutorial will also show you how to integrate a productivity suite called OnlyOffice, which is like having your own Google Documents or Microsoft OneDrive and Office 365.

Click on the follow up blogs above to read more.


Suenotek Blog

Seattle, Washington

Cookies | Privacy | Policy

About | Contact Us