TimeWorked-like custom fields, RT#11168
[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 MIME::Entity;
85
86
87 # {{{ LINKTYPEMAP
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
90
91 our %LINKTYPEMAP = (
92     MemberOf => { Type => 'MemberOf',
93                   Mode => 'Target', },
94     Parents => { Type => 'MemberOf',
95          Mode => 'Target', },
96     Members => { Type => 'MemberOf',
97                  Mode => 'Base', },
98     Children => { Type => 'MemberOf',
99           Mode => 'Base', },
100     HasMember => { Type => 'MemberOf',
101                    Mode => 'Base', },
102     RefersTo => { Type => 'RefersTo',
103                   Mode => 'Target', },
104     ReferredToBy => { Type => 'RefersTo',
105                       Mode => 'Base', },
106     DependsOn => { Type => 'DependsOn',
107                    Mode => 'Target', },
108     DependedOnBy => { Type => 'DependsOn',
109                       Mode => 'Base', },
110     MergedInto => { Type => 'MergedInto',
111                    Mode => 'Target', },
112
113 );
114
115 # }}}
116
117 # {{{ LINKDIRMAP
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
120
121 our %LINKDIRMAP = (
122     MemberOf => { Base => 'MemberOf',
123                   Target => 'HasMember', },
124     RefersTo => { Base => 'RefersTo',
125                 Target => 'ReferredToBy', },
126     DependsOn => { Base => 'DependsOn',
127                    Target => 'DependedOnBy', },
128     MergedInto => { Base => 'MergedInto',
129                    Target => 'MergedInto', },
130
131 );
132
133 # }}}
134
135 sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
136 sub LINKDIRMAP   { return \%LINKDIRMAP   }
137
138 our %MERGE_CACHE = (
139     effective => {},
140     merged => {},
141 );
142
143 # {{{ sub Load
144
145 =head2 Load
146
147 Takes a single argument. This can be a ticket id, ticket alias or 
148 local ticket uri.  If the ticket can't be loaded, returns undef.
149 Otherwise, returns the ticket id.
150
151 =cut
152
153 sub Load {
154     my $self = shift;
155     my $id   = shift;
156     $id = '' unless defined $id;
157
158     # TODO: modify this routine to look at EffectiveId and
159     # do the recursive load thing. be careful to cache all
160     # the interim tickets we try so we don't loop forever.
161
162     # FIXME: there is no TicketBaseURI option in config
163     my $base_uri = RT->Config->Get('TicketBaseURI') || '';
164     #If it's a local URI, turn it into a ticket id
165     if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
166         $id = $1;
167     }
168
169     unless ( $id =~ /^\d+$/ ) {
170         $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
171         return (undef);
172     }
173
174     $id = $MERGE_CACHE{'effective'}{ $id }
175         if $MERGE_CACHE{'effective'}{ $id };
176
177     my ($ticketid, $msg) = $self->LoadById( $id );
178     unless ( $self->Id ) {
179         $RT::Logger->debug("$self tried to load a bogus ticket: $id");
180         return (undef);
181     }
182
183     #If we're merged, resolve the merge.
184     if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
185         $RT::Logger->debug(
186             "We found a merged ticket. "
187             . $self->id ."/". $self->EffectiveId
188         );
189         my $real_id = $self->Load( $self->EffectiveId );
190         $MERGE_CACHE{'effective'}{ $id } = $real_id;
191         return $real_id;
192     }
193
194     #Ok. we're loaded. lets get outa here.
195     return $self->Id;
196 }
197
198 # }}}
199
200 # {{{ sub Create
201
202 =head2 Create (ARGS)
203
204 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
205
206   id 
207   Queue  - Either a Queue object or a Queue Name
208   Requestor -  A reference to a list of  email addresses or RT user Names
209   Cc  - A reference to a list of  email addresses or Names
210   AdminCc  - A reference to a  list of  email addresses or Names
211   SquelchMailTo - A reference to a list of email addresses - 
212                   who should this ticket not mail
213   Type -- The ticket\'s type. ignore this for now
214   Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
215   Subject -- A string describing the subject of the ticket
216   Priority -- an integer from 0 to 99
217   InitialPriority -- an integer from 0 to 99
218   FinalPriority -- an integer from 0 to 99
219   Status -- any valid status (Defined in RT::Queue)
220   TimeEstimated -- an integer. estimated time for this task in minutes
221   TimeWorked -- an integer. time worked so far in minutes
222   TimeLeft -- an integer. time remaining in minutes
223   Starts -- an ISO date describing the ticket\'s start date and time in GMT
224   Due -- an ISO date describing the ticket\'s due date and time in GMT
225   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
226   CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
227
228 Ticket links can be set up during create by passing the link type as a hask key and
229 the ticket id to be linked to as a value (or a URI when linking to other objects).
230 Multiple links of the same type can be created by passing an array ref. For example:
231
232   Parents => 45,
233   DependsOn => [ 15, 22 ],
234   RefersTo => 'http://www.bestpractical.com',
235
236 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
237 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
238 C<Members> and C<Children> are aliases for C<HasMember>.
239
240 Returns: TICKETID, Transaction Object, Error Message
241
242
243 =cut
244
245 sub Create {
246     my $self = shift;
247
248     my %args = (
249         id                 => undef,
250         EffectiveId        => undef,
251         Queue              => undef,
252         Requestor          => undef,
253         Cc                 => undef,
254         AdminCc            => undef,
255         SquelchMailTo      => undef,
256         Type               => 'ticket',
257         Owner              => undef,
258         Subject            => '',
259         InitialPriority    => undef,
260         FinalPriority      => undef,
261         Priority           => undef,
262         Status             => 'new',
263         TimeWorked         => "0",
264         TimeLeft           => 0,
265         TimeEstimated      => 0,
266         Due                => undef,
267         Starts             => undef,
268         Started            => undef,
269         Resolved           => undef,
270         MIMEObj            => undef,
271         _RecordTransaction => 1,
272         DryRun             => 0,
273         @_
274     );
275
276     my ($ErrStr, @non_fatal_errors);
277
278     my $QueueObj = RT::Queue->new( $RT::SystemUser );
279     if ( ref $args{'Queue'} eq 'RT::Queue' ) {
280         $QueueObj->Load( $args{'Queue'}->Id );
281     }
282     elsif ( $args{'Queue'} ) {
283         $QueueObj->Load( $args{'Queue'} );
284     }
285     else {
286         $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
287     }
288
289     #Can't create a ticket without a queue.
290     unless ( $QueueObj->Id ) {
291         $RT::Logger->debug("$self No queue given for ticket creation.");
292         return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
293     }
294
295
296     #Now that we have a queue, Check the ACLS
297     unless (
298         $self->CurrentUser->HasRight(
299             Right  => 'CreateTicket',
300             Object => $QueueObj
301         )
302       )
303     {
304         return (
305             0, 0,
306             $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
307     }
308
309     unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
310         return ( 0, 0, $self->loc('Invalid value for status') );
311     }
312
313     #Since we have a queue, we can set queue defaults
314
315     #Initial Priority
316     # If there's no queue default initial priority and it's not set, set it to 0
317     $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
318         unless defined $args{'InitialPriority'};
319
320     #Final priority
321     # If there's no queue default final priority and it's not set, set it to 0
322     $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
323         unless defined $args{'FinalPriority'};
324
325     # Priority may have changed from InitialPriority, for the case
326     # where we're importing tickets (eg, from an older RT version.)
327     $args{'Priority'} = $args{'InitialPriority'}
328         unless defined $args{'Priority'};
329
330     # {{{ Dates
331     #TODO we should see what sort of due date we're getting, rather +
332     # than assuming it's in ISO format.
333
334     #Set the due date. if we didn't get fed one, use the queue default due in
335     my $Due = new RT::Date( $self->CurrentUser );
336     if ( defined $args{'Due'} ) {
337         $Due->Set( Format => 'ISO', Value => $args{'Due'} );
338     }
339     elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
340         $Due->SetToNow;
341         $Due->AddDays( $due_in );
342     }
343
344     my $Starts = new RT::Date( $self->CurrentUser );
345     if ( defined $args{'Starts'} ) {
346         $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
347     }
348
349     my $Started = new RT::Date( $self->CurrentUser );
350     if ( defined $args{'Started'} ) {
351         $Started->Set( Format => 'ISO', Value => $args{'Started'} );
352     }
353     elsif ( $args{'Status'} ne 'new' ) {
354         $Started->SetToNow;
355     }
356
357     my $Resolved = new RT::Date( $self->CurrentUser );
358     if ( defined $args{'Resolved'} ) {
359         $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
360     }
361
362     #If the status is an inactive status, set the resolved date
363     elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
364     {
365         $RT::Logger->debug( "Got a ". $args{'Status'}
366             ."(inactive) ticket with undefined resolved date. Setting to now."
367         );
368         $Resolved->SetToNow;
369     }
370
371     # }}}
372
373     # {{{ Dealing with time fields
374
375     $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
376     $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
377     $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
378
379     # }}}
380
381     # {{{ Deal with setting the owner
382
383     my $Owner;
384     if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
385         if ( $args{'Owner'}->id ) {
386             $Owner = $args{'Owner'};
387         } else {
388             $RT::Logger->error('passed not loaded owner object');
389             push @non_fatal_errors, $self->loc("Invalid owner object");
390             $Owner = undef;
391         }
392     }
393
394     #If we've been handed something else, try to load the user.
395     elsif ( $args{'Owner'} ) {
396         $Owner = RT::User->new( $self->CurrentUser );
397         $Owner->Load( $args{'Owner'} );
398         $Owner->LoadByEmail( $args{'Owner'} )
399             unless $Owner->Id;
400         unless ( $Owner->Id ) {
401             push @non_fatal_errors,
402                 $self->loc("Owner could not be set.") . " "
403               . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
404             $Owner = undef;
405         }
406     }
407
408     #If we have a proposed owner and they don't have the right
409     #to own a ticket, scream about it and make them not the owner
410    
411     my $DeferOwner;  
412     if ( $Owner && $Owner->Id != $RT::Nobody->Id 
413         && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
414     {
415         $DeferOwner = $Owner;
416         $Owner = undef;
417         $RT::Logger->debug('going to deffer setting owner');
418
419     }
420
421     #If we haven't been handed a valid owner, make it nobody.
422     unless ( defined($Owner) && $Owner->Id ) {
423         $Owner = new RT::User( $self->CurrentUser );
424         $Owner->Load( $RT::Nobody->Id );
425     }
426
427     # }}}
428
429 # We attempt to load or create each of the people who might have a role for this ticket
430 # _outside_ the transaction, so we don't get into ticket creation races
431     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
432         $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
433         foreach my $watcher ( splice @{ $args{$type} } ) {
434             next unless $watcher;
435             if ( $watcher =~ /^\d+$/ ) {
436                 push @{ $args{$type} }, $watcher;
437             } else {
438                 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
439                 foreach my $address( @addresses ) {
440                     my $user = RT::User->new( $RT::SystemUser );
441                     my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
442                     unless ( $uid ) {
443                         push @non_fatal_errors,
444                             $self->loc("Couldn't load or create user: [_1]", $msg);
445                     } else {
446                         push @{ $args{$type} }, $user->id;
447                     }
448                 }
449             }
450         }
451     }
452
453     $RT::Handle->BeginTransaction();
454
455     my %params = (
456         Queue           => $QueueObj->Id,
457         Owner           => $Owner->Id,
458         Subject         => $args{'Subject'},
459         InitialPriority => $args{'InitialPriority'},
460         FinalPriority   => $args{'FinalPriority'},
461         Priority        => $args{'Priority'},
462         Status          => $args{'Status'},
463         TimeWorked      => $args{'TimeWorked'},
464         TimeEstimated   => $args{'TimeEstimated'},
465         TimeLeft        => $args{'TimeLeft'},
466         Type            => $args{'Type'},
467         Starts          => $Starts->ISO,
468         Started         => $Started->ISO,
469         Resolved        => $Resolved->ISO,
470         Due             => $Due->ISO
471     );
472
473 # Parameters passed in during an import that we probably don't want to touch, otherwise
474     foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
475         $params{$attr} = $args{$attr} if $args{$attr};
476     }
477
478     # Delete null integer parameters
479     foreach my $attr
480         qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
481     {
482         delete $params{$attr}
483           unless ( exists $params{$attr} && $params{$attr} );
484     }
485
486     # Delete the time worked if we're counting it in the transaction
487     delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
488
489     my ($id,$ticket_message) = $self->SUPER::Create( %params );
490     unless ($id) {
491         $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
492         $RT::Handle->Rollback();
493         return ( 0, 0,
494             $self->loc("Ticket could not be created due to an internal error")
495         );
496     }
497
498     #Set the ticket's effective ID now that we've created it.
499     my ( $val, $msg ) = $self->__Set(
500         Field => 'EffectiveId',
501         Value => ( $args{'EffectiveId'} || $id )
502     );
503     unless ( $val ) {
504         $RT::Logger->crit("Couldn't set EffectiveId: $msg");
505         $RT::Handle->Rollback;
506         return ( 0, 0,
507             $self->loc("Ticket could not be created due to an internal error")
508         );
509     }
510
511     my $create_groups_ret = $self->_CreateTicketGroups();
512     unless ($create_groups_ret) {
513         $RT::Logger->crit( "Couldn't create ticket groups for ticket "
514               . $self->Id
515               . ". aborting Ticket creation." );
516         $RT::Handle->Rollback();
517         return ( 0, 0,
518             $self->loc("Ticket could not be created due to an internal error")
519         );
520     }
521
522     # Set the owner in the Groups table
523     # We denormalize it into the Ticket table too because doing otherwise would
524     # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
525     $self->OwnerGroup->_AddMember(
526         PrincipalId       => $Owner->PrincipalId,
527         InsideTransaction => 1
528     ) unless $DeferOwner;
529
530
531
532     # {{{ Deal with setting up watchers
533
534     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
535         # we know it's an array ref
536         foreach my $watcher ( @{ $args{$type} } ) {
537
538             # Note that we're using AddWatcher, rather than _AddWatcher, as we
539             # actually _want_ that ACL check. Otherwise, random ticket creators
540             # could make themselves adminccs and maybe get ticket rights. that would
541             # be poor
542             my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
543
544             my ($val, $msg) = $self->$method(
545                 Type   => $type,
546                 PrincipalId => $watcher,
547                 Silent => 1,
548             );
549             push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
550                 unless $val;
551         }
552     } 
553
554     if ($args{'SquelchMailTo'}) {
555        my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
556         : $args{'SquelchMailTo'};
557         $self->_SquelchMailTo( @squelch );
558     }
559
560
561     # }}}
562
563     # {{{ Add all the custom fields
564
565     foreach my $arg ( keys %args ) {
566         next unless $arg =~ /^CustomField-(\d+)$/i;
567         my $cfid = $1;
568
569         foreach my $value (
570             UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
571         {
572             next unless defined $value && length $value;
573
574             # Allow passing in uploaded LargeContent etc by hash reference
575             my ($status, $msg) = $self->_AddCustomFieldValue(
576                 (UNIVERSAL::isa( $value => 'HASH' )
577                     ? %$value
578                     : (Value => $value)
579                 ),
580                 Field             => $cfid,
581                 RecordTransaction => 0,
582             );
583             push @non_fatal_errors, $msg unless $status;
584         }
585     }
586
587     # }}}
588
589     # {{{ Deal with setting up links
590
591     # TODO: Adding link may fire scrips on other end and those scrips
592     # could create transactions on this ticket before 'Create' transaction.
593     #
594     # We should implement different schema: record 'Create' transaction,
595     # create links and only then fire create transaction's scrips.
596     #
597     # Ideal variant: add all links without firing scrips, record create
598     # transaction and only then fire scrips on the other ends of links.
599     #
600     # //RUZ
601
602     foreach my $type ( keys %LINKTYPEMAP ) {
603         next unless ( defined $args{$type} );
604         foreach my $link (
605             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
606         {
607             # Check rights on the other end of the link if we must
608             # then run _AddLink that doesn't check for ACLs
609             if ( RT->Config->Get( 'StrictLinkACL' ) ) {
610                 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
611                 unless ( $val ) {
612                     push @non_fatal_errors, $msg;
613                     next;
614                 }
615                 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
616                     push @non_fatal_errors, $self->loc('Linking. Permission denied');
617                     next;
618                 }
619             }
620
621             #don't show transactions for reminders
622             my $silent = ( !$args{'_RecordTransaction'}
623                            || $self->Type eq 'reminder'
624                          );
625
626             my ( $wval, $wmsg ) = $self->_AddLink(
627                 Type                          => $LINKTYPEMAP{$type}->{'Type'},
628                 $LINKTYPEMAP{$type}->{'Mode'} => $link,
629                 Silent                        => $silent,
630                 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
631                                               => 1,
632             );
633
634             push @non_fatal_errors, $wmsg unless ($wval);
635         }
636     }
637
638     # }}}
639
640     # {{{ Deal with auto-customer association
641
642     #unless we already have (a) customer(s)...
643     unless ( $self->Customers->Count ) {
644
645       #first find any requestors with emails but *without* customer targets
646       my @NoCust_Requestors =
647         grep { $_->EmailAddress && ! $_->Customers->Count }
648              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
649
650       for my $Requestor (@NoCust_Requestors) {
651
652          #perhaps the stuff in here should be in a User method??
653          my @Customers =
654            &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
655
656          foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
657
658            ## false laziness w/RT/Interface/Web_Vendor.pm
659            my @link = ( 'Type'   => 'MemberOf',
660                         'Target' => "freeside://freeside/cust_main/$custnum",
661                       );
662
663            my( $val, $msg ) = $Requestor->_AddLink(@link);
664            #XXX should do something with $msg# push @non_fatal_errors, $msg;
665
666          }
667
668       }
669
670       #find any requestors with customer targets
671   
672       my %cust_target = ();
673
674       my @Requestors =
675         grep { $_->Customers->Count }
676              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
677   
678       foreach my $Requestor ( @Requestors ) {
679         foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
680           $cust_target{ $cust_link->Target } = 1;
681         }
682       }
683   
684       #and then auto-associate this ticket with those customers
685   
686       foreach my $cust_target ( keys %cust_target ) {
687   
688         my @link = ( 'Type'   => 'MemberOf',
689                      #'Target' => "freeside://freeside/cust_main/$custnum",
690                      'Target' => $cust_target,
691                    );
692   
693         my( $val, $msg ) = $self->_AddLink(@link);
694         push @non_fatal_errors, $msg;
695   
696       }
697
698     }
699
700     # }}}
701
702     # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself. 
703     # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
704     if (  $DeferOwner ) { 
705             if (!$DeferOwner->HasRight( Object => $self, Right  => 'OwnTicket')) {
706     
707             $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id 
708                 . ") was proposed as a ticket owner but has no rights to own "
709                 . "tickets in " . $QueueObj->Name );
710             push @non_fatal_errors, $self->loc(
711                 "Owner '[_1]' does not have rights to own this ticket.",
712                 $DeferOwner->Name
713             );
714         } else {
715             $Owner = $DeferOwner;
716             $self->__Set(Field => 'Owner', Value => $Owner->id);
717         }
718         $self->OwnerGroup->_AddMember(
719             PrincipalId       => $Owner->PrincipalId,
720             InsideTransaction => 1
721         );
722     }
723
724     #don't make a transaction or fire off any scrips for reminders either
725     if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
726
727         # {{{ Add a transaction for the create
728         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
729             Type         => "Create",
730             TimeTaken    => $args{'TimeWorked'},
731             MIMEObj      => $args{'MIMEObj'},
732             CommitScrips => !$args{'DryRun'},
733         );
734
735         if ( $self->Id && $Trans ) {
736
737           #$TransObj->UpdateCustomFields(ARGSRef => \%args);
738             $TransObj->UpdateCustomFields(%args);
739
740             $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
741             $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
742             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
743         }
744         else {
745             $RT::Handle->Rollback();
746
747             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
748             $RT::Logger->error("Ticket couldn't be created: $ErrStr");
749             return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
750         }
751
752         if ( $args{'DryRun'} ) {
753             $RT::Handle->Rollback();
754             return ($self->id, $TransObj, $ErrStr);
755         }
756         $RT::Handle->Commit();
757         return ( $self->Id, $TransObj->Id, $ErrStr );
758
759         # }}}
760     }
761     else {
762
763         # Not going to record a transaction
764         $RT::Handle->Commit();
765         $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
766         $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
767         return ( $self->Id, 0, $ErrStr );
768
769     }
770 }
771
772
773 # }}}
774
775 # {{{ _Parse822HeadersForAttributes Content
776
777 =head2 _Parse822HeadersForAttributes Content
778
779 Takes an RFC822 style message and parses its attributes into a hash.
780
781 =cut
782
783 sub _Parse822HeadersForAttributes {
784     my $self    = shift;
785     my $content = shift;
786     my %args;
787
788     my @lines = ( split ( /\n/, $content ) );
789     while ( defined( my $line = shift @lines ) ) {
790         if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
791             my $value = $2;
792             my $tag   = lc($1);
793
794             $tag =~ s/-//g;
795             if ( defined( $args{$tag} ) )
796             {    #if we're about to get a second value, make it an array
797                 $args{$tag} = [ $args{$tag} ];
798             }
799             if ( ref( $args{$tag} ) )
800             {    #If it's an array, we want to push the value
801                 push @{ $args{$tag} }, $value;
802             }
803             else {    #if there's nothing there, just set the value
804                 $args{$tag} = $value;
805             }
806         } elsif ($line =~ /^$/) {
807
808             #TODO: this won't work, since "" isn't of the form "foo:value"
809
810                 while ( defined( my $l = shift @lines ) ) {
811                     push @{ $args{'content'} }, $l;
812                 }
813             }
814         
815     }
816
817     foreach my $date qw(due starts started resolved) {
818         my $dateobj = RT::Date->new($RT::SystemUser);
819         if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
820             $dateobj->Set( Format => 'unix', Value => $args{$date} );
821         }
822         else {
823             $dateobj->Set( Format => 'unknown', Value => $args{$date} );
824         }
825         $args{$date} = $dateobj->ISO;
826     }
827     $args{'mimeobj'} = MIME::Entity->new();
828     $args{'mimeobj'}->build(
829         Type => ( $args{'contenttype'} || 'text/plain' ),
830         Data => ($args{'content'} || '')
831     );
832
833     return (%args);
834 }
835
836 # }}}
837
838 # {{{ sub Import
839
840 =head2 Import PARAMHASH
841
842 Import a ticket. 
843 Doesn\'t create a transaction. 
844 Doesn\'t supply queue defaults, etc.
845
846 Returns: TICKETID
847
848 =cut
849
850 sub Import {
851     my $self = shift;
852     my ( $ErrStr, $QueueObj, $Owner );
853
854     my %args = (
855         id              => undef,
856         EffectiveId     => undef,
857         Queue           => undef,
858         Requestor       => undef,
859         Type            => 'ticket',
860         Owner           => $RT::Nobody->Id,
861         Subject         => '[no subject]',
862         InitialPriority => undef,
863         FinalPriority   => undef,
864         Status          => 'new',
865         TimeWorked      => "0",
866         Due             => undef,
867         Created         => undef,
868         Updated         => undef,
869         Resolved        => undef,
870         Told            => undef,
871         @_
872     );
873
874     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
875         $QueueObj = RT::Queue->new($RT::SystemUser);
876         $QueueObj->Load( $args{'Queue'} );
877
878         #TODO error check this and return 0 if it\'s not loading properly +++
879     }
880     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
881         $QueueObj = RT::Queue->new($RT::SystemUser);
882         $QueueObj->Load( $args{'Queue'}->Id );
883     }
884     else {
885         $RT::Logger->debug(
886             "$self " . $args{'Queue'} . " not a recognised queue object." );
887     }
888
889     #Can't create a ticket without a queue.
890     unless ( defined($QueueObj) and $QueueObj->Id ) {
891         $RT::Logger->debug("$self No queue given for ticket creation.");
892         return ( 0, $self->loc('Could not create ticket. Queue not set') );
893     }
894
895     #Now that we have a queue, Check the ACLS
896     unless (
897         $self->CurrentUser->HasRight(
898             Right    => 'CreateTicket',
899             Object => $QueueObj
900         )
901       )
902     {
903         return ( 0,
904             $self->loc("No permission to create tickets in the queue '[_1]'"
905               , $QueueObj->Name));
906     }
907
908     # {{{ Deal with setting the owner
909
910     # Attempt to take user object, user name or user id.
911     # Assign to nobody if lookup fails.
912     if ( defined( $args{'Owner'} ) ) {
913         if ( ref( $args{'Owner'} ) ) {
914             $Owner = $args{'Owner'};
915         }
916         else {
917             $Owner = new RT::User( $self->CurrentUser );
918             $Owner->Load( $args{'Owner'} );
919             if ( !defined( $Owner->id ) ) {
920                 $Owner->Load( $RT::Nobody->id );
921             }
922         }
923     }
924
925     #If we have a proposed owner and they don't have the right 
926     #to own a ticket, scream about it and make them not the owner
927     if (
928         ( defined($Owner) )
929         and ( $Owner->Id != $RT::Nobody->Id )
930         and (
931             !$Owner->HasRight(
932                 Object => $QueueObj,
933                 Right    => 'OwnTicket'
934             )
935         )
936       )
937     {
938
939         $RT::Logger->warning( "$self user "
940               . $Owner->Name . "("
941               . $Owner->id
942               . ") was proposed "
943               . "as a ticket owner but has no rights to own "
944               . "tickets in '"
945               . $QueueObj->Name . "'" );
946
947         $Owner = undef;
948     }
949
950     #If we haven't been handed a valid owner, make it nobody.
951     unless ( defined($Owner) ) {
952         $Owner = new RT::User( $self->CurrentUser );
953         $Owner->Load( $RT::Nobody->UserObj->Id );
954     }
955
956     # }}}
957
958     unless ( $self->ValidateStatus( $args{'Status'} ) ) {
959         return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
960     }
961
962     $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
963     $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
964     $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
965     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
966
967     # If we're coming in with an id, set that now.
968     my $EffectiveId = undef;
969     if ( $args{'id'} ) {
970         $EffectiveId = $args{'id'};
971
972     }
973
974     my $id = $self->SUPER::Create(
975         id              => $args{'id'},
976         EffectiveId     => $EffectiveId,
977         Queue           => $QueueObj->Id,
978         Owner           => $Owner->Id,
979         Subject         => $args{'Subject'},        # loc
980         InitialPriority => $args{'InitialPriority'},    # loc
981         FinalPriority   => $args{'FinalPriority'},    # loc
982         Priority        => $args{'InitialPriority'},    # loc
983         Status          => $args{'Status'},        # loc
984         TimeWorked      => $args{'TimeWorked'},        # loc
985         Type            => $args{'Type'},        # loc
986         Created         => $args{'Created'},        # loc
987         Told            => $args{'Told'},        # loc
988         LastUpdated     => $args{'Updated'},        # loc
989         Resolved        => $args{'Resolved'},        # loc
990         Due             => $args{'Due'},        # loc
991     );
992
993     # If the ticket didn't have an id
994     # Set the ticket's effective ID now that we've created it.
995     if ( $args{'id'} ) {
996         $self->Load( $args{'id'} );
997     }
998     else {
999         my ( $val, $msg ) =
1000           $self->__Set( Field => 'EffectiveId', Value => $id );
1001
1002         unless ($val) {
1003             $RT::Logger->err(
1004                 $self . "->Import couldn't set EffectiveId: $msg" );
1005         }
1006     }
1007
1008     my $create_groups_ret = $self->_CreateTicketGroups();
1009     unless ($create_groups_ret) {
1010         $RT::Logger->crit(
1011             "Couldn't create ticket groups for ticket " . $self->Id );
1012     }
1013
1014     $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1015
1016     my $watcher;
1017     foreach $watcher ( @{ $args{'Cc'} } ) {
1018         $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1019     }
1020     foreach $watcher ( @{ $args{'AdminCc'} } ) {
1021         $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1022             Silent => 1 );
1023     }
1024     foreach $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->warn("$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 );
2315         return $links;
2316     }
2317
2318     # without this you will also get RT::User(s) instead of tickets!
2319     if ($field == 'Base' and $type == 'MemberOf') {
2320         my $rtname = RT->Config->Get('rtname');
2321         $links->Limit(
2322                        FIELD    => 'Base',
2323                        OPERATOR => 'STARTSWITH',
2324                        VALUE    => "fsck.com-rt://$rtname/ticket/",
2325                      );
2326     }
2327
2328     # Maybe this ticket is a merge ticket
2329     my $limit_on = 'Local'. $field;
2330     # at least to myself
2331     $links->Limit(
2332         FIELD           => $limit_on,
2333         VALUE           => $self->id,
2334         ENTRYAGGREGATOR => 'OR',
2335     );
2336     $links->Limit(
2337         FIELD           => $limit_on,
2338         VALUE           => $_,
2339         ENTRYAGGREGATOR => 'OR',
2340     ) foreach $self->Merged;
2341     $links->Limit(
2342         FIELD => 'Type',
2343         VALUE => $type,
2344     ) if $type;
2345
2346     return $links;
2347 }
2348
2349 # }}}
2350
2351 # {{{ sub DeleteLink 
2352
2353 =head2 DeleteLink
2354
2355 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2356 SilentBase and SilentTarget. Either Base or Target must be null.
2357 The null value will be replaced with this ticket\'s id.
2358
2359 If Silent is true then no transaction would be recorded, in other
2360 case you can control creation of transactions on both base and
2361 target with SilentBase and SilentTarget respectively. By default
2362 both transactions are created.
2363
2364 =cut 
2365
2366 sub DeleteLink {
2367     my $self = shift;
2368     my %args = (
2369         Base   => undef,
2370         Target => undef,
2371         Type   => undef,
2372         Silent => undef,
2373         SilentBase   => undef,
2374         SilentTarget => undef,
2375         @_
2376     );
2377
2378     unless ( $args{'Target'} || $args{'Base'} ) {
2379         $RT::Logger->error("Base or Target must be specified");
2380         return ( 0, $self->loc('Either base or target must be specified') );
2381     }
2382
2383     #check acls
2384     my $right = 0;
2385     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2386     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2387         return ( 0, $self->loc("Permission Denied") );
2388     }
2389
2390     # If the other URI is an RT::Ticket, we want to make sure the user
2391     # can modify it too...
2392     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2393     return (0, $msg) unless $status;
2394     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2395         $right++;
2396     }
2397     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2398          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2399     {
2400         return ( 0, $self->loc("Permission Denied") );
2401     }
2402
2403     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2404     return ( 0, $Msg ) unless $val;
2405
2406     return ( $val, $Msg ) if $args{'Silent'};
2407
2408     my ($direction, $remote_link);
2409
2410     if ( $args{'Base'} ) {
2411         $remote_link = $args{'Base'};
2412         $direction = 'Target';
2413     }
2414     elsif ( $args{'Target'} ) {
2415         $remote_link = $args{'Target'};
2416         $direction = 'Base';
2417     } 
2418
2419     my $remote_uri = RT::URI->new( $self->CurrentUser );
2420     $remote_uri->FromURI( $remote_link );
2421
2422     unless ( $args{ 'Silent'. $direction } ) {
2423         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2424             Type      => 'DeleteLink',
2425             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2426             OldValue  => $remote_uri->URI || $remote_link,
2427             TimeTaken => 0
2428         );
2429         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2430     }
2431
2432     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2433         my $OtherObj = $remote_uri->Object;
2434         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2435             Type           => 'DeleteLink',
2436             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2437                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2438             OldValue       => $self->URI,
2439             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2440             TimeTaken      => 0,
2441         );
2442         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2443     }
2444
2445     return ( $val, $Msg );
2446 }
2447
2448 # }}}
2449
2450 # {{{ sub AddLink
2451
2452 =head2 AddLink
2453
2454 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2455
2456 If Silent is true then no transaction would be recorded, in other
2457 case you can control creation of transactions on both base and
2458 target with SilentBase and SilentTarget respectively. By default
2459 both transactions are created.
2460
2461 =cut
2462
2463 sub AddLink {
2464     my $self = shift;
2465     my %args = ( Target       => '',
2466                  Base         => '',
2467                  Type         => '',
2468                  Silent       => undef,
2469                  SilentBase   => undef,
2470                  SilentTarget => undef,
2471                  @_ );
2472
2473     unless ( $args{'Target'} || $args{'Base'} ) {
2474         $RT::Logger->error("Base or Target must be specified");
2475         return ( 0, $self->loc('Either base or target must be specified') );
2476     }
2477
2478     my $right = 0;
2479     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2480     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2481         return ( 0, $self->loc("Permission Denied") );
2482     }
2483
2484     # If the other URI is an RT::Ticket, we want to make sure the user
2485     # can modify it too...
2486     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2487     return (0, $msg) unless $status;
2488     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2489         $right++;
2490     }
2491     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2492          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2493     {
2494         return ( 0, $self->loc("Permission Denied") );
2495     }
2496
2497     return $self->_AddLink(%args);
2498 }
2499
2500 sub __GetTicketFromURI {
2501     my $self = shift;
2502     my %args = ( URI => '', @_ );
2503
2504     # If the other URI is an RT::Ticket, we want to make sure the user
2505     # can modify it too...
2506     my $uri_obj = RT::URI->new( $self->CurrentUser );
2507     $uri_obj->FromURI( $args{'URI'} );
2508
2509     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2510         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2511         $RT::Logger->warning( $msg );
2512         return( 0, $msg );
2513     }
2514     my $obj = $uri_obj->Resolver->Object;
2515     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2516         return (1, 'Found not a ticket', undef);
2517     }
2518     return (1, 'Found ticket', $obj);
2519 }
2520
2521 =head2 _AddLink  
2522
2523 Private non-acled variant of AddLink so that links can be added during create.
2524
2525 =cut
2526
2527 sub _AddLink {
2528     my $self = shift;
2529     my %args = ( Target       => '',
2530                  Base         => '',
2531                  Type         => '',
2532                  Silent       => undef,
2533                  SilentBase   => undef,
2534                  SilentTarget => undef,
2535                  @_ );
2536
2537     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2538     return ($val, $msg) if !$val || $exist;
2539     return ($val, $msg) if $args{'Silent'};
2540
2541     my ($direction, $remote_link);
2542     if ( $args{'Target'} ) {
2543         $remote_link  = $args{'Target'};
2544         $direction    = 'Base';
2545     } elsif ( $args{'Base'} ) {
2546         $remote_link  = $args{'Base'};
2547         $direction    = 'Target';
2548     }
2549
2550     my $remote_uri = RT::URI->new( $self->CurrentUser );
2551     $remote_uri->FromURI( $remote_link );
2552
2553     unless ( $args{ 'Silent'. $direction } ) {
2554         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2555             Type      => 'AddLink',
2556             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2557             NewValue  =>  $remote_uri->URI || $remote_link,
2558             TimeTaken => 0
2559         );
2560         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2561     }
2562
2563     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2564         my $OtherObj = $remote_uri->Object;
2565         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2566             Type           => 'AddLink',
2567             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2568                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2569             NewValue       => $self->URI,
2570             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2571             TimeTaken      => 0,
2572         );
2573         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2574     }
2575
2576     return ( $val, $msg );
2577 }
2578
2579 # }}}
2580
2581
2582 # {{{ sub MergeInto
2583
2584 =head2 MergeInto
2585
2586 MergeInto take the id of the ticket to merge this ticket into.
2587
2588 =cut
2589
2590 sub MergeInto {
2591     my $self      = shift;
2592     my $ticket_id = shift;
2593
2594     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2595         return ( 0, $self->loc("Permission Denied") );
2596     }
2597
2598     # Load up the new ticket.
2599     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2600     $MergeInto->Load($ticket_id);
2601
2602     # make sure it exists.
2603     unless ( $MergeInto->Id ) {
2604         return ( 0, $self->loc("New ticket doesn't exist") );
2605     }
2606
2607     # Make sure the current user can modify the new ticket.
2608     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2609         return ( 0, $self->loc("Permission Denied") );
2610     }
2611
2612     delete $MERGE_CACHE{'effective'}{ $self->id };
2613     delete @{ $MERGE_CACHE{'merged'} }{
2614         $ticket_id, $MergeInto->id, $self->id
2615     };
2616
2617     $RT::Handle->BeginTransaction();
2618
2619     # We use EffectiveId here even though it duplicates information from
2620     # the links table becasue of the massive performance hit we'd take
2621     # by trying to do a separate database query for merge info everytime 
2622     # loaded a ticket. 
2623
2624     #update this ticket's effective id to the new ticket's id.
2625     my ( $id_val, $id_msg ) = $self->__Set(
2626         Field => 'EffectiveId',
2627         Value => $MergeInto->Id()
2628     );
2629
2630     unless ($id_val) {
2631         $RT::Handle->Rollback();
2632         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2633     }
2634
2635
2636     if ( $self->__Value('Status') ne 'resolved' ) {
2637
2638         my ( $status_val, $status_msg )
2639             = $self->__Set( Field => 'Status', Value => 'resolved' );
2640
2641         unless ($status_val) {
2642             $RT::Handle->Rollback();
2643             $RT::Logger->error(
2644                 $self->loc(
2645                     "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2646                     $self
2647                 )
2648             );
2649             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2650         }
2651     }
2652
2653     # update all the links that point to that old ticket
2654     my $old_links_to = RT::Links->new($self->CurrentUser);
2655     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2656
2657     my %old_seen;
2658     while (my $link = $old_links_to->Next) {
2659         if (exists $old_seen{$link->Base."-".$link->Type}) {
2660             $link->Delete;
2661         }   
2662         elsif ($link->Base eq $MergeInto->URI) {
2663             $link->Delete;
2664         } else {
2665             # First, make sure the link doesn't already exist. then move it over.
2666             my $tmp = RT::Link->new($RT::SystemUser);
2667             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2668             if ($tmp->id)   {
2669                     $link->Delete;
2670             } else { 
2671                 $link->SetTarget($MergeInto->URI);
2672                 $link->SetLocalTarget($MergeInto->id);
2673             }
2674             $old_seen{$link->Base."-".$link->Type} =1;
2675         }
2676
2677     }
2678
2679     my $old_links_from = RT::Links->new($self->CurrentUser);
2680     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2681
2682     while (my $link = $old_links_from->Next) {
2683         if (exists $old_seen{$link->Type."-".$link->Target}) {
2684             $link->Delete;
2685         }   
2686         if ($link->Target eq $MergeInto->URI) {
2687             $link->Delete;
2688         } else {
2689             # First, make sure the link doesn't already exist. then move it over.
2690             my $tmp = RT::Link->new($RT::SystemUser);
2691             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2692             if ($tmp->id)   {
2693                     $link->Delete;
2694             } else { 
2695                 $link->SetBase($MergeInto->URI);
2696                 $link->SetLocalBase($MergeInto->id);
2697                 $old_seen{$link->Type."-".$link->Target} =1;
2698             }
2699         }
2700
2701     }
2702
2703     # Update time fields
2704     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2705
2706         my $mutator = "Set$type";
2707         $MergeInto->$mutator(
2708             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2709
2710     }
2711 #add all of this ticket's watchers to that ticket.
2712     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2713
2714         my $people = $self->$watcher_type->MembersObj;
2715         my $addwatcher_type =  $watcher_type;
2716         $addwatcher_type  =~ s/s$//;
2717
2718         while ( my $watcher = $people->Next ) {
2719             
2720            my ($val, $msg) =  $MergeInto->_AddWatcher(
2721                 Type        => $addwatcher_type,
2722                 Silent => 1,
2723                 PrincipalId => $watcher->MemberId
2724             );
2725             unless ($val) {
2726                 $RT::Logger->warning($msg);
2727             }
2728     }
2729
2730     }
2731
2732     #find all of the tickets that were merged into this ticket. 
2733     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2734     $old_mergees->Limit(
2735         FIELD    => 'EffectiveId',
2736         OPERATOR => '=',
2737         VALUE    => $self->Id
2738     );
2739
2740     #   update their EffectiveId fields to the new ticket's id
2741     while ( my $ticket = $old_mergees->Next() ) {
2742         my ( $val, $msg ) = $ticket->__Set(
2743             Field => 'EffectiveId',
2744             Value => $MergeInto->Id()
2745         );
2746     }
2747
2748     #make a new link: this ticket is merged into that other ticket.
2749     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2750
2751     $MergeInto->_SetLastUpdated;    
2752
2753     $RT::Handle->Commit();
2754     return ( 1, $self->loc("Merge Successful") );
2755 }
2756
2757 =head2 Merged
2758
2759 Returns list of tickets' ids that's been merged into this ticket.
2760
2761 =cut
2762
2763 sub Merged {
2764     my $self = shift;
2765
2766     my $id = $self->id;
2767     return @{ $MERGE_CACHE{'merged'}{ $id } }
2768         if $MERGE_CACHE{'merged'}{ $id };
2769
2770     my $mergees = RT::Tickets->new( $self->CurrentUser );
2771     $mergees->Limit(
2772         FIELD    => 'EffectiveId',
2773         VALUE    => $id,
2774     );
2775     $mergees->Limit(
2776         FIELD    => 'id',
2777         OPERATOR => '!=',
2778         VALUE    => $id,
2779     );
2780     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2781         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2782 }
2783
2784 # }}}
2785
2786 # }}}
2787
2788 # {{{ Routines dealing with ownership
2789
2790 # {{{ sub OwnerObj
2791
2792 =head2 OwnerObj
2793
2794 Takes nothing and returns an RT::User object of 
2795 this ticket's owner
2796
2797 =cut
2798
2799 sub OwnerObj {
2800     my $self = shift;
2801
2802     #If this gets ACLed, we lose on a rights check in User.pm and
2803     #get deep recursion. if we need ACLs here, we need
2804     #an equiv without ACLs
2805
2806     my $owner = new RT::User( $self->CurrentUser );
2807     $owner->Load( $self->__Value('Owner') );
2808
2809     #Return the owner object
2810     return ($owner);
2811 }
2812
2813 # }}}
2814
2815 # {{{ sub OwnerAsString 
2816
2817 =head2 OwnerAsString
2818
2819 Returns the owner's email address
2820
2821 =cut
2822
2823 sub OwnerAsString {
2824     my $self = shift;
2825     return ( $self->OwnerObj->EmailAddress );
2826
2827 }
2828
2829 # }}}
2830
2831 # {{{ sub SetOwner
2832
2833 =head2 SetOwner
2834
2835 Takes two arguments:
2836      the Id or Name of the owner 
2837 and  (optionally) the type of the SetOwner Transaction. It defaults
2838 to 'Give'.  'Steal' is also a valid option.
2839
2840
2841 =cut
2842
2843 sub SetOwner {
2844     my $self     = shift;
2845     my $NewOwner = shift;
2846     my $Type     = shift || "Give";
2847
2848     $RT::Handle->BeginTransaction();
2849
2850     $self->_SetLastUpdated(); # lock the ticket
2851     $self->Load( $self->id ); # in case $self changed while waiting for lock
2852
2853     my $OldOwnerObj = $self->OwnerObj;
2854
2855     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2856     $NewOwnerObj->Load( $NewOwner );
2857     unless ( $NewOwnerObj->Id ) {
2858         $RT::Handle->Rollback();
2859         return ( 0, $self->loc("That user does not exist") );
2860     }
2861
2862
2863     # must have ModifyTicket rights
2864     # or TakeTicket/StealTicket and $NewOwner is self
2865     # see if it's a take
2866     if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2867         unless (    $self->CurrentUserHasRight('ModifyTicket')
2868                  || $self->CurrentUserHasRight('TakeTicket') ) {
2869             $RT::Handle->Rollback();
2870             return ( 0, $self->loc("Permission Denied") );
2871         }
2872     }
2873
2874     # see if it's a steal
2875     elsif (    $OldOwnerObj->Id != $RT::Nobody->Id
2876             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2877
2878         unless (    $self->CurrentUserHasRight('ModifyTicket')
2879                  || $self->CurrentUserHasRight('StealTicket') ) {
2880             $RT::Handle->Rollback();
2881             return ( 0, $self->loc("Permission Denied") );
2882         }
2883     }
2884     else {
2885         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2886             $RT::Handle->Rollback();
2887             return ( 0, $self->loc("Permission Denied") );
2888         }
2889     }
2890
2891     # If we're not stealing and the ticket has an owner and it's not
2892     # the current user
2893     if ( $Type ne 'Steal' and $Type ne 'Force'
2894          and $OldOwnerObj->Id != $RT::Nobody->Id
2895          and $OldOwnerObj->Id != $self->CurrentUser->Id )
2896     {
2897         $RT::Handle->Rollback();
2898         return ( 0, $self->loc("You can only take tickets that are unowned") )
2899             if $NewOwnerObj->id == $self->CurrentUser->id;
2900         return (
2901             0,
2902             $self->loc("You can only reassign tickets that you own or that are unowned" )
2903         );
2904     }
2905
2906     #If we've specified a new owner and that user can't modify the ticket
2907     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2908         $RT::Handle->Rollback();
2909         return ( 0, $self->loc("That user may not own tickets in that queue") );
2910     }
2911
2912     # If the ticket has an owner and it's the new owner, we don't need
2913     # To do anything
2914     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2915         $RT::Handle->Rollback();
2916         return ( 0, $self->loc("That user already owns that ticket") );
2917     }
2918
2919     # Delete the owner in the owner group, then add a new one
2920     # TODO: is this safe? it's not how we really want the API to work
2921     # for most things, but it's fast.
2922     my ( $del_id, $del_msg );
2923     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2924         ($del_id, $del_msg) = $owner->Delete();
2925         last unless ($del_id);
2926     }
2927
2928     unless ($del_id) {
2929         $RT::Handle->Rollback();
2930         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2931     }
2932
2933     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2934                                        PrincipalId => $NewOwnerObj->PrincipalId,
2935                                        InsideTransaction => 1 );
2936     unless ($add_id) {
2937         $RT::Handle->Rollback();
2938         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2939     }
2940
2941     # We call set twice with slightly different arguments, so
2942     # as to not have an SQL transaction span two RT transactions
2943
2944     my ( $val, $msg ) = $self->_Set(
2945                       Field             => 'Owner',
2946                       RecordTransaction => 0,
2947                       Value             => $NewOwnerObj->Id,
2948                       TimeTaken         => 0,
2949                       TransactionType   => $Type,
2950                       CheckACL          => 0,                  # don't check acl
2951     );
2952
2953     unless ($val) {
2954         $RT::Handle->Rollback;
2955         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2956     }
2957
2958     ($val, $msg) = $self->_NewTransaction(
2959         Type      => $Type,
2960         Field     => 'Owner',
2961         NewValue  => $NewOwnerObj->Id,
2962         OldValue  => $OldOwnerObj->Id,
2963         TimeTaken => 0,
2964     );
2965
2966     if ( $val ) {
2967         $msg = $self->loc( "Owner changed from [_1] to [_2]",
2968                            $OldOwnerObj->Name, $NewOwnerObj->Name );
2969     }
2970     else {
2971         $RT::Handle->Rollback();
2972         return ( 0, $msg );
2973     }
2974
2975     $RT::Handle->Commit();
2976
2977     return ( $val, $msg );
2978 }
2979
2980 # }}}
2981
2982 # {{{ sub Take
2983
2984 =head2 Take
2985
2986 A convenince method to set the ticket's owner to the current user
2987
2988 =cut
2989
2990 sub Take {
2991     my $self = shift;
2992     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2993 }
2994
2995 # }}}
2996
2997 # {{{ sub Untake
2998
2999 =head2 Untake
3000
3001 Convenience method to set the owner to 'nobody' if the current user is the owner.
3002
3003 =cut
3004
3005 sub Untake {
3006     my $self = shift;
3007     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3008 }
3009
3010 # }}}
3011
3012 # {{{ sub Steal 
3013
3014 =head2 Steal
3015
3016 A convenience method to change the owner of the current ticket to the
3017 current user. Even if it's owned by another user.
3018
3019 =cut
3020
3021 sub Steal {
3022     my $self = shift;
3023
3024     if ( $self->IsOwner( $self->CurrentUser ) ) {
3025         return ( 0, $self->loc("You already own this ticket") );
3026     }
3027     else {
3028         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3029
3030     }
3031
3032 }
3033
3034 # }}}
3035
3036 # }}}
3037
3038 # {{{ Routines dealing with status
3039
3040 # {{{ sub ValidateStatus 
3041
3042 =head2 ValidateStatus STATUS
3043
3044 Takes a string. Returns true if that status is a valid status for this ticket.
3045 Returns false otherwise.
3046
3047 =cut
3048
3049 sub ValidateStatus {
3050     my $self   = shift;
3051     my $status = shift;
3052
3053     #Make sure the status passed in is valid
3054     unless ( $self->QueueObj->IsValidStatus($status) ) {
3055         return (undef);
3056     }
3057
3058     return (1);
3059
3060 }
3061
3062 # }}}
3063
3064 # {{{ sub SetStatus
3065
3066 =head2 SetStatus STATUS
3067
3068 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3069
3070 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.
3071
3072
3073
3074 =cut
3075
3076 sub SetStatus {
3077     my $self   = shift;
3078     my %args;
3079
3080     if (@_ == 1) {
3081     $args{Status} = shift;
3082     }
3083     else {
3084     %args = (@_);
3085     }
3086
3087     #Check ACL
3088     if ( $args{Status} eq 'deleted') {
3089             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3090             return ( 0, $self->loc('Permission Denied') );
3091        }
3092     } else {
3093             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3094             return ( 0, $self->loc('Permission Denied') );
3095        }
3096     }
3097
3098     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3099         return (0, $self->loc('That ticket has unresolved dependencies'));
3100     }
3101
3102     my $now = RT::Date->new( $self->CurrentUser );
3103     $now->SetToNow();
3104
3105     #If we're changing the status from new, record that we've started
3106     if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3107
3108         #Set the Started time to "now"
3109         $self->_Set( Field             => 'Started',
3110                      Value             => $now->ISO,
3111                      RecordTransaction => 0 );
3112     }
3113
3114     #When we close a ticket, set the 'Resolved' attribute to now.
3115     # It's misnamed, but that's just historical.
3116     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3117         $self->_Set( Field             => 'Resolved',
3118                      Value             => $now->ISO,
3119                      RecordTransaction => 0 );
3120     }
3121
3122     #Actually update the status
3123    my ($val, $msg)= $self->_Set( Field           => 'Status',
3124                           Value           => $args{Status},
3125                           TimeTaken       => 0,
3126                           CheckACL      => 0,
3127                           TransactionType => 'Status'  );
3128
3129     return($val,$msg);
3130 }
3131
3132 # }}}
3133
3134 # {{{ sub Delete
3135
3136 =head2 Delete
3137
3138 Takes no arguments. Marks this ticket for garbage collection
3139
3140 =cut
3141
3142 sub Delete {
3143     my $self = shift;
3144     return ( $self->SetStatus('deleted') );
3145
3146     # TODO: garbage collection
3147 }
3148
3149 # }}}
3150
3151 # {{{ sub Stall
3152
3153 =head2 Stall
3154
3155 Sets this ticket's status to stalled
3156
3157 =cut
3158
3159 sub Stall {
3160     my $self = shift;
3161     return ( $self->SetStatus('stalled') );
3162 }
3163
3164 # }}}
3165
3166 # {{{ sub Reject
3167
3168 =head2 Reject
3169
3170 Sets this ticket's status to rejected
3171
3172 =cut
3173
3174 sub Reject {
3175     my $self = shift;
3176     return ( $self->SetStatus('rejected') );
3177 }
3178
3179 # }}}
3180
3181 # {{{ sub Open
3182
3183 =head2 Open
3184
3185 Sets this ticket\'s status to Open
3186
3187 =cut
3188
3189 sub Open {
3190     my $self = shift;
3191     return ( $self->SetStatus('open') );
3192 }
3193
3194 # }}}
3195
3196 # {{{ sub Resolve
3197
3198 =head2 Resolve
3199
3200 Sets this ticket\'s status to Resolved
3201
3202 =cut
3203
3204 sub Resolve {
3205     my $self = shift;
3206     return ( $self->SetStatus('resolved') );
3207 }
3208
3209 # }}}
3210
3211 # }}}
3212
3213     
3214 # {{{ Actions + Routines dealing with transactions
3215
3216 # {{{ sub SetTold and _SetTold
3217
3218 =head2 SetTold ISO  [TIMETAKEN]
3219
3220 Updates the told and records a transaction
3221
3222 =cut
3223
3224 sub SetTold {
3225     my $self = shift;
3226     my $told;
3227     $told = shift if (@_);
3228     my $timetaken = shift || 0;
3229
3230     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3231         return ( 0, $self->loc("Permission Denied") );
3232     }
3233
3234     my $datetold = new RT::Date( $self->CurrentUser );
3235     if ($told) {
3236         $datetold->Set( Format => 'iso',
3237                         Value  => $told );
3238     }
3239     else {
3240         $datetold->SetToNow();
3241     }
3242
3243     return ( $self->_Set( Field           => 'Told',
3244                           Value           => $datetold->ISO,
3245                           TimeTaken       => $timetaken,
3246                           TransactionType => 'Told' ) );
3247 }
3248
3249 =head2 _SetTold
3250
3251 Updates the told without a transaction or acl check. Useful when we're sending replies.
3252
3253 =cut
3254
3255 sub _SetTold {
3256     my $self = shift;
3257
3258     my $now = new RT::Date( $self->CurrentUser );
3259     $now->SetToNow();
3260
3261     #use __Set to get no ACLs ;)
3262     return ( $self->__Set( Field => 'Told',
3263                            Value => $now->ISO ) );
3264 }
3265
3266 =head2 SeenUpTo
3267
3268
3269 =cut
3270
3271 sub SeenUpTo {
3272     my $self = shift;
3273     my $uid = $self->CurrentUser->id;
3274     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3275     return if $attr && $attr->Content gt $self->LastUpdated;
3276
3277     my $txns = $self->Transactions;
3278     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3279     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3280     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3281     $txns->Limit(
3282         FIELD => 'Created',
3283         OPERATOR => '>',
3284         VALUE => $attr->Content
3285     ) if $attr;
3286     $txns->RowsPerPage(1);
3287     return $txns->First;
3288 }
3289
3290 # }}}
3291
3292 =head2 TransactionBatch
3293
3294 Returns an array reference of all transactions created on this ticket during
3295 this ticket object's lifetime or since last application of a batch, or undef
3296 if there were none.
3297
3298 Only works when the C<UseTransactionBatch> config option is set to true.
3299
3300 =cut
3301
3302 sub TransactionBatch {
3303     my $self = shift;
3304     return $self->{_TransactionBatch};
3305 }
3306
3307 =head2 ApplyTransactionBatch
3308
3309 Applies scrips on the current batch of transactions and shinks it. Usually
3310 batch is applied when object is destroyed, but in some cases it's too late.
3311
3312 =cut
3313
3314 sub ApplyTransactionBatch {
3315     my $self = shift;
3316
3317     my $batch = $self->TransactionBatch;
3318     return unless $batch && @$batch;
3319
3320     $self->_ApplyTransactionBatch;
3321
3322     $self->{_TransactionBatch} = [];
3323 }
3324
3325 sub _ApplyTransactionBatch {
3326     my $self = shift;
3327     my $batch = $self->TransactionBatch;
3328
3329     my %seen;
3330     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3331
3332     require RT::Scrips;
3333     RT::Scrips->new($RT::SystemUser)->Apply(
3334         Stage          => 'TransactionBatch',
3335         TicketObj      => $self,
3336         TransactionObj => $batch->[0],
3337         Type           => $types,
3338     );
3339
3340     # Entry point of the rule system
3341     my $rules = RT::Ruleset->FindAllRules(
3342         Stage          => 'TransactionBatch',
3343         TicketObj      => $self,
3344         TransactionObj => $batch->[0],
3345         Type           => $types,
3346     );
3347     RT::Ruleset->CommitRules($rules);
3348 }
3349
3350 sub DESTROY {
3351     my $self = shift;
3352
3353     # DESTROY methods need to localize $@, or it may unset it.  This
3354     # causes $m->abort to not bubble all of the way up.  See perlbug
3355     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3356     local $@;
3357
3358     # The following line eliminates reentrancy.
3359     # It protects against the fact that perl doesn't deal gracefully
3360     # when an object's refcount is changed in its destructor.
3361     return if $self->{_Destroyed}++;
3362
3363     my $batch = $self->TransactionBatch;
3364     return unless $batch && @$batch;
3365
3366     return $self->_ApplyTransactionBatch;
3367 }
3368
3369 # }}}
3370
3371 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3372
3373 # {{{ sub _OverlayAccessible
3374
3375 sub _OverlayAccessible {
3376     {
3377         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3378           Queue           => { 'read' => 1,  'write' => 1 },
3379           Requestors      => { 'read' => 1,  'write' => 1 },
3380           Owner           => { 'read' => 1,  'write' => 1 },
3381           Subject         => { 'read' => 1,  'write' => 1 },
3382           InitialPriority => { 'read' => 1,  'write' => 1 },
3383           FinalPriority   => { 'read' => 1,  'write' => 1 },
3384           Priority        => { 'read' => 1,  'write' => 1 },
3385           Status          => { 'read' => 1,  'write' => 1 },
3386           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3387           TimeWorked      => { 'read' => 1,  'write' => 1 },
3388           TimeLeft        => { 'read' => 1,  'write' => 1 },
3389           Told            => { 'read' => 1,  'write' => 1 },
3390           Resolved        => { 'read' => 1 },
3391           Type            => { 'read' => 1 },
3392           Starts        => { 'read' => 1, 'write' => 1 },
3393           Started       => { 'read' => 1, 'write' => 1 },
3394           Due           => { 'read' => 1, 'write' => 1 },
3395           Creator       => { 'read' => 1, 'auto'  => 1 },
3396           Created       => { 'read' => 1, 'auto'  => 1 },
3397           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3398           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3399     };
3400
3401 }
3402
3403 # }}}
3404
3405 # {{{ sub _Set
3406
3407 sub _Set {
3408     my $self = shift;
3409
3410     my %args = ( Field             => undef,
3411                  Value             => undef,
3412                  TimeTaken         => 0,
3413                  RecordTransaction => 1,
3414                  UpdateTicket      => 1,
3415                  CheckACL          => 1,
3416                  TransactionType   => 'Set',
3417                  @_ );
3418
3419     if ($args{'CheckACL'}) {
3420       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3421           return ( 0, $self->loc("Permission Denied"));
3422       }
3423    }
3424
3425     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3426         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3427         return(0, $self->loc("Internal Error"));
3428     }
3429
3430     #if the user is trying to modify the record
3431
3432     #Take care of the old value we really don't want to get in an ACL loop.
3433     # so ask the super::_Value
3434     my $Old = $self->SUPER::_Value("$args{'Field'}");
3435     
3436     my ($ret, $msg);
3437     if ( $args{'UpdateTicket'}  ) {
3438
3439         #Set the new value
3440         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3441                                                 Value => $args{'Value'} );
3442     
3443         #If we can't actually set the field to the value, don't record
3444         # a transaction. instead, get out of here.
3445         return ( 0, $msg ) unless $ret;
3446     }
3447
3448     if ( $args{'RecordTransaction'} == 1 ) {
3449
3450         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3451                                                Type => $args{'TransactionType'},
3452                                                Field     => $args{'Field'},
3453                                                NewValue  => $args{'Value'},
3454                                                OldValue  => $Old,
3455                                                TimeTaken => $args{'TimeTaken'},
3456         );
3457         return ( $Trans, scalar $TransObj->BriefDescription );
3458     }
3459     else {
3460         return ( $ret, $msg );
3461     }
3462 }
3463
3464 # }}}
3465
3466 # {{{ sub _Value 
3467
3468 =head2 _Value
3469
3470 Takes the name of a table column.
3471 Returns its value as a string, if the user passes an ACL check
3472
3473 =cut
3474
3475 sub _Value {
3476
3477     my $self  = shift;
3478     my $field = shift;
3479
3480     #if the field is public, return it.
3481     if ( $self->_Accessible( $field, 'public' ) ) {
3482
3483         #$RT::Logger->debug("Skipping ACL check for $field");
3484         return ( $self->SUPER::_Value($field) );
3485
3486     }
3487
3488     #If the current user doesn't have ACLs, don't let em at it.  
3489
3490     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3491         return (undef);
3492     }
3493     return ( $self->SUPER::_Value($field) );
3494
3495 }
3496
3497 # }}}
3498
3499 # {{{ sub _UpdateTimeTaken
3500
3501 =head2 _UpdateTimeTaken
3502
3503 This routine will increment the timeworked counter. it should
3504 only be called from _NewTransaction 
3505
3506 =cut
3507
3508 sub _UpdateTimeTaken {
3509     my $self    = shift;
3510     my $Minutes = shift;
3511     my ($Total);
3512
3513     $Total = $self->SUPER::_Value("TimeWorked");
3514     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3515     $self->SUPER::_Set(
3516         Field => "TimeWorked",
3517         Value => $Total
3518     );
3519
3520     return ($Total);
3521 }
3522
3523 # }}}
3524
3525 # }}}
3526
3527 # {{{ Routines dealing with ACCESS CONTROL
3528
3529 # {{{ sub CurrentUserHasRight 
3530
3531 =head2 CurrentUserHasRight
3532
3533   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3534 1 if the user has that right. It returns 0 if the user doesn't have that right.
3535
3536 =cut
3537
3538 sub CurrentUserHasRight {
3539     my $self  = shift;
3540     my $right = shift;
3541
3542     return $self->CurrentUser->PrincipalObj->HasRight(
3543         Object => $self,
3544         Right  => $right,
3545     )
3546 }
3547
3548 # }}}
3549
3550 # {{{ sub HasRight 
3551
3552 =head2 HasRight
3553
3554  Takes a paramhash with the attributes 'Right' and 'Principal'
3555   'Right' is a ticket-scoped textual right from RT::ACE 
3556   'Principal' is an RT::User object
3557
3558   Returns 1 if the principal has the right. Returns undef if not.
3559
3560 =cut
3561
3562 sub HasRight {
3563     my $self = shift;
3564     my %args = (
3565         Right     => undef,
3566         Principal => undef,
3567         @_
3568     );
3569
3570     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3571     {
3572         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3573         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3574         return(undef);
3575     }
3576
3577     return (
3578         $args{'Principal'}->HasRight(
3579             Object => $self,
3580             Right     => $args{'Right'}
3581           )
3582     );
3583 }
3584
3585 # }}}
3586
3587 # }}}
3588
3589 =head2 Reminders
3590
3591 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3592 It isn't acutally a searchbuilder collection itself.
3593
3594 =cut
3595
3596 sub Reminders {
3597     my $self = shift;
3598     
3599     unless ($self->{'__reminders'}) {
3600         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3601         $self->{'__reminders'}->Ticket($self->id);
3602     }
3603     return $self->{'__reminders'};
3604
3605 }
3606
3607
3608
3609 # {{{ sub Transactions 
3610
3611 =head2 Transactions
3612
3613   Returns an RT::Transactions object of all transactions on this ticket
3614
3615 =cut
3616
3617 sub Transactions {
3618     my $self = shift;
3619
3620     my $transactions = RT::Transactions->new( $self->CurrentUser );
3621
3622     #If the user has no rights, return an empty object
3623     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3624         $transactions->LimitToTicket($self->id);
3625
3626         # if the user may not see comments do not return them
3627         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3628             $transactions->Limit(
3629                 SUBCLAUSE => 'acl',
3630                 FIELD    => 'Type',
3631                 OPERATOR => '!=',
3632                 VALUE    => "Comment"
3633             );
3634             $transactions->Limit(
3635                 SUBCLAUSE => 'acl',
3636                 FIELD    => 'Type',
3637                 OPERATOR => '!=',
3638                 VALUE    => "CommentEmailRecord",
3639                 ENTRYAGGREGATOR => 'AND'
3640             );
3641
3642         }
3643     } else {
3644         $transactions->Limit(
3645             SUBCLAUSE => 'acl',
3646             FIELD    => 'id',
3647             VALUE    => 0,
3648             ENTRYAGGREGATOR => 'AND'
3649         );
3650     }
3651
3652     return ($transactions);
3653 }
3654
3655 # }}}
3656
3657
3658 # {{{ TransactionCustomFields
3659
3660 =head2 TransactionCustomFields
3661
3662     Returns the custom fields that transactions on tickets will have.
3663
3664 =cut
3665
3666 sub TransactionCustomFields {
3667     my $self = shift;
3668     return $self->QueueObj->TicketTransactionCustomFields;
3669 }
3670
3671 # }}}
3672
3673 # {{{ sub CustomFieldValues
3674
3675 =head2 CustomFieldValues
3676
3677 # Do name => id mapping (if needed) before falling back to
3678 # RT::Record's CustomFieldValues
3679
3680 See L<RT::Record>
3681
3682 =cut
3683
3684 sub CustomFieldValues {
3685     my $self  = shift;
3686     my $field = shift;
3687
3688     return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3689
3690     my $cf = RT::CustomField->new( $self->CurrentUser );
3691     $cf->SetContextObject( $self );
3692     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3693     unless ( $cf->id ) {
3694         $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3695     }
3696
3697     # If we didn't find a valid cfid, give up.
3698     return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3699
3700     return $self->SUPER::CustomFieldValues( $cf->id );
3701 }
3702
3703 # }}}
3704
3705 # {{{ sub CustomFieldLookupType
3706
3707 =head2 CustomFieldLookupType
3708
3709 Returns the RT::Ticket lookup type, which can be passed to 
3710 RT::CustomField->Create() via the 'LookupType' hash key.
3711
3712 =cut
3713
3714 # }}}
3715
3716 sub CustomFieldLookupType {
3717     "RT::Queue-RT::Ticket";
3718 }
3719
3720 =head2 ACLEquivalenceObjects
3721
3722 This method returns a list of objects for which a user's rights also apply
3723 to this ticket. Generally, this is only the ticket's queue, but some RT 
3724 extensions may make other objects available too.
3725
3726 This method is called from L<RT::Principal/HasRight>.
3727
3728 =cut
3729
3730 sub ACLEquivalenceObjects {
3731     my $self = shift;
3732     return $self->QueueObj;
3733
3734 }
3735
3736
3737 1;
3738
3739 =head1 AUTHOR
3740
3741 Jesse Vincent, jesse@bestpractical.com
3742
3743 =head1 SEE ALSO
3744
3745 RT
3746
3747 =cut
3748