Graham Eddy

dovecot & postfix servers

Install and configure dovecot (imap) and postfix (smtp) servers, co-deployed to share authentication. spamassassin is also installed to filter junk mail.

2021-06-11 Ubuntu 20.04


Overall configuration across the servers:

Domain user
user@domain
Mailbox
/var/mail/vhosts/domain/user/Maildir/
Authentication
Managed by dovecot, channel for postfix at /var/spool/postfix/private/auth
User lookup
/var/mail/vhosts/domain/passwd
Password lookup
/var/mail/vhosts/domain/shadow

The example being used is to add domain user fred.nerk@geddy.au. The server domain names are assumed to be set up as:

s1.geddy.au
Host on which the servers co-reside.
smtp.geddy.au
smtp server, pointing at s1.geddy.au. It must have a DNS A record not a DNS CNAME record to s1.geddy.au – this is part of the security paradigm between open internet smtp servers.
imap.geddy.au
imap server, pointing at s1.geddy.au.
pop.geddy.au
pop server, pointing at s1.geddy.au.

postfix

graham:~ sudo vi /etc/hostname
/etc/hostname replace all content
smtp
graham:~ sudo hostname smtp graham:~ sudo vi /etc/hosts
/etc/hosts add/update single setting
127.0.1.1  smtp.geddy.au
graham:~ hostname --all-fqdns smtp.geddy.au graham:~ sudo apt update graham:~ sudo apt install postfix mailutils bsd-mailx libsasl2-modules graham:~ sudo systemctl stop postfix # stop postfix for updating graham:~ sudo vi /etc/postfix/main.cf
/etc/postfix/main.cf replace all content
# Virtual Domain Postfix (geddy.au)
# See /usr/share/postfix/main.cf.dist for a commented, more complete version

# A safety net that causes Postfix to run with
# backwards-compatible default settings after an upgrade to a newer Postfix
# version. See http://www.postfix.org/COMPATIBILITY_README.html.
#
# new installs: set to 2
compatibility_level = 2

# The UNIX system account that owns the Postfix queue and most Postfix daemon
# processes. Specify the name of an unprivileged user account that does not
# share a user or group ID with other accounts, and that owns no other files
# or processes on the system. In particular, don't specify nobody or daemon.
# PLEASE USE A DEDICATED USER ID AND GROUP ID.
#
# default, leave un-commented
#mail_owner = postfix

# The group ownership of set-gid Postfix commands and of group-writable
# Postfix directories.
#
# default, leave commented-out
#setgid_group = postdrop

##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
## MTA Identification
##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

# The internet hostname of this mail system.
#
myhostname = smtp.geddy.au

# The domain name that locally-posted mail appears to  come  from,
# and that locally posted mail is delivered to.
#
# default, leave commented-out
#myorigin = /etc/mailname

# The list of domains that are delivered via the $local_transport mail
# delivery transport.
#
mydestination =
    $myhostname localhost localhost.$mydomain
    smtp.geddy.au
    s1.$mydomain
#   ^ note that domains are listed in virtual_mailbox_domains *instead*

# The list of "trusted" remote SMTP clients that have more privileges
# than "strangers".  In particular, "trusted" SMTP clients are allowed
# to relay mail through Postfix.
#
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128

##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
## LOCAL RECEIVE
##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

## SENDMAIL(1) :::::::::::::::::::::::::::::::::::::::::::::::::::::::

##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
## LOCAL DELIVERY
##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

## LOCAL(8) - LOCAL MAIL DELIVERY ::::::::::::::::::::::::::::::::::::

## Compatability

# Whether or not to use the local biff service.
#
biff = no

## Delivery Method

# The alias databases that are used for local(8) delivery.
#
alias_maps = hash:/etc/aliases

# The alias databases for local(8) delivery that are updated with
# "newaliases" or with "sendmail -bi".  This is a separate configuration
# parameter because not all the tables specified with $alias_maps have to
# be local files.
#
alias_database = hash:/etc/aliases

# The  set  of  characters  that can separate a user name from its
# extension (example: user+foo), or a .forward file name from  its
# extension (example: .forward+foo).
#
recipient_delimiter = +

