D. Postfix configuration 1. main.cf # A little shortcut to our query files query = sqlite:$config_directory/query # we use Mailman here, it maintains its own aliases alias_database=hash:/var/lib/mailman/aliases alias_maps = $query/maps-alias.query, $alias_database append_dot_mydomain = no # This is a Postfix 2.9 feature. This will have no effect in Postfix # 2.8 installs. enable_long_queue_ids = yes # We're doing the same thing with local and virtual delivery, delivering # to $HOME/Mail/ maildir. Dovecot likes consistency. home_mailbox = Mail/ html_directory = /usr/doc/postfix/html inet_protocols = ipv4 # uncomment this if you want to force all addresses into SQLite # local_recipient_maps = $query/maps-local.query mail_owner = postfix mailq_path = /usr/bin/mailq manpage_directory = /usr/man mydestination = $query/dom-local.query ### ### Note: myhostname is of crucial importance here. ### It is presumed that it is defined as a local domain (domClass 1) in ### the database. It also should be the system primary hostname, and the ### value of your reverse DNS PTR record. ### myhostname = myhostname.example.org mydomain = $myhostname # myorigin can be a cause of problems if misunderstood, as well. myorigin = $myhostname mynetworks = 127.0.0.0/8, 172.16.1.0/24, 192.168.0.0/20 newaliases_path = /usr/bin/newaliases # require explicit ".domain.tld" patterns when subdomain matching is # desired parent_domain_matches_subdomains = ### IMPORTANT ### # See the Postscreen README before enabling these options ### # The postscreen access file must exist, even if empty. postscreen_access_list = permit_mynetworks, cidr:/etc/postfix/postscreen_access.cidr postscreen_bare_newline_action = enforce postscreen_bare_newline_enable = yes postscreen_blacklist_action = drop postscreen_dnsbl_action = enforce # Likewise, this file must exist if defined. postscreen_dnsbl_reply_map = pcre:$config_directory/postscreen_dnsbl_reply_map.pcre postscreen_dnsbl_sites = zen.spamhaus.org*3 b.barracudacentral.org*2 bl.spameatingmonkey.net*2 dnsbl.njabl.org*2 dnsbl.ahbl.org*2 bl.spamcop.net dnsbl.sorbs.net spamtrap.trblspam.com swl.spamhaus.org*-4 list.dnswl.org=127.[0..255].[0..255].0*-2 list.dnswl.org=127.[0..255].[0..255].1*-4 list.dnswl.org=127.[0..255].[0..255].[2..255]*-6 postscreen_dnsbl_threshold = 3 postscreen_greet_action = enforce postscreen_non_smtp_command_enable = yes postscreen_pipelining_enable = yes # This is a Postfix 2.9 feature. .211 is my primary MX IP address, .214 # is the secondary. Both are bound on this host. We should never get # mail on the rest of the /29. postscreen_whitelist_interfaces = 192.0.2.211 !192.0.2.208/29 static:all queue_directory = /var/spool/postfix readme_directory = /usr/doc/postfix/README_FILES ### Recipient Delimiter ### # The Sendmail default is "+". The qmail default is "-". In general I # prefer to be positive rather than negative, but I have found many web # forms which stupidly insist that email addresses must not contain a # "+" character. Such is what happens when clueless people who don't # read standards are writing software. Sigh, I can't beat them, so I # joined them. recipient_delimiter = - ### IMPORTANT ### # If you change this, you must also change the maps-valias.query file # which has hardcoded the use of "-". It's also set in Dovecot's file, # dovecot/conf.d/15-lda.conf . # Unset relay_domains ("relay_domains =") and comment # relay_recipient_maps if not using relay domains. Although, if you # simply do not define any "domClass=2" in Domains, it is the same. relay_domains = $query/dom-relay.query relay_recipient_maps = $query/maps-relay.query sample_directory = /etc/postfix sendmail_path = /usr/sbin/sendmail setgid_group = postdrop # Look how simple this is! All the heavy lifting is done in one quick # sqlite query. smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination, check_recipient_access $query/access-rcpt.query # check_recipient_access $query/access-rcptDomain.query # the above is needed if using a catchall virtual alias smtpd_reject_footer = See your own postmaster for help, or http://nospam4.nodns4.us/ for more information about the policies of this site. # We only enabled SASL on port 587: MUAs and postscreen do not mix. # This setting is called from master.cf for 587. submission_rcpt_restrictions = permit_sasl_authenticated, permit_mynetworks, reject smtpd_sasl_path = private/auth smtpd_sasl_type = dovecot # Note: TLS configuration is not covered in this document; see here for # details: http://www.postfix.org/TLS_README.html smtpd_tls_CApath = /etc/ssl/certs smtpd_tls_cert_file = /etc/ssl/certs/myhostname.example.org.crt smtpd_tls_key_file = /etc/ssl/private/myhostname.example.org.key smtpd_tls_loglevel = 1 smtpd_tls_received_header = yes smtpd_tls_security_level = may smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache # If you are not going to use this feature, comment it transport_maps = $query/maps-transport.query unknown_address_reject_code = 550 virtual_alias_domains = $query/dom-valias.query virtual_alias_maps = $query/maps-valias.query virtual_gid_maps = $query/maps-vgid.query virtual_mailbox_base = /home virtual_mailbox_domains = $query/dom-vmbox.query virtual_mailbox_maps = $query/maps-vmbox.query virtual_minimum_uid = 800 virtual_uid_maps = $query/maps-vuid.query # restriction classes below # # NAMING SCHEME: # - ^RCdd-* Composite restriction choices for domain owners where # 'dd' is a numeric ranking, low=less spam, high=more spam # - ^z-* Component elements used to make up the RCdd-* classes # This keeps relevant parts together in the alphabetic-sorted output of # postconf(1), and the component elements collected at the end. # Note: it would have been nice if we could have kept this list and the # class definitions themselves in the SQLite database. I suspect there # are structural reasons why this would not work. I did originally try # keeping the classes and restrictions in SQLite, but since it had to be # dumped to main.cf anyway, it made the data larger and more confusing # for no benefit. smtpd_restriction_classes = z-swl,z-dnswlAll,z-dnswlLo,z-dnswlMed, z-dnswlHi,z-dnswlBLesp,z-zen,z-brbl,z-sem,z-ahbl,z-njabl, z-spamcop,z-sorbs,z-trbl,z-common,z-helo,z-noSender,z-unlist, z-semBscat,z-bscatOrg,z-bscat,z-dblC,z-dblH,z-dblS, z-noPtr,z-noFCrDns,z-unkHelo,z-warnDefer, RC00-reject, RC01-nuts,RC10-aggressive,RC20-moderate, RC29-whn,RC30-conservative,RC40-weak,RC50-open ### Composite classes # # Important note: these composite classes RCxx-* must be maintained in # two places: here and the database RClass table. The RClass table must # contain the same "xx" integer for the descriptive string as shown in # here, else trouble will ensue. # This is here to make our query easy. We could just return "reject" # in the query, but the tables store an integer for this purpose. RC00-reject = reject # The next five, in descending order of aggressiveness, are what are # presented to domain owners as their choices (although use of "weak" # and "open" are highly discouraged # # nuts: don't use this, it is nuts. You will block good mail. RC01-nuts = z-common,z-noFCrDns,z-unkHelo,z-dblC,z-dblH,z-dblS,z-brbl, z-sem,z-ahbl,z-njabl,z-trbl,z-sorbs,z-spamcop,z-bscat, z-dnswlBLesp # aggressive: when strong anti-spam policies are more important than # occasionally missing a legitimate email RC10-aggressive = z-common,z-noFCrDns,warn_if_reject,z-unkHelo, z-dblC,z-dblH,z-dblS,z-swl,z-dnswlHi,z-brbl,z-sem, z-dnswlMed,z-ahbl,z-njabl, z-dnswlAll,z-trbl,z-sorbs,z-spamcop,z-bscat RC20-moderate = z-common,z-noPtr,z-dblS,z-dblH,z-dblC, z-swl,z-dnswlLo,z-brbl,z-sem,z-ahbl,z-njabl,z-bscat # conservative: when more spam is considered better than the occasional # rejection of legitimate email RC30-conservative = z-common,warn_if_reject,z-noPtr, z-dblC,z-dblH,z-dblS,z-swl,z-dnswlAll,z-brbl,z-sem,z-ahbl, z-njabl,warn_if_reject,z-bscat # weak: does very little against spam RC40-weak = z-common # uncomment the following to get logs of what could have been # rejected: # warn_if_reject,z-noPtr,warn_if_reject,z-dblC, # warn_if_reject,z-dblH,warn_if_reject,z-dblS, # warn_if_reject,z-brbl,warn_if_reject,z-sem, # warn_if_reject,z-ahbl,warn_if_reject,z-njabl, # warn_if_reject,z-bscat # open: does nothing against spam. But do note, with postscreen in use, # this host cannot be truly "open". RC50-open = permit # a custom class chosen for a particular domain RC29-whn = z-common,z-noPtr,z-dblS,z-dblH,z-dblC,z-swl,z-dnswlAll, z-brbl,z-sem,z-ahbl,z-njabl,z-bscat ### Component classes ### Most of these will only do one thing, although some will call other ### components. z-ahbl = reject_rbl_client,dnsbl.ahbl.org z-brbl = reject_rbl_client,b.barracudacentral.org z-bscat = check_sender_access,$query/access-bscat.query z-bscatOrg = reject_rbl_client,ips.backscatterers.org z-common = z-helo,z-unlist,z-noSender,z-zen z-dblC = reject_rhsbl_client,dbl.spamhaus.org z-dblH = reject_rhsbl_helo,dbl.spamhaus.org z-dblS = reject_rhsbl_sender,dbl.spamhaus.org z-dnswlAll = permit_dnswl_client,list.dnswl.org ### ! using dnswl's code for ESP as a blacklist ! z-dnswlBLesp = reject_dnsbl_client,list.dnswl.org=127.[0..255].15.0 ### ! Kids, don't try that at home ! z-dnswlHi = permit_dnswl_client,list.dnswl.org=127.[0..255].[0..255].[3..255] z-dnswlLo = permit_dnswl_client,list.dnswl.org=127.[0..255].[0..255].[1..255] z-dnswlMed = permit_dnswl_client,list.dnswl.org=127.[0..255].[0..255].[2..255] z-helo = check_helo_access,pcre:$config_directory/helo_checks, z-njabl = reject_rbl_client,dnsbl.njabl.org z-noFCrDns = reject_unknown_client_hostname z-noPtr = reject_unknown_reverse_client_hostname z-noSender = reject_unknown_sender_domain, z-sem = reject_rbl_client,bl.spameatingmonkey.net z-semBscat = reject_rbl_client,backscatter.spameatingmonkey.net z-sorbs = reject_rbl_client,dnsbl.sorbs.net z-spamcop = reject_rbl_client,bl.spamcop.net z-swl = permit_dnswl_client,swl.spamhaus.org z-trbl = reject_rbl_client,spamtrap.trblspam.com z-unkHelo = reject_unknown_helo_hostname z-unlist = reject_unlisted_recipient z-warnDefer = check_client_access,static:warn,defer_if_reject z-zen = reject_rbl_client,zen.spamhaus.org ### end of main.cf 2. master.cf We're not adding anything here, but if you use any Transport.tpTransport value, it must exist in master.cf. Our sample data has one as "custom" and another as "throttled", so those would have to be added to master.cf. The sample main.cf shows postscreen in use. By default it is not, and it would need to be activated by editing master.cf. This document will not cover that, but rather just refer the reader to the Postfix Postscreen Howto: http://www.postfix.org/POSTSCREEN_README.html The sample main.cf also shows that SASL is not enabled. If you want to use SMTP AUTH, you should enable the submission port in your master.cf. Here is mine: submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes -o smtpd_recipient_restrictions=$submission_rcpt_restrictions -o milter_macro_daemon_name=ORIGINATING 3. Other files in /etc/postfix The example includes a helo_checks file and two postscreen_* files which you might find useful. Since they are not relevant to the goal of this document, they are not included here. See those files and the comments they contain if interested. 4. Query files (/etc/postfix/query) Naming scheme: the first token is one of "access", "dom", or "maps". This is the type of lookup: smtpd access(5) maps, domain lists, or address maps. Thus all access-*.query files are grouped together in a file listing, as are dom-*.query and maps-*.query files. a. Syntax summary There are three possible arguments within each file, given in standard postconf(5) "key = value" style. This section merely covers what has been used in the example implementation. For complete documentation, refer to SQLITE_README and the sqlite_table(5) manual. (Links are in the Bibliography at the end.) First, "dbpath", the pathname of the sqlite database. The file in this example implementation is /etc/postfix/private/mail.sqlite, where the /etc/postfix/private directory is root:postfix mode 750, to secure the sensitive data therein. Next, "query", which is the sqlite-validated SQL query to return the desired values. Within these queries, we use one or more of these three variables: %s Expands to the entire query string, the lookup key %u In an email address lookup of user@domain, expands to "user", the unqualified localpart, lefthand of the "@" %d In an email address lookup of user@domain, expands to "domain", the righthand side of the "@" If a query contains %u or %d, but the lookup key is NOT of the form, "user@domain", it is suppressed. Finally, "result_format", which has variables much like "query" does. We're only using it in two places, and we only use %s: %s Expands to the entire result string, the lookup value If omitted (as in most of these query files) result_format defaults to "%s". (In other words, the result is presented to Postfix exactly as it came from the query.) b. Access lookups This example only has two access lookups, but others could be added as needed/desired. The recipient access lookup is done for all arriving mail. The backscatter lookup is only invoked for moderate and above; conservative and below do not use it. *** access-bscat.query: dbpath = /etc/postfix/private/mail.sqlite query = SELECT ALL bsTarget FROM BScat WHERE lower(bsSender) IS '%s' ORDER BY bsPriority # This query is self-contained in the BScat table. If the sender address # matches one of the three suspicious characters, a restriction class is # returned which will cause the client IP address to be looked up # against backscatterer DNSBLs. *** access-rcpt.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT substr((100 + coalesce(A1.addRcNum, D1.domRcNum)), 2, 2) || '-' || (SELECT rcName FROM RClass WHERE rcNum=coalesce(A1.addRcNum, D1.domRcNum)) FROM Address AS A1 JOIN Domain AS D1 ON A1.addDomNum=D1.domNum WHERE A1.active!=0 AND D1.active!=0 AND A1.addLocalpart IS '%u' AND D1.domName IS '%d' # Postfix smtpd(8) access(5) lookups for email addresses # Case 1: smtpd searches for user-exten@domain # Case 2: smtpd searches for user@domain # Case 3: smtpd searches for domain (%u is suppressed) # This query will match without ever going to Case 3. If there is an # extension, it won't match unless user-exten@domain is in the Addresses # table. In most cases, it is not. # # We take the first non-null value from addRcNum then domRcNum. It's # padded with a leading zero if under 10 (we only have values from 0-50 # as rcNum.) We add a hyphen, then look up our number's rClassName in # the subquery. result_format = RC%s # BUG: This does not work for a catchall address. See # access-rcptDomain.query for the solution, or better yet, do not use # catchalls! *** access-rcptDomain.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT substr((100 + D1.domRcNum), 2, 2) || '-' || (SELECT rcName FROM RClass WHERE rcNum=D1.domRcNum) FROM Domain AS D1 WHERE D1.active!=0 AND D1.domName IS '%s' result_format = RC%s # BUG: access-rcpt.query does not work for a catchall address. This is # a solution, or for a better one: do not use catchalls! # # This query would need to be enabled at the end of your # smtpd_recipient_restrictions: # # smtpd_recipient_restrictions = permit_mynetworks, # reject_unauth_destination, # check_recipient_access $query/access-rcpt.query, # check_recipient_access $query/access-rcptDomain.query c. Domain lookups Each of these are very similar. They only differ in the domClass condition in the "WHERE" clause. *** dom-local.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT domName FROM Domain WHERE domClass=1 AND active!=0 AND domName IS '%s' *** dom-relay.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT domName FROM Domain WHERE domClass=2 AND active!=0 AND domName IS '%s' *** dom-valias.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT domName FROM Domain WHERE domClass=3 AND active!=0 AND domName IS '%s' *** dom-vmbox.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT domName FROM Domain WHERE domClass>800 AND active!=0 AND domName IS '%s' d. Maps (address) lookups These are not so similar as the domain lookups. Most of these files have their own comments, but in some cases further comment is needed. *** maps-alias.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT aliasTarget FROM Alias AS AL JOIN Address AS AD ON AL.aliasNum=AD.addNum JOIN Domain AS D1 ON AD.addDomNum=D1.domNum WHERE AD.addLocalpart IS '%s' AND D1.domName is 'myhostname.example.org' AND AL.active!=0; # Note the hardcoded value of $myhostname. *** maps-local.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT AD.addLocalpart FROM Address AS AD JOIN Domain AS D1 ON AD.addDomNum=D1.domNum WHERE AD.addLocalpart is '%s' AND D1.domName is 'myhostname.example.org' AND AD.active!=0; # This is an optional query for local_recipient_maps. I chose not to # deploy it for usability reasons, but this file is here as # demonstration that the one SQLite database can indeed manage all # Postfix lookups. # # This will work as-is with the sample data if the local_recipient_maps # line in main.cf is uncommented. To continue to use it, ensure that any # new local user is added to Addresses, and closed accounts are deleted. # # Note the hardcoded value of $myhostname, as in alias_maps. *** maps-relay.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT A1.addNum FROM Address AS A1 JOIN Domain AS D1 ON A1.addDomNum=D1.domNum WHERE A1.addLocalpart || '@' || D1.domName IS '%s' AND A1.active!=0 *** maps-transport.query is complex, so it deserves some extra comments. First, the SELECT clause is formatting the data the way Postfix needs it presented. Strings can be null, so coalesce() functions are used to replace null values with empty strings. The only part that is in all results is the colon between transport:nexthop. The first CASE expression encloses tpNexthop in square brackets if the tpMx boolean is set to 0 (false). This inhibits MX lookup of the tpNexthop name. The second CASE expression adds another colon and tpPort if that value is set. Finally, the tpNum value is looked up in a subquery. We take the first non-null result of a lookup of the address@domain or of the domain alone. Within the address@domain lookup, we take the first non-null of addTpNum (from the Address table) or domTpNum (from the Domain table.) This one is horribly ugly, but it can be translated with some generalizations: Select (a mess to make tpTransport, tpNexthop, tpMx and tpPort look the way Postfix wants it) from Transport where Transport.tpNum is (the first non-null result of either: (the first non-null result of either: "user@domain" lookup in Address, or "domName" lookup in Domain) or of the "domName" lookup in Domain) Whew. Charged with excessive use of coalesce(), I plead guilty. Without further ado, here is the file: dbpath = /etc/postfix/private/mail.sqlite query = SELECT coalesce(T1.tpTransport, '') || ':' || (CASE WHEN tpMx=0 THEN '[' || coalesce(T1.tpNexthop, '') || ']' ELSE coalesce(T1.tpNexthop, '') END) || (CASE WHEN tpPort IS NOT NULL THEN ':' || tpPort ELSE '' END) FROM Transport AS T1 WHERE T1.active!=0 AND T1.tpNum= coalesce((SELECT coalesce(A2.addTpNum, D2.domTpNum) FROM Address AS A2 JOIN Domain AS D2 ON A2.addDomNum=D2.domNum WHERE A2.addLocalpart || '@' || D2.domName IS '%s' AND A2.active!=0), (SELECT D1.domTpNum FROM Domain AS D1 WHERE D1.domName IS '%s' AND D1.active!=0)) # Postfix transport(5) lookups # Case 1: queries for user-exten@domain # Case 2: queries for user@domain # Case 3: queries for domain (%u is suppressed) # Case 4: queries for .domain (%u is suppressed) # Case 5: queries for wildcard "*" (%u is suppressed) # This query will match any listed address without ever going to # Case 3. If there is an extension, it won't match unless # user-exten@domain is in the Addresses table. # # Matched domains without any address listings will be matched in # case 3 or 4. We don't have a wildcard in the sample data, but # there's no reason why it wouldn't work too. # # Unmatched domains will be searched through all cases. *** maps-valias.query is better (I had some help on it, thanks, Frank.) The SELECT assembles the result email address[es] and optionally the delimiter and extension from a lookup of the aliased address in the VAlias table. Note the dual use of JOIN for each of Address and Domain tables. X1 are for the target address, and X2 for the alias. Here's the file: dbpath = /etc/postfix/private/mail.sqlite query = SELECT A1.addLocalpart || (case when V1.vaTargetExt is not null then '-' || V1.vaTargetExt else '' end) || '@' || D1.domName FROM VAlias AS V1 JOIN Address AS A1 ON (V1.vaTargetNum = A1.addNum) JOIN Domain AS D1 ON (A1.addDomNum = D1.domNum) JOIN Address AS A2 ON (V1.vaNum = A2.addNum) JOIN Domain AS D2 ON (A2.addDomNum = D2.domNum) WHERE A2.addLocalpart || '@' || D2.domName is '%s' AND V1.active!=0 # This query has the postconf value of $recipient_delimiter hardcoded: # '-' above. If you change that in main.cf you must change it here too! *** maps-vmbox.query returns the virtual mailbox location for virtual mailbox addresses. If vmHome is defined, that value is used; otherwise a default is constructed as "vmail/domain/user" for a user@domain lookup. "/Mail/" is appended to any result. Note, with virtual_mailbox_base of /home, any values of vmHome must be under /home, and the virtual_{u,g}id_maps user or group must have the necessary rwx permission to enter, read and write the maildir. The appendage of "/Mail/" is an inconvenience when delivering virtual mail to a local user; it means that the local user must either receive the mail in his/her regular inbox, or else make a symlink to an IMAP folder name under ~/Mail/. Here is the file: dbpath = /etc/postfix/private/mail.sqlite query = SELECT coalesce(VM.vmHome, 'vmail/%d/%u') FROM VMailbox AS VM JOIN Address AS A1 ON VM.vmNum=A1.addNum JOIN Domain AS D1 ON A1.addDomNum=D1.domNum WHERE A1.addLocalpart || '@' || D1.domName IS '%s' AND VM.active!=0 result_format = %s/Mail/ *** maps-vgid.query : dbpath = /etc/postfix/private/mail.sqlite query = SELECT coalesce(VM.vmGid, 800) FROM VMailbox AS VM JOIN Address AS A1 ON VM.vmNum=A1.addNum JOIN Domain AS D1 ON A1.addDomNum=D1.domNum WHERE A1.addLocalpart || '@' || D1.domName IS '%s' AND VM.active!=0 # Most howtos for Postfix virtual mailboxes, including the official # VIRTUAL_README document, use static maps for UID and GID: # virtual_uid_maps = static:5000 # virtual_gid_maps = static:5000 # This is easiest for usability, but exposes a potential security # weakness in that an exploit of the imapd or the virtual user/group # could lead to compromise or loss of all mailboxes. # # Here for the GID query, we use a default shared GID which owns and has # g+rwx privilege on /home/vmail, under which is where we deliver mail # to %d/%u/Mail/ maildirs. This means that virtual(8) can create new # mailboxes when mail arrives. *** maps-vuid.query (sorry for the mostly repetitive comments): dbpath = /etc/postfix/private/mail.sqlite query = SELECT coalesce(VM.vmUid, D1.domClass) FROM VMailbox AS VM JOIN Address AS A1 ON VM.vmNum=A1.addNum JOIN Domain AS D1 ON A1.addDomNum=D1.domNum WHERE A1.addLocalpart || '@' || D1.domName IS '%s' AND VM.active!=0 # Most howtos for Postfix virtual mailboxes, including the official # VIRTUAL_README document, use static maps for UID and GID: # virtual_uid_maps = static:5000 # virtual_gid_maps = static:5000 # This is easiest for usability, but exposes a potential security # weakness in that an exploit of the imapd or the virtual user/group # could lead to compromise or loss of all mailboxes. # # Here for the UID query, we use a default UID shared by all domains # under control of the same domSysUser. This UID owns and has u+rwx # privilege on /home/vmail/%d/, under which we deliver mail to %u/Mail/ # maildirs. This means that virtual(8) can create new mailboxes when # mail arrives.