# BEGIN LICENSE BLOCK
# 
# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
# 
# (Except where explictly superceded by other copyright notices)
# 
# 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
# {{{ Front Material 

=head1 SYNOPSIS

  use RT::Ticket;
  my $ticket = new RT::Ticket($CurrentUser);
  $ticket->Load($ticket_id);

=head1 DESCRIPTION

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

use strict;
no warnings qw(redefine);

use RT::Queue;
use RT::User;
use RT::Record;
use RT::Links;
use RT::Date;
use RT::CustomFields;
use RT::TicketCustomFieldValues;
use RT::Tickets;
use RT::URI::fsck_com_rt;
use RT::URI;

=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
# to build and parse links between tickets

use vars '%LINKTYPEMAP';

%LINKTYPEMAP = (
    MemberOf => { Type => 'MemberOf',
                  Mode => 'Target', },
    Members => { Type => 'MemberOf',
                 Mode => 'Base', },
    HasMember => { Type => 'MemberOf',
                   Mode => 'Base', },
    RefersTo => { Type => 'RefersTo',
                  Mode => 'Target', },
    ReferredToBy => { Type => 'RefersTo',
                      Mode => 'Base', },
    DependsOn => { Type => 'DependsOn',
                   Mode => 'Target', },
    DependedOnBy => { Type => 'DependsOn',
                      Mode => 'Base', },

);

# }}}

# {{{ LINKDIRMAP
# A helper table for relationships mapping to make it easier
# to build and parse links between tickets

use vars '%LINKDIRMAP';

%LINKDIRMAP = (
    MemberOf => { Base => 'MemberOf',
                  Target => 'HasMember', },
    RefersTo => { Base => 'RefersTo',
                Target => 'ReferredToBy', },
    DependsOn => { Base => 'DependsOn',
                   Target => 'DependedOnBy', },

);

# }}}

# {{{ sub Load

=head2 Load

Takes a single argument. This can be a ticket id, ticket alias or 
local ticket uri.  If the ticket can't be loaded, returns undef.
Otherwise, returns the ticket id.

=cut

sub Load {
    my $self = shift;
    my $id   = shift;

    #TODO modify this routine to look at EffectiveId and do the recursive load
    # thing. be careful to cache all the interim tickets we try so we don't loop forever.

    #If it's a local URI, turn it into a ticket id
    if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
        $id = $1;
    }

    #If it's a remote URI, we're going to punt for now
    elsif ( $id =~ '://' ) {
        return (undef);
    }

    #If we have an integer URI, load the ticket
    if ( $id =~ /^\d+$/ ) {
        my $ticketid = $self->LoadById($id);

        unless ($ticketid) {
            $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
            return (undef);
        }
    }

    #It's not a URI. It's not a numerical ticket ID. Punt!
    else {
        return (undef);
    }

    #If we're merged, resolve the merge.
    if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
        return ( $self->Load( $self->EffectiveId ) );
    }

    #Ok. we're loaded. lets get outa here.
    return ( $self->Id );

}

# }}}

# {{{ sub LoadByURI

=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);
    }
}

# }}}

# {{{ sub Create

=head2 Create (ARGS)

Arguments: ARGS is a hash of named parameters.  Valid parameters are:

  id 
  Queue  - Either a Queue object or a Queue Name
  Requestor -  A reference to a list of RT::User objects, email addresses or RT user Names
  Cc  - A reference to a list of RT::User objects, email addresses or Names
  AdminCc  - A reference to a  list of RT::User objects, email addresses or Names
  Type -- The ticket\'s type. ignore this for now
  Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
  Subject -- A string describing the subject of the ticket
  InitialPriority -- an integer from 0 to 99
  FinalPriority -- an integer from 0 to 99
  Status -- any valid status (Defined in RT::Queue)
  TimeEstimated -- an integer. estimated time for this task in minutes
  TimeWorked -- an integer. time worked so far in minutes
  TimeLeft -- an integer. time remaining in minutes
  Starts -- an ISO date describing the ticket\'s start date and time in GMT
  Due -- an ISO date describing the ticket\'s due date and time in GMT
  MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
  CustomField-<n> -- a scalar or array of values for the customfield with the id <n>


Returns: TICKETID, Transaction Object, Error Message


=begin testing

my $t = RT::Ticket->new($RT::SystemUser);

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");

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);

=end testing

=cut

sub Create {
    my $self = shift;

    my %args = ( id              => undef,
                 EffectiveId     => undef,
                 Queue           => undef,
                 Requestor       => undef,
                 Cc              => undef,
                 AdminCc         => 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,
                 


                 @_ );

    my ( $ErrStr, $Owner, $resolved );
    my (@non_fatal_errors);

    my $QueueObj = RT::Queue->new($RT::SystemUser);

    
    if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
        $QueueObj->Load( $args{'Queue'} );
    }
    elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
        $QueueObj->Load( $args{'Queue'}->Id );
    }
    else {
        $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object.");
    }
