086ba5fe4c6ffd34aaeac59ee1aaf7ef0825ae68
[usit-rt.git] / lib / RT / Test / GnuPG.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Test::GnuPG;
50 use strict;
51 use warnings;
52 use Test::More;
53 use base qw(RT::Test);
54 use File::Temp qw(tempdir);
55
56 our @EXPORT =
57   qw(create_a_ticket update_ticket cleanup_headers set_queue_crypt_options 
58           check_text_emails send_email_and_check_transaction
59           create_and_test_outgoing_emails
60           );
61
62 sub import {
63     my $class = shift;
64     my %args  = @_;
65     my $t     = $class->builder;
66
67     $t->plan( skip_all => 'GnuPG required.' )
68       unless eval { require GnuPG::Interface; 1 };
69     $t->plan( skip_all => 'gpg executable is required.' )
70       unless RT::Test->find_executable('gpg');
71
72     $class->SUPER::import(%args);
73     require RT::Crypt::GnuPG;
74
75     RT::Test::diag "GnuPG --homedir " . RT->Config->Get('GnuPGOptions')->{'homedir'};
76
77     $class->set_rights(
78         Principal => 'Everyone',
79         Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'],
80     );
81
82     $class->export_to_level(1);
83 }
84
85 sub bootstrap_more_config {
86     my $self = shift;
87     my $handle = shift;
88     my $args = shift;
89
90     $self->SUPER::bootstrap_more_config($handle, $args, @_);
91
92     my %gnupg_options = (
93         'no-permission-warning' => undef,
94         $args->{gnupg_options} ? %{ $args->{gnupg_options} } : (),
95     );
96     $gnupg_options{homedir} ||= scalar tempdir( CLEANUP => 1 );
97
98     use Data::Dumper;
99     local $Data::Dumper::Terse = 1; # "{...}" instead of "$VAR1 = {...};"
100     my $dumped_gnupg_options = Dumper(\%gnupg_options);
101
102     print $handle qq{
103 Set(\%GnuPG, (
104     Enable                 => 1,
105     OutgoingMessagesFormat => 'RFC',
106 ));
107 Set(\%GnuPGOptions => \%{ $dumped_gnupg_options });
108 Set(\@MailPlugins => qw(Auth::MailFrom Auth::GnuPG));
109 };
110
111 }
112
113 sub create_a_ticket {
114     my $queue = shift;
115     my $mail = shift;
116     my $m = shift;
117     my %args = (@_);
118
119     RT::Test->clean_caught_mails;
120
121     $m->goto_create_ticket( $queue );
122     $m->form_name('TicketCreate');
123     $m->field( Subject    => 'test' );
124     $m->field( Requestors => 'rt-test@example.com' );
125     $m->field( Content    => 'Some content' );
126
127     foreach ( qw(Sign Encrypt) ) {
128         if ( $args{ $_ } ) {
129             $m->tick( $_ => 1 );
130         } else {
131             $m->untick( $_ => 1 );
132         }
133     }
134
135     $m->submit;
136     is $m->status, 200, "request successful";
137
138     $m->content_lacks("unable to sign outgoing email messages");
139
140
141     my @mail = RT::Test->fetch_caught_mails;
142     check_text_emails(\%args, @mail );
143     categorize_emails($mail, \%args, @mail );
144 }
145
146 sub update_ticket {
147     my $tid = shift;
148     my $mail = shift;
149     my $m = shift;
150     my %args = (@_);
151
152     RT::Test->clean_caught_mails;
153
154     $m->get( $m->rt_base_url . "/Ticket/Update.html?Action=Respond&id=$tid" );
155     $m->form_number(3);
156     $m->field( UpdateContent => 'Some content' );
157
158     foreach ( qw(Sign Encrypt) ) {
159         if ( $args{ $_ } ) {
160             $m->tick( $_ => 1 );
161         } else {
162             $m->untick( $_ => 1 );
163         }
164     }
165
166     $m->click('SubmitTicket');
167     is $m->status, 200, "request successful";
168     $m->content_contains("Message recorded", 'Message recorded') or diag $m->content;
169
170
171     my @mail = RT::Test->fetch_caught_mails;
172     check_text_emails(\%args, @mail );
173     categorize_emails($mail, \%args, @mail );
174 }
175
176 sub categorize_emails {
177     my $mail = shift;
178     my $args = shift;
179     my @mail = @_;
180
181     if ( $args->{'Sign'} && $args->{'Encrypt'} ) {
182         push @{ $mail->{'signed_encrypted'} }, @mail;
183     }
184     elsif ( $args->{'Sign'} ) {
185         push @{ $mail->{'signed'} }, @mail;
186     }
187     elsif ( $args->{'Encrypt'} ) {
188         push @{ $mail->{'encrypted'} }, @mail;
189     }
190     else {
191         push @{ $mail->{'plain'} }, @mail;
192     }
193 }
194
195 sub check_text_emails {
196     my %args = %{ shift @_ };
197     my @mail = @_;
198
199     ok scalar @mail, "got some mail";
200     for my $mail (@mail) {
201         for my $type ('email', 'attachment') {
202             next if $type eq 'attachment' && !$args{'Attachment'};
203
204             my $content = $type eq 'email'
205                         ? "Some content"
206                         : "Attachment content";
207
208             if ( $args{'Encrypt'} ) {
209                 unlike $mail, qr/$content/, "outgoing $type was encrypted";
210             } else {
211                 like $mail, qr/$content/, "outgoing $type was not encrypted";
212             } 
213
214             next unless $type eq 'email';
215
216             if ( $args{'Sign'} && $args{'Encrypt'} ) {
217                 like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed';
218             } elsif ( $args{'Sign'} ) {
219                 like $mail, qr/SIGNATURE/, 'outgoing email was signed';
220             } else {
221                 unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed';
222             }
223         }
224     }
225 }
226
227 sub cleanup_headers {
228     my $mail = shift;
229     # strip id from subject to create new ticket
230     $mail =~ s/^(Subject:)\s*\[.*?\s+#\d+\]\s*/$1 /m;
231     # strip several headers
232     foreach my $field ( qw(Message-ID X-RT-Original-Encoding RT-Originator RT-Ticket X-RT-Loop-Prevention) ) {
233         $mail =~ s/^$field:.*?\n(?! |\t)//gmsi;
234     }
235     return $mail;
236 }
237
238 sub set_queue_crypt_options {
239     my $queue = shift;
240     my %args = @_;
241     $queue->SetEncrypt($args{'Encrypt'});
242     $queue->SetSign($args{'Sign'});
243 }
244
245 sub send_email_and_check_transaction {
246     my $mail = shift;
247     my $type = shift;
248
249     my ( $status, $id ) = RT::Test->send_via_mailgate($mail);
250     is( $status >> 8, 0, "The mail gateway exited normally" );
251     ok( $id, "got id of a newly created ticket - $id" );
252
253     my $tick = RT::Ticket->new( RT->SystemUser );
254     $tick->Load($id);
255     ok( $tick->id, "loaded ticket #$id" );
256
257     my $txn = $tick->Transactions->First;
258     my ( $msg, @attachments ) = @{ $txn->Attachments->ItemsArrayRef };
259
260     if ( $attachments[0] ) {
261         like $attachments[0]->Content, qr/Some content/,
262           "RT's mail includes copy of ticket text";
263     }
264     else {
265         like $msg->Content, qr/Some content/,
266           "RT's mail includes copy of ticket text";
267     }
268
269     if ( $type eq 'plain' ) {
270         ok !$msg->GetHeader('X-RT-Privacy'), "RT's outgoing mail has no crypto";
271         is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted',
272           "RT's outgoing mail looks not encrypted";
273         ok !$msg->GetHeader('X-RT-Incoming-Signature'),
274           "RT's outgoing mail looks not signed";
275     }
276     elsif ( $type eq 'signed' ) {
277         is $msg->GetHeader('X-RT-Privacy'), 'PGP',
278           "RT's outgoing mail has crypto";
279         is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted',
280           "RT's outgoing mail looks not encrypted";
281         like $msg->GetHeader('X-RT-Incoming-Signature'),
282           qr/<rt-recipient\@example.com>/,
283           "RT's outgoing mail looks signed";
284     }
285     elsif ( $type eq 'encrypted' ) {
286         is $msg->GetHeader('X-RT-Privacy'), 'PGP',
287           "RT's outgoing mail has crypto";
288         is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success',
289           "RT's outgoing mail looks encrypted";
290         ok !$msg->GetHeader('X-RT-Incoming-Signature'),
291           "RT's outgoing mail looks not signed";
292
293     }
294     elsif ( $type eq 'signed_encrypted' ) {
295         is $msg->GetHeader('X-RT-Privacy'), 'PGP',
296           "RT's outgoing mail has crypto";
297         is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success',
298           "RT's outgoing mail looks encrypted";
299         like $msg->GetHeader('X-RT-Incoming-Signature'),
300           qr/<rt-recipient\@example.com>/,
301           "RT's outgoing mail looks signed";
302     }
303     else {
304         die "unknown type: $type";
305     }
306 }
307
308 sub create_and_test_outgoing_emails {
309     my $queue = shift;
310     my $m     = shift;
311     my @variants =
312       ( {}, { Sign => 1 }, { Encrypt => 1 }, { Sign => 1, Encrypt => 1 }, );
313
314     # collect emails
315     my %mail;
316
317     # create a ticket for each combination
318     foreach my $ticket_set (@variants) {
319         create_a_ticket( $queue, \%mail, $m, %$ticket_set );
320     }
321
322     my $tid;
323     {
324         my $ticket = RT::Ticket->new( RT->SystemUser );
325         ($tid) = $ticket->Create(
326             Subject   => 'test',
327             Queue     => $queue->id,
328             Requestor => 'rt-test@example.com',
329         );
330         ok $tid, 'ticket created';
331     }
332
333     # again for each combination add a reply message
334     foreach my $ticket_set (@variants) {
335         update_ticket( $tid, \%mail, $m, %$ticket_set );
336     }
337
338 # ------------------------------------------------------------------------------
339 # now delete all keys from the keyring and put back secret/pub pair for rt-test@
340 # and only public key for rt-recipient@ so we can verify signatures and decrypt
341 # like we are on another side recieve emails
342 # ------------------------------------------------------------------------------
343
344     unlink $_
345       foreach glob( RT->Config->Get('GnuPGOptions')->{'homedir'} . "/*" );
346     RT::Test->import_gnupg_key( 'rt-recipient@example.com', 'public' );
347     RT::Test->import_gnupg_key('rt-test@example.com');
348
349     $queue = RT::Test->load_or_create_queue(
350         Name              => 'Regression',
351         CorrespondAddress => 'rt-test@example.com',
352         CommentAddress    => 'rt-test@example.com',
353     );
354     ok $queue && $queue->id, 'changed props of the queue';
355
356     for my $type ( keys %mail ) {
357         for my $mail ( map cleanup_headers($_), @{ $mail{$type} } ) {
358             send_email_and_check_transaction( $mail, $type );
359         }
360     }
361 }