## Resource Controls

# The  maximal  size of any local(8) individual mailbox or maildir
# file, or zero (no limit).
#
mailbox_size_limit = 0

# The maximal size in bytes of a message, including envelope information.
#
#message_size_limit = 120480000

## TRIVIAL-REWRITE(8) ::::::::::::::::::::::::::::::::::::::::::::::::

## Address Rewriting Rules

# With  locally  submitted mail, append the string ".$mydomain" to
# addresses that have no ".domain" information.
#
append_dot_mydomain = no

##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
## NETWORK RECEIVE
##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

# The network interface addresses that this mail system receives mail on.
# The parameter also controls delivery of mail to user@[ip.address].
#
inet_interfaces = all

# The Internet protocols Postfix will attempt to use when making or
# accepting connections.
#
#inet_protocols = all
inet_protocols = ipv4

## SMTPD(8) - SMTP SERVER ::::::::::::::::::::::::::::::::::::::::::::

## Compatability

# Enable  interoperability with remote SMTP clients that implement
# an obsolete version of the AUTH command (RFC 4954).
#
broken_sasl_auth_clients = yes

# Disable the SMTP VRFY command.
#
disable_vrfy_command = yes

# Require that addresses received in SMTP MAIL FROM  and  RCPT  TO
# commands  are  enclosed with <>, and that those addresses do not
# contain RFC 822 style comments or phrases.
#
strict_rfc821_envelopes = yes

## SASL

# Enable SASL authentication in the Postfix SMTP server.
#
smtpd_sasl_auth_enable = yes

# The name of the Postfix SMTP server's local SASL  authentication
# realm.
#
smtpd_sasl_local_domain = $mydomain

# Postfix SMTP server SASL security options; as of Postfix 2.3 the
# list of available features depends on the SASL server  implemen-
# tation that is selected with smtpd_sasl_type.
#
smtpd_sasl_security_options = noanonymous

# Implementation-specific information that the Postfix SMTP client
# passes through  to  the  SASL  plug-in  implementation  that  is
# selected with smtp_sasl_type.
#
smtpd_sasl_path = private/auth

# The  SASL  plug-in  type that the Postfix SMTP client should use
# for authentication.
#
smtpd_sasl_type = dovecot

## STARTTLS

# The  SMTP TLS security level for the Postfix SMTP server; when a
# non-empty value is specified, this overrides the obsolete param-
# eters smtpd_use_tls and smtpd_enforce_tls.
#
# select one:
# STARTTLS
smtpd_tls_security_level = may
# SSL/TLS
#smtpd_tls_security_level = encrypt

# When TLS encryption is optional in the Postfix SMTP server, do not
# announce or accept SASL authentication over unencrypted connections.
#
smtpd_tls_auth_only = no

# A  directory containing (PEM format) CA certificates of root CAs
# trusted to sign either remote SMTP client certificates or inter-
# mediate CA certificates.
#
# do not provide (not signing client certs)
#smtpd_tls_CApath =

# File with the Postfix SMTP server RSA certificate in PEM format.
#
smtpd_tls_cert_file = /etc/letsencrypt/live/smtp.geddy.au/fullchain.pem

# File with the Postfix SMTP server RSA private key in PEM format.
#
smtpd_tls_key_file = /etc/letsencrypt/live/smtp.geddy.au/privkey.pem

# Request that the Postfix SMTP server produces Received:  message
# headers  that  include information about the protocol and cipher
# used, as well as the remote SMTP client  CommonName  and  client
# certificate issuer CommonName.
#
smtpd_tls_received_header = yes

# Name of the file containing the optional Postfix SMTP server TLS
# session cache.
#
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache

##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
## NETWORK DELIVERY
##::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

## SMTP(8) - SMTP/LMTP CLIENT ::::::::::::::::::::::::::::::::::::::::

## STARTTLS