;

    #Can't create a ticket without a queue.
    unless ( defined($QueueObj) && $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 ( $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

    # 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 

    # 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'} );

    # Priority may have changed from InitialPriority, for the case
    # where we're importing tickets (eg, from an older RT version.)
    my $priority = $args{'Priority'} || $args{'InitialPriority'};


    # {{{ Dates
    #TODO we should see what sort of due date we're getting, rather +
    # than assuming it's in ISO format.

    #Set the due date. if we didn't get fed one, use the queue default due in
    my $Due = new RT::Date( $self->CurrentUser );

    if ( $args{'Due'} ) {
        $Due->Set( Format => 'ISO', Value  => $args{'Due'} );
    }
    elsif (  $QueueObj->DefaultDueIn  ) {
        $Due->SetToNow;
        $Due->AddDays( $QueueObj->DefaultDueIn );
    }

    my $Starts = new RT::Date( $self->CurrentUser );
    if ( defined $args{'Starts'} ) {
        $Starts->Set( Format => 'ISO', Value  => $args{'Starts'} );
    }

    my $Started = new RT::Date( $self->CurrentUser );
    if ( defined $args{'Started'} ) {
        $Started->Set( Format => 'ISO', Value  => $args{'Started'} );
    }

    my $Resolved = new RT::Date( $self->CurrentUser );
    if ( defined $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'});
        $Resolved->SetToNow;
    }

    # }}}

    # {{{ Dealing with time fields

    $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
    $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
    $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};

    # }}}

    # {{{ Deal with setting the owner

    if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
        $Owner = $args{'Owner'};
    }

    #If we've been handed something else, try to load the user.
    elsif ( defined $args{'Owner'} ) {
        $Owner = RT::User->new( $self->CurrentUser );
        $Owner->Load( $args{'Owner'} );

    }

    #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'.");

        $Owner = undef;
    }

    #If we haven't been handed a valid owner, make it nobody.
    unless ( defined($Owner) && $Owner->Id ) {
        $Owner = new RT::User( $self->CurrentUser );
        $Owner->Load( $RT::Nobody->Id );
    }

    # }}}

    # We attempt to load or create each of the people who might have a role for this ticket
    # _outside_ the transaction, so we don't get into ticket creation races
    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
     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 && $watcher !~ /^\d+$/);
        }
    }


    $RT::Handle->BeginTransaction();

    my %params =( Queue           => $QueueObj->Id,
                                   Owner           => $Owner->Id,
                                   Subject         => $args{'Subject'},
                                   InitialPriority => $args{'InitialPriority'},
                                   FinalPriority   => $args{'FinalPriority'},
                                   Priority        => $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});
    }

    # Delete null integer parameters
    foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
        delete $params{$attr}  unless (exists $params{$attr} && $params{$attr});
    }


    my $id = $self->SUPER::Create( %params);
    unless ($id) {
        $RT::Logger->crit( "Couldn't create a ticket");
        $RT::Handle->Rollback();
        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 => ($args{'EffectiveId'} || $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 $create_groups_ret = $self->_CreateTicketGroups();
    unless ($create_groups_ret) {
        $RT::Logger->crit( "Couldn't create ticket groups for ticket "
                           . $self->Id
                           . ". aborting Ticket creation." );
        $RT::Handle->Rollback();
        return ( 0, 0,
                 $self->loc( "Ticket could not be created due to an internal error") );
    }

    # Set the owner in the Groups table
    # We denormalize it into the Ticket table too because doing otherwise would 
    # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization

    $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1);

    # {{{ 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} ) ) {

	   # If there is an empty entry in the list, let's get out of here.
	   next unless $watcher;

	    # 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' ) {

                # 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 );
            }

            push @non_fatal_errors, $wmsg unless ($wval);
        }
    }

    # }}}
    # {{{ Deal with setting up links


    foreach my $type ( keys %LINKTYPEMAP ) {
        next unless (defined $args{$type});
        foreach my $link (
            ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
        {
            my ( $wval, $wmsg ) = $self->AddLink(
                Type                          => $LINKTYPEMAP{$type}->{'Type'},
                $LINKTYPEMAP{$type}->{'Mode'} => $link,
                Silent                        => 1
            );

            push @non_fatal_errors, $wmsg unless ($wval);
        }
    }

    # }}}

   # {{{ 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 (length($value));
        $self->_AddCustomFieldValue( Field => $cfid,
                                     Value => $value,
                                     RecordTransaction => 0
                                 );
    }
    }
    # }}}

    if ( $args{'_RecordTransaction'} ) {
        # {{{ Add a transaction for the create
        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
                                                     Type      => "Create",
                                                     TimeTaken => 0,
                                                     MIMEObj => $args{'MIMEObj'}
        );


        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);
        }
        else {
            $RT::Handle->Rollback();

            # TODO where does this get errstr from?
            $RT::Logger->error("Ticket couldn't be created: $ErrStr");
            return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
        }

        $RT::Handle->Commit();
        return ( $self->Id, $TransObj->Id, $ErrStr );
        # }}}
    }
    else {

        # 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 );

    }
}


# }}}

# {{{ 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);

=end testing


=cut

sub CreateFrom822 {
    my $self    = shift;
    my $content = shift;



    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'}
    );

    # Add custom field entries to %ticketargs.
    # TODO: allow named custom fields
    map {
        /^customfield-(\d+)$/
          && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
    } keys(%args);

    my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
    unless ($id) {
        $RT::Logger->error( "Couldn't create a related ticket for "
              . $self->TicketObj->Id . " "
              . $msg );
    }

    return (1);
}

# }}}

# {{{ UpdateFrom822 

=head2 UpdateFrom822 $MESSAGE

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


=begin testing

my $simple_update = <<EOF;
Subject: target
AddRequestor: jesse\@example.com
EOF

my $ticket = RT::Ticket->new($RT::SystemUser);
my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
$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 ");

=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->DeleteWatcher( 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'} );

        #TODO error check this and return 0 if it\'s not loading properly +++
    }
    elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
        $QueueObj = RT::Queue->new($RT::SystemUser);
        $QueueObj->Load( $args{'Queue'}->Id );
    }
    else {
        $RT::Logger->debug(
            "$self " . $args{'Queue'} . " not a recognised queue object." );
    }

    #Can't create a ticket without a queue.
    unless ( defined($QueueObj) and $QueueObj->Id ) {
        $RT::Logger->debug("$self No queue given for ticket creation.");
        return ( 0, $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,
            $self->loc("No permission to create tickets in the queue '[_1]'"
              , $QueueObj->Name));
    }

    # {{{ Deal with setting the owner

    # Attempt to take user object, user name or user id.
    # Assign to nobody if lookup fails.
    if ( defined( $args{'Owner'} ) ) {
        if ( ref( $args{'Owner'} ) ) {
            $Owner = $args{'Owner'};
        }
        else {
            $Owner = new RT::User( $self->CurrentUser );
            $Owner->Load( $args{'Owner'} );
            if ( !defined( $Owner->id ) ) {
                $Owner->Load( $RT::Nobody->id );
            }
        }
    }

    #If we have a proposed owner and they don't have the right 
    #to own a ticket, scream about it and make them not the owner
    if (
        ( defined($Owner) )
        and ( $Owner->Id != $RT::Nobody->Id )
        and (
            !$Owner->HasRight(
                Object => $QueueObj,
                Right    => 'OwnTicket'
            )
        )
      )
    {

        $RT::Logger->warning( "$self user "
              . $Owner->Name . "("
              . $Owner->id
              . ") was proposed "
              . "as a ticket owner but has no rights to own "
              . "tickets in '"
              . $QueueObj->Name . "'\n" );

        $Owner = undef;
    }

    #If we haven't been handed a valid owner, make it nobody.
    unless ( defined($Owner) ) {
        $Owner = new RT::User( $self->CurrentUser );
        $Owner->Load( $RT::Nobody->UserObj->Id );
    }

    # }}}

    unless ( $self->ValidateStatus( $args{'Status'} ) ) {
        return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
    }

    $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
    $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
    $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };

    # If we're coming in with an id, set that now.
    my $EffectiveId = undef;
    if ( $args{'id'} ) {
        $EffectiveId = $args{'id'};

    }

    my $id = $self->SUPER::Create(
        id              => $args{'id'},
        EffectiveId     => $EffectiveId,
        Queue           => $QueueObj->Id,
        Owner           => $Owner->Id,
        Subject         => $args{'Subject'},		# loc
        InitialPriority => $args{'InitialPriority'},	# loc
        FinalPriority   => $args{'FinalPriority'},	# loc
        Priority        => $args{'InitialPriority'},	# loc
        Status          => $args{'Status'},		# loc
        TimeWorked      => $args{'TimeWorked'},		# loc
        Type            => $args{'Type'},		# loc
        Created         => $args{'Created'},		# loc
        Told            => $args{'Told'},		# loc
        LastUpdated     => $args{'Updated'},		# loc
        Resolved        => $args{'Resolved'},		# loc
        Due             => $args{'Due'},		# loc
    );

    # If the ticket didn't have an id
    # Set the ticket's effective ID now that we've created it.
    if ( $args{'id'} ) {
        $self->Load( $args{'id'} );
    }
    else {
        my ( $val, $msg ) =
          $self->__Set( Field => 'EffectiveId', Value => $id );

        unless ($val) {
            $RT::Logger->err(
                $self . "->Import couldn't set EffectiveId: $msg\n" );
        }
    }

    my $watcher;
    foreach $watcher ( @{ $args{'Cc'} } ) {
        $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
    }
    foreach $watcher ( @{ $args{'AdminCc'} } ) {
        $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
            Silent => 1 );
    }
    foreach $watcher ( @{ $args{'Requestor'} } ) {
        $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
            Silent => 1 );
    }

    return ( $self->Id, $ErrStr );
}

