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