| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | ############################################################################### | 
| 2 |  |  |  |  |  |  | # Purpose : Build HTML emails | 
| 3 |  |  |  |  |  |  | # Author  : Tony Hennessy | 
| 4 |  |  |  |  |  |  | # Created : Aug 2006 | 
| 5 |  |  |  |  |  |  | ############################################################################### | 
| 6 |  |  |  |  |  |  |  | 
| 7 |  |  |  |  |  |  | package Email::MIME::CreateHTML; | 
| 8 |  |  |  |  |  |  |  | 
| 9 | 2 |  |  | 2 |  | 111076 | use strict; | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 68 |  | 
| 10 | 2 |  |  | 2 |  | 7 | use Carp; | 
|  | 2 |  |  |  |  | 3 |  | 
|  | 2 |  |  |  |  | 123 |  | 
| 11 | 2 |  |  | 2 |  | 8 | use Exporter; | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 52 |  | 
| 12 | 2 |  |  | 2 |  | 601 | use Email::MIME; | 
|  | 2 |  |  |  |  | 26344 |  | 
|  | 2 |  |  |  |  | 39 |  | 
| 13 | 2 |  |  | 2 |  | 1026 | use HTML::TokeParser::Simple; | 
|  | 2 |  |  |  |  | 33059 |  | 
|  | 2 |  |  |  |  | 59 |  | 
| 14 | 2 |  |  | 2 |  | 14 | use HTML::Tagset; | 
|  | 2 |  |  |  |  | 2 |  | 
|  | 2 |  |  |  |  | 60 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | our $VERSION = '1.041'; | 
| 17 |  |  |  |  |  |  |  | 
| 18 | 2 |  |  | 2 |  | 727 | use Email::MIME::CreateHTML::Resolver; | 
|  | 2 |  |  |  |  | 5 |  | 
|  | 2 |  |  |  |  | 66 |  | 
| 19 |  |  |  |  |  |  |  | 
| 20 |  |  |  |  |  |  | #Globals | 
| 21 | 2 |  |  | 2 |  | 10 | use vars qw(%EMBED @EXPORT_OK @ISA); | 
|  | 2 |  |  |  |  | 2 |  | 
|  | 2 |  |  |  |  | 2115 |  | 
| 22 |  |  |  |  |  |  | %EMBED = ( | 
| 23 |  |  |  |  |  |  | 'bgsound' => {'src'=>1}, | 
| 24 |  |  |  |  |  |  | 'body'    => {'background'=>1}, | 
| 25 |  |  |  |  |  |  | 'img'     => {'src'=>1}, | 
| 26 |  |  |  |  |  |  | 'input'   => {'src'=>1}, | 
| 27 |  |  |  |  |  |  | 'table'   => {'background'=>1}, | 
| 28 |  |  |  |  |  |  | 'td'      => {'background'=>1}, | 
| 29 |  |  |  |  |  |  | 'th'      => {'background'=>1}, | 
| 30 |  |  |  |  |  |  | 'tr'      => {'background'=>1}, | 
| 31 |  |  |  |  |  |  | ); | 
| 32 |  |  |  |  |  |  | @EXPORT_OK = qw(embed_objects parts_for_objects build_html_email); | 
| 33 |  |  |  |  |  |  | @ISA = qw(Exporter); | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | # | 
| 36 |  |  |  |  |  |  | # Public routines used by create_html and also exportable | 
| 37 |  |  |  |  |  |  | # | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | sub embed_objects { | 
| 40 | 9 |  |  | 9 | 1 | 12 | my ($html, $args) = @_; | 
| 41 | 9 | 100 | 66 |  |  | 38 | my $embed = ( defined $args->{embed} && $args->{embed} eq '0' ) ? 0 : 1; | 
| 42 | 9 | 100 | 66 |  |  | 33 | my $inline_css = ( defined $args->{inline_css} && $args->{inline_css} eq '0' ) ? 0 : 1; | 
| 43 | 9 |  |  |  |  | 53 | my $resolver = new Email::MIME::CreateHTML::Resolver($args); | 
| 44 | 8 |  | 50 |  |  | 32 | my $embed_tags = $args->{'embed_elements'} || \%EMBED; | 
| 45 |  |  |  |  |  |  |  | 
| 46 | 8 | 50 | 66 |  |  | 25 | return ($html, {}) unless ( $embed || $inline_css ); #No-op unless one of these is set | 
| 47 |  |  |  |  |  |  |  | 
| 48 | 8 |  |  |  |  | 7 | my ($html_modified, %embedded_cids); | 
| 49 | 8 |  |  |  |  | 56 | my $parser = HTML::TokeParser::Simple->new( \$html ); | 
| 50 | 8 |  |  |  |  | 1039 | my $regex = '^(' . join('|',keys %HTML::Tagset::linkElements) . ')'; | 
| 51 | 8 |  |  |  |  | 223 | $regex = qr/$regex/; | 
| 52 | 8 |  |  |  |  | 34 | while ( my $token = $parser->get_token ) { | 
| 53 |  |  |  |  |  |  |  | 
| 54 | 172 | 100 |  |  |  | 4022 | unless ( $token->is_start_tag( $regex ) ) { | 
| 55 | 147 |  |  |  |  | 709 | $html_modified .= $token->as_is; | 
| 56 | 147 |  |  |  |  | 678 | next; | 
| 57 |  |  |  |  |  |  | } | 
| 58 | 25 |  |  |  |  | 281 | my $token_tag = $token->get_tag(); | 
| 59 | 25 |  |  |  |  | 117 | my $token_attrs = $token->get_attr(); | 
| 60 |  |  |  |  |  |  |  | 
| 61 |  |  |  |  |  |  | # inline_css | 
| 62 | 25 | 100 | 66 |  |  | 244 | if ( $token_tag eq 'link' && $token_attrs->{type} eq 'text/css' ) { | 
| 63 | 2 | 100 |  |  |  | 6 | unless ( $inline_css ) { | 
| 64 | 1 |  |  |  |  | 4 | $html_modified .= $token->as_is; | 
| 65 | 1 |  |  |  |  | 5 | next; | 
| 66 |  |  |  |  |  |  | } | 
| 67 | 1 |  |  |  |  | 2 | my $link = $token_attrs->{'href'}; | 
| 68 | 1 |  |  |  |  | 4 | my ($content,$filename,$mimetype,$encoding) = $resolver->get_resource( $link ); | 
| 69 | 1 |  |  |  |  | 4 | $html_modified .= "\n".'\n"; | 
| 72 | 1 |  |  |  |  | 4 | next; | 
| 73 |  |  |  |  |  |  | } | 
| 74 |  |  |  |  |  |  |  | 
| 75 |  |  |  |  |  |  | # rewrite and embed | 
| 76 | 23 |  |  |  |  | 22 | for my $attr ( @{ $HTML::Tagset::linkElements{$token_tag} } ) { | 
|  | 23 |  |  |  |  | 49 |  | 
| 77 | 53 | 100 |  |  |  | 433 | if ( defined $token_attrs->{$attr} ) { | 
| 78 | 11 |  |  |  |  | 16 | my $link = $token_attrs->{$attr}; | 
| 79 | 11 | 100 |  |  |  | 32 | next if ($link =~ m/^cid:/i); | 
| 80 |  |  |  |  |  |  |  | 
| 81 |  |  |  |  |  |  | # embed | 
| 82 | 5 | 100 | 66 |  |  | 28 | if ( $embed && $embed_tags->{$token_tag}->{$attr} ) { | 
| 83 | 4 | 100 |  |  |  | 10 | unless ( defined $embedded_cids{$link} ) { | 
| 84 |  |  |  |  |  |  | # make a unique cid | 
| 85 | 3 |  |  |  |  | 14 | my $newcid = time().$$.int(rand(1e6)); | 
| 86 | 3 |  |  |  |  | 5 | $embedded_cids{$link} = $newcid; | 
| 87 |  |  |  |  |  |  | } | 
| 88 | 4 |  |  |  |  | 62 | my $link_rewrite = "cid:".$embedded_cids{$link}; | 
| 89 | 4 |  |  |  |  | 13 | $token->set_attr( $attr => $link_rewrite ); | 
| 90 |  |  |  |  |  |  | } | 
| 91 |  |  |  |  |  |  | } | 
| 92 |  |  |  |  |  |  | } | 
| 93 | 23 |  |  |  |  | 48 | $html_modified .= $token->as_is; | 
| 94 |  |  |  |  |  |  | } | 
| 95 |  |  |  |  |  |  |  | 
| 96 | 8 |  |  |  |  | 121 | my %objects = reverse %embedded_cids; #invert mapping | 
| 97 | 8 |  |  |  |  | 86 | return ($html_modified, \%objects); | 
| 98 |  |  |  |  |  |  | } | 
| 99 |  |  |  |  |  |  |  | 
| 100 |  |  |  |  |  |  | sub parts_for_objects { | 
| 101 | 8 |  |  | 8 | 1 | 8 | my ($objects, $args) = @_; | 
| 102 | 8 |  |  |  |  | 27 | my $resolver = new Email::MIME::CreateHTML::Resolver($args); | 
| 103 |  |  |  |  |  |  |  | 
| 104 | 8 |  |  |  |  | 7 | my @html_mime_parts; | 
| 105 | 8 |  |  |  |  | 17 | foreach my $cid (keys %$objects) { | 
| 106 | 9 | 50 |  |  |  | 52 | croak "Content-Id '$cid' contains bad characters" unless ($cid =~ m/^[\w\-\@\.]+$/); | 
| 107 | 9 | 50 |  |  |  | 19 | croak "Content-Id must be given" unless length($cid); | 
| 108 |  |  |  |  |  |  |  | 
| 109 | 9 |  |  |  |  | 11 | my $path = $objects->{$cid}; | 
| 110 | 9 |  |  |  |  | 23 | my ($content,$filename,$mimetype,$encoding) = $resolver->get_resource( $path ); | 
| 111 |  |  |  |  |  |  |  | 
| 112 | 9 |  | 50 |  |  | 28 | $mimetype ||= 'application/octet-stream'; | 
| 113 | 9 |  |  |  |  | 71 | my $newpart = Email::MIME->create( | 
| 114 |  |  |  |  |  |  | attributes => { | 
| 115 |  |  |  |  |  |  | content_type => $mimetype, | 
| 116 |  |  |  |  |  |  | encoding => $encoding, | 
| 117 |  |  |  |  |  |  | disposition => 'inline', # maybe useful rfc2387 | 
| 118 |  |  |  |  |  |  | charset => undef, | 
| 119 |  |  |  |  |  |  | name => $filename, | 
| 120 |  |  |  |  |  |  | }, | 
| 121 |  |  |  |  |  |  | body => $content, | 
| 122 |  |  |  |  |  |  | ); | 
| 123 | 9 |  |  |  |  | 8013 | $newpart->header_set('Content-ID',"<$cid>"); | 
| 124 |  |  |  |  |  |  | #		$newpart->header_set("Content-Transfer-Encoding", "base64"); | 
| 125 | 9 |  |  |  |  | 289 | push @html_mime_parts , $newpart; | 
| 126 |  |  |  |  |  |  | } | 
| 127 | 8 |  |  |  |  | 31 | return @html_mime_parts; | 
| 128 |  |  |  |  |  |  | } | 
| 129 |  |  |  |  |  |  |  | 
| 130 |  |  |  |  |  |  | sub build_html_email { | 
| 131 | 8 |  |  | 8 | 1 | 13 | my($header, $html, $body_attributes, $html_mime_parts, $plain_text_mime) = @_; | 
| 132 |  |  |  |  |  |  |  | 
| 133 | 8 | 100 |  |  |  | 24 | $body_attributes->{charset} = 'UTF-8' unless exists $body_attributes->{charset}; | 
| 134 | 8 | 100 |  |  |  | 21 | $body_attributes->{encoding}= 'quoted-printable' unless exists $body_attributes->{encoding}; | 
| 135 |  |  |  |  |  |  |  | 
| 136 | 8 |  |  |  |  | 12 | my $email; | 
| 137 | 8 | 100 | 100 |  |  | 72 | if ( ! scalar(@$html_mime_parts) && ! defined($plain_text_mime) ) { | 
|  |  | 100 | 66 |  |  |  |  | 
|  |  | 100 | 66 |  |  |  |  | 
|  |  | 50 | 33 |  |  |  |  | 
| 138 |  |  |  |  |  |  | # HTML, no embedded objects, no text alternative | 
| 139 | 2 |  |  |  |  | 13 | $email = Email::MIME->create( | 
| 140 |  |  |  |  |  |  | header => $header, | 
| 141 |  |  |  |  |  |  | attributes => $body_attributes, | 
| 142 |  |  |  |  |  |  | body_str => $html, | 
| 143 |  |  |  |  |  |  | ); | 
| 144 |  |  |  |  |  |  | } | 
| 145 |  |  |  |  |  |  | elsif ( ! scalar(@$html_mime_parts) && defined($plain_text_mime) ) { | 
| 146 |  |  |  |  |  |  | # HTML, no embedded objects, with text alternative | 
| 147 | 1 |  |  |  |  | 6 | $email = Email::MIME->create( | 
| 148 |  |  |  |  |  |  | header => $header, | 
| 149 |  |  |  |  |  |  | attributes => {content_type=>'multipart/alternative'}, | 
| 150 |  |  |  |  |  |  | parts => [ | 
| 151 |  |  |  |  |  |  | $plain_text_mime, | 
| 152 |  |  |  |  |  |  | Email::MIME->create( | 
| 153 |  |  |  |  |  |  | attributes => $body_attributes, | 
| 154 |  |  |  |  |  |  | body_str => $html, | 
| 155 |  |  |  |  |  |  | ), | 
| 156 |  |  |  |  |  |  | ], | 
| 157 |  |  |  |  |  |  | ); | 
| 158 |  |  |  |  |  |  | } | 
| 159 |  |  |  |  |  |  | elsif ( scalar(@$html_mime_parts) && ! defined($plain_text_mime) ) { | 
| 160 |  |  |  |  |  |  | # HTML with embedded objects, no text alternative | 
| 161 | 4 |  |  |  |  | 19 | $email = Email::MIME->create( | 
| 162 |  |  |  |  |  |  | header => $header, | 
| 163 |  |  |  |  |  |  | attributes => {content_type=>'multipart/related'}, | 
| 164 |  |  |  |  |  |  | parts => [ | 
| 165 |  |  |  |  |  |  | Email::MIME->create( | 
| 166 |  |  |  |  |  |  | attributes => $body_attributes, | 
| 167 |  |  |  |  |  |  | body_str => $html, | 
| 168 |  |  |  |  |  |  | ), | 
| 169 |  |  |  |  |  |  | @$html_mime_parts, | 
| 170 |  |  |  |  |  |  | ], | 
| 171 |  |  |  |  |  |  | ); | 
| 172 |  |  |  |  |  |  | } | 
| 173 |  |  |  |  |  |  | elsif ( scalar(@$html_mime_parts) && defined($plain_text_mime) ) { | 
| 174 |  |  |  |  |  |  | # HTML with embedded objects, with text alternative | 
| 175 | 1 |  |  |  |  | 7 | $email = Email::MIME->create( | 
| 176 |  |  |  |  |  |  | header => $header, | 
| 177 |  |  |  |  |  |  | attributes => {content_type=>'multipart/alternative'}, | 
| 178 |  |  |  |  |  |  | parts => [ | 
| 179 |  |  |  |  |  |  | $plain_text_mime, | 
| 180 |  |  |  |  |  |  | Email::MIME->create( | 
| 181 |  |  |  |  |  |  | attributes => {content_type=>'multipart/related'}, | 
| 182 |  |  |  |  |  |  | parts => [ | 
| 183 |  |  |  |  |  |  | Email::MIME->create( | 
| 184 |  |  |  |  |  |  | attributes => $body_attributes, | 
| 185 |  |  |  |  |  |  | body_str => $html, | 
| 186 |  |  |  |  |  |  | ), | 
| 187 |  |  |  |  |  |  | @$html_mime_parts, | 
| 188 |  |  |  |  |  |  | ], | 
| 189 |  |  |  |  |  |  | ), | 
| 190 |  |  |  |  |  |  | ], | 
| 191 |  |  |  |  |  |  | ); | 
| 192 |  |  |  |  |  |  | } | 
| 193 | 8 |  |  |  |  | 25268 | return $email; | 
| 194 |  |  |  |  |  |  | } | 
| 195 |  |  |  |  |  |  |  | 
| 196 |  |  |  |  |  |  | # Add to Email::MIME | 
| 197 |  |  |  |  |  |  | package # Hide from PAUSE | 
| 198 |  |  |  |  |  |  | Email::MIME; | 
| 199 |  |  |  |  |  |  |  | 
| 200 | 2 |  |  | 2 |  | 10 | use strict; | 
|  | 2 |  |  |  |  | 3 |  | 
|  | 2 |  |  |  |  | 70 |  | 
| 201 | 2 |  |  | 2 |  | 8 | use Carp; | 
|  | 2 |  |  |  |  | 2 |  | 
|  | 2 |  |  |  |  | 88 |  | 
| 202 | 2 |  |  | 2 |  | 8 | use Email::MIME::Creator; | 
|  | 2 |  |  |  |  | 3 |  | 
|  | 2 |  |  |  |  | 420 |  | 
| 203 |  |  |  |  |  |  |  | 
| 204 |  |  |  |  |  |  | sub create_html { | 
| 205 | 9 |  |  | 9 | 0 | 32796 | my ($class, %args) = @_; | 
| 206 |  |  |  |  |  |  |  | 
| 207 |  |  |  |  |  |  | #Argument checking/defaulting | 
| 208 | 9 |  | 33 |  |  | 33 | my $html = $args{body} || croak "You must supply a body"; | 
| 209 | 9 |  | 100 |  |  | 33 | my $objects = $args{'objects'} || undef; | 
| 210 |  |  |  |  |  |  |  | 
| 211 |  |  |  |  |  |  | # Make plain text Email::MIME object, we will never use this alone so we don't need the headers | 
| 212 | 9 |  |  |  |  | 8 | my $plain_text_mime; | 
| 213 | 9 | 100 |  |  |  | 28 | if ( exists($args{text_body}) ) { | 
| 214 | 2 | 50 |  |  |  | 3 | my %text_body_attributes = ( (content_type=>'text/plain'), %{$args{text_body_attributes} || {}} ); | 
|  | 2 |  |  |  |  | 13 |  | 
| 215 | 2 |  |  |  |  | 10 | $plain_text_mime = $class->create( | 
| 216 |  |  |  |  |  |  | attributes => \%text_body_attributes, | 
| 217 |  |  |  |  |  |  | body => $args{text_body}, | 
| 218 |  |  |  |  |  |  | ); | 
| 219 |  |  |  |  |  |  | } | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | # Parse the HTML and create a CID mapping for objects to embed | 
| 222 | 9 |  |  |  |  | 939 | my $embedded_cids; | 
| 223 | 9 |  |  |  |  | 25 | ($html, $embedded_cids) = Email::MIME::CreateHTML::embed_objects($html, \%args); | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  | # Create parts for each embedded object | 
| 226 | 8 |  |  |  |  | 13 | my @html_mime_parts; | 
| 227 | 8 | 100 |  |  |  | 26 | push @html_mime_parts, Email::MIME::CreateHTML::parts_for_objects($objects, \%args) if ($objects); | 
| 228 | 8 | 100 |  |  |  | 23 | push @html_mime_parts, Email::MIME::CreateHTML::parts_for_objects($embedded_cids, \%args) if(%$embedded_cids); | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | # Create the mail | 
| 231 | 8 |  |  |  |  | 14 | my $header = $args{header}; | 
| 232 | 8 | 100 |  |  |  | 10 | my %body_attributes = ( (content_type=>'text/html'), %{$args{body_attributes} || {}}); | 
|  | 8 |  |  |  |  | 50 |  | 
| 233 | 8 |  |  |  |  | 27 | my $email = Email::MIME::CreateHTML::build_html_email($header, $html, \%body_attributes, \@html_mime_parts, $plain_text_mime); | 
| 234 | 8 |  |  |  |  | 76 | return $email; | 
| 235 |  |  |  |  |  |  | } | 
| 236 |  |  |  |  |  |  |  | 
| 237 |  |  |  |  |  |  | #Log::Trace stubs | 
| 238 | 0 |  |  | 0 | 0 |  | sub DUMP {} | 
| 239 | 0 |  |  | 0 | 0 |  | sub TRACE {} | 
| 240 |  |  |  |  |  |  |  | 
| 241 |  |  |  |  |  |  | 1; | 
| 242 |  |  |  |  |  |  |  | 
| 243 |  |  |  |  |  |  | __END__ |