/[suikacvs]/messaging/manakai/lib/Message/Field/Address.pm
Suika

Contents of /messaging/manakai/lib/Message/Field/Address.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.3 - (hide annotations) (download)
Wed Mar 20 09:56:26 2002 UTC (23 years, 1 month ago) by wakaba
Branch: MAIN
Changes since 1.2: +49 -10 lines
2002-03-20  wakaba <w@suika.fam.cx>

	* MsgID.pm, Received.pm, Subject.pm: New modules.
	* MsgID/: New directory.

1 wakaba 1.1
2     =head1 NAME
3    
4     Message::Field::Address Perl module
5    
6     =head1 DESCRIPTION
7    
8     Perl module for RFC 822/2822 address related C<field>s.
9    
10     =cut
11    
12     package Message::Field::Address;
13     require 5.6.0;
14     use strict;
15     use re 'eval';
16 wakaba 1.3 use vars qw(%OPTION %REG $VERSION);
17 wakaba 1.1 $VERSION = '1.00';
18    
19 wakaba 1.2 use overload '@{}' => sub {shift->{address}},
20     '""' => sub {shift->stringify};
21 wakaba 1.1
22     $REG{comment} = qr/\x28(?:\x5C[\x00-\xFF]|[\x00-\x0C\x0E-\x27\x2A-\x5B\x5D-\xFF]+|(??{$REG{comment}}))*\x29/;
23     $REG{quoted_string} = qr/\x22(?:\x5C[\x00-\xFF]|[\x00-\x0C\x0E-\x21\x23-\x5B\x5D-\xFF])*\x22/;
24     $REG{domain_literal} = qr/\x5B(?:\x5C[\x00-\xFF]|[\x00-\x0C\x0E-\x5A\x5E-\xFF])*\x5D/;
25    
26     $REG{WSP} = qr/[\x20\x09]+/;
27     $REG{FWS} = qr/[\x20\x09]*/;
28     $REG{atext} = qr/[\x21\x23-\x27\x2A\x2B\x2D\x2F\x30-\x39\x3D\x3F\x41-\x5A\x5E-\x7E]+/;
29     $REG{dot_atom} = qr/$REG{atext}(?:$REG{FWS}\x2E$REG{FWS}$REG{atext})*/;
30     $REG{dot_word} = qr/(?:$REG{atext}|$REG{quoted_string})(?:$REG{FWS}\x2E$REG{FWS}(?:$REG{atext}|$REG{quoted_string}))*/;
31     $REG{phrase} = qr/(?:$REG{atext}|$REG{quoted_string})(?:$REG{atext}|$REG{quoted_string}|\.|$REG{FWS})*/;
32     $REG{obs_route} = qr/(?:\x40$REG{FWS}(?:$REG{dot_word}|$REG{domain_literal})(?:$REG{FWS},?$REG{FWS}\x40$REG{FWS}(?:$REG{dot_word}|$REG{domain_literal}))*):/;
33     $REG{addr_spec} = qr/$REG{dot_word}$REG{FWS}\x40$REG{FWS}(?:$REG{dot_atom}|$REG{domain_literal})/;
34     $REG{mailbox} = qr/(?:(?:$REG{phrase})?<$REG{FWS}(?:$REG{obs_route})?$REG{FWS}$REG{addr_spec}$REG{FWS}>|$REG{addr_spec})/;
35     $REG{mailbox_list} = qr/$REG{mailbox}(?:$REG{FWS},(?:$REG{FWS}$REG{mailbox})?)*/;
36     $REG{address} = qr/(?:(?:$REG{phrase})?(?:<$REG{FWS}(?:$REG{obs_route})?$REG{FWS}$REG{addr_spec}$REG{FWS}>|:$REG{FWS}(?:$REG{mailbox_list}$REG{FWS})?;)|$REG{addr_spec})/;
37     $REG{address_list} = qr/$REG{address}(?:$REG{FWS},(?:$REG{FWS}$REG{address})?)*/;
38     $REG{M_group} = qr/($REG{phrase}):/;
39     $REG{M_mailbox} = qr/(?:($REG{phrase})?<$REG{FWS}($REG{obs_route})?$REG{FWS}($REG{addr_spec})$REG{FWS}>|($REG{addr_spec}))/;
40     $REG{M_quoted_string} = qr/\x22((?:\x5C[\x00-\xFF]|[\x00-\x0C\x0E-\x21\x23-\x5B\x5D-\xFF])*)\x22/;
41    
42     $REG{NON_atom} = qr/[^\x09\x20\x21\x23-\x27\x2A\x2B\x2D\x2F\x30-\x39\x3D\x3F\x41-\x5A\x5E-\x7E]/;
43    
44 wakaba 1.3 %OPTION = (
45     is_mailbox => -1,
46     is_return_path => -1,
47     use_display_name => 1,
48     use_group => 1,
49     );
50    
51     sub _init_option ($$) {
52     my $self = shift;
53     my $field_name = shift;
54     if ($field_name eq 'from' || $field_name eq 'resent-from') {
55     $self->{option}->{is_mailbox} = 1;
56     } elsif ($field_name eq 'return-path') {
57     $self->{option}->{is_mailbox} = 1;
58     $self->{option}->{is_return_path} = 1;
59     $self->{option}->{use_display_name} = -1;
60     }
61     $self;
62     }
63    
64 wakaba 1.2 =head2 Message::Field::Address->new ()
65 wakaba 1.1
66     Return empty address object.
67    
68     =cut
69    
70 wakaba 1.3 sub new ($;%) {
71     my $self = bless {type => '_ROOT'}, shift;
72     my %option = @_;
73     for (%OPTION) {$option{$_} ||= $OPTION{$_}}
74     $self->{option} = \%option;
75     $self->_init_option ($self->{option}->{field_name});
76     $self;
77 wakaba 1.1 }
78    
79 wakaba 1.2 =head2 Message::Field::Address->parse ($unfolded_field_body)
80 wakaba 1.1
81     Parse structured C<field-body> contain of C<address-list>.
82    
83     =cut
84    
85 wakaba 1.3 sub parse ($$;%) {
86 wakaba 1.1 my $self = bless {}, shift;
87     my $field_body = shift;
88 wakaba 1.3 my %option = @_;
89     for (%OPTION) {$option{$_} ||= $OPTION{$_}}
90     $self->{option} = \%option;
91     $self->_init_option ($self->{option}->{field_name});
92 wakaba 1.1 $field_body = $self->delete_comment ($field_body);
93     my %addr = $self->parse_address_list ($field_body);
94     $self->{address} = $addr{address};
95     $self->{type} = $addr{type};
96     $self;
97     }
98    
99     =head2 $self->address ()
100    
101     Return address list in the format described in
102     L<$self-E<gt>parse_address_list ()>.
103    
104     =cut
105    
106     sub address ($) {@{shift->{address}}}
107    
108     =head2 $self->add ($addr_spec, [%option])
109    
110     Add an mail address to C<$self> (address object).
111     %option = (name => C<display-name>, route => C<route>,
112     group => C<display-name> of C<group>)
113    
114     Note that this method (and other methods) does not check
115     $addr_spec and $option{route} is valid or not.
116    
117     =cut
118    
119     sub add ($$;%) {
120     my $self = shift;
121     my ($addr, %option) = @_;
122     my $name = $option{name} || $option{display_name};
123     unless ($option{group}) {
124     push @{$self->{address}}, {type => 'mailbox',
125     addr_spec => $addr, display_name => $name, route => $option{route}};
126     } else {
127     for my $i (@{$self->{address}}) {
128     if ($i->{type} eq 'group' && $i->{display_name} eq $option{group}) {
129     push @{$i->{address}}, {type => 'mailbox',
130     addr_spec => $addr, display_name => $name, route => $option{route}};
131     return $self;
132     }
133     }
134     push @{$self->{address}}, {type => 'group', display_name => $option{group},
135     address => [
136     {type => 'mailbox',
137     addr_spec => $addr, display_name => $name, route => $option{route}}
138     ]};
139     }
140     $self;
141     }
142    
143 wakaba 1.3 sub stringify ($;%) {
144 wakaba 1.1 my $self = shift;
145 wakaba 1.3 my %option = @_;
146     $option{is_mailbox} ||= $self->{option}->{is_mailbox};
147     $option{is_return_path} ||= $self->{option}->{is_return_path};
148     $option{use_display_name} ||= $self->{option}->{use_display_name};
149     $option{use_group} ||= $self->{option}->{use_group};
150 wakaba 1.1 my @return;
151     for my $address (@{$self->{address}}) {
152     my $return = '';
153     next if !$address->{addr_spec} && $address->{type} ne 'group';
154 wakaba 1.3 if ($address->{display_name} && $option{use_display_name}>0) {
155 wakaba 1.1 $return = $self->quote_unsafe_string ($address->{display_name})
156 wakaba 1.3 .($address->{type} eq 'group' && $option{use_group}>0? ': ': ' ');
157 wakaba 1.1 }
158     if ($address->{type} ne 'group') {
159     $return .= '<'.$address->{route}.$address->{addr_spec}.'>';
160     } else {
161     my (@g_return);
162     for my $mailbox (@{$address->{address}}) {
163     next unless $mailbox->{addr_spec};
164     my $g_return = '';
165     $g_return = $self->quote_unsafe_string ($mailbox->{display_name}) .' '
166 wakaba 1.3 if $mailbox->{display_name} && $option{use_display_name}>0;
167 wakaba 1.1 $g_return .= '<'.$mailbox->{route}.$mailbox->{addr_spec}.'>';
168     push @g_return, $g_return;
169 wakaba 1.3 last if $option{is_mailbox}>0;
170 wakaba 1.1 }
171     $return .= join ', ', @g_return;
172 wakaba 1.3 $return .= ';' if $address->{type} eq 'group' && $option{use_group}>0;
173 wakaba 1.1 }
174     push @return, $return;
175 wakaba 1.3 last if $option{is_mailbox}>0;
176     }
177     if ($option{is_return_path}>0 && $#return == -1) {
178     push @return, '<>';
179 wakaba 1.1 }
180     join ', ', @return;
181     }
182    
183     sub quote_unsafe_string ($$) {
184     my $self = shift;
185     my $string = shift;
186     if ($string =~ /$REG{NON_atom}/ || $string =~ /$REG{WSP}$REG{WSP}+/) {
187     $string =~ s/([\x22\x5C])/\x5C$1/g;
188     $string = '"'.$string.'"';
189     }
190     $string;
191     }
192    
193     =head2 $self->unquote_quoted_string ($string)
194    
195     Unquote C<quoted-string>. Get rid of C<DQUOTE>s and
196     C<REVERSED SOLIDUS> included in C<quoted-pair>.
197     This method is intended for internal use.
198    
199     =cut
200    
201     sub unquote_quoted_string ($$) {
202     my $self = shift;
203     my $quoted_string = shift;
204     $quoted_string =~ s{$REG{M_quoted_string}}{
205     my $qtext = $1;
206     $qtext =~ s/\x5C([\x00-\xFF])/$1/g;
207     $qtext;
208     }goex;
209     $quoted_string;
210     }
211    
212     =head2 $self->parse_mailbox ($mailbox)
213    
214     Parse C<mailbox> and return array of C<addr-spec>,
215     C<display-name> and C<route> (aka C<obs-route> of RFC 2822).
216     This method is intended for internal use.
217    
218     =cut
219    
220     sub parse_mailbox ($$) {
221     my $self = shift;
222     my $mailbox = shift;
223     if ($mailbox =~ /$REG{M_mailbox}/) {
224     my ($display_name, $route, $addr_spec) = ($1, $2, $3 || $4);
225     $display_name =~ s/$REG{WSP}+$//;
226     $display_name = $self->unquote_quoted_string ($display_name);
227     $addr_spec =~ s{($REG{quoted_string}|$REG{domain_literal})|$REG{WSP}}{$1}go;
228     $route =~ s{($REG{quoted_string}|$REG{domain_literal})|$REG{WSP}}{$1}go;
229     return ($addr_spec, $display_name, $route);
230     }
231     }
232    
233     =head2 $self->parse_address_list ($address_list)
234    
235     Parse C<address-list> and return hash.
236     This method is intended for internal use.
237    
238     =head3 Structure of hash returned by parse_address_list
239    
240     %address = (
241    
242     type => '_ROOT',
243     address => [
244    
245     ## mailbox
246     {
247     type => 'mailbox',
248     display_name => 'Foo H. Bar',
249     addr_spec => 'foo@bar.example',
250     route => '@hoge.example:',
251     },
252    
253     ## group
254     {
255     type => 'group',
256     display_name => 'The committee',
257     address => [
258    
259     ## mailbox
260     {
261     type => 'mailbox',
262     display_name => 'Tom (Director)',
263     addr_spec => 'tom@committee.example',
264     route => '',
265     }
266    
267     ],
268     },
269    
270     ],
271    
272     );
273    
274     =cut
275    
276     sub parse_address_list ($$) {
277     my $self = shift;
278     my $address_list = shift;
279     my %r_addr = (type => '_ROOT');
280     $address_list =~ s{($REG{address})}{
281     my $address = $1;
282     if ($address =~ /^$REG{M_group}/) {
283     my %r_group = (type => 'group', display_name => $1);
284     $r_group{display_name} =~ s/$REG{WSP}+$//;
285     $r_group{display_name} = $self->unquote_quoted_string ($r_group{display_name});
286     $address =~ s{($REG{mailbox})}{
287     my ($addr, $name, $route) = $self->parse_mailbox ($1);
288     push @{$r_group{address}}, {type => 'mailbox',
289     display_name => $name, route => $route, addr_spec => $addr};
290     }goex;
291     push @{$r_addr{address}}, \%r_group;
292     } else {
293     my ($addr, $name, $route) = $self->parse_mailbox ($address);
294     push @{$r_addr{address}}, {type => 'mailbox',
295     display_name => $name, route => $route, addr_spec => $addr};
296     }
297     }goex;
298     %r_addr;
299     }
300    
301     =head2 $self->delete_comment ($field_body)
302    
303     Remove all C<comment> in given strictured C<field-body>.
304     This method is intended for internal use.
305    
306     =cut
307    
308     sub delete_comment ($$) {
309     my $self = shift;
310     my $body = shift;
311     $body =~ s{($REG{quoted_string}|$REG{domain_literal})|$REG{comment}}{
312     my $o = $1; $o? $o : ' ';
313     }gex;
314     $body;
315     }
316    
317     =head1 EXAMPLE
318    
319     ## Compose field-body for To: field.
320    
321 wakaba 1.2 use Message::Field::Address;
322 wakaba 1.1 my $addr = new Message::Field::Address;
323     $addr->add ('foo@example.org', name => 'Mr. foo bar');
324     $addr->add ('webmaster@example.org', group => 'administrators');
325     $addr->add ('postmaster@example.org', group => 'administrators');
326    
327     my $field_body = $addr->stringify ();
328    
329    
330     ## Output parsed address-list tree.
331    
332     use Message::Field::Address;
333     my $addr = Message::Field::Address->parse ($field_body);
334    
335     for my $i (@$addr) {
336     if ($i->{type} eq 'group') {
337     print "\x40 $i->{display_name}: \n";
338     for my $j (@{$i->{address}}) {
339     print "\t- $j->{display_name} <$j->{route}$j->{addr_spec}>\n";
340     }
341     } else {
342     print "- $i->{display_name} <$i->{route}$i->{addr_spec}>\n";
343     }
344     }
345    
346     =head1 LICENSE
347    
348     Copyright 2002 wakaba E<lt>w@suika.fam.cxE<gt>.
349    
350     This program is free software; you can redistribute it and/or modify
351     it under the terms of the GNU General Public License as published by
352     the Free Software Foundation; either version 2 of the License, or
353     (at your option) any later version.
354    
355     This program is distributed in the hope that it will be useful,
356     but WITHOUT ANY WARRANTY; without even the implied warranty of
357     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
358     GNU General Public License for more details.
359    
360     You should have received a copy of the GNU General Public License
361     along with this program; see the file COPYING. If not, write to
362     the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
363     Boston, MA 02111-1307, USA.
364    
365     =head1 CHANGE
366    
367     See F<ChangeLog>.
368 wakaba 1.3 $Date: 2002/03/16 01:26:30 $
369 wakaba 1.1
370     =cut
371    
372     1;

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24