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