import rt 3.8.10
[freeside.git] / rt / lib / RT / Ticket_Overlay.pm
index c88bbc9..8dd88c9 100644 (file)
@@ -1,26 +1,51 @@
-# BEGIN LICENSE BLOCK
-# 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-# 
-# (Except where explictly superceded by other copyright notices)
-# 
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2011 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.
-# 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
-# 
-# 
-# END LICENSE BLOCK
+#
+# 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 }}}
+
 # {{{ Front Material 
 
 =head1 SYNOPSIS
@@ -36,81 +61,12 @@ This module lets you manipulate RT\'s ticket object.
 
 =head1 METHODS
 
-=begin testing
-
-use_ok ( RT::Queue);
-ok(my $testqueue = RT::Queue->new($RT::SystemUser));
-ok($testqueue->Create( Name => 'ticket tests'));
-ok($testqueue->Id != 0);
-use_ok(RT::CustomField);
-ok(my $testcf = RT::CustomField->new($RT::SystemUser));
-ok($testcf->Create( Name => 'selectmulti',
-                    Queue => $testqueue->id,
-                               Type => 'SelectMultiple'));
-ok($testcf->AddValue ( Name => 'Value1',
-                        SortOrder => '1',
-                        Description => 'A testing value'));
-ok($testcf->AddValue ( Name => 'Value2',
-                        SortOrder => '2',
-                        Description => 'Another testing value'));
-ok($testcf->AddValue ( Name => 'Value3',
-                        SortOrder => '3',
-                        Description => 'Yet Another testing value'));
-                       
-ok($testcf->Values->Count == 3);
-
-use_ok(RT::Ticket);
-
-my $u = RT::User->new($RT::SystemUser);
-$u->Load("root");
-ok ($u->Id, "Found the root user");
-ok(my $t = RT::Ticket->new($RT::SystemUser));
-ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
-               Subject => 'Testing',
-               Owner => $u->Id
-              ));
-ok($id != 0);
-ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
-ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
-                           Value => 'Value1'));
-ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
-ok($t->CustomFieldValues($testcf->Id)->Count == 1);
-ok($t->CustomFieldValues($testcf->Id)->First &&
-    $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
-
-ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
-                        Value => 'Value1'));
-ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
-ok($t->CustomFieldValues($testcf->Id)->Count == 0);
-
-ok(my $t2 = RT::Ticket->new($RT::SystemUser));
-ok($t2->Load($id));
-ok($t2->Subject eq 'Testing');
-ok($t2->QueueObj->Id eq $testqueue->id);
-ok($t2->OwnerObj->Id == $u->Id);
-
-my $t3 = RT::Ticket->new($RT::SystemUser);
-my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
-                                Subject => 'Testing',
-                                Owner => $u->Id);
-my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
- Value => 'Value1');
-ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
-my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
- Value => 'Value2');
-ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
-my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
- Value => 'Value3');
-ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
-ok($t->CustomFieldValues($testcf->Id)->Count == 2,
-   "This ticket has 2 custom field values");
-ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
-   "This ticket has 1 custom field value");
-
-=end testing
 
 =cut
 
+
+package RT::Ticket;
+
 use strict;
 no warnings qw(redefine);
 
@@ -120,33 +76,27 @@ use RT::Record;
 use RT::Links;
 use RT::Date;
 use RT::CustomFields;
-use RT::TicketCustomFieldValues;
 use RT::Tickets;
+use RT::Transactions;
+use RT::Reminders;
 use RT::URI::fsck_com_rt;
 use RT::URI;
+use MIME::Entity;
 
-=begin testing
-
-
-ok(require RT::Ticket, "Loading the RT::Ticket library");
-
-=end testing
-
-=cut
-
-# }}}
 
 # {{{ LINKTYPEMAP
-# A helper table for relationships mapping to make it easier
+# A helper table for links mapping to make it easier
 # to build and parse links between tickets
 
-use vars '%LINKTYPEMAP';
-
-%LINKTYPEMAP = (
+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',
@@ -157,29 +107,39 @@ use vars '%LINKTYPEMAP';
                    Mode => 'Target', },
     DependedOnBy => { Type => 'DependsOn',
                       Mode => 'Base', },
+    MergedInto => { Type => 'MergedInto',
+                   Mode => 'Target', },
 
 );
 
 # }}}
 
 # {{{ LINKDIRMAP
-# A helper table for relationships mapping to make it easier
+# A helper table for links mapping to make it easier
 # to build and parse links between tickets
 
-use vars '%LINKDIRMAP';
-
-%LINKDIRMAP = (
+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
@@ -193,66 +153,46 @@ Otherwise, returns the ticket id.
 sub Load {
     my $self = shift;
     my $id   = shift;
+    $id = '' unless defined $id;
 
-    #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.
+    # 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.
 
+    # FIXME: there is no TicketBaseURI option in config
+    my $base_uri = RT->Config->Get('TicketBaseURI') || '';
     #If it's a local URI, turn it into a ticket id
-    if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
+    if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
         $id = $1;
     }
 
-    #If it's a remote URI, we're going to punt for now
-    elsif ( $id =~ '://' ) {
+    unless ( $id =~ /^\d+$/ ) {
+        $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
         return (undef);
     }
 
-    #If we have an integer URI, load the ticket
-    if ( $id =~ /^\d+$/ ) {
-        my $ticketid = $self->LoadById($id);
+    $id = $MERGE_CACHE{'effective'}{ $id }
+        if $MERGE_CACHE{'effective'}{ $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 {
+    my ($ticketid, $msg) = $self->LoadById( $id );
+    unless ( $self->Id ) {
+        $RT::Logger->debug("$self tried to load a bogus ticket: $id");
         return (undef);
     }
 
     #If we're merged, resolve the merge.
-    if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
-        return ( $self->Load( $self->EffectiveId ) );
+    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 LoadByURI
-
-=head2 LoadByURI
-
-Given a local ticket URI, loads the specified ticket.
-
-=cut
-
-sub LoadByURI {
-    my $self = shift;
-    my $uri  = shift;
-
-    if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
-        my $id = $1;
-        return ( $self->Load($id) );
-    }
-    else {
-        return (undef);
-    }
+    return $self->Id;
 }
 
 # }}}
@@ -265,12 +205,15 @@ 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
+  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)
@@ -282,101 +225,107 @@ Arguments: ARGS is a hash of named parameters.  Valid parameters are:
   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>
 
+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:
 
-Returns: TICKETID, Transaction Object, Error Message
-
-
-=begin testing
-
-my $t = RT::Ticket->new($RT::SystemUser);
+  Parents => 45,
+  DependsOn => [ 15, 22 ],
+  RefersTo => 'http://www.bestpractical.com',
 
-ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created");
+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 ( my $id = $t->Id, "Got ticket id");
-ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
-ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
-ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
+Returns: TICKETID, Transaction Object, Error Message
 
-=end testing
 
 =cut
 
 sub Create {
     my $self = shift;
 
-    my %args = ( id              => undef,
-                 Queue           => undef,
-                 Requestor       => undef,
-                 Cc              => undef,
-                 AdminCc         => undef,
-                 Type            => 'ticket',
-                 Owner           => undef,
-                 Subject         => '',
-                 InitialPriority => undef,
-                 FinalPriority   => undef,
-                 Status          => 'new',
-                 TimeWorked      => "0",
-                 TimeLeft        => 0,
-                 TimeEstimated        => 0,
-                 Due             => undef,
-                 Starts          => undef,
-                 Started         => undef,
-                 Resolved        => undef,
-                 MIMEObj         => undef,
-                 _RecordTransaction => 1,
-                 
-
-
-                 @_ );
-
-    my ( $ErrStr, $Owner, $resolved );
-    my (@non_fatal_errors);
+    my %args = (
+        id                 => undef,
+        EffectiveId        => undef,
+        Queue              => undef,
+        Requestor          => undef,
+        Cc                 => undef,
+        AdminCc            => undef,
+        SquelchMailTo      => undef,
+        Type               => 'ticket',
+        Owner              => undef,
+        Subject            => '',
+        InitialPriority    => undef,
+        FinalPriority      => undef,
+        Priority           => undef,
+        Status             => 'new',
+        TimeWorked         => "0",
+        TimeLeft           => 0,
+        TimeEstimated      => 0,
+        Due                => undef,
+        Starts             => undef,
+        Started            => undef,
+        Resolved           => undef,
+        MIMEObj            => undef,
+        _RecordTransaction => 1,
+        DryRun             => 0,
+        @_
+    );
 
-    my $QueueObj = RT::Queue->new($RT::SystemUser);
+    my ($ErrStr, @non_fatal_errors);
 
-    
-    if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
-        $QueueObj->Load( $args{'Queue'} );
-    }
-    elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
+    my $QueueObj = RT::Queue->new( $RT::SystemUser );
+    if ( ref $args{'Queue'} eq 'RT::Queue' ) {
         $QueueObj->Load( $args{'Queue'}->Id );
     }
+    elsif ( $args{'Queue'} ) {
+        $QueueObj->Load( $args{'Queue'} );
+    }
     else {
-        $RT::Logger->debug( $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) && $QueueObj->Id ) {
+    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->HasRight( Right    => 'CreateTicket',
-                                                Object => $QueueObj )
-      ) {
-        return ( 0, 0,
-                 $self->loc( "No permission to create tickets in the queue '[_1]'", $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));
     }
 
     unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
         return ( 0, 0, $self->loc('Invalid value for 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 +
@@ -384,34 +333,38 @@ sub Create {
 
     #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 ( $args{'Due'} ) {
-        $Due->Set( Format => 'ISO', Value  => $args{'Due'} );
+    if ( defined $args{'Due'} ) {
+        $Due->Set( Format => 'ISO', Value => $args{'Due'} );
     }
-    elsif (  $QueueObj->DefaultDueIn  ) {
+    elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
         $Due->SetToNow;
-        $Due->AddDays( $QueueObj->DefaultDueIn );
+        $Due->AddDays( $due_in );
     }
 
     my $Starts = new RT::Date( $self->CurrentUser );
     if ( defined $args{'Starts'} ) {
-        $Starts->Set( Format => 'ISO', Value  => $args{'Starts'} );
+        $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
     }
 
     my $Started = new RT::Date( $self->CurrentUser );
     if ( defined $args{'Started'} ) {
-        $Started->Set( Format => 'ISO', Value  => $args{'Started'} );
+        $Started->Set( Format => 'ISO', Value => $args{'Started'} );
+    }
+    elsif ( $args{'Status'} ne 'new' ) {
+        $Started->SetToNow;
     }
 
     my $Resolved = new RT::Date( $self->CurrentUser );
     if ( defined $args{'Resolved'} ) {
-        $Resolved->Set( Format => 'ISO', Value  => $args{'Resolved'} );
+        $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
     }
 
-
     #If the status is an inactive status, set the resolved date
-    if ($QueueObj->IsInactiveStatus($args{'Status'}) && !$args{'Resolved'}) {
-        $RT::Logger->debug("Got a ".$args{'Status'} . "ticket with a resolved of ".$args{'Resolved'});
+    elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
+    {
+        $RT::Logger->debug( "Got a ". $args{'Status'}
+            ."(inactive) ticket with undefined resolved date. Setting to now."
+        );
         $Resolved->SetToNow;
     }
 
@@ -427,36 +380,42 @@ sub Create {
 
     # {{{ Deal with setting the owner
 
+    my $Owner;
     if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
-        $Owner = $args{'Owner'};
+        if ( $args{'Owner'}->id ) {
+            $Owner = $args{'Owner'};
+        } else {
+            $RT::Logger->error('passed not loaded owner object');
+            push @non_fatal_errors, $self->loc("Invalid owner object");
+            $Owner = undef;
+        }
     }
 
     #If we've been handed something else, try to load the user.
-    elsif ( defined $args{'Owner'} ) {
+    elsif ( $args{'Owner'} ) {
         $Owner = RT::User->new( $self->CurrentUser );
         $Owner->Load( $args{'Owner'} );
-
+        $Owner->LoadByEmail( $args{'Owner'} )
+            unless $Owner->Id;
+        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;
+        }
     }
 
-    #If we have a proposed owner and they don't have the right 
+    #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 )
-         and ( $Owner->Id != $RT::Nobody->Id )
-         and ( !$Owner->HasRight( Object => $QueueObj,
-                                       Right    => 'OwnTicket' ) )
-      ) {
-
-        $RT::Logger->warning( "User "
-                              . $Owner->Name . "("
-                              . $Owner->id
-                              . ") was proposed "
-                              . "as a ticket owner but has no rights to own "
-                              . "tickets in ".$QueueObj->Name );
-
-        push @non_fatal_errors, $self->loc("Invalid owner. Defaulting to 'nobody'.");
-
+   
+    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');
+
     }
 
     #If we haven't been handed a valid owner, make it nobody.
@@ -467,125 +426,204 @@ sub Create {
 
     # }}}
 
-    # 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
+# 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" ) {
-     next unless (defined $args{$type});
-        foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
-        my $user = RT::User->new($RT::SystemUser);
-        $user->LoadOrCreateByEmail($watcher) if ($watcher !~ /^\d+$/);
+        $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;
+                    }
+                }
+            }
         }
     }
 
-
     $RT::Handle->BeginTransaction();
 
-    my %params =( Queue           => $QueueObj->Id,
-                                   Owner           => $Owner->Id,
-                                   Subject         => $args{'Subject'},
-                                   InitialPriority => $args{'InitialPriority'},
-                                   FinalPriority   => $args{'FinalPriority'},
-                                   Priority        => $args{'InitialPriority'},
-                                   Status          => $args{'Status'},
-                                   TimeWorked      => $args{'TimeWorked'},
-                                   TimeEstimated   => $args{'TimeEstimated'},
-                                   TimeLeft        => $args{'TimeLeft'},
-                                   Type            => $args{'Type'},
-                                   Starts          => $Starts->ISO,
-                                   Started         => $Started->ISO,
-                                   Resolved        => $Resolved->ISO,
-                                   Due             => $Due->ISO );
-
-    # Parameters passed in during an import that we probably don't want to touch, otherwise
+    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,
+        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});
+        $params{$attr} = $args{$attr} if $args{$attr};
     }
 
     # Delete null integer parameters
