File Coverage

blib/lib/Mojar/Message/Smtp.pm
Criterion Covered Total %
statement 18 80 22.5
branch 0 30 0.0
condition 0 22 0.0
subroutine 6 19 31.5
pod 2 10 20.0
total 26 161 16.1


line stmt bran cond sub pod time code
1             package Mojar::Message::Smtp;
2 1     1   213491 use Mojo::Base -base;
  1         10  
  1         7  
3              
4             our $VERSION = 0.041;
5              
6 1     1   164 use Carp ();
  1         2  
  1         13  
7 1     1   575 use MIME::Entity;
  1         70359  
  1         38  
8 1     1   417 use Mojar::Cron::Util 'tz_offset';
  1         8966  
  1         59  
9 1     1   356 use Mojar::Log;
  1         31029  
  1         5  
10 1     1   32 use POSIX 'strftime';
  1         2  
  1         3  
11              
12             # Attributes
13              
14             # Protocol
15             has ssl => 0;
16             has host => '127.0.0.1';
17             has port => sub { shift->ssl ? 465 : 25 };
18             has [qw(user secret agent)]; # SASL username, password
19             has domain => 'localhost.localdomain'; # for helo handshake
20             has timeout => 120;
21             has debug => 1;
22             #TODO: set 'debug => 0' before first CPAN release
23             has date_pattern => '%a, %d %b %Y %H:%M:%S';
24              
25             # Message
26             has From => sub { ($ENV{USER} // $ENV{USERNAME} // '_') .'@'. shift->domain };
27             has [qw(To Cc Bcc attachments)];
28             has [qw(Subject body)] => '';
29             has Type => 'text/plain';
30              
31             sub headers {
32 0     0 0   my $self = shift;
33 0 0         return $self->set(@_) if @_;
34 0           map +($_ => $self->{$_}), grep +(/^[A-Z]/), keys %$self # Titlecase fields
35             }
36             sub param {
37 0     0 0   my $self = shift;
38 0 0         return $self->set(@_) if @_;
39             return (
40 0   0       Host => $self->agent // $self->host,
41             Port => $self->port,
42             Hello => $self->domain,
43             Debug => $self->debug,
44             Timeout => $self->timeout
45             );
46             }
47              
48             has log => sub { Mojar::Log->new };
49              
50             # Public methods
51              
52             sub attach {
53 0     0 1   my ($self, %param) = @_;
54 0     0     my $fail = sub { $self->fail('Failed to attach', @_) };
  0            
55 0           %param = (
56             Disposition => 'attachment',
57             Encoding => '-SUGGEST',
58             %param # override defaults
59             );
60 0 0 0       if (exists $param{Path} and my $file = $param{Path}) {
61 0 0 0       $fail->('Failed to find attachment') unless -f $file or -l $file;
62 0 0         $fail->('Failed to read attachment') unless -r $file;
63             }
64 0   0       push @{ $self->{attachments} //= [] }, \%param;
  0            
65              
66 0           return $self;
67             }
68              
69             sub set {
70 0     0 0   my ($self, %param) = @_;
71 0           %$self = (%$self, %param);
72 0           return $self;
73             }
74              
75             sub reset {
76 0     0 0   my $self = shift;
77 0           delete @$self{ grep +(/^[A-Z]/), keys %$self }; # Titlecase fields
78 0           delete @$self{'body', 'attachments'};
79 0           return $self;
80             }
81              
82             sub connect {
83 0     0 0   my ($self, %param) = @_;
84 0     0     my $fail = sub { $self->fail('Failed to connect', @_) };
  0            
85              
86             #TODO: consider testing/reusing existing agent
87 0 0         $self->disconnect if $self->agent;
88              
89 0 0         my $class = $self->ssl ? 'Net::SMTP::SSL' : 'Net::SMTP';
90 0           (my $file = $class) =~ s{::}{/}g;
91 0 0         require "${file}.pm" or $fail->("Failed to load $class", $!);
92 0 0         my $agent = $class->new($self->param(%param))
93             or $self->fail('Connection rejected', $!);
94              
95 0 0         if ($self->user) {
96 0 0         $fail->('Missing required auth secret') unless defined $self->secret;
97 0 0         unless ($agent->auth($self->user, $self->secret)) {
98 0   0       my $msg = $agent->message // '';
99 0 0         $fail->('Missing MIME::Base64 (AUTH)') if $msg =~ /MIME::Base64/;
100 0 0         $fail->('Missing Authen::SASL (AUTH)') if $msg =~ /Authen::SASL/;
101 0           $fail->("Failed authentication\n$!\n$msg");
102             }
103             }
104 0           return $self->agent($agent);
105             }
106              
107             sub disconnect {
108 0     0 0   my ($self, %param) = @_;
109 0   0       $_ and $_->quit for delete $self->{agent};
110 0           return $self;
111             }
112              
113             sub send {
114 0     0 1   my ($self, %param) = @_;
115 0     0     my $fail = sub { $self->fail('Failed to send', @_) };
  0            
116              
117 0           $self->{Date} = $self->date;
118 0   0       $self->{Sender} //= $self->{From};
119 0   0       $self->{'X-Mailer'} //= "Mojar::Message::Smtp/$VERSION";
120              
121 0           my $mime;
122 0 0         if ($self->attachments) {
123 0           $mime = MIME::Entity->build(
124             Type => 'multipart/mixed',
125             $self->headers
126             );
127 0           $mime->attach(
128             Type => $self->Type,
129             Disposition => 'inline',
130             Encoding => '-SUGGEST',
131             Data => $self->body
132             );
133 0           $mime->attach(%$_) for @{$self->attachments};
  0            
134             }
135             else {
136 0           $mime = MIME::Entity->build(
137             Type => $self->Type,
138             Disposition => 'inline',
139             Encoding => '-SUGGEST',
140             $self->headers,
141             Data => $self->body
142             );
143             }
144              
145 0           my @sent = $mime->smtpsend($self->param, MailFrom => $self->From, %param);
146 0           $self->log->info(sprintf 'Sent email to %s', join q{,}, @sent);
147 0           return $self;
148             }
149              
150 0     0 0   sub date { strftime($_[0]->date_pattern, localtime) .' '. tz_offset }
151              
152             sub fail {
153 0     0 0   my $self = shift;
154 0           $self->log->error($_) for @_;
155 0           Carp::croak(join("\n", @_) ."\n");
156             }
157              
158             1;
159             __END__