Postfix mail server howto
Here we’ll walk through how to build a mail server that has a webmail client, bunches of authentication through a database backend (making it easy to add/delete/manage users) and a way to add malware/spam scanners if you want. There are lots of ways to do this, but this is down-and-dirty cut/paste howto. Chunks of it were taken from over here in case you want another reference or to support them (they’re good folks).
Understanding a “mail server” is non-trivial, because there are lots of little parts that allow mail to get to you, not just the “mail server”, which is really called a Mail Transfer Agent (MTA), which is what Postfix is. Still, without Postfix nothing would be trying to deliver anything, either locally or remotely. We also enable mail quotas, in case you need them.
Also, you have to have an idea of how DNS works, otherwise emails addressed to you@yourdomain.com would not know that you really get email on a server with an IP of 1.2.3.4. Oh, and this is normally a public, static IP (we just use the example 1.2.3.4, you should in this howto change it to what yours really is, it’s impossible that it will be 1.2.3.4) so you have to have a public static IP that doesn’t change often (or a way to tell dynamic dns that your home cable modem just changed to a different IP, which is sort of beyond the scope of this howto).
We’re using Debian Wheezy, though there are lots of successful mail servers which use other Linux/BSD variants. If you want to use them, you’ll have to adapt this to your target platform. First thing you do is login and get root. Okay, you could use sudo, and if you want just add sudo on the front of the commands.
su dpkg-reconfigure dash Use dash as the default system shell (/bin/sh)? <-- No <pre lang="bash"> apt-get install postfix postfix-mysql postfix-doc mysql-client mysql-server courier-authdaemon courier-authlib-mysql courier-pop courier-pop-ssl courier-imap courier-imap-ssl libsasl2-2 libsasl2-modules libsasl2-modules-sql sasl2-bin libpam-mysql openssl phpmyadmin apache2 libapache2-mod-php5 php5 php5-mysql libpam-smbpass |
Now it will ask you questions:
General type of mail configuration: <-- Internet Site System mail name: <-- mail.yourdomain.com New password for the MySQL "root" user: <-- yourrootsqlpassword Repeat password for the MySQL "root" user: <-- yourrootsqlpassword Create directories for web-based administration? <-- No SSL certificate required <-- Ok Web server to reconfigure automatically: <-- apache2 Configure database for phpmyadmin with dbconfig-common? <-- No |
Now we rebuild postfix with a quota patch NOTE: IF YOU USE JESSIE OR POSTFIX > 2.10 THE PATCH ISN’T AVAILABLE, so skip this step:
apt-get build-dep postfix cd /usr/src apt-get source postfix postconf -d | grep mail_version <-- mine said 2.9.6, use your number in the next commands for mail version wget http://vda.sourceforge.net/VDA/postfix-vda-v11-2.9.6.patch cd postfix-2.9.6 patch -p1 < ../postfix-vda-v11-2.9.6.patch vi debian/rules export DEB_BUILD_HARDENING=1 <-- change to 0 dpkg-buildpackage dpkg -i postfix_2.9.6-2_amd64.deb postfix-mysql_2.9.6-2_amd64.deb <-- change version to your real numbers |
now create the databases and configure them. We create a table for domains (if you want to use this box for multiple domains) and a table for users, which would be where you’d add me@mydomain.com. Change the “password” fields to what password you really want:
mysqladmin -u root -p create mail mysql -u root -p GRANT SELECT, INSERT, UPDATE, DELETE ON mail.* TO 'mail_admin'@'localhost' IDENTIFIED BY 'mail_admin_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON mail.* TO 'mail_admin'@'localhost.localdomain' IDENTIFIED BY 'mail_admin_password'; FLUSH PRIVILEGES; USE mail; CREATE TABLE domains ( domain varchar(50) NOT NULL, PRIMARY KEY (domain) ) ENGINE=MyISAM; CREATE TABLE forwardings ( source varchar(80) NOT NULL, destination TEXT NOT NULL, PRIMARY KEY (source) ) ENGINE=MyISAM; CREATE TABLE users ( email varchar(80) NOT NULL, password varchar(20) NOT NULL, quota INT(10) DEFAULT '10485760', PRIMARY KEY (email) ) ENGINE=MyISAM; CREATE TABLE transport ( domain varchar(128) NOT NULL default '', transport varchar(128) NOT NULL default '', UNIQUE KEY domain (domain) ) ENGINE=MyISAM; quit; |
Now we have to add the authentication config/mapping files, so create these new files with the contents shown:
vi /etc/postfix/mysql-virtual_domains.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT domain AS virtual FROM domains WHERE domain='%s' hosts = 127.0.0.1 vi /etc/postfix/mysql-virtual_forwardings.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT destination FROM forwardings WHERE source='%s' hosts = 127.0.0.1 vi /etc/postfix/mysql-virtual_mailboxes.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') FROM users WHERE email='%s' hosts = 127.0.0.1 vi /etc/postfix/mysql-virtual_email2email.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT email FROM users WHERE email='%s' hosts = 127.0.0.1 vi /etc/postfix/mysql-virtual_transports.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT transport FROM transport WHERE domain='%s' hosts = 127.0.0.1 vi /etc/postfix/mysql-virtual_mailbox_limit_maps.cf user = mail_admin password = mail_admin_password dbname = mail query = SELECT quota FROM users WHERE email='%s' hosts = 127.0.0.1 chmod o= /etc/postfix/mysql-virtual_*.cf chgrp postfix /etc/postfix/mysql-virtual_*.cf groupadd -g 5000 vmail useradd -g vmail -u 5000 vmail -d /home/vmail -m |
Now you have to modify some of Postfix’s settings:
postconf -e 'myhostname = mail.example.com' <-- change the domain to yours vi /etc/postfix/main.cf <-- change stuff to look like (add to end of file) virtual_alias_domains = virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual_forwardings.cf, mysql:/etc/postfix/mysql-virtual_email2email.cf virtual_mailbox_domains = proxy:mysql:/etc/postfix/mysql-virtual_domains.cf virtual_mailbox_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailboxes.cf virtual_mailbox_base = /home/vmail virtual_uid_maps = static:5000 virtual_gid_maps = static:5000 smtpd_sasl_auth_enable = yes broken_sasl_auth_clients = yes smtpd_sasl_authenticated_header = yes smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination smtpd_use_tls = yes smtpd_tls_cert_file = /etc/postfix/smtpd.cert smtpd_tls_key_file = /etc/postfix/smtpd.key transport_maps = proxy:mysql:/etc/postfix/mysql-virtual_transports.cf virtual_maildir_extended = yes virtual_mailbox_limit_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailbox_limit_maps.cf virtual_mailbox_limit_override = yes virtual_maildir_limit_message = "The user you are trying to reach is over quota." virtual_overquota_bounce = yes proxy_read_maps = $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $virtual_mailbox_limit_maps |
Now make a SSL cert. When it prompts you, add your details like city, state, etc.
openssl req -new -outform PEM -out smtpd.cert -newkey rsa:2048 -nodes -keyout smtpd.key -keyform PEM -days 365 -x509 chmod o= /etc/postfix/smtpd.key |
Now configure authentication:
mkdir -p /var/spool/postfix/var/run/saslauthd vi /etc/default/saslauthd START=yes <-- change to yes, default=no OPTIONS="-c -m /var/spool/postfix/var/run/saslauthd -r" <-- comment the line above it, and then add this one vi /etc/pam.d/smtp <-- add the next lines: auth required pam_mysql.so user=mail_admin passwd=mail_admin_password host=127.0.0.1 db=mail table=users usercolumn=email passwdcolumn=password crypt=1 account sufficient pam_mysql.so user=mail_admin passwd=mail_admin_password host=127.0.0.1 db=mail table=users usercolumn=email passwdcolumn=password crypt=1 vi /etc/postfix/sasl/smtpd.conf <-- create this file and put this stuff in it: pwcheck_method: saslauthd mech_list: plain login allow_plaintext: true auxprop_plugin: sql sql_engine: mysql sql_hostnames: 127.0.0.1 sql_user: mail_admin sql_passwd: mail_admin_password sql_database: mail sql_select: select password from users where email = '%u@%r' adduser postfix sasl /etc/init.d/postfix restart /etc/init.d/saslauthd restart |
Now we get Courier (mail delivery thing) to authenticate:
vi /etc/courier/authdaemonrc authmodulelist="authmysql" cp /etc/courier/authmysqlrc /etc/courier/authmysqlrc_orig cat /dev/null > /etc/courier/authmysqlrc vi /etc/courier/authmysqlrc MYSQL_SERVER localhost MYSQL_USERNAME mail_admin MYSQL_PASSWORD mail_admin_password MYSQL_PORT 0 MYSQL_DATABASE mail MYSQL_USER_TABLE users MYSQL_CRYPT_PWFIELD password #MYSQL_CLEAR_PWFIELD password MYSQL_UID_FIELD 5000 MYSQL_GID_FIELD 5000 MYSQL_LOGIN_FIELD email MYSQL_HOME_FIELD "/home/vmail" MYSQL_MAILDIR_FIELD CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') #MYSQL_NAME_FIELD MYSQL_QUOTA_FIELD quota cd /etc/courier rm -f /etc/courier/imapd.pem rm -f /etc/courier/pop3d.pem vi /etc/courier/imapd.cnf CN=mail.example.com <-- change to your real domain vi /etc/courier/pop3d.cnf CN=mail.example.com <-- change to your real domain mkimapdcert mkpop3dcert /etc/init.d/courier-authdaemon restart /etc/init.d/courier-imap restart /etc/init.d/courier-imap-ssl restart /etc/init.d/courier-pop restart /etc/init.d/courier-pop-ssl restart |
Now see if your courier is working by doing:
telnet localhost pop3 Trying ::1... Connected to localhost. Escape character is '^]'. +OK Hello there. quit <-- type this +OK Better luck next time. Connection closed by foreign host. |
Now set up your notification emails, change the email to whatever email account you want to get mail server health notifications on:
vi /etc/aliases root: you@yourdomain.com newaliases /etc/init.d/postfix restart |
Now install your scanners and spam filters. Change these to reflect whatever scanners you happen to have/want. Also, you can add scanners later and just change the Amavisd-new files to point to them, or remove old scanners you don’t want to use anymore.
apt-get install amavisd-new spamassassin clamav clamav-daemon zoo unzip bzip2 libnet-ph-perl libnet-snpp-perl libnet-telnet-perl nomarch lzop pax |
Now we configure the scanners:
vi /etc/amavis/conf.d/15-content_filter_mode <-- uncomment the next lines: @bypass_spam_checks_maps = ( \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); vi /etc/amavis/conf.d/50-user <-- add the next line in the middle of the file: $pax='pax'; adduser clamav amavis /etc/init.d/amavis restart /etc/init.d/clamav-freshclam restart /etc/init.d/clamav-daemon restart postconf -e 'content_filter = amavis:[127.0.0.1]:10024' postconf -e 'receive_override_options = no_address_mappings' vi /etc/postfix/master.cf <-- add these next lines to the end of the file: amavis unix - - - - 2 smtp -o smtp_data_done_timeout=1200 -o smtp_send_xforward_command=yes 127.0.0.1:10025 inet n - - - - smtpd -o content_filter= -o local_recipient_maps= -o relay_recipient_maps= -o smtpd_restriction_classes= -o smtpd_client_restrictions= -o smtpd_helo_restrictions= -o smtpd_sender_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,reject -o mynetworks=127.0.0.0/8 -o strict_rfc821_envelopes=yes -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks /etc/init.d/posfix restart |
Now postfix should be listening on port 25, and Amavisd-new should be listening on port 10024. Check this by running (your should see at least these two lines in the output):
netstat -tap tcp 0 0 *:smtp *:* LISTEN 23899/master tcp 0 0 localhost:10024 *:* LISTEN 27469/amavisd-new ( |
Now we install blacklisting doo-dads and link them into Amavis so your mail will be filtered through them:
apt-get install razor pyzor cd /usr/src wget http://www.dcc-servers.net/dcc/source/dcc-dccproc.tar.Z tar xzvf dcc-dccproc.tar.Z cd dcc-dccproc-1.3.147 ./configure --with-uid=amavis make make install chown -R amavis:amavis /var/dcc ln -s /var/dcc/libexec/dccifd /usr/local/bin/dccifd vi /etc/spamassassin/local.cf <-- add these lines to end of file: #dcc use_dcc 1 dcc_path /usr/local/bin/dccproc #pyzor use_pyzor 1 pyzor_path /usr/bin/pyzor #razor use_razor2 1 razor_config /etc/razor/razor-agent.conf #bayes use_bayes 1 use_bayes_rules 1 bayes_auto_learn 1 vi /etc/spamassassin/v310.pre loadplugin Mail::SpamAssassin::Plugin::DCC <-- uncomment this line spamassassin --lint <-- look for errors and fix them /etc/init.d/amavis restart sa-update --no-gpg crontab -e <-- add the next line to the end of the file: 47 2 */2 * * /usr/bin/sa-update --no-gpg &> /dev/null |
now your spamassassin will update every other morning at 2:47 a.m. Now we set up quota (or you don’t have to):
cd /usr/local/sbin/ vi quota_notify |
there’s a lot of cut/paste for this tasty bit of code (props to its creator jps@tntmax.com). Change the emails for postmaster to a real email address:
#!/usr/bin/perl -w # Author <jps@tntmax.com> # # This script assumes that virtual_mailbox_base in defined # in postfix's main.cf file. This directory is assumed to contain # directories which themselves contain your virtual user's maildirs. # For example: # # -----------/ # | # | # home/vmail/domains/ # | | # | | # example.com/ foo.com/ # | # | # ----------------- # | | | # | | | # user1/ user2/ user3/ # | # | # maildirsize # use strict; my $POSTFIX_CF = "/etc/postfix/main.cf"; my $MAILPROG = "/usr/sbin/sendmail -t"; my $WARNPERCENT = 80; my @POSTMASTERS = ('postmaster@domain.tld'); my $CONAME = 'My Company'; my $COADDR = 'postmaster@domain.tld'; my $SUADDR = 'postmaster@domain.tld'; my $MAIL_REPORT = 1; my $MAIL_WARNING = 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; } close(PCF); #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); } closedir(DIR); #iterate through domains for username/maildirsize files my @users; chdir($mboxBase); 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"}); } } closedir(DIR); #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); next; } die "Error: corrupt quotafile $quotafile" unless ($line =~ /\s*(-?\d+)/); $used += $1; } close(QF); next if (! $used); my $percent = int($used / $quota * 100); $lusers{$user} = $percent unless not $percent; } } #send a report to the postmasters if ($MAIL_REPORT) { open(MAIL, "| $MAILPROG"); select(MAIL); 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"; close(MAIL); } #email a warning to people over quota if ($MAIL_WARNING) { foreach my $luser (keys (%lusers)) { next unless $lusers{$luser} >= $WARNPERCENT; # skip those under quota open(MAIL, "| $MAILPROG"); select(MAIL); 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"; close(MAIL); } } |
Now we automate and make it executable:
chmod 755 quota_notify crontab -e <-- add next line to end of file: 0 0 * * * /usr/local/sbin/quota_notify &> /dev/null |
Okay, now let’s test Postfix by trying to use it from localhost. You’re really just looking to see if it tries to login and then after typing ehlo localhost gives you the 250-STARTTLS line:
telnet localhost 25 Trying ::1... Connected to localhost. Escape character is '^]'. 220 mail.setestbox.com ESMTP Postfix (Debian/GNU) ehlo localhost <-- type this 250-mail.setestbox.com 250-PIPELINING 250-SIZE 10240000 250-VRFY 250-ETRN 250-STARTTLS 250-AUTH PLAIN LOGIN 250-AUTH=PLAIN LOGIN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250 DSN quit 221 2.0.0 Bye Connection closed by foreign host. |
Now add a real email address and test:
mysql -u root -p USE mail; INSERT INTO `domains` (`domain`) VALUES ('example.com'); INSERT INTO `users` (`email`, `password`, `quota`) VALUES ('sales@example.com', ENCRYPT('secret'), 10485760); quit; postfix reload |
Now let’s send an email to that user, which will create the mail directory that we’ll log into later:
apt-get install mailutils mailx sales@example.com Subject: Welcome <-- ENTER Welcome! Have fun with your new mail account. <-- ENTER <-- CTRL+D Cc: <-- ENTER |
Okay, now we install a webmail client called Roundcube. Of course you can test the server with some other email client like Outlook or whatever, but if you want webmail, this is a nice feature. Jessie doesn’t have roundcube in the repos, so you’ll have to download and configure it. If you have it in the repo, skip this step:
mkdir /usr/src/roundcube cd /usr/src/roundcube wget https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/1.1.2/roundcubemail-1.1.2-complete.tar.gz tar xfj roundcubemail-1.1.2-complete.tar.gz cd roundcubemail-1.1.2 mkdir /var/www/mail rsync -hauv ./ /var/www/mail/ cd /var/www chown -R www-data.www-data mail vi /etc/apache2/sites-available/000-default (add these next lines somewhere in the middle) Alias /mail/ /var/www/mail/ <Directory "/var/www/mail"> Options Indexes Includes FollowSymLinks Multiviews AllowOverride All Order allow,deny Allow from all </Directory> /etc/init.d/apache2 reload <pre lang="bash"> Now create a roudcube mysql database like: <pre lang="bash"> mysql -u root -p create database roundcube; grant all privileges on roundcube.* TO 'roundcubemail'@'localhost' identified by 'some_password' with grant option; quit; cd /var/www/mail/SQL mysql -u root -p roundcube < mysql.initial.sql |
Now go do the rest of the install by opening a browser to:
http://mail.whateverservername.com/mail/installer/ |
Just follow prompts and enter your database password you set up. The rest of the settings should be fine. If you want to use en_US for the language, do that. Now remove the installer folder (after it succeeds with all the checks and visit the site:
cd /var/www/mail/ rm -rf installer http://mail.whateversite.com/mail/ |
apt-get install roundcube use db-common and type in the passwords vi /etc/roundcube/apache.conf <-- uncomment the next line Alias /roundcube /var/lib/roundcube /etc/init.d/apache2 reload |
Now try to login to your webmail at:
http://mail.yourdomain.com/roundcube/
and try to login as that user@example.com and see if you got your email 🙂
Write a comment
You need to login to post comments!