File Coverage

blib/lib/Net/SMTP/TLS.pm
Criterion Covered Total %
statement 30 177 16.9
branch 0 74 0.0
condition 0 15 0.0
subroutine 10 26 38.4
pod 0 13 0.0
total 40 305 13.1


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Net::SMTP::TLS - An SMTP client supporting TLS and AUTH
4              
5             =head1 VERSION
6              
7             Version 0.12
8              
9             =head1 SYNOPSIS
10              
11             use Net::SMTP::TLS;
12             my $mailer = new Net::SMTP::TLS(
13             'your.mail.host',
14             Hello => 'some.host.name',
15             Port => 25, #redundant
16             User => 'emailguy',
17             Password=> 's3cr3t');
18             $mailer->mail('emailguy@your.mail.host');
19             $mailer->to('someonecool@somewhere.else');
20             $mailer->data;
21             $mailer->datasend("Sent thru TLS!");
22             $mailer->dataend;
23             $mailer->quit;
24              
25             =head1 DESCRIPTION
26              
27             B is a TLS and AUTH capable SMTP client which offers an interface that users will find familiar from L. B implements a subset of the methods provided by that module, but certainly not (yet) a complete mirror image of that API.
28              
29             The methods supported by B are used in the above example. Though self explanatory for the most part, please see the perldoc for L if you are unclear.
30              
31             The differences in the methods provided are as follows:
32              
33             =over
34              
35             The I method does not take the options list taken by L
36              
37             The I method also does not take options, and is the only method available to set the recipient (unlike the many synonyms provided by L).
38              
39             The constructor takes a limited number of L's parameters. The constructor for B takes the following (in addition to the hostname of the mail server, which must be the first parameter and is not explicitly named):
40              
41             =over
42              
43             NoTLS - In the unlikely event that you need to use this class to perform non-TLS SMTP (you ought to be using Net::SMTP itself for that...), this will turn off TLS when supplied with a true value. This will most often cause an error related to authentication when used on a server that requires TLS
44              
45             Hello - hostname used in the EHLO command
46              
47             Port - port to connect to the SMTP service (defaults to 25)
48              
49             Timeout - Timeout for inital socket connection (defaults to 5, passed directly to L)
50              
51             User - username for SMTP AUTH
52              
53             Password - password for SMTP AUTH
54              
55             =back
56              
57             =back
58              
59             =head1 TLS and AUTHentication
60              
61             During construction of an B instance, the full login process will occur. This involves first sending EHLO to the server, then initiating a TLS session through STARTTLS. Once this is complete, the module will attempt to login using the credentials supplied by the constructor, if such credentials have been supplied.
62              
63             The AUTH method will depend on the features returned by the server after the EHLO command. Based on that, CRAM-MD5 will be used if available, followed by LOGIN, followed by PLAIN. Please note that LOGIN is the only method of authentication that has been tested. CRAM-MD5 and PLAIN login functionality was taken directly from the script mentioned in the acknowledgements section, however, I have not tested them personally.
64              
65             =head1 ERROR HANDLING
66              
67             This module will croak in the event of an SMTP error. Should you wish to handle this gracefully in your application, you may wrap your mail transmission in an eval {} block and check $@ afterward.
68              
69             =head1 ACKNOWLEDGEMENTS
70              
71             This code was blatantly plagiarized from Michal Ludvig's smtp-client.pl script. See L for his excellent work.
72              
73             =head1 AUTHOR
74              
75             Alexander Christian Westholm, awestholm at verizon dawt net
76              
77             Improvements courtesy of Tomek Zielinski
78              
79             =cut
80              
81             package Net::SMTP::TLS;
82              
83 1     1   138435 use strict;
  1         4  
  1         44  
84 1     1   6 use warnings;
  1         2  
  1         61  
85              
86             our $VERSION = '0.12';
87 1     1   6 use Carp;
  1         13  
  1         79  
88              
89 1     1   1398 use Net::SSLeay;
  1         56001  
  1         66  