# }}}


# {{{ Routines dealing with watchers.

# {{{ _CreateTicketGroups 

=head2 _CreateTicketGroups

Create the ticket groups and relationships 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


sub _CreateTicketGroups {
    my $self = shift;
    
    my @types = qw(Requestor Owner Cc AdminCc);

    foreach my $type (@types) {
        my $type_obj = RT::Group->new($self->CurrentUser);
        my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
                                                       Instance => $self->Id, 
                                                       Type => $type);
        unless ($id) {
            $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
                               $self->Id.": ".$msg);     
            return(undef);
        }
     }
    return(1);
    
}

# }}}

# {{{ sub OwnerGroup

=head2 OwnerGroup

A constructor which returns an RT::Group object containing the owner of this ticket.

=cut

sub OwnerGroup {
    my $self = shift;
    my $owner_obj = RT::Group->new($self->CurrentUser);
    $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
    return ($owner_obj);
}

# }}}


# {{{ sub AddWatcher

=head2 AddWatcher

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

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.

=cut

sub AddWatcher {
    my $self = shift;
    my %args = (
        Type  => undef,
        PrincipalId => undef,
        Email => undef,
        @_
    );

    # {{{ 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' ) ) {

            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 the watcher isn't the current user 
    # and the current user  doesn't have 'ModifyTicket'
    # bail
    else {
        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
            return ( 0, $self->loc("Permission Denied") );
        }
    }

    # }}}

    return ( $self->_AddWatcher(%args) );
}

#This contains the meat of AddWatcher. but can be called from a routine like
# Create, which doesn't need the additional acl check
sub _AddWatcher {
    my $self = shift;
    my %args = (
        Type   => undef,
        Silent => undef,
        PrincipalId => undef,
        Email => undef,
        @_
    );


    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 ($args{'PrincipalId'}) {
        $principal->Load($args{'PrincipalId'});
    } 

 
    # If we can't find this watcher, we need to bail.
    unless ($principal->Id) {
            $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
        return(0, $self->loc("Could not find or create that user"));
    }


    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"));
    }

    if ( $group->HasMember( $principal)) {

        return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
    }


    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);

        return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
    }

    unless ( $args{'Silent'} ) {
        $self->_NewTransaction(
            Type     => 'AddWatcher',
            NewValue => $principal->Id,
            Field    => $args{'Type'}
        );
    }

        return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
}

# }}}


# {{{ sub DeleteWatcher

=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }


Deletes a Ticket watcher.  Takes two arguments:

Type  (one of Requestor,Cc,AdminCc)

and one of

PrincipalId (an RT::Principal Id of the watcher you want to remove)
    OR
Email (the email address of an existing wathcer)


=cut


sub DeleteWatcher {
    my $self = shift;

    my %args = ( Type => undef,
                 PrincipalId => undef,
                 Email => undef,
                 @_ );

    unless ($args{'PrincipalId'} || $args{'Email'} ) {
        return(0, $self->loc("No principal specified"));
    }
    my $principal = RT::Principal->new($self->CurrentUser);
    if ($args{'PrincipalId'} ) {

        $principal->Load($args{'PrincipalId'});
    } else {
        my $user = RT::User->new($self->CurrentUser);
        $user->LoadByEmail($args{'Email'});
        $principal->Load($user->Id);
    }
    # If we can't find this watcher, we need to bail.
    unless ($principal->Id) {
        return(0, $self->loc("Could not find that principal"));
    }

    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 
        #   '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' ) ) {
            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') );
        }
    }

    # If the watcher isn't the current user 
    # and the current user  doesn't have 'ModifyTicket' bail
    else {
        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
            return ( 0, $self->loc("Permission Denied") );
        }
    }

    # }}}


    # 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'}) );
    }

    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);

        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'}
        );
    }

    return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} ));
}




# }}}


# {{{ 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

 B<Returns> String: All Ticket Requestor email addresses as a string.

=cut

sub RequestorAddresses {
    my $self = shift;

    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return undef;
    }

    return ( $self->Requestors->MemberEmailAddressesAsString );
}


=head2 AdminCcAddresses

returns String: All Ticket AdminCc email addresses as a string

=cut

sub AdminCcAddresses {
    my $self = shift;

    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return undef;
    }

    return ( $self->AdminCc->MemberEmailAddressesAsString )

}

=head2 CcAddresses

returns String: All Ticket Ccs as a string of email addresses

=cut

sub CcAddresses {
    my $self = shift;

    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return undef;
    }

    return ( $self->Cc->MemberEmailAddressesAsString);

}

# }}}

# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs

# {{{ sub Requestors

=head2 Requestors

Takes nothing.
Returns this ticket's Requestors as an RT::Group object

=cut

sub Requestors {
    my $self = shift;

    my $group = RT::Group->new($self->CurrentUser);
    if ( $self->CurrentUserHasRight('ShowTicket') ) {
        $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
    }
    return ($group);

}

