--- author: email: mail@petermolnar.net image: https://petermolnar.net/favicon.jpg name: Peter Molnar url: https://petermolnar.net copies: - http://web.archive.org/web/20120212035733/http://petermolnar.eu:80/linux-tech-coding/mail-server-for-a-vps-postfixdovecotspamassassinclamavpolicyd-weight lang: en published: '2012-01-05T10:39:22+00:00' summary: Full featured mail server with memory footprint small enough even for a VPS, with dovecot, postfix, spamassassin, clamav, policyd-weight with all the configs you need. tags: - server title: 'Mail server for a VPS: Postfix, Dovecot, Spamassassin, policyd-weight' --- ### UPDATE, 2014-12-18 I've recently wrote an updated version of a similar, better setup[^1]. ### UPDATE, 2012-02-25 I've updated my system to Dovecot 2, and removed ClamAV from the whole line. I haven't received any virus mails in the last 4 years, also they usually end up as spam, and ClamAV was eating up \~300 MB memory total (50 RAM, 250 swap). For nearly 5 years, I always used Virtualmin GPL[^2] everywhere I could, because I did not had to configure many features myself, it came with pre-configs and really good backend scripts. But as always, it had a price: memory and CPU usage, what is luxury in the world of VPS[^3]'. I tried to look for the best solution to handle emails, filtering them for spam and virus, and the only system I came across with was always Amavisd[^4]. amavisd is basically a wrapper for spam and virus filtering: it can simultaneously use more than one for both purpose, and most people say it's a nice program. Unfortunately, I tried to configure it, not just use it, and for me it was hell. I've known that Perl is somewhat evil[^5], but configuring amavisd is a mess at all, so I searched for a way to bypass it. ~~It wasn't easy, but in the depth of the postfix forums, I've found out, that postfix is able to pass the mail to a program than catch the output and pass to another program and so on. The trickiest part was to include ClamAV filtering in the way, because ClamAV does not passes the mail back, so the filtering had to be included into Spamassassin.~~ I've also found, that Virtualmin uses it's configured version of Procmail[^6], which was the most hard to replace. The reason for the replacement was that I haven't find any plugin for Roundcube to manage the server-side filtering of procmail. Since Dovecot has already added a version of Sieve[^7] to it's core combined with Dovecot's Local Delivery Agent[^8], it could replace, or could even be a better solution than Procmail. Also Sieve can be accessed from outside, from, for example from Thunderbird with a plugin. Probably for the best security Virtualmin used local users for everything. While this is an easy and truly secure solution, for a bit more flexibility I used MySQL as source of data. Per user SpamAssassin could also be stored in MySQL and RoundCube has a plugin for this purpose too. ## install & configure mysql server This is not the topic for a mysql server install, there are plenty of tutorials on this topic. ### SQL scheme for our mail server ``` {.sql} -- -- Table structure for table `domains` -- CREATE TABLE IF NOT EXISTS `domains` ( `domain` varchar(50) NOT NULL, PRIMARY KEY (`domain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -------------------------------------------------------- -- -- Table structure for table `forwardings` -- CREATE TABLE IF NOT EXISTS `forwardings` ( `source` varchar(80) NOT NULL, `destination` text NOT NULL, PRIMARY KEY (`source`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -------------------------------------------------------- -- -- Table structure for table `transport` -- CREATE TABLE IF NOT EXISTS `transport` ( `domain` varchar(128) NOT NULL DEFAULT '', `transport` varchar(128) NOT NULL DEFAULT '', UNIQUE KEY `domain` (`domain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -------------------------------------------------------- -- -- Table structure for table `users` -- CREATE TABLE IF NOT EXISTS `users` ( `email` varchar(80) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ## the vmail user We're about to use a system user, named `vmail` for all our purposes. All the mails will be stored under the name of this user, with private rights. ``` {.bash} groupadd --gid 5000 vmail adduser -uid 5000 -gid 5000 --home /vmail --create-home vmail ``` ## dovecot I start with Dovecot, because even Postfix will rely on it in the authentication process. Also, Dovecot is going to be the local delivery agent (LDA) in order to use Sieve. POP3 is already out of sight, don't search for it ;) ### install dovecot ``` {.bash} apt-get install dovecot-common dovecot-imapd ``` ### update (2012-02-25 06:27): config for dovecot 2.X/etc/dovecot/dovecot.conf ``` {.perl} # # Main # disable_plaintext_auth = no log_timestamp = "%Y-%m-%d %H:%M:%S " login_greeting = webportfolio.hu mail_location = maildir:~/Maildir:INDEX=/var/lib/dovecot/index/%u:CONTROL=/var/lib/dovecot/control/%u mail_privileged_group = mail protocols = imap sieve ssl_cert = /etc/dovecot/dovecot.conf ​``` ## Dovecot configuration file protocols = imap imaps managesieve disable_plaintext_auth = no ## ## Logging ## log_timestamp = "%Y-%m-%d %H:%M:%S " syslog_facility = mail ## ## SSL settings ## ssl = yes ssl_cert_file = /etc/ssl/your_domain.crt ssl_key_file = /etc/ssl/your_domain.key ## ## Login processes ## login_process_size = 64 login_processes_count = 4 login_max_processes_count = 32 login_max_connections = 32 login_greeting = hi ## ## Mailbox locations and namespaces ## mail_location = maildir:~/Maildir:INDEX=/var/lib/dovecot/index/%u:CONTROL=/var/lib/dovecot/control/%u mail_privileged_group = mail ## ## IMAP specific settings ## protocol imap { imap_client_workarounds = outlook-idle } ## ## MANAGESIEVE specific settings ## protocol managesieve { login_executable = /usr/lib/dovecot/managesieve-login mail_executable = /usr/lib/dovecot/managesieve } ## ## LDA specific settings ## auth_executable = /usr/lib/dovecot/dovecot-auth protocol lda { postmaster_address = root@localhost # you going to need to create this file by hand with 0777 rights, I could not make it writeable any other way log_path = /var/log/dovecot.log info_log_path = /var/log/dovecot.log mail_plugins = sieve } ## ## Authentication processes ## auth default { user = root passdb sql { args = /etc/dovecot/dovecot-sql.conf } userdb static { args = uid=5000 gid=5000 home=/vmail/%d/%n allow_all_users=yes } socket listen { master { path = /var/run/dovecot/auth-master mode = 0600 user = vmail } client { path = /var/spool/postfix/private/auth mode = 0660 user = postfix group = postfix } } } ## ## Plugin settings ## plugin { # the /etc/dovecot/sieve/sieve.default will run _before_ any user defined sieve scripts sieve_before = /etc/dovecot/sieve/sieve.default } ``` ### /etc/dovecot/dovecot-sql.conf ``` {.perl} driver = mysql connect = host=127.0.0.1 dbname=mail_mysql_db user=mail_mysql_user password=mail_mysql_password default_pass_scheme = CRYPT password_query = SELECT email as user, password FROM users WHERE email='%u'; ``` ### /etc/dovecot/sieve/sieve.default ``` {.perl} require "fileinto"; if header :contains "X-Spam-Virus" "Yes" { fileinto "spam"; stop; } if header :contains "X-Spam-Status" "Yes" { fileinto "spam"; stop; } ``` ## postfix Postfix is the SMTP server; this is the one that communicates with the other mail servers, so it also receives and sends all the messages. ### install postfix ``` {.bash} apt-get install postfix postfix-mysql ``` ### /etc/postfix/main.cf ``` {.apache} smtpd_banner = your_mailserver_domain biff = no append_dot_mydomain = no delay_warning_time = 4h readme_directory = no alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases 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 = /vmail virtual_uid_maps = static:5000 virtual_gid_maps = static:5000 virtual_create_maildirsize = yes virtual_maildir_extended = 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 virtual_transport=dovecot dovecot_destination_recipient_limit=1 local_recipient_maps = proxy:unix:passwd.byname $alias_maps smtpd_tls_cert_file = /etc/ssl/your_domain.crt smtpd_tls_key_file = /etc/ssl/your_domain.key smtpd_tls_note_starttls = yes smtpd_use_tls=yes smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache myhostname = your_mailserver_domain myorigin = your_mailserver_domain mydestination = your_mailserver_domain your_mailserver_name localhost.localdomain locahost mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 mailbox_size_limit = 0 message_size_limit = 52428800 recipient_delimiter = + inet_interfaces = all smtpd_sasl_auth_enable = yes smtpd_sasl_security_options = noanonymous broken_sasl_auth_clients = yes smtpd_sasl_authenticated_header = yes smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_tls_security_level = may maximal_queue_lifetime = 1d queue_run_delay = 500s minimal_backoff_time = 500s bounce_queue_lifetime = 1d smtpd_helo_required = yes smtpd_helo_restrictions = permit_mynetworks, reject_invalid_hostname, permit smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_hostname, reject_non_fqdn_hostname, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_pipelining, reject_unauth_destination, check_policy_service inet:127.0.0.1:12525, permit ``` \#\#\#/etc/postfix/master.cf ``` {.apache} smtp inet n - - - - smtpd smtps inet n - - - - smtpd pickup fifo n - - 60 1 pickup cleanup unix n - - - 0 cleanup qmgr fifo n - n 300 1 qmgr tlsmgr unix - - - 1000? 1 tlsmgr rewrite unix - - - - - trivial-rewrite bounce unix - - - - 0 bounce defer unix - - - - 0 bounce trace unix - - - - 0 bounce verify unix - - - - 1 verify flush unix n - - 1000? 0 flush proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap smtp unix - - - - - smtp relay unix - - - - - smtp -o smtp_fallback_relay= showq unix n - - - - showq error unix - - - - - error retry unix - - - - - error discard unix - - - - - discard local unix - n n - - local virtual unix - n n - - virtual lmtp unix - - - - - lmtp anvil unix - - - - 1 anvil scache unix - - - - 1 scache dovecot unix - n n - - pipe flags=DRhu user=vmail:vmail argv=/usr/bin/spamc -e /usr/lib/dovecot/deliver -d ${recipient} -f {sender} maildrop unix - n n - - pipe flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} uucp unix - n n - - pipe flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient) ifmail unix - n n - - pipe flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient) bsmtp unix - n n - - pipe flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient scalemail-backend unix - n n - 2 pipe flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension} mailman unix - n n - - pipe flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user} ``` ### /etc/postfix/mysql-virtual\_domains.cf ``` {.apache} user = mail_mysql_user password = mail_mysql_password dbname = mail_mysql_db hosts = 127.0.0.1 query = SELECT domain AS virtual FROM domains WHERE domain='%s' ``` ### /etc/postfix/mysql-virtual\_email2email.cf ``` {.apache} user = mail_mysql_user password = mail_mysql_password dbname = mail_mysql_db hosts = 127.0.0.1 query = SELECT email FROM users WHERE email='%s ``` ### /etc/postfix/mysql-virtual\_forwardings.cf ``` {.apache} user = mail_mysql_user password = mail_mysql_password dbname = mail_mysql_db hosts = 127.0.0.1 query = SELECT destination FROM forwardings WHERE source='%s' ``` ### /etc/postfix/mysql-virtual\_mailboxes.cf ``` {.apache} user = mail_mysql_user password = mail_mysql_password dbname = mail_mysql_db hosts = 127.0.0.1 query = SELECT CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') FROM users WHERE email='%s' ``` ### /etc/postfix/dynamicmaps.cf ``` {.apache} tcp /usr/lib/postfix/dict_tcp.so dict_tcp_open mysql /usr/lib/postfix/dict_mysql.so dict_mysql_open ``` ### /etc/postfix/sasl/smtpd.conf ``` {.apache} pwcheck_method: saslauthd mech_list: plain login allow_plaintext: true auxprop_plugin: mysql sql_hostnames: 127.0.0.1 sql_user: mail_mysql_user sql_passwd: mail_mysql_password sql_database: mail_mysql_db sql_select: select password from users where email = '%u' ``` ## policyd-weight I've already wrote a post about policyd-weight and why it is better than blocklists in postfix config[^9] directly, so this is only my current config file and the install. This config is tweaked at some points but I honestly forgot, what I configured exactly, so here is the full version. ``` {.bash} apt-get install policyd-weight ``` ### /etc/policyd-weight.conf ``` {.perl} # ---------------------------------------------------------------- # policyd-weight configuration (defaults) Version 0.1.14 beta-17 # ---------------------------------------------------------------- # 1 or 0 - don't comment $DEBUG = 0; $REJECTMSG = "550 Mail appeared to be SPAM or forged. Ask your Mail/DNS-Administrator to correct HELO and DNS MX settings or to get removed from DNSBLs"; $REJECTLEVEL = 4; # Mails with scores which exceed this REJECTLEVEL will be rejected # A space separated case-sensitive list of strings on which if found in the $RET logging-string policyd-weight changes # its action to $DEFER_ACTION in case. # of rejects. # USE WITH CAUTION! # DEFAULT: "IN_SPAMCOP= BOGUS_MX=" $DEFER_STRING = 'IN_SPAMCOP= BOGUS_MX='; # Possible values: DEFER_IF_PERMIT, DEFER_IF_REJECT, # 4xx response codes. See also access(5) # DEFAULT: 450 $DEFER_ACTION = '450'; # DEFER mail only up to this level # scores greater than DEFER_LEVEL will be # rejected # DEFAULT: 5 $DEFER_LEVEL = 5; $DNSERRMSG = '450 No DNS entries for your MTA, HELO and Domain. Contact YOUR administrator'; # 1: ON, 0: OFF (default) # If ON request that ALL clients are only checked against RBLs $dnsbl_checks_only = 0; # specify a comma-separated list of regexps # for client hostnames which shall only be RBL checked. # This does not work for postfix' "unknown" clients. # The usage of this should not be the norm and is a tool for people which like to shoot in their own foot. # DEFAULT: empty @dnsbl_checks_only_regexps = ( # qr/[^.]*(exch|smtp|mx|mail).*..*../, # qr/yahoo.com$/ ); # 1: ON (default), 0: OFF # When set to ON it logs only RBLs which affect scoring (positive or negative) $LOG_BAD_RBL_ONLY = 1; ## DNSBL settings @dnsbl_score = ( # host,hit score, miss score, log name 'bl.spamcop.net',3,0,'bl.spamcop.net', 'cbl.abuseat.org', 3, 0, 'cbl.abuseat.org', 'dnsbl.njabl.org', 3, 0, 'dnsbl.njabl.org', 'dnsbl.sorbs.net', 3, 0, 'dnsbl.sorbs.net', 'zen.spamhaus.org', 3, 0, 'zen.spamhaus.org', 'pbl.spamhaus.org', 3, 0, 'pbl.spamhaus.org', 'list.dsbl.org',3,0,'list.dsbl.org', ); # If Client IP is listed in MORE DNSBLS than this var, it gets REJECTed immediately $MAXDNSBLHITS = 3; # alternatively, if the score of DNSBLs is ABOVE this level, reject immediately $MAXDNSBLSCORE = 9; $MAXDNSBLMSG = '550 Az levelezoszerveruk IP cime tul sok spamlistan talahato, kerjuk ellenorizze! / Your MTA is listed in too many DNSBLs; please check.'; ## RHSBL settings @rhsbl_score = ( 'multi.surbl.org',4,0,'multi.surbl.org', 'rhsbl.ahbl.org',4,0,'rhsbl.ahbl.org', 'dsn.rfc-ignorant.org',3.5,0,'dsn.rfc-ignorant.org', 'postmaster.rfc-ignorant.org', 0.1,0,'postmaster.rfc-ignorant.org', 'abuse.rfc-ignorant.org',0.1,0,'abuse.rfc-ignorant.org' ); # skip a RBL if this RBL had this many continuous errors $BL_ERROR_SKIP = 2; # skip a RBL for that many times $BL_SKIP_RELEASE = 10; ## cache stuff # must be a directory (add trailing slash) $LOCKPATH = '/var/run/policyd-weight/'; # socket path for the cache daemon. $SPATH = $LOCKPATH.'/polw.sock'; # how many seconds the cache may be idle before starting maintenance routines #NOTE: standard maintenance jobs happen regardless of this setting. $MAXIDLECACHE = 60; # after this number of requests do following maintenance jobs: checking for config changes $MAINTENANCE_LEVEL = 5; # negative (i.e. SPAM) result cache settings ################################## # set to 0 to disable caching for spam results. To this level the cache will be cleaned. $CACHESIZE = 2000; # at this number of entries cleanup takes place $CACHEMAXSIZE = 4000; $CACHEREJECTMSG = '550 temporarily blocked because of previous errors'; # after NTTL retries the cache entry is deleted $NTTL = 1; # client MUST NOT retry within this seconds in order to decrease TTL counter $NTIME = 30; # positve (i.,e. HAM) result cache settings ################################### # set to 0 to disable caching of HAM. To this number of entries the cache will be cleaned $POSCACHESIZE = 1000; # at this number of entries cleanup takes place $POSCACHEMAXSIZE = 2000; $POSCACHEMSG = 'using cached result'; #after PTTL requests the HAM entry must succeed one time the RBL checks again $PTTL = 60; # after $PTIME in HAM Cache the client must pass one time the RBL checks again. #Values must be nonfractal. Accepted time-units: s, m, h, d $PTIME = '3h'; # The client must pass this time the RBL checks in order to be listed as hard-HAM # After this time the client will pass immediately for PTTL within PTIME $TEMP_PTIME = '1d'; ## DNS settings # Retries for ONE DNS-Lookup $DNS_RETRIES = 1; # Retry-interval for ONE DNS-Lookup $DNS_RETRY_IVAL = 5; # max error count for unresponded queries in a complete policy query $MAXDNSERR = 3; $MAXDNSERRMSG = 'passed - too many local DNS-errors'; # persistent udp connection for DNS queries. #broken in Net::DNS version 0.51. Works with Net::DNS 0.53; DEFAULT: off $PUDP= 0; # Force the usage of Net::DNS for RBL lookups. # Normally policyd-weight tries to use a faster RBL lookup routine instead of Net::DNS $USE_NET_DNS = 0; # A list of space separated NS IPs # This overrides resolv.conf settings # Example: $NS = '1.2.3.4 1.2.3.5'; # DEFAULT: empty $NS = ''; # timeout for receiving from cache instance $IPC_TIMEOUT = 2; # If set to 1 policyd-weight closes connections to smtpd clients in order to avoid too many #established connections to one policyd-weight child $TRY_BALANCE = 0; # scores for checks, WARNING: they may manipulate eachother # or be factors for other scores. # HIT score, MISS Score @client_ip_eq_helo_score = (1.5, -1.25 ); @helo_score = (1.5, -2 ); @helo_score = (0, -2 ); @helo_from_mx_eq_ip_score= (1.5, -3.1 ); @helo_numeric_score= (2.5, 0 ); @from_match_regex_verified_helo= (1,-2 ); @from_match_regex_unverified_helo = (1.6, -1.5 ); @from_match_regex_failed_helo = (2.5, 0 ); @helo_seems_dialup = (1.5, 0 ); @failed_helo_seems_dialup= (2, 0 ); @helo_ip_in_client_subnet= (0,-1.2 ); @helo_ip_in_cl16_subnet = (0,-0.41 ); #@client_seems_dialup_score = (3.75, 0 ); @client_seems_dialup_score = (0, 0 ); @from_multiparted = (1.09, 0 ); @from_anon= (1.17, 0 ); @bogus_mx_score = (2.1, 0 ); @random_sender_score = (0.25, 0 ); @rhsbl_penalty_score = (3.1, 0 ); @enforce_dyndns_score = (3, 0 ); $VERBOSE = 0; # Switch on or off an additional # X-policyd-weight: header # DEFAULT: on $ADD_X_HEADER = 1; # Fallback response in case the weighted check didn't return any response (should never appear). $DEFAULT_RESPONSE = 'DUNNO default'; # # Syslogging options for verbose mode and for fatal errors. # NOTE: comment out the $syslog_socktype line if syslogging does not # work on your system. # # inet, unix, stream, console $syslog_socktype = 'unix'; $syslog_facility = "mail"; $syslog_options = "pid"; $syslog_priority = "info"; $syslog_ident = "postfix/policyd-weight"; # # Process Options # # User must be a username, no UID $USER = "polw"; # specify GROUP if necessary # DEFAULT: empty, will be initialized as $USER $GROUP = "polw"; # Upper limit if child processes $MAX_PROC = 5; # keep that minimum processes alive $MIN_PROC = 1; # The TCP port on which policyd-weight listens for policy requests from postfix $TCP_PORT = 12525; # IP-Address on which policyd-weight will listen for requests. # You may only list ONE IP here, if you want# to listen on all IPs you need to say 'all' here. # Default is '127.0.0.1'. # You need to restart policyd-weight if you change this. $BIND_ADDRESS = '127.0.0.1'; # Maximum of client connections policyd-weight accepts # Default: 1024 $SOMAXCONN = 128; # how many seconds a child may be idle before it dies. $CHILDIDLE = 30; $PIDFILE= "/var/run/policyd-weight.pid"; ``` ## Spamassassin SpamAssassin in this case means both SpamAssassin itself and ClamAV as well, since we're going to use a plugin to get ClamAV to check for viruses in the spam filtering process as well. ### install SpamAssassin ~~and ClamAV~~ ``` {.bash} apt-get install spamassassin spamc ``` ~~To fire up the ClamAV plugin, Spamassassin[^10] already have a Wiki page for.~~ ## Afterwords This is the base of my current mail system, and it's doing it's job quite well, but I don't this it would stand as a very large traffic mailserver without some additional tweaks, for example, compiling the spamassassin rules[^11], and so on. That's for another day. [^1]: [^2]: [^3]: [^4]: [^5]: [^6]: [^7]: [^8]: [^9]: [^10]: [^11]: