aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew@mysociety.org>2017-03-17 22:18:45 +0000
committerMatthew Somerville <matthew-github@dracos.co.uk>2017-03-28 13:10:38 +0100
commitb26da0da5e1f8631646a34fdacbce9bb5bc3b706 (patch)
treecd42e16d51054606b85bf6ff28a47e586437406c
parent02fcb1606bc2b739fdc798e5ca06f2ed1b6bf6ea (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-example3
-rw-r--r--cpanfile6
-rw-r--r--cpanfile.snapshot290
-rw-r--r--perllib/Email/Sender/Transport/SMTP.pm380
-rw-r--r--perllib/FixMyStreet/App.pm11
-rw-r--r--perllib/FixMyStreet/App/Model/EmailSend.pm19
-rw-r--r--perllib/FixMyStreet/Email.pm4
-rw-r--r--perllib/FixMyStreet/Email/Sender.pm50
-rw-r--r--perllib/FixMyStreet/EmailSend.pm78
-rw-r--r--perllib/FixMyStreet/EmailSend/Variable.pm17
-rw-r--r--perllib/FixMyStreet/TestMech.pm9
-rw-r--r--t/app/helpers/send_email.t5
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: ''
diff --git a/cpanfile b/cpanfile
index c96e05f36..2907f7633 100644
--- a/cpanfile
+++ b/cpanfile
@@ -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