-#!/usr/bin/perl -w
+#!/usr/bin/perl
# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
-#
+#
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
# (Except where explicitly superseded by other copyright notices)
-#
-#
+#
+#
# LICENSE:
-#
+#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
-#
+#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
-#
+#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#
-#
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
# CONTRIBUTION SUBMISSION POLICY:
-#
+#
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
-#
+#
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
-#
+#
# END BPS TAGGED BLOCK }}}
-
=head1 NAME
-rt-mailgate - Mail interface to RT3.
+rt-mailgate - Mail interface to RT.
=cut
-
use strict;
+use warnings;
+
use Getopt::Long;
+
+my $opts = { };
+GetOptions( $opts, "queue=s", "action=s", "url=s",
+ "jar=s", "help", "debug", "extension=s",
+ "timeout=i", "verify-ssl!", "ca-file=s",
+ );
+
+my $gateway = RT::Client::MailGateway->new();
+
+$gateway->run($opts);
+
+package RT::Client::MailGateway;
+
use LWP::UserAgent;
+use HTTP::Request::Common qw($DYNAMIC_FILE_UPLOAD);
+use File::Temp qw(tempfile tempdir);
+$DYNAMIC_FILE_UPLOAD = 1;
use constant EX_TEMPFAIL => 75;
+use constant BUFFER_SIZE => 8192;
+
+sub new {
+ my $class = shift;
+ my $self = bless {}, $class;
+ return $self;
+}
+
+sub run {
+ my $self = shift;
+ my $opts = shift;
+
+ if ( $opts->{running_in_test_harness} ) {
+ $self->{running_in_test_harness} = 1;
+ }
-my %opts;
-GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s", "timeout=i" );
+ $self->validate_cli_flags($opts);
-if ( $opts{help} ) {
- require Pod::Usage;
- import Pod::Usage;
- pod2usage("RT Mail Gateway\n");
- exit 1; # Don't want to succeed if this is really an email!
+ my $ua = $self->get_useragent($opts);
+ my $post_params = $self->setup_session($opts);
+ $self->upload_message( $ua => $post_params );
+ $self->exit_with_success();
}
-for (qw(url)) {
- die "$0 invoked improperly\n\nNo $_ provided to mail gateway!\n" unless $opts{$_};
+sub exit_with_success {
+ my $self = shift;
+ if ( $self->{running_in_test_harness} ) {
+ return 1;
+ } else {
+ exit 0;
+ }
}
-my $ua = LWP::UserAgent->new();
-$ua->cookie_jar( { file => $opts{jar} } );
+sub tempfail {
+ my $self = shift;
+ if ( $self->{running_in_test_harness} ) {
+ die "tempfail";
+ } else {
-my %args = (
- queue => $opts{queue},
- action => $opts{action},
- SessionType => 'REST', # Surpress login box
-);
+ exit EX_TEMPFAIL;
+ }
+}
-# Read the message in from STDIN
-$args{'message'} = do { local (@ARGV, $/); <> };
+sub permfail {
+ my $self = shift;
+ if ( $self->{running_in_test_harness} ) {
+ die "permfail";
+ } else {
-unless ( $args{message} =~ /\S/ ) {
- print STDERR "$0: no message passed on STDIN!\n";
- exit 0;
+ exit 1;
+ }
}
-if ($opts{'extension'}) {
- $args{$opts{'extension'}} = $ENV{'EXTENSION'};
+sub validate_cli_flags {
+ my $self = shift;
+ my $opts = shift;
+ if ( $opts->{'help'} ) {
+ require Pod::Usage;
+ Pod::Usage::pod2usage( { verbose => 2 } );
+ return $self->permfail()
+ ; # Don't want to succeed if this is really an email!
+ }
+
+ unless ( $opts->{'url'} ) {
+ print STDERR
+ "$0 invoked improperly\n\nNo 'url' provided to mail gateway!\n";
+ return $self->permfail();
+ }
+
+ if (($opts->{'ca-file'} or $opts->{"verify-ssl"})
+ and not LWP::UserAgent->can("ssl_opts")) {
+ print STDERR "Verifying SSL certificates requires LWP::UserAgent 6.0 or higher.\n";
+ return $self->tempfail();
+ }
+
+ $opts->{"verify-ssl"} = 1 unless defined $opts->{"verify-ssl"};
}
-# Set up cookie here.
+sub get_useragent {
+ my $self = shift;
+ my $opts = shift;
+ my $ua = LWP::UserAgent->new();
+ $ua->cookie_jar( { file => $opts->{'jar'} } ) if $opts->{'jar'};
-my $full_url = $opts{'url'}. "/REST/1.0/NoAuth/mail-gateway";
-warn "Connecting to $full_url" if $opts{'debug'};
+ if ( $ua->can("ssl_opts") ) {
+ $ua->ssl_opts( verify_hostname => $opts->{'verify-ssl'} );
+ $ua->ssl_opts( SSL_ca_file => $opts->{'ca-file'} )
+ if $opts->{'ca-file'};
+ }
+ return $ua;
+}
+sub setup_session {
+ my $self = shift;
+ my $opts = shift;
+ my %post_params;
+ foreach (qw(queue action)) {
+ $post_params{$_} = $opts->{$_} if defined $opts->{$_};
+ }
+
+ if ( ( $opts->{'extension'} || '' ) =~ /^(?:action|queue|ticket)$/i ) {
+ $post_params{ lc $opts->{'extension'} } = $ENV{'EXTENSION'}
+ || $opts->{ $opts->{'extension'} };
+ } elsif ( $opts->{'extension'} && $ENV{'EXTENSION'} ) {
+ print STDERR
+ "Value of the --extension argument is not action, queue or ticket"
+ . ", but environment variable EXTENSION is also defined. The former is ignored.\n";
+ }
+
+ # add ENV{'EXTENSION'} as X-RT-MailExtension to the message header
+ if ( my $value = ( $ENV{'EXTENSION'} || $opts->{'extension'} ) ) {
+
+ # prepare value to avoid MIME format breakage
+ # strip trailing newline symbols
+ $value =~ s/(\r*\n)+$//;
+
+ # make a correct multiline header field,
+ # with tabs in the beginning of each line
+ $value =~ s/(\r*\n)/$1\t/g;
+ $opts->{'headers'} .= "X-RT-Mail-Extension: $value\n";
+ }
+
+ # Read the message in from STDIN
+ # _raw_message is used for testing
+ my $message = $opts->{'_raw_message'} || $self->slurp_message();
+ unless ( $message->{'filename'} ) {
+ $post_params{'message'} = [
+ undef, '',
+ 'Content-Type' => 'application/octet-stream',
+ Content => ${ $message->{'content'} },
+ ];
+ } else {
+ $post_params{'message'} = [
+ $message->{'filename'}, '',
+ 'Content-Type' => 'application/octet-stream',
+ ];
+ }
+
+ return \%post_params;
+}
+
+sub upload_message {
+ my $self = shift;
+ my $ua = shift;
+ my $post_params = shift;
+ my $full_url = $opts->{'url'} . "/REST/1.0/NoAuth/mail-gateway";
+ print STDERR "$0: connecting to $full_url\n" if $opts->{'debug'};
-$ua->timeout(exists($opts{'timeout'}) ? $opts{'timeout'} : 180);
-my $r = $ua->post( $full_url, {%args} );
-check_failure($r);
+ $ua->timeout( exists( $opts->{'timeout'} ) ? $opts->{'timeout'} : 180 );
+ my $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' );
+ $self->check_failure($r);
-my $content = $r->content;
-warn $content if ($opts{debug});
+ my $content = $r->content;
+ print STDERR $content . "\n" if $opts->{'debug'};
-if ( $content !~ /^(ok|not ok)/ ) {
+ return if ( $content =~ /^(ok|not ok)/ );
- # It's not the server's fault if the mail is bogus. We just want to know that
- # *something* came out of the server.
- warn <<EOF;
+ # It's not the server's fault if the mail is bogus. We just want to know that
+ # *something* came out of the server.
+ print STDERR <<EOF;
RT server error.
The RT server which handled your email did not behave as expected. It
$content
EOF
-exit EX_TEMPFAIL;
-
+ return $self->tempfail();
}
-exit;
-
-
sub check_failure {
- my $r = shift;
- return if $r->is_success();
-
- # This ordinarily oughtn't to be able to happen, suggests a bug in RT.
- # So only load these heavy modules when they're needed.
- require HTML::TreeBuilder;
- require HTML::FormatText;
-
- my $error = $r->error_as_HTML;
- my $tree = HTML::TreeBuilder->new->parse($error);
- $tree->eof;
-
- # It'll be a cold day in hell before RT sends out bounces in HTML
- my $formatter = HTML::FormatText->new( leftmargin => 0,
- rightmargin => 50 );
- warn $formatter->format($tree);
- warn "This is $0 exiting because of an undefined server error" if ($opts{debug});
- exit EX_TEMPFAIL;
+ my $self = shift;
+ my $r = shift;
+ return if $r->is_success;
+
+ # XXX TODO 4.2: Remove the multi-line error strings in favor of something more concise
+ print STDERR <<" ERROR";
+An Error Occurred
+=================
+
+@{[ $r->status_line ]}
+ ERROR
+ print STDERR "\n$0: undefined server error\n" if $opts->{'debug'};
+ return $self->tempfail();
}
+sub slurp_message {
+ my $self = shift;
+
+ local $@;
+
+ my %message;
+ my ( $fh, $filename )
+ = eval { tempfile( DIR => tempdir( CLEANUP => 1 ) ) };
+ if ( !$fh || $@ ) {
+ print STDERR "$0: Couldn't create temp file, using memory\n";
+ print STDERR "error: $@\n" if $@;
+
+ my $message = \do { local ( @ARGV, $/ ); <STDIN> };
+ unless ( $$message =~ /\S/ ) {
+ print STDERR "$0: no message passed on STDIN\n";
+ $self->exit_with_success;
+ }
+ $$message = $opts->{'headers'} . $$message if $opts->{'headers'};
+ return ( { content => $message } );
+ }
+
+ binmode $fh;
+ binmode \*STDIN;
+
+ print $fh $opts->{'headers'} if $opts->{'headers'};
+
+ my $buf;
+ my $empty = 1;
+ while (1) {
+ my $status = read \*STDIN, $buf, BUFFER_SIZE;
+ unless ( defined $status ) {
+ print STDERR "$0: couldn't read message: $!\n";
+ return $self->tempfail();
+ } elsif ( !$status ) {
+ last;
+ }
+ $empty = 0 if $buf =~ /\S/;
+ print $fh $buf;
+ }
+ close $fh;
+
+ if ($empty) {
+ print STDERR "$0: no message passed on STDIN\n";
+ $self->exit_with_success;
+ }
+ print STDERR "$0: temp file is '$filename'\n" if $opts->{'debug'};
+ return ( { filename => $filename } );
+}
=head1 SYNOPSIS
-See C<man rt-mailgate> for more.
-
=head1 OPTIONS
=over 3
basic actions are: C<correspond>, C<comment>.
-If you've set the RT configuration variable B<$RT::UnsafeEmailCommands>,
+If you've set the RT configuration variable B<< C<UnsafeEmailCommands> >>,
C<take> and C<resolve> are also available. You can execute two or more
actions on a single message using a C<-> separated list. RT will execute
the actions in the listed order. For example you can use C<take-comment>,
=item C<--url>
This flag tells the mail gateway where it can find your RT server. You should
-probably use the same URL that users use to log into RT.
+probably use the same URL that users use to log into RT.
+
+If your RT server uses SSL, you will need to install additional Perl
+libraries. RT will detect and install these dependencies if you pass the
+C<--enable-ssl-mailgate> flag to configure as documented in RT's README.
+
+If you have a self-signed SSL certificate, you may also need to pass
+C<--ca-file> or C<--no-verify-ssl>, below.
+
+=item C<--ca-file> I<path>
+
+Specifies the path to the public SSL certificate for the certificate
+authority that should be used to verify the website's SSL certificate.
+If your webserver uses a self-signed certificate, you should
+preferentially use this option over C<--no-verify-ssl>, as it will
+ensure that the self-signed certificate that the mailgate is seeing the
+I<right> self-signed certificate.
+
+=item C<--no-verify-ssl>
+
+This flag tells the mail gateway to trust all SSL certificates,
+regardless of if their hostname matches the certificate, and regardless
+of CA. This is required if you have a self-signed certificate, or some
+other certificate which is not traceable back to an certificate your
+system ultimitely trusts.
+Verifying SSL certificates requires L<LWP::UserAgent> version 6.0 or
+higher; explicitly passing C<--verify-ssl> on prior versions will error.
=item C<--extension> OPTIONAL
Configure the timeout for posting the message to the web server. The
default timeout is 3 minutes (180 seconds).
+=back
+
=head1 DESCRIPTION
monitoring. For instance, if you're using F</etc/aliases> and you have a
"bugs" queue, you will want something like this:
- bugs: "|/opt/rt3/bin/rt-mailgate --queue bugs --action correspond
+ bugs: "|/opt/rt4/bin/rt-mailgate --queue bugs --action correspond
--url http://rt.mycorp.com/"
- bugs-comment: "|/opt/rt3/bin/rt-mailgate --queue bugs --action comment
+ bugs-comment: "|/opt/rt4/bin/rt-mailgate --queue bugs --action comment
--url http://rt.mycorp.com/"
Note that you don't have to run your RT server on your mail server, as
before allowing them to communicate with the system. You can do this
via a plug-in mechanism in the RT configuration.
-You can set the array C<@RT::MailPlugins> to be a list of plugins. The
+You can set the array C<@MailPlugins> to be a list of plugins. The
default plugin, if this is not given, is C<Auth::MailFrom> - that is,
authentication of the person is done based on the C<From> header of the
email. If you have additional filters or authentication mechanisms, you
can list them here and they will be called in order:
- @RT::MailPlugins = (
+ Set( @MailPlugins =>
"Filter::SpamAssassin",
"Auth::LDAP",
# ...
See the documentation for any additional plugins you have.
-You may also put Perl subroutines into the C<@RT::MailPlugins> array, if
+You may also put Perl subroutines into the C<@MailPlugins> array, if
they behave as described below.
=head1 WRITING PLUGINS
-What's actually going on in the above is that C<@RT::MailPlugins> is a
+What's actually going on in the above is that C<@MailPlugins> is a
list of Perl modules; RT prepends C<RT::Interface::Email::> to the name,
to form a package name, and then C<use>'s this module. The module is
expected to provide a C<GetCurrentUser> subroutine, which takes a hash of
The action being performed. At the moment, it's one of "comment" or "correspond"
-=back 4
+=back
It returns two values, the new C<RT::CurrentUser> object, and the new
authentication level. The authentication level can be zero, not allowed
Additionally, if C<-1> is returned, then the processing of the plug-ins
stops immediately and the message is ignored.
+=head1 ENVIRONMENT
+
+=over 4
+
+=item EXTENSION
+
+Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host
+and present "foo" in the environment variable C<EXTENSION>. Mailgate adds value
+of this variable to message in the C<X-RT-Mail-Extension> field of the message
+header.
+
+See also C<--extension> option. Note that value of the environment variable is
+always added to the message header when it's not empty even if C<--extension>
+option is not provided.
+
+=back
+
=cut