[rt-users] 2.0 add-ons - Enhanced mailgate: enhancement

Kev Buckley k.buckley at lancaster.ac.uk
Wed Feb 19 05:04:32 EST 2003


> Kev,
> 	Thanks for the patch and the writeup. I'd recommend you send
> 	this to rt-users at lists.fsck.com and rt-2.0-addons at fsck.com, if
> 	you're up for it, so others can see the fruits of your labor.
> 
> 	Thanks!
> 
> 	Jesse
> 
> 
> On Tue, Feb 18, 2003 at 05:55:57PM +0000, Kev Buckley wrote:

Hello Jesse Vincent,

A bit of background,

Folk at Lancaster University's Computer Centre, mainly the networking
group but numbers are growing, have recently started using RT in earnest.

Previously we had had Remedy thrust upon us by management who were, we
can only assume, impressed with how much it cost. One of my (me being
a bit of a luddite) main gripes with that product was that one
couldn't act on or resolve tickets via email, web interfaces being all
well and good but by no means ideal in every situation. Anyway, I'd
been told that RT allowed this and so things looked promising.

Of course, when it came to the crunch, our networking folk didn't put
the enhanced mailgateway into their installation so I was forced to
set up my own one in order to try email interaction out there.

As I /played with it/investigated it/ (delete as experience dictates),
in another knock to my luddite tendencies, I discovered that were I to
use an pgp-unaware mailer and simply include plaintext signed stuff, I
would need to muck about with the exim (did I say we use exim as our
MTA, we do) config.

Ok, it was suggested to me that if we were ever going to get this
functionality accepted on a live system, it didn't seem a great idea
to have every message arriving at the box hosting RT being checked for
plaintext PGP content and ferkeling the headers, so I set out to add
the functionality into the gateway itself.

Find down below what I think does the necessary. I've included both a
diff-u and the full file in case there is some discrepancy between
what I started with.


I have added another routine similar to GetCurrentUserFromPGPSignature
called,

GetCurrentUserFromPGPSignedBody

which does (or appears to do) what it says it does. There may be some
redundancy across the two routines, I'll try and investigate that.

I also discovered that the usage of MailError (with its default
loglevel of 'crit') to inform folk that commands inside of a PGP
wrapper have been carried out, appears to cause the MTA to believe 
it hasn't suceeded in delivering the message OK, and so the user 
gets both a

"Your commands have been executed"   message

  and a

"I couldn't deliver your message"    message

  which, as you might expect, caused some confusion at first.

It is, to be sure, an "early release", and may be in need of some
reality checks and the like (eg, I set it up so that $Action=action is
defined at entry to the gateway but this isn't checked for, so every
message passes through the "SignedBody" call) but so far it has done
what I expected it to do.

Whatever, I hope it might be of some use to you, and if you do get a
chance to try it out and/or comment, I'd be interested to hear your
thoughts - even if they are "don't send me this again !"


As well as testing it through an MTA that passes the correct arguments
to the gateway, I tested from the command line in a similar fashion to 
this

cat plaintext-signed-mail |  | /usr/local/packages/rt2/bin/rt-enhanced-mailgate.local --queue queue1 --action action

where plaintext-signed-mail consists of the usual mailheaders and
then in the body, stuff like this

RT-Ticket: 18
RT-Status: resolved

which was run thorugh 

pgp -sta <plaintext_filename>

to create the PGP wrapping.

All the best,
Kevin

-- 
Regards,

----------------------------------------------------------------------
*  Kevin M. Buckley              e-mail: K.Buckley at lancaster.ac.uk   *
*                                                                    *
*  Systems Administrator                                             *
*  Computer Centre                                                   *
*  Lancaster University          Voice:  +44 (0) 1524 5 93718        *
*  LANCASTER. LA1 4YW            Fax  :  +44 (0) 1524 5 25113        *
*  England.                                                          *
*                                                                    *
*  My PC runs Linux/GNU, you still computing the Bill Gate$' way ?   *
----------------------------------------------------------------------


Output from cvs diff -u 

------8<------8<------8<------8<------8<------8<------8<------8<------
Index: rt-enhanced-mailgate.local
===================================================================
RCS file: /export/cvsroot/Kevin/RT/exim/rt-enhanced-mailgate.local,v
retrieving revision 1.1
retrieving revision 1.14
diff -u -r1.1 -r1.14
--- rt-enhanced-mailgate.local	14 Feb 2003 15:37:47 -0000	1.1
+++ rt-enhanced-mailgate.local	17 Feb 2003 18:16:14 -0000	1.14
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -w
 
 ### KMB Lancaster ###
-# $Id: rt-enhanced-mailgate.local,v 1.1 2003/02/14 15:37:47 kevin Exp $
+# $Id: rt-enhanced-mailgate.local,v 1.14 2003/02/17 18:16:14 kevin Exp $
 ### KMB Lancaster ###
 # was
 # %Header: /raid/cvsroot/rt-addons/enhanced-mailgate,v 1.9 2001/11/13 04:25:29 jesse Exp %
@@ -22,7 +22,7 @@
 # KEYIDIR should point to the directory containing a pubring.gpg
 # for gpg to use as its authentication database
 
-$KEYDIR = "/opt/rt2/etc/gnupg";
+$KEYDIR = "/usr/local/packages/rt2/etc/gnupg";
 
 # If you turn on the $PermitReplayAttacks flag in enhanced-mailgate, RT will
 #  treat
@@ -32,8 +32,8 @@
 
 $PermitReplayAttacks = 0;
 
