295474503f
The current postfix-queue plugin allows the administrator to set a single path to a local postfix cleanup socket file from the plugin 'command line'. This adds a 'cleanup_sockets' configuration directive that can contain a list of paths as well as host/port combinations pointing to postfix cleanup services, which will be tried in the order that they appear. Not yet tested.
224 lines
5.4 KiB
Perl
224 lines
5.4 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
|