-    foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
-        delete $params{$attr}  unless (exists $params{$attr} && $params{$attr});
+    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'};
 
-    my $id = $self->SUPER::Create( %params);
+    my ($id,$ticket_message) = $self->SUPER::Create( %params );
     unless ($id) {
-        $RT::Logger->crit( "Couldn't create a ticket");
+        $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") );
+        return ( 0, 0,
+            $self->loc("Ticket could not be created due to an internal error")
+        );
     }
 
     #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->crit("$self ->Create couldn't set EffectiveId: $msg\n");
-        $RT::Handle->Rollback();
-        return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
+    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")
+        );
     }
 
     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." );
+              . $self->Id
+              . ". aborting Ticket creation." );
         $RT::Handle->Rollback();
         return ( 0, 0,
-                 $self->loc( "Ticket could not be created due to an internal error") );
+            $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 
+    # 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;
 
-    $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1);
 
-    # {{{ Deal with setting up watchers
 
+    # {{{ Deal with setting up watchers
 
     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
-        next unless (defined $args{$type});
-        foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
+        # 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';
+
+            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 );
+    }
 
-           # we reason that all-digits number must be a principal id, not email
-           # this is the only way to can add
-           my $field = 'Email';
-           $field = 'PrincipalId' if $watcher =~ /^\d+$/;
 
-           my ( $wval, $wmsg );
+    # }}}
 
-            if ( $type eq 'AdminCc' ) {
+    # {{{ Add all the custom fields
 
-                # 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
-                ( $wval, $wmsg ) = $self->AddWatcher( Type   => $type,
-                                                         $field => $watcher,
-                                                         Silent => 1 );
-            }
-            else {
-                ( $wval, $wmsg ) = $self->_AddWatcher( Type   => $type,
-                                                          $field => $watcher,
-                                                          Silent => 1 );
-            }
+    foreach my $arg ( keys %args ) {
+        next unless $arg =~ /^CustomField-(\d+)$/i;
+        my $cfid = $1;
 
-            push @non_fatal_errors, $wmsg unless ($wval);
+        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;
         }
     }
 
     # }}}
+
     # {{{ Deal with setting up links
 
+    # 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 schema: 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});
+        next unless ( defined $args{$type} );
         foreach my $link (
             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
         {
-            my ( $wval, $wmsg ) = $self->AddLink(
+            # 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' ) ) {
+                my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
+                unless ( $val ) {
+                    push @non_fatal_errors, $msg;
+                    next;
+                }
+                if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
+                    push @non_fatal_errors, $self->loc('Linking. Permission denied');
+                    next;
+                }
+            }
+            
+            my ( $wval, $wmsg ) = $self->_AddLink(
                 Type                          => $LINKTYPEMAP{$type}->{'Type'},
                 $LINKTYPEMAP{$type}->{'Mode'} => $link,
-                Silent                        => 1
+                Silent                        => !$args{'_RecordTransaction'},
+                'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
+                                              => 1,
             );
 
             push @non_fatal_errors, $wmsg unless ($wval);
@@ -593,48 +631,62 @@ sub Create {
     }
 
     # }}}
+    # 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);
 
-   # {{{ Add all the custom fields 
-
-    foreach my $arg ( keys %args ) {
-    next unless ( $arg =~ /^CustomField-(\d+)$/i );
-    my $cfid = $1;
-    foreach
-      my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) ) {
-        next unless ($value);
-        $self->_AddCustomFieldValue( Field => $cfid,
-                                     Value => $value,
-                                     RecordTransaction => 0
-                                 );
-    }
+        }
+        $self->OwnerGroup->_AddMember(
+            PrincipalId       => $Owner->PrincipalId,
+            InsideTransaction => 1
+        );
     }
-    # }}}
 
     if ( $args{'_RecordTransaction'} ) {
+
         # {{{ Add a transaction for the create
         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
-                                                     Type      => "Create",
-                                                     TimeTaken => 0,
-                                                     MIMEObj => $args{'MIMEObj'}
+            Type         => "Create",
+            TimeTaken    => $args{'TimeWorked'},
+            MIMEObj      => $args{'MIMEObj'},
+            CommitScrips => !$args{'DryRun'},
         );
 
-
         if ( $self->Id && $Trans ) {
-            $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
-            $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
 
-            $RT::Logger->info("Ticket ".$self->Id. " created in queue '".$QueueObj->Name."' by ".$self->CurrentUser->Name);
+            $TransObj->UpdateCustomFields(ARGSRef => \%args);
+
+            $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();
 
-            # TODO where does this get errstr from?
+            $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 );
+
         # }}}
     }
     else {
@@ -642,8 +694,8 @@ sub Create {
         # 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 );
+        $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
+        return ( $self->Id, 0, $ErrStr );
 
     }
 }
@@ -651,446 +703,108 @@ sub Create {
 
 # }}}
 
-# {{{ sub CreateFromEmailMessage
-
-
-=head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders } 
-
-This code replaces what was once a large part of the email gateway.
-It takes an email message as a parameter, parses out the sender, subject
-and a MIME object. It then creates a ticket based on those attributes
-
-=cut
-
-sub CreateFromEmailMessage {
-    my $self = shift;
-    my %args = ( Message => undef,
-                 Queue => undef,
-                 ExtractActorFromSender => undef,
-                 @_ );
-
-    
-    # Pull out requestor
-
-    # Pull out Cc?
-
-    # 
-
-
-}
-
-# }}}
-
-
-# {{{ CreateFrom822
-
-=head2 FORMAT
-
-CreateTickets uses the template as a template for an ordered set of tickets 
-to create. The basic format is as follows:
-
-
- ===Create-Ticket: identifier
- Param: Value
- Param2: Value
- Param3: Value
- Content: Blah
- blah
- blah
- ENDOFCONTENT
-=head2 Acceptable fields
-
-A complete list of acceptable fields for this beastie:
-
-
-    *  Queue           => Name or id# of a queue
-       Subject         => A text string
-       Status          => A valid status. defaults to 'new'
-
-       Due             => Dates can be specified in seconds since the epoch
-                          to be handled literally or in a semi-free textual
-                          format which RT will attempt to parse.
-       Starts          => 
-       Started         => 
-       Resolved        => 
-       Owner           => Username or id of an RT user who can and should own 
-                          this ticket
-   +   Requestor       => Email address
-   +   Cc              => Email address 
-   +   AdminCc         => Email address 
-       TimeWorked      => 
-       TimeEstimated   => 
-       TimeLeft        => 
-       InitialPriority => 
-       FinalPriority   => 
-       Type            => 
-    +  DependsOn       => 
-    +  DependedOnBy    =>
-    +  RefersTo        =>
-    +  ReferredToBy    => 
-    +  Members         =>
-    +  MemberOf        => 
-       Content         => content. Can extend to multiple lines. Everything
-                          within a template after a Content: header is treated
-                          as content until we hit a line containing only 
-                          ENDOFCONTENT
-       ContentType     => the content-type of the Content field
-       CustomField-<id#> => custom field value
-
-Fields marked with an * are required.
-
-Fields marked with a + man have multiple values, simply
-by repeating the fieldname on a new line with an additional value.
-
-
-When parsed, field names are converted to lowercase and have -s stripped.
-Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all 
-be treated as the same thing.
-
-
-=begin testing
-
-use_ok(RT::Ticket);
+# {{{ _Parse822HeadersForAttributes Content
 
-=end testing
+=head2 _Parse822HeadersForAttributes Content
 
+Takes an RFC822 style message and parses its attributes into a hash.
 
 =cut
 
