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 “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 to just a few KB and send him several test emails using swaks. After a few emails you will see the rejection message:

-> RCPT TO:john@example.org
 <** 552 5.2.2 john@example.org: Recipient address rejected: Mailbox is full and cannot receive any more emails

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.

Automatic warning emails

The last step is to inform the poor users if they accidentally went over quota. After all they do not necessarily reccognize 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
   quota_warning3 = -storage=100%% quota-warning below %u
}
service quota-warning {
   executable = script /usr/local/bin/quota-warning.sh
   unix_listener quota-warning {
     group = dovecot
     mode = 0660
   }
 }

This section defines three automatic quota warnings. The first (quota_warning) is triggered if the user reaches 95% of the quota. The second (quota_warning2) at 80%. And the third if the user had reached 100% but has removed enough emails to have gone below 100%. 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

Recalculate quota

If you directly remove files from a user’s Maildir instead of properly accessing the mailbox using IMAP then you will mix up the quota calculation. To force Dovecot to recalculate the quota you will have to run:

doveadm quota recalc -u john@example.org

50 thoughts on “Quotas”

  1. Florian Effenberger

    I think (!) the proper error code for temporary rejection due to quota is

    quota_status_overquota = “452 4.2.2 Mailbox is full and cannot receive any more emails”

    Maybe add this for completeness. 🙂

      1. Christoph Haas

        The RFC seems to refer to “disk full” with 4.2.2 responses but not “user over quota”. However a 4.x.x error seems to be more appropriate because the error is not necessarily permanent and retrying is a good idea. I’ll fix that.

        1. How about: “552 Requested mail action aborted: exceeded storage allocation”?

          Also, I am wondering about the other code “4.2.2”, what does that refer to?

          1. Ignore me; realised that’d be understood as a permanent error…

  2. Adon Irani

    Dovecot was failing for me until I move the { onto the same line as unix_listener: doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/90-quota.conf line 95: Expecting ‘{‘

    This worked:
    service quota-status {
    executable = /usr/lib/dovecot/quota-status -p postfix
    unix_listener /var/spool/postfix/private/quota-status {
    user = postfix
    }
    }

          1. Honestly I don’t think so – if its really “Bytes” then it should be 1073741824…. and with an Int(11) in the database the maximum value would be 2147483648 which means it would only support around 2 GB…. nowadays that’s not that much if you imagine how much storage you could get on free mail hoster. Wouldn’t it be biter (if I’m not wrong) to change either the database structure to bigint or to use kilobytes ?

  3. Hi Christoph,

    I have a problem. With smtpd_recipient_restrictions = reject_unauth_destination I can send email to my virtual domain only. If I try to send mail to external domain, I receive a relay denied, even if I put my source ip in mynetworks variable.
    It’s resolvible? Thank You so much

    1. I have the exact same problem.
      Could it be possible that the smtpd_recipient_restrictions should be empty or commented out like in the relaying-through-postfix chapter it is said? Or that the smtpd_relay_restrictions replaces the smtpd_recipient_restrictions?

      1. I was wrong. The trick is to remove the ‘#’ in front of ‘-o smtpd_recipient_restrictions=’ in the submission section of /etc/postfix/master.cf. After that you can send out e-mails to other domains/mail-servers.
        The only question now is if you can remove the standard ‘smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination’ in /etc/postfix/main.cf, because there is ‘smtpd_recipient_restrictions = reject_unauth_destination check_policy_service unix:private/quota-status’ or if you can remove the latter one and set ‘smtpd_relay_restrictions’ to ‘smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination check_policy_service unix:private/quota-status’? I dont think that you need both of them or at least only one of them needs a *_unauth_destination option in it.

    2. Hi,

      I think the solution is to only have

      smtpd_recipient_restrictions = check_policy_service unix:private/quota-status

      From http://www.postfix.org/postconf.5.html#smtpd_recipient_restrictions:
      Optional restrictions that the Postfix SMTP server applies in the context of a client RCPT TO command, after smtpd_relay_restrictions. See SMTPD_ACCESS_README, section “Delayed evaluation of SMTP access restriction lists” for a discussion of evaluation context and time.

      With this, the reject_unauth_destination is not present anymore. Relaying is already stopped by smtpd_relay_restrictions, so I think there should be no harm done there.

      Best regards
      Christof

  4. The quota-warning service did not work for me:
    lmtp(john@example.org): Error: program unix:/var/run/dovecot/quota-warning: net_connect_unix(/var/run/dovecot/quota-warning) failed: Permission denied (euid=5000(vmail) egid=5000(vmail) missing +r perm: /var/run/dovecot/quota-warning, dir owned by 0:0 mode=0755)

    I needed to switch to ‘group = vmail’ in ‘service quota-warning’.

    Also I like to use two additional quota rules, one for additional space in trash and another one to limit the number of emails:
    quota_rule2 = Trash:storage=+100M
    quota_rule3 = *:messages=100000

  5. Hi,

    I changed the “mail_plugins” line in /etc/dovecot/conf.d/10-mail.conf
    from
    mail_plugins = quota
    to
    mail_plugins = quota imap_quota

    The result is, that I can see the quota setting of a Mailbox and the current usage of it in IMAP Clients (e.g. Thunderbird). Is there any reason why this setting shoud not be used?

    1. Correction:
      I had to add “imap_quota” to 20-imap.conf.
      So the “mail_plugins” line in /etc/dovecot/conf.d/10-mail.conf stays
      mail_plugins = quota
      I added the following line to to /etc/dovecot/conf.d/to 20-imap.conf:
      mail_plugins = $mail_plugins imap_quota
      in the “protocol imap” section.

    2. I can confirm that this added the little pie chart quota on the bottom of roundcube and also shows the quota on mail clients. Thanks Christian.

  6. Hello,

    I have tested this attractive solution, but alias does not work because Postfix does alias map too late when using “check_policy_service” (you can check it enabling verbose mode with “smtpd -v”).

    So only original “TO: ” address is used, i.e. alias, which is unknown for Dovecot.

    Tinkering Dovecot to include alias is not the solution because it’s the job of Postfix.

    Does anyone have a solution?

    1. Hello,

      I confirm the issues of k049873. I have the same here. A soon as I have in main.cf the line
      “smtpd_recipient_restrictions = reject_unauth_destination check_policy_service unix:private/quota-status” the server does not accept virtual aliases emails anymore and reject them with “Recipient address rejected: Unknown user;”
      The only way to get it to work, change it to the “smtpd_recipient_restrictions = reject_unauth_destination ”

      Any idea, how to get quota to work?

      1. I finally got it to work. Quota was not setup correctly. Now dovecot still shows “dovecot: auth: unknown user”, but the emails are delivered correctly for aliases.

        1. Timothy Kenno Handojo

          Hi,

          I don’t know if you still need the solution, but here it is.

          I had the same problem. I solved it by following the ‘Optional: Catch-all aliases’ section on ‘Making Postfix get its information …’ chapter.

          Apparently, the user still needs mapping to itself to be recognized.

          I hope the author can help with some clarification.

  7. Enrique Sardon

    if anyone wants to get the quota information of all mailboxes, you will get the following error:

    $ doveadm quota get -A
    Error: User listing returned failure
    doveadm: Error: Failed to iterate through some users
    Username Quota name Type Value Limit %

    to fix that, you sould edit the /etc/dovecot/dovecot-sql.conf.ext

    and add this line at the end of the file:
    iterate_query = SELECT email AS user FROM virtual_users

    1. That is the last line of text to be added to the bottom of the /etc/dovecot/dovecot-sql.conf.ext file as presented on the “Setting up Dovecot” page of the tutorial.

  8. If anyone is following this guide to configure a mail server on Centos 8, you need to replace
    executable = /usr/lib/dovecot/quota-status -p postfix
    with
    executable = /usr/libexec/dovecot/quota-status -p postfix

    1. I’m using this guide on Slackware and had exactly the same issue.

      “Fatal: service(quota-status) access(/usr/lib/dovecot/quota-status) failed: No such file or directory”

      executable = /usr/libexec/dovecot/quota-status -p postfix fixed this problem for me too

  9. You set:

    quota_status_overquota = “452 4.2.2 Mailbox is full and cannot receive any more emails”

    yet in your test example you get a completely different error code:

    <** 552 5.2.2 john@example.org: Recipient address rejected: Mailbox is full and cannot receive any more emails

    Googling some SMTP error codes, it seems that 552 5.2.2 is preferred for this case, even Dovecot uses it in its example at https://doc.dovecot.org/configuration_manual/quota_plugin/, so that's what I'm going to use.

  10. Hi!
    given the fact that ” check_policy_service unix” comes last:

    postconf “smtpd_recipient_restrictions = \
    reject_unauth_destination \
    check_policy_service unix:private/quota-status”

    What would happen if the policy server returns “action=DEFER_IF_PERMIT Temporary internal error” . Will the email be REJECTed ?

  11. Does this service provide status in realtime ? Is there any cron or config that updates the users quotas ?

    1. Christoph Haas

      Dovecot understands shortcuts like 5G. But we are using a BIGINT field in this guide. So you would have to convert 5 GB to bytes.

  12. koji.morikawa

    We use warning emails to notify you of quotas.

    In rare cases, the email address has changed to the logged-in user.
    So I couldn’t send the email with “Recipient address rejected: User unknown in local recipient table”.
    why?

  13. The part where postfix checks for quota isn’t working. If I check dovecots status using systemctl status dovecot it shows that it rejected the mail because of the quota but swaks still manages to send the email. swaks command is: swaks –to john@example.org –server 127.0.0.1

  14. Horst Schymaniuk

    Did I miss something? Where do I set the quota Limit (10GB or 5GB)? And is this limit per User? For the whole server? Cant figure it out with the Examples in 90-quota.conf

  15. Trying to add: “Postfix recipient restrictions” to /etc/dovecot/conf.d/90-quota.conf from this page.

    doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/90-quota.conf line 106: Expecting ‘{‘
    doveconf: Error: managesieve-login: dump-capability process returned 89
    doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/90-quota.conf line 106: Expecting ‘{‘

    Error message that I receive:

    doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/90-quota.conf line 106: Expecting ‘{‘
    doveconf: Error: managesieve-login: dump-capability process returned 89
    doveconf: Fatal: Error in configuration file /etc/dovecot/conf.d/90-quota.conf line 106: Expecting ‘{‘

    I have tryed moving { every place, no go. I thought I would be a good idea if I just asked for help..

    ~bob~
    Debian Buster

    1. Oops

      postconf “smtpd_recipient_restrictions = \
      reject_unauth_destination \
      check_policy_service unix:private/quota-status”

  16. If you need mail client to be able to get quota informations you should also enable imap_quota plugin in 20-imap.conf

    /etc/conf.d/20-imap.conf:

    protocol imap {
    # Space separated list of plugins to load (default is global mail_plugins).
    mail_plugins = $mail_plugins imap_quota imap_sieve

  17. The part where postfix checks for quota isn’t working and swaks still manages to send the email.
    Did I miss something?

    1. Christoph Haas

      Hi Ilaria. I have observed something similar. It seems that if you send an email internally to “localhost” with swaks then it does not trigger the quota check. I have to admit that I used swaks from another location and that worked well. In the process of the Bullseye guide (yes, I’m on it) I have taken a lot of notes about that and will have to double-check that.

  18. Why is there a minus sign next to the ‘storage’ in this line?

    > quota_warning3 = -storage=100%% quota-warning below %u

Leave a Reply

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

Scroll to Top