Building a mail server

Feedback on this guide is welcome and appreciated, and will be incorporated if it fits.

I wrote this guide because my old mail server setup stopped working and I switched to using external services such as Zoho. Zoho & co. work very well, but as someone who has more time than money, I could only create so many free domain/user accounts. So this past Sunday, I resolved to sit down and get my mail server back up and running in order to have all the email support I needed for my various community/non-profit projects, not to mention my own personal and business email. I also determined to write down everything I did because I have found no single usable mail server guide for Gentoo. This is probably not “the single usable guide,” either, but my goal is to document this way of doing it as completely as possible, with no ambiguity or skipped steps.

This guide assumes you have an existing Gentoo Linux server configured with Apache, Mysql and PHP. If you don’t have these, this guide will not work for you.

The following components will be used in this mail server:

  • Postfix Admin (for managing mail domains and users)
  • Postfix MTA (mail transfer agent)
  • Dovecot LDA (local delivery agent)
  • MySQL
  • Squirrelmail web interface
  • Clam antivirus integrated with Amavis
  • Spamassassin integrated with Amavis

Throughout this guide, remember to replace example.org, server.example.org and user@example.org with your actual server details.

I don’t go into great detail explaining individual lines – otherwise this guide would be several times as long – but info about any particular configuration line is easily found.

Use flags

Make sure you have the following use flags enabled in make.conf, in addition to whatever flags you already have:


USE="apache2 bindist bzip2 caps clamdtop crypt curl dovecot dovecot-sasl
filecaps fontconfig gd geoip imap ipv6 jpeg maildir mysql mysqldump
mysqlhotcopy mysqli pdo png sasl sieve sockets spamassassin spell ssl
threads truetype urandom vhosts xslt -mbox"

If you had to add any of these use flags, run


emerge -vuDN world

before proceeding.

Postfix Admin

Postfix Admin is in Portage. I love Portage – I think it’s a great package manager – but I have no love for installing web apps through a package manager of any kind. If you want to go this route, that’s up to you, but you need to look elsewhere for help with that. I do recommend manually installing Postfix Admin and other web apps. It’s not difficult, and you get the latest production version from the developer instead of whatever old version is hanging around in Portage, as often happens with web apps.

I have my webserver set up with virtual hosts, with /var/www/localhost unassigned. This means that while my web sites have friendly URLs, my server’s IP address is directly accessible in a browser – albeit secured from the public – and therefore available to me for running web apps. This guide assumes you have /var/www/localhost/htdocs set up to run web apps, but you can put your web apps wherever you would normally put them in your own web server config.

  • Download Postfix Admin from https://sourceforge.net/projects/postfixadmin/
  • If you access your server through ssh, scp the tarball to your server.
  • mv /scp/upload/location/postfixadmin-(version).tar.gz /var/www/localhost/htdocs
  • cd /var/www/localhost/htdocs
  • tar xvzf postfixadmin-(version).tar.gz
  • (Optional, but I find this makes for an easier URL) mv postfixadmin-(version) postfixadmin
  • (Optional, no reason to have it hanging around, though) rm postfixadmin-(version).tar.gz
  • chown -R apache:apache postfixadmin/

That’s the install. ls -lah should give you this:


drwxrwxr-x 17 apache apache 4.0K Sep 11 19:40 postfixadmin

Make sure you minimally secure the postfixadmin dir with an .htaccess file over SSL.

Now for the Postfix Admin setup.

Go to http://example.org/postfixadmin (or wherever you’ve configured Postfix Admin to be.) You should see the Postfix Admin welcome page.

You need to go back to your terminal and create the MySQL database for Postfix Admin:


mysql -u root -p
create database postfix;
use postfix;

We need two users for this database – one for Postfix Admin to manage domains and users, and one for Postfix and Dovecot to validate logins. The latter does not need to be able to write to the database or otherwise change anything.

Create Postfix Admin user:


CREATE USER 'postfix' IDENTIFIED BY 'somepassword';

Grant appropriate privileges to this user:


GRANT SELECT, INSERT, DELETE, UPDATE, CREATE, ALTER, INDEX, DROP, CREATE TEMPORARY TABLES, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE VIEW, EVENT, TRIGGER ON postfix.* TO 'postfix'@'localhost';

Create the user for Postfix and Dovecot:


CREATE USER 'mailuser' IDENTIFIED BY 'mailuserpassword';

Grant it appropriate permissions:


GRANT SELECT ON postfix.* TO 'mailuser'@'localhost';

With that done, the Postfix Admin configuration file needs to be edited manually. Change the following items in /var/www/localhost/htdocs/postfixadmin/config.inc.php:

Change

$CONF['configured'] = false;

to:

$CONF['configured'] = true;

Change

$CONF['database_user'] = 'postfix';

to: whatever Postfix Admin database user you created if it’s not “postfix.”

Change

$CONF['database_password'] = 'postfixadmin';

to: the password you created for database user “postfix.”

Change

$CONF['database_name'] = 'postfix';

to: the name of the database you created if it’s not “postfix.”

Change

$CONF['admin_email'] = '';

to: a working email address you want to use for administrative contact purposes.

Change

$CONF['encrypt'] = 'md5crypt';

to:

$CONF['encrypt'] = 'dovecot:PLAIN-MD5';

Change

$CONF['dovecotpw'] = "/usr/sbin/doveadm pw";

to:

$CONF['dovecotpw'] = "/usr/bin/doveadm pw";