-sub CreateFrom822 {
+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 =~ /^$/) {
 
-    my %args = $self->_Parse822HeadersForAttributes($content);
-
-    # Now we have a %args to work with.
-    # Make sure we have at least the minimum set of
-    # reasonable data and do our thang
-    my $ticket = RT::Ticket->new($RT::SystemUser);
-
-    my %ticketargs = (
-        Queue           => $args{'queue'},
-        Subject         => $args{'subject'},
-        Status          => $args{'status'},
-        Due             => $args{'due'},
-        Starts          => $args{'starts'},
-        Started         => $args{'started'},
-        Resolved        => $args{'resolved'},
-        Owner           => $args{'owner'},
-        Requestor       => $args{'requestor'},
-        Cc              => $args{'cc'},
-        AdminCc         => $args{'admincc'},
-        TimeWorked      => $args{'timeworked'},
-        TimeEstimated   => $args{'timeestimated'},
-        TimeLeft        => $args{'timeleft'},
-        InitialPriority => $args{'initialpriority'},
-        FinalPriority   => $args{'finalpriority'},
-        Type            => $args{'type'},
-        DependsOn       => $args{'dependson'},
-        DependedOnBy    => $args{'dependedonby'},
-        RefersTo        => $args{'refersto'},
-        ReferredToBy    => $args{'referredtoby'},
-        Members         => $args{'members'},
-        MemberOf        => $args{'memberof'},
-        MIMEObj         => $args{'mimeobj'}
-    );
+            #TODO: this won't work, since "" isn't of the form "foo:value"
 
-    # Add custom field entries to %ticketargs.
-    # TODO: allow named custom fields
-    map {
-        /^customfield-(\d+)$/
-          && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
-    } keys(%args);
+                while ( defined( my $l = shift @lines ) ) {
+                    push @{ $args{'content'} }, $l;
+                }
+            }
+        
+    }
 
-    my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
-    unless ($id) {
-        $RT::Logger->error( "Couldn't create a related ticket for "
-              . $self->TicketObj->Id . " "
-              . $msg );
+    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->new();
+    $args{'mimeobj'}->build(
+        Type => ( $args{'contenttype'} || 'text/plain' ),
+        Data => ($args{'content'} || '')
+    );
 
-    return (1);
+    return (%args);
 }
 
 # }}}
 
-# {{{ UpdateFrom822 
+# {{{ sub Import
 
-=head2 UpdateFrom822 $MESSAGE
+=head2 Import PARAMHASH
 
-Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
-Returns an um. ask me again when the code exists
+Import a ticket. 
+Doesn\'t create a transaction. 
+Doesn\'t supply queue defaults, etc.
 
+Returns: TICKETID
 
-=begin testing
+=cut
 
-my $simple_update = <<EOF;
-Subject: target
-AddRequestor: jesse\@example.com
-EOF
+sub Import {
+    my $self = shift;
+    my ( $ErrStr, $QueueObj, $Owner );
 
-my $ticket = RT::Ticket->new($RT::SystemUser);
-$ticket->Create(Subject => 'first', Queue => 'general');
-ok($ticket->Id, "Created the test ticket");
-$ticket->UpdateFrom822($simple_update);
-is($ticket->Subject, 'target', "changed the subject");
-my $jesse = RT::User->new($RT::SystemUser);
-$jesse->LoadByEmail('jesse@example.com');
-ok ($jesse->Id, "There's a user for jesse");
-ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
+    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,
+        @_
+    );
 
-=end testing
-
-
-=cut
-
-sub UpdateFrom822 {
-        my $self = shift;
-        my $content = shift;
-        my %args = $self->_Parse822HeadersForAttributes($content);
-
-        
-    my %ticketargs = (
-        Queue           => $args{'queue'},
-        Subject         => $args{'subject'},
-        Status          => $args{'status'},
-        Due             => $args{'due'},
-        Starts          => $args{'starts'},
-        Started         => $args{'started'},
-        Resolved        => $args{'resolved'},
-        Owner           => $args{'owner'},
-        Requestor       => $args{'requestor'},
-        Cc              => $args{'cc'},
-        AdminCc         => $args{'admincc'},
-        TimeWorked      => $args{'timeworked'},
-        TimeEstimated   => $args{'timeestimated'},
-        TimeLeft        => $args{'timeleft'},
-        InitialPriority => $args{'initialpriority'},
-        Priority => $args{'priority'},
-        FinalPriority   => $args{'finalpriority'},
-        Type            => $args{'type'},
-        DependsOn       => $args{'dependson'},
-        DependedOnBy    => $args{'dependedonby'},
-        RefersTo        => $args{'refersto'},
-        ReferredToBy    => $args{'referredtoby'},
-        Members         => $args{'members'},
-        MemberOf        => $args{'memberof'},
-        MIMEObj         => $args{'mimeobj'}
-    );
-
-    foreach my $type qw(Requestor Cc Admincc) {
-
-        foreach my $action ( 'Add', 'Del', '' ) {
-
-            my $lctag = lc($action) . lc($type);
-            foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
-
-                foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
-                    push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
-                }
-
-            }
-
-            # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
-
-        }
-    }
-
-    # Add custom field entries to %ticketargs.
-    # TODO: allow named custom fields
-    map {
-        /^customfield-(\d+)$/
-          && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
-    } keys(%args);
-
-# for each ticket we've been told to update, iterate through the set of
-# rfc822 headers and perform that update to the ticket.
-
-
-    # {{{ Set basic fields 
-    my @attribs = qw(
-      Subject
-      FinalPriority
-      Priority
-      TimeEstimated
-      TimeWorked
-      TimeLeft
-      Status
-      Queue
-      Type
-    );
-
-
-    # Resolve the queue from a name to a numeric id.
-    if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
-        my $tempqueue = RT::Queue->new($RT::SystemUser);
-        $tempqueue->Load( $ticketargs{'Queue'} );
-        $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
-    }
-
-    # die "updaterecordobject is a webui thingy";
-    my @results;
-
-    foreach my $attribute (@attribs) {
-        my $value = $ticketargs{$attribute};
-
-        if ( $value ne $self->$attribute() ) {
-
-            my $method = "Set$attribute";
-            my ( $code, $msg ) = $self->$method($value);
-
-            push @results, $self->loc($attribute) . ': ' . $msg;
-
-        }
-    }
-
-    # We special case owner changing, so we can use ForceOwnerChange
-    if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
-        my $ChownType = "Give";
-        $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
-
-        my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
-        push ( @results, $msg );
-    }
-
-    # }}}
-# Deal with setting watchers
-
-
-# Acceptable arguments:
-#  Requestor
-#  Requestors
-#  AddRequestor
-#  AddRequestors
-#  DelRequestor
- foreach my $type qw(Requestor Cc AdminCc) {
-
-        # If we've been given a number of delresses to del, do it.
-                foreach my $address (@{$ticketargs{'Del'.$type}}) {
-                my ($id, $msg) = $self->DelWatcher( Type => $type, Email => $address);
-                push (@results, $msg) ;
-                }
-
-        # If we've been given a number of addresses to add, do it.
-                foreach my $address (@{$ticketargs{'Add'.$type}}) {
-                $RT::Logger->debug("Adding $address as a $type");
-                my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
-                push (@results, $msg) ;
-
-        }
-
-
-}
-
-
-}
-# }}}
-
-# {{{ _Parse822HeadersForAttributes Content
-
-=head2 _Parse822HeadersForAttributes Content
-
-Takes an RFC822 style message and parses its attributes into a hash.
-
-=cut
-
-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;
-                }
-            }
-        
-    }
-
-    foreach my $date qw(due starts started resolved) {
-        my $dateobj = RT::Date->new($RT::SystemUser);
-        if ( $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->new();
-    $args{'mimeobj'}->build(
-        Type => ( $args{'contenttype'} || 'text/plain' ),
-        Data => ($args{'content'} || '')
-    );
-
-    return (%args);
-}
-
-# }}}
-
-# {{{ sub Import
-
-=head2 Import PARAMHASH
-
-Import a ticket. 
-Doesn\'t create a transaction. 
-Doesn\'t supply queue defaults, etc.
-
-Returns: TICKETID
-
-=cut
-
-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'} );
+    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 +++
     }
@@ -1159,7 +873,7 @@ sub Import {
               . ") was proposed "
               . "as a ticket owner but has no rights to own "
               . "tickets in '"
-              . $QueueObj->Name . "'\n" );
+              . $QueueObj->Name . "'" );
 
         $Owner = undef;
     }
@@ -1193,18 +907,18 @@ sub Import {
         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
+        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
     );
 
     # If the ticket didn't have an id
@@ -1218,20 +932,27 @@ sub Import {
 
         unless ($val) {
             $RT::Logger->err(
-                $self . "->Import couldn't set EffectiveId: $msg\n" );
+                $self . "->Import couldn't set EffectiveId: $msg" );
         }
     }
 
-    my $watcher;
-    foreach $watcher ( @{ $args{'Cc'} } ) {
-        $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
+    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 );
+
+    foreach my $watcher ( @{ $args{'Cc'} } ) {
+        $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
     }
-    foreach $watcher ( @{ $args{'AdminCc'} } ) {
-        $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
+    foreach my $watcher ( @{ $args{'AdminCc'} } ) {
+        $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
             Silent => 1 );
     }
-    foreach $watcher ( @{ $args{'Requestor'} } ) {
-        $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
+    foreach my $watcher ( @{ $args{'Requestor'} } ) {
+        $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
             Silent => 1 );
     }
 
@@ -1240,62 +961,19 @@ sub Import {
 
 # }}}
 
-
 # {{{ Routines dealing with watchers.
 
 # {{{ _CreateTicketGroups 
 
 =head2 _CreateTicketGroups
 
-Create the ticket groups and relationships for this ticket. 
+Create the ticket groups and links for this ticket. 
 This routine expects to be called from Ticket->Create _inside of a transaction_
 
 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
 
 It will return true on success and undef on failure.
 
-=begin testing
-
-my $ticket = RT::Ticket->new($RT::SystemUser);
-my ($id, $msg) = $ticket->Create(Subject => "Foo",
-                Owner => $RT::SystemUser->Id,
-                Status => 'open',
-                Requestor => ['jesse@example.com'],
-                Queue => '1'
-                );
-ok ($id, "Ticket $id was created");
-ok(my $group = RT::Group->new($RT::SystemUser));
-ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
-ok ($group->Id, "Found the requestors object for this ticket");
-
-ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
-$jesse->LoadByEmail('jesse@example.com');
-ok($jesse->Id,  "Found the jesse rt user");
-
-
-ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
-ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
-ok ($add_id, "Add succeeded: ($add_msg)");
-ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
-$bob->LoadByEmail('bob@fsck.com');
-ok($bob->Id,  "Found the bob rt user");
-ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
-ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
-ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
-
-
-$group = RT::Group->new($RT::SystemUser);
-ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
-ok ($group->Id, "Found the cc object for this ticket");
-$group = RT::Group->new($RT::SystemUser);
-ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
-ok ($group->Id, "Found the AdminCc object for this ticket");
-$group = RT::Group->new($RT::SystemUser);
-ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
-ok ($group->Id, "Found the Owner object for this ticket");
-ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
-
-=end testing
 
 =cut
 
@@ -1348,12 +1026,12 @@ AddWatcher takes a parameter hash. The keys are as follows:
 
 Type        One of Requestor, Cc, AdminCc
 
-PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
+PrincipalId The RT::Principal id of the user or group that's being added as a watcher
 
 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 Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
+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
 
@@ -1366,45 +1044,47 @@ sub AddWatcher {
         @_
     );
 
-    # {{{ Check ACLS
-    #If the watcher we're trying to add is for the current user
-    if ( $self->CurrentUser->PrincipalId  eq $args{'PrincipalId'}) {
-        #  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'))
-            }
-        }
-
-        #  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' ) ) {
+    # 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;
 
