Send unencrypted replies if encryption fails
[app-edwardng.git] / lib / App / EdwardNG.pm
CommitLineData
2f9e679a
MG
1package App::EdwardNG;
2
3use 5.014000;
4use strict;
5use warnings;
6use parent qw/Exporter/;
7our $VERSION = '0.001';
8our @EXPORT = qw/process_message/;
9
10use Email::Sender::Simple qw/sendmail/;
11use File::Slurp qw/read_file/;
12use Getopt::Long;
13use MIME::Entity;
14use MIME::Parser;
15use Mail::GnuPG;
16use PerlX::Maybe;
17use Template;
18use Try::Tiny;
19
20sub debug { say STDERR @_ if $ENV{EDWARDNG_DEBUG} }
21sub stringify ($) { join '', map {; '>', $_ } @{$_[0]} }
22sub mg {
23 Mail::GnuPG->new(
24 key => $ENV{EDWARDNG_KEY},
25 maybe always_trust => $ENV{EDWARDNG_ALWAYS_TRUST},
26 maybe keydir => $ENV{EDWARDNG_KEYDIR},
27 maybe passphrase => $ENV{EDWARDNG_PASSPHRASE},
28 maybe use_agent => $ENV{EDWARDNG_USE_AGENT},
29 @_);
30}
31
32sub first_part{
33 my ($ent) = @_;
34 return first_part $ent->parts(0) if $ent->parts;
35 $ent->bodyhandle->as_string
36}
37
38sub process_message {
39 my ($msg) = @_;
40 my $parser = MIME::Parser->new;
41 $parser->decode_bodies(0);
42 $parser->output_to_core(1);
43
44 if (ref $msg eq 'MIME::Entity') {
45 debug 'Got MIME::Entity';
46 } elsif (ref $msg eq 'IO') {
47 debug 'Parsing from filehandle';
48 $msg = $parser->parse($msg)
49 } elsif (ref $msg eq 'SCALAR') {
50 debug 'Parsing from string';
51 $msg = $parser->parse_data($$msg)
52 } elsif (!ref $msg) {
53 debug "Parsing from file $msg";
54 $msg = $parser->parse_open($msg)
55 } else {
56 die "Don't know how to parse $msg"
57 }
58
59 if ($msg->mime_type ne 'multipart/signed' && $msg->mime_type ne 'multipart/encrypted') {
60 # PGP/Inline requires decoding
61 $parser->decode_bodies(1);
62 $msg = $parser->parse_data($msg->stringify)
63 }
64
65 my $gpg = mg;
66 if ($gpg->is_signed($msg)) {
67 debug 'This mail looks signed';
68 my ($code, $keyid, $email) = $gpg->verify($msg);
69 return sign_error => (
70 message => stringify $gpg->{last_message}) if $code;
71 return sign => (
72 keyid => $keyid,
73 email => $email,
74 message => stringify $gpg->{last_message});
75 }
76
77 if ($gpg->is_encrypted($msg)) {
78 debug 'This mail looks encrypted';
79 my ($code, $keyid, $email) = $gpg->decrypt($msg);
80 return encrypt_error => (
81 message => stringify $gpg->{last_message}) if $code;
82 return encrypt => (
83 plaintext => stringify $gpg->{plaintext},
84 decrypted => $gpg->{decrypted},
85 message => stringify $gpg->{last_message}) unless defined $keyid;
86 return signencrypt => (
87 keyid => $keyid,
88 email => $email,
89 plaintext => stringify $gpg->{plaintext},
90 decrypted => $gpg->{decrypted},
91 message => stringify $gpg->{last_message});
92 }
93
94 debug 'This mail doesn\'t seem to be signed or encrypted';
95 return 'plain'
96}
97
98sub run {
99 GetOptions(
100 'always-trust!' => \$ENV{EDWARDNG_ALWAYS_TRUST},
101 'debug!' => \$ENV{EDWARDNG_DEBUG},
102 'from=s' => \$ENV{EDWARDNG_FROM},
103 'key=s' => \$ENV{EDWARDNG_KEY},
104 'keydir=s' => \$ENV{EDWARDNG_KEYDIR},
105 'passphrase=s' => \$ENV{EDWARDNG_PASSPHRASE},
106 'use-agent!' => \$ENV{EDWARDNG_USE_AGENT},
107 );
108
109 my $parser = MIME::Parser->new;
110 $parser->decode_bodies(0);
111 $parser->output_to_core(1);
112 my $in = $parser->parse(\*STDIN);
113
114 my ($tmpl, %params);
115 try {
116 ($tmpl, %params) = process_message $in
117 } catch {
118 ($tmpl, %params) = (error => message => $_)
119 };
120
121 $params{plaintext} = first_part $params{decrypted} if $params{decrypted};
122
123 my $tt = Template->new(INCLUDE_PATH => 'tmpl/en');
c3455a5b 124 my $data;
2f9e679a 125 $tt->process($tmpl, \%params, \$data);
2f9e679a
MG
126 my $email = MIME::Entity->build(
127 From => $ENV{EDWARDNG_FROM},
128 To => $in->get('From'),
c3455a5b 129 Subject => 'Re: ' . $in->get('Subject'),
2f9e679a
MG
130 Data => $data);
131
70f9c37d 132 my $email_unencrypted = $email->dup;
2f9e679a 133 my $mg = mg always_trust => 1;
70f9c37d
MG
134 my $encrypt_failed = $mg->mime_signencrypt($email, $in->get('From') =~ /<(.*)>/);
135 debug 'Could not encrypt message, sending unencrypted. GnuPG said ', stringify $mg->{last_message} if $encrypt_failed;
136 sendmail $encrypt_failed ? $email_unencrypted : $email
2f9e679a
MG
137}
138
1391;
140__END__
141
142=encoding utf-8
143
144=head1 NAME
145
146App::EdwardNG - GnuPG email sign/encrypt testing bot
147
148=head1 SYNOPSIS
149
150 use App::EdwardNG;
151 my ($status, %params) = process_message '/path/to/message';
152 if ($status eq 'signencrypt') {
153 say 'This message is encrypted and signed with key ', $params{keyid}, ' from ', $params{email};
154 say 'Its contents are: ', $params{plaintext};
155 } elsif ($status eq 'encrypt') {
156 say 'This message is encrypted but not signed';
157 say 'Its contents are: ', $params{plaintext};
158 } elsif ($status eq 'encrypt_error') {
159 say 'This message is encrypted but I was unable to decrypt it. GnuPG output: ', $params{message};
160 } elsif ($status eq 'sign') {
161 say 'This message is signed with key ', $params{keyid}, ' from ', $params{email};
162 } elsif ($status eq 'sign_error') {
163 say 'This message is signed but I was unable to verify the signature. GnuPG output: ', $params{message};
164 } elsif ($status eq 'plain') {
165 say 'This message is neither signed nor encrypted';
166 } elsif ($status eq 'error') {
167 say 'There was an error processing the message: ', $params{message};
168 }
169
170=head1 DESCRIPTION
171
172EdwardNG is a reimplementation of the Edward reply bot referenced in L<https://emailselfdefense.fsf.org/>.
173
174It takes mail messages, checks them for PGP signatures and encryption, then replies appropriately.
175
176This module exports a single function, B<process_message>, which takes a single parameter representing the message. This parameter can be:
177
178=over
179
180=item A filehandle reference, e.g. C<\*STDIN>.
181
182=item A reference to a scalar which holds the message contents.
183
184=item A scalar which represents a path to a message.
185
186=item A L<MIME::Entity> object
187
188=back
189
190The function returns a status followed by a hash. Possible results:
191
192=over
193
194=item plain
195
196The message is neither signed nor encrypted.
197
198=item sign_error, message => $message
199
200The message is signed but the signature could not be verified. GnuPG output is $message.
201
202=item sign, keyid => $keyid, email => $email, message => $message
203
204The message is signed with key $keyid from $email. GnuPG output is $message.
205
206=item encrypt_error, message => $message
207
208The message is encrypted and unable to be decrypted. GnuPG output is $message.
209
210=item encrypt, plaintext => $plaintext, decrypted => $decrypted, message => $message
211
212The message is encrypted and unsigned. $plaintext is the decrypted message as plain text, while $decrypted is a MIME::Entity representing the decrypted message. GnuPG output is $message.
213
214=item signencrypt, plaintext => $plaintext, decrypted => $decrypted, keyid => $keyid, email => $email, message => $message
215
216The message is encrypted and signed with key $keyid from $email. $plaintext is the decrypted message as plain text, while $decrypted is a MIME::Entity representing the decrypted message. GnuPG output is $message.
217
218=item error, message => $message
219
220There was an error while processing the message. The error can be found in $message.
221
222=back
223
224=head1 ENVIRONMENT
225
226This module is configured via the %ENV hash. See the L<edwardng(1)> manpage for more information.
227
228=head1 SEE ALSO
229
230L<edwardng(1)>
231
232=head1 AUTHOR
233
234Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
235
236=head1 COPYRIGHT AND LICENSE
237
1f08c352 238Copyright (C) 2014 by Fundația Ceata
2f9e679a
MG
239
240This library is free software; you can redistribute it and/or modify
241it under the same terms as Perl itself, either Perl version 5.18.2 or,
242at your option, any later version of Perl 5 you may have available.
243
244
245=cut
This page took 0.023315 seconds and 4 git commands to generate.