rt 4.0.23
[freeside.git] / rt / lib / RT / Ticket.pm
index f7275e4..068eec0 100755 (executable)
-# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
-# (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
-# This software is redistributable under the terms of the GNU GPL
+# BEGIN BPS TAGGED BLOCK {{{
 #
-
-=head1 NAME
-
-  RT::Ticket - RT ticket object
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
 
 =head1 SYNOPSIS
 
   use RT::Ticket;
-  my $ticket = new RT::Ticket($CurrentUser);
+  my $ticket = RT::Ticket->new($CurrentUser);
   $ticket->Load($ticket_id);
 
 =head1 DESCRIPTION
 
-This module lets you manipulate RT\'s ticket object.
+This module lets you manipulate RT's ticket object.
 
 
 =head1 METHODS
 
-=cut
 
+=cut
 
 
 package RT::Ticket;
+
+use strict;
+use warnings;
+
+
 use RT::Queue;
 use RT::User;
 use RT::Record;
-use RT::Link;
 use RT::Links;
 use RT::Date;
-use RT::Watcher;
-
-
-@ISA= qw(RT::Record);
-
-
-=begin testing
-
-use RT::TestHarness;
-
-ok(require RT::Ticket, "Loading the RT::Ticket library");
-
-=end testing
-
-=cut
-
-# {{{ sub _Init
-
-sub _Init {
-    my $self = shift;
-    $self->{'table'} = "Tickets";
-    return ($self->SUPER::_Init(@_));
-}
+use RT::CustomFields;
+use RT::Tickets;
+use RT::Transactions;
+use RT::Reminders;
+use RT::URI::fsck_com_rt;
+use RT::URI;
+use RT::URI::freeside;
+use MIME::Entity;
+use Devel::GlobalDestruction;
+
+
+# A helper table for links mapping to make it easier
+# to build and parse links between tickets
+
+our %LINKTYPEMAP = (
+    MemberOf => { Type => 'MemberOf',
+                  Mode => 'Target', },
+    Parents => { Type => 'MemberOf',
+         Mode => 'Target', },
+    Members => { Type => 'MemberOf',
+                 Mode => 'Base', },
+    Children => { Type => 'MemberOf',
+          Mode => 'Base', },
+    HasMember => { Type => 'MemberOf',
+                   Mode => 'Base', },
+    RefersTo => { Type => 'RefersTo',
+                  Mode => 'Target', },
+    ReferredToBy => { Type => 'RefersTo',
+                      Mode => 'Base', },
+    DependsOn => { Type => 'DependsOn',
+                   Mode => 'Target', },
+    DependedOnBy => { Type => 'DependsOn',
+                      Mode => 'Base', },
+    MergedInto => { Type => 'MergedInto',
+                   Mode => 'Target', },
+
+);
+
+
+# A helper table for links mapping to make it easier
+# to build and parse links between tickets
+
+our %LINKDIRMAP = (
+    MemberOf => { Base => 'MemberOf',
+                  Target => 'HasMember', },
+    RefersTo => { Base => 'RefersTo',
+                Target => 'ReferredToBy', },
+    DependsOn => { Base => 'DependsOn',
+                   Target => 'DependedOnBy', },
+    MergedInto => { Base => 'MergedInto',
+                   Target => 'MergedInto', },
+
+);
+
+
+sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
+sub LINKDIRMAP   { return \%LINKDIRMAP   }
+
+our %MERGE_CACHE = (
+    effective => {},
+    merged => {},
+);
 
-# }}}
-
-# {{{ sub Load
 
 =head2 Load
 
@@ -68,2937 +147,4236 @@ Otherwise, returns the ticket id.
 =cut
 
 sub Load {
-   my $self = shift;
-   my $id = shift;
-
-   #TODO modify this routine to look at EffectiveId and do the recursive load
-   # thing. be careful to cache all the interim tickets we try so we don't loop forever.
-   
-   #If it's a local URI, turn it into a ticket id
-   if ($id =~ /^$RT::TicketBaseURI(\d+)$/)  {
-       $id = $1;
-   }
-   #If it's a remote URI, we're going to punt for now
-   elsif ($id =~ '://' ) {
-       return (undef);
-   }
-   
-   #If we have an integer URI, load the ticket
-   if ( $id =~ /^\d+$/ ) {
-       my $ticketid = $self->LoadById($id);
-   
-       unless ($ticketid) {
-          $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
-          return(undef);
-       }
-   }
-   
-   #It's not a URI. It's not a numerical ticket ID. Punt!
-   else {
-       return(undef);
-   }
-   
-   #If we're merged, resolve the merge.
-   if (($self->EffectiveId) and
-       ($self->EffectiveId != $self->Id)) {
-          return ($self->Load($self->EffectiveId));
-       }
-
-   #Ok. we're loaded. lets get outa here.
-   return ($self->Id);
-   
-}
-
-# }}}
-
-# {{{ sub LoadByURI
+    my $self = shift;
+    my $id   = shift;
+    $id = '' unless defined $id;
 
-=head2 LoadByURI
+    # TODO: modify this routine to look at EffectiveId and
+    # do the recursive load thing. be careful to cache all
+    # the interim tickets we try so we don't loop forever.
 
-Given a local ticket URI, loads the specified ticket.
+    unless ( $id =~ /^\d+$/ ) {
+        $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
+        return (undef);
+    }
 
-=cut
+    $id = $MERGE_CACHE{'effective'}{ $id }
+        if $MERGE_CACHE{'effective'}{ $id };
 
-sub LoadByURI {
-    my $self = shift;
-    my $uri = shift;
-    
-    if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) {
-        my $id = $1;
-        return ($self->Load($id));
+    my ($ticketid, $msg) = $self->LoadById( $id );
+    unless ( $self->Id ) {
+        $RT::Logger->debug("$self tried to load a bogus ticket: $id");
+        return (undef);
     }
-    else {
-        return(undef);
+
+    #If we're merged, resolve the merge.
+    if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
+        $RT::Logger->debug(
+            "We found a merged ticket. "
+            . $self->id ."/". $self->EffectiveId
+        );
+        my $real_id = $self->Load( $self->EffectiveId );
+        $MERGE_CACHE{'effective'}{ $id } = $real_id;
+        return $real_id;
     }
+
+    #Ok. we're loaded. lets get outa here.
+    return $self->Id;
 }
 
-# }}}
 
-# {{{ sub Create
 
 =head2 Create (ARGS)
 
 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
 
+  id 
   Queue  - Either a Queue object or a Queue Name
-  Requestor -  A reference to a list of RT::User objects, email addresses or RT user Names
-  Cc  - A reference to a list of RT::User objects, email addresses or Names
-  AdminCc  - A reference to a  list of RT::User objects, email addresses or Names
-  Type -- The ticket\'s type. ignore this for now
-  Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
+  Requestor -  A reference to a list of  email addresses or RT user Names
+  Cc  - A reference to a list of  email addresses or Names
+  AdminCc  - A reference to a  list of  email addresses or Names
+  SquelchMailTo - A reference to a list of email addresses - 
+                  who should this ticket not mail
+  Type -- The ticket's type. ignore this for now
+  Owner -- This ticket's owner. either an RT::User object or this user's id
   Subject -- A string describing the subject of the ticket
+  Priority -- an integer from 0 to 99
   InitialPriority -- an integer from 0 to 99
   FinalPriority -- an integer from 0 to 99
-  Status -- any valid status (Defined in RT::Queue)
-  TimeWorked -- an integer
-  TimeLeft -- an integer
-  Starts -- an ISO date describing the ticket\'s start date and time in GMT
-  Due -- an ISO date describing the ticket\'s due date and time in GMT
+  Status -- any valid status for Queue's Lifecycle, otherwises uses on_create from Lifecycle default
+  TimeEstimated -- an integer. estimated time for this task in minutes
+  TimeWorked -- an integer. time worked so far in minutes
+  TimeLeft -- an integer. time remaining in minutes
+  Starts -- an ISO date describing the ticket's start date and time in GMT
+  Due -- an ISO date describing the ticket's due date and time in GMT
   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
+  CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
 
-  KeywordSelect-<id> -- an array of keyword ids for that keyword select
+Ticket links can be set up during create by passing the link type as a hask key and
+the ticket id to be linked to as a value (or a URI when linking to other objects).
+Multiple links of the same type can be created by passing an array ref. For example:
 
+  Parents => 45,
+  DependsOn => [ 15, 22 ],
+  RefersTo => 'http://www.bestpractical.com',
 
-Returns: TICKETID, Transaction Object, Error Message
-
-
-=begin testing
-
-my $t = RT::Ticket->new($RT::SystemUser);
+Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
+C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
+C<Members> and C<Children> are aliases for C<HasMember>.
 
-ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created");
-
-ok ( my $id = $t->Id, "Got ticket id");
+Returns: TICKETID, Transaction Object, Error Message
 
-=end testing
 
 =cut
 
 sub Create {
     my $self = shift;
-    
-    my %args = (
-               Queue => undef,
-               Requestor => undef,
-               Cc => undef,
-               AdminCc => undef,
-               Type => 'ticket',
-               Owner => $RT::Nobody->UserObj,
-               Subject => '[no subject]',
-               InitialPriority => undef,
-               FinalPriority => undef,
-               Status => 'new',
-               TimeWorked => "0",
-               TimeLeft => 0,
-               Due => undef,
-               Starts => undef,
-               MIMEObj => undef,
-               @_);
-
-    my ($ErrStr, $QueueObj, $Owner, $resolved);
-    my (@non_fatal_errors);
-    
-    my $now = RT::Date->new($self->CurrentUser);
-    $now->SetToNow();
 
-    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
-       $QueueObj=RT::Queue->new($RT::SystemUser);
-       $QueueObj->Load($args{'Queue'});
+    my %args = (
+        id                 => undef,
+        EffectiveId        => undef,
+        Queue              => undef,
+        Requestor          => undef,
+        Cc                 => undef,
+        AdminCc            => undef,
+        SquelchMailTo      => undef,
+        TransSquelchMailTo => undef,
+        Type               => 'ticket',
+        Owner              => undef,
+        Subject            => '',
+        InitialPriority    => undef,
+        FinalPriority      => undef,
+        Priority           => undef,
+        Status             => undef,
+        TimeWorked         => "0",
+        TimeLeft           => 0,
+        TimeEstimated      => 0,
+        Due                => undef,
+        Starts             => undef,
+        Started            => undef,
+        Resolved           => undef,
+        WillResolve        => undef,
+        MIMEObj            => undef,
+        _RecordTransaction => 1,
+        DryRun             => 0,
+        @_
+    );
+
+    my ($ErrStr, @non_fatal_errors);
+
+    my $QueueObj = RT::Queue->new( RT->SystemUser );
+    if ( ref $args{'Queue'} eq 'RT::Queue' ) {
+        $QueueObj->Load( $args{'Queue'}->Id );
     }
-    elsif (ref($args{'Queue'}) eq 'RT::Queue') {
-       $QueueObj=RT::Queue->new($RT::SystemUser);
-       $QueueObj->Load($args{'Queue'}->Id);
+    elsif ( $args{'Queue'} ) {
+        $QueueObj->Load( $args{'Queue'} );
     }
     else {
-       $RT::Logger->debug("$self ". $args{'Queue'} . 
-                        " not a recognised queue object.");
+        $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
     }
-  
+
     #Can't create a ticket without a queue.
-    unless (defined ($QueueObj)) {
-       $RT::Logger->debug( "$self No queue given for ticket creation.");
-       return (0, 0,'Could not create ticket. Queue not set');
+    unless ( $QueueObj->Id ) {
+        $RT::Logger->debug("$self No queue given for ticket creation.");
+        return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
     }
-    
+
+
     #Now that we have a queue, Check the ACLS
-    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
-                                             QueueObj => $QueueObj )) {
-       return (0,0,"No permission to create tickets in the queue '". 
-               $QueueObj->Name."'.");
+    unless (
+        $self->CurrentUser->HasRight(
+            Right  => 'CreateTicket',
+            Object => $QueueObj
+        )
+      )
+    {
+        return (
+            0, 0,
+            $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
     }
-    
+
+    my $cycle = $QueueObj->Lifecycle;
+    unless ( defined $args{'Status'} && length $args{'Status'} ) {
+        $args{'Status'} = $cycle->DefaultOnCreate;
+    }
+
+    $args{'Status'} = lc $args{'Status'};
+    unless ( $cycle->IsValid( $args{'Status'} ) ) {
+        return ( 0, 0,
+            $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
+                $self->loc($args{'Status'}))
+        );
+    }
+
+    unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
+        return ( 0, 0,
+            $self->loc("New tickets can not have status '[_1]' in this queue.",
+                $self->loc($args{'Status'}))
+        );
+    }
+
+
+
     #Since we have a queue, we can set queue defaults
-    #Initial Priority
 
+    #Initial Priority
     # If there's no queue default initial priority and it's not set, set it to 0
-    $args{'InitialPriority'} = ($QueueObj->InitialPriority || 0)
-      unless (defined $args{'InitialPriority'});
-       
-    #Final priority 
+    $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
+        unless defined $args{'InitialPriority'};
 
+    #Final priority
     # If there's no queue default final priority and it's not set, set it to 0
-    $args{'FinalPriority'} = ($QueueObj->FinalPriority  || 0)
-      unless (defined $args{'FinalPriority'});
-    
-    
+    $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
+        unless defined $args{'FinalPriority'};
+
+    # Priority may have changed from InitialPriority, for the case
+    # where we're importing tickets (eg, from an older RT version.)
+    $args{'Priority'} = $args{'InitialPriority'}
+        unless defined $args{'Priority'};
+
+    # Dates
     #TODO we should see what sort of due date we're getting, rather +
     # than assuming it's in ISO format.
-    
-    #Set the due date. if we didn't get fed one, use the queue default due in
-    my $due = new RT::Date($self->CurrentUser);
-    if (defined $args{'Due'}) {
-       $due->Set (Format => 'ISO',
-                  Value => $args{'Due'});
-    }  
-    elsif (defined ($QueueObj->DefaultDueIn)) {
-       $due->SetToNow;
-       $due->AddDays($QueueObj->DefaultDueIn);
-    }  
-    
-    my $starts = new RT::Date($self->CurrentUser);
-    if (defined $args{'Starts'}) {
-       $starts->Set (Format => 'ISO',
-                  Value => $args{'Starts'});
-    }
 
-       
-    # {{{ Deal with setting the owner
-    
-    if (ref($args{'Owner'}) eq 'RT::User') {
-       $Owner = $args{'Owner'};
-    }
-    #If we've been handed something else, try to load the user.
-    elsif ($args{'Owner'}) {
-       $Owner = new RT::User($self->CurrentUser);
-       $Owner->Load($args{'Owner'});
-       
+    #Set the due date. if we didn't get fed one, use the queue default due in
+    my $Due = RT::Date->new( $self->CurrentUser );
+    if ( defined $args{'Due'} ) {
+        $Due->Set( Format => 'ISO', Value => $args{'Due'} );
     }
-    #If we can't handle it, call it nobody
-    else {
-       if (ref($args{'Owner'})) {
-           $RT::Logger->warning("$ticket ->Create called with an Owner of ".
-                "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n");
-
-           push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
-       }
-       else { 
-           $RT::Logger->warning("$self ->Create called with an ".
-                                "unknown datatype for Owner: ".$args{'Owner'} .
-                                ". Defaulting to Nobody.\n");
-       }
+    elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
+        $Due->SetToNow;
+        $Due->AddDays( $due_in );
     }
-    
-    #If we have a proposed owner and they don't have the right 
-    #to own a ticket, scream about it and make them not the owner
-    if ((defined ($Owner)) and
-       ($Owner->Id != $RT::Nobody->Id) and 
-       (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
-                                Right => 'OwnTicket'))) {
-       
-       $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
-                            ") was proposed ".
-                            "as a ticket owner but has no rights to own ".
-                            "tickets in this queue\n");
-
-       push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
 
-       $Owner = undef;
+    my $Starts = RT::Date->new( $self->CurrentUser );
+    if ( defined $args{'Starts'} ) {
+        $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
     }
-    
-    #If we haven't been handed a valid owner, make it nobody.
-    unless (defined ($Owner)) {
-       $Owner = new RT::User($self->CurrentUser);
-       $Owner->Load($RT::Nobody->UserObj->Id);
-    }  
 
-    # }}}
-
-    unless ($self->ValidateStatus($args{'Status'})) {
-       return (0,0,'Invalid value for status');
+    my $Started = RT::Date->new( $self->CurrentUser );
+    if ( defined $args{'Started'} ) {
+        $Started->Set( Format => 'ISO', Value => $args{'Started'} );
     }
 
-    if ($args{'Status'} eq 'resolved') {
-       $resolved = $now->ISO;
-    } else{
-       $resolved = undef;
+    my $WillResolve = RT::Date->new($self->CurrentUser );
+    if ( defined $args{'WillResolve'} ) {
+        $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
     }
 
-    my $id = $self->SUPER::Create(
-                                 Queue => $QueueObj->Id,
-                                 Owner => $Owner->Id,
-                                 Subject => $args{'Subject'},
-                                 InitialPriority => $args{'InitialPriority'},
-                                 FinalPriority => $args{'FinalPriority'},
-                                 Priority => $args{'InitialPriority'},
-                                 Status => $args{'Status'},
-                                 TimeWorked => $args{'TimeWorked'},
-                                 TimeLeft => $args{'TimeLeft'},
-                                 Type => $args{'Type'},        
-                                 Starts => $starts->ISO,
-                                 Resolved => $resolved,
-                                 Due => $due->ISO
-                                );
-    #Set the ticket's effective ID now that we've created it.
-    my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
-    
-    unless ($val) {
-       $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n");
-    }  
-     
-
-    my $watcher;
-    foreach $watcher (@{$args{'Cc'}}) {
-       my ($wval, $wmsg) = 
-         $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
-       push @non_fatal_errors, $wmsg   unless ($wval);
-    }  
-
-    foreach $watcher (@{$args{'Requestor'}}) {
-       my ($wval, $wmsg) = 
-         $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
-       push @non_fatal_errors, $wmsg   unless ($wval);
+    # If the status is not an initial status, set the started date
+    elsif ( !$cycle->IsInitial($args{'Status'}) ) {
+        $Started->SetToNow;
     }
 
-    foreach $watcher (@{$args{'AdminCc'}}) {
-       # Note that we're using AddWatcher, rather than _AddWatcher, as we 
-       # actually _want_ that ACL check. Otherwise, random ticket creators
-       # could make themselves adminccs and maybe get ticket rights. that would
-       # be poor
-       my ($wval, $wmsg) = 
-         $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
-       push @non_fatal_errors, $wmsg   unless ($wval);
+    my $Resolved = RT::Date->new( $self->CurrentUser );
+    if ( defined $args{'Resolved'} ) {
+        $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
     }
 
-    # Iterate through all the KeywordSelect-<int> params passed in, calling _AddKeyword
-    # for each of them
+    #If the status is an inactive status, set the resolved date
+    elsif ( $cycle->IsInactive( $args{'Status'} ) )
+    {
+        $RT::Logger->debug( "Got a ". $args{'Status'}
+            ."(inactive) ticket with undefined resolved date. Setting to now."
+        );
+        $Resolved->SetToNow;
+    }
 
+    # }}}
 
-    foreach my $key (keys %args) {
+    # Dealing with time fields
 
-       next unless ($key =~ /^KeywordSelect-(.*)$/);
-       
-       my $ks = $1;
+    $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
+    $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
+    $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
 
+    # }}}
 
-       my @keywords = ref($args{$key}) eq 'ARRAY' ?
-             @{$args{$key}} : ($args{$key});
-       
-       foreach my $keyword (@keywords) {  
-           my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks,
-                                                  Keyword => $keyword,
-                                                  Silent => 1);
-       }       
-       push @non_fatal_errors, $kmsg unless ($kval);
+    # Deal with setting the owner
+
+    my $Owner;
+    if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
+        if ( $args{'Owner'}->id ) {
+            $Owner = $args{'Owner'};
+        } else {
+            $RT::Logger->error('Passed an empty RT::User for owner');
+            push @non_fatal_errors,
+                $self->loc("Owner could not be set.") . " ".
+            $self->loc("Invalid value for [_1]",loc('owner'));
+            $Owner = undef;
+        }
     }
 
-    
-    
-    #Add a transaction for the create
-    my ($Trans, $Msg, $TransObj) = 
-       $self->_NewTransaction( Type => "Create",
-                               TimeTaken => 0, 
-                               MIMEObj=>$args{'MIMEObj'});
-    
-    # Logging
-    if ($self->Id && $Trans) {
-       $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name. 
-         "'.\n" . join("\n", @non_fatal_errors);
-       
-       $RT::Logger->info($ErrStr);
-    }
-    else {
-       # TODO where does this get errstr from?
-       $RT::Logger->warning("Ticket couldn't be created: $ErrStr");
+    #If we've been handed something else, try to load the user.
+    elsif ( $args{'Owner'} ) {
+        $Owner = RT::User->new( $self->CurrentUser );
+        $Owner->Load( $args{'Owner'} );
+        if (!$Owner->id) {
+            $Owner->LoadByEmail( $args{'Owner'} )
+        }
+        unless ( $Owner->Id ) {
+            push @non_fatal_errors,
+                $self->loc("Owner could not be set.") . " "
+              . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
+            $Owner = undef;
+        }
     }
-    
-    return($self->Id, $TransObj->Id, $ErrStr);
-}
 
-# }}}
+    #If we have a proposed owner and they don't have the right
+    #to own a ticket, scream about it and make them not the owner
+   
+    my $DeferOwner;  
+    if ( $Owner && $Owner->Id != RT->Nobody->Id 
+        && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
+    {
+        $DeferOwner = $Owner;
+        $Owner = undef;
+        $RT::Logger->debug('going to deffer setting owner');
 
-# {{{ sub Import
+    }
 
-=head2 Import PARAMHASH
+    #If we haven't been handed a valid owner, make it nobody.
+    unless ( defined($Owner) && $Owner->Id ) {
+        $Owner = RT::User->new( $self->CurrentUser );
+        $Owner->Load( RT->Nobody->Id );
+    }
 
-Import a ticket. 
-Doesn\'t create a transaction. 
-Doesn\'t supply queue defaults, etc.
+    # }}}
 
-Arguments are identical to Create(), with the addition of
-    Id -    Ticket Id
+# We attempt to load or create each of the people who might have a role for this ticket
+# _outside_ the transaction, so we don't get into ticket creation races
+    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
+        $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
+        foreach my $watcher ( splice @{ $args{$type} } ) {
+            next unless $watcher;
+            if ( $watcher =~ /^\d+$/ ) {
+                push @{ $args{$type} }, $watcher;
+            } else {
+                my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
+                foreach my $address( @addresses ) {
+                    my $user = RT::User->new( RT->SystemUser );
+                    my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
+                    unless ( $uid ) {
+                        push @non_fatal_errors,
+                            $self->loc("Couldn't load or create user: [_1]", $msg);
+                    } else {
+                        push @{ $args{$type} }, $user->id;
+                    }
+                }
+            }
+        }
+    }
 
-Returns: TICKETID
+    $args{'Type'} = lc $args{'Type'}
+        if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
+
+    $args{'Subject'} =~ s/\n//g;
+
+    $RT::Handle->BeginTransaction();
+
+    my %params = (
+        Queue           => $QueueObj->Id,
+        Owner           => $Owner->Id,
+        Subject         => $args{'Subject'},
+        InitialPriority => $args{'InitialPriority'},
+        FinalPriority   => $args{'FinalPriority'},
+        Priority        => $args{'Priority'},
+        Status          => $args{'Status'},
+        TimeWorked      => $args{'TimeWorked'},
+        TimeEstimated   => $args{'TimeEstimated'},
+        TimeLeft        => $args{'TimeLeft'},
+        Type            => $args{'Type'},
+        Starts          => $Starts->ISO,
+        Started         => $Started->ISO,
+        Resolved        => $Resolved->ISO,
+        WillResolve     => $WillResolve->ISO,
+        Due             => $Due->ISO
+    );
+
+# Parameters passed in during an import that we probably don't want to touch, otherwise
+    foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
+        $params{$attr} = $args{$attr} if $args{$attr};
+    }
 
-=cut
+    # Delete null integer parameters
+    foreach my $attr
+        (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
+    {
+        delete $params{$attr}
+          unless ( exists $params{$attr} && $params{$attr} );
+    }
 
+    # Delete the time worked if we're counting it in the transaction
+    delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
 
-sub Import {
-    my $self = shift;
-    my ( $ErrStr, $QueueObj, $Owner);
-    
-    my %args = (id => undef,
-               EffectiveId => undef,
-               Queue => undef,
-               Requestor => undef,
-               Type => 'ticket',
-               Owner => $RT::Nobody->Id,
-               Subject => '[no subject]',
-               InitialPriority => undef,
-               FinalPriority => undef,
-               Status => 'new',
-               TimeWorked => "0",
-               Due => undef,
-               Created => undef,
-               Updated => undef,
-       Resolved => undef,
-               Told => undef,
-               @_);
-    
-    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
-       $QueueObj=RT::Queue->new($RT::SystemUser);
-       $QueueObj->Load($args{'Queue'});
-       #TODO error check this and return 0 if it\'s not loading properly +++
-    }
-    elsif (ref($args{'Queue'}) eq 'RT::Queue') {
-       $QueueObj=RT::Queue->new($RT::SystemUser);
-       $QueueObj->Load($args{'Queue'}->Id);
+    my ($id,$ticket_message) = $self->SUPER::Create( %params );
+    unless ($id) {
+        $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
+        $RT::Handle->Rollback();
+        return ( 0, 0,
+            $self->loc("Ticket could not be created due to an internal error")
+        );
     }
-    else {
-       $RT::Logger->debug("$self ". $args{'Queue'} . 
-                          " not a recognised queue object.");
-    }
-    
-    #Can't create a ticket without a queue.
-    unless (defined ($QueueObj) and $QueueObj->Id) {
-       $RT::Logger->debug( "$self No queue given for ticket creation.");
-       return (0,'Could not create ticket. Queue not set');
+
+    #Set the ticket's effective ID now that we've created it.
+    my ( $val, $msg ) = $self->__Set(
+        Field => 'EffectiveId',
+        Value => ( $args{'EffectiveId'} || $id )
+    );
+    unless ( $val ) {
+        $RT::Logger->crit("Couldn't set EffectiveId: $msg");
+        $RT::Handle->Rollback;
+        return ( 0, 0,
+            $self->loc("Ticket could not be created due to an internal error")
+        );
     }
-    
-    #Now that we have a queue, Check the ACLS
-    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
-                                             QueueObj => $QueueObj )) {
-       return (0,"No permission to create tickets in the queue '". 
-               $QueueObj->Name."'.");
+
+    my $create_groups_ret = $self->_CreateTicketGroups();
+    unless ($create_groups_ret) {
+        $RT::Logger->crit( "Couldn't create ticket groups for ticket "
+              . $self->Id
+              . ". aborting Ticket creation." );
+        $RT::Handle->Rollback();
+        return ( 0, 0,
+            $self->loc("Ticket could not be created due to an internal error")
+        );
     }
-    
-    
 
+    # Set the owner in the Groups table
+    # We denormalize it into the Ticket table too because doing otherwise would
+    # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
+    $self->OwnerGroup->_AddMember(
+        PrincipalId       => $Owner->PrincipalId,
+        InsideTransaction => 1
+    ) unless $DeferOwner;
 
-    # {{{ Deal with setting the owner
-      
-    # Attempt to take user object, user name or user id.
-    # Assign to nobody if lookup fails.
-    if (defined ($args{'Owner'})) { 
-       if ( ref($args{'Owner'}) ) {
-           $Owner = $args{'Owner'};
-       }
-       else {
-           $Owner = new RT::User($self->CurrentUser);
-           $Owner->Load($args{'Owner'});
-           if ( ! defined($Owner->id) ) {
-               $Owner->Load($RT::Nobody->id);
-           }
-       }
-    }
-    
 
-    #If we have a proposed owner and they don't have the right 
-    #to own a ticket, scream about it and make them not the owner
-    if ((defined ($Owner)) and
-       ($Owner->Id != $RT::Nobody->Id) and 
-       (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
-                                Right => 'OwnTicket'))) {
-       
-       $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
-                            ") was proposed ".
-                            "as a ticket owner but has no rights to own ".
-                            "tickets in '".$QueueObj->Name."'\n");
-       
-       $Owner = undef;
-    }
-    
-    #If we haven't been handed a valid owner, make it nobody.
-    unless (defined ($Owner)) {
-       $Owner = new RT::User($self->CurrentUser);
-       $Owner->Load($RT::Nobody->UserObj->Id);
-    }  
 
-    # }}}
+    # Deal with setting up watchers
 
-    unless ($self->ValidateStatus($args{'Status'})) {
-       return (0,"'$args{'Status'}' is an invalid value for status");
-    }
-    
-    $self->{'_AccessibleCache'}{Created} = { 'read'=>1, 'write'=>1 };
-    $self->{'_AccessibleCache'}{Creator} = { 'read'=>1, 'auto'=>1 };
-    $self->{'_AccessibleCache'}{LastUpdated} = { 'read'=>1, 'write'=>1 };
-    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read'=>1, 'auto'=>1 };
+    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
+        # we know it's an array ref
+        foreach my $watcher ( @{ $args{$type} } ) {
 
+            # Note that we're using AddWatcher, rather than _AddWatcher, as we
+            # actually _want_ that ACL check. Otherwise, random ticket creators
+            # could make themselves adminccs and maybe get ticket rights. that would
+            # be poor
+            my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
 
-    # If we're coming in with an id, set that now.
-    my $EffectiveId = undef;
-    if ($args{'id'}) {
-       $EffectiveId = $args{'id'};
+            my ($val, $msg) = $self->$method(
+                Type   => $type,
+                PrincipalId => $watcher,
+                Silent => 1,
+            );
+            push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
+                unless $val;
+        }
+    } 
 
+    if ($args{'SquelchMailTo'}) {
+       my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
+        : $args{'SquelchMailTo'};
+        $self->_SquelchMailTo( @squelch );
     }
 
 
-    my $id = $self->SUPER::Create(
-                                 id => $args{'id'},
-                                 EffectiveId => $EffectiveId,
-                                 Queue => $QueueObj->Id,
-                                 Owner => $Owner->Id,
-                                 Subject => $args{'Subject'},
-                                 InitialPriority => $args{'InitialPriority'},
-                                 FinalPriority => $args{'FinalPriority'},
-                                 Priority => $args{'InitialPriority'},
-                                 Status => $args{'Status'},
-                                 TimeWorked => $args{'TimeWorked'},
-                                 Type => $args{'Type'},        
-                                 Created => $args{'Created'},
-                                 Told => $args{'Told'},
-                                 LastUpdated => $args{'Updated'},
-       Resolved => $args{Resolved},
-                                 Due => $args{'Due'},
-                                );
+    # }}}
 
+    # Add all the custom fields
+
+    foreach my $arg ( keys %args ) {
+        next unless $arg =~ /^CustomField-(\d+)$/i;
+        my $cfid = $1;
+
+        foreach my $value (
+            UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
+        {
+            next unless defined $value && length $value;
+
+            # Allow passing in uploaded LargeContent etc by hash reference
+            my ($status, $msg) = $self->_AddCustomFieldValue(
+                (UNIVERSAL::isa( $value => 'HASH' )
+                    ? %$value
+                    : (Value => $value)
+                ),
+                Field             => $cfid,
+                RecordTransaction => 0,
+            );
+            push @non_fatal_errors, $msg unless $status;
+        }
+    }
 
+    # }}}
 
-    # If the ticket didn't have an id
-    # Set the ticket's effective ID now that we've created it.
-    if ($args{'id'} ) { 
-         $self->Load($args{'id'});
-    }
-    else {
-          my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
-    
-          unless ($val) {
-           $RT::Logger->err($self."->Import couldn't set EffectiveId: $msg\n");
-          }    
-    } 
+    # Deal with setting up links
 
-    my $watcher;
-    foreach $watcher (@{$args{'Cc'}}) {
-       $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
-    }  
-    foreach $watcher (@{$args{'AdminCc'}}) {
-       $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
-    }  
-    foreach $watcher (@{$args{'Requestor'}}) {
-       $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
+    # TODO: Adding link may fire scrips on other end and those scrips
+    # could create transactions on this ticket before 'Create' transaction.
+    #
+    # We should implement different lifecycle: record 'Create' transaction,
+    # create links and only then fire create transaction's scrips.
+    #
+    # Ideal variant: add all links without firing scrips, record create
+    # transaction and only then fire scrips on the other ends of links.
+    #
+    # //RUZ
+
+    foreach my $type ( keys %LINKTYPEMAP ) {
+        next unless ( defined $args{$type} );
+        foreach my $link (
+            ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
+        {
+            my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
+            unless ($val) {
+                push @non_fatal_errors, $msg;
+                next;
+            }
+
+            # Check rights on the other end of the link if we must
+            # then run _AddLink that doesn't check for ACLs
+            if ( RT->Config->Get( 'StrictLinkACL' ) ) {
+                if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
+                    push @non_fatal_errors, $self->loc('Linking. Permission denied');
+                    next;
+                }
+            }
+
+            if ( $obj && lc $obj->Status eq 'deleted' ) {
+                push @non_fatal_errors,
+                  $self->loc("Linking. Can't link to a deleted ticket");
+                next;
+            }
+
+            my ( $wval, $wmsg ) = $self->_AddLink(
+                Type                          => $LINKTYPEMAP{$type}->{'Type'},
+                $LINKTYPEMAP{$type}->{'Mode'} => $link,
+                Silent                        => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
+                'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
+                                              => 1,
+            );
+
+            push @non_fatal_errors, $wmsg unless ($wval);
+        }
     }
-    
-    return($self->Id, $ErrStr);
-}
 
-# }}}
+    # }}}
 
-# {{{ sub Delete
+    # {{{ Deal with auto-customer association
 
-sub Delete {
-    my $self = shift;
-    return (0, 'Deleting this object would violate referential integrity.'.
-           ' That\'s bad.');
-}
-# }}}
+    #unless we already have (a) customer(s)...
+    unless ( $self->Customers->Count ) {
 
-# {{{ Routines dealing with watchers.
+      #first find any requestors with emails but *without* customer targets
+      my @NoCust_Requestors =
+        grep { $_->EmailAddress && ! $_->Customers->Count }
+             @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
 
-# {{{ Routines dealing with adding new watchers
+      for my $Requestor (@NoCust_Requestors) {
 
-# {{{ sub AddWatcher
+         #perhaps the stuff in here should be in a User method??
+         my @Customers =
+           &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
 
-=head2 AddWatcher
+         foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
 
-AddWatcher takes a parameter hash. The keys are as follows:
+           ## false laziness w/RT/Interface/Web_Vendor.pm
+           my @link = ( 'Type'   => 'MemberOf',
+                        'Target' => "freeside://freeside/cust_main/$custnum",
+                      );
 
-Email
-Type
-Owner
+           my( $val, $msg ) = $Requestor->_AddLink(@link);
+           #XXX should do something with $msg# push @non_fatal_errors, $msg;
 
-If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
+         }
 
-=cut
+      }
 
-sub AddWatcher {
-    my $self = shift;
-    my %args = ( Email => undef,
-                Type => undef,
-                Owner => undef,
-                @_
-              );
+      #find any requestors with customer targets
+  
+      my %cust_target = ();
+
+      my @Requestors =
+        grep { $_->Customers->Count }
+             @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
+  
+      foreach my $Requestor ( @Requestors ) {
+        foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
+          $cust_target{ $cust_link->Target } = 1;
+        }
+      }
+  
+      #and then auto-associate this ticket with those customers
+  
+      foreach my $cust_target ( keys %cust_target ) {
+  
+        my @link = ( 'Type'   => 'MemberOf',
+                     #'Target' => "freeside://freeside/cust_main/$custnum",
+                     'Target' => $cust_target,
+                   );
+  
+        my( $val, $msg ) = $self->_AddLink(@link);
+        push @non_fatal_errors, $msg;
+  
+      }
 
-    # {{{ Check ACLS
-    #If the watcher we're trying to add is for the current user
-    if ( ( $self->CurrentUser->EmailAddress &&
-           ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or
-           ($args{'Owner'} eq $self->CurrentUser->Id) 
-        ) {
-
-       
-       #  If it's an AdminCc and they don't have 
-       #   'WatchAsAdminCc' or 'ModifyTicket', bail
-       if ($args{'Type'} eq 'AdminCc') {
-           unless ($self->CurrentUserHasRight('ModifyTicket') or 
-                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
-               return(0, 'Permission Denied');
-           }
-       }
-
-       #  If it's a Requestor or Cc and they don't have
-       #   'Watch' or 'ModifyTicket', bail
-       elsif (($args{'Type'} eq 'Cc') or 
-              ($args{'Type'} eq 'Requestor')) {
-                  
-           unless ($self->CurrentUserHasRight('ModifyTicket') or 
-                   $self->CurrentUserHasRight('Watch')) {
-               return(0, 'Permission Denied');
-           }
-       }
-       else {
-           $RT::Logger->warn("$self -> AddWatcher hit code".
-                             " it never should. We got passed ".
-                             " a type of ". $args{'Type'});
-           return (0,'Error in parameters to TicketAddWatcher');
-       }
-    }
-    # If the watcher isn't the current user 
-    # and the current user  doesn't have 'ModifyTicket'
-    # bail
-    else {
-       unless ($self->CurrentUserHasRight('ModifyTicket')) {
-           return (0, "Permission Denied");
-       }
     }
-    # }}}
 
-    return ($self->_AddWatcher(%args));
-}
+    # }}}
 
+    # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself. 
+    # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
+    if (  $DeferOwner ) { 
+            if (!$DeferOwner->HasRight( Object => $self, Right  => 'OwnTicket')) {
+    
+            $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id 
+                . ") was proposed as a ticket owner but has no rights to own "
+                . "tickets in " . $QueueObj->Name );
+            push @non_fatal_errors, $self->loc(
+                "Owner '[_1]' does not have rights to own this ticket.",
+                $DeferOwner->Name
+            );
+        } else {
+            $Owner = $DeferOwner;
+            $self->__Set(Field => 'Owner', Value => $Owner->id);
 
-#This contains the meat of AddWatcher. but can be called from a routine like
-# Create, which doesn't need the additional acl check
-sub _AddWatcher {
-    my $self = shift;
-    my %args = (
-               Type => undef,
-               Silent => undef,
-               Email => undef,
-               Owner => 0,
-               Person => undef,
-               @_ );
-    
-    
-    
-    #clear the watchers cache
-    $self->{'watchers_cache'} = undef;
-    
-    if (defined $args{'Person'}) {
-       #if it's an RT::User object, pull out the id and shove it in Owner
-       if (ref ($args{'Person'}) =~ /RT::User/) {
-           $args{'Owner'} = $args{'Person'}->id;
-       }       
-       #if it's an int, shove it in Owner
-       elsif ($args{'Person'} =~ /^\d+$/) {
-           $args{'Owner'} = $args{'Person'};
-       }
-       #if it's an email address, shove it in Email
-       else {
-          $args{'Email'} = $args{'Person'};
-       }       
-    }  
-
-    # Turn an email address int a watcher if we possibly can.
-    if ($args{'Email'}) {
-       my $watcher = new RT::User($self->CurrentUser);
-       $watcher->LoadByEmail($args{'Email'});
-       if ($watcher->Id) {
-               $args{'Owner'} = $watcher->Id;
-               delete $args{'Email'};
-       }
+        }
+        $self->OwnerGroup->_AddMember(
+            PrincipalId       => $Owner->PrincipalId,
+            InsideTransaction => 1
+        );
     }
 
+    #don't make a transaction or fire off any scrips for reminders either
+    if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
 
-    # see if this user is already a watcher. if we have an owner, check it
-    # otherwise, we've got an email-address watcher. use that.
+        # Add a transaction for the create
+        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+            Type         => "Create",
+            TimeTaken    => $args{'TimeWorked'},
+            MIMEObj      => $args{'MIMEObj'},
+            CommitScrips => !$args{'DryRun'},
+            SquelchMailTo => $args{'TransSquelchMailTo'},
+        );
 
-    if ($self->IsWatcher(Type => $args{'Type'},
-                         Id => ($args{'Owner'} || $args{'Email'}) ) ) {
+        if ( $self->Id && $Trans ) {
 
+          #$TransObj->UpdateCustomFields(ARGSRef => \%args);
+            $TransObj->UpdateCustomFields(%args);
 
-        return(0, 'That user is already that sort of watcher for this ticket');
-    }
+            $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
+            $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
+            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+        }
+        else {
+            $RT::Handle->Rollback();
 
-    
-    require RT::Watcher;
-    my $Watcher = new RT::Watcher ($self->CurrentUser);
-    my ($retval, $msg) = ($Watcher->Create( Value => $self->Id,
-                                           Scope => 'Ticket',
-                                           Email => $args{'Email'},
-                                           Type => $args{'Type'},
-                                           Owner => $args{'Owner'},
-                                         ));
-    
-    unless ($args{'Silent'}) {
-       $self->_NewTransaction( Type => 'AddWatcher',
-                               NewValue => $Watcher->Email,
-                               Field => $Watcher->Type);
-    }
-    
-    return ($retval, $msg);
-}
+            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+            $RT::Logger->error("Ticket couldn't be created: $ErrStr");
+            return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
+        }
 
-# }}}
+        if ( $args{'DryRun'} ) {
+            $RT::Handle->Rollback();
+            return ($self->id, $TransObj, $ErrStr);
+        }
+        $RT::Handle->Commit();
+        return ( $self->Id, $TransObj->Id, $ErrStr );
 
-# {{{ sub AddRequestor
+        # }}}
+    }
+    else {
 
-=head2 AddRequestor
+        # Not going to record a transaction
+        $RT::Handle->Commit();
+        $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
+        $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+        return ( $self->Id, 0, $ErrStr );
 
-AddRequestor takes what AddWatcher does, except it presets
-the "Type" parameter to \'Requestor\'
+    }
+}
 
-=cut
+sub SetType {
+    my $self = shift;
+    my $value = shift;
 
-sub AddRequestor {
-   my $self = shift;
-   return ($self->AddWatcher ( Type => 'Requestor', @_));
+    # Force lowercase on internal RT types
+    $value = lc $value
+        if $value =~ /^(ticket|approval|reminder)$/i;
+    return $self->_Set(Field => 'Type', Value => $value, @_);
 }
 
-# }}}
 
-# {{{ sub AddCc
 
-=head2 AddCc
+=head2 _Parse822HeadersForAttributes Content
 
-AddCc takes what AddWatcher does, except it presets
-the "Type" parameter to \'Cc\'
+Takes an RFC822 style message and parses its attributes into a hash.
 
 =cut
 
-sub AddCc {
-   my $self = shift;
-   return ($self->AddWatcher ( Type => 'Cc', @_));
-}
-# }}}
-       
-# {{{ sub AddAdminCc
-
-=head2 AddAdminCc
-
-AddAdminCc takes what AddWatcher does, except it presets
-the "Type" parameter to \'AdminCc\'
+sub _Parse822HeadersForAttributes {
+    my $self    = shift;
+    my $content = shift;
+    my %args;
+
+    my @lines = ( split ( /\n/, $content ) );
+    while ( defined( my $line = shift @lines ) ) {
+        if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
+            my $value = $2;
+            my $tag   = lc($1);
+
+            $tag =~ s/-//g;
+            if ( defined( $args{$tag} ) )
+            {    #if we're about to get a second value, make it an array
+                $args{$tag} = [ $args{$tag} ];
+            }
+            if ( ref( $args{$tag} ) )
+            {    #If it's an array, we want to push the value
+                push @{ $args{$tag} }, $value;
+            }
+            else {    #if there's nothing there, just set the value
+                $args{$tag} = $value;
+            }
+        } elsif ($line =~ /^$/) {
+
+            #TODO: this won't work, since "" isn't of the form "foo:value"
+
+                while ( defined( my $l = shift @lines ) ) {
+                    push @{ $args{'content'} }, $l;
+                }
+            }
+        
+    }
 
-=cut
+    foreach my $date (qw(due starts started resolved)) {
+        my $dateobj = RT::Date->new(RT->SystemUser);
+        if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
+            $dateobj->Set( Format => 'unix', Value => $args{$date} );
+        }
+        else {
+            $dateobj->Set( Format => 'unknown', Value => $args{$date} );
+        }
+        $args{$date} = $dateobj->ISO;
+    }
+    $args{'mimeobj'} = MIME::Entity->build(
+        Type    => ( $args{'contenttype'} || 'text/plain' ),
+        Charset => "UTF-8",
+        Data    => Encode::encode("UTF-8", ($args{'content'} || ''))
+    );
 
-sub AddAdminCc {
-   my $self = shift;
-   return ($self->AddWatcher ( Type => 'AdminCc', @_));
+    return (%args);
 }
 
-# }}}
-
-# }}}
 
-# {{{ sub DeleteWatcher
 
-=head2 DeleteWatcher id [type]
+=head2 Import PARAMHASH
 
-DeleteWatcher takes a single argument which is either an email address 
-or a watcher id.  
-If the first argument is an email address, you need to specify the watcher type you're talking
-about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'.
-It removes that watcher from this Ticket\'s list of watchers.
+Import a ticket. 
+Doesn't create a transaction. 
+Doesn't supply queue defaults, etc.
 
+Returns: TICKETID
 
 =cut
 
-#TODO It is lame that you can't call this the same way you can call AddWatcher
-
-sub DeleteWatcher {
+sub Import {
     my $self = shift;
-    my $id = shift;
+    my ( $ErrStr, $QueueObj, $Owner );
 
-    my $type;
-    
-    $type = shift if (@_);
-    
-    my $Watcher = new RT::Watcher($self->CurrentUser);
-    
-    #If it\'s a numeric watcherid
-    if ($id =~ /^(\d*)$/) {
-       $Watcher->Load($id);
-    }
-    
-    #Otherwise, we'll assume it's an email address
-    elsif ($type) {
-       my ($result, $msg) = 
-         $Watcher->LoadByValue( Email => $id,
-                                Scope => 'Ticket',
-                                Value => $self->id,
-                                Type => $type);
-       return (0,$msg) unless ($result);
+    my %args = (
+        id              => undef,
+        EffectiveId     => undef,
+        Queue           => undef,
+        Requestor       => undef,
+        Type            => 'ticket',
+        Owner           => RT->Nobody->Id,
+        Subject         => '[no subject]',
+        InitialPriority => undef,
+        FinalPriority   => undef,
+        Status          => 'new',
+        TimeWorked      => "0",
+        Due             => undef,
+        Created         => undef,
+        Updated         => undef,
+        Resolved        => undef,
+        Told            => undef,
+        @_
+    );
+
+    if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
+        $QueueObj = RT::Queue->new(RT->SystemUser);
+        $QueueObj->Load( $args{'Queue'} );
+
+        #TODO error check this and return 0 if it's not loading properly +++
     }
-    
-    else {
-       return(0,"Can\'t delete a watcher by email address without specifying a type");
+    elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
+        $QueueObj = RT::Queue->new(RT->SystemUser);
+        $QueueObj->Load( $args{'Queue'}->Id );
     }
-    
-    # {{{ Check ACLS 
-
-    #If the watcher we're trying to delete is for the current user
-    if ($Watcher->Email eq $self->CurrentUser->EmailAddress) {
-               
-       #  If it's an AdminCc and they don't have 
-       #   'WatchAsAdminCc' or 'ModifyTicket', bail
-       if ($Watcher->Type eq 'AdminCc') {
-           unless ($self->CurrentUserHasRight('ModifyTicket') or 
-                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
-               return(0, 'Permission Denied');
-           }
-       }
-
-       #  If it's a Requestor or Cc and they don't have
-       #   'Watch' or 'ModifyTicket', bail
-       elsif (($Watcher->Type eq 'Cc') or 
-              ($Watcher->Type eq 'Requestor')) {
-                  
-           unless ($self->CurrentUserHasRight('ModifyTicket') or 
-                   $self->CurrentUserHasRight('Watch')) {
-               return(0, 'Permission Denied');
-           }
-       }
-       else {
-           $RT::Logger->warn("$self -> DeleteWatcher hit code".
-                             " it never should. We got passed ".
-                             " a type of ". $args{'Type'});
-           return (0,'Error in parameters to $self DeleteWatcher');
-       }
-    }
-    # If the watcher isn't the current user 
-    # and the current user  doesn't have 'ModifyTicket'
-    # bail
     else {
-       unless ($self->CurrentUserHasRight('ModifyTicket')) {
-           return (0, "Permission Denied");
-       }
-    }  
-    
-    # }}}
-    
-    unless (($Watcher->Scope eq 'Ticket') and
-           ($Watcher->Value == $self->id) ) {
-       return (0, "Not a watcher for this ticket");
+        $RT::Logger->debug(
+            "$self " . $args{'Queue'} . " not a recognised queue object." );
     }
 
-
-    #Clear out the watchers hash.
-    $self->{'watchers'} = undef;
-    
-    #If we\'ve validated that it is a watcher for this ticket 
-    $self->_NewTransaction ( Type => 'DelWatcher',        
-                            OldValue => $Watcher->Email,
-                            Field => $Watcher->Type,
-                          );
-    
-    my $retval = $Watcher->Delete();
-    
-    unless ($retval) {
-       return(0,"Watcher could not be deleted. Database inconsistency possible.");
+    #Can't create a ticket without a queue.
+    unless ( defined($QueueObj) and $QueueObj->Id ) {
+        $RT::Logger->debug("$self No queue given for ticket creation.");
+        return ( 0, $self->loc('Could not create ticket. Queue not set') );
     }
-    
-    return(1, "Watcher deleted");
-}
 
-# {{{ sub DeleteRequestor
-
-=head2 DeleteRequestor EMAIL
+    #Now that we have a queue, Check the ACLS
+    unless (
+        $self->CurrentUser->HasRight(
+            Right    => 'CreateTicket',
+            Object => $QueueObj
+        )
+      )
+    {
+        return ( 0,
+            $self->loc("No permission to create tickets in the queue '[_1]'"
+              , $QueueObj->Name));
+    }
 
-Takes an email address. It calls DeleteWatcher with a preset 
-type of 'Requestor'
+    # Deal with setting the owner
 
+    # Attempt to take user object, user name or user id.
+    # Assign to nobody if lookup fails.
+    if ( defined( $args{'Owner'} ) ) {
+        if ( ref( $args{'Owner'} ) ) {
+            $Owner = $args{'Owner'};
+        }
+        else {
+            $Owner = RT::User->new( $self->CurrentUser );
+            $Owner->Load( $args{'Owner'} );
+            if ( !defined( $Owner->id ) ) {
+                $Owner->Load( RT->Nobody->id );
+            }
+        }
+    }
 
-=cut
+    #If we have a proposed owner and they don't have the right 
+    #to own a ticket, scream about it and make them not the owner
+    if (
+        ( defined($Owner) )
+        and ( $Owner->Id != RT->Nobody->Id )
+        and (
+            !$Owner->HasRight(
+                Object => $QueueObj,
+                Right    => 'OwnTicket'
+            )
+        )
+      )
+    {
 
-sub DeleteRequestor {
-   my $self = shift;
-   my $id = shift;
-   return ($self->DeleteWatcher ($id, 'Requestor'))
-}
+        $RT::Logger->warning( "$self user "
+              . $Owner->Name . "("
+              . $Owner->id
+              . ") was proposed "
+              . "as a ticket owner but has no rights to own "
+              . "tickets in '"
+              . $QueueObj->Name . "'" );
 
-# }}}
+        $Owner = undef;
+    }
 
-# {{{ sub DeleteCc
+    #If we haven't been handed a valid owner, make it nobody.
+    unless ( defined($Owner) ) {
+        $Owner = RT::User->new( $self->CurrentUser );
+        $Owner->Load( RT->Nobody->UserObj->Id );
+    }
 
-=head2 DeleteCc EMAIL
+    # }}}
 
-Takes an email address. It calls DeleteWatcher with a preset 
-type of 'Cc'
+    unless ( $self->ValidateStatus( $args{'Status'} ) ) {
+        return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
+    }
 
+    $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
+    $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
+    $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
+    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
 
-=cut
+    # If we're coming in with an id, set that now.
+    my $EffectiveId = undef;
+    if ( $args{'id'} ) {
+        $EffectiveId = $args{'id'};
 
-sub DeleteCc {
-   my $self = shift;
-   my $id = shift;
-   return ($self->DeleteWatcher ($id, 'Cc'))
-}
+    }
 
-# }}}
+    my $id = $self->SUPER::Create(
+        id              => $args{'id'},
+        EffectiveId     => $EffectiveId,
+        Queue           => $QueueObj->Id,
+        Owner           => $Owner->Id,
+        Subject         => $args{'Subject'},        # loc
+        InitialPriority => $args{'InitialPriority'},    # loc
+        FinalPriority   => $args{'FinalPriority'},    # loc
+        Priority        => $args{'InitialPriority'},    # loc
+        Status          => $args{'Status'},        # loc
+        TimeWorked      => $args{'TimeWorked'},        # loc
+        Type            => $args{'Type'},        # loc
+        Created         => $args{'Created'},        # loc
+        Told            => $args{'Told'},        # loc
+        LastUpdated     => $args{'Updated'},        # loc
+        Resolved        => $args{'Resolved'},        # loc
+        Due             => $args{'Due'},        # loc
+    );
 
-# {{{ sub DeleteAdminCc
+    # If the ticket didn't have an id
+    # Set the ticket's effective ID now that we've created it.
+    if ( $args{'id'} ) {
+        $self->Load( $args{'id'} );
+    }
+    else {
+        my ( $val, $msg ) =
+          $self->__Set( Field => 'EffectiveId', Value => $id );
 
-=head2 DeleteAdminCc EMAIL
+        unless ($val) {
+            $RT::Logger->err(
+                $self . "->Import couldn't set EffectiveId: $msg" );
+        }
+    }
 
-Takes an email address. It calls DeleteWatcher with a preset 
-type of 'AdminCc'
+    my $create_groups_ret = $self->_CreateTicketGroups();
+    unless ($create_groups_ret) {
+        $RT::Logger->crit(
+            "Couldn't create ticket groups for ticket " . $self->Id );
+    }
 
+    $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
 
-=cut
+    foreach my $watcher ( @{ $args{'Cc'} } ) {
+        $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
+    }
+    foreach my $watcher ( @{ $args{'AdminCc'} } ) {
+        $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
+            Silent => 1 );
+    }
+    foreach my $watcher ( @{ $args{'Requestor'} } ) {
+        $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
+            Silent => 1 );
+    }
 
-sub DeleteAdminCc {
-   my $self = shift;
-   my $id = shift;
-   return ($self->DeleteWatcher ($id, 'AdminCc'))
+    return ( $self->Id, $ErrStr );
 }
 
-# }}}
 
 
-# }}}
 
-# {{{ sub Watchers
+=head2 _CreateTicketGroups
+
+Create the ticket groups and links for this ticket. 
+This routine expects to be called from Ticket->Create _inside of a transaction_
 
-=head2 Watchers
+It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
 
-Watchers returns a Watchers object preloaded with this ticket\'s watchers.
+It will return true on success and undef on failure.
 
-# It should return only the ticket watchers. the actual FooAsString
-# methods capture the queue watchers too. I don't feel thrilled about this,
-# but we don't want the Cc Requestors and AdminCc objects to get filled up
-# with all the queue watchers too. we've got seperate objects for that.
-  # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch?
 
 =cut
 
-sub Watchers {
-  my $self = shift;
-  
-  require RT::Watchers;
-  my $watchers=RT::Watchers->new($self->CurrentUser);
-  if ($self->CurrentUserHasRight('ShowTicket')) {
-      $watchers->LimitToTicket($self->id);
-  }
-  
-  return($watchers);
-  
+
+sub _CreateTicketGroups {
+    my $self = shift;
+    
+    my @types = (qw(Requestor Owner Cc AdminCc));
+
+    foreach my $type (@types) {
+        my $type_obj = RT::Group->new($self->CurrentUser);
+        my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
+                                                       Instance => $self->Id, 
+                                                       Type => $type);
+        unless ($id) {
+            $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
+                               $self->Id.": ".$msg);     
+            return(undef);
+        }
+     }
+    return(1);
+    
 }
 
