import of rt 3.0.4
[freeside.git] / rt / bin / rt-mailgate
1 #!/usr/bin/perl -w
2 # BEGIN LICENSE BLOCK
3 #
4 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 #
6 # (Except where explictly superceded by other copyright notices)
7 #
8 # This work is made available to you under the terms of Version 2 of
9 # the GNU General Public License. A copy of that license should have
10 # been provided with this software, but in any event can be snarfed
11 # from www.gnu.org.
12 #
13 # This work is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # General Public License for more details.
17 #
18 # Unless otherwise specified, all modifications, corrections or
19 # extensions to this work which alter its source code become the
20 # property of Best Practical Solutions, LLC when submitted for
21 # inclusion in the work.
22 #
23 #
24 # END LICENSE BLOCK
25
26 =head1 NAME
27
28 rt-mailgate - Mail interface to RT3.
29
30 =begin testing
31
32 use RT::I18N;
33
34
35 # {{{ Test new ticket creation by root who is privileged and superuser
36
37 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
38 print MAIL <<EOF;
39 From: root\@localhost
40 To: rt\@example.com
41 Subject: This is a test of new ticket creation
42
43 Blah!
44 Foob!
45 EOF
46 close (MAIL);
47
48 use RT::Tickets;
49 my $tickets = RT::Tickets->new($RT::SystemUser);
50 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
51 $tickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
52 my $tick = $tickets->First();
53 ok (UNIVERSAL::isa($tick,'RT::Ticket'));
54 ok ($tick->Id, "found ticket ".$tick->Id);
55 ok ($tick->Subject eq 'This is a test of new ticket creation', "Created the ticket");
56
57 # }}}
58
59
60 # {{{This is a test of new ticket creation as an unknown user
61
62 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
63 print MAIL <<EOF;
64 From: doesnotexist\@example.com
65 To: rt\@example.com
66 Subject: This is a test of new ticket creation as an unknown user
67
68 Blah!
69 Foob!
70 EOF
71 close (MAIL);
72
73 $tickets = RT::Tickets->new($RT::SystemUser);
74 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
75 $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
76 $tick = $tickets->First();
77 ok ($tick->Id, "found ticket ".$tick->Id);
78 ok ($tick->Subject ne 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account");
79 my $u = RT::User->new($RT::SystemUser);
80 $u->Load('doesnotexist@example.com');
81 ok( $u->Id == 0, " user does not exist and was not created by failed ticket submission");
82
83
84 # }}}
85
86 # {{{ now everybody can create tickets.  can a random unkown user create tickets?
87
88 my $g = RT::Group->new($RT::SystemUser);
89 $g->LoadSystemInternalGroup('Everyone');
90 ok( $g->Id, "Found 'everybody'");
91
92 my ($val,$msg) = $g->PrincipalObj->GrantRight(Right => 'CreateTicket');
93 ok ($val, "Granted everybody the right to create tickets - $msg");
94
95 sleep(60); # gotta sleep so the remote process' ACL cache times out
96
97 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
98 print MAIL <<EOF;
99 From: doesnotexist\@example.com
100 To: rt\@example.com
101 Subject: This is a test of new ticket creation as an unknown user
102
103 Blah!
104 Foob!
105 EOF
106 close (MAIL);
107
108
109 $tickets = RT::Tickets->new($RT::SystemUser);
110 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
111 $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
112 $tick = $tickets->First();
113 ok ($tick->Id, "found ticket ".$tick->Id);
114 ok ($tick->Subject eq 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account");
115 my $u = RT::User->new($RT::SystemUser);
116 $u->Load('doesnotexist@example.com');
117 ok( $u->Id != 0, " user does not exist and was created by ticket submission");
118
119 # }}}
120
121
122 # {{{  can another random reply to a ticket without being granted privs? answer should be no.
123
124
125 #($val,$msg) = $g->PrincipalObj->GrantRight(Right => 'CreateTicket');
126 #ok ($val, "Granted everybody the right to create tickets - $msg");
127 #sleep(60); # gotta sleep so the remote process' ACL cache times out
128
129 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
130 print MAIL <<EOF;
131 From: doesnotexist-2\@example.com
132 To: rt\@example.com
133 Subject: [example.com #@{[$tick->Id]}] This is a test of a reply as an unknown user
134
135 Blah!
136 Foob!
137 EOF
138 close (MAIL);
139
140 $u = RT::User->new($RT::SystemUser);
141 $u->Load('doesnotexist-2@example.com');
142 ok( $u->Id == 0, " user does not exist and was not created by ticket correspondence submission");
143 # }}}
144 # {{{  can another random reply to a ticket after being granted privs? answer should be yes
145
146
147 ($val,$msg) = $g->PrincipalObj->GrantRight(Right => 'ReplyToTicket');
148 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
149 sleep(60); # gotta sleep so the remote process' ACL cache times out
150
151 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
152 print MAIL <<EOF;
153 From: doesnotexist-2\@example.com
154 To: rt\@example.com
155 Subject: [example.com #@{[$tick->Id]}] This is a test of a reply as an unknown user
156
157 Blah!
158 Foob!
159 EOF
160 close (MAIL);
161
162
163 $u = RT::User->new($RT::SystemUser);
164 $u->Load('doesnotexist-2@example.com');
165 ok( $u->Id != 0, " user exists and was created by ticket correspondence submission");
166
167 # }}}
168
169 # {{{  can another random comment on a ticket without being granted privs? answer should be no.
170
171
172 #($val,$msg) = $g->PrincipalObj->GrantRight(Right => 'CreateTicket');
173 #ok ($val, "Granted everybody the right to create tickets - $msg");
174 #sleep(60); # gotta sleep so the remote process' ACL cache times out
175
176 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
177 print MAIL <<EOF;
178 From: doesnotexist-3\@example.com
179 To: rt\@example.com
180 Subject: [example.com #@{[$tick->Id]}] This is a test of a comment as an unknown user
181
182 Blah!
183 Foob!
184 EOF
185 close (MAIL);
186
187 $u = RT::User->new($RT::SystemUser);
188 $u->Load('doesnotexist-3@example.com');
189 ok( $u->Id == 0, " user does not exist and was not created by ticket comment submission");
190
191 # }}}
192 # {{{  can another random reply to a ticket after being granted privs? answer should be yes
193
194
195 ($val,$msg) = $g->PrincipalObj->GrantRight(Right => 'CommentOnTicket');
196 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
197 sleep(60); # gotta sleep so the remote process' ACL cache times out
198
199 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
200 print MAIL <<EOF;
201 From: doesnotexist-3\@example.com
202 To: rt\@example.com
203 Subject: [example.com #@{[$tick->Id]}] This is a test of a comment as an unknown user
204
205 Blah!
206 Foob!
207 EOF
208 close (MAIL);
209
210
211 $u = RT::User->new($RT::SystemUser);
212 $u->Load('doesnotexist-3@example.com');
213 ok( $u->Id != 0, " user exists and was created by ticket comment submission");
214
215 # }}}
216
217 # {{{ Testing preservation of binary attachments
218
219 # Get a binary blob (Best Practical logo) 
220
221 # Create a mime entity with an attachment
222
223 use MIME::Entity;
224 my $entity = MIME::Entity->build( From => 'root@localhost',
225                                  To => 'rt@localhost',
226                                 Subject => 'binary attachment test',
227                                 Data => ['This is a test of a binary attachment']);
228
229 # currently in lib/t/autogen
230 $entity->attach(Path => '../../../html/NoAuth/images/spacer.gif', 
231                 Type => 'image/gif',
232                 Encoding => 'base64');
233
234 # Create a ticket with a binary attachment
235 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
236
237 $entity->print(\*MAIL);
238
239 close (MAIL);
240
241 my $tickets = RT::Tickets->new($RT::SystemUser);
242 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
243 $tickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
244  $tick = $tickets->First();
245 ok (UNIVERSAL::isa($tick,'RT::Ticket'));
246 ok ($tick->Id, "found ticket ".$tick->Id);
247 ok ($tick->Subject eq 'binary attachment test', "Created the ticket - ".$tick->Id);
248
249 my $file = `cat ../../../html/NoAuth/images/spacer.gif`;
250 ok ($file, "Read in the logo image");
251
252
253         use Digest::MD5;
254 warn "for the raw file the content is ".Digest::MD5::md5_base64($file);
255
256
257
258 # Verify that the binary attachment is valid in the database
259 my $attachments = RT::Attachments->new($RT::SystemUser);
260 $attachments->Limit(FIELD => 'ContentType', VALUE => 'image/gif');
261 ok ($attachments->Count == 1, 'Found only one gif in the database');
262 my $attachment = $attachments->First;
263 my $acontent = $attachment->Content;
264
265         warn "coming from the  database, the content is ".Digest::MD5::md5_base64($acontent);
266
267 is( $acontent, $file, 'The attachment isn\'t screwed up in the database.');
268 # Log in as root
269 use Getopt::Long;
270 use LWP::UserAgent;
271
272
273 # Grab the binary attachment via the web ui
274 my $ua      = LWP::UserAgent->new();
275
276 my $full_url = "http://localhost/Ticket/Attachment/".$attachment->TransactionId."/".$attachment->id."/spacer.gif?&user=root&pass=password";
277 my $r = $ua->get( $full_url);
278
279
280 # Verify that the downloaded attachment is the same as what we uploaded.
281 is($file, $r->content, 'The attachment isn\'t screwed up in download');
282
283
284
285 # }}}
286
287 # {{{ Simple I18N testing
288
289 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
290                                                                          
291 print MAIL <<EOF;
292 From: root\@localhost
293 To: rtemail\@example.com
294 Subject: This is a test of I18N ticket creation
295 Content-Type: text/plain; charset="utf-8"
296
297 2 accented lines
298 \303\242\303\252\303\256\303\264\303\273
299 \303\241\303\251\303\255\303\263\303\272
300 bye
301 EOF
302 close (MAIL);
303
304 my $unitickets = RT::Tickets->new($RT::SystemUser);
305 $unitickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
306 $unitickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
307 my $unitick = $unitickets->First();
308 ok (UNIVERSAL::isa($unitick,'RT::Ticket'));
309 ok ($unitick->Id, "found ticket ".$unitick->Id);
310 ok ($unitick->Subject eq 'This is a test of I18N ticket creation', "Created the ticket - ". $unitick->Subject);
311
312
313
314 my $unistring = "\303\241\303\251\303\255\303\263\303\272";
315 Encode::_utf8_on($unistring);
316 is ($unitick->Transactions->First->Content, $unitick->Transactions->First->Attachments->First->Content, "Content is ". $unitick->Transactions->First->Attachments->First->Content);
317 ok($unitick->Transactions->First->Attachments->First->Content =~ /$unistring/i, $unitick->Id." appears to be unicode ". $unitick->Transactions->First->Attachments->First->Id);
318 # supposedly I18N fails on the second message sent in.
319
320 ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
321                                                                          
322 print MAIL <<EOF;
323 From: root\@localhost
324 To: rtemail\@example.com
325 Subject: This is a test of I18N ticket creation
326 Content-Type: text/plain; charset="utf-8"
327
328 2 accented lines
329 \303\242\303\252\303\256\303\264\303\273
330 \303\241\303\251\303\255\303\263\303\272
331 bye
332 EOF
333 close (MAIL);
334
335 my $tickets2 = RT::Tickets->new($RT::SystemUser);
336 $tickets2->OrderBy(FIELD => 'id', ORDER => 'DESC');
337 $tickets2->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
338 my $tick2 = $tickets2->First();
339 ok (UNIVERSAL::isa($tick2,'RT::Ticket'));
340 ok ($tick2->Id, "found ticket ".$tick2->Id);
341 ok ($tick2->Subject eq 'This is a test of I18N ticket creation', "Created the ticket");
342
343
344
345 my $unistring = "\303\241\303\251\303\255\303\263\303\272";
346 Encode::_utf8_on($unistring);
347
348 ok ($tick2->Transactions->First->Content =~ $unistring, "It appears to be unicode - ".$tick2->Transactions->First->Content);
349
350 # }}}
351
352
353 ($val,$msg) = $g->PrincipalObj->RevokeRight(Right => 'CreateTicket');
354 ok ($val, $msg);
355
356
357
358 =end testing
359
360 =cut
361
362
363 use strict;
364 use Getopt::Long;
365 use LWP::UserAgent;
366
367 use constant EX_TEMPFAIL => 75;
368
369 my %opts;
370 GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s" );
371
372 if ( $opts{help} ) {
373     require Pod::Usage;
374     import Pod::Usage;
375     pod2usage("RT Mail Gateway\n");
376     exit 1;    # Don't want to succeed if this is really an email!
377 }
378
379 for (qw(url)) {
380     die "$0 invoked improperly\n\nNo $_ provided to mail gateway!\n" unless $opts{$_};
381 }
382
383 undef $/;
384 my $message = <>;
385 my $ua      = LWP::UserAgent->new();
386 $ua->cookie_jar( { file => $opts{jar} } );
387
388 my %args = (
389     queue   => $opts{queue},
390     action  => $opts{action},
391     message => $message,
392     SessionType => 'REST',    # Surpress login box
393 );
394
395
396 if ($opts{'extension'}) {
397         $args{$opts{'extension'}} = $ENV{'EXTENSION'};
398 }
399
400 # Set up cookie here.
401
402 my $full_url = $opts{'url'}. "/REST/1.0/NoAuth/mail-gateway";
403 warn "Connecting to $full_url" if $opts{'debug'};
404
405
406
407 my $r = $ua->post( $full_url, {%args} );
408 check_failure($r);
409
410 my $content = $r->content;
411 warn $content if ($opts{debug});
412
413 if ( $content !~ /^(ok|not ok)/ ) {
414
415     # It's not the server's fault if the mail is bogus. We just want to know that
416     # *something* came out of the server.
417     die <<EOF
418 RT server error.
419
420 The RT server which handled your email did not behave as expected. It
421 said:
422
423 $content
424 EOF
425
426 }
427
428 sub check_failure {
429     my $r = shift;
430     return if $r->is_success();
431
432     # This ordinarily oughtn't to be able to happen, suggests a bug in RT.
433     # So only load these heavy modules when they're needed.
434     require HTML::TreeBuilder;
435     require HTML::FormatText;
436
437     my $error = $r->error_as_HTML;
438     my $tree  = HTML::TreeBuilder->new->parse($error);
439     $tree->eof;
440
441     # It'll be a cold day in hell before RT sends out bounces in HTML
442     my $formatter = HTML::FormatText->new( leftmargin  => 0,
443                                            rightmargin => 50 );
444     warn $formatter->format($tree);
445     warn "This is $0 exiting because of an undefined server error" if ($opts{debug});
446     exit EX_TEMPFAIL;
447 }
448
449
450 =head1 SYNOPSIS
451
452     rt-mailgate --help : this text
453
454 Usual invocation (from MTA):
455
456     rt-mailgate --action (correspond|comment) --queue queuename
457                 --url http://your.rt.server/
458                 [ --extension (queue|action|ticket)
459
460 See C<man rt-mailgate> for more.
461
462 =head1 OPTIONS
463
464 =over 3
465
466 =item C<--action>
467
468 Specifies whether this is a correspondence or comment address.
469
470 =item C<--queue>
471
472 Reflects which queue this address handles.
473
474 =item C<--url>
475
476 The location of the web server for your RT instance.
477
478
479 =item C<--extension> OPTIONAL
480
481 Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host
482 and present "foo" in the environment variable $EXTENSION. By specifying
483 the value "queue" for this parameter, the queue this message should be
484 submitted to will be set to the value of $EXTENSION. By specifying
485 "ticket", $EXTENSION will be interpreted as the id of the ticket this message
486 is related to.  "action" will allow the user to specify either "comment" or
487 "correspond" in the address extension.
488
489
490 =head1 DESCRIPTION
491
492 The RT mail gateway is the primary mechanism for communicating with RT
493 via email. This program simply directs the email to the RT web server,
494 which handles filing correspondence and sending out any required mail.
495 It is designed to be run as part of the mail delivery process, either
496 called directly by the MTA or C<procmail>, or in a F<.forward> or
497 equivalent.
498
499 =head1 SETUP
500
501 Much of the set up of the mail gateway depends on your MTA and mail
502 routing configuration. However, you will need first of all to create an
503 RT user for the mail gateway and assign it a password; this helps to
504 ensure that mail coming into the web server did originate from the
505 gateway.
506
507 Next, you need to route mail to C<rt-mailgate> for the queues you're
508 monitoring. For instance, if you're using F</etc/aliases> and you have a
509 "bugs" queue, you will want something like this:
510
511     bugs:         "|/opt/rt3/bin/rt-mailgate --queue bugs --action correspond
512               --url http://rt.mycorp.com/"
513
514     bugs-comment: "|/opt/rt3/bin/rt-mailgate --queue bugs --action comment
515               --url http://rt.mycorp.com/"
516
517 Note that you don't have to run your RT server on your mail server, as
518 the mail gateway will happily relay to a different machine.
519
520 =head1 CUSTOMIZATION
521
522 By default, the mail gateway will accept mail from anyone. However,
523 there are situations in which you will want to authenticate users
524 before allowing them to communicate with the system. You can do this
525 via a plug-in mechanism in the RT configuration.
526
527 You can set the array C<@RT::MailPlugins> to be a list of plugins. The
528 default plugin, if this is not given, is C<Auth::MailFrom> - that is,
529 authentication of the person is done based on the C<From> header of the
530 email. If you have additional filters or authentication mechanisms, you
531 can list them here and they will be called in order:
532
533     @RT::MailPlugins = (
534         "Filter::SpamAssassin",
535         "Auth::LDAP",
536         # ...
537     );
538
539 See the documentation for any additional plugins you have.
540
541 You may also put Perl subroutines into the C<@RT::MailPlugins> array, if
542 they behave as described below.
543
544 =head1 WRITING PLUGINS
545
546 What's actually going on in the above is that C<@RT::MailPlugins> is a
547 list of Perl modules; RT prepends C<RT::Interface::Email::> to the name,
548 to form a package name, and then C<use>'s this module. The module is
549 expected to provide a C<GetCurrentUser> subroutine, which takes a hash of
550 several parameters:
551
552 =over 4
553
554 =item Message
555
556 A C<MIME::Entity> object representing the email
557 =item CurrentUser
558
559 An C<RT::CurrentUser> object
560
561 =item AuthStat
562
563 The authentication level returned from the previous plugin.
564
565 =item Ticket [OPTIONAL]
566
567 The ticket under discussion
568
569 =item Queue [OPTIONAL]
570
571 If we don't already have a ticket id, we need to know which queue we're talking about
572
573 =item Action
574
575 The action being performed. At the moment, it's one of "comment" or "correspond"
576
577 =back 4
578
579 It returns two values, the new C<RT::CurrentUser> object, and the new
580 authentication level. The authentication level can be zero, not allowed
581 to communicate with RT at all, (a "permission denied" error is mailed to
582 the correspondent) or one, which is the normal mode of operation.
583 Additionally, if C<-1> is returned, then the processing of the plug-ins
584 stops immediately and the message is ignored.
585
586 =cut
587