File Coverage

blib/lib/Courriel.pm
Criterion Covered Total %
statement 131 195 67.1
branch 25 50 50.0
condition 10 21 47.6
subroutine 28 42 66.6
pod 6 7 85.7
total 200 315 63.4


line stmt bran cond sub pod time code
1             package Courriel;
2              
3 6     6   140782 use 5.10.0;
  6         17  
4              
5 6     6   18 use strict;
  6         6  
  6         94  
6 6     6   17 use warnings;
  6         5  
  6         110  
7 6     6   2366 use namespace::autoclean;
  6         63982  
  6         20  
8              
9             our $VERSION = '0.42';
10              
11 6     6   2597 use Courriel::Headers;
  6         94  
  6         266  
12 6     6   198 use Courriel::Helpers qw( unique_boundary );
  6         23  
  6         317  
13 6     6   2730 use Courriel::Part::Multipart;
  6         19  
  6         215  
14 6     6   2801 use Courriel::Part::Single;
  6         17  
  6         256  
15 6     6   42 use Courriel::Types qw( ArrayRef Bool Headers Maybe Part Str StringRef );
  6         7  
  6         42  
16 6     6   32483 use DateTime;
  6         8  
  6         158  
17 6     6   21 use DateTime::Format::Mail 0.403;
  6         168  
  6         105  
18 6     6   3437 use DateTime::Format::Natural;
  6         197644  
  6         356  
19 6     6   43 use Email::Address;
  6         7  
  6         128  
20 6     6   20 use Encode qw( encode );
  6         9  
  6         224  
21 6     6   25 use List::AllUtils qw( uniq );
  6         8  
  6         239  
22 6     6   25 use MooseX::Params::Validate 0.21 qw( validated_list );
  6         129  
  6         43  
23              
24 6     6   1462 use Moose;
  6         10  
  6         46  