-use lib "/opt/rt2/lib";
-use lib "/opt/rt2/etc";
+use lib "/usr/local/packages/rt2/lib";
+use lib "/usr/local/packages/rt2/etc";
 
 
 use RT::Interface::Email  qw(CleanEnv LoadConfig DBConnect
@@ -120,6 +120,11 @@
     
 }	
 
+unless ($CurrentUser) {
+    ($CurrentUser, $CurrentUserAuth) =
+	GetCurrentUserFromPGPSignedBody($entity, $ErrorsTo);
+}
+
 # Get us a current user object, if we couldn't validate the sig 
 # or there was no sig
 unless ($CurrentUser) {
@@ -128,9 +133,6 @@
 }
 
 
-
-
-
 my $MessageId = $head->get('Message-Id') || 
   "<no-message-id-".time.rand(2000)."\@.$RT::rtname>";
 
@@ -213,6 +215,7 @@
 # {{{ If we don't have a ticket Id, we're creating a new ticket
 
 # {{{ if we're processing an action
+
 if ($Action =~ /action/i) {
     
     #Get pseudo headers out of the message body before we go there.
@@ -220,10 +223,16 @@
 
     if ($CurrentUserAuth eq 'pgp-signature') {
 	my $ResultsMessage = ActOnPseudoHeaders($TicketId, $PseudoHeaders);
+
+# Leaving this MailError in as was (implies LogLevel => 'crit')
+#  causes the gateway to return in such a way that Exim thinks it has
+# failed and generates a not-delivered message for the aliased address
+
 	MailError( To => $ErrorsTo,
 		   Subject => "RT has proccessed your commands",
 		   Explanation => $ResultsMessage,
-		   MIMEObj => $entity->parts(0)
+		   MIMEObj => $entity->parts(0),
+		   LogLevel => 'warning'
 		 );    
     }
     else {
@@ -322,7 +331,6 @@
 	$RT::Logger->crit("$Action aliases require a TicketId to work on ".
 			  "(from ".$CurrentUser->UserObj->EmailAddress.") ".
 			  $MessageId);
-	return();
     }
 
 }
@@ -360,6 +368,7 @@
     
     # If the message is correspondence, add it to the ticket
     elsif ($Action =~ /correspond/i) {
+
 	my $Ticket = RT::Ticket->new($CurrentUser);
 	$Ticket->Load($TicketId);
 	$Ticket->Open;   #TODO: Don't open if it's alreadyopen
@@ -390,16 +399,41 @@
 	$RT::Logger->crit("$Action type unknown for $MessageId");
 	
     }
+
     }
 
 # }}}
 
 $RT::Handle->Disconnect();
 
-
 # Everything below this line is a helper sub. most of them will eventually
 # move to Interface::Email
 
+
+# {{{ sub DIE
+
+#When we call die, trap it and log->crit with the value of the die.
+
+$SIG{__DIE__}  = sub {
+    unless ($^S || !defined $^S ) {
+        $RT::Logger->crit("$_[0]");
+	MailError( To => $ErrorsTo,  
+		   Bcc => $RT::OwnerEmail,
+		   Subject => "RT Critical error. Message not recorded!",
+		   Explanation => "$_[0]",
+		   MIMEObj => $entity
+		 );
+	exit(-1);
+    }
+    else {
+        #Get out of here if we're in an eval
+        die $_[0];
+    }
+};
+
+
+# }}}
+
 # {{{ Helper Subs
 
 # {{{ sub GetCurrentUserFromPGPSignature 
@@ -506,33 +540,141 @@
 # }}}
 
 
