diff options
author | Matthew Somerville <matthew@mysociety.org> | 2017-03-17 22:18:45 +0000 |
---|---|---|
committer | Matthew Somerville <matthew-github@dracos.co.uk> | 2017-03-28 13:10:38 +0100 |
commit | b26da0da5e1f8631646a34fdacbce9bb5bc3b706 (patch) | |
tree | cd42e16d51054606b85bf6ff28a47e586437406c | |
parent | 02fcb1606bc2b739fdc798e5ca06f2ed1b6bf6ea (diff) |
Upgrade to using Email::Sender.
Email::Send is long deprecated and uses submodules that no longer work
correctly (e.g. Net::SMTP::TLS breaks with recent IO::Socket::SSL). We
create an Email::Sender subclass to perform the same functionality and
this also simplifies the email code with simpler envelope handling.
Bundle Email::Sender::Transport::SMTP to include fix from
https://github.com/rjbs/Email-Sender/issues/46
-rw-r--r-- | conf/general.yml-example | 3 | ||||
-rw-r--r-- | cpanfile | 6 | ||||
-rw-r--r-- | cpanfile.snapshot | 290 | ||||
-rw-r--r-- | perllib/Email/Sender/Transport/SMTP.pm | 380 | ||||
-rw-r--r-- | perllib/FixMyStreet/App.pm | 11 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Model/EmailSend.pm | 19 | ||||
-rw-r--r-- | perllib/FixMyStreet/Email.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/Email/Sender.pm | 50 | ||||
-rw-r--r-- | perllib/FixMyStreet/EmailSend.pm | 78 | ||||
-rw-r--r-- | perllib/FixMyStreet/EmailSend/Variable.pm | 17 | ||||
-rw-r--r-- | perllib/FixMyStreet/TestMech.pm | 9 | ||||
-rw-r--r-- | t/app/helpers/send_email.t | 5 |
12 files changed, 662 insertions, 210 deletions
diff --git a/conf/general.yml-example b/conf/general.yml-example index 6c694024d..794b0780b 100644 --- a/conf/general.yml-example +++ b/conf/general.yml-example @@ -180,7 +180,8 @@ TWITTER_KEY: '' TWITTER_SECRET: '' # If you wish to send email through a SMTP server elsewhere, change these -# variables. SMTP_TYPE should be one of '', 'ssl' or 'tls'. +# variables. SMTP_TYPE should be one of '', 'ssl' or 'tls'. SMTP_PORT will +# default to 587 (tls), 465 (ssl), or 25. SMTP_SMARTHOST: 'localhost' SMTP_TYPE: '' SMTP_PORT: '' @@ -49,8 +49,7 @@ requires 'DBIx::Class::Schema::Loader'; requires 'Digest::MD5'; requires 'Digest::SHA'; requires 'Email::MIME'; -requires 'Email::Send'; -requires 'Email::Send::SMTP'; +requires 'Email::Sender'; requires 'Email::Valid'; requires 'Error'; requires 'FCGI'; @@ -62,6 +61,7 @@ requires 'Getopt::Long::Descriptive'; requires 'HTML::Entities'; requires 'HTTP::Request::Common'; requires 'Image::Size'; +requires 'IO::Socket::SSL', '2.007'; requires 'IO::String'; requires 'JSON::MaybeXS'; requires 'Locale::gettext'; @@ -76,8 +76,6 @@ requires 'Net::DNS::Resolver'; requires 'Net::Domain::TLD', '1.75'; requires 'Net::Facebook::Oauth2'; requires 'Net::OAuth'; -requires 'Net::SMTP::SSL', '1.03'; -requires 'Net::SMTP::TLS'; requires 'Net::Twitter::Lite::WithAPIv1_1'; requires 'Path::Class'; requires 'POSIX'; diff --git a/cpanfile.snapshot b/cpanfile.snapshot index 2c565f8cd..c842dad99 100644 --- a/cpanfile.snapshot +++ b/cpanfile.snapshot @@ -214,16 +214,6 @@ DISTRIBUTIONS Storable 0 String::CRC32 0 Time::HiRes 0 - Carp-1.26 - pathname: Z/ZE/ZEFRAM/Carp-1.26.tar.gz - provides: - Carp 1.26 - Carp::Heavy 1.26 - requirements: - Exporter 0 - ExtUtils::MakeMaker 0 - IPC::Open3 1.0103 - Test::More 0 Capture-Tiny-0.40 pathname: D/DA/DAGOLDEN/Capture-Tiny-0.40.tar.gz provides: @@ -239,6 +229,18 @@ DISTRIBUTIONS perl 5.006 strict 0 warnings 0 + Carp-1.26 + pathname: Z/ZE/ZEFRAM/Carp-1.26.tar.gz + provides: + Carp 1.26 + Carp::Heavy 1.26 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + IPC::Open3 1.0103 + Test::More 0 + strict 0 + warnings 0 Carp-Assert-0.20 pathname: M/MS/MSCHWERN/Carp-Assert-0.20.tar.gz provides: @@ -2445,19 +2447,17 @@ DISTRIBUTIONS ExtUtils::CBuilder 0.27 ExtUtils::MakeMaker 0 perl 5.006 - Devel-StackTrace-1.30 - pathname: D/DR/DROLSKY/Devel-StackTrace-1.30.tar.gz + Devel-StackTrace-2.02 + pathname: D/DR/DROLSKY/Devel-StackTrace-2.02.tar.gz provides: - Devel::StackTrace 1.30 - Devel::StackTrace::Frame 1.30 + Devel::StackTrace 2.02 + Devel::StackTrace::Frame 2.02 requirements: - ExtUtils::MakeMaker 6.30 + ExtUtils::MakeMaker 0 File::Spec 0 Scalar::Util 0 - Test::More 0.88 - base 0 - bytes 0 overload 0 + perl 5.006 strict 0 warnings 0 Devel-StackTrace-AsHTML-0.14 @@ -2510,6 +2510,26 @@ DISTRIBUTIONS base 0 strict 0 warnings 0 + Email-Abstract-3.008 + pathname: R/RJ/RJBS/Email-Abstract-3.008.tar.gz + provides: + Email::Abstract 3.008 + Email::Abstract::EmailMIME 3.008 + Email::Abstract::EmailSimple 3.008 + Email::Abstract::MIMEEntity 3.008 + Email::Abstract::MailInternet 3.008 + Email::Abstract::MailMessage 3.008 + Email::Abstract::Plugin 3.008 + requirements: + Carp 0 + Email::Simple 1.998 + ExtUtils::MakeMaker 0 + MRO::Compat 0 + Module::Pluggable 1.5 + Scalar::Util 0 + perl 5.006 + strict 0 + warnings 0 Email-Address-1.898 pathname: R/RJ/RJBS/Email-Address-1.898.tar.gz provides: @@ -2582,25 +2602,61 @@ DISTRIBUTIONS overload 0 strict 0 warnings 0 - Email-Send-2.198 - pathname: R/RJ/RJBS/Email-Send-2.198.tar.gz - provides: - Email::Send 2.198 - Email::Send::NNTP 2.198 - Email::Send::Qmail 2.198 - Email::Send::SMTP 2.198 - Email::Send::Sendmail 2.198 - Email::Send::Test 2.198 + Email-Sender-1.300030 + pathname: R/RJ/RJBS/Email-Sender-1.300030.tar.gz + provides: + Email::Sender 1.300030 + Email::Sender::Failure 1.300030 + Email::Sender::Failure::Multi 1.300030 + Email::Sender::Failure::Permanent 1.300030 + Email::Sender::Failure::Temporary 1.300030 + Email::Sender::Manual 1.300030 + Email::Sender::Manual::QuickStart 1.300030 + Email::Sender::Role::CommonSending 1.300030 + Email::Sender::Role::HasMessage 1.300030 + Email::Sender::Simple 1.300030 + Email::Sender::Success 1.300030 + Email::Sender::Success::Partial 1.300030 + Email::Sender::Transport 1.300030 + Email::Sender::Transport::DevNull 1.300030 + Email::Sender::Transport::Failable 1.300030 + Email::Sender::Transport::Maildir 1.300030 + Email::Sender::Transport::Mbox 1.300030 + Email::Sender::Transport::Print 1.300030 + Email::Sender::Transport::SMTP 1.300030 + Email::Sender::Transport::SMTP::Persistent 1.300030 + Email::Sender::Transport::Sendmail 1.300030 + Email::Sender::Transport::Test 1.300030 + Email::Sender::Transport::Wrapper 1.300030 + Email::Sender::Util 1.300030 requirements: - Email::Address 1.80 - Email::Simple 1.92 + Carp 0 + Email::Abstract 3.006 + Email::Address 0 + Email::Simple 1.998 ExtUtils::MakeMaker 0 + Fcntl 0 + File::Basename 0 + File::Path 2.06 File::Spec 0 - Module::Pluggable 2.97 - Return::Value 1.28 - Scalar::Util 1.02 - Symbol 0 - Test::More 0.47 + IO::File 1.11 + IO::Handle 0 + List::Util 1.45 + Module::Runtime 0 + Moo 2.000000 + Moo::Role 0 + MooX::Types::MooseLike 0.15 + MooX::Types::MooseLike::Base 0 + Net::SMTP 3.07 + Scalar::Util 0 + Sub::Exporter 0 + Sub::Exporter::Util 0 + Sys::Hostname 0 + Throwable::Error 0.200003 + Try::Tiny 0 + strict 0 + utf8 0 + warnings 0 Email-Simple-2.102 pathname: R/RJ/RJBS/Email-Simple-2.102.tar.gz provides: @@ -3262,16 +3318,21 @@ DISTRIBUTIONS File::Temp 0 Scalar::Util 0 Test::More 0.88 - IO-Socket-SSL-1.84 - pathname: S/SU/SULLR/IO-Socket-SSL-1.84.tar.gz + IO-Socket-SSL-2.047 + pathname: S/SU/SULLR/IO-Socket-SSL-2.047.tar.gz provides: - IO::Socket::SSL 1.84 - IO::Socket::SSL::SSL_Context 1.84 - IO::Socket::SSL::SSL_HANDLE 1.84 - IO::Socket::SSL::Session_Cache 1.84 + IO::Socket::SSL 2.047 + IO::Socket::SSL::Intercept 2.014 + IO::Socket::SSL::OCSP_Cache 2.047 + IO::Socket::SSL::OCSP_Resolver 2.047 + IO::Socket::SSL::PublicSuffix undef + IO::Socket::SSL::SSL_Context 2.047 + IO::Socket::SSL::SSL_HANDLE 2.047 + IO::Socket::SSL::Session_Cache 2.047 + IO::Socket::SSL::Utils 2.014 requirements: ExtUtils::MakeMaker 0 - Net::SSLeay 1.21 + Net::SSLeay 1.46 Scalar::Util 0 IO-String-1.08 pathname: G/GA/GAAS/IO-String-1.08.tar.gz @@ -3847,34 +3908,36 @@ DISTRIBUTIONS Test::More 0 perl 5.008001 version 0 - Moo-1.003000 - pathname: H/HA/HAARG/Moo-1.003000.tar.gz + Moo-2.003001 + pathname: H/HA/HAARG/Moo-2.003001.tar.gz provides: Method::Generate::Accessor undef Method::Generate::BuildAll undef Method::Generate::Constructor undef Method::Generate::DemolishAll undef - Method::Inliner undef - Moo 1.003000 + Moo 2.003001 Moo::HandleMoose undef Moo::HandleMoose::FakeConstructor undef Moo::HandleMoose::FakeMetaClass undef + Moo::HandleMoose::_TypeMap undef Moo::Object undef - Moo::Role undef + Moo::Role 2.003001 Moo::_Utils undef Moo::_mro undef + Moo::_strictures undef Moo::sification undef - Sub::Defer undef - Sub::Quote undef oo undef requirements: Class::Method::Modifiers 1.1 Devel::GlobalDestruction 0.11 - Dist::CheckConflicts 0.02 + Exporter 5.57 ExtUtils::MakeMaker 0 - Module::Runtime 0.012 - Role::Tiny 1.003 - strictures 1.004003 + Module::Runtime 0.014 + Role::Tiny 2.000004 + Scalar::Util 0 + Sub::Defer 2.003001 + Sub::Quote 2.003001 + perl 5.006 MooX-Types-MooseLike-0.29 pathname: M/MA/MATEU/MooX-Types-MooseLike-0.29.tar.gz provides: @@ -4379,27 +4442,6 @@ DISTRIBUTIONS Test::More 0.66 Test::Warn 0.21 URI::Escape 3.28 - Net-SMTP-SSL-1.03 - pathname: R/RJ/RJBS/Net-SMTP-SSL-1.03.tar.gz - provides: - Net::SMTP::SSL 1.03 - requirements: - ExtUtils::MakeMaker 0 - IO::Socket::SSL 0 - Net::SMTP 0 - Test::More 0.47 - Net-SMTP-TLS-0.12 - pathname: A/AW/AWESTHOLM/Net-SMTP-TLS-0.12.tar.gz - provides: - Net::SMTP::TLS 0.12 - requirements: - Digest::HMAC_MD5 0 - ExtUtils::MakeMaker 0 - IO::Socket::INET 0 - IO::Socket::SSL 0 - MIME::Base64 0 - Net::SSLeay 0 - Test::More 0 Net-SSLeay-1.52 pathname: M/MI/MIKEM/Net-SSLeay-1.52.tar.gz provides: @@ -4901,15 +4943,14 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 Test::More 0.47 - Role-Tiny-1.003001 - pathname: H/HA/HAARG/Role-Tiny-1.003001.tar.gz + Role-Tiny-2.000005 + pathname: H/HA/HAARG/Role-Tiny-2.000005.tar.gz provides: - Role::Tiny 1.003001 - Role::Tiny::With undef + Role::Tiny 2.000005 + Role::Tiny::With 2.000005 requirements: - ExtUtils::MakeMaker 0 - Test::Fatal 0.003 - Test::More 0.96 + Exporter 5.57 + perl 5.006 SOAP-Lite-0.715 pathname: M/MK/MKUTTER/SOAP-Lite-0.715.tar.gz provides: @@ -5064,6 +5105,17 @@ DISTRIBUTIONS Safe::Isa 1.000002 requirements: ExtUtils::MakeMaker 0 + Scalar-List-Utils-1.47 + pathname: P/PE/PEVANS/Scalar-List-Utils-1.47.tar.gz + provides: + List::Util 1.47 + List::Util::XS 1.47 + Scalar::Util 1.47 + Sub::Util 1.47 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + perl 5.006 Scope-Guard-0.20 pathname: C/CH/CHOCOLATE/Scope-Guard-0.20.tar.gz provides: @@ -5084,6 +5136,15 @@ DISTRIBUTIONS Lingua::Stem::Snowball::Se 1.2 requirements: Test::More 0.42 + Socket-2.024 + pathname: P/PE/PEVANS/Socket-2.024.tar.gz + provides: + Socket 2.024 + requirements: + ExtUtils::CBuilder 0 + ExtUtils::Constant 0.23 + ExtUtils::MakeMaker 0 + perl 5.006001 Sort-Key-1.32 pathname: S/SA/SALVA/Sort-Key-1.32.tar.gz provides: @@ -5230,6 +5291,15 @@ DISTRIBUTIONS ExtUtils::MakeMaker 0 Test::Fatal 0.010 Test::More 0.47 + Sub-Quote-2.003001 + pathname: H/HA/HAARG/Sub-Quote-2.003001.tar.gz + provides: + Sub::Defer 2.003001 + Sub::Quote 2.003001 + requirements: + ExtUtils::MakeMaker 0 + Scalar::Util 0 + perl 5.006 Sub-Uplevel-0.24 pathname: D/DA/DAGOLDEN/Sub-Uplevel-0.24.tar.gz provides: @@ -5827,6 +5897,22 @@ DISTRIBUTIONS Text::Unidecode 0.04 requirements: ExtUtils::MakeMaker 0 + Throwable-0.200013 + pathname: R/RJ/RJBS/Throwable-0.200013.tar.gz + provides: + StackTrace::Auto 0.200013 + Throwable 0.200013 + Throwable::Error 0.200013 + requirements: + Carp 0 + Devel::StackTrace 1.32 + ExtUtils::MakeMaker 0 + Module::Runtime 0.002 + Moo 1.000001 + Moo::Role 0 + Scalar::Util 0 + Sub::Quote 0 + overload 0 Tie-IxHash-1.23 pathname: C/CH/CHORNY/Tie-IxHash-1.23.tar.gz provides: @@ -6357,6 +6443,50 @@ DISTRIBUTIONS Locale::gettext 1.05 requirements: ExtUtils::MakeMaker 0 + libnet-3.10 + pathname: S/SH/SHAY/libnet-3.10.tar.gz + provides: + Net undef + Net::Cmd 3.10 + Net::Config 3.10 + Net::Domain 3.10 + Net::FTP 3.10 + Net::FTP::A 3.10 + Net::FTP::E 3.10 + Net::FTP::I 3.10 + Net::FTP::L 3.10 + Net::FTP::_SSL_SingleSessionCache 3.10 + Net::FTP::dataconn 3.10 + Net::NNTP 3.10 + Net::NNTP::_SSL 3.10 + Net::Netrc 3.10 + Net::POP3 3.10 + Net::POP3::_SSL 3.10 + Net::SMTP 3.10 + Net::SMTP::_SSL 3.10 + Net::Time 3.10 + requirements: + Carp 0 + Errno 0 + Exporter 0 + ExtUtils::MakeMaker 6.64 + Fcntl 0 + File::Basename 0 + FileHandle 0 + Getopt::Std 0 + IO::File 0 + IO::Select 0 + IO::Socket 1.05 + POSIX 0 + Socket 2.016 + Symbol 0 + Time::Local 0 + constant 0 + perl 5.008001 + strict 0 + utf8 0 + vars 0 + warnings 0 libwww-perl-6.05 pathname: G/GA/GAAS/libwww-perl-6.05.tar.gz provides: diff --git a/perllib/Email/Sender/Transport/SMTP.pm b/perllib/Email/Sender/Transport/SMTP.pm new file mode 100644 index 000000000..c4eb6890c --- /dev/null +++ b/perllib/Email/Sender/Transport/SMTP.pm @@ -0,0 +1,380 @@ +package Email::Sender::Transport::SMTP; +# ABSTRACT: send email over SMTP +$Email::Sender::Transport::SMTP::VERSION = '1.300030'; +use Moo; + +use Email::Sender::Failure::Multi; +use Email::Sender::Success::Partial; +use Email::Sender::Role::HasMessage (); +use Email::Sender::Util; +use MooX::Types::MooseLike::Base qw(Bool Int Str HashRef); +use Net::SMTP 3.07; # SSL support, fixed datasend + +use utf8 (); # See below. -- rjbs, 2015-05-14 +use version (); + +#pod =head1 DESCRIPTION +#pod +#pod This transport is used to send email over SMTP, either with or without secure +#pod sockets (SSL/TLS). It is one of the most complex transports available, capable +#pod of partial success. +#pod +#pod For a potentially more efficient version of this transport, see +#pod L<Email::Sender::Transport::SMTP::Persistent>. +#pod +#pod =head1 ATTRIBUTES +#pod +#pod The following attributes may be passed to the constructor: +#pod +#pod =over 4 +#pod +#pod =item C<host>: the name of the host to connect to; defaults to C<localhost> +#pod +#pod =item C<ssl>: if 'starttls', use STARTTLS; if 'ssl' (or 1), connect securely; +#pod otherwise, no security +#pod +#pod =item C<ssl_options>: passed to Net::SMTP constructor for 'ssl' connections or +#pod to starttls for 'starttls' connections; should contain extra options for +#pod IO::Socket::SSL +#pod +#pod =item C<port>: port to connect to; defaults to 25 for non-SSL, 465 for 'ssl', +#pod 587 for 'starttls' +#pod +#pod =item C<timeout>: maximum time in secs to wait for server; default is 120 +#pod +#pod =cut + +sub BUILD { + my ($self) = @_; + Carp::croak("do not pass port number to SMTP transport in host, use port parameter") + if $self->host =~ /:/; +} + +has host => (is => 'ro', isa => Str, default => sub { 'localhost' }); +has ssl => (is => 'ro', isa => Str, default => sub { 0 }); + +has _security => ( + is => 'ro', + lazy => 1, + init_arg => undef, + default => sub { + my $ssl = $_[0]->ssl; + return '' unless $ssl; + $ssl = lc $ssl; + return 'starttls' if 'starttls' eq $ssl; + return 'ssl' if $ssl eq 1 or $ssl eq 'ssl'; + + Carp::cluck(qq{true "ssl" argument to Email::Sender::Transport::SMTP should be 'ssl' or 'startls' or '1' but got '$ssl'}); + + return 1; + }, +); + +has ssl_options => (is => 'ro', isa => HashRef, default => sub { {} }); + +has port => ( + is => 'ro', + isa => Int, + lazy => 1, + default => sub { + return $_[0]->_security eq 'starttls' ? 587 + : $_[0]->_security eq 'ssl' ? 465 + : 25 + }, +); + +has timeout => (is => 'ro', isa => Int, default => sub { 120 }); + +#pod =item C<sasl_username>: the username to use for auth; optional +#pod +#pod =item C<sasl_password>: the password to use for auth; required if C<username> is provided +#pod +#pod =item C<allow_partial_success>: if true, will send data even if some recipients were rejected; defaults to false +#pod +#pod =cut + +has sasl_username => (is => 'ro', isa => Str); +has sasl_password => (is => 'ro', isa => Str); + +has allow_partial_success => (is => 'ro', isa => Bool, default => sub { 0 }); + +#pod =item C<helo>: what to say when saying HELO; no default +#pod +#pod =item C<localaddr>: local address from which to connect +#pod +#pod =item C<localport>: local port from which to connect +#pod +#pod =cut + +has helo => (is => 'ro', isa => Str); +has localaddr => (is => 'ro'); +has localport => (is => 'ro', isa => Int); + +#pod =item C<debug>: if true, put the L<Net::SMTP> object in debug mode +#pod +#pod =back +#pod +#pod =cut + +has debug => (is => 'ro', isa => Bool, default => sub { 0 }); + +# I am basically -sure- that this is wrong, but sending hundreds of millions of +# messages has shown that it is right enough. I will try to make it textbook +# later. -- rjbs, 2008-12-05 +sub _quoteaddr { + my $addr = shift; + my @localparts = split /\@/, $addr; + my $domain = pop @localparts; + my $localpart = join q{@}, @localparts; + + return $addr # The first regex here is RFC 821 "specials" excepting dot. + unless $localpart =~ /[\x00-\x1F\x7F<>\(\)\[\]\\,;:@"]/ + or $localpart =~ /^\./ + or $localpart =~ /\.$/; + return join q{@}, qq("$localpart"), $domain; +} + +sub _smtp_client { + my ($self) = @_; + + my $class = "Net::SMTP"; + + my $smtp = $class->new( $self->_net_smtp_args ); + + unless ($smtp) { + $self->_throw( + sprintf "unable to establish SMTP connection to %s port %s", + $self->host, + $self->port, + ); + } + + if ($self->_security eq 'starttls') { + $self->_throw("can't STARTTLS: " . $smtp->message) + unless $smtp->starttls(%{ $self->ssl_options }); + } + + if ($self->sasl_username) { + $self->_throw("sasl_username but no sasl_password") + unless defined $self->sasl_password; + + unless ($smtp->auth($self->sasl_username, $self->sasl_password)) { + if ($smtp->message =~ /MIME::Base64|Authen::SASL/) { + Carp::confess("SMTP auth requires MIME::Base64 and Authen::SASL"); + } + + $self->_throw('failed AUTH', $smtp); + } + } + + return $smtp; +} + +sub _net_smtp_args { + my ($self) = @_; + + return ( + $self->host, + Port => $self->port, + Timeout => $self->timeout, + Debug => $self->debug, + + (($self->_security eq 'ssl') + ? (SSL => 1, %{ $self->ssl_options }) + : ()), + + defined $self->helo ? (Hello => $self->helo) : (), + defined $self->localaddr ? (LocalAddr => $self->localaddr) : (), + defined $self->localport ? (LocalPort => $self->localport) : (), + ); +} + +sub _throw { + my ($self, @rest) = @_; + Email::Sender::Util->_failure(@rest)->throw; +} + +sub send_email { + my ($self, $email, $env) = @_; + + Email::Sender::Failure->throw("no valid addresses in recipient list") + unless my @to = grep { defined and length } @{ $env->{to} }; + + my $smtp = $self->_smtp_client; + + my $FAULT = sub { $self->_throw($_[0], $smtp); }; + + $smtp->mail(_quoteaddr($env->{from})) + or $FAULT->("$env->{from} failed after MAIL FROM"); + + my @failures; + my @ok_rcpts; + + for my $addr (@to) { + if ($smtp->to(_quoteaddr($addr))) { + push @ok_rcpts, $addr; + } else { + # my ($self, $error, $smtp, $error_class, @rest) = @_; + push @failures, Email::Sender::Util->_failure( + undef, + $smtp, + recipients => [ $addr ], + ); + } + } + + # This logic used to include: or (@ok_rcpts == 1 and $ok_rcpts[0] eq '0') + # because if called without SkipBad, $smtp->to can return 1 or 0. This + # should not happen because we now always pass SkipBad and do the counting + # ourselves. Still, I've put this comment here (a) in memory of the + # suffering it caused to have to find that problem and (b) in case the + # original problem is more insidious than I thought! -- rjbs, 2008-12-05 + + if ( + @failures + and ((@ok_rcpts == 0) or (! $self->allow_partial_success)) + ) { + $failures[0]->throw if @failures == 1; + + my $message = sprintf '%s recipients were rejected during RCPT', + @ok_rcpts ? 'some' : 'all'; + + Email::Sender::Failure::Multi->throw( + message => $message, + failures => \@failures, + ); + } + + # restore Pobox's support for streaming, code-based messages, and arrays here + # -- rjbs, 2008-12-04 + + $smtp->data or $FAULT->("error at DATA start"); + + my $msg_string = $email->as_string; + my $hunk_size = $self->_hunk_size; + + while (length $msg_string) { + my $next_hunk = substr $msg_string, 0, $hunk_size, ''; + $smtp->datasend($next_hunk) or $FAULT->("error at during DATA"); + } + + $smtp->dataend or $FAULT->("error at after DATA"); + + my $message = $smtp->message; + + $self->_message_complete($smtp); + + # We must report partial success (failures) if applicable. + return $self->success({ message => $message }) unless @failures; + return $self->partial_success({ + message => $message, + failure => Email::Sender::Failure::Multi->new({ + message => 'some recipients were rejected during RCPT', + failures => \@failures + }), + }); +} + +sub _hunk_size { 2**20 } # send messages to DATA in hunks of 1 mebibyte + +sub success { + my $self = shift; + my $success = Moo::Role->create_class_with_roles('Email::Sender::Success', 'Email::Sender::Role::HasMessage')->new(@_); +} + +sub partial_success { + my $self = shift; + my $partial_success = Moo::Role->create_class_with_roles('Email::Sender::Success::Partial', 'Email::Sender::Role::HasMessage')->new(@_); +} + +sub _message_complete { $_[1]->quit; } + +#pod =head1 PARTIAL SUCCESS +#pod +#pod If C<allow_partial_success> was set when creating the transport, the transport +#pod may return L<Email::Sender::Success::Partial> objects. Consult that module's +#pod documentation. +#pod +#pod =cut + +with 'Email::Sender::Transport'; +no Moo; +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Email::Sender::Transport::SMTP - send email over SMTP + +=head1 VERSION + +version 1.300030 + +=head1 DESCRIPTION + +This transport is used to send email over SMTP, either with or without secure +sockets (SSL/TLS). It is one of the most complex transports available, capable +of partial success. + +For a potentially more efficient version of this transport, see +L<Email::Sender::Transport::SMTP::Persistent>. + +=head1 ATTRIBUTES + +The following attributes may be passed to the constructor: + +=over 4 + +=item C<host>: the name of the host to connect to; defaults to C<localhost> + +=item C<ssl>: if 'starttls', use STARTTLS; if 'ssl' (or 1), connect securely; +otherwise, no security + +=item C<ssl_options>: passed to Net::SMTP constructor for 'ssl' connections or +to starttls for 'starttls' connections; should contain extra options for +IO::Socket::SSL + +=item C<port>: port to connect to; defaults to 25 for non-SSL, 465 for 'ssl', +587 for 'starttls' + +=item C<timeout>: maximum time in secs to wait for server; default is 120 + +=item C<sasl_username>: the username to use for auth; optional + +=item C<sasl_password>: the password to use for auth; required if C<username> is provided + +=item C<allow_partial_success>: if true, will send data even if some recipients were rejected; defaults to false + +=item C<helo>: what to say when saying HELO; no default + +=item C<localaddr>: local address from which to connect + +=item C<localport>: local port from which to connect + +=item C<debug>: if true, put the L<Net::SMTP> object in debug mode + +=back + +=head1 PARTIAL SUCCESS + +If C<allow_partial_success> was set when creating the transport, the transport +may return L<Email::Sender::Success::Partial> objects. Consult that module's +documentation. + +=head1 AUTHOR + +Ricardo Signes <rjbs@cpan.org> + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2016 by Ricardo Signes. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +=cut diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index 9660d327a..35e8c2537 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -9,9 +9,11 @@ use FixMyStreet::Cobrand; use Memcached; use FixMyStreet::Map; use FixMyStreet::Email; +use FixMyStreet::Email::Sender; use Utils; use Path::Tiny 'path'; +use Try::Tiny; use URI; use URI::QueryParam; @@ -346,8 +348,13 @@ sub send_email { $data->{_html_images_} = \@inline_images if @inline_images; my $email = mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email($data) }; - my $return = $c->model('EmailSend')->send($email); - $c->log->error("$return") if !$return; + + try { + FixMyStreet::Email::Sender->send($email, { from => $sender }); + } catch { + my $error = $_ || 'unknown error'; + $c->log->error("$error"); + }; return $email; } diff --git a/perllib/FixMyStreet/App/Model/EmailSend.pm b/perllib/FixMyStreet/App/Model/EmailSend.pm deleted file mode 100644 index 93751d4a6..000000000 --- a/perllib/FixMyStreet/App/Model/EmailSend.pm +++ /dev/null @@ -1,19 +0,0 @@ -package FixMyStreet::App::Model::EmailSend; -use base 'Catalyst::Model::Factory'; - -use strict; -use warnings; - -=head1 NAME - -FixMyStreet::App::Model::EmailSend - -=head1 DESCRIPTION - -Catalyst Model wrapper around FixMyStreet::EmailSend - -=cut - -__PACKAGE__->config( - class => 'FixMyStreet::EmailSend', -); diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm index e0d82a8ef..ea84e3966 100644 --- a/perllib/FixMyStreet/Email.pm +++ b/perllib/FixMyStreet/Email.pm @@ -17,7 +17,7 @@ use mySociety::Random qw(random_bytes); use Utils::Email; use FixMyStreet; use FixMyStreet::DB; -use FixMyStreet::EmailSend; +use FixMyStreet::Email::Sender; sub test_dmarc { my $email = shift; @@ -187,7 +187,7 @@ sub send_cron { print $email->as_string; return 1; # Failure } else { - my $result = FixMyStreet::EmailSend->new({ env_from => $env_from })->send($email); + my $result = FixMyStreet::Email::Sender->try_to_send($email, { from => $env_from }); return $result ? 0 : 1; } } diff --git a/perllib/FixMyStreet/Email/Sender.pm b/perllib/FixMyStreet/Email/Sender.pm new file mode 100644 index 000000000..e6148a56c --- /dev/null +++ b/perllib/FixMyStreet/Email/Sender.pm @@ -0,0 +1,50 @@ +package FixMyStreet::Email::Sender; + +use parent Email::Sender::Simple; +use strict; +use warnings; + +use Email::Sender::Util; +use FixMyStreet; + +=head1 NAME + +FixMyStreet::Email::Sender + +=head1 DESCRIPTION + +Subclass of Email::Sender - configuring it correctly according to our config. + +If the config value 'SMTP_SMARTHOST' is set then email is routed via SMTP to +that. Otherwise it is sent using a 'sendmail' like binary on the local system. + +And finally if if FixMyStreet->test_mode returns true then emails are not sent +at all but are stored in memory for the test suite to inspect (using +Email::Send::Test). + +=cut + +sub build_default_transport { + if ( FixMyStreet->test_mode ) { + Email::Sender::Util->easy_transport(Test => {}); + } elsif ( my $smtp_host = FixMyStreet->config('SMTP_SMARTHOST') ) { + my $type = FixMyStreet->config('SMTP_TYPE') || ''; + my $port = FixMyStreet->config('SMTP_PORT') || ''; + my $username = FixMyStreet->config('SMTP_USERNAME') || ''; + my $password = FixMyStreet->config('SMTP_PASSWORD') || ''; + + my $ssl = $type eq 'tls' ? 'starttls' : $type eq 'ssl' ? 'ssl' : ''; + my $args = { + host => $smtp_host, + ssl => $ssl, + sasl_username => $username, + sasl_password => $password, + }; + $args->{port} = $port if $port; + Email::Sender::Util->easy_transport(SMTP => $args); + } else { + Email::Sender::Util->easy_transport(Sendmail => {}); + } +} + +1; diff --git a/perllib/FixMyStreet/EmailSend.pm b/perllib/FixMyStreet/EmailSend.pm deleted file mode 100644 index 09f434931..000000000 --- a/perllib/FixMyStreet/EmailSend.pm +++ /dev/null @@ -1,78 +0,0 @@ -package FixMyStreet::EmailSend; - -use strict; -use warnings; - -BEGIN { - # Should move away from Email::Send, but until then: - $Return::Value::NO_CLUCK = 1; -} - -use FixMyStreet; -use Email::Send; - -=head1 NAME - -FixMyStreet::EmailSend - -=head1 DESCRIPTION - -Thin wrapper around Email::Send - configuring it correctly according to our config. - -If the config value 'SMTP_SMARTHOST' is set then email is routed via SMTP to -that. Otherwise it is sent using a 'sendmail' like binary on the local system. - -And finally if if FixMyStreet->test_mode returns true then emails are not sent -at all but are stored in memory for the test suite to inspect (using -Email::Send::Test). - -=cut - -my $args = undef; - -if ( FixMyStreet->test_mode ) { - # Email::Send::Test - $args = { mailer => 'Test', }; -} elsif ( my $smtp_host = FixMyStreet->config('SMTP_SMARTHOST') ) { - # Email::Send::SMTP - my $type = FixMyStreet->config('SMTP_TYPE') || ''; - my $port = FixMyStreet->config('SMTP_PORT') || ''; - my $username = FixMyStreet->config('SMTP_USERNAME') || ''; - my $password = FixMyStreet->config('SMTP_PASSWORD') || ''; - - unless ($port) { - $port = 25; - $port = 465 if $type eq 'ssl'; - $port = 587 if $type eq 'tls'; - } - - my $mailer_args = [ - Host => $smtp_host, - Port => $port, - ]; - push @$mailer_args, ssl => 1 if $type eq 'ssl'; - push @$mailer_args, tls => 1 if $type eq 'tls'; - push @$mailer_args, username => $username, password => $password - if $username && $password; - $args = { - mailer => 'FixMyStreet::EmailSend::Variable', - mailer_args => $mailer_args, - }; -} else { - # Email::Send::Sendmail - $args = { mailer => 'Sendmail' }; -} - -sub new { - my ($cls, $hash) = @_; - $hash ||= {}; - my %args = ( %$args, %$hash ); - - my $sender = delete($args{env_from}); - if ($sender) { - $args{mailer_args} = [ @{$args{mailer_args}} ] if $args{mailer_args}; - push @{$args{mailer_args}}, env_from => $sender; - } - - return Email::Send->new(\%args); -} diff --git a/perllib/FixMyStreet/EmailSend/Variable.pm b/perllib/FixMyStreet/EmailSend/Variable.pm deleted file mode 100644 index 4ba56dd41..000000000 --- a/perllib/FixMyStreet/EmailSend/Variable.pm +++ /dev/null @@ -1,17 +0,0 @@ -package FixMyStreet::EmailSend::Variable; -use base Email::Send::SMTP; -use FixMyStreet; - -my $sender; - -sub send { - my ($class, $message, %args) = @_; - $sender = delete($args{env_from}) || FixMyStreet->config('DO_NOT_REPLY_EMAIL'); - $class->SUPER::send($message, %args); -} - -sub get_env_sender { - $sender; -} - -1; diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm index 122a5d0c9..c22789fb0 100644 --- a/perllib/FixMyStreet/TestMech.pm +++ b/perllib/FixMyStreet/TestMech.pm @@ -13,7 +13,7 @@ use Test::WWW::Mechanize::Catalyst 'FixMyStreet::App'; use Test::More; use Web::Scraper; use Carp; -use Email::Send::Test; +use FixMyStreet::Email::Sender; use JSON::MaybeXS; =head1 NAME @@ -182,7 +182,7 @@ Clear the email queue. sub clear_emails_ok { my $mech = shift; - Email::Send::Test->clear; + FixMyStreet::Email::Sender->default_transport->clear_deliveries; $mech->builder->ok( 1, 'cleared email queue' ); return 1; } @@ -199,7 +199,7 @@ sub email_count_is { my $mech = shift; my $number = shift || 0; - $mech->builder->is_num( scalar( Email::Send::Test->emails ), + $mech->builder->is_num( scalar( FixMyStreet::Email::Sender->default_transport->delivery_count ), $number, "checking for $number email(s) in the queue" ); } @@ -215,7 +215,8 @@ In list context returns all the emails (or none). sub get_email { my $mech = shift; - my @emails = Email::Send::Test->emails; + my @emails = FixMyStreet::Email::Sender->default_transport->deliveries; + @emails = map { $_->{email}->object } @emails; return @emails if wantarray; diff --git a/t/app/helpers/send_email.t b/t/app/helpers/send_email.t index 3975002fa..66b771292 100644 --- a/t/app/helpers/send_email.t +++ b/t/app/helpers/send_email.t @@ -19,7 +19,6 @@ use Test::LongString; use Catalyst::Test 'FixMyStreet::App'; -use Email::Send::Test; use Path::Tiny; use FixMyStreet::TestMech; @@ -31,7 +30,7 @@ my $c = ctx_request("/"); $c->stash->{foo} = 'bar'; # clear the email queue -Email::Send::Test->clear; +$mech->clear_emails_ok; # send the test email FixMyStreet::override_config { @@ -42,7 +41,7 @@ FixMyStreet::override_config { }; # check it got templated and sent correctly -my @emails = Email::Send::Test->emails; +my @emails = $mech->get_email; is scalar(@emails), 1, "caught one email"; # Get the email, check it has a date and then strip it out |