diff options
Diffstat (limited to 'rt/lib/RT/Interface/Email')
-rwxr-xr-x | rt/lib/RT/Interface/Email/Auth/GnuPG.pm | 211 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Email/Auth/MailFrom.pm | 99 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm | 8 |
3 files changed, 224 insertions, 94 deletions
diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm index e543c4b24..df987d806 100755 --- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm +++ b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,70 +45,204 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} -# + package RT::Interface::Email::Auth::GnuPG; -use Mail::GnuPG; + +use strict; +use warnings; =head2 GetCurrentUser To use the gnupg-secured mail gateway, you need to do the following: -Set up a gnupgp key directory with a pubring containing only the keys +Set up a GnuPG key directory with a pubring containing only the keys you care about and specify the following in your SiteConfig.pm -Set($RT::GPGKeyDir, "/path/to/keyring-directory"); -@RT::MailPlugins = qw(Auth::MailFrom Auth::GnuPG Filter::TakeAction); - - + Set(%GnuPGOptions, homedir => '/opt/rt3/var/data/GnuPG'); + Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...); =cut +sub ApplyBeforeDecode { return 1 } +use RT::Crypt::GnuPG; +use RT::EmailParser (); sub GetCurrentUser { my %args = ( - Message => undef, - RawMessageRef => undef, - CurrentUser => undef, - AuthLevel => undef, - Ticket => undef, - Queue => undef, - Action => undef, + Message => undef, + RawMessageRef => undef, @_ ); - my ( $val, $key, $address,$gpg ); + $args{'Message'}->head->delete($_) + for qw(X-RT-GnuPG-Status X-RT-Incoming-Encrypton + X-RT-Incoming-Signature X-RT-Privacy); + + my $msg = $args{'Message'}->dup; + + my ($status, @res) = VerifyDecrypt( Entity => $args{'Message'} ); + if ( $status && !@res ) { + $args{'Message'}->head->add( + 'X-RT-Incoming-Encryption' => 'Not encrypted' + ); + + return 1; + } + + # FIXME: Check if the message is encrypted to the address of + # _this_ queue. send rejecting mail otherwise. + + unless ( $status ) { + $RT::Logger->error("Had a problem during decrypting and verifying"); + my $reject = HandleErrors( Message => $args{'Message'}, Result => \@res ); + return (0, 'rejected because of problems during decrypting and verifying') + if $reject; + } + + # attach the original encrypted message + $args{'Message'}->attach( + Type => 'application/x-rt-original-message', + Disposition => 'inline', + Data => ${ $args{'RawMessageRef'} }, + ); + + $args{'Message'}->head->add( 'X-RT-Privacy' => 'PGP' ); - eval { + foreach my $part ( $args{'Message'}->parts_DFS ) { + my $decrypted; - my $parser = RT::EmailParser->new(); - $parser->SmartParseMIMEEntityFromScalar(Message => ${$args{'RawMessageRef'}}, Decode => 0); - $gpg = Mail::GnuPG->new( keydir => $RT::GPGKeyDir ); - my $entity = $parser->Entity; - ( $val, $key, $address ) = $gpg->verify( $parser->Entity); - $RT::Logger->crit("Got $val - $key - $address"); - }; - - if ($@) { - $RT::Logger->crit($@); + my $status = $part->head->get( 'X-RT-GnuPG-Status' ); + if ( $status ) { + for ( RT::Crypt::GnuPG::ParseStatus( $status ) ) { + if ( $_->{Operation} eq 'Decrypt' && $_->{Status} eq 'DONE' ) { + $decrypted = 1; + } + if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) { + $part->head->add( + 'X-RT-Incoming-Signature' => $_->{UserString} + ); + } + } } - unless ($address) { - $RT::Logger->crit( "Couldn't find a valid signature" . join ( "\n", @{ $gpg->{'last_message'} } ) ); - return ( $args{'CurrentUser'}, $args{'AuthLevel'} ); + $part->head->add( + 'X-RT-Incoming-Encryption' => + $decrypted ? 'Success' : 'Not encrypted' + ); } - my @addrs = Mail::Address->parse($address); - $address = $addrs[0]->address(); + return 1; +} + +sub HandleErrors { + my %args = ( + Message => undef, + Result => [], + @_ + ); - my $CurrentUser = RT::CurrentUser->new(); - $CurrentUser->LoadByEmail($address); + my $reject = 0; - if ( $CurrentUser->Id ) { - $RT::Logger->crit($address . " authenticated via PGP signature"); - return ( $CurrentUser, 2 ); + my %sent_once = (); + foreach my $run ( @{ $args{'Result'} } ) { + my @status = RT::Crypt::GnuPG::ParseStatus( $run->{'status'} ); + unless ( $sent_once{'NoPrivateKey'} ) { + unless ( CheckNoPrivateKey( Message => $args{'Message'}, Status => \@status ) ) { + $sent_once{'NoPrivateKey'}++; + $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnMissingPrivateKey'}; + } + } + unless ( $sent_once{'BadData'} ) { + unless ( CheckBadData( Message => $args{'Message'}, Status => \@status ) ) { + $sent_once{'BadData'}++; + $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnBadData'}; + } + } } + return $reject; +} + +sub CheckNoPrivateKey { + my %args = (Message => undef, Status => [], @_ ); + my @status = @{ $args{'Status'} }; + my @decrypts = grep $_->{'Operation'} eq 'Decrypt', @status; + return 1 unless @decrypts; + foreach my $action ( @decrypts ) { + # if at least one secrete key exist then it's another error + return 1 if + grep !$_->{'User'}{'SecretKeyMissing'}, + @{ $action->{'EncryptedTo'} }; + } + + $RT::Logger->error("Couldn't decrypt a message: have no private key"); + + my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0]; + my ($status) = RT::Interface::Email::SendEmailUsingTemplate( + To => $address, + Template => 'Error: no private key', + Arguments => { + Message => $args{'Message'}, + TicketObj => $args{'Ticket'}, + }, + InReplyTo => $args{'Message'}, + ); + unless ( $status ) { + $RT::Logger->error("Couldn't send 'Error: no private key'"); + } + return 0; +} + +sub CheckBadData { + my %args = (Message => undef, Status => [], @_ ); + my @bad_data_messages = + map $_->{'Message'}, + grep $_->{'Status'} ne 'DONE' && $_->{'Operation'} eq 'Data', + @{ $args{'Status'} }; + return 1 unless @bad_data_messages; + + $RT::Logger->error("Couldn't process a message: ". join ', ', @bad_data_messages ); + + my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0]; + my ($status) = RT::Interface::Email::SendEmailUsingTemplate( + To => $address, + Template => 'Error: bad GnuPG data', + Arguments => { + Messages => [ @bad_data_messages ], + TicketObj => $args{'Ticket'}, + }, + InReplyTo => $args{'Message'}, + ); + unless ( $status ) { + $RT::Logger->error("Couldn't send 'Error: bad GnuPG data'"); + } + return 0; +} + +sub VerifyDecrypt { + my %args = ( + Entity => undef, + @_ + ); + + my @res = RT::Crypt::GnuPG::VerifyDecrypt( %args ); + unless ( @res ) { + $RT::Logger->debug("No more encrypted/signed parts"); + return 1; + } + + $RT::Logger->debug('Found GnuPG protected parts'); + + # return on any error + if ( grep $_->{'exit_code'}, @res ) { + $RT::Logger->debug("Error during verify/decrypt operation"); + return (0, @res); + } + + # nesting + my ($status, @nested) = VerifyDecrypt( %args ); + return $status, @res, @nested; } eval "require RT::Interface::Email::Auth::GnuPG_Vendor"; @@ -121,3 +255,4 @@ die $@ && $@ !~ qr{^Can't locate RT/Interface/Email/Auth/GnuPG_Local.pm} ); 1; + diff --git a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm index 71cdf606c..0673c735c 100644 --- a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm +++ b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,6 +45,7 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Interface::Email::Auth::MailFrom; use RT::Interface::Email qw(ParseSenderAddressFromHead CreateUser); @@ -62,84 +63,74 @@ sub GetCurrentUser { # We don't need to do any external lookups my ( $Address, $Name ) = ParseSenderAddressFromHead( $args{'Message'}->head ); - - unless ($Address) { + unless ( $Address ) { + $RT::Logger->error("Couldn't find sender's address"); return ( $args{'CurrentUser'}, -1 ); } - my $CurrentUser = RT::CurrentUser->new(); - $CurrentUser->LoadByEmail($Address); - - unless ( $CurrentUser->Id ) { - $CurrentUser->LoadByName($Address); - } - + my $CurrentUser = new RT::CurrentUser; + $CurrentUser->LoadByEmail( $Address ); + $CurrentUser->LoadByName( $Address ) unless $CurrentUser->Id; if ( $CurrentUser->Id ) { + $RT::Logger->debug("Mail from user #". $CurrentUser->Id ." ($Address)" ); return ( $CurrentUser, 1 ); } - - # If the user can't be loaded, we may need to create one. Figure out the acl situation. - my $unpriv = RT::Group->new($RT::SystemUser); + my $unpriv = RT::Group->new( $RT::SystemUser ); $unpriv->LoadSystemInternalGroup('Unprivileged'); unless ( $unpriv->Id ) { - $RT::Logger->crit( "Auth::MailFrom couldn't find the 'Unprivileged' internal group" ); + $RT::Logger->crit("Couldn't find the 'Unprivileged' internal group"); return ( $args{'CurrentUser'}, -1 ); } - my $everyone = RT::Group->new($RT::SystemUser); + my $everyone = RT::Group->new( $RT::SystemUser ); $everyone->LoadSystemInternalGroup('Everyone'); unless ( $everyone->Id ) { - $RT::Logger->crit( "Auth::MailFrom couldn't find the 'Everyone' internal group"); + $RT::Logger->crit("Couldn't find the 'Everyone' internal group"); return ( $args{'CurrentUser'}, -1 ); } + $RT::Logger->debug("Going to create user with address '$Address'" ); + # but before we do that, we need to make sure that the created user would have the right # to do what we're doing. if ( $args{'Ticket'} && $args{'Ticket'}->Id ) { + my $qname = $args{'Queue'}->Name; # We have a ticket. that means we're commenting or corresponding if ( $args{'Action'} =~ /^comment$/i ) { # check to see whether "Everyone" or "Unprivileged users" can comment on tickets - unless ( $everyone->PrincipalObj->HasRight( - Object => $args{'Queue'}, - Right => 'CommentOnTicket' - ) - || $unpriv->PrincipalObj->HasRight( - Object => $args{'Queue'}, - Right => 'CommentOnTicket' - ) - ) { + unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'CommentOnTicket' ) + || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'CommentOnTicket' ) ) + { + $RT::Logger->debug("Unprivileged users have no right to comment on ticket in queue '$qname'"); return ( $args{'CurrentUser'}, 0 ); } } elsif ( $args{'Action'} =~ /^correspond$/i ) { # check to see whether "Everybody" or "Unprivileged users" can correspond on tickets - unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'}, - Right => 'ReplyToTicket' - ) - || $unpriv->PrincipalObj->HasRight( - Object => $args{'Queue'}, - Right => 'ReplyToTicket' - ) - ) { + unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'ReplyToTicket' ) + || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'ReplyToTicket' ) ) + { + $RT::Logger->debug("Unprivileged users have no right to reply to ticket in queue '$qname'"); return ( $args{'CurrentUser'}, 0 ); } - } elsif ( $args{'Action'} =~ /^take$/i ) { # check to see whether "Everybody" or "Unprivileged users" can correspond on tickets - unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'}, - Right => 'OwnTicket' - ) - || $unpriv->PrincipalObj->HasRight( - Object => $args{'Queue'}, - Right => 'OwnTicket' - ) - ) { + unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'OwnTicket' ) + || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'OwnTicket' ) ) + { + $RT::Logger->debug("Unprivileged users have no right to own ticket in queue '$qname'"); return ( $args{'CurrentUser'}, 0 ); } @@ -147,33 +138,35 @@ sub GetCurrentUser { elsif ( $args{'Action'} =~ /^resolve$/i ) { # check to see whether "Everybody" or "Unprivileged users" can correspond on tickets - unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'}, - Right => 'ModifyTicket' - ) - || $unpriv->PrincipalObj->HasRight( - Object => $args{'Queue'}, - Right => 'ModifyTicket' - ) - ) { + unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'ModifyTicket' ) + || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'ModifyTicket' ) ) + { + $RT::Logger->debug("Unprivileged users have no right to resolve ticket in queue '$qname'"); return ( $args{'CurrentUser'}, 0 ); } } else { + $RT::Logger->warning("Action '". ($args{'Action'}||'') ."' is unknown"); return ( $args{'CurrentUser'}, 0 ); } } # We're creating a ticket elsif ( $args{'Queue'} && $args{'Queue'}->Id ) { + my $qname = $args{'Queue'}->Name; # check to see whether "Everybody" or "Unprivileged users" can create tickets in this queue unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'}, Right => 'CreateTicket' ) - ) { + || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'}, + Right => 'CreateTicket' ) ) + { + $RT::Logger->debug("Unprivileged users have no right to create ticket in queue '$qname'"); return ( $args{'CurrentUser'}, 0 ); } - } $CurrentUser = CreateUser( undef, $Address, $Name, $Address, $args{'Message'} ); diff --git a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm b/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm index 176b39414..49e89c570 100644 --- a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm +++ b/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,6 +45,7 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Interface::Email::Filter::SpamAssassin; use Mail::SpamAssassin; @@ -77,7 +78,8 @@ RT::Interface::Email::Filter::SpamAssassin - Spam filter for RT =head1 SYNOPSIS - @RT::MailPlugins = ("Filter::SpamAssassin", ...); + # in RT config + Set(@MailPlugins, 'Filter::SpamAssassin', ...other filters...); =head1 DESCRIPTION |