Postfix does not have built-in quota inspection feature. i’ve seen people on mailing list asking how to check maildir usage on the fly. Add-ons such as VDA , vda inspect quota after email was accepted. When someone over quota, they will bounce mail, Just imagine, when bad guys sent lots of email to overdrawn account with fake return address/sender envelope, on purpose. Our mail server will be backscatter source. spamming undelivered message to innocent people.
Maildrop and Dovecot can handle quota better, but still they’re all inspect maildir usage after accepting the mail. And likely they will bounce email after the first inspect overquota maildir. Ideally, sender should be rejected at smtp conversation time. RCPT TO stage will perfect place for inspecting recipient maildir usage. Before postfix introducing tcp_table, the best solutions was creating map for overquota user. this can be done by using script by querying user quota constant specified in database, then compared to usage in maildirsize file or maildir disk usage.
I wrote this simple perl script, has functions to inspect user quota specified in database, and maildir disk usage. it runs as daemon. it’s not perfect. the script lack of ability when dealing with email address alias or email address extension. Just keep in mind tcp_table connection is not protected and the server is not authenticated.
There are two main functions which are used in this script. checksqlsize and checksize.
checksqlsize is used to check user quota specified in the database. you can adjust parameters in the script as needed,
sub checksqlsize { my $user = $_[0]; my $sqlresult; trim($user); my $dbh = DBI->connect('DBI:mysql:postfixdb:localhost', 'user', 'password', { RaiseError => 1 }); my $sth = $dbh->prepare(qq{SELECT quota FROM mailbox WHERE username='$user'}); $sth->execute(); while (my @row = $sth->fetchrow_array) { $sqlresult = $row[0]; } $sth->finish(); $dbh->disconnect; if ($sqlresult >= 0 ) { return $sqlresult; } else { return undef; } }
checksize is used to check amount of bytes that have been in use maildir.
sub checksize { my $diruser = $_[0]; trim($diruser); my $size; find(sub{ -f and ( $size += -s ) }, $diruser ); if (defined $size) { $size = sprintf("%u",$size); return $size; } return undef; }
Complete script will be like this, save it as quota.pl or you can name it as you wish.
#!/usr/bin/perl use File::Find; use strict; use warnings; use DBI; use DBD::mysql; use Sys::Syslog qw(:DEFAULT setlogsock); use base qw(Net::Server::PreFork); # # Initalize and open syslog. # openlog('postfix-quota::','pid','mail'); __PACKAGE__->run; exit; ### sub configure_hook { my $self = shift; $self->{server}->{port} = '127.0.0.1:20028'; $self->{server}->{user} = 'postfix'; $self->{server}->{group} = 'postfix'; $self->{server}->{pid_file} = '/tmp/size.pid'; $self->{server}->{setsid} = 1; $self->{basedir} = "/path/to/postfix/maildir/"; } ### process the request sub process_request { my $self = shift; while(my $line = <STDIN>) { chomp($line); if ($line=~/^get\s+(.+)/i) { my $user = $1; trim($user); my $sqlsize = checksqlsize($user); if (defined $sqlsize && $sqlsize == 0) { print STDOUT "200 DUNNO\n"; next; } my $usrdirsize = $user; $usrdirsize =~ s/\@example\.com$/\//; my $dir = $self->{basedir} . $usrdirsize; my $dirsize = checksize($dir); if (defined $dirsize && defined $sqlsize) { syslog("info","Checking %s maildir size: define=%s, diskusage=%s", $user, $sqlsize, $dirsize); if ( $dirsize > $sqlsize ) { print STDOUT "200 452 4.2.2 $user is over quota! maildir size: define=$sqlsize, diskusage=$dirsize\n"; next; } } } print STDOUT "200 DUNNO\n"; } } sub trim{ $_[0]=~s/^\s+//; $_[0]=~s/\s+$//; return; } sub checksize { my $diruser = $_[0]; trim($diruser); my $size; find(sub{ -f and ( $size += -s ) }, $diruser ); if (defined $size) { $size = sprintf("%u",$size); return $size; } return undef; } sub checksqlsize { my $user = $_[0]; my $sqlresult; trim($user); my $dbh = DBI->connect('DBI:mysql:postfixdb:localhost', 'user', 'password', { RaiseError => 1 }); my $sth = $dbh->prepare(qq{SELECT quota FROM mailbox WHERE username='$user'}); $sth->execute(); while (my @row = $sth->fetchrow_array) { $sqlresult = $row[0]; } $sth->finish(); $dbh->disconnect; if ($sqlresult >= 0 ) { return $sqlresult; } else { return undef; } } 1;
In my test environment i’m using maildir path like this:
/path/to/postfix/maildir/
maildir are username part, without domain.
example:
/path/to/postfix/kutukupret/
maildir has user and group permission set to “postfix”.
the script run on localhost port 20028 or whatever number you would like to specified.
$self->{server}->{port} = '127.0.0.1:20028'; $self->{server}->{user} = 'postfix'; $self->{server}->{group} = 'postfix'; $self->{server}->{pid_file} = '/tmp/quota.pid'; $self->{server}->{setsid} = 1; $self->{basedir} = "/path/to/postfix/maildir/";
Run it from shell
# ./quota.pl
I reproduced overquota user condition for testing the script, testing can be done directly by telneting localhost 20028.
# telnet localhost 20028 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. get test@example.com 200 REJECT test@example.com is over quota! maildir size: define=1024000, diskusage=1728398
Now, Let’s put it alltogether
In main.cf somewhere in smtpd_recipient_restrictions add this check_recipient_access
smtpd_recipient_restrictions = check_recipient_access tcp:[127.0.0.1]:20028, permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, ..... .....
Reload postfix, check it from real world
# telnet mx.example.com 25 Trying xxx.xxx.xx.xxx... Connected to xxx.xxx.xx.xxx. Escape character is '^]'. 220 mx.example.com ehlo mx.example.org 250-mx1.csmcom.com 250-SIZE 5242880 250-STARTTLS 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN PLAIN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250 DSN mail from:<me@example.org> 250 2.1.0 Ok rcpt to:<test@example.com> 554-5.7.1 <test@example.com>: Recipient address rejected: test@example.com is over quota! maildir size: define=1024000, diskusage=1728398 554-5.7.1 For assistance, send email to support@help.example.com. 554-5.7.1 Please provide the following information in your problem report: 554 5.7.1 time (May 16 09:38:40), client address (xxx.xxx.xxx.xxx) and server (mx.example.com). quit 221 2.0.0 Bye Connection closed by foreign host.
Use it with caution, because the script will hammering the disk alot since it will do the probing frequently.
Victor says you should use different error code:
452 4.2.2 Mailbox full
http://tech.groups.yahoo.com/group/postfix-users/message/270402
Otherwise great approach. We are using similar approach with perl Quota module 😉
hmm, yes you’re right. sender must have a chance to retry. good point. 🙂
Hi and thanks for all! I’ve a question: on my system I use postfix and courier with virtual users. Quota of each email should be manage to postfix or pop/imap server? (or both?)
thanks
Pol
how did you store your virtual user? if you are using sql database you can do it with postfixadmin, it’s support quota entry. the database used by both postfix virtual user and courier.