1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
|
package MIME::Lite::HTML;
# module MIME::Lite::HTML : Provide routine to transform a HTML page in
# a MIME::Lite mail
# Copyright 2000 A.Barbet alian@alianwebserver.com. All rights reserved.
# $Log: HTML.pm,v $
# Revision 1.2 2001/03/20 22:35:56 alian
# - Add lot of pod documentation
# - Change how final mail is build:
# If no images are found when parse routine is used, this modules did'nt
# use a multipart/related part, but a text/html part. Thus, we can reach
# a max. of mail clients (See "clients tested" in documentation).
# - Add size function
#
# Revision 1.1 2001/03/04 22:29:07 alian
# - Correct an error with background image quote
#
# Revision 1.0 2001/03/04 22:13:19 alian
# - Correct major problem with Eudora (See Clients tested in documentation)
# - Build final MIME-Lite object with knowledge of RFC-2257
# - Add some POD documentation and references
#
# Revision 0.9 2001/02/02 01:15:35 alian
# Correct some other things with error handling (suggested by Steve Harvey <sgh@vex.net>
#
# Revision 0.8 2001/01/21 00:58:48 alian
# Correct error function
#
# Revision 0.7 2000/12/30 20:22:27 alian
# - Allow to send a string of text to the parse function, instead of an url
# - Add feature to put data on the fly when image are available only on memory
# - Put comments on print when buffer find url
# Ideas suggested by mtveerman@mindless.com
#
# Revision 0.6 2000/12/13 11:02:58 alian
# - Allow sup parameter for MIME-Lite in constructor
# - Add parameter for parse url to include a text file when HTML
# is not supported by client.
#
# Revision 0.5 2000/11/13 21:36:58 Administrateur
# - Arg, forgot cariage return in fill_template :-(
#
# Revision 0.4 2000/11/12 18:52:56 Administrateur
# - Add feature of replace word in gabarit (for newsletter by example)
# - Include body background
#
# Revision 0.3 2000/10/26 22:55:46 Administrateur
# Add parsing for form (action and input image)
#
# Revision 0.2 2000/10/26 20:08:06 Administrateur
# Update remplacement of relative url
use LWP::UserAgent;
use HTML::LinkExtor;
use URI::URL;
use MIME::Lite;
use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw();
$VERSION = ('$Revision: 1.2 $ ' =~ /(\d+\.\d+)/)[0];
=head1 NAME
MIME::Lite::HTML - Provide routine to transform a HTML page in a MIME-Lite mail
=head1 SYNOPSIS
use MIME::Lite;
use MIME::Lite::HTML;
my $mailHTML = new MIME::Lite::HTML
From => 'MIME-Lite@alianwebserver.com',
To => 'alian@jupiter',
Subject => 'Mail in HTML with images';
$MIMEmail = $mailHTML->parse('http://www.alianwebserver.com');
$MIMEmail->send; # or for win user : $mail->send_by_smtp('smtp.fai.com');
=head1 VERSION
$Revision: 1.2 $
=head1 DESCRIPTION
This module provide routine to transform a HTML page in MIME::Lite mail.
So you need this module to use MIME-Lite-HTML possibilities
=head2 What's happen ?
The job done is:
=over
=item *
Get the file (LWP) if needed
=item *
Parse page to find include images
=item *
Attach them to mail with adequat cid
=item *
Include external CSS,Javascript file
=item *
Replace relative url with absolute one
=item *
Build the final MIME-Lite object with each part found
=back
=head2 Usage
It can be used by example in a HTML newsletter. You make a classic HTML page,
and give just url to MIME::Lite::HTML.
=head2 Construction
MIME-Lite-HTML use a MIME-Lite object, and RFC2257 construction:
If images are present, construction use is:
--> multipart/alternative
------> text/plain if present
------> multipart/related
-------------> text/html
-------------> each images
If no images is present, this is that:
---> multipart/alternative
-------> text/plain if present
-------> text/html
=head2 Documentation
Additionnal documentation can be found here:
=over
=item *
MIME-lite module
=item *
RFC 822, RFC 1521, RFC 1522 and specially RFC 2257 (MIME Encapsulation
of Aggregate Documents, such as HTML)
=back
=head2 Clients tested
HTML in mail is not full supported so this module can't work with all email clients.
If some client recognize HTML, they didn't support images include in HTML.
So in fact, they recognize multipart/relative but not multipart/related.
=over
=item Netscape Messager (Linux-Windows)
100% ok
=item Outlook Express (Windows)
100% ok
=item Eudora (Windows)
If this module just send HTML and text, (without images), 100% ok.
With images, Eudora didn't recognize multipart/related part as describe in RFC 2257,
even if he can read his own HTML mail. So if images are present in HTML part,
text and HTML part will be displayed both, text part in first. Two additional
headers will be displayed in HTML part too in this case. Version 1.0 of this
module correct major problem of headers displayed with image include in
HTML part.
=item KMail (Linux)
If this module just send HTML and text, (without images), 100% ok.
In other case, Kmail didn't support image include in HTML. So if you set in KMail
"Prefer HTML to text", it display HTML with images broken. Otherwise, it display
text part.
=item Pegasus (Windows)
If this module just send HTML and text, (without images), 100% ok.
Pegasus didn't support images in HTML. When it find a multipart/related message,
it ignore it, and display text part.
=back
If you find others mail client who support (or not support) MIME-Lite-HTML
module, give me some feedback ! If you want be sure that your mail can be read by
maximum of people, (so not only OE and Netscape), don't include images in your mail,
and use a text buffer too. If multipart/related mail is not recognize, multipart/alternative
can be read by the most of mail client.
=head1 Public Interface
=over
=item new(%hash)
Create a new instance of MIME::Lite::HTML.
%hash can have this key : [Proxy], [Debug], [HashTemplate]
Others keys are use with MIME::Lite constructor.
This MIME-Lite keys are: Bcc, Encrypted, Received, Sender, Cc, From,
References, Subject, Comments, Keywords, Reply-To To, Content-*,
Message-ID,Resent-*, X-*,Date,MIME-Version,Return-Path,
Organization
$hash{'HashTemplate'} is a reference to a hash. If present, MIME::Lite::HTML will
substitute <? $name ?> with $hash{'HashTemplate'}{'name'} when parse url to
send. $hash{'HashTemplate'} can be used too for include data for subelement. Ex:
$hash{'HashTemplate'}{'http://www.alianwebserver.com/images/sommaire.gif'}=\@data;
or $hash{'HashTemplate'}{'http://www.alianwebserver.com/script.js'}="alert("Hello world");";
When module find the image http://www.alianwebserver.com/images/sommaire.gif in buffer,
it don't get image with LWP but use data found in $hash{'HashTemplate'}.
=cut
sub new
{
my $class = shift;
my $self = {};
bless $self, $class;
my %param = @_;
# Agent name
$self->{_AGENT} = new LWP::UserAgent "MIME-Lite-HTML $VERSION", 'alian@alianwebserver.com';
# Set debug level
if ($param{'Debug'})
{
$self->{_DEBUG} = 1;
delete $param{'Debug'};
}
# Set proxy to use to get file
if ($param{'Proxy'})
{
$self->{_AGENT}->proxy('http',$param{'Proxy'}) ;
print "Set proxy for http : ", $param{'Proxy'},"\n" if ($self->{_DEBUG});
delete $param{'Proxy'};
}
# Set hash to use with template
if ($param{'HashTemplate'})
{
$param{'HashTemplate'} = ref($param{'HashTemplate'}) eq "HASH" ? $param{'HashTemplate'} : %{$param{'HashTemplate'}}; #CHANGE
$self->{_HASH_TEMPLATE}= $param{'HashTemplate'};
delete $param{'HashTemplate'};
}
# Create multipart/alternative part
$param{'Type'}='multipart/alternative';
my $mail = new MIME::Lite (%param);
# Ok I hope I known what I do ;-)
MIME::Lite->quiet(1);
$mail->replace('X-Mailer',"MIME::Lite::HTML $VERSION");
$self->{_MAIL} = $mail;
return $self;
}
=item parse($html, [$url_txt], [$url_base])
Subroutine used for created HTML mail with MIME-Lite
Parameters:
=over
=item $html
Url of HTML file to send, can be a local file. If $url is not an
url (http or https or ftp or file or nntp), $url is used as a buffer.
Example : http://www.alianwebserver.com, file://c|/tmp/index.html
or '<img src=toto.gif>'.
=item $url_txt
Url of text part to send for person who doesn't support HTML mail.
As $html, $url_txt can be a simple buffer.
=item $url_base
$url_base is used if $html is a buffer, for get element found in HTML buffer.
=back
Return the MIME::Lite part to send
=cut
sub parse
{
my($self,$url_page,$url_txt,$url1)=@_;
my ($type,@mail,$gabarit,$gabarit_txt,$racinePage);
if ($url_page=~/^(https?|ftp|file|nntp):\/\//)
{
# Get content of $url_page with LWP
print "Get ", $url_page,"\n" if $self->{_DEBUG};
my $req = new HTTP::Request('GET' => $url_page);
my $res = $self->{_AGENT}->request($req);
if (!$res->is_success) {$self->set_err("$url_page n'est pas accessible");}
else {$gabarit = $res->content;}
$racinePage=$res->base;
}
else {$gabarit=$url_page;$racinePage=$url1;}
# Get content of $url_txt with LWP if needed
if ($url_txt)
{
if ($url_txt=~/^(https?|ftp|file|nntp):\/\//)
{
print "Get ", $url_txt,"\n" if $self->{_DEBUG};
my $req2 = new HTTP::Request('GET' => $url_txt);
my $res3 = $self->{_AGENT}->request($req2);
if (!$res3->is_success) {$self->set_err("$url_txt n'est pas accessible");}
else {$gabarit_txt = $res3->content;}
}
else {$gabarit_txt=$url_txt;}
}
# Get all images and create part for each of them
my $analyseur = HTML::LinkExtor->new;
$analyseur->parse($gabarit);
my @l = $analyseur->links;
# Include external CSS files
$gabarit = $self->include_css($gabarit,$racinePage);
# Include external Javascript files
$gabarit = $self->include_javascript($gabarit,$racinePage);
($gabarit,@mail) = $self->input_image($gabarit,$racinePage);
$gabarit = $self->link_form($gabarit,$racinePage);
my (%images_read,%url_remplace);
# Scan each part found by linkExtor
foreach my $url (@l)
{#print @$url,"\n";
my $urlAbs = URI::WithBase->new($$url[2],$racinePage)->abs;
# Replace relative href found to absolute one
if ( ($$url[0] eq 'a')
&& ($$url[1] eq 'href')
&& ($$url[2]!~m!^http://!)
&& (!$url_remplace{$urlAbs}) )
{
$gabarit=~s/href="?'?$$url[2]("?|'?)/href="$urlAbs"/gim;
print "Replace ", $$url[2]," with ",$urlAbs,"\n" if $self->{_DEBUG};
$url_remplace{$urlAbs}=1;
}
# Replace images background
elsif ($$url[1] eq 'background')
{
my $v ="background=\"cid:$urlAbs\"";
$gabarit=~s/background=\"$$url[2]\"/$v/im;
print "Get ", $urlAbs,"\n" if $self->{_DEBUG};
my $res2 = $self->{_AGENT}->request
(new HTTP::Request('GET' => $urlAbs));
# Create MIME type
if (lc($urlAbs)=~/gif$/) {$type = "image/gif";}
else {$type = "image/jpg";}
# Create part
my $mail = new MIME::Lite
Data => $res2->content,
Encoding =>'base64';
$mail->attr("Content-type"=>$type);
$mail->attr('Content-ID'=>$urlAbs);
push(@mail,$mail);
}
# Get only new <img src>
next if ((lc($$url[0]) ne 'img') && (lc($$url[0]) ne 'src') || ($images_read{$urlAbs}) );
# Create MIME type
if (lc($urlAbs)=~/gif$/) {$type = "image/gif";}
else {$type = "image/jpg";}
my $buff1;
if ($self->{_HASH_TEMPLATE}{$urlAbs})
{
print "Using buffer on: ", $urlAbs,"\n" if $self->{_DEBUG};
$buff1 = ref($self->{_HASH_TEMPLATE}{$urlAbs}) eq "ARRAY" ? join "", @{$self->{_HASH_TEMPLATE}{$urlAbs}} : $self->{_HASH_TEMPLATE}{$urlAbs};
delete $self->{_HASH_TEMPLATE}{$urlAbs};
}
else
{
# Get image
print "Get ", $urlAbs,"\n" if $self->{_DEBUG};
my $res2 = $self->{_AGENT}->request(new HTTP::Request('GET' => $urlAbs));
if (!$res2->is_success) {$self->set_err("Can't get $urlAbs\n");}
$buff1=$res2->content;
}
# Create part
$images_read{$urlAbs}=1;
my $mail = new MIME::Lite
Data => $buff1,
Encoding =>'base64';
$mail->replace("X-Mailer" => "");
my $t = '<'.$urlAbs.'>';
$mail->attr("Content-type"=>$type);
$mail->attr('Content-ID' =>$t);
push(@mail,$mail);
}
# Replace in HTML link with image with cid:key
sub pattern_image {return '<img '.$_[0].'src="cid:'.URI::WithBase->new($_[1],$_[2])->abs.'"';}
$gabarit=~s/<img([^<>]*)src=(["']?)([^"'> ]*)(["']?)/pattern_image($1,$3,$racinePage)/ieg;
# Substitue value in template if needed
if (scalar keys %{$self->{_HASH_TEMPLATE}}!=0)
{$gabarit=$self->fill_template($gabarit,$self->{_HASH_TEMPLATE});}
# Create MIME-Lite object
$self->build_mime_object($gabarit,$gabarit_txt,@mail);
return $self->{_MAIL};
}
=item size()
Display size of mail in characters (so octets) that will be send.
(So use it *after* parse method). Use this method for control
size of mail send, I personnaly hate receive 500k by mail.
I pay for a 33k modem :-(
=cut
sub size
{
my ($self)=shift;
return length($self->{_MAIL}->as_string);
}
=back
=head1 Private methods
=over
=item build_mime_object($html,[$txt],[@mail])
(private)
Build the final MIME-Lite object to send with each part read before
=over
=item $html
Buffer of HTML part
=item $txt
Buffer of text part
=item @mail
List of images attached to HTML part. Each item is a MIME-Lite object.
=back
See "Construction" in "Description" for know how MIME-Lite object is build.
=cut
sub build_mime_object
{
my ($self,$html,$txt,@mail)=@_;
# Create part for HTML
my $part = new MIME::Lite
'Type' =>'TEXT',
'Encoding'=>'quoted-printable',
'Data' =>$html;
$part->attr("content-type"=> "text/html; charset=iso-8859-1");
my $email2;
# Create related part if images
if (@mail)
{
$email2 = new MIME::Lite ('Type'=>'multipart/related');
# Remove some header for Eudora client in HTML and related part
$part->replace("MIME-Version" => "");
$part->replace('X-Mailer',"MIME::Lite::HTML $VERSION");
$email2->replace("Content-transfer-encoding" => "");
$email2->replace("MIME-Version" => "");
$email2->replace("X-Mailer" => "");
# Attach HTML part to related part
$email2->attach($part);
# Attach each image to related part
foreach (@mail) {$email2->attach($_);} # Attach list of part
}
if ($txt)
{
# Create part for text if needed
my $part2 = new MIME::Lite 'Type' => 'TEXT', 'Encoding' => '7bit', 'Data' => $txt;
$part2->attr("content-type"=> "text/plain; charset=us-ascii");
# Remove some header for Eudora client
$part2->replace("MIME-Version" => "");
$part2->replace("X-Mailer" => "");
# Attach text part to relative part
$self->{_MAIL}->attach($part2);
}
# Attach related part to alternative part
if (@mail) {$self->{_MAIL}->attach($email2);}
# Attach HTML part to alternative part
else {$self->{_MAIL}->attach($part);}
}
=item include_css($gabarit,$root)
(private)
Search in HTML buffer ($gabarit) to remplace call to extern CSS file
with his content. $root is original absolute url where css file will
be found.
=cut
sub include_css
{
my ($self,$gabarit,$root)=@_;
sub pattern_css
{
my ($self,$url,$milieu,$fin,$root)=@_;
my $ur = URI::URL->new($url, $root)->abs;
print "Include CSS file $ur\n" if $self->{_DEBUG};
my $res2 = $self->{_AGENT}->request(new HTTP::Request('GET' => $ur));
print "Ok file downloaded\n" if $self->{_DEBUG};
return '<style type="text/css">'."\n".
'<!--'."\n".$res2->content.
"\n-->\n</style>\n";
}
$gabarit=~s/<link([^<>]*?)href="?([^" ]*css)"?([^>]*)>/$self->pattern_css($2,$1,$3,$root)/iegm;
print "Done CSS\n" if $self->{_DEBUG};
return $gabarit;
}
=item include_javascript($gabarit,$root)
(private)
Search in HTML buffer ($gabarit) to remplace call to extern javascript file
with his content. $root is original absolute url where javascript file will
be found.
=cut
sub include_javascript
{
my ($self,$gabarit,$root)=@_;
sub pattern_js
{
my ($self,$url,$milieu,$fin,$root)=@_;
my $ur = URI::URL->new($url, $root)->abs;
print "Include Javascript file $ur\n" if $self->{_DEBUG};
my $res2 = $self->{_AGENT}->request(new HTTP::Request('GET' => $ur));
my $content = $res2->content;
print "Ok file downloaded\n" if $self->{_DEBUG};
return "\n"."<!-- $ur -->\n".
'<script '.$milieu.$fin.">\n".
'<!--'."\n".$content.
"\n-->\n</script>\n";
}
$gabarit=~s/<script([^>]*)src="?([^" ]*js)"?([^>]*)>/$self->pattern_js($2,$1,$3,$root)/iegm;
print "Done Javascript\n" if $self->{_DEBUG};
return $gabarit;
}
=item input_image($gabarit,$root)
(private)
Search in HTML buffer ($gabarit) to remplace input form image with his cid
Return final buffer and list of MIME::Lite part
=cut
sub input_image
{
my ($self,$gabarit,$root)=@_;
my @mail;
sub pattern_input_image
{
my ($self,$deb,$url,$fin,$base,$ref_tab_mail)=@_;
my $type;
my $ur = URI::URL->new($url, $base)->abs;
# Create MIME type
if (lc($ur)=~/gif$/) {$type="image/gif";}
else {$type = "image/jpg";}
my $res = $self->{_AGENT}->request(new HTTP::Request('GET' => $ur));
# Create part
my $mail = new MIME::Lite
Data => $res->content,
Encoding =>'base64';
$mail->attr("Content-type"=>$type);
$mail->attr('Content-ID'=>$ur);
push(@$ref_tab_mail,$mail);
return '<input '.$deb.' src="cid:'.$ur.'"'.$fin;
}
$gabarit=~s/<input([^<>]*)src="?([^"'> ]*)"?([^>]*)>/$self->pattern_input_image($1,$2,$3,$root,\@mail)/iegm;
print "Done input image\n" if $self->{_DEBUG};
return ($gabarit,@mail);
}
=item link_form($gabarit,$root)
(private)
Replace link to formulaire with absolute link
=cut
sub link_form
{
my ($self,$gabarit,$root)=@_;
my @mail;
sub pattern_link_form
{
my ($self,$deb,$url,$fin,$base)=@_;
my $type;
my $ur = URI::URL->new($url, $base)->abs;
return '<form '.$deb.' action="'.$ur.'"'.$fin.'>';
}
$gabarit=~s/<form([^<>]*)action="?([^"'> ]*)"?([^>]*)>/$self->pattern_link_form($1,$2,$3,$root)/iegm;
print "Done form\n" if $self->{_DEBUG};
return $gabarit;
}
=item fill_template($masque,$vars)
$masque : Path of template
$vars : hash ref with keys/val to substitue
Give template with remplaced variables
Ex: if $$vars{age}=12, and $masque have
J'ai <? $age ?> ans,
this function give:
J'ai 12 ans,
=cut
sub fill_template
{
my ($self,$masque,$vars)=@_;
my @buf=split(/\n/,$masque);
my $i=0;
while (my ($n,$v)=each(%$vars))
{
if ($v) {map {s/<\?\s\$$n\s\?>/$v/gm} @buf;}
else {map {s/<\?\s\$$n\s\?>//gm} @buf;}
$i++;
}
return join("\n",@buf);
}
=back
=head1 Error Handling
The set_err routine is used privately. You can ask for an array of all the errors
which occured inside the parse routine by calling:
@errors = $mailHTML->errstr;
If no errors where found, it'll return undef.
=cut
sub set_err {
my($self,$error) = @_;
my @array = @{$self->{_ERRORS}} if ($self->{_ERRORS});
push @array, $error;
$self->{_ERRORS} = \@array;
return 1;
}
sub errstr {
my($self) = @_;
return @{$self->{_ERRORS}} if ($self->{_ERRORS});
return ();
}
=head1 AUTHOR
Alain BARBET alian@alianwebserver.com
=cut
|