Author: vetinari
Date: Wed Aug  8 10:04:40 2007
New Revision: 768

Added:
   contrib/vetinari/experimental/
   contrib/vetinari/experimental/chunking
Modified:
   contrib/vetinari/logging_apache

Log:
experimental plugin for the CHUNKING SMTP extension (RFC 1869, 1830/3030)

Added: contrib/vetinari/experimental/chunking
==============================================================================
--- (empty file)
+++ contrib/vetinari/experimental/chunking      Wed Aug  8 10:04:40 2007
@@ -0,0 +1,261 @@
+#
+# chunking - plugin for the CHUNKING SMTP extension
+# (RFC 1869, 1830/3030)
+#
+
+=head1 NAME
+
+chunking - plugin for the CHUNKING SMTP extension (RFC 1830/3030)
+
+=head1 DESCRIPTION
+
+The B<chunking> plugin adds the SMTP CHUNKING extension from RFC 3030
+``SMTP Service Extensions for Transmission of Large and Binary MIME Messages''
+to qpsmtpd.
+
+=head1 CONFIG
+
+This plugin just accepts one parameter:
+
+=head2 binarymime
+
+This enables the "BODY=BINARYMIME" MAIL FROM: parameter. If not present, it 
+will reject a
+  MAIL FROM:<[EMAIL PROTECTED]> BODY=BINARYMIME
+
+with a 555 error (see RFC 1869).
+
+Don't enable unless you're sure the backend MTA can handle this.
+
+=head1 NOTES
+
+This plugin has never been tested with a real remote MTA
+
+DON'T USE :P
+
+=cut
+
+use Qpsmtpd::DSN;
+use POSIX qw(strftime);
+
+sub register {
+    my ($self, $qp, @args) = @_;
+    if (@args > 2) {
+        $self->log(LOGERROR, "Bad parameters for the chunking plugin")
+    }
+    $self->{_binarymime} = 0;
+    if (lc $args[0] eq 'binarymime') {
+        $self->{_binarymime} = 1;
+    }
+}
+
+sub hook_ehlo {
+    my ($self, $transaction) = @_;
+    my $cap = $transaction->notes('capabilities');
+    $cap ||= [];
+    push @$cap, 'CHUNKING';
+    if ($self->{_binarymime}) {
+        push @$cap, 'BINARYMIME';
+    }
+    $transaction->notes('capabilities', $cap);
+    return(DECLINED);
+}
+
+sub hook_mail {
+    my ($self,$transaction,$sender,%params) = @_;
+    if (exists $params{'body'} && uc($params{'body'}) eq 'BINARYMIME') {
+        $transaction->notes('bdat_body_binarymime', 1);
+        unless ($self->{_binarymime}) {
+            my ($err) = 
+                (Qpsmtpd::DSN->proto_syntax_error(
+                    "BODY=BINARYMIME not supported AND not announced"))[1];
+            $self->qp->respond(555, $err);
+            return (DONE);
+        }
+    }
+    return (DECLINED);
+}
+
+sub hook_unrecognized_command {
+    my ($self, $transaction, $cmd, $size) = @_;
+    return (DECLINED) unless $cmd eq 'bdat';
+    my ($err, $last);
+
+    my $msg_size = $transaction->notes('bdat_size') || 0;
+    
+    # DATA and BDAT commands cannot be used in the same transaction.  If a
+    # DATA statement is issued after a BDAT for the current transaction, a
+    # 503 "Bad sequence of commands" MUST be issued.  The state resulting
+    # from this error is indeterminate.  
+    if ($transaction->notes('bdat_data')) {
+        ($err) = 
+          (Qpsmtpd::DSN->proto_syntax_error("You cannot use BDAT and 
DATA"))[1];
+        $self->qp->respond(503, $err);
+        return (DONE);
+    }
+
+    # Any BDAT command sent after the BDAT LAST is illegal and
+    # MUST be replied to with a 503 "Bad sequence of commands" reply code.
+    # The state resulting from this error is indeterminate. A RSET command
+    # MUST be sent to clear the transaction before continuing.
+    if ($transaction->notes('bdat_last')) {
+        ($err) =
+            (Qpsmtpd::DSN->proto_syntax_error("Bad sequence of commands"))[1];
+        $self->qp->respond(503, $err);
+        return (DONE);
+    }
+
+    ($err) = 
+        (Qpsmtpd::DSN->proto_syntax_error("Syntax error in BDAT 
parameter"))[1];
+    
+    unless (defined $size || $size =~ /^\d+$/) {
+        $self->qp->respond(552, $err);
+        return (DONE);
+    }
+
+    if ($size =~ /^(\d+)\s*(LAST)?\s*$/) {
+        $size = $1;
+        $last = $2;
+    }
+
+    if (defined $last) {
+        if ($last =~ /^LAST$/i) { # RFC says LAST all upper, we don't care
+            $last = 1;
+        }
+        else {
+            $self->qp->respond(552, $err);
+            return (DONE);
+        }
+    }
+    else {
+        $last = 0;
+        ($last = 1) unless $size;
+    }
+
+    $transaction->notes('bdat_bdat', 1); # remember we've seen BDAT
+
+    # get a file to write the data chunks
+    my $file = $transaction->body_filename;
+    # ouch :o), don't do this abuse of internals at home kids :P 
+    my $fh   = $transaction->body_fh; 
+    seek($fh, 0, 2)
+      or $self->log(LOGERROR, "failed to seek: $!"),
+         $self->qp->respond(452, "Temporary storage allocation error"), 
+         return (DONE);
+
+    # we're at the end of the file, now read the chunk
+    my $rest = $size % 4096;
+    my $num  = ($size-$rest)/4096;
+    my $i    = 0;
+    my $buffer;
+    my $bytes;
+
+    while($i < $num) {
+        $bytes = read(STDIN, $buffer, 4096);
+        if ($bytes != 4096) {
+            $self->log(LOGERROR, "Failed to read: $!");
+            $self->qp->respond(452, "Error reading your data");
+            return (DONE);
+        }
+        print $fh $buffer
+            or $self->log(LOGERROR, "Failed to write: $!"),
+               $self->qp->respond(452, "Temporary storage allocation error"), 
+               return (DONE);
+        ++$i;
+    }
+    $bytes = read(STDIN, $buffer, $rest);
+    if ($bytes != $rest) {
+        $self->log(LOGERROR, "Failed to read: $!");
+        $self->qp->respond(452, "Error reading your data");
+        return (DONE);
+    }
+    print $fh $buffer
+        or $self->log(LOGERROR, "Failed to write: $!"),
+           $self->qp->respond(452, "Temporary storage allocation error"), 
+           return (DONE);
+    # ok, got the chunk on disk
+
+    # let's see if the mail is too big... 
+    # ...we can't do this before reading the chunk, because the BDAT command
+    # requires us to read the chunk before responding
+    my $max = $self->qp->config('databytes');
+    if ($max) {
+        if (($msg_size + $size) > $max) {
+            $self->qp->respond(552, "Message too big!");
+            return(DONE);
+        }
+    }
+    $transaction->notes('bdat_size', $msg_size + $size);
+
+    if (!$last) { # get the next chunk
+        $self->qp->respond(250, "Ok, got $size octets");
+        return (DONE);
+    } 
+    # else 
+
+    # ... get the headers, run data_post & queue hooks
+    $transaction->notes('bdat_last', 1);
+    seek($fh, 0, 0)
+      or $self->log(LOGERROR, "Failed to seek: $!"),
+         $self->qp->respond(452, "Temporary storage allocation error"), 
+         return (DONE);
+
+    $buffer = "";
+    while (<$fh>) {
+        last if /^\r?\n$/;
+        s/\r\n$/\n/;
+        $buffer .= $_;
+    }
+    # the body starts here...
+    $self->transaction->set_body_start();
+
+    my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
+    my @header = split /^/m, $buffer;
+    $header->extract([EMAIL PROTECTED]);
+    $self->transaction->header($header);
+
+    my $authheader = (defined $self->{_auth} and $self->{_auth} == OK) 
+        ?  "(smtp-auth username $self->{_auth_user}, "
+          ."mechanism $self->{_auth_mechanism})\n" 
+        : "";
+
+    # no need for SMPT/ESMTP diff, we know we've just received via ESMTP (EHLO)
+    $header->add("Received", 
+        "from ".$self->connection->remote_info
+         # can/should/must this be EHLO instead of HELO?
+        ." (HELO ".$self->connection->hello_host.")"
+        ." (". $self->connection->remote_ip. ")\n "
+        .$authheader
+        ." by ".$self->qp->config('me')." (qpsmtpd/".$self->qp->version.") "
+        ."with ESMTP". ($authheader ? "A" : "")."; " # ESMPTA: RFC 3848
+        .(strftime('%a, %d %b %Y %H:%M:%S %z', localtime)), 0);
+    
+    # everything done for running data_post... 
+    # this will call the spamassassin, virus scanner and queue plugins 
+    # for us and do all the cleanup stuff
+    # ... in earlier versions (pre 0.40) of qpsmtpd we had to handle the
+    # return codes and do all the stuff
+    $self->qp->run_hooks("data_post");
+
+    # BDAT (0( LAST)?|$num LAST) is always the end of a "transaction"
+    # ... doesn't matter if it had done before
+    $self->qp->reset_transaction;
+    return (DONE);
+}
+
+sub hook_data {
+    my ($self, $transaction) = @_;
+    if ($transaction->notes('bdat_body_binarymime') 
+        || $transaction->notes('bdat_bdat')) 
+    {
+        my ($err) =
+            (Qpsmtpd::DSN->proto_syntax_error("Bad sequence of commands"))[1];
+        $self->qp->respond(503, $err);
+        return (DONE);
+    }
+    $transaction->notes('bdat_data', 1); # remeber we've seen DATA
+
+    return(DECLINED);
+}
+
+# vim: ts=4 sw=4 expandtab syn=perl

Modified: contrib/vetinari/logging_apache
==============================================================================
--- contrib/vetinari/logging_apache     (original)
+++ contrib/vetinari/logging_apache     Wed Aug  8 10:04:40 2007
@@ -48,7 +48,7 @@
 
   # luckily apache uses the same log levels as qpsmtpd...
   ($trace = lc Qpsmtpd::Constants::log_level($trace)) =~ s/^log//;
-  $trace = 'emerg' 
+  $trace = 'emerg' # ... well, nearly...
     if $trace eq 'radar';
 
   my $log = $self->{_log};
@@ -76,7 +76,7 @@
 =head1 INSTALL AND CONFIG
 
 Place this plugin in the plugin/logging directory beneath the standard
-qpsmtpd installation.  Edit the config/logging file and add a line like
+qpsmtpd installation. Edit the config/logging file and add a line like
 this:
 
   logging/apache

Reply via email to