[rt-users] Searching ticket subjects

Kenneth Marshall ktm at rice.edu
Tue Jul 12 14:33:18 EDT 2016


On Tue, Jul 12, 2016 at 11:06:44PM +0530, Nilesh wrote:
> ... 
> > > > Does the default RT strip that off? Because I'm not seeing in one recent
> > > > duplicate created in the manner I described.
> > > > 
> > > Ah it's hidden inside 'show full headers'. This is interesting. Is there
> > > some documentation about this header?
> > Hi,
> > 
> > You can look at the RFC's, but that was the header that provided the most
> > value in reducing duplicate tickets.
> > 
> > Regards,
> > Ken
> 
> As this discussion is going on a few duplicate tickets were created in the
> system and this definitely looks like a good way to solve it. Did you use
> ExtractCustomFields module to get this done? 
> 
> I'm thinking of doing it like this:
> Run ExtractCustomFields on every correspond to get the Message ID out of the
> headers. Then I can search through this field probably using a customized rt-
> mailgate. Does that sound good? I don't think there's a way using scrips because
> by the time scrip executes ticket is already created (unless something is
> possible in the "On transaction" condition).
> 

Hi Nilesh,

I have attached my current version of ./local/lib/RT/Interface/Email_Local.pm.
It includes our modifications gathered from the list recommendations in the function

ParseInReplyTo()

This patch is against RT 3.8.13+ and has not been updated to RT 4+ but is should
be a straight-forward update. To make your own version of Email_Local.pm, start
with just the function ParseInReplyTo() and the calling function and add functions
from ./lib/RT/Interface/Email.pm until you have a working file. That will be the
minimum that you will need. Take a look at http://requesttracker.wikia.com/wiki/Customizing
for more details. Let me know if you have any questions.

Regards,
Ken
-------------- next part --------------
package RT::Interface::Email;

use strict;
no warnings qw(redefine);

use Email::Address;
use MIME::Entity;
use RT::EmailParser;
use File::Temp;
use UNIVERSAL::require;
use Mail::Mailer ();
use Text::ParseWords qw/shellwords/;

sub ParseTicketId {
    my $Subject = shift;

    my $rtname = RT->Config->Get('rtname');
    my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;

    my $id;
    if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
        $id = $1;
    } else {
        foreach my $tag ( RT->System->SubjectTag ) {
            next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
            $id = $1;
            last;
        }
    }
    return undef unless $id;

    $RT::Logger->debug("Found a ticket ID. It's $id");
    return $id;
}

sub ExtractTicketId {
    my $entity = shift;

    my $subject = $entity->head->get('Subject') || '';
    chomp $subject;
    return ParseTicketId( $subject );
}

sub ParseInReplyTo {
    my $MessageId = shift;
    $MessageId =~ s/<(.+)>/$1/;

    my $id;
    if ($MessageId ne '') {
        $RT::Logger->debug("Looking for matching ticket using In-Reply-To: $MessageId");
        my $query = "SELECT ObjectId" .
                    "  FROM Transactions JOIN Attachments" .
                    "    ON Attachments.TransactionId = Transactions.id" .
                    " WHERE ObjectType = 'RT::Ticket'" .
                    "   AND MessageId = ?;";

        my @result = $RT::Handle->FetchResult($query, $MessageId);

        if ( $result[0] =~ /^\d+$/ ) {
            $id = $result[0];
            $RT::Logger->debug("Found a ticket ID using In-Reply-To header. It's $id");
        }
    }
    return undef unless defined($id);

    return $id;
}

