1 package LJ
::DoSendEmail
;
4 use LJ
::User
::EmailStatus
;
17 $error = $_[0] if @_ > 0;
23 $status = $_[0] if @_ > 0;
29 $code = $_[0] if @_ > 0;
36 $details = $_[0] if @_ > 0;
45 # 0 - cannot connect to MX-host or email domain.
48 #sub log_complete_status {
49 # my $status_code = shift;
50 # my $emails = shift; # One email if scalar or list of emails if array ref.
51 # my $message = shift;
53 # LJ::User::Email->mark($status_code, $emails, $message);
56 sub set_resolver
{ $resolver = $_[1] }
57 sub resolver
{ $resolver ||= Net
::DNS
::Resolver
->new() }
61 use constant NO_RCPT
=> 1;
62 use constant NO_SUPPORTED_RCPT
=> 2;
63 use constant CONNECTION_FAILED
=> 3;
64 use constant SMTP_ERROR_NO_RCPT_ON_SERVER
=> 4;
65 use constant SMTP_ERROR_PERMANENT
=> 5;
66 use constant SMTP_ERROR_GENERAL
=> 6;
73 ## data = raw email with headers and bod
74 ## timeout = Maximum time, in seconds, to wait for a response from the SMTP server (perldoc Net::SMTP). Default: 300
76 ## hello_domain = ... (optional)
80 ## Returns one of constants defined above.
83 my ($rcpt, $opts) = @_;
85 ## do not issuing banned emails
86 return NO_SUPPORTED_RCPT
if LJ
::sysban_check
('email_domain', $rcpt);
89 my $from = $opts->{from
}; # Envelope From
90 my $data = $opts->{data
};
91 my $timeout = $opts->{timeout
} || 300;
92 my $hello_domain = $opts->{hello_domain
} || $LJ::DOMAIN
;
94 ## flush class properties
100 ## is there other side? ))
101 return NO_RCPT
unless $rcpt;
103 my ($host) = $rcpt =~ /\@(.+?)$/;
104 return NO_SUPPORTED_RCPT
unless $host;
107 if ($LJ::IS_DEV_SERVER
){
108 @ex = ('127.0.0.1'); ## use local relay
109 # @ex = ('172.19.1.1');
110 if ($opts->{internal_relays
}) {
111 warn "internal_relays";
112 @ex = ('172.19.1.1');
114 } elsif ( $opts->{internal_relays
} && scalar(@LJ::INTERNAL_MAIL_RELAYS
) ) {
115 @ex = @LJ::INTERNAL_MAIL_RELAYS
;
117 ## give me the numbers!
118 my @mailhosts = mx
(resolver
(), $host);
119 @ex = map { $_->exchange } @mailhosts;
122 # seen in wild: no MX records, but port 25 of domain is an SMTP server. think it's in SMTP spec too?
123 @ex = ($host) unless @ex;
125 my $smtp = Net
::SMTP
::BetterConnecting
->new(
127 Hello
=> $hello_domain,
132 # TODO: Need to handle the exact reason why we have no smtp connection here. In case of DNS related problems,
133 # or target host connection failure we need to handle a error here. It should make sense when we
134 # have some global problems on our side
136 $class->error("Connection failed to domain '$host', MXes: [@ex]");
138 return CONNECTION_FAILED
;
141 ## Maximum time, in seconds, to wait for a response from the SMTP server
142 $smtp->timeout($timeout);
143 # FIXME: need to detect timeouts to log to errors, so people with ridiculous timeouts can see that's why we're not delivering mail
145 my ($this_domain) = $from =~ /\@(.+)/;
149 $body =~ s/^(.+?\r?\n\r?\n)//s;
151 $headers =~ s/^bcc:.+\r?\n//mig; ## remove
153 ## sender_id should provide as much info for debug as possible.
154 ## For emails that send TheSchwartz worker is may be a
155 ## $job->handle->as_string.
157 ## Also $sender_id is used as mail id.
158 my $sender_id = $opts->{sender_id
};
161 require Sys
::Hostname
;
162 $sender_id = Sys
::Hostname
::hostname
();
163 $sender_id =~ s/[^-]+//;
165 $sender_id .= "-" . $$ . "-" . time();
168 # unless they specified a message ID, let's prepend our own:
169 unless ($headers =~ m!^message-id:.+!mi) {
170 my $rand = LJ
::rand_chars
(8);
171 my $message_id = qq|<sch
-$sender_id-$rand\@
$this_domain>|;
172 $headers = "Message-ID: $message_id\r\n" . $headers;
175 ## _do_send returns nothing on success or failed command on error.
176 my $res = $class->_do_send($smtp, $from, $rcpt, $sender_id,
178 $class->status($smtp->status);
179 eval { $class->code( $smtp->code ) };
180 my $details = eval { $smtp->code . " " . $smtp->message };
186 # #? $class->on_5xx_rcpt($job, $rcpt, $details->());
188 $class->error("Permanent failure during $res phase to [$rcpt]: $details \n");
191 LJ
::User
::EmailStatus
->handle_code(code
=> 5, email
=> $rcpt);
193 ## handle other errors
196 ## no need to retry attempts
197 return SMTP_ERROR_NO_RCPT_ON_SERVER
;
200 if ($class->status == 5){
201 return SMTP_ERROR_PERMANENT
;
204 return SMTP_ERROR_GENERAL
;
208 ## flush errors if they are.
209 LJ
::User
::EmailStatus
->handle_code(code
=> 0, email
=> $rcpt);
215 ## Send SMTP commands to server.
216 ## On success returns nothing
217 ## On error returns a command that failed.
220 my ($smtp, $env_from, $rcpt, $mail_id, $headers, $body) = @_;
222 my ($this_domain) = $env_from =~ /\@(.+)/;
224 ## Send command MAIL to server.
225 my $res = $smtp->mail($env_from);
227 ## In case of error return name of command that failed.
228 return "MAIL" unless $res;
230 ## Provide recipient to server
231 $res = $smtp->to($rcpt);
232 return "TO" unless $res; # return error
234 # have to add a fake "Received: " line in here, otherwise some
235 # stupid over-strict MTAs like bellsouth.net reject it thinking
236 # it's spam that was sent directly (it was). Called
238 $mail_id =~ s/-/00/; # not sure if hyphen is allowed in
239 my $date = _rfc2822_date
(time());
240 my $rcvd = qq{Received
: from localhost
(theschwartz
[127.0.0.1])
241 by
$this_domain (TheSchwartzMTA
) with ESMTP id
$mail_id;
245 $rcvd =~ s/\n\s+/\r\n\t/g;
248 ## Send commands to server. On error returns the stage name.
249 return "DATA" unless $smtp->data;
250 return "DATASEND" unless $smtp->datasend($rcvd . $headers . $body);
251 return "DATAEND" unless $smtp->dataend;
259 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) =
261 my @days = qw(Sun Mon Tue Wed Thu Fri Sat Sun);
262 my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
263 return sprintf("%s, %d %s %4d %02d:%02d:%02d +0000 (UTC)",
264 $days[$wday], $mday, $mon[$mon], $year+1900, $hour, $min, $sec);
267 package Net
::SMTP
::BetterConnecting
;
269 use base
'Net::SMTP';
273 # Net::SMTP's constructor could use improvement, so this is it:
274 # -- retry hosts, even if they connect and say "4xx service too busy", etc.
275 # -- let you specify different connect timeout vs. command timeout
278 my $type = ref($self) || $self;
285 $host = delete $arg{Host
};
288 my $hosts = defined $host ?
$host : $NetConfig{smtp_hosts
};
290 my $timeout = $arg{Timeout
} || 120;
291 my $connect_timeout = $arg{ConnectTimeout
} || $timeout;
294 foreach $h (@
{ref($hosts) ?
$hosts : [ $hosts ]}) {
295 $obj = $type->IO::Socket
::INET
::new
(PeerAddr
=> ($host = $h),
296 PeerPort
=> $arg{Port
} || 'smtp(25)',
297 LocalAddr
=> $arg{LocalAddr
},
298 LocalPort
=> $arg{LocalPort
},
300 Timeout
=> $connect_timeout,
304 $obj->timeout($timeout); # restore the original timeout
306 $obj->debug(exists $arg{Debug
} ?
$arg{Debug
} : undef);
308 my $res = $obj->response();
309 unless ($res == CMD_OK
) {
318 return undef unless $obj;
320 ${*$obj}{'net_smtp_exact_addr'} = $arg{ExactAddresses
};
321 ${*$obj}{'net_smtp_host'} = $host;
322 (${*$obj}{'net_smtp_banner'}) = $obj->message;
323 (${*$obj}{'net_smtp_domain'}) = $obj->message =~ /\A\s*(\S+)/;
325 unless ($obj->hello($arg{Hello
} || "")) {