Virtual Users And Domains With Postfix, Courier, MySQL And SquirrelMail (Fedora 18 x86_64) - Page 5

13 Install Razor, Pyzor And DCC And Configure SpamAssassin

Razor, Pyzor and DCC are spamfilters that use a collaborative filtering network. To install Razor and Pyzor, run

yum install perl-Razor-Agent pyzor

Then initialize both services:

chmod -R a+rX /usr/share/doc/pyzor-0.5.0 /usr/bin/pyzor /usr/bin/pyzord
chmod -R a+rX /usr/lib/python2.7/site-packages/pyzor
su -m amavis -c 'pyzor --homedir /var/spool/amavisd discover'
su -m amavis -c 'razor-admin -home=/var/spool/amavisd -create'
su -m amavis -c 'razor-admin -home=/var/spool/amavisd -register'

Then we install DCC as follows:

cd /tmp
tar xzvf dcc-dccproc.tar.Z
cd dcc-dccproc-1.3.144
./configure --with-uid=amavis
make install
chown -R amavis:amavis /var/dcc
ln -s /var/dcc/libexec/dccifd /usr/local/bin/dccifd

Now we have to tell SpamAssassin to use these three programs. Edit /etc/mail/spamassassin/ so that it looks like this:

vi /etc/mail/spamassassin/

# These values can be overridden by editing ~/.spamassassin/
# (see spamassassin(1) for details)

# These should be safe assumptions and allow for simple visual sifting
# without risking lost emails.

#required_hits 5
#report_safe 0
#rewrite_header Subject [SPAM]

# dcc
use_dcc 1
dcc_path /usr/local/bin/dccproc

use_pyzor 1
pyzor_path /usr/bin/pyzor

use_razor2 1
razor_config /var/spool/amavisd/razor-agent.conf

use_bayes 1
use_bayes_rules 1
bayes_auto_learn 1

Then we must enable the DCC plugin in SpamAssassin. Open /etc/mail/spamassassin/v310.pre and uncomment the loadplugin Mail::SpamAssassin::Plugin::DCC line:

vi /etc/mail/spamassassin/v310.pre

# DCC - perform DCC message checks.
# DCC is disabled here because it is not open source.  See the DCC
# license for more details.
loadplugin Mail::SpamAssassin::Plugin::DCC

You can check your SpamAssassin configuration by executing:

spamassassin --lint

It shouldn't show any errors.


systemctl restart amavisd.service


Now we update our SpamAssassin rulesets as follows:

sa-update --no-gpg

We create a cron job so that the rulesets will be updated regularly. Run

crontab -e

to open the cron job editor. Create the following cron job:

23 4 */2 * * /usr/bin/sa-update --no-gpg &> /dev/null

This will update the rulesets every second day at 4.23h.


14 Quota Exceedance Notifications

If you want to get notifications about all the email accounts that are over quota, then create the file /usr/local/sbin/quota_notify:

cd /usr/local/sbin/
vi quota_notify

#!/usr/bin/perl -w

# Author <>
# This script assumes that virtual_mailbox_base in defined
# in postfix's file. This directory is assumed to contain
# directories which themselves contain your virtual user's maildirs.
# For example:
# -----------/
#            |
#            |
#    home/vmail/domains/
#        |          |
#        |          |
#                   |
#                   |
#           -----------------
#           |       |       |
#           |       |       |
#         user1/   user2/  user3/
#                           |
#                           |
#                        maildirsize

use strict;

my $POSTFIX_CF = "/etc/postfix/";
my $MAILPROG = "/usr/sbin/sendmail -t";
my @POSTMASTERS = ('postmaster@domain.tld');
my $CONAME = 'My Company';
my $COADDR = 'postmaster@domain.tld';
my $SUADDR = 'postmaster@domain.tld';
my $MAIL_REPORT = 1;

