[Bps-public-commit] RT-Extension-CommandByMail branch, 4.4/updates-for-44, created. 1.00-7-gb6f86dc

Jim Brandt jbrandt at bestpractical.com
Tue Mar 29 10:15:33 EDT 2016


The branch, 4.4/updates-for-44 has been created
        at  b6f86dc3c8034679bd56637f008e655bde340264 (commit)

- Log -----------------------------------------------------------------
commit a14e65a15f3c59b61fa63e97ed45486993dc0b4a
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Mar 28 13:43:37 2016 -0400

    Initial refactor to prepare for 4.4 compatibility
    
    Move the main functionality for processing commands into
    the main CommandByMail module to make it accessible for
    new Handle* hooks to be added for RT 4.4 compatibility.

diff --git a/lib/RT/Extension/CommandByMail.pm b/lib/RT/Extension/CommandByMail.pm
index 882f2b9..a0e6077 100644
--- a/lib/RT/Extension/CommandByMail.pm
+++ b/lib/RT/Extension/CommandByMail.pm
@@ -1,17 +1,25 @@
 use 5.008003;
 package RT::Extension::CommandByMail;
 
-our $VERSION = '1.00';
+use RT::Interface::Email qw(ParseCcAddressesFromHead);
 
-1;
-__END__
+our @REGULAR_ATTRIBUTES = qw(Queue Owner Subject Status Priority FinalPriority);
+our @TIME_ATTRIBUTES    = qw(TimeWorked TimeLeft TimeEstimated);
+our @DATE_ATTRIBUTES    = qw(Due Starts Started Resolved Told);
+our @LINK_ATTRIBUTES    = qw(MemberOf Parents Members Children
+            HasMember RefersTo ReferredToBy DependsOn DependedOnBy);
+our @WATCHER_ATTRIBUTES = qw(Requestor Cc AdminCc);
+
+our $VERSION = '1.00';
 
 =head1 NAME
 
-RT::Extension::CommandByMail - Change metadata of ticket via email
+RT::Extension::CommandByMail - Change ticket metadata via email
 
 =head1 SYNOPSIS
 
+    (Send email with content that looks like the following.)
+
     Status: stalled
     Subject: change subject
     AddAdminCc: boss at example.com
@@ -20,22 +28,6 @@ RT::Extension::CommandByMail - Change metadata of ticket via email
 
     The comment/reply text goes here
 
-=head1 DESCRIPTION
-
-This extension allows you to manage tickets via email interface.  You
-may put commands into the beginning of a mail, and extension will apply
-them. See the list of commands in the
-L<RT::Interface::Email::Filter::TakeAction> docs.
-
-B<CAVEAT:> commands are line oriented, so you can't expand to multiple
-lines for each command, i.e. values can't contains new lines. The module
-also currently expects and parses text, not HTML.
-
-=head1 SECURITY
-
-This extension has no extended auth system; so all security issues that
-apply to the RT in general also apply to the extension.
-
 =head1 INSTALLATION
 
 =over
@@ -60,7 +52,7 @@ For RT 4.0, add this line:
 
 or add C<RT::Extension::CommandByMail> to your existing C<@Plugins> line.
 
-Regardless of which version of RT, also C<Filter::TakeAction> to your
+For RT 4.2 or older, also add C<Filter::TakeAction> to your
 C<@MailPlugins> configuration, as follows:
 
     Set(@MailPlugins, qw(Auth::MailFrom Filter::TakeAction));
@@ -71,6 +63,146 @@ Be sure to include C<Auth::MailFrom> in the list as well.
 
 =back
 
+=head1 DESCRIPTION
+
+This extension allows you to manage tickets via email interface.  You
+may put commands into the beginning of an email, and the extension will apply
+them. The list of commands is detailed below.
+
+B<CAVEAT:> commands are line oriented, so you can't expand to multiple
+lines for each command, i.e. values can't contains new lines. The module
+also currently expects and parses text, not HTML.
+
+=head2 FORMAT
+
+This extension parses the body and headers of incoming messages for
+list commands. Format of commands is:
+
+    Command: value
+    Command: value
+    ...
+
+You can find list of L</COMMANDS commands below>.
+
+Some commands (like Status, Queue and other) can be used only once. Commands
+that manage lists can be used multiple times, for example link, custom fields
+and watchers commands. Also, the latter can be used with C<Add> and C<Del>
+prefixes to add/delete values from the current list of the ticket you reply to
+or comment on.
+
+=head2 COMMANDS
+
+=head3 Basic
+
+=over 4
+
+=item Queue: <name>
+
+Set new queue for the ticket
+
+=item Subject: <string>
+
+Set new subject to the given string
+
+=item Status: <status>
+
+Set new status, one of new, open, stalled, resolved, rejected or deleted
+
+=item Owner: <username>
+
+Set new owner using the given username
+
+=item Priority: <#>
+
+Set new priority to the given value
+
+=item FinalPriority: <#>
+
+Set new final priority to the given value
+
+=back
+
+=head3 Dates
+
+Set new date/timestamp, or 0 to unset:
+
+    Due: <new timestamp>
+    Starts: <new timestamp>
+    Started: <new timestamp>
+
+=head3 Time
+
+Set new times to the given value in minutes. Note that
+on correspond/comment B<< C<TimeWorked> add time >> to the current
+value.
+
+    TimeWorked: <minutes>
+    TimeEstimated: <minutes>
+    TimeLeft: <minutes>
+
+=head3 Watchers
+
+Manage watchers: requestors, ccs and admin ccs. This commands
+can be used several times and/or with C<Add> and C<Del> prefixes,
+for example C<Requestor> comand set requestor(s) and the current
+requestors would be deleted, but C<AddRequestor> command adds
+to the current list.
+
+    Requestor: <address> Set requestor(s) using the email address
+    AddRequestor: <address> Add new requestor using the email address
+    DelRequestor: <address> Remove email address as requestor
+    Cc: <address> Set Cc watcher(s) using the email address
+    AddCc: <address> Add new Cc watcher using the email address
+    DelCc: <address> Remove email address as Cc watcher
+    AdminCc: <address> Set AdminCc watcher(s) using the email address
+    AddAdminCc: <address> Add new AdminCc watcher using the email address
+    DelAdminCc: <address> Remove email address as AdminCc watcher
+
+=head3 Links
+
+Manage links. These commands are also could be used several times in one
+message.
+
+    DependsOn: <ticket id>
+    DependedOnBy: <ticket id>
+    RefersTo: <ticket id>
+    ReferredToBy: <ticket id>
+    Members: <ticket id>
+    MemberOf: <ticket id>
+
+=head3 Custom field values
+
+Manage custom field values. Could be used multiple times.  (The curly braces
+are literal.)
+
+    CustomField.{<CFName>}: <custom field value>
+    AddCustomField.{<CFName>}: <custom field value>
+    DelCustomField.{<CFName>}: <custom field value>
+
+Short forms:
+
+    CF.{<CFName>}: <custom field value>
+    AddCF.{<CFName>}: <custom field value>
+    DelCF.{<CFName>}: <custom field value>
+
+=head3 Transaction Custom field values
+
+Manage custom field values of transactions. Could be used multiple times.  (The curly braces
+are literal.)
+
+    TransactionCustomField.{<CFName>}: <custom field value>
+
+Short forms:
+
+    TxnCustomField.{<CFName>}: <custom field value>
+    TransactionCF.{<CFName>}: <custom field value>
+    TxnCF.{<CFName>}: <custom field value>
+
+=head1 SECURITY
+
+This extension has no extended auth system; so all security issues that
+apply to the RT in general also apply to the extension.
+
 =head1 CONFIGURATION
 
 =head2 C<$CommandByMailGroup>
@@ -89,20 +221,641 @@ as well.  For example:
 
 If set, the body will not be examined, only the headers.
 
-=head1 COMMANDS
+=head1 CAVEATS
 
-This extension parses the body and headers of incoming messages
-for list commands. Format of commands is:
+This extension is incompatible with C<UnsafeEmailCommands> RT option.
 
-    Command: value
-    Command: value
-    ...
+=cut
 
