1package Qpsmtpd::SMTP;
2use Qpsmtpd;
3@ISA = qw(Qpsmtpd);
4my %auth_mechanisms = ();
5
6package Qpsmtpd::SMTP;
7use strict;
8use Carp;
9
10use Qpsmtpd::Connection;
11use Qpsmtpd::Transaction;
12use Qpsmtpd::Plugin;
13use Qpsmtpd::Constants;
14use Qpsmtpd::Auth;
15use Qpsmtpd::Address ();
16use Qpsmtpd::Command;
17
18use Mail::Header ();
19
20#use Data::Dumper;
21use POSIX qw(strftime);
22use Net::DNS;
23
24# this is only good for forkserver
25# can't set these here, cause forkserver resets them
26#$SIG{ALRM} = sub { respond(421, "timeout; I can't wait that long..."); exit };
27#$SIG{ALRM} = sub { warn "Connection Timed Out\n"; exit; };
28
29sub new {
30    my $proto = shift;
31    my $class = ref($proto) || $proto;
32
33    my %args = @_;
34
35    my $self = bless({args => \%args}, $class);
36
37    my (@commands) = qw(ehlo helo rset mail rcpt data help vrfy noop quit);
38    my (%commands);
39    @commands{@commands} = ('') x @commands;
40
41   # this list of valid commands should probably be a method or a set of methods
42    $self->{_commands} = \%commands;
43    $self->SUPER::_restart(%args) if $args{restart}; # calls Qpsmtpd::_restart()
44    $self;
45}
46
47sub command_counter {
48    my $self = shift;
49    $self->{_counter} || 0;
50}
51
52sub dispatch {
53    my $self = shift;
54    my ($cmd) = shift;
55    if (!$cmd) {
56        $self->run_hooks("unrecognized_command", '', @_);
57        return 1;
58    }
59    $cmd = lc $cmd;
60
61    $self->{_counter}++;
62
63    if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) {
64        $self->run_hooks("unrecognized_command", $cmd, @_);
65        return 1;
66    }
67    $cmd = $1;
68
69    my ($result) = eval { $self->$cmd(@_) };
70    $self->log(LOGERROR, "XX: $@") if $@;
71    return $result if defined $result;
72    return $self->fault("command '$cmd' failed unexpectedly");
73}
74
75sub unrecognized_command_respond {
76    my ($self, $rc, $msg) = @_;
77    if ($rc == DENY_DISCONNECT) {
78        $self->respond(521, @$msg);
79        $self->disconnect;
80    }
81    elsif ($rc == DENY) {
82        $self->respond(500, @$msg);
83    }
84    elsif ($rc != DONE) {
85        $self->respond(500, "Unrecognized command");
86    }
87}
88
89sub fault {
90    my $self = shift;
91    my ($msg) = shift || "program fault - command not performed";
92    my ($name) = split /\s+/, $0, 2;
93    print STDERR $name, "[$$]: $msg ($!)\n";
94    return $self->respond(451, "Internal error - try again later - " . $msg);
95}
96
97sub start_conversation {
98    my $self = shift;
99
100    # this should maybe be called something else than "connect", see
101    # lib/Qpsmtpd/TcpServer.pm for more confusion.
102    $self->run_hooks("connect");
103    return DONE;
104}
105
106sub connect_respond {
107    my ($self, $rc, $msg) = @_;
108    if ($rc == DENY || $rc == DENY_DISCONNECT) {
109        $msg->[0] ||= 'Connection from you denied, bye bye.';
110        $self->respond(550, @$msg);
111        $self->disconnect;
112    }
113    elsif ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {
114        $msg->[0] ||= 'Connection from you temporarily denied, bye bye.';
115        $self->respond(450, @$msg);
116        $self->disconnect;
117    }
118    elsif ($rc != DONE) {
119        my $greets = $self->config('smtpgreeting');
120        if ($greets) {
121            $greets .= " ESMTP" unless $greets =~ /(^|\W)ESMTP(\W|$)/;
122        }
123        else {
124            $greets =
125                $self->config('me')
126              . " ESMTP qpsmtpd "
127              . $self->version
128              . " ready; send us your mail, but not your spam.";
129        }
130
131        $self->respond(220, $greets);
132    }
133}
134
135sub transaction {
136    my $self = shift;
137    return $self->{_transaction} || $self->reset_transaction();
138}
139
140sub reset_transaction {
141    my $self = shift;
142    $self->run_hooks("reset_transaction") if $self->{_transaction};
143    return $self->{_transaction} = Qpsmtpd::Transaction->new();
144}
145
146sub connection {
147    my $self = shift;
148    @_ and $self->{_connection} = shift;
149    return $self->{_connection}
150      || ($self->{_connection} = Qpsmtpd::Connection->new());
151}
152
153sub helo {
154    my ($self, $line) = @_;
155    my ($rc,   @msg)  = $self->run_hooks('helo_parse');
156    my ($ok, $hello_host, @stuff) =
157      Qpsmtpd::Command->parse('helo', $line, $msg[0]);
158
159    return $self->respond(501,
160                          "helo requires domain/address - see RFC-2821 4.1.1.1")
161      unless $hello_host;
162    my $conn = $self->connection;
163    return $self->respond(503, "but you already said HELO ...") if $conn->hello;
164
165    $self->run_hooks("helo", $hello_host, @stuff);
166}
167
168sub helo_respond {
169    my ($self, $rc, $msg, $args) = @_;
170    my ($hello_host) = @$args;
171    if ($rc == DONE) {
172
173        # do nothing:
174        1;
175    }
176    elsif ($rc == DENY) {
177        $self->respond(550, @$msg);
178    }
179    elsif ($rc == DENYSOFT) {
180        $self->respond(450, @$msg);
181    }
182    elsif ($rc == DENY_DISCONNECT) {
183        $self->respond(550, @$msg);
184        $self->disconnect;
185    }
186    elsif ($rc == DENYSOFT_DISCONNECT) {
187        $self->respond(450, @$msg);
188        $self->disconnect;
189    }
190    else {
191        my $conn = $self->connection;
192        $conn->hello("helo");
193        $conn->hello_host($hello_host);
194        $self->transaction;
195        $self->respond(
196                       250,
197                       $self->config('me') . " Hi "
198                         . $conn->remote_info . " ["
199                         . $conn->remote_ip
200                         . "]; I am so happy to meet you."
201                      );
202    }
203}
204
205sub ehlo {
206    my ($self, $line) = @_;
207    my ($rc,   @msg)  = $self->run_hooks('ehlo_parse');
208    my ($ok, $hello_host, @stuff) =
209      Qpsmtpd::Command->parse('ehlo', $line, $msg[0]);
210    return $self->respond(501,
211                          "ehlo requires domain/address - see RFC-2821 4.1.1.1")
212      unless $hello_host;
213    my $conn = $self->connection;
214    return $self->respond(503, "but you already said HELO ...") if $conn->hello;
215
216    $self->run_hooks("ehlo", $hello_host, @stuff);
217}
218
219sub ehlo_respond {
220    my ($self, $rc, $msg, $args) = @_;
221    my ($hello_host) = @$args;
222    if ($rc == DONE) {
223
224        # do nothing:
225        1;
226    }
227    elsif ($rc == DENY) {
228        $self->respond(550, @$msg);
229    }
230    elsif ($rc == DENYSOFT) {
231        $self->respond(450, @$msg);
232    }
233    elsif ($rc == DENY_DISCONNECT) {
234        $self->respond(550, @$msg);
235        $self->disconnect;
236    }
237    elsif ($rc == DENYSOFT_DISCONNECT) {
238        $self->respond(450, @$msg);
239        $self->disconnect;
240    }
241    else {
242        my $conn = $self->connection;
243        $conn->hello("ehlo");
244        $conn->hello_host($hello_host);
245        $self->transaction;
246
247        my @capabilities =
248          $self->transaction->notes('capabilities')
249          ? @{$self->transaction->notes('capabilities')}
250          : ();
251
252        # Check for possible AUTH mechanisms
253      HOOK: foreach my $hook (keys %{$self->hooks}) {
254            if ($hook =~ m/^auth-?(.+)?$/) {
255                if (defined $1) {
256                    $auth_mechanisms{uc($1)} = 1;
257                }
258                else {    # at least one polymorphous auth provider
259                    %auth_mechanisms = map { $_, 1 } qw(PLAIN CRAM-MD5 LOGIN);
260                    last HOOK;
261                }
262            }
263        }
264
265        # Check if we should only offer AUTH after TLS is completed
266        my $tls_before_auth =
267          ($self->config('tls_before_auth')
268            ? ($self->config('tls_before_auth'))[0]
269              && $self->transaction->notes('tls_enabled')
270            : 0);
271        if (%auth_mechanisms && !$tls_before_auth) {
272            push @capabilities, 'AUTH ' . join(" ", keys(%auth_mechanisms));
273            $self->{_commands}->{'auth'} = "";
274        }
275
276        $self->respond(
277                       250,
278                       $self->config("me") . " Hi "
279                         . $conn->remote_info . " ["
280                         . $conn->remote_ip . "]",
281                       "PIPELINING",
282                       "8BITMIME",
283                       (
284                        $self->config('databytes')
285                        ? "SIZE " . ($self->config('databytes'))[0]
286                        : ()
287                       ),
288                       @capabilities,
289                      );
290    }
291}
292
293sub auth {
294    my ($self, $line) = @_;
295    $self->run_hooks('auth_parse', $line);
296}
297
298sub auth_parse_respond {
299    my ($self, $rc, $msg, $args) = @_;
300    my ($line) = @$args;
301
302    my ($ok, $mechanism, @stuff) =
303      Qpsmtpd::Command->parse('auth', $line, $msg->[0]);
304    return $self->respond(501, $mechanism || "Syntax error in command")
305      unless ($ok == OK);
306
307    $mechanism = lc($mechanism);
308
309    #they AUTH'd once already
310    return $self->respond(503, "but you already said AUTH ...")
311      if (defined $self->{_auth} && $self->{_auth} == OK);
312
313    return $self->respond(503, "AUTH not defined for HELO")
314      if ($self->connection->hello eq "helo");
315
316    return $self->respond(503, "SSL/TLS required before AUTH")
317      if (($self->config('tls_before_auth'))[0]
318          && $self->transaction->notes('tls_enabled'));
319
320    # we don't have a plugin implementing this auth mechanism, 504
321    if (exists $auth_mechanisms{uc($mechanism)}) {
322        return $self->{_auth} = Qpsmtpd::Auth::SASL($self, $mechanism, @stuff);
323    }
324
325    $self->respond(504, "Unimplemented authentification mechanism: $mechanism");
326    return DENY;
327}
328
329sub mail {
330    my ($self, $line) = @_;
331
332    # -> from RFC2821
333    # The MAIL command (or the obsolete SEND, SOML, or SAML commands)
334    # begins a mail transaction.  Once started, a mail transaction
335    # consists of a transaction beginning command, one or more RCPT
336    # commands, and a DATA command, in that order.  A mail transaction
337    # may be aborted by the RSET (or a new EHLO) command.  There may be
338    # zero or more transactions in a session.  MAIL (or SEND, SOML, or
339    # SAML) MUST NOT be sent if a mail transaction is already open,
340    # i.e., it should be sent only if no mail transaction had been
341    # started in the session, or it the previous one successfully
342    # concluded with a successful DATA command, or if the previous one
343    # was aborted with a RSET.
344
345    # sendmail (8.11) rejects a second MAIL command.
346
347    # qmail-smtpd (1.03) accepts it and just starts a new transaction.
348    # Since we are a qmail-smtpd thing we will do the same.
349
350    $self->reset_transaction;
351
352    if (!$self->connection->hello) {
353        return $self->respond(503, "please say hello first ...");
354    }
355
356    $self->log(LOGDEBUG, "full from_parameter: $line");
357    $self->connection->notes('envelope_from', $line);
358    $self->run_hooks("mail_parse", $line);
359}
360
361sub mail_parse_respond {
362    my ($self, $rc, $msg, $args) = @_;
363    my ($line) = @$args;
364    my ($ok, $from, @params) =
365      Qpsmtpd::Command->parse('mail', $line, $msg->[0]);
366    return $self->respond(501, $from || "Syntax error in command")
367      unless ($ok == OK);
368    my %param;
369    foreach (@params) {
370        my ($k, $v) = split /=/, $_, 2;
371        $param{lc $k} = $v;
372    }
373
374    # to support addresses without <> we now require a plugin
375    # hooking "mail_pre" to
376    #   return (OK, "<$from>");
377    # (...or anything else parseable by Qpsmtpd::Address ;-))
378    # see also comment in sub rcpt()
379    $self->run_hooks("mail_pre", $from, \%param);
380}
381
382sub mail_pre_respond {
383    my ($self, $rc, $msg, $args) = @_;
384    my ($from, $param) = @$args;
385    if ($rc == OK) {
386        $from = shift @$msg;
387    }
388
389    $self->log(LOGDEBUG, "from email address : [$from]");
390    return $self->respond(501, "could not parse your mail from command")
391      unless $from =~ /^<.*>$/;
392
393    if ($from eq "<>" or $from =~ m/\[undefined\]/ or $from eq "<#@[]>") {
394        $from = Qpsmtpd::Address->new("<>");
395    }
396    else {
397        $from = (Qpsmtpd::Address->parse($from))[0];
398    }
399    return $self->respond(501, "could not parse your mail from command")
400      unless $from;
401
402    $self->run_hooks("mail", $from, %$param);
403}
404
405sub mail_respond {
406    my ($self, $rc, $msg, $args) = @_;
407    my ($from, $param) = @$args;
408    if ($rc == DONE) {
409        return 1;
410    }
411    elsif ($rc == DENY) {
412        $msg->[0] ||= $from->format . ', denied';
413        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");
414        $self->respond(550, @$msg);
415    }
416    elsif ($rc == DENYSOFT) {
417        $msg->[0] ||= $from->format . ', temporarily denied';
418        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");
419        $self->respond(450, @$msg);
420    }
421    elsif ($rc == DENY_DISCONNECT) {
422        $msg->[0] ||= $from->format . ', denied';
423        $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)");
424        $self->respond(550, @$msg);
425        $self->disconnect;
426    }
427    elsif ($rc == DENYSOFT_DISCONNECT) {
428        $msg->[0] ||= $from->format . ', temporarily denied';
429        $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)");
430        $self->respond(421, @$msg);
431        $self->disconnect;
432    }
433    else {    # includes OK
434        $self->log(LOGDEBUG, "getting mail from " . $from->format);
435        $self->respond(
436                       250,
437                       $from->format
438                         . ", sender OK - how exciting to get mail from you!"
439                      );
440        $self->transaction->sender($from);
441    }
442}
443
444sub rcpt {
445    my ($self, $line) = @_;
446    $self->connection->notes('envelope_rcpt', $line);
447    $self->run_hooks("rcpt_parse", $line);
448}
449
450sub rcpt_parse_respond {
451    my ($self, $rc, $msg, $args) = @_;
452    my ($line) = @$args;
453    my ($ok, $rcpt, @param) = Qpsmtpd::Command->parse("rcpt", $line, $msg->[0]);
454    return $self->respond(501, $rcpt || "Syntax error in command")
455      unless ($ok == OK);
456    return $self->respond(503, "Use MAIL before RCPT")
457      unless $self->transaction->sender;
458
459    my %param;
460    foreach (@param) {
461        my ($k, $v) = split /=/, $_, 2;
462        $param{lc $k} = $v;
463    }
464
465    # to support addresses without <> we now require a plugin
466    # hooking "rcpt_pre" to
467    #   return (OK, "<$rcpt>");
468    # (... or anything else parseable by Qpsmtpd::Address ;-))
469    # this means, a plugin can decide to (pre-)accept
470    # addresses like <user@example.com.> or <user@example.com >
471    # by removing the trailing dot or space from this example.
472    $self->run_hooks("rcpt_pre", $rcpt, \%param);
473}
474
475sub rcpt_pre_respond {
476    my ($self, $rc, $msg, $args) = @_;
477    my ($rcpt, $param) = @$args;
478    if ($rc == OK) {
479        $rcpt = shift @$msg;
480    }
481    $self->log(LOGDEBUG, "to email address : [$rcpt]");
482    return $self->respond(501, "could not parse recipient")
483      unless $rcpt =~ /^<.*>$/;
484
485    $rcpt = (Qpsmtpd::Address->parse($rcpt))[0];
486
487    return $self->respond(501, "could not parse recipient")
488      if (!$rcpt or ($rcpt->format eq '<>'));
489
490    $self->run_hooks("rcpt", $rcpt, %$param);
491}
492
493sub rcpt_respond {
494    my ($self, $rc, $msg, $args) = @_;
495    my ($rcpt, $param) = @$args;
496    if ($rc == DONE) {
497        return 1;
498    }
499    elsif ($rc == DENY) {
500        $msg->[0] ||= 'relaying denied';
501        $self->respond(550, @$msg);
502    }
503    elsif ($rc == DENYSOFT) {
504        $msg->[0] ||= 'relaying denied';
505        return $self->respond(450, @$msg);
506    }
507    elsif ($rc == DENY_DISCONNECT) {
508        $msg->[0] ||= 'delivery denied';
509        $self->log(LOGDEBUG, "delivery denied (@$msg)");
510        $self->respond(550, @$msg);
511        $self->disconnect;
512    }
513    elsif ($rc == DENYSOFT_DISCONNECT) {
514        $msg->[0] ||= 'relaying denied';
515        $self->log(LOGDEBUG, "delivery denied (@$msg)");
516        $self->respond(421, @$msg);
517        $self->disconnect;
518    }
519    elsif ($rc == OK) {
520        $self->respond(250, $rcpt->format . ", recipient ok");
521        return $self->transaction->add_recipient($rcpt);
522    }
523    else {
524        return $self->respond(450, "No plugin decided if relaying is allowed");
525    }
526    return 0;
527}
528
529sub help {
530    my ($self, @args) = @_;
531    $self->run_hooks("help", @args);
532}
533
534sub help_respond {
535    my ($self, $rc, $msg, $args) = @_;
536
537    return 1
538      if $rc == DONE;
539
540    if ($rc == DENY) {
541        $msg->[0] ||= "Syntax error, command not recognized";
542        $self->respond(500, @$msg);
543    }
544    else {
545        unless ($msg->[0]) {
546            @$msg = (
547                     "This is qpsmtpd "
548                       . ($self->config('smtpgreeting') ? '' : $self->version),
549                     "See http://smtpd.develooper.com/",
550'To report bugs or send comments, mail to <ask@develooper.com>.'
551                    );
552        }
553        $self->respond(214, @$msg);
554    }
555    return 1;
556}
557
558sub noop {
559    my $self = shift;
560    $self->run_hooks("noop");
561}
562
563sub noop_respond {
564    my ($self, $rc, $msg, $args) = @_;
565    return 1 if $rc == DONE;
566
567    if ($rc == DENY || $rc == DENY_DISCONNECT) {
568        $msg->[0] ||= "Stop wasting my time.";  # FIXME: better default message?
569        $self->respond(500, @$msg);
570        $self->disconnect if $rc == DENY_DISCONNECT;
571        return 1;
572    }
573
574    $self->respond(250, "OK");
575    return 1;
576}
577
578sub vrfy {
579    my $self = shift;
580
581    # Note, this doesn't support the multiple ambiguous results
582    # documented in RFC2821#3.5.1
583    # I also don't think it provides all the proper result codes.
584
585    $self->run_hooks("vrfy");
586}
587
588sub vrfy_respond {
589    my ($self, $rc, $msg, $args) = @_;
590    if ($rc == DONE) {
591        return 1;
592    }
593    elsif ($rc == DENY) {
594        $msg->[0] ||= "Access Denied";
595        $self->respond(554, @$msg);
596        $self->reset_transaction();
597        return 1;
598    }
599    elsif ($rc == OK) {
600        $msg->[0] ||= "User OK";
601        $self->respond(250, @$msg);
602        return 1;
603    }
604    else {    # $rc == DECLINED or anything else
605        $self->respond(252,
606                  "Just try sending a mail and we'll see how it turns out ...");
607        return 1;
608    }
609}
610
611sub rset {
612    my $self = shift;
613    $self->reset_transaction;
614    $self->respond(250, "OK");
615}
616
617sub quit {
618    my $self = shift;
619    $self->run_hooks("quit");
620}
621
622sub quit_respond {
623    my ($self, $rc, $msg, $args) = @_;
624    if ($rc != DONE) {
625        $msg->[0] ||=
626          $self->config('me') . " closing connection. Have a wonderful day.";
627        $self->respond(221, @$msg);
628    }
629    $self->disconnect();
630}
631
632sub disconnect {
633    my $self = shift;
634    $self->run_hooks("disconnect");
635    $self->connection->notes(disconnected => 1);
636    $self->reset_transaction;
637}
638
639sub data {
640    my $self = shift;
641    $self->run_hooks("data");
642}
643
644sub data_respond {
645    my ($self, $rc, $msg, $args) = @_;
646    if ($rc == DONE) {
647        return 1;
648    }
649    elsif ($rc == DENY) {
650        $msg->[0] ||= "Message denied";
651        $self->respond(554, @$msg);
652        $self->reset_transaction();
653        return 1;
654    }
655    elsif ($rc == DENYSOFT) {
656        $msg->[0] ||= "Message denied temporarily";
657        $self->respond(451, @$msg);
658        $self->reset_transaction();
659        return 1;
660    }
661    elsif ($rc == DENY_DISCONNECT) {
662        $msg->[0] ||= "Message denied";
663        $self->respond(554, @$msg);
664        $self->disconnect;
665        return 1;
666    }
667    elsif ($rc == DENYSOFT_DISCONNECT) {
668        $msg->[0] ||= "Message denied temporarily";
669        $self->respond(421, @$msg);
670        $self->disconnect;
671        return 1;
672    }
673    $self->respond(503, "MAIL first"), return 1
674      unless $self->transaction->sender;
675    $self->respond(503, "RCPT first"), return 1
676      unless $self->transaction->recipients;
677    $self->respond(354, "go ahead");
678
679    my $buffer = '';
680    my $size   = 0;
681    my $i      = 0;
682    my $max_size =
683      ($self->config('databytes'))[0] || 0; # this should work in scalar context
684    my $blocked = "";
685    my %matches;
686    my $in_header = 1;
687    my $complete  = 0;
688
689    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");
690
691    my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
692
693    my $timeout = $self->config('timeout');
694    while (defined($_ = $self->getline($timeout))) {
695        if ($_ eq ".\r\n") {
696            $complete++;
697            $_ = '';
698        }
699        $i++;
700
701    # should probably use \012 and \015 in these checks instead of \r and \n ...
702
703        # Reject messages that have either bare LF or CR. rjkaes noticed a
704        # lot of spam that is malformed in the header.
705
706        ($_ eq ".\n" or $_ eq ".\r")
707          and $self->respond(421, "See http://smtpd.develooper.com/barelf.html")
708          and return $self->disconnect;
709
710# add a transaction->blocked check back here when we have line by line plugin access...
711        unless (($max_size and $size > $max_size)) {
712            s/\r\n$/\n/;
713            s/^\.\./\./;
714            if ($in_header && (m/^$/ || $complete > 0)) {
715                $in_header = 0;
716                my @headers = split /^/m, $buffer;
717
718        # ... need to check that we don't reformat any of the received lines.
719        #
720        # 3.8.2 Received Lines in Gatewaying
721        #   When forwarding a message into or out of the Internet environment, a
722        #   gateway MUST prepend a Received: line, but it MUST NOT alter in any
723        #   way a Received: line that is already in the header.
724
725                $header->extract(\@headers);
726
727#$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.develooper.com/");
728
729                $buffer = "";
730
731                $self->transaction->header($header);
732
733                # NOTE: This will not work properly under async.  A
734                # data_headers_end_respond needs to be created.
735                my ($rc, $msg) = $self->run_hooks('data_headers_end');
736                if ($rc == DENY_DISCONNECT) {
737                    $self->respond(554, $msg || "Message denied");
738                    $self->disconnect;
739                    return 1;
740                }
741                elsif ($rc == DENYSOFT_DISCONNECT) {
742                    $self->respond(421, $msg || "Message denied temporarily");
743                    $self->disconnect;
744                    return 1;
745                }
746
747                # Save the start of just the body itself
748                $self->transaction->set_body_start();
749
750            }
751
752            # grab a copy of all of the header lines
753            if ($in_header) {
754                $buffer .= $_;
755            }
756
757            # copy all lines into the spool file, including the headers
758            # we will create a new header later before sending onwards
759            $self->transaction->body_write($_) if !$complete;
760            $size += length $_;
761        }
762        last if $complete > 0;
763
764        #$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300);
765    }
766
767    $self->log(LOGDEBUG, "max_size: $max_size / size: $size");
768
769    # if we get here without seeing a terminator, the connection is
770    # probably dead.
771    unless ($complete) {
772        $self->respond(451, "Incomplete DATA");
773        $self->reset_transaction;    # clean up after ourselves
774        return 1;
775    }
776
777#$self->respond(550, $self->transaction->blocked),return 1 if ($self->transaction->blocked);
778    if ($max_size and $size > $max_size) {
779        $self->log(LOGALERT,
780                   "Message too big: size: $size (max size: $max_size)");
781        $self->respond(552, "Message too big!");
782        $self->reset_transaction;    # clean up after ourselves
783        return 1;
784    }
785
786    $self->run_hooks("data_post");
787}
788
789sub authentication_results {
790    my ($self) = @_;
791
792    my @auth_list = $self->config('me');
793#   $self->clean_authentication_results();
794
795    if ( ! defined $self->{_auth} ) {
796        push @auth_list, 'auth=none';
797    }
798    else {
799        my $mechanism = "(" . $self->{_auth_mechanism} . ")";
800        my $user = "smtp.auth=" . $self->{_auth_user};
801        if ( $self->{_auth} == OK) {
802            push @auth_list, "auth=pass $mechanism $user";
803        }
804        else {
805            push @auth_list, "auth=fail $mechanism $user";
806        };
807    };
808
809    # RFC 5451: used in AUTH, DKIM, DOMAINKEYS, SENDERID, SPF
810    if ( $self->connection->notes('authentication_results') ) {
811        push @auth_list, $self->connection->notes('authentication_results');
812    };
813
814    $self->log(LOGDEBUG, "adding auth results header" );
815    $self->transaction->header->add('Authentication-Results', join('; ', @auth_list), 0);
816};
817
818sub clean_authentication_results {
819    my $self = shift;
820
821# http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html
822
823# On messages received from the internet, move Authentication-Results headers
824# to Original-AR, so our downstream can trust the A-R header we insert.
825
826# TODO: Do not invalidate DKIM signatures.
827#   if $self->transaction->header->get('DKIM-Signature')
828#       Parse the DKIM signature(s)
829#       return if A-R header is signed;
830#   }
831
832    my @ar_headers = $self->transaction->header->get('Authentication-Results');
833    for ( my $i = 0; $i < scalar @ar_headers; $i++ ) {
834        $self->transaction->header->delete('Authentication-Results', $i);
835        $self->transaction->header->add('Original-Authentication-Results', $ar_headers[$i]);
836    }
837
838    $self->log(LOGDEBUG, "Authentication-Results moved to Original-Authentication-Results" );
839};
840
841sub received_line {
842    my ($self) = @_;
843
844    my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP";
845    my $esmtp      = substr($smtp, 0, 1) eq "E";
846    my $authheader = '';
847    my $sslheader  = '';
848
849    if (defined $self->connection->notes('tls_enabled')
850        and $self->connection->notes('tls_enabled'))
851    {
852        $smtp .= "S" if $esmtp;    # RFC3848
853        $sslheader = "("
854          . $self->connection->notes('tls_socket')->get_cipher()
855          . " encrypted) ";
856    }
857    if (defined $self->{_auth} && $self->{_auth} == OK) {
858        my $mech = $self->{_auth_mechanism};
859        my $user = $self->{_auth_user};
860        $smtp .= "A" if $esmtp;    # RFC3848
861        $authheader = "(smtp-auth username $user, mechanism $mech)\n";
862    }
863
864    my $header_str;
865    my ($rc, @received) =
866      $self->run_hooks("received_line", $smtp, $authheader, $sslheader);
867    if ($rc == YIELD) {
868        die "YIELD not supported for received_line hook";
869    }
870    elsif ($rc == OK) {
871        return join("\n", @received);
872    }
873    else {    # assume $rc == DECLINED
874        $header_str =
875            "from "
876          . $self->connection->remote_info
877          . " (HELO "
878          . $self->connection->hello_host . ") ("
879          . $self->connection->remote_ip
880          . ")\n by "
881          . $self->config('me')
882          . " (qpsmtpd/"
883          . $self->version
884          . ") with $sslheader$smtp; "
885          . (strftime('%a, %d %b %Y %H:%M:%S %z', localtime));
886    }
887    $self->transaction->header->add('Received', $header_str, 0 );
888}
889
890sub data_post_respond {
891    my ($self, $rc, $msg, $args) = @_;
892    if ($rc == DONE) {
893        return 1;
894    }
895    elsif ($rc == DENY) {
896        $msg->[0] ||= "Message denied";
897        $self->respond(552, @$msg);
898
899        # DATA is always the end of a "transaction"
900        return $self->reset_transaction;
901    }
902    elsif ($rc == DENYSOFT) {
903        $msg->[0] ||= "Message denied temporarily";
904        $self->respond(452, @$msg);
905
906        # DATA is always the end of a "transaction"
907        return $self->reset_transaction;
908    }
909    elsif ($rc == DENY_DISCONNECT) {
910        $msg->[0] ||= "Message denied";
911        $self->respond(552, @$msg);
912        $self->disconnect;
913        return 1;
914    }
915    elsif ($rc == DENYSOFT_DISCONNECT) {
916        $msg->[0] ||= "Message denied temporarily";
917        $self->respond(452, @$msg);
918        $self->disconnect;
919        return 1;
920    }
921    else {
922        $self->authentication_results();
923        $self->received_line();
924        $self->queue($self->transaction);
925    }
926}
927
928sub getline {
929    my ($self, $timeout) = @_;
930
931    alarm $timeout;
932    my $line = <STDIN>;    # default implementation
933    alarm 0;
934    return $line;
935}
936
937sub queue {
938    my ($self, $transaction) = @_;
939
940    # First fire any queue_pre hooks
941    $self->run_hooks("queue_pre");
942}
943
944sub queue_pre_respond {
945    my ($self, $rc, $msg, $args) = @_;
946    if ($rc == DONE) {
947        return 1;
948    }
949    elsif ($rc != OK and $rc != DECLINED and $rc != 0) {
950        return $self->log(LOGERROR, "pre plugin returned illegal value");
951        return 0;
952    }
953
954    # If we got this far, run the queue hooks
955    $self->run_hooks("queue");
956}
957
958sub queue_respond {
959    my ($self, $rc, $msg, $args) = @_;
960
961    # reset transaction if we queued the mail
962    $self->reset_transaction;
963
964    if ($rc == DONE) {
965        return 1;
966    }
967    elsif ($rc == OK) {
968        $msg->[0] ||= 'Queued';
969        $self->respond(250, @$msg);
970    }
971    elsif ($rc == DENY) {
972        $msg->[0] ||= 'Message denied';
973        $self->respond(552, @$msg);
974    }
975    elsif ($rc == DENYSOFT) {
976        $msg->[0] ||= 'Message denied temporarily';
977        $self->respond(452, @$msg);
978    }
979    else {
980        $msg->[0] ||= 'Queuing declined or disabled; try again later';
981        $self->respond(451, @$msg);
982    }
983
984    # And finally run any queue_post hooks
985    $self->run_hooks("queue_post");
986}
987
988sub queue_post_respond {
989    my ($self, $rc, $msg, $args) = @_;
990    $self->log(LOGERROR, @$msg) unless ($rc == OK or $rc == 0);
991}
992
9931;
994