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