Save and quit. Return to the Postfix Admin welcome page in your browser and go to http://example.org/postfixadmin/setup.php. If everything above was configured correctly, the page should take a minute or so to load while it creates database tables. If it is not able to proceed, it will tell you why and you should fix those items listed. When it’s done creating database tables, at the bottom of the page should be a form to change the setup password. Enter a password and click on “Generate password hash.” Open config.inc.php again and paste the hash into:


$CONF['setup_password'] = 'changeme';

Now create your Postfix Admin superadmin account.

If all went well, you should now be looking at a link to login to Postfix Admin.

Do not delete any files from the postfixadmin dir. Some web apps want you to delete initial setup or config files/dirs. Postfix Admin requires no such thing and is not designed to have anything deleted from its file dir.

Use Postfix Admin to set up your first mail domain and at least one mailbox for that domain. Leave “send welcome mail” unchecked when creating a mailbox – the mail server isn’t set up yet. Create a catch-all alias, if you want. I generally don’t. If spammers try to hit a bunch of random – and non-existent – mail users at my domain, I don’t want them to get through.

Installing Postfix and Dovecot


emerge postfix

If you didn’t skip the section above about use flags, this will pull in Dovecot automatically.

If you’re like me and have started over several times to set up a mail server using this or that guide, you might get this:


* IMPORTANT: config file '/etc/sasl2/smtpd.conf' needs updating.

Go ahead and run etc-update on that, overwriting the existing smtpd.conf.

Edit /etc/mail/aliases and add the following:


root: root@example.org
operator: operator@example.org

I also change everything above that to “root” on the right, which ensures that everything goes to “root” as defined above, which in turn is mapped to a real email address. Note that this also means whatever the system mails to root will show up in the inbox of the root-mapped email address. That could be handy or annoying, depending on how much of a server admin you are.

Generate alias database:


newaliases

Symlink the aliases to the Postfix dir to make them easier to find:


ln -s /etc/mail/aliases /etc/postfix/aliases
ln -s /etc/mail/aliases.db /etc/postfix/aliases.db

To integrate Dovecot with Postfix, add the following to the end of /etc/postfix/master.cf:


dovecot unix - n n - - pipe
flags=DRhu user=mail:mail argv=/usr/libexec/dovecot/deliver -f ${sender} -d ${recipient}

Open /etc/postfix/main.cf and set myhostname to your server’s FQDN. It should look like servername.example.org. Uncomment the line for myorigin = $myhostname. Uncomment the line for mydestination = $myhostname, localhost.$mydomain, localhost.

Add to the bottom of /etc/postfix/main.cf:


virtual_transport = dovecot
dovecot_destination_recipient_limit = 1

To integrate Dovecot SASL authentication, add the following to /etc/postfix/main.cf:


smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth

Create the following files, which Postfix will use to look up user details in the postfix database. (Database maps.)

/etc/postfix/sql_virtual_alias_maps.cf:


user = mailuser
password = mailuserpassword
hosts = localhost
dbname = postfix
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'

/etc/postfix/sql_virtual_domain_maps.cf:


user = mailuser
password = mailuserpassword
hosts = localhost
dbname = postfix
query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '0' AND active = '1'

/etc/postfix/sql_virtual_mailbox_maps.cf:


user = mailuser
password = mailuserpassword
hosts = localhost
dbname = postfix
query = SELECT CONCAT(maildir, 'Maildir/') AS maildir FROM mailbox WHERE username='%s' AND active = '1'

These files need to be known by Postfix. Open /etc/postfix/main.cf and add:


virtual_alias_maps = mysql:/etc/postfix/sql_virtual_alias_maps.cf
virtual_mailbox_domains = mysql:/etc/postfix/sql_virtual_domain_maps.cf
virtual_mailbox_maps = mysql:/etc/postfix/sql_virtual_mailbox_maps.cf

To enforce sender restrictions and to handle rejection, add to /etc/postfix/main.cf:


smtpd_sender_restrictions = reject_non_fqdn_sender
smtpd_reject_unlisted_sender = yes
smtpd_recipient_restrictions =
permit_mynetworks
reject_non_fqdn_recipient
permit_sasl_authenticated
reject_unauth_destination
reject_rbl_client zen.spamhaus.org
reject_rbl_client bl.spamcop.net

Secure SMTP

To securely access SMTP to send mail, the following steps should be taken.

Either acquire certificates from http://www.cacert.org/ or http://www.startssl.com/, or generate your own self-signed certificates. I personally prefer the later, since I don’t need to prove any authenticity to anyone but myself and keep spammers out, but either works just as well.

Generate a self-signed cert:

cd /etc/postfix (You can put your certs anywhere, but I find it simplifies things to keep Postfix-related items in the Postfix dir.)
openssl genrsa -des3 -out server.example.org.key 2048
openssl req -new -key server.example.org.key -out server.example.org.csr
openssl x509 -req -days 365 -in server.example.org.csr -signkey server.example.org.key -out server.example.org.crt
openssl rsa -in server.example.org.key -out server.example.org.key.nopass
mv server.example.org.key.nopass server.example.org.key
openssl req -new -x509 -extensions v3_ca -keyout cakey.pem -out cacert.pem -days 3650

Add the following to /etc/postfix/main.cf:

smtpd_tls_key_file = /etc/postfix/server.example.org.key
smtpd_tls_cert_file = /etc/postfix/server.example.org.crt
smtpd_tls_CAfile = /etc/postfix/cacert.pem
smtpd_tls_loglevel = 0
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 10800s
smtp_tls_mandatory_ciphers = high
smtp_tls_security_level = may
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes

And add to /etc/postfix/master.cf:

smtps inet n - n - - smtpd -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes
submission inet n - n - - smtpd -o smtpd_enforce_tls=yes -o smtpd_sasl_auth_enable=yes

Note that in the chroot column, everything should be “n.”

Dovecot configuration

Edit /etc/dovecot/dovecot.conf:

Change

#protocols = imap pop3 lmtp

to: Uncomment.

Change

#listen = *, ::

to: Uncomment.

Edit /etc/dovecot/conf.d/10-auth.conf:

Change

#disable_plaintext_auth = yes

to: Uncomment.

Change

auth_mechanisms = plain

to:

auth_mechanisms = plain login cram-md5

Change

!include auth-system.conf.ext

to: Comment out.

Edit /etc/dovecot/conf.d/10-mail.conf:

Change

mail_location = maildir:~/.maildir

to:

mail_location = maildir:/var/mail/%d/%n/Maildir/:INDEX=/var/mail/%d/%n/indexes

Change

#mail_uid =

to:

mail_uid = 8

Change

#mail_gid =

to:

mail_gid = 12

Change

#first_valid_uid = 500

to:

first_valid_uid = 8

Change

#last_valid_uid = 0

to:

last_valid_uid = 8

Change

#first_valid_gid = 1

to:

first_valid_gid = 12

Change

#last_valid_gid = 0

to:

last_valid_gid = 12

Change

#mail_plugins =

to:

mail_plugins = quota

The above mail user and group ids belong to the mail user. If your mail user and group has different ids, you should use those. Double check the uid and gid with:


id mail

Open /etc/dovecot/conf.d/10-master.conf and find the section “service auth”. Uncomment lines in the code below, and add any lines in the code below if they are missing from the default config:


service auth {
unix_listener auth-userdb {
mode = 0600
user = mail
group = mail
}

unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}

service auth-worker {
user = mail
}

service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
user = postfix
group = postfix
}

}

Edit /etc/dovecot/conf.d/15-lda.conf:

Change

#postmaster_address =

to:

postmaster_address = your-administrative-email@example.org

Edit /etc/dovecot/conf.d/20-imap.conf:

Change

#mail_plugins = $mail_plugins

to:

mail_plugins = $mail_plugins imap_quota

Edit /etc/dovecot/dovecot-sql.conf.ext:

Change

#driver =

to:

driver = mysql

Change

#connect =

to:

connect = host=/var/run/mysqld/mysqld.sock user=mailuser password=mailuserpassword dbname=postfix

Change

#default_pass_scheme = MD5

to:

default_pass_scheme = CRAM-MD5

Change

#password_query = \
# SELECT username, domain, password \
# FROM users WHERE username = '%n' AND domain = '%d'

to:

password_query = SELECT CONCAT('/var/mail/', maildir) AS userdb_home, \
username AS user, password, CONCAT('*:bytes=', quota) AS userdb_quota_rule \
FROM mailbox WHERE username = '%u' AND active = 1

Change

#user_query = \
# SELECT home, uid, gid \
# FROM users WHERE username = '%n' AND domain = '%d'

to:

user_query = SELECT CONCAT('/var/mail/', maildir) AS home, \
CONCAT('*:bytes=', quota) AS quota_rule \
FROM mailbox WHERE username = '%u' AND active = 1

Edit /etc/dovecot/conf.d/auth-sql.conf.ext:

Change

#userdb {
# driver = prefetch
#}

to:

userdb {
driver = prefetch
}

Starting services


/etc/init.d/postfix start
rc-update add postfix default
/etc/init.d/dovecot start
rc-update add dovecot default
/etc/init.d/saslauthd start
rc-update add saslauthd default

Testing

Now we can test the mail server locally, in a terminal on the server or through ssh.


telnet example.org 25

You should see this:

Trying 1.2.3.4...
Connected to example.org.
Escape character is '^]'.
220 server.example.org ESMTP Postfix

Enter the auth login command:

auth login

You should see this:

334 VXNlcm5hbWU6

This is base64 for “Username.” You’ll need to reply in base64. In another terminal, do

perl -MMIME::Base64 -e 'print encode_base64("username\@example.org");'

It is necessary to escape @ with \. Enter the output of that into your telnet session – you can copy/paste – and you should see:

334 UGFzc3dvcmQ6

This is base64 for “Password.” Repeat the above perl command, putting the password you created for this mailbox in Postfix Admin inside the quotes, and enter it into your telnet session. If everything was done right, you should see:

235 2.7.0 Authentication successful

Now test sending mail, using the same telnet session.

Enter

mail from:user@example.org

You should see:

250 2.1.0 Ok

Enter:

rcpt to:user@example.org

You should see:

250 2.1.5 Ok

Enter:

data

You should see:

354 End data with .

Type a message – any message:

Today is a good day to succeed.

Now type a dot on a line by itself:

.

You should see:

250 2.0.0 Ok: queued as D39E6538005D

Quit:

quit

You’ll see:

221 2.0.0 Bye
Connection closed by foreign host.

The whole thing should look like this:

Trying 1.2.3.4...
Connected to example.org.
Escape character is '^]'.
220 server.example.org ESMTP Postfix
auth login
334 VXNlcm5hbWU6
dXNlcm5hbWVAZXhhbXBsZS5vcmc=
334 UGFzc3dvcmQ6
eW91cnBhc3N3b3Jk
235 2.7.0 Authentication successful
mail from:user@example.org
250 2.1.0 Ok
rcpt to:user@example.org
250 2.1.5 Ok
data
354 End data with .
Today is a good day to succeed.
.
250 2.0.0 Ok: queued as D39E6538005D
quit
221 2.0.0 Bye
Connection closed by foreign host.

Go to /var/mail/example.org/user/.maildir/. This is your mail user’s home dir. The test message you just sent should be in the “new” dir. You can read it with nano/vi/less/whatever.

If all that is working, it’s time to set up Squirrelmail.

Squirrelmail

Squirrelmail is not flashy, but it is maintained and it works well. It doesn’t need its own database, which is one less thing for you to worry about.

Download Squirrelmail from http://squirrelmail.org/download.php. Download the bz2 file and scp it to your server. Then:

  • mv /scp/upload/location/squirrelmail-webmail-1.4.22.tar.bz2 /var/www/localhost/htdocs/
  • tar xvjpf squirrelmail-webmail-1.4.22.tar.bz2
  • mv squirrelmail-webmail-1.4.22 squirrelmail
  • chown -R apache:apache squirrelmail
  • rm squirrelmail-webmail-1.4.22.tar.bz2
  • cd squirrelmail
  • ./configure

You need to set the data and attachment dirs in 4. General Options. They should look like:

1. Data Directory : /var/www/localhost/htdocs/squirrelmail/data/
2. Attachment Directory : /var/www/localhost/htdocs/squirrelmail/attach/

You should also set your domain in 2. Server Settings. Everything else is according to your preferences. Go through the options and see what you want to change. When you’re done, hit S to save. You should now be able to visit http://example.org/squirrelmail and log in with the user@example.org you created in Postfix Admin. This is another web app should be minimally secure with .htaccess over SSL. Send a test message to an account on another server – such as a gmail account – and reply back when you get it.

Setting up antivirus

Integrating anti-spam and antivirus


emerge amavisd-new

Edit /etc/amavisd.conf:

Change

$mydomain = 'example.com';

to:

$mydomain = 'localhost';

Change

virus_admin_maps => ["virusalert\@$mydomain"],

to:

virus_admin_maps => ["root\@$mydomain"],

Change

spam_admin_maps => ["virusalert\@$mydomain"],

to:

spam_admin_maps => ["root\@$mydomain"],

Change

$virus_admin = "virusalert\@$mydomain";
$mailfrom_notify_admin = "virusalert\@$mydomain";
$mailfrom_notify_recip = "virusalert\@$mydomain";
$mailfrom_notify_spamadmin = "spam.police\@$mydomain";

to: Change all these to root\@$mydomain.

The above domain and address settings work in conjunction with the /etc/mail/aliases file you set up earlier in this guide. Reports will go to root@localhost, which in /etc/mail/aliases is already mapped to a real email address.

Change

# $myhostname = 'host.example.com';

to: Uncomment and fill in your FQDN.

Change

$log_level = 0;

to:

$log_level = 5;

This is for testing purposes. You can leave it at 5 or turn it down when everything is confirmed to be working.

Change

# ['ClamAV-clamd',
# \&ask_daemon, ["CONTSCAN {}\n", "/var/run/clamav/clamd.sock"],
# qr/\bOK$/m, qr/\bFOUND$/m,
# qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],

to: Uncomment these lines.

Edit /etc/postfix/master.cf and add to the bottom of the file:

localhost:10025 inet n - n - 2 smtpd
-o smtp_dns_support_level=enabled
-o content_filter=
-o myhostname=server.example.org
-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 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_unknown_recipient_checks,no_header_body_checks
-o smtpd_authorized_xforward_hosts=127.0.0.0/8

amavis unix – – n – 2 lmtp
-o disable_dns_lookups=yes
-o lmtp_send_xforward_command=yes
-o lmtp_data_done_timeout=1200

Replace the lines for smtp and smtps with:

smtp inet n - n - 2 smtpd
-o content_filter=amavis:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings
smtps inet n - n - 2 smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o content_filter=amavis:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings

Now run:

sa-update
gpasswd -a clamav amavis
/etc/init.d/clamd restart.

(Clamav needs to be a part of amavisd’s group, and then restarted.)

Start amavisd:

/etc/init.d/amavisd start
rc-update add amavisd default
postfix reload

Test by using a remote client to send mail from an external mail address, with “X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*” in the body of the message. Check the headers by viewing the source of the reply. In the source, you should see:

X-Virus-Scanned: amavisd-new at localhost

In /var/log/messages, you should find (you can tail -f /var/log/messages before you send the email):

Jan 14 17:07:58 (servername) amavis[3714]: (03714-02) Blocked INFECTED (Eicar-Test-Signature) {DiscardedOpenRelay,Quarantined}, [ip address]:21429 [ip address] <external@emailaddress.com> -> <testuser@example.org>, quarantine: virus-Vj-v2__IqQnQ, Queue-ID: 945005380061, Message-ID: <f2e8f414-0896-2fae-4e88-8e772da686a7@example.org>, mail_id: Vj-v2__IqQnQ, Hits: -, size: 1652, 124 ms
Jan 14 17:07:58 (servername) postfix/lmtp[20424]: 945005380061: to=<testuser@example.org>, relay=127.0.0.1[127.0.0.1]:10024, delay=0.84, delays=0.71/0.01/0/0.12, dsn=2.7.0, status=sent (250 2.7.0 Ok, discarded, id=03714-02 - INFECTED: Eicar-Test-Signature)