-See the list of commands in the L<RT::Interface::Email::Filter::TakeAction> docs.
+sub ProcessCommands {
+    my %args = (
+        Message       => undef,
+        RawMessageRef => undef,
+        CurrentUser   => undef,
+        AuthLevel     => undef,
+        Action        => undef,
+        Ticket        => undef,
+        Queue         => undef,
+        @_
+    );
+
+    unless ( $args{'CurrentUser'} && $args{'CurrentUser'}->Id ) {
+        $RT::Logger->error(
+            "CommandByMail executed when "
+            ."CurrentUser (actor) is not authorized. "
+        );
+        return { CurrentUser => $args{'CurrentUser'},
+                 AuthLevel   => $args{'AuthLevel'} };
+    }
+
+    # If the user isn't asking for a comment or a correspond,
+    # bail out
+    unless ( $args{'Action'} =~ /^(?:comment|correspond)$/i ) {
+        return { CurrentUser => $args{'CurrentUser'},
+                 AuthLevel   => $args{'AuthLevel'} };
+    }
+
+    # If only a particular group may perform commands by mail,
+    # bail out
+    my $new_config = RT->can('Config') && RT->Config->can('Get');
+    my $group_id = $new_config
+                 ? RT->Config->Get('CommandByMailGroup')
+                 : $RT::CommandByMailGroup;
+
+    if (defined $group_id) {
+        my $group = RT::Group->new($args{'CurrentUser'});
+        $group->Load($group_id);
+
+        if (!$group->HasMemberRecursively($args{'CurrentUser'}->PrincipalObj)) {
+            $RT::Logger->debug("CurrentUser not in CommandByMailGroup");
+            return { CurrentUser => $args{'CurrentUser'},
+                     AuthLevel   => $args{'AuthLevel'} };
+        }
+    }
+
+    $RT::Logger->debug("Running CommandByMail as ".$args{'CurrentUser'}->UserObj->Name);
+
+    my $headername = $new_config
+        ? RT->Config->Get('CommandByMailHeader')
+        : $RT::CommandByMailHeader;
+
+    my $only_headers = $new_config
+        ? RT->Config->Get('CommandByMailOnlyHeaders')
+        : $RT::CommandByMailOnlyHeaders;
+
+    # find the content
+    my @content = ();
+    my @parts = $only_headers ? () : $args{'Message'}->parts_DFS;
+    foreach my $part (@parts) {
+        my $body = $part->bodyhandle or next;
+
+        #if it looks like it has pseudoheaders, that's our content
+        if ( $body->as_string =~ /^(?:\S+)(?:{.*})?:/m ) {
+            @content = $body->as_lines;
+            last;
+        }
+    }
+
+    if (defined $headername) {
+        unshift @content, $args{'Message'}->head->get_all($headername);
+    }
+
+    my @items;
+    my $found_pseudoheaders = 0;
+    foreach my $line (@content) {
+        next if $line =~ /^\s*$/ && ! $found_pseudoheaders;
+        last if $line !~ /^(?:(\S+(?:{.*})?)\s*?:\s*?(.*)\s*?|)$/;
+        last if not defined $1 and $found_pseudoheaders;
+        next if not defined $1;
+
+        $found_pseudoheaders = 1;
+        push( @items, $1 => $2 );
+        $RT::Logger->debug("Found pseudoheader: $1 => $2");
+    }
+    my %cmds;
+    while ( my $key = _CanonicalizeCommand( shift @items ) ) {
+        my $val = shift @items;
+        # strip leading and trailing spaces
+        $val =~ s/^\s+|\s+$//g;
+        $RT::Logger->debug("Got command $key => $val");
+
+        if ( exists $cmds{$key} ) {
+            $cmds{$key} = [ $cmds{$key} ] unless ref $cmds{$key};
+            push @{ $cmds{$key} }, $val;
+        } else {
+            $cmds{$key} = $val;
+        }
+    }
+
+    my %results;
+
+    foreach my $cmd ( keys %cmds ) {
+        my ($val, $msg) = _CheckCommand( $cmd );
+        unless ( $val ) {
+            $results{ $cmd } = {
+                value   => delete $cmds{ $cmd },
+                result  => $val,
+                message => $msg,
+            };
+        }
+    }
+
+    my $ticket_as_user = RT::Ticket->new( $args{'CurrentUser'} );
+    my $queue          = RT::Queue->new( $args{'CurrentUser'} );
+    if ( $cmds{'queue'} ) {
+        $queue->Load( $cmds{'queue'} );
+    }
+
+    if ( !$queue->id ) {
+        $queue->Load( $args{'Queue'}->id );
+    }
+
+    my $transaction;
+
+    # If we're updating.
+    if ( $args{'Ticket'}->id ) {
+        $ticket_as_user->Load( $args{'Ticket'}->id );
+        $RT::Logger->debug("Updating Ticket ".$ticket_as_user->Id." in Queue ".$queue->Name);
+
+        # we set status later as correspond can reopen ticket
+        foreach my $attribute (grep !/^(Status|TimeWorked)/, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
+            next unless defined $cmds{ lc $attribute };
+            next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
+
+            # canonicalize owner -- accept an e-mail address
+            if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
+                my $user = RT::User->new($RT::SystemUser);
+                $user->LoadByEmail( $cmds{ lc $attribute } );
+                $cmds{ lc $attribute } = $user->Name if $user->id;
+            }
+
+            _SetAttribute(
+                $ticket_as_user,        $attribute,
+                $cmds{ lc $attribute }, \%results
+            );
+        }
+
+        # we want the queue the ticket is currently in, not the queue
+        # that was passed to rt-mailgate, otherwise we can't find the
+        # proper set of Custom Fields.  But, we have to do this after 
+        # we potentially update the Queue from @REGULAR_ATTRIBUTES
+        $queue = $ticket_as_user->QueueObj();
+
+        foreach my $attribute (@DATE_ATTRIBUTES) {
+            next unless ( $cmds{ lc $attribute } );
+
+            my $date = RT::Date->new( $args{'CurrentUser'} );
+            $date->Set(
+                Format => 'unknown',
+                Value  => $cmds{ lc $attribute },
+            );
+            _SetAttribute( $ticket_as_user, $attribute, $date->ISO,
+                \%results );
+            $results{ $attribute }->{value} = $cmds{ lc $attribute };
+        }
+
+        foreach my $type ( @WATCHER_ATTRIBUTES ) {
+            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
+            next unless keys %tmp;
+
+            $tmp{'Default'} = [ do {
+                my $method = $type;
+                $method .= 's' if $type eq 'Requestor';
+                $args{'Ticket'}->$method->MemberEmailAddresses;
+            } ];
+            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
+            foreach my $text ( @$del ) {
+                my $user = RT::User->new($RT::SystemUser);
+                $user->LoadByEmail($text) if $text =~ /\@/;
+                $user->Load($text) unless $user->id;
+                my ( $val, $msg ) = $ticket_as_user->DeleteWatcher(
+                    Type  => $type,
+                    PrincipalId => $user->PrincipalId,
+                );
+                push @{ $results{ 'Del'. $type } }, {
+                    value   => $text,
+                    result  => $val,
+                    message => $msg
+                };
+            }
+            foreach my $text ( @$add ) {
+                my $user = RT::User->new($RT::SystemUser);
+                $user->LoadByEmail($text) if $text =~ /\@/;
+                $user->Load($text) unless $user->id;
+                my ( $val, $msg ) = $ticket_as_user->AddWatcher(
+                    Type  => $type,
+                    $user->id
+                        ? (PrincipalId => $user->PrincipalId)
+                        : (Email => $text)
+                    ,
+                );
+                push @{ $results{ 'Add'. $type } }, {
+                    value   => $text,
+                    result  => $val,
+                    message => $msg
+                };
+            }
+        }
+
+        {
+            my $time_taken = 0;
+            if (grep $_ eq 'TimeWorked', @TIME_ATTRIBUTES) {
+                if (ref $cmds{'timeworked'}) { 
+                    map { $time_taken += ($_ || 0) }  @{ $cmds{'timeworked'} };
+                    $RT::Logger->debug("Time taken: $time_taken");
+                }
+                else {
+                    $time_taken = $cmds{'timeworked'} || 0;
+                }
+            }
+
+            my $method = ucfirst $args{'Action'};
+            (my $status, my $msg, $transaction) = $ticket_as_user->$method(
+                TimeTaken => $time_taken,
+                MIMEObj   => $args{'Message'},
+            );
+            unless ( $status ) {
+                $RT::Logger->warning(
+                    "Couldn't write $args{'Action'}."
+                    ." Fallback to standard mailgate. Error: $msg");
+                return { CurrentUser => $args{'CurrentUser'},
+                         AuthLevel   => $args{'AuthLevel'} };
+            }
+        }
+
+        foreach my $type ( @LINK_ATTRIBUTES ) {
+            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
+            next unless keys %tmp;
+
+            my $typemap   = keys %RT::Link::TYPEMAP ? \%RT::Link::TYPEMAP : $ticket_as_user->LINKTYPEMAP;
+            my $link_type = $typemap->{$type}->{'Type'};
+            my $link_mode = $typemap->{$type}->{'Mode'};
+
+            $tmp{'Default'} = [ do {
+                my %h = ( Base => 'Target', Target => 'Base' );
+                my $links = $args{'Ticket'}->_Links( $h{$link_mode}, $link_type );
+                my @res;
+                while ( my $link = $links->Next ) {
+                    my $method = $link_mode .'URI';
+                    my $uri = $link->$method();
+                    next unless $uri->IsLocal;
+                    push @res, $uri->Object->Id;
+                }
+                @res;
+            } ];
+            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
+            foreach ( @$del ) {
+                my ($val, $msg) = $ticket_as_user->DeleteLink(
+                    Type => $link_type,
+                    $link_mode => $_,
+                );
+                $results{ 'Del'. $type } = {
+                    value => $_,
+                    result => $val,
+                    message => $msg,
+                };
+            }
+            foreach ( @$add ) {
+                my ($val, $msg) = $ticket_as_user->AddLink(
+                    Type => $link_type,
+                    $link_mode => $_,
+                );
+                $results{ 'Add'. $type } = {
+                    value => $_,
+                    result => $val,
+                    message => $msg,
+                };
+            }
+        }
+
+        my $custom_fields = $queue->TicketCustomFields;
+        while ( my $cf = $custom_fields->Next ) {
+            my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
+            next unless keys %tmp;
+
+            $tmp{'Default'} = [ do {
+                my $values = $args{'Ticket'}->CustomFieldValues( $cf->id );
+                my @res;
+                while ( my $value = $values->Next ) {
+                    push @res, $value->Content;
+                }
+                @res;
+            } ];
+            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
+            foreach ( @$del ) {
+                my ( $val, $msg ) = $ticket_as_user->DeleteCustomFieldValue(
+                    Field => $cf->id,
+                    Value => $_
+                );
+                $results{ "DelCustomField{". $cf->Name ."}" } = {
+                    value => $_,
+                    result => $val,
+                    message => $msg,
+                };
+            }
+            foreach ( @$add ) {
+                my ( $val, $msg ) = $ticket_as_user->AddCustomFieldValue(
+                    Field => $cf->id,
+                    Value => $_
+                );
+                $results{ "DelCustomField{". $cf->Name ."}" } = {
+                    value => $_,
+                    result => $val,
+                    message => $msg,
+                };
+            }
+        }
+
+        foreach my $attribute (grep $_ eq 'Status', @REGULAR_ATTRIBUTES) {
+            next unless defined $cmds{ lc $attribute };
+            next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
+
+            _SetAttribute(
+                $ticket_as_user,        $attribute,
+                lc $cmds{ lc $attribute }, \%results
+            );
+        }
+
+    } else {
+
+        my %create_args = ();
+        foreach my $attribute (@REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
+            next unless exists $cmds{ lc $attribute };
+
+            # canonicalize owner -- accept an e-mail address
+            if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
+                my $user = RT::User->new($RT::SystemUser);
+                $user->LoadByEmail( $cmds{ lc $attribute } );
+                $cmds{ lc $attribute } = $user->Name if $user->id;
+            }
+
+            if ( $attribute eq 'TimeWorked' && ref $cmds{ lc $attribute } ) {
+                my $time_taken = 0;
+                map { $time_taken += ($_ || 0) }  @{ $cmds{'timeworked'} };
+                $cmds{'timeworked'} = $time_taken;
+                $RT::Logger->debug("Time taken on create: $time_taken");
+            }
+
+            if ( $attribute eq 'Status' && $cmds{ lc $attribute } ) {
+                $cmds{ lc $attribute } = lc $cmds{ lc $attribute };
+            }
+
+            $create_args{$attribute} = $cmds{ lc $attribute };
+        }
+        foreach my $attribute (@DATE_ATTRIBUTES) {
+            next unless exists $cmds{ lc $attribute };
+            my $date = RT::Date->new( $args{'CurrentUser'} );
+            $date->Set(
+                Format => 'unknown',
+                Value  => $cmds{ lc $attribute }
+            );
+            $create_args{$attribute} = $date->ISO;
+        }
+
+        # Canonicalize links
+        foreach my $type ( @LINK_ATTRIBUTES ) {
+            $create_args{ $type } = [ _CompileAdditiveForCreate( 
+                _ParseAdditiveCommand( \%cmds, 0, $type ),
+            ) ];
+        }
+
+        # Canonicalize custom fields
+        my $custom_fields = $queue->TicketCustomFields;
+        while ( my $cf = $custom_fields->Next ) {
+            my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
+            next unless keys %tmp;
+            $create_args{ 'CustomField-' . $cf->id } = [ _CompileAdditiveForCreate(%tmp) ];
+        }
+
+        # Canonicalize watchers
+        # First of all fetch default values
+        foreach my $type ( @WATCHER_ATTRIBUTES ) {
+            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
+            $tmp{'Default'} = [ $args{'CurrentUser'}->EmailAddress ] if $type eq 'Requestor';
+            $tmp{'Default'} = [
+                ParseCcAddressesFromHead(
+                    Head        => $args{'Message'}->head,
+                    CurrentUser => $args{'CurrentUser'},
+                    QueueObj    => $args{'Queue'},
+                )
+            ] if $type eq 'Cc' && $RT::ParseNewMessageForTicketCcs;
+
+            $create_args{ $type } = [ _CompileAdditiveForCreate( %tmp ) ];
+        }
+
+        # get queue unless mail contain it
+        $create_args{'Queue'} = $args{'Queue'}->Id unless exists $create_args{'Queue'};
+
+        # subject
+        unless ( $create_args{'Subject'} ) {
+            $create_args{'Subject'} = $args{'Message'}->head->get('Subject');
+            chomp $create_args{'Subject'};
+        }
+
+        # If we don't already have a ticket, we're going to create a new
+        # ticket
+
+        my ( $id, $txn_id, $msg ) = $ticket_as_user->Create(
+            %create_args,
+            MIMEObj => $args{'Message'}
+        );
+        unless ( $id ) {
+            $msg = "Couldn't create ticket from message with commands, ".
+                   "fallback to standard mailgate.\n\nError: $msg";
+            $RT::Logger->error( $msg );
+            $results{'Create'} = {
+                result => $id,
+                message => $msg,
+            };
+
+            _ReportResults( Results => \%results, Message => $args{'Message'} );
+
+            return { CurrentUser => $args{'CurrentUser'},
+                     AuthLevel   => $args{'AuthLevel'} };
+        }
+        $transaction = RT::Transaction->new( $ticket_as_user->CurrentUser );
+        $transaction->Load( $txn_id );
+
+    }
+
+    if ( $transaction && $transaction->id ) {
+        my $custom_fields = $transaction->CustomFields;
+        while ( my $cf = $custom_fields->Next ) {
+            my $cmd = 'TransactionCustomField{'. $cf->Name .'}';
+            my @values = ($cmds{ lc $cmd });
+            @values = @{ $values[0] } if ref $values[0] eq 'ARRAY';
+            @values = grep defined && length, @values;
+            next unless @values;
+
+            foreach my $value ( @values ) {
+                my ($status, $msg) = $transaction->AddCustomFieldValue(
+                    Field => $cf->Name, Value => $value,
+                );
+                push @{ $results{ $cmd } ||= [] }, {
+                    value => $value, result => $status, message => $msg,
+                };
+            }
+        }
+    }
+
+    _ReportResults(
+        Ticket => $args{'Ticket'},
+        Results => \%results,
+        Message => $args{'Message'},
+    );
+
+    # make sure ticket is loaded
+    $args{'Ticket'}->Load( $transaction->ObjectId );
+
+    # The -2 magic value is the pre-4.4 method of stopping evaluation
+    # of additional GetCurrentUser methods and telling the Gateway to
+    # return success. See RT::Interface::Email::Gateway in pre-4.4 codebase.
+
+    return { CurrentUser => $args{'CurrentUser'},
+             AuthLevel   => -2,
+             Transaction => $transaction };
+}
+
+sub _ParseAdditiveCommand {
+    my ($cmds, $plural_forms, $base) = @_;
+    my (%res);
+
+    my @types = $base;
+    push @types, $base.'s' if $plural_forms;
+    push @types, 'Add'. $base;
+    push @types, 'Add'. $base .'s' if $plural_forms;
+    push @types, 'Del'. $base;
+    push @types, 'Del'. $base .'s' if $plural_forms;
+
+    foreach my $type ( @types ) {
+        next unless defined $cmds->{lc $type};
+
+        my @values = ref $cmds->{lc $type} eq 'ARRAY'?
+            @{ $cmds->{lc $type} }: $cmds->{lc $type};
+
+        if ( $type =~ /^\Q$base\Es?/ ) {
+            push @{ $res{'Set'} }, @values;
+        } elsif ( $type =~ /^Add/ ) {
+            push @{ $res{'Add'} }, @values;
+        } else {
+            push @{ $res{'Del'} }, @values;
+        }
+    }
+
+    return %res;
+}
+
+sub _CompileAdditiveForCreate {
+    my %cmd = @_;
+
+    unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
+        $cmd{'Default'} = [];
+    } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
+        $cmd{'Default'} = [ $cmd{'Default'} ];
+    }
+
+    my @list;
+    @list = @{ $cmd{'Default'} } unless $cmd{'Set'};
+    @list = @{ $cmd{'Set'} } if $cmd{'Set'};
+    push @list, @{ $cmd{'Add'} } if $cmd{'Add'};
+    if ( $cmd{'Del'} ) {
+        my %seen;
+        $seen{$_} = 1 foreach @{ $cmd{'Del'} };
+        @list = grep !$seen{$_}, @list;
+    }
+    return @list;
+}
+
+sub _CompileAdditiveForUpdate {
+    my %cmd = @_;
+
+    my @new = _CompileAdditiveForCreate( %cmd );
+
+    unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
+        $cmd{'Default'} = [];
+    } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
+        $cmd{'Default'} = [ $cmd{'Default'} ];
+    }
+
+    my ($add, $del);
+    unless ( @{ $cmd{'Default'} } ) {
+        $add = \@new;
+    } elsif ( !@new ) {
+        $del = $cmd{'Default'};
+    } else {
+        my (%cur, %new);
+        $cur{$_} = 1 foreach @{ $cmd{'Default'} };
+        $new{$_} = 1 foreach @new;
+
+        $add = [ grep !$cur{$_}, @new ];
+        $del = [ grep !$new{$_}, @{ $cmd{'Default'} } ];
+    }
+    $_ ||= [] foreach ($add, $del);
+    return ($add, $del);
+}
+
+sub _SetAttribute {
+    my $ticket    = shift;
+    my $attribute = shift;
+    my $value     = shift;
+    my $results   = shift;
+    my $setter    = "Set$attribute";
+    my ( $val, $msg ) = $ticket->$setter($value);
+    $results->{$attribute} = {
+        value   => $value,
+        result  => $val,
+        message => $msg
+    };
+}
+
+sub _CanonicalizeCommand {
+    my $key = shift;
+    return $key unless defined $key;
+
+    $key = lc $key;
+    # CustomField commands
+    $key =~ s/^(add|del|)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/$1customfield{$2}/i;
+    $key =~ s/^(?:transaction|txn)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/transactioncustomfield{$1}/i;
+    return $key;
+}
+
+sub _CheckCommand {
+    my ($cmd, $val) = (lc shift, shift);
+    return 1 if $cmd =~ /^(add|del|)customfield{.*}$/i;
+    return 1 if $cmd =~ /^transactioncustomfield{.*}$/i;
+    if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
+        return 1 unless ref $val;
+        return (0, "Command '$cmd' doesn't support multiple values");
+    }
+    return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
+    if ( $cmd =~ /^(?:add|del)(.*)$/i ) {
+        my $cmd = $1;
+        if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
+            return (0, "Command '$cmd' doesn't support multiple values");
+        }
+        return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
+    }
+
+    return (0, "Command '$cmd' is unknown");
+}
+
+sub _ReportResults {
+    my %args = ( Ticket => undef, Message => undef, Results => {}, @_ );
+
+    my $msg = '';
+    unless ( $args{'Ticket'} && $args{'Ticket'}->id ) {
+        $msg .= $args{'Results'}{'Create'}{'message'} || '';
+        $msg .= "\n" if $msg;
+        delete $args{'Results'}{'Create'};
+    }
+
+    foreach my $key ( keys %{ $args{'Results'} } ) {
+        my @records = ref $args{'Results'}->{ $key } eq 'ARRAY'?
+                         @{$args{'Results'}->{ $key }}: $args{'Results'}->{ $key };
+        foreach my $rec ( @records ) {
+            next if $rec->{'result'};
+            $msg .= "Failed command '". $key .": ". $rec->{'value'} ."'\n";
+            $msg .= "Error message: ". ($rec->{'message'}||"(no message)") ."\n\n";
+        }
+    }
+    return unless $msg && $msg !~ /^\s*$/;
+
+    $RT::Logger->warning( $msg );
+    my $ErrorsTo = RT::Interface::Email::ParseErrorsToAddressFromHead( $args{'Message'}->head );
+    RT::Interface::Email::MailError(
+        To          => $ErrorsTo,
+        Subject     => "Extended mailgate error",
+        Explanation => $msg,
+        MIMEObj     => $args{'Message'},
+        Attach      => $args{'Message'}->as_string,
+    );
+    return;
+}
 
