Created
November 12, 2015 19:18
-
-
Save wraithgar/b0babc62d9fbf5d84c40 to your computer and use it in GitHub Desktop.
Postfix maildir policy server.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
############################################## | |
# Postfix policy server # | |
# postfix-policyd-quota # | |
# v1.01 # | |
# by Michael Garvin <gar@comrade.us> # | |
# Queries user's maildirquota, and rejects # | |
# if they are over. Takes no action # | |
# (DUNNO) otherwise. # | |
# Must be able to read ~/Maildir/maildirsize # | |
# # | |
# borrowed heavily from postfix-policy-spf # | |
# see http://spf.pobox.com/ for original # | |
############################################## | |
#12/30/2004 1.0 | |
# Initial Release | |
#1/03/2004 1.01 | |
# Added verbose condition checking to syslog statements | |
# Added explicit close to MAILDIRQUOTA on else to $quota > 0 | |
# Changed syslog level "warn" to "warning" in places, as "warn" is invalid | |
#1/10/2004 1.1 | |
# Decided to include the recipient_map lookup code in the publicly | |
# available source | |
use Fcntl; | |
use Sys::Syslog qw(:DEFAULT setlogsock); | |
use strict; | |
$ENV{PATH} = "/bin:/usr/sbin"; #suid protection | |
# ---------------------------------------------------------- | |
# configuration | |
# ---------------------------------------------------------- | |
my @HANDLERS; | |
push @HANDLERS, "maildirquota_check"; | |
my $VERBOSE = 0; | |
my $DEFAULT_RESPONSE = "DUNNO"; | |
# | |
# Syslogging options for verbose mode and for fatal errors. | |
# NOTE: comment out the $syslog_socktype line if syslogging does not | |
# work on your system. | |
# | |
my $syslog_socktype = 'unix'; # inet, unix, stream, console | |
my $syslog_facility = "mail"; | |
my $syslog_options = "pid"; | |
my $syslog_priority = "info"; | |
my $syslog_ident = "postfix/policy-quota"; | |
# ---------------------------------------------------------- | |
# minimal documentation | |
# ---------------------------------------------------------- | |
# | |
# Usage: quota-policy.pl [-v] | |
# | |
# Demo delegated Postfix SMTPD policy server. | |
# This server implements Maildirquota. | |
# Another server implements greylisting. | |
# Another server implements SPF. | |
# Postfix has a pluggable policy server architecture. | |
# You can call one or both from Postfix. | |
# | |
# This documentation assumes you have read Postfix's README_FILES/SMTPD_POLICY_README | |
# | |
# Logging is sent to syslogd. | |
# | |
# How it works: each time a Postfix SMTP server process is started | |
# it connects to the policy service socket, and Postfix runs one | |
# instance of this PERL script. By default, a Postfix SMTP server | |
# process terminates after 100 seconds of idle time, or after serving | |
# 100 clients. Thus, the cost of starting this PERL script is smoothed | |
# out over time. | |
# | |
# To run this from /etc/postfix/master.cf: | |
# | |
# policy unix - n n - - spawn | |
# user=nobody argv=/usr/bin/perl /usr/libexec/postfix/smtpd-policy.pl | |
# | |
# To use this from Postfix SMTPD, use in /etc/postfix/main.cf: | |
# | |
# smtpd_recipient_restrictions = | |
# ... | |
# reject_unauth_destination | |
# check_policy_service unix:private/policy | |
# ... | |
# | |
# NOTE: specify check_policy_service AFTER reject_unauth_destination | |
# or else your system can become an open relay. | |
# | |
# To test this script by hand, execute: | |
# | |
# % perl smtpd-policy.pl | |
# | |
# Each query is a bunch of attributes. Order does not matter, and | |
# the demo script uses only a few of all the attributes shown below: | |
# | |
# request=smtpd_access_policy | |
# protocol_state=RCPT | |
# protocol_name=SMTP | |
# helo_name=some.domain.tld | |
# queue_id=8045F2AB23 | |
# sender=foo@bar.tld | |
# recipient=bar@foo.tld | |
# client_address=1.2.3.4 | |
# client_name=another.domain.tld | |
# [empty line] | |
# | |
# The policy server script will answer in the same style, with an | |
# attribute list followed by a empty line: | |
# | |
# action=dunno | |
# [empty line] | |
# | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: client_address=208.210.125.227 | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: client_name=newbabe.mengwong.com | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: helo_name=newbabe.mengwong.com | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: protocol_name=ESMTP | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: protocol_state=RCPT | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: queue_id= | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: recipient=mengwong@dumbo.pobox.com | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: request=smtpd_access_policy | |
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: sender=mengwong@newbabe.mengwong.com | |
# ---------------------------------------------------------- | |
# initialization | |
# ---------------------------------------------------------- | |
# | |
# Log an error and abort. | |
# | |
sub fatal_exit { | |
syslog(err => "fatal_exit: @_"); | |
syslog(warning => "fatal_exit: @_"); | |
syslog(info => "fatal_exit: @_"); | |
die "fatal: @_"; | |
} | |
# | |
# Unbuffer standard output. | |
# | |
select((select(STDOUT), $| = 1)[0]); | |
# | |
# This process runs as a daemon, so it can't log to a terminal. Use | |
# syslog so that people can actually see our messages. | |
# | |
setlogsock $syslog_socktype; | |
openlog $syslog_ident, $syslog_options, $syslog_facility; | |
# ---------------------------------------------------------- | |
# main | |
# ---------------------------------------------------------- | |
# | |
# Receive a bunch of attributes, evaluate the policy, send the result. | |
# | |
my %attr; | |
while (<STDIN>) { | |
chomp; | |
if (/=/) { my ($k, $v) = split (/=/, $_, 2); $attr{$k} = $v; next } | |
elsif (length) { syslog(warning=>sprintf("warning: ignoring garbage: %.100s", $_)); next; } | |
if ($VERBOSE) { | |
for (sort keys %attr) { | |
syslog(debug=> "Attribute: %s=%s", $_, $attr{$_}); | |
} | |
} | |
fatal_exit ("unrecognized request type: '$attr{request}'") unless $attr{request} eq "smtpd_access_policy"; | |
my $action = $DEFAULT_RESPONSE; | |
my %responses; | |
foreach my $handler (@HANDLERS) { | |
no strict 'refs'; | |
my $response = $handler->(attr=>\%attr); | |
if ($VERBOSE) { | |
syslog(debug=> "handler %s: %s", $handler, $response); | |
} | |
if ($response and $response !~ /^dunno/i) { | |
syslog(info=> "handler %s: %s is decisive.", $handler, $response); | |
$action = $response; last; | |
} | |
} | |
if ($VERBOSE) { | |
syslog(info=> "decided action=%s", $action); | |
} | |
#THIS IS THE ACTUAL ACTION GETTING REPORTED TO POSTFIX | |
print STDOUT "action=$action\n\n"; | |
%attr = (); | |
} | |
# ---------------------------------------------------------- | |
# plugin: maildirquota | |
# ---------------------------------------------------------- | |
sub maildirquota_check { | |
local %_ = @_; | |
my %attr = %{ $_{attr} }; | |
my ($smtp_comment,$header_comment,$result,$lookup_loop,$lookup_result); | |
my $local_hostname = "mail.comrade.us"; | |
if ($attr{recipient}) { | |
if ($attr{recipient} =~ /^([A-Za-z0-9@._\-]+)$/) { | |
$attr{recipient} = $1; # $data now untainted | |
} else { | |
syslog(warning=> "Bad data in %s", $attr{recipient}); | |
return "DUNNO"; | |
} | |
} else { | |
return "DUNNO"; | |
} | |
chomp $attr{recipient}; | |
my $recipient = $attr{recipient}; | |
#this loop is a primitive attempt at doing recipient lookups. | |
#you'll probably want to chuck it for your own code | |
# while ($lookup_loop != 1) { | |
#put all your virtual_alias_maps, etc here | |
my @MAPS; | |
push @MAPS, "ldap:ldapvirtual"; | |
my @MAPS_CA = @MAPS; | |
foreach my $map (@MAPS) { | |
if ($VERBOSE) { syslog(info=> "looking up %s with %s", $recipient, $map); } | |
$lookup_result = `/usr/sbin/postmap -c /etc/postfix -q $recipient $map`; | |
if ($VERBOSE) { syslog(info=> "found %s", $lookup_result); } | |
if ($lookup_result) { $recipient = $lookup_result; } | |
chomp $recipient; | |
} | |
$recipient =~ /[^@]+@(.*)/; | |
my $domain = $1; | |
if ($attr{recipient} == $recipient) { | |
foreach my $map (@MAPS_CA) { | |
if ($VERBOSE) { syslog(info=> "looking up @%s with %s", $domain, $map); } | |
$lookup_result = `postmap -c /etc/postfix -q \@$domain $map`; | |
if ($lookup_result) { $recipient = $lookup_result; } | |
chomp $recipient; | |
} | |
} | |
if ($recipient =~ /([^@]+)\@$local_hostname/) { | |
$recipient=$1; | |
# $lookup_loop=1; | |
# } elsif ($attr{recipient} == $recipient) { | |
# if ($VERBOSE) { syslog(info=> "Asked to lookup non-local %s", $recipient); } | |
# $lookup_loop=1; | |
# return "DUNNO"; | |
} | |
# } | |
$attr{recipient}=$recipient; | |
if (my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $homedir, $shell) = getpwnam($attr{recipient})) { | |
open (MAILDIRSIZE, $homedir."/Maildir/maildirsize"); | |
my $quota = <MAILDIRSIZE>; | |
$quota =~ s/[^0-9]//; | |
chomp $quota; | |
if ($quota > 0) { | |
my ($used,$entry); | |
while ($entry = <MAILDIRSIZE>) { | |
$entry =~ s/^[[:space:]]*//; | |
my @entry = split ("[[:space:]]+",$entry); | |
$used += $entry[0]; | |
} | |
close MAILDIRSIZE; | |
if ($used > $quota) { | |
$result = "fail"; $smtp_comment = "User over quota"; | |
} else { | |
$used += $attr{size}; | |
if ($used > $quota) { | |
$result = "fail"; $smtp_comment = "Message would put user over quota"; | |
} else { | |
$result = "pass"; | |
} | |
} | |
} else { | |
close MAILDIRSIZE; | |
} | |
} | |
if ($result eq "pass") { return "DUNNO"; } | |
elsif ($result eq "fail") { return "REJECT " . ($smtp_comment || $header_comment); } | |
elsif ($result eq "error") { return "450 temporary failure: $smtp_comment"; } | |
else { return "DUNNO"; } | |
# unknown, softfail, and none all return DUNNO | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment