Initial commit 4.0.5-3
[usit-rt.git] / local / lib / RT / Interface / Email / Filter / TakeAction.pm
1 package RT::Interface::Email::Filter::TakeAction;
2
3 use warnings;
4 use strict;
5
6 use RT::Interface::Email qw(ParseCcAddressesFromHead);
7
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);
14
15 =head1 NAME
16
17 RT::Interface::Email::Filter::TakeAction - Change metadata of ticket via email
18
19 =head1 DESCRIPTION
20
21 This extension parse content of incomming messages for list commands. Format
22 of commands is:
23
24     Command: value
25     Command: value
26     ...
27
28 You can find list of L</COMMANDS commands below>.
29
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
34 or comment on.
35
36 =head2 COMMANDS
37
38 =head3 Basic
39
40 =over 4
41
42 =item Queue: <name>
43
44 Set new queue for the ticket
45
46 =item Subject: <string>
47
48 Set new subject to the given string
49
50 =item Status: <status>
51
52 Set new status, one of new, open, stalled, resolved, rejected or deleted
53
54 =item Owner: <username>
55
56 Set new owner using the given username
57
58 =item Priority: <#>
59
60 Set new priority to the given value
61
62 =item FinalPriority: <#>
63
64 Set new final priority to the given value
65
66 =back
67
68 =head3 Dates
69
70 Set 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
78 Set new times to the given value in minutes. Note that
79 on correspond/comment B<< C<TimeWorked> add time >> to the current
80 value.
81
82     TimeWorked: <minutes>
83     TimeEstimated: <minutes>
84     TimeLeft: <minutes>
85
86 =head3 Watchers
87
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
92 to 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
106 Manage links. These commands are also could be used several times in one
107 message.
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
118 Manage custom field values. Could be used multiple times.  (The curly braces
119 are literal.)
120
121     CustomField.{<CFName>}: <custom field value>
122     AddCustomField.{<CFName>}: <custom field value>
123     DelCustomField.{<CFName>}: <custom field value>
124
125 Short 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
135 Returns a CurrentUser object.  Also performs all the commands.
136
137 =cut
138
139 sub 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
329             my $link_type = $ticket_as_user->LINKTYPEMAP->{ $type }->{'Type'};
330             my $link_mode = $ticket_as_user->LINKTYPEMAP->{ $type }->{'Mode'};
331
332             $tmp{'Default'} = [ do {
333                 my %h = ( Base => 'Target', Target => 'Base' );
334                 my $links = $args{'Ticket'}->_Links( $h{$link_mode}, $link_type );
335                 my @res;
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;
341                 }
342                 @res;
343             } ];
344             my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
345             foreach ( @$del ) {
346                 my ($val, $msg) = $ticket_as_user->DeleteLink(
347                     Type => $link_type,
348                     $link_mode => $_,
349                 );
350                 $results{ 'Del'. $type } = {
351                     value => $_,
352                     result => $val,
353                     message => $msg,
354                 };
355             }
356             foreach ( @$add ) {
357                 my ($val, $msg) = $ticket_as_user->AddLink(
358                     Type => $link_type,
359                     $link_mode => $_,
360                 );
361                 $results{ 'Add'. $type } = {
362                     value => $_,
363                     result => $val,
364                     message => $msg,
365                 };
366             }
367         }
368
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;
373
374             $tmp{'Default'} = [ do {
375                 my $values = $args{'Ticket'}->CustomFieldValues( $cf->id );
376                 my @res;
377                 while ( my $value = $values->Next ) {
378                     push @res, $value->Content;
379                 }
380                 @res;
381             } ];
382             my ($add, $del) = _CompileAdditiveForUpdate( %tmp );
383             foreach ( @$del ) {
384                 my ( $val, $msg ) = $ticket_as_user->DeleteCustomFieldValue(
385                     Field => $cf->id,
386                     Value => $_
387                 );
388                 $results{ "DelCustomField{". $cf->Name ."}" } = {
389                     value => $_,
390                     result => $val,
391                     message => $msg,
392                 };
393             }
394             foreach ( @$add ) {
395                 my ( $val, $msg ) = $ticket_as_user->AddCustomFieldValue(
396                     Field => $cf->id,
397                     Value => $_
398                 );
399                 $results{ "DelCustomField{". $cf->Name ."}" } = {
400                     value => $_,
401                     result => $val,
402                     message => $msg,
403                 };
404             }
405         }
406
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;
411
412             _SetAttribute(
413                 $ticket_as_user,        $attribute,
414                 lc $cmds{ lc $attribute }, \%results
415             );
416         }
417
418         _ReportResults(
419             Ticket => $args{'Ticket'},
420             Results => \%results,
421             Message => $args{'Message'}
422         );
423         return ( $args{'CurrentUser'}, -2 );
424
425     } else {
426
427         my %create_args = ();
428         foreach my $attribute (@REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES) {
429             next unless exists $cmds{ lc $attribute };
430
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;
436             }
437
438             if ( $attribute eq 'TimeWorked' && ref $cmds{ lc $attribute } ) {
439                 my $time_taken = 0;
440                 map { $time_taken += ($_ || 0) }  @{ $cmds{'timeworked'} };
441                 $cmds{'timeworked'} = $time_taken;
442                 $RT::Logger->debug("Time taken on create: $time_taken");
443             }
444
445             if ( $attribute eq 'Status' && $cmds{ lc $attribute } ) {
446                 $cmds{ lc $attribute } = lc $cmds{ lc $attribute };
447             }
448
449             $create_args{$attribute} = $cmds{ lc $attribute };
450         }
451         foreach my $attribute (@DATE_ATTRIBUTES) {
452             next unless exists $cmds{ lc $attribute };
453             my $date = RT::Date->new( $args{'CurrentUser'} );
454             $date->Set(
455                 Format => 'unknown',
456                 Value  => $cmds{ lc $attribute }
457             );
458             $create_args{$attribute} = $date->ISO;
459         }
460
461         # Canonicalize links
462         foreach my $type ( @LINK_ATTRIBUTES ) {
463             $create_args{ $type } = [ _CompileAdditiveForCreate( 
464                 _ParseAdditiveCommand( \%cmds, 0, $type ),
465             ) ];
466         }
467
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) ];
474         }
475
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';
481             $tmp{'Default'} = [
482                 ParseCcAddressesFromHead(
483                     Head        => $args{'Message'}->head,
484                     CurrentUser => $args{'CurrentUser'},
485                     QueueObj    => $args{'Queue'},
486                 )
487             ] if $type eq 'Cc' && $RT::ParseNewMessageForTicketCcs;
488
489             $create_args{ $type } = [ _CompileAdditiveForCreate( %tmp ) ];
490         }
491
492         # get queue unless mail contain it
493         $create_args{'Queue'} = $args{'Queue'}->Id unless exists $create_args{'Queue'};
494
495         # subject
496         unless ( $create_args{'Subject'} ) {
497             $create_args{'Subject'} = $args{'Message'}->head->get('Subject');
498             chomp $create_args{'Subject'};
499         }
500
501         # If we don't already have a ticket, we're going to create a new
502         # ticket
503
504         my ( $id, $txn_id, $msg ) = $ticket_as_user->Create(
505             %create_args,
506             MIMEObj => $args{'Message'}
507         );
508         unless ( $id ) {
509             $msg = "Couldn't create ticket from message with commands, ".
510                    "fallback to standard mailgate.\n\nError: $msg";
511             $RT::Logger->error( $msg );
512             $results{'Create'} = {
513                 result => $id,
514                 message => $msg,
515             };
516
517             _ReportResults( Results => \%results, Message => $args{'Message'} );
518
519             return ($args{'CurrentUser'}, $args{'AuthLevel'});
520         }
521
522         _ReportResults( Results => \%results, Message => $args{'Message'} );
523
524         # now that we've created a ticket, we abort so we don't create another.
525         $args{'Ticket'}->Load( $id );
526         return ( $args{'CurrentUser'}, -2 );
527     }
528 }
529
530 sub _ParseAdditiveCommand {
531     my ($cmds, $plural_forms, $base) = @_;
532     my (%res);
533
534     my @types = $base;
535     push @types, $base.'s' if $plural_forms;
536     push @types, 'Add'. $base;
537     push @types, 'Add'. $base .'s' if $plural_forms;
538     push @types, 'Del'. $base;
539     push @types, 'Del'. $base .'s' if $plural_forms;
540
541     foreach my $type ( @types ) {
542         next unless defined $cmds->{lc $type};
543
544         my @values = ref $cmds->{lc $type} eq 'ARRAY'?
545             @{ $cmds->{lc $type} }: $cmds->{lc $type};
546
547         if ( $type =~ /^\Q$base\Es?/ ) {
548             push @{ $res{'Set'} }, @values;
549         } elsif ( $type =~ /^Add/ ) {
550             push @{ $res{'Add'} }, @values;
551         } else {
552             push @{ $res{'Del'} }, @values;
553         }
554     }
555
556     return %res;
557 }
558
559 sub _CompileAdditiveForCreate {
560     my %cmd = @_;
561
562     unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
563         $cmd{'Default'} = [];
564     } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
565         $cmd{'Default'} = [ $cmd{'Default'} ];
566     }
567
568     my @list;
569     @list = @{ $cmd{'Default'} } unless $cmd{'Set'};
570     @list = @{ $cmd{'Set'} } if $cmd{'Set'};
571     push @list, @{ $cmd{'Add'} } if $cmd{'Add'};
572     if ( $cmd{'Del'} ) {
573         my %seen;
574         $seen{$_} = 1 foreach @{ $cmd{'Del'} };
575         @list = grep !$seen{$_}, @list;
576     }
577     return @list;
578 }
579
580 sub _CompileAdditiveForUpdate {
581     my %cmd = @_;
582
583     my @new = _CompileAdditiveForCreate( %cmd );
584
585     unless ( exists $cmd{'Default'} && defined $cmd{'Default'} ) {
586         $cmd{'Default'} = [];
587     } elsif ( ref $cmd{'Default'} ne 'ARRAY' ) {
588         $cmd{'Default'} = [ $cmd{'Default'} ];
589     }
590
591     my ($add, $del);
592     unless ( @{ $cmd{'Default'} } ) {
593         $add = \@new;
594     } elsif ( !@new ) {
595         $del = $cmd{'Default'};
596     } else {
597         my (%cur, %new);
598         $cur{$_} = 1 foreach @{ $cmd{'Default'} };
599         $new{$_} = 1 foreach @new;
600
601         $add = [ grep !$cur{$_}, @new ];
602         $del = [ grep !$new{$_}, @{ $cmd{'Default'} } ];
603     }
604     $_ ||= [] foreach ($add, $del);
605     return ($add, $del);
606 }
607
608 sub _SetAttribute {
609     my $ticket    = shift;
610     my $attribute = shift;
611     my $value     = shift;
612     my $results   = shift;
613     my $setter    = "Set$attribute";
614     my ( $val, $msg ) = $ticket->$setter($value);
615     $results->{$attribute} = {
616         value   => $value,
617         result  => $val,
618         message => $msg
619     };
620 }
621
622 sub _CanonicalizeCommand {
623     my $key = shift;
624     # CustomField commands
625     $key =~ s/^(add|del|)c(?:ustom)?-?f(?:ield)?\.?[({\[](.*)[)}\]]$/$1customfield{$2}/i;
626     return $key;
627 }
628
629 sub _CheckCommand {
630     my ($cmd, $val) = (lc shift, shift);
631     return 1 if $cmd =~ /^(add|del|)customfield{.*}$/i;
632     if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
633         return 1 unless ref $val;
634         return (0, "Command '$cmd' doesn't support multiple values");
635     }
636     return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES;
637     if ( $cmd =~ /^(?:add)(.*)$/i ) {
638         my $cmd = $1;
639         if ( grep $cmd eq lc $_, @REGULAR_ATTRIBUTES, @TIME_ATTRIBUTES, @DATE_ATTRIBUTES ) {
640             return (0, "Command '$cmd' doesn't support multiple values");
641         }
642         return 1 if grep $cmd eq lc $_, @LINK_ATTRIBUTES, @WATCHER_ATTRIBUTES;
643     }
644
645     return (0, "Command '$cmd' is unknown");
646 }
647
648 sub _ReportResults {
649     my %args = ( Ticket => undef, Message => undef, Results => {}, @_ );
650
651     my $msg = '';
652     unless ( $args{'Ticket'} ) {
653         $msg .= $args{'Results'}{'Create'}{'message'} || '';
654         $msg .= "\n" if $msg;
655         delete $args{'Results'}{'Create'};
656     }
657
658     foreach my $key ( keys %{ $args{'Results'} } ) {
659         my @records = ref $args{'Results'}->{ $key } eq 'ARRAY'?
660                          @{$args{'Results'}->{ $key }}: $args{'Results'}->{ $key };
661         foreach my $rec ( @records ) {
662             next if $rec->{'result'};
663             $msg .= "Your message has been delivered, but command by mail failed:\n\n";
664             $msg .= "Failed command '". $key .": ". $rec->{'value'} ."'\n";
665             $msg .= "Error message: ". ($rec->{'message'}||"(no message)") ."\n\n";
666             $msg .= "UiO has dissabeled some command by mail options.\n";
667             $msg .= "See: http://www.uio.no/tjenester/it/applikasjoner/rt/hjelp/cbm.html\n\n";
668         }
669     }
670     return unless $msg && $msg !~ /^\s*$/;
671
672     $RT::Logger->warning( $msg );
673     my $ErrorsTo = RT::Interface::Email::ParseErrorsToAddressFromHead( $args{'Message'}->head );
674     RT::Interface::Email::MailError(
675         To          => $ErrorsTo,
676         Subject     => "Command by mail error",
677         Explanation => $msg,
678         MIMEObj     => $args{'Message'},
679         Attach      => $args{'Message'}->as_string,
680     );
681     return;
682 }
683
684 1;