-# }}}
 
-# {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
 
-=head2 RequestorsAsString
+=head2 OwnerGroup
 
- B<Returns> String: All Ticket Requestor email addresses as a string.
+A constructor which returns an RT::Group object containing the owner of this ticket.
 
 =cut
 
-sub RequestorsAsString {
-    my $self=shift;
-
-    unless ($self->CurrentUserHasRight('ShowTicket')) {
-        return undef;
-    }
-    
-    return ($self->Requestors->EmailsAsString() );
+sub OwnerGroup {
+    my $self = shift;
+    my $owner_obj = RT::Group->new($self->CurrentUser);
+    $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
+    return ($owner_obj);
 }
 
-=head2 WatchersAsString
 
-B<Returns> String: All Ticket Watchers email addresses as a string
 
-=cut
 
-sub WatchersAsString {
-    my $self=shift;
+=head2 AddWatcher
 
-    unless ($self->CurrentUserHasRight('ShowTicket')) {
-       return (0, "Permission Denied");
-    }
-    
-    return ($self->Watchers->EmailsAsString());
+AddWatcher takes a parameter hash. The keys are as follows:
 
-}
+Type        One of Requestor, Cc, AdminCc
 
-=head2 AdminCcAsString
+PrincipalId The RT::Principal id of the user or group that's being added as a watcher
 
-returns String: All Ticket AdminCc email addresses as a string
+Email       The email address of the new watcher. If a user with this 
+            email address can't be found, a new nonprivileged user will be created.
+
+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.
 
 =cut
 
+sub AddWatcher {
+    my $self = shift;
+    my %args = (
+        Type  => undef,
+        PrincipalId => undef,
+        Email => undef,
+        @_
+    );
+
+    # ModifyTicket works in any case
+    return $self->_AddWatcher( %args )
+        if $self->CurrentUserHasRight('ModifyTicket');
+    if ( $args{'Email'} ) {
+        my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
+        return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
+            unless $addr;
+
+        if ( lc $self->CurrentUser->EmailAddress
+            eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
+        {
+            $args{'PrincipalId'} = $self->CurrentUser->id;
+            delete $args{'Email'};
+        }
+    }
+
+    # If the watcher isn't the current user then the current user has no right
+    # bail
+    unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
 
-sub AdminCcAsString {
-    my $self=shift;
+    #  If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
+    if ( $args{'Type'} eq 'AdminCc' ) {
+        unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
+            return ( 0, $self->loc('Permission Denied') );
+        }
+    }
 
-    unless ($self->CurrentUserHasRight('ShowTicket')) {
-       return undef;
+    #  If it's a Requestor or Cc and they don't have 'Watch', bail
+    elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
+        unless ( $self->CurrentUserHasRight('Watch') ) {
+            return ( 0, $self->loc('Permission Denied') );
+        }
     }
-    
-    return ($self->AdminCc->EmailsAsString());
-    
+    else {
+        $RT::Logger->warning( "AddWatcher got passed a bogus type");
+        return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
+    }
+
+    return $self->_AddWatcher( %args );
 }
 
-=head2 CcAsString
+#This contains the meat of AddWatcher. but can be called from a routine like
+# Create, which doesn't need the additional acl check
+sub _AddWatcher {
+    my $self = shift;
+    my %args = (
+        Type   => undef,
+        Silent => undef,
+        PrincipalId => undef,
+        Email => undef,
+        @_
+    );
 
-returns String: All Ticket Ccs as a string of email addresses
 
-=cut
+    my $principal = RT::Principal->new($self->CurrentUser);
+    if ($args{'Email'}) {
+        if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
+            return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
+        }
+        my $user = RT::User->new(RT->SystemUser);
+        my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
+        $args{'PrincipalId'} = $pid if $pid; 
+    }
+    if ($args{'PrincipalId'}) {
+        $principal->Load($args{'PrincipalId'});
+        if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
+            return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
+                if RT::EmailParser->IsRTAddress( $email );
 
-sub CcAsString {
-    my $self=shift;
+        }
+    } 
 
-    unless ($self->CurrentUserHasRight('ShowTicket')) {
-        return undef; 
+    # If we can't find this watcher, we need to bail.
+    unless ($principal->Id) {
+            $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
+        return(0, $self->loc("Could not find or create that user"));
     }
-    
-    return ($self->Cc->EmailsAsString());
 
-}
 
-# }}}
+    my $group = RT::Group->new($self->CurrentUser);
+    $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
+    unless ($group->id) {
+        return(0,$self->loc("Group not found"));
+    }
 
-# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
+    if ( $group->HasMember( $principal)) {
 
-# {{{ sub Requestors
+        return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
+                    $principal->Object->Name, $self->loc($args{'Type'})) );
+    }
 
-=head2 Requestors
 
-Takes nothing.
-Returns this ticket's Requestors as an RT::Watchers object
+    my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
+                                               InsideTransaction => 1 );
+    unless ($m_id) {
+        $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
 
-=cut
+        return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
+                    $principal->Object->Name, $self->loc($args{'Type'})) );
+    }
 
-sub Requestors {
-    my $self = shift;
-    
-    my $requestors = $self->Watchers();
-    if ($self->CurrentUserHasRight('ShowTicket')) {
-       $requestors->LimitToRequestors();
-    }  
-    
-    return($requestors);
-    
+    unless ( $args{'Silent'} ) {
+        $self->_NewTransaction(
+            Type     => 'AddWatcher',
+            NewValue => $principal->Id,
+            Field    => $args{'Type'}
+        );
+    }
+
+    return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
+                $principal->Object->Name, $self->loc($args{'Type'})) );
 }
 
-# }}}
 
-# {{{ sub Cc
 
-=head2 Cc
 
-Takes nothing.
-Returns a watchers object which contains this ticket's Cc watchers
+=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
 
-=cut
 
-sub Cc {
-    my $self = shift;
-    
-    my $cc = $self->Watchers();
-    
-    if ($self->CurrentUserHasRight('ShowTicket')) {
-       $cc->LimitToCc();
-    }
-    
-    return($cc);
-    
-}
+Deletes a Ticket watcher.  Takes two arguments:
 
-# }}}
+Type  (one of Requestor,Cc,AdminCc)
 
-# {{{ sub AdminCc
+and one of
 
-=head2 AdminCc
+PrincipalId (an RT::Principal Id of the watcher you want to remove)
+    OR
+Email (the email address of an existing wathcer)
 
-Takes nothing.
-Returns this ticket\'s administrative Ccs as an RT::Watchers object
 
 =cut
 
-sub AdminCc {
+
+sub DeleteWatcher {
     my $self = shift;
-    
-    my $admincc = $self->Watchers();
-    if ($self->CurrentUserHasRight('ShowTicket')) {
-       $admincc->LimitToAdminCc();
+
+    my %args = ( Type        => undef,
+                 PrincipalId => undef,
+                 Email       => undef,
+                 @_ );
+
+    unless ( $args{'PrincipalId'} || $args{'Email'} ) {
+        return ( 0, $self->loc("No principal specified") );
     }
-    return($admincc);
-}
+    my $principal = RT::Principal->new( $self->CurrentUser );
+    if ( $args{'PrincipalId'} ) {
 
-# }}}
+        $principal->Load( $args{'PrincipalId'} );
+    }
+    else {
+        my $user = RT::User->new( $self->CurrentUser );
+        $user->LoadByEmail( $args{'Email'} );
+        $principal->Load( $user->Id );
+    }
 
-# }}}
+    # If we can't find this watcher, we need to bail.
+    unless ( $principal->Id ) {
+        return ( 0, $self->loc("Could not find that principal") );
+    }
 
-# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
+    my $group = RT::Group->new( $self->CurrentUser );
+    $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
+    unless ( $group->id ) {
+        return ( 0, $self->loc("Group not found") );
+    }
 
-# {{{ sub IsWatcher
-# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
+    # Check ACLS
+    #If the watcher we're trying to add is for the current user
+    if ( $self->CurrentUser->PrincipalId == $principal->id ) {
+
+        #  If it's an AdminCc and they don't have
+        #   'WatchAsAdminCc' or 'ModifyTicket', bail
+        if ( $args{'Type'} eq 'AdminCc' ) {
+            unless (    $self->CurrentUserHasRight('ModifyTicket')
+                     or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
+                return ( 0, $self->loc('Permission Denied') );
+            }
+        }
 
-=head2 IsWatcher
+        #  If it's a Requestor or Cc and they don't have
+        #   'Watch' or 'ModifyTicket', bail
+        elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
+        {
+            unless (    $self->CurrentUserHasRight('ModifyTicket')
+                     or $self->CurrentUserHasRight('Watch') ) {
+                return ( 0, $self->loc('Permission Denied') );
+            }
+        }
+        else {
+            $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
+            return ( 0,
+                     $self->loc('Error in parameters to Ticket->DeleteWatcher') );
+        }
+    }
 
-Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string
-is a ticket watcher. Returns undef otherwise
+    # If the watcher isn't the current user
+    # and the current user  doesn't have 'ModifyTicket' bail
+    else {
+        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+            return ( 0, $self->loc("Permission Denied") );
+        }
+    }
 
-=cut
+    # }}}
 
-sub IsWatcher {
-    my $self = shift;
+    # see if this user is already a watcher.
 
-    my %args = ( Type => 'Requestor',
-                Email => undef,
-                Id => undef,
-                @_
-              );
-    
-    my %cols = ('Type' => $args{'Type'},
-               'Scope' => 'Ticket',
-               'Value' => $self->Id,
-               'Owner' => undef,
-               'Email' => undef
-              );
-    
-    if (ref($args{'Id'})){ 
-       #If it's a ref, it's an RT::User object;
-       $cols{'Owner'} = $args{'Id'}->Id;
-    }
-    elsif ($args{'Id'} =~ /^\d+$/) { 
-       # if it's an integer, it's a reference to an RT::User obj
-       $cols{'Owner'} = $args{'Id'};
-    }
-    else {
-       $cols{'Email'} = $args{'Id'};
-    }  
-    
-    if ($args{'Email'}) {
-       $cols{'Email'} = $args{'Email'};
+    unless ( $group->HasMember($principal) ) {
+        return ( 0,
+                 $self->loc( '[_1] is not a [_2] for this ticket',
+                             $principal->Object->Name, $args{'Type'} ) );
     }
 
-    my $description = join(":",%cols);
-    
-    #If we've cached a positive match...
-    if (defined $self->{'watchers_cache'}->{"$description"}) {
-       if ($self->{'watchers_cache'}->{"$description"} == 1) {
-           return(1);
-       }
-       else { #If we've cached a negative match...
-           return(undef);
-       }
+    my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
+    unless ($m_id) {
+        $RT::Logger->error( "Failed to delete "
+                            . $principal->Id
+                            . " as a member of group "
+                            . $group->Id . ": "
+                            . $m_msg );
+
+        return (0,
+                $self->loc(
+                    'Could not remove [_1] as a [_2] for this ticket',
+                    $principal->Object->Name, $args{'Type'} ) );
     }
-    
-    
-    my $watcher = new RT::Watcher($self->CurrentUser);
-    $watcher->LoadByCols(%cols);
-    
-    
-    if ($watcher->id) {
-       $self->{'watchers_cache'}->{"$description"} = 1;
-       return(1);
-    }  
-    else {
-       $self->{'watchers_cache'}->{"$description"} = 0;
-       return(undef);
+
+    unless ( $args{'Silent'} ) {
+        $self->_NewTransaction( Type     => 'DelWatcher',
+                                OldValue => $principal->Id,
+                                Field    => $args{'Type'} );
     }
-    
-}
-# }}}
 
-# {{{ sub IsRequestor
+    return ( 1,
+             $self->loc( "[_1] is no longer a [_2] for this ticket.",
+                         $principal->Object->Name,
+                         $args{'Type'} ) );
+}
 
-=head2 IsRequestor
-  
-  Takes an email address, RT::User object or integer (RT user id)
-  Returns true if the string is a requestor of the current ticket.
 
 
-=cut
 
-sub IsRequestor {
-    my $self = shift;
-    my $person = shift;
 
-    return ($self->IsWatcher(Type => 'Requestor', Id => $person));
-           
-};
+=head2 SquelchMailTo [EMAIL]
 
-# }}}
+Takes an optional email address to never email about updates to this ticket.
 
-# {{{ sub IsCc
 
-=head2 IsCc
+Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
 
-Takes a string. Returns true if the string is a Cc watcher of the current ticket.
 
 =cut
 
-sub IsCc {
-  my $self = shift;
-  my $cc = shift;
-  
-  return ($self->IsWatcher( Type => 'Cc', Id => $cc ));
-  
+sub SquelchMailTo {
+    my $self = shift;
+    if (@_) {
+        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+            return ();
+        }
+    } else {
+        unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+            return ();
+        }
+
+    }
+    return $self->_SquelchMailTo(@_);
+}
+
+sub _SquelchMailTo {
+    my $self = shift;
+    if (@_) {
+        my $attr = shift;
+        $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
+            unless grep { $_->Content eq $attr }
+                $self->Attributes->Named('SquelchMailTo');
+    }
+    my @attributes = $self->Attributes->Named('SquelchMailTo');
+    return (@attributes);
 }
 
-# }}}
 
-# {{{ sub IsAdminCc
+=head2 UnsquelchMailTo ADDRESS
 
-=head2 IsAdminCc
+Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
 
-Takes a string. Returns true if the string is an AdminCc watcher of the current ticket.
+Returns a tuple of (status, message)
 
 =cut
 
-sub IsAdminCc {
-  my $self = shift;
-  my $person = shift;
-  
-  return ($self->IsWatcher( Type => 'AdminCc', Id => $person ));
-  
+sub UnsquelchMailTo {
+    my $self = shift;
+
+    my $address = shift;
+    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
+    return ($val, $msg);
 }
 
-# }}}
 
-# {{{ sub IsOwner
 
-=head2 IsOwner
+=head2 RequestorAddresses
 
-  Takes an RT::User object. Returns true if that user is this ticket's owner.
-returns undef otherwise
+B<Returns> String: All Ticket Requestor email addresses as a string.
 
 =cut
 
-sub IsOwner {
+sub RequestorAddresses {
     my $self = shift;
-    my $person = shift;
-  
 
-    # no ACL check since this is used in acl decisions
-    # unless ($self->CurrentUserHasRight('ShowTicket')) {
-    #  return(undef);
-    #   }      
-
-    
-    #Tickets won't yet have owners when they're being created.
-    unless ($self->OwnerObj->id) {
-        return(undef);
-    }
-    
-    if ($person->id == $self->OwnerObj->id) {
-       return(1);
-    }
-    else {
-       return(undef);
+    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+        return undef;
     }
+
+    return ( $self->Requestors->MemberEmailAddressesAsString );
 }
 
 
-# }}}
+=head2 AdminCcAddresses
 
-# }}}
+returns String: All Ticket AdminCc email addresses as a string
 
-# }}}
+=cut
 
-# {{{ Routines dealing with queues 
+sub AdminCcAddresses {
+    my $self = shift;
 
-# {{{ sub ValidateQueue
+    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+        return undef;
+    }
+
+    return ( $self->AdminCc->MemberEmailAddressesAsString )
 
-sub ValidateQueue {
-  my $self = shift;
-  my $Value = shift;
-  
-  #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue,
-  if (!$Value) {
-    $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
-    return (1);
-  }
-  
-  my $QueueObj = RT::Queue->new($self->CurrentUser);
-  my $id = $QueueObj->Load($Value);
-  
-  if ($id) {
-    return (1);
-  }
-  else {
-    return (undef);
-  }
 }
 
-# }}}
+=head2 CcAddresses
 
-# {{{ sub SetQueue  
+returns String: All Ticket Ccs as a string of email addresses
 
-sub SetQueue {
+=cut
+
+sub CcAddresses {
     my $self = shift;
-    my $NewQueue = shift;
 
-    #Redundant. ACL gets checked in _Set;
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
-    }
-   
-    my $NewQueueObj = RT::Queue->new($self->CurrentUser);
-    $NewQueueObj->Load($NewQueue);
-    
-    unless ($NewQueueObj->Id()) {
-       return (0, "That queue does not exist");
-    }
-    
-    if ($NewQueueObj->Id == $self->QueueObj->Id) {
-       return (0, 'That is the same value');
-    }
-    unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket',
-                                             QueueObj => $NewQueueObj )) {
-       return (0, "You may not create requests in that queue.");
-    }
-    
-    unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket',  
-                                          QueueObj => $NewQueueObj)) {
-           $self->Untake();
+    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+        return undef;
     }
+    return ( $self->Cc->MemberEmailAddressesAsString);
 
-    return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id()));
-    
 }
 
-# }}}
 
-# {{{ sub QueueObj
 
-=head2 QueueObj
 
-Takes nothing. returns this ticket's queue object
-
-=cut
-
-sub QueueObj {
-    my $self = shift;
-    
-    my $queue_obj = RT::Queue->new($self->CurrentUser);
-    #We call __Value so that we can avoid the ACL decision and some deep recursion
-    my ($result) = $queue_obj->Load($self->__Value('Queue'));
-    return ($queue_obj);
-}
+=head2 Requestors
 
+Takes nothing.
+Returns this ticket's Requestors as an RT::Group object
 
-# }}}
+=cut
 
-# }}}
+sub Requestors {
+    my $self = shift;
 
-# {{{ Date printing routines
+    my $group = RT::Group->new($self->CurrentUser);
+    if ( $self->CurrentUserHasRight('ShowTicket') ) {
+        $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
+    }
+    return ($group);
 
-# {{{ sub DueObj
+}
 
-=head2 DueObj
+=head2 _Requestors
 
-  Returns an RT::Date object containing this ticket's due date
+Private non-ACLed variant of Reqeustors so that we can look them up for the
+purposes of customer auto-association during create.
 
 =cut