-            unless ( $self->CurrentUserHasRight('ModifyTicket')
-                or $self->CurrentUserHasRight('Watch') ) {
-                return ( 0, $self->loc('Permission Denied'))
-            }
-        }
-        else {
-            $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type");
-            return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
+        if ( lc $self->CurrentUser->UserObj->EmailAddress
+            eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
+        {
+            $args{'PrincipalId'} = $self->CurrentUser->id;
+            delete $args{'Email'};
         }
     }
 
-    # If the watcher isn't the current user 
-    # and the current user  doesn't have 'ModifyTicket'
+    # If the watcher isn't the current user then the current user has no right
     # bail
-    else {
-        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-            return ( 0, $self->loc("Permission Denied") );
+    unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    #  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') );
         }
     }
 
-    # }}}
+    #  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') );
+        }
+    }
+    else {
+        $RT::Logger->warning( "AddWatcher got passed a bogus type");
+        return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
+    }
 
-    return ( $self->_AddWatcher(%args) );
+    return $self->_AddWatcher( %args );
 }
 
 #This contains the meat of AddWatcher. but can be called from a routine like
@@ -1422,14 +1102,20 @@ sub _AddWatcher {
 
     my $principal = RT::Principal->new($self->CurrentUser);
     if ($args{'Email'}) {
-        my $user = RT::User->new($RT::SystemUser);
-        my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
-        if ($pid) {
-            $args{'PrincipalId'} = $pid; 
+        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 );
+
+        }
     } 
 
  
@@ -1455,7 +1141,7 @@ sub _AddWatcher {
     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."\n".$m_msg);
+        $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."".$m_msg);
 
         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
     }
@@ -1496,61 +1182,66 @@ Email (the email address of an existing wathcer)
 sub DeleteWatcher {
     my $self = shift;
 
-    my %args = ( Type => undef,
+    my %args = ( Type        => undef,
                  PrincipalId => undef,
-                 Email => undef,
+                 Email       => undef,
                  @_ );
 
-    unless ($args{'PrincipalId'} || $args{'Email'} ) {
-        return(0, $self->loc("No principal specified"));
+    unless ( $args{'PrincipalId'} || $args{'Email'} ) {
+        return ( 0, $self->loc("No principal specified") );
     }
-    my $principal = RT::Principal->new($self->CurrentUser);
-    if ($args{'PrincipalId'} ) {
+    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);
+        $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"));
+    unless ( $principal->Id ) {
+        return ( 0, $self->loc("Could not find that principal") );
     }
 
-    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"));
+    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") );
     }
 
     # {{{ Check ACLS
     #If the watcher we're trying to add is for the current user
-    if ( $self->CurrentUser->PrincipalId  eq $args{'PrincipalId'}) {
-        #  If it's an AdminCc and they don't have 
+    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'))
+            unless (    $self->CurrentUserHasRight('ModifyTicket')
+                     or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
+                return ( 0, $self->loc('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, $self->loc('Permission Denied'))
+        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->warn( "$self -> DeleteWatcher got passed a bogus type");
-            return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') );
+            $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
+            return ( 0,
+                     $self->loc('Error in parameters to Ticket->DeleteWatcher') );
         }
     }
 
-    # If the watcher isn't the current user 
+    # If the watcher isn't the current user
     # and the current user  doesn't have 'ModifyTicket' bail
     else {
         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
@@ -1560,39 +1251,104 @@ sub DeleteWatcher {
 
     # }}}
 
-
     # see if this user is already a watcher.
 
-    unless ( $group->HasMember($principal)) {
-        return ( 0, 
-        $self->loc('That principal is not a [_1] for this ticket', $args{'Type'}) );
+    unless ( $group->HasMember($principal) ) {
+        return ( 0,
+                 $self->loc( 'That principal is not a [_1] for this ticket',
+                             $args{'Type'} ) );
     }
 
-    my ($m_id, $m_msg) = $group->_DeleteMember($principal->Id);
+    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."\n".$m_msg);
+        $RT::Logger->error( "Failed to delete "
+                            . $principal->Id
+                            . " as a member of group "
+                            . $group->Id . ": "
+                            . $m_msg );
 
-        return ( 0,    $self->loc('Could not remove that principal as a [_1] for this ticket', $args{'Type'}) );
+        return (0,
+                $self->loc(
+                    'Could not remove that principal as a [_1] for this ticket',
+                    $args{'Type'} ) );
     }
 
     unless ( $args{'Silent'} ) {
-        $self->_NewTransaction(
-            Type     => 'DelWatcher',
-            OldValue => $principal->Id,
-            Field    => $args{'Type'}
-        );
+        $self->_NewTransaction( Type     => 'DelWatcher',
+                                OldValue => $principal->Id,
+                                Field    => $args{'Type'} );
     }
 
-    return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} ));
+    return ( 1,
+             $self->loc( "[_1] is no longer a [_2] for this ticket.",
+                         $principal->Object->Name,
+                         $args{'Type'} ) );
 }
 
 
 
-
 # }}}
 
 
+=head2 SquelchMailTo [EMAIL]
+
+Takes an optional email address to never email about updates to this ticket.
+
+
+Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
+
+
+=cut
+
+sub SquelchMailTo {
+    my $self = shift;
+    if (@_) {
+        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+            return undef;
+        }
+    } else {
+        unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+            return undef;
+        }
+
+    }
+    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);
+}
+
+
+=head2 UnsquelchMailTo ADDRESS
+
+Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
+
+Returns a tuple of (status, message)
+
+=cut
+
+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);
+}
+
+
 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
 
 =head2 RequestorAddresses
@@ -1641,7 +1397,6 @@ sub CcAddresses {
     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
         return undef;
     }
-
     return ( $self->Cc->MemberEmailAddressesAsString);
 
 }
@@ -1736,6 +1491,8 @@ PrincipalId is an RT::Principal id, and Email is an email address.
 Returns true if the specified principal (or the one corresponding to the
 specified address) is a member of the group Type for this ticket.
 
