2.0 add-ons - Enhanced mailgate: enhancement

Kev,
Thanks for the patch and the writeup. I’d recommend you send
this to rt-users@lists.fsck.com and rt-2.0-addons@fsck.com, if
you’re up for it, so others can see the fruits of your labor.

Thanks!

Jesse

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@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”,@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@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 #] 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("",@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",@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",@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<------