# The default SMTP TLS security level for the Postfix SMTP client;
# when a non-empty value is specified, this overrides the obsolete
# parameters       smtp_use_tls,       smtp_enforce_tls,       and
# smtp_tls_enforce_peername.
#
# select one:
# STARTTLS
smtp_tls_security_level = may
# SSL/TLS
#smtp_tls_security_level = encyrpt

# Directory with PEM format Certification  Authority  certificates
# that the Postfix SMTP client uses to verify a remote SMTP server
# certificate.
#
smtp_tls_CApath = /etc/ssl/certs

# File with the Postfix SMTP client RSA certificate in PEM format.
#
# do not provide
#smtp_tls_cert_file =

# File with the Postfix SMTP client RSA private key in PEM format.
#
# do not provide
#smtp_tls_key_file =

# The  external  entropy source for the in-memory tlsmgr(8) pseudo
# random number generator (PRNG) pool.
#
#tls_random_source = dev:/dev/urandom

## Resource and Rate Controls

# The time unit over which client connection rates and other rates are
# calculated.
#
anvil_rate_time_unit = 60s

# The maximal number of recipients that the  Postfix  SMTP  server
# accepts per message delivery request.
#
smtpd_recipient_limit = 250
# How  many simultaneous connections any client is allowed to make
# to this service.
#
smtpd_client_connection_count_limit = 5

# The maximal number of connection attempts any client is  allowed
# to make to this service per time unit.
#
smtpd_client_connection_rate_limit = 5

# The  number  of  errors  a remote SMTP client is allowed to make
# without delivering mail before the  Postfix  SMTP  server  slows
# down all its responses.
#
smtpd_soft_error_limit = 2

# The  number  of  errors  a remote SMTP client is allowed to make
# without delivering mail before the  Postfix  SMTP  server  slows
# down all its responses.
#
smtpd_hard_error_limit = 3

# the SMTP server response delay  after  a  client has made more than
# $smtpd_soft_error_limit errors, and fewer than $smtpd_hard_error_limit
# errors, without delivering mail.
#
smtpd_error_sleep_time = 5s

# Wait   until   the   RCPT   TO   command    before    evaluating
# $smtpd_client_restrictions,     $smtpd_helo_restrictions     and
# $smtpd_sender_restrictions, or wait until the ETRN  command  be‐
# fore  evaluating  $smtpd_client_restrictions and $smtpd_helo_re‐
# strictions.
#
smtpd_delay_reject = yes

# Require  that  a  remote  SMTP client introduces itself with the
# HELO or EHLO command before sending the MAIL  command  or  other
# commands that require EHLO negotiation.
#
smtpd_helo_required = yes
# Optional restrictions that the Postfix SMTP  server  applies  in
# the context of a client connection request.
#
smtpd_client_restrictions =
    permit_mynetworks
    permit_sasl_authenticated
    reject_unknown_client_hostname
    reject_unauth_pipelining
#    reject_rbl_client zen.spamhaus.org
#    ^ recommended use RBL in spamassassin not MTA

# Optional  restrictions  that  the Postfix SMTP server applies in
# the context of a client HELO command.
#
smtpd_helo_restrictions =
    reject_non_fqdn_hostname
    reject_invalid_helo_hostname
#    reject_unknown_helo_hostname
#    ^ rejects internal domain before chance to SASL so disable

# Optional restrictions that the Postfix SMTP  server  applies  in
# the context of a client MAIL FROM command.
#
smtpd_sender_restrictions =
    permit_mynetworks
    reject_non_fqdn_sender
    reject_unknown_sender_domain

# Optional  restrictions  that  the Postfix SMTP server applies in
# the context of a client RCPT TO command,  after  smtpd_relay_re‐
# strictions.
#
smtpd_recipient_restrictions =
    permit_mynetworks
    reject_unauth_pipelining
    permit_sasl_authenticated
    reject_invalid_hostname
    reject_non_fqdn_hostname
    reject_non_fqdn_sender
    reject_non_fqdn_recipient
    reject_unauth_destination
#    reject_rbl_client zen.spamhaus.org
#    reject_rbl_client dul.dnsbl.sorbs.net
#    ^ recommended use RBL in spamassassin not MTA