-=head1 CAVEATS
 
-This extension is incompatible with C<UnsafeEmailCommands> RT option.
+
+1;
+__END__
 
 =head1 AUTHOR
 
diff --git a/lib/RT/Interface/Email/Filter/TakeAction.pm b/lib/RT/Interface/Email/Filter/TakeAction.pm
index cb409ae..4553d28 100644
--- a/lib/RT/Interface/Email/Filter/TakeAction.pm
+++ b/lib/RT/Interface/Email/Filter/TakeAction.pm
@@ -3,14 +3,7 @@ package RT::Interface::Email::Filter::TakeAction;
 use warnings;
 use strict;
 
-use RT::Interface::Email qw(ParseCcAddressesFromHead);
-
-our @REGULAR_ATTRIBUTES = qw(Queue Owner Subject Status Priority FinalPriority);
-our @TIME_ATTRIBUTES    = qw(TimeWorked TimeLeft TimeEstimated);
-our @DATE_ATTRIBUTES    = qw(Due Starts Started Resolved Told);
-our @LINK_ATTRIBUTES    = qw(MemberOf Parents Members Children
-            HasMember RefersTo ReferredToBy DependsOn DependedOnBy);
-our @WATCHER_ATTRIBUTES = qw(Requestor Cc AdminCc);
+use RT::Extension::CommandByMail;
 
 =head1 NAME
 
@@ -18,134 +11,20 @@ RT::Interface::Email::Filter::TakeAction - Change metadata of ticket via email
 
 =head1 DESCRIPTION
 
-This extension parses the body and headers of incoming messages for
-list commands. Format of commands is:
-
-    Command: value
-    Command: value
-    ...
-
-You can find list of L</COMMANDS commands below>.
-
-Some commands (like Status, Queue and other) can be used only once. Commands
-that manage lists can be used multiple times, for example link, custom fields
-and watchers commands. Also, the latter can be used with C<Add> and C<Del>
-prefixes to add/delete values from the current list of the ticket you reply to
-or comment on.
-
-=head2 COMMANDS
-
-=head3 Basic
-
-=over 4
-
-=item Queue: <name>
-
-Set new queue for the ticket
-
-=item Subject: <string>
-
-Set new subject to the given string
-
-=item Status: <status>
-
-Set new status, one of new, open, stalled, resolved, rejected or deleted
-
-=item Owner: <username>
-
-Set new owner using the given username
-
-=item Priority: <#>
-
-Set new priority to the given value
-
-=item FinalPriority: <#>
-
-Set new final priority to the given value
-
-=back
-
-=head3 Dates
-
-Set new date/timestamp, or 0 to unset:
-
-    Due: <new timestamp>
-    Starts: <new timestamp>
-    Started: <new timestamp>
-
-=head3 Time
-
-Set new times to the given value in minutes. Note that
-on correspond/comment B<< C<TimeWorked> add time >> to the current
-value.
-
-    TimeWorked: <minutes>
-    TimeEstimated: <minutes>
-    TimeLeft: <minutes>
-
-=head3 Watchers
-
-Manage watchers: requestors, ccs and admin ccs. This commands
-can be used several times and/or with C<Add> and C<Del> prefixes,
-for example C<Requestor> comand set requestor(s) and the current
-requestors would be deleted, but C<AddRequestor> command adds
-to the current list.
-
-    Requestor: <address> Set requestor(s) using the email address
-    AddRequestor: <address> Add new requestor using the email address
-    DelRequestor: <address> Remove email address as requestor
-    Cc: <address> Set Cc watcher(s) using the email address
-    AddCc: <address> Add new Cc watcher using the email address
-    DelCc: <address> Remove email address as Cc watcher
-    AdminCc: <address> Set AdminCc watcher(s) using the email address
-    AddAdminCc: <address> Add new AdminCc watcher using the email address
-    DelAdminCc: <address> Remove email address as AdminCc watcher
-
-=head3 Links
-
-Manage links. These commands are also could be used several times in one
-message.
-
-    DependsOn: <ticket id>
-    DependedOnBy: <ticket id>
-    RefersTo: <ticket id>
-    ReferredToBy: <ticket id>
-    Members: <ticket id>
-    MemberOf: <ticket id>
-
-=head3 Custom field values
-
-Manage custom field values. Could be used multiple times.  (The curly braces
-are literal.)
-
-    CustomField.{<CFName>}: <custom field value>
-    AddCustomField.{<CFName>}: <custom field value>
-    DelCustomField.{<CFName>}: <custom field value>
-
-Short forms:
-
-    CF.{<CFName>}: <custom field value>
-    AddCF.{<CFName>}: <custom field value>
-    DelCF.{<CFName>}: <custom field value>
-
-=head3 Transaction Custom field values
-
-Manage custom field values of transactions. Could be used multiple times.  (The curly braces
-are literal.)
-
-    TransactionCustomField.{<CFName>}: <custom field value>
-
-Short forms:
+This filter action is built to work with the email plugin interface for
+RT 4.2 and earlier. As such, it implements the C<GetCurrentUser> method
+and provides all functionality via that plugin hook.
 
-    TxnCustomField.{<CFName>}: <custom field value>
-    TransactionCF.{<CFName>}: <custom field value>
-    TxnCF.{<CFName>}: <custom field value>
+The email plugin interface is changed in RT 4.4. For details on the
+implementation for RT 4.4 and later, see
+L<RT::Interface::Email::Action::CommandByMail>.
 
-=cut
+=head1 METHODS
 
 =head2 GetCurrentUser
 
-Returns a CurrentUser object.  Also performs all the commands.
+Returns a CurrentUser object and an appropriate AuthLevel code to be
+interpreted by RT's email gateway.
 
 =cut
 
@@ -194,575 +73,16 @@ sub GetCurrentUser {
         }
     }
 
-    $RT::Logger->debug("Running CommandByMail as ".$args{'CurrentUser'}->UserObj->Name);
-
-    my $headername = $new_config
-        ? RT->Config->Get('CommandByMailHeader')
-        : $RT::CommandByMailHeader;
-
-    my $only_headers = $new_config
-        ? RT->Config->Get('CommandByMailOnlyHeaders')
-        : $RT::CommandByMailOnlyHeaders;
-
-    # find the content
-    my @content = ();
-    my @parts = $only_headers ? () : $args{'Message'}->parts_DFS;
-    foreach my $part (@parts) {
-        my $body = $part->bodyhandle or next;
-
-        #if it looks like it has pseudoheaders, that's our content
-        if ( $body->as_string =~ /^(?:\S+)(?:{.*})?:/m ) {
-            @content = $body->as_lines;
-            last;
-        }
-    }
-
-    if (defined $headername) {
-        unshift @content, $args{'Message'}->head->get_all($headername);
-    }
-
-    my @items;
-    my $found_pseudoheaders = 0;
-    foreach my $line (@content) {
-        next if $line =~ /^\s*$/ && ! $found_pseudoheaders;
-        last if $line !~ /^(?:(\S+(?:{.*})?)\s*?:\s*?(.*)\s*?|)$/;
-        last if not defined $1 and $found_pseudoheaders;
-        next if not defined $1;
-
-        $found_pseudoheaders = 1;
-        push( @items, $1 => $2 );
-        $RT::Logger->debug("Found pseudoheader: $1 => $2");
-    }
-    my %cmds;
-    while ( my $key = _CanonicalizeCommand( shift @items ) ) {
-        my $val = shift @items;
-        # strip leading and trailing spaces
-        $val =~ s/^\s+|\s+$//g;
-        $RT::Logger->debug("Got command $key => $val");
-
-        if ( exists $cmds{$key} ) {
-            $cmds{$key} = [ $cmds{$key} ] unless ref $cmds{$key};
-            push @{ $cmds{$key} }, $val;
-        } else {
-            $cmds{$key} = $val;
-        }
-    }
-
-    my %results;
-
-    foreach my $cmd ( keys %cmds ) {
-        my ($val, $msg) = _CheckCommand( $cmd );
-        unless ( $val ) {
-            $results{ $cmd } = {
-                value   => delete $cmds{ $cmd },
-                result  => $val,
-                message => $msg,
-            };
-        }
-    }
-
-    my $ticket_as_user = RT::Ticket->new( $args{'CurrentUser'} );
-    my $queue          = RT::Queue->new( $args{'CurrentUser'} );
-    if ( $cmds{'queue'} ) {
-        $queue->Load( $cmds{'queue'} );
-    }
-
-    if ( !$queue->id ) {
-        $queue->Load( $args{'Queue'}->id );
-    }
-
-    my $transaction;
-
-    # If we're updating.
-    if ( $args{'Ticket'}->id ) {
-        $ticket_as_user->Load( $args{'Ticket'}->id );
-        $RT::Logger->debug("Updating Ticket ".$ticket_as_user->Id." in Queue ".$queue->Name);
-
-        # we set status later as correspond can reopen ticket
-        foreach my $attribute (grep !/^(Status|TimeWorked)/, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
-            next unless defined $cmds{ lc $attribute };
-            next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
-
-            # canonicalize owner -- accept an e-mail address
-            if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
-                my $user = RT::User->new($RT::SystemUser);
-                $user->LoadByEmail( $cmds{ lc $attribute } );
-                $cmds{ lc $attribute } = $user->Name if $user->id;
-            }
-
-            _SetAttribute(
-                $ticket_as_user,        $attribute,
-                $cmds{ lc $attribute }, \%results
-            );
-        }
-
-        # we want the queue the ticket is currently in, not the queue
-        # that was passed to rt-mailgate, otherwise we can't find the
-        # proper set of Custom Fields.  But, we have to do this after 
-        # we potentially update the Queue from @REGULAR_ATTRIBUTES
-        $queue = $ticket_as_user->QueueObj();
-
-        foreach my $attribute (@DATE_ATTRIBUTES) {
-            next unless ( $cmds{ lc $attribute } );
-
-            my $date = RT::Date->new( $args{'CurrentUser'} );
-            $date->Set(
-                Format => 'unknown',
-                Value  => $cmds{ lc $attribute },
-            );
-            _SetAttribute( $ticket_as_user, $attribute, $date->ISO,
-                \%results );
-            $results{ $attribute }->{value} = $cmds{ lc $attribute };
-        }
-
-        foreach my $type ( @WATCHER_ATTRIBUTES ) {
-            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
-            next unless keys %tmp;
-
-            $tmp{'Default'} = [ do {
-                my $method = $type;
-                $method .= 's' if $type eq 'Requestor';
-                $args{'Ticket'}->$method->MemberEmailAddresses;
-            } ];
-            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
-            foreach my $text ( @$del ) {
-                my $user = RT::User->new($RT::SystemUser);
-                $user->LoadByEmail($text) if $text =~ /\@/;
-                $user->Load($text) unless $user->id;
-                my ( $val, $msg ) = $ticket_as_user->DeleteWatcher(
-                    Type  => $type,
-                    PrincipalId => $user->PrincipalId,
-                );
-                push @{ $results{ 'Del'. $type } }, {
-                    value   => $text,
-                    result  => $val,
-                    message => $msg
-                };
-            }
-            foreach my $text ( @$add ) {
-                my $user = RT::User->new($RT::SystemUser);
-                $user->LoadByEmail($text) if $text =~ /\@/;
-                $user->Load($text) unless $user->id;
-                my ( $val, $msg ) = $ticket_as_user->AddWatcher(
-                    Type  => $type,
-                    $user->id
-                        ? (PrincipalId => $user->PrincipalId)
-                        : (Email => $text)
-                    ,
-                );
-                push @{ $results{ 'Add'. $type } }, {
-                    value   => $text,
-                    result  => $val,
-                    message => $msg
-                };
-            }
-        }
-
-        {
-            my $time_taken = 0;
-            if (grep $_ eq 'TimeWorked', @TIME_ATTRIBUTES) {
-                if (ref $cmds{'timeworked'}) { 
-                    map { $time_taken += ($_ || 0) }  @{ $cmds{'timeworked'} };
-                    $RT::Logger->debug("Time taken: $time_taken");
-                }
-                else {
-                    $time_taken = $cmds{'timeworked'} || 0;
-                }
-            }
-
-            my $method = ucfirst $args{'Action'};
-            (my $status, my $msg, $transaction) = $ticket_as_user->$method(
-                TimeTaken => $time_taken,
-                MIMEObj   => $args{'Message'},
-            );
-            unless ( $status ) {
-                $RT::Logger->warning(
-                    "Couldn't write $args{'Action'}."
-                    ." Fallback to standard mailgate. Error: $msg");
-                return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
-            }
-        }
-
-        foreach my $type ( @LINK_ATTRIBUTES ) {
-            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
-            next unless keys %tmp;
-
-            my $typemap   = keys %RT::Link::TYPEMAP ? \%RT::Link::TYPEMAP : $ticket_as_user->LINKTYPEMAP;
-            my $link_type = $typemap->{$type}->{'Type'};
-            my $link_mode = $typemap->{$type}->{'Mode'};
-
-            $tmp{'Default'} = [ do {
-                my %h = ( Base => 'Target', Target => 'Base' );
-                my $links = $args{'Ticket'}->_Links( $h{$link_mode}, $link_type );
-                my @res;
-                while ( my $link = $links->Next ) {
-                    my $method = $link_mode .'URI';
-                    my $uri = $link->$method();
-                    next unless $uri->IsLocal;
-                    push @res, $uri->Object->Id;
-                }
-                @res;
-            } ];
-            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
-            foreach ( @$del ) {
-                my ($val, $msg) = $ticket_as_user->DeleteLink(
-                    Type => $link_type,
-                    $link_mode => $_,
-                );
-                $results{ 'Del'. $type } = {
-                    value => $_,
-                    result => $val,
-                    message => $msg,
-                };
-            }
-            foreach ( @$add ) {
-                my ($val, $msg) = $ticket_as_user->AddLink(
-                    Type => $link_type,
-                    $link_mode => $_,
-                );
-                $results{ 'Add'. $type } = {
-                    value => $_,
-                    result => $val,
-                    message => $msg,
-                };
-            }
-        }
-
-        my $custom_fields = $queue->TicketCustomFields;
-        while ( my $cf = $custom_fields->Next ) {
-            my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
-            next unless keys %tmp;
-
-            $tmp{'Default'} = [ do {
-                my $values = $args{'Ticket'}->CustomFieldValues( $cf->id );
-                my @res;
-                while ( my $value = $values->Next ) {
-                    push @res, $value->Content;
-                }
-                @res;
-            } ];
-            my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
-            foreach ( @$del ) {
-                my ( $val, $msg ) = $ticket_as_user->DeleteCustomFieldValue(
-                    Field => $cf->id,
-                    Value => $_
-                );
-                $results{ "DelCustomField{". $cf->Name ."}" } = {
-                    value => $_,
-                    result => $val,
-                    message => $msg,
-                };
-            }
-            foreach ( @$add ) {
-                my ( $val, $msg ) = $ticket_as_user->AddCustomFieldValue(
-                    Field => $cf->id,
-                    Value => $_
-                );
-                $results{ "DelCustomField{". $cf->Name ."}" } = {
-                    value => $_,
-                    result => $val,
-                    message => $msg,
-                };
-            }
-        }
-
-        foreach my $attribute (grep $_ eq 'Status', @REGULAR_ATTRIBUTES) {
-            next unless defined $cmds{ lc $attribute };
-            next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
-
-            _SetAttribute(
-                $ticket_as_user,        $attribute,
-                lc $cmds{ lc $attribute }, \%results
-            );
-        }
-
-    } else {
-
-        my %create_args = ();
-        foreach my $attribute (@REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
-            next unless exists $cmds{ lc $attribute };
-
-            # canonicalize owner -- accept an e-mail address
-            if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
-                my $user = RT::User->new($RT::SystemUser);
-                $user->LoadByEmail( $cmds{ lc $attribute } );
-                $cmds{ lc $attribute } = $user->Name if $user->id;
-            }
-
-            if ( $attribute eq 'TimeWorked' && ref $cmds{ lc $attribute } ) {
-                my $time_taken = 0;
-                map { $time_taken += ($_ || 0) }  @{ $cmds{'timeworked'} };
-                $cmds{'timeworked'} = $time_taken;
-                $RT::Logger->debug("Time taken on create: $time_taken");
-            }
-
-            if ( $attribute eq 'Status' && $cmds{ lc $attribute } ) {
-                $cmds{ lc $attribute } = lc $cmds{ lc $attribute };
-            }
-
-            $create_args{$attribute} = $cmds{ lc $attribute };
-        }
-        foreach my $attribute (@DATE_ATTRIBUTES) {
-            next unless exists $cmds{ lc $attribute };
-            my $date = RT::Date->new( $args{'CurrentUser'} );
-            $date->Set(
-                Format => 'unknown',
-                Value  => $cmds{ lc $attribute }
-            );
-            $create_args{$attribute} = $date->ISO;
-        }
-
-        # Canonicalize links
-        foreach my $type ( @LINK_ATTRIBUTES ) {
-            $create_args{ $type } = [ _CompileAdditiveForCreate( 
-                _ParseAdditiveCommand( \%cmds, 0, $type ),
-            ) ];
-        }
-
-        # Canonicalize custom fields
-        my $custom_fields = $queue->TicketCustomFields;
-        while ( my $cf = $custom_fields->Next ) {
-            my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
-            next unless keys %tmp;
-            $create_args{ 'CustomField-' . $cf->id } = [ _CompileAdditiveForCreate(%tmp) ];
-        }
-
-        # Canonicalize watchers
-        # First of all fetch default values
-        foreach my $type ( @WATCHER_ATTRIBUTES ) {
-            my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
-            $tmp{'Default'} = [ $args{'CurrentUser'}->EmailAddress ] if $type eq 'Requestor';
-            $tmp{'Default'} = [
-                ParseCcAddressesFromHead(
-                    Head        => $args{'Message'}->head,
-                    CurrentUser => $args{'CurrentUser'},
-                    QueueObj    => $args{'Queue'},
-                )
-            ] if $type eq 'Cc' && $RT::ParseNewMessageForTicketCcs;
-
-            $create_args{ $type } = [ _CompileAdditiveForCreate( %tmp ) ];
-        }
-
-        # get queue unless mail contain it
-        $create_args{'Queue'} = $args{'Queue'}->Id unless exists $create_args{'Queue'};
-
-        # subject
-        unless ( $create_args{'Subject'} ) {
-            $create_args{'Subject'} = $args{'Message'}->head->get('Subject');
-            chomp $create_args{'Subject'};
-        }
-
-        # If we don't already have a ticket, we're going to create a new
-        # ticket
-
-        my ( $id, $txn_id, $msg ) = $ticket_as_user->Create(
-            %create_args,
-            MIMEObj => $args{'Message'}
-        );
-        unless ( $id ) {
-            $msg = "Couldn't create ticket from message with commands, ".
-                   "fallback to standard mailgate.\n\nError: $msg";
-            $RT::Logger->error( $msg );
-            $results{'Create'} = {
-                result => $id,
-                message => $msg,
-            };
-
-            _ReportResults( Results => \%results, Message => $args{'Message'} );
-
-            return ($args{'CurrentUser'}, $args{'AuthLevel'});
-        }
-        $transaction = RT::Transaction->new( $ticket_as_user->CurrentUser );
-        $transaction->Load( $txn_id );
-
-    }
-
-    if ( $transaction && $transaction->id ) {
-        my $custom_fields = $transaction->CustomFields;
-        while ( my $cf = $custom_fields->Next ) {
-            my $cmd = 'TransactionCustomField{'. $cf->Name .'}';
-            my @values = ($cmds{ lc $cmd });
-            @values = @{ $values[0] } if ref $values[0] eq 'ARRAY';
-            @values = grep defined && length, @values;
-            next unless @values;
-
-            foreach my $value ( @values ) {
-                my ($status, $msg) = $transaction->AddCustomFieldValue(
-                    Field => $cf->Name, Value => $value,
-                );
-                push @{ $results{ $cmd } ||= [] }, {
-                    value => $value, result => $status, message => $msg,
-                };
-            }
-        }
-    }
-
-    _ReportResults(
-        Ticket => $args{'Ticket'},
-        Results => \%results,
-        Message => $args{'Message'},
-    );
+    my $return_ref = RT::Extension::CommandByMail::ProcessCommands(%args);
 
     # make sure ticket is loaded
-    $args{'Ticket'}->Load( $transaction->ObjectId );
+    $args{'Ticket'}->Load( $return_ref->{'Transaction'}->ObjectId );
 
