File Coverage

blib/lib/Email/Reply.pm
Criterion Covered Total %
statement 102 102 100.0
branch 36 48 75.0
condition 16 28 57.1
subroutine 16 16 100.0
pod 1 1 100.0
total 171 195 87.6


line stmt bran cond sub pod time code
1 1     1   22297 use strict;
  1         2  
  1         24  
2 1     1   4 use warnings;
  1         3  
  1         52  
3             package Email::Reply;
4             # ABSTRACT: reply to an email message
5             $Email::Reply::VERSION = '1.204';
6 1     1   786 use Email::Abstract 2.01;
  1         34303  
  1         41  
7 1     1   778 use Email::Address 1.80;
  1         32469  
  1         66  
8 1     1   899 use Email::MIME 1.82;
  1         29988  
  1         32  
9 1     1   8 use Exporter 5.57 'import';
  1         12  
  1         1573  
10              
11             my $CLASS = __PACKAGE__;
12             our @EXPORT = qw[reply];
13             my $CRLF = "\x0d\x0a";
14              
15             # Want to subclass and still use the functional interface?
16             # That's cool, just add these lines to your package:
17             # use base qw[Exporter];
18             # use vars qw[@EXPORT $CLASS];
19             # @EXPORT = qw[reply];
20             # $CLASS = __PACKAGE__;
21             # *reply = \&Email::Reply::reply;
22              
23             sub reply {
24 3     3 1 19391 my $reply = $CLASS->_new(@_);
25 3         11 $reply->_make_headers;
26 3 100       10 $reply->_encapsulate_message if $reply->{attach};
27             $reply->_quote_body($reply->{original})
28 3 50 33     807 if $reply->{quote} || $reply->{top_post};
29 3 50       14 $reply->_post_reply if $reply->{body};
30 3 100       13 return $reply->{message} ? $reply->_mime : $reply->_simple;
31             }
32              
33             sub _new {
34 3     3   17 my ($class, %args) = @_;
35 3         5 my $self = {};
36 3         19 $self->{original} = Email::MIME->new(Email::Abstract->as_string($args{to}));
37              
38             ($self->{from}) =
39 3   66     2050 Email::Address->parse($args{from} || $self->{original}->header('To'));
40              
41             # There are three headers which may give the 'to' address.
42 3         1272 my $addr_to_parse;
43 3         9 my @headers = qw(Reply-To From Return-Path);
44 3         8 my $orig = $self->{original};
45 3         7 foreach (@headers) {
46 6         20 my $v = $orig->header($_);
47 6 100 66     280 if (defined $v and $v ne '') {
48 3         6 $addr_to_parse = $v;
49 3         7 last;
50             }
51             }
52 3 50       9 die "did not find any of the headers: @headers" if not defined $addr_to_parse;
53              
54             # Parse it and check it succeeded.
55 3         11 my (@parsed) = Email::Address->parse($addr_to_parse);
56 3 50       115 foreach (@parsed) { die if not defined }
  3         11  
57 3 50       8 die "failed to parse address '$addr_to_parse'" if not @parsed;
58 3 50       9 die "strange, '$addr_to_parse' parses to more than one address: @parsed" if @parsed != 1;
59 3         6 $self->{to} = $parsed[0];
60              
61             $self->{attrib} = $args{attrib}
62 3   66     18 || (($self->{to}->name || join($self->{to}->address, '<', '>')) . ' wrote:');
63              
64 3   100     264 $self->{prefix} = $args{prefix} || '> ';
65 3         7 $self->{top_post} = $args{top_post};
66 3 50       9 $self->{quote} = exists $args{quote} ? $args{quote} : 1;
67 3         8 $self->{all} = $args{all};
68 3         7 $self->{quoted} = '';
69 3         7 $self->{body} = $args{body};
70 3         5 $self->{attach} = $args{attach};
71 3         5 $self->{keep_sig} = $args{keep_sig};
72              
73 3         13 return bless $self, $class;
74             }
75              
76             sub _make_headers {
77 3     3   4 my $self = shift;
78              
79 3         13 my @header = (From => $self->{from},);
80              
81             $self->{to}
82             ->name((Email::Address->parse($self->{original}->header('From')))[0]->name)
83 3 50       11 unless $self->{to}->name;
84 3         57 push @header, To => $self->{to};
85              
86 3   50     13 my $subject = $self->{original}->header('Subject') || '';
87 3 100       118 $subject = "Re: $subject" unless $subject =~ /\bRe:/i;
88 3         7 push @header, Subject => $subject;
89              
90 3         11 my ($msg_id) = Email::Address->parse($self->{original}->header('Message-ID'));
91 3         244 push @header, 'In-Reply-To' => $msg_id;
92              
93 3         12 my @refs = Email::Address->parse($self->{original}->header('References'));
94 3 100       124 @refs = Email::Address->parse($self->{original}->header('In-Reply-To'))
95             unless @refs;
96 3 100       68 push @refs, $msg_id if $msg_id;
97 3 100       72 push @header, References => join ' ', @refs if @refs;
98              
99 3 100       39 if ($self->{all}) {
100             my @addrs = (
101             Email::Address->parse($self->{original}->header('To')),
102 1         4 Email::Address->parse($self->{original}->header('Cc')),
103             );
104 1 50       207 unless ($self->{self}) {
105 1         2 @addrs = grep { $_->address ne $self->{from}->address } @addrs;
  3         22  
106             }
107 1 50       12 push @header, Cc => join ', ', @addrs if @addrs;
108             }
109              
110 3         60 $self->{header} = \@header;
111             }
112              
113             sub _encapsulate_message {
114 1     1   2 my $self = shift;
115             $self->{message} = Email::MIME->create(
116             attributes => { content_type => 'message/rfc822', },
117             body => $self->{original}->as_string,
118 1         7 );
119             }
120              
121             my $crlf = qr/\x0a\x0d|\x0d\x0a|\x0a|\x0d/;
122              
123             sub _quote_body {
124 5     5   27 my ($self, $part) = @_;
125 5 100       17 return if length $self->{quoted};
126 4 100       10 return map $self->_quote_body($_), $part->parts if $part->parts > 1;
127 3 50 33     31 return if $part->content_type && $part->content_type !~ m[\btext/plain\b];
128              
129 3         107 my $body = $part->body;
130              
131             $body = ($self->_strip_sig($body) || $body)
132 3 100 33     190 if !$self->{keep_sig} && $body =~ /$crlf--\s*$crlf/o;
      100        
133              
134 3         39 my ($end) = $body =~ /($crlf)/;
135 3   33     10 $end ||= $CRLF;
136 3         22 $body =~ s/[\r\n\s]+$//;
137 3         9 $body = $self->_quote_orig_body($body);
138 3         10 $body = "$self->{attrib}$end$body$end";
139              
140 3         7 $self->{crlf} = $end;
141 3         9 $self->{quoted} = $body;
142             }
143              
144             # Yes, you are witnessing elitism.
145 1     1   38 sub _strip_sig { reverse +(split /$crlf\s*--$crlf/o, reverse(pop), 2)[1] }
146              
147             sub _quote_orig_body {
148 3     3   5 my ($self, $body) = @_;
149 3         58 $body =~ s/($crlf)/$1$self->{prefix}/g;
150 3         12 "$self->{prefix}$body";
151             }
152              
153             sub _post_reply {
154 3     3   4 my $self = shift;
155             return $self->{reply_body} = $self->{body}
156 3 50       13 unless length $self->{quoted};
157 3         5 my @parts = (@{$self}{qw[quoted body]});
  3         8  
158 3 100       10 @parts = reverse @parts if $self->{top_post};
159 3         13 $self->{reply_body} = join $self->{crlf}, @parts;
160             }
161              
162             sub _mime {
163 1     1   3 my $self = shift;
164             Email::MIME->create(
165             header => $self->{header},
166             parts =>
167 1         5 [ Email::MIME->create(body => $self->{reply_body}), $self->{message}, ],
168             );
169             }
170              
171             sub _simple {
172 2     2   3 my $self = shift;
173             Email::Simple->create(
174             header => $self->{header},
175             body => $self->{reply_body},
176 2         9 );
177             }
178              
179             1;
180              
181             #pod =head1 SYNOPSIS
182             #pod
183             #pod use Email::Reply;
184             #pod
185             #pod my $message = Email::Simple->new(join '', <>);
186             #pod my $from = (Email::Address->parse($message->header('From'))[0];
187             #pod
188             #pod my $reply = reply to => $message,
189             #pod from => '"Casey West" ',
190             #pod all => 1,
191             #pod body => <<__RESPONSE__;
192             #pod Thanks for the message, I'll be glad to explain...
193             #pod __RESPONSE__
194             #pod
195             #pod =head1 DESCRIPTION
196             #pod
197             #pod This software takes the hard out of generating replies to email messages.
198             #pod
199             #pod =func reply
200             #pod
201             #pod my $reply = reply to => $message,
202             #pod from => '"Casey West" ',
203             #pod all => 1;
204             #pod self => 0,
205             #pod attach => 1,
206             #pod quote => 1,
207             #pod top_post => 0,
208             #pod keep_sig => 1,
209             #pod prefix => ': ',
210             #pod attrib => sprintf("From %s, typer of many words:",
211             #pod $from->name),
212             #pod body => <<__RESPONSE__;
213             #pod Thanks for the message, I'll be glad to explain the picture...
214             #pod __RESPONSE__
215             #pod
216             #pod This function accepts a number of named parameters and returns an email
217             #pod message object of type C or C, depending
218             #pod on the parameters passed. Lets review those parameters now.
219             #pod
220             #pod =begin :list
221             #pod
222             #pod = C
223             #pod
224             #pod This required parameter is the email message you're replying to. It can
225             #pod represent a number of object types, or a string containing the message. This
226             #pod value is passed directly to C without passing go or collecting
227             #pod $200 so please, read up on its available plugins for what is allowed here.
228             #pod
229             #pod = C
230             #pod
231             #pod This optional parameter specifies an email address to use indicating the sender
232             #pod of the reply message. It can be a string or an C object. In the
233             #pod absence of this parameter, the first address found in the original message's
234             #pod C header is used. This may not always be what you want, so this parameter
235             #pod comes highly recommended.
236             #pod
237             #pod = C
238             #pod
239             #pod This optional parameter indicates weather or not you'd like to "Reply to All."
240             #pod If true, the reply's C header will be populated with all the addresses in
241             #pod the original's C and C headers. By default, the parameter is false,
242             #pod indicating "Reply to Sender."
243             #pod
244             #pod = C
245             #pod
246             #pod This optional parameter decides weather or not an address matching the C
247             #pod address will be included in the list of C addresses. If true, your address
248             #pod will be preserved in that list if it is found. If false, as it is by default,
249             #pod your address will be removed from the list. As you might expect, this parameter
250             #pod is only useful if C is true.
251             #pod
252             #pod = C
253             #pod
254             #pod This optional parameter allows for the original message, in
255             #pod its entirety, to be encapsulated in a MIME part of type C.
256             #pod If true, the returned object from C will be a C object
257             #pod whose second part is the encapsulated message. If false, none of this happens.
258             #pod By default, none of this happens.
259             #pod
260             #pod = C
261             #pod
262             #pod This optional parameter, which is true by default, will quote
263             #pod the original message for your reply. If the original message is a MIME
264             #pod message, the first C type part will be quoted. If it's a Simple
265             #pod message, the body will be quoted. Well, that's only if you keep the
266             #pod parameter true. If you don't, none of this occurs.
267             #pod
268             #pod = C
269             #pod
270             #pod This optional parameter, whose use is generally discouraged, will allow top
271             #pod posting when true. It will implicitly set C to true, and put your
272             #pod C before the quoted text. It is false by default, and you should do your
273             #pod best to keep it that way.
274             #pod
275             #pod = C
276             #pod
277             #pod This optional parameter toggles the signature stripping mechanism. True by
278             #pod default, the original quoted body will have its signature removed. When false,
279             #pod the signature is left in-tact and will be quoted accordingly. This is only
280             #pod useful when C is true.
281             #pod
282             #pod = C
283             #pod
284             #pod This optional parameter specifies the quoting prefix. By default, it's
285             #pod C<< > >>, but you can change it by setting this parameter. Again, only useful
286             #pod when C is true.
287             #pod
288             #pod = C
289             #pod
290             #pod This optional parameter specifies the attribution line to add to the beginning
291             #pod of quoted text. By default, the name or email address of the original sender is
292             #pod used to replace C<%s> in the string, C<"%s wrote:">. You may change that with
293             #pod this parameter. No special formats, C or otherwise, are provided for
294             #pod your convenience. Sorry, you'll have to make due. Like C and
295             #pod C, this is only good when C is true.
296             #pod
297             #pod = C
298             #pod
299             #pod This required parameter contains your prose, your manifesto, your reply.
300             #pod Remember to spell check!
301             #pod
302             #pod =end :list
303             #pod
304             #pod =head1 SEE ALSO
305             #pod
306             #pod L,
307             #pod L,
308             #pod L,
309             #pod L,
310             #pod L,
311             #pod L.
312              
313             __END__