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   21120 use 5.008;
  1         5  
  1         72  
4              
5 1     1   8 use strict;
  1         2  
  1         48  
6 1     1   6 use warnings;
  1         8  
  1         64  
7              
8 1     1   808 use Encode qw(decode encode);
  1         8695  
  1         91  
9              
10 1     1   530 use Mail::Sendmail '0.79_16';
  1         10970  
  1         139  
11             use MIME::Base64;
12              
13             our $VERSION = '0.03';
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             $mail->{commit} ||= 0;
54              
55             $mail->send() if $mail->{commit};
56              
57             return $mail;
58             }
59              
60             ################################################################################
61              
62             sub send
63             {
64             my ( $self, $ARG ) = ( shift, shift );
65              
66             return 'No address! Please set [to] or/and [cc] fileds.' unless $ARG->{to} || $ARG->{cc} || $self->{to} || $self->{cc};
67              
68             my $charset = $ARG->{charset} || $self->{charset} || 'utf-8';
69             my $type = $ARG->{type} || $self->{type} || 'text/plain';
70             my $subject = $ARG->{subject} || $self->{subject} || '';
71             my $message = $ARG->{message} || $self->{message} || '';
72              
73             my $boundary = "====" . time() . "====";
74              
75             # Email subject is encoded using proper character encoding.
76             # original "encode_qp" function contains up to 2 arguments,
77             # but in a case of character set it is needed to start every
78             # new line with a statemant of the encoding, so - as a the
79             # third parameter - the charset is sent to the function.
80              
81             my $flnoc = 67;
82             my $nlnoc = 78;
83             my $bol = " =?$charset?Q?";
84             my $eol = "?=\n";
85              
86             {
87             # this part consider multibyte characters and keep that folding
88             # does not divide the multibyte characters into two lines.
89             # The reason is that some email clients are not able to put
90             # together these separated bytes into one character.
91             require bytes;
92              
93              
94             my $t_subject = decode( $charset, $subject ) if $charset;
95              
96             if ( bytes::length($t_subject) > length($t_subject) || $t_subject =~ /[^\0-\xFF]/ ) {
97              
98             $subject = '';
99             my $t_string = ''; # substring of $t_subject which is testing if can be added to $subject
100             my $t_length = 0; # the length of $t_subject
101              
102             my $t_return = ''; # $t_string which match to the condition
103             my $t_result = ''; # encoded string of $t_string
104             my $t_number = 0; # number of row of the folded "Subject" field
105             while ( $t_subject ) {
106             foreach(0..$flnoc) {
107             $t_string = substr($t_subject,0,$_);
108             $t_result = encode_qp(encode($charset,$t_string),{bol=>$bol,eol=>$eol,flnoc=>0,nlnoc=>0,charset=>$charset,});
109              
110             #checking if encoded string $t_result of the tested substring $t_string satisfies length condition:
111             # and if yes we go out with the last good value $t_return and $t_subject get shorter by $t_length
112             last if length( $t_result ) > ($t_number ? $nlnoc : $flnoc);
113             $t_return = $t_result;
114             $t_length = length($t_string);
115             }
116             $subject = $subject.$t_return;
117             $t_subject = substr( $t_subject, $t_length );
118             $t_number++;
119             }
120              
121             } else {
122             $subject = encode_qp( $subject , { bol=>$bol, eol=>$eol, flnoc=>$flnoc, nlnoc=>$nlnoc, charset=>$charset, } );
123             }
124             }
125              
126             $subject = substr($subject,1);
127              
128             my %mail = (
129             'X-Mailer' => "This is Perl Mail::Sendmail::Enhanced version $Mail::Sendmail::Enhanced::VERSION",
130             'Content-Type' => "multipart/mixed; charset=$charset; boundary=\"$boundary\"",
131             'Smtp' => ($ARG->{smpt}|| $self->{smtp} ),
132             'From' => ($ARG->{from}|| $self->{from} ),
133             'To' => ($ARG->{to} || $self->{to} || '' ),
134             'Cc' => ($ARG->{cc} || $self->{cc} || '' ),
135             'Subject' => $subject,
136             auth => {
137             user => ($ARG->{user} || $self->{user} ),
138             pass => ($ARG->{pass} || $self->{pass} ),
139             method => ($ARG->{method} || $self->{method} ),
140             required => ($ARG->{required}|| $self->{required} ),
141             },
142             );
143              
144             $boundary = '--'.$boundary;
145             $mail{'Message'} = "$boundary\n"
146             ."Content-Type: $type; charset=$charset; format=flowed\n"
147             ."Content-Transfer-Encoding: 8bit\n\n"
148             ."$message\n";
149              
150             # ."Content-Transfer-Encoding: quoted-printable\n\n"
151             # .encode_qp( $ARG->{'message'}, {} )."\n";
152              
153             $ARG->{attachments} ||= $self->{attachments} || {};
154             $ARG->{attachments_size_max} ||= $self->{attachments_size_max} || 0; #no limit "-1" means no attachment allowed
155              
156             $ARG->{attachments_size_max}=~s/[B ]//g;
157             if ( $ARG->{attachments_size_max} =~ /^(-\d+)$|^(\d+)(|k|K|m|M|t|T)?\s*$/ )
158             {
159             if ( $1 ) { $ARG->{attachments_size_max} = $1
160              
161             } else {
162              
163             $ARG->{attachments_size_max} = $2 if $3 eq 'B' || !$3;
164             $ARG->{attachments_size_max} = 1000 * $2 if $3 eq 'k';
165             $ARG->{attachments_size_max} = 1024 * $2 if $3 eq 'K';
166             $ARG->{attachments_size_max} = 1000 * 1000 * $2 if $3 eq 'm';
167             $ARG->{attachments_size_max} = 1024 * 1024 * $2 if $3 eq 'M';
168             $ARG->{attachments_size_max} = 1000 * 1000 * 1000 * $2 if $3 eq 't';
169             $ARG->{attachments_size_max} = 1024 * 1024 * 1024 * $2 if $3 eq 'T';
170             }
171             }
172             else {
173             return 'Malform in attachments_size_max='.$ARG->{attachments_size_max}.'! Accepted form is: positive or negative integer plus optional one of the letters: (k,K,m,M,t,T).'
174             }
175              
176             return "Attachments are not allowed whereas some are preperad to send!" if %{$ARG->{attachments}} && $ARG->{attachments_size_max} < 0;
177             # attachment files are packed one by one into the message part each divided by boundary
178              
179             # checking attachments:
180             foreach my $fileName ( sort keys %{$ARG->{attachments}} ) {
181              
182             my $fileLocation = $ARG->{attachments}->{$fileName};
183              
184             # if does not exists:
185             return "Attachment does not exists! [$fileLocation]" unless -f $fileLocation;
186              
187             # if it is too big:
188             my $size = -s $fileLocation || 0;
189             return "Attachment too big! [$fileLocation: $size > ".$ARG->{attachments_size_max}."B max.]"
190             if $ARG->{attachments_size_max} > 0 && $size > $ARG->{attachments_size_max};
191             }
192              
193              
194             foreach my $fileName ( sort keys %{$ARG->{attachments}} ) {
195             my $fileLocation = $ARG->{attachments}->{$fileName};
196             if (open (my $F, $fileLocation )) {
197             my $input_record_separator = $/;
198             binmode $F; undef $/;
199             my $attachment = encode_base64(<$F>);
200             close $F;
201             $/ = $input_record_separator;
202              
203             $mail{'Message'} .= "$boundary\n"
204             ."Content-Type: application/octet-stream; name=\"$fileName\"\n"
205             ."Content-ID: <$fileName>\n"
206             ."Content-Transfer-Encoding: base64\n"
207             ."Content-Disposition: attachment; filename=\"$fileName\"\n\n"
208             ."$attachment\n";
209             }
210             }
211              
212             $mail{'Message'} .= "$boundary--\n";
213              
214              
215             return $Mail::Sendmail::error unless sendmail( %mail );
216              
217             return;
218             }
219              
220             ################################################################################
221              
222             sub encode_qp
223             {
224             ############################################################################
225             # This function is an exact copy of the that of the same name from the
226             # module: "MIME::QuotedPrint::Perl" '1.00' with the following changes:
227             # 1. The second argument can be scalar - as previously -
228             # or hash which would contain more information
229             # 2. There are changes in counting character in each line in accordance
230             # with hash sent to the function: it can be different in first line
231             # and the next ones. It is so, because usually in the firs line there
232             # is some word (Subject for instance).
233             # The behaviour of the function is identical with the original one in case
234             # we send two scalar arguments only.
235             ############################################################################
236              
237             # $res = text to be encoded
238             my ( $res ) = ( shift );
239             return '' unless $res;
240              
241             # The arguments can be sent in old way, when the second argument was the
242             # end of character rows, or in a new way - as a hash:
243             my %par = (
244             bol => " ", # characters at the begining of each lines
245             eol => "\n", # characters at the end of each line
246             flnoc => 68, # first line number of characters, 0 = unlimit
247             nlnoc => 78, # next lines number of characters, 0 = unlimit
248             );
249              
250             while ( my $key = shift ) {
251             if ( ref ( $key ) eq 'HASH' ) {
252             foreach my $k (sort keys %{$key} ) {
253             next unless $k =~ /^(bol|charset|eol|flnoc|nlnoc)$/;
254             next if $k eq 'flnoc' && $par{$k} !~ /^\d+$/;
255             next if $k eq 'nlnoc' && $par{$k} !~ /^\d+$/;
256              
257             $par{$k} = $$key{$k};
258             }
259             } else { # you can only send - as a second scalar argument the "EOL"
260             # characters in accordance with the original function
261             $par{eol} = $key;
262             }
263             }
264              
265             if ($] >= 5.006) {
266             require bytes;
267             if (bytes::length($res) > length($res) || ($] >= 5.008 && $res =~ /[^\0-\xFF]/))
268             {
269             require Carp;
270             Carp::croak("The Quoted-Printable encoding is only defined for bytes");
271             }
272             }
273              
274             # usefull shorthands
275             my $bol = $par{bol};
276             my $eol = $par{eol};
277             my $flnoc = $par{flnoc} - 0 - length($eol) - length($bol);
278             my $nlnoc = $par{nlnoc} - 1 - length($eol) - length($bol);
279             my $mid = '';
280             unless ( defined $bol ) { $mid = '='; $bol = '' }
281              
282             # Do not mention ranges such as $res =~ s/([^ \t\n!-<>-~])/sprintf("=%02X", ord($1))/eg;
283             # since that will not even compile on an EBCDIC machine (where ord('!') > ord('<')).
284             if (ord('A') == 193) { # EBCDIC style machine
285             if (ord('[') == 173) {
286             $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
287             $res =~ s/([ \t]+)$/
288             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp1047',$_)))) }
289             split('', $1)
290             )/egm; # rule #3 (encode whitespace at eol)
291             }
292             elsif (ord('[') == 187) {
293             $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
294             $res =~ s/([ \t]+)$/
295             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('posix-bc',$_)))) }
296             split('', $1)
297             )/egm; # rule #3 (encode whitespace at eol)
298             }
299             elsif (ord('[') == 186) {
300             $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
301             $res =~ s/([ \t]+)$/
302             join('', map { sprintf("=%02X", ord(Encode::encode('iso-8859-1',Encode::decode('cp37',$_)))) }
303             split('', $1)
304             )/egm; # rule #3 (encode whitespace at eol)
305             }
306             }
307             else { # ASCII style machine
308             $res =~ s/([^ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z[\\\]^_`a-z{|}~])/sprintf("=%02X", ord($1))/eg; # rule #2,#3
309             $res =~ s/\n/=0A/g unless length($eol);
310             $res =~ s/([ \t]+)$/
311             join('', map { sprintf("=%02X", ord($_)) }
312             split('', $1)
313             )/egm; # rule #3 (encode whitespace at eol)
314             }
315              
316             return $res unless length($eol);
317              
318             # rule #5 (lines must be shorter than 76 chars, but we are not allowed
319             # to break =XX escapes. This makes things complicated :-( )
320             my $brokenlines = "";
321              
322             $brokenlines .= "$bol$1$mid$eol" if $flnoc && $res =~ s/(.*?^[^\n]{$flnoc} (?:
323             [^=\n]{2} (?! [^=\n]{0,1} $) # 75 not followed by .?\n
324             |[^=\n] (?! [^=\n]{0,2} $) # 74 not followed by .?.?\n
325             | (?! [^=\n]{0,3} $) # 73 not followed by .?.?.?\n
326             ))//xsm;
327              
328             $brokenlines .= "$bol$1$mid$eol" while $nlnoc && $res =~ s/(.*?^[^\n]{$nlnoc} (?:
329             [^=\n]{2} (?! [^=\n]{0,1} $) # 75 not followed by .?\n
330             |[^=\n] (?! [^=\n]{0,2} $) # 74 not followed by .?.?\n
331             | (?! [^=\n]{0,3} $) # 73 not followed by .?.?.?\n
332             ))//xsm;
333              
334             $brokenlines .= "$bol$res$eol" if $res;
335              
336             #print "$brokenlines\n";
337             $brokenlines;
338             }
339              
340             ################################################################################
341              
342             1;
343              
344             ################################################################################
345              
346             =pod
347              
348             =head1 NAME
349              
350             Mail::Sendmail::Enhanced v.0.03 - Pure Perl email sender with multibyte characters encoding and easy attachments managment
351              
352             =head1 SYNOPSIS
353              
354             #!/usr/bin/perl -w
355              
356             use strict;
357             use warnings;
358              
359             use Mail::Sendmail::Enhanced;
360              
361             # This part simulate the general setup of application mailer.
362             # It sets smtp server and size limit of attachments (1MB)
363             # This configuration is set by admin.
364             my $mail = Mail::Sendmail::Enhanced-> new(
365             charset => 'cp1250',
366             smtp => 'Your SMTP server',
367             from => 'Your mail',
368             user => 'user',
369             pass => 'password',
370             method => 'LOGIN',
371             required => 1,
372             attachments => {
373             'name for email of the file1' => 'OS file1 location',
374             'name for email of the file2' => 'OS file2 location',
375             },
376             attachments_size_max => '1MB',
377             commit => 0,
378             );
379              
380             # This part simulate how clients can use the mailer.
381             # Configuration here is set by clients themself.
382             my @client = qw(John Henry Newman);
383             for (@client) {
384              
385             my $lowercase = chr(185).chr(230).chr(234).chr(179).chr(241).chr(243).chr(156).chr(159).chr(191);
386             my $uppercase = chr(165).chr(198).chr(202).chr(163).chr(209).chr(211).chr(140).chr(143).chr(175);
387              
388             print $mail-> send( {
389             to => 'author of the module: ',
390             subject => "Subject longer than 80 characters with Polish letters: lowercase: $lowercase and uppercase: $uppercase.",
391             message => "This is the message from $_ in the character encoding ".$mail->{charset}.".
392              
393             This is an example of mailing Polish letters in a header field named \"Subject\".
394             Additionally this field is longer than 80 characters.
395              
396             Additional text:
397             Polish lowercase letters: $lowercase
398             Polish uppercase letters: $uppercase
399             ",
400             });
401             }
402              
403             __END__
404              
405             =head1 DESCRIPTION
406              
407             Mail::Sendmail::Enhanced is an enhanced version of the module
408             L. It is still pure Perl solution. In the module the
409             problem of encoding multibyte characters in L was
410             solved. Some procedure of sending very easily a list of attachments
411             was prepared.
412              
413             After preparing multibyte characters encoding and building message
414             with attachments the module calls I function from the
415             L module which does all the job.So please read there
416             in L about how to set up connections to email servers.
417             This module behaves identically.
418              
419             As already mentioned this adds two things:
420              
421             1. Multibyte characters encoding - which uses refurbish and imported
422             function I from the module L.
423              
424             The problem with encoding multibyte characters was that simple
425             implemented encoding - especially in the "Subject:" field of email
426             header - results that some characters are divided between two rows
427             when long lines are folded. Some email clients are not able to put
428             together these separated bytes into one character and letters are
429             displeyed inproperly. The new encoding function keeps bytes of one
430             character in one folded row.
431              
432             2. Simple attachments managment. List of attachments is a hash:
433              
434             attachments => {
435             'name for email of the file1' => 'OS file1 location',
436             'name for email of the file2' => 'OS file2 location',
437             },
438              
439             where the keys are the attachments email names and the values are
440             OS locations.
441              
442             It is possible to add some control to sending attachment. It is done
443             by the parameter B. Possible values are:
444              
445              
446             attachments_size_max => -1, # Negative value means that sending attachments is forbidden.
447             # Every try of sending them with this value negative is fatal one.
448              
449             attachments_size_max => 0, # No size limit of attachments
450              
451             attachments_size_max => '50 000 B', # Positive value is a maximum size of attachment.
452             # When size is bigger then fatal error is return.
453             # Spaces and the letter B (byte) are ignored.
454              
455             # shorthand for sizes: k, K, m, M:
456             attachments_size_max => '100k', # k = 1000, so maximum = 100 000
457             attachments_size_max => '100 K', # K = 1024, so maximum = 102 400
458             attachments_size_max => '2 m', # m = 1000x1000, so maximum = 1 000 000
459             attachments_size_max => '2M', # M = 1024x1024, so maximum = 1 048 576
460              
461              
462             =head1 INTERFACE
463              
464             Interface L, gets two methods:
465              
466             =head2 new()
467              
468             The method I creates mail object.
469              
470             =head2 send()
471              
472             The method I sends mail.
473              
474             Arguments of both methods are the same and discussed earlier.
475             Dispersing data between I and I is fully optional.
476             Assuming that we have three hashes %n, %s and %d which fullfiled the
477             abstract equality:
478              
479             "%n + %s = %d"
480              
481             all the three ways of sending email have the same effect:
482              
483             1. my $mail = Mail::Sendmail::Enhanced->new(%n); $mail->send(%s);
484              
485             2. my $mail = Mail::Sendmail::Enhanced->new(); $mail->send(%d);
486              
487             3. my $mail = Mail::Sendmail::Enhanced->new(%d); $mail->send();
488              
489             This third way can be replaced by only one call with additional argument
490             "commit" with the value 1 (look back at the SYNOPSIS):
491              
492             commit => 1,
493              
494             in that case email is sent at the end of the method I.
495              
496              
497             =head1 BUGS
498              
499             Please report any bugs or feature requests to C, or through
500             the web interface at L. I will
501             be notified, and then you'll automatically be notified of progress on your bug as I make changes.
502              
503             =head1 LICENSE AND COPYRIGHT
504              
505             Copyright (C) 2015 Waldemar Biernacki, C<< >>
506              
507             This program is free software; you can redistribute it and/or modify
508             it under the terms of the the Artistic License (2.0). You may obtain
509             a copy of the full license at:
510              
511             L
512              
513             Any use, modification, and distribution of the Standard or Modified
514             Versions is governed by this Artistic License. By using, modifying
515             or distributing the Package, you accept this license. Do not use,
516             modify, or distribute the Package, if you do not accept this license.
517              
518             If your Modified Version has been derived from a Modified Version
519             made by someone other than you, you are nevertheless required to
520             ensure that your Modified Version complies with the requirements of
521             this license.
522              
523             This license does not grant you the right to use any trademark,
524             service mark, tradename, or logo of the Copyright Holder.
525              
526             This license includes the non-exclusive, worldwide, free-of-charge
527             patent license to make, have made, use, offer to sell, sell, import
528             and otherwise transfer the Package with respect to any patent claims
529             licensable by the Copyright Holder that are necessarily infringed by
530             the Package. If you institute patent litigation (including a
531             cross-claim or counterclaim) against any party alleging that the
532             Package constitutes direct or contributory patent infringement, then
533             this Artistic License to you shall terminate on the date that such
534             litigation is filed.
535              
536             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT
537             HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED
538             WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR
539             A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE
540             EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO
541             COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT,
542             INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY
543             OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF
544             SUCH DAMAGE.
545              
546             =head1 SEE ALSO
547              
548             L, L
549              
550             =cut