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.