# Access restrictions for mail relay control that the Postfix SMTP
# server  applies  in  the  context of the RCPT TO command, before
# smtpd_recipient_restrictions.
#
smtpd_relay_restrictions =
    permit_mynetworks
    permit_sasl_authenticated
    defer_unauth_destination

## VIRTUAL(8) - VIRTUAL DOMAIN DELIVERY AGENT ::::::::::::::::::::::::

## Mailbox Delivery

# Optional lookup tables that alias specific mail addresses or domains
# to other local or remote address.
#
virtual_alias_maps =
    hash:/etc/postfix/virtual/geddy.au

# Postfix  is final destination for the specified list of domains;
# mail is  delivered  via  the  $virtual_transport  mail  delivery
# transport.
#
virtual_mailbox_domains = /etc/postfix/vdomain

# Optional  lookup  tables with all valid addresses in the domains
# that match $virtual_mailbox_domains.
#
virtual_mailbox_maps =
    hash:/etc/postfix/vmailbox/geddy.au

# A prefix that the virtual(8)  delivery  agent  prepends  to  all
# pathname results from $virtual_mailbox_maps table lookups.
#
virtual_mailbox_base = /var/mail/vhosts

# The minimum user ID value that  the  virtual(8)  delivery  agent
# accepts as a result from $virtual_uid_maps table lookup.
#
virtual_minimum_uid = 100

# Lookup tables with the per-recipient user ID that the virtual(8)
# delivery agent uses while writing to the recipient's mailbox.
#
virtual_uid_maps = static:5000

# Lookup tables with the per-recipient  group  ID  for  virtual(8)
# mailbox delivery.
#
virtual_gid_maps = static:5000

# The default mail delivery transport and next-hop destination for
# final delivery to domains listed with  $virtual_mailbox_domains.
#
#virtual_transport = virtual

# call Dovecot's delivery (virtual) for each recipient instead of only
# one time for all recipients.
#
dovecot_destination_recipient_limit = 1
graham:~ sudo vi /etc/aliases
/etc/aliases replace all content
# formulate delivery address for (apparent) local user
  
# important local mail roles
postmaster: root
admin:      root
nobody:     /dev/null

# local account re-redirects
root:       root+s1@geddy.au
graham:~ sudo newaliases graham:~ sudo addgroup --gid 5000 vmail # group of virtual users' email graham:~ sudo adduser --uid 5000 --ingroup vmail --no-create-home --home /var/mail \ --gecos 'virtual mail users' --shell /usr/sbin/nologin vmail # proxy of virtual users' email graham:~ sudo usermod --expiredate 1 vmail graham:~ sudo adduser postfix vmail graham:~ sudo mkdir /var/mail/vhosts # container for per-domain email volumes graham:~ sudo chmod 2770 /var/mail/vhosts graham:~ sudo mkdir /var/mail/vhosts/geddy.au # container for this domain's email volume graham:~ sudo chown -R vmail:vmail /var/mail/vhosts graham:~ sudo mkdir /etc/postfix/vmailbox # container for per-domain mailboxes maps graham:~ sudo vi /etc/postfix/vmailbox/geddy.au # this domain's mailboxes map
/etc/postfix/vmailbox/geddy.au new file
# map virtual domain user to physical mailbox (geddy.au).
# note: always use trailing '/' on mailbox file to ensure maildir format

fred.nerk@geddy.au      geddy.au/fred.nerk/Maildir/
#OFF robot@geddy.au     geddy.au/robot/Maildir
graham:~ sudo postmap /etc/postfix/vmailbox/geddy.au graham:~ sudo mkdir /etc/postfix/virtual # container for per-domain forwarders maps graham:~ sudo vi /etc/postfix/virtual/geddy.au # this domain's forwarders map
/etc/postfix/virtual/geddy.au new file
# formulate delivery addresses for virtual domain (geddy.au)

# deal with domain root email if any
root@geddy.au           fred.nerk@geddy.au

# important domain mail roles
postmaster@geddy.au     root@geddy.au
webmaster@geddy.au      root@geddy.au
admin@geddy.au          root@geddy.au
abuse@geddy.au          root@geddy.au
nobody@geddy.au         nobody@localhost
robot@geddy.au          nobody@geddy.au

# domain pseudo-account redirects
no-reply@geddy.au       nobody@geddy.au

# domain distribution lists

# domain mail catch all - disabled to discourage spammer guesses
#OFF @geddy.au          root@geddy.au
graham:~ sudo postmap /etc/postfix/virtual/geddy.au graham:~ sudo vi /etc/postfix/vdomain # list of virtual domains supported
/etc/postfix/vdomain new file
# list of virtual domains recognised

geddy.au
graham:~ sudo vi /etc/postfix/master.cf
/etc/postfix/main.cf add highlighted line (previous line also shown)
smtp      inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
… … uncomment lines
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
… … uncomment lines
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
… … add highlighted line (previous line also shown)
#  -o smtpd_recipient_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
… … add highlighted lines (previous line also shown)
#  -o milter_macro_daemon_name=ORIGINATING
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
… … append lines to end of file
dovecot   unix  -       n       n       -       -       pipe
  flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient}
spamassassin unix -     n       n       -       -       pipe
  user=debian-spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}
graham:~ sudo certbot --standalone -d smtp.geddy.au # generate certificate graham:~ sudo ufw allow 'Postfix SMTPS' # poke holes in firewall graham:~ sudo ufw allow 'Postfix Submission'

We cannot start postfix until dependencies upon dovecot and spamassassin are satisfied.

dovecot

graham:~ sudo apt install dovecot-core dovecot-pop3d dovecot-impad dovecot-lmtpd dovecot-sieve graham:~ sudo systemctl stop dovecot # stop dovecot for updating

The configuration of dovecot is spread across many files under /etc/dovecot/conf.d/.

graham:~ cd /etc/dovecot/conf.d graham:/etc/dovecot/conf.d sudo vi 10-auth.conf
/etc/dovecot/conf.d/10-auth.conf change settings
disable_plaintext_auth = no
auth_mechanisms = plain login
#!include auth-system.conf.ext
!include auth-passwdfile.conf.ext
graham:/etc/dovecot/conf.d sudo vi 10-mail.conf
/etc/dovecot/conf.d/10-mail.conf change settings
mail_location = maildir:/var/mail/vhosts/%d/%n/Maildir
separator = /
#namespace virtual {
#    type = private
#    prefix = Virtual/
#    separator = /
#    location = maildir:/var/mail/vhosts/%d/%n/Maildir/virtual
#    list = children
#}

namespace {
  type = public
  separator = /
  prefix = Public/
  location = maildir:/var/mail/vhosts/%d/public
  subscriptions = yes
  list = children
}
graham:/etc/dovecot/conf.d sudo vi 10-master.conf
/etc/dovecot/conf.d/10-master.conf change settings
  #unix_listener auth-userdb {
    #mode = 0666
    #user = 
    #group = 
    extra_groups = vmail
  #}

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    user = postfix
    group = vmail
    mode = 0600
  }

  unix_listener auth-master {
    user = vmail
    group = vmail
    mode = 0600
  }
graham:/etc/dovecot/conf.d sudo vi 10-ssl.conf
/etc/dovecot/conf.d/10-ssl.conf change settings
ssl = yes
ssl_cert = /etc/letsencrypt/live/imap.geddy.au/fullchain.pem
ssl_key = /etc/letsencrypt/live/imap.geddy.au/privkey.pem
ssl_client_ca_dir = /etc/ssl/certs
graham:/etc/dovecot/conf.d sudo vi 15-lda.conf
/etc/dovecot/conf.d/15-lda.conf change settongs
protocol lda {
  # Space separated list of plugins to load (default is global mail_plugins).
  #mail_plugins = $mail_plugins

  auth_socket_path = /var/run/dovecot/auth-master
  hostname = imap.geddy.au
  mail_plugin_dir = /usr/lib/dovecot/modules
  mail_plugins = sieve
  postmaster_address = postmaster
}
graham:/etc/dovecot/conf.d sudo vi auth-passwdfile.conf.ext
/etc/dovecot/conf.d/auth-passwdfile.conf.ext change settings
passdb {
  driver = passwd-file
  args = /var/mail/vhosts/%d/shadow
}