#get virtual mailbox base from postfix config
open(PCF, "< $POSTFIX_CF") or die $!;
my $mboxBase;
while (<PCF>) {
   next unless /virtual_mailbox_base\s*=\s*(.*)\s*/;
   $mboxBase = $1;

#assume one level of subdirectories for domain names
my @domains;
opendir(DIR, $mboxBase) or die $!;
while (defined(my $name = readdir(DIR))) {
   next if $name =~ /^\.\.?$/;        #skip '.' and '..'
   next unless (-d "$mboxBase/$name");
   push(@domains, $name);
#iterate through domains for username/maildirsize files
my @users;
foreach my $domain (@domains) {
        opendir(DIR, $domain) or die $!;
        while (defined(my $name = readdir(DIR))) {
           next if $name =~ /^\.\.?$/;        #skip '.' and '..'
           next unless (-d "$domain/$name");
      push(@users, {"$name\@$domain" => "$mboxBase/$domain/$name"});

#get user quotas and percent used
my (%lusers, $report);
foreach my $href (@users) {
   foreach my $user (keys %$href) {
      my $quotafile = "$href->{$user}/maildirsize";
      next unless (-f $quotafile);
      open(QF, "< $quotafile") or die $!;
      my ($firstln, $quota, $used);
      while (<QF>) {
         my $line = $_;
              if (! $firstln) {
                 $firstln = 1;
                 die "Error: corrupt quotafile $quotafile"
                    unless ($line =~ /^(\d+)S/);
                 $quota = $1;
            last if (! $quota);
         die "Error: corrupt quotafile $quotafile"
            unless ($line =~ /\s*(-?\d+)/);
         $used += $1;
      next if (! $used);
      my $percent = int($used / $quota * 100);
      $lusers{$user} = $percent unless not $percent;

#send a report to the postmasters
   open(MAIL, "| $MAILPROG");
   map {print "To: $_\n"} @POSTMASTERS;
   print "From: $COADDR\n";
   print "Subject: Daily Quota Report.\n";
   print "DAILY QUOTA REPORT:\n\n";
   print "----------------------------------------------\n";
   print "| % USAGE |            ACCOUNT NAME          |\n";
   print "----------------------------------------------\n";
   foreach my $luser ( sort { $lusers{$b} <=> $lusers{$a} } keys %lusers ) {
      printf("|   %3d   | %32s |\n", $lusers{$luser}, $luser);
      print "---------------------------------------------\n";
        print "\n--\n";
        print "$CONAME\n";

#email a warning to people over quota
        foreach my $luser (keys (%lusers)) {
           next unless $lusers{$luser} >= $WARNPERCENT;       # skip those under quota
           open(MAIL, "| $MAILPROG");
           print "To: $luser\n";
      map {print "BCC: $_\n"} @POSTMASTERS;
           print "From: $SUADDR\n";
           print "Subject: WARNING: Your mailbox is $lusers{$luser}% full.\n";
           print "Reply-to: $SUADDR\n";
           print "Your mailbox: $luser is $lusers{$luser}% full.\n\n";
           print "Once your e-mail box has exceeded your monthly storage quota\n";
      print "your monthly billing will be automatically adjusted.\n";
      print "Please consider deleting e-mail and emptying your trash folder to clear some space.\n\n";
           print "Contact <$SUADDR> for further assistance.\n\n";
           print "Thank You.\n\n";
           print "--\n";
           print "$CONAME\n";

Make sure that you adjust the variables at the top (especially the postmaster@domain.tld email address).

We must make the file executable:

chmod 755 quota_notify


crontab -e

to create a cron job for that script:

0 0 * * * /usr/local/sbin/quota_notify &> /dev/null


15 Test Postfix

To see if Postfix is ready for SMTP-AUTH and TLS, run

telnet localhost 25

After you have established the connection to your Postfix mail server type

ehlo localhost

If you see the lines




everything is fine.

[root@server1 sbin]# telnet localhost 25
Trying ::1...
telnet: connect to address ::1: Connection refused
Connected to localhost.
Escape character is '^]'.
220 ESMTP Postfix

<-- ehlo localhost
250-SIZE 10240000
250 DSN

<-- quit
221 2.0.0 Bye
Connection closed by foreign host.
[root@server1 sbin]#



to return to the system's shell.


16 Populate The Database And Test

To populate the database you can use the MySQL shell:

mysql -u root -p

USE mail;

At least you have to create entries in the tables domains and users:

INSERT INTO `domains` (`domain`) VALUES ('');
INSERT INTO `users` (`email`, `password`, `quota`) VALUES ('', ENCRYPT('secret'), 10485760);

(Please take care you use the ENCRYPT syntax in the second INSERT statement in order to encrypt the password!)

If you want to make entries in the other two tables, that would look like this:

INSERT INTO `forwardings` (`source`, `destination`) VALUES ('', '');
INSERT INTO `transport` (`domain`, `transport`) VALUES ('', '');

To leave the MySQL shell, type


For most people it is easier if they have a graphical front-end to MySQL; therefore you can also use phpMyAdmin (in this example under or to administrate the mail database. Again, when you create a user, go sure that you use the ENCRYPT function to encrypt the password:

I do not think I have to explain the domains and users table further.

The forwardings table can have entries like the following:

source destination Redirects emails for to Creates a Catch-All account for All emails to will arrive at, except those that exist in the users table (i.e., if exists in the users table, mails to will still arrive at @anotherdomain.tld This redirects all emails to to the same user at anotherdomain.tld. E.g., emails to will be forwarded to thomas@anotherdomain.tld., billing@anotherdomain.tld Forward emails for to two or more email addresses. All listed email addresses under destination receive a copy of the email.

The transport table can have entries like these:

domain transport : Delivers emails for locally. This is as if this record would not exist in this table at all. smtp:mail.anotherdomain.tld Delivers all emails for via smtp to the server smtp:mail.anotherdomain.tld:2025 Delivers all emails for via smtp to the server, but on port 2025, not 25 which is the default port for smtp.


The square brackets prevent Postfix from doing lookups of the MX DNS record for the address in square brackets. Makes sense for IP addresses. smtp:mail.anotherdomain.tld Mail for any subdomain of is delivered to mail.anotherdomain.tld.
* smtp:mail.anotherdomain.tld All emails are delivered to mail.anotherdomain.tld. smtp:mail.anotherdomain.tld Emails for are delivered to mail.anotherdomain.tld.


man transport

for more details.

Please keep in mind that the order of entries in the transport table is important! The entries will be followed from the top to the bottom.

Important: Postfix uses a caching mechanism for the transports, therefore it might take a while until you changes in the transport table take effect. If you want them to take effect immediately, run

postfix reload

after you have made your changes in the transport table.


17 Send A Welcome Email For Creating Maildir

When you create a new email account and try to fetch emails from it (with POP3/IMAP) you will probably get error messages saying that the Maildir doesn't exist. The Maildir is created automatically when the first email arrives for the new account. Therefore it's a good idea to send a welcome email to a new account.

First, we install the mailx package:

yum install mailx

To send a welcome email to, we do this:


You will be prompted for the subject. Type in the subject (e.g. Welcome), then press ENTER, and in the next line type your message. When the message is finished, press ENTER again so that you are in a new line, then press CTRL+D:

[root@server1 ~]# mailx
Subject: Welcome
Welcome! Have fun with your new mail account. <-- ENTER
<-- CTRL+D
[root@server1 ~]#

Share this page:

7 Comment(s)

Add comment


From: at: 2013-02-15 16:34:11

Need to install: yum install libtool-ltld-devel

Need to install: yum install expect

Need to install: yum install postgresql-devel

Need to install: yum install sqlite-devel

Need to install: yum install mysql-devel

From: at: 2013-02-15 16:50:56

yum install expect gdbm-devel pam-devel gamin-devel openssl-perl ghostscript mgetty-sendfax netpbm-progs pcre-devel libidn-devel


From: mbsouth at: 2013-02-05 20:03:27

Just another excellent tutorial by Falko.

In this case with a little criticism from me: Squirrel webmail and Courier have the best days already behind itself.
Really interesting would be a similar tutorial with Dovecot 1.2 or 2.0 (with Dovecots quota support [SQL based] and over-quota-warning mails to the user), SQL-Grey, Amavis (incl. DKIM support)  and Roundcube webmail.

Just my 2 cents

From: Ferry at: 2013-02-08 09:03:30

Disable SELinux? You've got to be kidding me!

Especially on a setup like this.

I'd to see the article updated with SELinux _enabled_

From: Cristian Sava at: 2013-09-23 19:40:24

Wonderfull tutorial, congrats Falko! It also applies to Fedora 19 (with minimal tweaks).

Selinux is easy to setup if you understand what youre doing.

Edit /etc/selinux/config and enable SELINUX:

nano /etc/selinux/config

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
#     enforcing - SELinux security policy is enforced.
#     permissive - SELinux prints warnings instead of enforcing.
#     disabled - No SELinux policy is loaded.
# SELINUXTYPE= can take one of these two values:
#     targeted - Targeted processes are protected,
#     minimum - Modification of targeted policy. Only selected processes are protected.
#     mls - Multi Level Security protection.

Afterwards we must reboot the system.

Then we set some needed booleans:

[root@s168 ~]# setsebool -P httpd_can_network_connect on
[root@s168 ~]# setsebool -P httpd_can_network_connect_db on
[root@s168 ~]# setsebool -P httpd_can_sendmail on
[root@s168 ~]# setsebool -P httpd_enable_homedirs on
[root@s168 ~]# setsebool -P httpd_mod_auth_pam on
[root@s168 ~]# setsebool -P httpd_use_gpg on
[root@s168 ~]# setsebool -P httpd_sys_script_anon_write on
[root@s168 ~]# setsebool -P selinuxuser_mysql_connect_enabled on
[root@s168 ~]# setsebool -P saslauthd_read_shadow on
[root@s168 ~]# setsebool -P clamd_use_jit on

We set clamd_use_jit to on to avoid messages like this:
ERROR: During database load : LibClamAV Warning: RWX mapping denied: Can't allocate RWX Memory: Permission denied


You can see what values are set for booleans using something like this:

[root@s168 ~]# getsebool -a | grep http | grep " on"
httpd_builtin_scripting --> on
httpd_can_network_connect --> on
httpd_can_network_connect_db --> on
httpd_can_sendmail --> on
httpd_enable_cgi --> on
httpd_enable_homedirs --> on
httpd_graceful_shutdown --> on
httpd_mod_auth_pam --> on
httpd_sys_script_anon_write --> on
httpd_use_gpg --> on

To analyze and to build the custom modules we will need audit2allow so we have to install the policycoreutils-devel package:

# yum install policycoreutils-devel

The virtual mail directory is choosen by the administrator (folder set by us) so we can't have a default rule for that. That's why we have to build a policy module to get access to our virtual directory.
Another thing is that the antivirus, generally, should not access the database, but in /etc/amavisd/amavisd.conf we have this rule:
@lookup_sql_dsn = ( ['DBI:mysql:database=maildb;host=;port=3306', 'mail_admin', 'mail_admin_password'] );
It is something legitimate but not expected for an antivirus.
We will investigate the logs first (find out if we have related AVCs) and we will react accordingly:

[root@s168 ~]# audit2allow -a
#============= postfix_virtual_t ==============
allow postfix_virtual_t home_root_t:dir { write remove_name create add_name };
allow postfix_virtual_t home_root_t:file { write getattr link create unlink open };
#============= antivirus_t ==============
allow antivirus_t mysqld_port_t:tcp_socket name_connect;

Now we build the needed policy modules:

[root@s168 ~]# grep virt /var/log/audit/audit.log | audit2allow -M myvirtual
[root@s168 ~]# semodule -i myvirtual.pp

[root@s168 ~]# grep antivirus /var/log/audit/audit.log | audit2allow -M myantivir_mysql
[root@s168 ~]# semodule -i myantivir_mysql.pp

We verify our policy modules:

[root@s168 ~]# audit2allow -a
#============= postfix_virtual_t ==============

#!!!! This avc is allowed in the current policy
allow postfix_virtual_t home_root_t:dir { write remove_name create add_name };

#!!!! This avc is allowed in the current policy
allow postfix_virtual_t home_root_t:file { write getattr link create unlink open };
#============= antivirus_t ==============

#!!!! This avc is allowed in the current policy
allow antivirus_t mysqld_port_t:tcp_socket name_connect;

That's all.


From: Cristian Sava at: 2013-09-25 06:09:34

Congrats Falko!

Very good and complete tutorial, rock solid server resulting. Excellent for small/medium business. Easy to install and maintain.

Keep posting tutorials like this for actual and future Fedora releases (F19, F20, ...).

 Great work!

 C. Sava


From: fly at: 2015-01-02 07:19:53


This is a very good tutorial to show us the methods.

And i do it step by step. I have one question about Creat the mail folder. Everything is Ok but it can not generate the User Mail Folder at ../Vmail/  automatically. Mysql Is Ok, and i want to konw whether you  had met such a problem.

Best wishes