#-*- perl -*- # # Copyright (C) 2000,2001,2002,2003,2004,2006,2012 Ken'ichi Fukamachi # All rights reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as Perl itself. # # $FML: SMTP.pm,v 1.50 2012/12/13 13:42:26 fukachan Exp $ # package Mail::Delivery::SMTP; use strict; use vars qw(@ISA @EXPORT @EXPORT_OK); use Carp; use IO::Socket; # Mail::Delivery::SMTP IS-A Mail::Delivery::Protocol. use Mail::Delivery::Protocol; @ISA = qw(Mail::Delivery::Protocol); BEGIN {} END {} # MAP IO STATUS CODE my $MAP_DONE = 'DONE'; my $MAP_NOT_DONE = 'NOT DONE'; my $MAP_ERR_OPEN = 'CANNOT OPEN'; # MTA IO STATUS CODE my $MTA_OK = 'OK'; my $MTA_ERR_TIMEOUT = 'TIMEOUT'; my $MTA_ERR_FATAL = 'FATAL'; # SMTP STATUS my $SMTP_OK = 'OK'; my $SMTP_ERR_RETRY = 'SMTP RETRY'; my $SMTP_ERR_FATAL = 'SMTP FATAL ERROR'; =head1 NAME Mail::Delivery::SMTP - interface for SMTP service =head1 SYNOPSIS To initialize, use Mail::Message; ... make $message (Mail::Message object) ... use Mail::Delivery::SMTP; my $fp = sub { $curproc->log(@_);}; # pointer to the log function my $sfp = sub { my ($s) = @_; print $s; print "\n" if $s !~ /\n$/o;}; my $service = new Mail::Delivery::SMTP { log_function => $fp, smtp_log_function => $sfp, default_io_timeout => 10, }; if ($service->error) { $curproc->logerror($service->error); return;} To start delivery, use deliver() method in this way. $service->deliver( { smtp_servers => '[::1]:25 127.0.0.1:25', smtp_sender => 'rudo@nuinui.net', recipient_maps => $recipient_maps, recipient_limit => 1000, message => $message, }); C is a C object. You can specify the recipient list as an ARRAY REFERENCE. # reference to an array of recipients @array = ( 'kenken@nuinui.net' ); $service->deliver( { smtp_servers => '[::1]:25 127.0.0.1:25', smtp_sender => 'rudo@nuinui.net', recipient_array => \@array, recipient_limit => 1000, message => $message, }); =head1 DESCRIPTION This module provides SMTP/ESMTP mail delivery service. It tries IPv6 connection If possible. The socket creation and tcp connection is controlled by sub-classes, C and C. It sends a list of all recipients indicated by $recipient_maps. C resolves $recipient_maps and provides the abstract IO layer. It provides the usual file IO methods for each C. See L for more details. =head1 METHODS =head2 new($args) the constructor. Please specify it in a hash reference as an argument of new(). Several parameters on logging and timeout et. al. are avialable. hash key value -------------------------------------------- log_function reference to function for logging smtp_log_function reference to function for logging default_io_timeout default timeout associated with the socket IO C is the function pointer to write a message in the log file. C is special function pointer to log SMTP transactions. =cut # Descriptions: Mail::Delivery::SMTP constructor. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: $self ($me) hash has some default values # Return Value: OBJ sub new { my ($self, $args) = @_; my ($type) = ref($self) || $self; my $me = {}; # malloc new SMTP session struct $me->{ _num_recipients } = 0; bless $me, $type; $me->set_smtp_recipient_limit($args->{ recipient_limit } || 1000); $me->set_smtp_default_timeout($args->{ default_io_timeout } || 10); $me->_set_queue_directory($args->{ queue_dir } || ''); my $fp_address_validate = $args->{address_validate_function} || undef; $me->set_address_validate_function($fp_address_validate); $me->_init_delivery_transaction($args); $me->_init_logging($args); return bless $me, $type; } # Descriptions: send a (SMTP/LMTP) command string to BSD socket. # Arguments: OBJ($self) STR($command) # Side Effects: update log file by smtplog. # set _last_command and _error_action in object itself # Return Value: none sub _send_command { my ($self, $command) = @_; my $socket = $self->get_socket() || undef; unless (defined $socket) { $self->logerror("_send_command: undefined socket"); } $self->set_last_command($command); $self->set_send_command_status(''); $self->smtplog($command."\r\n"); if (defined $socket) { $socket->print($command, "\r\n"); } else { $self->logerror("_send_command: undefined socket"); } } # Descriptions: receive a reply for a (SMTP/LMTP) command. # Arguments: OBJ($self) # Side Effects: update log file by smtplog. # Return Value: none sub _read_reply { my ($self) = @_; my $socket = $self->get_socket() || undef; # unique identifier to clarify the trapped error message my $id = sprintf("%s-%s", time, $$); # toggle flag whether we should check SMTP attributes or not. # we should check it only in HELO phase. my $check_attributes = 0; my $last_command = $self->get_last_command(); if ($last_command =~ /^(EHLO|HELO|LHLO)/o) { $check_attributes = 1; } # XXX Attention! dynamic scope by local() for %SIG is essential. # See books on Perl for more details on my() and local() difference. eval { local($SIG{ALRM}) = sub { croak("$id socket timeout");}; alarm( $self->get_smtp_default_timeout() ); my $buf = ''; croak("socket is not connected") unless $self->is_socket_connected($socket); SMTP_REPLY: while (1) { defined ( $buf = $socket->getline ) || next SMTP_REPLY; $self->smtplog($buf); # check smtp attributes if ($check_attributes) { if ($buf =~ /^250.PIPELINING/i) { $self->_set_attribute("use_pipelining", "yes"); } if ($buf =~ /^250.ETRN/i) { $self->_set_attribute("use_etrn", "yes"); } if ($buf =~ /^250.SIZE\s+(\d+)/i) { $self->_set_attribute("size_limit", $1); } } # store the latest status code if ($buf =~ /^(\d{3})/) { $self->set_status_code($1);} # check status code if ($buf =~ /^[45]\d{2}\s/) { my $command = $self->get_last_command(); $self->logerror(sprintf("%s ... %s", $command, $buf)); die("$id temprary failure") if $buf =~ /^4/; die("$id fatal") if $buf =~ /^5/; } # end of reply e.g. "250 ..." last SMTP_REPLY if $buf =~ /^\d{3}\s/; } }; if ($@ =~ /$id temprary failure/) { $self->set_send_command_status($SMTP_ERR_RETRY); $self->logerror("temporary failure, retry"); $self->set_error("temporary failure, retry"); } elsif ($@ =~ /$id fatal/) { $self->set_send_command_status($SMTP_ERR_FATAL); $self->logerror("fatal error"); $self->set_error("fatal error"); } elsif ($@ =~ /$id socket timeout/) { my $command = $self->get_last_command(); $self->logerror("smtp reply for \"$command\" is timeout"); $self->set_error("smtp reply for \"$command\" is timeout"); } elsif ($@) { $self->logerror($@); $self->set_error($@); } # reset latest alarm() setting alarm(0); } # Descriptions: connect(2) # 1. try connect(2) by IPv6 if we can use Socket6.pm # 2. try connect(2) by IPv4 # if $host is not IPv6 raw address e.g. [::1]:25 # Arguments: OBJ($self) HASH_REF($args) # Side Effects: set file handle (BSD socket) in $self->{_socket} # Return Value: HANDLE(created BSD socket) or undef() sub _connect { my ($self, $args) = @_; my $mta = $args->{ mta } || '127.0.0.1:25'; $self->clear_error; # 1. try to connect(2) $mta by IPv6 if # (1) we can use Socket6. # (2) mta is not pure IPv4 syntax (Iv6 syntax or hostname). if ($self->is_ipv6_ready() && (! $self->is_pure_ipv4_syntax($mta))) { $self->logdebug("try mta=$mta by IPv6"); $self->connect6($mta); my $socket = $self->get_socket() || undef; if (defined $socket) { return $socket; } else { $self->logerror("cannot connect $mta"); } } else { $self->logdebug("IPv6 is not ready"); } $self->clear_error; # 2. try to connect(2) $mta by IPv4. # XXX check the mta syntax. # XXX if $mta looks [$ipv6_addr]:$port style, # XXX we do not try to connect the host by IPv4. if ($self->is_ipv6_mta_syntax($mta)) { $self->logdebug("not try MTA $mta"); return undef; } else { $self->logdebug("try mta=$mta by IPv4"); my $socket = $self->connect4($mta); if (defined $socket) { return $socket; } else { $self->logerror("cannot connect $mta"); return undef; } } return undef; } ############################################################ ##### ##### SMTP delivery main loop ##### =head2 deliver($args) start delivery process. You can specify the following parameter at C<$args> HASH REFERENCE. hash key value -------------------------------------------- smtp_servers 127.0.0.1:25 [::1]:25 smtp_sender sender's mail address recipient_maps $recipient_maps recipient_limit recipients in one SMTP transactions header FML::Header object body Mail::Message object C is a list of MTA's (Mail Transport Agents). The syntax of each MTA is C or C style. If you use a raw IPv6 address, use C<[address]:port> syntax. For example, [::1]:25 (IPv6 loopback address). You can specify a combination of IPv4 and IPv6 addresses at C. C automatically tries smtp connection on both protocols. C is the sender's email address. It is used at MAIL FROM: command. C is a list of C. See L for more details. For example, To read addresses from a file, specify the map as file:/var/spool/ml/elena/recipients and to read addresses from /etc/group unix.group:fml C is the max number of recipients in one SMTP transaction. 1000 by default, which corresponds to the limit by C. C
is an C object. C is a C object. See L for more details. =cut # Descriptions: main delivery loop for each recipient_maps and each mta. # real delivery is done within _deliver() method. # algorithm: # for each $map { # for each $mta { # call _deliver() # send recipients up to $recipient_limit # } # } # # Arguments: OBJ($self) HASH_REF($args) # Side Effects: See Mail::Delivery::Utils for recipient_map utilities # to track the delivery process status. # Return Value: none sub deliver { my ($self, $args) = @_; # smtp_recipient_limit: maximum recipients in one smtp session. # default_timeout: basic timeout parameter for smtp session $self->set_smtp_recipient_limit($args->{ recipient_limit } || 1000); $self->set_smtp_default_timeout($args->{ default_io_timeout } || 10); $self->_set_queue_directory($args->{ queue_dir } || ''); # temporary hash to check whether the map/mta is used already. my %used_mta = (); my %used_map = (); # prepare loop for each mta and map my $mta = $self->_get_mta_list($args); my $maps = $self->_get_map_list($args); MAP: for my $map ( @$maps ) { # uniq $map next MAP if $used_map{ $map }; $used_map{ $map } = 1; # try to open $map eval q{ use IO::Adapter; my $obj = new IO::Adapter $map, $args->{ map_params }; if (defined $obj) { $obj->open || croak("cannot open $map"); } }; if ($@) { $self->set_map_status($map, $MAP_ERR_OPEN); $self->logerror("cannot open and ignore $map"); next MAP; } if ($self->get_map_status($map) eq $MAP_ERR_OPEN) { next MAP; } $self->set_target_map($map); $self->set_map_status($map, $MAP_NOT_DONE); $self->set_map_position($map, 0); # To avoid infinite loop, we enforce some artificial limit. my $loop_count = 0; my $max_retry = 1024 * 1024 * 512 * ($#$mta + 1); my $max_loop_count = $args->{ mta_max_retry } || $max_retry; MTA_RETRY_LOOP: while (1) { my $n_mta = 0; # avoid infinite loop if ($self->_is_stop_loop($loop_count++, $max_loop_count)) { $self->logdebug("too many smtp retry, give up map=$map"); $self->set_error("too many smtp retry, give up"); last MTA_RETRY_LOOP; } # check the number of effective MTA's. { my $is_valid_mta = 0; MTA: for my $mta (@$mta) { unless ($self->get_mta_status($mta) eq $MTA_ERR_TIMEOUT || $self->get_mta_status($mta) eq $MTA_ERR_FATAL ) { $is_valid_mta++; } } unless ($is_valid_mta) { $self->log("no valid MTA"); last MTA_RETRY_LOOP; } } MTA: for my $mta (@$mta) { # not retry to broken MTA. # we may use a normal MTA frequently in results. if ($self->get_mta_status($mta) eq $MTA_ERR_TIMEOUT || $self->get_mta_status($mta) eq $MTA_ERR_FATAL ) { $self->logdebug("ignore $mta (fatal)"); next MTA; } else { if ($used_mta{ $mta }) { $self->logdebug("not try $mta (cached)"); } else { $self->logdebug("try $mta"); } } # uniq $mta next MTA if $used_mta{ $mta }; $used_mta{ $mta } = 1; # count the number of effective mta in this inter loop. $n_mta++; # o.k. try to deliver mail by using $mta. $self->logdebug("use $mta for map=$map"); $args->{ mta } = $mta; $self->_deliver($args); if ($self->get_map_status($map) eq $MAP_ERR_OPEN) { next MAP; } # we read the whole $map now. if ($self->get_map_status($map) eq $MAP_DONE) { last MTA; } } # MTA: loop if ($self->get_map_status($map) eq $MAP_DONE) { last MTA_RETRY_LOOP; } # NO effective mta in this inter loop. It impiles that # we used all MTA candidates. We reuse @mta again. if ($n_mta == 0) { $self->logdebug("we used all MTA candidates. reuse \$mta"); my (@c) = keys %used_mta; $self->logdebug("candidates = (@c)"); undef %used_mta; next MTA_RETRY_LOOP; } } # MTA_RETRY_LOOP loop } # MAP loop # check map status. MAP: for my $map (@$maps) { my $status = $self->get_map_status($map); next MAP if $status eq $MAP_ERR_OPEN; unless ($self->get_map_status($map) eq $MAP_DONE) { $self->_fallback_into_queue($args, $map, $status); } } # clean up recipient_map information after "all delivery". # CAUTION: this mapinfo tracks the delivery status. $self->clear_mapinfo; if ( $self->{ _num_recipients } ) { my $n = $self->{ _num_recipients }; my $queue = $args->{ queue } || undef; my $qid = defined($queue) ? $queue->id() : ''; if ($qid) { $self->log("status=sent total=$n qid=$qid"); } else { $self->log("status=sent total=$n"); } use File::Basename; my (@m) = (); for my $m (@$maps) { push(@m, basename($m));} $self->logdebug("status=sent total=$n maps=(@m)"); } } # Descriptions: check if this loop should stop or not. # Arguments: OBJ($self) NUM($count) NUM($limit) # Side Effects: none # Return Value: none sub _is_stop_loop { my ($self, $count, $limit) = @_; my $penalty_cost = $self->_get_retry_penalty() || 1; my $max_count = int( $limit / $penalty_cost ); $self->logdebug("stop? $count > $max_count"); if ($count > $max_count) { $self->logerror("too many error! $count > $max_count"); return 1; } else { return 0; } } # Descriptions: check if the specified map exists within queue or not. # Arguments: OBJ($self) STR($map) # Side Effects: none # Return Value: NUM sub _is_map_in_queue { my ($self, $map) = @_; my $queue_dir = $self->_get_queue_directory() || ''; if ($queue_dir && $map && $map =~ /$queue_dir/) { return 1; } else { return 0; } } # Descriptions: delivery fallback due to something error. # add this transaction into mail queue for later delivery. # Arguments: OBJ($self) HASH_REF($args) STR($map) STR($status) # Side Effects: add this transaction into mail queue for later delivery. # Return Value: none sub _fallback_into_queue { my ($self, $args, $map, $status) = @_; my $queue = $args->{ queue } || undef; # ASSERT my $retry_count = $queue->get_retry_count() || 0; my $in_queue_dir = $self->_is_map_in_queue($map) || 0; my $map_position = $self->get_map_position($map) || 0; if ($in_queue_dir && $retry_count > 0 && $map_position == 0) { # XXX we should hold this old queue as it is. $self->log("not need fallback, retry=$retry_count pos=0"); $self->set_not_done(); return; } else { my $msg = "in=$in_queue_dir retry=$retry_count pos=$map_position"; $self->log("fallback: (debug) $msg"); } # log current status. my $pos = $self->get_map_position($map) || 0; $self->logdebug("map=$map pos=$pos status=\"$status\""); # dump into queue. if (defined $args->{ use_queue_dir } && $args->{ use_queue_dir }) { my $queue_dir = $args->{ queue_dir } || ''; if ($queue_dir && -d $queue_dir) { my $msg = $args->{ message } || undef; my $sender = $args->{ smtp_sender } || ''; my $ra_rcpt = []; my $rcpt_total = 0; use IO::Adapter; my $obj = new IO::Adapter $map, $args->{ map_params }; if (defined $obj) { $obj->open || do { $self->logerror("cannot open $map"); $self->logerror("fatal: delivery fallback failed."); return; }; if ($pos) { $obj->setpos($pos); } my $rcpt; while (defined ($rcpt = $obj->get_next_key)) { $rcpt_total++; push(@$ra_rcpt, $rcpt); } } my $qid = '?'; eval q{ croak("no sender") unless $sender; use Mail::Delivery::Queue; my $queue = new Mail::Delivery::Queue { directory => $queue_dir, }; $queue->set('sender', $sender); if (@$ra_rcpt) { $queue->set('recipients', $ra_rcpt); } my $cur_print_mode = $msg->get_print_mode(); $msg->set_print_mode('raw'); $queue->in( $msg ) || croak("fail to queue in"); { my $error; if ($error = $queue->error()) { $self->logerror("fallback: $error"); } my $n = $queue->write_count(); $self->logdebug("queue: size=$n written"); } $msg->set_print_mode($cur_print_mode); unless ($queue->setrunnable()) { $self->logerror("queue-in failed."); } $qid = $queue->id(); # into deferred queue for later retry. $queue->sleep_queue(); }; unless ($@) { $self->log("fallback: total=$rcpt_total qid=$qid"); } else { $self->logerror("fallback error: $@"); $self->logerror("fatal: delivery fallback failed."); } } else { $self->logerror("fallback error: no queue"); } } else { $self->logerror("fallback error: no queue"); } } # Descriptions: ordinary SMTP sequence (see RFC821 for more details) # >220 I am some MTA ... # 250 ok # # >250 ok # # >250 oK # 354 ... # < message # <. # >250 oK # 221 good bye # Arguments: OBJ($self) HASH_REF($args) # Side Effects: remove error messages when we return from here # for the next _deliver() session. # Return Value: none sub _deliver { my ($self, $args) = @_; # reset error messages. $self->clear_error; $self->_init_delivery_transaction($args); # prepare smtp information my $myhostname = $args->{ myhostname } || 'localhost'; # 0. create BSD SOCKET as the communication terminal. # IF_ERROR_FOUND: do nothing and return as soon as possible my $socket = $self->_connect($args); my $is_connected = $self->is_socket_connected($socket); unless (defined($socket) && $is_connected) { my $mta = $args->{mta} || 'unknown'; $self->logdebug("cannot connect to $mta"); $self->_set_mta_as_fatal($args); return undef; } else { my $mta = $args->{mta} || 'unknown'; $self->logdebug("connected to $mta"); } # 1. receive the first "220 .." message # If you faces some error in this stage, you have to do nothing # since smtp connection has not established yet. # IF_ERROR_FOUND: do nothing and return as soon as possible $self->_read_reply; if ($self->get_error) { $self->_set_mta_as_timeout($args); $self->_set_mta_as_fatal($args); return; } # 2. EHLO/HELO; # IF_ERROR_FOUND: do nothing and return as soon as possible $self->_send_command("EHLO $myhostname"); $self->_read_reply; if ($self->get_error) { $self->clear_error; $self->_reset_smtp_transaction($args); # if EHLO fails, try HELO. $self->_send_command("HELO $myhostname"); $self->_read_reply; if ($self->get_error) { $self->_reset_smtp_transaction($args); $self->_set_mta_as_fatal($args); return; } } # 3. MAIL FROM; # IF_ERROR_FOUND: do nothing and return as soon as possible $self->_send_mail_from($args); if ($self->get_error) { $self->_reset_smtp_transaction($args); $self->_set_mta_as_fatal($args); return; } # 4. RCPT TO; ... send list of recipients # IF_ERROR_FOUND: roll back the process to the state before this $self->_send_recipient_list($args); if ($self->get_error) { $self->rollback_map_position; $self->_set_retry_penalty(); # XXX rollback gains penalty. $self->_reset_smtp_transaction($args); $self->_set_mta_as_fatal($args); return; } # 5. DATA; send the mail body itself # IF_ERROR_FOUND: handled in _send_data_to_mta(), so # return as soon as possible from here. $self->_send_data_to_mta($args); if ($self->get_error) { $self->rollback_map_position; $self->_reset_smtp_transaction($args); $self->_set_mta_as_fatal($args); return; } # 6. QUIT; SMTP session closing ... # IF_ERROR_FOUND: do nothing ? $self->_send_command("QUIT"); $self->_read_reply; if ($self->get_error) { $self->_reset_smtp_transaction; $self->_set_mta_as_fatal($args); return; } # o.k. succeded to deliver. my $n = $self->{ _num_recipients_in_this_transaction } || 0; if ($n) { my $mta = $args->{ mta } || 'unknown'; $self->logdebug("status=sent total=$n mta=$mta"); $self->{ _num_recipients } += $n; } } # Descriptions: initialize _deliver() process. # this routine is called at the first phase in _deliver(). # Arguments: OBJ($self) HASH_REF($args) # Side Effects: initialize object # Return Value: none sub _init_delivery_transaction { my ($self, $args) = @_; $self->set_last_command(''); $self->set_status_code(''); } # Descriptions: initialize logging interface. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _init_logging { my ($self, $args) = @_; # default logging interface (infomational). my ($fp_log_info) = $args->{ log_info_function } || undef; if (defined $fp_log_info) { $self->set_log_info_function($fp_log_info);} # error level log. my ($fp_log_error) = $args->{ log_error_function } || undef; if (defined $fp_log_error) { $self->set_log_error_function($fp_log_error);} # debug level log. my ($fp_log_debug) = $args->{ log_debug_function } || undef; if (defined $fp_log_debug) { $self->set_log_debug_function($fp_log_debug);} # smtp transaction logging interface. my ($fp_smtp_log) = $args->{ smtp_log_function } || undef; if (defined $fp_smtp_log) { $self->set_smtp_log_function($fp_smtp_log);} my ($handle) = $args->{smtp_log_handle} || undef; if (defined $handle) { $self->set_smtp_log_handle($handle);} } # Descriptions: get MTA list as ARRAY_REF. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: ARRAY_REF sub _get_mta_list { my ($self, $args) = @_; my $list = $args->{ smtp_servers } || '127.0.0.1:25'; $list =~ s/^\s*//; $list =~ s/\s*$//; my (@mta) = split(/\s+/, $list); return \@mta; } # Descriptions: get map list as ARRAY_REF. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: ARRAY_REF sub _get_map_list { my ($self, $args) = @_; my (@maps) = (); if ($args->{ recipient_maps }) { @maps = split(/\s+/, $args->{ recipient_maps }); } # alloc virtual recipient map if (ref( $args->{ recipient_array } ) eq 'ARRAY') { my $map = $args->{ recipient_array }; push(@maps, $map); } return \@maps; } ############################################################ ##### ##### MAIL FROM: ##### # Descriptions: send SMTP command "MAIL FROM". # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none # See Also: RFC821, RFC1123 # TODO: VERP's sub _send_mail_from { my ($self, $args) = @_; my $sender = $args->{ smtp_sender }; if (defined $sender) { $self->_send_command("MAIL FROM:<$sender>"); $self->_read_reply; } else { $self->logerror("smtp_sender undefined"); } } ############################################################ ##### ##### RCPT TO: ##### # Descriptions: We evaluate recipient_maps parameter here. # You can use a lot of classes for this directive: e.g. # file, UNIX's /etc/group, YP, SQL, LDAP, ... # Example: recipient_maps = file:members # unix.group:admin # mysql:toymodel # IO::Adapter class is essential to handle abstract # $recipient_map. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: $self->{ _retry_recipient_table } has recipients which # causes some errors. # _{set,get}_map_position() and _{set,get}_map_status() # tracks the delivery process. # Return Value: none sub _send_recipient_list_by_recipient_map { my ($self, $args) = @_; my $map = $self->get_target_map; use IO::Adapter; my $obj = new IO::Adapter $map, $args->{ map_params }; unless (defined $obj) { $self->set_map_status($map, $MAP_ERR_OPEN); $self->logerror("fail to create map=$map object."); return undef; } my $rcpt; my $num_recipients = 0; my $recipient_limit = $self->get_smtp_recipient_limit(); $obj->open || do { $self->set_error( $obj->error ); return undef; }; # roll back the previous file offset if ($self->get_map_position($map) > 0) { $obj->setpos( $self->get_map_position($map) ); } # address validate function. my $fp_address_validate = $self->get_address_validate_function(); # XXX $obj->get_recipient returns a mail address. RCPT_INPUT: while (defined ($rcpt = $obj->get_next_key)) { # firstly, validate the format of the specified address $rcpt. if (defined $fp_address_validate) { my $r = 0; eval q{ $r = &$fp_address_validate($rcpt);}; unless ($@) { unless ($r) { $self->smtplog("==> ignore invalid recipient <$rcpt>"); $self->logerror("invalid recipient <$rcpt>"); next RCPT_INPUT; } } else { $self->logerror("cannot call address validate function"); } } $num_recipients++; $self->_send_command("RCPT TO:<$rcpt>"); $self->_read_reply; # save addresses to retry later. my $action = $self->get_send_command_status(); if ($action eq $SMTP_ERR_RETRY) { # XXX-TODO: actual code required. $self->{ _retry_recipient_table }->{ $rcpt } = $SMTP_ERR_RETRY; } last RCPT_INPUT if $num_recipients >= $recipient_limit; } # save the current position in the file handle $self->set_map_position($map, $obj->getpos); # done. if ($obj->eof) { $self->set_map_status($map, $MAP_DONE); } # ends $obj->close; # count up the total number of recipients $self->{ _num_recipients_in_this_transaction } = $num_recipients; unless ($num_recipients) { $self->logerror("no recipient for $map"); $self->_send_command("RSET"); $self->_read_reply; } } # Descriptions: send "RCPT TO:" to MTA. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _send_recipient_list { my ($self, $args) = @_; # evaluate recipient_maps if ( $self->get_target_map ) { $self->_send_recipient_list_by_recipient_map($args); } } ############################################################ ##### ##### DATA: ##### # Descriptions: send the header part of the message to socket # Arguments: OBJ($self) HANDLE($socket) OBJ($header) # $ref_to_header is the FML::Header class object. # Side Effects: none # Return Value: none sub _send_header_to_mta { my ($self, $socket, $header) = @_; # ASSERT unless (defined $socket) { $self->logerror("_send_header_to_mta: socket undefined"); return; } unless (defined $header) { $self->logerror("_send_header_to_mta: header undefined"); return; } my $h = $header->as_string($socket); $h =~ s/\n/\r\n/g; print $socket $h; $self->smtplog($h); } # Descriptions: send the body part of the message to socket # Arguments: OBJ($self) HANDLE($socket) OBJ($msg) # Side Effects: none # Return Value: none sub _send_body_to_mta { my ($self, $socket, $msg) = @_; # ASSERT unless (defined $socket) { $self->logerror("_send_body_to_mta: socket undefined"); return; } unless (defined $msg) { $self->logerror("_send_body_to_mta: message undefined"); return; } # XXX $msg is Mail::Message object. my $fp = $self->get_smtp_log_function(); $msg->set_log_function($fp, $fp); $msg->set_print_mode('smtp'); $msg->print($socket); my $wh = $self->get_smtp_log_handle(); if (defined $wh) { $msg->print( $wh ); } $msg->unset_log_function(); } # Descriptions: send message itself to file handle (BSD socket here). # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _send_data_to_mta { my ($self, $args) = @_; # prepare smtp information my $header = $args->{ message }->whole_message_header; my $body = $args->{ message }->whole_message_body; my $socket = $self->get_socket() || undef; if (defined $body) { $self->_send_command("DATA"); $self->_read_reply; # XXX if "DATA" transaction cannot start, retry ? if ($self->get_status_code != '354' || $self->get_error) { $self->logerror($self->get_error); return undef; } # 1. header; send header $self->_send_header_to_mta($socket, $header); # 2. separator between header and body print $socket "\r\n"; $self->smtplog("\r\n"); # 3. body; send(copy) body on memory to socket each line $self->_send_body_to_mta($socket, $body); # end "DATA" transaction $self->_send_command("\r\n."); $self->_read_reply; } } ############################################################ ##### ##### QUIT / RSET ##### # Descriptions: send the SMTP reset "RSET" command. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _reset_smtp_transaction { my ($self, $args) = @_; # mark this mta is invalid. $self->_set_mta_as_timeout($args); # reset SMTP transaction. $self->_send_command("RSET"); $self->_read_reply; $self->log("reset smtp transcation"); } # Descriptions: mark this mta should be ignored. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _set_mta_as_timeout { my ($self, $args) = @_; my $mta = $args->{ mta } || ''; if ($mta) { $self->set_mta_status($mta, $MTA_ERR_TIMEOUT); } } # Descriptions: mark this mta should be ignored. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: none sub _set_mta_as_fatal { my ($self, $args) = @_; my $mta = $args->{ mta } || ''; if ($mta) { $self->set_mta_status($mta, $MTA_ERR_FATAL); } } # Descriptions: set attributes # Arguments: OBJ($self) STR($key) STR($value) # Side Effects: update $self # Return Value: none sub _set_attribute { my ($self, $key, $value) = @_; $self->{ _attr }->{ $key } = $value; } # Descriptions: get attributes # Arguments: OBJ($self) STR($key) # Side Effects: none # Return Value: STR sub _get_attribute { my ($self, $key, $value) = @_; return ( $self->{ _attr }->{ $key } || undef ); } # Descriptions: set retry penalty cost. # Arguments: OBJ($self) # Side Effects: update $self # Return Value: none sub _set_retry_penalty { my ($self) = @_; my $penalty = $self->{ _retry_penalty } || 1; $self->{ _retry_penalty } = 2 * $penalty; } # Descriptions: get retry penalty cost. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub _get_retry_penalty { my ($self) = @_; return ($self->{ _retry_penalty } || 1); } # Descriptions: save queue_directory. # Arguments: OBJ($self) STR($queue_dir) # Side Effects: update $self # Return Value: none sub _set_queue_directory { my ($self, $queue_dir) = @_; $self->{ _queue_directory } = $queue_dir || ''; } # Descriptions: return queue_directory. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub _get_queue_directory { my ($self) = @_; return( $self->{ _queue_directory } || '' ); } # Descriptions: set NOT DONE flag. # Arguments: OBJ($self) # Side Effects: update $self # Return Value: none sub set_not_done { my ($self) = @_; $self->{ _not_done } = 1; } # Descriptions: check NOT DONE flag. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_not_done { my ($self) = @_; return( $self->{ _not_done } || 0 ); } =head1 SEE ALSO L, L, L, L, L See I on C which replaces sendmail with little effort but provides a lot of compatibility except for sendmail.cf. =head1 CODING STYLE See C on fml coding style guide. =head1 AUTHOR Ken'ichi Fukamachi =head1 COPYRIGHT Copyright (C) 2000,2001,2002,2003,2004,2006,2012 Ken'ichi Fukamachi All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 HISTORY Mail::Delivery::SMTP first appeared in fml8 mailing list driver package. See C for more details. =cut 1;