90 1     1   1251 use IO::Socket::INET;
  1         42079  
  1         9  
91 1     1   2328 use IO::Socket::SSL;
  1         72714  
  1         11  
92 1     1   1379 use MIME::Base64 qw[encode_base64 decode_base64];
  1         850  
  1         82  
93 1     1   75123 use Digest::HMAC_MD5 qw[hmac_md5_hex];
  1         97787  
  1         71  
94              
95             BEGIN { #set up Net::SSLeay's internals
96 1     1   88 Net::SSLeay::load_error_strings();
97 1         5 Net::SSLeay::SSLeay_add_ssl_algorithms();
98 1         29 Net::SSLeay::randomize();
99             }
100              
101             sub new {
102 0     0 0   my $pkg = shift;
103 0           my $host= shift;
104 0           my %args= @_;
105 0           $args{Host} = $host;
106 0 0         $args{Hello}= "localhost" if not $args{Hello};
107             # make the non-SSL socket that will later be
108             # transformed
109 0 0 0       $args{sock} = new IO::Socket::INET(
      0        
110             PeerAddr => $host,
111             PeerPort => $args{Port} || 25,
112             Proto => 'tcp',
113             Timeout => $args{Timeout} || 5)
114             or croak "Connect failed :$@\n";
115            
116 0           my $me = bless \%args, $pkg;
117             # read the line immediately after connecting
118 0           my ($rsp,$txt) = $me->_response();
119 0 0         if(not $rsp == 220){
120 0           croak "Could not connect to SMTP server: $host $txt\n";
121             }
122 0           $me->hello(); # the first hello, 2nd after starttls
123 0 0         $me->starttls() if not $args{NoTLS}; # why we're here, after all
124 0 0 0       $me->login() if($me->{User} and $me->{Password});
125 0           return $me;
126             }
127              
128             # simply print a command to the server
129             sub _command {
130 0     0     my $me = shift;
131 0           my $command = shift;
132 0           $me->{sock}->printf($command."\015\012");
133             }
134              
135             # read a line from the server and parse the
136             # CODE SEPERATOR TEXT response format
137             sub _response {
138 0     0     my $me = shift;
139 0           my $line = $me->{sock}->getline();
140 0           my @rsp = ($line =~ /(\d+)(.)([^\r]*)/);
141             # reverse things so the seperator is at the end...
142             # that way we don't have to get fancy with the return
143             # values for calls that don't require the "more indicator"
144 0           return ($rsp[0],$rsp[2],$rsp[1]);
145             }
146              
147             # issue an EHLO command using the hostname provided to the constructor via
148             # the Hello paramter, which defaults to localhost. After that, read
149             # all the ESMTP capabilities returned by the server
150             sub hello {
151 0     0 0   my $me = shift;
152 0           $me->_command("EHLO ".$me->{Hello});
153 0           my ($num,$txt,$more) = $me->_response();
154 0 0         if(not $num == 250){
155 0           croak "EHLO command failed: $num $txt\n";
156             }
157 0           my %features = ();
158             # SMTP uses the dash to seperate the status code from
159             # the response text while there are more lines remaining
160 0           while($more eq '-'){
161 0           ($num,$txt,$more) = $me->_response();
162 0           $txt =~ s/[\n|\r]//g;
163 0           $txt =~ /(\S+)\s(.*)$/;
164 0           my ($feat, $parm) = ($txt =~ /^(\w+)[= ]*(.*)$/);
165 0           $features{$feat} = $parm;
166             }
167 0           $me->{features} = \%features;
168 0           return 1;
169             }
170              
171             # the magic! issue the STARTTLS command and
172             # use IO::Socket::SSL to transform that no-good
173             # plain old socket into an SSL socket
174             sub starttls {
175 0     0 0   my $me = shift;
176 0           $me->_command("STARTTLS");
177 0           my ($num,$txt) = $me->_response();
178 0 0         if(not $num == 220){
179 0           croak "Invalid response for STARTTLS: $num $txt\n";
180             }
181 0 0         if(not IO::Socket::SSL::socket_to_SSL($me->{sock},
182             SSL_version => "SSLv3 TLSv1")){
183 0           croak "Couldn't start TLS: ".IO::Socket::SSL::errstr."\n";
184             }
185 0           $me->hello();
186             }
187              
188             # based on the AUTH line returned in the features after EHLO,
189             # determine which type of authentication to perform
190             sub login {
191 0     0 0   my $me = shift;
192 0           my $type= $me->{features}->{AUTH};
193 0 0         if(not $type){
194 0           croak "Server did not return AUTH in capabilities\n";
195             }
196 0 0         if($type =~ /CRAM\-MD5/){
    0          
    0          
197 0           $me->auth_MD5();
198             }elsif($type =~ /LOGIN/){
199 0           $me->auth_LOGIN();
200             }elsif($type =~ /PLAIN/){
201 0           $me->auth_PLAIN();
202             }else{
203 0           croak "Unsupported Authentication mechanism\n";
204             }
205             }
206              
207             # perform a LOGIN authentication...
208             # works well on my box.
209             sub auth_LOGIN {
210 0     0 0   my $me = shift;
211 0           $me->_command("AUTH LOGIN");
212 0           my ($num,$txt) = $me->_response();
213 0 0         if(not $num == 334){
214 0           croak "Cannot authenticate via LOGIN: $num $txt\n";
215             }
216 0           $me->_command(encode_base64($me->{User},""));
217 0           ($num,$txt) = $me->_response();
218 0 0         if(not $num == 334){
219 0           croak "Auth failed: $num $txt\n";
220             }
221 0           $me->_command(encode_base64($me->{Password},""));
222 0           ($num,$txt) = $me->_response();
223 0 0         if(not $num == 235){
224 0           croak "Auth failed: $num $txt\n";
225             }
226             }
227              
228             # use MD5 to login... gets the ticket from the text
229             # of the line returned after the auth command is issued.
230             # NOTE: untested
231             sub auth_MD5{
232 0     0 0   my $me =shift;
233 0           $me->_command("AUTH CRAM-MD5");
234 0           my ($num,$txt) = $me->_response();
235 0 0         if(not $num == 334){
236 0           croak "Cannot authenticate via CRAM-MD5: $num $txt\n";
237             }
238 0 0         my $ticket = decode_base64($txt) or
239             croak "Unable to decode ticket";
240 0           my $md5_pass = hmac_md5_hex($ticket, $me->{Password});
241 0           $me->_command(encode_base64(
242             $me->{User}." ".$md5_pass, ""));
243 0           ($num,$txt) = $me->_response();
244 0 0         if(not $num == 235){
245 0           croak "Auth failed: $num $txt\n";
246             }
247             }
248              
249             # perform plain authentication
250             sub auth_PLAIN{
251 0     0 0   my $me = shift;
252 0           my $user= $me->{User};
253 0           my $pass= $me->{Password};
254 0           $me->_command(sprintf("AUTH PLAIN %S",
255             encode_base64("$user\0$user\0$pass","")));
256 0           my ($num,$txt) = $me->_response();
257 0 0         if(not $num == 235){
258 0           croak "Auth failed: $num $txt\n";
259             }
260             }
261              
262             sub _addr {
263 0     0     my $addr = shift;
264 0 0         $addr = "" unless defined $addr;
265              
266 0 0         return $1 if $addr =~ /(<[^>]*>)/;
267 0           $addr =~ s/^\s+|\s+$//sg;
268              
269 0           "<$addr>";
270             }
271              
272             # send the MAIL FROM: command
273             sub mail {
274 0     0 0   my $me = shift;
275 0           my $from= shift;
276 0           $me->_command("MAIL FROM: "._addr($from));
277 0           my ($num,$txt) = $me->_response();
278 0 0         if(not $num == 250){
279 0           croak "Could't set FROM: $num $txt\n";
280             }
281             }
282              
283             # send the RCPT TO: command
284             sub recipient
285             {
286 0     0 0   my $me = shift;
287              
288              
289 0           my $addr;
290 0           foreach $addr (@_)
291             {
292 0           $me->_command("RCPT TO: "._addr($addr));
293 0           my ($num,$txt) = $me->_response();
294 0 0         if(not $num == 250){
295 0           croak "Couldn't send TO <$addr>: $num $txt\n";
296             }
297             }
298             }
299              
300             BEGIN {
301 1     1   1610 *to = \&recipient;
302 1         3 *cc = \&recipient;
303 1         524 *bcc = \&recipient;
304             }
305              
306              
307             # start the body of the message
308             # I would probably have designed the public methods of
309             # this class differently, but this is to keep with
310             # Net::SMTP's API
311             sub data {
312 0     0 0   my $me = shift;
313 0           $me->_command("DATA");
314 0           my ($num,$txt) = $me->_response();
315 0 0         if(not $num == 354){
316 0           croak "Data failed: $num $txt\n";
317             }
318             }
319              
320             # send stuff over raw (for use as message body)
321             sub datasend {
322 0     0 0   my $cmd = shift;
323 0 0 0       my $arr = @_ == 1 && ref($_[0]) ? $_[0] : \@_;
324 0           my $line = join("" ,@$arr);
325              
326 0 0         return 0 unless defined(fileno($cmd->{sock}));
327              
328 0           my $last_ch = $cmd->{last_ch};
329 0 0         $last_ch = $cmd->{last_ch} = "\012" unless defined $last_ch;
330              
331 0 0         return 1 unless length $line;
332              
333 0           $line =~ tr/\r\n/\015\012/ unless "\r" eq "\015";
334              
335 0           my $first_ch = '';
336              
337 0 0         if ($last_ch eq "\015") {
    0          
338 0 0         $first_ch = "\012" if $line =~ s/^\012//;
339             }
340             elsif ($last_ch eq "\012") {
341 0 0         $first_ch = "." if $line =~ /^\./;
342             }
343              
344 0           $line =~ s/\015?\012(\.?)/\015\012$1$1/sg;
345              
346 0           substr($line,0,0) = $first_ch;
347              
348 0           $cmd->{last_ch} = substr($line,-1,1);
349              
350 0           my $len = length($line);
351 0           my $offset = 0;
352 0           my $win = "";
353 0           vec($win,fileno($cmd->{sock}),1) = 1;
354 0   0       my $timeout = $cmd->{sock}->timeout || undef;
355              
356 0 0         local $SIG{PIPE} = 'IGNORE' unless $^O eq 'MacOS';
357              
358 0           while($len)
359             {
360 0           my $wout;
361 0 0 0       if (select(undef,$wout=$win, undef, $timeout) > 0 or -f $cmd->{sock}) # -f for testing on win32
362             {
363 0           my $w = syswrite($cmd->{sock}, $line, $len, $offset);
364 0 0         unless (defined($w))
365             {
366 0           carp("Error: $!");
367 0           return undef;
368             }
369 0           $len -= $w;
370             }
371             else
372             {
373 0           carp("Error: Timeout");
374 0           return undef;
375             }
376             }
377              
378             }
379              
380             # end the message body submission by a line with nothing
381             # but a period on it.
382             sub dataend {
383 0     0 0   my $me = shift;
384 0           $me->_command("\015\012.");
385 0           my ($num,$txt) = $me->_response();
386 0 0         if(not $num == 250){
387 0           croak "Couldn't send mail: $num $txt\n";
388             }
389             }
390              
391             # politely disconnect from the SMTP server.
392             sub quit {
393 0     0 0   my $me = shift;
394 0           $me->_command("QUIT");
395 0           my ($num, $txt) = $me->_response();
396 0 0         if(not $num == 221){
397 0           croak "An error occurred disconnecting from the mail server: $num $txt\n";
398             }
399             }
400              
401             1;