User Tools

Site Tools


Email server using postfix and dovecot

For a small-scale mail server, it is best to use OS users for authentication. In this setup, users have a home directory that stores the mail directory (~/mail/).

Address Mapping

To map email addresses to OS users, postfix uses the file /etc/postfix/virtual, which is specified in, property virtual_alias_maps. It contains mappings like the following: target_user1, target_user2,

The domain of the incoming address must be one of the domains listed in property virtual_alias_domains. User aliases can be defined in /etc/aliases.

After editing the file, run the following commands:

# postmap /etc/postfix/virtual
# postfix reload


Dovecot allows to process incoming mails with sieve scripts. This is very useful to file messages into folders with skipping the inbox. By default, there is a global sieve script that files all spam mails into the trash folder. It has the following content:

require ["fileinto"];
# Move spam to spam folder
if header :contains "X-Spam-Flag" ["YES"] {
  fileinto "spam";

To add custom sieve scripts, I recommend installing the Thunderbird Sieve extension. It allows the creation and syntax checking of sieve scripts directly on the server. On Debian, it is available as xul-ext-sieve package.

Important: sieve scripts will only work when formatted with DOS-type CRLF linebreaks. The above extension takes care of that.

If you set up a new user, do not forget that dovecot looks for a .dovecot.sieve script in that user's home directory (which may be also a symlink to an include script in a sieve folder).

For specific information about managing sieve on look at

Mail aggregation aka unified inbox

Objective: collect email messages from several legacy freemail inboxes, pass them through a reliable virus and spam filter, and dispatch them to the IMAP inbox on the server.

Benefits: reliable and controllable spam filtering, POP3 inboxes made accessible via IMAP

Drawback: on clients that can't be configured otherwise, answering mails received that way will result in the answers carrying your IMAP inbox address as sender.

User Agents

Icedove/Thunderbird and Claws Mail work without problems with the setup described. Both clients support the creation of folders and will also check for mail delivered into subfolders by the sieve scripts.

Take care when creating new folders: If you don't create them as subfolders to the mailbox root, but instead as subfolders to INBOX, it may happen that you end up with an INBOX folder on the server, containing nothing but the aforementioned subfolder. This is because in this here setup, the INBOX is no folder, but only exists virtually. So: never create subfolders to INBOX, and you won't run into difficulties (like clients not even seeing these folders).

When it comes to mobile user agents, the picture changes: The stock email app for Android Lollipop, called 'GMail', does perform rather poorly with this setup. Subfolders show up in the folder tree of the account and after they've been opened as recent labels (what seems to be Googlish for folders), too. But to see mail filed into these folders, you have to open them, they don't seem to get refreshed otherwise. Also, the overall layout of the app is very untidy and it is simply hard to use.

There are a lot of mail user agents or clients for Android, but most of them are either still a whole lot more ugly than GMail, or do not offer more functionality, or are infested with ads, or have to be paid before testing, or a combination of the above. In the jolly old times of WebOS, we had one email app which did its job greatly; now we have hundreds and all do their job poorly, or so it seems.

By 'doing its job' I mean: the email client should at least take note of mails appearing in subfolders, and possibly offer a unified inbox, a virtual folder where all unread mail is presented in a bundle. It should not pester you with advertisements and, if to be paid, at least be free to try out.

After testing dozens of email clients, I can conclude that I found only one that matched these criteria, and even to some extent surpassed them. It is called AquamailPlay Store Link, and it offers all the quoted features (you can even control which folders to use for the virtual inbox and which folders not to scan for new mail). The paid version even seems to allow to send mail from different sender addresses, which would perfectly fit our setup, but which remains hitherto untested. The app offers a clean design, tons of options (too many, indeed, for the menus, so that another layer of menus had to be hidden behind long presses on, e.g., folders, which I had some trouble to deduce at first), a nice widget which is a) resizable and b) updates after you read/deleted a mail from the folder it shows (both being features Google seems to be unable to accomplish), and it does not use ads to get on your nerve, but decorates outgoing mails with a promotional signature (promised to disappear in the unlocked version).

