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