-sub DueObj {
+
+sub _Requestors {
     my $self = shift;
-    
-    my $time = new RT::Date($self->CurrentUser);
 
-    # -1 is RT::Date slang for never
-    if ($self->Due) {
-       $time->Set(Format => 'sql', Value => $self->Due );
-    }
-    else {
-       $time->Set(Format => 'unix', Value => -1);
-    }
-    
-    return $time;
+    my $group = RT::Group->new($RT::SystemUser);
+    $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
+    return ($group);
 }
-# }}}
-
-# {{{ sub DueAsString 
 
-=head2 DueAsString
+=head2 Cc
 
-Returns this ticket's due date as a human readable string
+Takes nothing.
+Returns an RT::Group object which contains this ticket's Ccs.
+If the user doesn't have "ShowTicket" permission, returns an empty group
 
 =cut
 
-sub DueAsString {
-  my $self = shift;
-  return $self->DueObj->AsString();
+sub Cc {
+    my $self = shift;
+
+    my $group = RT::Group->new($self->CurrentUser);
+    if ( $self->CurrentUserHasRight('ShowTicket') ) {
+        $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
+    }
+    return ($group);
+
 }
 
-# }}}
 
-# {{{ sub GraceTimeAsString 
 
-=head2 GraceTimeAsString
+=head2 AdminCc
 
-Return the time until this ticket is due as a string
+Takes nothing.
+Returns an RT::Group object which contains this ticket's AdminCcs.
+If the user doesn't have "ShowTicket" permission, returns an empty group
 
 =cut
 
-# TODO This should be deprecated 
+sub AdminCc {
+    my $self = shift;
 
-sub GraceTimeAsString {
-    my $self=shift;
-    
-    if ($self->Due) {
-       return ($self->DueObj->AgeAsString());
-    } else {
-       return "";
+    my $group = RT::Group->new($self->CurrentUser);
+    if ( $self->CurrentUserHasRight('ShowTicket') ) {
+        $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
     }
-}
+    return ($group);
 
-# }}}
+}
 
 
-# {{{ sub ResolvedObj
 
-=head2 ResolvedObj
 
-  Returns an RT::Date object of this ticket's 'resolved' time.
+# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
 
-=cut
+=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
 
-sub ResolvedObj {
-  my $self = shift;
+Takes a param hash with the attributes Type and either PrincipalId or Email
 
-  my $time = new RT::Date($self->CurrentUser);
-  $time->Set(Format => 'sql', Value => $self->Resolved);
-  return $time;
-}
-# }}}
+Type is one of Requestor, Cc, AdminCc and Owner
 
-# {{{ sub SetStarted
+PrincipalId is an RT::Principal id, and Email is an email address.
 
-=head2 SetStarted
+Returns true if the specified principal (or the one corresponding to the
+specified address) is a member of the group Type for this ticket.
 
-Takes a date in ISO format or undef
-Returns a transaction id and a message
-The client calls "Start" to note that the project was started on the date in $date.
-A null date means "now"
+XX TODO: This should be Memoized. 
 
 =cut
-  
-sub SetStarted {
+
+sub IsWatcher {
     my $self = shift;
-    my $time = shift || 0;
-    
 
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
+    my %args = ( Type  => 'Requestor',
+        PrincipalId    => undef,
+        Email          => undef,
+        @_
+    );
+
+    # Load the relevant group. 
+    my $group = RT::Group->new($self->CurrentUser);
+    $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
+
+    # Find the relevant principal.
+    if (!$args{PrincipalId} && $args{Email}) {
+        # Look up the specified user.
+        my $user = RT::User->new($self->CurrentUser);
+        $user->LoadByEmail($args{Email});
+        if ($user->Id) {
+            $args{PrincipalId} = $user->PrincipalId;
+        }
+        else {
+            # A non-existent user can't be a group member.
+            return 0;
+        }
     }
 
-    #We create a date object to catch date weirdness
-    my $time_obj = new RT::Date($self->CurrentUser());
-    if ($time != 0)  {
-       $time_obj->Set(Format => 'ISO', Value => $time);
-    }
-    else {
-       $time_obj->SetToNow();
-    }
-    
-    #Now that we're starting, open this ticket
-    #TODO do we really want to force this as policy? it should be a scrip
-    
-    #We need $TicketAsSystem, in case the current user doesn't have
-    #ShowTicket
-    #
-    my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
-    $TicketAsSystem->Load($self->Id);  
-    if ($TicketAsSystem->Status eq 'new') {
-       $TicketAsSystem->Open();
-    }  
-    
-    return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO));
-    
+    # Ask if it has the member in question
+    return $group->HasMember( $args{'PrincipalId'} );
 }
 
-# }}}
 
-# {{{ sub StartedObj
 
-=head2 StartedObj
+=head2 IsRequestor PRINCIPAL_ID
+  
+Takes an L<RT::Principal> id.
 
-  Returns an RT::Date object which contains this ticket's 
-'Started' time.
+Returns true if the principal is a requestor of the current ticket.
 
 =cut
 
+sub IsRequestor {
+    my $self   = shift;
+    my $person = shift;
 
-sub StartedObj {
-    my $self = shift;
-    
-    my $time = new RT::Date($self->CurrentUser);
-    $time->Set(Format => 'sql', Value => $self->Started);
-    return $time;
-}
-# }}}
-
-# {{{ sub StartsObj
-
-=head2 StartsObj
+    return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
 
-  Returns an RT::Date object which contains this ticket's 
-'Starts' time.
+};
 
-=cut
 
-sub StartsObj {
-  my $self = shift;
-  
-  my $time = new RT::Date($self->CurrentUser);
-  $time->Set(Format => 'sql', Value => $self->Starts);
-  return $time;
-}
-# }}}
 
-# {{{ sub ToldObj
+=head2 IsCc PRINCIPAL_ID
 
-=head2 ToldObj
+  Takes an RT::Principal id.
+  Returns true if the principal is a Cc of the current ticket.
 
-  Returns an RT::Date object which contains this ticket's 
-'Told' time.
 
 =cut
 
+sub IsCc {
+    my $self = shift;
+    my $cc   = shift;
+
+    return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
 
-sub ToldObj {
-  my $self = shift;
-  
-  my $time = new RT::Date($self->CurrentUser);
-  $time->Set(Format => 'sql', Value => $self->Told);
-  return $time;
 }
 
-# }}}
 
-# {{{ sub LongSinceToldAsString
 
-# TODO this should be deprecated
+=head2 IsAdminCc PRINCIPAL_ID
+
+  Takes an RT::Principal id.
+  Returns true if the principal is an AdminCc of the current ticket.
 
+=cut
+
+sub IsAdminCc {
+    my $self   = shift;
+    my $person = shift;
 
-sub LongSinceToldAsString {
-  my $self = shift;
+    return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
 
-  if ($self->Told) {
-      return $self->ToldObj->AgeAsString();
-  } else {
-      return "Never";
-  }
 }
-# }}}
 
-# {{{ sub ToldAsString
 
-=head2 ToldAsString
 
-A convenience method that returns ToldObj->AsString
+=head2 IsOwner
 
-TODO: This should be deprecated
+  Takes an RT::User object. Returns true if that user is this ticket's owner.
+returns undef otherwise
 
 =cut
 
+sub IsOwner {
+    my $self   = shift;
+    my $person = shift;
+
+    # no ACL check since this is used in acl decisions
+    # unless ($self->CurrentUserHasRight('ShowTicket')) {
+    #    return(undef);
+    #   }    
 
-sub ToldAsString {
-    my $self = shift;
-    if ($self->Told) {
-       return $self->ToldObj->AsString();
+    #Tickets won't yet have owners when they're being created.
+    unless ( $self->OwnerObj->id ) {
+        return (undef);
+    }
+
+    if ( $person->id == $self->OwnerObj->id ) {
+        return (1);
     }
     else {
-       return("Never");
+        return (undef);
     }
 }
-# }}}
 
-# {{{ sub TimeWorkedAsString
 
-=head2 TimeWorkedAsString
 
-Returns the amount of time worked on this ticket as a Text String
 
-=cut
 
-sub TimeWorkedAsString {
-    my $self=shift;
-    return "0" unless $self->TimeWorked;
-    
-    #This is not really a date object, but if we diff a number of seconds 
-    #vs the epoch, we'll get a nice description of time worked.
-    
-    my $worked = new RT::Date($self->CurrentUser);
-    #return the  #of minutes worked turned into seconds and written as
-    # a simple text string
+=head2 TransactionAddresses
 
-    return($worked->DurationAsString($self->TimeWorked*60));
-}
+Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
+all this ticket's Create, Comment or Correspond transactions. The keys are
+stringified email addresses. Each value is an L<Email::Address> object.
+
+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.
 
-# }}}
+=cut
 
 
-# }}}
+sub TransactionAddresses {
+    my $self = shift;
+    my $txns = $self->Transactions;
 
-# {{{ Routines dealing with correspondence/comments
+    my %addresses = ();
 
-# {{{ sub Comment
+    my $attachments = RT::Attachments->new( $self->CurrentUser );
+    $attachments->LimitByTicket( $self->id );
+    $attachments->Columns( qw( id Headers TransactionId));
 
-=head2 Comment
 
-Comment on this ticket.
-Takes a hashref with the follwoing attributes:
+    foreach my $type (qw(Create Comment Correspond)) {
+        $attachments->Limit( ALIAS    => $attachments->TransactionAlias,
+                             FIELD    => 'Type',
+                             OPERATOR => '=',
+                             VALUE    => $type,
+                             ENTRYAGGREGATOR => 'OR',
+                             CASESENSITIVE   => 1
+                           );
+    }
 
-MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
+    while ( my $att = $attachments->Next ) {
+        foreach my $addrlist ( values %{$att->Addresses } ) {
+            foreach my $addr (@$addrlist) {
 
-=cut
+# Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
+                next
+                    if (    $addresses{ $addr->address }
+                         && $addresses{ $addr->address }->phrase
+                         && not $addr->phrase );
 
-sub Comment {
-  my $self = shift;
-  
-  my %args = (
-          CcMessageTo => undef,
-          BccMessageTo => undef,
-             MIMEObj => undef,
-             TimeTaken => 0,
-             @_ );
-
-  unless (($self->CurrentUserHasRight('CommentOnTicket')) or
-         ($self->CurrentUserHasRight('ModifyTicket'))) {
-      return (0, "Permission Denied");
-  }
-   unless ($args{'MIMEObj'}) {
-       return(0,"No correspondence attached");
-   }
+                # skips "comment-only" addresses
+                next unless ( $addr->address );
+                $addresses{ $addr->address } = $addr;
+            }
+        }
+    }
+
+    return \%addresses;
 
-  # If we've been passed in CcMessageTo and BccMessageTo fields,
-  # add them to the mime object for passing on to the transaction handler
-  # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
-  # RT-Send-Bcc: headers
-  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
-  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
-
-  #Record the correspondence (write the transaction)
-  my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment',
-                                     Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
-                                     TimeTaken => $args{'TimeTaken'},
-                                     MIMEObj => $args{'MIMEObj'}
-                                   );
-  
-  
-  return ($Trans, "The comment has been recorded");
 }
 
-# }}}
 
-# {{{ sub Correspond
 
-=head2 Correspond
 
-Correspond on this ticket.
-Takes a hashref with the following attributes:
 
 
-MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
+sub ValidateQueue {
+    my $self  = shift;
+    my $Value = shift;
 
-=cut
+    if ( !$Value ) {
+        $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
+        return (1);
+    }
 
-sub Correspond {
-    my $self = shift;
-    my %args = (
-          CcMessageTo => undef,
-          BccMessageTo => undef,
-               MIMEObj => undef,
-               TimeTaken => 0,
-               @_ );
-    
-    unless (($self->CurrentUserHasRight('ReplyToTicket')) or
-           ($self->CurrentUserHasRight('ModifyTicket'))) {
-       return (0, "Permission Denied");
+    my $QueueObj = RT::Queue->new( $self->CurrentUser );
+    my $id       = $QueueObj->Load($Value);
+
+    if ($id) {
+        return (1);
     }
-    
-    unless ($args{'MIMEObj'}) {
-       return(0,"No correspondence attached");
+    else {
+        return (undef);
     }
-    
-  # If we've been passed in CcMessageTo and BccMessageTo fields,
-  # add them to the mime object for passing on to the transaction handler
-  # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
-  # headers
-  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
-  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
+}
 
-    #Record the correspondence (write the transaction)
-    my ($Trans,$msg, $TransObj) = $self->_NewTransaction
-      (Type => 'Correspond',
-       Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
-       TimeTaken => $args{'TimeTaken'},
-       MIMEObj=> $args{'MIMEObj'}     
-      );
-    
-    # TODO this bit of logic should really become a scrip for 2.2
-    my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
-    $TicketAsSystem->Load($self->Id);
-       
-    if (
-       ($TicketAsSystem->Status ne 'open') and
-       ($TicketAsSystem->Status ne 'new')
-       ) {
-       
-       my $oldstatus = $TicketAsSystem->Status();
-       $TicketAsSystem->__Set(Field => 'Status', Value => 'open');
-       $TicketAsSystem->_NewTransaction 
-         ( Type => 'Set',
-           Field => 'Status',
-           OldValue => $oldstatus,
-           NewValue => 'open',
-           Data => 'Ticket auto-opened on incoming correspondence'
-         );
+
+
+sub SetQueue {
+    my $self     = shift;
+    my $NewQueue = shift;
+
+    #Redundant. ACL gets checked in _Set;
+    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
     }
-    
-    unless ($Trans) {
-       $RT::Logger->err("$self couldn't init a transaction ($msg)\n");
-       return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'});
+
+    my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
+    $NewQueueObj->Load($NewQueue);
+
+    unless ( $NewQueueObj->Id() ) {
+        return ( 0, $self->loc("That queue does not exist") );
     }
-    
-    #Set the last told date to now if this isn't mail from the requestor.
-    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
-    
-    unless ($TransObj->IsInbound) {
-       $self->_SetTold;
+
+    if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
+        return ( 0, $self->loc('That is the same value') );
+    }
+    unless ( $self->CurrentUser->HasRight( Right    => 'CreateTicket', Object => $NewQueueObj)) {
+        return ( 0, $self->loc("You may not create requests in that queue.") );
+    }
+
+    my $new_status;
+    my $old_lifecycle = $self->QueueObj->Lifecycle;
+    my $new_lifecycle = $NewQueueObj->Lifecycle;
+    if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
+        unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
+            return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
+        }
+        $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
+        return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
+            unless $new_status;
     }
-    
-    return ($Trans, "correspondence sent");
-}
 
-# }}}
+    if ( $new_status ) {
+        my $clone = RT::Ticket->new( RT->SystemUser );
+        $clone->Load( $self->Id );
+        unless ( $clone->Id ) {
+            return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
+        }
 
-# }}}
+        my $now = RT::Date->new( $self->CurrentUser );
+        $now->SetToNow;
 
-# {{{ Routines dealing with Links and Relations between tickets
+        my $old_status = $clone->Status;
 
-# {{{ Link Collections
+        #If we're changing the status from initial in old to not intial in new,
+        # record that we've started
+        if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status)  && $clone->StartedObj->Unix == 0 ) {
+            #Set the Started time to "now"
+            $clone->_Set(
+                Field             => 'Started',
+                Value             => $now->ISO,
+                RecordTransaction => 0
+            );
+        }
 
-# {{{ sub Members
+        #When we close a ticket, set the 'Resolved' attribute to now.
+        # It's misnamed, but that's just historical.
+        if ( $new_lifecycle->IsInactive($new_status) ) {
+            $clone->_Set(
+                Field             => 'Resolved',
+                Value             => $now->ISO,
+                RecordTransaction => 0,
+            );
+        }
 
-=head2 Members
+        #Actually update the status
+        my ($val, $msg)= $clone->_Set(
+            Field             => 'Status',
+            Value             => $new_status,
+            RecordTransaction => 0,
+        );
+        $RT::Logger->error( 'Status change failed on queue change: '. $msg )
+            unless $val;
+    }
 
-  This returns an RT::Links object which references all the tickets 
-which are 'MembersOf' this ticket
+    my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
+
+    if ( $status ) {
+        # Clear the queue object cache;
+        $self->{_queue_obj} = undef;
+
+        # Untake the ticket if we have no permissions in the new queue
+        unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
+            my $clone = RT::Ticket->new( RT->SystemUser );
+            $clone->Load( $self->Id );
+            unless ( $clone->Id ) {
+                return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
+            }
+            my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
+            $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
+        }
 
-=cut
+        # On queue change, change queue for reminders too
+        my $reminder_collection = $self->Reminders->Collection;
+        while ( my $reminder = $reminder_collection->Next ) {
+            my ($status, $msg) = $reminder->SetQueue($NewQueue);
+            $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
+        }
+    }
 
-sub Members {
-   my $self = shift;
-   return ($self->_Links('Target', 'MemberOf'));
+    return ($status, $msg);
 }
 
-# }}}
 
-# {{{ sub MemberOf
 
-=head2 MemberOf
+=head2 QueueObj
 
-  This returns an RT::Links object which references all the tickets that this
-ticket is a 'MemberOf'
+Takes nothing. returns this ticket's queue object
 
 =cut
 
-sub MemberOf {
-   my $self = shift;
-   return ($self->_Links('Base', 'MemberOf'));
+sub QueueObj {
+    my $self = shift;
+
+    if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
+
+        $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
+
+        #We call __Value so that we can avoid the ACL decision and some deep recursion
+        my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
+    }
+    return ($self->{_queue_obj});
 }
 
-# }}}
+sub SetSubject {
+    my $self = shift;
+    my $value = shift;
+    $value =~ s/\n//g;
+    return $self->_Set( Field => 'Subject', Value => $value );
+}
 
-# {{{ RefersTo
+=head2 SubjectTag
 
-=head2 RefersTo
+Takes nothing. Returns SubjectTag for this ticket. Includes
+queue's subject tag or rtname if that is not set, ticket
+id and braces, for example:
 
-  This returns an RT::Links object which shows all references for which this ticket is a base
+    [support.example.com #123456]
 
 =cut
 
-sub RefersTo {
+sub SubjectTag {
     my $self = shift;
-    return ($self->_Links('Base', 'RefersTo'));
+    return
+        '['
+        . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
+        .' #'. $self->id
+        .']'
+    ;
 }
 
-# }}}
-
-# {{{ ReferredToBy
 
-=head2 ReferredToBy
+=head2 DueObj
 
-  This returns an RT::Links object which shows all references for which this ticket is a target
+  Returns an RT::Date object containing this ticket's due date
 
 =cut
 
-sub ReferredToBy {
+sub DueObj {
     my $self = shift;
-    return ($self->_Links('Target', 'RefersTo'));
+
+    my $time = RT::Date->new( $self->CurrentUser );
+
+    # -1 is RT::Date slang for never
+    if ( my $due = $self->Due ) {
+        $time->Set( Format => 'sql', Value => $due );
+    }
+    else {
+        $time->Set( Format => 'unix', Value => -1 );
+    }
+
+    return $time;
 }
 
-# }}}
 
-# {{{ DependedOnBy
 
-=head2 DependedOnBy
+=head2 DueAsString
 
-  This returns an RT::Links object which references all the tickets that depend on this one
+Returns this ticket's due date as a human readable string
 
 =cut
-sub DependedOnBy {
+
+sub DueAsString {
     my $self = shift;
-    return ($self->_Links('Target','DependsOn'));
+    return $self->DueObj->AsString();
 }
 
-# }}}
 
-# {{{ DependsOn
 
-=head2 DependsOn
+=head2 ResolvedObj
 
-  This returns an RT::Links object which references all the tickets that this ticket depends on
+  Returns an RT::Date object of this ticket's 'resolved' time.
 
 =cut
-sub DependsOn {
-   my $self = shift;
-    return ($self->_Links('Base','DependsOn'));
+
+sub ResolvedObj {
+    my $self = shift;
+
+    my $time = RT::Date->new( $self->CurrentUser );
+    $time->Set( Format => 'sql', Value => $self->Resolved );
+    return $time;
 }
 
-# }}}
 
-# {{{ sub _Links 
+=head2 FirstActiveStatus
 
-sub _Links {
+Returns the first active status that the ticket could transition to,
+according to its current Queue's lifecycle.  May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in L<RT::Action::AutoOpen>, for instance.
+
+=cut
+
+sub FirstActiveStatus {
     my $self = shift;
-    
-    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
-    #tobias meant by $f
-    my $field = shift;
-    my $type =shift || "";
 
-    unless ($self->{"$field$type"}) {
-       $self->{"$field$type"} = new RT::Links($self->CurrentUser);
-       if ($self->CurrentUserHasRight('ShowTicket')) {
-           
-           $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI);
-           $self->{"$field$type"}->Limit(FIELD=>'Type', 
-                                         VALUE=>$type) if ($type);
-       }
-    }
-    return ($self->{"$field$type"});
+    my $lifecycle = $self->QueueObj->Lifecycle;
+    my $status = $self->Status;
+    my @active = $lifecycle->Active;
+    # no change if no active statuses in the lifecycle
+    return undef unless @active;
+
+    # no change if the ticket is already has first status from the list of active
+    return undef if lc $status eq lc $active[0];
+
+    my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
+    return $next;
 }
 
-# }}}
+=head2 FirstInactiveStatus
 
-# }}}
+Returns the first inactive status that the ticket could transition to,
+according to its current Queue's lifecycle.  May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in resolve action in UnsafeEmailCommands, for instance.
 
+=cut
 
-# {{{ sub DeleteLink 
+sub FirstInactiveStatus {
+    my $self = shift;
 
-=head2 DeleteLink
+    my $lifecycle = $self->QueueObj->Lifecycle;
+    my $status = $self->Status;
+    my @inactive = $lifecycle->Inactive;
+    # no change if no inactive statuses in the lifecycle
+    return undef unless @inactive;
 
-Delete a link. takes a paramhash of Base, Target and Type.
-Either Base or Target must be null. The null value will 
-be replaced with this ticket\'s id
+    # no change if the ticket is already has first status from the list of inactive
+    return undef if lc $status eq lc $inactive[0];
 
-=cut 
+    my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
+    return $next;
+}
 
-sub DeleteLink {
-    my $self = shift;
-    my %args = ( Base =>  undef,
-                Target => undef,
-                Type => undef,
-                @_ );
-    
-    #check acls
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-        $RT::Logger->debug("No permission to delete links\n"); 
-        return (0, 'Permission Denied');
+=head2 SetStarted
 
-    
-    }
-    
-    #we want one of base and target. we don't care which
-    #but we only want _one_
+Takes a date in ISO format or undef
+Returns a transaction id and a message
+The client calls "Start" to note that the project was started on the date in $date.
+A null date means "now"
 
-    if ($args{'Base'} and $args{'Target'}) {
-       $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
-       return (0, 'Can\'t specifiy both base and target');
-    }
-    elsif ($args{'Base'}) {
-       $args{'Target'} = $self->Id();
-    }
-    elsif ($args{'Target'}) {
-       $args{'Base'} = $self->Id();
+=cut
+
+sub SetStarted {
+    my $self = shift;
+    my $time = shift || 0;
+
+    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
     }
-    else {  
-        $RT::Logger->debug("$self: Base or Target must be specified\n");
-       return (0, 'Either base or target must be specified');
+
+    #We create a date object to catch date weirdness
+    my $time_obj = RT::Date->new( $self->CurrentUser() );
+    if ( $time ) {
+        $time_obj->Set( Format => 'ISO', Value => $time );
     }
-     
-    my $link = new RT::Link($self->CurrentUser);
-    $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n");
-    
-    $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
-    
-    
-    
-    #it's a real link. 
-    if ($link->id) {
-        $RT::Logger->debug("We're going to delete link ".$link->id."\n");
-       $link->Delete();
-
-       my $TransString=
-         "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
-       my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
-         (Type => 'DeleteLink',
-          Field => $args{'Type'},
-          Data => $TransString,
-          TimeTaken => 0
-         );
-       
-       return ($linkid, "Link deleted ($TransString)", $transactionid);
-    }
-    #if it's not a link we can find
     else {
-        $RT::Logger->debug("Couldn't find that link\n");
-       return (0, "Link not found");
+        $time_obj->SetToNow();
     }
+
+    # We need $TicketAsSystem, in case the current user doesn't have
+    # ShowTicket
+    my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
+    $TicketAsSystem->Load( $self->Id );
+    # Now that we're starting, open this ticket
+    # TODO: do we really want to force this as policy? it should be a scrip
+    my $next = $TicketAsSystem->FirstActiveStatus;
+
+    $self->SetStatus( $next ) if defined $next;
+
+    return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
+
 }
 
-# }}}
 
-# {{{ sub AddLink
 
-=head2 AddLink
+=head2 StartedObj
 
-Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
+  Returns an RT::Date object which contains this ticket's 
+'Started' time.
+
+=cut
+
+sub StartedObj {
+    my $self = shift;
+
+    my $time = RT::Date->new( $self->CurrentUser );
+    $time->Set( Format => 'sql', Value => $self->Started );
+    return $time;
+}
+
+
+
+=head2 StartsObj
 
+  Returns an RT::Date object which contains this ticket's 
+'Starts' time.
 
 =cut
 
-sub AddLink {
+sub StartsObj {
     my $self = shift;
-    my %args = ( Target => '',
-                Base => '',
-                Type => '',
-                @_ );
-    
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
+
+    my $time = RT::Date->new( $self->CurrentUser );
+    $time->Set( Format => 'sql', Value => $self->Starts );
+    return $time;
+}
+
+
+
+=head2 ToldObj
+
+  Returns an RT::Date object which contains this ticket's 
+'Told' time.
+
+=cut
+
+sub ToldObj {
+    my $self = shift;
+
+    my $time = RT::Date->new( $self->CurrentUser );
+    $time->Set( Format => 'sql', Value => $self->Told );
+    return $time;
+}
+
+
+
+=head2 ToldAsString
+
+A convenience method that returns ToldObj->AsString
+
+TODO: This should be deprecated
+
+=cut
+
+sub ToldAsString {
+    my $self = shift;
+    if ( $self->Told ) {
+        return $self->ToldObj->AsString();
     }
-    
-    if ($args{'Base'} and $args{'Target'}) {
-       $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n");
-       return (0, 'Can\'t specifiy both base and target');
+    else {
+        return ("Never");
     }
-    elsif ($args{'Base'}) {
-        $args{'Target'} = $self->Id();
+}
+
+
+
+=head2 TimeWorkedAsString
+
+Returns the amount of time worked on this ticket as a Text String
+
+=cut
+
+sub TimeWorkedAsString {
+    my $self = shift;
+    my $value = $self->TimeWorked;
+
+    # return the # of minutes worked turned into seconds and written as
+    # a simple text string, this is not really a date object, but if we
+    # diff a number of seconds vs the epoch, we'll get a nice description
+    # of time worked.
+    return "" unless $value;
+    return RT::Date->new( $self->CurrentUser )
+        ->DurationAsString( $value * 60 );
+}
+
+
+
+=head2  TimeLeftAsString
+
+Returns the amount of time left on this ticket as a Text String
+
+=cut
+
+sub TimeLeftAsString {
+    my $self = shift;
+    my $value = $self->TimeLeft;
+    return "" unless $value;
+    return RT::Date->new( $self->CurrentUser )
+        ->DurationAsString( $value * 60 );
+}
+
+
+
+
+=head2 Comment
+
+Comment on this ticket.
+Takes a hash with the following attributes:
+If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
+comment.
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
+
+If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
+They will, however, be prepared and you'll be able to access them through the TransactionObj
+
+Returns: Transaction id, Error Message, Transaction Object
+(note the different order from Create()!)
+
+=cut
+
+sub Comment {
+    my $self = shift;
+
+    my %args = ( CcMessageTo  => undef,
+                 BccMessageTo => undef,
+                 MIMEObj      => undef,
+                 Content      => undef,
+                 TimeTaken => 0,
+                 DryRun     => 0, 
+                 @_ );
+
+    unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
+             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
+        return ( 0, $self->loc("Permission Denied"), undef );
     }
-    elsif ($args{'Target'}) {
-       $args{'Base'} = $self->Id();
+    $args{'NoteType'} = 'Comment';
+
+    $RT::Handle->BeginTransaction();
+    if ($args{'DryRun'}) {
+        $args{'CommitScrips'} = 0;
     }
-    else {  
-       return (0, 'Either base or target must be specified');
+
+    my @results = $self->_RecordNote(%args);
+    if ($args{'DryRun'}) {
+        $RT::Handle->Rollback();
+    } else {
+        $RT::Handle->Commit();
     }
-    
-    # {{{ We don't want references to ourself
-    if ($args{Base} eq $args{Target}) {
-       return (0, "Can\'t link a ticket to itself");
-    }  
-               
-    # }}}
-    
-    # If the base isn't a URI, make it a URI. 
-    # If the target isn't a URI, make it a URI. 
-        
-    # {{{ Check if the link already exists - we don't want duplicates
-    my $old_link= new RT::Link ($self->CurrentUser);
-    $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
-    if ($old_link->Id) {
-       $RT::Logger->debug("$self Somebody tried to duplicate a link");
-       return ($old_link->id, "Link already exists",0);
+
+    return(@results);
+}
+
+
+=head2 Correspond
+
+Correspond on this ticket.
+Takes a hashref with the following attributes:
+
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
+
+if there's no MIMEObj, Content is used to build a MIME::Entity object
+
+If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
+They will, however, be prepared and you'll be able to access them through the TransactionObj
+
+Returns: Transaction id, Error Message, Transaction Object
+(note the different order from Create()!)
+
+
+=cut
+
+sub Correspond {
+    my $self = shift;
+    my %args = ( CcMessageTo  => undef,
+                 BccMessageTo => undef,
+                 MIMEObj      => undef,
+                 Content      => undef,
+                 TimeTaken    => 0,
+                 @_ );
+
+    unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
+             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
+        return ( 0, $self->loc("Permission Denied"), undef );
     }
-    # }}}
-    
-    # Storing the link in the DB.
-    my $link = RT::Link->new($self->CurrentUser);
-    my ($linkid) = $link->Create(Target => $args{Target}, 
-                                Base => $args{Base}, 
-                                Type => $args{Type});
-    
-    unless ($linkid) {
-       return (0,"Link could not be created");
+    $args{'NoteType'} = 'Correspond';
+
+    $RT::Handle->BeginTransaction();
+    if ($args{'DryRun'}) {
+        $args{'CommitScrips'} = 0;
     }
-       #Write the transaction
-    
-    my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
-    
-    my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
-      (Type => 'AddLink',
-       Field => $args{'Type'},
-       Data => $TransString,
-       TimeTaken => 0
-      );
-    
-    return ($Trans, "Link created ($TransString)");
-       
-       
+
+    my @results = $self->_RecordNote(%args);
+
+    unless ( $results[0] ) {
+        $RT::Handle->Rollback();
+        return @results;
+    }
+
+    #Set the last told date to now if this isn't mail from the requestor.
+    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
+    unless ( $self->IsRequestor($self->CurrentUser->id) ) {
+        my %squelch;
+        $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
+        $self->_SetTold
+            if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
+    }
+
+    if ($args{'DryRun'}) {
+        $RT::Handle->Rollback();
+    } else {
+        $RT::Handle->Commit();
+    }
+
+    return (@results);
+
 }
-# }}}
 
-# {{{ sub URI 
 
-=head2 URI
 
-Returns this ticket's URI
+=head2 _RecordNote
+
+the meat of both comment and correspond. 
+
+Performs no access control checks. hence, dangerous.
 
 =cut
 
-sub URI {
+sub _RecordNote {
     my $self = shift;
-    return $RT::TicketBaseURI.$self->id;
+    my %args = ( 
+        CcMessageTo  => undef,
+        BccMessageTo => undef,
+        Encrypt      => undef,
+        Sign         => undef,
+        MIMEObj      => undef,
+        Content      => undef,
+        NoteType     => 'Correspond',
+        TimeTaken    => 0,
+        CommitScrips => 1,
+        SquelchMailTo => undef,
+        CustomFields => {},
+        @_
+    );
+
+    unless ( $args{'MIMEObj'} || $args{'Content'} ) {
+        return ( 0, $self->loc("No message attached"), undef );
+    }
+
+    unless ( $args{'MIMEObj'} ) {
+        my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
+        $args{'MIMEObj'} = MIME::Entity->build(
+            Type    => "text/plain",
+            Charset => "UTF-8",
+            Data    => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
+        );
+    }
+
+    $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
+        unless $args{'MIMEObj'}->head->get('X-RT-Interface');
+
+    # convert text parts into utf-8
+    RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
+
+    # If we've been passed in CcMessageTo and BccMessageTo fields,
+    # add them to the mime object for passing on to the transaction handler
+    # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
+    # RT-Send-Bcc: headers
+
+
+    foreach my $type (qw/Cc Bcc/) {
+        if ( defined $args{ $type . 'MessageTo' } ) {
+
+            my $addresses = join ', ', (
+                map { RT::User->CanonicalizeEmailAddress( $_->address ) }
+                    Email::Address->parse( $args{ $type . 'MessageTo' } ) );
+            $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
+        }
+    }
+
+    foreach my $argument (qw(Encrypt Sign)) {
+        $args{'MIMEObj'}->head->replace(
+            "X-RT-$argument" => Encode::encode( "UTF-8", $args{ $argument } )
+        ) if defined $args{ $argument };
+    }
+
+    # If this is from an external source, we need to come up with its
+    # internal Message-ID now, so all emails sent because of this
+    # message have a common Message-ID
+    my $org = RT->Config->Get('Organization');
+    my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
+    unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
+        $args{'MIMEObj'}->head->set(
+            'RT-Message-ID' => Encode::encode( "UTF-8",
+                RT::Interface::Email::GenMessageId( Ticket => $self )
+            )
+        );
+    }
+
+    #Record the correspondence (write the transaction)
+    my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
+             Type => $args{'NoteType'},
+             Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
+             TimeTaken => $args{'TimeTaken'},
+             MIMEObj   => $args{'MIMEObj'}, 
+             CommitScrips => $args{'CommitScrips'},
+             SquelchMailTo => $args{'SquelchMailTo'},
+             CustomFields => $args{'CustomFields'},
+    );
+
+    unless ($Trans) {
+        $RT::Logger->err("$self couldn't init a transaction $msg");
+        return ( $Trans, $self->loc("Message could not be recorded"), undef );
+    }
+
+    return ( $Trans, $self->loc("Message recorded"), $TransObj );
 }
 
-# }}}
 
-# {{{ sub MergeInto
+=head2 DryRun
 
-=head2 MergeInto
-MergeInto take the id of the ticket to merge this ticket into.
+Builds a MIME object from the given C<UpdateSubject> and
+C<UpdateContent>, then calls L</Comment> or L</Correspond> with
+C<< DryRun => 1 >>, and returns the transaction so produced.
 
 =cut
 
-sub MergeInto {
+sub DryRun {
     my $self = shift;
-    my $MergeInto = shift;
-    
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
+    my %args = @_;
+    my $action;
+    if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
+        $action = 'Correspond';
+    } else {
+        $action = 'Comment';
     }
-    
-    # Load up the new ticket.
-    my $NewTicket = RT::Ticket->new($RT::SystemUser);
-    $NewTicket->Load($MergeInto);
 
-    # make sure it exists.
-    unless (defined $NewTicket->Id) {
-       return (0, 'New ticket doesn\'t exist');
+    my $Message = MIME::Entity->build(
+        Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
+        Type    => 'text/plain',
+        Charset => 'UTF-8',
+        Data    => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
+    );
+
+    my ( $Transaction, $Description, $Object ) = $self->$action(
+        CcMessageTo  => $args{'UpdateCc'},
+        BccMessageTo => $args{'UpdateBcc'},
+        MIMEObj      => $Message,
+        TimeTaken    => $args{'UpdateTimeWorked'},
+        DryRun       => 1,
+    );
+    unless ( $Transaction ) {
+        $RT::Logger->error("Couldn't fire '$action' action: $Description");
     }
 
-    
-    # Make sure the current user can modify the new ticket.
-    unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) {
-       $RT::Logger->debug("failed...");
-       return (0, "Permission Denied");
+    return $Object;
+}
+
+=head2 DryRunCreate
+
+Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
+C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
+the resulting L<RT::Transaction>.
+
+=cut
+
+sub DryRunCreate {
+    my $self = shift;
+    my %args = @_;
+    my $Message = MIME::Entity->build(
+        Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
+        (defined $args{'Cc'} ?
+             ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
+        Type    => 'text/plain',
+        Charset => 'UTF-8',
+        Data    => Encode::encode( "UTF-8", $args{'Content'} || ""),
+    );
+
+    my ( $Transaction, $Object, $Description ) = $self->Create(
+        Type            => $args{'Type'} || 'ticket',
+        Queue           => $args{'Queue'},
+        Owner           => $args{'Owner'},
+        Requestor       => $args{'Requestors'},
+        Cc              => $args{'Cc'},
+        AdminCc         => $args{'AdminCc'},
+        InitialPriority => $args{'InitialPriority'},
+        FinalPriority   => $args{'FinalPriority'},
+        TimeLeft        => $args{'TimeLeft'},
+        TimeEstimated   => $args{'TimeEstimated'},
+        TimeWorked      => $args{'TimeWorked'},
+        Subject         => $args{'Subject'},
+        Status          => $args{'Status'},
+        MIMEObj         => $Message,
+        DryRun          => 1,
+    );
+    unless ( $Transaction ) {
+        $RT::Logger->error("Couldn't fire Create action: $Description");
     }
-    
-    $RT::Logger->debug("checking if the new ticket has the same id and effective id...");
-    unless ($NewTicket->id == $NewTicket->EffectiveId) {
-       $RT::Logger->err('$self trying to merge into '.$NewTicket->Id .
-                        ' which is itself merged.\n');
-       return (0, "Can't merge into a merged ticket. ".
-               "You should never get this error");
+
+    return $Object;
+}
+
+
+
+sub _Links {
+    my $self = shift;
+
+    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
+    #tobias meant by $f
+    my $field = shift;
+    my $type  = shift || "";
+
+    my $cache_key = "$field$type";
+    return $self->{ $cache_key } if $self->{ $cache_key };
+
+    my $links = $self->{ $cache_key }
+              = RT::Links->new( $self->CurrentUser );
+    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+        $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
+        return $links;
     }
 
-    
-    # We use EffectiveId here even though it duplicates information from
-    # the links table becasue of the massive performance hit we'd take
-    # by trying to do a seperate database query for merge info everytime 
-    # loaded a ticket. 
-    
-    
-    #update this ticket's effective id to the new ticket's id.
-    my ($id_val, $id_msg) = $self->__Set(Field => 'EffectiveId', 
-                                        Value => $NewTicket->Id());
-    
-    unless ($id_val) {
-       $RT::Logger->error("Couldn't set effective ID for ".$self->Id.
-                          ": $id_msg");
-       return(0,"Merge failed. Couldn't set EffectiveId");
+    # Maybe this ticket is a merge ticket
+    #my $limit_on = 'Local'. $field;
+    # at least to myself
+    $links->Limit(
+        FIELD           => $field, #$limit_on,
+        OPERATOR        => 'MATCHES',
+        VALUE           => 'fsck.com-rt://%/ticket/'. $self->id,
+        ENTRYAGGREGATOR => 'OR',
+    );
+    $links->Limit(
+        FIELD           => $field, #$limit_on,
+        OPERATOR        => 'MATCHES',
+        VALUE           => 'fsck.com-rt://%/ticket/'. $_,
+        ENTRYAGGREGATOR => 'OR',
+    ) foreach $self->Merged;
+    $links->Limit(
+        FIELD => 'Type',
+        VALUE => $type,
+    ) if $type;
+
+    return $links;
+}
+
+
+
+=head2 DeleteLink
+
+Delete a link. takes a paramhash of Base, Target, Type, Silent,
+SilentBase and SilentTarget. Either Base or Target must be null.
+The null value will be replaced with this ticket's id.
+
+If Silent is true then no transaction would be recorded, in other
+case you can control creation of transactions on both base and
+target with SilentBase and SilentTarget respectively. By default
+both transactions are created.
+
+=cut 
+
+sub DeleteLink {
+    my $self = shift;
+    my %args = (
+        Base   => undef,
+        Target => undef,
+        Type   => undef,
+        Silent => undef,
+        SilentBase   => undef,
+        SilentTarget => undef,
+        @_
+    );
+
+    unless ( $args{'Target'} || $args{'Base'} ) {
+        $RT::Logger->error("Base or Target must be specified");
+        return ( 0, $self->loc('Either base or target must be specified') );
     }
-    
-    my ($status_val, $status_msg) = $self->__Set(Field => 'Status',
-                                                Value => 'resolved');
-    
-    unless ($status_val) {
-       $RT::Logger->error("$self couldn't set status to resolved.".
-                          "RT's Database may be inconsistent.");
-    }      
-    
-    #make a new link: this ticket is merged into that other ticket.
-    $self->AddLink( Type =>'MergedInto',
-                   Target => $NewTicket->Id() );
-    
-    #add all of this ticket's watchers to that ticket.
-    my $watchers = $self->Watchers();
-    
-    while (my $watcher = $watchers->Next()) {
-       unless (
-               ($watcher->Owner && 
-               $NewTicket->IsWatcher (Type => $watcher->Type,
-                                      Id => $watcher->Owner)) or 
-               ($watcher->Email && 
-                $NewTicket->IsWatcher (Type => $watcher->Type,
-                                       Id => $watcher->Email)) 
-              ) {
-           
-           
-           
-           $NewTicket->_AddWatcher(Silent => 1, 
-                                   Type => $watcher->Type, 
-                                   Email => $watcher->Email,
-                                   Owner => $watcher->Owner);
-       }
+
+    #check acls
+    my $right = 0;
+    $right++ if $self->CurrentUserHasRight('ModifyTicket');
+    if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+        return ( 0, $self->loc("Permission Denied") );
     }
-    
-    
-    #find all of the tickets that were merged into this ticket. 
-    my $old_mergees = new RT::Tickets($self->CurrentUser);
-    $old_mergees->Limit( FIELD => 'EffectiveId',
-                        OPERATOR => '=',
-                        VALUE => $self->Id );
-    
-    #   update their EffectiveId fields to the new ticket's id
-    while (my $ticket = $old_mergees->Next()) {
-       my ($val, $msg) = $ticket->__Set(Field => 'EffectiveId', 
-                                        Value => $NewTicket->Id());
-    }  
-    $NewTicket->_SetLastUpdated;
 
-    return ($TransactionObj, "Merge Successful");
-}  
+    # If the other URI is an RT::Ticket, we want to make sure the user
+    # can modify it too...
+    my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+    return (0, $msg) unless $status;
+    if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+        $right++;
+    }
+    if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+         ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+    {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+    return ( 0, $Msg ) unless $val;
+
+    return ( $val, $Msg ) if $args{'Silent'};
+
+    my ($direction, $remote_link);
+
+    if ( $args{'Base'} ) {
+        $remote_link = $args{'Base'};
+        $direction = 'Target';
+    }
+    elsif ( $args{'Target'} ) {
+        $remote_link = $args{'Target'};
+        $direction = 'Base';
+    } 
+
+    my $remote_uri = RT::URI->new( $self->CurrentUser );
+    $remote_uri->FromURI( $remote_link );
+
+    unless ( $args{ 'Silent'. $direction } ) {
+        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+            Type      => 'DeleteLink',
+            Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
+            OldValue  => $remote_uri->URI || $remote_link,
+            TimeTaken => 0
+        );
+        $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+    }
+
+    if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+        my $OtherObj = $remote_uri->Object;
+        my ( $val, $Msg ) = $OtherObj->_NewTransaction(
+            Type           => 'DeleteLink',
+            Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
+            OldValue       => $self->URI,
+            ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+            TimeTaken      => 0,
+        );
+        $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
+    }
+
+    return ( $val, $Msg );
+}
+
+
+
+=head2 AddLink
+
+Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
+
+If Silent is true then no transaction would be recorded, in other
+case you can control creation of transactions on both base and
+target with SilentBase and SilentTarget respectively. By default
+both transactions are created.
+
+=cut
+
+sub AddLink {
+    my $self = shift;
+    my %args = ( Target       => '',
+                 Base         => '',
+                 Type         => '',
+                 Silent       => undef,
+                 SilentBase   => undef,
+                 SilentTarget => undef,
+                 @_ );
+
+    unless ( $args{'Target'} || $args{'Base'} ) {
+        $RT::Logger->error("Base or Target must be specified");
+        return ( 0, $self->loc('Either base or target must be specified') );
+    }
+
+    my $right = 0;
+    $right++ if $self->CurrentUserHasRight('ModifyTicket');
+    if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    # If the other URI is an RT::Ticket, we want to make sure the user
+    # can modify it too...
+    my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+    return (0, $msg) unless $status;
+    if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+        $right++;
+    }
+    if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+         ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+    {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    return ( 0, "Can't link to a deleted ticket" )
+      if $other_ticket && lc $other_ticket->Status eq 'deleted';
+
+    return $self->_AddLink(%args);
+}
+
+sub __GetTicketFromURI {
+    my $self = shift;
+    my %args = ( URI => '', @_ );
+
+    # If the other URI is an RT::Ticket, we want to make sure the user
+    # can modify it too...
+    my $uri_obj = RT::URI->new( $self->CurrentUser );
+    unless ($uri_obj->FromURI( $args{'URI'} )) {
+        my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
+        $RT::Logger->warning( $msg );
+        return( 0, $msg );
+    }
+    my $obj = $uri_obj->Resolver->Object;
+    unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
+        return (1, 'Found not a ticket', undef);
+    }
+    return (1, 'Found ticket', $obj);
+}
+
+=head2 _AddLink  
+
+Private non-acled variant of AddLink so that links can be added during create.
+
+=cut
+
+sub _AddLink {
+    my $self = shift;
+    my %args = ( Target       => '',
+                 Base         => '',
+                 Type         => '',
+                 Silent       => undef,
+                 SilentBase   => undef,
+                 SilentTarget => undef,
+                 @_ );
+
+    my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+    return ($val, $msg) if !$val || $exist;
+    return ($val, $msg) if $args{'Silent'};
+
+    my ($direction, $remote_link);
+    if ( $args{'Target'} ) {
+        $remote_link  = $args{'Target'};
+        $direction    = 'Base';
+    } elsif ( $args{'Base'} ) {
+        $remote_link  = $args{'Base'};
+        $direction    = 'Target';
+    }
+
+    my $remote_uri = RT::URI->new( $self->CurrentUser );
+    $remote_uri->FromURI( $remote_link );
+
+    unless ( $args{ 'Silent'. $direction } ) {
+        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+            Type      => 'AddLink',
+            Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
+            NewValue  =>  $remote_uri->URI || $remote_link,
+            TimeTaken => 0
+        );
+        $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+    }
+
+    if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+        my $OtherObj = $remote_uri->Object;
+        my ( $val, $msg ) = $OtherObj->_NewTransaction(
+            Type           => 'AddLink',
+            Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
+            NewValue       => $self->URI,
+            ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+            TimeTaken      => 0,
+        );
+        $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
+    }
+
+    return ( $val, $msg );
+}
+
+
+
+
+=head2 MergeInto
+
+MergeInto take the id of the ticket to merge this ticket into.
+
+=cut
+
+sub MergeInto {
+    my $self      = shift;
+    my $ticket_id = shift;
+
+    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    # Load up the new ticket.
+    my $MergeInto = RT::Ticket->new($self->CurrentUser);
+    $MergeInto->Load($ticket_id);
+
+    # make sure it exists.
+    unless ( $MergeInto->Id ) {
+        return ( 0, $self->loc("New ticket doesn't exist") );
+    }
+
+    # Make sure the current user can modify the new ticket.
+    unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    delete $MERGE_CACHE{'effective'}{ $self->id };
+    delete @{ $MERGE_CACHE{'merged'} }{
+        $ticket_id, $MergeInto->id, $self->id
+    };
+
+    $RT::Handle->BeginTransaction();
+
+    $self->_MergeInto( $MergeInto );
+
+    $RT::Handle->Commit();
+
+    return ( 1, $self->loc("Merge Successful") );
+}
+
+sub _MergeInto {
+    my $self      = shift;
+    my $MergeInto = shift;
+
+
+    # We use EffectiveId here even though it duplicates information from
+    # the links table becasue of the massive performance hit we'd take
+    # by trying to do a separate database query for merge info everytime 
+    # loaded a ticket. 
+
+    #update this ticket's effective id to the new ticket's id.
+    my ( $id_val, $id_msg ) = $self->__Set(
+        Field => 'EffectiveId',
+        Value => $MergeInto->Id()
+    );
+
+    unless ($id_val) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
+    }
+
+
+    my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
+    if ( $force_status && $force_status ne $self->__Value('Status') ) {
+        my ( $status_val, $status_msg )
+            = $self->__Set( Field => 'Status', Value => $force_status );
+
+        unless ($status_val) {
+            $RT::Handle->Rollback();
+            $RT::Logger->error(
+                "Couldn't set status to $force_status. RT's Database may be inconsistent."
+            );
+            return ( 0, $self->loc("Merge failed. Couldn't set Status") );
+        }
+    }
+
+    # update all the links that point to that old ticket
+    my $old_links_to = RT::Links->new($self->CurrentUser);
+    $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
+
+    my %old_seen;
+    while (my $link = $old_links_to->Next) {
+        if (exists $old_seen{$link->Base."-".$link->Type}) {
+            $link->Delete;
+        }   
+        elsif ($link->Base eq $MergeInto->URI) {
+            $link->Delete;
+        } else {
+            # First, make sure the link doesn't already exist. then move it over.
+            my $tmp = RT::Link->new(RT->SystemUser);
+            $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
+            if ($tmp->id)   {
+                    $link->Delete;
+            } else { 
+                $link->SetTarget($MergeInto->URI);
+                $link->SetLocalTarget($MergeInto->id);
+            }
+            $old_seen{$link->Base."-".$link->Type} =1;
+        }
+
+    }
+
+    my $old_links_from = RT::Links->new($self->CurrentUser);
+    $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
+
+    while (my $link = $old_links_from->Next) {
+        if (exists $old_seen{$link->Type."-".$link->Target}) {
+            $link->Delete;
+        }   
+        if ($link->Target eq $MergeInto->URI) {
+            $link->Delete;
+        } else {
+            # First, make sure the link doesn't already exist. then move it over.
+            my $tmp = RT::Link->new(RT->SystemUser);
+            $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
+            if ($tmp->id)   {
+                    $link->Delete;
+            } else { 
+                $link->SetBase($MergeInto->URI);
+                $link->SetLocalBase($MergeInto->id);
+                $old_seen{$link->Type."-".$link->Target} =1;
+            }
+        }
+
+    }
+
+    # Update time fields
+    foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
+
+        my $mutator = "Set$type";
+        $MergeInto->$mutator(
+            ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
+
+    }
+#add all of this ticket's watchers to that ticket.
+    foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
+
+        my $people = $self->$watcher_type->MembersObj;
+        my $addwatcher_type =  $watcher_type;
+        $addwatcher_type  =~ s/s$//;
+
+        while ( my $watcher = $people->Next ) {
+            
+           my ($val, $msg) =  $MergeInto->_AddWatcher(
+                Type        => $addwatcher_type,
+                Silent => 1,
+                PrincipalId => $watcher->MemberId
+            );
+            unless ($val) {
+                $RT::Logger->debug($msg);
+            }
+    }
+
+    }
+
+    #find all of the tickets that were merged into this ticket. 
+    my $old_mergees = RT::Tickets->new( $self->CurrentUser );
+    $old_mergees->Limit(
+        FIELD    => 'EffectiveId',
+        OPERATOR => '=',
+        VALUE    => $self->Id
+    );
+
+    #   update their EffectiveId fields to the new ticket's id
+    while ( my $ticket = $old_mergees->Next() ) {
+        my ( $val, $msg ) = $ticket->__Set(
+            Field => 'EffectiveId',
+            Value => $MergeInto->Id()
+        );
+    }
+
+    #make a new link: this ticket is merged into that other ticket.
+    $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
+
+    $MergeInto->_SetLastUpdated;    
+}
+
+=head2 Merged
+
+Returns list of tickets' ids that's been merged into this ticket.
+
+=cut
+
+sub Merged {
+    my $self = shift;
+
+    my $id = $self->id;
+    return @{ $MERGE_CACHE{'merged'}{ $id } }
+        if $MERGE_CACHE{'merged'}{ $id };
+
+    my $mergees = RT::Tickets->new( $self->CurrentUser );
+    $mergees->Limit(
+        FIELD    => 'EffectiveId',
+        VALUE    => $id,
+    );
+    $mergees->Limit(
+        FIELD    => 'id',
+        OPERATOR => '!=',
+        VALUE    => $id,
+    );
+    return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
+        = map $_->id, @{ $mergees->ItemsArrayRef || [] };
+}
+
+
+
+
+
+=head2 OwnerObj
+
+Takes nothing and returns an RT::User object of 
+this ticket's owner
+
+=cut
+
+sub OwnerObj {
+    my $self = shift;
+
+    #If this gets ACLed, we lose on a rights check in User.pm and
+    #get deep recursion. if we need ACLs here, we need
+    #an equiv without ACLs
+
+    my $owner = RT::User->new( $self->CurrentUser );
+    $owner->Load( $self->__Value('Owner') );
+
+    #Return the owner object
+    return ($owner);
+}
+
+
+
+=head2 OwnerAsString
+
+Returns the owner's email address
+
+=cut
+
+sub OwnerAsString {
+    my $self = shift;
+    return ( $self->OwnerObj->EmailAddress );
+
+}
+
+
+
+=head2 SetOwner
+
+Takes two arguments:
+     the Id or Name of the owner 
+and  (optionally) the type of the SetOwner Transaction. It defaults
+to 'Set'.  'Steal' is also a valid option.
+
+
+=cut
+
+sub SetOwner {
+    my $self     = shift;
+    my $NewOwner = shift;
+    my $Type     = shift || "Set";
+
+    $RT::Handle->BeginTransaction();
+
+    $self->_SetLastUpdated(); # lock the ticket
+    $self->Load( $self->id ); # in case $self changed while waiting for lock
+
+    my $OldOwnerObj = $self->OwnerObj;
+
+    my $NewOwnerObj = RT::User->new( $self->CurrentUser );
+    $NewOwnerObj->Load( $NewOwner );
+    unless ( $NewOwnerObj->Id ) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("That user does not exist") );
+    }
+
+
+    # must have ModifyTicket rights
+    # or TakeTicket/StealTicket and $NewOwner is self
+    # see if it's a take
+    if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
+        unless (    $self->CurrentUserHasRight('ModifyTicket')
+                 || $self->CurrentUserHasRight('TakeTicket') ) {
+            $RT::Handle->Rollback();
+            return ( 0, $self->loc("Permission Denied") );
+        }
+    }
+
+    # see if it's a steal
+    elsif (    $OldOwnerObj->Id != RT->Nobody->Id
+            && $OldOwnerObj->Id != $self->CurrentUser->id ) {
+
+        unless (    $self->CurrentUserHasRight('ModifyTicket')
+                 || $self->CurrentUserHasRight('StealTicket') ) {
+            $RT::Handle->Rollback();
+            return ( 0, $self->loc("Permission Denied") );
+        }
+    }
+    else {
+        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+            $RT::Handle->Rollback();
+            return ( 0, $self->loc("Permission Denied") );
+        }
+    }
+
+    # If we're not stealing and the ticket has an owner and it's not
+    # the current user
+    if ( $Type ne 'Steal' and $Type ne 'Force'
+         and $OldOwnerObj->Id != RT->Nobody->Id
+         and $OldOwnerObj->Id != $self->CurrentUser->Id )
+    {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("You can only take tickets that are unowned") )
+            if $NewOwnerObj->id == $self->CurrentUser->id;
+        return (
+            0,
+            $self->loc("You can only reassign tickets that you own or that are unowned" )
+        );
+    }
+
+    #If we've specified a new owner and that user can't modify the ticket
+    elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("That user may not own tickets in that queue") );
+    }
+
+    # If the ticket has an owner and it's the new owner, we don't need
+    # To do anything
+    elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("That user already owns that ticket") );
+    }
+
+    # Delete the owner in the owner group, then add a new one
+    # TODO: is this safe? it's not how we really want the API to work
+    # for most things, but it's fast.
+    my ( $del_id, $del_msg );
+    for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
+        ($del_id, $del_msg) = $owner->Delete();
+        last unless ($del_id);
+    }
+
+    unless ($del_id) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
+    }
+
+    my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
+                                       PrincipalId => $NewOwnerObj->PrincipalId,
+                                       InsideTransaction => 1 );
+    unless ($add_id) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
+    }
+
+    # We call set twice with slightly different arguments, so
+    # as to not have an SQL transaction span two RT transactions
+
+    my ( $val, $msg ) = $self->_Set(
+                      Field             => 'Owner',
+                      RecordTransaction => 0,
+                      Value             => $NewOwnerObj->Id,
+                      TimeTaken         => 0,
+                      TransactionType   => 'Set',
+                      CheckACL          => 0,                  # don't check acl
+    );
+
+    unless ($val) {
+        $RT::Handle->Rollback;
+        return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
+    }
+
+    ($val, $msg) = $self->_NewTransaction(
+        Type      => 'Set',
+        Field     => 'Owner',
+        NewValue  => $NewOwnerObj->Id,
+        OldValue  => $OldOwnerObj->Id,
+        TimeTaken => 0,
+    );
+
+    if ( $val ) {
+        $msg = $self->loc( "Owner changed from [_1] to [_2]",
+                           $OldOwnerObj->Name, $NewOwnerObj->Name );
+    }
+    else {
+        $RT::Handle->Rollback();
+        return ( 0, $msg );
+    }
+
+    $RT::Handle->Commit();
+
+    return ( $val, $msg );
+}
+
+
+
+=head2 Take
+
+A convenince method to set the ticket's owner to the current user
+
+=cut
+
+sub Take {
+    my $self = shift;
+    return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
+}
+
+
+
+=head2 Untake
+
+Convenience method to set the owner to 'nobody' if the current user is the owner.
+
+=cut
+
+sub Untake {
+    my $self = shift;
+    return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
+}
+
+
+
+=head2 Steal
+
+A convenience method to change the owner of the current ticket to the
+current user. Even if it's owned by another user.
+
+=cut
+
+sub Steal {
+    my $self = shift;
+
+    if ( $self->IsOwner( $self->CurrentUser ) ) {
+        return ( 0, $self->loc("You already own this ticket") );
+    }
+    else {
+        return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
+
+    }
+
+}
+
+
+
+
+
+=head2 ValidateStatus STATUS
+
+Takes a string. Returns true if that status is a valid status for this ticket.
+Returns false otherwise.
+
+=cut
+
+sub ValidateStatus {
+    my $self   = shift;
+    my $status = shift;
+
+    #Make sure the status passed in is valid
+    return 1 if $self->QueueObj->IsValidStatus($status);
+
+    my $i = 0;
+    while ( my $caller = (caller($i++))[3] ) {
+        return 1 if $caller eq 'RT::Ticket::SetQueue';
+    }
+
+    return 0;
+}
+
+sub Status {
+    my $self = shift;
+    my $value = $self->_Value( 'Status' );
+    return $value unless $self->QueueObj;
+    return $self->QueueObj->Lifecycle->CanonicalCase( $value );
+}
+
+=head2 SetStatus STATUS
+
+Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
+
+Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
+If FORCE is true, ignore unresolved dependencies and force a status change.
+if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
+is not set and the status is changed from initial to not initial. 
+
+=cut
+
+sub SetStatus {
+    my $self = shift;
+    my %args;
+    if (@_ == 1) {
+        $args{Status} = shift;
+    }
+    else {
+        %args = (@_);
+    }
+
+    # this only allows us to SetStarted, not we must SetStarted.
+    # this option was added for rtir initially
+    $args{SetStarted} = 1 unless exists $args{SetStarted};
+
+
+    my $lifecycle = $self->QueueObj->Lifecycle;
+
+    my $new = lc $args{'Status'};
+    unless ( $lifecycle->IsValid( $new ) ) {
+        return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
+    }
+
+    my $old = $self->__Value('Status');
+    unless ( $lifecycle->IsTransition( $old => $new ) ) {
+        return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
+    }
+
+    my $check_right = $lifecycle->CheckRight( $old => $new );
+    unless ( $self->CurrentUserHasRight( $check_right ) ) {
+        return ( 0, $self->loc('Permission Denied') );
+    }
+
+    if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
+        return (0, $self->loc('That ticket has unresolved dependencies'));
+    }
+
+    my $now = RT::Date->new( $self->CurrentUser );
+    $now->SetToNow();
+
+    my $raw_started = RT::Date->new(RT->SystemUser);
+    $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
+
+    #If we're changing the status from new, record that we've started
+    if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
+        #Set the Started time to "now"
+        $self->_Set(
+            Field             => 'Started',
+            Value             => $now->ISO,
+            RecordTransaction => 0
+        );
+    }
+
+    #When we close a ticket, set the 'Resolved' attribute to now.
+    # It's misnamed, but that's just historical.
+    if ( $lifecycle->IsInactive($new) ) {
+        $self->_Set(
+            Field             => 'Resolved',
+            Value             => $now->ISO,
+            RecordTransaction => 0,
+        );
+    }
+
+    #Actually update the status
+    my ($val, $msg)= $self->_Set(
+        Field           => 'Status',
+        Value           => $new,
+        TimeTaken       => 0,
+        CheckACL        => 0,
+        TransactionType => 'Status',
+    );
+    return ($val, $msg);
+}
+
+
+
+=head2 Delete
+
+Takes no arguments. Marks this ticket for garbage collection
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
+        return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
+    }
+    return ( $self->SetStatus('deleted') );
+}
+
+
+=head2 SetTold ISO  [TIMETAKEN]
+
+Updates the told and records a transaction
+
+=cut
+
+sub SetTold {
+    my $self = shift;
+    my $told;
+    $told = shift if (@_);
+    my $timetaken = shift || 0;
+
+    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my $datetold = RT::Date->new( $self->CurrentUser );
+    if ($told) {
+        $datetold->Set( Format => 'iso',
+                        Value  => $told );
+    }
+    else {
+        $datetold->SetToNow();
+    }
+
+    return ( $self->_Set( Field           => 'Told',
+                          Value           => $datetold->ISO,
+                          TimeTaken       => $timetaken,
+                          TransactionType => 'Told' ) );
+}
+
+=head2 _SetTold
+
+Updates the told without a transaction or acl check. Useful when we're sending replies.
+
+=cut
+
+sub _SetTold {
+    my $self = shift;
+
+    my $now = RT::Date->new( $self->CurrentUser );
+    $now->SetToNow();
+
+    #use __Set to get no ACLs ;)
+    return ( $self->__Set( Field => 'Told',
+                           Value => $now->ISO ) );
+}
+
+=head2 SeenUpTo
+
+
+=cut
+
+sub SeenUpTo {
+    my $self = shift;
+    my $uid = $self->CurrentUser->id;
+    my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
+    return if $attr && $attr->Content gt $self->LastUpdated;
+
+    my $txns = $self->Transactions;
+    $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
+    $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+    $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
+    $txns->Limit(
+        FIELD => 'Created',
+        OPERATOR => '>',
+        VALUE => $attr->Content
+    ) if $attr;
+    $txns->RowsPerPage(1);
+    return $txns->First;
+}
+
+=head2 RanTransactionBatch
+
+Acts as a guard around running TransactionBatch scrips.
+
+Should be false until you enter the code that runs TransactionBatch scrips
+
+Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
+
+=cut
+
+sub RanTransactionBatch {
+    my $self = shift;
+    my $val = shift;
+
+    if ( defined $val ) {
+        return $self->{_RanTransactionBatch} = $val;
+    } else {
+        return $self->{_RanTransactionBatch};
+    }
+
+}
+
+
+=head2 TransactionBatch
+
+Returns an array reference of all transactions created on this ticket during
+this ticket object's lifetime or since last application of a batch, or undef
+if there were none.
+
+Only works when the C<UseTransactionBatch> config option is set to true.
+
+=cut
+
+sub TransactionBatch {
+    my $self = shift;
+    return $self->{_TransactionBatch};
+}
+
+=head2 ApplyTransactionBatch
+
+Applies scrips on the current batch of transactions and shinks it. Usually
+batch is applied when object is destroyed, but in some cases it's too late.
+
+=cut
+
+sub ApplyTransactionBatch {
+    my $self = shift;
+
+    my $batch = $self->TransactionBatch;
+    return unless $batch && @$batch;
+
+    $self->_ApplyTransactionBatch;
+
+    $self->{_TransactionBatch} = [];
+}
+
+sub _ApplyTransactionBatch {
+    my $self = shift;
+
+    return if $self->RanTransactionBatch;
+    $self->RanTransactionBatch(1);
+
+    my $still_exists = RT::Ticket->new( RT->SystemUser );
+    $still_exists->Load( $self->Id );
+    if (not $still_exists->Id) {
+        # The ticket has been removed from the database, but we still
+        # have pending TransactionBatch txns for it.  Unfortunately,
+        # because it isn't in the DB anymore, attempting to run scrips
+        # on it may produce unpredictable results; simply drop the
+        # batched transactions.
+        $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips!  Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
+        return;
+    }
+
+    my $batch = $self->TransactionBatch;
+
+    my %seen;
+    my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
+
+    require RT::Scrips;
+    RT::Scrips->new(RT->SystemUser)->Apply(
+        Stage          => 'TransactionBatch',
+        TicketObj      => $self,
+        TransactionObj => $batch->[0],
+        Type           => $types,
+    );
+
+    # Entry point of the rule system
+    my $rules = RT::Ruleset->FindAllRules(
+        Stage          => 'TransactionBatch',
+        TicketObj      => $self,
+        TransactionObj => $batch->[0],
+        Type           => $types,
+    );
+    RT::Ruleset->CommitRules($rules);
+}
+
+sub DESTROY {
+    my $self = shift;
+
+    # DESTROY methods need to localize $@, or it may unset it.  This
+    # causes $m->abort to not bubble all of the way up.  See perlbug
+    # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
+    local $@;
+
+    # The following line eliminates reentrancy.
+    # It protects against the fact that perl doesn't deal gracefully
+    # when an object's refcount is changed in its destructor.
+    return if $self->{_Destroyed}++;
+
+    if (in_global_destruction()) {
+       unless ($ENV{'HARNESS_ACTIVE'}) {
+            warn "Too late to safely run transaction-batch scrips!"
+                ." This is typically caused by using ticket objects"
+                ." at the top-level of a script which uses the RT API."
+               ." Be sure to explicitly undef such ticket objects,"
+                ." or put them inside of a lexical scope.";
+        }
+        return;
+    }
+
+    return $self->ApplyTransactionBatch;
+}
+
+
+
+
+sub _OverlayAccessible {
+    {
+        EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
+          Queue           => { 'read' => 1,  'write' => 1 },
+          Requestors      => { 'read' => 1,  'write' => 1 },
+          Owner           => { 'read' => 1,  'write' => 1 },
+          Subject         => { 'read' => 1,  'write' => 1 },
+          InitialPriority => { 'read' => 1,  'write' => 1 },
+          FinalPriority   => { 'read' => 1,  'write' => 1 },
+          Priority        => { 'read' => 1,  'write' => 1 },
+          Status          => { 'read' => 1,  'write' => 1 },
+          TimeEstimated      => { 'read' => 1,  'write' => 1 },
+          TimeWorked      => { 'read' => 1,  'write' => 1 },
+          TimeLeft        => { 'read' => 1,  'write' => 1 },
+          Told            => { 'read' => 1,  'write' => 1 },
+          Resolved        => { 'read' => 1 },
+          Type            => { 'read' => 1 },
+          Starts        => { 'read' => 1, 'write' => 1 },
+          Started       => { 'read' => 1, 'write' => 1 },
+          Due           => { 'read' => 1, 'write' => 1 },
+          Creator       => { 'read' => 1, 'auto'  => 1 },
+          Created       => { 'read' => 1, 'auto'  => 1 },
+          LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
+          LastUpdated   => { 'read' => 1, 'auto'  => 1 }
+    };
+
+}
+
+
+
+sub _Set {
+    my $self = shift;
+
+    my %args = ( Field             => undef,
+                 Value             => undef,
+                 TimeTaken         => 0,
+                 RecordTransaction => 1,
+                 UpdateTicket      => 1,
+                 CheckACL          => 1,
+                 TransactionType   => 'Set',
+                 @_ );
+
+    if ($args{'CheckACL'}) {
+      unless ( $self->CurrentUserHasRight('ModifyTicket')) {
+          return ( 0, $self->loc("Permission Denied"));
+      }
+   }
+
+    unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
+        $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
+        return(0, $self->loc("Internal Error"));
+    }
+
+    #if the user is trying to modify the record
+
+    #Take care of the old value we really don't want to get in an ACL loop.
+    # so ask the super::_Value
+    my $Old = $self->SUPER::_Value("$args{'Field'}");
+    
+    my ($ret, $msg);
+    if ( $args{'UpdateTicket'}  ) {
+
+        #Set the new value
+        ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
+                                                Value => $args{'Value'} );
+    
+        #If we can't actually set the field to the value, don't record
+        # a transaction. instead, get out of here.
+        return ( 0, $msg ) unless $ret;
+    }
+
+    if ( $args{'RecordTransaction'} == 1 ) {
+
+        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+                                               Type => $args{'TransactionType'},
+                                               Field     => $args{'Field'},
+                                               NewValue  => $args{'Value'},
+                                               OldValue  => $Old,
+                                               TimeTaken => $args{'TimeTaken'},
+        );
+        # Ensure that we can read the transaction, even if the change
+        # just made the ticket unreadable to us
+        $TransObj->{ _object_is_readable } = 1;
+        return ( $Trans, scalar $TransObj->BriefDescription );
+    }
+    else {
+        return ( $ret, $msg );
+    }
+}
+
+
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value {
+
+    my $self  = shift;
+    my $field = shift;
+
+    #if the field is public, return it.
+    if ( $self->_Accessible( $field, 'public' ) ) {
+
+        #$RT::Logger->debug("Skipping ACL check for $field");
+        return ( $self->SUPER::_Value($field) );
+
+    }
+
+    #If the current user doesn't have ACLs, don't let em at it.  
+
+    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+        return (undef);
+    }
+    return ( $self->SUPER::_Value($field) );
+
+}
+
+
+
+=head2 _UpdateTimeTaken
+
+This routine will increment the timeworked counter. it should
+only be called from _NewTransaction 
+
+=cut
+
+sub _UpdateTimeTaken {
+    my $self    = shift;
+    my $Minutes = shift;
+    my ($Total);
+
+    $Total = $self->SUPER::_Value("TimeWorked");
+    $Total = ( $Total || 0 ) + ( $Minutes || 0 );
+    $self->SUPER::_Set(
+        Field => "TimeWorked",
+        Value => $Total
+    );
+
+    return ($Total);
+}
+
+
+
+
+
+=head2 CurrentUserHasRight
+
+  Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
+1 if the user has that right. It returns 0 if the user doesn't have that right.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self  = shift;
+    my $right = shift;
+
+    return $self->CurrentUser->PrincipalObj->HasRight(
+        Object => $self,
+        Right  => $right,
+    )
+}
+
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the ticket, using ShowTicket
+
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+    return $self->CurrentUserHasRight('ShowTicket');
+}
+
+=head2 HasRight
+
+ Takes a paramhash with the attributes 'Right' and 'Principal'
+  'Right' is a ticket-scoped textual right from RT::ACE 
+  'Principal' is an RT::User object
+
+  Returns 1 if the principal has the right. Returns undef if not.
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = (
+        Right     => undef,
+        Principal => undef,
+        @_
+    );
+
+    unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
+    {
+        Carp::cluck("Principal attrib undefined for Ticket::HasRight");
+        $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
+        return(undef);
+    }
+
+    return (
+        $args{'Principal'}->HasRight(
+            Object => $self,
+            Right     => $args{'Right'}
+          )
+    );
+}
+
+
+
+=head2 Reminders
+
+Return the Reminders object for this ticket. (It's an RT::Reminders object.)
+It isn't acutally a searchbuilder collection itself.
+
+=cut
+
+sub Reminders {
+    my $self = shift;
+    
+    unless ($self->{'__reminders'}) {
+        $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
+        $self->{'__reminders'}->Ticket($self->id);
+    }
+    return $self->{'__reminders'};
+
+}
+
+
+
+
+=head2 Transactions
+
+  Returns an RT::Transactions object of all transactions on this ticket
+
+=cut
+
+sub Transactions {
+    my $self = shift;
+
+    my $transactions = RT::Transactions->new( $self->CurrentUser );
+
+    #If the user has no rights, return an empty object
+    if ( $self->CurrentUserHasRight('ShowTicket') ) {
+        $transactions->LimitToTicket($self->id);
+
+        # if the user may not see comments do not return them
+        unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
+            $transactions->Limit(
+                SUBCLAUSE => 'acl',
+                FIELD    => 'Type',
+                OPERATOR => '!=',
+                VALUE    => "Comment"
+            );
+            $transactions->Limit(
+                SUBCLAUSE => 'acl',
+                FIELD    => 'Type',
+                OPERATOR => '!=',
+                VALUE    => "CommentEmailRecord",
+                ENTRYAGGREGATOR => 'AND'
+            );
+
+        }
+    } else {
+        $transactions->Limit(
+            SUBCLAUSE => 'acl',
+            FIELD    => 'id',
+            VALUE    => 0,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+
+    return ($transactions);
+}
+
+
+
+
+=head2 TransactionCustomFields
+
+    Returns the custom fields that transactions on tickets will have.
+
+=cut
+
+sub TransactionCustomFields {
+    my $self = shift;
+    my $cfs = $self->QueueObj->TicketTransactionCustomFields;
+    $cfs->SetContextObject( $self );
+    return $cfs;
+}
+
+
+=head2 LoadCustomFieldByIdentifier
+
+Finds and returns the custom field of the given name for the ticket,
+overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
+queue-specific CFs before global ones.
+
+=cut
+
+sub LoadCustomFieldByIdentifier {
+    my $self  = shift;
+    my $field = shift;
+
+    return $self->SUPER::LoadCustomFieldByIdentifier($field)
+        if ref $field or $field =~ /^\d+$/;
+
+    my $cf = RT::CustomField->new( $self->CurrentUser );
+    $cf->SetContextObject( $self );
+    $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
+    $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
+    return $cf;
+}
+
+
+=head2 CustomFieldLookupType
+
+Returns the RT::Ticket lookup type, which can be passed to 
+RT::CustomField->Create() via the 'LookupType' hash key.
+
+=cut
+
+
+sub CustomFieldLookupType {
+    "RT::Queue-RT::Ticket";
+}
+
+=head2 ACLEquivalenceObjects
+
+This method returns a list of objects for which a user's rights also apply
+to this ticket. Generally, this is only the ticket's queue, but some RT 
+extensions may make other objects available too.
+
+This method is called from L<RT::Principal/HasRight>.
+
+=cut
+
+sub ACLEquivalenceObjects {
+    my $self = shift;
+    return $self->QueueObj;
+
+}
+
+
+1;
+
+=head1 AUTHOR
+
+Jesse Vincent, jesse@bestpractical.com
+
+=head1 SEE ALSO
+
+RT
+
+=cut
+
+
+use RT::Queue;
+use base 'RT::Record';
+
+sub Table {'Tickets'}
+
+
+
+
+
+
+=head2 id
+
+Returns the current value of id.
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=head2 EffectiveId
+
+Returns the current value of EffectiveId.
+(In the database, EffectiveId is stored as int(11).)
+
+
+
+=head2 SetEffectiveId VALUE
+
+
+Set EffectiveId to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, EffectiveId will be stored as a int(11).)
+
+
+=cut
+
+
+=head2 Queue
+
+Returns the current value of Queue.
+(In the database, Queue is stored as int(11).)
+
+
+
+=head2 SetQueue VALUE
+
+
+Set Queue to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Queue will be stored as a int(11).)
+
+
+=cut
+
+
+=head2 Type
+
+Returns the current value of Type.
+(In the database, Type is stored as varchar(16).)
+
+
+
+=head2 SetType VALUE
 