# }}}

# {{{ sub Cc

=head2 Cc

Takes nothing.
Returns an RT::Group object which contains this ticket's Ccs.
If the user doesn't have "ShowTicket" permission, returns an empty group

=cut

sub Cc {
    my $self = shift;

    my $group = RT::Group->new($self->CurrentUser);
    if ( $self->CurrentUserHasRight('ShowTicket') ) {
        $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
    }
    return ($group);

}

# }}}

# {{{ sub AdminCc

=head2 AdminCc

Takes nothing.
Returns an RT::Group object which contains this ticket's AdminCcs.
If the user doesn't have "ShowTicket" permission, returns an empty group

=cut

sub AdminCc {
    my $self = shift;

    my $group = RT::Group->new($self->CurrentUser);
    if ( $self->CurrentUserHasRight('ShowTicket') ) {
        $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
    }
    return ($group);

}

# }}}

# }}}

# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc

# {{{ sub IsWatcher
# a generic routine to be called by IsRequestor, IsCc and IsAdminCc

=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }

Takes a param hash with the attributes Type and either PrincipalId or Email

Type is one of Requestor, Cc, AdminCc and Owner

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.

=cut

sub IsWatcher {
    my $self = shift;

    my %args = ( Type  => 'Requestor',
        PrincipalId    => undef,
        Email          => undef,
        @_
    );

    # Load the relevant group. 
    my $group = RT::Group->new($self->CurrentUser);
    $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);

    # Find the relevant principal.
    my $principal = RT::Principal->new($self->CurrentUser);
    if (!$args{PrincipalId} && $args{Email}) {
        # Look up the specified user.
        my $user = RT::User->new($self->CurrentUser);
        $user->LoadByEmail($args{Email});
        if ($user->Id) {
            $args{PrincipalId} = $user->PrincipalId;
        }
        else {
            # A non-existent user can't be a group member.
            return 0;
        }
    }
    $principal->Load($args{'PrincipalId'});

    # Ask if it has the member in question
    return ($group->HasMember($principal));
}

# }}}

# {{{ sub IsRequestor

=head2 IsRequestor PRINCIPAL_ID
  
  Takes an RT::Principal id
  Returns true if the principal is a requestor of the current ticket.


=cut

sub IsRequestor {
    my $self   = shift;
    my $person = shift;

    return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );

};

# }}}

# {{{ sub IsCc

=head2 IsCc PRINCIPAL_ID

  Takes an RT::Principal id.
  Returns true if the principal is a requestor of the current ticket.


=cut

sub IsCc {
    my $self = shift;
    my $cc   = shift;

    return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );

}

# }}}

# {{{ sub IsAdminCc

=head2 IsAdminCc PRINCIPAL_ID

  Takes an RT::Principal id.
  Returns true if the principal is a requestor of the current ticket.

=cut

sub IsAdminCc {
    my $self   = shift;
    my $person = shift;

    return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );

}

# }}}

# {{{ sub IsOwner

=head2 IsOwner

  Takes an RT::User object. Returns true if that user is this ticket's owner.
returns undef otherwise

=cut

sub IsOwner {
    my $self   = shift;
    my $person = shift;

    # no ACL check since this is used in acl decisions
    # unless ($self->CurrentUserHasRight('ShowTicket')) {
    #	return(undef);
    #   }	

    #Tickets won't yet have owners when they're being created.
    unless ( $self->OwnerObj->id ) {
        return (undef);
    }

    if ( $person->id == $self->OwnerObj->id ) {
        return (1);
    }
    else {
        return (undef);
    }
}

# }}}

# }}}

# }}}

# {{{ Routines dealing with queues 

# {{{ sub ValidateQueue

sub ValidateQueue {
    my $self  = shift;
    my $Value = shift;

    if ( !$Value ) {
        $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
        return (1);
    }

    my $QueueObj = RT::Queue->new( $self->CurrentUser );
    my $id       = $QueueObj->Load($Value);

    if ($id) {
        return (1);
    }
    else {
        return (undef);
    }
}

# }}}

# {{{ sub SetQueue  

sub SetQueue {
    my $self     = shift;
    my $NewQueue = shift;

    #Redundant. ACL gets checked in _Set;
    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
        return ( 0, $self->loc("Permission Denied") );
    }

    my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
    $NewQueueObj->Load($NewQueue);

    unless ( $NewQueueObj->Id() ) {
        return ( 0, $self->loc("That queue does not exist") );
    }

    if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
        return ( 0, $self->loc('That is the same value') );
    }
    unless (
        $self->CurrentUser->HasRight(
            Right    => 'CreateTicket',
            Object => $NewQueueObj
        )
      )
    {
        return ( 0, $self->loc("You may not create requests in that queue.") );
    }

    unless (
        $self->OwnerObj->HasRight(
            Right    => 'OwnTicket',
            Object => $NewQueueObj
        )
      )
    {
        $self->Untake();
    }

    return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );

}

# }}}

# {{{ sub QueueObj

=head2 QueueObj

Takes nothing. returns this ticket's queue object

=cut

sub QueueObj {
    my $self = shift;

    my $queue_obj = RT::Queue->new( $self->CurrentUser );

    #We call __Value so that we can avoid the ACL decision and some deep recursion
    my ($result) = $queue_obj->Load( $self->__Value('Queue') );
    return ($queue_obj);
}

# }}}

# }}}

# {{{ Date printing routines

# {{{ sub DueObj

=head2 DueObj

  Returns an RT::Date object containing this ticket's due date

=cut

sub DueObj {
    my $self = shift;

    my $time = new RT::Date( $self->CurrentUser );

    # -1 is RT::Date slang for never
    if ( $self->Due ) {
        $time->Set( Format => 'sql', Value => $self->Due );
    }
    else {
        $time->Set( Format => 'unix', Value => -1 );
    }

    return $time;
}

# }}}

# {{{ sub DueAsString 

=head2 DueAsString

Returns this ticket's due date as a human readable string

=cut

sub DueAsString {
    my $self = shift;
    return $self->DueObj->AsString();
}

# }}}

# {{{ sub ResolvedObj

=head2 ResolvedObj

  Returns an RT::Date object of this ticket's 'resolved' time.

=cut

sub ResolvedObj {
    my $self = shift;

    my $time = new RT::Date( $self->CurrentUser );
    $time->Set( Format => 'sql', Value => $self->Resolved );
    return $time;
}

# }}}

# {{{ sub SetStarted

=head2 SetStarted

Takes a date in ISO format or undef
Returns a transaction id and a message
The client calls "Start" to note that the project was started on the date in $date.
A null date means "now"

=cut

sub SetStarted {
    my $self = shift;
    my $time = shift || 0;

    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
        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 ) {
        $time_obj->Set( Format => 'ISO', Value => $time );
    }
    else {
        $time_obj->SetToNow();
    }

    #Now that we're starting, open this ticket
    #TODO do we really want to force this as policy? it should be a scrip

    #We need $TicketAsSystem, in case the current user doesn't have
    #ShowTicket
    #
    my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
    $TicketAsSystem->Load( $self->Id );
    if ( $TicketAsSystem->Status eq 'new' ) {
        $TicketAsSystem->Open();
    }

    return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );

}

# }}}

# {{{ sub StartedObj

=head2 StartedObj

  Returns an RT::Date object which contains this ticket's 
'Started' time.

=cut

sub StartedObj {
    my $self = shift;

    my $time = new RT::Date( $self->CurrentUser );
    $time->Set( Format => 'sql', Value => $self->Started );
    return $time;
}

# }}}

# {{{ sub StartsObj

=head2 StartsObj

  Returns an RT::Date object which contains this ticket's 
'Starts' time.

=cut

sub StartsObj {
    my $self = shift;

    my $time = new RT::Date( $self->CurrentUser );
    $time->Set( Format => 'sql', Value => $self->Starts );
    return $time;
}

# }}}

# {{{ sub ToldObj

=head2 ToldObj

  Returns an RT::Date object which contains this ticket's 
'Told' time.

=cut

sub ToldObj {
    my $self = shift;

    my $time = new RT::Date( $self->CurrentUser );
    $time->Set( Format => 'sql', Value => $self->Told );
    return $time;
}

# }}}

# {{{ sub ToldAsString

=head2 ToldAsString

A convenience method that returns ToldObj->AsString

TODO: This should be deprecated

=cut

sub ToldAsString {
    my $self = shift;
    if ( $self->Told ) {
        return $self->ToldObj->AsString();
    }
    else {
        return ("Never");
    }
}

# }}}

# {{{ sub TimeWorkedAsString

=head2 TimeWorkedAsString

Returns the amount of time worked on this ticket as a Text String

=cut

sub TimeWorkedAsString {
    my $self = shift;
    return "0" unless $self->TimeWorked;

    #This is not really a date object, but if we diff a number of seconds 
    #vs the epoch, we'll get a nice description of time worked.

    my $worked = new RT::Date( $self->CurrentUser );

    #return the  #of minutes worked turned into seconds and written as
    # a simple text string

    return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
}

# }}}

# }}}

# {{{ Routines dealing with correspondence/comments

# {{{ sub Comment

=head2 Comment

Comment on this ticket.
Takes a hashref with the following attributes:
If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
commentl

MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content.

=cut

## Please see file perltidy.ERR
sub Comment {
    my $self = shift;

    my %args = ( CcMessageTo  => undef,
                 BccMessageTo => undef,
                 MIMEObj      => undef,
                 Content      => undef,
                 TimeTaken => 0,
                 @_ );

    unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
        return ( 0, $self->loc("Permission Denied") );
    }

    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") );
        }
    }

    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',
        RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
	if defined $args{'CcMessageTo'};
    $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
        RT::User::CanonicalizeEmailAddress(undef, $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'}
    );

    return ( $Trans, $self->loc("The comment has been recorded") );
}

# }}}

# {{{ sub Correspond

=head2 Correspond

Correspond on this ticket.
Takes a hashref with the following attributes:


MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content

if there's no MIMEObj, Content is used to build a MIME::Entity object


=cut

sub Correspond {
    my $self = shift;
    my %args = ( CcMessageTo  => undef,
                 BccMessageTo => undef,
                 MIMEObj      => undef,
                 Content      => undef,
                 TimeTaken    => 0,
                 @_ );

    unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
        return ( 0, $self->loc("Permission Denied") );
    }

    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") );
        }
    }

    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',
        RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
	if defined $args{'CcMessageTo'};
    $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
        RT::User::CanonicalizeEmailAddress(undef, $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'} );
    }

    #Set the last told date to now if this isn't mail from the requestor.
    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"

    unless ( $TransObj->IsInbound ) {
        $self->_SetTold;
    }

    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

=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' ) );
}

# }}}



=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

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

=cut

sub HasUnresolvedDependencies {
    my $self = shift;
    my %args = (
        Type   => undef,
        @_
    );

    my $deps = $self->UnresolvedDependencies;

    if ($args{Type}) {
        $deps->Limit( FIELD => 'Type', 
              OPERATOR => '=',
              VALUE => $args{Type}); 
    }
    else {
	    $deps->IgnoreType;
    }

    if ($deps->Count > 0) {
        return 1;
    }
    else {
        return (undef);
    }
}


# {{{ 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


sub UnresolvedDependencies {
    my $self = shift;
    my $deps = RT::Tickets->new($self->CurrentUser);

    my @live_statuses = RT::Queue->ActiveStatusArray();
    foreach my $status (@live_statuses) {
        $deps->LimitStatus(VALUE => $status);
    }
    $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.

=cut

sub AllDependedOnBy {
    my $self = shift;
    my $dep = $self->DependedOnBy;
    my %args = (
        Type   => undef,
	_found => {},
	_top   => 1,
        @_
    );

    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 );
	}
    }

    if ($args{_top}) {
	return map { $args{_found}{$_} } sort keys %{$args{_found}};
    }
    else {
	return 1;
    }
}

# }}}

# {{{ 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 {
    my $self = shift;

    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
    #tobias meant by $f
    my $field = shift;
    my $type  = shift || "";

    unless ( $self->{"$field$type"} ) {
        $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
        if ( $self->CurrentUserHasRight('ShowTicket') ) {
            # 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);
        }
    }
    return ( $self->{"$field$type"} );
}

# }}}

# }}}

# {{{ sub DeleteLink 

=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

=cut 

sub DeleteLink {
    my $self = shift;
    my %args = (
        Base   => undef,
        Target => undef,
        Type   => undef,
        @_
    );

    #check acls
    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
        $RT::Logger->debug("No permission to delete links\n");
        return ( 0, $self->loc('Permission Denied'))

    }

    #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';
    }
    elsif ( $args{'Target'} ) {
        $args{'Base'} = $self->URI();
	$remote_link = $args{'Target'};
        $direction='Base';
    }
    else {
        $RT::Logger->debug("$self: Base or Target must be specified\n");
        return ( 0, $self->loc('Either base or target must be specified') );
    }

    my $link = new RT::Link( $self->CurrentUser );
    $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );


    $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=>  $args{'Target'} );
    #it's a real link. 
    if ( $link->id ) {

        my $linkid = $link->id;
        $link->Delete();

        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 ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
            Type      => 'DeleteLink',
            Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
	    OldValue =>  $remote_uri->URI || $remote_link,
            TimeTaken => 0
        );

        return ( $Trans, $self->loc("Link deleted ([_1])", $TransString));
    }

    #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") );
    }
}