userdb {
  driver = passwd-file
  args = /var/mail/vhosts/%d/passwd

  # Default fields that can be overridden by passwd-file
  #default_fields = quota_rule=*:storage=1G

  # Override fields from passwd-file
  #override_fields = home=/home/virtual/%u
}
graham:/etc/dovecot/conf.d cd graham:~ sudo adduser dovecot vmail graham:~ sudo certbot --standalone -d imap.geddy.au -d pop.geddy.au # generate certificate graham:~ sudo ufw allow 'Dovecot Secure IMAP' # poke holes in firewall graham:~ sudo ufw allow 'Dovecot Secure POP3' graham:~ sudo crontab -e # schedule 'taking out the trash'
temp filenew file or append to end
# purge Trash and Junk older than a month - monthly
# (-A option fails for sqlite backend, so workaround by iterating users)
#11  5  1   *   *   doveadm -v expunge -A mailbox Trash savedbefore 4w
 11  5  1   *   *   for user in $(cut -f1 -d: /var/mail/vhosts/*/passwd) ; do doveadm -Dv expunge -u "$user" mailbox Trash savedbefore 4w ; done
#12  5  1   *   *   doveadm -v expunge -A mailbox Spam savedbefore 4w
 12  5  1   *   *   for user in $(cut -f1 -d: /var/mail/vhosts/*/passwd) ; do doveadm -Dv expunge -u "$user" mailbox Spam savedbefore 4w ; done

spamassassin

graham:~ sudo apt install spamassassin graham:~ sudo systemctl stop spamassassin # stop spamassassin for updating graham:~ sudo vi /etc/default/spamassassin
/etc/default/spamassassin change setting
CRON=1
graham:~ sudo vi /etc/spamassassin/local.cf
/etc/spamassassin/local.cf change settings
rewrite_header      Subject *****SPAM _SCORE_*****
report_safe         0
trusted_networks    s0.geddy.au s1.geddy.au
#trusted_networks    142.250.0.0/15    # google
# lock_method         flock
# required_score      5.0
# use_bayes           1
# bayes_auto_learn    1
shortcircuit        ALL_TRUSTED on
… … append to end of file
# Turning on the skip_rbl_checks setting will disable the DNSEval plugin,
# which implements Real-time Block List (or: Blackhole List) (RBL) lookups.
# By default, SpamAssassin will run RBL checks. Individual blocklists may
# be disabled selectively by setting a score of a corresponding rule to 0.
#
# skip_rbl_checks 0

# Turning on the skip_uribl_checks setting will disable the URIDNSBL plugin.
# By default, SpamAssassin will run URI DNSBL checks. Individual URI
# blocklists may be disabled selectively by setting a score of a corresponding
# rule to 0 or through the uridnsbl_skip_domain parameter. Turning on the
# skip_uribl_checks setting will disable the URIDNSBL plugin.
#
# skip_urirbl_checks 0
graham:~ sudo vi /etc/spamassassin/65_debian.cf
/etc/spamassassin/65_debian.cf comment out last line
#score RCVD_IN_BRBL_LASTEXT 0
graham:~ host 2.0.0.127.b.barracudacentral.org 2.0.0.127.b.barracudacentral.org has address 127.0.0.2

Start them all

graham:~ sudo systemctl start postfix dovecot spamassassin graham:~ sudo systemctl status postfix dovecot spamassassin graham:~ sudo tail -f /var/log/mail.log graham:~ sudo postsuper -d ALL # purge outgoing mail queue graham:~ mailx -s test root < /dev/null graham:~ mailq graham:~ sudo tail -f /var/log/mail.log

Appendix: Client configuration

For our example virtual user:

user
fred.nerk@geddy.au
password
****(as assigned)****
incoming
imap.geddy.au:993 SSL/TLS
outgoing
smtp.geddy.au:465 SSL/TLS

Appendix: Script to add virtual user