-# }}}
 
-# }}}
+Set Type to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Type will be stored as a varchar(16).)
 
-# {{{ Routines dealing with keywords
 
-# {{{ sub KeywordsObj
+=cut
 
-=head2 KeywordsObj [KEYWORD_SELECT_ID]
 
-  Returns an B<RT::ObjectKeywords> object preloaded with this ticket's ObjectKeywords.
-If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword
-select.
+=head2 IssueStatement
 
-=cut
+Returns the current value of IssueStatement.
+(In the database, IssueStatement is stored as int(11).)
 
-sub KeywordsObj {
-    my $self = shift;
-    my $keyword_select; 
-    
-    $keyword_select = shift if (@_);
-    
-    use RT::ObjectKeywords;
-    my $Keywords = new RT::ObjectKeywords($self->CurrentUser);
 
-    #ACL check
-    if ($self->CurrentUserHasRight('ShowTicket')) {
-       $Keywords->LimitToTicket($self->id);
-       if ($keyword_select) {
-           $Keywords->LimitToKeywordSelect($keyword_select);
-       }       
-    }
-    return ($Keywords);
-}
-# }}}
 
-# {{{ sub AddKeyword
+=head2 SetIssueStatement VALUE
 
-=head2 AddKeyword
 
-Takes a paramhash of Keyword and KeywordSelect.  If Keyword is a valid choice
-for KeywordSelect, creates a KeywordObject.  If the KeywordSelect says this should
-be a single KeywordObject, automatically removes the old value.
+Set IssueStatement to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, IssueStatement will be stored as a int(11).)
 