+XX TODO: This should be Memoized. 
+
 =cut
 
 sub IsWatcher {
@@ -1752,7 +1509,6 @@ sub IsWatcher {
     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
 
     # Find the relevant principal.
-    my $principal = RT::Principal->new($self->CurrentUser);
     if (!$args{PrincipalId} && $args{Email}) {
         # Look up the specified user.
         my $user = RT::User->new($self->CurrentUser);
@@ -1765,10 +1521,9 @@ sub IsWatcher {
             return 0;
         }
     }
-    $principal->Load($args{'PrincipalId'});
 
     # Ask if it has the member in question
-    return ($group->HasMember($principal));
+    return $group->HasMember( $args{'PrincipalId'} );
 }
 
 # }}}
@@ -1777,9 +1532,9 @@ sub IsWatcher {
 
 =head2 IsRequestor PRINCIPAL_ID
   
-  Takes an RT::Principal id
-  Returns true if the principal is a requestor of the current ticket.
+Takes an L<RT::Principal> id.
 
+Returns true if the principal is a requestor of the current ticket.
 
 =cut
 
@@ -1798,7 +1553,7 @@ sub IsRequestor {
 =head2 IsCc PRINCIPAL_ID
 
   Takes an RT::Principal id.
-  Returns true if the principal is a requestor of the current ticket.
+  Returns true if the principal is a Cc of the current ticket.
 
 
 =cut
@@ -1818,7 +1573,7 @@ sub IsCc {
 =head2 IsAdminCc PRINCIPAL_ID
 
   Takes an RT::Principal id.
-  Returns true if the principal is a requestor of the current ticket.
+  Returns true if the principal is an AdminCc of the current ticket.
 
 =cut
 
@@ -1847,8 +1602,8 @@ sub IsOwner {
 
     # no ACL check since this is used in acl decisions
     # unless ($self->CurrentUserHasRight('ShowTicket')) {
-    #  return(undef);
-    #   }      
+    #    return(undef);
+    #   }    
 
     #Tickets won't yet have owners when they're being created.
     unless ( $self->OwnerObj->id ) {
@@ -1869,11 +1624,52 @@ sub IsOwner {
 
 # }}}
 
-# {{{ Routines dealing with queues 
 
-# {{{ sub ValidateQueue
+=head2 TransactionAddresses
 
-sub ValidateQueue {
+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;
+
+    my %addresses = ();
+    foreach my $type (qw(Create Comment Correspond)) {
+    $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
+        }
+
+    while (my $txn = $txns->Next) {
+        my $txnaddrs = $txn->Addresses; 
+        foreach my $addrlist ( values %$txnaddrs ) {
+                foreach my $addr (@$addrlist) {
+                    # 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);
+                    # skips "comment-only" addresses
+                    next unless ($addr->address);
+                    $addresses{$addr->address} = $addr;
+                }
+        }
+    }
+
+    return \%addresses;
+
+}
+
+
+
+
+# {{{ Routines dealing with queues 
+
+# {{{ sub ValidateQueue
+
+sub ValidateQueue {
     my $self  = shift;
     my $Value = shift;
 
@@ -1933,11 +1729,27 @@ sub SetQueue {
         )
       )
     {
-        $self->Untake();
+        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;
     }
 
-    return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
+    my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
 
+    if ( $status ) {
+        # 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;
+        }
+    }
+    
+    return ($status, $msg);
 }
 
 # }}}
@@ -1980,8 +1792,8 @@ sub DueObj {
     my $time = new RT::Date( $self->CurrentUser );
 
     # -1 is RT::Date slang for never
-    if ( $self->Due ) {
-        $time->Set( Format => 'sql', Value => $self->Due );
+    if ( my $due = $self->Due ) {
+        $time->Set( Format => 'sql', Value => $due );
     }
     else {
         $time->Set( Format => 'unix', Value => -1 );
@@ -2041,12 +1853,12 @@ sub SetStarted {
     my $time = shift || 0;
 
     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        return ( 0, self->loc("Permission Denied") );
+        return ( 0, $self->loc("Permission Denied") );
     }
 
     #We create a date object to catch date weirdness
     my $time_obj = new RT::Date( $self->CurrentUser() );
-    if ( $time != 0 ) {
+    if ( $time ) {
         $time_obj->Set( Format => 'ISO', Value => $time );
     }
     else {
@@ -2160,20 +1972,34 @@ Returns the amount of time worked on this ticket as a Text String
 
 sub TimeWorkedAsString {
     my $self = shift;
-    return "0" unless $self->TimeWorked;
+    my $value = $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.
+    # 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 );
+}
 
-    my $worked = new RT::Date( $self->CurrentUser );
+# }}}
 
-    #return the  #of minutes worked turned into seconds and written as
-    # a simple text string
+# {{{ sub TimeLeftAsString
 
-    return ( $worked->DurationAsString( $self->TimeWorked * 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 );
+}
 
 # }}}
 
@@ -2184,15 +2010,20 @@ sub TimeWorkedAsString {
 =head2 Comment
 
 Comment on this ticket.
-Takes a hashref with the following attributes:
+Takes a hash with the following attributes:
 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
-commentl
+comment.
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
 
-MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content.
+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
 
-## Please see file perltidy.ERR
 sub Comment {
     my $self = shift;
 
@@ -2201,49 +2032,27 @@ sub Comment {
                  MIMEObj      => undef,
                  Content      => undef,
                  TimeTaken => 0,
+                 DryRun     => 0, 
                  @_ );
 
     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
-        return ( 0, $self->loc("Permission Denied") );
+        return ( 0, $self->loc("Permission Denied"), undef );
     }
+    $args{'NoteType'} = 'Comment';
 
-    unless ( $args{'MIMEObj'} ) {
-        if ( $args{'Content'} ) {
-            use MIME::Entity;
-            $args{'MIMEObj'} = MIME::Entity->build(
-               Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
-           );
-        }
-        else {
-
-            return ( 0, $self->loc("No correspondence attached") );
-        }
+    if ($args{'DryRun'}) {
+        $RT::Handle->BeginTransaction();
+        $args{'CommitScrips'} = 0;
     }
 
-    RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
-
-    # 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'} )
-       if defined $args{'CcMessageTo'};
-    $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} )
-       if defined $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'}
-    );
+    my @results = $self->_RecordNote(%args);
+    if ($args{'DryRun'}) {
+        $RT::Handle->Rollback();
+    }
 
-    return ( $Trans, $self->loc("The comment has been recorded") );
+    return(@results);
 }
-
 # }}}
 
 # {{{ sub Correspond
@@ -2254,10 +2063,16 @@ Correspond on this ticket.
 Takes a hashref with the following attributes:
 
 
-MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content
+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
 
@@ -2272,305 +2087,123 @@ sub Correspond {
 
     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
-        return ( 0, $self->loc("Permission Denied") );
+        return ( 0, $self->loc("Permission Denied"), undef );
     }
 
-    unless ( $args{'MIMEObj'} ) {
-        if ( $args{'Content'} ) {
-            use MIME::Entity;
-            $args{'MIMEObj'} = MIME::Entity->build(
-               Data => ( ref $args{'Content'} ?  $args{'Content'} : [ $args{'Content'} ] )
-           );
-
-        }
-        else {
-
-            return ( 0, $self->loc("No correspondence attached") );
-        }
+    $args{'NoteType'} = 'Correspond'; 
+    if ($args{'DryRun'}) {
+        $RT::Handle->BeginTransaction();
+        $args{'CommitScrips'} = 0;
     }
 
-    RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
-
-    # 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'} )
-       if defined $args{'CcMessageTo'};
-    $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} )
-       if defined $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'} );
-
-    unless ($Trans) {
-        $RT::Logger->err( "$self couldn't init a transaction $msg");
-        return ( $Trans, $self->loc("correspondence (probably) not sent"), $args{'MIMEObj'} );
-    }
+    my @results = $self->_RecordNote(%args);
 
     #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"
+    $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
 
-    unless ( $TransObj->IsInbound ) {
-        $self->_SetTold;
+    if ($args{'DryRun'}) {
+        $RT::Handle->Rollback();
     }
 
-    return ( $Trans, $self->loc("correspondence sent") );
-}
-
-# }}}
-
-# }}}
-
-# {{{ Routines dealing with Links and Relations between tickets
-
-# {{{ Link Collections
-
-# {{{ sub Members
-
-=head2 Members
-
-  This returns an RT::Links object which references all the tickets 
-which are 'MembersOf' this ticket
-
-=cut
-
-sub Members {
-    my $self = shift;
-    return ( $self->_Links( 'Target', 'MemberOf' ) );
-}
-
-# }}}
-
-# {{{ sub MemberOf
-
-=head2 MemberOf
-
-  This returns an RT::Links object which references all the tickets that this
-ticket is a 'MemberOf'
-
-=cut
-
-sub MemberOf {
-    my $self = shift;
-    return ( $self->_Links( 'Base', 'MemberOf' ) );
-}
-
-# }}}
-
-# {{{ RefersTo
-
-=head2 RefersTo
-
-  This returns an RT::Links object which shows all references for which this ticket is a base
-
-=cut
-
-sub RefersTo {
-    my $self = shift;
-    return ( $self->_Links( 'Base', 'RefersTo' ) );
-}
-
-# }}}
-
-# {{{ ReferredToBy
-
-=head2 ReferredToBy
-
-  This returns an RT::Links object which shows all references for which this ticket is a target
-
-=cut
-
-sub ReferredToBy {
-    my $self = shift;
-    return ( $self->_Links( 'Target', 'RefersTo' ) );
-}
-
-# }}}
-
-# {{{ DependedOnBy
+    return (@results);
 
-=head2 DependedOnBy
-
-  This returns an RT::Links object which references all the tickets that depend on this one
-
-=cut
-
-sub DependedOnBy {
-    my $self = shift;
-    return ( $self->_Links( 'Target', 'DependsOn' ) );
 }
 
 # }}}
 
+# {{{ sub _RecordNote
 
+=head2 _RecordNote
 
-=head2 HasUnresolvedDependencies
-
-  Takes a paramhash of Type (default to '__any').  Returns true if
-$self->UnresolvedDependencies returns an object with one or more members
-of that type.  Returns false otherwise
-
-
-=begin testing
+the meat of both comment and correspond. 
 
-my $t1 = RT::Ticket->new($RT::SystemUser);
-my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
-ok($id, "Created dep test 1 - $msg");
-
-my $t2 = RT::Ticket->new($RT::SystemUser);
-my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
-ok($id2, "Created dep test 2 - $msg2");
-my $t3 = RT::Ticket->new($RT::SystemUser);
-my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
-ok($id3, "Created dep test 3 - $msg3");
-
-ok ($t1->AddLink( Type => 'DependsOn', Target => $t2->id));
-ok ($t1->AddLink( Type => 'DependsOn', Target => $t3->id));
-
-ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
-ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
-ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
-ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
-my ($rid, $rmsg)= $t1->Resolve();
-ok(!$rid, $rmsg);
-ok($t2->Resolve);
-($rid, $rmsg)= $t1->Resolve();
-ok(!$rid, $rmsg);
-ok($t3->Resolve);
-($rid, $rmsg)= $t1->Resolve();
-ok($rid, $rmsg);
-
-
-=end testing
+Performs no access control checks. hence, dangerous.
 
 =cut
 
-sub HasUnresolvedDependencies {
+sub _RecordNote {
     my $self = shift;
-    my %args = (
-        Type   => undef,
+    my %args = ( 
+        CcMessageTo  => undef,
+        BccMessageTo => undef,
+        Encrypt      => undef,
+        Sign         => undef,
+        MIMEObj      => undef,
+        Content      => undef,
+        NoteType     => 'Correspond',
+        TimeTaken    => 0,
+        CommitScrips => 1,
         @_
     );
 
-    my $deps = $self->UnresolvedDependencies;
-
-    if ($args{Type}) {
-        $deps->Limit( FIELD => 'Type', 
-              OPERATOR => '=',
-              VALUE => $args{Type}); 
-    }
-    else {
-           $deps->IgnoreType;
+    unless ( $args{'MIMEObj'} || $args{'Content'} ) {
+        return ( 0, $self->loc("No message attached"), undef );
     }
 
-    if ($deps->Count > 0) {
-        return 1;
-    }
-    else {
-        return (undef);
+    unless ( $args{'MIMEObj'} ) {
+        $args{'MIMEObj'} = MIME::Entity->build(
+            Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
+        );
     }
-}
 
+    # convert text parts into utf-8
+    RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
 
-# {{{ UnresolvedDependencies 
-
-=head2 UnresolvedDependencies
-
-Returns an RT::Tickets object of tickets which this ticket depends on
-and which have a status of new, open or stalled. (That list comes from
-RT::Queue->ActiveStatusArray
-
-=cut
+    # 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
 
 
-sub UnresolvedDependencies {
-    my $self = shift;
-    my $deps = RT::Tickets->new($self->CurrentUser);
+    foreach my $type (qw/Cc Bcc/) {
+        if ( defined $args{ $type . 'MessageTo' } ) {
 
-    my @live_statuses = RT::Queue->ActiveStatusArray();
-    foreach my $status (@live_statuses) {
-        $deps->LimitStatus(VALUE => $status);
+            my $addresses = join ', ', (
+                map { RT::User->CanonicalizeEmailAddress( $_->address ) }
+                    Email::Address->parse( $args{ $type . 'MessageTo' } ) );
+            $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
+        }
     }
-    $deps->LimitDependedOnBy($self->Id);
-
-    return($deps);
-
-}
-
-# }}}
-
-# {{{ AllDependedOnBy
-
-=head2 AllDependedOnBy
 
-Returns an array of RT::Ticket objects which (directly or indirectly)
-depends on this ticket; takes an optional 'Type' argument in the param
-hash, which will limit returned tickets to that type, as well as cause
-tickets with that type to serve as 'leaf' nodes that stops the recursive
-dependency search.
+    foreach my $argument (qw(Encrypt Sign)) {
+        $args{'MIMEObj'}->head->add(
+            "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
+        ) if defined $args{ $argument };
+    }
 
-=cut
+    # 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 = $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' => RT::Interface::Email::GenMessageId( Ticket => $self )
+        );
+    }
 
-sub AllDependedOnBy {
-    my $self = shift;
-    my $dep = $self->DependedOnBy;
-    my %args = (
-        Type   => undef,
-       _found => {},
-       _top   => 1,
-        @_
+    #Record the correspondence (write the transaction)
+    my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
+             Type => $args{'NoteType'},
+             Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
+             TimeTaken => $args{'TimeTaken'},
+             MIMEObj   => $args{'MIMEObj'}, 
+             CommitScrips => $args{'CommitScrips'},
     );
 
-    while (my $link = $dep->Next()) {
-       next unless ($link->BaseURI->IsLocal());
-       next if $args{_found}{$link->BaseObj->Id};
-
-       if (!$args{Type}) {
-           $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
-           $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
-       }
-       elsif ($link->BaseObj->Type eq $args{Type}) {
-           $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
-       }
-       else {
-           $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
-       }
+    unless ($Trans) {
+        $RT::Logger->err("$self couldn't init a transaction $msg");
+        return ( $Trans, $self->loc("Message could not be recorded"), undef );
     }
 
-    if ($args{_top}) {
-       return map { $args{_found}{$_} } sort keys %{$args{_found}};
-    }
-    else {
-       return 1;
-    }
+    return ( $Trans, $self->loc("Message recorded"), $TransObj );
 }
 
 # }}}
 
-# {{{ DependsOn
-
-=head2 DependsOn
-
-  This returns an RT::Links object which references all the tickets that this ticket depends on
-
-=cut
-
-sub DependsOn {
-    my $self = shift;
-    return ( $self->_Links( 'Base', 'DependsOn' ) );
-}
-
 # }}}
 
-
-
-
 # {{{ sub _Links 
 
 sub _Links {
@@ -2581,31 +2214,36 @@ sub _Links {
     my $field = shift;
     my $type  = shift || "";
 
-    unless ( $self->{"$field$type"} ) {
-        $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
-        if ( $self->CurrentUserHasRight('ShowTicket') ) {
-            # Maybe this ticket is a merged ticket
-            my $Tickets = new RT::Tickets( $self->CurrentUser );
-            # at least to myself
-            $self->{"$field$type"}->Limit( FIELD => $field,
-                                           VALUE => $self->URI,
-                                           ENTRYAGGREGATOR => 'OR' );
-            $Tickets->Limit( FIELD => 'EffectiveId',
-                             VALUE => $self->EffectiveId );
-            while (my $Ticket = $Tickets->Next) {
-                $self->{"$field$type"}->Limit( FIELD => $field,
-                                               VALUE => $Ticket->URI,
-                                               ENTRYAGGREGATOR => 'OR' );
-            }
-            $self->{"$field$type"}->Limit( FIELD => 'Type',
-                                           VALUE => $type )
-              if ($type);
-        }
+    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 );
+        return $links;
     }
-    return ( $self->{"$field$type"} );
-}
 
-# }}}
+    # Maybe this ticket is a merge ticket
+    my $limit_on = 'Local'. $field;
+    # at least to myself
+    $links->Limit(
+        FIELD           => $limit_on,
+        VALUE           => $self->id,
+        ENTRYAGGREGATOR => 'OR',
+    );
+    $links->Limit(
+        FIELD           => $limit_on,
+        VALUE           => $_,
+        ENTRYAGGREGATOR => 'OR',
+    ) foreach $self->Merged;
+    $links->Limit(
+        FIELD => 'Type',
+        VALUE => $type,
+    ) if $type;
+
+    return $links;
+}
 
 # }}}
 
@@ -2613,9 +2251,14 @@ sub _Links {
 
 =head2 DeleteLink
 
-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
+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 
 
@@ -2625,71 +2268,80 @@ sub DeleteLink {
         Base   => undef,
         Target => undef,
         Type   => undef,
+        Silent => undef,
+        SilentBase   => undef,
+        SilentTarget => undef,
         @_
     );
 
-    #check acls
-    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        $RT::Logger->debug("No permission to delete links\n");
-        return ( 0, $self->loc('Permission Denied'))
-
+    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') );
     }
 
-    #we want one of base and target. we don't care which
-    #but we only want _one_
-
-    my $direction;
-    my $remote_link;
-
-    if ( $args{'Base'} and $args{'Target'} ) {
-        $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
-        return ( 0, $self->loc("Can't specifiy both base and target") );
-    }
-    elsif ( $args{'Base'} ) {
-        $args{'Target'} = $self->URI();
-       $remote_link = $args{'Base'};
-       $direction = 'Target';
+    #check acls
+    my $right = 0;
+    $right++ if $self->CurrentUserHasRight('ModifyTicket');
+    if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+        return ( 0, $self->loc("Permission Denied") );
     }
-    elsif ( $args{'Target'} ) {
-        $args{'Base'} = $self->URI();
-       $remote_link = $args{'Target'};
-        $direction='Base';
+
+    # 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++;
     }
-    else {
-        $RT::Logger->debug("$self: Base or Target must be specified\n");
-        return ( 0, $self->loc('Either base or target must be specified') );
+    if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+         ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+    {
+        return ( 0, $self->loc("Permission Denied") );
     }
 
-    my $link = new RT::Link( $self->CurrentUser );
-    $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
+    my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+    return ( 0, $Msg ) unless $val;
 
+    return ( $val, $Msg ) if $args{'Silent'};
 
-    $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=>  $args{'Target'} );
-    #it's a real link. 
-    if ( $link->id ) {
+    my ($direction, $remote_link);
 
-        my $linkid = $link->id;
-        $link->Delete();
+    if ( $args{'Base'} ) {
+        $remote_link = $args{'Base'};
+        $direction = 'Target';
+    }
+    elsif ( $args{'Target'} ) {
+        $remote_link = $args{'Target'};
+        $direction = 'Base';
+    } 
 
-        my $TransString = "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
-       my $remote_uri = RT::URI->new( $RT::SystemUser );
-       $remote_uri->FromURI( $remote_link );
+    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,
+            Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
+            OldValue  => $remote_uri->URI || $remote_link,
             TimeTaken => 0
         );
-
-        return ( $Trans, $self->loc("Link deleted ([_1])", $TransString));
+        $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;
     }
 
-    #if it's not a link we can find
-    else {
-        $RT::Logger->debug("Couldn't find that link\n");
-        return ( 0, $self->loc("Link not found") );
-    }
+    return ( $val, $Msg );
 }
 
 # }}}
@@ -2700,183 +2352,225 @@ sub DeleteLink {
 
 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,
+    my %args = ( Target       => '',
+                 Base         => '',
+                 Type         => '',
+                 Silent       => undef,
+                 SilentBase   => undef,
+                 SilentTarget => undef,
                  @_ );
 
-    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        return ( 0, $self->loc("Permission Denied") );
-    }
-
-    # Remote_link is the URI of the object that is not this ticket
-    my $remote_link;
-    my $direction;
-
-    if ( $args{'Base'} and $args{'Target'} ) {
-        $RT::Logger->debug(
-"$self tried to delete a link. both base and target were specified\n" );
-        return ( 0, $self->loc("Can't specifiy both base and target") );
-    }
-    elsif ( $args{'Base'} ) {
-        $args{'Target'} = $self->URI();
-       $remote_link = $args{'Base'};
-       $direction = 'Target';
-    }
-    elsif ( $args{'Target'} ) {
-        $args{'Base'} = $self->URI();
-       $remote_link = $args{'Target'};
-        $direction='Base';
-    }
-    else {
+    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') );
     }
 
-    # 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
-    use RT::Link;
-    my $old_link = RT::Link->new( $self->CurrentUser );
-    $old_link->LoadByParams( Base   => $args{'Base'},
-                             Type   => $args{'Type'},
-                             Target => $args{'Target'} );
-    if ( $old_link->Id ) {
-        $RT::Logger->debug("$self Somebody tried to duplicate a link");
-        return ( $old_link->id, $self->loc("Link already exists"), 0 );
+    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") );
+    }
 
-    # 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} );
+    return $self->_AddLink(%args);
+}
 
-    unless ($linkid) {
-        return ( 0, $self->loc("Link could not be created") );
-    }
+sub __GetTicketFromURI {
+    my $self = shift;
+    my %args = ( URI => '', @_ );
 
-    my $TransString =
-      "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
+    # 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 );
+    $uri_obj->FromURI( $args{'URI'} );
 
-    # Don't write the transaction if we're doing this on create
-    if ( $args{'Silent'} ) {
-        return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
+    unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
+        my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
+        $RT::Logger->warning( $msg );
+        return( 0, $msg );
     }
-    else {
-       my $remote_uri = RT::URI->new( $RT::SystemUser );
-       $remote_uri->FromURI( $remote_link );
-
-        #Write the transaction
-        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
-                                                         Type  => 'AddLink',
-                                                         Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
-                                                                                    NewValue =>  $remote_uri->URI || $remote_link,
-                                                         TimeTaken => 0 );
-        return ( $Trans, $self->loc( "Link created ([_1])", $TransString ) );
+    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  
 
-# {{{ sub URI 
+Private non-acled variant of AddLink so that links can be added during create.
 
-=head2 URI
+=cut
 
-Returns this ticket's URI
+sub _AddLink {
+    my $self = shift;
+    my %args = ( Target       => '',
+                 Base         => '',
+                 Type         => '',
+                 Silent       => undef,
+                 SilentBase   => undef,
+                 SilentTarget => undef,
+                 @_ );
 
-=cut
+    my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+    return ($val, $msg) if !$val || $exist;
+    return ($val, $msg) if $args{'Silent'};
 
-sub URI {
-    my $self = shift;
-    my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
-    return($uri->URIForObject($self));
+    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 );
 }
 
 # }}}
 
+
 # {{{ sub MergeInto
 
 =head2 MergeInto
+
 MergeInto take the id of the ticket to merge this ticket into.
 
 =cut
 
 sub MergeInto {
     my $self      = shift;
-    my $MergeInto = shift;
+    my $ticket_id = shift;
 
     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
         return ( 0, $self->loc("Permission Denied") );
     }
 
     # Load up the new ticket.
-    my $NewTicket = RT::Ticket->new($RT::SystemUser);
-    $NewTicket->Load($MergeInto);
+    my $MergeInto = RT::Ticket->new($self->CurrentUser);
+    $MergeInto->Load($ticket_id);
 
     # make sure it exists.
-    unless ( defined $NewTicket->Id ) {
+    unless ( $MergeInto->Id ) {
         return ( 0, $self->loc("New ticket doesn't exist") );
     }
 
     # Make sure the current user can modify the new ticket.
-    unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) {
-        $RT::Logger->debug("failed...");
+    unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
         return ( 0, $self->loc("Permission Denied") );
     }
 
-    $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,
-            $self->loc("Can't merge into a merged ticket. You should never get this error") );
-    }
+    delete $MERGE_CACHE{'effective'}{ $self->id };
+    delete @{ $MERGE_CACHE{'merged'} }{
+        $ticket_id, $MergeInto->id, $self->id
+    };
+
+    $RT::Handle->BeginTransaction();
 
     # 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 
+    # 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 => $NewTicket->Id()
+        Value => $MergeInto->Id()
     );
 
     unless ($id_val) {
-        $RT::Logger->error(
-            "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
+        $RT::Handle->Rollback();
         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
     }
 
-    my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
 
-    unless ($status_val) {
-        $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
-    }
+    if ( $self->__Value('Status') ne 'resolved' ) {
+
+        my ( $status_val, $status_msg )
+            = $self->__Set( Field => 'Status', Value => 'resolved' );
 
+        unless ($status_val) {
+            $RT::Handle->Rollback();
+            $RT::Logger->error(
+                $self->loc(
+                    "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
+                    $self
+                )
+            );
+            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 ($link->Base eq $NewTicket->URI) {
+        if (exists $old_seen{$link->Base."-".$link->Type}) {
+            $link->Delete;
+        }   
+        elsif ($link->Base eq $MergeInto->URI) {
             $link->Delete;
         } else {
-            $link->SetTarget($NewTicket->URI);
+            # 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;
         }
 
     }
@@ -2885,40 +2579,54 @@ sub MergeInto {
     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
 
     while (my $link = $old_links_from->Next) {
-        if ($link->Target eq $NewTicket->URI) {
+        if (exists $old_seen{$link->Type."-".$link->Target}) {
+            $link->Delete;
+        }   
+        if ($link->Target eq $MergeInto->URI) {
             $link->Delete;
         } else {
-            $link->SetBase($NewTicket->URI);
+            # 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) {
 
-    #make a new link: this ticket is merged into that other ticket.
-    $self->AddLink( Type   => 'MergedInto', Target => $NewTicket->Id());
+        my $mutator = "Set$type";
+        $MergeInto->$mutator(
+            ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
 
-    #add all of this ticket's watchers to that ticket.
-    my $requestors = $self->Requestors->MembersObj;
-    while (my $watcher = $requestors->Next) { 
-        $NewTicket->_AddWatcher( Type => 'Requestor',
-                                  Silent => 1,
-                                  PrincipalId => $watcher->MemberId);
     }
+#add all of this ticket's watchers to that ticket.
+    foreach my $watcher_type qw(Requestors Cc AdminCc) {
 
-    my $Ccs = $self->Cc->MembersObj;
-    while (my $watcher = $Ccs->Next) { 
-        $NewTicket->_AddWatcher( Type => 'Cc',
-                                  Silent => 1,
-                                  PrincipalId => $watcher->MemberId);
-    }
+        my $people = $self->$watcher_type->MembersObj;
+        my $addwatcher_type =  $watcher_type;
+        $addwatcher_type  =~ s/s$//;
 
-    my $AdminCcs = $self->AdminCc->MembersObj;
-    while (my $watcher = $AdminCcs->Next) { 
-        $NewTicket->_AddWatcher( Type => 'AdminCc',
-                                  Silent => 1,
-                                  PrincipalId => $watcher->MemberId);
+        while ( my $watcher = $people->Next ) {
+            
+           my ($val, $msg) =  $MergeInto->_AddWatcher(
+                Type        => $addwatcher_type,
+                Silent => 1,
+                PrincipalId => $watcher->MemberId
+            );
+            unless ($val) {
+                $RT::Logger->warning($msg);
+            }
     }
 
+    }
 
     #find all of the tickets that were merged into this ticket. 
     my $old_mergees = new RT::Tickets( $self->CurrentUser );
@@ -2932,15 +2640,46 @@ sub MergeInto {
     while ( my $ticket = $old_mergees->Next() ) {
         my ( $val, $msg ) = $ticket->__Set(
             Field => 'EffectiveId',
-            Value => $NewTicket->Id()
+            Value => $MergeInto->Id()
         );
     }
 
-    $NewTicket->_SetLastUpdated;
+    #make a new link: this ticket is merged into that other ticket.
+    $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
+
+    $MergeInto->_SetLastUpdated;    
 
+    $RT::Handle->Commit();
     return ( 1, $self->loc("Merge Successful") );
 }
 
+=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 || [] };
+}
+
 # }}}
 
 # }}}
@@ -2997,25 +2736,6 @@ Takes two arguments:
 and  (optionally) the type of the SetOwner Transaction. It defaults
 to 'Give'.  'Steal' is also a valid option.
 
-=begin testing
-
-my $root = RT::User->new($RT::SystemUser);
-$root->Load('root');
-ok ($root->Id, "Loaded the root user");
-my $t = RT::Ticket->new($RT::SystemUser);
-$t->Load(1);
-$t->SetOwner('root');
-ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket");
-$t->Steal();
-ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket");
-my $txns = RT::Transactions->new($RT::SystemUser);
-$txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
-$txns->Limit(FIELD => 'Ticket', VALUE => '1');
-my $steal  = $txns->First;
-ok($steal->OldValue == $root->Id , "Stolen from root");
-ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
-
-=end testing
 
 =cut
 
@@ -3024,75 +2744,89 @@ sub SetOwner {
     my $NewOwner = shift;
     my $Type     = shift || "Give";
 
+    $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 ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
+    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 (    $self->OwnerObj->Id != $RT::Nobody->Id
-            && $self->OwnerObj->Id != $self->CurrentUser->id ) {
+    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") );
         }
     }
-    my $NewOwnerObj = RT::User->new( $self->CurrentUser );
-    my $OldOwnerObj = $self->OwnerObj;
-
-    $NewOwnerObj->Load($NewOwner);
-    if ( !$NewOwnerObj->Id ) {
-        return ( 0, $self->loc("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,
-                 $self->loc(
-"You can only reassign tickets that you own or that are unowned" ) );
+    # 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->Id )
-            and ( !$NewOwnerObj->HasRight( Right  => 'OwnTicket',
-                                           Object => $self ) )
-      ) {
+    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 (     ( $self->OwnerObj )
-            and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
+    # 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") );
     }
 
-    $RT::Handle->BeginTransaction();
-
     # 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 ) = $self->OwnerGroup->MembersObj->First->Delete();
+    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. ") . $del_msg );
+        return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
     }
 
     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
@@ -3100,7 +2834,7 @@ sub SetOwner {
                                        InsideTransaction => 1 );
     unless ($add_id) {
         $RT::Handle->Rollback();
-        return ( 0, $self->loc("Could not change owner. ") . $add_msg );
+        return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
     }
 
     # We call set twice with slightly different arguments, so
@@ -3117,26 +2851,29 @@ sub SetOwner {
 
     unless ($val) {
         $RT::Handle->Rollback;
-        return ( 0, $self->loc("Could not change owner. ") . $msg );
+        return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
     }
 
-    $RT::Handle->Commit();
-
-    my ( $trans, $msg, undef ) = $self->_NewTransaction(
-                                                   Type     => $Type,
-                                                   Field    => 'Owner',
-                                                   NewValue => $NewOwnerObj->Id,
-                                                   OldValue => $OldOwnerObj->Id,
-                                                   TimeTaken => 0 );
+    ($val, $msg) = $self->_NewTransaction(
+        Type      => $Type,
+        Field     => 'Owner',
+        NewValue  => $NewOwnerObj->Id,
+        OldValue  => $OldOwnerObj->Id,
+        TimeTaken => 0,
+    );
 
-    if ($trans) {
+    if ( $val ) {
         $msg = $self->loc( "Owner changed from [_1] to [_2]",
                            $OldOwnerObj->Name, $NewOwnerObj->Name );
-
-        # TODO: make sure the trans committed properly
     }
-    return ( $trans, $msg );
+    else {
+        $RT::Handle->Rollback();
+        return ( 0, $msg );
+    }
+
+    $RT::Handle->Commit();
 
+    return ( $val, $msg );
 }
 
 # }}}
@@ -3231,25 +2968,6 @@ Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, r
 
 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE).  If FORCE is true, ignore unresolved dependencies and force a status change.
 
-=begin testing
-
-my $tt = RT::Ticket->new($RT::SystemUser);
-my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
-            Subject => 'test');
-ok($id, $msg);
-ok($tt->Status eq 'new', "New ticket is created as new");
-
-($id, $msg) = $tt->SetStatus('open');
-ok($id, $msg);
-ok ($msg =~ /open/i, "Status message is correct");
-($id, $msg) = $tt->SetStatus('resolved');
-ok($id, $msg);
-ok ($msg =~ /resolved/i, "Status message is correct");
-($id, $msg) = $tt->SetStatus('resolved');
-ok(!$id,$msg);
-
-
-=end testing
 
 
 =cut
@@ -3259,15 +2977,21 @@ sub SetStatus {
     my %args;
 
     if (@_ == 1) {
-       $args{Status} = shift;
+    $args{Status} = shift;
     }
     else {
-       %args = (@_);
+    %args = (@_);
     }
 
     #Check ACL
-    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        return ( 0, $self->loc('Permission Denied') );
+    if ( $args{Status} eq 'deleted') {
+            unless ($self->CurrentUserHasRight('DeleteTicket')) {
+            return ( 0, $self->loc('Permission Denied') );
+       }
+    } else {
+            unless ($self->CurrentUserHasRight('ModifyTicket')) {
+            return ( 0, $self->loc('Permission Denied') );
+       }
     }
 
     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
@@ -3278,7 +3002,7 @@ sub SetStatus {
     $now->SetToNow();
 
     #If we're changing the status from new, record that we've started
-    if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
+    if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
 
         #Set the Started time to "now"
         $self->_Set( Field             => 'Started',
@@ -3286,9 +3010,9 @@ sub SetStatus {
                      RecordTransaction => 0 );
     }
 
-    if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {
-
-        #When we resolve a ticket, set the 'Resolved' attribute to now.
+    #When we close a ticket, set the 'Resolved' attribute to now.
+    # It's misnamed, but that's just historical.
+    if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
         $self->_Set( Field             => 'Resolved',
                      Value             => $now->ISO,
                      RecordTransaction => 0 );
@@ -3298,6 +3022,7 @@ sub SetStatus {
    my ($val, $msg)= $self->_Set( Field           => 'Status',
                           Value           => $args{Status},
                           TimeTaken       => 0,
+                          CheckACL      => 0,
                           TransactionType => 'Status'  );
 
     return($val,$msg);
@@ -3305,20 +3030,14 @@ sub SetStatus {
 
 # }}}
 
-# {{{ sub Kill
+# {{{ sub Delete
 
-=head2 Kill
+=head2 Delete
 
 Takes no arguments. Marks this ticket for garbage collection
 
 =cut
 
-sub Kill {
-    my $self = shift;
-    $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead.");
-    return $self->Delete;
-}
-
 sub Delete {
     my $self = shift;
     return ( $self->SetStatus('deleted') );
@@ -3390,278 +3109,7 @@ sub Resolve {
 
 # }}}
 
-# {{{ Routines dealing with custom fields
-
-
-# {{{ FirstCustomFieldValue
-
-=item FirstCustomFieldValue FIELD
-
-Return the content of the first value of CustomField FIELD for this ticket
-Takes a field id or name
-
-=cut
-
-sub FirstCustomFieldValue {
-    my $self = shift;
-    my $field = shift;
-    my $values = $self->CustomFieldValues($field);
-    if ($values->First) {
-        return $values->First->Content;
-    } else {
-        return undef;
-    }
-
-}
-
-
-
-# {{{ CustomFieldValues
-
-=item CustomFieldValues FIELD
-
-Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket.  
-Takes a field id or name.
-
-
-=cut
-
-sub CustomFieldValues {
-    my $self  = shift;
-    my $field = shift;
-
-    my $cf = RT::CustomField->new($self->CurrentUser);
-
-    if ($field =~ /^\d+$/) {
-        $cf->LoadById($field);
-    } else {
-        $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id);
-    }
-    my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser );
-    $cf_values->LimitToCustomField($cf->id);
-    $cf_values->LimitToTicket($self->Id());
-
-    # @values is a CustomFieldValues object;
-    return ($cf_values);
-}
-
-# }}}
-
-# {{{ AddCustomFieldValue
-
-=item AddCustomFieldValue { Field => FIELD, Value => VALUE }
-
-VALUE can either be a CustomFieldValue object or a string.
-FIELD can be a CustomField object OR a CustomField ID.
-
-
-Adds VALUE as a value of CustomField FIELD.  If this is a single-value custom field,
-deletes the old value. 
-If VALUE isn't a valid value for the custom field, returns 
-(0, 'Error message' ) otherwise, returns (1, 'Success Message')
-
-=cut
-
-sub AddCustomFieldValue {
-    my $self = shift;
-    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        return ( 0, $self->loc("Permission Denied") );
-    }
-    $self->_AddCustomFieldValue(@_);
-}
-
-sub _AddCustomFieldValue {
-    my $self = shift;
-    my %args = (
-        Field => undef,
-        Value => undef,
-       RecordTransaction => 1,
-        @_
-    );
-
-    my $cf = RT::CustomField->new( $self->CurrentUser );
-    if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
-        $cf->Load( $args{'Field'}->id );
-    }
-    else {
-        $cf->Load( $args{'Field'} );
-    }
-
-    unless ( $cf->Id ) {
-        return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) );
-    }
-
-    # Load up a TicketCustomFieldValues object for this custom field and this ticket
-    my $values = $cf->ValuesForTicket( $self->id );
-
-    unless ( $cf->ValidateValue( $args{'Value'} ) ) {
-        return ( 0, $self->loc("Invalid value for custom field") );
-    }
-
-    # If the custom field only accepts a single value, delete the existing
-    # value and record a "changed from foo to bar" transaction
-    if ( $cf->SingleValue ) {
-
-        # We need to whack any old values here.  In most cases, the custom field should
-        # only have one value to delete.  In the pathalogical case, this custom field
-        # used to be a multiple and we have many values to whack....
-        my $cf_values = $values->Count;
-
-        if ( $cf_values > 1 ) {
-            my $i = 0;   #We want to delete all but the last one, so we can then
-                 # execute the same code to "change" the value from old to new
-            while ( my $value = $values->Next ) {
-                $i++;
-                if ( $i < $cf_values ) {
-                    my $old_value = $value->Content;
-                    my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content);
-                    unless ($val) {
-                        return (0,$msg);
-                    }
-                    my ( $TransactionId, $Msg, $TransactionObj ) =
-                      $self->_NewTransaction(
-                        Type     => 'CustomField',
-                        Field    => $cf->Id,
-                        OldValue => $old_value
-                      );
-                }
-            }
-        }
-
-        my $old_value;
-        if (my $value = $cf->ValuesForTicket( $self->Id )->First) {
-           $old_value = $value->Content();
-           return (1) if $old_value eq $args{'Value'};
-       }
-
-        my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket(
-            Ticket  => $self->Id,
-            Content => $args{'Value'}
-        );
-
-        unless ($new_value_id) {
-            return ( 0,
-                $self->loc("Could not add new custom field value for ticket. [_1] ",
-                  ,$value_msg) );
-        }
-
-        my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser );
-        $new_value->Load($new_value_id);
-
-        # now that adding the new value was successful, delete the old one
-       if ($old_value) {
-           my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value);
-           unless ($val) { 
-                       return (0,$msg);
-           }
-       }
-
-       if ($args{'RecordTransaction'}) {
-        my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
-            Type     => 'CustomField',
-            Field    => $cf->Id,
-            OldValue => $old_value,
-            NewValue => $new_value->Content
-        );
-       }
-
-        if ( $old_value eq '' ) {
-            return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) );
-        }
-        elsif ( $new_value->Content eq '' ) {
-            return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) );
-        }
-        else {
-            return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) );
-        }
-
-    }
-
-    # otherwise, just add a new value and record "new value added"
-    else {
-        my ( $new_value_id ) = $cf->AddValueForTicket(
-            Ticket  => $self->Id,
-            Content => $args{'Value'}
-        );
-
-        unless ($new_value_id) {
-            return ( 0,
-                $self->loc("Could not add new custom field value for ticket. "));
-        }
-    if ( $args{'RecordTransaction'} ) {
-        my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
-            Type     => 'CustomField',
-            Field    => $cf->Id,
-            NewValue => $args{'Value'}
-        );
-        unless ($TransactionId) {
-            return ( 0,
-                $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
-        }
-    }
-        return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name));
-    }
-
-}
-
-# }}}
-
-# {{{ DeleteCustomFieldValue
-
-=item DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
-
-Deletes VALUE as a value of CustomField FIELD. 
-
-VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue.
-
-If VALUE isn't a valid value for the custom field, returns 
-(0, 'Error message' ) otherwise, returns (1, 'Success Message')
-
-=cut
-
-sub DeleteCustomFieldValue {
-    my $self = shift;
-    my %args = (
-        Field => undef,
-        Value => undef,
-        @_);
-
-    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
-        return ( 0, $self->loc("Permission Denied") );
-    }
-    my $cf = RT::CustomField->new( $self->CurrentUser );
-    if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
-        $cf->LoadById( $args{'Field'}->id );
-    }
-    else {
-        $cf->LoadById( $args{'Field'} );
-    }
-
-    unless ( $cf->Id ) {
-        return ( 0, $self->loc("Custom field not found") );
-    }
-
-
-     my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'});
-     unless ($val) { 
-            return (0,$msg);
-     }
-        my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
-            Type     => 'CustomField',
-            Field    => $cf->Id,
-            OldValue => $args{'Value'}
-        );
-        unless($TransactionId) {
-            return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg));
-        } 
-
-        return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name));
-}
-
-# }}}
-
-# }}}
-
+    
 # {{{ Actions + Routines dealing with transactions
 
 # {{{ sub SetTold and _SetTold
