Optional
Quotas are size limits for users. You can make sure that users do not waste arbitrary amounts of disk space but are forced to clean up old emails every now and then.
The magic happens in two places:
- Postfix needs to reject new emails if the user’s mailbox is over quota.
- Dovecot needs to keep track of the quota and how much the user has already used up of it.
Dovecot quota policy service
Let’s start with Dovecot. Find the file /etc/dovecot/conf.d/90-quota.conf
and edit it. There are several “plugin {…}” sections. Take one and make it look like:
plugin { quota = count:User quota quota_vsizes = yes quota_status_success = DUNNO quota_status_nouser = DUNNO quota_status_overquota = "452 4.2.2 Mailbox is full and cannot receive any more emails" }
The first line defines that you want to calculate the used space in a user’s maildir. There are several backends like that but the count is the best choice in this context. (Previous guides used maildir here.) The string “User quota” is just an arbitrary string that may be queried from a mail user agent.
The lines starting with “quota_status_…
” set return values for the service that you will set up in a minute. It will tell Postfix that it will not interfere (DUNNO – colloquial way to say “I don’t know”). And it will return a string with a return code 452 if the user is over quota. Codes starting with “4” mean temporary errors. It will tell the sending party that it is worth retrying at a later time. However if the user does not resolve the issue it will lead to a bounce error email after three days.
In the same file (90-quota.conf) add another section:
service quota-status { executable = /usr/lib/dovecot/quota-status -p postfix unix_listener /var/spool/postfix/private/quota-status { user = postfix } }
This creates a new Dovecot service responding to requests from other processes. You surely recognize that we put it into the jail that Postfix runs in (/var/spool/postfix), so that Postfix can access it.
Time to restart Dovecot:
systemctl restart dovecot
Take a look at the /var/spool/postfix/private directory. If all went as intended you will find a socket file called quota-status
there. Otherwise please check the /var/log/mail.log
file for errors.
Postfix recipient restrictions
If we stopped here, then Dovecot would reject emails for users who have no space left. However Postfix would still happily receive new emails and attempt to forward them to Dovecot via LMTP. Dovecot however will deny that. It will then keep the email in its queue and retry for a while. In the end it will send a bounce back to the sender telling them about the problem. So why is this bad?
- The sender will assume that the email was delivered while it is stuck in the queue for up to three days.
- Spam emails use forged senders. So at the time that Postfix generates the bounce email it will likely send it to an innocent person. This is called backscatter and considered a mail server misconfiguration. Such a problem may get your mail server blacklisted. You don’t want that.
So the next logical step is to make Postfix check whether a mailbox is over quota whenever a new email arrives. Let’s hook up into the “RCPT TO” phase of the SMTP dialog when a new email comes in. Postfix checks its smtpd_recipient_restrictions configuration at this stage. Run this command in the shell:
postconf smtpd_recipient_restrictions=reject_unauth_destination,"check_policy_service unix:private/quota-status"
This adds two checks:
reject_unauth_destination
checks whether the mail server is the final destination for the recipient’s email address. This is pretty much the default behavior if you do not define any restrictions.check_policy_service
connects to the socket file at/var/spool/postfix/private/quota-status
that was put there by Dovecot. It will use it to ask Dovecot whether the user is over quota in which case the email would get rejected.
Test it
If you are curious to see this working, then set John’s mailbox quota to 5 KB:
# mysql mailserver mysql> update virtual_users set quota=4000 where email='john@example.org';
Send him a few emails using the ‘swaks’ tool:
swaks --server localhost --to john@example.org
After a few emails you will see the rejection message:
-> RCPT TO:john@example.org <** 452 4.2.2 john@example.org: Recipient address rejected: Mailbox is full and cannot receive any more emails
Troubleshooting
These are things you should consider if quotas do not seem to work properly:
- Check if you have enabled “quota” in the “mail_plugins” in the 10-mail.conf file.
- Your users may complain that they have deleted many emails but are still over quota. Let them check if they actually emptied the Trash folder. Of course emails in that folder also contribute to the disk space usage. Once the Trash folder is expunged the problem should be gone. You may also allow your users more space in the Trash folder. That’s explained in the Dovecot documentation.
- If you directly remove files from a user’s Maildir instead of properly accessing the mailbox using IMAP then you will screw up the quota calculation. In that case let Dovecot recalculate the quota:
“doveadm quota recalc -u john@example.org”
Automatic warning emails
The last step is to inform the poor users if they accidentally went over quota. After all they do not necessarily recognize that on their own. Let’s do that by sending them an email with a warning. Yes, we will make sure that the email gets through even if the quota is reached.
Edit the 90-quota.conf
file again. Add this section to the file (derived from the Dovecot documentation):
plugin { quota_warning = storage=95%% quota-warning 95 %u quota_warning2 = storage=80%% quota-warning 80 %u } service quota-warning { executable = script /usr/local/bin/quota-warning.sh unix_listener quota-warning { user = vmail group = vmail mode = 0660 } }
This section defines two automatic quota warnings. The first (quota_warning) is triggered if the user reaches 95% of the quota. The second (quota_warning2) at 80%. These lines follow this schema:
- Trigger (e.g. “storage=95%”). The “%” sign needs to be used twice if you want to emit a literal percent sign. So this is not a typo.
- The socket you want to call in that case. Our socket is the “service quota-warning” that calls a shell script.
- Additional parameters that are passed to the shell script in our case. They tell the script the percentage that has been reached (e.g. 95) and the address of the user who should get the warning.
Apparently we need the script to run. So please create a new file at /usr/local/bin/quota-warning.sh
and put these lines into it:
#!/bin/sh PERCENT=$1 USER=$2 cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing" From: postmaster@webmail.example.org Subject: Quota warning - $PERCENT% reached Your mailbox can only store a limited amount of emails. Currently it is $PERCENT% full. If you reach 100% then new emails cannot be stored. Thanks for your understanding. EOF
Make this file executable:
chmod +x /usr/local/bin/quota-warning.sh
Time to restart Dovecot again:
systemctl restart dovecot
Dovecot’s quota limits can be configured in many ways. If you have special needs then give their documentation a look.
Hi again,
I am also having an issue in this part as well… I have done everything but when I use swaks instead of
‘Recipient address rejected: Mailbox is full and cannot receive any more emails’
I get a response
‘Recipient address rejected: User unknown in local recipient table’
What all trouble shooting can I do for this.
“local” almost always refers to your local/canonical domains. Make sure that your don’t have that domain listed in $mydestinaion
Hi,
Thank you for your reply on the ‘/var/log/mail.log’ comment. Your solution for that (apt install rsyslog) worked. After install this I re-ran the swaks command and checked the mail.log file, looks like you were right. There is a warning in mail.log to not list my domain in BOTH mydestination and virtual_mailbox_domains. Removing my domain from mydestination worked.
Thanks a lot.
When I perform the swaks test to fill John’s mailbox, the system continues to accept submissions (no rejection message) and the bounce messages are going to root. What’s up?
Never mind. It seems to have fixed itself.
Hello, when I send multiple test emails to john@example.org I don’t get the same error code. However, when I change the quota size to 1kb, it does send me the correct error code. When I check the mail logs, I can see that dovecot is reporting the quota error when my setting is at 5kb, but postfix does not seem to note the error. I checked all my files and everything is supposed to be where it is. What could be the issue here?
Hi, same behavior here. I set the quota to 4000 and no error shown in swaks, but dovecots rejects de message and postfix bounces it:
Sep 01 14:13:14 mail01 postfix/smtpd[23030]: connect from localhost[127.0.0.1]
Sep 01 14:13:14 mail01 postfix/smtpd[23030]: 4B8A64035E: client=localhost[127.0.0.1]
Sep 01 14:13:14 mail01 postfix/cleanup[23037]: 4B8A64035E: message-id=
Sep 01 14:13:14 mail01 postfix/smtpd[23030]: disconnect from localhost[127.0.0.1] ehlo=1 mail=1 rcpt=1 data=1 quit=1 commands=5
Sep 01 14:13:14 mail01 postfix/qmgr[22881]: 4B8A64035E: from=, size=419, nrcpt=1 (queue active)
Sep 01 14:13:14 mail01 dovecot[23018]: lmtp(23039): Connect from local
Sep 01 14:13:14 mail01 dovecot[23018]: lmtp(javier@jaalsa.me): msgid=: save failed to INBOX: Quota exceeded (mailbox for user is full)
Sep 01 14:13:14 mail01 dovecot[23018]: lmtp(23039): Disconnect from local: Logged out (state=READY)
Sep 01 14:13:14 mail01 postfix/lmtp[23038]: 4B8A64035E: to=, relay=mail01[private/dovecot-lmtp], delay=0.12, delays=0.08/0.01/0.02/0, dsn=5.2.2, status=bounced (host mail01[private/dovecot-lmtp] said: 552 5.2.2 Quota exceeded (mailbox for user is full) (in reply to end of DATA command))
Sep 01 14:13:14 mail01 postfix/cleanup[23037]: 5AB6D4132A: message-id=
Sep 01 14:13:14 mail01 postfix/bounce[23040]: 4B8A64035E: sender non-delivery notification: 5AB6D4132A
Sep 01 14:13:14 mail01 postfix/qmgr[22881]: 5AB6D4132A: from=, size=2376, nrcpt=1 (queue active)
Sep 01 14:13:14 mail01 postfix/qmgr[22881]: 4B8A64035E: removed
Sep 01 14:13:14 mail01 postfix/local[23041]: 5AB6D4132A: to=, relay=local, delay=0.02, delays=0/0.01/0/0, dsn=2.0.0, status=sent (delivered to mailbox)
Sep 01 14:13:14 mail01 postfix/qmgr[22881]: 5AB6D4132A: removed
Errors start appearing in swaks when the quota is smaller than circa 3500 bytes.
Did you clarify what was going on?
Umm, to test the quota warnings i raised the limit from 4000 to 6000, to be below 80%, and I got both warning emails and then the reject in swaks. I then emptied the mailbox with mutt and started sending againg test emails and a reject ended up coming with no bounces.
Good, but I don’t know why :).
The SQL user query for dovecot doesn’t take Aliases into account.
To get also the quota from original account if only an alias exists (no virtual mailbox), the SQL query could be something like this:
user_query = SELECT ‘/var/vmail/%d/%n’ AS home, 5000 AS uid, 5000 AS gid, CONCAT(‘*:storage=’, quota,’M’) AS quota_rule FROM virtual_users WHERE email=’%u’ UNION SELECT ‘/var/vmail/%d/%n’ AS home, 5000 AS uid, 5000 AS gid, CONCAT(‘*:storage=’, quota,’M’) AS quota_rule FROM virtual_users WHERE email=(SELECT destination FROM virtual_aliases WHERE source = ‘%u’)
Or SELECT ‘/var/vmail/%d/%n’ AS home, 5000 AS uid, 5000 AS gid, CONCAT(‘*:storage=’, quota,’M’) AS quota_rule FROM virtual_users WHERE email=’%u’ OR email=(SELECT destination FROM virtual_aliases WHERE source = ‘%u’) LIMIT 1
… you know what I mean
Hi again,
After adding root@mx:~# swaks –server localhost –to john@janahome.eu in the terminal, I receive <** 450 4.7.1 : Recipient address rejected: Temporary internal error.
There is a /var/vmail atalog on the server.
mail.log
“2024-03-28T13:21:36.015144+01:00 mx postfix/smtpd[1969]: connect from localhost[::1]
2024-03-28T13:21:36.112407+01:00 mx dovecot: quota-status(1972): Error: Failed to lookup user john@janahome.eu: Namespace ”: mbox: mbox requires client_limit=1 for service
2024-03-28T13:21:36.112691+01:00 mx postfix/smtpd[1969]: NOQUEUE: reject: RCPT from localhost[::1]: 450 4.7.1 : Recipient address rejected: Temporary internal error; from= to= proto=ESMTP helo=
2024-03-28T13:21:36.112812+01:00 mx postfix/smtpd[1969]: disconnect from localhost[::1] ehlo=1 mail=1 rcpt=0/1 quit=1 commands=3/4″
Hey,
I have no idea if you are still looking for a solution, but I’ve encountered the same error as you and the thing that did it for me was to insert the line client_limit = 1 into service quota-status directive inside 90-quota.conf like this.
91 service quota-status {
92 client_limit = 1
93 executable = /usr/lib/dovecot/quota-status -p postfix
94 unix_listener /var/spool/postfix/private/quota-status {
95 user = postfix
96 }
97 }
Hope that helps, or anyone else who stumbled upon this error.
Hi thanks for the guide. I’m able to send and receive emails, but i dont see the quotas getting filled in the table? Did i miss something?
It works the other way round. You define a limit/quota in the database/table and emails get rejected once the quota is reached.
I thought that as soon as i posted 😅. I did have another question to clear my doubt. Port 25 is used to send emails between servers, and port 587 is used for clients to request email servers(port 25) to send a email. And because we have enabled sasl, postfix wont send emails unless the user exists in the database correct? Thanks in advance!
Thank you for your guide.
Until now everything has worked, except the things that I screwed up myself.
Some of the stuff because of my setup being a bit different than what you describe. (hiding behind reverse proxy, so certificates needed some adjustments).
Now I have a problem though.
I can send to john@ via swaks and the mail come through.
After some attempt the quota is reached and the mails are rejected.
But I don’t get any warning mails.
Will this fix itself when I stop using “example” as recipient?
Hi, I had the same problem just now, and I think it might be related to “plugin/quota=maildir:User quota:noenforcing” in the warning script, as the quota method was actually set to “count” instead of “maildir”. When changed to “count” I started getting the messages.
Not quite sure though, because I had some other issues (dovecot-lda path) along with that.