- Issues: probably doesn't enforce the depth restrictions or make sure that keywords
-are coming from the right part of the tree. really should.
 
 =cut
 
-sub AddKeyword {
-    my $self = shift;
-   #ACL check
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, 'Permission Denied');
-    }
-    
-    return($self->_AddKeyword(@_));
-    
-}
-
 
-# Helper version of AddKeyword without that pesky ACL check
-sub _AddKeyword {
-    my $self = shift;
-    my %args = ( KeywordSelect => undef,  # id of a keyword select record
-                Keyword => undef, #id of the keyword to add
-                Silent => 0,
-                @_
-              );
-    
-    my ($OldValue);
+=head2 Resolution
 
-    #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'}
+Returns the current value of Resolution.
+(In the database, Resolution is stored as int(11).)
 
-    #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue.
-    
-    my $Keyword = new RT::Keyword($self->CurrentUser);
-    unless ($Keyword->Load($args{'Keyword'}) ) {
-       $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n");
-       return(0, "Couldn't load keyword");
-    }
-    
-    my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser);
-    unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) {
-       $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'});
-       return(0, "Couldn't load keywordselect");
-    }
-    
-    my $Keywords = $self->KeywordsObj($KeywordSelectObj->id);
-
-    #If the ticket already has this keyword, just get out of here.
-    if ($Keywords->HasEntry($Keyword->id)) {
-       return(0, "That is already the current value");
-    }  
-
-    #If the keywordselect wants this to be a singleton:
-
-    if ($KeywordSelectObj->Single) {
-
-       #Whack any old values...keep track of the last value that we get.
-       #we shouldn't need a loop ehre, but we do it anyway, to try to 
-       # help keep the database clean.
-       while (my $OldKey = $Keywords->Next) {
-           $OldValue = $OldKey->KeywordObj->Name;
-           $OldKey->Delete();
-       }       
-       
-       
-    }
-
-    # create the new objectkeyword 
-    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
-    my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id,
-                                        ObjectType => 'Ticket',
-                                        ObjectId => $self->Id,
-                                        KeywordSelect => $KeywordSelectObj->Id );
-    
 