So far, testing Aquamail has shown very positive results. I'll keep you updated.


Now that the system wide setup is working, it is simple to add a service that is polling an external inbox into your user's IMAP inbox here. Just create a file called .fetchmailrc in your user directory and adjust the permissions so that fetchmail is going to accept them.

  $ touch ~/.fetchmailrc
  $ chmod 0600 /etc/fetchmailrc

Then, edit this file to match your credentials for the inbox you want to poll:

#log to system log - enable after verifying your setup
#set syslog
poll protocol pop3:
 username "XXXXXX" password "..........", is someuser here smtphost localhost/2345
  # use secure connection relying on CA certificates
  # do not delete from server (for testing)
  # get all messages, not just the ones that arrived after the last poll (use this after commenting out keep)

Now, run fetchmail verbosely:

$ fetchmail -v

and check the output. If all is well, change the .fetchmailrc to delete messages from the remote inbox and log to system log, and add it to your crontab, so that it will be executed regularly by the system.

$ crontab -e

Add a line like this to your crontab:

*/5 * * * * /usr/bin/fetchmail &> /dev/null

(this will poll every 5 minutes). Then, save and exit the editor. Now, the fetchmail job will run unattended as set in the crontab.

Once this works, one could filter mail using the sieve method described above.

For the administrator

To fine-tune the setup, there are some things to be taken into account:

  • spam scores: In /etc/amavis/50-user, we define numerical spam score levels. The sa_tag2_level_deflt variable lets the spam filter decide if a mail is to be considered SPAMMY. This variable is set to 4.51 on our server at the time of writing, which may have to be adjusted after a test period.
  • spam from the inboxes polled by fetchmail will be silently deleted when the score of sa_kill_level_deflt (now 20) is reached. You might want to review the logs for some time to look for false-positives being discarded.
  • on the server, the rewriting of the subject lines of messages considered spam has been disabled. This makes it easier to deal with false positives. Just move them out of the spam folder.
  • learning: do not delete true spam messages. After collecting lots of them, they should be used to train the spam filter.
  • Ports: now, all clients submitting messages on port 465 (smtps) are considered local. Check if this is a valid assumption. If not, modify the system to accept these submissions on port 587 (submission). See below.

Setup details

The setup described does now fulfill the expectations. Amavisd does now scan and decorate mails from fetchmail.

After lots of trials which didn't work, I decided to move away from the production server. On my trusted Sparcstation, I set up a testbed system, and seemingly, this works.

But, as it turned out, there were some differences to the production system that had to be taken into acoount. See below.


First, you've got to configure postfix. To the end of /etc/postfix/, add (but only if the stanza is not there already, in our conguration, it was!):

mailbox_command = /usr/lib/dovecot/deliver

Also, we need to add or change the amavisfeed identifier/content filter to

content_filter = amavisfeed:[]:10040

All other things in stayed unchanged.

To the end of /etc/postfix/, I added

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================

amavisfeed unix    -       -       n       -       2     smtp
     -o smtp_data_done_timeout=1200
     -o smtp_send_xforward_command=yes
     -o disable_dns_lookups=yes
     -o max_use=20
# this sets the name and default options for the amavis smtp feed

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ========================================================================== inet n    -       n       -       -     smtpd
     -o content_filter=
     -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 smtpd_restriction_classes=
     -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
     -o local_header_rewrite_clients=

# this establishes the smtp listener to take back processed messages from amavis

# ==========================================================================
# content filters to match the policies of amavis
# ==========================================================================
# regular incoming mail, originating from anywhere (usually from outside)
# the MX record (or backup mailers) should point to this IP address
# set to your external IP address
# this does not work when you want to run this service chrooted
# inet  n  -  n  -  -  smtpd
#  -o content_filter=amavisfeed:[]:10040