You should get a message from root@localhost with

VIRUS (Eicar-Test-Signature) in mail FROM [ip address]:21429 <external@user.com>

as the subject.

Spamassassin integration

Spamassassin should already be installed from your use flags pulling it in.

Edit /etc/mail/spamassassin/local.cf:
Change:

# rewrite_header Subject *****SPAM*****

to: Uncomment.

Change:

# report_safe 1

to: Uncomment.

Change:

# required_score 5.0

to: Uncomment.

Change:

# bayes_auto_learn 1

to: Uncomment.

Edit /etc/amavisd.conf:
Change:

# @bypass_spam_checks_maps = (1);

to: Make sure this line is commented out.

Change:

@local_domains_maps = ( [".$mydomain"]);

to, if you have multiple mail domains:

@local_domains_maps = ( [".$mydomain", "anotherdomain.com","yetanother.org"]);

Stub: Would like to have a better way to have this check the virtual domains already set up for Postfix.

Edit /etc/postfix/main.cf:
Add:

content_filter = amavis:[127.0.0.1]:10024

Edit /etc/postfix/master.cf:
Add:

localhost:10025 inet n - n - 2 smtpd
-o smtp_dns_support_level=enabled
-o content_filter=
-o myhostname=serverdef.audiodef.com
-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 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_unknown_recipient_checks,no_header_body_checks
-o smtpd_authorized_xforward_hosts=127.0.0.0/8

amavis unix – – n – 2 lmtp
-o disable_dns_lookups=yes
-o lmtp_send_xforward_command=yes
-o lmtp_data_done_timeout=1200

Restart Amavis:

/etc/initd.amavisd restart

Reload Postfix:

postfix reload

Test Spamassassin by sending an email to one of your users from an external source, such as Gmail or Yahoo mail. In the body should be, on it’s own line:

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

This should trigger your setup to deliver the message with a spam label, if everything has been done correctly.

Appendix

Your config files should look like these. You should not simply copy/paste them unless you’re already familiar with this guide and need to restore something or are rebuilding your server.

/etc/postfix/main.cf:

compatibility_level = 2
soft_bounce = yes
queue_directory = /var/spool/postfix
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix
mail_owner = postfix
myhostname = fullyqualified.domain.com
myorigin = $myhostname
mydestination = $myhostname, localhost.$mydomain, localhost
unknown_local_recipient_reject_code = 550
debug_peer_level = 2
debugger_command =
PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
ddd $daemon_directory/$process_name $process_id & sleep 5
sendmail_path = /usr/sbin/sendmail
newaliases_path = /usr/bin/newaliases
mailq_path = /usr/bin/mailq
setgid_group = postdrop
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /etc/postfix
readme_directory = no
inet_protocols = ipv4
meta_directory = /etc/postfix
shlib_directory = /usr/lib64/postfix/${mail_version}
home_mailbox = .maildir/

#Guide config options
virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
virtual_alias_maps = mysql:/etc/postfix/sql_virtual_alias_maps.cf
virtual_mailbox_domains = mysql:/etc/postfix/sql_virtual_domain_maps.cf
virtual_mailbox_maps = mysql:/etc/postfix/sql_virtual_mailbox_maps.cf
smtpd_sender_restrictions = reject_non_fqdn_sender
smtpd_reject_unlisted_sender = yes
smtpd_recipient_restrictions =
permit_mynetworks
reject_non_fqdn_recipient
permit_sasl_authenticated
reject_unauth_destination
reject_rbl_client zen.spamhaus.org
reject_rbl_client bl.spamcop.net
smtpd_tls_key_file = /etc/postfix/fullyqualified.domain.com.key
smtpd_tls_cert_file = /etc/postfix/fullyqualified.domain.com.crt
smtpd_tls_CAfile = /etc/postfix/cacert.pem
smtpd_tls_loglevel = 3
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 10800s
smtp_tls_mandatory_ciphers = high
smtp_tls_security_level = may
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes

/etc/postfix/master.cf:

#
# Postfix master process configuration file. For details on the format
# of the file, see the master(5) manual page (command: "man 5 master" or
# on-line: http://www.postfix.org/master.5.html).
#
# Do not forget to execute "postfix reload" after editing this file.
#
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
smtp inet n - n - 2 smtpd
-o content_filter=amavis:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings
smtps inet n - n - 2 smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o content_filter=amavis:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings

submission inet n – n – – smtpd -o smtpd_enforce_tls=yes -o smtpd_sasl_auth_enable=yes
pickup unix n – n 60 1 pickup
cleanup unix n – n – 0 cleanup
qmgr unix n – n 300 1 qmgr
tlsmgr unix – – n 1000? 1 tlsmgr
rewrite unix – – n – – trivial-rewrite
bounce unix – – n – 0 bounce
defer unix – – n – 0 bounce
trace unix – – n – 0 bounce
verify unix – – n – 1 verify
flush unix n – n 1000? 0 flush
proxymap unix – – n – – proxymap
proxywrite unix – – n – 1 proxymap
smtp unix – – n – – smtp
relay unix – – n – – smtp
showq unix n – n – – showq
error unix – – n – – error
retry unix – – n – – error
discard unix – – n – – discard
local unix – n n – – local
virtual unix – n n – – virtual
lmtp unix – – n – – lmtp
anvil unix – – n – 1 anvil
scache unix – – n – 1 scache

