Skip to content

Instantly share code, notes, and snippets.

@wraithgar
Created November 12, 2015 19:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wraithgar/b0babc62d9fbf5d84c40 to your computer and use it in GitHub Desktop.
Save wraithgar/b0babc62d9fbf5d84c40 to your computer and use it in GitHub Desktop.
Postfix maildir policy server.
#!/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