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