File Coverage

blib/lib/XML/Generator/RFC822/RDF.pm
Criterion Covered Total %
statement 116 197 58.8
branch 5 28 17.8
condition 0 7 0.0
subroutine 19 23 82.6
pod 2 2 100.0
total 142 257 55.2


line stmt bran cond sub pod time code
1             # $Id: RDF.pm,v 1.11 2004/12/22 23:21:21 asc Exp $
2 2     2   2886 use strict;
  2         6  
  2         120  
3              
4             package XML::Generator::RFC822::RDF;
5 2     2   12 use base qw (XML::SAX::Base);
  2         4  
  2         3224  
6              
7             $XML::Generator::RFC822::RDF::VERSION = '1.1';
8              
9             =head1 NAME
10              
11             XML::Generator::RFC822::RDF - generate RDF/XML SAX2 events for RFC822 messages
12              
13             =head1 SYNOPSIS
14              
15             my $folder = Email::Folder->new($path_mbox);
16              
17             while (my $msg = $folder->next_message()) {
18              
19             my $writer = XML::SAX::Writer->new();
20             my $filter = XML::Filter::DataIndenter->new(Handler=>$writer);
21             my $generator = XML::Generator::RFC822::RDF->new(Handler=>$filter);
22              
23             $generator->parse($msg);
24             }
25              
26             =head1 DESCRIPTION
27              
28             Generate RDF/XML SAX2 events for RFC822 messages.
29              
30             Messages are keyed using SHA1 digests of Message-IDs and email addresses. In
31             the case of the latter this makes it easier to merge messages with contact data
32             that has been serialized using XML::Generator::vCard::RDF (version 1.3+)
33              
34             =head1 DOCUMENT FORMAT
35              
36             + rdf:RDF
37              
38             + rdf:Description
39             @rdf:about = x-urn:ietf:params:rfc822#SHA1([MESSAGEID])
40             - rfc822:To
41             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
42             - rfc822:From
43             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
44             - rfc822:Cc
45             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
46             - rfc822:Return-Path
47             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
48             - rfc822:Delivered-To
49             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
50             - rfc822:Reply-To
51             @rdf:resource = http://http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
52             - rfc822:In-Reply-To
53             @rdf:resource x-urn:ietf:params:rfc822#SHA1([INREPLYTO])
54             - rfc8822:References
55             @rdf:resource x-urn:ietf:params:rfc822#SHA1([REFERENCES])
56             - rfc822:Date [REFORMATTED AS W3CDTF]
57             - rfc822:[ALLOTHERHEADERS]
58             + rfc822:Body
59             + rdf:Seq
60             - rdf:li
61             @rdf:resource = x-urn:ietf:params:rfc822:Body#SHA1([MESSAGEID])_[n]
62              
63             # Body/MIME parts
64             # (1) or more
65              
66             + rdf:Description
67             @rdf:aboout = x-urn:ietf:params:rfc822:Body#SHA1([MESSAGEID])_[n]
68             - rfc822:content-type
69             - rdf:value
70              
71             # To, From, Cc, Return-Path, Delivered-To, Reply-To
72             # (1) or more
73              
74             + rdf:Descripion
75             @rdf:about = http://xmlns.com/foaf/0.1/mbox_sha1sum#SHA1([EMAILADDRESS])
76             - vCard:FN
77             - vCard:EMAIL
78              
79             # In-Reply-To, References
80             # (1) or more
81              
82             + rdf:Description
83             @rdf:about = x-urn:ietf:params:rfc822#SHA1([MESSAGEID])
84             - rfc822:Message-ID
85              
86             All MIME values are decoded and everything is encoded as UTF-8.
87              
88             =cut
89              
90 2     2   61451 use Email::Address;
  2         102333  
  2         238  
91 2     2   12228 use Email::MIME;
  2         140668  
  2         77  