-    # record a single transaction, unless we were told not to
-    unless ($args{'Silent'}) {
-       my ($TransactionId, $Msg, $TransactionObj) = 
-         $self->_NewTransaction( Type => 'Keyword',
-                                 Field => $KeywordSelectObj->Id,
-                                 OldValue => $OldValue,
-                                 NewValue => $Keyword->Name );
-    }
-    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added.");    
 
-}      
+=head2 SetResolution VALUE
 
-# }}}
 
-# {{{ sub DeleteKeyword
+Set Resolution to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Resolution will be stored as a int(11).)
 
-=head2 DeleteKeyword
-  
-  Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this
-  ticket's object keywords.
 
 =cut
 
-sub DeleteKeyword {
-    my $self = shift;
-    my %args = ( Keyword => undef,
-                KeywordSelect => undef,
-                @_ );
 
-   #ACL check
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {    
-       return (0, 'Permission Denied');
-    }
+=head2 Owner
 
-    
-    #Load up the ObjectKeyword we\'re talking about
-    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
-    $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'},
-                              KeywordSelect => $args{'KeywordSelect'},
-                              ObjectType => 'Ticket',
-                              ObjectId => $self->id()
-                             );
-    
-    #if we can\'t find it, bail
-    unless ($ObjectKeyword->id) {
-       $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} .
-                        " for keywordselect ". $args{'KeywordSelect'} . 
-                        "for ticket ".$self->id );
-       return (undef, "Couldn't load keyword while trying to delete it.");
-    };
-    
-    #record transaction here.
-    my ($TransactionId, $Msg, $TransObj) = 
-      $self->_NewTransaction( Type => 'Keyword', 
-                             OldValue => $ObjectKeyword->KeywordObj->Name);
-    
-    $ObjectKeyword->Delete();
-    
-    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted.");
-    
-}
+Returns the current value of Owner.
+(In the database, Owner is stored as int(11).)
 
-# }}}
 
-# }}}
 
-# {{{ Routines dealing with ownership
+=head2 SetOwner VALUE
 
-# {{{ sub OwnerObj
 
-=head2 OwnerObj
+Set Owner to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Owner will be stored as a int(11).)
 
-Takes nothing and returns an RT::User object of 
-this ticket's owner
 
 =cut
 
-sub OwnerObj {
-    my $self = shift;
-    
-    #If this gets ACLed, we lose on a rights check in User.pm and
-    #get deep recursion. if we need ACLs here, we need
-    #an equiv without ACLs
-    
-    $owner = new RT::User ($self->CurrentUser);
-    $owner->Load($self->__Value('Owner'));
-    
-    #Return the owner object
-    return ($owner);
-}
 
-# }}}
+=head2 Subject
 
-# {{{ sub OwnerAsString 
+Returns the current value of Subject.
+(In the database, Subject is stored as varchar(200).)
 
-=head2 OwnerAsString
 
-Returns the owner's email address
 
-=cut
+=head2 SetSubject VALUE
 
-sub OwnerAsString {
-  my $self = shift;
-  return($self->OwnerObj->EmailAddress);
 
-}
+Set Subject to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Subject will be stored as a varchar(200).)
 
-# }}}
 
-# {{{ sub SetOwner
+=cut
 
-=head2 SetOwner
 
-Takes two arguments:
-     the Id or Name of the owner 
-and  (optionally) the type of the SetOwner Transaction. It defaults
-to 'Give'.  'Steal' is also a valid option.
+=head2 InitialPriority
 
-=cut
+Returns the current value of InitialPriority.
+(In the database, InitialPriority is stored as int(11).)
 
-sub SetOwner {
-    my $self = shift;
-    my $NewOwner = shift;
-    my $Type = shift || "Give";
-    
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
-    }  
-    
-    my $NewOwnerObj = RT::User->new($self->CurrentUser);
-    my $OldOwnerObj = $self->OwnerObj;
-  
-    $NewOwnerObj->Load($NewOwner);
-    if (!$NewOwnerObj->Id) {
-           return (0, "That user does not exist");
-    }
-    
-    #If thie ticket has an owner and it's not the current user
-    
-    if (($Type ne 'Steal' ) and ($Type ne 'Force') and #If we're not stealing
-       ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
-       ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us
-       return(0, "You can only reassign tickets that you own or that are unowned");
-    }
-    
-    #If we've specified a new owner and that user can't modify the ticket
-    elsif (($NewOwnerObj->Id) and 
-          (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket',
-                                        QueueObj => $self->QueueObj,
-                                        TicketObj => $self))
-         ) {
-       return (0, "That user may not own requests in that queue");
-    }
-  
-  
-    #If the ticket has an owner and it's the new owner, we don't need
-    #To do anything
-    elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) {
-       return(0, "That user already owns that request");
-    }
-  
-  
-    my ($trans,$msg)=$self->_Set(Field => 'Owner',
-                                Value => $NewOwnerObj->Id, 
-                                TimeTaken => 0,
-                                TransactionType => $Type);
-  
-    if ($trans) {
-       $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name;
-    }
-    return ($trans, $msg);
-         
-}
 
-# }}}
 
-# {{{ sub Take
+=head2 SetInitialPriority VALUE
 
-=head2 Take
 
-A convenince method to set the ticket's owner to the current user
+Set InitialPriority to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, InitialPriority will be stored as a int(11).)
 
-=cut
 
-sub Take {
-    my $self = shift;
-    return ($self->SetOwner($self->CurrentUser->Id, 'Take'));
-}
+=cut
 
-# }}}
 
-# {{{ sub Untake
+=head2 FinalPriority
 
-=head2 Untake
+Returns the current value of FinalPriority.
+(In the database, FinalPriority is stored as int(11).)
 
-Convenience method to set the owner to 'nobody' if the current user is the owner.
 
-=cut
 
-sub Untake {
-    my $self = shift;
-    return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake'));
-}
-# }}}
+=head2 SetFinalPriority VALUE
 
-# {{{ sub Steal 
 
-=head2 Steal
+Set FinalPriority to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, FinalPriority will be stored as a int(11).)
 
-A convenience method to change the owner of the current ticket to the
-current user. Even if it's owned by another user.
 
 =cut
 
-sub Steal {
-    my $self = shift;
-  
-    if ($self->IsOwner($self->CurrentUser)) {
-       return (0,"You already own this ticket"); 
-    } else {
-       return($self->SetOwner($self->CurrentUser->Id, 'Steal'));
-      
-    }
-  
-}
 
-# }}}
+=head2 Priority
 
-# }}}
+Returns the current value of Priority.
+(In the database, Priority is stored as int(11).)
 
-# {{{ Routines dealing with status
 
-# {{{ sub ValidateStatus 
 
-=head2 ValidateStatus STATUS
+=head2 SetPriority VALUE
+
+
+Set Priority to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Priority will be stored as a int(11).)
 
-Takes a string. Returns true if that status is a valid status for this ticket.
-Returns false otherwise.
 
 =cut
 
-sub ValidateStatus {
-    my $self = shift;
-    my $status = shift;
 
-    #Make sure the status passed in is valid
-    unless ($self->QueueObj->IsValidStatus($status)) {
-       return (undef);
-    }
-    
-    return (1);
+=head2 TimeEstimated
 
-}
+Returns the current value of TimeEstimated.
+(In the database, TimeEstimated is stored as int(11).)
 
 
-# }}}
 
-# {{{ sub SetStatus
+=head2 SetTimeEstimated VALUE
 
-=head2 SetStatus STATUS
 
-Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead.
+Set TimeEstimated to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, TimeEstimated will be stored as a int(11).)
+
 
 =cut
 
-sub SetStatus { 
-    my $self = shift;
-    my $status = shift;
 
-    #Check ACL
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, 'Permission Denied');
-    }
+=head2 TimeWorked
 
-    my $now = new RT::Date($self->CurrentUser);
-    $now->SetToNow();
-    
-    #If we're changing the status from new, record that we've started
-    if (($self->Status =~ /new/) && ($status ne 'new')) {
-       #Set the Started time to "now"
-       $self->_Set(Field => 'Started',
-                   Value => $now->ISO,
-                   RecordTransaction => 0);
-    }
-    
+Returns the current value of TimeWorked.
+(In the database, TimeWorked is stored as int(11).)
 
-    if ($status eq 'resolved') {
-       #When we resolve a ticket, set the 'Resolved' attribute to now.
-       $self->_Set(Field => 'Resolved',
-                   Value => $now->ISO, 
-                   RecordTransaction => 0);
-    }
-    
-    
-    #Actually update the status
-    return($self->_Set(Field => 'Status', 
-                      Value => $status,
-                      TimeTaken => 0,
-                      TransactionType => 'Status'));
-}
 
-# }}}
 
-# {{{ sub Kill
+=head2 SetTimeWorked VALUE
 
-=head2 Kill
 
-Takes no arguments. Marks this ticket for garbage collection
+Set TimeWorked to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, TimeWorked will be stored as a int(11).)
+
 
 =cut
 
-sub Kill {
-  my $self = shift;
-  return ($self->SetStatus('dead'));
-  # TODO: garbage collection
-}
 
-# }}}
+=head2 Status
 
-# {{{ sub Stall
+Returns the current value of Status.
+(In the database, Status is stored as varchar(64).)
 
-=head2 Stall
 
-Sets this ticket's status to stalled
 
-=cut
+=head2 SetStatus VALUE
 
-sub Stall {
-  my $self = shift;
-  return ($self->SetStatus('stalled'));
-}
 
-# }}}
+Set Status to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Status will be stored as a varchar(64).)
+
 
-# {{{ sub Open
+=cut
 
-=head2 Open
 
-Sets this ticket\'s status to Open
+=head2 TimeLeft
 
-=cut
+Returns the current value of TimeLeft.
+(In the database, TimeLeft is stored as int(11).)
 
-sub Open {
-    my $self = shift;
-    return ($self->SetStatus('open'));
-}
 
-# }}}
 
-# {{{ sub Resolve
+=head2 SetTimeLeft VALUE
 
-=head2 Resolve
 
-Sets this ticket\'s status to Resolved
+Set TimeLeft to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, TimeLeft will be stored as a int(11).)
+
 
 =cut
 
-sub Resolve {
-    my $self = shift;
-    return ($self->SetStatus('resolved'));
-}
 
-# }}}
+=head2 Told
 
-# }}}
+Returns the current value of Told.
+(In the database, Told is stored as datetime.)
 
-# {{{ Actions + Routines dealing with transactions
 
-# {{{ sub SetTold and _SetTold
 
-=head2 SetTold ISO  [TIMETAKEN]
+=head2 SetTold VALUE
 
-Updates the told and records a transaction
 
-=cut
+Set Told to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Told will be stored as a datetime.)
 
-sub SetTold {
-    my $self=shift;
-    my $told;
-    $told = shift if (@_);
-    my $timetaken=shift || 0;
-   
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
-    }
 
-    my $datetold = new RT::Date($self->CurrentUser);
-    if ($told) {
-       $datetold->Set( Format => 'iso',
-                       Value => $told);
-    }
-    else {
-        $datetold->SetToNow(); 
-    }
-    
-    return($self->_Set(Field => 'Told', 
-                      Value => $datetold->ISO,
-                      TimeTaken => $timetaken,
-                      TransactionType => 'Told'));
-}
+=cut
 
-=head2 _SetTold
 
-Updates the told without a transaction or acl check. Useful when we're sending replies.
+=head2 Starts
 
-=cut
+Returns the current value of Starts.
+(In the database, Starts is stored as datetime.)
 
-sub _SetTold {
-    my $self=shift;
-    
-    my $now = new RT::Date($self->CurrentUser);
-    $now->SetToNow();
-    #use __Set to get no ACLs ;)
-    return($self->__Set(Field => 'Told',
-                       Value => $now->ISO));
-}
 
-# }}}
 
-# {{{ sub Transactions 
+=head2 SetStarts VALUE
 
-=head2 Transactions
 
-  Returns an RT::Transactions object of all transactions on this ticket
+Set Starts to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Starts will be stored as a datetime.)
+
 
 =cut
-  
-sub Transactions {
-    my $self = shift;
-    
-    use RT::Transactions;
-    my $transactions = RT::Transactions->new($self->CurrentUser);
 
-    #If the user has no rights, return an empty object
-    if ($self->CurrentUserHasRight('ShowTicket')) {
-       my $tickets = $transactions->NewAlias('Tickets');
-       $transactions->Join( ALIAS1 => 'main',
-                             FIELD1 => 'Ticket',
-                             ALIAS2 => $tickets,
-                             FIELD2 => 'id');
-       $transactions->Limit( ALIAS => $tickets,
-                             FIELD => 'EffectiveId',
-                             VALUE => $self->id());
-        # if the user may not see comments do not return them
-        unless ($self->CurrentUserHasRight('ShowTicketComments')) {
-            $transactions->Limit( FIELD => 'Type',
-                                  OPERATOR => '!=',
-                                  VALUE => "Comment");
-        }
-    }
-    
-    return($transactions);
-}
 
-# }}}
+=head2 Started
 
-# {{{ sub _NewTransaction
+Returns the current value of Started.
+(In the database, Started is stored as datetime.)
 
-sub _NewTransaction {
-    my $self = shift;
-    my %args = ( TimeTaken => 0,
-                Type => undef,
-                OldValue => undef,
-                NewValue => undef,
-                Data => undef,
-                Field => undef,
-                MIMEObj => undef,
-                @_ );
-    
-    
-    require RT::Transaction;
-    my $trans = new RT::Transaction($self->CurrentUser);
-    my ($transaction, $msg) = 
-      $trans->Create( Ticket => $self->Id,
-                     TimeTaken => $args{'TimeTaken'},
-                     Type => $args{'Type'},
-                     Data => $args{'Data'},
-                     Field => $args{'Field'},
-                     NewValue => $args{'NewValue'},
-                     OldValue => $args{'OldValue'},
-                     MIMEObj => $args{'MIMEObj'}
-                   );
-    
-    $RT::Logger->warning($msg) unless $transaction;
-    
-    $self->_SetLastUpdated;
-    
-    if (defined $args{'TimeTaken'} ) {
-       $self->_UpdateTimeTaken($args{'TimeTaken'}); 
-    }
-    return($transaction, $msg, $trans);
-}
 
-# }}}
 
-# }}}
+=head2 SetStarted VALUE
 
-# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
 
-# {{{ sub _ClassAccessible
+Set Started to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Started will be stored as a datetime.)
 
-sub _ClassAccessible {
-    {
-       EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
-       Queue => { 'read' => 1, 'write' => 1 },
-       Requestors => { 'read' => 1, 'write' => 1 },
-       Owner => { 'read' => 1, 'write' => 1 },
-       Subject => { 'read' => 1, 'write' => 1 },
-       InitialPriority => { 'read' => 1, 'write' => 1 },
-       FinalPriority => { 'read' => 1, 'write' => 1 },
-       Priority => { 'read' => 1, 'write' => 1 },
-       Status => { 'read' => 1, 'write' => 1 },
-       TimeWorked => { 'read' => 1, 'write' => 1 },
-       TimeLeft => { 'read' => 1, 'write' => 1 },
-       Created => { 'read' => 1, 'auto' => 1 },
-       Creator => { 'read' => 1,  'auto' => 1 },
-       Told => { 'read' => 1, 'write' => 1 },
-       Resolved => {'read' => 1},
-       Starts => { 'read' => 1, 'write' => 1 },
-       Started => { 'read' => 1, 'write' => 1 },
-       Due => { 'read' => 1, 'write' => 1 },
-       Creator => { 'read' => 1, 'auto' => 1 },
-       Created => { 'read' => 1, 'auto' => 1 },
-       LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
-       LastUpdated => { 'read' => 1, 'auto' => 1 }
-    };
 
-}    
+=cut
 
-# }}}
 
-# {{{ sub _Set
+=head2 Due
 
-sub _Set {
-    my $self = shift;
-    
-    unless ($self->CurrentUserHasRight('ModifyTicket')) {
-       return (0, "Permission Denied");
-    }
-    
-    my %args = (Field => undef,
-               Value => undef,
-               TimeTaken => 0,
-               RecordTransaction => 1,
-               TransactionType => 'Set',
-               @_
-              );
-    #if the user is trying to modify the record
-    
-    #Take care of the old value we really don't want to get in an ACL loop.
-    # so ask the super::_Value
-    my $Old=$self->SUPER::_Value("$args{'Field'}");
-    
-    #Set the new value
-    my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
-                                      Value=> $args{'Value'});
-    
-    #If we can't actually set the field to the value, don't record
-    # a transaction. instead, get out of here.
-    if ($ret==0) {return (0,$msg);}
-    
-    if ($args{'RecordTransaction'} == 1) {
-       
-       my ($Trans, $Msg, $TransObj) =  
-         $self->_NewTransaction(Type => $args{'TransactionType'},
-                                Field => $args{'Field'},
-                                NewValue => $args{'Value'},
-                                OldValue =>  $Old,
-                                TimeTaken => $args{'TimeTaken'},
-                               );
-      return ($Trans,$TransObj->Description);
-    }
-    else {
-       return ($ret, $msg);
-  }
-}
+Returns the current value of Due.
+(In the database, Due is stored as datetime.)
 
-# }}}
 
-# {{{ sub _Value 
 
-=head2 _Value
+=head2 SetDue VALUE
+
+
+Set Due to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Due will be stored as a datetime.)
 
-Takes the name of a table column.
-Returns its value as a string, if the user passes an ACL check
 
 =cut
 
-sub _Value  {
 
-  my $self = shift;
-  my $field = shift;
+=head2 Resolved
 
-  
-  #if the field is public, return it.
-  if ($self->_Accessible($field, 'public')) {
-      #$RT::Logger->debug("Skipping ACL check for $field\n");
-      return($self->SUPER::_Value($field));
-      
-  }
-  
-  #If the current user doesn't have ACLs, don't let em at it.  
-  
-  unless ($self->CurrentUserHasRight('ShowTicket')) {
-      return (undef);
-  }
-  return($self->SUPER::_Value($field));
-  
-}
+Returns the current value of Resolved.
+(In the database, Resolved is stored as datetime.)
 
-# }}}
 
-# {{{ sub _UpdateTimeTaken
 
-=head2 _UpdateTimeTaken
+=head2 SetResolved VALUE
+
+
+Set Resolved to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Resolved will be stored as a datetime.)
 
-This routine will increment the timeworked counter. it should
-only be called from _NewTransaction 
 
 =cut
 
-sub _UpdateTimeTaken {
-  my $self = shift;
-  my $Minutes = shift;
-  my ($Total);
-   
-  $Total = $self->SUPER::_Value("TimeWorked");
-  $Total = ($Total || 0) + ($Minutes || 0);
-  $self->SUPER::_Set(Field => "TimeWorked", 
-                    Value => $Total);
 
-  return ($Total);
-}
+=head2 LastUpdatedBy
+
+Returns the current value of LastUpdatedBy.
+(In the database, LastUpdatedBy is stored as int(11).)
 
-# }}}
 
-# }}}
+=cut
 
-# {{{ Routines dealing with ACCESS CONTROL
 
-# {{{ sub CurrentUserHasRight 
+=head2 LastUpdated
 
-=head2 CurrentUserHasRight
+Returns the current value of LastUpdated.
+(In the database, LastUpdated is stored as datetime.)
 
-  Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
-1 if the user has that right. It returns 0 if the user doesn't have that right.
 
 =cut
 
-sub CurrentUserHasRight {
-  my $self = shift;
-  my $right = shift;
-  
-  return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(),
-                           Right => "$right"));
 
-}
+=head2 Creator
 
-# }}}
+Returns the current value of Creator.
+(In the database, Creator is stored as int(11).)
 
-# {{{ sub HasRight 
 
-=head2 HasRight
+=cut
 
- Takes a paramhash with the attributes 'Right' and 'Principal'
-  'Right' is a ticket-scoped textual right from RT::ACE 
-  'Principal' is an RT::User object
 
-  Returns 1 if the principal has the right. Returns undef if not.
+=head2 Created
+
+Returns the current value of Created.
+(In the database, Created is stored as datetime.)
+
 
 =cut
 
-sub HasRight {
-    my $self = shift;
-    my %args = ( Right => undef,
-                Principal => undef,
-                @_);
-    
-    unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) {
-       $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
-    }
-    
-    return($args{'Principal'}->HasQueueRight(TicketObj => $self,
-                                            Right => $args{'Right'}));
-}
 
-# }}}
+=head2 Disabled
 
-# }}}
+Returns the current value of Disabled.
+(In the database, Disabled is stored as smallint(6).)
 
 
-1;
 
-=head1 AUTHOR
+=head2 SetDisabled VALUE
 
-Jesse Vincent, jesse@fsck.com
 
-=head1 SEE ALSO
+Set Disabled to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Disabled will be stored as a smallint(6).)
 
-RT
 
 =cut
 
 
+
+sub _CoreAccessible {
+    {
+
+        id =>
+               {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+        EffectiveId =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Queue =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Type =>
+               {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        IssueStatement =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Resolution =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Owner =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Subject =>
+               {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => '[no subject]'},
+        InitialPriority =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        FinalPriority =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Priority =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        TimeEstimated =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        TimeWorked =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Status =>
+               {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
+        TimeLeft =>
+               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Told =>
+               {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Starts =>
+               {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Started =>
+               {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Due =>
+               {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Resolved =>
+               {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        LastUpdatedBy =>
+               {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        LastUpdated =>
+               {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Creator =>
+               {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Created =>
+               {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        Disabled =>
+               {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
+
+ }
+};
+
+RT::Base->_ImportOverlays();
+
+1;