# ==========================================================================
# incoming mail from fetchmail, considered externally originating
# (add 'smtphost localhost/2345' to the poll section in .fetchmailrc) inet  n  -  n  -  -  smtpd
  -o content_filter=amavisfeed:[]:10041
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o mynetworks=

# ==========================================================================
# IP address to be used by internal hosts for mail submission inet  n  -  n  -  -  smtpd
  -o content_filter=amavisfeed:[]:10042
  -o mynetworks=
  -o smtpd_client_restrictions=permit_mynetworks,reject

# ==========================================================================
# locally originating mail submitted on this host through a sendmail binary
pickup     fifo  n  -  n  60  1  pickup
  -o content_filter=amavisfeed:[]:10042
# ==========================================================================

Important: There may not be whitespace before the first lines of the rules in, otherwise they'll be considered a continuation of the last rule starting at the first character, even if they don't make sense, without error message.

Also important: further up in master,cf, look for this entry:

smtp      inet  n       -       -       -       -       smtpd

and change it to:

# regular incoming mail, originating from anywhere (usually from outside)
# this works chrooted if you've set a content filter in
# like so:  content_filter = amavisfeed:[]:10040
# (this name must match the one given above).
# the MX record (or backup mailers) should point to this IP address
# set to your external IP address
EXTERNAL.IP.OF.MX:smtp      inet  n       -       -       -       -       smtpd

Of course, you'll have to enter the external IP address of your mail server (MX record) here. This will run the smtp service that can be reached from outside in a chroot environment.

For roaming users reaching the server via smtps, we add a content filter to map them to our local port for amavisd:

smtps     inet  n       -       -       -       -       smtpd
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
  -o content_filter=amavisfeed:[]:10042

(just the last line was added!).

Still problematic

But this assumes that only roaming clients will connect to the smtps port, which possibly is false.

So, I think it would actually be preferable to separate the roaming user service from the other smtp(s) services. That's where the port used for mail submission comes in (port 587), it is in postfix' We'll have to take care that the chroot works for this too, and move the content flter for port 10042 to this service in And we'll need to open it in the firewall. Then, we'll have to change the port for the roaming smtp clients accordingly, so that the new submission path will be used.

Finally, look for a line like this and comment it out as shown.

#pickup    fifo  n       -       -       60      1       pickup

If you get an error like: postfix cannot start ('port already in use'), you should stop postfix and restart it, because simply reloading will not cause it to release ports and addresses.

So, what does this configuration achieve? We set up postfix to take back mails (from amavis) on port 10025, and we prepare postfix to

  • listen on the external IP address as smtp server and hand over mail from there to amavis at port 10040
  • listen on the smtps port and hand over email messages received there to amavis at port 10042
  • listen on the local loopback email address at port 2345 as smtp server and hand over mail from there to amavis at port 10041
  • listen on the local loopback email address as smtp server and hand over mail from there to amavis at port 10042

Backward compatibility: before, postfix was setup to listen to the smtp port on all addresses (in the smtp service line we commented out, see above). Now, to differentiate, we set up listeners on the loopback address and on the external address. So, in this setup, there's no universal local smtp listener (for all local IP addresses). If you need such listeners, you'll have to add a rule for each of them to the postfix configuration, because the preceding rules don't allow you to adress the smtp port globally any more in a rule.


Of course, this setup has to be matched by a corresponding configuration for amavisd-new. Apart from uncommenting the spam filter lines in /etc/amavis/15-content-filter-mode all other changes could be made to /etc/amavis/conf.d/50-user. I added:

# be more verbose
log_level = 2;

# ports we listen on
$inet_socket_port = [10040,10041,10042];

# policies for these ports
# cf. /etc/postfix/ for the corresponding setup

$interface_policy{'10040'} = 'EXT';
$interface_policy{'10041'} = 'EXT-FM';
$interface_policy{'10042'} = 'INT-HOST';

# some global settings
# local domains
@local_domains_maps = ( [".$mydomain", "", ".localhost"] );