dovecot unix – n n – – pipe
flags=DRhu user=mail:mail argv=/usr/libexec/dovecot/deliver -f ${sender} -d ${recipient}

localhost:10025 inet n – n – 2 smtpd
-o smtp_dns_support_level=enabled
-o content_filter=
-o myhostname=serverdef.audiodef.com
-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 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_unknown_recipient_checks,no_header_body_checks
-o smtpd_authorized_xforward_hosts=127.0.0.0/8

amavis unix – – n – 2 lmtp
-o disable_dns_lookups=yes
-o lmtp_send_xforward_command=yes
-o lmtp_data_done_timeout=1200

/etc/dovecot/dovecot.conf:

protocols = imap pop3 lmtp
listen = *, ::
dict {
#quota = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
#expire = sqlite:/etc/dovecot/dovecot-dict-sql.conf.ext
}
!include conf.d/*.conf
!include_try local.conf

#log_path = /var/log/dovecot.log

/etc/amavisd.conf

use strict;

$max_servers = 2; # num of pre-forked children (2..30 is common), -m
$daemon_user = ‘amavis’; # (no default; customary: vscan or amavis), -u
$daemon_group = ‘amavis’; # (no default; customary: vscan or amavis), -g

$mydomain = ‘fullyqualified.domain.com’; # a convenient default for other settings

$TEMPBASE = “$MYHOME/tmp”; # working directory, needs to exist, -T
$ENV{TMPDIR} = $TEMPBASE; # environment variable TMPDIR, used by SA, etc.
$QUARANTINEDIR = “$MYHOME/quarantine”; # -Q

$log_level = 0; # verbosity 0..5, -d
$log_recip_templ = undef; # disable by-recipient level-0 log entries
$do_syslog = 1; # log via syslogd (preferred)
$syslog_facility = ‘mail’; # Syslog facility as a string
# e.g.: mail, daemon, user, local0, … local7

$enable_db = 1; # enable use of BerkeleyDB/libdb (SNMP and nanny)
$nanny_details_level = 2; # nanny verbosity: 1: traditional, 2: detailed
$enable_dkim_verification = 0; # enable DKIM signatures verification
$enable_dkim_signing = 0; # load DKIM signing code, keys defined by dkim_key

@local_domains_maps = ( [“.$mydomain”, “anotherdomain.com”]); # list of all local domains

@mynetworks = qw( 127.0.0.0/8 [::1] [FE80::]/10 [FEC0::]/10
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 );

$unix_socketname = “$MYHOME/amavisd.sock”; # amavisd-release or amavis-milter
# option(s) -p overrides $inet_socket_port and $unix_socketname

$inet_socket_port = 10024; # listen on this local TCP port(s)

$policy_bank{‘MYNETS’} = { # mail originating from @mynetworks
originating => 1, # is true in MYNETS by default, but let’s make it explicit
os_fingerprint_method => undef, # don’t query p0f for internal clients
};

$interface_policy{‘10026’} = ‘ORIGINATING’;

$policy_bank{‘ORIGINATING’} = { # mail supposedly originating from our users
originating => 1, # declare that mail was submitted by our smtp client
allow_disclaimers => 1, # enables disclaimer insertion if available
# notify administrator of locally originating malware
virus_admin_maps => [“root\@$mydomain”],
spam_admin_maps => [“root\@$mydomain”],
warnbadhsender => 1,
# forward to a smtpd service providing DKIM signing service
forward_method => ‘smtp:[127.0.0.1]:10027’,
# force MTA conversion to 7-bit (e.g. before DKIM signing)
smtpd_discard_ehlo_keywords => [‘8BITMIME’],
bypass_banned_checks_maps => [1], # allow sending any file names and types
terminate_dsn_on_notify_success => 0, # don’t remove NOTIFY=SUCCESS option
};

$interface_policy{‘SOCK’} = ‘AM.PDP-SOCK’; # only applies with $unix_socketname

$policy_bank{‘AM.PDP-SOCK’} = {
protocol => ‘AM.PDP’,
auth_required_release => 0, # do not require secret_id for amavisd-release
};

$sa_tag_level_deflt = -999; # add spam info headers if at, or above that level
$sa_tag2_level_deflt = 5.0; # add ‘spam detected’ headers at that level
$sa_kill_level_deflt = 6.9; # triggers spam evasive actions (e.g. blocks mail)
$sa_dsn_cutoff_level = 10; # spam level beyond which a DSN is not sent
$sa_crediblefrom_dsn_cutoff_level = 18; # likewise, but for a likely valid From
$penpals_bonus_score = 8; # (no effect without a @storage_sql_dsn database)
$penpals_threshold_high = $sa_kill_level_deflt; # don’t waste time on hi spam
$bounce_killer_score = 100; # spam score points to add for joe-jobbed bounces

$sa_mail_body_size_limit = 400*1024; # don’t waste time on SA if mail is larger
$sa_local_tests_only = 0; # only tests which do not require internet access?

$virus_admin = “root\@$mydomain”; # notifications recip.

$mailfrom_notify_admin = “root\@$mydomain”; # notifications sender
$mailfrom_notify_recip = “root\@$mydomain”; # notifications sender
$mailfrom_notify_spamadmin = “root\@$mydomain”; # notifications sender
$mailfrom_to_quarantine = ”; # null return path; uses original sender if undef

@addr_extension_virus_maps = (‘virus’);
@addr_extension_banned_maps = (‘banned’);
@addr_extension_spam_maps = (‘spam’);
@addr_extension_bad_header_maps = (‘badh’);

$path = ‘/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/usr/bin:/bin’;

$MAXLEVELS = 14;
$MAXFILES = 3000;
$MIN_EXPANSION_QUOTA = 100*1024; # bytes (default undef, not enforced)
$MAX_EXPANSION_QUOTA = 500*1024*1024; # bytes (default undef, not enforced)

$sa_spam_subject_tag = ‘***Spam*** ‘;
$defang_virus = 1; # MIME-wrap passed infected mail
$defang_banned = 1; # MIME-wrap passed mail containing banned name
$defang_by_ccat{CC_BADH.”,3″} = 1; # NUL or CR character in header
$defang_by_ccat{CC_BADH.”,5″} = 1; # header line longer than 998 characters
$defang_by_ccat{CC_BADH.”,6″} = 1; # header field syntax error

$myhostname = ‘fullyqualified.domain.com’; # must be a fully-qualified domain name!

$final_virus_destiny = D_DISCARD;
$final_banned_destiny = D_DISCARD;
$final_spam_destiny = D_PASS; #!!! D_DISCARD / D_REJECT

@keep_decoded_original_maps = (new_RE(
qr’^MAIL$’, # let virus scanner see full original message
qr’^MAIL-UNDECIPHERABLE$’, # same as ^MAIL$ if mail is undecipherable
qr’^(ASCII(?! cpio)|text|uuencoded|xxencoded|binhex)’i,
# qr’^Zip archive data’, # don’t trust Archive::Zip
));

$banned_filename_re = new_RE(

### BLOCKED ANYWHERE
qr’^\.(exe-ms|dll)$’, # banned file(1) types, rudimentary

### BLOCK THE FOLLOWING, EXCEPT WITHIN UNIX ARCHIVES:
[ qr’^\.(rpm|cpio|tar)$’ => 0 ], # allow any in Unix-type archives

qr’.\.(pif|scr)$’i, # banned extensions – rudimentary

### BLOCK THE FOLLOWING, EXCEPT WITHIN ARCHIVES:
qr’^application/x-msdownload$’i, # block these MIME types
qr’^application/x-msdos-program$’i,
qr’^application/hta$’i,
qr’^(?!cid:).*\.[^./]*[A-Za-z][^./]*\.\s*(exe|vbs|pif|scr|bat|cmd|com|cpl|dll)[.\s]*$’i,
qr’.\.(exe|vbs|pif|scr|cpl)$’i, # banned extension – basic
);

# ENVELOPE SENDER SOFT-WHITELISTING / SOFT-BLACKLISTING

@score_sender_maps = ({ # a by-recipient hash lookup table,
# results from all matching recipient tables are summed

## site-wide opinions about senders (the ‘.’ matches any recipient)
‘.’ => [ # the _first_ matching sender determines the score boost

new_RE( # regexp-type lookup table, just happens to be all soft-blacklist
[qr’^(bulkmail|offers|cheapbenefits|earnmoney|foryou)@’i => 5.0],
[qr’^(greatcasino|investments|lose_weight_today|market\.alert)@’i=> 5.0],
[qr’^(money2you|MyGreenCard|new\.tld\.registry|opt-out|opt-in)@’i=> 5.0],
[qr’^(optin|saveonlsmoking2002k|specialoffer|specialoffers)@’i => 5.0],
[qr’^(stockalert|stopsnoring|wantsome|workathome|yesitsfree)@’i => 5.0],
[qr’^(your_friend|greatoffers)@’i => 5.0],
[qr’^(inkjetplanet|marketopt|MakeMoney)\d*@’i => 5.0],
),

# read_hash(“/var/amavis/sender_scores_sitewide”),

{ # a hash-type lookup table (associative array)
‘nobody@cert.org’ => -3.0,
‘cert-advisory@us-cert.gov’ => -3.0,
‘owner-alert@iss.net’ => -3.0,
‘slashdot@slashdot.org’ => -3.0,
‘securityfocus.com’ => -3.0,
‘ntbugtraq@listserv.ntbugtraq.com’ => -3.0,
‘security-alerts@linuxsecurity.com’ => -3.0,
‘mailman-announce-admin@python.org’ => -3.0,
‘amavis-user-admin@lists.sourceforge.net’=> -3.0,
‘amavis-user-bounces@lists.sourceforge.net’ => -3.0,
‘spamassassin.apache.org’ => -3.0,
‘notification-return@lists.sophos.com’ => -3.0,
‘owner-postfix-users@postfix.org’ => -3.0,
‘owner-postfix-announce@postfix.org’ => -3.0,
‘owner-sendmail-announce@lists.sendmail.org’ => -3.0,
‘sendmail-announce-request@lists.sendmail.org’ => -3.0,
‘donotreply@sendmail.org’ => -3.0,
‘ca+envelope@sendmail.org’ => -3.0,
‘noreply@freshmeat.net’ => -3.0,
‘owner-technews@postel.acm.org’ => -3.0,
‘ietf-123-owner@loki.ietf.org’ => -3.0,
‘cvs-commits-list-admin@gnome.org’ => -3.0,
‘rt-users-admin@lists.fsck.com’ => -3.0,
‘clp-request@comp.nus.edu.sg’ => -3.0,
‘surveys-errors@lists.nua.ie’ => -3.0,
’emailnews@genomeweb.com’ => -5.0,
‘yahoo-dev-null@yahoo-inc.com’ => -3.0,
‘returns.groups.yahoo.com’ => -3.0,
‘clusternews@linuxnetworx.com’ => -3.0,
lc(‘lvs-users-admin@LinuxVirtualServer.org’) => -3.0,
lc(‘owner-textbreakingnews@CNNIMAIL12.CNN.COM’) => -5.0,

# soft-blacklisting (positive score)
‘sender@example.net’ => 3.0,
‘.example.net’ => 1.0,

},
], # end of site-wide tables
});

@decoders = (
[‘mail’, \&do_mime_decode],
[‘F’, \&do_uncompress, [‘unfreeze’, ‘freeze -d’, ‘melt’, ‘fcat’] ],
[‘Z’, \&do_uncompress, [‘uncompress’, ‘gzip -d’, ‘zcat’] ],
[‘gz’, \&do_uncompress, ‘gzip -d’],
[‘gz’, \&do_gunzip],
[‘bz2’, \&do_uncompress, ‘bzip2 -d’],
[‘xz’, \&do_uncompress,
[‘xzdec’, ‘xz -dc’, ‘unxz -c’, ‘xzcat’] ],
[‘lzma’, \&do_uncompress,
[‘lzmadec’, ‘xz -dc –format=lzma’,
‘lzma -dc’, ‘unlzma -c’, ‘lzcat’, ‘lzmadec’] ],
[‘lrz’, \&do_uncompress,
[‘lrzip -q -k -d -o -‘, ‘lrzcat -q -k’] ],
[‘lzo’, \&do_uncompress, ‘lzop -d’],
[‘rpm’, \&do_uncompress, [‘rpm2cpio.pl’, ‘rpm2cpio’] ],
[[‘cpio’,’tar’], \&do_pax_cpio, [‘pax’, ‘gcpio’, ‘cpio’] ],
# [‘/usr/local/heirloom/usr/5bin/pax’, ‘pax’, ‘gcpio’, ‘cpio’]
[‘deb’, \&do_ar, ‘ar’],
[‘rar’, \&do_unrar, [‘unrar’, ‘rar’] ],
[‘arj’, \&do_unarj, [‘unarj’, ‘arj’] ],
[‘arc’, \&do_arc, [‘nomarch’, ‘arc’] ],
[‘zoo’, \&do_zoo, [‘zoo’, ‘unzoo’] ],
[‘doc’, \&do_ole, ‘ripole’],
[‘cab’, \&do_cabextract, ‘cabextract’],
[‘tnef’, \&do_tnef_ext, ‘tnef’],
[‘tnef’, \&do_tnef],
[[‘zip’,’kmz’], \&do_7zip, [‘7za’, ‘7z’] ],
[[‘zip’,’kmz’], \&do_unzip],
[‘7z’, \&do_7zip, [‘7zr’, ‘7za’, ‘7z’] ],
[[qw(7z zip gz bz2 Z tar)],
\&do_7zip, [‘7za’, ‘7z’] ],
[[qw(xz lzma jar cpio arj rar swf lha iso cab deb rpm)],
\&do_7zip, ‘7z’ ],
[‘exe’, \&do_executable, [‘unrar’,’rar’], ‘lha’, [‘unarj’,’arj’] ],
);

@av_scanners = (

[‘ClamAV-clamd’,
\&ask_daemon, [“CONTSCAN {}\n”, “/var/run/clamav/clamd.sock”],
qr/\bOK$/m, qr/\bFOUND$/m,
qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],
);

@av_scanners_backup = (

### http://www.clamav.net/ – backs up clamd or Mail::ClamAV
[‘ClamAV-clamscan’, ‘clamscan’,
“–stdout –no-summary -r –tempdir=$TEMPBASE {}”,
[0], qr/:.*\sFOUND$/m, qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],

# ### http://www.clamav.net/ – using remote clamd scanner as a backup
# [‘ClamAV-clamdscan’, ‘clamdscan’,
# “–stdout –no-summary –config-file=/etc/clamd-client.conf {}”,
# [0], qr/:.*\sFOUND$/m, qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],

# [‘ClamAV-clamd-stream’,
# \&ask_daemon, [“*”, ‘clamd:/var/run/clamav/clamd.sock’],
# qr/\bOK$/m, qr/\bFOUND$/m,
# qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],

);

1; # insure a defined return value

/etc/mail/spamassassin/local.cf:

rewrite_header Subject *****SPAM*****
report_safe 1
required_score 5.0
bayes_auto_learn 1
ifplugin Mail::SpamAssassin::Plugin::Shortcircuit

endif # Mail::SpamAssassin::Plugin::Shortcircuit

Connecting from Thunderbird

Fetching mail:
Server Name: example.org
User Name: user@example.org
Port: 143
Connection security: STARTTLS
Authentication method: Normal password

Sending mail:
Server Name: example.org
Port 587
Connection security: STARTTLS
Authentication method: Normal password
User Name: user@example.org

You need to click on Advanced config, which will basically close the new account setup window. Go into account settings and manually set the above options. This will get you set up with reading your inbox and connecting to your SMTP server to send mail.

Enjoy!

Thanks to:

  • grknight
  • Ant P.
  • freke

TODO: fail2ban integration to block repeated unauth login attempts to mail server.

Leave a Reply

Your email address will not be published. Required fields are marked *