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
