RT NonCustomerEmailRegexp option, #15847
[freeside.git] / rt / lib / RT / Interface / Web_Vendor.pm
1 # Copyright (c) 2004 Ivan Kohler <ivan-rt@420.am>
2 # Copyright (c) 2008 Freeside Internet Services, Inc.
3 #
4 # This work is made available to you under the terms of Version 2 of
5 # the GNU General Public License. A copy of that license should have
6 # been provided with this software, but in any event can be snarfed
7 # from www.gnu.org.
8
9 # This work is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # General Public License for more details.
13
14 =head1 NAME
15
16 RT::Interface::Web_Vendor
17
18 =head1 SYNOPSIS
19
20 =head1 DESCRIPTION
21
22 Freeside vendor overlay for RT::Interface::Web.
23
24 =begin testing
25
26 use_ok(RT::Interface::Web_Vendor);
27
28 =end testing
29
30 =cut
31
32 #package RT::Interface::Web;
33 #use strict;
34
35 package HTML::Mason::Commands;
36 use strict;
37 no warnings qw(redefine);
38
39 =head2 ProcessTicketCustomers 
40
41 =cut
42
43 sub ProcessTicketCustomers {
44     my %args = (
45         TicketObj => undef,
46         ARGSRef   => undef,
47         Debug     => 0,
48         @_
49     );
50     my @results = ();
51
52     my $Ticket  = $args{'TicketObj'};
53     my $ARGSRef = $args{'ARGSRef'};
54     my $Debug   = $args{'Debug'};
55     my $me = 'ProcessTicketCustomers';
56
57     ### false laziness w/RT::Interface::Web::ProcessTicketLinks
58     # Delete links that are gone gone gone.
59     foreach my $arg ( keys %$ARGSRef ) {
60         if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
61             my $base   = $1;
62             my $type   = $2;
63             my $target = $3;
64
65             push @results,
66               "Trying to delete: Base: $base Target: $target  Type $type";
67             my ( $val, $msg ) = $Ticket->DeleteLink( Base   => $base,
68                                                      Type   => $type,
69                                                      Target => $target );
70
71             push @results, $msg;
72
73         }
74
75     }
76     ###
77
78     ###
79     #find new customers
80     ###
81
82     my @custnums = map  { /^Ticket-AddCustomer-(\d+)$/; $1 }
83                    grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
84                    keys %$ARGSRef;
85
86     #my @delete_custnums =
87     #  map  { /^Ticket-AddCustomer-(\d+)$/; $1 }
88     #  grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
89     #  keys %$ARGSRef;
90
91     ###
92     #figure out if we're going to auto-link requestors, and find them if so
93     ###
94
95     my $num_cur_cust = $Ticket->Customers->Count;
96     my $num_new_cust = scalar(@custnums);
97     warn "$me: $num_cur_cust current customers / $num_new_cust new customers\n"
98       if $Debug;
99
100     #if we're linking the first ticket to one customer
101     my $link_requestors = ( $num_cur_cust == 0 && $num_new_cust == 1 );
102     warn "$me: adding a single customer to a previously customerless".
103          " ticket, so linking customers to requestor too\n"
104       if $Debug && $link_requestors;
105
106     my @Requestors = ();
107     if ( $link_requestors ) {
108
109       #find any requestors without customers
110       @Requestors =
111         grep { ! $_->Customers->Count }
112              @{ $Ticket->Requestors->UserMembersObj->ItemsArrayRef };
113
114       warn "$me: found ". scalar(@Requestors). " requestors without".
115            " customers; linking them\n"
116         if $Debug;
117
118     }
119
120     ###
121     #remove any declared non-customer addresses
122     ###
123
124     my $exclude_regexp = RT->Config->Get('NonCustomerEmailRegexp');
125     @Requestors = grep { not $_->EmailAddress =~ $exclude_regexp } @Requestors
126       if defined $exclude_regexp;
127
128     ###
129     #link ticket (and requestors) to customers
130     ###
131
132     foreach my $custnum ( @custnums ) {
133
134       my @link = ( 'Type'   => 'MemberOf',
135                    'Target' => "freeside://freeside/cust_main/$custnum",
136                  );
137
138       my( $val, $msg ) = $Ticket->AddLink(@link);
139       push @results, $msg;
140
141       #add customer links to requestors
142       foreach my $Requestor ( @Requestors ) {
143         my( $val, $msg ) = $Requestor->AddLink(@link);
144         push @results, $msg;
145         warn "$me: linking requestor to custnum $custnum: $msg\n"
146           if $Debug > 1;
147       }
148
149     }
150
151     return @results;
152
153 }
154
155 #false laziness w/above... eventually it should go away in favor of this
156 sub ProcessObjectCustomers {
157     my %args = (
158         Object => undef,
159         ARGSRef   => undef,
160         @_
161     );
162     my @results = ();
163
164     my $Object  = $args{'Object'};
165     my $ARGSRef = $args{'ARGSRef'};
166
167     ### false laziness w/RT::Interface::Web::ProcessTicketLinks
168     # Delete links that are gone gone gone.
169     foreach my $arg ( keys %$ARGSRef ) {
170         if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
171             my $base   = $1;
172             my $type   = $2;
173             my $target = $3;
174
175             push @results,
176               "Trying to delete: Base: $base Target: $target  Type $type";
177             my ( $val, $msg ) = $Object->DeleteLink( Base   => $base,
178                                                      Type   => $type,
179                                                      Target => $target );
180
181             push @results, $msg;
182
183         }
184
185     }
186     ###
187
188     #my @delete_custnums =
189     #  map  { /^Object-AddCustomer-(\d+)$/; $1 }
190     #  grep { /^Object-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
191     #  keys %$ARGSRef;
192
193     my @custnums = map  { /^Object-AddCustomer-(\d+)$/; $1 }
194                    grep { /^Object-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
195                    keys %$ARGSRef;
196
197     foreach my $custnum ( @custnums ) {
198       my( $val, $msg ) =
199         $Object->AddLink( 'Type'   => 'MemberOf',
200                           'Target' => "freeside://freeside/cust_main/$custnum",
201                         );
202       push @results, $msg;
203     }
204
205     return @results;
206
207 }
208
209 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
210
211 Updates all core ticket fields except Status, and returns an array of results
212 messages.
213
214 =cut
215
216 sub ProcessTicketBasics {
217
218     my %args = (
219         TicketObj => undef,
220         ARGSRef   => undef,
221         @_
222     );
223
224     my $TicketObj = $args{'TicketObj'};
225     my $ARGSRef   = $args{'ARGSRef'};
226
227     # {{{ Set basic fields
228     my @attribs = qw(
229         Subject
230         FinalPriority
231         Priority
232         TimeEstimated
233         TimeWorked
234         TimeLeft
235         Type
236         Queue
237     );
238
239     if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
240         my $tempqueue = RT::Queue->new($RT::SystemUser);
241         $tempqueue->Load( $ARGSRef->{'Queue'} );
242         if ( $tempqueue->id ) {
243             $ARGSRef->{'Queue'} = $tempqueue->id;
244         }
245     }
246
247     my @results = UpdateRecordObject(
248         AttributesRef => \@attribs,
249         Object        => $TicketObj,
250         ARGSRef       => $ARGSRef,
251     );
252
253     # We special case owner changing, so we can use ForceOwnerChange
254     if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
255         my ($ChownType);
256         if ( $ARGSRef->{'ForceOwnerChange'} ) {
257             $ChownType = "Force";
258         } else {
259             $ChownType = "Give";
260         }
261
262         my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
263         push( @results, $msg );
264     }
265
266     return (@results);
267 }
268
269 =head2 ProcessTicketDates (TicketObj => RT::Ticket, ARGSRef => {}) 
270
271 Process updates to the Starts, Started, Told, Resolved, and WillResolve 
272 fields.
273
274 =cut
275
276 sub ProcessTicketDates {
277     my %args = (
278         TicketObj => undef,
279         ARGSRef   => undef,
280         @_
281     );
282
283     my $Ticket  = $args{'TicketObj'};
284     my $ARGSRef = $args{'ARGSRef'};
285
286     my (@results);
287
288     # {{{ Set date fields
289     my @date_fields = qw(
290         Told
291         Resolved
292         Starts
293         Started
294         Due
295         WillResolve
296     );
297
298     #Run through each field in this list. update the value if apropriate
299     foreach my $field (@date_fields) {
300         next unless exists $ARGSRef->{ $field . '_Date' };
301         next if $ARGSRef->{ $field . '_Date' } eq '';
302
303         my ( $code, $msg );
304
305         my $DateObj = RT::Date->new( $session{'CurrentUser'} );
306         $DateObj->Set(
307             Format => 'unknown',
308             Value  => $ARGSRef->{ $field . '_Date' }
309         );
310
311         my $obj = $field . "Obj";
312         if (    ( defined $DateObj->Unix )
313             and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) )
314         {
315             my $method = "Set$field";
316             my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
317             push @results, "$msg";
318         }
319     }
320
321     # }}}
322     return (@results);
323 }
324
325 =head2 ProcessTicketStatus (TicketObj => RT::Ticket, ARGSRef => {})
326
327 Process updates to the 'Status' field of the ticket.  If the new value 
328 of Status is 'resolved', this will check required custom fields before 
329 allowing the update.
330
331 =cut
332
333 sub ProcessTicketStatus {
334     my %args = (
335         TicketObj => undef,
336         ARGSRef   => undef,
337         @_
338     );
339
340     my $TicketObj = $args{'TicketObj'};
341     my $ARGSRef   = $args{'ARGSRef'};
342     my @results;
343
344     return () if !$ARGSRef->{'Status'};
345
346     if ( lc( $ARGSRef->{'Status'} ) eq 'resolved' ) {
347         foreach my $field ( $TicketObj->MissingRequiredFields ) {
348             push @results, loc('Missing required field: [_1]', $field->Name);
349         }
350     }
351     if ( @results ) {
352         $m->notes('RedirectToBasics' => 1);
353         return @results;
354     }
355
356     return UpdateRecordObject(
357         AttributesRef => [ 'Status' ],
358         Object        => $TicketObj,
359         ARGSRef       => $ARGSRef,
360     );
361 }
362
363 =head2 ProcessUpdateMessage
364
365 Takes paramhash with fields ARGSRef, TicketObj and SkipSignatureOnly.
366
367 Don't write message if it only contains current user's signature and
368 SkipSignatureOnly argument is true. Function anyway adds attachments
369 and updates time worked field even if skips message. The default value
370 is true.
371
372 =cut
373
374 # change from stock: if txn custom fields are set but there's no content
375 # or attachment, create a Touch txn instead of doing nothing
376
377 sub ProcessUpdateMessage {
378
379     my %args = (
380         ARGSRef           => undef,
381         TicketObj         => undef,
382         SkipSignatureOnly => 1,
383         @_
384     );
385
386     if ( $args{ARGSRef}->{'UpdateAttachments'}
387         && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
388     {
389         delete $args{ARGSRef}->{'UpdateAttachments'};
390     }
391
392     # Strip the signature
393     $args{ARGSRef}->{UpdateContent} = RT::Interface::Web::StripContent(
394         Content        => $args{ARGSRef}->{UpdateContent},
395         ContentType    => $args{ARGSRef}->{UpdateContentType},
396         StripSignature => $args{SkipSignatureOnly},
397         CurrentUser    => $args{'TicketObj'}->CurrentUser,
398     );
399
400     my %txn_customfields;
401
402     foreach my $key ( keys %{ $args{ARGSRef} } ) {
403       if ( $key =~ /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ ) {
404         next if $key =~ /(TimeUnits|Magic)$/;
405         $txn_customfields{$key} = $args{ARGSRef}->{$key};
406       }
407     }
408
409     # If, after stripping the signature, we have no message, create a 
410     # Touch transaction if necessary
411     if (    not $args{ARGSRef}->{'UpdateAttachments'}
412         and not length $args{ARGSRef}->{'UpdateContent'} )
413     {
414         #if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) {
415         #      $args{ARGSRef}->{TimeWorked} = $args{TicketObj}->TimeWorked +
416         #          delete $args{ARGSRef}->{'UpdateTimeWorked'};
417         #  }
418
419         my $timetaken = $args{ARGSRef}->{'UpdateTimeWorked'};
420         if ( $timetaken or grep {length $_} values %txn_customfields ) {
421             my ( $Transaction, $Description, $Object ) =
422                 $args{TicketObj}->Touch( 
423                   CustomFields => \%txn_customfields,
424                   TimeTaken => $timetaken
425                 );
426             return $Description;
427         }
428
429         return;
430     }
431
432     if ( $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject ) {
433         $args{ARGSRef}->{'UpdateSubject'} = undef;
434     }
435
436     my $Message = MakeMIMEEntity(
437         Subject => $args{ARGSRef}->{'UpdateSubject'},
438         Body    => $args{ARGSRef}->{'UpdateContent'},
439         Type    => $args{ARGSRef}->{'UpdateContentType'},
440     );
441
442     $Message->head->add( 'Message-ID' => Encode::encode_utf8(
443         RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} )
444     ) );
445     my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
446     if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
447         $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
448     } else {
449         $old_txn = $args{TicketObj}->Transactions->First();
450     }
451
452     if ( my $msg = $old_txn->Message->First ) {
453         RT::Interface::Email::SetInReplyTo(
454             Message   => $Message,
455             InReplyTo => $msg
456         );
457     }
458
459     if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
460         $Message->make_multipart;
461         $Message->add_part($_) foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
462     }
463
464     if ( $args{ARGSRef}->{'AttachTickets'} ) {
465         require RT::Action::SendEmail;
466         RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
467             ref $args{ARGSRef}->{'AttachTickets'}
468             ? @{ $args{ARGSRef}->{'AttachTickets'} }
469             : ( $args{ARGSRef}->{'AttachTickets'} ) );
470     }
471
472     my $bcc = $args{ARGSRef}->{'UpdateBcc'};
473     my $cc  = $args{ARGSRef}->{'UpdateCc'};
474
475     my %message_args = (
476         CcMessageTo  => $cc,
477         BccMessageTo => $bcc,
478         Sign         => $args{ARGSRef}->{'Sign'},
479         Encrypt      => $args{ARGSRef}->{'Encrypt'},
480         MIMEObj      => $Message,
481         TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'},
482         CustomFields => \%txn_customfields,
483     );
484
485     my @temp_squelch;
486     foreach my $type (qw(Cc AdminCc)) {
487         if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
488             push @temp_squelch, map $_->address, Email::Address->parse( $message_args{$type} );
489             push @temp_squelch, $args{TicketObj}->$type->MemberEmailAddresses;
490             push @temp_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses;
491         }
492     }
493     if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
494             push @temp_squelch, map $_->address, Email::Address->parse( $message_args{Requestor} );
495             push @temp_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses;
496     }
497
498     if (@temp_squelch) {
499         require RT::Action::SendEmail;
500         RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch );
501     }
502
503     unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) {
504         foreach my $key ( keys %{ $args{ARGSRef} } ) {
505             next unless $key =~ /^Update(Cc|Bcc)-(.*)$/;
506
507             my $var   = ucfirst($1) . 'MessageTo';
508             my $value = $2;
509             if ( $message_args{$var} ) {
510                 $message_args{$var} .= ", $value";
511             } else {
512                 $message_args{$var} = $value;
513             }
514         }
515     }
516
517     my @results;
518     # Do the update via the appropriate Ticket method
519     if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
520         my ( $Transaction, $Description, $Object ) =
521             $args{TicketObj}->Comment(%message_args);
522         push( @results, $Description );
523         #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
524     } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
525         my ( $Transaction, $Description, $Object ) =
526             $args{TicketObj}->Correspond(%message_args);
527         push( @results, $Description );
528         #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
529     } else {
530         push( @results,
531             loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") );
532     }
533     return @results;
534 }
535
536 1;
537