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