# }}}

# {{{ sub AddLink

=head2 AddLink

Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.


=cut

sub AddLink {
    my $self = shift;
    my %args = ( Target => '',
                 Base   => '',
                 Type   => '',
                 Silent => 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 {
        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 );
    }

    # }}}

    # Storing the link in the DB.
    my $link = RT::Link->new( $self->CurrentUser );
    my ($linkid) = $link->Create( Target => $args{Target},
                                  Base   => $args{Base},
                                  Type   => $args{Type} );

    unless ($linkid) {
        return ( 0, $self->loc("Link could not be created") );
    }

    my $TransString =
      "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";

    # Don't write the transaction if we're doing this on create
    if ( $args{'Silent'} ) {
        return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
    }
    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 ) );
    }

}

# }}}

# {{{ sub URI 

=head2 URI

Returns this ticket's URI

=cut

sub URI {
    my $self = shift;
    my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
    return($uri->URIForObject($self));
}

# }}}

# {{{ 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;

    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);

    # make sure it exists.
    unless ( defined $NewTicket->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...");
        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") );
    }

    # We use EffectiveId here even though it duplicates information from
    # the links table becasue of the massive performance hit we'd take
    # by trying to do a seperate database query for merge info everytime 
    # loaded a ticket. 

    #update this ticket's effective id to the new ticket's id.
    my ( $id_val, $id_msg ) = $self->__Set(
        Field => 'EffectiveId',
        Value => $NewTicket->Id()
    );

    unless ($id_val) {
        $RT::Logger->error(
            "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
        return ( 0, $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) );
    }


    # 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);

    while (my $link = $old_links_to->Next) {
        if ($link->Base eq $NewTicket->URI) {
            $link->Delete;
        } else {
            $link->SetTarget($NewTicket->URI);
        }

    }

    my $old_links_from = RT::Links->new($self->CurrentUser);
    $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);

    while (my $link = $old_links_from->Next) {
        if ($link->Target eq $NewTicket->URI) {
            $link->Delete;
        } else {
            $link->SetBase($NewTicket->URI);
        }

    }


    #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);
    }

    my $Ccs = $self->Cc->MembersObj;
    while (my $watcher = $Ccs->Next) { 
        $NewTicket->_AddWatcher( Type => 'Cc',
                                  Silent => 1,
                                  PrincipalId => $watcher->MemberId);
    }

    my $AdminCcs = $self->AdminCc->MembersObj;
    while (my $watcher = $AdminCcs->Next) { 
        $NewTicket->_AddWatcher( Type => 'AdminCc',
                                  Silent => 1,
                                  PrincipalId => $watcher->MemberId);
    }


    #find all of the tickets that were merged into this ticket. 
    my $old_mergees = new RT::Tickets( $self->CurrentUser );
    $old_mergees->Limit(
        FIELD    => 'EffectiveId',
        OPERATOR => '=',
        VALUE    => $self->Id
    );

    #   update their EffectiveId fields to the new ticket's id
    while ( my $ticket = $old_mergees->Next() ) {
        my ( $val, $msg ) = $ticket->__Set(
            Field => 'EffectiveId',
            Value => $NewTicket->Id()
        );
    }

    #make a new link: this ticket is merged into that other ticket.
    $self->AddLink( Type   => 'MergedInto', Target => $NewTicket->Id());

    $NewTicket->_SetLastUpdated;

    return ( 1, $self->loc("Merge Successful") );
}

# }}}

# }}}

# {{{ Routines dealing with ownership

# {{{ sub OwnerObj

=head2 OwnerObj

Takes nothing and returns an RT::User object of 
this ticket's owner

=cut

sub OwnerObj {
    my $self = shift;

    #If this gets ACLed, we lose on a rights check in User.pm and
    #get deep recursion. if we need ACLs here, we need
    #an equiv without ACLs

    my $owner = new RT::User( $self->CurrentUser );
    $owner->Load( $self->__Value('Owner') );

    #Return the owner object
    return ($owner);
}

# }}}

# {{{ sub OwnerAsString 

=head2 OwnerAsString

Returns the owner's email address

=cut

sub OwnerAsString {
    my $self = shift;
    return ( $self->OwnerObj->EmailAddress );

}

# }}}

# {{{ sub SetOwner

=head2 SetOwner

Takes two arguments:
     the Id or Name of the owner 
and  (optionally) the type of the SetOwner Transaction. It defaults
to 'Give'.  'Steal' is also a valid option.

=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

sub SetOwner {
    my $self     = shift;
    my $NewOwner = shift;
    my $Type     = shift || "Give";

    # must have ModifyTicket rights
    # or TakeTicket/StealTicket and $NewOwner is self
    # see if it's a take
    if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
        unless (    $self->CurrentUserHasRight('ModifyTicket')
                 || $self->CurrentUserHasRight('TakeTicket') ) {
            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 ) {

        unless (    $self->CurrentUserHasRight('ModifyTicket')
                 || $self->CurrentUserHasRight('StealTicket') ) {
            return ( 0, $self->loc("Permission Denied") );
        }
    }
    else {
        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
            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've specified a new owner and that user can't modify the ticket
    elsif ( ( $NewOwnerObj->Id )
            and ( !$NewOwnerObj->HasRight( Right  => 'OwnTicket',
                                           Object => $self ) )
      ) {
        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 ) ) {
        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();
    unless ($del_id) {
        $RT::Handle->Rollback();
        return ( 0, $self->loc("Could not change owner. ") . $del_msg );
    }

    my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
                                       PrincipalId => $NewOwnerObj->PrincipalId,
                                       InsideTransaction => 1 );
    unless ($add_id) {
        $RT::Handle->Rollback();
        return ( 0, $self->loc("Could not change owner. ") . $add_msg );
    }

    # We call set twice with slightly different arguments, so
    # as to not have an SQL transaction span two RT transactions

    my ( $val, $msg ) = $self->_Set(
                      Field             => 'Owner',
                      RecordTransaction => 0,
                      Value             => $NewOwnerObj->Id,
                      TimeTaken         => 0,
                      TransactionType   => $Type,
                      CheckACL          => 0,                  # don't check acl
    );

    unless ($val) {
        $RT::Handle->Rollback;
        return ( 0, $self->loc("Could not change owner. ") . $msg );
    }

    $RT::Handle->Commit();

    my ( $trans, $msg, undef ) = $self->_NewTransaction(
                                                   Type     => $Type,
                                                   Field    => 'Owner',
                                                   NewValue => $NewOwnerObj->Id,
                                                   OldValue => $OldOwnerObj->Id,
                                                   TimeTaken => 0 );

    if ($trans) {
        $msg = $self->loc( "Owner changed from [_1] to [_2]",
                           $OldOwnerObj->Name, $NewOwnerObj->Name );

        # TODO: make sure the trans committed properly
    }
    return ( $trans, $msg );

}