sub ParseCcAddressesFromHead {
    my %args = (
        Head        => undef,
        QueueObj    => undef,
        CurrentUser => undef,
        @_
    );

    my @recipients =
        map lc $_->address,
        map Email::Address->parse( $args{'Head'}->get( $_ ) ),
        qw(To Cc);

    my @res;
    foreach my $address ( @recipients ) {
        $address = $args{'CurrentUser'}->UserObj->CanonicalizeEmailAddress( $address );
        next if lc $args{'CurrentUser'}->EmailAddress   eq $address;
        next if lc $args{'QueueObj'}->CorrespondAddress eq $address;
        next if lc $args{'QueueObj'}->CommentAddress    eq $address;
        next if RT::EmailParser->IsRTAddress( $address );

        push @res, $address;
   }

    #
    # Limit the number of Cc addresses that we add to a
    # ticket during the initial create to minimize damage
    # to our Email reputation when SPAM slips through DSPAM.

    $RT::Logger->debug("$#res Ccs");
    if ( $#res > 3 ) {
        my @riceCc;
        my @nonriceCc;
        @riceCc = grep /rice.edu/i, @res;
        @nonriceCc = grep !/rice.edu/i, @res;
        $RT::Logger->debug("$#riceCc riceCcs, $#nonriceCc nonriceCcs");
        if ($#nonriceCc > 1) {
            @res = (@riceCc, @nonriceCc[0]);
        }
    }

    return @res;
}

sub Gateway {
    my $argsref = shift;
    my %args    = (
        action  => 'correspond',
        queue   => '1',
        ticket  => undef,
        message => undef,
        %$argsref
    );

    my $SystemTicket;
    my $Right;

    # Validate the action
    my ( $status, @actions ) = IsCorrectAction( $args{'action'} );
    unless ($status) {
        return (
            -75,
            "Invalid 'action' parameter "
                . $actions[0]
                . " for queue "
                . $args{'queue'},
            undef
        );
    }

    my $parser = RT::EmailParser->new();
    $parser->SmartParseMIMEEntityFromScalar(
        Message => $args{'message'},
        Decode => 0,
        Exact => 1,
    );

    my $Message = $parser->Entity();
    unless ($Message) {
        MailError(
            Subject     => "RT Bounce: Unparseable message",
            Explanation => "RT couldn't process the message below",
            Attach      => $args{'message'}
        );

        return ( 0,
            "Failed to parse this message. Something is likely badly wrong with the message"
        );
    }

    my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
    push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
    @mail_plugins = _LoadPlugins( @mail_plugins );

    my %skip_plugin;
    foreach my $class( grep !ref, @mail_plugins ) {
        # check if we should apply filter before decoding
        my $check_cb = do {
            no strict 'refs';
            *{ $class . "::ApplyBeforeDecode" }{CODE};
        };
        next unless defined $check_cb;
        next unless $check_cb->(
            Message       => $Message,
            RawMessageRef => \$args{'message'},
        );

        $skip_plugin{ $class }++;

        my $Code = do {
            no strict 'refs';
            *{ $class . "::GetCurrentUser" }{CODE};
        };
        my ($status, $msg) = $Code->(
            Message       => $Message,
            RawMessageRef => \$args{'message'},
        );
        next if $status > 0;

        if ( $status == -2 ) {
            return (1, $msg, undef);
        } elsif ( $status == -1 ) {
            return (0, $msg, undef);
        }
    }
    @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
    $parser->_DecodeBodies;
    $parser->_PostProcessNewEntity;

    my $head = $Message->head;
    my $ErrorsTo = ParseErrorsToAddressFromHead( $head );

    my $MessageId = $head->get('Message-ID')
        || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';

    #Pull apart the subject line
    my $Subject = $head->get('Subject') || '';
    chomp $Subject;
    
    # Lets check for mail loops of various sorts.
    my ($should_store_machine_generated_message, $IsALoop, $result);
    ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) =
      _HandleMachineGeneratedMail(
        Message  => $Message,
        ErrorsTo => $ErrorsTo,
        Subject  => $Subject,
        MessageId => $MessageId
    );

    # Do not pass loop messages to MailPlugins, to make sure the loop
    # is broken, unless $RT::StoreLoops is set.
    if ($IsALoop && !$should_store_machine_generated_message) {
        return ( 0, $result, undef );
    }
    # }}}

    my $InReplyToMessageId = $head->get('In-Reply-To') || '';
    chomp($InReplyToMessageId);

    $args{'ticket'} ||= ExtractTicketId( $Message );
    $args{'ticket'} ||= ParseInReplyTo( $InReplyToMessageId );

    # ExtractTicketId may have been overridden, and edited the Subject
    my $NewSubject = $Message->head->get('Subject');
    chomp $NewSubject;

    $SystemTicket = RT::Ticket->new( RT->SystemUser );
    $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
    if ( $SystemTicket->id ) {
        $Right = 'ReplyToTicket';
    } else {
        $Right = 'CreateTicket';
    }

    #Set up a queue object
    my $SystemQueueObj = RT::Queue->new( RT->SystemUser );
    $SystemQueueObj->Load( $args{'queue'} );

    # We can safely have no queue of we have a known-good ticket
    unless ( $SystemTicket->id || $SystemQueueObj->id ) {
        return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
    }

    my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
        MailPlugins   => \@mail_plugins,
        Actions       => \@actions,
        Message       => $Message,
        RawMessageRef => \$args{message},
        SystemTicket  => $SystemTicket,
        SystemQueue   => $SystemQueueObj,
    );

    # If authentication fails and no new user was created, get out.
    if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {

        # If the plugins refused to create one, they lose.
        unless ( $AuthStat == -1 ) {
            _NoAuthorizedUserFound(
                Right     => $Right,
                Message   => $Message,
                Requestor => $ErrorsTo,
                Queue     => $args{'queue'}
            );

        }
        return ( 0, "Could not load a valid user", undef );
    }

    # If we got a user, but they don't have the right to say things
    if ( $AuthStat == 0 ) {
        MailError(
            To          => $ErrorsTo,
            Subject     => "Permission Denied",
            Explanation =>
                "You do not have permission to communicate with RT",
            MIMEObj => $Message
        );
        return (
            0,
            "$ErrorsTo tried to submit a message to "
                . $args{'Queue'}
                . " without permission.",
            undef
        );
    }


    unless ($should_store_machine_generated_message) {
        return ( 0, $result, undef );
    }

    # if plugin's updated SystemTicket then update arguments
    $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;

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

    if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions )
    {

        my @Cc;
        my @Requestors = ( $CurrentUser->id );

        if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
            @Cc = ParseCcAddressesFromHead(
                Head        => $head,
                CurrentUser => $CurrentUser,
                QueueObj    => $SystemQueueObj
            );
        }

        my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
            Queue     => $SystemQueueObj->Id,
            Subject   => $NewSubject,
            Requestor => \@Requestors,
            Cc        => \@Cc,
            MIMEObj   => $Message
        );
        if ( $id == 0 ) {
            MailError(
                To          => $ErrorsTo,
                Subject     => "Ticket creation failed: $Subject",
                Explanation => $ErrStr,
                MIMEObj     => $Message
            );
            return ( 0, "Ticket creation failed: $ErrStr", $Ticket );
        }

        # strip comments&corresponds from the actions we don't need
        # to record them if we've created the ticket just now
        @actions = grep !/^(comment|correspond)$/, @actions;
        $args{'ticket'} = $id;

    } elsif ( $args{'ticket'} ) {

        $Ticket->Load( $args{'ticket'} );
        unless ( $Ticket->Id ) {
            my $error = "Could not find a ticket with id " . $args{'ticket'};
            MailError(
                To          => $ErrorsTo,
                Subject     => "Message not recorded: $Subject",
                Explanation => $error,
                MIMEObj     => $Message
            );

            return ( 0, $error );
        }
        $args{'ticket'} = $Ticket->id;
    } else {
        return ( 1, "Success", $Ticket );
    }

    # }}}

    my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
    foreach my $action (@actions) {

        #   If the action is comment, add a comment.
        if ( $action =~ /^(?:comment|correspond)$/i ) {
            my $method = ucfirst lc $action;
            my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message );
            unless ($status) {

                #Warn the sender that we couldn't actually submit the comment.
                MailError(
                    To          => $ErrorsTo,
                    Subject     => "Message not recorded: $Subject",
                    Explanation => $msg,
                    MIMEObj     => $Message
                );
                return ( 0, "Message not recorded: $msg", $Ticket );
            }
        } elsif ($unsafe_actions) {
            my ( $status, $msg ) = _RunUnsafeAction(
                Action      => $action,
                ErrorsTo    => $ErrorsTo,
                Message     => $Message,
                Ticket      => $Ticket,
                CurrentUser => $CurrentUser,
            );
            return ($status, $msg, $Ticket) unless $status == 1;
        }
    }
    return ( 1, "Success", $Ticket );
}

1;


More information about the rt-users mailing list