File Coverage

blib/lib/Email/MIME/XMTP.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Email::MIME::XMTP;
2              
3 1     1   80472 use vars qw[$VERSION];
  1         3  
  1         66  
4             $VERSION = '0.42';
5              
6 1     1   7 use Email::MIME;
  1         2  
  1         221  
7              
8             # adds XMTP to Email::MIME namespace (pollution?)
9             package Email::MIME;
10              
11 1     1   18 use strict;
  1         3  
  1         41  
12              
13 1     1   7 use Email::MIME::Encodings;
  1         2  
  1         22  
14 1     1   544 use XML::Parser; #not used yet...
  0            
  0            
15              
16             my %namespaces = (
17             'rdf' => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
18             'rdfs' => "http://www.w3.org/2000/01/rdf-schema#",
19             'xmtp' => "http://www.openhealth.org/xmtp#",
20             'thread' => "http://www.w3.org/2001/03/thread#"
21             );
22              
23             =head1 NAME
24              
25             Email::MIME::XMTP - Extends Email::MIME objects to read and write XMTP
26              
27             =head1 SYNOPSIS
28              
29             use Email::MIME;
30             use Email::MIME::XMTP;
31             my $mail = Email::MIME->new($text);
32              
33             # Email::MIME::XMTP extra methods
34             my $xmlstring = $mail->as_XML;
35              
36             my $mailer = Email::Simple->new;
37             my $mail = $mailer->parseXML( xml => $xmlstring );
38              
39              
40             =head1 DESCRIPTION
41              
42             C extends Email::MIME to read and write XMTP.
43              
44             Read more about XMTP at http://www.openhealth.org/xmtp/
45              
46             =head1 METHODS
47              
48             Methods are the same as the one of Email::MIME. With the addition of
49             two extra ones for reading (parsing) and writing XMTP format. Plus one
50             to set the elements/headers XML namespaces and prefixes.
51              
52             =head2 parseXML
53              
54             Parse an XML SOURCE containing an XMTP formatted message and return a mail object.
55              
56             The parseXML method takes any of the following parameters:
57              
58             filename
59             xml
60             ioref
61              
62             One must be spacefied - it is an error if none is passed.
63              
64             This uses the familiar hash syntax, so an example might be:
65              
66             use Email::MIME::XMTP;
67             my $mailer = new Email::MIME::XMTP;
68              
69             my $mail = $mailer->parseXML( filename => 'example-mail-xmtp.xml');
70              
71             The parameters represent a filename, a string containing XML and
72             an open filehandle ref respectively.
73              
74             =cut
75              
76             # TODO
77             sub parseXML {
78             my ( $self, %params ) = @_;
79              
80             return
81             unless( exists $params{'xml'} or
82             exists $params{'filename'} or
83             exists $params{'ioref'}
84             );
85              
86             #my $parser = XML::Parser->new( %params );
87             #$parser->parse;
88             };
89              
90             sub _init_namespaces {
91             my ($self) = @_;
92              
93             $self->{'_XML_namespaces'} = {};
94              
95             map {
96             $self->{'_XML_namespaces'}->{ $_ } = $namespaces{ $_ };
97             } keys %namespaces;
98             };
99              
100             =head2 set_namespace( PREFIX, URI )
101              
102             Set the XML Namespace PREFIX to URI.
103              
104             Note a particular XML-Namespace can also be set and transported using
105             the special MIME header X-XMTP-xmlns as follows:
106              
107             X-XMTP-xmlns-:
108              
109             And then further referred into the MIME message using a X-XMTP-
110             header like:
111              
112             X-XMTP-: value
113              
114             In a multipart message each part can have its scoped namespaces.
115              
116             =cut
117              
118             sub set_namespace {
119             my ($self, $prefix, $uri) = @_;
120              
121             $self->_init_namespaces()
122             unless( exists $self->{'_XML_namespaces'} );
123              
124             return
125             unless( defined $prefix and
126             defined $uri );
127              
128             return
129             if( $prefix eq 'rdf' or
130             $prefix eq 'rdfs' or
131             $prefix eq 'xmtp' or
132             $prefix eq 'thread' );
133              
134             $self->{'_XML_namespaces'}->{ $prefix } = $uri;
135             };
136              
137             =head2 as_XML( [@FILTER_HEADERS] )
138              
139             Returns an XML XMTP representation of a message. Optionally the FILTER_HEADERS array
140             can be used to restrict the MIME headers to return. In case any special XML
141             namespace is set, in addition to XMTP; the headers must be listed with their
142             fully qualified XML QNAME e.g. myprefix:My-Header.
143              
144             =cut
145              
146             sub as_XML {
147             my ( $self, @filter_headers ) = @_;
148              
149             my $xml = "\n";
150              
151             my @parts = $self->parts; #take one part for the moment
152              
153             # be sure the first part is the message itself - then its multi-parts eventually
154             unshift @parts, $self
155             unless($#parts==0);
156              
157             # NOTE: we do primite nesting - and do not recurse over parts - is that what MIME spec says? nested multipart??!?
158             my $i = 1;
159             foreach my $part ( @parts ) {
160             # well we should recurse to nested parts here too...
161              
162             $part->_init_namespaces()
163             unless( exists $part->{'_XML_namespaces'} );
164              
165             # restore namespaces unless specified explicitly (correct how is done?)
166             map {
167             # we check special MIME (hacked!) headers
168             # NOTE: not sure that mail filters/firewalls will let these through :-(
169             my $header = $_;
170             if( $header =~ m/^X-XMTP-xmlns-(.+)/ ) {
171             my $prefix = $1;
172             my @vals = grep {defined $_ } map {
173             my $h = Encode::decode('MIME-Q', $self->header( $_ ));
174             $h = Encode::decode_utf8( $h )
175             unless( Encode::is_utf8( $h ) );
176             $h;
177             } @{ $part->{head}->{ $header } };
178             my $uri = pop @vals; #always take the last header to allow override
179             $uri =~ s/^\s*//;
180             $uri =~ s/\s*$//;
181             $part->set_namespace( $prefix, $uri )
182             unless( exists $part->{'_XML_namespaces'}->{ $prefix } );
183             };
184             } keys %{$part->{head}};
185              
186             unless( $i == 1 ) {
187             # inherit any namespace from top part unless set differently
188             map {
189             $part->set_namespace( $_, $self->{'_XML_namespaces'}->{ $_ } )
190             unless( exists $part->{'_XML_namespaces'}->{ $_ } );
191             } sort keys %{ $self->{'_XML_namespaces'} };
192             };
193              
194             my $about = Encode::decode('MIME-Q', $part->header( "Message-Id" ) );
195             $about = Encode::decode_utf8( $about )
196             unless( Encode::is_utf8( $about ) );
197             if($about) {
198             $about =~ m/<([^>]+)>/;
199             $about = "mid:" . $1;
200             $about = $part->xml_escape( $about );
201             };
202              
203             unless($i==1) {
204             next
205             unless( ( $#filter_headers < 0 ) ||
206             ( grep /^xmtp:Body-Multipart$/, @filter_headers ) );
207              
208             # fix XMTP bug - proper RDF striped nesting
209             $xml .= "\n".(" " x $i).""
210             if( $self->content_type =~ m/^\s*multipart/ );
211             };
212              
213             $xml .= "\n".(" " x $i)."
214             map {
215             $xml .= "\n".(" " x $i)."xmlns:". $_ ."='".$part->xml_escape( $part->{'_XML_namespaces'}->{ $_ } )."'";
216             } sort keys %{ $part->{'_XML_namespaces'} };
217             $xml .= "\n".(" " x $i)."rdf:about='$about'"
218             if($about);
219             $xml .= ">";
220              
221             my $body = $part->xml_escape( $part->_XMLbodyEncode() )
222             if( ( $#filter_headers < 0 ) ||
223             ( grep /^xmtp:Body$/, @filter_headers ) );
224              
225             # need to UTF-8 encode headers then, if possible (otheriwise it will print invalid XML and warn the user!)
226             $xml .= $part->_headers_as_XML( $i, @filter_headers );
227              
228             $xml .= "\n".(" " x $i)."". $body .""
229             if($body);
230              
231             # add special ones to the generated our just to make the generated XML more RDF-ish
232             # NOTE: we do not actually need to have these headers into the MIME message itself due
233             # we just map some special headers to some RDF meaningful ones.
234             #
235             # Add simple email threading using Annotea thread schema
236             # see http://www.w3.org/2001/03/thread
237            
238             # add rdfs:seeAlso to each 'References' header
239             my @seeAlso;
240             if( exists $part->{head}->{ 'References' } ) {
241             @seeAlso = map { split /\s+/; } grep { defined $_ } map {
242             my $h = Encode::decode('MIME-Q', $self->header( $_ ));
243             $h = Encode::decode_utf8( $h )
244             unless( Encode::is_utf8( $h ) );
245             $h;
246             } @{ $part->{head}->{ 'References' } };
247             my $i=0;
248             foreach my $seeAlso ( @seeAlso ) { # first is the root of the thread - last is the in-reply-to
249             $seeAlso =~ m/<([^>]+)>/;
250             $seeAlso = "mid:" . $1;
251             $seeAlso = $part->xml_escape( $seeAlso );
252             $xml .= "\n".(" " x $i)."";
253             $xml .= "\n".(" " x $i).""
254             if($i==0);
255             $xml .= "\n".(" " x $i).""
256             if($i==$#seeAlso);
257             $i++;
258             };
259             };
260              
261             # make the xmtp:Message of type thread:Reply if a reply
262             $xml .= "\n".(" " x $i)."{ 'thread' }."Reply' />"
263             if( exists $part->{head}->{ 'In-Reply-To' } );
264              
265             unless($i==1) {
266             $xml .= "\n".(" " x $i)."";
267              
268             # fix XMTP bug - proper RDF striped nesting
269             $xml .= "\n".(" " x $i).""
270             if( $self->content_type =~ m/^\s*multipart/ );
271             };
272            
273             $i++;
274             };
275              
276             $xml .= "\n";
277              
278             #print STDERR $xml;
279              
280             return $xml;
281             };
282              
283             # convert body to UTF-8 and encode it if necessary
284             sub _XMLbodyEncode {
285             my ( $self ) = @_;
286            
287             my $body = $self->{body};
288            
289             my $cte = $self->header("Content-Transfer-Encoding");
290              
291             if( $cte ne 'base64' and
292             $cte ne 'quoted-printable' ) {
293             # NOTE: we do not further check $cte here...
294              
295             # need to UTF-8 encode it then, if possible (otheriwise it will print invalid XML and warn the user!)
296             if( eval { require Encode } ) {
297             eval {
298              
299             # default to UTF-8 if no charset set - correct? what the RFC really says here? I guess force US-ASCII
300             # NOTE: due that US-ASCII is covered by UTF-8 it should be safe enough here - and assuming a client/MTA
301             # will add proper charset="...somthing..." to their Content-Type header otherwise
302              
303             # both the following decode() set the internal Perl UTF-8 flag (see man Encode)
304             if( $self->content_type =~ m/charset=([^;]+);?\s*/mi ) {
305             $body = Encode::decode( $1, $body );
306             } else {
307             $body = Encode::decode_utf8( $body )
308             unless( Encode::is_utf8( $body ) );
309             };
310             };
311              
312             # set Content-Type charset to UTF-8 for the output XML message
313             my $ct = $self->content_type;
314             $self->header_set( 'Content-Type', $ct )
315             if( $ct =~ s/charset=([^;]+)(;?\s*)/charset=UTF-8$2/mi );
316              
317             # we do not update Content-Transfer-Encoding to 8bit - correct?
318             $self->header_set( 'Content-Transfer-Encoding', '8bit' );
319             } else {
320             #warn "XMTP message xmtp:body not UTF-8 encoded. The Encode module is missing in your Perl installation.\n";
321             };
322              
323             # force base64 encoding due we do not make any euristics on Content-Type or Content-Transfer-Encoding yet
324             if( $cte eq 'binary' or
325             ( defined $self->content_type and
326             $self->content_type ne '' and
327             $self->content_type !~ m/text/i and
328             $self->content_type !~ m/^\s*multipart/i ) ) {
329             # set Content-Transfer-Encoding header to base64 if not there
330             $body = Email::MIME::Encodings::encode( base64 => $body );
331              
332             $self->header_set( 'Content-Transfer-Encoding', 'base64' );
333             };
334             };
335              
336             return $body;
337             };
338              
339             sub _headers_as_XML {
340             my ( $self, $i, @filter_headers ) = @_;
341              
342             my @order = @{$self->{order}};
343             my %head = %{$self->{head}};
344             my $stuff = "";
345              
346             # a valid XML tag
347             my $NameStrt = "[A-Za-z_:]|[^\\x00-\\x7F]";
348             my $NameChar = "[A-Za-z0-9_:.-]|[^\\x00-\\x7F]";
349             my $Name = "(?:$NameStrt)(?:$NameChar)*";
350              
351             while (keys %head) {
352             my $thing = shift @order;
353             next unless exists $head{$thing}; # We have already dealt with it
354              
355             my @hds = map {
356             ( Encode::is_utf8( $_ ) ) ? $_ : Encode::decode_utf8( $_ );
357             } map {
358             Encode::decode('MIME-Q', $_ );
359             } $self->header( $thing ); # be sure Email::MIME decode headers to UTF-8 for me
360              
361             if( eval { require Encode } ) {
362             eval {
363             @hds = map {
364             # the following set the internal Perl UTF-8 flag (see man Encode)
365             ( Encode::is_utf8( $_ ) ) ? $_ : Encode::decode_utf8( $_ );
366             } @hds;
367             };
368             } else {
369             #warn "XMTP message header/s not UTF-8 encoded. The Encode module is missing in your Perl installation.\n";
370             };
371              
372             $stuff .= $self->_header_as_XML($thing, \@hds, $i)
373             if( ( $thing =~ m/^$Name$/o ) && #skip non-XML tag alike headers
374             ( ( $#filter_headers < 0 ) ||
375             ( grep /^$thing$/, @filter_headers ) ) );
376              
377             delete $head{$thing};
378             };
379             return $stuff;
380             };
381              
382             sub _header_as_XML {
383             my ($self, $field, $data, $i) = @_;
384             my @stuff = @$data;
385             # Ignore "empty" headers
386             return '' unless @stuff = grep { defined $_ } @stuff;
387              
388             my $xml;
389             foreach ( @stuff ) {
390             my $ff = $_;
391             my @parts = split(/\s*;\s/, $ff );
392             # check if we really have a a MIME headers parameter/s list
393             foreach (@parts) {
394             my $p = $_; #copy
395             $p =~ s/^\s*//;
396             $p =~ s/\s*$//;
397             if( my @ss = split(/\s*=\s*/, $p ) ) {
398             # check each parameter
399             my $stop=0;
400             foreach my $sp ( @ss ) {
401             $sp =~ s/^\s*//;
402             $sp =~ s/\s*$//;
403             if( ( not ( $sp =~ m/^["']/ and # not a literal - correct?
404             $sp =~ m/["']$/ ) ) and
405             $sp =~ m/\s+/ ) { #wrong then
406             $stop=1;
407             last;
408             };
409             };
410             if($stop) {
411             (@parts) = shift @parts;
412             last;
413             };
414             } elsif( $p =~ m/\s+/ ) { # wrong then
415             (@parts) = shift @parts;
416             last;
417             };
418             };
419              
420             # skip special ones
421             next
422             if( $field =~ m/^X-XMTP-xmlns-/ );
423              
424             my $prefix;
425             $field =~ s/^X-XMTP-(.+)-(.+)$/$2/;
426             if($1) {
427             if( exists $self->{'_XML_namespaces'}->{ $1 } ) {
428             $prefix = $1;
429             } else {
430             next; # skip unknown fields
431             };
432             } else {
433             $prefix = 'xmtp';
434             };
435            
436             if($#parts == 0 ) {
437             $ff = "\n".(" " x $i)."<".$self->xml_escape( $prefix ).":".$self->xml_escape( $field ).
438             ">". $self->xml_escape( $ff ) . "xml_escape( $prefix ).":".$self->xml_escape( $field ).">";
439             } else {
440             # rdf:parseType="Resource"
441             $ff = "\n".(" " x $i)."<".$self->xml_escape( $prefix ).":".$self->xml_escape( $field )." rdf:parseType='Resource'>";
442             foreach my $part ( @parts ) {
443             my $subfield;
444             my $subvalue;
445             my @subparts = split(/\s*=\s*/, $part );
446             if( $#subparts == 0 ) {
447             $subfield = "rdf:value"; # right - but we can have at most ONE rdf:value is that true for MIME headers parameters??
448             $subvalue = $subparts[0];
449             } else {
450             $subfield = $prefix.':' . shift @subparts;
451             $subvalue = shift @subparts;
452             # trim value
453             $subvalue =~ s/^\s*["']//gm;
454             $subvalue =~ s/["']\s*$//gm;
455             };
456              
457             $ff .= "\n".(" " x $i)."<".$self->xml_escape( $subfield ).">". $self->xml_escape( $subvalue ) . "xml_escape( $subfield ).">";
458             };
459             $ff .= "\n".(" " x $i)."xml_escape( $prefix ).":".$self->xml_escape( $field ).">";
460             };
461              
462             $xml .= $ff;
463             };
464              
465             return $xml;
466             };
467              
468             sub xml_escape {
469             my $self = shift;
470             my $text = shift;
471              
472             $text =~ s/\&/\&/g;
473             $text =~ s/
474             foreach (@_) {
475             croak "xml_escape: '$_' isn't a single character" if length($_) > 1;
476              
477             if ($_ eq '>') {
478             $text =~ s/>/\>/g;
479             } elsif ($_ eq '"') {
480             $text =~ s/\"/\"/g;
481             } elsif ($_ eq "'") {
482             $text =~ s/\'/\'/g;
483             } else {
484             my $rep = '&#' . sprintf('x%X', ord($_)) . ';';
485             if (/\W/) {
486             my $ptrn = "\\$_";
487             $text =~ s/$ptrn/$rep/g;
488             } else {
489             $text =~ s/$_/$rep/g;
490             };
491             };
492             };
493             return $text;
494             };
495              
496             1;
497              
498             __END__