line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Courriel; |
2
|
|
|
|
|
|
|
|
3
|
6
|
|
|
6
|
|
352233
|
use 5.10.0; |
|
6
|
|
|
|
|
24
|
|
4
|
|
|
|
|
|
|
|
5
|
6
|
|
|
6
|
|
33
|
use strict; |
|
6
|
|
|
|
|
20
|
|
|
6
|
|
|
|
|
163
|
|
6
|
6
|
|
|
6
|
|
27
|
use warnings; |
|
6
|
|
|
|
|
19
|
|
|
6
|
|
|
|
|
184
|
|
7
|
6
|
|
|
6
|
|
3767
|
use namespace::autoclean; |
|
6
|
|
|
|
|
96402
|
|
|
6
|
|
|
|
|
38
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
our $VERSION = '0.44'; |
10
|
|
|
|
|
|
|
|
11
|
6
|
|
|
6
|
|
3935
|
use Courriel::Headers; |
|
6
|
|
|
|
|
21
|
|
|
6
|
|
|
|
|
338
|
|
12
|
6
|
|
|
6
|
|
60
|
use Courriel::Helpers qw( unique_boundary ); |
|
6
|
|
|
|
|
128
|
|
|
6
|
|
|
|
|
676
|
|
13
|
6
|
|
|
6
|
|
4350
|
use Courriel::Part::Multipart; |
|
6
|
|
|
|
|
24
|
|
|
6
|
|
|
|
|
305
|
|
14
|
6
|
|
|
6
|
|
4267
|
use Courriel::Part::Single; |
|
6
|
|
|
|
|
22
|
|
|
6
|
|
|
|
|
312
|
|
15
|
6
|
|
|
6
|
|
48
|
use Courriel::Types qw( ArrayRef Bool Headers Maybe Part Str StringRef ); |
|
6
|
|
|
|
|
10
|
|
|
6
|
|
|
|
|
55
|
|
16
|
6
|
|
|
6
|
|
39770
|
use DateTime; |
|
6
|
|
|
|
|
13
|
|
|
6
|
|
|
|
|
191
|
|
17
|
6
|
|
|
6
|
|
27
|
use DateTime::Format::Mail 0.403; |
|
6
|
|
|
|
|
227
|
|
|
6
|
|
|
|
|
135
|
|
18
|
6
|
|
|
6
|
|
4739
|
use DateTime::Format::Natural; |
|
6
|
|
|
|
|
264730
|
|
|
6
|
|
|
|
|
468
|
|
19
|
6
|
|
|
6
|
|
58
|
use Email::Address; |
|
6
|
|
|
|
|
9
|
|
|
6
|
|
|
|
|
166
|
|
20
|
6
|
|
|
6
|
|
26
|
use Encode qw( encode ); |
|
6
|
|
|
|
|
10
|
|
|
6
|
|
|
|
|
308
|
|
21
|
6
|
|
|
6
|
|
30
|
use List::AllUtils qw( uniq ); |
|
6
|
|
|
|
|
11
|
|
|
6
|
|
|
|
|
307
|
|
22
|
6
|
|
|
6
|
|
30
|
use Params::ValidationCompiler 0.18 qw( validation_for ); |
|
6
|
|
|
|
|
245
|
|
|
6
|
|
|
|
|
423
|
|
23
|
|
|
|
|
|
|
|
24
|
6
|
|
|
6
|
|
35
|
use Moose; |
|
6
|
|
|
|
|
11
|
|
|
6
|
|
|
|
|
68
|
|
25
|
6
|
|
|
6
|
|
37211
|
use MooseX::StrictConstructor; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
61
|
|
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
has top_level_part => ( |
28
|
|
|
|
|
|
|
is => 'rw', |
29
|
|
|
|
|
|
|
writer => '_replace_top_level_part', |
30
|
|
|
|
|
|
|
isa => Part, |
31
|
|
|
|
|
|
|
init_arg => 'part', |
32
|
|
|
|
|
|
|
required => 1, |
33
|
|
|
|
|
|
|
handles => [ |
34
|
|
|
|
|
|
|
qw( |
35
|
|
|
|
|
|
|
as_string |
36
|
|
|
|
|
|
|
content_type |
37
|
|
|
|
|
|
|
headers |
38
|
|
|
|
|
|
|
is_multipart |
39
|
|
|
|
|
|
|
stream_to |
40
|
|
|
|
|
|
|
) |
41
|
|
|
|
|
|
|
], |
42
|
|
|
|
|
|
|
); |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
has subject => ( |
45
|
|
|
|
|
|
|
is => 'ro', |
46
|
|
|
|
|
|
|
isa => Maybe [Str], |
47
|
|
|
|
|
|
|
init_arg => undef, |
48
|
|
|
|
|
|
|
lazy => 1, |
49
|
|
|
|
|
|
|
builder => '_build_subject', |
50
|
|
|
|
|
|
|
); |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
has datetime => ( |
53
|
|
|
|
|
|
|
is => 'ro', |
54
|
|
|
|
|
|
|
isa => 'DateTime', |
55
|
|
|
|
|
|
|
init_arg => undef, |
56
|
|
|
|
|
|
|
lazy => 1, |
57
|
|
|
|
|
|
|
builder => '_build_datetime', |
58
|
|
|
|
|
|
|
); |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
has _to => ( |
61
|
|
|
|
|
|
|
traits => ['Array'], |
62
|
|
|
|
|
|
|
isa => ArrayRef ['Email::Address'], |
63
|
|
|
|
|
|
|
init_arg => undef, |
64
|
|
|
|
|
|
|
lazy => 1, |
65
|
|
|
|
|
|
|
builder => '_build_to', |
66
|
|
|
|
|
|
|
handles => { |
67
|
|
|
|
|
|
|
to => 'elements', |
68
|
|
|
|
|
|
|
}, |
69
|
|
|
|
|
|
|
); |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
has _cc => ( |
72
|
|
|
|
|
|
|
traits => ['Array'], |
73
|
|
|
|
|
|
|
isa => ArrayRef ['Email::Address'], |
74
|
|
|
|
|
|
|
init_arg => undef, |
75
|
|
|
|
|
|
|
lazy => 1, |
76
|
|
|
|
|
|
|
builder => '_build_cc', |
77
|
|
|
|
|
|
|
handles => { |
78
|
|
|
|
|
|
|
cc => 'elements', |
79
|
|
|
|
|
|
|
}, |
80
|
|
|
|
|
|
|
); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
has from => ( |
83
|
|
|
|
|
|
|
is => 'ro', |
84
|
|
|
|
|
|
|
isa => Maybe ['Email::Address'], |
85
|
|
|
|
|
|
|
init_arg => undef, |
86
|
|
|
|
|
|
|
lazy => 1, |
87
|
|
|
|
|
|
|
builder => '_build_from', |
88
|
|
|
|
|
|
|
); |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
has _participants => ( |
91
|
|
|
|
|
|
|
traits => ['Array'], |
92
|
|
|
|
|
|
|
isa => ArrayRef ['Email::Address'], |
93
|
|
|
|
|
|
|
init_arg => undef, |
94
|
|
|
|
|
|
|
lazy => 1, |
95
|
|
|
|
|
|
|
builder => '_build_participants', |
96
|
|
|
|
|
|
|
handles => { |
97
|
|
|
|
|
|
|
participants => 'elements', |
98
|
|
|
|
|
|
|
}, |
99
|
|
|
|
|
|
|
); |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
has _recipients => ( |
102
|
|
|
|
|
|
|
traits => ['Array'], |
103
|
|
|
|
|
|
|
isa => ArrayRef ['Email::Address'], |
104
|
|
|
|
|
|
|
init_arg => undef, |
105
|
|
|
|
|
|
|
lazy => 1, |
106
|
|
|
|
|
|
|
builder => '_build_recipients', |
107
|
|
|
|
|
|
|
handles => { |
108
|
|
|
|
|
|
|
recipients => 'elements', |
109
|
|
|
|
|
|
|
}, |
110
|
|
|
|
|
|
|
); |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
has plain_body_part => ( |
113
|
|
|
|
|
|
|
is => 'ro', |
114
|
|
|
|
|
|
|
isa => Maybe ['Courriel::Part::Single'], |
115
|
|
|
|
|
|
|
init_arg => undef, |
116
|
|
|
|
|
|
|
lazy => 1, |
117
|
|
|
|
|
|
|
builder => '_build_plain_body_part', |
118
|
|
|
|
|
|
|
); |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
has html_body_part => ( |
121
|
|
|
|
|
|
|
is => 'ro', |
122
|
|
|
|
|
|
|
isa => Maybe ['Courriel::Part::Single'], |
123
|
|
|
|
|
|
|
init_arg => undef, |
124
|
|
|
|
|
|
|
lazy => 1, |
125
|
|
|
|
|
|
|
builder => '_build_html_body_part', |
126
|
|
|
|
|
|
|
); |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
sub part_count { |
129
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
130
|
|
|
|
|
|
|
|
131
|
0
|
0
|
|
|
|
0
|
return $self->is_multipart |
132
|
|
|
|
|
|
|
? $self->top_level_part->part_count |
133
|
|
|
|
|
|
|
: 1; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
sub parts { |
137
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
138
|
|
|
|
|
|
|
|
139
|
0
|
0
|
|
|
|
0
|
return $self->is_multipart |
140
|
|
|
|
|
|
|
? $self->top_level_part->parts |
141
|
|
|
|
|
|
|
: $self->top_level_part; |
142
|
|
|
|
|
|
|
} |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
sub clone_without_attachments { |
145
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
146
|
|
|
|
|
|
|
|
147
|
0
|
|
|
|
|
0
|
my $plain_body = $self->plain_body_part; |
148
|
0
|
|
|
|
|
0
|
my $html_body = $self->html_body_part; |
149
|
|
|
|
|
|
|
|
150
|
0
|
|
|
|
|
0
|
my $headers = $self->headers; |
151
|
|
|
|
|
|
|
|
152
|
0
|
0
|
0
|
|
|
0
|
if ( $plain_body && $html_body ) { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
153
|
0
|
|
|
|
|
0
|
my $ct = Courriel::Header::ContentType->new( |
154
|
|
|
|
|
|
|
mime_type => 'multipart/alternative', |
155
|
|
|
|
|
|
|
attributes => { boundary => unique_boundary }, |
156
|
|
|
|
|
|
|
); |
157
|
|
|
|
|
|
|
|
158
|
0
|
|
|
|
|
0
|
return Courriel->new( |
159
|
|
|
|
|
|
|
part => Courriel::Part::Multipart->new( |
160
|
|
|
|
|
|
|
content_type => $ct, |
161
|
|
|
|
|
|
|
headers => $headers, |
162
|
|
|
|
|
|
|
parts => [ $plain_body, $html_body ], |
163
|
|
|
|
|
|
|
) |
164
|
|
|
|
|
|
|
); |
165
|
|
|
|
|
|
|
} |
166
|
|
|
|
|
|
|
elsif ($plain_body) { |
167
|
0
|
|
|
|
|
0
|
return Courriel->new( |
168
|
|
|
|
|
|
|
part => Courriel::Part::Single->new( |
169
|
|
|
|
|
|
|
content_type => $plain_body->content_type, |
170
|
|
|
|
|
|
|
headers => $headers, |
171
|
|
|
|
|
|
|
encoding => $plain_body->encoding, |
172
|
|
|
|
|
|
|
encoded_content => $plain_body->encoded_content, |
173
|
|
|
|
|
|
|
) |
174
|
|
|
|
|
|
|
); |
175
|
|
|
|
|
|
|
} |
176
|
|
|
|
|
|
|
elsif ($html_body) { |
177
|
0
|
|
|
|
|
0
|
return Courriel->new( |
178
|
|
|
|
|
|
|
part => Courriel::Part::Single->new( |
179
|
|
|
|
|
|
|
content_type => $html_body->content_type, |
180
|
|
|
|
|
|
|
headers => $headers, |
181
|
|
|
|
|
|
|
encoding => $html_body->encoding, |
182
|
|
|
|
|
|
|
encoded_content => $html_body->encoded_content, |
183
|
|
|
|
|
|
|
) |
184
|
|
|
|
|
|
|
); |
185
|
|
|
|
|
|
|
} |
186
|
|
|
|
|
|
|
|
187
|
0
|
|
|
|
|
0
|
die 'Cannot find a text or html body in this email!'; |
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
sub _build_subject { |
191
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
192
|
|
|
|
|
|
|
|
193
|
0
|
|
|
|
|
0
|
my $subject = $self->headers->get('Subject'); |
194
|
|
|
|
|
|
|
|
195
|
0
|
0
|
|
|
|
0
|
return $subject ? $subject->value : undef; |
196
|
|
|
|
|
|
|
} |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
{ |
199
|
|
|
|
|
|
|
my $mail_parser = DateTime::Format::Mail->new( loose => 1 ); |
200
|
|
|
|
|
|
|
my $natural_parser = DateTime::Format::Natural->new( time_zone => 'UTC' ); |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub _build_datetime { |
203
|
2
|
|
|
2
|
|
4
|
my $self = shift; |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
my @possible = ( |
206
|
2
|
|
|
|
|
51
|
( map { $_->value } $self->headers->get('Date') ), |
207
|
|
|
|
|
|
|
( |
208
|
|
|
|
|
|
|
reverse |
209
|
7
|
|
|
|
|
181
|
map { $self->_find_date_received( $_->value ) } |
210
|
|
|
|
|
|
|
$self->headers->get('Received') |
211
|
|
|
|
|
|
|
), |
212
|
2
|
|
|
|
|
14
|
( map { $_->value } $self->headers->get('Resent-Date') ), |
|
0
|
|
|
|
|
0
|
|
213
|
|
|
|
|
|
|
); |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
# Stolen from Email::Date and then modified |
216
|
2
|
|
|
|
|
9
|
for my $possible (@possible) { |
217
|
5
|
50
|
33
|
|
|
92
|
next unless defined $possible && length $possible; |
218
|
|
|
|
|
|
|
|
219
|
5
|
|
|
|
|
7
|
my $dt = eval { $mail_parser->parse_datetime($possible) }; |
|
5
|
|
|
|
|
28
|
|
220
|
|
|
|
|
|
|
|
221
|
5
|
100
|
|
|
|
1456
|
unless ($dt) { |
222
|
3
|
|
|
|
|
17
|
$dt = $natural_parser->parse_datetime($possible); |
223
|
3
|
50
|
|
|
|
7318
|
next unless $natural_parser->success; |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
|
226
|
2
|
|
|
|
|
86
|
$dt->set_time_zone('UTC'); |
227
|
2
|
|
|
|
|
398
|
return $dt; |
228
|
|
|
|
|
|
|
} |
229
|
|
|
|
|
|
|
|
230
|
0
|
|
|
|
|
0
|
return DateTime->now( time_zone => 'UTC' ); |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# Stolen from Email::Date and modified |
235
|
|
|
|
|
|
|
sub _find_date_received { |
236
|
7
|
|
|
7
|
|
10
|
shift; |
237
|
7
|
|
|
|
|
8
|
my $received = shift; |
238
|
|
|
|
|
|
|
|
239
|
7
|
50
|
33
|
|
|
29
|
return unless defined $received && length $received; |
240
|
|
|
|
|
|
|
|
241
|
7
|
|
|
|
|
44
|
$received =~ s/.+;//; |
242
|
|
|
|
|
|
|
|
243
|
7
|
|
|
|
|
22
|
return $received; |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
sub _build_to { |
247
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
248
|
|
|
|
|
|
|
|
249
|
0
|
|
|
|
|
0
|
my @addresses = map { Email::Address->parse( $_->value ) } |
|
0
|
|
|
|
|
0
|
|
250
|
|
|
|
|
|
|
$self->headers->get('To'); |
251
|
|
|
|
|
|
|
|
252
|
0
|
|
|
|
|
0
|
return $self->_unique_addresses( \@addresses ); |
253
|
|
|
|
|
|
|
} |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
sub _build_cc { |
256
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
257
|
|
|
|
|
|
|
|
258
|
0
|
|
|
|
|
0
|
my @addresses = map { Email::Address->parse( $_->value ) } |
|
0
|
|
|
|
|
0
|
|
259
|
|
|
|
|
|
|
$self->headers->get('CC'); |
260
|
|
|
|
|
|
|
|
261
|
0
|
|
|
|
|
0
|
return $self->_unique_addresses( \@addresses ); |
262
|
|
|
|
|
|
|
} |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
sub _build_from { |
265
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
266
|
|
|
|
|
|
|
|
267
|
0
|
|
|
|
|
0
|
my @addresses = Email::Address->parse( map { $_->value } |
|
0
|
|
|
|
|
0
|
|
268
|
|
|
|
|
|
|
$self->headers->get('From') ); |
269
|
|
|
|
|
|
|
|
270
|
0
|
|
|
|
|
0
|
return $addresses[0]; |
271
|
|
|
|
|
|
|
} |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
sub _build_recipients { |
274
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
275
|
|
|
|
|
|
|
|
276
|
0
|
|
|
|
|
0
|
my @addresses = ( $self->to, $self->cc ); |
277
|
|
|
|
|
|
|
|
278
|
0
|
|
|
|
|
0
|
return $self->_unique_addresses( \@addresses ); |
279
|
|
|
|
|
|
|
} |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
sub _build_participants { |
282
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
283
|
|
|
|
|
|
|
|
284
|
0
|
|
|
|
|
0
|
my @addresses = grep {defined} ( $self->from, $self->to, $self->cc ); |
|
0
|
|
|
|
|
0
|
|
285
|
|
|
|
|
|
|
|
286
|
0
|
|
|
|
|
0
|
return $self->_unique_addresses( \@addresses ); |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
sub _unique_addresses { |
290
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
291
|
0
|
|
|
|
|
0
|
my $addresses = shift; |
292
|
|
|
|
|
|
|
|
293
|
0
|
|
|
|
|
0
|
my %seen; |
294
|
0
|
|
|
|
|
0
|
return [ grep { !$seen{ $_->original }++ } @{$addresses} ]; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
295
|
|
|
|
|
|
|
} |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
sub _build_plain_body_part { |
298
|
3
|
|
|
3
|
|
4
|
my $self = shift; |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
return $self->first_part_matching( |
301
|
|
|
|
|
|
|
sub { |
302
|
5
|
100
|
|
5
|
|
25
|
$_[0]->mime_type eq 'text/plain' |
303
|
|
|
|
|
|
|
&& $_[0]->is_inline; |
304
|
|
|
|
|
|
|
} |
305
|
3
|
|
|
|
|
17
|
); |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
sub _build_html_body_part { |
309
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
return $self->first_part_matching( |
312
|
|
|
|
|
|
|
sub { |
313
|
0
|
0
|
|
0
|
|
0
|
$_[0]->mime_type eq 'text/html' |
314
|
|
|
|
|
|
|
&& $_[0]->is_inline; |
315
|
|
|
|
|
|
|
} |
316
|
0
|
|
|
|
|
0
|
); |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
sub first_part_matching { |
320
|
3
|
|
|
3
|
1
|
4
|
my $self = shift; |
321
|
3
|
|
|
|
|
5
|
my $match = shift; |
322
|
|
|
|
|
|
|
|
323
|
3
|
|
|
|
|
78
|
my @parts = $self->top_level_part; |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
## no critic (ControlStructures::ProhibitCStyleForLoops) |
326
|
3
|
|
|
|
|
13
|
for ( my $part = shift @parts; $part; $part = shift @parts ) { |
327
|
5
|
100
|
|
|
|
12
|
return $part if $match->($part); |
328
|
|
|
|
|
|
|
|
329
|
2
|
50
|
|
|
|
9
|
push @parts, $part->parts if $part->is_multipart; |
330
|
|
|
|
|
|
|
} |
331
|
|
|
|
|
|
|
} |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
sub all_parts_matching { |
334
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
335
|
0
|
|
|
|
|
0
|
my $match = shift; |
336
|
|
|
|
|
|
|
|
337
|
0
|
|
|
|
|
0
|
my @parts = $self->top_level_part; |
338
|
|
|
|
|
|
|
|
339
|
0
|
|
|
|
|
0
|
my @match; |
340
|
|
|
|
|
|
|
## no critic (ControlStructures::ProhibitCStyleForLoops) |
341
|
0
|
|
|
|
|
0
|
for ( my $part = shift @parts; $part; $part = shift @parts ) { |
342
|
0
|
0
|
|
|
|
0
|
push @match, $part if $match->($part); |
343
|
|
|
|
|
|
|
|
344
|
0
|
0
|
|
|
|
0
|
push @parts, $part->parts if $part->is_multipart; |
345
|
|
|
|
|
|
|
} |
346
|
|
|
|
|
|
|
|
347
|
0
|
|
|
|
|
0
|
return @match; |
348
|
|
|
|
|
|
|
} |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
{ |
351
|
|
|
|
|
|
|
my $validator = validation_for( |
352
|
|
|
|
|
|
|
params => [ |
353
|
|
|
|
|
|
|
text => { type => StringRef }, |
354
|
|
|
|
|
|
|
], |
355
|
|
|
|
|
|
|
named_to_list => 1, |
356
|
|
|
|
|
|
|
); |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
# This is needed for Email::Abstract compatibility but it's a godawful |
359
|
|
|
|
|
|
|
# idea, and even Email::Abstract says not to do this. |
360
|
|
|
|
|
|
|
# |
361
|
|
|
|
|
|
|
# It's much safer to just make a new Courriel object from scratch. |
362
|
|
|
|
|
|
|
sub replace_body { |
363
|
0
|
|
|
0
|
0
|
0
|
my $self = shift; |
364
|
0
|
|
|
|
|
0
|
my ($text) = $validator->(@_); |
365
|
|
|
|
|
|
|
|
366
|
0
|
|
|
|
|
0
|
my $part = Courriel::Part::Single->new( |
367
|
|
|
|
|
|
|
headers => $self->headers, |
368
|
|
|
|
|
|
|
encoded_content => $text, |
369
|
|
|
|
|
|
|
); |
370
|
|
|
|
|
|
|
|
371
|
0
|
|
|
|
|
0
|
$self->_replace_top_level_part($part); |
372
|
|
|
|
|
|
|
|
373
|
0
|
|
|
|
|
0
|
return; |
374
|
|
|
|
|
|
|
} |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
{ |
378
|
|
|
|
|
|
|
my $validator = validation_for( |
379
|
|
|
|
|
|
|
params => [ |
380
|
|
|
|
|
|
|
text => { type => StringRef }, |
381
|
|
|
|
|
|
|
is_character => { type => Bool, default => 0 }, |
382
|
|
|
|
|
|
|
], |
383
|
|
|
|
|
|
|
named_to_list => 1, |
384
|
|
|
|
|
|
|
); |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
sub parse { |
387
|
65
|
|
|
65
|
1
|
85052
|
my $class = shift; |
388
|
65
|
|
|
|
|
2727
|
my ( $text, $is_character ) = $validator->(@_); |
389
|
|
|
|
|
|
|
|
390
|
65
|
50
|
|
|
|
2818
|
if ($is_character) { |
391
|
0
|
|
|
|
|
0
|
${$text} = encode( 'UTF-8', ${$text} ); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
392
|
|
|
|
|
|
|
} |
393
|
|
|
|
|
|
|
|
394
|
65
|
|
|
|
|
361
|
return $class->new( part => $class->_parse($text) ); |
395
|
|
|
|
|
|
|
} |
396
|
|
|
|
|
|
|
} |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
sub _parse { |
399
|
143
|
|
|
143
|
|
243
|
my $class = shift; |
400
|
143
|
|
|
|
|
176
|
my $text = shift; |
401
|
|
|
|
|
|
|
|
402
|
143
|
|
|
|
|
438
|
my ( $sep_idx, $headers ) = $class->_parse_headers($text); |
403
|
|
|
|
|
|
|
|
404
|
143
|
|
|
|
|
214
|
substr( ${$text}, 0, $sep_idx, q{} ); |
|
143
|
|
|
|
|
1381
|
|
405
|
|
|
|
|
|
|
|
406
|
143
|
|
|
|
|
524
|
return $class->_parse_parts( $text, $headers ); |
407
|
|
|
|
|
|
|
} |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
sub _parse_headers { |
410
|
143
|
|
|
143
|
|
194
|
my $class = shift; |
411
|
143
|
|
|
|
|
213
|
my $text = shift; |
412
|
|
|
|
|
|
|
|
413
|
143
|
|
|
|
|
187
|
my $header_text; |
414
|
|
|
|
|
|
|
my $sep_idx; |
415
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
# We want to ignore mbox message separators - this is a pretty lax parser, |
417
|
|
|
|
|
|
|
# but we may find broken lines. The key is that it starts with From |
418
|
|
|
|
|
|
|
# followed by space, not a colon. |
419
|
143
|
|
|
|
|
182
|
${$text} =~ s/^From\s+.+$Courriel::Helpers::LINE_SEP_RE//; |
|
143
|
|
|
|
|
1513
|
|
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
# Some broken emails may split the From line in an arbitrary spot |
422
|
143
|
|
|
|
|
256
|
${$text} =~ s/^[^:]+$Courriel::Helpers::LINE_SEP_RE//g; |
|
143
|
|
|
|
|
798
|
|
423
|
|
|
|
|
|
|
|
424
|
143
|
100
|
|
|
|
297
|
if ( ${$text} =~ /^(.+?)($Courriel::Helpers::LINE_SEP_RE)\g{2}/s ) { |
|
143
|
|
|
|
|
7981
|
|
425
|
142
|
|
|
|
|
684
|
$header_text = $1 . $2; |
426
|
142
|
|
|
|
|
345
|
$sep_idx = ( length $header_text ) + ( length $2 ); |
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
else { |
429
|
1
|
|
|
|
|
61
|
return ( 0, Courriel::Headers::->new ); |
430
|
|
|
|
|
|
|
} |
431
|
|
|
|
|
|
|
|
432
|
142
|
|
|
|
|
957
|
my $headers = Courriel::Headers::->parse( |
433
|
|
|
|
|
|
|
text => \$header_text, |
434
|
|
|
|
|
|
|
); |
435
|
|
|
|
|
|
|
|
436
|
142
|
|
|
|
|
521
|
return ( $sep_idx, $headers ); |
437
|
|
|
|
|
|
|
} |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
{ |
440
|
|
|
|
|
|
|
my $fake_ct = Courriel::Header::ContentType->new_from_value( |
441
|
|
|
|
|
|
|
name => 'Content-Type', |
442
|
|
|
|
|
|
|
value => 'text/plain' |
443
|
|
|
|
|
|
|
); |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
sub _parse_parts { |
446
|
143
|
|
|
143
|
|
257
|
my $class = shift; |
447
|
143
|
|
|
|
|
192
|
my $text = shift; |
448
|
143
|
|
|
|
|
168
|
my $headers = shift; |
449
|
|
|
|
|
|
|
|
450
|
143
|
|
|
|
|
526
|
my @ct = $headers->get('Content-Type'); |
451
|
143
|
50
|
|
|
|
466
|
if ( @ct > 1 ) { |
452
|
0
|
|
|
|
|
0
|
die 'This email defines more than one Content-Type header.'; |
453
|
|
|
|
|
|
|
} |
454
|
|
|
|
|
|
|
|
455
|
143
|
|
66
|
|
|
462
|
my $ct = $ct[0] // $fake_ct; |
456
|
|
|
|
|
|
|
|
457
|
143
|
100
|
|
|
|
5410
|
if ( $ct->mime_type !~ /^multipart/i ) { |
458
|
107
|
|
|
|
|
4338
|
return Courriel::Part::Single->new( |
459
|
|
|
|
|
|
|
headers => $headers, |
460
|
|
|
|
|
|
|
encoded_content => $text, |
461
|
|
|
|
|
|
|
); |
462
|
|
|
|
|
|
|
} |
463
|
|
|
|
|
|
|
|
464
|
36
|
|
|
|
|
185
|
return $class->_parse_multipart( $text, $headers, $ct ); |
465
|
|
|
|
|
|
|
} |
466
|
|
|
|
|
|
|
} |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
sub _parse_multipart { |
469
|
36
|
|
|
36
|
|
60
|
my $class = shift; |
470
|
36
|
|
|
|
|
55
|
my $text = shift; |
471
|
36
|
|
|
|
|
42
|
my $headers = shift; |
472
|
36
|
|
|
|
|
46
|
my $ct = shift; |
473
|
|
|
|
|
|
|
|
474
|
36
|
|
|
|
|
182
|
my $boundary = $ct->attribute_value('boundary'); |
475
|
|
|
|
|
|
|
|
476
|
36
|
50
|
33
|
|
|
226
|
die q{The message's mime type claims this is a multipart message (} |
477
|
|
|
|
|
|
|
. $ct->mime_type |
478
|
|
|
|
|
|
|
. q{) but it does not specify a boundary.} |
479
|
|
|
|
|
|
|
unless defined $boundary && length $boundary; |
480
|
|
|
|
|
|
|
|
481
|
36
|
|
|
|
|
76
|
my ( $preamble, $all_parts, $epilogue ) = ${$text} =~ / |
|
36
|
|
|
|
|
3357
|
|
482
|
|
|
|
|
|
|
(.*?) # preamble |
483
|
|
|
|
|
|
|
^--\Q$boundary\E\s* |
484
|
|
|
|
|
|
|
(.+) # all parts |
485
|
|
|
|
|
|
|
^--\Q$boundary\E--\s* |
486
|
|
|
|
|
|
|
(.*) # epilogue |
487
|
|
|
|
|
|
|
/smx; |
488
|
|
|
|
|
|
|
|
489
|
36
|
|
|
|
|
84
|
my @part_text; |
490
|
|
|
|
|
|
|
|
491
|
36
|
100
|
|
|
|
117
|
if ( defined $all_parts ) { |
492
|
31
|
|
|
|
|
1107
|
@part_text = split /^--\Q$boundary\E\s*/m, $all_parts; |
493
|
|
|
|
|
|
|
} |
494
|
|
|
|
|
|
|
|
495
|
36
|
100
|
|
|
|
131
|
unless (@part_text) { |
496
|
5
|
|
|
|
|
10
|
${$text} =~ s/^--\Q$boundary\E\s*//m; |
|
5
|
|
|
|
|
155
|
|
497
|
5
|
|
|
|
|
16
|
push @part_text, ${$text}; |
|
5
|
|
|
|
|
40
|
|
498
|
|
|
|
|
|
|
} |
499
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
return Courriel::Part::Multipart->new( |
501
|
|
|
|
|
|
|
headers => $headers, |
502
|
|
|
|
|
|
|
( |
503
|
|
|
|
|
|
|
defined $preamble |
504
|
|
|
|
|
|
|
&& length $preamble |
505
|
|
|
|
|
|
|
&& $preamble =~ /\S/ ? ( preamble => $preamble ) : () |
506
|
|
|
|
|
|
|
), |
507
|
|
|
|
|
|
|
( |
508
|
|
|
|
|
|
|
defined $epilogue |
509
|
|
|
|
|
|
|
&& length $epilogue |
510
|
|
|
|
|
|
|
&& $epilogue =~ /\S/ ? ( epilogue => $epilogue ) : () |
511
|
|
|
|
|
|
|
), |
512
|
|
|
|
|
|
|
boundary => $boundary, |
513
|
36
|
100
|
100
|
|
|
552
|
parts => [ map { $class->_parse( \$_ ) } @part_text ], |
|
78
|
100
|
66
|
|
|
363
|
|
514
|
|
|
|
|
|
|
); |
515
|
|
|
|
|
|
|
} |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
1; |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
# ABSTRACT: High level email parsing and manipulation |
522
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
__END__ |
524
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
=pod |
526
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
=encoding UTF-8 |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
=head1 NAME |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
Courriel - High level email parsing and manipulation |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
=head1 VERSION |
534
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
version 0.44 |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=head1 SYNOPSIS |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
my $email = Courriel->parse( text => $raw_email ); |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
print $email->subject; |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
print $_->address for $email->participants; |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
print $email->datetime->year; |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
if ( my $part = $email->plain_body_part ) { |
548
|
|
|
|
|
|
|
print $part->content; |
549
|
|
|
|
|
|
|
} |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
=head1 DESCRIPTION |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
This class exists to provide a high level API for working with emails, |
554
|
|
|
|
|
|
|
particular for processing incoming email. It is primarily a wrapper around the |
555
|
|
|
|
|
|
|
other classes in the Courriel distro, especially L<Courriel::Headers>, |
556
|
|
|
|
|
|
|
L<Courriel::Part::Single>, and L<Courriel::Part::Multipart>. If you need lower |
557
|
|
|
|
|
|
|
level information about an email, it should be available from one of these |
558
|
|
|
|
|
|
|
classes. |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
=head1 API |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
This class provides the following methods: |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
=head2 Courriel->parse( text => $raw_email, is_character => 0|1 ) |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
This parses the given text and returns a new Courriel object. The text can be |
567
|
|
|
|
|
|
|
provided as a string or a reference to a string. |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
If you pass a reference, then the scalar underlying the reference I<will> be |
570
|
|
|
|
|
|
|
modified, so don't pass in something you don't want modified. |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
By default, Courriel expects that content passed in text is binary data. This |
573
|
|
|
|
|
|
|
means that it has not been decoded into utf-8 with C<Encode::decode()> or by |
574
|
|
|
|
|
|
|
using a C<:encoding(UTF-8)> IO layer. |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
In practice, this doesn't matter for most emails, since they either contain |
577
|
|
|
|
|
|
|
only ASCII data or they actually do contain binary (non-character) |
578
|
|
|
|
|
|
|
data. However, if an email is using the 8bit Content-Transfer-Encoding, then |
579
|
|
|
|
|
|
|
this does matter. |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
If the email has already been decoded, you must set C<is_character> to a true |
582
|
|
|
|
|
|
|
value. |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
It's safest to simply pass binary data to Courriel and let it handle decoding |
585
|
|
|
|
|
|
|
internally. |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=head2 $email->parts() |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
Returns an array (not a reference) of the parts this email contains. |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
=head2 $email->part_count() |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
Returns the number of parts this email contains. |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
=head2 $email->is_multipart() |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
Returns true if the top-level part is a multipart part, false otherwise. |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=head2 $email->top_level_part() |
600
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
Returns the actual top level part for the object. You're probably better off |
602
|
|
|
|
|
|
|
just calling C<< $email->parts() >> most of the time, since when the email is |
603
|
|
|
|
|
|
|
multipart, the top level part is just a container. |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
=head2 $email->subject() |
606
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
Returns the email's Subject header value, or C<undef> if it doesn't have one. |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
=head2 $email->datetime() |
610
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
Returns a L<DateTime> object for the email. The DateTime object is always in |
612
|
|
|
|
|
|
|
the "UTC" time zone. |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
This uses the Date header by default one. Otherwise it looks at the date in |
615
|
|
|
|
|
|
|
each Received header, and then it looks for a Resent-Date header. If none of |
616
|
|
|
|
|
|
|
these exists, it just returns C<< DateTime->now() >>. |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=head2 $email->from() |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
This returns a single L<Email::Address> object based on the From header of the |
621
|
|
|
|
|
|
|
email. If the email has no From header or if the From header is broken, it |
622
|
|
|
|
|
|
|
returns C<undef>. |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
=head2 $email->participants() |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
This returns a list of L<Email::Address> objects, one for each unique |
627
|
|
|
|
|
|
|
participant in the email. This includes any address in the From, To, or CC |
628
|
|
|
|
|
|
|
headers. |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
Just like with the From header, broken addresses will not be included. |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
=head2 $email->recipients() |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
This returns a list of L<Email::Address> objects, one for each unique |
635
|
|
|
|
|
|
|
recipient in the email. This includes any address in the To or CC headers. |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
Just like with the From header, broken addresses will not be included. |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=head2 $email->to() |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
This returns a list of L<Email::Address> objects, one for each unique |
642
|
|
|
|
|
|
|
address in the To header. |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
Just like with the From header, broken addresses will not be included. |
645
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
=head2 $email->cc() |
647
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
This returns a list of L<Email::Address> objects, one for each unique |
649
|
|
|
|
|
|
|
address in the CC header. |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
Just like with the From header, broken addresses will not be included. |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
=head2 $email->plain_body_part() |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
This returns the first L<Courriel::Part::Single> object in the email with a |
656
|
|
|
|
|
|
|
mime type of "text/plain" and an inline disposition, if one exists. |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
=head2 $email->html_body_part() |
659
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
This returns the first L<Courriel::Part::Single> object in the email with a |
661
|
|
|
|
|
|
|
mime type of "text/html" and an inline disposition, if one exists. |
662
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
=head2 $email->clone_without_attachments() |
664
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
Returns a new Courriel object that only contains inline parts from the |
666
|
|
|
|
|
|
|
original email, effectively removing all attachments. |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
=head2 $email->first_part_matching( sub { ... } ) |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
Given a subroutine reference, this method calls that subroutine for each part |
671
|
|
|
|
|
|
|
in the email, in a depth-first search. |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
The subroutine receives the part as its only argument. If it returns true, |
674
|
|
|
|
|
|
|
this method returns that part. |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
=head2 $email->all_parts_matching( sub { ... } ) |
677
|
|
|
|
|
|
|
|
678
|
|
|
|
|
|
|
Given a subroutine reference, this method calls that subroutine for each part |
679
|
|
|
|
|
|
|
in the email, in a depth-first search. |
680
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
The subroutine receives the part as its only argument. If it returns true, |
682
|
|
|
|
|
|
|
this method includes that part. |
683
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
This method returns all of the parts that match the subroutine. |
685
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
=head2 $email->content_type() |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
Returns the L<Courriel::Header::ContentType> object associated with the email. |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
=head2 $email->headers() |
691
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
Returns the L<Courriel::Headers> object for this email. |
693
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
=head2 $email->stream_to( output => $output ) |
695
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
This method will send the stringified email to the specified output. The |
697
|
|
|
|
|
|
|
output can be a subroutine reference, a filehandle, or an object with a |
698
|
|
|
|
|
|
|
C<print()> method. The output may be sent as a single string, as a list of |
699
|
|
|
|
|
|
|
strings, or via multiple calls to the output. |
700
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
For large emails, streaming can be much more memory efficient than generating |
702
|
|
|
|
|
|
|
a single string in memory. |
703
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
=head2 $email->as_string() |
705
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
Returns the email as a string, along with its headers. Lines will be |
707
|
|
|
|
|
|
|
terminated with "\r\n". |
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
=head1 ROBUSTNESS PRINCIPLE |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
Courriel aims to respect the common Internet robustness principle (aka |
712
|
|
|
|
|
|
|
Postel's law). Courriel is conservative in the output it generates, and |
713
|
|
|
|
|
|
|
liberal in what it accepts. |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
When parsing, the goal is to never die and always return as much information |
716
|
|
|
|
|
|
|
as possible. Any input that causes the C<< Courriel->parse() >> to die means |
717
|
|
|
|
|
|
|
there's a bug in the parser. Please report these bugs. |
718
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
Conversely, Courriel aims to respect all relevant RFCs in its output, except |
720
|
|
|
|
|
|
|
when it preserves the original data in a parsed email. If you're using |
721
|
|
|
|
|
|
|
L<Courriel::Builder> to create emails from scratch, any output that isn't |
722
|
|
|
|
|
|
|
RFC-compliant is a bug. |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
=head1 FUTURE PLANS |
725
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
This release is still rough, and I have some plans for additional features: |
727
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
=head2 More methods for walking all parts |
729
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
Some more methods for walking/collecting multiple parts would be useful. |
731
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
=head2 More? |
733
|
|
|
|
|
|
|
|
734
|
|
|
|
|
|
|
Stay tuned for details. |
735
|
|
|
|
|
|
|
|
736
|
|
|
|
|
|
|
=head1 WHY DID I WRITE THIS MODULE? |
737
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
There a lot of email modules/distros on CPAN. Why didn't I use/fix one of them? |
739
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
=over 4 |
741
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
=item * L<Mail::Box> |
743
|
|
|
|
|
|
|
|
744
|
|
|
|
|
|
|
This one probably does everything this module does and more, but it's really, |
745
|
|
|
|
|
|
|
really big and complicated, forcing the end user to make a lot of choices just |
746
|
|
|
|
|
|
|
to get started. If you need it, it's great, but I generally find it to be too |
747
|
|
|
|
|
|
|
much module for me. |
748
|
|
|
|
|
|
|
|
749
|
|
|
|
|
|
|
=item * L<Email::Simple> and L<Email::MIME> |
750
|
|
|
|
|
|
|
|
751
|
|
|
|
|
|
|
These are surprisingly B<not> simple. They suffer from a problematic API (too |
752
|
|
|
|
|
|
|
high level in some spots, too low in others), and a poor separation of |
753
|
|
|
|
|
|
|
concerns. I've hacked on these enough to know that I can never make them do |
754
|
|
|
|
|
|
|
what I want. |
755
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
=item * Everything Else |
757
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
There's a lot of other email modules on CPAN, but none of them really seem any |
759
|
|
|
|
|
|
|
better than the ones mentioned above. |
760
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
=back |
762
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
=head1 CREDITS |
764
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
This module rips some chunks of code from a few other places, notably several |
766
|
|
|
|
|
|
|
of the Email suite modules. |
767
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
=head1 DONATIONS |
769
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
If you'd like to thank me for the work I've done on this module, please |
771
|
|
|
|
|
|
|
consider making a "donation" to me via PayPal. I spend a lot of free time |
772
|
|
|
|
|
|
|
creating free software, and would appreciate any support you'd care to offer. |
773
|
|
|
|
|
|
|
|
774
|
|
|
|
|
|
|
Please note that B<I am not suggesting that you must do this> in order for me |
775
|
|
|
|
|
|
|
to continue working on this particular software. I will continue to do so, |
776
|
|
|
|
|
|
|
inasmuch as I have in the past, for as long as it interests me. |
777
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
Similarly, a donation made in this way will probably not make me work on this |
779
|
|
|
|
|
|
|
software much more, unless I get so many donations that I can consider working |
780
|
|
|
|
|
|
|
on free software full time, which seems unlikely at best. |
781
|
|
|
|
|
|
|
|
782
|
|
|
|
|
|
|
To donate, log into PayPal and send money to autarch@urth.org or use the |
783
|
|
|
|
|
|
|
button on this page: L<http://www.urth.org/~autarch/fs-donation.html> |
784
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
=head1 BUGS |
786
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
Please report any bugs or feature requests to C<bug-courriel@rt.cpan.org>, or |
788
|
|
|
|
|
|
|
through the web interface at L<http://rt.cpan.org>. I will be notified, and |
789
|
|
|
|
|
|
|
then you'll automatically be notified of progress on your bug as I make |
790
|
|
|
|
|
|
|
changes. |
791
|
|
|
|
|
|
|
|
792
|
|
|
|
|
|
|
Bugs may be submitted through L<the RT bug tracker|http://rt.cpan.org/Public/Dist/Display.html?Name=Courriel> |
793
|
|
|
|
|
|
|
(or L<bug-courriel@rt.cpan.org|mailto:bug-courriel@rt.cpan.org>). |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
I am also usually active on IRC as 'autarch' on C<irc://irc.perl.org>. |
796
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
=head1 DONATIONS |
798
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
If you'd like to thank me for the work I've done on this module, please |
800
|
|
|
|
|
|
|
consider making a "donation" to me via PayPal. I spend a lot of free time |
801
|
|
|
|
|
|
|
creating free software, and would appreciate any support you'd care to offer. |
802
|
|
|
|
|
|
|
|
803
|
|
|
|
|
|
|
Please note that B<I am not suggesting that you must do this> in order for me |
804
|
|
|
|
|
|
|
to continue working on this particular software. I will continue to do so, |
805
|
|
|
|
|
|
|
inasmuch as I have in the past, for as long as it interests me. |
806
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
Similarly, a donation made in this way will probably not make me work on this |
808
|
|
|
|
|
|
|
software much more, unless I get so many donations that I can consider working |
809
|
|
|
|
|
|
|
on free software full time (let's all have a chuckle at that together). |
810
|
|
|
|
|
|
|
|
811
|
|
|
|
|
|
|
To donate, log into PayPal and send money to autarch@urth.org, or use the |
812
|
|
|
|
|
|
|
button at L<http://www.urth.org/~autarch/fs-donation.html>. |
813
|
|
|
|
|
|
|
|
814
|
|
|
|
|
|
|
=head1 AUTHOR |
815
|
|
|
|
|
|
|
|
816
|
|
|
|
|
|
|
Dave Rolsky <autarch@urth.org> |
817
|
|
|
|
|
|
|
|
818
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
819
|
|
|
|
|
|
|
|
820
|
|
|
|
|
|
|
=for stopwords Gregory Oschwald Ricardo Signes Zbigniew Åukasiak |
821
|
|
|
|
|
|
|
|
822
|
|
|
|
|
|
|
=over 4 |
823
|
|
|
|
|
|
|
|
824
|
|
|
|
|
|
|
=item * |
825
|
|
|
|
|
|
|
|
826
|
|
|
|
|
|
|
Gregory Oschwald <goschwald@maxmind.com> |
827
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
=item * |
829
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
Ricardo Signes <rjbs@users.noreply.github.com> |
831
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
=item * |
833
|
|
|
|
|
|
|
|
834
|
|
|
|
|
|
|
Zbigniew Åukasiak <zzbbyy@gmail.com> |
835
|
|
|
|
|
|
|
|
836
|
|
|
|
|
|
|
=back |
837
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
839
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
This software is Copyright (c) 2016 by Dave Rolsky. |
841
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
This is free software, licensed under: |
843
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
The Artistic License 2.0 (GPL Compatible) |
845
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
=cut |