+# {{{ sub GetCurrentUserFromPGPSignedBody
 
 
+sub GetCurrentUserFromPGPSignedBody {
+    my $entity = shift;
+    my $ErrorsTo = shift;
+    require IO::Handle;
+    require GnuPG::Interface;
+   	
+    $RT::Logger->debug("Getting the current user from a pgp sigbody\n"); 
+#    printf("Getting the current user from a pgp sig\n"); 
+    my $gnupg = GnuPG::Interface->new();
+    $gnupg->options->meta_interactive( 0 );
+     $gnupg->options->hash_init( armor   => 1,
+                                 homedir => $KEYDIR );
+
+    # how we create some handles to interact with GnuPG
+    my $input   = IO::Handle->new();
+    my $error   = IO::Handle->new();
+    my $handles = GnuPG::Handles->new( stderr => $error,
+				       stdin  => $input	     
+				     );
+    
+    my ($bodyh, $bodyfile)  = File::Temp::tempfile('/tmp/rtsigXXXXXXXX', 
+						  UNLINK => 1);
+    open(BODY, ">$bodyfile");
+    $entity->print_body(\*BODY);
+    close(BODY);
 
+    my $pid = $gnupg->verify( handles => $handles,
+			      command_args => ["$bodyfile"]);
+ 
+    # Now we write to the input of GnuPG
+  
+  # now we read the output
+  my @result = <$error>;
+  close $error;
+  close $input;
+  
+  waitpid $pid, 0;
+  
+  while (my $line = shift @result) {	
 
+#	  $RT::Logger->debug("pgp says:\n$line:\n"); 
 
-# {{{ sub DIE
+      if ($line =~ /^gpg: Good signature from "(.*)"$/i) {
+	  my $userid = $1;
+	  my $address;
 
+	  #Parse out the user id.
+	  if ($userid =~ /<(.*?)>/) {
+	      $address = $1;
+	  }	
+	  $RT::Logger->debug("Good pgp sig from $userid\n"); 
+#	  printf("Good pgp sig from $userid\n"); 
 
+	  # since we have an authenticated sender, lets try to find them
+	  # in RT's database
 
-#When we call die, trap it and log->crit with the value of the die.
+	  my $user = new RT::CurrentUser($RT::SystemUser);
+	  $user->LoadByEmail($address);
 
-$SIG{__DIE__}  = sub {
-    unless ($^S || !defined $^S ) {
-        $RT::Logger->crit("$_[0]");
-	MailError( To => $ErrorsTo,  
-		   Bcc => $RT::OwnerEmail,
-		   Subject => "RT Critical error. Message not recorded!",
-		   Explanation => "$_[0]",
-		   MIMEObj => $entity
-		 );
-	exit(-1);
-    }
-    else {
-        #Get out of here if we're in an eval
-        die $_[0];
-    }
-};
+	  #if we couldn't find any users with that email, maybe we can find
+	  # one with that username.
+
+	  unless ($user->Id) {
+	      $RT::Logger->debug("Didn't LoadByEmail $address\n"); 
+	      $user->Load($address);
+	  }
+	  
+	  unless ($user->Id) {
+	      $RT::Logger->debug("Didn't Load $address either\n"); 
+	      $user->Load($RT::Nobody->id);
+	  }
+
+	  # Also need to extract the actual data, otherwise the
+	  #  command parser sees all the PGP/GPG guff 
+
+## This more or less duplicates the stuff above : can we rationalise it 
+	  
+	  my $input   = IO::Handle->new();
+	  my $output  = IO::Handle->new();
+	  my $error   = IO::Handle->new();
+	  my $handles = GnuPG::Handles->new( stderr => $error,
+				       stdout => $output,
+				       stdin  => $input	     
+				     );
+    
+	  my ($bodyh, $bodyfile)  = File::Temp::tempfile('/tmp/rtsigXXXXXXXX', 
+						  UNLINK => 1);
+	  open(BODY, ">$bodyfile");
+	  $entity->print_body(\*BODY);
+	  close(BODY);
+
+	  my $pid = $gnupg->decrypt( handles => $handles,
+				     command_args => ["$bodyfile"]);
+ 
+	  # now we read the output
+	  my @plaintext = <$output>;
+	  close $error;
+	  close $output;
+	  close $input;
+  
+	  waitpid $pid, 0;
+
+	  # Replace the body of the message with the "decoded" content
+	  # If this fails, we will still have the full message
+	  if (my $io = $entity->open("w")) {
+	      foreach (@plaintext) { $io->print($_) }
+	      $io->close;
+	  }
+	  
+## End of duplication
+
+	  return ($user, 'pgp-signature');
+	  
+      }
+      # If we got a bad signature, warn the user and the admin.
+      elsif ($line =~ /^gpg: BAD/) {
+	  $RT::Logger->warning("Bad PGP Signature: $line\n"); 
+#  	  printf("Bad PGP Signature: $line\n"); 
+	  MailError( To => $ErrorsTo,  
+		     Bcc => $RT::OwnerEmail,
+		     Subject => "RT Authentication error.",
+		     Explanation => "RT couldn't validate this PGP signature. \n".
+		     "RT will process this message as if it were unsigned.\n",
+		     MIMEObj => $entity
+		   );
+	  return (undef,undef);
+      }	
+  }
+  $RT::Logger->debug("Couldn't figure out what to do from gpg's reply\n");
+#  printf("Couldn't figure out what to do from gpg's reply\n");
+  return(undef,undef);
+}
 
 
 # }}}
@@ -554,16 +696,15 @@
     unless ($BodyHandle) {
 	return(undef);
     }
-    
-    
+        
     ### Slurp all the UNENCODED data in, and put it in an array of lines:
     my @lines = $BodyHandle->as_lines;
-    
-    
+        
     # yank all the pseudoheaders until we find a blank line.
     while (my $line = shift @lines) {
 	next if $line =~ /^\s*?$/;
 	if ($line =~ /^RT-/i) {
+
 	    $PseudoHeaders .= $line;
 	}
 	#If we find a line that's not a command, get out.
@@ -629,7 +770,7 @@
 			    join("\n", at actions);
 		    return($ResultsMessage);
 		}
