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