Quotas

Now this is a feature that caused me quite some headache. But in the end it was pretty simple. Quotas are size limits for users. You can make sure that users do not waste infinite 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. And 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 = maildir:User quota

  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 maildir is the best choice in this context. The string “User quota” is just some random string that may be queried from a mail user agent.

The lines starting with “quota_status_…” set return values for the service that 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 a new 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 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 happily receive new emails and only later get rejected when talking to Dovecot via LMTP. 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?

  1. The sender will assume that the email was delivered while it is stuck in the queue for up to three days.
  2. 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 need 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:

  1. 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.
  2. 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 just a few KB…

# mysql mailserver
mysql> update virtual_users set quota=5000 where email='john@example.org';

…and send him a few test emails. 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.

You still have the 90-quota.conf open in your editor? Good. 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.

10 thoughts on “Quotas”

  1. Add imap_quota to
    /etc/dovecot/conf.d/20-imap.conf

    If you want the mailbox space indicator in Roundcube to work.

    Like this:
    mail_plugins = $mail_plugins imap_quota imap_sieve

  2. Thank you a lot for this tutorial. – Here is a small hint for roundcube admins

    Adding the following to /etc/postfix/main.cf
    smtpd_recipient_restrictions=reject_unauth_destination, (…)

    prevents roundcube users from sending e-mail to external e-mail-adresses,
    if roundcube’s default_host and smtp_server are still set to ‘localhost’
    (in roundcube’s config.inc.php). This was the case in my old configuration.

    Following your recommendation in the subpage “Webmail using Roundcube” of this tutorial
    I set the default_host value and especially the smtp_server value to my server
    (like this: tls://webmail.example.org) in roundcube’s config.inc.php, and that was the solution.

    Additionally, I had to set smtp_user (%u) and smtp_pass (%p) in roundcube’s config.inc.php.

    On the other hand, smtpd_recipient_restrictions=reject_unauth_destination could be left out
    in postfix’s main.cf, but the above configuration seems more secure to me.

    Best regards, Florian

    1. smtpd_relay_restrictions (Postfix > 2.10) are evaluated BEFORE smtpd_recipient_restrictions. So there’s no need to specify a reject in “smtpd_recipient_restrictions”.

      smtpd_recipient_restrictions is empty in a fresh installation whereas smtpd_relay_restrictions already contain:
      permit_mynetworks
      permit_sasl_authenticated
      defer_unauth_destination

      Thus adding reject_unauth_destination or defer_unauth_destination in smtpd_recipient_restrictions may result in rejected mails from authenticated users.

  3. Until I prepended smtpd_recipient_restrictions with permit_mynetworks, server was refusing to relay from unauthenticated mynetworks hosts to elsewhere with “554 5.7.1 Relay access denied”. I had to use “debug_peer_list” to discover that it was the recipient restriction affecting this…

    Is there a better way?

    1. Christoph Haas

      I had expected that the mail.log gives you a hint. As in:

      Oct 2 20:01:55 tron postfix/smtpd[574618]: NOQUEUE: reject: RCPT from unknown[·…]: 550 5.1.1 <…>: Recipient address rejected: …

      1. All I got was the “NOQUEUE: reject: RCPT from … Relay access denied” so it wasn’t clear why, no indication it was about a failed recipient check.

        It’s only thanks to the debug_peer_list log details that I figured it was caused by the smtpd_recipient_restrictions being evaluated, logging this:

        Oct 2 18:53:59 mail postfix/smtpd[1485]: >>> START Recipient address RESTRICTIONS <<>> END Recipient address RESTRICTIONS <<>> END Recipient address RESTRICTIONS <<<

        1. aww… lost content.

          Before:

          Oct 2 18:53:59 mail postfix/smtpd[1485]: generic_checks: name=reject_unauth_destination status=2

          After:

          Oct 2 19:38:41 mail postfix/smtpd[1690]: generic_checks: name=permit_mynetworks status=1

  4. Niccolo Belli

    I’m getting the following when I go out of quota:

    Sep 2 11:44:20 mail postfix/smtpd[1623]: NOQUEUE: reject: RCPT from mail-wm1-x335.google.com[2a00:1450:4864:20::335]: 452 4.2.2 : Recipient address rejected: Mailbox is full and cannot receive any more emails; from= to= proto=ESMTP helo=
    Sep 2 11:44:20 mail postfix/smtpd[1623]: disconnect from mail-wm1-x335.google.com[2a00:1450:4864:20::335] ehlo=2 starttls=1 mail=1 rcpt=0/1 bdat=0/1 quit=1 commands=5/7
    Sep 2 11:44:25 mail dovecot: imap-login: Login: user=, method=PLAIN, rip=127.0.0.1, lip=127.0.1.1, mpid=1628, TLS, session=
    Sep 2 11:44:36 mail dovecot: imap(): Fatal: block_alloc(268435456): Out of memory
    Sep 2 11:44:36 mail dovecot: imap(): Error: Raw backtrace: /usr/lib/dovecot/libdovecot.so.0(backtrace_append+0x42) [0x7f22e69084e2] -> /usr/lib/dovecot/libdovecot.so.0(backtrace_get+0x1e) [0x7f22e69085fe] -> /usr/lib/dovecot/libdovecot.so.0(+0xfc49b) [0x7f22e691449b] -> /usr/lib/dovecot/libdovecot.so.0(+0xfc531) [0x7f22e6914531] -> /usr/lib/dovecot/libdovecot.so.0(+0x53cd8) [0x7f22e686bcd8] -> /usr/lib/dovecot/libdovecot.so.0(+0x57f3d) [0x7f22e686ff3d] -> /usr/lib/dovecot/libdovecot.so.0(pool_alloconly_create+0x58) [0x7f22e69335e8] -> /usr/lib/dovecot/libdovecot-storage.so.0(maildir_uidlist_refresh+0x8fc) [0x7f22e6a6975c] -> /usr/lib/dovecot/libdovecot-storage.so.0(maildir_uidlist_get_mailbox_guid+0x55) [0x7f22e6a6a465] -> /usr/lib/dovecot/libdovecot-storage.so.0(+0x87298) [0x7f22e6a63298] -> /usr/lib/dovecot/libdovecot-storage.so.0(mailbox_get_metadata+0x46) [0x7f22e6a3b396] -> /usr/lib/dovecot/libdovecot-storage.so.0(+0xbb32b) [0x7f22e6a9732b] -> /usr/lib/dovecot/libdovecot-storage.so.0(+0xbb998) [0x7f22e6a97998] -> /usr/lib/dovecot/libdovecot-storage.so.0(mailbox_list_index_status_sync_deinit+0xda) [0x7f22e6a984ca] -> /usr/lib/dovecot/libdovecot-storage.so.0(+0xbdcd4) [0x7f22e6a99cd4] -> /usr/lib/dovecot/modules/lib10_quota_plugin.so(+0x1194b) [0x7f22e662c94b] -> /usr/lib/dovecot/libdovecot-storage.so.0(mailbox_sync_deinit+0x37) [0x7f22e6a39617] -> /usr/lib/dovecot/libdovecot-storage.so.0(mailbox_sync+0x49) [0x7f22e6a3af19] -> /usr/lib/dovecot/libdovecot-storage.so.0(index_storage_get_status+0x33) [0x7f22e6ab3e43] -> /usr/lib/dovecot/modules/lib10_quota_plugin.so(+0x10d22) [0x7f22e662bd22] -> /usr/lib/dovecot/libdovecot-storage.so.0(mailbox_get_status+0x60) [0x7f22e6a3b310] -> dovecot/imap(imap_status_get+0xa3) [0x55a50c0dbb33] -> dovecot/imap(cmd_status+0x18e) [0x55a50c0cc44e] -> dovecot/imap(command_exec+0xa4) [0x55a50c0d2bc4] -> dovecot/imap(+0x1ebbf) [0x55a50c0d0bbf] -> dovecot/imap(+0x1ec6a) [0x55a50c0d0c6a] -> dovecot/imap(client_handle_input+0x1b5) [0x55a50c0d10e5] -> dovecot/imap(client_input+0x70) [0x55a50c0d1650] -> /usr/lib/dovecot/libdovecot.so.0(io_loop_call_io+0x69) [0x7f22e6929f59] -> /usr/lib/dovecot/libdovecot.so.0(io_loop_handler_run_internal+0x132) [0x7f22e692b592]
    Sep 2 11:44:36 mail dovecot: imap(): Fatal: master: service(imap): child 1628 returned error 83 (Out of memory (service imap { vsz_limit=256 MB }, you may need to increase it) – set CORE_OUTOFMEM=1 environment to get core dump)

    1. Niccolo Belli

      The disk is NOT full:
      # df -h
      Filesystem Size Used Avail Use% Mounted on
      udev 958M 0 958M 0% /dev
      tmpfs 195M 636K 195M 1% /run
      /dev/vda2 49G 47G 1.7G 97% /

      1. Niccolo Belli

        Turned out I had to put default_vsz_limit = 1024M into /etc/dovecot/conf.d/10-master.conf to increase the available memory for dovecot.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to Top