| 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 |