postfix-quota is a little perl script which offers a service for mailbox-size checking to postfix. The script listens as a daemon on a TCP port and serves requests from postfix. The script answers with a ACTION code for postfix (like DUNNO). If the mailbox is over quota a 450 error is returned until the mailbox is 200% over limit. If more than 200% over limit the script returns a 550 (hard error) to postfix.
The script requires some modules/libraries. Furthermore mysql is expected as the quota is per user is requested from a mysql table
#!/usr/bin/perl # load necessary stuff 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 { # config for tcp socket # user/group MUST have read access on $self->{basedir} my $self = shift; $self->{server}->{port} = '127.0.0.1:20028'; $self->{server}->{user} = 'vmail'; $self->{server}->{group} = 'vmail'; $self->{server}->{pid_file} = '/tmp/size.pid'; $self->{server}->{setsid} = 1; $self->{basedir} = "/home/vmail/"; } ### process the request sub process_request { my $self = shift; while(my $line = <STDIN>) { # split request on whitespace # split the later part (mailaddress) on @ chomp($line); my @parts = split(' ',$line); my @values = split('@',$parts[1]); # non-valid request received, return DUNNO if(!defined $parts[0] || $parts[0] ne "get" || !defined $parts[1] || !defined $values[0] || !defined $values[1]) { print STDOUT "200 DUNNO\n"; next; } # $user and $domain my $user = $values[0]; my $domain = $values[1]; trim($user); trim($domain); # max size of mailbox my $sqlsize = quotaFromDB("$user\@$domain"); # instant DUNNO if quotaFromDB() returns an error OR mailbox size 0 # with 0 as mailbox size, users can be whitelisted from postfix-quota if (defined $sqlsize && $sqlsize == 0) { print STDOUT "200 DUNNO\n"; next; } # create path to users mailbox and call checkSize() upon that path my $dirsize = checksize($self->{basedir} . $domain. '/'. $user); if (defined $dirsize && defined $sqlsize) { # syslog message to mail.log # as every action is logged better keep it commented out to avoid performance issues or huge maillogs # syslog("info","Checking %s maildir size: define=%s, diskusage=%s", "$user\@$domain", format_byte($sqlsize), format_byte($dirsize)); # mailbox is overquota if ( $dirsize > $sqlsize ) { # compute relative usage of mailbox my $usage = (100 * $dirsize) / $sqlsize; $usage = sprintf("%.1f",$usage); # $sqlsize and $dirsize formated for output by format_byte() $sqlsize = format_byte($sqlsize); $dirsize = format_byte($dirsize); syslog("info","%s maildir overquota size: define=%s, diskusage=%s", "$user\@$domain", $sqlsize, $dirsize); # return to client that the mailbox is full # so postfix will deny mail for this user # up to 200% overquota a 450 error is returned, this tells the sending client # to try it again later. From 200% overquota the mail is rejected by a 550 hard error. # in this case the client must NOT try it again and has to send an error notification to the origin sender if ( $usage < 200 ) { print STDOUT "200 452 4.2.2 Mailbox full!! mailbox size: allowed=$sqlsize, used=$dirsize, usage=$usage%\n"; next; } else { print STDOUT "200 550 5.7.1 Mailbox full!! mailbox size: allowed=$sqlsize, used=$dirsize, usage=$usage%\n"; next; } } else { # NO overquota -> DUNNO print STDOUT "200 DUNNO\n"; next; } } else { # default answer DUNNO should ensure that postfix does not # receive block action due to an error print STDOUT "200 DUNNO\n"; next; } } } # get the quota for the user from mysql # returns the quota value sub quotaFromDB { my $user = $_[0]; my $sqlresult; trim($user); my $dbh = DBI->connect('DBI:mysql:postfix-quota:localhost', 'MYSQLUSER', 'MYSQLPASSWD', { 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; } } # removes whitespaces from start and tail of this string sub trim{ $_[0]=~s/^\s+//; $_[0]=~s/\s+$//; return; } # checks how much space is used in the filesystem by a mailbox sub checksize { my $diruser = $_[0]; trim($diruser); # get dirsize from filesystem # "du" is much faster than "find" # -b is important as it forces "du" to return the size in bytes and not kbytes my @size = split(' ',`du -sb $diruser`); if (defined $size[0]) { my $size = sprintf("%u",$size[0]); return $size; } return undef; } # formats a byte number to more readable fashion sub format_byte { my $number = shift; if($number >= 1000000000000){ return sprintf("%.2f TB", $number / 1000000000000); }elsif($number >= 1000000000){ return sprintf("%.2f GB", $number / 1000000000); }elsif($number >= 1000000){ return sprintf("%.2f MB", $number / 1000000); }elsif($number >= 1000){ return sprintf("%.2f KB", $number / 1000); }else{ return sprintf("%.2f Bytes", $number); } } 1;