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