Email::Sender::Transport::SMTP - send email over SMTP


Email-Sender documentation Contained in the Email-Sender distribution.

Index


Code Index:

NAME

Top

Email::Sender::Transport::SMTP - send email over SMTP

VERSION

Top

version 0.110001

DESCRIPTION

Top

This transport is used to send email over SMTP, either with or without secure sockets (SSL). It is one of the most complex transports available, capable of partial success.

For a potentially more efficient version of this transport, see Email::Sender::Transport::SMTP::Persistent.

ATTRIBUTES

Top

The following attributes may be passed to the constructor:

host: the name of the host to connect to; defaults to localhost
ssl: if true, connect via SSL; defaults to false
port: port to connect to; defaults to 25 for non-SSL, 465 for SSL
timeout: maximum time in secs to wait for server; default is 120
sasl_username: the username to use for auth; optional
sasl_password: the password to use for auth; required if username is provided
allow_partial_success: if true, will send data even if some recipients were rejected; defaults to false
helo: what to say when saying HELO; no default
localaddr: local address from which to connect
localport: local port from which to connect

PARTIAL SUCCESS

Top

If allow_partial_success was set when creating the transport, the transport may return Email::Sender::Success::Partial objects. Consult that module's documentation.

AUTHOR

Top

Ricardo Signes <rjbs@cpan.org>

COPYRIGHT AND LICENSE

Top


Email-Sender documentation Contained in the Email-Sender distribution.

package Email::Sender::Transport::SMTP;
BEGIN {
  $Email::Sender::Transport::SMTP::VERSION = '0.110001';
}
use Moose 0.90;
# ABSTRACT: send email over SMTP

use Email::Sender::Failure::Multi;
use Email::Sender::Success::Partial;
use Email::Sender::Util;


has host => (is => 'ro', isa => 'Str',  default => 'localhost');
has ssl  => (is => 'ro', isa => 'Bool', default => 0);
has port => (
  is  => 'ro',
  isa => 'Int',
  lazy    => 1,
  default => sub { return $_[0]->ssl ? 465 : 25; },
);

has timeout => (is => 'ro', isa => 'Int', default => 120);


has sasl_username => (is => 'ro', isa => 'Str');
has sasl_password => (is => 'ro', isa => 'Str');

has allow_partial_success => (is => 'ro', isa => 'Bool', default => 0);


has helo      => (is => 'ro', isa => 'Str');
has localaddr => (is => 'ro');
has localport => (is => 'ro', isa => 'Int');

# 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;

  # this is probably a little too paranoid
  return $addr unless $localpart =~ /[^\w.+-]/ or $localpart =~ /^\./;
  return join q{@}, qq("$localpart"), $domain;
}

sub _smtp_client {
  my ($self) = @_;

  my $class = "Net::SMTP";
  if ($self->ssl) {
    require Net::SMTP::SSL;
    $class = "Net::SMTP::SSL";
  } else {
    require Net::SMTP;
  }

  my $smtp = $class->new( $self->_net_smtp_args );

  $self->_throw("unable to establish SMTP connection") unless $smtp;

  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,
    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");
  $smtp->datasend($email->as_string) 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
    }),
  });
}

my %SUCCESS_CLASS;
BEGIN {
  $SUCCESS_CLASS{FULL} = Moose::Meta::Class->create_anon_class(
    superclasses => [ 'Email::Sender::Success' ],
    roles        => [ 'Email::Sender::Role::HasMessage' ],
    cache        => 1,
  );
  $SUCCESS_CLASS{PARTIAL} = Moose::Meta::Class->create_anon_class(
    superclasses => [ 'Email::Sender::Success::Partial' ],
    roles        => [ 'Email::Sender::Role::HasMessage' ],
    cache        => 1,
  );
}

sub success {
  my $self = shift;
  my $success = $SUCCESS_CLASS{FULL}->name->new(@_);
}

sub partial_success {
  my ($self, @args) = @_;
  my $obj = $SUCCESS_CLASS{PARTIAL}->name->new(@args);
  return $obj;
}

sub _message_complete { $_[1]->quit; }


with 'Email::Sender::Transport';
__PACKAGE__->meta->make_immutable;
no Moose;
1;

__END__