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