freeside3 theme
[freeside.git] / rt / bin / rt-mailgate
1 #!/usr/bin/perl
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
8 #
9 # (Except where explicitly superseded by other copyright notices)
10 #
11 #
12 # LICENSE:
13 #
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18 #
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29 #
30 #
31 # CONTRIBUTION SUBMISSION POLICY:
32 #
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38 #
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47 #
48 # END BPS TAGGED BLOCK }}}
49 =head1 NAME
50
51 rt-mailgate - Mail interface to RT.
52
53 =cut
54
55 use strict;
56 use warnings;
57
58 use Getopt::Long;
59
60 my $opts = { };
61 GetOptions( $opts,   "queue=s", "action=s", "url=s",
62             "jar=s", "help",    "debug",    "extension=s",
63             "timeout=i", "verify-ssl!", "ca-file=s",
64           );
65
66 my $gateway = RT::Client::MailGateway->new();
67
68 $gateway->run($opts);
69
70 package RT::Client::MailGateway;
71
72 use LWP::UserAgent;
73 use HTTP::Request::Common qw($DYNAMIC_FILE_UPLOAD);
74 use File::Temp qw(tempfile tempdir);
75 $DYNAMIC_FILE_UPLOAD = 1;
76
77 use constant EX_TEMPFAIL => 75;
78 use constant BUFFER_SIZE => 8192;
79
80 sub new {
81     my $class = shift;
82     my $self = bless {}, $class;
83     return $self;
84 }
85
86 sub run {
87     my $self = shift;
88     my $opts = shift;
89
90     if ( $opts->{running_in_test_harness} ) {
91         $self->{running_in_test_harness} = 1;
92     }
93
94     $self->validate_cli_flags($opts);
95
96     my $ua          = $self->get_useragent($opts);
97     my $post_params = $self->setup_session($opts);
98     $self->upload_message( $ua => $post_params );
99     $self->exit_with_success();
100 }
101
102 sub exit_with_success {
103     my $self = shift;
104     if ( $self->{running_in_test_harness} ) {
105         return 1;
106     } else {
107         exit 0;
108     }
109 }
110
111 sub tempfail {
112     my $self = shift;
113     if ( $self->{running_in_test_harness} ) {
114         die "tempfail";
115     } else {
116
117         exit EX_TEMPFAIL;
118     }
119 }
120
121 sub permfail {
122     my $self = shift;
123     if ( $self->{running_in_test_harness} ) {
124         die "permfail";
125     } else {
126
127         exit 1;
128     }
129 }
130
131 sub validate_cli_flags {
132     my $self = shift;
133     my $opts = shift;
134     if ( $opts->{'help'} ) {
135         require Pod::Usage;
136         Pod::Usage::pod2usage( { verbose => 2 } );
137         return $self->permfail()
138             ;    # Don't want to succeed if this is really an email!
139     }
140
141     unless ( $opts->{'url'} ) {
142         print STDERR
143             "$0 invoked improperly\n\nNo 'url' provided to mail gateway!\n";
144         return $self->permfail();
145     }
146
147     if (($opts->{'ca-file'} or $opts->{"verify-ssl"})
148             and not LWP::UserAgent->can("ssl_opts")) {
149         print STDERR "Verifying SSL certificates requires LWP::UserAgent 6.0 or higher.\n";
150         return $self->tempfail();
151     }
152
153     $opts->{"verify-ssl"} = 1 unless defined $opts->{"verify-ssl"};
154 }
155
156 sub get_useragent {
157     my $self = shift;
158     my $opts = shift;
159     my $ua   = LWP::UserAgent->new();
160     $ua->cookie_jar( { file => $opts->{'jar'} } ) if $opts->{'jar'};
161
162     if ( $ua->can("ssl_opts") ) {
163         $ua->ssl_opts( verify_hostname => $opts->{'verify-ssl'} );
164         $ua->ssl_opts( SSL_ca_file => $opts->{'ca-file'} )
165             if $opts->{'ca-file'};
166     }
167
168     return $ua;
169 }
170
171 sub setup_session {
172     my $self = shift;
173     my $opts = shift;
174     my %post_params;
175     $post_params{SessionType} = 'REST';    # Surpress login box
176     foreach (qw(queue action)) {
177         $post_params{$_} = $opts->{$_} if defined $opts->{$_};
178     }
179
180     if ( ( $opts->{'extension'} || '' ) =~ /^(?:action|queue|ticket)$/i ) {
181         $post_params{ lc $opts->{'extension'} } = $ENV{'EXTENSION'}
182             || $opts->{ $opts->{'extension'} };
183     } elsif ( $opts->{'extension'} && $ENV{'EXTENSION'} ) {
184         print STDERR
185             "Value of the --extension argument is not action, queue or ticket"
186             . ", but environment variable EXTENSION is also defined. The former is ignored.\n";
187     }
188
189     # add ENV{'EXTENSION'} as X-RT-MailExtension to the message header
190     if ( my $value = ( $ENV{'EXTENSION'} || $opts->{'extension'} ) ) {
191
192         # prepare value to avoid MIME format breakage
193         # strip trailing newline symbols
194         $value =~ s/(\r*\n)+$//;
195
196         # make a correct multiline header field,
197         # with tabs in the beginning of each line
198         $value =~ s/(\r*\n)/$1\t/g;
199         $opts->{'headers'} .= "X-RT-Mail-Extension: $value\n";
200     }
201
202     # Read the message in from STDIN
203     # _raw_message is used for testing
204     my $message = $opts->{'_raw_message'} || $self->slurp_message();
205     unless ( $message->{'filename'} ) {
206         $post_params{'message'} = [
207                                  undef, '',
208                                  'Content-Type' => 'application/octet-stream',
209                                  Content        => ${ $message->{'content'} },
210         ];
211     } else {
212         $post_params{'message'} = [
213                                  $message->{'filename'}, '',
214                                  'Content-Type' => 'application/octet-stream',
215         ];
216     }
217
218     return \%post_params;
219 }
220
221 sub upload_message {
222     my $self        = shift;
223     my $ua          = shift;
224     my $post_params = shift;
225     my $full_url    = $opts->{'url'} . "/REST/1.0/NoAuth/mail-gateway";
226     print STDERR "$0: connecting to $full_url\n" if $opts->{'debug'};
227
228     $ua->timeout( exists( $opts->{'timeout'} ) ? $opts->{'timeout'} : 180 );
229     my $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' );
230     $self->check_failure($r);
231
232     my $content = $r->content;
233     print STDERR $content . "\n" if $opts->{'debug'};
234
235     return if ( $content =~ /^(ok|not ok)/ );
236
237  # It's not the server's fault if the mail is bogus. We just want to know that
238  # *something* came out of the server.
239     print STDERR <<EOF;
240 RT server error.
241
242 The RT server which handled your email did not behave as expected. It
243 said:
244
245 $content
246 EOF
247
248     return $self->tempfail();
249 }
250
251 sub check_failure {
252     my $self = shift;
253     my $r    = shift;
254     return if $r->is_success;
255
256     # This ordinarily oughtn't to be able to happen, suggests a bug in RT.
257     # So only load these heavy modules when they're needed.
258     require HTML::TreeBuilder;
259     require HTML::FormatText;
260
261     my $error = $r->error_as_HTML;
262     my $tree  = HTML::TreeBuilder->new->parse($error);
263     $tree->eof;
264
265     # It'll be a cold day in hell before RT sends out bounces in HTML
266     my $formatter =
267         HTML::FormatText->new( leftmargin  => 0,
268                                rightmargin => 50, );
269     print STDERR $formatter->format($tree);
270     print STDERR "\n$0: undefined server error\n" if $opts->{'debug'};
271     return $self->tempfail();
272 }
273
274 sub slurp_message {
275     my $self = shift;
276
277     local $@;
278
279     my %message;
280     my ( $fh, $filename )
281         = eval { tempfile( DIR => tempdir( CLEANUP => 1 ) ) };
282     if ( !$fh || $@ ) {
283         print STDERR "$0: Couldn't create temp file, using memory\n";
284         print STDERR "error: $@\n" if $@;
285
286         my $message = \do { local ( @ARGV, $/ ); <STDIN> };
287         unless ( $$message =~ /\S/ ) {
288             print STDERR "$0: no message passed on STDIN\n";
289             $self->exit_with_success;
290         }
291         $$message = $opts->{'headers'} . $$message if $opts->{'headers'};
292         return ( { content => $message } );
293     }
294
295     binmode $fh;
296     binmode \*STDIN;
297
298     print $fh $opts->{'headers'} if $opts->{'headers'};
299
300     my $buf;
301     my $empty = 1;
302     while (1) {
303         my $status = read \*STDIN, $buf, BUFFER_SIZE;
304         unless ( defined $status ) {
305             print STDERR "$0: couldn't read message: $!\n";
306             return $self->tempfail();
307         } elsif ( !$status ) {
308             last;
309         }
310         $empty = 0 if $buf =~ /\S/;
311         print $fh $buf;
312     }
313     close $fh;
314
315     if ($empty) {
316         print STDERR "$0: no message passed on STDIN\n";
317         $self->exit_with_success;
318     }
319     print STDERR "$0: temp file is '$filename'\n" if $opts->{'debug'};
320     return ( { filename => $filename } );
321 }
322
323 =head1 SYNOPSIS
324
325     rt-mailgate --help : this text
326
327 Usual invocation (from MTA):
328
329     rt-mailgate --action (correspond|comment|...) --queue queuename
330                 --url http://your.rt.server/
331                 [ --debug ]
332                 [ --extension (queue|action|ticket) ]
333                 [ --timeout seconds ]
334
335
336
337 =head1 OPTIONS
338
339 =over 3
340
341 =item C<--action>
342
343 Specifies what happens to email sent to this alias.  The avaliable
344 basic actions are: C<correspond>, C<comment>.
345
346
347 If you've set the RT configuration variable B<< C<UnsafeEmailCommands> >>,
348 C<take> and C<resolve> are also available.  You can execute two or more
349 actions on a single message using a C<-> separated list.  RT will execute
350 the actions in the listed order.  For example you can use C<take-comment>,
351 C<correspond-resolve> or C<take-comment-resolve> as actions.
352
353 Note that C<take> and C<resolve> actions ignore message text if used
354 alone.  Include a  C<comment> or C<correspond> action if you want RT
355 to record the incoming message.
356
357 The default action is C<correspond>.
358
359 =item C<--queue>
360
361 This flag determines which queue this alias should create a ticket in if no ticket identifier
362 is found.
363
364 =item C<--url>
365
366 This flag tells the mail gateway where it can find your RT server. You should 
367 probably use the same URL that users use to log into RT.  
368
369 If your RT server uses SSL, you will need to install additional Perl
370 libraries. RT will detect and install these dependencies if you pass the
371 C<--enable-ssl-mailgate> flag to configure as documented in RT's README.
372
373 If you have a self-signed SSL certificate, you may also need to pass
374 C<--ca-file> or C<--no-verify-ssl>, below.
375
376 =item C<--ca-file> I<path>
377
378 Specifies the path to the public SSL certificate for the certificate
379 authority that should be used to verify the website's SSL certificate.
380 If your webserver uses a self-signed certificate, you should
381 preferentially use this option over C<--no-verify-ssl>, as it will
382 ensure that the self-signed certificate that the mailgate is seeing the
383 I<right> self-signed certificate.
384
385 =item C<--no-verify-ssl>
386
387 This flag tells the mail gateway to trust all SSL certificates,
388 regardless of if their hostname matches the certificate, and regardless
389 of CA.  This is required if you have a self-signed certificate, or some
390 other certificate which is not traceable back to an certificate your
391 system ultimitely trusts.
392
393 Verifying SSL certificates requires L<LWP::UserAgent> version 6.0 or
394 higher; explicitly passing C<--verify-ssl> on prior versions will error.
395
396 =item C<--extension> OPTIONAL
397
398 Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host
399 and present "foo" in the environment variable $EXTENSION. By specifying
400 the value "queue" for this parameter, the queue this message should be
401 submitted to will be set to the value of $EXTENSION. By specifying
402 "ticket", $EXTENSION will be interpreted as the id of the ticket this message
403 is related to.  "action" will allow the user to specify either "comment" or
404 "correspond" in the address extension.
405
406 =item C<--debug> OPTIONAL
407
408 Print debugging output to standard error
409
410
411 =item C<--timeout> OPTIONAL
412
413 Configure the timeout for posting the message to the web server.  The
414 default timeout is 3 minutes (180 seconds).
415
416 =back
417
418
419 =head1 DESCRIPTION
420
421 The RT mail gateway is the primary mechanism for communicating with RT
422 via email. This program simply directs the email to the RT web server,
423 which handles filing correspondence and sending out any required mail.
424 It is designed to be run as part of the mail delivery process, either
425 called directly by the MTA or C<procmail>, or in a F<.forward> or
426 equivalent.
427
428 =head1 SETUP
429
430 Much of the set up of the mail gateway depends on your MTA and mail
431 routing configuration. However, you will need first of all to create an
432 RT user for the mail gateway and assign it a password; this helps to
433 ensure that mail coming into the web server did originate from the
434 gateway.
435
436 Next, you need to route mail to C<rt-mailgate> for the queues you're
437 monitoring. For instance, if you're using F</etc/aliases> and you have a
438 "bugs" queue, you will want something like this:
439
440     bugs:         "|/opt/rt4/bin/rt-mailgate --queue bugs --action correspond
441               --url http://rt.mycorp.com/"
442
443     bugs-comment: "|/opt/rt4/bin/rt-mailgate --queue bugs --action comment
444               --url http://rt.mycorp.com/"
445
446 Note that you don't have to run your RT server on your mail server, as
447 the mail gateway will happily relay to a different machine.
448
449 =head1 CUSTOMIZATION
450
451 By default, the mail gateway will accept mail from anyone. However,
452 there are situations in which you will want to authenticate users
453 before allowing them to communicate with the system. You can do this
454 via a plug-in mechanism in the RT configuration.
455
456 You can set the array C<@MailPlugins> to be a list of plugins. The
457 default plugin, if this is not given, is C<Auth::MailFrom> - that is,
458 authentication of the person is done based on the C<From> header of the
459 email. If you have additional filters or authentication mechanisms, you
460 can list them here and they will be called in order:
461
462     Set( @MailPlugins =>
463         "Filter::SpamAssassin",
464         "Auth::LDAP",
465         # ...
466     );
467
468 See the documentation for any additional plugins you have.
469
470 You may also put Perl subroutines into the C<@MailPlugins> array, if
471 they behave as described below.
472
473 =head1 WRITING PLUGINS
474
475 What's actually going on in the above is that C<@MailPlugins> is a
476 list of Perl modules; RT prepends C<RT::Interface::Email::> to the name,
477 to form a package name, and then C<use>'s this module. The module is
478 expected to provide a C<GetCurrentUser> subroutine, which takes a hash of
479 several parameters:
480
481 =over 4
482
483 =item Message
484
485 A C<MIME::Entity> object representing the email
486
487 =item CurrentUser
488
489 An C<RT::CurrentUser> object
490
491 =item AuthStat
492
493 The authentication level returned from the previous plugin.
494
495 =item Ticket [OPTIONAL]
496
497 The ticket under discussion
498
499 =item Queue [OPTIONAL]
500
501 If we don't already have a ticket id, we need to know which queue we're talking about
502
503 =item Action
504
505 The action being performed. At the moment, it's one of "comment" or "correspond"
506
507 =back
508
509 It returns two values, the new C<RT::CurrentUser> object, and the new
510 authentication level. The authentication level can be zero, not allowed
511 to communicate with RT at all, (a "permission denied" error is mailed to
512 the correspondent) or one, which is the normal mode of operation.
513 Additionally, if C<-1> is returned, then the processing of the plug-ins
514 stops immediately and the message is ignored.
515
516 =head1 ENVIRONMENT
517
518 =over 4
519
520 =item EXTENSION
521
522 Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host
523 and present "foo" in the environment variable C<EXTENSION>. Mailgate adds value
524 of this variable to message in the C<X-RT-Mail-Extension> field of the message
525 header.
526
527 See also C<--extension> option. Note that value of the environment variable is
528 always added to the message header when it's not empty even if C<--extension>
529 option is not provided.
530
531 =back
532
533 =cut
534