155ecf258fd8aef5dd2e86e9a14be11bd4df0419
[freeside.git] / rt / lib / RT / Ticket_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46
47 # END BPS TAGGED BLOCK }}}
48
49 # {{{ Front Material 
50
51 =head1 SYNOPSIS
52
53   use RT::Ticket;
54   my $ticket = new RT::Ticket($CurrentUser);
55   $ticket->Load($ticket_id);
56
57 =head1 DESCRIPTION
58
59 This module lets you manipulate RT\'s ticket object.
60
61
62 =head1 METHODS
63
64
65 =cut
66
67
68 package RT::Ticket;
69
70 use strict;
71 no warnings qw(redefine);
72
73 use RT::Queue;
74 use RT::User;
75 use RT::Record;
76 use RT::Links;
77 use RT::Date;
78 use RT::CustomFields;
79 use RT::Tickets;
80 use RT::Transactions;
81 use RT::Reminders;
82 use RT::URI::fsck_com_rt;
83 use RT::URI;
84 use MIME::Entity;
85
86
87 # {{{ LINKTYPEMAP
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
90
91 our %LINKTYPEMAP = (
92     MemberOf => { Type => 'MemberOf',
93                   Mode => 'Target', },
94     Parents => { Type => 'MemberOf',
95          Mode => 'Target', },
96     Members => { Type => 'MemberOf',
97                  Mode => 'Base', },
98     Children => { Type => 'MemberOf',
99           Mode => 'Base', },
100     HasMember => { Type => 'MemberOf',
101                    Mode => 'Base', },
102     RefersTo => { Type => 'RefersTo',
103                   Mode => 'Target', },
104     ReferredToBy => { Type => 'RefersTo',
105                       Mode => 'Base', },
106     DependsOn => { Type => 'DependsOn',
107                    Mode => 'Target', },
108     DependedOnBy => { Type => 'DependsOn',
109                       Mode => 'Base', },
110     MergedInto => { Type => 'MergedInto',
111                    Mode => 'Target', },
112
113 );
114
115 # }}}
116
117 # {{{ LINKDIRMAP
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
120
121 our %LINKDIRMAP = (
122     MemberOf => { Base => 'MemberOf',
123                   Target => 'HasMember', },
124     RefersTo => { Base => 'RefersTo',
125                 Target => 'ReferredToBy', },
126     DependsOn => { Base => 'DependsOn',
127                    Target => 'DependedOnBy', },
128     MergedInto => { Base => 'MergedInto',
129                    Target => 'MergedInto', },
130
131 );
132
133 # }}}
134
135 sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
136 sub LINKDIRMAP   { return \%LINKDIRMAP   }
137
138 # {{{ sub Load
139
140 =head2 Load
141
142 Takes a single argument. This can be a ticket id, ticket alias or 
143 local ticket uri.  If the ticket can't be loaded, returns undef.
144 Otherwise, returns the ticket id.
145
146 =cut
147
148 sub Load {
149     my $self = shift;
150     my $id   = shift;
151
152     #TODO modify this routine to look at EffectiveId and do the recursive load
153     # thing. be careful to cache all the interim tickets we try so we don't loop forever.
154
155     # FIXME: there is no TicketBaseURI option in config
156     my $base_uri = RT->Config->Get('TicketBaseURI') || '';
157     #If it's a local URI, turn it into a ticket id
158     if ( $base_uri && defined $id && $id =~ /^$base_uri(\d+)$/ ) {
159         $id = $1;
160     }
161
162     #If it's a remote URI, we're going to punt for now
163     elsif ( $id =~ '://' ) {
164         return (undef);
165     }
166
167     #If we have an integer URI, load the ticket
168     if ( defined $id && $id =~ /^\d+$/ ) {
169         my ($ticketid,$msg) = $self->LoadById($id);
170
171         unless ($self->Id) {
172             $RT::Logger->debug("$self tried to load a bogus ticket: $id");
173             return (undef);
174         }
175     }
176
177     #It's not a URI. It's not a numerical ticket ID. Punt!
178     else {
179         $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
180         return (undef);
181     }
182
183     #If we're merged, resolve the merge.
184     if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
185         $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
186         return ( $self->Load( $self->EffectiveId ) );
187     }
188
189     #Ok. we're loaded. lets get outa here.
190     return ( $self->Id );
191
192 }
193
194 # }}}
195
196 # {{{ sub Create
197
198 =head2 Create (ARGS)
199
200 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
201
202   id 
203   Queue  - Either a Queue object or a Queue Name
204   Requestor -  A reference to a list of  email addresses or RT user Names
205   Cc  - A reference to a list of  email addresses or Names
206   AdminCc  - A reference to a  list of  email addresses or Names
207   SquelchMailTo - A reference to a list of email addresses - 
208                   who should this ticket not mail
209   Type -- The ticket\'s type. ignore this for now
210   Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
211   Subject -- A string describing the subject of the ticket
212   Priority -- an integer from 0 to 99
213   InitialPriority -- an integer from 0 to 99
214   FinalPriority -- an integer from 0 to 99
215   Status -- any valid status (Defined in RT::Queue)
216   TimeEstimated -- an integer. estimated time for this task in minutes
217   TimeWorked -- an integer. time worked so far in minutes
218   TimeLeft -- an integer. time remaining in minutes
219   Starts -- an ISO date describing the ticket\'s start date and time in GMT
220   Due -- an ISO date describing the ticket\'s due date and time in GMT
221   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
222   CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
223
224 Ticket links can be set up during create by passing the link type as a hask key and
225 the ticket id to be linked to as a value (or a URI when linking to other objects).
226 Multiple links of the same type can be created by passing an array ref. For example:
227
228   Parent => 45,
229   DependsOn => [ 15, 22 ],
230   RefersTo => 'http://www.bestpractical.com',
231
232 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
233 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
234 C<Members> and C<Children> are aliases for C<HasMember>.
235
236 Returns: TICKETID, Transaction Object, Error Message
237
238
239 =cut
240
241 sub Create {
242     my $self = shift;
243
244     my %args = (
245         id                 => undef,
246         EffectiveId        => undef,
247         Queue              => undef,
248         Requestor          => undef,
249         Cc                 => undef,
250         AdminCc            => undef,
251         SquelchMailTo      => undef,
252         Type               => 'ticket',
253         Owner              => undef,
254         Subject            => '',
255         InitialPriority    => undef,
256         FinalPriority      => undef,
257         Priority           => undef,
258         Status             => 'new',
259         TimeWorked         => "0",
260         TimeLeft           => 0,
261         TimeEstimated      => 0,
262         Due                => undef,
263         Starts             => undef,
264         Started            => undef,
265         Resolved           => undef,
266         MIMEObj            => undef,
267         _RecordTransaction => 1,
268         DryRun             => 0,
269         @_
270     );
271
272     my ($ErrStr, @non_fatal_errors);
273
274     my $QueueObj = RT::Queue->new( $RT::SystemUser );
275     if ( ref $args{'Queue'} eq 'RT::Queue' ) {
276         $QueueObj->Load( $args{'Queue'}->Id );
277     }
278     elsif ( $args{'Queue'} ) {
279         $QueueObj->Load( $args{'Queue'} );
280     }
281     else {
282         $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
283     }
284
285     #Can't create a ticket without a queue.
286     unless ( $QueueObj->Id ) {
287         $RT::Logger->debug("$self No queue given for ticket creation.");
288         return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
289     }
290
291
292     #Now that we have a queue, Check the ACLS
293     unless (
294         $self->CurrentUser->HasRight(
295             Right  => 'CreateTicket',
296             Object => $QueueObj
297         )
298       )
299     {
300         return (
301             0, 0,
302             $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
303     }
304
305     unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
306         return ( 0, 0, $self->loc('Invalid value for status') );
307     }
308
309     #Since we have a queue, we can set queue defaults
310
311     #Initial Priority
312     # If there's no queue default initial priority and it's not set, set it to 0
313     $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
314         unless defined $args{'InitialPriority'};
315
316     #Final priority
317     # If there's no queue default final priority and it's not set, set it to 0
318     $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
319         unless defined $args{'FinalPriority'};
320
321     # Priority may have changed from InitialPriority, for the case
322     # where we're importing tickets (eg, from an older RT version.)
323     $args{'Priority'} = $args{'InitialPriority'}
324         unless defined $args{'Priority'};
325
326     # {{{ Dates
327     #TODO we should see what sort of due date we're getting, rather +
328     # than assuming it's in ISO format.
329
330     #Set the due date. if we didn't get fed one, use the queue default due in
331     my $Due = new RT::Date( $self->CurrentUser );
332     if ( defined $args{'Due'} ) {
333         $Due->Set( Format => 'ISO', Value => $args{'Due'} );
334     }
335     elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
336         $Due->SetToNow;
337         $Due->AddDays( $due_in );
338     }
339
340     my $Starts = new RT::Date( $self->CurrentUser );
341     if ( defined $args{'Starts'} ) {
342         $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
343     }
344
345     my $Started = new RT::Date( $self->CurrentUser );
346     if ( defined $args{'Started'} ) {
347         $Started->Set( Format => 'ISO', Value => $args{'Started'} );
348     }
349     elsif ( $args{'Status'} ne 'new' ) {
350         $Started->SetToNow;
351     }
352
353     my $Resolved = new RT::Date( $self->CurrentUser );
354     if ( defined $args{'Resolved'} ) {
355         $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
356     }
357
358     #If the status is an inactive status, set the resolved date
359     elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
360     {
361         $RT::Logger->debug( "Got a ". $args{'Status'}
362             ."(inactive) ticket with undefined resolved date. Setting to now."
363         );
364         $Resolved->SetToNow;
365     }
366
367     # }}}
368
369     # {{{ Dealing with time fields
370
371     $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
372     $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
373     $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
374
375     # }}}
376
377     # {{{ Deal with setting the owner
378
379     my $Owner;
380     if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
381         if ( $args{'Owner'}->id ) {
382             $Owner = $args{'Owner'};
383         } else {
384             $RT::Logger->error('passed not loaded owner object');
385             push @non_fatal_errors, $self->loc("Invalid owner object");
386             $Owner = undef;
387         }
388     }
389
390     #If we've been handed something else, try to load the user.
391     elsif ( $args{'Owner'} ) {
392         $Owner = RT::User->new( $self->CurrentUser );
393         $Owner->Load( $args{'Owner'} );
394         $Owner->LoadByEmail( $args{'Owner'} )
395             unless $Owner->Id;
396         unless ( $Owner->Id ) {
397             push @non_fatal_errors,
398                 $self->loc("Owner could not be set.") . " "
399               . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
400             $Owner = undef;
401         }
402     }
403
404     #If we have a proposed owner and they don't have the right
405     #to own a ticket, scream about it and make them not the owner
406    
407     my $DeferOwner;  
408     if ( $Owner && $Owner->Id != $RT::Nobody->Id 
409         && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
410     {
411         $DeferOwner = $Owner;
412         $Owner = undef;
413         $RT::Logger->debug('going to deffer setting owner');
414
415     }
416
417     #If we haven't been handed a valid owner, make it nobody.
418     unless ( defined($Owner) && $Owner->Id ) {
419         $Owner = new RT::User( $self->CurrentUser );
420         $Owner->Load( $RT::Nobody->Id );
421     }
422
423     # }}}
424
425 # We attempt to load or create each of the people who might have a role for this ticket
426 # _outside_ the transaction, so we don't get into ticket creation races
427     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
428         $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
429         foreach my $watcher ( splice @{ $args{$type} } ) {
430             next unless $watcher;
431             if ( $watcher =~ /^\d+$/ ) {
432                 push @{ $args{$type} }, $watcher;
433             } else {
434                 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
435                 foreach my $address( @addresses ) {
436                     my $user = RT::User->new( $RT::SystemUser );
437                     my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
438                     unless ( $uid ) {
439                         push @non_fatal_errors,
440                             $self->loc("Couldn't load or create user: [_1]", $msg);
441                     } else {
442                         push @{ $args{$type} }, $user->id;
443                     }
444                 }
445             }
446         }
447     }
448
449     $RT::Handle->BeginTransaction();
450
451     my %params = (
452         Queue           => $QueueObj->Id,
453         Owner           => $Owner->Id,
454         Subject         => $args{'Subject'},
455         InitialPriority => $args{'InitialPriority'},
456         FinalPriority   => $args{'FinalPriority'},
457         Priority        => $args{'Priority'},
458         Status          => $args{'Status'},
459         TimeWorked      => $args{'TimeWorked'},
460         TimeEstimated   => $args{'TimeEstimated'},
461         TimeLeft        => $args{'TimeLeft'},
462         Type            => $args{'Type'},
463         Starts          => $Starts->ISO,
464         Started         => $Started->ISO,
465         Resolved        => $Resolved->ISO,
466         Due             => $Due->ISO
467     );
468
469 # Parameters passed in during an import that we probably don't want to touch, otherwise
470     foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
471         $params{$attr} = $args{$attr} if $args{$attr};
472     }
473
474     # Delete null integer parameters
475     foreach my $attr
476         qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
477     {
478         delete $params{$attr}
479           unless ( exists $params{$attr} && $params{$attr} );
480     }
481
482     # Delete the time worked if we're counting it in the transaction
483     delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
484
485     my ($id,$ticket_message) = $self->SUPER::Create( %params );
486     unless ($id) {
487         $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
488         $RT::Handle->Rollback();
489         return ( 0, 0,
490             $self->loc("Ticket could not be created due to an internal error")
491         );
492     }
493
494     #Set the ticket's effective ID now that we've created it.
495     my ( $val, $msg ) = $self->__Set(
496         Field => 'EffectiveId',
497         Value => ( $args{'EffectiveId'} || $id )
498     );
499     unless ( $val ) {
500         $RT::Logger->crit("Couldn't set EffectiveId: $msg");
501         $RT::Handle->Rollback;
502         return ( 0, 0,
503             $self->loc("Ticket could not be created due to an internal error")
504         );
505     }
506
507     my $create_groups_ret = $self->_CreateTicketGroups();
508     unless ($create_groups_ret) {
509         $RT::Logger->crit( "Couldn't create ticket groups for ticket "
510               . $self->Id
511               . ". aborting Ticket creation." );
512         $RT::Handle->Rollback();
513         return ( 0, 0,
514             $self->loc("Ticket could not be created due to an internal error")
515         );
516     }
517
518     # Set the owner in the Groups table
519     # We denormalize it into the Ticket table too because doing otherwise would
520     # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
521     $self->OwnerGroup->_AddMember(
522         PrincipalId       => $Owner->PrincipalId,
523         InsideTransaction => 1
524     ) unless $DeferOwner;
525
526
527
528     # {{{ Deal with setting up watchers
529
530     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
531         # we know it's an array ref
532         foreach my $watcher ( @{ $args{$type} } ) {
533
534             # Note that we're using AddWatcher, rather than _AddWatcher, as we
535             # actually _want_ that ACL check. Otherwise, random ticket creators
536             # could make themselves adminccs and maybe get ticket rights. that would
537             # be poor
538             my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
539
540             my ($val, $msg) = $self->$method(
541                 Type   => $type,
542                 PrincipalId => $watcher,
543                 Silent => 1,
544             );
545             push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
546                 unless $val;
547         }
548     } 
549
550     if ($args{'SquelchMailTo'}) {
551        my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
552         : $args{'SquelchMailTo'};
553         $self->_SquelchMailTo( @squelch );
554     }
555
556
557     # }}}
558
559     # {{{ Add all the custom fields
560
561     foreach my $arg ( keys %args ) {
562         next unless $arg =~ /^CustomField-(\d+)$/i;
563         my $cfid = $1;
564
565         foreach my $value (
566             UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
567         {
568             next unless defined $value && length $value;
569
570             # Allow passing in uploaded LargeContent etc by hash reference
571             my ($status, $msg) = $self->_AddCustomFieldValue(
572                 (UNIVERSAL::isa( $value => 'HASH' )
573                     ? %$value
574                     : (Value => $value)
575                 ),
576                 Field             => $cfid,
577                 RecordTransaction => 0,
578             );
579             push @non_fatal_errors, $msg unless $status;
580         }
581     }
582
583     # }}}
584
585     # {{{ Deal with setting up links
586
587     # TODO: Adding link may fire scrips on other end and those scrips
588     # could create transactions on this ticket before 'Create' transaction.
589     #
590     # We should implement different schema: record 'Create' transaction,
591     # create links and only then fire create transaction's scrips.
592     #
593     # Ideal variant: add all links without firing scrips, record create
594     # transaction and only then fire scrips on the other ends of links.
595     #
596     # //RUZ
597
598     foreach my $type ( keys %LINKTYPEMAP ) {
599         next unless ( defined $args{$type} );
600         foreach my $link (
601             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
602         {
603             # Check rights on the other end of the link if we must
604             # then run _AddLink that doesn't check for ACLs
605             if ( RT->Config->Get( 'StrictLinkACL' ) ) {
606                 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
607                 unless ( $val ) {
608                     push @non_fatal_errors, $msg;
609                     next;
610                 }
611                 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
612                     push @non_fatal_errors, $self->loc('Linking. Permission denied');
613                     next;
614                 }
615             }
616             
617             my ( $wval, $wmsg ) = $self->_AddLink(
618                 Type                          => $LINKTYPEMAP{$type}->{'Type'},
619                 $LINKTYPEMAP{$type}->{'Mode'} => $link,
620                 Silent                        => !$args{'_RecordTransaction'},
621                 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
622                                               => 1,
623             );
624
625             push @non_fatal_errors, $wmsg unless ($wval);
626         }
627     }
628
629     # }}}
630     # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself. 
631     # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
632     if (  $DeferOwner ) { 
633             if (!$DeferOwner->HasRight( Object => $self, Right  => 'OwnTicket')) {
634     
635             $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id 
636                 . ") was proposed as a ticket owner but has no rights to own "
637                 . "tickets in " . $QueueObj->Name );
638             push @non_fatal_errors, $self->loc(
639                 "Owner '[_1]' does not have rights to own this ticket.",
640                 $DeferOwner->Name
641             );
642         } else {
643             $Owner = $DeferOwner;
644             $self->__Set(Field => 'Owner', Value => $Owner->id);
645
646     # {{{ Deal with auto-customer association
647
648     #unless we already have (a) customer(s)...
649     unless ( $self->Customers->Count ) {
650
651       #first find any requestors with emails but *without* customer targets
652       my @NoCust_Requestors =
653         grep { $_->EmailAddress && ! $_->Customers->Count }
654              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
655
656       for my $Requestor (@NoCust_Requestors) {
657
658          #perhaps the stuff in here should be in a User method??
659          my @Customers =
660            &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
661
662          foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
663
664            ## false laziness w/RT/Interface/Web_Vendor.pm
665            my @link = ( 'Type'   => 'MemberOf',
666                         'Target' => "freeside://freeside/cust_main/$custnum",
667                       );
668
669            my( $val, $msg ) = $Requestor->_AddLink(@link);
670            #XXX should do something with $msg# push @non_fatal_errors, $msg;
671
672          }
673
674       }
675
676       #find any requestors with customer targets
677   
678       my %cust_target = ();
679   
680       my @Requestors =
681         grep { $_->Customers->Count }
682              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
683   
684       foreach my $Requestor ( @Requestors ) {
685         foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
686           $cust_target{ $cust_link->Target } = 1;
687         }
688       }
689   
690       #and then auto-associate this ticket with those customers
691   
692       foreach my $cust_target ( keys %cust_target ) {
693   
694         my @link = ( 'Type'   => 'MemberOf',
695                      #'Target' => "freeside://freeside/cust_main/$custnum",
696                      'Target' => $cust_target,
697                    );
698   
699         my( $val, $msg ) = $self->_AddLink(@link);
700         push @non_fatal_errors, $msg;
701   
702       }
703
704     }
705
706     # }}}
707
708         }
709         $self->OwnerGroup->_AddMember(
710             PrincipalId       => $Owner->PrincipalId,
711             InsideTransaction => 1
712         );
713     }
714
715     if ( $args{'_RecordTransaction'} ) {
716
717         # {{{ Add a transaction for the create
718         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
719             Type         => "Create",
720             TimeTaken    => $args{'TimeWorked'},
721             MIMEObj      => $args{'MIMEObj'},
722             CommitScrips => !$args{'DryRun'},
723         );
724
725         if ( $self->Id && $Trans ) {
726
727             $TransObj->UpdateCustomFields(ARGSRef => \%args);
728
729             $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
730             $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
731             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
732         }
733         else {
734             $RT::Handle->Rollback();
735
736             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
737             $RT::Logger->error("Ticket couldn't be created: $ErrStr");
738             return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
739         }
740
741         if ( $args{'DryRun'} ) {
742             $RT::Handle->Rollback();
743             return ($self->id, $TransObj, $ErrStr);
744         }
745         $RT::Handle->Commit();
746         return ( $self->Id, $TransObj->Id, $ErrStr );
747
748         # }}}
749     }
750     else {
751
752         # Not going to record a transaction
753         $RT::Handle->Commit();
754         $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
755         $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
756         return ( $self->Id, 0, $ErrStr );
757
758     }
759 }
760
761
762 # }}}
763
764 # {{{ _Parse822HeadersForAttributes Content
765
766 =head2 _Parse822HeadersForAttributes Content
767
768 Takes an RFC822 style message and parses its attributes into a hash.
769
770 =cut
771
772 sub _Parse822HeadersForAttributes {
773     my $self    = shift;
774     my $content = shift;
775     my %args;
776
777     my @lines = ( split ( /\n/, $content ) );
778     while ( defined( my $line = shift @lines ) ) {
779         if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
780             my $value = $2;
781             my $tag   = lc($1);
782
783             $tag =~ s/-//g;
784             if ( defined( $args{$tag} ) )
785             {    #if we're about to get a second value, make it an array
786                 $args{$tag} = [ $args{$tag} ];
787             }
788             if ( ref( $args{$tag} ) )
789             {    #If it's an array, we want to push the value
790                 push @{ $args{$tag} }, $value;
791             }
792             else {    #if there's nothing there, just set the value
793                 $args{$tag} = $value;
794             }
795         } elsif ($line =~ /^$/) {
796
797             #TODO: this won't work, since "" isn't of the form "foo:value"
798
799                 while ( defined( my $l = shift @lines ) ) {
800                     push @{ $args{'content'} }, $l;
801                 }
802             }
803         
804     }
805
806     foreach my $date qw(due starts started resolved) {
807         my $dateobj = RT::Date->new($RT::SystemUser);
808         if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
809             $dateobj->Set( Format => 'unix', Value => $args{$date} );
810         }
811         else {
812             $dateobj->Set( Format => 'unknown', Value => $args{$date} );
813         }
814         $args{$date} = $dateobj->ISO;
815     }
816     $args{'mimeobj'} = MIME::Entity->new();
817     $args{'mimeobj'}->build(
818         Type => ( $args{'contenttype'} || 'text/plain' ),
819         Data => ($args{'content'} || '')
820     );
821
822     return (%args);
823 }
824
825 # }}}
826
827 # {{{ sub Import
828
829 =head2 Import PARAMHASH
830
831 Import a ticket. 
832 Doesn\'t create a transaction. 
833 Doesn\'t supply queue defaults, etc.
834
835 Returns: TICKETID
836
837 =cut
838
839 sub Import {
840     my $self = shift;
841     my ( $ErrStr, $QueueObj, $Owner );
842
843     my %args = (
844         id              => undef,
845         EffectiveId     => undef,
846         Queue           => undef,
847         Requestor       => undef,
848         Type            => 'ticket',
849         Owner           => $RT::Nobody->Id,
850         Subject         => '[no subject]',
851         InitialPriority => undef,
852         FinalPriority   => undef,
853         Status          => 'new',
854         TimeWorked      => "0",
855         Due             => undef,
856         Created         => undef,
857         Updated         => undef,
858         Resolved        => undef,
859         Told            => undef,
860         @_
861     );
862
863     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
864         $QueueObj = RT::Queue->new($RT::SystemUser);
865         $QueueObj->Load( $args{'Queue'} );
866
867         #TODO error check this and return 0 if it\'s not loading properly +++
868     }
869     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
870         $QueueObj = RT::Queue->new($RT::SystemUser);
871         $QueueObj->Load( $args{'Queue'}->Id );
872     }
873     else {
874         $RT::Logger->debug(
875             "$self " . $args{'Queue'} . " not a recognised queue object." );
876     }
877
878     #Can't create a ticket without a queue.
879     unless ( defined($QueueObj) and $QueueObj->Id ) {
880         $RT::Logger->debug("$self No queue given for ticket creation.");
881         return ( 0, $self->loc('Could not create ticket. Queue not set') );
882     }
883
884     #Now that we have a queue, Check the ACLS
885     unless (
886         $self->CurrentUser->HasRight(
887             Right    => 'CreateTicket',
888             Object => $QueueObj
889         )
890       )
891     {
892         return ( 0,
893             $self->loc("No permission to create tickets in the queue '[_1]'"
894               , $QueueObj->Name));
895     }
896
897     # {{{ Deal with setting the owner
898
899     # Attempt to take user object, user name or user id.
900     # Assign to nobody if lookup fails.
901     if ( defined( $args{'Owner'} ) ) {
902         if ( ref( $args{'Owner'} ) ) {
903             $Owner = $args{'Owner'};
904         }
905         else {
906             $Owner = new RT::User( $self->CurrentUser );
907             $Owner->Load( $args{'Owner'} );
908             if ( !defined( $Owner->id ) ) {
909                 $Owner->Load( $RT::Nobody->id );
910             }
911         }
912     }
913
914     #If we have a proposed owner and they don't have the right 
915     #to own a ticket, scream about it and make them not the owner
916     if (
917         ( defined($Owner) )
918         and ( $Owner->Id != $RT::Nobody->Id )
919         and (
920             !$Owner->HasRight(
921                 Object => $QueueObj,
922                 Right    => 'OwnTicket'
923             )
924         )
925       )
926     {
927
928         $RT::Logger->warning( "$self user "
929               . $Owner->Name . "("
930               . $Owner->id
931               . ") was proposed "
932               . "as a ticket owner but has no rights to own "
933               . "tickets in '"
934               . $QueueObj->Name . "'" );
935
936         $Owner = undef;
937     }
938
939     #If we haven't been handed a valid owner, make it nobody.
940     unless ( defined($Owner) ) {
941         $Owner = new RT::User( $self->CurrentUser );
942         $Owner->Load( $RT::Nobody->UserObj->Id );
943     }
944
945     # }}}
946
947     unless ( $self->ValidateStatus( $args{'Status'} ) ) {
948         return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
949     }
950
951     $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
952     $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
953     $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
954     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
955
956     # If we're coming in with an id, set that now.
957     my $EffectiveId = undef;
958     if ( $args{'id'} ) {
959         $EffectiveId = $args{'id'};
960
961     }
962
963     my $id = $self->SUPER::Create(
964         id              => $args{'id'},
965         EffectiveId     => $EffectiveId,
966         Queue           => $QueueObj->Id,
967         Owner           => $Owner->Id,
968         Subject         => $args{'Subject'},        # loc
969         InitialPriority => $args{'InitialPriority'},    # loc
970         FinalPriority   => $args{'FinalPriority'},    # loc
971         Priority        => $args{'InitialPriority'},    # loc
972         Status          => $args{'Status'},        # loc
973         TimeWorked      => $args{'TimeWorked'},        # loc
974         Type            => $args{'Type'},        # loc
975         Created         => $args{'Created'},        # loc
976         Told            => $args{'Told'},        # loc
977         LastUpdated     => $args{'Updated'},        # loc
978         Resolved        => $args{'Resolved'},        # loc
979         Due             => $args{'Due'},        # loc
980     );
981
982     # If the ticket didn't have an id
983     # Set the ticket's effective ID now that we've created it.
984     if ( $args{'id'} ) {
985         $self->Load( $args{'id'} );
986     }
987     else {
988         my ( $val, $msg ) =
989           $self->__Set( Field => 'EffectiveId', Value => $id );
990
991         unless ($val) {
992             $RT::Logger->err(
993                 $self . "->Import couldn't set EffectiveId: $msg" );
994         }
995     }
996
997     my $create_groups_ret = $self->_CreateTicketGroups();
998     unless ($create_groups_ret) {
999         $RT::Logger->crit(
1000             "Couldn't create ticket groups for ticket " . $self->Id );
1001     }
1002
1003     $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1004
1005     my $watcher;
1006     foreach $watcher ( @{ $args{'Cc'} } ) {
1007         $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1008     }
1009     foreach $watcher ( @{ $args{'AdminCc'} } ) {
1010         $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1011             Silent => 1 );
1012     }
1013     foreach $watcher ( @{ $args{'Requestor'} } ) {
1014         $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1015             Silent => 1 );
1016     }
1017
1018     return ( $self->Id, $ErrStr );
1019 }
1020
1021 # }}}
1022
1023 # {{{ Routines dealing with watchers.
1024
1025 # {{{ _CreateTicketGroups 
1026
1027 =head2 _CreateTicketGroups
1028
1029 Create the ticket groups and links for this ticket. 
1030 This routine expects to be called from Ticket->Create _inside of a transaction_
1031
1032 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1033
1034 It will return true on success and undef on failure.
1035
1036
1037 =cut
1038
1039
1040 sub _CreateTicketGroups {
1041     my $self = shift;
1042     
1043     my @types = qw(Requestor Owner Cc AdminCc);
1044
1045     foreach my $type (@types) {
1046         my $type_obj = RT::Group->new($self->CurrentUser);
1047         my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1048                                                        Instance => $self->Id, 
1049                                                        Type => $type);
1050         unless ($id) {
1051             $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1052                                $self->Id.": ".$msg);     
1053             return(undef);
1054         }
1055      }
1056     return(1);
1057     
1058 }
1059
1060 # }}}
1061
1062 # {{{ sub OwnerGroup
1063
1064 =head2 OwnerGroup
1065
1066 A constructor which returns an RT::Group object containing the owner of this ticket.
1067
1068 =cut
1069
1070 sub OwnerGroup {
1071     my $self = shift;
1072     my $owner_obj = RT::Group->new($self->CurrentUser);
1073     $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
1074     return ($owner_obj);
1075 }
1076
1077 # }}}
1078
1079
1080 # {{{ sub AddWatcher
1081
1082 =head2 AddWatcher
1083
1084 AddWatcher takes a parameter hash. The keys are as follows:
1085
1086 Type        One of Requestor, Cc, AdminCc
1087
1088 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1089
1090 Email       The email address of the new watcher. If a user with this 
1091             email address can't be found, a new nonprivileged user will be created.
1092
1093 If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1094
1095 =cut
1096
1097 sub AddWatcher {
1098     my $self = shift;
1099     my %args = (
1100         Type  => undef,
1101         PrincipalId => undef,
1102         Email => undef,
1103         @_
1104     );
1105
1106     # ModifyTicket works in any case
1107     return $self->_AddWatcher( %args )
1108         if $self->CurrentUserHasRight('ModifyTicket');
1109     if ( $args{'Email'} ) {
1110         my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1111         return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1112             unless $addr;
1113
1114         if ( lc $self->CurrentUser->UserObj->EmailAddress
1115             eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1116         {
1117             $args{'PrincipalId'} = $self->CurrentUser->id;
1118             delete $args{'Email'};
1119         }
1120     }
1121
1122     # If the watcher isn't the current user then the current user has no right
1123     # bail
1124     unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1125         return ( 0, $self->loc("Permission Denied") );
1126     }
1127
1128     #  If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1129     if ( $args{'Type'} eq 'AdminCc' ) {
1130         unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1131             return ( 0, $self->loc('Permission Denied') );
1132         }
1133     }
1134
1135     #  If it's a Requestor or Cc and they don't have 'Watch', bail
1136     elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1137         unless ( $self->CurrentUserHasRight('Watch') ) {
1138             return ( 0, $self->loc('Permission Denied') );
1139         }
1140     }
1141     else {
1142         $RT::Logger->warning( "AddWatcher got passed a bogus type");
1143         return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1144     }
1145
1146     return $self->_AddWatcher( %args );
1147 }
1148
1149 #This contains the meat of AddWatcher. but can be called from a routine like
1150 # Create, which doesn't need the additional acl check
1151 sub _AddWatcher {
1152     my $self = shift;
1153     my %args = (
1154         Type   => undef,
1155         Silent => undef,
1156         PrincipalId => undef,
1157         Email => undef,
1158         @_
1159     );
1160
1161
1162     my $principal = RT::Principal->new($self->CurrentUser);
1163     if ($args{'Email'}) {
1164         my $user = RT::User->new($RT::SystemUser);
1165         my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1166         $args{'PrincipalId'} = $pid if $pid; 
1167     }
1168     if ($args{'PrincipalId'}) {
1169         $principal->Load($args{'PrincipalId'});
1170     } 
1171
1172  
1173     # If we can't find this watcher, we need to bail.
1174     unless ($principal->Id) {
1175             $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1176         return(0, $self->loc("Could not find or create that user"));
1177     }
1178
1179
1180     my $group = RT::Group->new($self->CurrentUser);
1181     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1182     unless ($group->id) {
1183         return(0,$self->loc("Group not found"));
1184     }
1185
1186     if ( $group->HasMember( $principal)) {
1187
1188         return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1189     }
1190
1191
1192     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1193                                                InsideTransaction => 1 );
1194     unless ($m_id) {
1195         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1196
1197         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1198     }
1199
1200     unless ( $args{'Silent'} ) {
1201         $self->_NewTransaction(
1202             Type     => 'AddWatcher',
1203             NewValue => $principal->Id,
1204             Field    => $args{'Type'}
1205         );
1206     }
1207
1208         return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1209 }
1210
1211 # }}}
1212
1213
1214 # {{{ sub DeleteWatcher
1215
1216 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1217
1218
1219 Deletes a Ticket watcher.  Takes two arguments:
1220
1221 Type  (one of Requestor,Cc,AdminCc)
1222
1223 and one of
1224
1225 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1226     OR
1227 Email (the email address of an existing wathcer)
1228
1229
1230 =cut
1231
1232
1233 sub DeleteWatcher {
1234     my $self = shift;
1235
1236     my %args = ( Type        => undef,
1237                  PrincipalId => undef,
1238                  Email       => undef,
1239                  @_ );
1240
1241     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1242         return ( 0, $self->loc("No principal specified") );
1243     }
1244     my $principal = RT::Principal->new( $self->CurrentUser );
1245     if ( $args{'PrincipalId'} ) {
1246
1247         $principal->Load( $args{'PrincipalId'} );
1248     }
1249     else {
1250         my $user = RT::User->new( $self->CurrentUser );
1251         $user->LoadByEmail( $args{'Email'} );
1252         $principal->Load( $user->Id );
1253     }
1254
1255     # If we can't find this watcher, we need to bail.
1256     unless ( $principal->Id ) {
1257         return ( 0, $self->loc("Could not find that principal") );
1258     }
1259
1260     my $group = RT::Group->new( $self->CurrentUser );
1261     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1262     unless ( $group->id ) {
1263         return ( 0, $self->loc("Group not found") );
1264     }
1265
1266     # {{{ Check ACLS
1267     #If the watcher we're trying to add is for the current user
1268     if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1269
1270         #  If it's an AdminCc and they don't have
1271         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1272         if ( $args{'Type'} eq 'AdminCc' ) {
1273             unless (    $self->CurrentUserHasRight('ModifyTicket')
1274                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1275                 return ( 0, $self->loc('Permission Denied') );
1276             }
1277         }
1278
1279         #  If it's a Requestor or Cc and they don't have
1280         #   'Watch' or 'ModifyTicket', bail
1281         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1282         {
1283             unless (    $self->CurrentUserHasRight('ModifyTicket')
1284                      or $self->CurrentUserHasRight('Watch') ) {
1285                 return ( 0, $self->loc('Permission Denied') );
1286             }
1287         }
1288         else {
1289             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1290             return ( 0,
1291                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1292         }
1293     }
1294
1295     # If the watcher isn't the current user
1296     # and the current user  doesn't have 'ModifyTicket' bail
1297     else {
1298         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1299             return ( 0, $self->loc("Permission Denied") );
1300         }
1301     }
1302
1303     # }}}
1304
1305     # see if this user is already a watcher.
1306
1307     unless ( $group->HasMember($principal) ) {
1308         return ( 0,
1309                  $self->loc( 'That principal is not a [_1] for this ticket',
1310                              $args{'Type'} ) );
1311     }
1312
1313     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1314     unless ($m_id) {
1315         $RT::Logger->error( "Failed to delete "
1316                             . $principal->Id
1317                             . " as a member of group "
1318                             . $group->Id . ": "
1319                             . $m_msg );
1320
1321         return (0,
1322                 $self->loc(
1323                     'Could not remove that principal as a [_1] for this ticket',
1324                     $args{'Type'} ) );
1325     }
1326
1327     unless ( $args{'Silent'} ) {
1328         $self->_NewTransaction( Type     => 'DelWatcher',
1329                                 OldValue => $principal->Id,
1330                                 Field    => $args{'Type'} );
1331     }
1332
1333     return ( 1,
1334              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1335                          $principal->Object->Name,
1336                          $args{'Type'} ) );
1337 }
1338
1339
1340
1341 # }}}
1342
1343
1344 =head2 SquelchMailTo [EMAIL]
1345
1346 Takes an optional email address to never email about updates to this ticket.
1347
1348
1349 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1350
1351
1352 =cut
1353
1354 sub SquelchMailTo {
1355     my $self = shift;
1356     if (@_) {
1357         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1358             return undef;
1359         }
1360     } else {
1361         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1362             return undef;
1363         }
1364
1365     }
1366     return $self->_SquelchMailTo(@_);
1367 }
1368
1369 sub _SquelchMailTo {
1370     my $self = shift;
1371     if (@_) {
1372         my $attr = shift;
1373         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1374             unless grep { $_->Content eq $attr }
1375                 $self->Attributes->Named('SquelchMailTo');
1376     }
1377     my @attributes = $self->Attributes->Named('SquelchMailTo');
1378     return (@attributes);
1379 }
1380
1381
1382 =head2 UnsquelchMailTo ADDRESS
1383
1384 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1385
1386 Returns a tuple of (status, message)
1387
1388 =cut
1389
1390 sub UnsquelchMailTo {
1391     my $self = shift;
1392
1393     my $address = shift;
1394     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1395         return ( 0, $self->loc("Permission Denied") );
1396     }
1397
1398     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1399     return ($val, $msg);
1400 }
1401
1402
1403 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1404
1405 =head2 RequestorAddresses
1406
1407  B<Returns> String: All Ticket Requestor email addresses as a string.
1408
1409 =cut
1410
1411 sub RequestorAddresses {
1412     my $self = shift;
1413
1414     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1415         return undef;
1416     }
1417
1418     return ( $self->Requestors->MemberEmailAddressesAsString );
1419 }
1420
1421
1422 =head2 AdminCcAddresses
1423
1424 returns String: All Ticket AdminCc email addresses as a string
1425
1426 =cut
1427
1428 sub AdminCcAddresses {
1429     my $self = shift;
1430
1431     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1432         return undef;
1433     }
1434
1435     return ( $self->AdminCc->MemberEmailAddressesAsString )
1436
1437 }
1438
1439 =head2 CcAddresses
1440
1441 returns String: All Ticket Ccs as a string of email addresses
1442
1443 =cut
1444
1445 sub CcAddresses {
1446     my $self = shift;
1447
1448     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1449         return undef;
1450     }
1451     return ( $self->Cc->MemberEmailAddressesAsString);
1452
1453 }
1454
1455 # }}}
1456
1457 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1458
1459 # {{{ sub Requestors
1460
1461 =head2 Requestors
1462
1463 Takes nothing.
1464 Returns this ticket's Requestors as an RT::Group object
1465
1466 =cut
1467
1468 sub Requestors {
1469     my $self = shift;
1470
1471     my $group = RT::Group->new($self->CurrentUser);
1472     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1473         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1474     }
1475     return ($group);
1476
1477 }
1478
1479 # }}}
1480
1481 # {{{ sub _Requestors
1482
1483 =head2 _Requestors
1484
1485 Private non-ACLed variant of Reqeustors so that we can look them up for the
1486 purposes of customer auto-association during create.
1487
1488 =cut
1489
1490 sub _Requestors {
1491     my $self = shift;
1492
1493     my $group = RT::Group->new($RT::SystemUser);
1494     $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1495     return ($group);
1496 }
1497
1498 # }}}
1499
1500 # {{{ sub Cc
1501
1502 =head2 Cc
1503
1504 Takes nothing.
1505 Returns an RT::Group object which contains this ticket's Ccs.
1506 If the user doesn't have "ShowTicket" permission, returns an empty group
1507
1508 =cut
1509
1510 sub Cc {
1511     my $self = shift;
1512
1513     my $group = RT::Group->new($self->CurrentUser);
1514     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1515         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1516     }
1517     return ($group);
1518
1519 }
1520
1521 # }}}
1522
1523 # {{{ sub AdminCc
1524
1525 =head2 AdminCc
1526
1527 Takes nothing.
1528 Returns an RT::Group object which contains this ticket's AdminCcs.
1529 If the user doesn't have "ShowTicket" permission, returns an empty group
1530
1531 =cut
1532
1533 sub AdminCc {
1534     my $self = shift;
1535
1536     my $group = RT::Group->new($self->CurrentUser);
1537     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1538         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1539     }
1540     return ($group);
1541
1542 }
1543
1544 # }}}
1545
1546 # }}}
1547
1548 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1549
1550 # {{{ sub IsWatcher
1551 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1552
1553 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1554
1555 Takes a param hash with the attributes Type and either PrincipalId or Email
1556
1557 Type is one of Requestor, Cc, AdminCc and Owner
1558
1559 PrincipalId is an RT::Principal id, and Email is an email address.
1560
1561 Returns true if the specified principal (or the one corresponding to the
1562 specified address) is a member of the group Type for this ticket.
1563
1564 XX TODO: This should be Memoized. 
1565
1566 =cut
1567
1568 sub IsWatcher {
1569     my $self = shift;
1570
1571     my %args = ( Type  => 'Requestor',
1572         PrincipalId    => undef,
1573         Email          => undef,
1574         @_
1575     );
1576
1577     # Load the relevant group. 
1578     my $group = RT::Group->new($self->CurrentUser);
1579     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1580
1581     # Find the relevant principal.
1582     if (!$args{PrincipalId} && $args{Email}) {
1583         # Look up the specified user.
1584         my $user = RT::User->new($self->CurrentUser);
1585         $user->LoadByEmail($args{Email});
1586         if ($user->Id) {
1587             $args{PrincipalId} = $user->PrincipalId;
1588         }
1589         else {
1590             # A non-existent user can't be a group member.
1591             return 0;
1592         }
1593     }
1594
1595     # Ask if it has the member in question
1596     return $group->HasMember( $args{'PrincipalId'} );
1597 }
1598
1599 # }}}
1600
1601 # {{{ sub IsRequestor
1602
1603 =head2 IsRequestor PRINCIPAL_ID
1604   
1605 Takes an L<RT::Principal> id.
1606
1607 Returns true if the principal is a requestor of the current ticket.
1608
1609 =cut
1610
1611 sub IsRequestor {
1612     my $self   = shift;
1613     my $person = shift;
1614
1615     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1616
1617 };
1618
1619 # }}}
1620
1621 # {{{ sub IsCc
1622
1623 =head2 IsCc PRINCIPAL_ID
1624
1625   Takes an RT::Principal id.
1626   Returns true if the principal is a Cc of the current ticket.
1627
1628
1629 =cut
1630
1631 sub IsCc {
1632     my $self = shift;
1633     my $cc   = shift;
1634
1635     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1636
1637 }
1638
1639 # }}}
1640
1641 # {{{ sub IsAdminCc
1642
1643 =head2 IsAdminCc PRINCIPAL_ID
1644
1645   Takes an RT::Principal id.
1646   Returns true if the principal is an AdminCc of the current ticket.
1647
1648 =cut
1649
1650 sub IsAdminCc {
1651     my $self   = shift;
1652     my $person = shift;
1653
1654     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1655
1656 }
1657
1658 # }}}
1659
1660 # {{{ sub IsOwner
1661
1662 =head2 IsOwner
1663
1664   Takes an RT::User object. Returns true if that user is this ticket's owner.
1665 returns undef otherwise
1666
1667 =cut
1668
1669 sub IsOwner {
1670     my $self   = shift;
1671     my $person = shift;
1672
1673     # no ACL check since this is used in acl decisions
1674     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1675     #    return(undef);
1676     #   }    
1677
1678     #Tickets won't yet have owners when they're being created.
1679     unless ( $self->OwnerObj->id ) {
1680         return (undef);
1681     }
1682
1683     if ( $person->id == $self->OwnerObj->id ) {
1684         return (1);
1685     }
1686     else {
1687         return (undef);
1688     }
1689 }
1690
1691 # }}}
1692
1693 # }}}
1694
1695 # }}}
1696
1697
1698 =head2 TransactionAddresses
1699
1700 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for all this ticket's Create, Comment or Correspond transactions.
1701 The keys are C<To>, C<Cc> and C<Bcc>. The values are lists of C<Email::Address> objects.
1702
1703 NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
1704
1705 =cut
1706
1707
1708 sub TransactionAddresses {
1709     my $self = shift;
1710     my $txns = $self->Transactions;
1711
1712     my %addresses = ();
1713     foreach my $type (qw(Create Comment Correspond)) {
1714     $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1715         }
1716
1717     while (my $txn = $txns->Next) {
1718         my $txnaddrs = $txn->Addresses; 
1719         foreach my $addrlist ( values %$txnaddrs ) {
1720                 foreach my $addr (@$addrlist) {
1721                     # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1722                     next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1723                     # skips "comment-only" addresses
1724                     next unless ($addr->address);
1725                     $addresses{$addr->address} = $addr;
1726                 }
1727         }
1728     }
1729
1730     return \%addresses;
1731
1732 }
1733
1734
1735
1736
1737 # {{{ Routines dealing with queues 
1738
1739 # {{{ sub ValidateQueue
1740
1741 sub ValidateQueue {
1742     my $self  = shift;
1743     my $Value = shift;
1744
1745     if ( !$Value ) {
1746         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1747         return (1);
1748     }
1749
1750     my $QueueObj = RT::Queue->new( $self->CurrentUser );
1751     my $id       = $QueueObj->Load($Value);
1752
1753     if ($id) {
1754         return (1);
1755     }
1756     else {
1757         return (undef);
1758     }
1759 }
1760
1761 # }}}
1762
1763 # {{{ sub SetQueue  
1764
1765 sub SetQueue {
1766     my $self     = shift;
1767     my $NewQueue = shift;
1768
1769     #Redundant. ACL gets checked in _Set;
1770     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1771         return ( 0, $self->loc("Permission Denied") );
1772     }
1773
1774     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1775     $NewQueueObj->Load($NewQueue);
1776
1777     unless ( $NewQueueObj->Id() ) {
1778         return ( 0, $self->loc("That queue does not exist") );
1779     }
1780
1781     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1782         return ( 0, $self->loc('That is the same value') );
1783     }
1784     unless (
1785         $self->CurrentUser->HasRight(
1786             Right    => 'CreateTicket',
1787             Object => $NewQueueObj
1788         )
1789       )
1790     {
1791         return ( 0, $self->loc("You may not create requests in that queue.") );
1792     }
1793
1794     unless (
1795         $self->OwnerObj->HasRight(
1796             Right    => 'OwnTicket',
1797             Object => $NewQueueObj
1798         )
1799       )
1800     {
1801         my $clone = RT::Ticket->new( $RT::SystemUser );
1802         $clone->Load( $self->Id );
1803         unless ( $clone->Id ) {
1804             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1805         }
1806         my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1807         $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1808     }
1809
1810     my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1811
1812     if ( $status ) {
1813         # On queue change, change queue for reminders too
1814         my $reminder_collection = $self->Reminders->Collection;
1815         while ( my $reminder = $reminder_collection->Next ) {
1816             my ($status, $msg) = $reminder->SetQueue($NewQueue);
1817             $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1818         }
1819     }
1820     
1821     return ($status, $msg);
1822 }
1823
1824 # }}}
1825
1826 # {{{ sub QueueObj
1827
1828 =head2 QueueObj
1829
1830 Takes nothing. returns this ticket's queue object
1831
1832 =cut
1833
1834 sub QueueObj {
1835     my $self = shift;
1836
1837     my $queue_obj = RT::Queue->new( $self->CurrentUser );
1838
1839     #We call __Value so that we can avoid the ACL decision and some deep recursion
1840     my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1841     return ($queue_obj);
1842 }
1843
1844 # }}}
1845
1846 # }}}
1847
1848 # {{{ Date printing routines
1849
1850 # {{{ sub DueObj
1851
1852 =head2 DueObj
1853
1854   Returns an RT::Date object containing this ticket's due date
1855
1856 =cut
1857
1858 sub DueObj {
1859     my $self = shift;
1860
1861     my $time = new RT::Date( $self->CurrentUser );
1862
1863     # -1 is RT::Date slang for never
1864     if ( my $due = $self->Due ) {
1865         $time->Set( Format => 'sql', Value => $due );
1866     }
1867     else {
1868         $time->Set( Format => 'unix', Value => -1 );
1869     }
1870
1871     return $time;
1872 }
1873
1874 # }}}
1875
1876 # {{{ sub DueAsString 
1877
1878 =head2 DueAsString
1879
1880 Returns this ticket's due date as a human readable string
1881
1882 =cut
1883
1884 sub DueAsString {
1885     my $self = shift;
1886     return $self->DueObj->AsString();
1887 }
1888
1889 # }}}
1890
1891 # {{{ sub ResolvedObj
1892
1893 =head2 ResolvedObj
1894
1895   Returns an RT::Date object of this ticket's 'resolved' time.
1896
1897 =cut
1898
1899 sub ResolvedObj {
1900     my $self = shift;
1901
1902     my $time = new RT::Date( $self->CurrentUser );
1903     $time->Set( Format => 'sql', Value => $self->Resolved );
1904     return $time;
1905 }
1906
1907 # }}}
1908
1909 # {{{ sub SetStarted
1910
1911 =head2 SetStarted
1912
1913 Takes a date in ISO format or undef
1914 Returns a transaction id and a message
1915 The client calls "Start" to note that the project was started on the date in $date.
1916 A null date means "now"
1917
1918 =cut
1919
1920 sub SetStarted {
1921     my $self = shift;
1922     my $time = shift || 0;
1923
1924     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1925         return ( 0, $self->loc("Permission Denied") );
1926     }
1927
1928     #We create a date object to catch date weirdness
1929     my $time_obj = new RT::Date( $self->CurrentUser() );
1930     if ( $time ) {
1931         $time_obj->Set( Format => 'ISO', Value => $time );
1932     }
1933     else {
1934         $time_obj->SetToNow();
1935     }
1936
1937     #Now that we're starting, open this ticket
1938     #TODO do we really want to force this as policy? it should be a scrip
1939
1940     #We need $TicketAsSystem, in case the current user doesn't have
1941     #ShowTicket
1942     #
1943     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1944     $TicketAsSystem->Load( $self->Id );
1945     if ( $TicketAsSystem->Status eq 'new' ) {
1946         $TicketAsSystem->Open();
1947     }
1948
1949     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1950
1951 }
1952
1953 # }}}
1954
1955 # {{{ sub StartedObj
1956
1957 =head2 StartedObj
1958
1959   Returns an RT::Date object which contains this ticket's 
1960 'Started' time.
1961
1962 =cut
1963
1964 sub StartedObj {
1965     my $self = shift;
1966
1967     my $time = new RT::Date( $self->CurrentUser );
1968     $time->Set( Format => 'sql', Value => $self->Started );
1969     return $time;
1970 }
1971
1972 # }}}
1973
1974 # {{{ sub StartsObj
1975
1976 =head2 StartsObj
1977
1978   Returns an RT::Date object which contains this ticket's 
1979 'Starts' time.
1980
1981 =cut
1982
1983 sub StartsObj {
1984     my $self = shift;
1985
1986     my $time = new RT::Date( $self->CurrentUser );
1987     $time->Set( Format => 'sql', Value => $self->Starts );
1988     return $time;
1989 }
1990
1991 # }}}
1992
1993 # {{{ sub ToldObj
1994
1995 =head2 ToldObj
1996
1997   Returns an RT::Date object which contains this ticket's 
1998 'Told' time.
1999
2000 =cut
2001
2002 sub ToldObj {
2003     my $self = shift;
2004
2005     my $time = new RT::Date( $self->CurrentUser );
2006     $time->Set( Format => 'sql', Value => $self->Told );
2007     return $time;
2008 }
2009
2010 # }}}
2011
2012 # {{{ sub ToldAsString
2013
2014 =head2 ToldAsString
2015
2016 A convenience method that returns ToldObj->AsString
2017
2018 TODO: This should be deprecated
2019
2020 =cut
2021
2022 sub ToldAsString {
2023     my $self = shift;
2024     if ( $self->Told ) {
2025         return $self->ToldObj->AsString();
2026     }
2027     else {
2028         return ("Never");
2029     }
2030 }
2031
2032 # }}}
2033
2034 # {{{ sub TimeWorkedAsString
2035
2036 =head2 TimeWorkedAsString
2037
2038 Returns the amount of time worked on this ticket as a Text String
2039
2040 =cut
2041
2042 sub TimeWorkedAsString {
2043     my $self = shift;
2044     my $value = $self->TimeWorked;
2045
2046     # return the # of minutes worked turned into seconds and written as
2047     # a simple text string, this is not really a date object, but if we
2048     # diff a number of seconds vs the epoch, we'll get a nice description
2049     # of time worked.
2050     return "" unless $value;
2051     return RT::Date->new( $self->CurrentUser )
2052         ->DurationAsString( $value * 60 );
2053 }
2054
2055 # }}}
2056
2057 # {{{ sub TimeLeftAsString
2058
2059 =head2  TimeLeftAsString
2060
2061 Returns the amount of time left on this ticket as a Text String
2062
2063 =cut
2064
2065 sub TimeLeftAsString {
2066     my $self = shift;
2067     my $value = $self->TimeLeft;
2068     return "" unless $value;
2069     return RT::Date->new( $self->CurrentUser )
2070         ->DurationAsString( $value * 60 );
2071 }
2072
2073 # }}}
2074
2075 # {{{ Routines dealing with correspondence/comments
2076
2077 # {{{ sub Comment
2078
2079 =head2 Comment
2080
2081 Comment on this ticket.
2082 Takes a hash with the following attributes:
2083 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2084 comment.
2085
2086 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2087
2088 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2089 They will, however, be prepared and you'll be able to access them through the TransactionObj
2090
2091 Returns: Transaction id, Error Message, Transaction Object
2092 (note the different order from Create()!)
2093
2094 =cut
2095
2096 sub Comment {
2097     my $self = shift;
2098
2099     my %args = ( CcMessageTo  => undef,
2100                  BccMessageTo => undef,
2101                  MIMEObj      => undef,
2102                  Content      => undef,
2103                  TimeTaken => 0,
2104                  DryRun     => 0, 
2105                  @_ );
2106
2107     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2108              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2109         return ( 0, $self->loc("Permission Denied"), undef );
2110     }
2111     $args{'NoteType'} = 'Comment';
2112
2113     if ($args{'DryRun'}) {
2114         $RT::Handle->BeginTransaction();
2115         $args{'CommitScrips'} = 0;
2116     }
2117
2118     my @results = $self->_RecordNote(%args);
2119     if ($args{'DryRun'}) {
2120         $RT::Handle->Rollback();
2121     }
2122
2123     return(@results);
2124 }
2125 # }}}
2126
2127 # {{{ sub Correspond
2128
2129 =head2 Correspond
2130
2131 Correspond on this ticket.
2132 Takes a hashref with the following attributes:
2133
2134
2135 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2136
2137 if there's no MIMEObj, Content is used to build a MIME::Entity object
2138
2139 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2140 They will, however, be prepared and you'll be able to access them through the TransactionObj
2141
2142 Returns: Transaction id, Error Message, Transaction Object
2143 (note the different order from Create()!)
2144
2145
2146 =cut
2147
2148 sub Correspond {
2149     my $self = shift;
2150     my %args = ( CcMessageTo  => undef,
2151                  BccMessageTo => undef,
2152                  MIMEObj      => undef,
2153                  Content      => undef,
2154                  TimeTaken    => 0,
2155                  @_ );
2156
2157     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2158              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2159         return ( 0, $self->loc("Permission Denied"), undef );
2160     }
2161
2162     $args{'NoteType'} = 'Correspond'; 
2163     if ($args{'DryRun'}) {
2164         $RT::Handle->BeginTransaction();
2165         $args{'CommitScrips'} = 0;
2166     }
2167
2168     my @results = $self->_RecordNote(%args);
2169
2170     #Set the last told date to now if this isn't mail from the requestor.
2171     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2172     $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2173
2174     if ($args{'DryRun'}) {
2175         $RT::Handle->Rollback();
2176     }
2177
2178     return (@results);
2179
2180 }
2181
2182 # }}}
2183
2184 # {{{ sub _RecordNote
2185
2186 =head2 _RecordNote
2187
2188 the meat of both comment and correspond. 
2189
2190 Performs no access control checks. hence, dangerous.
2191
2192 =cut
2193
2194 sub _RecordNote {
2195     my $self = shift;
2196     my %args = ( 
2197         CcMessageTo  => undef,
2198         BccMessageTo => undef,
2199         Encrypt      => undef,
2200         Sign         => undef,
2201         MIMEObj      => undef,
2202         Content      => undef,
2203         NoteType     => 'Correspond',
2204         TimeTaken    => 0,
2205         CommitScrips => 1,
2206         @_
2207     );
2208
2209     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2210         return ( 0, $self->loc("No message attached"), undef );
2211     }
2212
2213     unless ( $args{'MIMEObj'} ) {
2214         $args{'MIMEObj'} = MIME::Entity->build(
2215             Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2216         );
2217     }
2218
2219     # convert text parts into utf-8
2220     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2221
2222     # If we've been passed in CcMessageTo and BccMessageTo fields,
2223     # add them to the mime object for passing on to the transaction handler
2224     # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2225     # RT-Send-Bcc: headers
2226
2227
2228     foreach my $type (qw/Cc Bcc/) {
2229         if ( defined $args{ $type . 'MessageTo' } ) {
2230
2231             my $addresses = join ', ', (
2232                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2233                     Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2234             $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2235         }
2236     }
2237
2238     foreach my $argument (qw(Encrypt Sign)) {
2239         $args{'MIMEObj'}->head->add(
2240             "X-RT-$argument" => $args{ $argument }
2241         ) if defined $args{ $argument };
2242     }
2243
2244     # If this is from an external source, we need to come up with its
2245     # internal Message-ID now, so all emails sent because of this
2246     # message have a common Message-ID
2247     my $org = RT->Config->Get('Organization');
2248     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2249     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2250         $args{'MIMEObj'}->head->set(
2251             'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2252         );
2253     }
2254
2255     #Record the correspondence (write the transaction)
2256     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2257              Type => $args{'NoteType'},
2258              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2259              TimeTaken => $args{'TimeTaken'},
2260              MIMEObj   => $args{'MIMEObj'}, 
2261              CommitScrips => $args{'CommitScrips'},
2262     );
2263
2264     unless ($Trans) {
2265         $RT::Logger->err("$self couldn't init a transaction $msg");
2266         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2267     }
2268
2269     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2270 }
2271
2272 # }}}
2273
2274 # }}}
2275
2276 # {{{ sub _Links 
2277
2278 sub _Links {
2279     my $self = shift;
2280
2281     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2282     #tobias meant by $f
2283     my $field = shift;
2284     my $type  = shift || "";
2285
2286     unless ( $self->{"$field$type"} ) {
2287         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2288
2289         #not sure what this ACL was supposed to do... but returning the
2290         # bare (unlimited) RT::Links certainly seems wrong, it causes the
2291         # $Ticket->Customers method during creation to return results for every
2292         # ticket...
2293         #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2294
2295             # Maybe this ticket is a merged ticket
2296             my $Tickets = new RT::Tickets( $self->CurrentUser );
2297             # at least to myself
2298             $self->{"$field$type"}->Limit( FIELD => $field,
2299                                            VALUE => $self->URI,
2300                                            ENTRYAGGREGATOR => 'OR' );
2301             $Tickets->Limit( FIELD => 'EffectiveId',
2302                              VALUE => $self->EffectiveId );
2303             while (my $Ticket = $Tickets->Next) {
2304                 $self->{"$field$type"}->Limit( FIELD => $field,
2305                                                VALUE => $Ticket->URI,
2306                                                ENTRYAGGREGATOR => 'OR' );
2307             }
2308             $self->{"$field$type"}->Limit( FIELD => 'Type',
2309                                            VALUE => $type )
2310               if ($type);
2311         #}
2312     }
2313     return ( $self->{"$field$type"} );
2314 }
2315
2316 # }}}
2317
2318 # {{{ sub DeleteLink 
2319
2320 =head2 DeleteLink
2321
2322 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2323 SilentBase and SilentTarget. Either Base or Target must be null.
2324 The null value will be replaced with this ticket\'s id.
2325
2326 If Silent is true then no transaction would be recorded, in other
2327 case you can control creation of transactions on both base and
2328 target with SilentBase and SilentTarget respectively. By default
2329 both transactions are created.
2330
2331 =cut 
2332
2333 sub DeleteLink {
2334     my $self = shift;
2335     my %args = (
2336         Base   => undef,
2337         Target => undef,
2338         Type   => undef,
2339         Silent => undef,
2340         SilentBase   => undef,
2341         SilentTarget => undef,
2342         @_
2343     );
2344
2345     unless ( $args{'Target'} || $args{'Base'} ) {
2346         $RT::Logger->error("Base or Target must be specified");
2347         return ( 0, $self->loc('Either base or target must be specified') );
2348     }
2349
2350     #check acls
2351     my $right = 0;
2352     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2353     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2354         return ( 0, $self->loc("Permission Denied") );
2355     }
2356
2357     # If the other URI is an RT::Ticket, we want to make sure the user
2358     # can modify it too...
2359     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2360     return (0, $msg) unless $status;
2361     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2362         $right++;
2363     }
2364     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2365          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2366     {
2367         return ( 0, $self->loc("Permission Denied") );
2368     }
2369
2370     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2371     return ( 0, $Msg ) unless $val;
2372
2373     return ( $val, $Msg ) if $args{'Silent'};
2374
2375     my ($direction, $remote_link);
2376
2377     if ( $args{'Base'} ) {
2378         $remote_link = $args{'Base'};
2379         $direction = 'Target';
2380     }
2381     elsif ( $args{'Target'} ) {
2382         $remote_link = $args{'Target'};
2383         $direction = 'Base';
2384     } 
2385
2386     my $remote_uri = RT::URI->new( $self->CurrentUser );
2387     $remote_uri->FromURI( $remote_link );
2388
2389     unless ( $args{ 'Silent'. $direction } ) {
2390         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2391             Type      => 'DeleteLink',
2392             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2393             OldValue  => $remote_uri->URI || $remote_link,
2394             TimeTaken => 0
2395         );
2396         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2397     }
2398
2399     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2400         my $OtherObj = $remote_uri->Object;
2401         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2402             Type           => 'DeleteLink',
2403             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2404                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2405             OldValue       => $self->URI,
2406             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2407             TimeTaken      => 0,
2408         );
2409         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2410     }
2411
2412     return ( $val, $Msg );
2413 }
2414
2415 # }}}
2416
2417 # {{{ sub AddLink
2418
2419 =head2 AddLink
2420
2421 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2422
2423 If Silent is true then no transaction would be recorded, in other
2424 case you can control creation of transactions on both base and
2425 target with SilentBase and SilentTarget respectively. By default
2426 both transactions are created.
2427
2428 =cut
2429
2430 sub AddLink {
2431     my $self = shift;
2432     my %args = ( Target       => '',
2433                  Base         => '',
2434                  Type         => '',
2435                  Silent       => undef,
2436                  SilentBase   => undef,
2437                  SilentTarget => undef,
2438                  @_ );
2439
2440     unless ( $args{'Target'} || $args{'Base'} ) {
2441         $RT::Logger->error("Base or Target must be specified");
2442         return ( 0, $self->loc('Either base or target must be specified') );
2443     }
2444
2445     my $right = 0;
2446     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2447     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2448         return ( 0, $self->loc("Permission Denied") );
2449     }
2450
2451     # If the other URI is an RT::Ticket, we want to make sure the user
2452     # can modify it too...
2453     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2454     return (0, $msg) unless $status;
2455     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2456         $right++;
2457     }
2458     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2459          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2460     {
2461         return ( 0, $self->loc("Permission Denied") );
2462     }
2463
2464     return $self->_AddLink(%args);
2465 }
2466
2467 sub __GetTicketFromURI {
2468     my $self = shift;
2469     my %args = ( URI => '', @_ );
2470
2471     # If the other URI is an RT::Ticket, we want to make sure the user
2472     # can modify it too...
2473     my $uri_obj = RT::URI->new( $self->CurrentUser );
2474     $uri_obj->FromURI( $args{'URI'} );
2475
2476     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2477         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2478         $RT::Logger->warning( $msg );
2479         return( 0, $msg );
2480     }
2481     my $obj = $uri_obj->Resolver->Object;
2482     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2483         return (1, 'Found not a ticket', undef);
2484     }
2485     return (1, 'Found ticket', $obj);
2486 }
2487
2488 =head2 _AddLink  
2489
2490 Private non-acled variant of AddLink so that links can be added during create.
2491
2492 =cut
2493
2494 sub _AddLink {
2495     my $self = shift;
2496     my %args = ( Target       => '',
2497                  Base         => '',
2498                  Type         => '',
2499                  Silent       => undef,
2500                  SilentBase   => undef,
2501                  SilentTarget => undef,
2502                  @_ );
2503
2504     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2505     return ($val, $msg) if !$val || $exist;
2506     return ($val, $msg) if $args{'Silent'};
2507
2508     my ($direction, $remote_link);
2509     if ( $args{'Target'} ) {
2510         $remote_link  = $args{'Target'};
2511         $direction    = 'Base';
2512     } elsif ( $args{'Base'} ) {
2513         $remote_link  = $args{'Base'};
2514         $direction    = 'Target';
2515     }
2516
2517     my $remote_uri = RT::URI->new( $self->CurrentUser );
2518     $remote_uri->FromURI( $remote_link );
2519
2520     unless ( $args{ 'Silent'. $direction } ) {
2521         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2522             Type      => 'AddLink',
2523             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2524             NewValue  =>  $remote_uri->URI || $remote_link,
2525             TimeTaken => 0
2526         );
2527         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2528     }
2529
2530     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2531         my $OtherObj = $remote_uri->Object;
2532         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2533             Type           => 'AddLink',
2534             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2535                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2536             NewValue       => $self->URI,
2537             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2538             TimeTaken      => 0,
2539         );
2540         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2541     }
2542
2543     return ( $val, $msg );
2544 }
2545
2546 # }}}
2547
2548
2549 # {{{ sub MergeInto
2550
2551 =head2 MergeInto
2552
2553 MergeInto take the id of the ticket to merge this ticket into.
2554
2555
2556
2557 =cut
2558
2559 sub MergeInto {
2560     my $self      = shift;
2561     my $ticket_id = shift;
2562
2563     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2564         return ( 0, $self->loc("Permission Denied") );
2565     }
2566
2567     # Load up the new ticket.
2568     my $MergeInto = RT::Ticket->new($RT::SystemUser);
2569     $MergeInto->Load($ticket_id);
2570
2571     # make sure it exists.
2572     unless ( $MergeInto->Id ) {
2573         return ( 0, $self->loc("New ticket doesn't exist") );
2574     }
2575
2576     # Make sure the current user can modify the new ticket.
2577     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2578         return ( 0, $self->loc("Permission Denied") );
2579     }
2580
2581     $RT::Handle->BeginTransaction();
2582
2583     # We use EffectiveId here even though it duplicates information from
2584     # the links table becasue of the massive performance hit we'd take
2585     # by trying to do a separate database query for merge info everytime 
2586     # loaded a ticket. 
2587
2588     #update this ticket's effective id to the new ticket's id.
2589     my ( $id_val, $id_msg ) = $self->__Set(
2590         Field => 'EffectiveId',
2591         Value => $MergeInto->Id()
2592     );
2593
2594     unless ($id_val) {
2595         $RT::Handle->Rollback();
2596         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2597     }
2598
2599
2600     if ( $self->__Value('Status') ne 'resolved' ) {
2601
2602         my ( $status_val, $status_msg )
2603             = $self->__Set( Field => 'Status', Value => 'resolved' );
2604
2605         unless ($status_val) {
2606             $RT::Handle->Rollback();
2607             $RT::Logger->error(
2608                 $self->loc(
2609                     "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2610                     $self
2611                 )
2612             );
2613             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2614         }
2615     }
2616
2617     # update all the links that point to that old ticket
2618     my $old_links_to = RT::Links->new($self->CurrentUser);
2619     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2620
2621     my %old_seen;
2622     while (my $link = $old_links_to->Next) {
2623         if (exists $old_seen{$link->Base."-".$link->Type}) {
2624             $link->Delete;
2625         }   
2626         elsif ($link->Base eq $MergeInto->URI) {
2627             $link->Delete;
2628         } else {
2629             # First, make sure the link doesn't already exist. then move it over.
2630             my $tmp = RT::Link->new($RT::SystemUser);
2631             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2632             if ($tmp->id)   {
2633                     $link->Delete;
2634             } else { 
2635                 $link->SetTarget($MergeInto->URI);
2636                 $link->SetLocalTarget($MergeInto->id);
2637             }
2638             $old_seen{$link->Base."-".$link->Type} =1;
2639         }
2640
2641     }
2642
2643     my $old_links_from = RT::Links->new($self->CurrentUser);
2644     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2645
2646     while (my $link = $old_links_from->Next) {
2647         if (exists $old_seen{$link->Type."-".$link->Target}) {
2648             $link->Delete;
2649         }   
2650         if ($link->Target eq $MergeInto->URI) {
2651             $link->Delete;
2652         } else {
2653             # First, make sure the link doesn't already exist. then move it over.
2654             my $tmp = RT::Link->new($RT::SystemUser);
2655             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2656             if ($tmp->id)   {
2657                     $link->Delete;
2658             } else { 
2659                 $link->SetBase($MergeInto->URI);
2660                 $link->SetLocalBase($MergeInto->id);
2661                 $old_seen{$link->Type."-".$link->Target} =1;
2662             }
2663         }
2664
2665     }
2666
2667     # Update time fields
2668     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2669
2670         my $mutator = "Set$type";
2671         $MergeInto->$mutator(
2672             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2673
2674     }
2675 #add all of this ticket's watchers to that ticket.
2676     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2677
2678         my $people = $self->$watcher_type->MembersObj;
2679         my $addwatcher_type =  $watcher_type;
2680         $addwatcher_type  =~ s/s$//;
2681
2682         while ( my $watcher = $people->Next ) {
2683             
2684            my ($val, $msg) =  $MergeInto->_AddWatcher(
2685                 Type        => $addwatcher_type,
2686                 Silent => 1,
2687                 PrincipalId => $watcher->MemberId
2688             );
2689             unless ($val) {
2690                 $RT::Logger->warning($msg);
2691             }
2692     }
2693
2694     }
2695
2696     #find all of the tickets that were merged into this ticket. 
2697     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2698     $old_mergees->Limit(
2699         FIELD    => 'EffectiveId',
2700         OPERATOR => '=',
2701         VALUE    => $self->Id
2702     );
2703
2704     #   update their EffectiveId fields to the new ticket's id
2705     while ( my $ticket = $old_mergees->Next() ) {
2706         my ( $val, $msg ) = $ticket->__Set(
2707             Field => 'EffectiveId',
2708             Value => $MergeInto->Id()
2709         );
2710     }
2711
2712     #make a new link: this ticket is merged into that other ticket.
2713     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2714
2715     $MergeInto->_SetLastUpdated;    
2716
2717     $RT::Handle->Commit();
2718     return ( 1, $self->loc("Merge Successful") );
2719 }
2720
2721 =head2 Merged
2722
2723 Returns list of tickets' ids that's been merged into this ticket.
2724
2725 =cut
2726
2727 sub Merged {
2728     my $self = shift;
2729
2730     my $mergees = new RT::Tickets( $self->CurrentUser );
2731     $mergees->Limit(
2732         FIELD    => 'EffectiveId',
2733         OPERATOR => '=',
2734         VALUE    => $self->Id,
2735     );
2736     $mergees->Limit(
2737         FIELD    => 'id',
2738         OPERATOR => '!=',
2739         VALUE    => $self->Id,
2740     );
2741     return map $_->id, @{ $mergees->ItemsArrayRef || [] };
2742 }
2743
2744 # }}}
2745
2746 # }}}
2747
2748 # {{{ Routines dealing with ownership
2749
2750 # {{{ sub OwnerObj
2751
2752 =head2 OwnerObj
2753
2754 Takes nothing and returns an RT::User object of 
2755 this ticket's owner
2756
2757 =cut
2758
2759 sub OwnerObj {
2760     my $self = shift;
2761
2762     #If this gets ACLed, we lose on a rights check in User.pm and
2763     #get deep recursion. if we need ACLs here, we need
2764     #an equiv without ACLs
2765
2766     my $owner = new RT::User( $self->CurrentUser );
2767     $owner->Load( $self->__Value('Owner') );
2768
2769     #Return the owner object
2770     return ($owner);
2771 }
2772
2773 # }}}
2774
2775 # {{{ sub OwnerAsString 
2776
2777 =head2 OwnerAsString
2778
2779 Returns the owner's email address
2780
2781 =cut
2782
2783 sub OwnerAsString {
2784     my $self = shift;
2785     return ( $self->OwnerObj->EmailAddress );
2786
2787 }
2788
2789 # }}}
2790
2791 # {{{ sub SetOwner
2792
2793 =head2 SetOwner
2794
2795 Takes two arguments:
2796      the Id or Name of the owner 
2797 and  (optionally) the type of the SetOwner Transaction. It defaults
2798 to 'Give'.  'Steal' is also a valid option.
2799
2800
2801 =cut
2802
2803 sub SetOwner {
2804     my $self     = shift;
2805     my $NewOwner = shift;
2806     my $Type     = shift || "Give";
2807
2808     $RT::Handle->BeginTransaction();
2809
2810     $self->_SetLastUpdated(); # lock the ticket
2811     $self->Load( $self->id ); # in case $self changed while waiting for lock
2812
2813     my $OldOwnerObj = $self->OwnerObj;
2814
2815     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2816     $NewOwnerObj->Load( $NewOwner );
2817     unless ( $NewOwnerObj->Id ) {
2818         $RT::Handle->Rollback();
2819         return ( 0, $self->loc("That user does not exist") );
2820     }
2821
2822
2823     # must have ModifyTicket rights
2824     # or TakeTicket/StealTicket and $NewOwner is self
2825     # see if it's a take
2826     if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2827         unless (    $self->CurrentUserHasRight('ModifyTicket')
2828                  || $self->CurrentUserHasRight('TakeTicket') ) {
2829             $RT::Handle->Rollback();
2830             return ( 0, $self->loc("Permission Denied") );
2831         }
2832     }
2833
2834     # see if it's a steal
2835     elsif (    $OldOwnerObj->Id != $RT::Nobody->Id
2836             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2837
2838         unless (    $self->CurrentUserHasRight('ModifyTicket')
2839                  || $self->CurrentUserHasRight('StealTicket') ) {
2840             $RT::Handle->Rollback();
2841             return ( 0, $self->loc("Permission Denied") );
2842         }
2843     }
2844     else {
2845         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2846             $RT::Handle->Rollback();
2847             return ( 0, $self->loc("Permission Denied") );
2848         }
2849     }
2850
2851     # If we're not stealing and the ticket has an owner and it's not
2852     # the current user
2853     if ( $Type ne 'Steal' and $Type ne 'Force'
2854          and $OldOwnerObj->Id != $RT::Nobody->Id
2855          and $OldOwnerObj->Id != $self->CurrentUser->Id )
2856     {
2857         $RT::Handle->Rollback();
2858         return ( 0, $self->loc("You can only take tickets that are unowned") )
2859             if $NewOwnerObj->id == $self->CurrentUser->id;
2860         return (
2861             0,
2862             $self->loc("You can only reassign tickets that you own or that are unowned" )
2863         );
2864     }
2865
2866     #If we've specified a new owner and that user can't modify the ticket
2867     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2868         $RT::Handle->Rollback();
2869         return ( 0, $self->loc("That user may not own tickets in that queue") );
2870     }
2871
2872     # If the ticket has an owner and it's the new owner, we don't need
2873     # To do anything
2874     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2875         $RT::Handle->Rollback();
2876         return ( 0, $self->loc("That user already owns that ticket") );
2877     }
2878
2879     # Delete the owner in the owner group, then add a new one
2880     # TODO: is this safe? it's not how we really want the API to work
2881     # for most things, but it's fast.
2882     my ( $del_id, $del_msg );
2883     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2884         ($del_id, $del_msg) = $owner->Delete();
2885         last unless ($del_id);
2886     }
2887
2888     unless ($del_id) {
2889         $RT::Handle->Rollback();
2890         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2891     }
2892
2893     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2894                                        PrincipalId => $NewOwnerObj->PrincipalId,
2895                                        InsideTransaction => 1 );
2896     unless ($add_id) {
2897         $RT::Handle->Rollback();
2898         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2899     }
2900
2901     # We call set twice with slightly different arguments, so
2902     # as to not have an SQL transaction span two RT transactions
2903
2904     my ( $val, $msg ) = $self->_Set(
2905                       Field             => 'Owner',
2906                       RecordTransaction => 0,
2907                       Value             => $NewOwnerObj->Id,
2908                       TimeTaken         => 0,
2909                       TransactionType   => $Type,
2910                       CheckACL          => 0,                  # don't check acl
2911     );
2912
2913     unless ($val) {
2914         $RT::Handle->Rollback;
2915         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2916     }
2917
2918     ($val, $msg) = $self->_NewTransaction(
2919         Type      => $Type,
2920         Field     => 'Owner',
2921         NewValue  => $NewOwnerObj->Id,
2922         OldValue  => $OldOwnerObj->Id,
2923         TimeTaken => 0,
2924     );
2925
2926     if ( $val ) {
2927         $msg = $self->loc( "Owner changed from [_1] to [_2]",
2928                            $OldOwnerObj->Name, $NewOwnerObj->Name );
2929     }
2930     else {
2931         $RT::Handle->Rollback();
2932         return ( 0, $msg );
2933     }
2934
2935     $RT::Handle->Commit();
2936
2937     return ( $val, $msg );
2938 }
2939
2940 # }}}
2941
2942 # {{{ sub Take
2943
2944 =head2 Take
2945
2946 A convenince method to set the ticket's owner to the current user
2947
2948 =cut
2949
2950 sub Take {
2951     my $self = shift;
2952     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2953 }
2954
2955 # }}}
2956
2957 # {{{ sub Untake
2958
2959 =head2 Untake
2960
2961 Convenience method to set the owner to 'nobody' if the current user is the owner.
2962
2963 =cut
2964
2965 sub Untake {
2966     my $self = shift;
2967     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2968 }
2969
2970 # }}}
2971
2972 # {{{ sub Steal 
2973
2974 =head2 Steal
2975
2976 A convenience method to change the owner of the current ticket to the
2977 current user. Even if it's owned by another user.
2978
2979 =cut
2980
2981 sub Steal {
2982     my $self = shift;
2983
2984     if ( $self->IsOwner( $self->CurrentUser ) ) {
2985         return ( 0, $self->loc("You already own this ticket") );
2986     }
2987     else {
2988         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2989
2990     }
2991
2992 }
2993
2994 # }}}
2995
2996 # }}}
2997
2998 # {{{ Routines dealing with status
2999
3000 # {{{ sub ValidateStatus 
3001
3002 =head2 ValidateStatus STATUS
3003
3004 Takes a string. Returns true if that status is a valid status for this ticket.
3005 Returns false otherwise.
3006
3007 =cut
3008
3009 sub ValidateStatus {
3010     my $self   = shift;
3011     my $status = shift;
3012
3013     #Make sure the status passed in is valid
3014     unless ( $self->QueueObj->IsValidStatus($status) ) {
3015         return (undef);
3016     }
3017
3018     return (1);
3019
3020 }
3021
3022 # }}}
3023
3024 # {{{ sub SetStatus
3025
3026 =head2 SetStatus STATUS
3027
3028 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3029
3030 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE).  If FORCE is true, ignore unresolved dependencies and force a status change.
3031
3032
3033
3034 =cut
3035
3036 sub SetStatus {
3037     my $self   = shift;
3038     my %args;
3039
3040     if (@_ == 1) {
3041     $args{Status} = shift;
3042     }
3043     else {
3044     %args = (@_);
3045     }
3046
3047     #Check ACL
3048     if ( $args{Status} eq 'deleted') {
3049             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3050             return ( 0, $self->loc('Permission Denied') );
3051        }
3052     } else {
3053             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3054             return ( 0, $self->loc('Permission Denied') );
3055        }
3056     }
3057
3058     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3059         return (0, $self->loc('That ticket has unresolved dependencies'));
3060     }
3061
3062     my $now = RT::Date->new( $self->CurrentUser );
3063     $now->SetToNow();
3064
3065     #If we're changing the status from new, record that we've started
3066     if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3067
3068         #Set the Started time to "now"
3069         $self->_Set( Field             => 'Started',
3070                      Value             => $now->ISO,
3071                      RecordTransaction => 0 );
3072     }
3073
3074     #When we close a ticket, set the 'Resolved' attribute to now.
3075     # It's misnamed, but that's just historical.
3076     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3077         $self->_Set( Field             => 'Resolved',
3078                      Value             => $now->ISO,
3079                      RecordTransaction => 0 );
3080     }
3081
3082     #Actually update the status
3083    my ($val, $msg)= $self->_Set( Field           => 'Status',
3084                           Value           => $args{Status},
3085                           TimeTaken       => 0,
3086                           CheckACL      => 0,
3087                           TransactionType => 'Status'  );
3088
3089     return($val,$msg);
3090 }
3091
3092 # }}}
3093
3094 # {{{ sub Delete
3095
3096 =head2 Delete
3097
3098 Takes no arguments. Marks this ticket for garbage collection
3099
3100 =cut
3101
3102 sub Delete {
3103     my $self = shift;
3104     return ( $self->SetStatus('deleted') );
3105
3106     # TODO: garbage collection
3107 }
3108
3109 # }}}
3110
3111 # {{{ sub Stall
3112
3113 =head2 Stall
3114
3115 Sets this ticket's status to stalled
3116
3117 =cut
3118
3119 sub Stall {
3120     my $self = shift;
3121     return ( $self->SetStatus('stalled') );
3122 }
3123
3124 # }}}
3125
3126 # {{{ sub Reject
3127
3128 =head2 Reject
3129
3130 Sets this ticket's status to rejected
3131
3132 =cut
3133
3134 sub Reject {
3135     my $self = shift;
3136     return ( $self->SetStatus('rejected') );
3137 }
3138
3139 # }}}
3140
3141 # {{{ sub Open
3142
3143 =head2 Open
3144
3145 Sets this ticket\'s status to Open
3146
3147 =cut
3148
3149 sub Open {
3150     my $self = shift;
3151     return ( $self->SetStatus('open') );
3152 }
3153
3154 # }}}
3155
3156 # {{{ sub Resolve
3157
3158 =head2 Resolve
3159
3160 Sets this ticket\'s status to Resolved
3161
3162 =cut
3163
3164 sub Resolve {
3165     my $self = shift;
3166     return ( $self->SetStatus('resolved') );
3167 }
3168
3169 # }}}
3170
3171 # }}}
3172
3173     
3174 # {{{ Actions + Routines dealing with transactions
3175
3176 # {{{ sub SetTold and _SetTold
3177
3178 =head2 SetTold ISO  [TIMETAKEN]
3179
3180 Updates the told and records a transaction
3181
3182 =cut
3183
3184 sub SetTold {
3185     my $self = shift;
3186     my $told;
3187     $told = shift if (@_);
3188     my $timetaken = shift || 0;
3189
3190     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3191         return ( 0, $self->loc("Permission Denied") );
3192     }
3193
3194     my $datetold = new RT::Date( $self->CurrentUser );
3195     if ($told) {
3196         $datetold->Set( Format => 'iso',
3197                         Value  => $told );
3198     }
3199     else {
3200         $datetold->SetToNow();
3201     }
3202
3203     return ( $self->_Set( Field           => 'Told',
3204                           Value           => $datetold->ISO,
3205                           TimeTaken       => $timetaken,
3206                           TransactionType => 'Told' ) );
3207 }
3208
3209 =head2 _SetTold
3210
3211 Updates the told without a transaction or acl check. Useful when we're sending replies.
3212
3213 =cut
3214
3215 sub _SetTold {
3216     my $self = shift;
3217
3218     my $now = new RT::Date( $self->CurrentUser );
3219     $now->SetToNow();
3220
3221     #use __Set to get no ACLs ;)
3222     return ( $self->__Set( Field => 'Told',
3223                            Value => $now->ISO ) );
3224 }
3225
3226 =head2 SeenUpTo
3227
3228
3229 =cut
3230
3231 sub SeenUpTo {
3232     my $self = shift;
3233     my $uid = $self->CurrentUser->id;
3234     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3235     return if $attr && $attr->Content gt $self->LastUpdated;
3236
3237     my $txns = $self->Transactions;
3238     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3239     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3240     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3241     $txns->Limit(
3242         FIELD => 'Created',
3243         OPERATOR => '>',
3244         VALUE => $attr->Content
3245     ) if $attr;
3246     $txns->RowsPerPage(1);
3247     return $txns->First;
3248 }
3249
3250 # }}}
3251
3252 =head2 TransactionBatch
3253
3254 Returns an array reference of all transactions created on this ticket during
3255 this ticket object's lifetime or since last application of a batch, or undef
3256 if there were none.
3257
3258 Only works when the C<UseTransactionBatch> config option is set to true.
3259
3260 =cut
3261
3262 sub TransactionBatch {
3263     my $self = shift;
3264     return $self->{_TransactionBatch};
3265 }
3266
3267 =head2 ApplyTransactionBatch
3268
3269 Applies scrips on the current batch of transactions and shinks it. Usually
3270 batch is applied when object is destroyed, but in some cases it's too late.
3271
3272 =cut
3273
3274 sub ApplyTransactionBatch {
3275     my $self = shift;
3276
3277     my $batch = $self->TransactionBatch;
3278     return unless $batch && @$batch;
3279
3280     $self->_ApplyTransactionBatch;
3281
3282     $self->{_TransactionBatch} = [];
3283 }
3284
3285 sub _ApplyTransactionBatch {
3286     my $self = shift;
3287     my $batch = $self->TransactionBatch;
3288
3289     my %seen;
3290     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3291
3292     require RT::Scrips;
3293     RT::Scrips->new($RT::SystemUser)->Apply(
3294         Stage          => 'TransactionBatch',
3295         TicketObj      => $self,
3296         TransactionObj => $batch->[0],
3297         Type           => $types,
3298     );
3299
3300     # Entry point of the rule system
3301     my $rules = RT::Ruleset->FindAllRules(
3302         Stage          => 'TransactionBatch',
3303         TicketObj      => $self,
3304         TransactionObj => $batch->[0],
3305         Type           => $types,
3306     );
3307     RT::Ruleset->CommitRules($rules);
3308 }
3309
3310 sub DESTROY {
3311     my $self = shift;
3312
3313     # DESTROY methods need to localize $@, or it may unset it.  This
3314     # causes $m->abort to not bubble all of the way up.  See perlbug
3315     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3316     local $@;
3317
3318     # The following line eliminates reentrancy.
3319     # It protects against the fact that perl doesn't deal gracefully
3320     # when an object's refcount is changed in its destructor.
3321     return if $self->{_Destroyed}++;
3322
3323     my $batch = $self->TransactionBatch;
3324     return unless $batch && @$batch;
3325
3326     return $self->_ApplyTransactionBatch;
3327 }
3328
3329 # }}}
3330
3331 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3332
3333 # {{{ sub _OverlayAccessible
3334
3335 sub _OverlayAccessible {
3336     {
3337         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3338           Queue           => { 'read' => 1,  'write' => 1 },
3339           Requestors      => { 'read' => 1,  'write' => 1 },
3340           Owner           => { 'read' => 1,  'write' => 1 },
3341           Subject         => { 'read' => 1,  'write' => 1 },
3342           InitialPriority => { 'read' => 1,  'write' => 1 },
3343           FinalPriority   => { 'read' => 1,  'write' => 1 },
3344           Priority        => { 'read' => 1,  'write' => 1 },
3345           Status          => { 'read' => 1,  'write' => 1 },
3346           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3347           TimeWorked      => { 'read' => 1,  'write' => 1 },
3348           TimeLeft        => { 'read' => 1,  'write' => 1 },
3349           Told            => { 'read' => 1,  'write' => 1 },
3350           Resolved        => { 'read' => 1 },
3351           Type            => { 'read' => 1 },
3352           Starts        => { 'read' => 1, 'write' => 1 },
3353           Started       => { 'read' => 1, 'write' => 1 },
3354           Due           => { 'read' => 1, 'write' => 1 },
3355           Creator       => { 'read' => 1, 'auto'  => 1 },
3356           Created       => { 'read' => 1, 'auto'  => 1 },
3357           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3358           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3359     };
3360
3361 }
3362
3363 # }}}
3364
3365 # {{{ sub _Set
3366
3367 sub _Set {
3368     my $self = shift;
3369
3370     my %args = ( Field             => undef,
3371                  Value             => undef,
3372                  TimeTaken         => 0,
3373                  RecordTransaction => 1,
3374                  UpdateTicket      => 1,
3375                  CheckACL          => 1,
3376                  TransactionType   => 'Set',
3377                  @_ );
3378
3379     if ($args{'CheckACL'}) {
3380       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3381           return ( 0, $self->loc("Permission Denied"));
3382       }
3383    }
3384
3385     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3386         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3387         return(0, $self->loc("Internal Error"));
3388     }
3389
3390     #if the user is trying to modify the record
3391
3392     #Take care of the old value we really don't want to get in an ACL loop.
3393     # so ask the super::_Value
3394     my $Old = $self->SUPER::_Value("$args{'Field'}");
3395     
3396     my ($ret, $msg);
3397     if ( $args{'UpdateTicket'}  ) {
3398
3399         #Set the new value
3400         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3401                                                 Value => $args{'Value'} );
3402     
3403         #If we can't actually set the field to the value, don't record
3404         # a transaction. instead, get out of here.
3405         return ( 0, $msg ) unless $ret;
3406     }
3407
3408     if ( $args{'RecordTransaction'} == 1 ) {
3409
3410         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3411                                                Type => $args{'TransactionType'},
3412                                                Field     => $args{'Field'},
3413                                                NewValue  => $args{'Value'},
3414                                                OldValue  => $Old,
3415                                                TimeTaken => $args{'TimeTaken'},
3416         );
3417         return ( $Trans, scalar $TransObj->BriefDescription );
3418     }
3419     else {
3420         return ( $ret, $msg );
3421     }
3422 }
3423
3424 # }}}
3425
3426 # {{{ sub _Value 
3427
3428 =head2 _Value
3429
3430 Takes the name of a table column.
3431 Returns its value as a string, if the user passes an ACL check
3432
3433 =cut
3434
3435 sub _Value {
3436
3437     my $self  = shift;
3438     my $field = shift;
3439
3440     #if the field is public, return it.
3441     if ( $self->_Accessible( $field, 'public' ) ) {
3442
3443         #$RT::Logger->debug("Skipping ACL check for $field");
3444         return ( $self->SUPER::_Value($field) );
3445
3446     }
3447
3448     #If the current user doesn't have ACLs, don't let em at it.  
3449
3450     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3451         return (undef);
3452     }
3453     return ( $self->SUPER::_Value($field) );
3454
3455 }
3456
3457 # }}}
3458
3459 # {{{ sub _UpdateTimeTaken
3460
3461 =head2 _UpdateTimeTaken
3462
3463 This routine will increment the timeworked counter. it should
3464 only be called from _NewTransaction 
3465
3466 =cut
3467
3468 sub _UpdateTimeTaken {
3469     my $self    = shift;
3470     my $Minutes = shift;
3471     my ($Total);
3472
3473     $Total = $self->SUPER::_Value("TimeWorked");
3474     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3475     $self->SUPER::_Set(
3476         Field => "TimeWorked",
3477         Value => $Total
3478     );
3479
3480     return ($Total);
3481 }
3482
3483 # }}}
3484
3485 # }}}
3486
3487 # {{{ Routines dealing with ACCESS CONTROL
3488
3489 # {{{ sub CurrentUserHasRight 
3490
3491 =head2 CurrentUserHasRight
3492
3493   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3494 1 if the user has that right. It returns 0 if the user doesn't have that right.
3495
3496 =cut
3497
3498 sub CurrentUserHasRight {
3499     my $self  = shift;
3500     my $right = shift;
3501
3502     return $self->CurrentUser->PrincipalObj->HasRight(
3503         Object => $self,
3504         Right  => $right,
3505     )
3506 }
3507
3508 # }}}
3509
3510 # {{{ sub HasRight 
3511
3512 =head2 HasRight
3513
3514  Takes a paramhash with the attributes 'Right' and 'Principal'
3515   'Right' is a ticket-scoped textual right from RT::ACE 
3516   'Principal' is an RT::User object
3517
3518   Returns 1 if the principal has the right. Returns undef if not.
3519
3520 =cut
3521
3522 sub HasRight {
3523     my $self = shift;
3524     my %args = (
3525         Right     => undef,
3526         Principal => undef,
3527         @_
3528     );
3529
3530     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3531     {
3532         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3533         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3534         return(undef);
3535     }
3536
3537     return (
3538         $args{'Principal'}->HasRight(
3539             Object => $self,
3540             Right     => $args{'Right'}
3541           )
3542     );
3543 }
3544
3545 # }}}
3546
3547 # }}}
3548
3549 =head2 Reminders
3550
3551 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3552 It isn't acutally a searchbuilder collection itself.
3553
3554 =cut
3555
3556 sub Reminders {
3557     my $self = shift;
3558     
3559     unless ($self->{'__reminders'}) {
3560         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3561         $self->{'__reminders'}->Ticket($self->id);
3562     }
3563     return $self->{'__reminders'};
3564
3565 }
3566
3567
3568
3569 # {{{ sub Transactions 
3570
3571 =head2 Transactions
3572
3573   Returns an RT::Transactions object of all transactions on this ticket
3574
3575 =cut
3576
3577 sub Transactions {
3578     my $self = shift;
3579
3580     my $transactions = RT::Transactions->new( $self->CurrentUser );
3581
3582     #If the user has no rights, return an empty object
3583     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3584         $transactions->LimitToTicket($self->id);
3585
3586         # if the user may not see comments do not return them
3587         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3588             $transactions->Limit(
3589                 SUBCLAUSE => 'acl',
3590                 FIELD    => 'Type',
3591                 OPERATOR => '!=',
3592                 VALUE    => "Comment"
3593             );
3594             $transactions->Limit(
3595                 SUBCLAUSE => 'acl',
3596                 FIELD    => 'Type',
3597                 OPERATOR => '!=',
3598                 VALUE    => "CommentEmailRecord",
3599                 ENTRYAGGREGATOR => 'AND'
3600             );
3601
3602         }
3603     } else {
3604         $transactions->Limit(
3605             SUBCLAUSE => 'acl',
3606             FIELD    => 'id',
3607             VALUE    => 0,
3608             ENTRYAGGREGATOR => 'AND'
3609         );
3610     }
3611
3612     return ($transactions);
3613 }
3614
3615 # }}}
3616
3617
3618 # {{{ TransactionCustomFields
3619
3620 =head2 TransactionCustomFields
3621
3622     Returns the custom fields that transactions on tickets will have.
3623
3624 =cut
3625
3626 sub TransactionCustomFields {
3627     my $self = shift;
3628     return $self->QueueObj->TicketTransactionCustomFields;
3629 }
3630
3631 # }}}
3632
3633 # {{{ sub CustomFieldValues
3634
3635 =head2 CustomFieldValues
3636
3637 # Do name => id mapping (if needed) before falling back to
3638 # RT::Record's CustomFieldValues
3639
3640 See L<RT::Record>
3641
3642 =cut
3643
3644 sub CustomFieldValues {
3645     my $self  = shift;
3646     my $field = shift;
3647
3648     return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3649
3650     my $cf = RT::CustomField->new( $self->CurrentUser );
3651     $cf->SetContextObject( $self );
3652     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3653     unless ( $cf->id ) {
3654         $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3655     }
3656
3657     # If we didn't find a valid cfid, give up.
3658     return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3659
3660     return $self->SUPER::CustomFieldValues( $cf->id );
3661 }
3662
3663 # }}}
3664
3665 # {{{ sub CustomFieldLookupType
3666
3667 =head2 CustomFieldLookupType
3668
3669 Returns the RT::Ticket lookup type, which can be passed to 
3670 RT::CustomField->Create() via the 'LookupType' hash key.
3671
3672 =cut
3673
3674 # }}}
3675
3676 sub CustomFieldLookupType {
3677     "RT::Queue-RT::Ticket";
3678 }
3679
3680 =head2 ACLEquivalenceObjects
3681
3682 This method returns a list of objects for which a user's rights also apply
3683 to this ticket. Generally, this is only the ticket's queue, but some RT 
3684 extensions may make other objects available too.
3685
3686 This method is called from L<RT::Principal/HasRight>.
3687
3688 =cut
3689
3690 sub ACLEquivalenceObjects {
3691     my $self = shift;
3692     return $self->QueueObj;
3693
3694 }
3695
3696
3697 1;
3698
3699 =head1 AUTHOR
3700
3701 Jesse Vincent, jesse@bestpractical.com
3702
3703 =head1 SEE ALSO
3704
3705 RT
3706
3707 =cut
3708