-    return ( $args{'CurrentUser'}, -2 );
-}
-
-sub _ParseAdditiveCommand {
-    my ($cmds, $plural_forms, $base) = @_;
-    my (%res);
-
-    my @types = $base;
-    push @types, $base.'s' if $plural_forms;
-    push @types, 'Add'. $base;
-    push @types, 'Add'. $base .'s' if $plural_forms;
-    push @types, 'Del'. $base;
-    push @types, 'Del'. $base .'s' if $plural_forms;
-
-    foreach my $type ( @types ) {
-        next unless defined $cmds->{lc $type};
-
-        my @values = ref $cmds->{lc $type} eq 'ARRAY'?
-            @{ $cmds->{lc $type} }: $cmds->{lc $type};
-
-        if ( $type =~ /^\Q$base\Es?/ ) {
-            push @{ $res{'Set'} }, @values;
-        } elsif ( $type =~ /^Add/ ) {
-            push @{ $res{'Add'} }, @values;
-        } else {
-            push @{ $res{'Del'} }, @values;
-        }
+    if ( ref $return_ref eq 'HASH' ) {
+        # ProcessCommands returned with values, use them in the return code
+        return ( $return_ref->{'CurrentUser'}, $return_ref->{'AuthLevel'} );
     }
 
-    return %res;
-}
-
-sub _CompileAdditiveForCreate {
-    my %cmd = @_;
-
-    unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
-        $cmd{'Default'} = [];
-    } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
-        $cmd{'Default'} = [ $cmd{'Default'} ];
-    }
-
-    my @list;
-    @list = @{ $cmd{'Default'} } unless $cmd{'Set'};
-    @list = @{ $cmd{'Set'} } if $cmd{'Set'};
-    push @list, @{ $cmd{'Add'} } if $cmd{'Add'};
-    if ( $cmd{'Del'} ) {
-        my %seen;
-        $seen{$_} = 1 foreach @{ $cmd{'Del'} };
-        @list = grep !$seen{$_}, @list;
-    }
-    return @list;
-}
-
-sub _CompileAdditiveForUpdate {
-    my %cmd = @_;
-
-    my @new = _CompileAdditiveForCreate( %cmd );
-
-    unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
-        $cmd{'Default'} = [];
-    } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
-        $cmd{'Default'} = [ $cmd{'Default'} ];
-    }
-
-    my ($add, $del);
-    unless ( @{ $cmd{'Default'} } ) {
-        $add = \@new;
-    } elsif ( !@new ) {
-        $del = $cmd{'Default'};
-    } else {
-        my (%cur, %new);
-        $cur{$_} = 1 foreach @{ $cmd{'Default'} };
-        $new{$_} = 1 foreach @new;
-
-        $add = [ grep !$cur{$_}, @new ];
-        $del = [ grep !$new{$_}, @{ $cmd{'Default'} } ];
-    }
-    $_ ||= [] foreach ($add, $del);
-    return ($add, $del);
-}
-
-sub _SetAttribute {
-    my $ticket    = shift;
-    my $attribute = shift;
-    my $value     = shift;
-    my $results   = shift;
-    my $setter    = "Set$attribute";
-    my ( $val, $msg ) = $ticket->$setter($value);
-    $results->{$attribute} = {
-        value   => $value,
-        result  => $val,
-        message => $msg
-    };
-}
-
-sub _CanonicalizeCommand {
-    my $key = shift;
-    return $key unless defined $key;
-
-    $key = lc $key;
-    # CustomField commands
-    $key =~ s/^(add|del|)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/$1customfield{$2}/i;
-    $key =~ s/^(?:transaction|txn)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/transactioncustomfield{$1}/i;
-    return $key;
-}
-
-sub _CheckCommand {
-    my ($cmd, $val) = (lc shift, shift);
-    return 1 if $cmd =~ /^(add|del|)customfield{.*}$/i;
-    return 1 if $cmd =~ /^transactioncustomfield{.*}$/i;
-    if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
-        return 1 unless ref $val;
-        return (0, "Command '$cmd' doesn't support multiple values");
-    }
-    return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
-    if ( $cmd =~ /^(?:add|del)(.*)$/i ) {
-        my $cmd = $1;
-        if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
-            return (0, "Command '$cmd' doesn't support multiple values");
-        }
-        return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
-    }
-
-    return (0, "Command '$cmd' is unknown");
-}
-
-sub _ReportResults {
-    my %args = ( Ticket => undef, Message => undef, Results => {}, @_ );
-
-    my $msg = '';
-    unless ( $args{'Ticket'} && $args{'Ticket'}->id ) {
-        $msg .= $args{'Results'}{'Create'}{'message'} || '';
-        $msg .= "\n" if $msg;
-        delete $args{'Results'}{'Create'};
-    }
-
-    foreach my $key ( keys %{ $args{'Results'} } ) {
-        my @records = ref $args{'Results'}->{ $key } eq 'ARRAY'?
-                         @{$args{'Results'}->{ $key }}: $args{'Results'}->{ $key };
-        foreach my $rec ( @records ) {
-            next if $rec->{'result'};
-            $msg .= "Failed command '". $key .": ". $rec->{'value'} ."'\n";
-            $msg .= "Error message: ". ($rec->{'message'}||"(no message)") ."\n\n";
-        }
-    }
-    return unless $msg && $msg !~ /^\s*$/;
-
-    $RT::Logger->warning( $msg );
-    my $ErrorsTo = RT::Interface::Email::ParseErrorsToAddressFromHead( $args{'Message'}->head );
-    RT::Interface::Email::MailError(
-        To          => $ErrorsTo,
-        Subject     => "Extended mailgate error",
-        Explanation => $msg,
-        MIMEObj     => $args{'Message'},
-        Attach      => $args{'Message'}->as_string,
-    );
-    return;
 }
 
 1;
diff --git a/xt/internals.t b/xt/internals.t
index 64f5942..5edbed9 100644
--- a/xt/internals.t
+++ b/xt/internals.t
@@ -3,134 +3,134 @@ use warnings;
 
 use RT::Extension::CommandByMail::Test tests => undef, nodb => 1;
 
-use_ok('RT::Interface::Email::Filter::TakeAction');
+use_ok('RT::Extension::CommandByMail');
 
 diag( "test _ParseAdditiveCommand") if $ENV{'TEST_VERBOSE'};
 {
-    my %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand({}, 0, 'Foo');
+    my %res = RT::Extension::CommandByMail::_ParseAdditiveCommand({}, 0, 'Foo');
     is_deeply( \%res, {}, 'empty' );
 
     my $cmd = { foo => 'qwe' };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo');
     is_deeply(\%res, { Set => ['qwe'] }, 'simple set');
 
     $cmd = { foo => ['qwe', 'asd'] };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo');
     is_deeply(\%res, { Set => ['qwe', 'asd'] }, 'simple set with array ref');
 
     $cmd = { foos => 'qwe' };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 1, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 1, 'Foo');
     is_deeply(\%res, { Set => ['qwe'] }, 'simple set with plural form');
 
     $cmd = { foos => 'qwe' };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo');
     is_deeply(\%res, { }, 'single form shouldnt eat plural forms');
 
     $cmd = { foo => 'qwe', foos => 'qwe' };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 1, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 1, 'Foo');
     is_deeply(\%res, { Set => ['qwe', 'qwe'] }, 'set with plural and single form at the same time');
 
     $cmd = { foo => 'qwe', addfoo  => 'asd' };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo');
     is_deeply(\%res, { Set => ['qwe'], Add => ['asd'] }, 'set+add');
 
     $cmd = { foo => ['qwe'], addfoo  => ['asd'], delfoo => ['zxc'] };
-    %res = RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo');
+    %res = RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo');
     is_deeply(\%res, { Set => ['qwe'], Add => ['asd'], Del => ['zxc'] }, 'set+add+del');
 }
 
 diag( "test _CompileAdditiveForCreate") if $ENV{'TEST_VERBOSE'};
 {
-    my @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand({}, 0, 'Foo')
+    my @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand({}, 0, 'Foo')
     );
     is_deeply(\@res, [], 'empty');
 
     my $cmd = { foo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, ['qwe'], 'simple set');
 
     $cmd = { foo => 'qwe', addfoo => 'asd' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, ['qwe', 'asd'], 'set+add');
 
     $cmd = { foo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, ['qwe'], 'set+default: set overrides defaults');
 
     $cmd = { addfoo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, ['def', 'qwe'], 'add+default: add adds to defaults');
 
     $cmd = { addfoo => 'qwe', delfoo => 'def' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForCreate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForCreate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, ['qwe'], 'add+default+del: delete default');
 }
 
 diag( "test _CompileAdditiveForUpdate") if $ENV{'TEST_VERBOSE'};
 {
-    my @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand({}, 0, 'Foo')
+    my @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand({}, 0, 'Foo')
     );
     is_deeply(\@res, [[], []], 'empty');
 
     my $cmd = { foo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [['qwe'],[]], 'simple set');
 
     $cmd = { foo => 'qwe', addfoo => 'asd' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [['qwe', 'asd'],[]], 'set+add');
 
     $cmd = { foo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [['qwe'],['def']], 'set+default: set overrides defaults');
 
     $cmd = { addfoo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [['qwe'],[]], 'add+default: add adds to defaults');
 
     $cmd = { addfoo => 'def' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [[],[]], 'add current: do nothing');
 
     $cmd = { addfoo => 'qwe', delfoo => 'def' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [['qwe'],['def']], 'add+default+del: delete default');
 
     $cmd = { delfoo => 'qwe' };
-    @res = RT::Interface::Email::Filter::TakeAction::_CompileAdditiveForUpdate(
+    @res = RT::Extension::CommandByMail::_CompileAdditiveForUpdate(
         Default => 'def',
-        RT::Interface::Email::Filter::TakeAction::_ParseAdditiveCommand($cmd, 0, 'Foo')
+        RT::Extension::CommandByMail::_ParseAdditiveCommand($cmd, 0, 'Foo')
     );
     is_deeply(\@res, [[],[]], 'del not current: do nothing');
 }

commit 45bb68eb2599e15d004b86f235a8e624c75f9753
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 09:25:13 2016 -0400

    Add Handle* methods for compatibility with RT 4.4
    
    Email plugin handling for RT 4.4 was refactored, providing
    new HandleAction hooks rather than including all functionality
    in GetCurrentUser. Create a new Action module and provide
    Handle methods for the standard comment and correspond actions
    to allow users to upgrade in place.
    
    The updates allow the code to run for both RT 4.2 and
    4.4 with minimal duplication. However the code is in a new
    module, so users will need to update their RT_SiteConfig.pm.

diff --git a/etc/handle_action_pass_currentuser.patch b/etc/handle_action_pass_currentuser.patch
new file mode 100644
index 0000000..0014aa2
--- /dev/null
+++ b/etc/handle_action_pass_currentuser.patch
@@ -0,0 +1,12 @@
+diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm
+index 175805d..7ffba8e 100644
+--- a/lib/RT/Interface/Email.pm
++++ b/lib/RT/Interface/Email.pm
+@@ -248,6 +248,7 @@ sub Gateway {
+             Action      => $action,
+             Subject     => $Subject,
+             Message     => $Message,
++            CurrentUser => $CurrentUser,
+             Ticket      => $Ticket,
+             TicketId    => $args{ticket},
+             Queue       => $SystemQueueObj,
diff --git a/lib/RT/Extension/CommandByMail.pm b/lib/RT/Extension/CommandByMail.pm
index a0e6077..3e4b4fb 100644
--- a/lib/RT/Extension/CommandByMail.pm
+++ b/lib/RT/Extension/CommandByMail.pm
@@ -57,8 +57,24 @@ C<@MailPlugins> configuration, as follows:
 
     Set(@MailPlugins, qw(Auth::MailFrom Filter::TakeAction));
 
+For RT 4.4 or newer, the plugin code is in C<Action::CommandByMail>, so
+add this:
+
+    Set(@MailPlugins, qw(Auth::MailFrom Action::CommandByMail));
+
 Be sure to include C<Auth::MailFrom> in the list as well.
 
+B<Note:> The plugin name has changed for RT 4.4, so after upgrading you
+must also update your C<RT_SiteConfig.pm> file to change
+C<Filter::TakeAction> to the new C<Action::CommandByMail>.
+
+=item Patch RT
+
+For RT 4.4.0, apply the included patch:
+
+    cd /opt/rt4  # Your location may be different
+    patch -p1 < /download/dir/RT-Extension-CommandByMail/etc/handle_action_pass_currentuser.patch
+
 =item Restart your webserver
 
 =back
@@ -225,6 +241,14 @@ If set, the body will not be examined, only the headers.
 
 This extension is incompatible with C<UnsafeEmailCommands> RT option.
 
+=head1 METHODS
+
+=head2 ProcessCommands
+
+This method provides the main email processing functionality. It supports
+both RT 4.2 and earlier and 4.4 and later. To do this, the return hashes
+contain some values used by 4.2 code and some used by 4.4. The return
+values coexist and unused values are ignored by the different versions.
 =cut
 
 sub ProcessCommands {
@@ -245,11 +269,19 @@ sub ProcessCommands {
             ."CurrentUser (actor) is not authorized. "
         );
         return { CurrentUser => $args{'CurrentUser'},
-                 AuthLevel   => $args{'AuthLevel'} };
+                 AuthLevel   => $args{'AuthLevel'},
+                 MailError   => 1,
+                 ErrorSubject     => "Permission Denied",
+                 Explanation => "CurrentUser is not set when trying to "
+                 . "process email command via CommandByMail",
+                 Failure     => 1
+             };
     }
 
+    $RT::Logger->debug("Running CommandByMail as ".$args{'CurrentUser'}->UserObj->Name);
+
     # If the user isn't asking for a comment or a correspond,
-    # bail out
+    # bail out. Only relevant for pre-4.2.
     unless ( $args{'Action'} =~ /^(?:comment|correspond)$/i ) {
         return { CurrentUser => $args{'CurrentUser'},
                  AuthLevel   => $args{'AuthLevel'} };
@@ -269,12 +301,16 @@ sub ProcessCommands {
         if (!$group->HasMemberRecursively($args{'CurrentUser'}->PrincipalObj)) {
             $RT::Logger->debug("CurrentUser not in CommandByMailGroup");
             return { CurrentUser => $args{'CurrentUser'},
-                     AuthLevel   => $args{'AuthLevel'} };
+                     AuthLevel   => $args{'AuthLevel'},
+                     MailError   => 1,
+                     ErrorSubject     => "Permission Denied",
+                     Explanation => "User " . $args{'CurrentUser'}->UserObj->EmailAddress
+                     . " is not in the configured CommandByMailGroup",
+                     Failure     => 1
+                 };
         }
     }
 
