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