25 6     6   27654 use MooseX::StrictConstructor;
  6         8  
  6         42  
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   2 my $self = shift;
204              
205             my @possible = (
206 2         43 ( map { $_->value } $self->headers->get('Date') ),
207             (
208             reverse
209 7         135 map { $self->_find_date_received( $_->value ) }
210             $self->headers->get('Received')
211             ),
212 2         9 ( map { $_->value } $self->headers->get('Resent-Date') ),
  0         0  
213             );
214              
215             # Stolen from Email::Date and then modified
216 2         7 for my $possible (@possible) {
217 5 50 33     76 next unless defined $possible && length $possible;
218              
219 5         5 my $dt = eval { $mail_parser->parse_datetime($possible) };
  5         18  
220              
221 5 100       986 unless ($dt) {
222 3         11 $dt = $natural_parser->parse_datetime($possible);
223 3 50       5420 next unless $natural_parser->success;
224             }
225              
226 2         101 $dt->set_time_zone('UTC');
227 2         274 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   6 shift;
237 7         5 my $received = shift;
238              
239 7 50 33     23 return unless defined $received && length $received;
240              
241 7         19 $received =~ s/.+;//;
242              
243 7         16 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   5 my $self = shift;
299              
300             return $self->first_part_matching(
301             sub {
302 5 100   5   17 $_[0]->mime_type eq 'text/plain'
303             && $_[0]->is_inline;
304             }
305 3         13 );
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         4 my $match = shift;
322              
323 3         64 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       8 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 @spec = ( text => { isa => StringRef, coerce => 1 } );
352              
353             # This is needed for Email::Abstract compatibility but it's a godawful
354             # idea, and even Email::Abstract says not to do this.
355             #
356             # It's much safer to just make a new Courriel object from scratch.
357             sub replace_body {
358 0     0 0 0 my $self = shift;
359 0         0 my ($text) = validated_list(
360             \@_,
361             @spec,
362             );
363              
364 0         0 my $part = Courriel::Part::Single->new(
365             headers => $self->headers,
366             encoded_content => $text,
367             );
368              
369 0         0 $self->_replace_top_level_part($part);
370              
371 0         0 return;
372             }
373             }
374              
375             {
376             my @spec = (
377             text => { isa => StringRef, coerce => 1 },
378             is_character => { isa => Bool, default => 0 },
379             );
380              
381             sub parse {
382 65     65 1 38796 my $class = shift;
383 65         254 my ( $text, $is_character ) = validated_list(
384             \@_,
385             @spec,
386             );
387              
388 65 50       36939 if ($is_character) {
389 0         0 ${$text} = encode( 'UTF-8', ${$text} );
  0         0  
  0         0  
390             }
391              
392 65         208 return $class->new( part => $class->_parse($text) );
393             }
394             }
395              
396             sub _parse {
397 143     143   212 my $class = shift;
398 143         153 my $text = shift;
399              
400 143         258 my ( $sep_idx, $headers ) = $class->_parse_headers($text);
401              
402 143         143 substr( ${$text}, 0, $sep_idx, q{} );
  143         966  
403              
404 143         398 return $class->_parse_parts( $text, $headers );
405             }
406              
407             sub _parse_headers {
408 143     143   158 my $class = shift;
409 143         153 my $text = shift;
410              
411 143         130 my $header_text;
412             my $sep_idx;
413              
414             # We want to ignore mbox message separators - this is a pretty lax parser,
415             # but we may find broken lines. The key is that it starts with From
416             # followed by space, not a colon.
417 143         116 ${$text} =~ s/^From\s+.+$Courriel::Helpers::LINE_SEP_RE//;
  143         1023  
418              
419             # Some broken emails may split the From line in an arbitrary spot
420 143         152 ${$text} =~ s/^[^:]+$Courriel::Helpers::LINE_SEP_RE//g;
  143         522  
421              
422 143 100       136 if ( ${$text} =~ /^(.+?)($Courriel::Helpers::LINE_SEP_RE)\g{2}/s ) {
  143         5356  
423 142         474 $header_text = $1 . $2;
424 142         225 $sep_idx = ( length $header_text ) + ( length $2 );
425             }
426             else {
427 1         25 return ( 0, Courriel::Headers::->new );
428             }
429              
430 142         581 my $headers = Courriel::Headers::->parse(
431             text => \$header_text,
432             );
433              
434 142         384 return ( $sep_idx, $headers );
435             }
436              
437             {
438             my $fake_ct = Courriel::Header::ContentType->new_from_value(
439             name => 'Content-Type',
440             value => 'text/plain'
441             );
442              
443             sub _parse_parts {
444 143     143   158 my $class = shift;
445 143         119 my $text = shift;
446 143         120 my $headers = shift;
447              
448 143         425 my @ct = $headers->get('Content-Type');
449 143 50       332 if ( @ct > 1 ) {
450 0         0 die 'This email defines more than one Content-Type header.';
451             }
452              
453 143   66     281 my $ct = $ct[0] // $fake_ct;
454              
455 143 100       3553 if ( $ct->mime_type !~ /^multipart/i ) {
456 107         2742 return Courriel::Part::Single->new(
457             headers => $headers,
458             encoded_content => $text,
459             );
460             }
461              
462 36         97 return $class->_parse_multipart( $text, $headers, $ct );
463             }
464             }
465              
466             sub _parse_multipart {
467 36     36   36 my $class = shift;
468 36         36 my $text = shift;
469 36         63 my $headers = shift;
470 36         38 my $ct = shift;
471              
472 36         127 my $boundary = $ct->attribute_value('boundary');
473              
474 36 50 33     169 die q{The message's mime type claims this is a multipart message (}
475             . $ct->mime_type
476             . q{) but it does not specify a boundary.}
477             unless defined $boundary && length $boundary;
478              
479 36         47 my ( $preamble, $all_parts, $epilogue ) = ${$text} =~ /
  36         2204  
480             (.*?) # preamble
481             ^--\Q$boundary\E\s*
482             (.+) # all parts
483             ^--\Q$boundary\E--\s*
484             (.*) # epilogue
485             /smx;
486              
487 36         46 my @part_text;
488              
489 36 100       69 if ( defined $all_parts ) {
490 31         686 @part_text = split /^--\Q$boundary\E\s*/m, $all_parts;
491             }
492              
493 36 100       80 unless (@part_text) {
494 5         6 ${$text} =~ s/^--\Q$boundary\E\s*//m;
  5         75  
495 5         7 push @part_text, ${$text};
  5         17  
496             }
497              
498             return Courriel::Part::Multipart->new(
499             headers => $headers,
500             (
501             defined $preamble
502             && length $preamble
503             && $preamble =~ /\S/ ? ( preamble => $preamble ) : ()
504             ),
505             (
506             defined $epilogue
507             && length $epilogue
508             && $epilogue =~ /\S/ ? ( epilogue => $epilogue ) : ()
509             ),
510             boundary => $boundary,
511 36 100 100     367 parts => [ map { $class->_parse( \$_ ) } @part_text ],
  78 100 66     214  
512             );
513             }
514              
515             __PACKAGE__->meta->make_immutable;
516              
517             1;
518              
519             # ABSTRACT: High level email parsing and manipulation
520              
521             __END__
522              
523             =pod
524              
525             =encoding utf-8
526              
527             =head1 NAME
528              
529             Courriel - High level email parsing and manipulation
530              
531             =head1 VERSION
532              
533             version 0.42
534              
535             =head1 SYNOPSIS
536              
537             my $email = Courriel->parse( text => $raw_email );
538              
539             print $email->subject;
540              
541             print $_->address for $email->participants;
542              
543             print $email->datetime->year;
544              
545             if ( my $part = $email->plain_body_part ) {
546             print $part->content;
547             }
548              
549             =head1 DESCRIPTION
550              
551             This class exists to provide a high level API for working with emails,
552             particular for processing incoming email. It is primarily a wrapper around the
553             other classes in the Courriel distro, especially L<Courriel::Headers>,
554             L<Courriel::Part::Single>, and L<Courriel::Part::Multipart>. If you need lower
555             level information about an email, it should be available from one of these
556             classes.
557              
558             =head1 API
559              
560             This class provides the following methods:
561              
562             =head2 Courriel->parse( text => $raw_email, is_character => 0|1 )
563              
564             This parses the given text and returns a new Courriel object. The text can be
565             provided as a string or a reference to a string.
566              
567             If you pass a reference, then the scalar underlying the reference I<will> be
568             modified, so don't pass in something you don't want modified.
569              
570             By default, Courriel expects that content passed in text is binary data. This
571             means that it has not been decoded into utf-8 with C<Encode::decode()> or by
572             using a C<:encoding(UTF-8)> IO layer.
573              
574             In practice, this doesn't matter for most emails, since they either contain
575             only ASCII data or they actually do contain binary (non-character)
576             data. However, if an email is using the 8bit Content-Transfer-Encoding, then
577             this does matter.
578              
579             If the email has already been decoded, you must set C<is_character> to a true
580             value.
581              
582             It's safest to simply pass binary data to Courriel and let it handle decoding
583             internally.
584              
585             =head2 $email->parts()
586              
587             Returns an array (not a reference) of the parts this email contains.
588              
589             =head2 $email->part_count()
590              
591             Returns the number of parts this email contains.
592              
593             =head2 $email->is_multipart()
594              
595             Returns true if the top-level part is a multipart part, false otherwise.
596              
597             =head2 $email->top_level_part()
598              
599             Returns the actual top level part for the object. You're probably better off
600             just calling C<< $email->parts() >> most of the time, since when the email is
601             multipart, the top level part is just a container.
602              
603             =head2 $email->subject()
604              
605             Returns the email's Subject header value, or C<undef> if it doesn't have one.
606              
607             =head2 $email->datetime()
608              
609             Returns a L<DateTime> object for the email. The DateTime object is always in
610             the "UTC" time zone.
611              
612             This uses the Date header by default one. Otherwise it looks at the date in
613             each Received header, and then it looks for a Resent-Date header. If none of
614             these exists, it just returns C<< DateTime->now() >>.
615              
616             =head2 $email->from()
617              
618             This returns a single L<Email::Address> object based on the From header of the
619             email. If the email has no From header or if the From header is broken, it
620             returns C<undef>.
621              
622             =head2 $email->participants()
623              
624             This returns a list of L<Email::Address> objects, one for each unique
625             participant in the email. This includes any address in the From, To, or CC
626             headers.
627              
628             Just like with the From header, broken addresses will not be included.
629              
630             =head2 $email->recipients()
631              
632             This returns a list of L<Email::Address> objects, one for each unique
633             recipient in the email. This includes any address in the To or CC headers.
634              
635             Just like with the From header, broken addresses will not be included.
636              
637             =head2 $email->to()
638              
639             This returns a list of L<Email::Address> objects, one for each unique
640             address in the To header.
641              
642             Just like with the From header, broken addresses will not be included.
643              
644             =head2 $email->cc()
645              
646             This returns a list of L<Email::Address> objects, one for each unique
647             address in the CC header.
648              
649             Just like with the From header, broken addresses will not be included.
650              
651             =head2 $email->plain_body_part()
652              
653             This returns the first L<Courriel::Part::Single> object in the email with a
654             mime type of "text/plain" and an inline disposition, if one exists.
655              
656             =head2 $email->html_body_part()
657              
658             This returns the first L<Courriel::Part::Single> object in the email with a
659             mime type of "text/html" and an inline disposition, if one exists.
660              
661             =head2 $email->clone_without_attachments()
662              
663             Returns a new Courriel object that only contains inline parts from the
664             original email, effectively removing all attachments.
665              
666             =head2 $email->first_part_matching( sub { ... } )
667              
668             Given a subroutine reference, this method calls that subroutine for each part
669             in the email, in a depth-first search.
670              
671             The subroutine receives the part as its only argument. If it returns true,
672             this method returns that part.
673              
674             =head2 $email->all_parts_matching( sub { ... } )
675              
676             Given a subroutine reference, this method calls that subroutine for each part
677             in the email, in a depth-first search.
678              
679             The subroutine receives the part as its only argument. If it returns true,
680             this method includes that part.
681              
682             This method returns all of the parts that match the subroutine.
683              
684             =head2 $email->content_type()
685              
686             Returns the L<Courriel::Header::ContentType> object associated with the email.
687              
688             =head2 $email->headers()
689              
690             Returns the L<Courriel::Headers> object for this email.
691              
692             =head2 $email->stream_to( output => $output )
693              
694             This method will send the stringified email to the specified output. The
695             output can be a subroutine reference, a filehandle, or an object with a
696             C<print()> method. The output may be sent as a single string, as a list of
697             strings, or via multiple calls to the output.
698              
699             For large emails, streaming can be much more memory efficient than generating
700             a single string in memory.
701              
702             =head2 $email->as_string()
703              
704             Returns the email as a string, along with its headers. Lines will be
705             terminated with "\r\n".
706              
707             =head1 ROBUSTNESS PRINCIPLE
708              
709             Courriel aims to respect the common Internet robustness principle (aka
710             Postel's law). Courriel is conservative in the output it generates, and
711             liberal in what it accepts.
712              
713             When parsing, the goal is to never die and always return as much information
714             as possible. Any input that causes the C<< Courriel->parse() >> to die means
715             there's a bug in the parser. Please report these bugs.
716              
717             Conversely, Courriel aims to respect all relevant RFCs in its output, except
718             when it preserves the original data in a parsed email. If you're using
719             L<Courriel::Builder> to create emails from scratch, any output that isn't
720             RFC-compliant is a bug.
721              
722             =head1 FUTURE PLANS
723              
724             This release is still rough, and I have some plans for additional features:
725              
726             =head2 More methods for walking all parts
727              
728             Some more methods for walking/collecting multiple parts would be useful.
729              
730             =head2 More?
731              
732             Stay tuned for details.
733              
734             =head1 WHY DID I WRITE THIS MODULE?
735              
736             There a lot of email modules/distros on CPAN. Why didn't I use/fix one of them?
737              
738             =over 4
739              
740             =item * L<Mail::Box>
741              
742             This one probably does everything this module does and more, but it's really,
743             really big and complicated, forcing the end user to make a lot of choices just
744             to get started. If you need it, it's great, but I generally find it to be too
745             much module for me.
746              
747             =item * L<Email::Simple> and L<Email::MIME>
748              
749             These are surprisingly B<not> simple. They suffer from a problematic API (too
750             high level in some spots, too low in others), and a poor separation of
751             concerns. I've hacked on these enough to know that I can never make them do
752             what I want.
753              
754             =item * Everything Else
755              
756             There's a lot of other email modules on CPAN, but none of them really seem any
757             better than the ones mentioned above.
758              
759             =back
760              
761             =head1 CREDITS
762              
763             This module rips some chunks of code from a few other places, notably several
764             of the Email suite modules.
765              
766             =head1 DONATIONS
767              
768             If you'd like to thank me for the work I've done on this module, please
769             consider making a "donation" to me via PayPal. I spend a lot of free time
770             creating free software, and would appreciate any support you'd care to offer.
771              
772             Please note that B<I am not suggesting that you must do this> in order for me
773             to continue working on this particular software. I will continue to do so,
774             inasmuch as I have in the past, for as long as it interests me.
775              
776             Similarly, a donation made in this way will probably not make me work on this
777             software much more, unless I get so many donations that I can consider working
778             on free software full time, which seems unlikely at best.
779              
780             To donate, log into PayPal and send money to autarch@urth.org or use the
781             button on this page: L<http://www.urth.org/~autarch/fs-donation.html>
782              
783             =head1 BUGS
784              
785             Please report any bugs or feature requests to C<bug-courriel@rt.cpan.org>, or
786             through the web interface at L<http://rt.cpan.org>. I will be notified, and
787             then you'll automatically be notified of progress on your bug as I make
788             changes.
789              
790             Bugs may be submitted through L<the RT bug tracker|http://rt.cpan.org/Public/Dist/Display.html?Name=Courriel>
791             (or L<bug-courriel@rt.cpan.org|mailto:bug-courriel@rt.cpan.org>).
792              
793             I am also usually active on IRC as 'drolsky' on C<irc://irc.perl.org>.
794              
795             =head1 DONATIONS
796              
797             If you'd like to thank me for the work I've done on this module, please
798             consider making a "donation" to me via PayPal. I spend a lot of free time
799             creating free software, and would appreciate any support you'd care to offer.
800              
801             Please note that B<I am not suggesting that you must do this> in order for me
802             to continue working on this particular software. I will continue to do so,
803             inasmuch as I have in the past, for as long as it interests me.
804              
805             Similarly, a donation made in this way will probably not make me work on this
806             software much more, unless I get so many donations that I can consider working
807             on free software full time (let's all have a chuckle at that together).
808              
809             To donate, log into PayPal and send money to autarch@urth.org, or use the
810             button at L<http://www.urth.org/~autarch/fs-donation.html>.
811              
812             =head1 AUTHOR
813              
814             Dave Rolsky <autarch@urth.org>
815              
816             =head1 CONTRIBUTORS
817              
818             =for stopwords Gregory Oschwald Ricardo Signes Zbigniew Łukasiak
819              
820             =over 4
821              
822             =item *
823              
824             Gregory Oschwald <goschwald@maxmind.com>
825              
826             =item *
827              
828             Ricardo Signes <rjbs@users.noreply.github.com>
829              
830             =item *
831              
832             Zbigniew Łukasiak <zzbbyy@gmail.com>
833              
834             =back
835              
836             =head1 COPYRIGHT AND LICENCE
837              
838             This software is Copyright (c) 2016 by Dave Rolsky.
839              
840             This is free software, licensed under:
841              
842             The Artistic License 2.0 (GPL Compatible)
843              
844             =cut