# do not quarantine spam
$virus_quarantine_to = $QUARANTINEDIR;
$spam_quarantine_to = undef;
# Spam levels
 # always tag
$sa_tag_level_deflt  = undef;
$sa_tag2_level_deflt = 5;
$sa_kill_level_deflt = 20;

### POLICIES ###
# regular incoming mail, originating from anywhere (usually from outside)
$policy_bank{'EXT'} = {

# incoming mail from fetchmail, considered externally originating
$policy_bank{'EXT-FM'} = {
   log_level => 2, 
   # if '.localhost' is in @local_domians_maps (above), this should work without
   # explicitely setting this flag. If set, this means: we declare that all
   # mail passed through this policy is local-destined and has to be scanned
   originating => 1,
    # no bounces for spam, not even for score below spam_dsn_cutoff_level_maps:
  final_spam_destiny => D_DISCARD,

# mail locally submitted on the host on which MTA runs
# or submitted via the smtps port by roaming users
$policy_bank{'INT-HOST'} = {
  originating => 1,
    # NOTE: this is just an example; ignoring internally generated spam
    # may not be such a good idea, consider zombified infected local PCs
  bypass_spam_checks_maps   => [ 1 ],
  bypass_banned_checks_maps => [ 1 ],
  final_spam_destiny   => D_PASS,
  final_banned_destiny => D_PASS,


So far, so good. I've only got one question: how does amavisd detect which mail messages are to be considered local/inbound? By exploiting the xforward option of postfix? The correct setting of local_domains_maps is also important:

“If recipient matches @local_domains_maps it goes to an inside recipient (inbound or all-internal), otherwise mail is outbound.” [from the amavisd mailing list]

If the automatic decision if a mail is considered local does not work as wanted, we'll have to investigate the originating flag of amavisd-new. If set to 1, this means that mail is considered to be outbound (originating from the server), and should not be processed. I added this flag to two of the policy banks laid out above.

As last step, you'll have to add the port for the fetchmail rule above to the user's .fetchmailrc:

poll protocol pop3:
 username "XXXXXX" password "..........", is someuser here smtphost localhost/2345
  # use secure connection relying on CA certificates
  # do not delete from server (for testing)
  # get all messages, not just the ones that arrived after the last poll (use this after commenting out keep)

Note: if you've got an active firewall and experience timeouts running fetchmail, you'll have to open the outgoing port for pop3s (995).

Result: this will process mails from fetchmail through the EXT-FM policy and these mails will be decorated with spam markings.


To poll mail from external POP3 accounts and make it accessible by IMAP here, the simple method is to use fetchmail to check out the messages from the server and then directly hand them to dovecot's deliver. This means we'll not use an MTA (Mail Transfer Agent) like postfix (which basically means sending an email between the different programs), but just use an MDA (Mail Delivery Agent), basically copying or piping the message from one program to the other.

So, we'll use fetchmail in conjunction with dovecot's deliver as MDA.

Because running fetchmail system-wide (as user fetchmail/group nobody) creates lots of seemingly unsolvable permission problems when attempting to use the MDA to deliver to user's inboxes, we instead choose to run everything for the individual user. (It would be possible to run the MDA as root user, either setuid or via sudo, but this is a dangerous kludge, and unnecessary, as the method described here works.)

So, after installing fetchmail, we have to create a user-specific config and adjust the permissions so that fetchmail will find them acceptable. In the user's directory, logged in as that user, execute

$ touch ~/.fetchmailrc
$ chmod 0600 /etc/fetchmailrc

Fetchmail is ssl-enabled and can either use one of the root CA certificates or check if the remote ssl fingerprint matches the one stored locally (for self-signed certificates). For the usual freemail providers, the first method should work.

Fetchmail can be set up to use dovecot's deliver as local delivery agent (aka lda) to deliver the retrieved mail to the local mail directories. Compared to other solutions like the popular procmail, this is the preferred solution as it will update dovecot's folder indices and allow filtering via the sieve plugin (as mentioned above). Using procmail will not update indices and only allow filtering via procmail's own mechanism.

So, we edit our ~/.fetchmailrc like this:

set syslog

poll protocol pop3:
 username "XXXXXXX" with password "........." is someuser here
 # use secure connection relying on CA certificates
 # do not delete from server (for testing)
 # get all messages, not just the ones that arrived after the last poll (use this after commenting out keep)
 # dovecot lda (Debian location)
 mda "/usr/lib/dovecot/deliver"

Now, start fetchmail:

$ fetchmail -v

You can look (with the correct permissions) at /var/log/mail.log[err,&c] for messages about errors or success or failure.

If you don't see error messages here, set up a cron job to have fetchmail poll the POP3 server(s) regularly:

$ crontab -e

Add a line like this to your crontab:

*/5 * * * * /usr/bin/fetchmail &> /dev/null

(this will poll every 5 minutes). Then, save and exit the editor. Now, the fetchmail job will run unattended as set in the crontab.

Once this works, one could filter mail using the sieve method described above.

Spam checker

While this method is very simple and easy to set up, integrating spam and antivirus filters takes some more work.

If you only want spam filtering, this is quite easy. After installing and configuring spamassassin (on which I won't elaborate), change the mda line in your .fetchmailrc like

 mda "spamc -e /usr/lib/dovecot/deliver"

This will filter your mail through spamassassin, which will 'decorate' it with headers showing how spammy spamassassin thinks the mail message is. The filtering will then be handled by sieve, as shown above.

While this works on my test machine, it fails on the production server, because there, users do not have permission to access spamc/spamd. As I wanted to move on to the setup described above, I didn't innvestigate how to change this.

Additional AV filter

Regarding antivirus filtering, the picture changes, though.

Apparently, amavisd-new, which is installed here, does not offer to pipe mail messages through its scanning system. So the method employed till now has to be abandoned. It's no longer possible to just use an MDA, we'll have to switch to our MTA (postfix) to process to mail messages.

This turns out to be less complicated than it sounds. (Of course, I assume that the basic mail processing system already works correctly and is set uo to send and receive mail, scan received mail for viruses and spam, and deliver to dovecot's inboxes.)

The setup would then look like this: fetchmail → postfix → amavisd & spamassassin → lda → dovecot.

How could this be implemented?

  • omitting the mda line from .fetchmailrc leads to mail messages being processed by the local mail system (i.e. postfix)
  • postfix is already configured to pass mails through amavisd
  • Amavisd calls spamassassin

While this basically works, it turned out that amavisd, when configured to ignore local mails, also ignores all mails polled by fetchmail. At first, it seemed that there was no sane way to tell amavisd to behave differently. After digging through lots of stupid blog posts of the “I'm so cool” type, I finally discovered the way how this might be achieved. The key is to have several ports listening for mail and define matching policies for amavisd. See at the beginning of this section.


Q: Which variant should I choose?

A: For a LAN aggregator, I'd choose the first variant, maybe enhanced by adding spamassassin as shown. That way, there's no need to run postfix. You could then scan for viruses in your MUA (email client).

For a server with a public IP, the last variant is the right one. That way, you'll get a unified inbox for all accounts polled by fetchmail, with mails scanned for viruses and spam, reachable from wherever you can freely access the internet.

Q: Don't I have to use procmail? This can be read all over the Internet?

A: While one could use procmail, this has lots of disadvantages in conjunction with dovecot. Procmail does not update the folder indices when delivering mail, and there's no way to run sieve scripts on the incoming mail. So, use deliver (dovecot's lda) instead, which does all this.

Q: Do you have to change dovecot's configuration to make this work?

A: On a fresh install, the lda section should be uncommented, and maybe you'll need to set the postmaster_address (IIRC). On our setup, this had been done already.

Q: How to test this without wreaking havoc? Make a backup copy of ~/mail/ and move it back in case things break?

A: Use your old Sparcstation ;-) (but the other method might work, too).

info/mail.txt · Last modified: 2015/02/01 21:08 by hartmut