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   383462 use 5.10.0;
  6         22  
4              
5 6     6   32 use strict;
  6         9  
  6         151  
6 6     6   28 use warnings;
  6         8  
  6         411  
7 6     6   3813 use namespace::autoclean;
  6         105720  
  6         28  
8              
9             our $VERSION = '0.43';
10              
11 6     6   4196 use Courriel::Headers;
  6         22  
  6         309  
12 6     6   57 use Courriel::Helpers qw( unique_boundary );
  6         353  
  6         434  
13 6     6   3673 use Courriel::Part::Multipart;
  6         24  
  6         291  
14 6     6   4107 use Courriel::Part::Single;
  6         21  
  6         306  
15 6     6   54 use Courriel::Types qw( ArrayRef Bool Headers Maybe Part Str StringRef );
  6         12  
  6         55  
16 6     6   45735 use DateTime;
  6         15  
  6         222  
17 6     6   34 use DateTime::Format::Mail 0.403;
  6         236  
  6         140  
18 6     6   4746 use DateTime::Format::Natural;
  6         281050  
  6         551  
19 6     6   66 use Email::Address;
  6         11  
  6         209  
20 6     6   30 use Encode qw( encode );
  6         13  
  6         323  
21 6     6   55 use List::AllUtils qw( uniq );
  6         14  
  6         408  
22 6     6   38 use Params::ValidationCompiler 0.18 qw( validation_for );
  6         194  
  6         323  
23              
24 6     6   39 use Moose;
  6         9  
  6         75  
25 6     6   40124 use MooseX::StrictConstructor;
  6         16  
  6         68  
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   5 my $self = shift;
204              
205             my @possible = (
206 2         57 ( map { $_->value } $self->headers->get('Date') ),
207             (
208             reverse
209 7         200 map { $self->_find_date_received( $_->value ) }
210             $self->headers->get('Received')
211             ),
212 2         16 ( 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     135 next unless defined $possible && length $possible;
218              
219 5         10 my $dt = eval { $mail_parser->parse_datetime($possible) };
  5         28  
220              
221 5 100       1742 unless ($dt) {
222 3         21 $dt = $natural_parser->parse_datetime($possible);
223 3 50       8734 next unless $natural_parser->success;
224             }
225              
226 2         119 $dt->set_time_zone('UTC');
227 2         414 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   7 shift;
237 7         6 my $received = shift;
238              
239 7 50 33     35 return unless defined $received && length $received;
240              
241 7         27 $received =~ s/.+;//;
242              
243 7         35 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   6 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         18 );
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         6 my $match = shift;
322              
323 3         84 my @parts = $self->top_level_part;
324              
325             ## no critic (ControlStructures::ProhibitCStyleForLoops)
326 3         16 for ( my $part = shift @parts; $part; $part = shift @parts ) {
327 5 100       10 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 72557 my $class = shift;
388 65         2133 my ( $text, $is_character ) = $validator->(@_);
389              
390 65 50       2734 if ($is_character) {
391 0         0 ${$text} = encode( 'UTF-8', ${$text} );
  0         0  
  0         0  
392             }
393              
394 65         250 return $class->new( part => $class->_parse($text) );
395             }
396             }
397              
398             sub _parse {
399 143     143   260 my $class = shift;
400 143         167 my $text = shift;
401              
402 143         382 my ( $sep_idx, $headers ) = $class->_parse_headers($text);
403              
404 143         179 substr( ${$text}, 0, $sep_idx, q{} );
  143         1286  
405              
406 143         524 return $class->_parse_parts( $text, $headers );
407             }
408              
409             sub _parse_headers {
410 143     143   180 my $class = shift;
411 143         176 my $text = shift;
412              
413 143         171 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         172 ${$text} =~ s/^From\s+.+$Courriel::Helpers::LINE_SEP_RE//;
  143         1302  
420              
421             # Some broken emails may split the From line in an arbitrary spot
422 143         217 ${$text} =~ s/^[^:]+$Courriel::Helpers::LINE_SEP_RE//g;
  143         751  
423              
424 143 100       170 if ( ${$text} =~ /^(.+?)($Courriel::Helpers::LINE_SEP_RE)\g{2}/s ) {
  143         7338  
425 142         625 $header_text = $1 . $2;
426 142         348 $sep_idx = ( length $header_text ) + ( length $2 );
427             }
428             else {
429 1         35 return ( 0, Courriel::Headers::->new );
430             }
431              
432 142         810 my $headers = Courriel::Headers::->parse(
433             text => \$header_text,
434             );
435              
436 142         490 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   216 my $class = shift;
447 143         170 my $text = shift;
448 143         162 my $headers = shift;
449              
450 143         499 my @ct = $headers->get('Content-Type');
451 143 50       407 if ( @ct > 1 ) {
452 0         0 die 'This email defines more than one Content-Type header.';
453             }
454              
455 143   66     393 my $ct = $ct[0] // $fake_ct;
456              
457 143 100       5192 if ( $ct->mime_type !~ /^multipart/i ) {
458 107         3951 return Courriel::Part::Single->new(
459             headers => $headers,
460             encoded_content => $text,
461             );
462             }
463              
464 36         154 return $class->_parse_multipart( $text, $headers, $ct );
465             }
466             }
467              
468             sub _parse_multipart {
469 36     36   61 my $class = shift;
470 36         54 my $text = shift;
471 36         49 my $headers = shift;
472 36         45 my $ct = shift;
473              
474 36         186 my $boundary = $ct->attribute_value('boundary');
475              
476 36 50 33     202 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         57 my ( $preamble, $all_parts, $epilogue ) = ${$text} =~ /
  36         2978  
482             (.*?) # preamble
483             ^--\Q$boundary\E\s*
484             (.+) # all parts
485             ^--\Q$boundary\E--\s*
486             (.*) # epilogue
487             /smx;
488              
489 36         67 my @part_text;
490              
491 36 100       116 if ( defined $all_parts ) {
492 31         967 @part_text = split /^--\Q$boundary\E\s*/m, $all_parts;
493             }
494              
495 36 100       119 unless (@part_text) {
496 5         6 ${$text} =~ s/^--\Q$boundary\E\s*//m;
  5         102  
497 5         10 push @part_text, ${$text};
  5         24  
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     530 parts => [ map { $class->_parse( \$_ ) } @part_text ],
  78 100 66     296  
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.43
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