-		$ResultsMessage .= "Ticket ".$Ticket->Id." loaded\n";
+		$ResultsMessage .= "Ticket ".$Ticket->Id." loaded";
 	    }
 	    else {
 		unless ($Ticket->Id) {
------8<------8<------8<------8<------8<------8<------8<------8<------


That file in full

------8<------8<------8<------8<------8<------8<------8<------8<------
#!/usr/bin/perl -w

### KMB Lancaster ###
# $Id: rt-enhanced-mailgate.local,v 1.14 2003/02/17 18:16:14 kevin Exp $
### KMB Lancaster ###
# was
# %Header: /raid/cvsroot/rt-addons/enhanced-mailgate,v 1.9 2001/11/13 04:25:29 jesse Exp %
# (c) 1996-2001 Jesse Vincent <jesse at fsck.com>

# This software is redistributable under the terms of version 2 of the GNU GPL

# This product works with RT 2.0, but is
# not part of the RT distribution and IS NOT A FREELY SUPPORTED PRODUCT.  
# Patches are, of course, appreciated.
	
package RT;
use strict;
use vars qw($VERSION $KEYDIR $Handle $Nobody $SystemUser $PermitReplayAttacks);

$VERSION='0.3';

# KEYIDIR should point to the directory containing a pubring.gpg
# for gpg to use as its authentication database

$KEYDIR = "/usr/local/packages/rt2/etc/gnupg";

# If you turn on the $PermitReplayAttacks flag in enhanced-mailgate, RT will
#  treat
# the [tag #<int>] in the message's subject as an initial RT-Ticket: header.
# This leaves you open to the possibility of a hostile user applying your
# updates to another ticket.

$PermitReplayAttacks = 0;

use lib "/usr/local/packages/rt2/lib";
use lib "/usr/local/packages/rt2/etc";


use RT::Interface::Email  qw(CleanEnv LoadConfig DBConnect
			     GetCurrentUser
                             GetMessageContent
                             CheckForLoops
                             CheckForSuspiciousSender
                             CheckForAutoGenerated
                             ParseMIMEEntityFromSTDIN
                             ParseTicketId
                             MailError
                             ParseCcAddressesFromHead
                             ParseSenderAddressFromHead
                             ParseErrorsToAddressFromHead
			    );
 
use RT::Interface::Email  qw(CleanEnv LoadConfig DBConnect);

#Clean out all the nasties from the environment
CleanEnv();

#Load etc/config.pm and drop privs
LoadConfig();

#Connect to the database and get RT::SystemUser and RT::Nobody loaded
DBConnect();


use RT::Ticket;
use RT::Queue;
use MIME::Parser;
use File::Temp;
use Mail::Address;


#Set some sensible defaults 
my $Queue = 1;
my $Action = "correspond";  

my ($Verbose, $ReturnTid, $Debug);
my ($From, $TicketId, $Subject,$SquelchReplies);
my ($status, $msg, $CurrentUser, $CurrentUserAuth);

# {{{ parse commandline 

while (my $flag = shift @ARGV) {
    if (($flag eq '-v') or ($flag eq '--verbose')) {
	$Verbose = 1;
    }
    if (($flag eq '-t') or ($flag eq '--ticketid')) {
	$ReturnTid = 1;
    }
    
    if (($flag eq '-d') or ($flag eq '--debug')) {
	$RT::Logger->debug("Debug mode enabled\n");
	$Debug = 1;
      }
    
    if (($flag eq '-q') or ($flag eq '--queue')) {
	$Queue = shift @ARGV;
    } 
      if (($flag eq '-a') or ($flag eq '--action')) {
	  $Action = shift @ARGV;
      } 
    
    
}

# }}}

# get the current mime entity from stdin
my ($entity, $head) = ParseMIMEEntityFromSTDIN();

#Get someone to send runtime errors to;
my $ErrorsTo = ParseErrorsToAddressFromHead($head);


# If there's a gpg signature, try to validate it.
if ( ($head->mime_type =~ /multipart\/signed/i) and
     ( $entity->parts(1)->head->mime_type =~ /application\/pgp-signature/i) ) {
    
    ($CurrentUser, $CurrentUserAuth) =
      GetCurrentUserFromPGPSignature($entity, $ErrorsTo);
    
}	

unless ($CurrentUser) {
    ($CurrentUser, $CurrentUserAuth) =
	GetCurrentUserFromPGPSignedBody($entity, $ErrorsTo);
}

# Get us a current user object, if we couldn't validate the sig 
# or there was no sig
unless ($CurrentUser) {
    $CurrentUser = GetCurrentUser($head);
    $CurrentUserAuth = 'mailfrom';
}


my $MessageId = $head->get('Message-Id') || 
  "<no-message-id-".time.rand(2000)."\@.$RT::rtname>";

#Pull apart the subject line
$Subject = $head->get('Subject') || "[no subject]";
chomp $Subject;

# Get the ticket ID
$TicketId = ParseTicketId($Subject);

#Set up a queue object
my $QueueObj = RT::Queue->new($CurrentUser);
$QueueObj->Load($Queue);
unless ($QueueObj->id ) {
    MailError(To => $RT::OwnerEmail,
	      Subject => "RT Bounce: $Subject",
	      Explanation => "RT couldn't find the queue: $Queue",
	      MIMEObj => $entity);
    
}

# {{{ Lets check for mail loops of various sorts.

my $IsAutoGenerated = CheckForAutoGenerated($head);

my $IsSuspiciousSender = CheckForSuspiciousSender($head);

my $IsALoop = CheckForLoops($head);


#If the message is autogenerated, we need to know, so we can not 
# send mail to the sender
if ($IsSuspiciousSender || $IsAutoGenerated || $IsALoop) {
    $SquelchReplies = 1;
    
    $ErrorsTo = $RT::OwnerEmail;

}


# {{{ Warn someone  if it's a loop

# Warn someone if it's a loop, before we drop it on the ground
if ($IsALoop) {
    $RT::Logger->crit("RT Recieved mail ($MessageId) from itself.");
    
    #Should we mail it to RTOwner?
    if ($RT::LoopsToRTOwner) {
	MailError(To => $RT::OwnerEmail,
		  Subject => "RT Bounce: $Subject",
		  Explanation => "RT thinks this message may be a bounce",
		  MIMEObj => $entity);
	
	#Do we actually want to store it?
	exit unless ($RT::StoreLoops);
    }
}

# }}}


   #Don't let the user stuff the RT-Squelch-Replies-To header.
    if ($head->get('RT-Squelch-Replies-To')) {
        $head->add('RT-Relocated-Squelch-Replies-To',
                   $head->get('RT-Squelch-Replies-To'));
        $head->delete('RT-Squelch-Replies-To')
    }


if ($SquelchReplies) {
    ## TODO: This is a hack.  It should be some other way to
    ## indicate that the transaction should be "silent".

    my ($Sender, $junk) = ParseSenderAddressFromHead($head);
    $head->add('RT-Squelch-Replies-To', $Sender);
}

# }}}

# {{{ If we don't have a ticket Id, we're creating a new ticket

# {{{ if we're processing an action

if ($Action =~ /action/i) {
    
    #Get pseudo headers out of the message body before we go there.
    my $PseudoHeaders = ParseMessageForCommands($entity);    

    if ($CurrentUserAuth eq 'pgp-signature') {
	my $ResultsMessage = ActOnPseudoHeaders($TicketId, $PseudoHeaders);

# Leaving this MailError in as was (implies LogLevel => 'crit')
#  causes the gateway to return in such a way that Exim thinks it has
# failed and generates a not-delivered message for the aliased address

	MailError( To => $ErrorsTo,
		   Subject => "RT has proccessed your commands",
		   Explanation => $ResultsMessage,
		   MIMEObj => $entity->parts(0),
		   LogLevel => 'warning'
		 );    
    }
    else {
	MailError( To => $ErrorsTo,
		   Subject => "RT couldn't authenticate you",
		   MIMEObj => $entity->parts(0),
		   Explanation => 
"RT's email command mode requires PGP authentication. 
Either you didn't sign your message, or your signature could not be verified."
		 );    
    }
}
# }}}

elsif (!defined($TicketId)) {
    
    #If the message doesn't reference a ticket #, create a new ticket
    
    # {{{ Create a new ticket
    if ($Action =~ /correspond/) {
	
	#    open a new ticket 
	my @Requestors = ($CurrentUser->id);
	
	my @Cc;
	if ($RT::ParseNewMessageForTicketCcs) {
	    @Cc = ParseCcAddressesFromHead(Head => $head, QueueObj => $QueueObj );
	}
	
	# Pull commands out of $entity.
	my $Commands = ParseMessageForCommands($entity);
	
	# Pull values out of commands, setting some defaults.
	my $values = ParsePseudoHeadersForNewTicket($Commands,
						    status => 'new',
						    queue => $Queue,
						    subject => $Subject,
						    requestor => \@Requestors,
						    cc => \@Cc,
						    admincc => undef,
						   );
		
	my $Ticket = new RT::Ticket($CurrentUser);
	my ($id, $Transaction, $ErrStr) = 
	  $Ticket->Create ( MIMEObj => $entity,
	 		    Status => $values->{'status'},
 			    Queue => $values->{'queue'},
			    Subject => $values->{'subject'},
			    Requestor => \@{$values->{'requestor'}},
			    Cc => \@{$values->{'cc'}},
			    AdminCc => \@{$values->{'admincc'}},
			    Owner => $values->{'owner'},
			    TimeWorked => $values->{'timeworked'},
			    TimeLeft => $values->{'timeleft'},
			    Priority => $values->{'priority'},
			    FinalPriority => $values->{'finalpriority'},
			    Due => $values->{'due'},
			  );
	if ($id == 0 ) {
	    MailError( To => $ErrorsTo,
 		       Subject => "Ticket creation failed",
		       Explanation => $ErrStr,
		       MIMEObj => $entity
		     );
	    $RT::Logger->error("Create failed: $id / $Transaction / $ErrStr ");
	}	
	else {
	    
	    if ($values->{'keywords'}) {
		foreach my $keywordsel (keys %{$values->{'keywords'}}) {
		    my $ks_obj = $Ticket->QueueObj->KeywordSelect($keywordsel);
		    next unless ($ks_obj->id);
		    foreach my $key (keys %{$values->{'keywords'}{$keywordsel}}) {
			my $kids = $ks_obj->KeywordObj->Descendents;
			foreach my $kid (keys %{$kids}) {
			    next unless ($kids->{$kid} =~ /^$key$/i);
			    my ($val, $msg) = 
			      $Ticket->AddKeyword(KeywordSelect => $ks_obj->id,
						  Keyword => $kid);
			}
		    }
		}
	    }
	}
    }
    # }}}
    
    else {
	#TODO Return an error message
	MailError( To => $ErrorsTo,
		   Subject => "No ticket id specified",
		   Explanation => "$Action aliases require a TicketId to work on",
		   MIMEObj => $entity
		 );

	$RT::Logger->crit("$Action aliases require a TicketId to work on ".
			  "(from ".$CurrentUser->UserObj->EmailAddress.") ".
			  $MessageId);
    }

}

# }}}

# {{{ If we've got a ticket ID, update the ticket

else {
    
    #   If the action is comment, add a comment.
    if ($Action =~ /comment/i){
	
	my $Ticket = new RT::Ticket($CurrentUser);
	$Ticket->Load($TicketId);
	unless ($Ticket->Id) {
	    MailError( To => $ErrorsTo,
		       Subject => "Comment not recorded",
		       Explanation => "Could not find a ticket with id $TicketId",
		       MIMEObj => $entity
		     );
	    #Return an error message saying that Ticket "#foo" wasn't found.
	}
	
	($status, $msg) = $Ticket->Comment(MIMEObj=>$entity);
	unless ($status) {
	    #Warn the sender that we couldn't actually submit the comment.
	    MailError( To => $ErrorsTo,
		       Subject => "Comment not recorded",
		       Explanation => $msg,
		       MIMEObj => $entity
		     );
	}	
    }
    
    # If the message is correspondence, add it to the ticket
    elsif ($Action =~ /correspond/i) {

	my $Ticket = RT::Ticket->new($CurrentUser);
	$Ticket->Load($TicketId);
	$Ticket->Open;   #TODO: Don't open if it's alreadyopen
	
	#TODO: Check for error conditions
	($status, $msg) = $Ticket->Correspond(MIMEObj => $entity);
	unless ($status) {
	    #Return mail to the sender with an error
	    MailError( To => $ErrorsTo,
		       Subject => "Correspondence not recorded",
		       Explanation => $msg,
		       MIMEObj => $entity
		     );
	}
    }
    

    else {
	#Return mail to the sender with an error
	MailError( To => $ErrorsTo,
		   Subject => "RT Configuration error",
		   Explanation => "'$Action' not a recognized action.".
		   " Your RT administrator has misconfigured ".
		   "the mail aliases which invoke RT" ,
		   MIMEObj => $entity
		 );
	
	$RT::Logger->crit("$Action type unknown for $MessageId");
	
    }

    }

# }}}

$RT::Handle->Disconnect();

# Everything below this line is a helper sub. most of them will eventually
# move to Interface::Email


# {{{ sub DIE

#When we call die, trap it and log->crit with the value of the die.

$SIG{__DIE__}  = sub {
    unless ($^S || !defined $^S ) {
        $RT::Logger->crit("$_[0]");
	MailError( To => $ErrorsTo,  
		   Bcc => $RT::OwnerEmail,
		   Subject => "RT Critical error. Message not recorded!",
		   Explanation => "$_[0]",
		   MIMEObj => $entity
		 );
	exit(-1);
    }
    else {
        #Get out of here if we're in an eval
        die $_[0];
    }
};


# }}}

# {{{ Helper Subs

# {{{ sub GetCurrentUserFromPGPSignature 


sub GetCurrentUserFromPGPSignature {
    my $entity = shift;
    my $ErrorsTo = shift;
    require IO::Handle;
    require GnuPG::Interface;
   	
    $RT::Logger->debug("Getting the current user from a pgp sig\n"); 
    my $gnupg = GnuPG::Interface->new();
    $gnupg->options->meta_interactive( 0 );
     $gnupg->options->hash_init( armor   => 1,
                                 homedir => $KEYDIR );

    # how we create some handles to interact with GnuPG
    my $input   = IO::Handle->new();
    my $error   = IO::Handle->new();
    my $handles = GnuPG::Handles->new( stderr => $error,
				       stdin  => $input,
				     
				     );
    
    my ($sigfh, $sigfile)  = File::Temp::tempfile('/tmp/rtsigXXXXXXXX', 
						  UNLINK => 1);
    open(SIG, ">$sigfile");
    $entity->parts(1)->print_body(\*SIG);
    close(SIG);
    
    my ($datafh, $datafile)  = File::Temp::tempfile('/tmp/rtdataXXXXXXXX', 
						    UNLINK => 1);
    open(DATA, ">$datafile");
    ### Read the (unencoded) body data:

    print DATA $entity->parts(0)->as_string;
    close(DATA);
    
    
    my $pid = $gnupg->verify( handles => $handles,
			      command_args => ["$sigfile",
					       "$datafile",
					      ]);
    
    # Now we write to the input of GnuPG
  
  # now we read the output
  my @result = <$error>;
  close $error;
  close $input;
  
  waitpid $pid, 0;
  
  while (my $line = shift @result) {	

      if ($line =~ /^gpg: Good signature from "(.*)"$/i) {
	  my $userid = $1;
	  my $address;

	  #Parse out the user id.
	  if ($userid =~ /<(.*?)>/) {
	      $address = $1;
	  }	
	  $RT::Logger->debug("Good pgp sig from $userid\n"); 

	  # since we have an authenticated sender, lets try to find them
	  # in RT's database

	  my $user = new RT::CurrentUser($RT::SystemUser);
	  $user->LoadByEmail($address);


	  #if we couldn't find any users with that email, maybe we can find
	  # one with that username.

	  $user->Load($address) unless ($user->Id);
	  	  
	  unless ($user->Id) {
	      $user->Load($RT::Nobody->id);
	  }
	  
	  return ($user, 'pgp-signature');
	  
      }
      # If we got a bad signature, warn the user and the admin.
      elsif ($line =~ /^gpg: BAD/) {
	  $RT::Logger->warning("Bad PGP Signature: $line\n"); 
	  MailError( To => $ErrorsTo,  
		     Bcc => $RT::OwnerEmail,
		     Subject => "RT Authentication error.",
		     Explanation => "RT couldn't validate this PGP signature. \n".
		     "RT will process this message as if it were unsigned.\n",
		     MIMEObj => $entity
		   );
	  return (undef,undef);
      }	
  }
  $RT::Logger->debug("Couldn't figure out what to do from gpg's reply\n");
  return(undef,undef);
}


# }}}


# {{{ sub GetCurrentUserFromPGPSignedBody


sub GetCurrentUserFromPGPSignedBody {
    my $entity = shift;
    my $ErrorsTo = shift;
    require IO::Handle;
    require GnuPG::Interface;
   	
    $RT::Logger->debug("Getting the current user from a pgp sigbody\n"); 
#    printf("Getting the current user from a pgp sig\n"); 
    my $gnupg = GnuPG::Interface->new();
    $gnupg->options->meta_interactive( 0 );
     $gnupg->options->hash_init( armor   => 1,
                                 homedir => $KEYDIR );

    # how we create some handles to interact with GnuPG
    my $input   = IO::Handle->new();
    my $error   = IO::Handle->new();
    my $handles = GnuPG::Handles->new( stderr => $error,
				       stdin  => $input	     
				     );
    
    my ($bodyh, $bodyfile)  = File::Temp::tempfile('/tmp/rtsigXXXXXXXX', 
						  UNLINK => 1);
    open(BODY, ">$bodyfile");
    $entity->print_body(\*BODY);
    close(BODY);

    my $pid = $gnupg->verify( handles => $handles,
			      command_args => ["$bodyfile"]);
 
    # Now we write to the input of GnuPG
  
  # now we read the output
  my @result = <$error>;
  close $error;
  close $input;
  
  waitpid $pid, 0;
  
  while (my $line = shift @result) {	

#	  $RT::Logger->debug("pgp says:\n$line:\n"); 

      if ($line =~ /^gpg: Good signature from "(.*)"$/i) {
	  my $userid = $1;
	  my $address;

	  #Parse out the user id.
	  if ($userid =~ /<(.*?)>/) {
	      $address = $1;
	  }	
	  $RT::Logger->debug("Good pgp sig from $userid\n"); 
#	  printf("Good pgp sig from $userid\n"); 

	  # since we have an authenticated sender, lets try to find them
	  # in RT's database

	  my $user = new RT::CurrentUser($RT::SystemUser);
	  $user->LoadByEmail($address);

	  #if we couldn't find any users with that email, maybe we can find
	  # one with that username.

	  unless ($user->Id) {
	      $RT::Logger->debug("Didn't LoadByEmail $address\n"); 
	      $user->Load($address);
	  }
	  
	  unless ($user->Id) {
	      $RT::Logger->debug("Didn't Load $address either\n"); 
	      $user->Load($RT::Nobody->id);
	  }

	  # Also need to extract the actual data, otherwise the
	  #  command parser sees all the PGP/GPG guff 

## This more or less duplicates the stuff above : can we rationalise it 
	  
	  my $input   = IO::Handle->new();
	  my $output  = IO::Handle->new();
	  my $error   = IO::Handle->new();
	  my $handles = GnuPG::Handles->new( stderr => $error,
				       stdout => $output,
				       stdin  => $input	     
				     );
    
	  my ($bodyh, $bodyfile)  = File::Temp::tempfile('/tmp/rtsigXXXXXXXX', 
						  UNLINK => 1);
	  open(BODY, ">$bodyfile");
	  $entity->print_body(\*BODY);
	  close(BODY);

	  my $pid = $gnupg->decrypt( handles => $handles,
				     command_args => ["$bodyfile"]);
 
	  # now we read the output
	  my @plaintext = <$output>;
	  close $error;
	  close $output;
	  close $input;
  
	  waitpid $pid, 0;

	  # Replace the body of the message with the "decoded" content
	  # If this fails, we will still have the full message
	  if (my $io = $entity->open("w")) {
	      foreach (@plaintext) { $io->print($_) }
	      $io->close;
	  }
	  
## End of duplication

	  return ($user, 'pgp-signature');
	  
      }
      # If we got a bad signature, warn the user and the admin.
      elsif ($line =~ /^gpg: BAD/) {
	  $RT::Logger->warning("Bad PGP Signature: $line\n"); 
#  	  printf("Bad PGP Signature: $line\n"); 
	  MailError( To => $ErrorsTo,  
		     Bcc => $RT::OwnerEmail,
		     Subject => "RT Authentication error.",
		     Explanation => "RT couldn't validate this PGP signature. \n".
		     "RT will process this message as if it were unsigned.\n",
		     MIMEObj => $entity
		   );
	  return (undef,undef);
      }	
  }
  $RT::Logger->debug("Couldn't figure out what to do from gpg's reply\n");
#  printf("Couldn't figure out what to do from gpg's reply\n");
  return(undef,undef);
}


# }}}

# {{{ sub ParseMessageForCommands

=item ParseMessageForCommands MIMEObj

Removes RT- pseudo headers from the MIMEObj and returns them as a string.

=cut

sub ParseMessageForCommands {
    my $MIMEObj = shift;
    my $PseudoHeaders = '';
    
    my $BodyHandle = GetFirstBodyHandle($MIMEObj);
    
    unless ($BodyHandle) {
	return(undef);
    }
        
    ### Slurp all the UNENCODED data in, and put it in an array of lines:
    my @lines = $BodyHandle->as_lines;
        
    # yank all the pseudoheaders until we find a blank line.
    while (my $line = shift @lines) {
	next if $line =~ /^\s*?$/;
	if ($line =~ /^RT-/i) {

	    $PseudoHeaders .= $line;
	}
	#If we find a line that's not a command, get out.
	else {
	    unshift @lines, $line;
	    last;
	}	
    }
    
    
    ### Write data to the body:
    #TODO +++ get rid of the dies.
    my $IO = $BodyHandle->open("w")      || die "open body: $!";
    $IO->print(join("", at lines));
    $IO->close                  || die "close I/O handle: $!";
    
    return ($PseudoHeaders);
    
}

# }}}

# {{{ sub ActOnPseudoHeaders

=item ActOnPseudoHeaders $PseudoHeaders

Takes a string of pseudo-headers, iterates through them and does what they tell it to.

=cut

sub ActOnPseudoHeaders {
    my $FirstTicketId = shift;
    my $PseudoHeaders = shift;
     
    my $ResultsMessage = '';

    my $Ticket = RT::Ticket->new($CurrentUser);

    if ($PermitReplayAttacks) {
    	$Ticket->Load($FirstTicketId);
    }
    
    my @actions = split('\n',$PseudoHeaders);
    
    foreach my $action (@actions) {
	my ($val);
	my  $msg = '';


	$ResultsMessage .= ">>> $action\n";

	if ($action =~ /^RT-(.*?):\s+(.*)$/) {
	    my $command = $1;
	    my $args = $2;
	    
	    if ($command =~ /^ticket$/i) {
		$val = $Ticket->Load($args);
		unless ($val) {
		    $ResultsMessage .= 
		      "ERROR: Couldn't load ticket '$1': $msg.\n".
			"Aborting to avoid unintended ticket modifications.\n".
			  "The following commands were not proccessed:\n\n".
			    join("\n", at actions);
		    return($ResultsMessage);
		}
		$ResultsMessage .= "Ticket ".$Ticket->Id." loaded";
	    }
	    else {
		unless ($Ticket->Id) {
		    $ResultsMessage .= "No Ticket specified. Aborting ticket ".
		      "modifications\n\n".
			"The following commands were not proccessed:\n\n".
			  join("\n", at actions);
		    return($ResultsMessage);
		}
		
		# Deal with the basics
		if ($command =~ /^(Subject|Owner|Status|Queue)$/i) {
		    my $method = 'Set' . ucfirst(lc($1));
		    ($val, $msg) = $Ticket->$method($args);
		}	
		# Deal with the dates
		elsif ($command =~ /^(due|starts|started|resolved)$/i) {
		    my $method = 'Set'. ucfirst(lc($1));
		    my $date = new RT::Date($CurrentUser);
		    $date->Set( Format => 'unknown', Value => $args);
		    ($val, $msg) = $Ticket->$method($date->ISO);
		}
		
		# Deal with the watchers
		elsif ($command =~ /^(requestor|requestors|cc|admincc)$/i) {
		    my $operator = "+";
		    my ($type);
		    if ($args =~ /^(\+|\-)(.*)$/) {
			$operator = $1;
			$args = $2;
		    }  
		    $type = 'Requestor' if ($command =~ /^requestor/i) ;
		    $type = 'Cc' if ($command =~ /^cc/i) ;
		    $type = 'AdminCc' if ($command =~ /^admincc/i) ;
		    
		    if ($operator eq '+') {
		  	($val, $msg) = $Ticket->AddWatcher( Type => $type,
							    Email => $args);
		    } elsif ($operator eq '-') {
		  	($val, $msg) = $Ticket->DeleteWatcher( Type => $type,
							       Email => $args);
		    }
		}
		
		
		# {{{ Deal with ticket keywords
		else {
		    #Default is to add keywords
		    my $op = '+';
		    my $ks = $Ticket->QueueObj->KeywordSelect($command);
		    
		    unless ($ks->Id) {
			$ResultsMessage .= "ERROR: couldn't find a keyword ".
			  "selection matching '$command'\n";
			next;
		    }
		    
		    if ($args =~ /^(\-|\+)(.*)$/) {
			$op = $1;
			$args = $2;
		    }
		    my $kids = $ks->KeywordObj->Descendents;
		    
		    #TODO: looping is lossy.
		    foreach my $kid (keys %{$kids}) {
			next unless ($kids->{$kid} =~ /^$args$/i);
			if ($op eq '-') {
			    ($val, $msg) = 
			      $Ticket->DeleteKeyword(KeywordSelect => $ks->id,
						     Keyword => $kid);
			}
			elsif ($op eq '+') {
			    ($val, $msg) =
			      $Ticket->AddKeyword(KeywordSelect => $ks->id,
						  Keyword => $kid);
			}
			else {
			    $msg = "'$op' is not a valid operator.\n";
			}
			
		    }
		}
	    }
	    
	    # }}}
	    
	    
	    
	    $ResultsMessage .= $msg." succeeded\n";
	    
	}
	
	else {
	    $ResultsMessage .= "Command not understood!\n";
	}
	
    }	
    return ($ResultsMessage);
    
}

# }}}

# {{{ sub ParsePseudoHeadersForNewTicket

sub ParsePseudoHeadersForNewTicket {
    my $PseudoHeaders = shift;
    my %commandvalues = (queue => undef,
			 subject => undef,
			 status => undef,
			 owner => undef,
			 due => undef,
			 requestor => undef,
			 cc => undef,
			 admincc => undef,
			 @_);
    my @headers = split('\n',$PseudoHeaders);
    foreach my $action (@headers) {
	if ($action =~ /^RT-(.*?):\s+(.*)$/) {
	    my $command = $1;
	    my $args = $2;	
	    
	    if ($command =~ 
		/^(owner|priority|finalpriority|status|queue|subject)$/i) {
		my $attrib = lc ($1);
		$commandvalues{$attrib} = $args;
	    } 
	    elsif ($command =~ /^due$/i) {
		my $date = new RT::Date($CurrentUser);
		$date->Set( Format => 'unknown', Value => $args);	    
		$commandvalues{'due'} = $date->ISO;
	    } 
	    elsif ($command =~ /^(requestor|cc|admincc)$/i) {
		$args =~ s/^\+//; #Remove leading + signs. They just don't mean anything
		                  # in this context
		push @{$commandvalues{lc($command)}}, $args;
	    }
	    #Deal with keywords
	    else {
		$commandvalues{'keywords'}{$command}{$args} = 1;
	    }	
	    
	}
    }	
    return (\%commandvalues);
    
}

# }}}

# {{{ sub GetFirstBodyHandle

# When Handed a MIME::Entity, recurses through its parts until it finds
# a body part. returns a reference to that MIME::Body 

sub GetFirstBodyHandle {
    my $MIMEObj = shift;
    if ($MIMEObj->parts() || $MIMEObj->mime_type =~ /^multipart/) {
	return (GetFirstBodyHandle($MIMEObj->parts(0)));
    }	
    else {
	return ($MIMEObj->bodyhandle());
    }
    
}
# }}} 

# }}}

1;

------8<------8<------8<------8<------8<------8<------8<------8<------




More information about the rt-users mailing list