qpsmtpd/lib/Qpsmtpd/Postfix.pm
2013-08-05 15:05:15 -07:00

233 lines
5.9 KiB
Perl

package Qpsmtpd::Postfix;
=head1 NAME
Qpsmtpd::Postfix - postfix queueing support for qpsmtpd
=head2 DESCRIPTION
This package implements the protocol Postfix servers use to communicate
with each other. See src/global/rec_type.h in the postfix source for
details.
=cut
use strict;
use IO::Socket::UNIX;
use IO::Socket::INET;
use vars qw(@ISA);
@ISA = qw(IO::Socket::UNIX);
my %rec_types;
sub init {
my ($self) = @_;
%rec_types = (
REC_TYPE_SIZE => 'C', # first record, created by cleanup
REC_TYPE_TIME => 'T', # time stamp, required
REC_TYPE_FULL => 'F', # full name, optional
REC_TYPE_INSP => 'I', # inspector transport
REC_TYPE_FILT => 'L', # loop filter transport
REC_TYPE_FROM => 'S', # sender, required
REC_TYPE_DONE => 'D', # delivered recipient, optional
REC_TYPE_RCPT => 'R', # todo recipient, optional
REC_TYPE_ORCP => 'O', # original recipient, optional
REC_TYPE_WARN => 'W', # warning message time
REC_TYPE_ATTR => 'A', # named attribute for extensions
REC_TYPE_MESG => 'M', # start message records
REC_TYPE_CONT => 'L', # long data record
REC_TYPE_NORM => 'N', # normal data record
REC_TYPE_XTRA => 'X', # start extracted records
REC_TYPE_RRTO => 'r', # return-receipt, from headers
REC_TYPE_ERTO => 'e', # errors-to, from headers
REC_TYPE_PRIO => 'P', # priority
REC_TYPE_VERP => 'V', # VERP delimiters
REC_TYPE_END => 'E', # terminator, required
);
}
sub print_rec {
my ($self, $type, @list) = @_;
die "unknown record type" unless ($rec_types{$type});
$self->print($rec_types{$type});
# the length is a little endian base-128 number where each
# byte except the last has the high bit set:
my $s = "@list";
my $ln = length($s);
while ($ln >= 0x80) {
my $lnl = $ln & 0x7F;
$ln >>= 7;
$self->print(chr($lnl | 0x80));
}
$self->print(chr($ln));
$self->print($s);
}
sub print_rec_size {
my ($self, $content_size, $data_offset, $rcpt_count) = @_;
my $s =
sprintf("%15ld %15ld %15ld", $content_size, $data_offset, $rcpt_count);
$self->print_rec('REC_TYPE_SIZE', $s);
}
sub print_rec_time {
my ($self, $time) = @_;
$time = time() unless (defined($time));
my $s = sprintf("%d", $time);
$self->print_rec('REC_TYPE_TIME', $s);
}
sub open_cleanup {
my ($class, $socket) = @_;
my $self;
if ($socket =~ m#^(/.+)#) {
$socket = $1; # un-taint socket path
$self = IO::Socket::UNIX->new(Type => SOCK_STREAM,
Peer => $socket)
if $socket;
}
elsif ($socket =~ /(.*):(\d+)/) {
my ($host, $port) = ($1, $2); # un-taint address and port
$self = IO::Socket::INET->new(
Proto => 'tcp',
PeerAddr => $host,
PeerPort => $port
)
if $host and $port;
}
unless (ref $self) {
warn "Couldn't open \"$socket\": $!";
return;
}
# allow buffered writes
$self->autoflush(0);
bless($self, $class);
$self->init();
return $self;
}
sub print_attr {
my ($self, @kv) = @_;
for (@kv) {
$self->print("$_\0");
}
$self->print("\0");
}
sub get_attr {
my ($self) = @_;
local $/ = "\0";
my %kv;
for (; ;) {
my $k = $self->getline;
chomp($k);
last unless ($k);
my $v = $self->getline;
chomp($v);
$kv{$k} = $v;
}
return %kv;
}
=head2 print_msg_line($line)
print one line of a message to cleanup.
This removes any linefeed characters from the end of the line
and splits the line across several records if it is longer than
1024 chars.
=cut
sub print_msg_line {
my ($self, $line) = @_;
$line =~ s/\r?\n$//s;
# split into 1k chunks.
while (length($line) > 1024) {
my $s = substr($line, 0, 1024);
$line = substr($line, 1024);
$self->print_rec('REC_TYPE_CONT', $s);
}
$self->print_rec('REC_TYPE_NORM', $line);
}
=head2 inject_mail($transaction)
(class method) inject mail in $transaction into postfix queue via cleanup.
$transaction is supposed to be a Qpsmtpd::Transaction object.
=cut
sub inject_mail {
my ($class, $transaction) = @_;
my @sockets = @{$transaction->notes('postfix-queue-sockets')
// ['/var/spool/postfix/public/cleanup']};
my $strm;
$strm = $class->open_cleanup($_) and last for @sockets;
die "Unable to open any cleanup sockets!" unless $strm;
my %at = $strm->get_attr;
my $qid = $at{queue_id};
print STDERR "qid=$qid\n";
$strm->print_attr('flags' => $transaction->notes('postfix-queue-flags'));
$strm->print_rec_time();
$strm->print_rec('REC_TYPE_FROM', $transaction->sender->address || "");
for (map { $_->address } $transaction->recipients) {
$strm->print_rec('REC_TYPE_RCPT', $_);
}
# add an empty message length record.
# cleanup is supposed to understand that.
# see src/pickup/pickup.c
$strm->print_rec('REC_TYPE_MESG', "");
# a received header has already been added in SMTP.pm
# so we can just copy the message:
my $hdr = $transaction->header->as_string;
for (split(/\r?\n/, $hdr)) {
print STDERR "hdr: $_\n";
$strm->print_msg_line($_);
}
$transaction->body_resetpos;
while (my $line = $transaction->body_getline) {
# print STDERR "body: $line\n";
$strm->print_msg_line($line);
}
# finish it.
$strm->print_rec('REC_TYPE_XTRA', "");
$strm->print_rec('REC_TYPE_END', "");
$strm->flush();
%at = $strm->get_attr;
my $status = $at{status};
my $reason = $at{reason};
$strm->close();
return wantarray ? ($status, $qid, $reason || "") : $status;
}
1;
# vim:sw=2