-    $RT::Logger->debug("Running CommandByMail as ".$args{'CurrentUser'}->UserObj->Name);
-
     my $headername = $new_config
         ? RT->Config->Get('CommandByMailHeader')
         : $RT::CommandByMailHeader;
@@ -459,7 +495,13 @@ sub ProcessCommands {
                     "Couldn't write $args{'Action'}."
                     ." Fallback to standard mailgate. Error: $msg");
                 return { CurrentUser => $args{'CurrentUser'},
-                         AuthLevel   => $args{'AuthLevel'} };
+                         AuthLevel   => $args{'AuthLevel'},
+                         MailError   => 1,
+                         ErrorSubject     => "Unable to execute $args{'Action'}",
+                         Explanation => "Unable to execute $args{'Action'} on ticket "
+                         . $args{'Ticket'}->Id . ": $msg",
+                         Failure     => 1
+                     };
             }
         }
 
@@ -853,7 +895,6 @@ sub _ReportResults {
 }
 
 
-
 1;
 __END__
 
diff --git a/lib/RT/Interface/Email/Action/CommandByMail.pm b/lib/RT/Interface/Email/Action/CommandByMail.pm
new file mode 100644
index 0000000..6195345
--- /dev/null
+++ b/lib/RT/Interface/Email/Action/CommandByMail.pm
@@ -0,0 +1,56 @@
+package RT::Interface::Email::Action::CommandByMail;
+
+use warnings;
+use strict;
+
+use Role::Basic 'with';
+with 'RT::Interface::Email::Role';
+
+=head1 NAME
+
+RT::Interface::Email::Action::CommandByMail - Change metadata of ticket via email
+
+=head1 DESCRIPTION
+
+This action provides compatibility with the new mail plugin system introduced
+in RT 4.4. It provides an alternate to the default comment and correspond
+handlers provided by RT.
+
+=cut
+
+# To maintain compatibility with previous versions of CommandByMail,
+# handle the standard comment and correspond actions. Follow the
+# pattern from RT's default action handling for providing both.
+
+sub HandleComment {
+    _HandleEither( @_, Action => "Comment" );
+}
+
+sub HandleCorrespond {
+    _HandleEither( @_, Action => "Correspond" );
+}
+
+sub _HandleEither {
+    my %args = (
+        Action      => undef,
+        Message     => undef,
+        Subject     => undef,
+        Ticket      => undef,
+        TicketId    => undef,
+        Queue       => undef,
+        @_,
+    );
+
+    my $return_ref = RT::Extension::CommandByMail::ProcessCommands(%args);
+
+    if ( exists $return_ref->{'MailError'} and $return_ref->{'MailError'} ){
+        MailError(
+            Subject     => $return_ref->{'ErrorSubject'},
+            Explanation => $return_ref->{'Explanation'},
+            FAILURE     => $return_ref->{'Failure'},
+        );
+    }
+    return;
+}
+
+1;

commit 12cf1d6f8d783d6c4bd7677e2741d2f1afdda5cc
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 09:50:46 2016 -0400

    Move Test.pm to a .in file for easier dev

diff --git a/Makefile.PL b/Makefile.PL
index be1e65e..024e268 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -18,6 +18,9 @@ substitute(
         RT_BIN_PATH  => $bin_path,
         RT_SBIN_PATH => $sbin_path,
     },
+    {
+        sufix => '.in'
+    },
     qw(lib/RT/Extension/CommandByMail/Test.pm),
 );
 
diff --git a/lib/RT/Extension/CommandByMail/Test.pm b/lib/RT/Extension/CommandByMail/Test.pm.in
similarity index 100%
rename from lib/RT/Extension/CommandByMail/Test.pm
rename to lib/RT/Extension/CommandByMail/Test.pm.in

commit 4d9998a70096032195301ca814ee320a75e1fd2f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 09:52:20 2016 -0400

    Update gitignore to ignore Test.pm

diff --git a/.gitignore b/.gitignore
index aceb043..9d8e5e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ pod2htm*.tmp
 /MYMETA.*
 /t/tmp
 /xt/tmp
+/lib/RT/Extension/CommandByMail/Test.pm

commit 75f52a36964ea7a124b2886d3325d132af8df250
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 09:57:36 2016 -0400

    Update tests to run with both RT 4.2 and 4.4 config

diff --git a/lib/RT/Extension/CommandByMail/Test.pm.in b/lib/RT/Extension/CommandByMail/Test.pm.in
index 8255b39..12b2812 100644
--- a/lib/RT/Extension/CommandByMail/Test.pm.in
+++ b/lib/RT/Extension/CommandByMail/Test.pm.in
@@ -25,4 +25,28 @@ sub import {
     require RT::Extension::CommandByMail;
 }
 
+sub bootstrap_more_config{
+    my $self = shift;
+    my $config = shift;
+    my $args_ref = shift;
+
+    if ( RT_at_or_newer_than('4.4.0') ){
+        print $config "Set( \@MailPlugins, qw(Auth::MailFrom Action::CommandByMail));\n";
+    }
+    else{
+        print $config "Set( \@MailPlugins, qw(Auth::MailFrom Filter::TakeAction));\n";
+    }
+    return;
+}
+
+sub RT_at_or_newer_than{
+    my $version = shift;
+    my ($my_major, $my_minor, $my_sub) = split(/\./, $version);
+    my ($major, $minor, $sub) = split(/\./, $RT::VERSION);
+    return ($my_major >= $major
+            and $my_minor >= $minor
+            and $my_sub >= $sub)
+            ? 1 : 0;
+}
+
 1;
diff --git a/xt/create.t b/xt/create.t
index b098d49..83b568f 100644
--- a/xt/create.t
+++ b/xt/create.t
@@ -3,7 +3,6 @@ use warnings;
 
 use RT::Extension::CommandByMail::Test tests => undef;
 my $test = 'RT::Extension::CommandByMail::Test';
-RT->Config->Set('MailPlugins', 'Auth::MailFrom', 'Filter::TakeAction');
 
 my $test_ticket_id;
 
diff --git a/xt/txn-cfs.t b/xt/txn-cfs.t
index 1bf64fa..2a868cb 100644
--- a/xt/txn-cfs.t
+++ b/xt/txn-cfs.t
@@ -3,7 +3,6 @@ use warnings;
 
 use RT::Extension::CommandByMail::Test tests => undef;
 my $test = 'RT::Extension::CommandByMail::Test';
-RT->Config->Set('MailPlugins', 'Auth::MailFrom', 'Filter::TakeAction');
 
 my $cf_name = 'Test CF';
 {
diff --git a/xt/update.t b/xt/update.t
index be8b415..f2fb4bf 100644
--- a/xt/update.t
+++ b/xt/update.t
@@ -3,7 +3,6 @@ use warnings;
 
 use RT::Extension::CommandByMail::Test tests => undef;
 my $test = 'RT::Extension::CommandByMail::Test';
-RT->Config->Set('MailPlugins', 'Auth::MailFrom', 'Filter::TakeAction');
 
 my $test_ticket_id;
 

commit a402bd56952f95b4c067c7d4a3e83036bad9f2bb
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 10:05:46 2016 -0400

    Update Module::Install

diff --git a/inc/Module/AutoInstall.pm b/inc/Module/AutoInstall.pm
index 4aca606..22dfa82 100644
--- a/inc/Module/AutoInstall.pm
+++ b/inc/Module/AutoInstall.pm
@@ -8,7 +8,7 @@ use ExtUtils::MakeMaker ();
 
 use vars qw{$VERSION};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 }
 
 # special map on pre-defined feature sets