# }}}

# {{{ sub Take

=head2 Take

A convenince method to set the ticket's owner to the current user

=cut

sub Take {
    my $self = shift;
    return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
}

# }}}

# {{{ sub Untake

=head2 Untake

Convenience method to set the owner to 'nobody' if the current user is the owner.

=cut

sub Untake {
    my $self = shift;
    return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
}

# }}}

# {{{ sub Steal 

=head2 Steal

A convenience method to change the owner of the current ticket to the
current user. Even if it's owned by another user.

=cut

sub Steal {
    my $self = shift;

    if ( $self->IsOwner( $self->CurrentUser ) ) {
        return ( 0, $self->loc("You already own this ticket") );
    }
    else {
        return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );

    }

}

# }}}

# }}}

# {{{ Routines dealing with status

# {{{ sub ValidateStatus 

=head2 ValidateStatus STATUS

Takes a string. Returns true if that status is a valid status for this ticket.
Returns false otherwise.

=cut

sub ValidateStatus {
    my $self   = shift;
    my $status = shift;

    #Make sure the status passed in is valid
    unless ( $self->QueueObj->IsValidStatus($status) ) {
        return (undef);
    }

    return (1);

}

# }}}

# {{{ sub SetStatus

=head2 SetStatus STATUS

Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.

Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE).  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

sub SetStatus {
    my $self   = shift;
    my %args;

    if (@_ == 1) {
	$args{Status} = shift;
    }
    else {
	%args = (@_);
    }

    #Check ACL
    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) {
        return (0, $self->loc('That ticket has unresolved dependencies'));
    }

    my $now = RT::Date->new( $self->CurrentUser );
    $now->SetToNow();

    #If we're changing the status from new, record that we've started
    if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {

        #Set the Started time to "now"
        $self->_Set( Field             => 'Started',
                     Value             => $now->ISO,
                     RecordTransaction => 0 );
    }

    if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {

        #When we resolve a ticket, set the 'Resolved' attribute to now.
        $self->_Set( Field             => 'Resolved',
                     Value             => $now->ISO,
                     RecordTransaction => 0 );
    }

    #Actually update the status
   my ($val, $msg)= $self->_Set( Field           => 'Status',
                          Value           => $args{Status},
                          TimeTaken       => 0,
                          TransactionType => 'Status'  );

    return($val,$msg);
}

# }}}

# {{{ sub Kill

=head2 Kill

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') );

    # TODO: garbage collection
}

# }}}

# {{{ sub Stall

=head2 Stall

Sets this ticket's status to stalled

=cut

sub Stall {
    my $self = shift;
    return ( $self->SetStatus('stalled') );
}

# }}}

# {{{ sub Reject

=head2 Reject

Sets this ticket's status to rejected

=cut

sub Reject {
    my $self = shift;
    return ( $self->SetStatus('rejected') );
}

# }}}

# {{{ sub Open

=head2 Open

Sets this ticket\'s status to Open

=cut

sub Open {
    my $self = shift;
    return ( $self->SetStatus('open') );
}

# }}}

# {{{ sub Resolve

=head2 Resolve

Sets this ticket\'s status to Resolved

=cut

sub Resolve {
    my $self = shift;
    return ( $self->SetStatus('resolved') );
}

# }}}

# }}}

# {{{ 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());
    $cf_values->OrderBy( FIELD => 'id' );

    # @values is a CustomFieldValues object;
    return ($cf_values);
}

# }}}

# {{{ AddCustomFieldValue

=item AddCustomFieldValue { Field => FIELD, Value => VALUE }

VALUE should be 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

=head2 SetTold ISO  [TIMETAKEN]

Updates the told and records a transaction

=cut

sub SetTold {
    my $self = shift;
    my $told;
    $told = shift if (@_);
    my $timetaken = shift || 0;

    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
        return ( 0, $self->loc("Permission Denied") );
    }

    my $datetold = new RT::Date( $self->CurrentUser );
    if ($told) {
        $datetold->Set( Format => 'iso',
                        Value  => $told );
    }
    else {
        $datetold->SetToNow();
    }

    return ( $self->_Set( Field           => 'Told',
                          Value           => $datetold->ISO,
                          TimeTaken       => $timetaken,
                          TransactionType => 'Told' ) );
}

=head2 _SetTold

Updates the told without a transaction or acl check. Useful when we're sending replies.

=cut

sub _SetTold {
    my $self = shift;

    my $now = new RT::Date( $self->CurrentUser );
    $now->SetToNow();

    #use __Set to get no ACLs ;)
    return ( $self->__Set( Field => 'Told',
                           Value => $now->ISO ) );
}

# }}}

# {{{ sub Transactions 

=head2 Transactions

  Returns an RT::Transactions object of all transactions on this ticket

=cut

sub Transactions {
    my $self = shift;

    use RT::Transactions;
    my $transactions = RT::Transactions->new( $self->CurrentUser );

    #If the user has no rights, return an empty object
    if ( $self->CurrentUserHasRight('ShowTicket') ) {
        my $tickets = $transactions->NewAlias('Tickets');
        $transactions->Join(
            ALIAS1 => 'main',
            FIELD1 => 'Ticket',
            ALIAS2 => $tickets,
            FIELD2 => 'id'
        );
        $transactions->Limit(
            ALIAS => $tickets,
            FIELD => 'EffectiveId',
            VALUE => $self->id()
        );

        # if the user may not see comments do not return them
        unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
            $transactions->Limit(
                FIELD    => 'Type',
                OPERATOR => '!=',
                VALUE    => "Comment"
            );
        }
    }

    return ($transactions);
}

# }}}

# {{{ sub _NewTransaction