92              
93 2     2   3173 use Digest::SHA1 qw (sha1_hex);
  2         1920  
  2         229  
94 2     2   15 use Encode;
  2         5  
  2         191  
95 2     2   2067 use MIME::Words qw (decode_mimewords);
  2         6182  
  2         177  
96              
97 2     2   2083 use Date::Parse;
  2         27955  
  2         289  
98 2     2   1763 use Date::Format;
  2         6918  
  2         153  
99              
100 2     2   2638 use Memoize;
  2         5374  
  2         5470  
101              
102             sub import {
103 2     2   35 my $pkg = shift;
104 2         21 $pkg->SUPER::import(@_);
105              
106 2         49 memoize("_prepare_text","_prepare_mbox");
107             }
108              
109             =head1 PACKAGE METHODS
110              
111             =cut
112              
113             =head2 __PACKAGE__->new(%args)
114              
115             This method is inherited from I and returns a
116             I object. Additionally, the following
117             parameters are allowed :
118              
119             =over 4
120              
121             =item * B
122              
123             Boolean.
124              
125             If true, the parser will ignore a message's body and all headers
126             except : To, From, Cc, Return-Path, Delivered-To, Reply-To, Date,
127             Subject
128              
129             Default is false.
130              
131             =back
132              
133             =cut
134              
135             sub new {
136 1     1 1 8067 my $pkg = shift;
137 1         3 my %args = @_;
138              
139 1         14 my $self = $pkg->SUPER::new(%args);
140              
141 1 50       84 if (! $self) {
142 0         0 return undef;
143             }
144              
145 1         3 $self->{'__addrs'} = {};
146 1         2 $self->{'__relations'} = {};
147 1         3 $self->{'__parts'} = [];
148 1 50       5 $self->{'__brief'} = ($args{'Brief'}) ? 1 : 0;
149              
150 1         5 return bless $self,$pkg;
151             }
152              
153             =head1 OBJECT METHODS
154              
155             =cut
156              
157             =head2 $obj->parse(@messages)
158              
159             Where I<@messages> is one or more I objects.
160              
161             =cut
162              
163             sub parse {
164 1     1 1 471 my $self = shift;
165 1         2 my @messages = @_;
166              
167             #
168              
169 1         10 $self->start_document();
170              
171 1         46 $self->xml_decl({Version => "1.0",Encoding => "UTF-8"});
172              
173 1         25 my $ns = $self->_namespaces();
174              
175 1         3 foreach my $prefix (keys %$ns) {
176 4         56 $self->start_prefix_mapping({Prefix => $prefix,
177             NamespaceURI => $ns->{$prefix}});
178             }
179              
180 1         16 $self->start_element({Name=>"rdf:RDF"});
181              
182             #
183            
184 1         26 foreach my $msg (@messages) {
185 1         7 $self->_parse($msg);
186             }
187              
188             #
189            
190 1         6 $self->end_element({Name=>"rdf:RDF"});
191            
192 1         9 foreach my $prefix (keys %$ns) {
193 4         136 $self->end_prefix_mapping({Prefix=>$prefix});
194             }
195            
196 1         19 $self->end_document();
197 1         35 return 1;
198             }
199              
200             sub _parse {
201 1     1   2 my $self = shift;
202 1         2 my $msg = shift;
203              
204 1         6 my $sha1_msgid = sha1_hex($msg->header("Message-ID"));
205 1         101 my $about = sprintf("x-urn:ietf:params:rfc822#%s",$sha1_msgid);
206              
207 1         10 $self->start_element({Name => "rdf:Description",
208             Attributes => {"{}rdf:about" => {Name => "rdf:about",
209             Value => $about}}});
210              
211 1         8 foreach my $header (keys %{$msg->{head}}) {
  1         4  
212              
213 0         0 my $utf8_header = $header;
214              
215 0         0 $utf8_header =~ s/^\s+//;
216 0         0 $utf8_header =~ s/\s+$//;
217 0         0 $utf8_header =~ s/:$//;
218              
219 0         0 $utf8_header = encode_utf8($utf8_header);
220            
221             #
222              
223 0 0 0     0 if ($utf8_header =~ /^(?:from|to|cc|return-path|(?:delivered|reply)-to)$/i) {
    0          
    0          
    0          
224 0         0 $self->_email_address($utf8_header,$msg->header($utf8_header));
225             }
226            
227             elsif ($utf8_header =~ /^(?:in-reply-to|references)$/i) {
228              
229 0         0 my $resource = sprintf("x-urn:ietf:params:rfc822#%s",
230             sha1_hex($msg->header($header)));
231              
232 0         0 $self->start_element({Name => "rfc822:$utf8_header",
233             Attributes => {"{}rdf:resource" => {Name => "rdf:resource",
234             Value => encode_utf8($resource)}}});
235 0         0 $self->end_element({Name => "rfc822:$utf8_header"});
236              
237 0   0     0 $self->{'__relations'}->{$resource} ||= [ $msg->header($header), $resource ];
238             }
239            
240             elsif ($utf8_header eq "Date") {
241              
242 0         0 my $time = str2time($msg->header($header));
243 0         0 my $dt = time2str("%Y-%m-%dT%H:%M:%S%z",$time);
244              
245 0         0 $self->start_element({Name => "rfc822:$utf8_header"});
246 0         0 $self->characters({Data=>encode_utf8($dt)});
247 0         0 $self->end_element({Name => "rfc822:$utf8_header"});
248             }
249              
250             elsif (($utf8_header eq "Subject") || (! $self->{'__brief'})) {
251 0         0 $self->start_element({Name=>"rfc822:$utf8_header"});
252 0         0 $self->characters({Data=>&_prepare_text($msg->header($header))});
253 0         0 $self->end_element({Name=>"rfc822:$utf8_header"});
254             }
255              
256             else {}
257             }
258              
259 1         4 $self->_body($msg);
260 1         4 $self->end_element({Name=>"rdf:Description"});
261              
262 1         8 $self->_dump_body_parts($msg);
263 1         4 $self->_dump_emails();
264 1         13 $self->_dump_relations();
265              
266 1         3 return 1;
267             }
268              
269             sub _body {
270 1     1   2 my $self = shift;
271 1         1 my $msg = shift;
272              
273 1 50       4 if ($self->{'__brief'}) {
274 0         0 return 1;
275             }
276              
277 1         2 my $count = 1;
278            
279 1         4 my $parsed = Email::MIME->new($msg->as_string());
280 1         1451 my @parts = $parsed->parts();
281            
282 1         13 $self->start_element({Name => "rfc822:Body"});
283 1         70 $self->start_element({Name => "rdf:Seq"});
284            
285 1         9 my $sha1_msgid = sha1_hex($msg->header("Message-ID"));
286 1         67 my $body = sprintf("x-urn:ietf:params:rfc822:Body#%s",$sha1_msgid);
287            
288 1         3 foreach (@parts) {
289            
290 1         4 my $mpart = sprintf("%s_%s",$body,$count++);
291            
292 1         7 $self->start_element({Name => "rdf:li",
293             Attributes => {"{}rdf:resource" => {Name => "rdf:resource",
294             Value => encode_utf8($mpart)}}});
295 1         29 $self->end_element({Name => "rdf:li"});
296             }
297            
298 1         33 $self->end_element({Name => "rdf:Seq"});
299 1         9 $self->end_element({Name=>"rfc822:Body"});
300              
301 1         6 $self->{'__parts'} = \@parts;
302 1         3 return 1;
303             }
304              
305             sub _dump_body_parts {
306 1     1   1 my $self = shift;
307 1         2 my $msg = shift;
308              
309 1 50       15 if ($self->{'__brief'}) {
310 0         0 return 1;
311             }
312              
313 1         1 my $count = 1;
314            
315 1         2 foreach my $part (@{$self->{'__parts'}}) {
  1         4  
316            
317 1         4 my $mpart = sprintf("x-urn:ietf:params:rfc822:Body#%s_%s",
318             sha1_hex($msg->header("Message-ID")),
319             $count++);
320            
321 1         58 $self->start_element({Name=>"rdf:Description",
322             Attributes=>{ "{}rdf:about" => {Name => "rdf:about",
323             Value => encode_utf8($mpart)}}});
324            
325 1         49 $self->start_element({Name=>"rfc822:content-type"});
326 1         10 $self->characters({Data=>&_prepare_text($self->{'__parts'}->[0]->content_type())});
327 1         60 $self->end_element({Name=>"rfc822:content-type"});
328            
329 1         12 $self->start_element({Name=>"rdf:value"});
330 1         15 $self->start_cdata();
331             # Oof - do I need to mime_decode all this too?
332 1         31 $self->characters({Data=>&_prepare_text($self->{'__parts'}->[0]->body_raw())});
333 1         28 $self->end_cdata();
334 1         39 $self->end_element({Name=>"rdf:value"});
335 1         11 $self->end_element({Name=>"rdf:Description"});
336             }
337              
338 1         10 return 1;
339             }
340              
341             sub _dump_emails {
342 1     1   2 my $self = shift;
343              
344 1         2 foreach my $email (keys %{$self->{'__addrs'}}) {
  1         5  
345 0         0 $self->start_element({Name=>"rdf:Description",
346             Attributes=>{"{}rdf:about" => {Name => "rdf:about",
347             Value => &_prepare_mbox($email)}}});
348              
349             #
350              
351 0         0 my $fn = $self->{'__addrs'}->{$email};
352              
353 0         0 $self->start_element({Name => "vCard:FN"});
354              
355 0         0 my @keys = grep { /^\w/ } keys %$fn;
  0         0  
356              
357 0 0       0 if (scalar(@keys) > 1) {
358 0         0 $self->start_element({Name => "rdf:Bag"});
359              
360 0         0 foreach my $name (@keys) {
361 0         0 $self->start_element({Name=>"rdf:li"});
362 0         0 $self->characters({Data => &_prepare_text($name)});
363 0         0 $self->end_element({Name=>"rdf:li"});
364             }
365              
366 0         0 $self->end_element({Name => "rdf:Bag"});
367             }
368              
369             else {
370 0         0 $self->characters({Data => &_prepare_text($keys[0]) });
371             }
372              
373 0         0 $self->end_element({Name => "vCard:FN"});
374            
375             #
376              
377 0         0 $self->start_element({Name => "vCard:EMAIL"});
378 0         0 $self->characters({Data => $email});
379 0         0 $self->end_element({Name => "vCard:EMAIL"});
380            
381 0         0 $self->end_element({Name => "rdf:Description"});
382             }
383              
384 1         3 return 1;
385             }
386              
387             sub _dump_relations {
388 1     1   2 my $self = shift;
389              
390 1 50       4 if (! exists($self->{'__relations'})) {
391 0         0 return 1;
392             }
393              
394 1         2 foreach my $rel (keys %{$self->{'__relations'}}) {
  1         4  
395              
396 0 0       0 if (! exists($self->{'__relations'}->{$rel})) {
397 0         0 next;
398             }
399              
400 0         0 $self->_dump_relation($self->{'__relations'}->{$rel});
401             }
402              
403 1         2 return 1;
404             }
405              
406             sub _dump_relation {
407 0     0   0 my $self = shift;
408 0         0 my $data = shift;
409              
410 0         0 $self->start_element({Name=>"rdf:Description",
411             Attributes=>{"{}rdf:about" => {Name => "rdf:about",
412             Value => $data->[1]}}});
413 0         0 $self->start_element({Name => "rfc822:Message-ID"});
414 0         0 $self->characters({Data=>&_prepare_text($data->[0])});
415 0         0 $self->end_element({Name => "rfc822:Message-ID"});
416            
417 0         0 $self->end_element({Name=>"rdf:Description"});
418            
419 0         0 return 1;
420             }
421              
422             sub _email_address {
423 0     0   0 my $self = shift;
424 0         0 my $header = shift;
425              
426 0         0 my @addrs = Email::Address->parse(join(" ",@_));
427              
428 0 0       0 if (scalar(@addrs) > 1) {
429 0         0 $self->start_element({Name => "rfc822:$header"});
430 0         0 $self->start_element({Name => "rdf:Bag"});
431              
432 0         0 foreach my $addr (@addrs) {
433              
434 0         0 my ($email,$fn) = &_parse_address($addr);
435              
436 0         0 $self->start_element({Name => "rdf:li",
437             Attributes => {"{}rdf:parseType" => {Name => "rdf:resource",
438             Value => &_prepare_mbox($email)}}});
439 0         0 $self->end_element({Name => "rdf:li"});
440 0         0 $self->{'__addrs'}->{$email}->{$fn} ++;
441             }
442            
443 0         0 $self->end_element({Name => "rdf:Bag"});
444 0         0 $self->end_element({Name => "rfc822:$header"});
445             }
446            
447             else {
448              
449 0         0 my ($email,$fn) = &_parse_address($addrs[0]);
450              
451 0         0 $self->start_element({Name => "rfc822:$header",
452             Attributes => {"{}rdf:parseType" => {Name => "rdf:resource",
453             Value => &_prepare_mbox($email)}}});
454 0         0 $self->end_element({Name => "rfc822:$header"});
455              
456 0         0 $self->{'__addrs'}->{$email}->{$fn} ++;
457             }
458              
459 0         0 return 1;
460             }
461              
462             sub _parse_address {
463 0     0   0 my $addr = shift;
464              
465 0 0       0 if (! UNIVERSAL::isa($addr,"Email::Address")) {
466 0         0 return ("","");
467             }
468              
469 0         0 my $email = $addr->address();
470 0         0 my $fn = $addr->phrase();
471              
472 0 0       0 if ($fn) {
473 0         0 $fn =~ s/^["']//;
474 0         0 $fn =~ s/["']$//;
475             }
476              
477 0   0     0 return ($email,($fn || ""));
478             }
479              
480             # memoized
481              
482             sub _prepare_text {
483             my $txt = shift;
484              
485             my @decoded = decode_mimewords($txt);
486             return encode_utf8(join("", map{ $_->[0] }@decoded));
487             }
488              
489             # memoized
490              
491             sub _prepare_mbox {
492 0     0   0 my $email_addr = shift;
493 0         0 return encode_utf8(sprintf("%smbox_sha1sum#%s",
494             __PACKAGE__->_namespaces()->{foaf},
495             sha1_hex($email_addr)));
496             }
497              
498             sub _namespaces {
499             return {
500 1     1   7 "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
501             "rfc822" => "x-urn:ietf:params:rfc822#",
502             "foaf" => "http://xmlns.com/foaf/0.1/",
503             "vCard" => "http://www.w3.org/2001/vcard-rdf/3.0#",
504             }
505             }
506              
507             =head1 VERSION
508              
509             1.1
510              
511             =head1 DATE
512              
513             $Date: 2004/12/22 23:21:21 $
514              
515             =head1 AUTHOR
516              
517             Aaron Straup Cope Eascope@cpan.orgE
518              
519             =head1 SEE ALSO
520              
521             L
522              
523             =head1 LICENSE
524              
525             Copyright (c) 2004 Aaron Straup Cope. All Rights Reserved.
526              
527             This is free software, you may use it and distribute it
528             under the same terms as Perl itself.
529              
530             =cut
531              
532             return 1;
533              
534             __END__