@@ -537,7 +537,7 @@ sub _install_cpan {
     while ( my ( $opt, $arg ) = splice( @config, 0, 2 ) ) {
         ( $args{$opt} = $arg, next )
           if $opt =~ /^(?:force|notest)$/;    # pseudo-option
-        $CPAN::Config->{$opt} = $arg;
+        $CPAN::Config->{$opt} = $opt eq 'urllist' ? [$arg] : $arg;
     }
 
     if ($args{notest} && (not CPAN::Shell->can('notest'))) {
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
index 5460dd5..f44ab4d 100644
--- a/inc/Module/Install.pm
+++ b/inc/Module/Install.pm
@@ -31,7 +31,7 @@ BEGIN {
 	# This is not enforced yet, but will be some time in the next few
 	# releases once we can make sure it won't clash with custom
 	# Module::Install extensions.
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 
 	# Storage for the pseudo-singleton
 	$MAIN    = undef;
@@ -378,6 +378,7 @@ eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
 sub _read {
 	local *FH;
 	open( FH, '<', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
 	my $string = do { local $/; <FH> };
 	close FH or die "close($_[0]): $!";
 	return $string;
@@ -386,6 +387,7 @@ END_NEW
 sub _read {
 	local *FH;
 	open( FH, "< $_[0]"  ) or die "open($_[0]): $!";
+	binmode FH;
 	my $string = do { local $/; <FH> };
 	close FH or die "close($_[0]): $!";
 	return $string;
@@ -416,6 +418,7 @@ eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
 sub _write {
 	local *FH;
 	open( FH, '>', $_[0] ) or die "open($_[0]): $!";
+	binmode FH;
 	foreach ( 1 .. $#_ ) {
 		print FH $_[$_] or die "print($_[0]): $!";
 	}
@@ -425,6 +428,7 @@ END_NEW
 sub _write {
 	local *FH;
 	open( FH, "> $_[0]"  ) or die "open($_[0]): $!";
+	binmode FH;
 	foreach ( 1 .. $#_ ) {
 		print FH $_[$_] or die "print($_[0]): $!";
 	}
diff --git a/inc/Module/Install/AutoInstall.pm b/inc/Module/Install/AutoInstall.pm
index ab1e5fa..e19d259 100644
--- a/inc/Module/Install/AutoInstall.pm
+++ b/inc/Module/Install/AutoInstall.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
index f9bf5de..5762a74 100644
--- a/inc/Module/Install/Base.pm
+++ b/inc/Module/Install/Base.pm
@@ -4,7 +4,7 @@ package Module::Install::Base;
 use strict 'vars';
 use vars qw{$VERSION};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 }
 
 # Suspend handler for "redefined" warnings
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
index b4e5e3b..d859276 100644
--- a/inc/Module/Install/Can.pm
+++ b/inc/Module/Install/Can.pm
@@ -8,7 +8,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
index 54f14fb..41d3517 100644
--- a/inc/Module/Install/Fetch.pm
+++ b/inc/Module/Install/Fetch.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
index 7224cff..2eb1d1f 100644
--- a/inc/Module/Install/Include.pm
+++ b/inc/Module/Install/Include.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
index 81cddd5..e9918d2 100644
--- a/inc/Module/Install/Makefile.pm
+++ b/inc/Module/Install/Makefile.pm
@@ -8,7 +8,7 @@ use Fcntl qw/:flock :seek/;
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
index 2c66b1e..9792685 100644
--- a/inc/Module/Install/Metadata.pm
+++ b/inc/Module/Install/Metadata.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
index 73e7245..cb4cfde 100644
--- a/inc/Module/Install/RTx.pm
+++ b/inc/Module/Install/RTx.pm
@@ -8,7 +8,7 @@ no warnings 'once';
 
 use Module::Install::Base;
 use base 'Module::Install::Base';
-our $VERSION = '0.36';
+our $VERSION = '0.37';
 
 use FindBin;
 use File::Glob     ();
@@ -37,6 +37,13 @@ sub RTx {
     }
     $self->add_metadata("x_module_install_rtx_version", $VERSION );
 
+    my $installdirs = $ENV{INSTALLDIRS};
+    for ( @ARGV ) {
+        if ( /INSTALLDIRS=(.*)/ ) {
+            $installdirs = $1;
+        }
+    }
+
     # Try to find RT.pm
     my @prefixes = qw( /opt /usr/local /home /usr /sw /usr/share/request-tracker4);
     $ENV{RTHOME} =~ s{/RT\.pm$}{} if defined $ENV{RTHOME};
@@ -71,7 +78,13 @@ sub RTx {
 
     # Installation locations
     my %path;
-    $path{$_} = $RT::LocalPluginPath . "/$name/$_"
+    my $plugin_path;
+    if ( $installdirs && $installdirs eq 'vendor' ) {
+        $plugin_path = $RT::PluginPath;
+    } else {
+        $plugin_path = $RT::LocalPluginPath;
+    }
+    $path{$_} = $plugin_path . "/$name/$_"
         foreach @DIRS;
 
     # Copy RT 4.2.0 static files into NoAuth; insufficient for
@@ -85,7 +98,7 @@ sub RTx {
     my %index = map { $_ => 1 } @INDEX_DIRS;
     $self->no_index( directory => $_ ) foreach grep !$index{$_}, @DIRS;
 
-    my $args = join ', ', map "q($_)", map { ($_, $path{$_}) }
+    my $args = join ', ', map "q($_)", map { ($_, "\$(DESTDIR)$path{$_}") }
         sort keys %path;
 
     printf "%-10s => %s\n", $_, $path{$_} for sort keys %path;
@@ -123,7 +136,7 @@ install ::
         $has_etc{acl}++;
     }
     if ( -e 'etc/initialdata' ) { $has_etc{initialdata}++; }
-    if ( grep { /\d+\.\d+(\.\d+)?.*$/ } glob('etc/upgrade/*.*') ) {
+    if ( grep { /\d+\.\d+\.\d+.*$/ } glob('etc/upgrade/*.*.*') ) {
         $has_etc{upgrade}++;
     }
 
@@ -131,6 +144,7 @@ install ::
     if ( $path{lib} ) {
         $self->makemaker_args( INSTALLSITELIB => $path{'lib'} );
         $self->makemaker_args( INSTALLARCHLIB => $path{'lib'} );
+        $self->makemaker_args( INSTALLVENDORLIB => $path{'lib'} )
     } else {
         $self->makemaker_args( PM => { "" => "" }, );
     }
@@ -139,6 +153,13 @@ install ::
     $self->makemaker_args( INSTALLSITEMAN3DIR => "$RT::LocalPath/man/man3" );
     $self->makemaker_args( INSTALLSITEARCH => "$RT::LocalPath/man" );
 
+    # INSTALLDIRS=vendor should install manpages into /usr/share/man.
+    # That is the default path in most distributions. Need input from
+    # Redhat, Centos etc.
+    $self->makemaker_args( INSTALLVENDORMAN1DIR => "/usr/share/man/man1" );
+    $self->makemaker_args( INSTALLVENDORMAN3DIR => "/usr/share/man/man3" );
+    $self->makemaker_args( INSTALLVENDORARCH => "/usr/share/man" );
+
     if (%has_etc) {
         print "For first-time installation, type 'make initdb'.\n";
         my $initdb = '';
@@ -258,4 +279,4 @@ sub _load_rt_handle {
 
 __END__
 
-#line 390
+#line 428
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
index e48c32d..218a66b 100644
--- a/inc/Module/Install/Win32.pm
+++ b/inc/Module/Install/Win32.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
index 409ef40..530749b 100644
--- a/inc/Module/Install/WriteAll.pm
+++ b/inc/Module/Install/WriteAll.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.12';
+	$VERSION = '1.16';
 	@ISA     = qw{Module::Install::Base};
 	$ISCORE  = 1;
 }
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
index 1be0cb1..aa539f7 100644
--- a/inc/YAML/Tiny.pm
+++ b/inc/YAML/Tiny.pm
@@ -2,16 +2,12 @@
 use 5.008001; # sane UTF-8 support
 use strict;
 use warnings;
-package YAML::Tiny;
-BEGIN {
-  $YAML::Tiny::AUTHORITY = 'cpan:ADAMK';
-}
-# git description: v1.61-3-g0a82466
-$YAML::Tiny::VERSION = '1.62';
+package YAML::Tiny; # git description: v1.68-2-gcc5324e
 # XXX-INGY is 5.8.1 too old/broken for utf8?
 # XXX-XDG Lancaster consensus was that it was sufficient until
 # proven otherwise
 
+our $VERSION = '1.69';
 
 #####################################################################
 # The YAML::Tiny API.
@@ -300,10 +296,11 @@ Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
             }
         }
     };
-    if ( ref $@ eq 'SCALAR' ) {
-        $self->_error(${$@});
-    } elsif ( $@ ) {
-        $self->_error($@);
+    my $err = $@;
+    if ( ref $err eq 'SCALAR' ) {
+        $self->_error(${$err});
+    } elsif ( $err ) {
+        $self->_error($err);
     }
 
     return $self;
@@ -515,6 +512,10 @@ sub _load_hash {
             die \"YAML::Tiny failed to classify line '$lines->[0]'";
         }
 
+        if ( exists $hash->{$key} ) {
+            warn "YAML::Tiny found a duplicate key '$key' in line '$lines->[0]'";
+        }
+
         # Do we have a value?
         if ( length $lines->[0] ) {
             # Yes
@@ -828,9 +829,10 @@ sub _can_flock {
 #####################################################################
 # Use Scalar::Util if possible, otherwise emulate it
 
+use Scalar::Util ();
 BEGIN {
     local $@;
-    if ( eval { require Scalar::Util; Scalar::Util->VERSION(1.18); } ) {
+    if ( eval { Scalar::Util->VERSION(1.18); } ) {
         *refaddr = *Scalar::Util::refaddr;
     }
     else {
@@ -852,8 +854,7 @@ END_PERL
     }
 }
 
-
-
+delete $YAML::Tiny::{refaddr};
 
 1;
 
@@ -870,4 +871,4 @@ END_PERL
 
 __END__
 
-#line 1488
+#line 1489
diff --git a/inc/unicore/Name.pm b/inc/unicore/Name.pm
deleted file mode 100644
index d72eb6e..0000000
--- a/inc/unicore/Name.pm
+++ /dev/null
@@ -1,417 +0,0 @@
-#line 1
-# !!!!!!!   DO NOT EDIT THIS FILE   !!!!!!!
-# This file is machine-generated by lib/unicore/mktables from the Unicode
-# database, Version 6.3.0.  Any changes made here will be lost!
-
-
-# !!!!!!!   INTERNAL PERL USE ONLY   !!!!!!!
-# This file is for internal use by core Perl only.  The format and even the
-# name or existence of this file are subject to change without notice.  Don't
-# use it directly.  Use Unicode::UCD to access the Unicode character data
-# base.
-
-
-package charnames;
-
-# This module contains machine-generated tables and code for the
-# algorithmically-determinable Unicode character names.  The following
-# routines can be used to translate between name and code point and vice versa
-
-{ # Closure
-
-    # Matches legal code point.  4-6 hex numbers, If there are 6, the first
-    # two must be 10; if there are 5, the first must not be a 0.  Written this
-    # way to decrease backtracking.  The first regex allows the code point to
-    # be at the end of a word, but to work properly, the word shouldn't end
-    # with a valid hex character.  The second one won't match a code point at
-    # the end of a word, and doesn't have the run-on issue
-    my $run_on_code_point_re = qr/(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b)/;
-    my $code_point_re = qr/(?^aa:\b(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b))/;
-
-    # In the following hash, the keys are the bases of names which include
-    # the code point in the name, like CJK UNIFIED IDEOGRAPH-4E01.  The value
-    # of each key is another hash which is used to get the low and high ends
-    # for each range of code points that apply to the name.
-    my %names_ending_in_code_point = (
-'CJK COMPATIBILITY IDEOGRAPH' => 
-{
-'high' => 
-[
-64109,
-64217,
-195101,
-],
-'low' => 
-[
-63744,
-64112,
-194560,
-],
-},
-'CJK UNIFIED IDEOGRAPH' => 
-{
-'high' => 
-[
-19893,
-40908,
-173782,
-177972,
-178205,
-],
-'low' => 
-[
-13312,
-19968,
-131072,
-173824,
-177984,
-],
-},
-
-    );
-
-    # The following hash is a copy of the previous one, except is for loose
-    # matching, so each name has blanks and dashes squeezed out
-    my %loose_names_ending_in_code_point = (
-'CJKCOMPATIBILITYIDEOGRAPH' => 
-{
-'high' => 
-[
-64109,
-64217,
-195101,
-],
-'low' => 
-[
-63744,
-64112,
-194560,
-],
-},
-'CJKUNIFIEDIDEOGRAPH' => 
-{
-'high' => 
-[
-19893,
-40908,
-173782,
-177972,
-178205,
-],
-'low' => 
-[
-13312,
-19968,
-131072,
-173824,
-177984,
-],
-},
-
-    );
-
-    # And the following array gives the inverse mapping from code points to
-    # names.  Lowest code points are first
-    my @code_points_ending_in_code_point = (
-
-{
-'high' => 19893,
-'low' => 13312,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 40908,
-'low' => 19968,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 64109,
-'low' => 63744,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-{
-'high' => 64217,
-'low' => 64112,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-{
-'high' => 173782,
-'low' => 131072,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 177972,
-'low' => 173824,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 178205,
-'low' => 177984,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 195101,
-'low' => 194560,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-,
-
-    );
-
-    # Convert from code point to Jamo short name for use in composing Hangul
-    # syllable names
-    my %Jamo = (
-4352 => 'G',
-4353 => 'GG',
-4354 => 'N',
-4355 => 'D',
-4356 => 'DD',
-4357 => 'R',
-4358 => 'M',
-4359 => 'B',
-4360 => 'BB',
-4361 => 'S',
-4362 => 'SS',
-4363 => '',
-4364 => 'J',
-4365 => 'JJ',
-4366 => 'C',
-4367 => 'K',
-4368 => 'T',
-4369 => 'P',
-4370 => 'H',
-4449 => 'A',
-4450 => 'AE',
-4451 => 'YA',
-4452 => 'YAE',
-4453 => 'EO',
-4454 => 'E',
-4455 => 'YEO',
-4456 => 'YE',
-4457 => 'O',
-4458 => 'WA',
-4459 => 'WAE',
-4460 => 'OE',
-4461 => 'YO',
-4462 => 'U',
-4463 => 'WEO',
-4464 => 'WE',
-4465 => 'WI',
-4466 => 'YU',
-4467 => 'EU',
-4468 => 'YI',
-4469 => 'I',
-4520 => 'G',
-4521 => 'GG',
-4522 => 'GS',
-4523 => 'N',
-4524 => 'NJ',
-4525 => 'NH',
-4526 => 'D',
-4527 => 'L',
-4528 => 'LG',
-4529 => 'LM',
-4530 => 'LB',
-4531 => 'LS',
-4532 => 'LT',
-4533 => 'LP',
-4534 => 'LH',
-4535 => 'M',
-4536 => 'B',
-4537 => 'BS',
-4538 => 'S',
-4539 => 'SS',
-4540 => 'NG',
-4541 => 'J',
-4542 => 'C',
-4543 => 'K',
-4544 => 'T',
-4545 => 'P',
-4546 => 'H',
-
-    );
-
-    # Leading consonant (can be null)
-    my %Jamo_L = (
-'' => 11,
-'B' => 7,
-'BB' => 8,
-'C' => 14,
-'D' => 3,
-'DD' => 4,
-'G' => 0,
-'GG' => 1,
-'H' => 18,
-'J' => 12,
-'JJ' => 13,
-'K' => 15,
-'M' => 6,
-'N' => 2,
-'P' => 17,
-'R' => 5,
-'S' => 9,
-'SS' => 10,
-'T' => 16,
-
-    );
-
-    # Vowel
-    my %Jamo_V = (
-'A' => 0,
-'AE' => 1,
-'E' => 5,
-'EO' => 4,
-'EU' => 18,
-'I' => 20,
-'O' => 8,
-'OE' => 11,
-'U' => 13,
-'WA' => 9,
-'WAE' => 10,
-'WE' => 15,
-'WEO' => 14,
-'WI' => 16,
-'YA' => 2,
-'YAE' => 3,
-'YE' => 7,
-'YEO' => 6,
-'YI' => 19,
-'YO' => 12,
-'YU' => 17,
-
-    );
-
-    # Optional trailing consonant
-    my %Jamo_T = (
-'B' => 17,
-'BS' => 18,
-'C' => 23,
-'D' => 7,
-'G' => 1,
-'GG' => 2,
-'GS' => 3,
-'H' => 27,
-'J' => 22,
-'K' => 24,
-'L' => 8,
-'LB' => 11,
-'LG' => 9,
-'LH' => 15,
-'LM' => 10,
-'LP' => 14,
-'LS' => 12,
-'LT' => 13,
-'M' => 16,
-'N' => 4,
-'NG' => 21,
-'NH' => 6,
-'NJ' => 5,
-'P' => 26,
-'S' => 19,
-'SS' => 20,
-'T' => 25,
-
-    );
-
-    # Computed re that splits up a Hangul name into LVT or LV syllables
-    my $syllable_re = qr/(|B|BB|C|D|DD|G|GG|H|J|JJ|K|M|N|P|R|S|SS|T)(A|AE|E|EO|EU|I|O|OE|U|WA|WAE|WE|WEO|WI|YA|YAE|YE|YEO|YI|YO|YU)(B|BS|C|D|G|GG|GS|H|J|K|L|LB|LG|LH|LM|LP|LS|LT|M|N|NG|NH|NJ|P|S|SS|T)?/;
-
-    my $HANGUL_SYLLABLE = "HANGUL SYLLABLE ";
-    my $loose_HANGUL_SYLLABLE = "HANGULSYLLABLE";
-
-    # These constants names and values were taken from the Unicode standard,
-    # version 5.1, section 3.12.  They are used in conjunction with Hangul
-    # syllables
-    my $SBase = 0xAC00;
-    my $LBase = 0x1100;
-    my $VBase = 0x1161;
-    my $TBase = 0x11A7;
-    my $SCount = 11172;
-    my $LCount = 19;
-    my $VCount = 21;
-    my $TCount = 28;
-    my $NCount = $VCount * $TCount;
-
-    sub name_to_code_point_special {
-        my ($name, $loose) = @_;
-
-        # Returns undef if not one of the specially handled names; otherwise
-        # returns the code point equivalent to the input name
-        # $loose is non-zero if to use loose matching, 'name' in that case
-        # must be input as upper case with all blanks and dashes squeezed out.
-
-        if ((! $loose && $name =~ s/$HANGUL_SYLLABLE//)
-            || ($loose && $name =~ s/$loose_HANGUL_SYLLABLE//))
-        {
-            return if $name !~ qr/^$syllable_re$/;
-            my $L = $Jamo_L{$1};
-            my $V = $Jamo_V{$2};
-            my $T = (defined $3) ? $Jamo_T{$3} : 0;
-            return ($L * $VCount + $V) * $TCount + $T + $SBase;
-        }
-
-        # Name must end in 'code_point' for this to handle.
-        return if (($loose && $name !~ /^ (.*?) ($run_on_code_point_re) $/x)
-                   || (! $loose && $name !~ /^ (.*) ($code_point_re) $/x));
-
-        my $base = $1;
-        my $code_point = CORE::hex $2;
-        my $names_ref;
-
-        if ($loose) {
-            $names_ref = \%loose_names_ending_in_code_point;
-        }
-        else {
-            return if $base !~ s/-$//;
-            $names_ref = \%names_ending_in_code_point;
-        }
-
-        # Name must be one of the ones which has the code point in it.
-        return if ! $names_ref->{$base};
-
-        # Look through the list of ranges that apply to this name to see if
-        # the code point is in one of them.
-        for (my $i = 0; $i < scalar @{$names_ref->{$base}{'low'}}; $i++) {
-            return if $names_ref->{$base}{'low'}->[$i] > $code_point;
-            next if $names_ref->{$base}{'high'}->[$i] < $code_point;
-
-            # Here, the code point is in the range.
-            return $code_point;
-        }
-
-        # Here, looked like the name had a code point number in it, but
-        # did not match one of the valid ones.
-        return;
-    }
-
-    sub code_point_to_name_special {
-        my $code_point = shift;
-
-        # Returns the name of a code point if algorithmically determinable;
-        # undef if not
-
-        # If in the Hangul range, calculate the name based on Unicode's
-        # algorithm
-        if ($code_point >= $SBase && $code_point <= $SBase + $SCount -1) {
-            use integer;
-            my $SIndex = $code_point - $SBase;
-            my $L = $LBase + $SIndex / $NCount;
-            my $V = $VBase + ($SIndex % $NCount) / $TCount;
-            my $T = $TBase + $SIndex % $TCount;
-            $name = "$HANGUL_SYLLABLE$Jamo{$L}$Jamo{$V}";
-            $name .= $Jamo{$T} if $T != $TBase;
-            return $name;
-        }
-
-        # Look through list of these code points for one in range.
-        foreach my $hash (@code_points_ending_in_code_point) {
-            return if $code_point < $hash->{'low'};
-            if ($code_point <= $hash->{'high'}) {
-                return sprintf("%s-%04X", $hash->{'name'}, $code_point);
-            }
-        }
-        return;            # None found
-    }
-} # End closure
-
-1;

commit b6f86dc3c8034679bd56637f008e655bde340264
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 10:13:35 2016 -0400

    Update version and support files for release

diff --git a/Changes b/Changes
index cacbd4e..88eb993 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,6 @@
+2.00 2016-03-29
+ - Refactor to add support for RT 4.4
+
 1.00 2014-12-15
  - Packaging and documentation updates
 
diff --git a/MANIFEST b/MANIFEST
index f59ae26..c60d608 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,5 @@
 Changes
+etc/handle_action_pass_currentuser.patch
 inc/Module/AutoInstall.pm
 inc/Module/Install.pm
 inc/Module/Install/AuthorTests.pm
@@ -15,10 +16,10 @@ inc/Module/Install/RTx/Runtime.pm
 inc/Module/Install/Substitute.pm
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
-inc/unicore/Name.pm
 inc/YAML/Tiny.pm
 lib/RT/Extension/CommandByMail.pm
-lib/RT/Extension/CommandByMail/Test.pm
+lib/RT/Extension/CommandByMail/Test.pm.in
+lib/RT/Interface/Email/Action/CommandByMail.pm
 lib/RT/Interface/Email/Filter/TakeAction.pm
 Makefile.PL
 MANIFEST			This list of files
diff --git a/META.yml b/META.yml
index 2de312b..bd4483a 100644
--- a/META.yml
+++ b/META.yml
@@ -1,5 +1,5 @@
 ---
-abstract: 'Change metadata of ticket via email'
+abstract: 'Change ticket metadata via email'
 author:
   - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
 build_requires:
@@ -10,7 +10,7 @@ configure_requires:
   ExtUtils::MakeMaker: 6.59
 distribution_type: module
 dynamic_config: 1
-generated_by: 'Module::Install version 1.12'
+generated_by: 'Module::Install version 1.16'
 license: gpl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
@@ -18,6 +18,7 @@ meta-spec:
 name: RT-Extension-CommandByMail
 no_index:
   directory:
+    - etc
     - inc
     - xt
   package:
@@ -26,6 +27,6 @@ requires:
   perl: 5.8.3
 resources:
   license: http://opensource.org/licenses/gpl-license.php
-version: '1.00'
-x_module_install_rtx_version: '0.36'
+version: '2.00'
+x_module_install_rtx_version: '0.37'
 x_requires_rt: 4.0.0
diff --git a/README b/README
index 8813e00..2a3189c 100644
--- a/README
+++ b/README
@@ -1,7 +1,12 @@
 NAME
-    RT::Extension::CommandByMail - Change metadata of ticket via email
+    RT::Extension::CommandByMail - Change ticket metadata via email
+
+RT VERSION
+    Works with RT 4.0, 4.2, 4.4
 
 SYNOPSIS
+        (Send email with content that looks like the following.)
+
         Status: stalled
         Subject: change subject
         AddAdminCc: boss at example.com
@@ -10,20 +15,6 @@ SYNOPSIS
 
         The comment/reply text goes here
 
-DESCRIPTION
-    This extension allows you to manage tickets via email interface. You may
-    put commands into the beginning of a mail, and extension will apply
-    them. See the list of commands in the
-    RT::Interface::Email::Filter::TakeAction docs.
-
-    CAVEAT: commands are line oriented, so you can't expand to multiple
-    lines for each command, i.e. values can't contains new lines. The module
-    also currently expects and parses text, not HTML.
-
-SECURITY
-    This extension has no extended auth system; so all security issues that
-    apply to the RT in general also apply to the extension.
-
 INSTALLATION
     perl Makefile.PL
     make
@@ -41,15 +32,148 @@ INSTALLATION
 
         or add RT::Extension::CommandByMail to your existing @Plugins line.
 
-        Regardless of which version of RT, also Filter::TakeAction to your
+        For RT 4.2 or older, also add Filter::TakeAction to your
         @MailPlugins configuration, as follows:
 
             Set(@MailPlugins, qw(Auth::MailFrom Filter::TakeAction));
 
+        For RT 4.4 or newer, the plugin code is in Action::CommandByMail, so
+        add this:
+
+            Set(@MailPlugins, qw(Auth::MailFrom Action::CommandByMail));
+
         Be sure to include Auth::MailFrom in the list as well.
 
+        Note: The plugin name has changed for RT 4.4, so after upgrading you
+        must also update your RT_SiteConfig.pm file to change
+        Filter::TakeAction to the new Action::CommandByMail.
+
+    Patch RT
+        For RT 4.4.0, apply the included patch:
+
+            cd /opt/rt4  # Your location may be different
+            patch -p1 < /download/dir/RT-Extension-CommandByMail/etc/handle_action_pass_currentuser.patch
+
     Restart your webserver
 
+DESCRIPTION
+    This extension allows you to manage tickets via email interface. You may
+    put commands into the beginning of an email, and the extension will
+    apply them. The list of commands is detailed below.
+
+    CAVEAT: commands are line oriented, so you can't expand to multiple
+    lines for each command, i.e. values can't contains new lines. The module
+    also currently expects and parses text, not HTML.
+
+  FORMAT
+    This extension parses the body and headers of incoming messages for list
+    commands. Format of commands is:
+
+        Command: value
+        Command: value
+        ...
+
+    You can find list of "COMMANDS commands below".
+
+    Some commands (like Status, Queue and other) can be used only once.
+    Commands that manage lists can be used multiple times, for example link,
+    custom fields and watchers commands. Also, the latter can be used with
+    Add and Del prefixes to add/delete values from the current list of the
+    ticket you reply to or comment on.
+
+  COMMANDS
+   Basic
+    Queue: <name>
+        Set new queue for the ticket
+
+    Subject: <string>
+        Set new subject to the given string
+
+    Status: <status>
+        Set new status, one of new, open, stalled, resolved, rejected or
+        deleted
+
+    Owner: <username>
+        Set new owner using the given username
+
+    Priority: <#>
+        Set new priority to the given value
+
+    FinalPriority: <#>
+        Set new final priority to the given value
+
+   Dates
+    Set new date/timestamp, or 0 to unset:
+
+        Due: <new timestamp>
+        Starts: <new timestamp>
+        Started: <new timestamp>
+
+   Time
+    Set new times to the given value in minutes. Note that on
+    correspond/comment TimeWorked add time to the current value.
+
+        TimeWorked: <minutes>
+        TimeEstimated: <minutes>
+        TimeLeft: <minutes>
+
+   Watchers
+    Manage watchers: requestors, ccs and admin ccs. This commands can be
+    used several times and/or with Add and Del prefixes, for example
+    Requestor comand set requestor(s) and the current requestors would be
+    deleted, but AddRequestor command adds to the current list.
+
+        Requestor: <address> Set requestor(s) using the email address
+        AddRequestor: <address> Add new requestor using the email address
+        DelRequestor: <address> Remove email address as requestor
+        Cc: <address> Set Cc watcher(s) using the email address
+        AddCc: <address> Add new Cc watcher using the email address
+        DelCc: <address> Remove email address as Cc watcher
+        AdminCc: <address> Set AdminCc watcher(s) using the email address
+        AddAdminCc: <address> Add new AdminCc watcher using the email address
+        DelAdminCc: <address> Remove email address as AdminCc watcher
+
+   Links
+    Manage links. These commands are also could be used several times in one
+    message.
+
+        DependsOn: <ticket id>
+        DependedOnBy: <ticket id>
+        RefersTo: <ticket id>
+        ReferredToBy: <ticket id>
+        Members: <ticket id>
+        MemberOf: <ticket id>
+
+   Custom field values
+    Manage custom field values. Could be used multiple times. (The curly
+    braces are literal.)
+
+        CustomField.{<CFName>}: <custom field value>
+        AddCustomField.{<CFName>}: <custom field value>
+        DelCustomField.{<CFName>}: <custom field value>
+
+    Short forms:
+
+        CF.{<CFName>}: <custom field value>
+        AddCF.{<CFName>}: <custom field value>
+        DelCF.{<CFName>}: <custom field value>
+
+   Transaction Custom field values
+    Manage custom field values of transactions. Could be used multiple
+    times. (The curly braces are literal.)
+
+        TransactionCustomField.{<CFName>}: <custom field value>
+
+    Short forms:
+
+        TxnCustomField.{<CFName>}: <custom field value>
+        TransactionCF.{<CFName>}: <custom field value>
+        TxnCF.{<CFName>}: <custom field value>
+
+SECURITY
+    This extension has no extended auth system; so all security issues that
+    apply to the RT in general also apply to the extension.
+
 CONFIGURATION
   $CommandByMailGroup
     You may set a $CommandByMailGroup to a particular group ID in
@@ -65,20 +189,17 @@ CONFIGURATION
   $CommandByMailOnlyHeaders
     If set, the body will not be examined, only the headers.
 
-COMMANDS
-    This extension parses the body and headers of incoming messages for list
-    commands. Format of commands is:
-
-        Command: value
-        Command: value
-        ...
-
-    See the list of commands in the RT::Interface::Email::Filter::TakeAction
-    docs.
-
 CAVEATS
     This extension is incompatible with UnsafeEmailCommands RT option.
 
+METHODS
+  ProcessCommands
+    This method provides the main email processing functionality. It
+    supports both RT 4.2 and earlier and 4.4 and later. To do this, the
+    return hashes contain some values used by 4.2 code and some used by 4.4.
+    The return values coexist and unused values are ignored by the different
+    versions.
+
 AUTHOR
     Best Practical Solutions, LLC <modules at bestpractical.com>
 
diff --git a/lib/RT/Extension/CommandByMail.pm b/lib/RT/Extension/CommandByMail.pm
index 3e4b4fb..46cf4b7 100644
--- a/lib/RT/Extension/CommandByMail.pm
+++ b/lib/RT/Extension/CommandByMail.pm
@@ -10,12 +10,16 @@ our @LINK_ATTRIBUTES    = qw(MemberOf Parents Members Children
             HasMember RefersTo ReferredToBy DependsOn DependedOnBy);
 our @WATCHER_ATTRIBUTES = qw(Requestor Cc AdminCc);
 
-our $VERSION = '1.00';
+our $VERSION = '2.00';
 
 =head1 NAME
 
 RT::Extension::CommandByMail - Change ticket metadata via email
 
+=head1 RT VERSION
+
+Works with RT 4.0, 4.2, 4.4
+
 =head1 SYNOPSIS
 
     (Send email with content that looks like the following.)

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list