#!/usr/bin/perl # # Send mail outbound using a weighted random selection. # Supports a variety of mail protocols. # package LJ; use strict; use Text::Wrap (); use MIME::Lite (); use Time::HiRes qw/ gettimeofday tv_interval /; use IO::Socket::INET (); # temp, for use with DMTP require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; sub maildebug { return unless $LJ::EMAIL_OUTGOING_DEBUG; print STDERR "ljmail: " . shift() . "\n"; } sub store_message { my ( $data, $type ) = @_; $type ||= 'none'; maildebug "Storing message for retry."; my $time = [ gettimeofday() ]; # try this on each cluster my $frozen = Storable::nfreeze($data); my $rval = LJ::do_to_cluster( sub { # first parameter is cluster id return LJ::cmd_buffer_add( shift(@_), 0, 'send_mail', $frozen ); } ); return undef unless $rval; my $notes = sprintf( "Queued mail send to %s %s: %s", $data->get('to'), $rval ? "succeeded" : "failed", $data->get('subject') ); maildebug $notes; LJ::blocking_report( $type, 'send_mail', tv_interval($time), $notes ); # we only attempt to store the message # on delivery failure. if we're here, something # failed, so always return false. return 0; } # # name: LJ::send_mail # des: Sends email. Character set will only be used if message is not ascii. # args: opt[, async_caller] # des-opt: Hashref of arguments. Required: to, from, subject, body. # Optional: toname, fromname, cc, bcc, charset, wrap # sub send_mail { my $opts = shift; my $async_caller = shift; my $time = [gettimeofday()]; my ( $proto, # what protocol we decided to use $msg, # email message (ascii) $data, # email message (MIME::Lite) $server, # remote server object $hostname # hostname of mailserver selected ); # support being given a direct MIME::Lite object, # for queued cmdbuffer 'frozen' retries $data = ( ref $opts eq 'MIME::Lite' ) ? $opts : build_message($opts); return 0 unless $data; $msg = $data->as_string(); # ok, we're sending via the network. # get a preferred server/protocol, or failover to cmdbuffer. ( $server, $proto, $hostname ) = find_server(); unless ( $server && $proto ) { maildebug "Suitable mail transport not found."; return store_message $data, undef; } my $info = "$hostname-$proto"; # Now we have an active server connection, # and we know what protocol to use. # clean addresses. my ( @recips, %headers ); $headers{$_} = $data->get( $_ ) foreach qw/ from to cc bcc /; $opts->{'from'} = ( Mail::Address->parse( $data->get('from') ) )[0]->address() if $headers{'from'}; push @recips, map { $_->address() } Mail::Address->parse( $headers{'to'} ) if $headers{'to'}; push @recips, map { $_->address() } Mail::Address->parse( $headers{'cc'} ) if $headers{'cc'}; push @recips, map { $_->address() } Mail::Address->parse( $headers{'bcc'} ) if $headers{'bcc'}; unless (scalar @recips) { maildebug "No recipients to send to!"; return 0; } # QMTP if ( $proto eq 'qmtp' ) { $server->recipient($_) foreach @recips; $server->sender( $opts->{'from'} ); $server->message($msg); # send! my $response = $server->send() or return store_message $data, $info; foreach ( keys %$response ) { return store_message $data, $info if $response->{$_} !~ /success/; } $server->disconnect(); } # SMTP if ( $proto eq 'smtp' ) { $server->mail( $opts->{'from'} ); # this would only fail on denied relay access # or somesuch. return store_message $data, $info unless $server->to( join ', ', @recips ); $server->data(); $server->datasend($msg); $server->dataend(); $server->quit; } # DMTP (Danga Mail Transfer Protocol) # (slated for removal if our QMTP stuff is worry-free.) if ( $proto eq 'dmtp' ) { my $len = length $msg; my $env = $opts->{'from'}; $server->print("Content-Length: $len\r\n"); $server->print("Envelope-Sender: $env\r\n\r\n$msg"); return store_message $data, $info unless $server->getline() =~ /^OK/; } # system mailer if ( $proto eq 'sendmail' ) { MIME::Lite->send( 'sendmail', $hostname ); unless ( $data->send() ) { maildebug "Unable to send via system mailer!"; return store_message $data, 'sendmail'; } } report( $data, $time, $info, $async_caller ); return 1; } sub report { my ( $data, $time, $info, $async_caller ) = @_; # report deliveries my $notes = sprintf( "Direct mail send to %s succeeded: %s", $data->get('to') || $data->get('cc') || $data->get('bcc'), $data->get('subject') ); maildebug $notes; LJ::blocking_report( $info, 'send_mail', tv_interval( $time ), $notes ) unless $async_caller; return; } # locate a network server, # return (serverobj, protocol, hostname) sub find_server { # operate on a copy of the server list. my @objects = @LJ::MAIL_TRANSPORTS; # backwards compatibility with earlier ljconfig. unless (@objects) { push @objects, [ 'sendmail', $LJ::SENDMAIL, 0 ] if $LJ::SENDMAIL; push @objects, [ 'smtp', $LJ::SMTP_SERVER, 0 ] if $LJ::SMTP_SERVER; push @objects, [ 'dmtp', $LJ::DMTP_SERVER, 1 ] if $LJ::DMTP_SERVER; } my ( $server, $proto, $hostname ); while ( @objects && !$proto ) { my $item = get_slice(@objects); my $select = $objects[$item]; maildebug "Trying server $select->[1] ($select->[0])..."; # check service connectivity # QMTP if ( $select->[0] eq 'qmtp' ) { eval 'use Net::QMTP'; if ($@) { maildebug "Net::QMTP not installed?"; splice @objects, $item, 1; next; } eval { $server = Net::QMTP->new( $select->[1], ConnectTimeout => 10 ); }; } # SMTP elsif ( $select->[0] eq 'smtp' ) { eval 'use Net::SMTP'; if ($@) { maildebug "Net::SMTP not installed?"; splice @objects, $item, 1; next; } eval { $server = Net::SMTP->new( $select->[1], Timeout => 10 ); }; } # DMTP elsif ( $select->[0] eq 'dmtp' ) { my $host = $select->[1]; my $port = $host =~ s/:(\d+)$// ? $1 : 7005; $server = IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port, Proto => 'tcp' ); } # system sendmail binary elsif ( $select->[0] eq 'sendmail' ) { my $sendmail = $1 if $select->[1] =~ /(\S+)/; $server = $sendmail if -e $sendmail && -x _; } else { maildebug "Unknown mail protocol"; splice @objects, $item, 1; next; } # do we have a server connection? # if not, remove from our selection pool and try again. if ( ! $server ) { maildebug "Could not connect"; splice @objects, $item, 1; } else { maildebug "Connected"; ( $proto, $hostname ) = ( $select->[0], $select->[1] ); } } return ( $server, $proto, $hostname ); } # return a ready to stringify MIME::Lite object. sub build_message { my $opts = shift; local($Text::Tabs::columns) = 20000; ##temp hack; default 76 my $body = $opts->{'wrap'} ? Text::Wrap::wrap( '', '', $opts->{'body'} ) : $opts->{'body'}; my $to = Mail::Address->new( $opts->{'toname'}, $opts->{'to'} ); my $from = Mail::Address->new( $opts->{'fromname'}, $opts->{'from'} ); my $msg = MIME::Lite->new ( To => $to->format(), From => $from->format(), Cc => $opts->{'cc'} || '', Bcc => $opts->{'bcc'} || '', Data => "$body\n", Subject => $opts->{'subject'}, ); return unless $msg; $msg->add(%{ $opts->{'headers'} }) if ref $opts->{'headers'}; $msg->attr("content-type.charset" => $opts->{'charset'}) if $opts->{'charset'} && ! (LJ::is_ascii($opts->{'body'}) && LJ::is_ascii($opts->{'subject'})); return $msg; } # return a weighted random slice from an array. sub get_slice { my @objects = @_; # Find cumulative values between weights, and in total. my (@csums, $cumulative_sum); @csums = map { $cumulative_sum += abs $_->[2] } @objects; # *nothing* has weight? (all zeros?) just choose one. # same thing as equal weights. return int rand scalar @objects unless $cumulative_sum; # Get a random number that will be compared to # the 'window' of probability for quotes. my $rand = rand $cumulative_sum; # Create number ranges between each cumulative value, # and check the random number to see if it falls within # the weighted 'window size'. # Remember the array slice for matching the original object to. my $lastval = 0; my $slice = 0; foreach (@csums) { last if $rand >= $lastval && $rand <= $_; $slice++; $lastval = $_; } return $slice; } 1;