Fix for the link maps that are now in RT::Link.
[usit-rt.git] / local / lib / RT / Interface / Email / Filter / TakeAction.pm
CommitLineData
84fb5b46
MKG
1package RT::Interface::Email::Filter::TakeAction;
2
3use warnings;
4use strict;
5
6use RT::Interface::Email qw(ParseCcAddressesFromHead);
7
8our @REGULAR_ATTRIBUTES = qw(Owner Subject Status Priority FinalPriority);
9our @TIME_ATTRIBUTES = qw(TimeWorked TimeLeft TimeEstimated);
10our @DATE_ATTRIBUTES = qw(Due Starts Started Resolved Told);
11our @LINK_ATTRIBUTES = qw(MemberOf Parents Members Children
12 HasMember RefersTo ReferredToBy DependsOn DependedOnBy);
13our @WATCHER_ATTRIBUTES = qw(Requestor Cc AdminCc);
14
15=head1 NAME
16
17RT::Interface::Email::Filter::TakeAction - Change metadata of ticket via email
18
19=head1 DESCRIPTION
20
21This extension parse content of incomming messages for list commands. Format
22of commands is:
23
24 Command: value
25 Command: value
26 ...
27
28You can find list of L</COMMANDS commands below>.
29
30Some commands (like Status, Queue and other) can be used only once. Commands
31that manage lists can be used multiple times, for example link, custom fields
32and watchers commands. Also, the latter can be used with C<Add> and C<Del>
33prefixes to add/delete values from the current list of the ticket you reply to
34or comment on.
35
36=head2 COMMANDS
37
38=head3 Basic
39
40=over 4
41
42=item Queue: <name>
43
44Set new queue for the ticket
45
46=item Subject: <string>
47
48Set new subject to the given string
49
50=item Status: <status>
51
52Set new status, one of new, open, stalled, resolved, rejected or deleted
53
54=item Owner: <username>
55
56Set new owner using the given username
57
58=item Priority: <#>
59
60Set new priority to the given value
61
62=item FinalPriority: <#>
63
64Set new final priority to the given value
65
66=back
67
68=head3 Dates
69
70Set new date/timestamp, or 0 to unset:
71
72 Due: <new timestamp>
73 Starts: <new timestamp>
74 Started: <new timestamp>
75
76=head3 Time
77
78Set new times to the given value in minutes. Note that
79on correspond/comment B<< C<TimeWorked> add time >> to the current
80value.
81
82 TimeWorked: <minutes>
83 TimeEstimated: <minutes>
84 TimeLeft: <minutes>
85
86=head3 Watchers
87
88Manage watchers: requestors, ccs and admin ccs. This commands
89can be used several times and/or with C<Add> and C<Del> prefixes,
90for example C<Requestor> comand set requestor(s) and the current
91requestors would be deleted, but C<AddRequestor> command adds
92to the current list.
93
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
103
104=head3 Links
105
106Manage links. These commands are also could be used several times in one
107message.
108
109 DependsOn: <ticket id>
110 DependedOnBy: <ticket id>
111 RefersTo: <ticket id>
112 ReferredToBy: <ticket id>
113 Members: <ticket id>
114 MemberOf: <ticket id>
115
116=head3 Custom field values
117
118Manage custom field values. Could be used multiple times. (The curly braces
119are literal.)
120
121 CustomField.{<CFName>}: <custom field value>
122 AddCustomField.{<CFName>}: <custom field value>
123 DelCustomField.{<CFName>}: <custom field value>
124
125Short forms:
126
127 CF.{<CFName>}: <custom field value>
128 AddCF.{<CFName>}: <custom field value>
129 DelCF.{<CFName>}: <custom field value>
130
131=cut
132
133=head2 GetCurrentUser
134
135Returns a CurrentUser object. Also performs all the commands.
136
137=cut
138
139sub GetCurrentUser {
140 my %args = (
141 Message => undef,
142 RawMessageRef => undef,
143 CurrentUser => undef,
144 AuthLevel => undef,
145 Action => undef,
146 Ticket => undef,
147 Queue => undef,
148 @_
149 );
150
151 unless ( $args{'CurrentUser'} ) {
152 $RT::Logger->error(
153 "Filter::TakeAction executed when "
154 ."CurrentUser (actor) is not authorized. "
155 ."Most probably you want to add Auth::MailFrom plugin before."
156 );
157 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
158 }
159
160 # If the user isn't asking for a comment or a correspond,
161 # bail out
162 unless ( $args{'Action'} =~ /^(?:comment|correspond)$/i ) {
163 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
164 }
165
166 my @content;
167 my @parts = $args{'Message'}->parts_DFS;
168 foreach my $part (@parts) {
169 my $body = $part->bodyhandle or next;
170
171 #if it looks like it has pseudoheaders, that's our content
172 if ( $body->as_string =~ /^(?:\S+):/m ) {
173 @content = $body->as_lines;
174 last;
175 }
176 }
177
178 my @items;
179 my $found_pseudoheaders = 0;
180 my $separator = ":";
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 );
187 }
188 my %cmds;
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;
193
194 if ( exists $cmds{$key} ) {
195 $cmds{$key} = [ $cmds{$key} ] unless ref $cmds{$key};
196 push @{ $cmds{$key} }, $val;
197 } else {
198 $cmds{$key} = $val;
199 }
200 }
201
202 my %results;
203
204 foreach my $cmd ( keys %cmds ) {
205 my ($val, $msg) = _CheckCommand( $cmd );
206 unless ( $val ) {
207 $results{ $cmd } = {
208 value => delete $cmds{ $cmd },
209 result => $val,
210 message => $msg,
211 };
212 }
213 }
214
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'} );
219 }
220
221 if ( !$queue->id ) {
222 $queue->Load( $args{'Queue'}->id );
223 }
224
225 # If we're updating.
226 if ( $args{'Ticket'}->id ) {
227 $ticket_as_user->Load( $args{'Ticket'}->id );
228
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 };
233
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;
239 }
240
241 _SetAttribute(
242 $ticket_as_user, $attribute,
243 $cmds{ lc $attribute }, \%results
244 );
245 }
246
247 foreach my $attribute (@DATE_ATTRIBUTES) {
248 next unless ( $cmds{ lc $attribute } );
249
250 my $date = RT::Date->new( $args{'CurrentUser'} );
251 $date->Set(
252 Format => 'unknown',
253 Value => $cmds{ lc $attribute },
254 );
255 _SetAttribute( $ticket_as_user, $attribute, $date->ISO,
256 \%results );
257 $results{ lc $attribute }->{value} = $cmds{ lc $attribute };
258 }
259
260 foreach my $type ( @WATCHER_ATTRIBUTES ) {
261 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
262 next unless keys %tmp;
263
264 $tmp{'Default'} = [ do {
265 my $method = $type;
266 $method .= 's' if $type eq 'Requestor';
267 $args{'Ticket'}->$method->MemberEmailAddresses;
268 } ];
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(
275 Type => $type,
276 PrincipalId => $user->PrincipalId,
277 );
278 push @{ $results{ 'Del'. $type } }, {
279 value => $text,
280 result => $val,
281 message => $msg
282 };
283 }
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(
289 Type => $type,
290 PrincipalId => $user->PrincipalId,
291 );
292 push @{ $results{ 'Add'. $type } }, {
293 value => $text,
294 result => $val,
295 message => $msg
296 };
297 }
298 }
299
300 {
301 my $time_taken = 0;
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");
306 }
307 else {
308 $time_taken = $cmds{'timeworked'} || 0;
309 }
310 }
311
312 my $method = ucfirst $args{'Action'};
313 my ($status, $msg) = $ticket_as_user->$method(
314 TimeTaken => $time_taken,
315 MIMEObj => $args{'Message'},
316 );
317 unless ( $status ) {
318 $RT::Logger->warning(
319 "Couldn't write $args{'Action'}."
320 ." Fallback to standard mailgate. Error: $msg");
321 return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
322 }
323 }
324
325 foreach my $type ( @LINK_ATTRIBUTES ) {
326 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
327 next unless keys %tmp;
328
2d967a12
MKG
329 my $typemap = keys %RT::Link::TYPEMAP ? \%RT::Link::TYPEMAP : $ticket_as_user->LINKTYPEMAP;
330 my $link_type = $typemap->{$type}->{'Type'};
331 my $link_mode = $typemap->{$type}->{'Mode'};
84fb5b46
MKG
332
333 $tmp{'Default'} = [ do {
334 my %h = ( Base => 'Target', Target => 'Base' );
335 my $links = $args{'Ticket'}->_Links( $h{$link_mode}, $link_type );
336 my @res;
337 while ( my $link = $links->Next ) {
338 my $method = $link_mode .'URI';
339 my $uri = $link->$method();
340 next unless $uri->IsLocal;
341 push @res, $uri->Object->Id;
342 }
343 @res;
344 } ];
345 my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
346 foreach ( @$del ) {
347 my ($val, $msg) = $ticket_as_user->DeleteLink(
348 Type => $link_type,
349 $link_mode => $_,
350 );
351 $results{ 'Del'. $type } = {
352 value => $_,
353 result => $val,
354 message => $msg,
355 };
356 }
357 foreach ( @$add ) {
358 my ($val, $msg) = $ticket_as_user->AddLink(
359 Type => $link_type,
360 $link_mode => $_,
361 );
362 $results{ 'Add'. $type } = {
363 value => $_,
364 result => $val,
365 message => $msg,
366 };
367 }
368 }
369
370 my $custom_fields = $queue->TicketCustomFields;
371 while ( my $cf = $custom_fields->Next ) {
372 my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
373 next unless keys %tmp;
374
375 $tmp{'Default'} = [ do {
376 my $values = $args{'Ticket'}->CustomFieldValues( $cf->id );
377 my @res;
378 while ( my $value = $values->Next ) {
379 push @res, $value->Content;
380 }
381 @res;
382 } ];
383 my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
384 foreach ( @$del ) {
385 my ( $val, $msg ) = $ticket_as_user->DeleteCustomFieldValue(
386 Field => $cf->id,
387 Value => $_
388 );
389 $results{ "DelCustomField{". $cf->Name ."}" } = {
390 value => $_,
391 result => $val,
392 message => $msg,
393 };
394 }
395 foreach ( @$add ) {
396 my ( $val, $msg ) = $ticket_as_user->AddCustomFieldValue(
397 Field => $cf->id,
398 Value => $_
399 );
400 $results{ "DelCustomField{". $cf->Name ."}" } = {
401 value => $_,
402 result => $val,
403 message => $msg,
404 };
405 }
406 }
407
408 foreach my $attribute (grep $_ eq 'Status', @REGULAR_ATTRIBUTES) {
409 next unless defined $cmds{ lc $attribute };
410 next if $ticket_as_user->$attribute() eq $cmds{ lc $attribute };
411 next if $attribute =~ /deleted/i;
412
413 _SetAttribute(
414 $ticket_as_user, $attribute,
415 lc $cmds{ lc $attribute }, \%results
416 );
417 }
418
419 _ReportResults(
420 Ticket => $args{'Ticket'},
421 Results => \%results,
422 Message => $args{'Message'}
423 );
424 return ( $args{'CurrentUser'}, -2 );
425
426 } else {
427
428 my %create_args = ();
429 foreach my $attribute (@REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
430 next unless exists $cmds{ lc $attribute };
431
432 # canonicalize owner -- accept an e-mail address
433 if ( $attribute eq 'Owner' && $cmds{ lc $attribute } =~ /\@/ ) {
434 my $user = RT::User->new($RT::SystemUser);
435 $user->LoadByEmail( $cmds{ lc $attribute } );
436 $cmds{ lc $attribute } = $user->Name if $user->id;
437 }
438
439 if ( $attribute eq 'TimeWorked' && ref $cmds{ lc $attribute } ) {
440 my $time_taken = 0;
441 map { $time_taken += ($_ || 0) } @{ $cmds{'timeworked'} };
442 $cmds{'timeworked'} = $time_taken;
443 $RT::Logger->debug("Time taken on create: $time_taken");
444 }
445
446 if ( $attribute eq 'Status' && $cmds{ lc $attribute } ) {
447 $cmds{ lc $attribute } = lc $cmds{ lc $attribute };
448 }
449
450 $create_args{$attribute} = $cmds{ lc $attribute };
451 }
452 foreach my $attribute (@DATE_ATTRIBUTES) {
453 next unless exists $cmds{ lc $attribute };
454 my $date = RT::Date->new( $args{'CurrentUser'} );
455 $date->Set(
456 Format => 'unknown',
457 Value => $cmds{ lc $attribute }
458 );
459 $create_args{$attribute} = $date->ISO;
460 }
461
462 # Canonicalize links
463 foreach my $type ( @LINK_ATTRIBUTES ) {
464 $create_args{ $type } = [ _CompileAdditiveForCreate(
465 _ParseAdditiveCommand( \%cmds, 0, $type ),
466 ) ];
467 }
468
469 # Canonicalize custom fields
470 my $custom_fields = $queue->TicketCustomFields;
471 while ( my $cf = $custom_fields->Next ) {
472 my %tmp = _ParseAdditiveCommand( \%cmds, 0, "CustomField{". $cf->Name ."}" );
473 next unless keys %tmp;
474 $create_args{ 'CustomField-' . $cf->id } = [ _CompileAdditiveForCreate(%tmp) ];
475 }
476
477 # Canonicalize watchers
478 # First of all fetch default values
479 foreach my $type ( @WATCHER_ATTRIBUTES ) {
480 my %tmp = _ParseAdditiveCommand( \%cmds, 1, $type );
481 $tmp{'Default'} = [ $args{'CurrentUser'}->EmailAddress ] if $type eq 'Requestor';
482 $tmp{'Default'} = [
483 ParseCcAddressesFromHead(
484 Head => $args{'Message'}->head,
485 CurrentUser => $args{'CurrentUser'},
486 QueueObj => $args{'Queue'},
487 )
488 ] if $type eq 'Cc' && $RT::ParseNewMessageForTicketCcs;
489
490 $create_args{ $type } = [ _CompileAdditiveForCreate( %tmp ) ];
491 }
492
493 # get queue unless mail contain it
494 $create_args{'Queue'} = $args{'Queue'}->Id unless exists $create_args{'Queue'};
495
496 # subject
497 unless ( $create_args{'Subject'} ) {
498 $create_args{'Subject'} = $args{'Message'}->head->get('Subject');
499 chomp $create_args{'Subject'};
500 }
501
502 # If we don't already have a ticket, we're going to create a new
503 # ticket
504
505 my ( $id, $txn_id, $msg ) = $ticket_as_user->Create(
506 %create_args,
507 MIMEObj => $args{'Message'}
508 );
509 unless ( $id ) {
fe26c0cc
MKG
510 if ($msg =~ /No permission to create/) {
511 $msg.= "\n\nIf you feel that you should have permission to create ".
512 "this ticket contact us \@ rt-nonrt\@usit.uio.no";
513 };
84fb5b46
MKG
514 $msg = "Couldn't create ticket from message with commands, ".
515 "fallback to standard mailgate.\n\nError: $msg";
516 $RT::Logger->error( $msg );
517 $results{'Create'} = {
518 result => $id,
519 message => $msg,
520 };
521
522 _ReportResults( Results => \%results, Message => $args{'Message'} );
523
524 return ($args{'CurrentUser'}, $args{'AuthLevel'});
525 }
526
527 _ReportResults( Results => \%results, Message => $args{'Message'} );
528
529 # now that we've created a ticket, we abort so we don't create another.
530 $args{'Ticket'}->Load( $id );
531 return ( $args{'CurrentUser'}, -2 );
532 }
533}
534
535sub _ParseAdditiveCommand {
536 my ($cmds, $plural_forms, $base) = @_;
537 my (%res);
538
539 my @types = $base;
540 push @types, $base.'s' if $plural_forms;
541 push @types, 'Add'. $base;
542 push @types, 'Add'. $base .'s' if $plural_forms;
543 push @types, 'Del'. $base;
544 push @types, 'Del'. $base .'s' if $plural_forms;
545
546 foreach my $type ( @types ) {
547 next unless defined $cmds->{lc $type};
548
549 my @values = ref $cmds->{lc $type} eq 'ARRAY'?
550 @{ $cmds->{lc $type} }: $cmds->{lc $type};
551
552 if ( $type =~ /^\Q$base\Es?/ ) {
553 push @{ $res{'Set'} }, @values;
554 } elsif ( $type =~ /^Add/ ) {
555 push @{ $res{'Add'} }, @values;
556 } else {
557 push @{ $res{'Del'} }, @values;
558 }
559 }
560
561 return %res;
562}
563
564sub _CompileAdditiveForCreate {
565 my %cmd = @_;
566
567 unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
568 $cmd{'Default'} = [];
569 } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
570 $cmd{'Default'} = [ $cmd{'Default'} ];
571 }
572
573 my @list;
574 @list = @{ $cmd{'Default'} } unless $cmd{'Set'};
575 @list = @{ $cmd{'Set'} } if $cmd{'Set'};
576 push @list, @{ $cmd{'Add'} } if $cmd{'Add'};
577 if ( $cmd{'Del'} ) {
578 my %seen;
579 $seen{$_} = 1 foreach @{ $cmd{'Del'} };
580 @list = grep !$seen{$_}, @list;
581 }
582 return @list;
583}
584
585sub _CompileAdditiveForUpdate {
586 my %cmd = @_;
587
588 my @new = _CompileAdditiveForCreate( %cmd );
589
590 unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
591 $cmd{'Default'} = [];
592 } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
593 $cmd{'Default'} = [ $cmd{'Default'} ];
594 }
595
596 my ($add, $del);
597 unless ( @{ $cmd{'Default'} } ) {
598 $add = \@new;
599 } elsif ( !@new ) {
600 $del = $cmd{'Default'};
601 } else {
602 my (%cur, %new);
603 $cur{$_} = 1 foreach @{ $cmd{'Default'} };
604 $new{$_} = 1 foreach @new;
605
606 $add = [ grep !$cur{$_}, @new ];
607 $del = [ grep !$new{$_}, @{ $cmd{'Default'} } ];
608 }
609 $_ ||= [] foreach ($add, $del);
610 return ($add, $del);
611}
612
613sub _SetAttribute {
614 my $ticket = shift;
615 my $attribute = shift;
616 my $value = shift;
617 my $results = shift;
618 my $setter = "Set$attribute";
619 my ( $val, $msg ) = $ticket->$setter($value);
620 $results->{$attribute} = {
621 value => $value,
622 result => $val,
623 message => $msg
624 };
625}
626
627sub _CanonicalizeCommand {
628 my $key = shift;
629 # CustomField commands
630 $key =~ s/^(add|del|)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/$1customfield{$2}/i;
631 return $key;
632}
633
634sub _CheckCommand {
635 my ($cmd, $val) = (lc shift, shift);
636 return 1 if $cmd =~ /^(add|del|)customfield{.*}$/i;
637 if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
638 return 1 unless ref $val;
639 return (0, "Command '$cmd' doesn't support multiple values");
640 }
641 return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES;
642 if ( $cmd =~ /^(?:add)(.*)$/i ) {
643 my $cmd = $1;
644 if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
645 return (0, "Command '$cmd' doesn't support multiple values");
646 }
647 return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
648 }
649
650 return (0, "Command '$cmd' is unknown");
651}
652
653sub _ReportResults {
654 my %args = ( Ticket => undef, Message => undef, Results => {}, @_ );
655
656 my $msg = '';
657 unless ( $args{'Ticket'} ) {
658 $msg .= $args{'Results'}{'Create'}{'message'} || '';
659 $msg .= "\n" if $msg;
660 delete $args{'Results'}{'Create'};
661 }
662
663 foreach my $key ( keys %{ $args{'Results'} } ) {
664 my @records = ref $args{'Results'}->{ $key } eq 'ARRAY'?
665 @{$args{'Results'}->{ $key }}: $args{'Results'}->{ $key };
666 foreach my $rec ( @records ) {
667 next if $rec->{'result'};
668 $msg .= "Your message has been delivered, but command by mail failed:\n\n";
669 $msg .= "Failed command '". $key .": ". $rec->{'value'} ."'\n";
670 $msg .= "Error message: ". ($rec->{'message'}||"(no message)") ."\n\n";
671 $msg .= "UiO has dissabeled some command by mail options.\n";
672 $msg .= "See: http://www.uio.no/tjenester/it/applikasjoner/rt/hjelp/cbm.html\n\n";
673 }
674 }
675 return unless $msg && $msg !~ /^\s*$/;
676
677 $RT::Logger->warning( $msg );
678 my $ErrorsTo = RT::Interface::Email::ParseErrorsToAddressFromHead( $args{'Message'}->head );
679 RT::Interface::Email::MailError(
680 To => $ErrorsTo,
681 Subject => "Command by mail error",
682 Explanation => $msg,
683 MIMEObj => $args{'Message'},
684 Attach => $args{'Message'}->as_string,
685 );
686 return;
687}
688
6891;