How to let postfix use dovecot to check quotas for virtual aliases
Hi all.
Receiving an email for an alias, I got those errors:
<alias@domain.tld>Recipient address rejected: Unknown user;
I realized this happened when postfix queries dovecot about user quota.
Basically, what happens is that postfix queries dovecot using the virutal alias as name, not the virtual user. And since dovecot doesn't find a user under that name, it returns an error and postfix rejects the mail.
Configuration
I followed the docs at https://doc.dovecot.org/latest/core/plugins/quota.html when setting up the server.
/etc/postfix/main.cf
smtpd_recipient_restrictions =
check_policy_service unix:private/quota-status
/etc/dovecot/conf.d/90-quota.conf
service quota-status {
executable = quota-status -p postfix
unix_listener /var/spool/postfix/private/quota-status {
user = postfix
}
client_limit = 1
}
/etc/dovecot/conf.d/auth-sql.conf.ext
userdb sql {
query = SELECT '/var/mail/vmail/'||maildir AS home, 110 AS uid, 113 AS gid, quota || 'B' AS quota_storage_size FROM mailbox WHERE username = '%{user}'
iterate_query = SELECT username FROM mailbox
}
Solution 1: modify user query
This issue was reported and analyzed here:
https://dovecot.dovecot.narkive.com/FtjhqCuU/postfix-aliases-with-quota-status-service
and there:
https://github.com/docker-mailserver/docker-mailserver/issues/2091
A comment (https://dovecot.dovecot.narkive.com/FtjhqCuU/postfix-aliases-with-quota-stat...) suggests to modify the SQL query to also look for aliases.
I modified my query like so:
query = SELECT '/var/mail/vmail/'||maildir AS home, 110 AS uid, 113 AS gid, quota || 'B' AS quota_storage_size FROM mailbox JOIN alias ON mailbox.username = alias.goto WHERE alias.address = '%{user}';
and it seemed to work.
I mean, I can query user quota with an alias:
doveadm quota get -u alias@domain.tld
but the iterate query ignores aliases so
doveadm quota get -A
only lists users, not aliases (which is what I want) and I can't IMAP login using aliases (which is also what I want) since I didn't modify the password query either.
I find this configuration slightly inconsistent: the user query returns aliases, not the iterate query. Is the user query really meant to return aliases as well?
Also, I realized this query is broken for alias to multiple addresses. I'd get the following error:
Recipient address rejected: Unknown user
Surprisingly, it seems to only affect messages from an external source to an alias pointing to an external mailbox. Messages submitted locally (and perhaps messages sent to an alias to local destinations only, I'm not sure) were not affected.
Solution 2: ignore aliases, it will fail later anyway
On another server, I had this configuration:
/etc/dovecot/conf.d $ vi 90-quota.conf
(Don't put this in plugin on dovecot 2.4+.)
plugin {
quota_status_success = DUNNO
quota_status_nouser = DUNNO
quota_status_overquota = "552 5.2.2 Mailbox is full"
}
The alias is unknown so the check returns DUNNO and postfix lets the message pass (postfix log read "delivered via spamassassin service") but then it fails at sieve stage when writing the message:
sieve: msgid=<xxx>: failed to store into mailbox 'INBOX': Quota exceeded (mailbox for user is full)
The result is what is expected: the message is rejected due to mailbox full. But I don't think it was intended to work this way. And since the message is quota-rejected at sieve stage, I could remove the check_policy_service line since it will be rejected for a virtual user (not alias) as well anyway. Right?
Question
What is the recommended configuration to let postfix use dovecot to check quotas for virtual aliases? I couldn't find documentation for this specific use case.
Ideally, postfix would query using the resolved user, not the alias. Can this be achieved?
Should I remove the check and rely on the fact that writing the message fails at a later step anyway?
Thanks.
(Question also posted on ServerFault: https://serverfault.com/questions/1197914/how-to-let-postfix-use-dovecot-to-...)
On 02/02/2026 21:46, jerome--- via dovecot wrote:
Hi all.
Receiving an email for an alias, I got those errors:
<alias@domain.tld>Recipient address rejected: Unknown user;I realized this happened when postfix queries dovecot about user quota.
Basically, what happens is that postfix queries dovecot using the virutal alias as name, not the virtual user. And since dovecot doesn't find a user under that name, it returns an error and postfix rejects the mail.
Configuration
I followed the docs at https://doc.dovecot.org/latest/core/plugins/quota.html when setting up the server.
/etc/postfix/main.cf
smtpd_recipient_restrictions = check_policy_service unix:private/quota-status/etc/dovecot/conf.d/90-quota.conf
service quota-status { executable = quota-status -p postfix unix_listener /var/spool/postfix/private/quota-status { user = postfix } client_limit = 1 }/etc/dovecot/conf.d/auth-sql.conf.ext
userdb sql { query = SELECT '/var/mail/vmail/'||maildir AS home, 110 AS uid, 113 AS gid, quota || 'B' AS quota_storage_size FROM mailbox WHERE username = '%{user}' iterate_query = SELECT username FROM mailbox }Solution 1: modify user query
This issue was reported and analyzed here:
https://dovecot.dovecot.narkive.com/FtjhqCuU/postfix-aliases-with-quota-status-serviceand there:
https://github.com/docker-mailserver/docker-mailserver/issues/2091A comment (https://dovecot.dovecot.narkive.com/FtjhqCuU/postfix-aliases-with-quota-stat...) suggests to modify the SQL query to also look for aliases.
I modified my query like so:
query = SELECT '/var/mail/vmail/'||maildir AS home, 110 AS uid, 113 AS gid, quota || 'B' AS quota_storage_size FROM mailbox JOIN alias ON mailbox.username = alias.goto WHERE alias.address = '%{user}';and it seemed to work.
I mean, I can query user quota with an alias:
doveadm quota get -u alias@domain.tldbut the iterate query ignores aliases so
doveadm quota get -Aonly lists users, not aliases (which is what I want) and I can't IMAP login using aliases (which is also what I want) since I didn't modify the password query either.
I find this configuration slightly inconsistent: the user query returns aliases, not the iterate query. Is the user query really meant to return aliases as well?
Also, I realized this query is broken for alias to multiple addresses. I'd get the following error:
Recipient address rejected: Unknown userSurprisingly, it seems to only affect messages from an external source to an alias pointing to an external mailbox. Messages submitted locally (and perhaps messages sent to an alias to local destinations only, I'm not sure) were not affected.
Solution 2: ignore aliases, it will fail later anyway
On another server, I had this configuration:
/etc/dovecot/conf.d $ vi 90-quota.conf
(Don't put this in
pluginon dovecot 2.4+.)plugin { quota_status_success = DUNNO quota_status_nouser = DUNNO quota_status_overquota = "552 5.2.2 Mailbox is full" }The alias is unknown so the check returns DUNNO and postfix lets the message pass (postfix log read "delivered via spamassassin service") but then it fails at sieve stage when writing the message:
sieve: msgid=<xxx>: failed to store into mailbox 'INBOX': Quota exceeded (mailbox for user is full)The result is what is expected: the message is rejected due to mailbox full. But I don't think it was intended to work this way. And since the message is quota-rejected at sieve stage, I could remove the
check_policy_serviceline since it will be rejected for a virtual user (not alias) as well anyway. Right?Question
What is the recommended configuration to let postfix use dovecot to check quotas for virtual aliases? I couldn't find documentation for this specific use case.
Ideally, postfix would query using the resolved user, not the alias. Can this be achieved?
Should I remove the check and rely on the fact that writing the message fails at a later step anyway?
Thanks.
(Question also posted on ServerFault: https://serverfault.com/questions/1197914/how-to-let-postfix-use-dovecot-to-...)
dovecot mailing list -- dovecot@dovecot.org To unsubscribe send an email to dovecot-leave@dovecot.org
Hi
unfortunately I don't think there is a satsifactory way to solve it for all cases. As you already noticed there are some workarounds by adjusting the dovecot user query to take aliases into account, but the general case cannot be managed just by configuration (e.g. aliases going to multiple mailboxes) and the idea of an overquota alias is probably an undefined or poorly defined concept.
So I guess the logic behind the dovecot quota status policy service (which can be configured to reply DUNNO for addresses it knows nothing about) is that it will enable reject on over quota mailboxes (which is a bit safer than bouncing in case the sender address is forged), but for aliases it leaves the decision to after the email has been accepted requiring it to be bounced if necessary.
I think whether to use the quota status policy service with it's standard configuration in the end boils down to whether you get a lot of over quota cases and whether those are for emails addresses that are not aliases.
I personally prefer to keep alias processing in postfix and leave dovecot just to deliver to mailboxes, without tweaking the dovecot query to allow alias lookup, since it won't solve all cases anyway. I realize though that it's just a preference, not a reason to avoid if it works for you.
John
Hi.
Thanks for anwsering. Things are much clearer now. I had the pieces, but couldn't get them together.
The part I overlooked was the interest of checking before delivery to avoid backscatter (reject vs. bounce).
Also, it wasn't obvious to me from the docs that the quota status policy service was an option, an optimization. I thought it was the "normal" way or enforcing quotas. And for some reason I thought the rejection that occurs in a later step was a side effect of sieve or whatever that happened to do the right thing, rather than the actual intended quota enforcement.
Thinking of it, it is clear to me that rejection can't always be achieved right away (e.g. external or multiple alias) so the DUNNO line is mandatory.
About the query, I could keep the modified one in a best effort to resolve aliases to get more rejections and less bounces. My initial question remains. Is it safe to do so? I mean is this query used in places where returning a virtual alias could be wrong and harmful?
Thanks.
-- Jérôme
On 03/02/2026 22:01, Jérôme via dovecot wrote:
Hi.
Thanks for anwsering. Things are much clearer now. I had the pieces, but couldn't get them together.
The part I overlooked was the interest of checking before delivery to avoid backscatter (reject vs. bounce).
Also, it wasn't obvious to me from the docs that the quota status policy service was an option, an optimization. I thought it was the "normal" way or enforcing quotas. And for some reason I thought the rejection that occurs in a later step was a side effect of sieve or whatever that happened to do the right thing, rather than the actual intended quota enforcement.
Thinking of it, it is clear to me that rejection can't always be achieved right away (e.g. external or multiple alias) so the DUNNO line is mandatory.
About the query, I could keep the modified one in a best effort to resolve aliases to get more rejections and less bounces. My initial question remains. Is it safe to do so? I mean is this query used in places where returning a virtual alias could be wrong and harmful?
Thanks.
Hi
yes, you could keep the modified query and get more rejections and less bounces. I don't see anything inherently unsafe about it, but I'll leave a definitive answer to whoever may be using it and has more experience to comment on it.
So basically what you are doing is alias expansion in dovecot (that may or may not be 100% the same as that done in postfix but worst case it's a subset) for quota checking of aliases which have a direct mapping to mailboxes and quota checking of mailboxes and then doing alias expansion in postfix and delivering to mailboxes. I think the unusual part of this configuration is that your query is common for quota status and presumably for lmtp delivery and also imap login. You managed the imap login part by not modifying the password query. For lmtp you are not actually ever calling this query with an alias (and even if you were it would probably work anyway). So I think it all logically fits together though the key point is to test it out before using it in case something has been overlooked.
One thing I can suggest if you want to converge towards a configuration that works similarly to how your old query does for lmtp and imap is to use the variable %s (2.3) or %{service} (2.4) in the query. It is populated to differentiate the service being called. This takes the value quota-status for quota checking. It should be possible to make a query that includes aliases only when called for quota-status and only mailboxes for service different to quota-status. This is an example of how I use it in a different context (to allow a different condition for smtp respect to all other services, that can disable email sending while allowing login and mail delivery etc.)
user_query = SELECT concat('/var/vmail/', maildir) as home, concat('maildir:/var/vmail/', maildir, 'mail/') as mail, 200 AS uid, 12 AS gid, concat('*:storage=', quota, 'K') as quota_rule FROM mailbox WHERE username = '%u' AND active = '1' AND (('%s' = 'smtp' AND smtp_active = '1') OR '%s' <> 'smtp')
If should be possible to make a query that returns values where user is a mailbox only when called for services different to quota-status and returns values where user is alias and mailboxes for quota-status.
John
participants (3)
-
jerome@jolimont.fr
-
John Fawcett
-
Jérôme