233 lines
5.9 KiB
Perl
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
|