File Coverage

blib/lib/Mail/Sendmail/Enhanced.pm
Criterion Covered Total %
statement 15 15 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 20 20 100.0


line stmt bran cond sub pod time code
1             package Mail::Sendmail::Enhanced;
2              
3 1     1   14390 use 5.008;
  1         2  
  1         40  
4              
5 1     1   5 use strict;
  1         0  
  1         27  
6 1     1   4 use warnings;
  1         5  
  1         36  
7              
8 1     1   517 use Encode qw(decode encode);
  1         7674  
  1         100  
9              
10 1     1   5741 use Mail::Sendmail '0.79_16';
  1         11288  
  1         141  
11             use MIME::Base64;
12              
13             our $VERSION = '0.02';
14              
15             ################################################################################
16              
17             sub new
18             {
19             my ( $this ) = ( shift );
20              
21             my $mail = {};
22             bless ( $mail, $this );
23              
24             while ( my $key = shift ) {
25             if ( ref ( $key ) eq 'HASH' ) {
26             foreach my $k (sort keys %{$key} ) {
27             $mail->{$k} = $$key{$k};
28             }
29             } else {
30             my $value = shift;
31             $mail->{$key} = $value;
32             }
33             }
34              
35             $mail->{smtp} ||= '';
36             $mail->{from} ||= '';
37             $mail->{charset} ||= 'utf-8';
38             $mail->{type} ||= 'text/plain';
39              
40             $mail->{user} ||= '';
41             $mail->{pass} ||= '';
42             $mail->{method} ||= 'LOGIN';
43             $mail->{required} ||= 1;
44              
45             $mail->{to} ||= '';
46             $mail->{cc} ||= '';
47             $mail->{subject} ||= 'No subject defined';
48             $mail->{message} ||= 'No message defined!';
49              
50             $mail->{attachments} ||= {};
51             $mail->{attachments_size_max} ||= 0; #no limit "-1" means no attachment allowed
52              
53             return $mail;
54             }
55              
56             ################################################################################
57              
58             sub send
59             {
60             my ( $self, $ARG ) = ( shift, shift );
61              
62             $ARG->{to} || $ARG->{cc} || die 'No to: or cc: email address given!';
63              
64             my $charset = $ARG->{charset} || $self->{charset} || 'utf-8';
65             my $type = $ARG->{type} || $self->{type} || 'text/plain';
66              
67             my $boundary = "====" . time() . "====";
68              
69             # Email subject is encoded using proper character encoding.
70             # original "encode_qp" function contains up to 2 arguments,
71             # but in a case of character set it is needed to start every
72             # new line with a statemant of the encoding, so - as a the
73             # third parameter - the charset is sent to the function.
74              
75             my $subject = $ARG->{subject} || $self->{subject} || '';
76              
77             my $flnoc = 67;
78             my $nlnoc = 78;
79             my $bol = " =?$charset?Q?";
80             my $eol = "?=\n";
81              
82             {
83             # this part consider multibytes characters and keep that folding
84             # does not divide the multibyte characters into two lines.
85             # The reason is that some email clients are not able to put
86             # together these separated bytes into one character.
87             require bytes;
88              
89              
90             my $t_subject = decode( $charset, $subject ) if $charset;
91              
92             if ( bytes::length($t_subject) > length($t_subject) || $t_subject =~ /[^\0-\xFF]/ ) {
93              
94             $subject = '';
95             my $t_string = ''; # substring of $t_subject which is testing if can be added to $subject
96             my $t_length = 0; # the length of $t_subject
97              
98             my $t_return = ''; # $t_string which match to the condition
99             my $t_result = ''; # encoded string of $t_string
100             my $t_number = 0; # number of row of the folded "Subject" field
101             while ( $t_subject ) {
102             foreach(0..$flnoc) {
103             $t_string = substr($t_subject,0,$_);
104             $t_result = encode_qp(encode($charset,$t_string),{bol=>$bol,eol=>$eol,flnoc=>0,nlnoc=>0,charset=>$charset,});
105              
106             #checking if encoded string $t_result of the tested substring $t_string satisfies length condition:
107             # and if yes we go out with the last good value $t_return and $t_subject get shorter by $t_length
108             last if length( $t_result ) > ($t_number ? $nlnoc : $flnoc);
109             $t_return = $t_result;
110             $t_length = length($t_string);
111             }
112             $subject = $subject.$t_return;
113             $t_subject = substr( $t_subject, $t_length );
114             $t_number++;
115             }
116              
117             } else {
118             $subject = encode_qp( $subject , { bol=>$bol, eol=>$eol, flnoc=>$flnoc, nlnoc=>$nlnoc, charset=>$charset, } );
119             }
120             }
121              
122             $subject = substr($subject,1);
123              
124             my %mail = (
125             'X-Mailer' => "This is Perl Mail::Sendmail::Enhanced version $Mail::Sendmail::Enhanced::VERSION",
126             'Content-Type' => "multipart/mixed; charset=$charset; boundary=\"$boundary\"",
127             'Smtp' => ($ARG->{smpt}|| $self->{smtp} ),
128             'From' => ($ARG->{from}|| $self->{from} ),
129             'To' => ($ARG->{to} || $self->{to} || '' ),
130             'Cc' => ($ARG->{cc} || $self->{cc} || '' ),
131             'Subject' => $subject,
132             auth => {
133             user => ($ARG->{user} || $self->{user} ),
134             pass => ($ARG->{pass} || $self->{pass} ),
135             method => ($ARG->{method} || $self->{method} ),
136             required => ($ARG->{required}|| $self->{required} ),
137             },
138             );
139              
140             $boundary = '--'.$boundary;
141             $mail{'Message'} = "$boundary\n"
142             ."Content-Type: $type; charset=$charset; format=flowed\n"
143             ."Content-Transfer-Encoding: 8bit\n\n"
144             .$ARG->{'message'}."\n";
145              
146             # ."Content-Transfer-Encoding: quoted-printable\n\n"
147             # .encode_qp( $ARG->{'message'}, {} )."\n";
148              
149             $ARG->{attachments} ||= $self->{attachments} || {};
150             $ARG->{attachments_size_max} ||= $self->{attachments_size_max} || 0; #no limit "-1" means no attachment allowed
151              
152             return "Attachments are not allowed whereas some are preperad to send!" if $ARG->{attachments} && $ARG->{attachments_size_max} < 0;
153             # attachment files are packed one by one into the message part each divided by boundary
154              
155             # checking attachments:
156             foreach my $fileName ( sort keys %{$ARG->{'attachments'}} ) {
157              
158             my $fileLocation = $ARG->{'attachments'}->{$fileName};
159              
160             # if does not exists:
161             return "Attachment does not exists! [$fileLocation]" unless -f $fileLocation;
162              
163             # if it is too big:
164             my $size = -s $fileLocation || 0;
165             return "Attachment too big! [$fileLocation: $size > ".$ARG->{attachments_size_max}."B max.]"
166             if $ARG->{attachments_size_max} > 0 && $size > $ARG->{attachments_size_max};
167             }
168              
169              
170             foreach my $fileName ( sort keys %{$ARG->{'attachments'}} ) {
171             my $fileLocation = $ARG->{'attachments'}->{$fileName};
172             if (open (my $F, $fileLocation )) {
173             my $input_record_separator = $/;
174             binmode $F; undef $/;
175             my $attachment = encode_base64(<$F>);
176             close $F;
177             $/ = $input_record_separator;
178              
179             $mail{'Message'} .= "$boundary\n"
180             ."Content-Type: application/octet-stream; name=\"$fileName\"\n"
181             ."Content-ID: <$fileName>\n"
182             ."Content-Transfer-Encoding: base64\n"
183             ."Content-Disposition: attachment; filename=\"$fileName\"\n\n"
184             ."$attachment\n";
185             }
186             }
187              
188             $mail{'Message'} .= "$boundary--\n";
189              
190              
191             return $Mail::Sendmail::error unless sendmail( %mail );
192              
193             return;
194             }
195              
196             ################################################################################
197              
198             sub encode_qp
199             {
200             ############################################################################
201             # This function is an exact copy of the that of the same name from the
202             # module: "MIME::QuotedPrint::Perl" '1.00' with the following changes:
203             # 1. The second argument can be scalar - as previously -
204             # or hash which would contain more information
205             # 2. There are changes in counting character in each line in accordance
206             # with hash sent to the function: it can be different in first line
207             # and the next ones. It is so, because usually in the firs line there
208             # is some word (Subject for instance).
209             # The behaviour of the function is identical with the original one in case
210             # we send two scalar arguments only.
211             ############################################################################
212              
213             # $res = text to be encoded
214             my ( $res ) = ( shift );
215             return '' unless $res;
216              
217             # The arguments can be sent in old way, when the second argument was the
218             # end of character rows, or in a new way - as a hash:
219             my %par = (
220             bol => " ", # characters at the begining of each lines
221             eol => "\n", # characters at the end of each line
222             flnoc => 68, # first line number of characters, 0 = unlimit
223             nlnoc => 78, # next lines number of characters, 0 = unlimit
224             );
225              
226             while ( my $key = shift ) {
227             if ( ref ( $key ) eq 'HASH' ) {
228             foreach my $k (sort keys %{$key} ) {
229             next unless $k =~ /^(bol|charset|eol|flnoc|nlnoc)$/;
230             next if $k eq 'flnoc' && $par{$k} !~ /^\d+$/;
231             next if $k eq 'nlnoc' && $par{$k} !~ /^\d+$/;
232              
233             $par{$k} = $$key{$k};
234             }
235             } else { # you can only send - as a second scalar argument the "EOL"
236             # characters in accordance with the original function
237             $par{eol} = $key;
238             }
239             }
240              
241             if ($] >= 5.006) {
242             require bytes;
243             if (bytes::length($res) > length($res) || ($] >= 5.008 && $res =~ /[^\0-\xFF]/))
244             {
245             require Carp;
246             Carp::croak("The Quoted-Printable encoding is only defined for bytes");
247             }
248             }
249              
250             # usefull shorthands
251             my $bol = $par{bol};
252             my $eol = $par{eol};
253             my $flnoc = $par{flnoc} - 0 - length($eol) - length($bol);
254             my $nlnoc = $par{nlnoc} - 1 - length($eol) - length($bol);
255             my $mid = '';
256             unless ( defined $bol ) { $mid = '='; $bol = '' }
257              
258             # Do not mention ranges such as $res =~ s/([^ \t\n!-<>-~])/sprintf("=%02X", ord($1))/eg;
259             # since that will not even compile on an EBCDIC machine (where ord('!') > ord('<')).
260             if (ord('A') == 193) { # EBCDIC style machine
261             if (ord('[') == 173) {
262             $res =~ s/([^ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z[\\\]^_`a-z{|}~])/sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp1047',$1))))/eg; # rule #2,#3
263             $res =~ s/([ \t]+)$/
264             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp1047',$_)))) }
265             split('', $1)
266             )/egm; # rule #3 (encode whitespace at eol)
267             }
268             elsif (ord('[') == 187) {
269             $res =~ s/([^ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z[\\\]^_`a-z{|}~])/sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('posix-bc',$1))))/eg; # rule #2,#3
270             $res =~ s/([ \t]+)$/
271             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('posix-bc',$_)))) }
272             split('', $1)
273             )/egm; # rule #3 (encode whitespace at eol)
274             }
275             elsif (ord('[') == 186) {
276             $res =~ s/([^ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z[\\\]^_`a-z{|}~])/sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp37',$1))))/eg; # rule #2,#3
277             $res =~ s/([ \t]+)$/
278             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp37',$_)))) }
279             split('', $1)
280             )/egm; # rule #3 (encode whitespace at eol)
281             }
282             }
283             else { # ASCII style machine
284             $res =~ s/([^ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z[\\\]^_`a-z{|}~])/sprintf("=%02X", ord($1))/eg; # rule #2,#3
285             $res =~ s/\n/=0A/g unless length($eol);
286             $res =~ s/([ \t]+)$/
287             join('', map { sprintf("=%02X", ord($_)) }
288             split('', $1)
289             )/egm; # rule #3 (encode whitespace at eol)
290             }
291              
292             return $res unless length($eol);
293              
294             # rule #5 (lines must be shorter than 76 chars, but we are not allowed
295             # to break =XX escapes. This makes things complicated :-( )
296             my $brokenlines = "";
297              
298             $brokenlines .= "$bol$1$mid$eol" if $flnoc && $res =~ s/(.*?^[^\n]{$flnoc} (?:
299             [^=\n]{2} (?! [^=\n]{0,1} $) # 75 not followed by .?\n
300             |[^=\n] (?! [^=\n]{0,2} $) # 74 not followed by .?.?\n
301             | (?! [^=\n]{0,3} $) # 73 not followed by .?.?.?\n
302             ))//xsm;
303              
304             $brokenlines .= "$bol$1$mid$eol" while $nlnoc && $res =~ s/(.*?^[^\n]{$nlnoc} (?:
305             [^=\n]{2} (?! [^=\n]{0,1} $) # 75 not followed by .?\n
306             |[^=\n] (?! [^=\n]{0,2} $) # 74 not followed by .?.?\n
307             | (?! [^=\n]{0,3} $) # 73 not followed by .?.?.?\n
308             ))//xsm;
309              
310             $brokenlines .= "$bol$res$eol" if $res;
311              
312             #print "$brokenlines\n";
313             $brokenlines;
314             }
315              
316             ################################################################################
317              
318             1;
319              
320             ################################################################################
321              
322             =pod
323              
324             =head1 NAME
325              
326             Mail::Sendmail::Enhanced v.0.02 = L + encoding + attachments (pure Perl)
327              
328             =head1 SYNOPSIS
329              
330             #!/usr/bin/perl -w
331              
332             use strict;
333             use warnings;
334              
335             use Mail::Sendmail::Enhanced;
336              
337             my $MAIL = Mail::Sendmail::Enhanced-> new(
338             charset => 'UTF-8',
339             smtp => 'Your SMTP server',
340             from => 'Your mail',
341             user => 'user',
342             pass => 'password',
343             method => 'LOGIN',
344             required => 1,
345             attachments => {
346             'name for email of the file1' => 'OS file1 location',
347             'name for email of the file2' => 'OS file2 location',
348             },
349             attachments_size_max => 0,
350             );
351              
352             for (1..2) {
353             print $MAIL-> send( {
354             to => 'author of the module: ',
355             subject => "Subject longer than 80 characters with Polish letters: lowercase: ąćęłńóśźż and uppercase: ĄĆĘŁŃÓŚŹŻ.",
356             message => "This is the message nr $_. in the character encoding UTF-8.
357              
358             This is an example of using UTF-8 Polish letters in an email subject field: encoded and longer than 80 characters.",
359              
360             __END__
361              
362             =head1 DESCRIPTION
363              
364             Enhanced version of the module Mail::Sendmail with multibytes encoding
365             and attachments. It is pure Perl solution.
366              
367             From L:
368              
369             "Simple platform independent e-mail from your perl script. Only
370             requires Perl 5 and a network connection. Mail::Sendmail takes a hash
371             with the message to send and sends it to your mail server. It is
372             intended to be very easy to setup and use."
373              
374              
375             In L two things were added:
376              
377             1. Encoding - which uses the refurbish function B from the
378             module L which is put into the current one.
379             This is pure Perl solution.
380              
381             Simple encoding multibytes character long header field "Subject:"
382             caused that some characters were divided between two folded rows.
383             Some email clients are not able to put together these separated bytes
384             into one character and words were displeyed inproperly. Since the
385             version 0.02 this problem is solved by keeping bytes of one character
386             in one folded row.
387              
388             2. Attachments - which allows to add attachments easily. It makes it
389             by using the technique connected with "multipart/mixed" and "boundary"
390             'Content-Type' attribute.
391              
392             List of files to send (attachments) is given as a simple hash:
393              
394             attachments => {
395             'name for email of the file1' => 'OS file1 location',
396             'name for email of the file2' => 'OS file2 location',
397             },
398              
399             where the keys of the hash are "public" (in email) names of files and
400             values of the hash are these files OS locations, respectively.
401              
402             It possible to do additional specificification of sending attachments
403             throug the parameter B. The possible values are:
404              
405             attachments_size_max => 0, # No limit for sizes of attachments
406              
407             attachments_size_max => -1, # Negative value means that sending attachments is forbidden.
408             # Every try of sending them with this value negative is fatal one.
409              
410             attachments_size_max => 100000, # Positive value is maximum size limit of attachment.
411             # When attachment is bigger then fatal error is return;
412              
413              
414             =head1 BUGS
415              
416             Please report any bugs or feature requests to
417             C, or through the web interface at
418             L.
419             I will be notified, and then you'll automatically be notified of progress on your
420             bug as I make changes.
421              
422             =head1 LICENSE AND COPYRIGHT
423              
424             Copyright (C) 2015 Waldemar Biernacki, C<< >>
425              
426             This program is free software; you can redistribute it and/or modify it
427             under the terms of the the Artistic License (2.0). You may obtain a
428             copy of the full license at:
429              
430             L
431              
432             Any use, modification, and distribution of the Standard or Modified
433             Versions is governed by this Artistic License. By using, modifying or
434             distributing the Package, you accept this license. Do not use, modify,
435             or distribute the Package, if you do not accept this license.
436              
437             If your Modified Version has been derived from a Modified Version made
438             by someone other than you, you are nevertheless required to ensure that
439             your Modified Version complies with the requirements of this license.
440              
441             This license does not grant you the right to use any trademark, service
442             mark, tradename, or logo of the Copyright Holder.
443              
444             This license includes the non-exclusive, worldwide, free-of-charge
445             patent license to make, have made, use, offer to sell, sell, import and
446             otherwise transfer the Package with respect to any patent claims
447             licensable by the Copyright Holder that are necessarily infringed by the
448             Package. If you institute patent litigation (including a cross-claim or
449             counterclaim) against any party alleging that the Package constitutes
450             direct or contributory patent infringement, then this Artistic License
451             to you shall terminate on the date that such litigation is filed.
452              
453             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
454             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
455             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
456             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
457             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
458             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
459             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
460             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
461              
462             =head1 SEE ALSO
463              
464             L, L
465              
466             =cut
467