sub _NewTransaction {
    my $self = shift;
    my %args = (
        TimeTaken => 0,
        Type      => undef,
        OldValue  => undef,
        NewValue  => undef,
        Data      => undef,
        Field     => undef,
        MIMEObj   => undef,
        @_
    );

    require RT::Transaction;
    my $trans = new RT::Transaction( $self->CurrentUser );
    my ( $transaction, $msg ) = $trans->Create(
        Ticket    => $self->Id,
        TimeTaken => $args{'TimeTaken'},
        Type      => $args{'Type'},
        Data      => $args{'Data'},
        Field     => $args{'Field'},
        NewValue  => $args{'NewValue'},
        OldValue  => $args{'OldValue'},
        MIMEObj   => $args{'MIMEObj'}
    );


    $self->Load($self->Id);

    $RT::Logger->warning($msg) unless $transaction;

    $self->_SetLastUpdated;

    if ( defined $args{'TimeTaken'} ) {
        $self->_UpdateTimeTaken( $args{'TimeTaken'} );
    }
    if ( $RT::UseTransactionBatch and $transaction ) {
	push @{$self->{_TransactionBatch}}, $trans;
    }
    return ( $transaction, $msg, $trans );
}

# }}}

=head2 TransactionBatch

  Returns an array reference of all transactions created on this ticket during
  this ticket object's lifetime, or undef if there were none.

  Only works when the $RT::UseTransactionBatch config variable is set to true.

=cut

sub TransactionBatch {
    my $self = shift;
    return $self->{_TransactionBatch};
}

sub DESTROY {
    my $self = shift;

    # 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}++;

    my $batch = $self->TransactionBatch or return;
    require RT::Scrips;
    RT::Scrips->new($RT::SystemUser)->Apply(
	Stage		=> 'TransactionBatch',
	TicketObj	=> $self,
	TransactionObj	=> $batch->[0],
    );
}

# }}}

# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record

# {{{ sub _ClassAccessible

sub _ClassAccessible {
    {
        EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
          Queue           => { 'read' => 1,  'write' => 1 },
          Requestors      => { 'read' => 1,  'write' => 1 },
          Owner           => { 'read' => 1,  'write' => 1 },
          Subject         => { 'read' => 1,  'write' => 1 },
          InitialPriority => { 'read' => 1,  'write' => 1 },
          FinalPriority   => { 'read' => 1,  'write' => 1 },
          Priority        => { 'read' => 1,  'write' => 1 },
          Status          => { 'read' => 1,  'write' => 1 },
          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 },
          Starts        => { 'read' => 1, 'write' => 1 },
          Started       => { 'read' => 1, 'write' => 1 },
          Due           => { 'read' => 1, 'write' => 1 },
          Creator       => { 'read' => 1, 'auto'  => 1 },
          Created       => { 'read' => 1, 'auto'  => 1 },
          LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
          LastUpdated   => { 'read' => 1, 'auto'  => 1 }
    };

}

# }}}

# {{{ sub _Set

sub _Set {
    my $self = shift;

    my %args = ( Field             => undef,
                 Value             => undef,
                 TimeTaken         => 0,
                 RecordTransaction => 1,
                 UpdateTicket      => 1,
                 CheckACL          => 1,
                 TransactionType   => 'Set',
                 @_ );

    if ($args{'CheckACL'}) {
      unless ( $self->CurrentUserHasRight('ModifyTicket')) {
          return ( 0, $self->loc("Permission Denied"));
      }
   }

    unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
        $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
        return(0, $self->loc("Internal Error"));
    }

    #if the user is trying to modify the record

    #Take care of the old value we really don't want to get in an ACL loop.
    # so ask the super::_Value
    my $Old = $self->SUPER::_Value("$args{'Field'}");
    
    my ($ret, $msg);
    if ( $args{'UpdateTicket'}  ) {

        #Set the new value
        ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
                                                Value => $args{'Value'} );
    
        #If we can't actually set the field to the value, don't record
        # a transaction. instead, get out of here.
        if ( $ret == 0 ) { return ( 0, $msg ); }
    }

    if ( $args{'RecordTransaction'} == 1 ) {

        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
                                               Type => $args{'TransactionType'},
                                               Field     => $args{'Field'},
                                               NewValue  => $args{'Value'},
                                               OldValue  => $Old,
                                               TimeTaken => $args{'TimeTaken'},
        );
        return ( $Trans, scalar $TransObj->Description );
    }
    else {
        return ( $ret, $msg );
    }
}

# }}}

# {{{ sub _Value 

=head2 _Value

Takes the name of a table column.
Returns its value as a string, if the user passes an ACL check

=cut

sub _Value {

    my $self  = shift;
    my $field = shift;

    #if the field is public, return it.
    if ( $self->_Accessible( $field, 'public' ) ) {

        #$RT::Logger->debug("Skipping ACL check for $field\n");
        return ( $self->SUPER::_Value($field) );

    }

    #If the current user doesn't have ACLs, don't let em at it.  

    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return (undef);
    }
    return ( $self->SUPER::_Value($field) );

}

# }}}

# {{{ sub _UpdateTimeTaken

=head2 _UpdateTimeTaken

This routine will increment the timeworked counter. it should
only be called from _NewTransaction 

=cut

sub _UpdateTimeTaken {
    my $self    = shift;
    my $Minutes = shift;
    my ($Total);

    $Total = $self->SUPER::_Value("TimeWorked");
    $Total = ( $Total || 0 ) + ( $Minutes || 0 );
    $self->SUPER::_Set(
        Field => "TimeWorked",
        Value => $Total
    );

    return ($Total);
}

# }}}

# }}}

# {{{ Routines dealing with ACCESS CONTROL

# {{{ sub CurrentUserHasRight 

=head2 CurrentUserHasRight

  Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
1 if the user has that right. It returns 0 if the user doesn't have that right.

=cut

sub CurrentUserHasRight {
    my $self  = shift;
    my $right = shift;

    return (
        $self->HasRight(
            Principal => $self->CurrentUser->UserObj(),
            Right     => "$right"
          )
    );

}

# }}}

# {{{ sub HasRight 

=head2 HasRight

 Takes a paramhash with the attributes 'Right' and 'Principal'
  'Right' is a ticket-scoped textual right from RT::ACE 
  'Principal' is an RT::User object

  Returns 1 if the principal has the right. Returns undef if not.

=cut

sub HasRight {
    my $self = shift;
    my %args = (
        Right     => undef,
        Principal => undef,
        @_
    );

    unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
    {
        $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
    }

    return (
        $args{'Principal'}->HasRight(
            Object => $self,
            Right     => $args{'Right'}
          )
    );
}

# }}}

# }}}

1;

=head1 AUTHOR

Jesse Vincent, jesse@bestpractical.com

=head1 SEE ALSO

RT

=cut