graham:~ sudo vi /usr/local/sbin/add-virtual-user
/use/local/sbin/add-virtual-user new file
#!/bin/bash
# add-virtual-user: add user to postfix and dovecot virtual domain
# (c)2021 Graham Eddy ; provided under GPLv3 license
# run as superuser
#
# originated from script on https://tech.tiq.cc/2014/02/how-to-set-up-an-\
#vemail-server-with-postfix-and-dovecot-without-mysql-on-debian-7/

abort() {
    [ $# -gt 0 ] && echo "$@" 1>&2
    exit 2
}

usage() {
    echo "usage: $0 virtuser password [basedir]" 1>& 2
    abort $@
}

# check privileges
[ "$EUID" == 0 ] || abort "$0: not superuser"

# process command line args
case $# in
  2 | 3 )
    virtuser="$1"
    { echo "$virtuser" | grep '@' > /dev/null ; } || abort "$virtuser: no @ in virtuser"
    username=$(echo "$virtuser" | cut -f1 -d'@')
    domain=$(echo "$virtuser" | cut -f2 -d'@')
    [ "$username" ] || abort "$virtuser: missing username"
    [ "$domain" ] || abort "$virtuser: missing domain"
    passwd="$2"
    basedir="${3:-$(postconf | grep '^virtual_mailbox_base ' | cut -f3 -d' ')}"
    ;;
  * ) usage ;;
esac
grep "$domain" /etc/postfix/vdomain > /dev/null || abort "$domain: domain not defined"

########################################################################
# postfix

# container for domains
mkdir -p /etc/postfix/vmailbox || abort
chown root:root /etc/postfix/vmailbox || abort
chmod 755 /etc/postfix/vmailbox || abort

# this domain
vmailbox_file="/etc/postfix/vmailbox/$domain"
[ -f "$vmailbox_file" ] || touch "$vmailbox_file"
chown root:root "$vmailbox_file" || abort
chmod 644 "$vmailbox_file" || abort

# this user
if grep "^$virtuser " "$vmailbox_file" > /dev/null ; then
    tty -s && echo "$virtuser: virtuser already known to Postfix, skipped add"
else
    # force maildir format by using trailing '/'
    echo "$virtuser $domain/$username/Maildir/" >> "$vmailbox_file" || abort
    postmap "$vmailbox_file" || abort
fi

# no need to restart postfix

########################################################################
# dovecot

dovecot_changed=

# container for domains
mkdir -p "$basedir" || abort
chown vmail:vmail "$basedir" || abort
chmod 2750 "$basedir" || abort

# this domain
mkdir -p "$basedir/$domain" || abort
chown vmail:vmail "$basedir/$domain" || abort
chmod 2755 "$basedir/$domain" || abort

# user database for this domain
passwd_file="$basedir/$domain/passwd"
[ -f "$passwd_file" ] || touch "$passwd_file" || abort
chown root:dovecot "$passwd_file" || abort
chmod 644 "$passwd_file" || abort

# add this user
if grep "^$virtuser:" "$passwd_file" > /dev/null ; then
    tty -s && echo "$virtuser: virtuser already known to Dovecot/passwd, skipped add"
else
    echo "$virtuser::5000:5000::$basedir/$domain/$username" >> "$passwd_file" || abort
    dovecot_changed=yes
fi

# password database for this domain
shadow_file="$basedir/$domain/shadow"
[ -f "$shadow_file" ] || touch "$shadow_file" || abort
chown root:dovecot "$shadow_file" || abort
chmod 640 "$shadow_file" || abort

# add this user's password (uses `doveadm pw` to encrypt password)
if grep "^$virtuser:" "$shadow_file" > /dev/null ; then
    tty -s && echo "$virtuser: virtuser already known to Dovecot/shadow, skipped add"
else
    echo "$virtuser:$(doveadm pw -p $passwd)" >> "$shadow_file" || abort
    dovecot_changed=yes
fi

# restart dovecot if config changed
[ "$dovecot_changed" ] && systemctl reload dovecot
graham:~ sudo chmod 554 /usr/local/sbin/add-virtual-user