@@ -3714,102 +3162,116 @@ sub _SetTold {
                            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;
+}
+
 # }}}
 
-# {{{ sub Transactions 
+=head2 TransactionBatch
 
-=head2 Transactions
+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.
 
-  Returns an RT::Transactions object of all transactions on this ticket
+Only works when the C<UseTransactionBatch> config option is set to true.
 
 =cut
 
-sub Transactions {
+sub TransactionBatch {
     my $self = shift;
+    return $self->{_TransactionBatch};
+}
 
-    use RT::Transactions;
-    my $transactions = RT::Transactions->new( $self->CurrentUser );
+=head2 ApplyTransactionBatch
 
-    #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()
-        );
+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.
 
-        # if the user may not see comments do not return them
-        unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
-            $transactions->Limit(
-                FIELD    => 'Type',
-                OPERATOR => '!=',
-                VALUE    => "Comment"
-            );
-        }
-    }
+=cut
 
-    return ($transactions);
-}
+sub ApplyTransactionBatch {
+    my $self = shift;
 
-# }}}
+    my $batch = $self->TransactionBatch;
+    return unless $batch && @$batch;
 
-# {{{ sub _NewTransaction
+    $self->_ApplyTransactionBatch;
 
-sub _NewTransaction {
+    $self->{_TransactionBatch} = [];
+}
+
+sub _ApplyTransactionBatch {
     my $self = shift;
-    my %args = (
-        TimeTaken => 0,
-        Type      => undef,
-        OldValue  => undef,
-        NewValue  => undef,
-        Data      => undef,
-        Field     => undef,
-        MIMEObj   => undef,
-        @_
+    my $batch = $self->TransactionBatch;
+
+    my %seen;
+    my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
+
+    require RT::Scrips;
+    RT::Scrips->new($RT::SystemUser)->Apply(
+        Stage          => 'TransactionBatch',
+        TicketObj      => $self,
+        TransactionObj => $batch->[0],
+        Type           => $types,
     );
 
-    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'}
+    # 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;
 
-    $self->Load($self->Id);
+    # 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 $@;
 
-    $RT::Logger->warning($msg) unless $transaction;
+    # 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}++;
 
-    $self->_SetLastUpdated;
+    my $batch = $self->TransactionBatch;
+    return unless $batch && @$batch;
 
-    if ( defined $args{'TimeTaken'} ) {
-        $self->_UpdateTimeTaken( $args{'TimeTaken'} );
-    }
-    return ( $transaction, $msg, $trans );
+    return $self->_ApplyTransactionBatch;
 }
 
 # }}}
 
-# }}}
-
 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
 
-# {{{ sub _ClassAccessible
+# {{{ sub _OverlayAccessible
 
-sub _ClassAccessible {
+sub _OverlayAccessible {
     {
         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
           Queue           => { 'read' => 1,  'write' => 1 },
@@ -3823,8 +3285,6 @@ sub _ClassAccessible {
           TimeEstimated      => { '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 },
           Type            => { 'read' => 1 },
@@ -3881,7 +3341,7 @@ sub _Set {
     
         #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 ); }
+        return ( 0, $msg ) unless $ret;
     }
 
     if ( $args{'RecordTransaction'} == 1 ) {
@@ -3893,7 +3353,7 @@ sub _Set {
                                                OldValue  => $Old,
                                                TimeTaken => $args{'TimeTaken'},
         );
-        return ( $Trans, scalar $TransObj->Description );
+        return ( $Trans, scalar $TransObj->BriefDescription );
     }
     else {
         return ( $ret, $msg );
@@ -3919,7 +3379,7 @@ sub _Value {
     #if the field is public, return it.
     if ( $self->_Accessible( $field, 'public' ) ) {
 
-        #$RT::Logger->debug("Skipping ACL check for $field\n");
+        #$RT::Logger->debug("Skipping ACL check for $field");
         return ( $self->SUPER::_Value($field) );
 
     }
@@ -3978,13 +3438,10 @@ sub CurrentUserHasRight {
     my $self  = shift;
     my $right = shift;
 
-    return (
-        $self->HasRight(
-            Principal => $self->CurrentUser->UserObj(),
-            Right     => "$right"
-          )
-    );
-
+    return $self->CurrentUser->PrincipalObj->HasRight(
+        Object => $self,
+        Right  => $right,
+    )
 }
 
 # }}}
@@ -4011,7 +3468,9 @@ sub HasRight {
 
     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
     {
-        $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
+        Carp::cluck("Principal attrib undefined for Ticket::HasRight");
+        $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
+        return(undef);
     }
 
     return (
@@ -4026,6 +3485,154 @@ sub HasRight {
 
 # }}}
 
+=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'};
+
+}
+
+
+
+# {{{ sub Transactions 
+
+=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);
+}
+
+# }}}
+
+
+# {{{ TransactionCustomFields
+
+=head2 TransactionCustomFields
+
+    Returns the custom fields that transactions on tickets will have.
+
+=cut
+
+sub TransactionCustomFields {
+    my $self = shift;
+    return $self->QueueObj->TicketTransactionCustomFields;
+}
+
+# }}}
+
+# {{{ sub CustomFieldValues
+
+=head2 CustomFieldValues
+
+# Do name => id mapping (if needed) before falling back to
+# RT::Record's CustomFieldValues
+
+See L<RT::Record>
+
+=cut
+
+sub CustomFieldValues {
+    my $self  = shift;
+    my $field = shift;
+
+    return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
+
+    my $cf = RT::CustomField->new( $self->CurrentUser );
+    $cf->SetContextObject( $self );
+    $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
+    unless ( $cf->id ) {
+        $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
+    }
+
+    # If we didn't find a valid cfid, give up.
+    return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
+
+    return $self->SUPER::CustomFieldValues( $cf->id );
+}
+
+# }}}
+
+# {{{ sub CustomFieldLookupType
+
+=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