1 package RT::Interface::Email::Filter::TakeAction;
6 use RT::Interface::Email qw(ParseCcAddressesFromHead);
8 our @REGULAR_ATTRIBUTES = qw(Owner Subject Status Priority FinalPriority);
9 our @TIME_ATTRIBUTES = qw(TimeWorked TimeLeft TimeEstimated);
10 our @DATE_ATTRIBUTES = qw(Due Starts Started Resolved Told);
11 our @LINK_ATTRIBUTES = qw(MemberOf Parents Members Children
12 HasMember RefersTo ReferredToBy DependsOn DependedOnBy);
13 our @WATCHER_ATTRIBUTES = qw(Requestor Cc AdminCc);
17 RT::Interface::Email::Filter::TakeAction - Change metadata of ticket via email
21 This extension parse content of incomming messages for list commands. Format
28 You can find list of L</COMMANDS commands below>.
30 Some commands (like Status, Queue and other) can be used only once. Commands
31 that manage lists can be used multiple times, for example link, custom fields
32 and watchers commands. Also, the latter can be used with C<Add> and C<Del>
33 prefixes to add/delete values from the current list of the ticket you reply to
44 Set new queue for the ticket
46 =item Subject: <string>
48 Set new subject to the given string
50 =item Status: <status>
52 Set new status, one of new, open, stalled, resolved, rejected or deleted
54 =item Owner: <username>
56 Set new owner using the given username
60 Set new priority to the given value
62 =item FinalPriority: <#>
64 Set new final priority to the given value
70 Set new date/timestamp, or 0 to unset:
73 Starts: <new timestamp>
74 Started: <new timestamp>
78 Set new times to the given value in minutes. Note that
79 on correspond/comment B<< C<TimeWorked> add time >> to the current
83 TimeEstimated: <minutes>
88 Manage watchers: requestors, ccs and admin ccs. This commands
89 can be used several times and/or with C<Add> and C<Del> prefixes,
90 for example C<Requestor> comand set requestor(s) and the current
91 requestors would be deleted, but C<AddRequestor> command adds
94 Requestor: <address> Set requestor(s) using the email address
95 AddRequestor: <address> Add new requestor using the email address
96 DelRequestor: <address> Remove email address as requestor
97 Cc: <address> Set Cc watcher(s) using the email address
98 AddCc: <address> Add new Cc watcher using the email address
99 DelCc: <address> Remove email address as Cc watcher
100 AdminCc: <address> Set AdminCc watcher(s) using the email address
101 AddAdminCc: <address> Add new AdminCc watcher using the email address
102 DelAdminCc: <address> Remove email address as AdminCc watcher
106 Manage links. These commands are also could be used several times in one
109 DependsOn: <ticket id>
110 DependedOnBy: <ticket id>
111 RefersTo: <ticket id>
112 ReferredToBy: <ticket id>
114 MemberOf: <ticket id>
116 =head3 Custom field values
118 Manage custom field values. Could be used multiple times. (The curly braces
121 CustomField.{<CFName>}: <custom field value>
122 AddCustomField.{<CFName>}: <custom field value>
123 DelCustomField.{<CFName>}: <custom field value>
127 CF.{<CFName>}: <custom field value>
128 AddCF.{<CFName>}: <custom field value>
129 DelCF.{<CFName>}: <custom field value>
133 =head2 GetCurrentUser
135 Returns a CurrentUser object. Also performs all the commands.
142 RawMessageRef => undef,
143 CurrentUser => undef,
151 unless ( $args{'CurrentUser'} ) {
153 "Filter::TakeAction executed when "
154 ."CurrentUser (actor) is not authorized. "
155 ."Most probably you want to add Auth::MailFrom plugin before."
157 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
160 # If the user isn't asking for a comment or a correspond,
162 unless ( $args{'Action'} =~ /^(?:comment|correspond)$/i ) {
163 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
167 my @parts = $args{'Message'}->parts_DFS;
168 foreach my $part (@parts) {
169 my $body = $part->bodyhandle or next;
171 #if it looks like it has pseudoheaders, that's our content
172 if ( $body->as_string =~ /^(?:\S+):/m ) {
173 @content = $body->as_lines;
179 my $found_pseudoheaders = 0;
181 if (defined $RT::CBMSeparator) {$separator = quotemeta($RT::CBMSeparator)}
182 foreach my $line (@content) {
183 next if $line =~ /^\s*$/ && ! $found_pseudoheaders;
184 last if $line !~ /^(?:(\S+)\s*?$separator\s*?(.*)\s*?|)$/;
185 $found_pseudoheaders = 1;
186 push( @items, $1 => $2 );
189 while ( my $key = _CanonicalizeCommand( lc shift @items ) ) {
190 my $val = shift @items;
191 # strip leading and trailing spaces
192 $val =~ s/^\s+|\s+$//g;
194 if ( exists $cmds{$key} ) {
195 $cmds{$key} = [ $cmds{$key} ] unless ref $cmds{$key};
196 push @{ $cmds{$key} }, $val;
204 foreach my $cmd ( keys %cmds ) {
205 my ($val, $msg) = _CheckCommand( $cmd );
208 value => delete $cmds{ $cmd },
215 my $ticket_as_user = RT::Ticket->new( $args{'CurrentUser'} );
216 my $queue = RT::Queue->new( $args{'CurrentUser'} );
217 if ( $cmds{'queue'} ) {
218 $queue->Load( $cmds{'queue'} );
222 $queue->Load( $args{'Queue'}->id );
226 if ( $args{'Ticket'}->id ) {
227 $ticket_as_user->Load( $args{'Ticket'}->id );
229 # we set status later as correspond can reopen ticket
230 foreach my $attribute (grep !/^(Status|TimeWorked)/, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
231 next unless defined $cmds{ lc $attribute };
232 next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
234 # canonicalize owner -- accept an e-mail address
235 if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
236 my $user = RT::User->new($RT::SystemUser);
237 $user->LoadByEmail( $cmds{ lc $attribute } );
238 $cmds{ lc $attribute } = $user->Name if $user->id;
242 $ticket_as_user, $attribute,
243 $cmds{ lc $attribute }, \%results
247 foreach my $attribute (@DATE_ATTRIBUTES) {
248 next unless ( $cmds{ lc $attribute } );
250 my $date = RT::Date->new( $args{'CurrentUser'} );
253 Value => $cmds{ lc $attribute },
255 _SetAttribute( $ticket_as_user, $attribute, $date->ISO,
257 $results{ lc $attribute }->{value} = $cmds{ lc $attribute };
260 foreach my $type ( @WATCHER_ATTRIBUTES ) {
261 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
262 next unless keys %tmp;
264 $tmp{'Default'} = [ do {
266 $method .= 's' if $type eq 'Requestor';
267 $args{'Ticket'}->$method->MemberEmailAddresses;
269 my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
270 foreach my $text ( @$del ) {
271 my $user = RT::User->new($RT::SystemUser);
272 $user->LoadByEmail($text) if $text =~ /\@/;
273 $user->Load($text) unless $user->id;
274 my ( $val, $msg ) = $ticket_as_user->DeleteWatcher(
276 PrincipalId => $user->PrincipalId,
278 push @{ $results{ 'Del'. $type } }, {
284 foreach my $text ( @$add ) {
285 my $user = RT::User->new($RT::SystemUser);
286 $user->LoadByEmail($text) if $text =~ /\@/;
287 $user->Load($text) unless $user->id;
288 my ( $val, $msg ) = $ticket_as_user->AddWatcher(
290 PrincipalId => $user->PrincipalId,
292 push @{ $results{ 'Add'. $type } }, {
302 if (grep $_ eq 'TimeWorked', @TIME_ATTRIBUTES) {
303 if (ref $cmds{'timeworked'}) {
304 map { $time_taken += ($_ || 0) } @{ $cmds{'timeworked'} };
305 $RT::Logger->debug("Time taken: $time_taken");
308 $time_taken = $cmds{'timeworked'} || 0;
312 my $method = ucfirst $args{'Action'};
313 my ($status, $msg) = $ticket_as_user->$method(
314 TimeTaken => $time_taken,
315 MIMEObj => $args{'Message'},
318 $RT::Logger->warning(
319 "Couldn't write $args{'Action'}."
320 ." Fallback to standard mailgate. Error: $msg");
321 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
325 foreach my $type ( @LINK_ATTRIBUTES ) {
326 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
327 next unless keys %tmp;
329 my $link_type = $ticket_as_user->LINKTYPEMAP->{ $type }->{'Type'};
330 my $link_mode = $ticket_as_user->LINKTYPEMAP->{ $type }->{'Mode'};
332 $tmp{'Default'} = [ do {
333 my %h = ( Base => 'Target', Target => 'Base' );
334 my $links = $args{'Ticket'}->_Links( $h{$link_mode}, $link_type );
336 while ( my $link = $links->Next ) {
337 my $method = $link_mode .'URI';
338 my $uri = $link->$method();
339 next unless $uri->IsLocal;
340 push @res, $uri->Object->Id;
344 my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
346 my ($val, $msg) = $ticket_as_user->DeleteLink(
350 $results{ 'Del'. $type } = {
357 my ($val, $msg) = $ticket_as_user->AddLink(
361 $results{ 'Add'. $type } = {
369 my $custom_fields = $queue->TicketCustomFields;
370 while ( my $cf = $custom_fields->Next ) {
371 my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
372 next unless keys %tmp;
374 $tmp{'Default'} = [ do {
375 my $values = $args{'Ticket'}->CustomFieldValues( $cf->id );
377 while ( my $value = $values->Next ) {
378 push @res, $value->Content;
382 my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
384 my ( $val, $msg ) = $ticket_as_user->DeleteCustomFieldValue(
388 $results{ "DelCustomField{". $cf->Name ."}" } = {
395 my ( $val, $msg ) = $ticket_as_user->AddCustomFieldValue(
399 $results{ "DelCustomField{". $cf->Name ."}" } = {
407 foreach my $attribute (grep $_ eq 'Status', @REGULAR_ATTRIBUTES) {
408 next unless defined $cmds{ lc $attribute };
409 next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
410 next if $attribute =~ /deleted/i;
413 $ticket_as_user, $attribute,
414 lc $cmds{ lc $attribute }, \%results
419 Ticket => $args{'Ticket'},
420 Results => \%results,
421 Message => $args{'Message'}
423 return ( $args{'CurrentUser'}, -2 );
427 my %create_args = ();
428 foreach my $attribute (@REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
429 next unless exists $cmds{ lc $attribute };
431 # canonicalize owner -- accept an e-mail address
432 if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
433 my $user = RT::User->new($RT::SystemUser);
434 $user->LoadByEmail( $cmds{ lc $attribute } );
435 $cmds{ lc $attribute } = $user->Name if $user->id;
438 if ( $attribute eq 'TimeWorked' && ref $cmds{ lc $attribute } ) {
440 map { $time_taken += ($_ || 0) } @{ $cmds{'timeworked'} };
441 $cmds{'timeworked'} = $time_taken;
442 $RT::Logger->debug("Time taken on create: $time_taken");
445 if ( $attribute eq 'Status' && $cmds{ lc $attribute } ) {
446 $cmds{ lc $attribute } = lc $cmds{ lc $attribute };
449 $create_args{$attribute} = $cmds{ lc $attribute };
451 foreach my $attribute (@DATE_ATTRIBUTES) {
452 next unless exists $cmds{ lc $attribute };
453 my $date = RT::Date->new( $args{'CurrentUser'} );
456 Value => $cmds{ lc $attribute }
458 $create_args{$attribute} = $date->ISO;
462 foreach my $type ( @LINK_ATTRIBUTES ) {
463 $create_args{ $type } = [ _CompileAdditiveForCreate(
464 _ParseAdditiveCommand( \%cmds, 0, $type ),
468 # Canonicalize custom fields
469 my $custom_fields = $queue->TicketCustomFields;
470 while ( my $cf = $custom_fields->Next ) {
471 my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
472 next unless keys %tmp;
473 $create_args{ 'CustomField-' . $cf->id } = [ _CompileAdditiveForCreate(%tmp) ];
476 # Canonicalize watchers
477 # First of all fetch default values
478 foreach my $type ( @WATCHER_ATTRIBUTES ) {
479 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
480 $tmp{'Default'} = [ $args{'CurrentUser'}->EmailAddress ] if $type eq 'Requestor';
482 ParseCcAddressesFromHead(
483 Head => $args{'Message'}->head,
484 CurrentUser => $args{'CurrentUser'},
485 QueueObj => $args{'Queue'},
487 ] if $type eq 'Cc' && $RT::ParseNewMessageForTicketCcs;
489 $create_args{ $type } = [ _CompileAdditiveForCreate( %tmp ) ];
492 # get queue unless mail contain it
493 $create_args{'Queue'} = $args{'Queue'}->Id unless exists $create_args{'Queue'};
496 unless ( $create_args{'Subject'} ) {
497 $create_args{'Subject'} = $args{'Message'}->head->get('Subject');
498 chomp $create_args{'Subject'};
501 # If we don't already have a ticket, we're going to create a new
504 my ( $id, $txn_id, $msg ) = $ticket_as_user->Create(
506 MIMEObj => $args{'Message'}
509 if ($msg =~ /No permission to create/) {
510 $msg.= "\n\nIf you feel that you should have permission to create ".
511 "this ticket contact us \@ rt-nonrt\@usit.uio.no";
513 $msg = "Couldn't create ticket from message with commands, ".
514 "fallback to standard mailgate.\n\nError: $msg";
515 $RT::Logger->error( $msg );
516 $results{'Create'} = {
521 _ReportResults( Results => \%results, Message => $args{'Message'} );
523 return ($args{'CurrentUser'}, $args{'AuthLevel'});
526 _ReportResults( Results => \%results, Message => $args{'Message'} );
528 # now that we've created a ticket, we abort so we don't create another.
529 $args{'Ticket'}->Load( $id );
530 return ( $args{'CurrentUser'}, -2 );
534 sub _ParseAdditiveCommand {
535 my ($cmds, $plural_forms, $base) = @_;
539 push @types, $base.'s' if $plural_forms;
540 push @types, 'Add'. $base;
541 push @types, 'Add'. $base .'s' if $plural_forms;
542 push @types, 'Del'. $base;
543 push @types, 'Del'. $base .'s' if $plural_forms;
545 foreach my $type ( @types ) {
546 next unless defined $cmds->{lc $type};
548 my @values = ref $cmds->{lc $type} eq 'ARRAY'?
549 @{ $cmds->{lc $type} }: $cmds->{lc $type};
551 if ( $type =~ /^\Q$base\Es?/ ) {
552 push @{ $res{'Set'} }, @values;
553 } elsif ( $type =~ /^Add/ ) {
554 push @{ $res{'Add'} }, @values;
556 push @{ $res{'Del'} }, @values;
563 sub _CompileAdditiveForCreate {
566 unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
567 $cmd{'Default'} = [];
568 } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
569 $cmd{'Default'} = [ $cmd{'Default'} ];
573 @list = @{ $cmd{'Default'} } unless $cmd{'Set'};
574 @list = @{ $cmd{'Set'} } if $cmd{'Set'};
575 push @list, @{ $cmd{'Add'} } if $cmd{'Add'};
578 $seen{$_} = 1 foreach @{ $cmd{'Del'} };
579 @list = grep !$seen{$_}, @list;
584 sub _CompileAdditiveForUpdate {
587 my @new = _CompileAdditiveForCreate( %cmd );
589 unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
590 $cmd{'Default'} = [];
591 } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
592 $cmd{'Default'} = [ $cmd{'Default'} ];
596 unless ( @{ $cmd{'Default'} } ) {
599 $del = $cmd{'Default'};
602 $cur{$_} = 1 foreach @{ $cmd{'Default'} };
603 $new{$_} = 1 foreach @new;
605 $add = [ grep !$cur{$_}, @new ];
606 $del = [ grep !$new{$_}, @{ $cmd{'Default'} } ];
608 $_ ||= [] foreach ($add, $del);
614 my $attribute = shift;
617 my $setter = "Set$attribute";
618 my ( $val, $msg ) = $ticket->$setter($value);
619 $results->{$attribute} = {
626 sub _CanonicalizeCommand {
628 # CustomField commands
629 $key =~ s/^(add|del|)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/$1customfield{$2}/i;
634 my ($cmd, $val) = (lc shift, shift);
635 return 1 if $cmd =~ /^(add|del|)customfield{.*}$/i;
636 if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
637 return 1 unless ref $val;
638 return (0, "Command '$cmd' doesn't support multiple values");
640 return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES;
641 if ( $cmd =~ /^(?:add)(.*)$/i ) {
643 if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
644 return (0, "Command '$cmd' doesn't support multiple values");
646 return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
649 return (0, "Command '$cmd' is unknown");
653 my %args = ( Ticket => undef, Message => undef, Results => {}, @_ );
656 unless ( $args{'Ticket'} ) {
657 $msg .= $args{'Results'}{'Create'}{'message'} || '';
658 $msg .= "\n" if $msg;
659 delete $args{'Results'}{'Create'};
662 foreach my $key ( keys %{ $args{'Results'} } ) {
663 my @records = ref $args{'Results'}->{ $key } eq 'ARRAY'?
664 @{$args{'Results'}->{ $key }}: $args{'Results'}->{ $key };
665 foreach my $rec ( @records ) {
666 next if $rec->{'result'};
667 $msg .= "Your message has been delivered, but command by mail failed:\n\n";
668 $msg .= "Failed command '". $key .": ". $rec->{'value'} ."'\n";
669 $msg .= "Error message: ". ($rec->{'message'}||"(no message)") ."\n\n";
670 $msg .= "UiO has dissabeled some command by mail options.\n";
671 $msg .= "See: http://www.uio.no/tjenester/it/applikasjoner/rt/hjelp/cbm.html\n\n";
674 return unless $msg && $msg !~ /^\s*$/;
676 $RT::Logger->warning( $msg );
677 my $ErrorsTo = RT::Interface::Email::ParseErrorsToAddressFromHead( $args{'Message'}->head );
678 RT::Interface::Email::MailError(
680 Subject => "Command by mail error",
682 MIMEObj => $args{'Message'},
683 Attach => $args{'Message'}->as_string,