Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / User.pm
index 4e85540..b8a51f0 100755 (executable)
@@ -1,6 +1,50 @@
-# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/User.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
-# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
-# This software is redistributable under the terms of the GNU GPL
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
 
 =head1 NAME
 
 
 =head1 DESCRIPTION
 
-
 =head1 METHODS
 
-=begin testing
+=cut
 
-ok(require RT::TestHarness);
-ok(require RT::User);
 
-=end testing
+package RT::User;
 
+use strict;
+use warnings;
 
-=cut
+use Scalar::Util qw(blessed);
 
+use base 'RT::Record';
 
-package RT::User;
-use RT::Record;
-@ISA= qw(RT::Record);
+sub Table {'Users'}
 
-# {{{ sub _Init
-sub _Init  {
-    my $self = shift;
-    $self->{'table'} = "Users";
-    return($self->SUPER::_Init(@_));
-}
-# }}}
-
-# {{{ sub _Accessible 
-
-sub _Accessible  {
-  my $self = shift;
-  my %Cols = (
-             # {{{ Core RT info
-             Name => 'public/read/write/admin',
-             Password => 'write',
-             Comments => 'read/write/admin',
-             Signature => 'read/write',
-             EmailAddress => 'public/read/write',
-             PagerEmailAddress => 'read/write',
-             FreeformContactInfo => 'read/write',
-             Organization => 'public/read/write/admin',
-             Disabled => 'public/read/write/admin', #To modify this attribute, we have helper
-             #methods
-             Privileged => 'read/write/admin', # 0=no 1=user 2=system
-
-             # }}}
-             
-             # {{{ Names
-             
-             RealName => 'public/read/write',
-             NickName => 'public/read/write',
-             # }}}
-                     
-             # {{{ Localization and Internationalization
-             Lang => 'public/read/write',
-             EmailEncoding => 'public/read/write',
-             WebEncoding => 'public/read/write',
-             # }}}
-             
-             # {{{ External ContactInfo Linkage
-             ExternalContactInfoId => 'public/read/write/admin',
-             ContactInfoSystem => 'public/read/write/admin',
-             # }}}
-             
-             # {{{ User Authentication identifier
-             ExternalAuthId => 'public/read/write/admin',
-             #Authentication system used for user 
-             AuthSystem => 'public/read/write/admin',
-             Gecos => 'public/read/write/admin', #Gecos is the name of the fields in a 
-             # unix passwd file. In this case, it refers to "Unix Username"
-             # }}}
-             
-             # {{{ Telephone numbers
-             HomePhone =>  'read/write',
-             WorkPhone => 'read/write',
-             MobilePhone => 'read/write',
-             PagerPhone => 'read/write',
-
-             # }}}
-             
-             # {{{ Paper Address
-             Address1 => 'read/write',
-             Address2 => 'read/write',
-             City => 'read/write',
-             State => 'read/write',
-             Zip => 'read/write',
-             Country => 'read/write',
-             # }}}
-             
-             # {{{ Core DBIx::Record Attributes
-             Creator => 'read/auto',
-             Created => 'read/auto',
-             LastUpdatedBy => 'read/auto',
-             LastUpdated => 'read/auto'
-
-             # }}}
-            );
-  return($self->SUPER::_Accessible(@_, %Cols));
-}
-
-# }}}
-
-# {{{ sub Create 
-
-sub Create  {
-    my $self = shift;
-    my %args = (Privileged => 0,
-               @_ # get the real argumentlist
-              );
-    
-    #Check the ACL
-    unless ($self->CurrentUserHasRight('AdminUsers')) {
-       return (0, 'No permission to create users');
-    }
-    
-    if (! $args{'Password'})  {
-       $args{'Password'} = '*NO-PASSWORD*';
-    }
-    elsif (length($args{'Password'}) < $RT::MinimumPasswordLength) {
-        return(0,"Password too short");
-    }
-    else {
-        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
-        $args{'Password'} = crypt($args{'Password'}, $salt);     
-    }   
-        
-    
-    #TODO Specify some sensible defaults.
-    
-    unless (defined ($args{'Name'})) {
-       return(0, "Must specify 'Name' attribute");
-    }  
-    
-    
-    #SANITY CHECK THE NAME AND ABORT IF IT'S TAKEN
-    if ($RT::SystemUser) { #This only works if RT::SystemUser has been defined
-               my $TempUser = RT::User->new($RT::SystemUser);
-               $TempUser->Load($args{'Name'});
-               return (0, 'Name in use') if ($TempUser->Id);
-       
-               return(0, 'Email address in use') 
-                       unless ($self->ValidateEmailAddress($args{'EmailAddress'}));
-    }
-    else {
-               $RT::Logger->warning("$self couldn't check for pre-existing ".
-                            " users on create. This will happen".
-                            " on installation\n");
-    }
-    
-    my $id = $self->SUPER::Create(%args);
-    
-    #If the create failed.
-    unless ($id) {
-               return (0, 'Could not create user');
+
+
+
+
+
+use Digest::SHA;
+use Digest::MD5;
+use Crypt::Eksblowfish::Bcrypt qw();
+use RT::Principals;
+use RT::ACE;
+use RT::Interface::Email;
+use Text::Password::Pronounceable;
+
+sub _OverlayAccessible {
+    {
+
+          Name                  => { public => 1,  admin => 1 },    # loc_left_pair
+          Password              => { read   => 0 },
+          EmailAddress          => { public => 1 },                 # loc_left_pair
+          Organization          => { public => 1,  admin => 1 },    # loc_left_pair
+          RealName              => { public => 1 },                 # loc_left_pair
+          NickName              => { public => 1 },                 # loc_left_pair
+          Lang                  => { public => 1 },                 # loc_left_pair
+          EmailEncoding         => { public => 1 },
+          WebEncoding           => { public => 1 },
+          ExternalContactInfoId => { public => 1,  admin => 1 },
+          ContactInfoSystem     => { public => 1,  admin => 1 },
+          ExternalAuthId        => { public => 1,  admin => 1 },
+          AuthSystem            => { public => 1,  admin => 1 },
+          Gecos                 => { public => 1,  admin => 1 },    # loc_left_pair
+          PGPKey                => { public => 1,  admin => 1 },    # loc_left_pair
+          SMIMECertificate      => { public => 1,  admin => 1 },    # loc_left_pair
+          City                  => { public => 1 },                 # loc_left_pair
+          Country               => { public => 1 },                 # loc_left_pair
+          Timezone              => { public => 1 },                 # loc_left_pair
     }
-      
-    
-    #TODO post 2.0
-    #if ($args{'SendWelcomeMessage'}) {
-    #  #TODO: Check if the email exists and looks valid
-    #  #TODO: Send the user a "welcome message" 
-    #}
-    
-    return ($id, 'User created');
 }
 
-# }}}
 
-# {{{ sub _BootstrapCreate 
 
-#create a user without validating _any_ data.
+=head2 Create { PARAMHASH }
 
-#To be used only on database init.
 
-sub _BootstrapCreate {
+
+=cut
+
+
+sub Create {
     my $self = shift;
-    my %args = (@_);
+    my %args = (
+        Privileged => 0,
+        Disabled => 0,
+        EmailAddress => '',
+        _RecordTransaction => 1,
+        @_    # get the real argumentlist
+    );
 
-    $args{'Password'} = "*NO-PASSWORD*";
-    my $id = $self->SUPER::Create(%args);
-    
-    #If the create failed.
-    return (0, 'Could not create user') 
-      unless ($id);
+    # remove the value so it does not cripple SUPER::Create
+    my $record_transaction = delete $args{'_RecordTransaction'};
 
-    return ($id, 'User created');
-}
+    #Check the ACL
+    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+        return ( 0, $self->loc('Permission Denied') );
+    }
 
-# }}}
 
-# {{{ sub Delete 
+    unless ($self->CanonicalizeUserInfo(\%args)) {
+        return ( 0, $self->loc("Could not set user info") );
+    }
 
-sub Delete  {
-    my $self = shift;
-    
-    return(0, 'Deleting this object would violate referential integrity');
-    
-}
+    $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'});
 
-# }}}
+    # if the user doesn't have a name defined, set it to the email address
+    $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'});
 
-# {{{ sub Load 
 
-=head2 Load
 
-Load a user object from the database. Takes a single argument.
-If the argument is numerical, load by the column 'id'. Otherwise, load by
-the "Name" column which is the user's textual username.
+    my $privileged = delete $args{'Privileged'};
 
-=cut
 
-sub Load  {
-    my $self = shift;
-    my $identifier = shift || return undef;
-    
-    #if it's an int, load by id. otherwise, load by name.
-    if ($identifier !~ /\D/) {
-       $self->SUPER::LoadById($identifier);
+    if ($args{'CryptedPassword'} ) {
+        $args{'Password'} = $args{'CryptedPassword'};
+        delete $args{'CryptedPassword'};
+    } elsif ( !$args{'Password'} ) {
+        $args{'Password'} = '*NO-PASSWORD*';
+    } else {
+        my ($ok, $msg) = $self->ValidatePassword($args{'Password'});
+        return ($ok, $msg) if !$ok;
+
+        $args{'Password'} = $self->_GeneratePassword($args{'Password'});
     }
-    else {
-       $self->LoadByCol("Name",$identifier);
+
+    #TODO Specify some sensible defaults.
+
+    unless ( $args{'Name'} ) {
+        return ( 0, $self->loc("Must specify 'Name' attribute") );
     }
-}
 
-# }}}
+    my ( $val, $msg ) = $self->ValidateName( $args{'Name'} );
+    return ( 0, $msg ) unless $val;
+    ( $val, $msg ) = $self->ValidateEmailAddress( $args{'EmailAddress'} );
+    return ( 0, $msg ) unless ($val);
+
+    $RT::Handle->BeginTransaction();
+    # Groups deal with principal ids, rather than user ids.
+    # When creating this user, set up a principal Id for it.
+    my $principal = RT::Principal->new($self->CurrentUser);
+    my $principal_id = $principal->Create(PrincipalType => 'User',
+                                Disabled => $args{'Disabled'},
+                                ObjectId => '0');
+    # If we couldn't create a principal Id, get the fuck out.
+    unless ($principal_id) {
+        $RT::Handle->Rollback();
+        $RT::Logger->crit("Couldn't create a Principal on new user create.");
+        $RT::Logger->crit("Strange things are afoot at the circle K");
+        return ( 0, $self->loc('Could not create user') );
+    }
 
+    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
+    delete $args{'Disabled'};
 
-# {{{ sub LoadByEmail
+    $self->SUPER::Create(id => $principal_id , %args);
+    my $id = $self->Id;
 
-=head2 LoadByEmail
+    #If the create failed.
+    unless ($id) {
+        $RT::Handle->Rollback();
+        $RT::Logger->error("Could not create a new user - " .join('-', %args));
 
-Tries to load this user object from the database by the user's email address.
+        return ( 0, $self->loc('Could not create user') );
+    }
 
+    my $aclstash = RT::Group->new($self->CurrentUser);
+    my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal);
 
-=cut
+    unless ($stash_id) {
+        $RT::Handle->Rollback();
+        $RT::Logger->crit("Couldn't stash the user in groupmembers");
+        return ( 0, $self->loc('Could not create user') );
+    }
 
-sub LoadByEmail {
-    my $self=shift;
-    my $address = shift;
 
-    # Never load an empty address as an email address.
-    unless ($address) {
-       return(undef);
+    my $everyone = RT::Group->new($self->CurrentUser);
+    $everyone->LoadSystemInternalGroup('Everyone');
+    unless ($everyone->id) {
+        $RT::Logger->crit("Could not load Everyone group on user creation.");
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc('Could not create user') );
     }
 
-    $address = RT::CanonicalizeAddress($address);
-    #$RT::Logger->debug("Trying to load an email address: $address\n");
-    return $self->LoadByCol("EmailAddress", $address);
-}
-# }}}
 
+    my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
+    unless ($everyone_id) {
+        $RT::Logger->crit("Could not add user to Everyone group on user creation.");
+        $RT::Logger->crit($everyone_msg);
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc('Could not create user') );
+    }
 
-# {{{ sub ValidateEmailAddress
 
-=head2 ValidateEmailAddress ADDRESS
+    my $access_class = RT::Group->new($self->CurrentUser);
+    if ($privileged)  {
+        $access_class->LoadSystemInternalGroup('Privileged');
+    } else {
+        $access_class->LoadSystemInternalGroup('Unprivileged');
+    }
+
+    unless ($access_class->id) {
+        $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation");
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc('Could not create user') );
+    }
 
-Returns true if the email address entered is not in use by another user or is 
-undef or ''. Returns false if it's in use. 
 
-=cut
+    my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
 
-sub ValidateEmailAddress {
-       my $self = shift;
-       my $Value = shift;
+    unless ($ac_id) {
+        $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted");
+        $RT::Logger->crit($ac_msg);
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc('Could not create user') );
+    }
 
-       # if the email address is null, it's always valid
-       return (1) if(!$Value || $Value eq "");
 
-       my $TempUser = RT::User->new($RT::SystemUser);
-       $TempUser->LoadByEmail($Value);
+    if ( $record_transaction ) {
+        $self->_NewTransaction( Type => "Create" );
+    }
+
+    $RT::Handle->Commit;
 
-       if( $TempUser->id && 
-          ($TempUser->id != $self->id)) { # if we found a user with that address 
-                                       # it's invalid to set this user's address to it
-               return(undef);
-       }
-       else { #it's a valid email address
-               return(1);
-       }
+    return ( $id, $self->loc('User created') );
 }
 
-# }}}
+=head2 ValidateName STRING
 
+Returns either (0, "failure reason") or 1 depending on whether the given
+name is valid.
 
+=cut
 
+sub ValidateName {
+    my $self = shift;
+    my $name = shift;
 
-# {{{ sub SetRandomPassword
+    return ( 0, $self->loc('empty name') ) unless defined $name && length $name;
 
-=head2 SetRandomPassword
+    my $TempUser = RT::User->new( RT->SystemUser );
+    $TempUser->Load($name);
 
-Takes no arguments. Returns a status code and a new password or an error message.
-If the status is 1, the second value returned is the new password.
-If the status is anything else, the new value returned is the error code.
+    if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) {
+        return ( 0, $self->loc('Name in use') );
+    }
+    else {
+        return 1;
+    }
+}
+
+=head2 ValidatePassword STRING
+
+Returns either (0, "failure reason") or 1 depending on whether the given
+password is valid.
 
 =cut
 
-sub SetRandomPassword  {
+sub ValidatePassword {
     my $self = shift;
+    my $password = shift;
 
-
-    unless ($self->CurrentUserCanModify('Password')) {
-       return (0, "Permission Denied");
+    if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) {
+        return ( 0, $self->loc("Password needs to be at least [quant,_1,character,characters] long", RT->Config->Get('MinimumPasswordLength')) );
     }
-    
-    my $pass = $self->GenerateRandomPassword(6,8);
-
-    # If we have "notify user on 
 
-    my ($val, $msg) = $self->SetPassword($pass);
-    
-    #If we got an error return the error.
-    return (0, $msg) unless ($val);
-    
-    #Otherwise, we changed the password, lets return it.
-    return (1, $pass);
-    
+    return 1;
 }
 
-# }}}
+=head2 SetPrivileged BOOL
 
+If passed a true value, makes this user a member of the "Privileged"  PseudoGroup.
+Otherwise, makes this user a member of the "Unprivileged" pseudogroup.
 
-# {{{ sub ResetPassword
+Returns a standard RT tuple of (val, msg);
 
-=head2 ResetPassword
-
-Returns status, [ERROR or new password].  Resets this user\'s password to
-a randomly generated pronouncable password and emails them, using a 
-global template called "RT_PasswordChange", which can be overridden
-with global templates "RT_PasswordChange_Privileged" or "RT_PasswordChange_NonPrivileged" 
-for privileged and Non-privileged users respectively.
 
 =cut
 
-sub ResetPassword {
+sub SetPrivileged {
     my $self = shift;
-    
-    unless ($self->CurrentUserCanModify('Password')) {
-       return (0, "Permission Denied");
-    }
-    my ($status, $pass) = $self->SetRandomPassword();
+    my $val = shift;
 
-    unless ($status) {
-       return (0, "$pass");
+    #Check the ACL
+    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+        return ( 0, $self->loc('Permission Denied') );
     }
-    
-    my $template = RT::Template->new($self->CurrentUser);
-
 
-    if ($self->IsPrivileged) {
-       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
-    } 
-    else {
-       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
-    }  
-    
-    unless ($template->Id) {
-       $template->LoadGlobalTemplate('RT_PasswordChange');
-    }  
-    
-    unless ($template->Id) {
-       $RT::Logger->crit("$self tried to send ".$self->Name." a password reminder ".
-                         "but couldn't find a password change template");
-    }  
-
-    my $notification =  RT::Action::SendPasswordEmail->new(TemplateObj => $template,
-                                                          Argument => $pass);
-    
-    $notification->SetTo($self->EmailAddress);
-
-    my ($ret);
-    $ret = $notification->Prepare();
-    if ($ret) {
-       $ret = $notification->Commit();
-    }
-    
-    if ($ret) {
-       return(1, 'New password notification sent');
-    }  else {
-       return (0, 'Notification could not be sent');
-    }  
-    
+    $self->_SetPrivileged($val);
 }
 
+sub _SetPrivileged {
+    my $self = shift;
+    my $val = shift;
+    my $priv = RT::Group->new($self->CurrentUser);
+    $priv->LoadSystemInternalGroup('Privileged');
+    unless ($priv->Id) {
+        $RT::Logger->crit("Could not find Privileged pseudogroup");
+        return(0,$self->loc("Failed to find 'Privileged' users pseudogroup."));
+    }
 
-# }}}
+    my $unpriv = RT::Group->new($self->CurrentUser);
+    $unpriv->LoadSystemInternalGroup('Unprivileged');
+    unless ($unpriv->Id) {
+        $RT::Logger->crit("Could not find unprivileged pseudogroup");
+        return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup"));
+    }
 
-# {{{ sub GenerateRandomPassword
+    my $principal = $self->PrincipalId;
+    if ($val) {
+        if ($priv->HasMember($principal)) {
+            #$RT::Logger->debug("That user is already privileged");
+            return (0,$self->loc("That user is already privileged"));
+        }
+        if ($unpriv->HasMember($principal)) {
+            $unpriv->_DeleteMember($principal);
+        } else {
+        # if we had layered transactions, life would be good
+        # sadly, we have to just go ahead, even if something
+        # bogus happened
+            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
+                "unprivileged. something is drastically wrong.");
+        }
+        my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
+        if ($status) {
+            return (1, $self->loc("That user is now privileged"));
+        } else {
+            return (0, $msg);
+        }
+    } else {
+        if ($unpriv->HasMember($principal)) {
+            #$RT::Logger->debug("That user is already unprivileged");
+            return (0,$self->loc("That user is already unprivileged"));
+        }
+        if ($priv->HasMember($principal)) {
+            $priv->_DeleteMember( $principal );
+        } else {
+        # if we had layered transactions, life would be good
+        # sadly, we have to just go ahead, even if something
+        # bogus happened
+            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
+                "unprivileged. something is drastically wrong.");
+        }
+        my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
+        if ($status) {
+            return (1, $self->loc("That user is now unprivileged"));
+        } else {
+            return (0, $msg);
+        }
+    }
+}
 
-=head2 GenerateRandomPassword MIN_LEN and MAX_LEN
+=head2 Privileged
 
-Returns a random password between MIN_LEN and MAX_LEN characters long.
+Returns true if this user is privileged. Returns undef otherwise.
 
 =cut
 
-sub GenerateRandomPassword {
-    my $self = shift;
-    my $min_length = shift;
-    my $max_length = shift;
-    
-    #This code derived from mpw.pl, a bit of code with a sordid history
-    # Its notes: 
-    
-    # Perl cleaned up a bit by Jesse Vincent 1/14/2001.
-    # Converted to perl from C by Marc Horowitz, 1/20/2000.
-    # Converted to C from Multics PL/I by Bill Sommerfeld, 4/21/86.
-    # Original PL/I version provided by Jerry Saltzer.
-
-    
-    my ($frequency, $start_freq, $total_sum, $row_sums);
-
-    #When munging characters, we need to know where to start counting letters from
-    my $a = ord('a');
-
-    # frequency of English digraphs (from D Edwards 1/27/66) 
-    $frequency =
-      [ [ 4, 20, 28, 52, 2, 11, 28, 4, 32, 4, 6, 62, 23,
-         167, 2, 14, 0, 83, 76, 127, 7, 25, 8, 1, 9, 1 ], # aa - az
-       [ 13, 0, 0, 0, 55, 0, 0, 0, 8, 2, 0, 22, 0,
-         0, 11, 0, 0, 15, 4, 2, 13, 0, 0, 0, 15, 0 ], # ba - bz
-       [ 32, 0, 7, 1, 69, 0, 0, 33, 17, 0, 10, 9, 1,
-         0, 50, 3, 0, 10, 0, 28, 11, 0, 0, 0, 3, 0 ], # ca - cz
-       [ 40, 16, 9, 5, 65, 18, 3, 9, 56, 0, 1, 4, 15,
-         6, 16, 4, 0, 21, 18, 53, 19, 5, 15, 0, 3, 0 ], # da - dz
-       [ 84, 20, 55, 125, 51, 40, 19, 16, 50, 1, 4, 55, 54,
-         146, 35, 37, 6, 191, 149, 65, 9, 26, 21, 12, 5, 0 ], # ea - ez
-       [ 19, 3, 5, 1, 19, 21, 1, 3, 30, 2, 0, 11, 1,
-         0, 51, 0, 0, 26, 8, 47, 6, 3, 3, 0, 2, 0 ], # fa - fz
-       [ 20, 4, 3, 2, 35, 1, 3, 15, 18, 0, 0, 5, 1,
-         4, 21, 1, 1, 20, 9, 21, 9, 0, 5, 0, 1, 0 ], # ga - gz
-       [ 101, 1, 3, 0, 270, 5, 1, 6, 57, 0, 0, 0, 3,
-         2, 44, 1, 0, 3, 10, 18, 6, 0, 5, 0, 3, 0 ], # ha - hz
-       [ 40, 7, 51, 23, 25, 9, 11, 3, 0, 0, 2, 38, 25,
-         202, 56, 12, 1, 46, 79, 117, 1, 22, 0, 4, 0, 3 ], # ia - iz
-       [ 3, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0,
-         0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0 ], # ja - jz
-       [ 1, 0, 0, 0, 11, 0, 0, 0, 13, 0, 0, 0, 0,
-         2, 0, 0, 0, 0, 6, 2, 1, 0, 2, 0, 1, 0 ], # ka - kz
-       [ 44, 2, 5, 12, 62, 7, 5, 2, 42, 1, 1, 53, 2,
-         2, 25, 1, 1, 2, 16, 23, 9, 0, 1, 0, 33, 0 ], # la - lz
-       [ 52, 14, 1, 0, 64, 0, 0, 3, 37, 0, 0, 0, 7,
-         1, 17, 18, 1, 2, 12, 3, 8, 0, 1, 0, 2, 0 ], # ma - mz
-       [ 42, 10, 47, 122, 63, 19, 106, 12, 30, 1, 6, 6, 9,
-         7, 54, 7, 1, 7, 44, 124, 6, 1, 15, 0, 12, 0 ], # na - nz
-       [ 7, 12, 14, 17, 5, 95, 3, 5, 14, 0, 0, 19, 41,
-         134, 13, 23, 0, 91, 23, 42, 55, 16, 28, 0, 4, 1 ], # oa - oz
-       [ 19, 1, 0, 0, 37, 0, 0, 4, 8, 0, 0, 15, 1,
-         0, 27, 9, 0, 33, 14, 7, 6, 0, 0, 0, 0, 0 ], # pa - pz
-       [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-         0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0 ], # qa - qz
-       [ 83, 8, 16, 23, 169, 4, 8, 8, 77, 1, 10, 5, 26,
-         16, 60, 4, 0, 24, 37, 55, 6, 11, 4, 0, 28, 0 ], # ra - rz
-       [ 65, 9, 17, 9, 73, 13, 1, 47, 75, 3, 0, 7, 11,
-         12, 56, 17, 6, 9, 48, 116, 35, 1, 28, 0, 4, 0 ], # sa - sz
-       [ 57, 22, 3, 1, 76, 5, 2, 330, 126, 1, 0, 14, 10,
-         6, 79, 7, 0, 49, 50, 56, 21, 2, 27, 0, 24, 0 ], # ta - tz
-       [ 11, 5, 9, 6, 9, 1, 6, 0, 9, 0, 1, 19, 5,
-         31, 1, 15, 0, 47, 39, 31, 0, 3, 0, 0, 0, 0 ], # ua - uz
-       [ 7, 0, 0, 0, 72, 0, 0, 0, 28, 0, 0, 0, 0,
-         0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0 ], # va - vz
-       [ 36, 1, 1, 0, 38, 0, 0, 33, 36, 0, 0, 4, 1,
-         8, 15, 0, 0, 0, 4, 2, 0, 0, 1, 0, 0, 0 ], # wa - wz
-       [ 1, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0,
-         0, 1, 5, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0 ], # xa - xz
-       [ 14, 5, 4, 2, 7, 12, 12, 6, 10, 0, 0, 3, 7,
-         5, 17, 3, 0, 4, 16, 30, 0, 0, 5, 0, 0, 0 ], # ya - yz
-       [ 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0,
-         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ]; # za - zz
-
-    #We need to know the totals for each row 
-    $row_sums =
-      [ map { my $sum = 0; map { $sum += $_ } @$_; $sum } @$frequency ];
-    
-
-    #Frequency with which a given letter starts a word.
-    $start_freq =
-      [ 1299, 425, 725, 271, 375, 470, 93, 223, 1009, 24, 20, 355, 379,
-       319, 823, 618, 21, 317, 962, 1991, 271, 104, 516, 6, 16, 14 ];
-    
-    $total_sum = 0; map { $total_sum += $_ } @$start_freq;
-    
-    
-    my $length = $min_length + int(rand($max_length-$min_length));
-    
-    my $char = $self->GenerateRandomNextChar($total_sum, $start_freq);
-    my @word = ($char+$a);
-    for (2..$length) {
-       $char = $self->_GenerateRandomNextChar($row_sums->[$char], $frequency->[$char]);
-       push(@word, $char+$a);
-    }
-    
-    #Return the password
-    return pack("C*",@word);
-    
-}
-
-
-#A private helper function for RandomPassword
-# Takes a row summary and a frequency chart for the next character to be searched
-sub _GenerateRandomNextChar {
+sub Privileged {
     my $self = shift;
-    my($all, $freq) = @_;
-    my($pos, $i);
-    
-    for ($pos = int(rand($all)), $i=0;
-        $pos >= $freq->[$i];
-        $pos -= $freq->[$i], $i++) {};
-    
-    return($i);
+    if ( RT->PrivilegedUsers->HasMember( $self->id ) ) {
+        return(1);
+    } else {
+        return(undef);
+    }
 }
 
-# }}}
+#create a user without validating _any_ data.
+
+#To be used only on database init.
+# We can't localize here because it's before we _have_ a loc framework
 
-# {{{ sub SetPassword
+sub _BootstrapCreate {
+    my $self = shift;
+    my %args = (@_);
 
-=head2 SetPassword
+    $args{'Password'} = '*NO-PASSWORD*';
 
-Takes a string. Checks the string's length and sets this user's password 
-to that string.
 
-=cut
+    $RT::Handle->BeginTransaction();
 
-sub SetPassword {
-    my $self = shift;
-    my $password = shift;
-    
-    unless ($self->CurrentUserCanModify('Password')) {
-       return(0, 'Permission Denied');
+    # Groups deal with principal ids, rather than user ids.
+    # When creating this user, set up a principal Id for it.
+    my $principal = RT::Principal->new($self->CurrentUser);
+    my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0');
+    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
+
+    # If we couldn't create a principal Id, get the fuck out.
+    unless ($principal_id) {
+        $RT::Handle->Rollback();
+        $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K");
+        return ( 0, 'Could not create user' );
     }
-    
-    if (! $password)  {
-        return(0, "No password set");
+    $self->SUPER::Create(id => $principal_id, %args);
+    my $id = $self->Id;
+    #If the create failed.
+      unless ($id) {
+      $RT::Handle->Rollback();
+      return ( 0, 'Could not create user' ) ; #never loc this
     }
-    elsif (length($password) < $RT::MinimumPasswordLength) {
-        return(0,"Password too short");
+
+    my $aclstash = RT::Group->new($self->CurrentUser);
+    my $stash_id  = $aclstash->_CreateACLEquivalenceGroup($principal);
+
+    unless ($stash_id) {
+        $RT::Handle->Rollback();
+        $RT::Logger->crit("Couldn't stash the user in groupmembers");
+        return ( 0, $self->loc('Could not create user') );
     }
-    else {
-        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
-        return ( $self->SUPER::SetPassword(crypt($password, $salt)) );
-    }   
-    
+
+    $RT::Handle->Commit();
+
+    return ( $id, 'User created' );
 }
 
-# }}}
+sub Delete {
+    my $self = shift;
 
-# {{{ sub IsPassword 
+    return ( 0, $self->loc('Deleting this object would violate referential integrity') );
 
-=head2 IsPassword
+}
 
-Returns true if the passed in value is this user's password.
-Returns undef otherwise.
+=head2 Load
+
+Load a user object from the database. Takes a single argument.
+If the argument is numerical, load by the column 'id'. If a user
+object or its subclass passed then loads the same user by id.
+Otherwise, load by the "Name" column which is the user's textual
+username.
 
 =cut
 
-sub IsPassword { 
+sub Load {
     my $self = shift;
-    my $value = shift;
-
-    #TODO there isn't any apparent way to legitimately ACL this
-
-    # RT does not allow null passwords 
-    if ((!defined ($value)) or ($value eq '')) {
-       return(undef);
-    } 
-    if ($self->Disabled) {
-       $RT::Logger->info("Disabled user ".$self->Name." tried to log in");
-       return(undef);
-    }
+    my $identifier = shift || return undef;
 
-    if ( ($self->__Value('Password') eq '') || 
-         ($self->__Value('Password') eq undef) )  {
-        return(undef);
-     }
-    if ($self->__Value('Password') eq crypt($value, $self->__Value('Password'))) {
-       return (1);
-    }
-    else {
-       return (undef);
+    if ( $identifier !~ /\D/ ) {
+        return $self->SUPER::LoadById( $identifier );
+    } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) {
+        return $self->SUPER::LoadById( $identifier->Id );
+    } else {
+        return $self->LoadByCol( "Name", $identifier );
     }
 }
 
-# }}}
+=head2 LoadByEmail
 
-# {{{ sub SetDisabled
+Tries to load this user object from the database by the user's email address.
 
-=head2 Sub SetDisabled
+=cut
 
-Toggles the user's disabled flag.
-If this flag is
-set, all password checks for this user will fail. All ACL checks for this
-user will fail. The user will appear in no user listings.
+sub LoadByEmail {
+    my $self    = shift;
+    my $address = shift;
 
-=cut 
+    # Never load an empty address as an email address.
+    unless ($address) {
+        return (undef);
+    }
 
-# }}}
+    $address = $self->CanonicalizeEmailAddress($address);
 
-# {{{ ACL Related routines
+    #$RT::Logger->debug("Trying to load an email address: $address");
+    return $self->LoadByCol( "EmailAddress", $address );
+}
 
-# {{{ GrantQueueRight
+=head2 LoadOrCreateByEmail ADDRESS
 
-=head2 GrantQueueRight
+Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with
+the provided email address and loads them. Address can be provided either as L<Email::Address> object
+or string which is parsed using the module.
 
-Grant a queue right to this user.  Takes a paramhash of which the elements
-RightAppliesTo and RightName are important.
+Returns a tuple of the user's id and a status message.
+0 will be returned in place of the user's id in case of failure.
 
 =cut
 
-sub GrantQueueRight {
-    
+sub LoadOrCreateByEmail {
     my $self = shift;
-    my %args = ( RightScope => 'Queue',
-                RightName => undef,
-                RightAppliesTo => undef,
-                PrincipalType => 'User',
-                PrincipalId => $self->Id,
-                @_);
-   
-    #ACL check handled in ACE.pm
+    my $email = shift;
 
-    require RT::ACE;
+    my ($message, $name);
+    if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) {
+        ($email, $name) = ($email->address, $email->phrase);
+    } else {
+        ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email );
+    }
 
-#    $RT::Logger->debug("$self ->GrantQueueRight right:". $args{'RightName'} .
-#                     " applies to queue ".$args{'RightAppliesTo'}."\n");
-    
-    my $ace = new RT::ACE($self->CurrentUser);
-    
-    return ($ace->Create(%args));
+    $self->LoadByEmail( $email );
+    $self->Load( $email ) unless $self->Id;
+    $message = $self->loc('User loaded');
+
+    unless( $self->Id ) {
+        my $val;
+        ($val, $message) = $self->Create(
+            Name         => $email,
+            EmailAddress => $email,
+            RealName     => $name,
+            Privileged   => 0,
+            Comments     => 'Autocreated when added as a watcher',
+        );
+        unless ( $val ) {
+            # Deal with the race condition of two account creations at once
+            $self->LoadByEmail( $email );
+            unless ( $self->Id ) {
+                sleep 5;
+                $self->LoadByEmail( $email );
+            }
+            if ( $self->Id ) {
+                $RT::Logger->error("Recovered from creation failure due to race condition");
+                $message = $self->loc("User loaded");
+            } else {
+                $RT::Logger->crit("Failed to create user ". $email .": " .$message);
+            }
+        }
+    }
+    return wantarray ? (0, $message) : 0 unless $self->id;
+    return wantarray ? ($self->Id, $message) : $self->Id;
 }
 
-# }}}
+=head2 ValidateEmailAddress ADDRESS
+
+Returns true if the email address entered is not in use by another user or is
+undef or ''. Returns false if it's in use.
+
+=cut
+
+sub ValidateEmailAddress {
+    my $self  = shift;
+    my $Value = shift;
+
+    # if the email address is null, it's always valid
+    return (1) if ( !$Value || $Value eq "" );
 
-# {{{ GrantSystemRight
+    if ( RT->Config->Get('ValidateUserEmailAddresses') ) {
+        # We only allow one valid email address
+        my @addresses = Email::Address->parse($Value);
+        return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) );
+    }
+
+
+    my $TempUser = RT::User->new(RT->SystemUser);
+    $TempUser->LoadByEmail($Value);
+
+    if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) )
+    {    # if we found a user with that address
+            # it's invalid to set this user's address to it
+        return ( 0, $self->loc('Email address in use') );
+    } else {    #it's a valid email address
+        return (1);
+    }
+}
 
-=head2 GrantSystemRight
+=head2 SetName
 
-Grant a system right to this user. 
-The only element that's important to set is RightName.
+Check to make sure someone else isn't using this name already
 
 =cut
-sub GrantSystemRight {
-    
-    my $self = shift;
-    my %args = ( RightScope => 'System',
-                RightName => undef,
-                RightAppliesTo => 0,
-                PrincipalType => 'User',
-                PrincipalId => $self->Id,
-                @_);
-   
 
-    #ACL check handled in ACE.pm
+sub SetName {
+    my $self  = shift;
+    my $Value = shift;
 
-    require RT::ACE;    
-    my $ace = new RT::ACE($self->CurrentUser);
-    
-    return ($ace->Create(%args));
+    my ( $val, $message ) = $self->ValidateName($Value);
+    if ($val) {
+        return $self->_Set( Field => 'Name', Value => $Value );
+    }
+    else {
+        return ( 0, $message );
+    }
 }
 
+=head2 SetEmailAddress
 
-# }}}
+Check to make sure someone else isn't using this email address already
+so that a better email address can be returned
 
-# {{{ sub HasQueueRight
+=cut
 
-=head2 HasQueueRight
+sub SetEmailAddress {
+    my $self  = shift;
+    my $Value = shift;
+    $Value = '' unless defined $Value;
 
-Takes a paramhash which can contain
-these items:
-    TicketObj => RT::Ticket or QueueObj => RT::Queue or Queue => integer
-    IsRequestor => undef, (for bootstrapping create)
-    Right => 'Right' 
+    my ($val, $message) = $self->ValidateEmailAddress( $Value );
+    if ( $val ) {
+        return $self->_Set( Field => 'EmailAddress', Value => $Value );
+    } else {
+        return ( 0, $message )
+    }
 
+}
 
-Returns 1 if this user has the right specified in the paramhash. for the queue
-passed in.
+=head2 EmailFrequency
 
-Returns undef if they don't
+Takes optional Ticket argument in paramhash. Returns a string, suitable
+for localization, describing any notable properties about email delivery
+to the user.  This includes lack of email address, ticket-level
+squelching (if C<Ticket> is provided in the paramhash), or user email
+delivery preferences.
+
+Returns the empty string if there are no notable properties.
 
 =cut
 
-sub HasQueueRight {
+sub EmailFrequency {
     my $self = shift;
-    my %args = ( TicketObj => undef,
-                 QueueObj => undef,
-                Queue => undef,
-                IsRequestor => undef,
-                Right => undef,
-                @_);
-    
-    my ($IsRequestor, $IsCc, $IsAdminCc, $IsOwner);
-    
-    if (defined $args{'Queue'}) {
-       $args{'QueueObj'} = new RT::Queue($self->CurrentUser);
-       $args{'QueueObj'}->Load($args{'Queue'});
-    }
-    
-    if (defined $args{'TicketObj'}) {
-       $args{'QueueObj'} = $args{'TicketObj'}->QueueObj();
-    }
-
-    # {{{ Validate and load up the QueueId
-    unless ((defined $args{'QueueObj'}) and ($args{'QueueObj'}->Id)) {
-       require Carp;
-       $RT::Logger->debug(Carp::cluck ("$self->HasQueueRight Couldn't find a queue id"));
-       return undef;
-    }
-
-    # }}}
-
-        
-    # Figure out whether a user has the right we're asking about.
-    # first see if they have the right personally for the queue in question. 
-    my $retval = $self->_HasRight(Scope => 'Queue',
-                                 AppliesTo => $args{'QueueObj'}->Id,
-                                 Right => $args{'Right'},
-                                 IsOwner => $IsOwner);
-
-    return ($retval) if (defined $retval);
-    
-    # then we see whether they have the right personally globally. 
-    $retval = $self->HasSystemRight( $args{'Right'});
-
-    return ($retval) if (defined $retval);
-    
-    # now that we know they don't have the right personally,
-    
-    # {{{ Find out about whether the current user is a Requestor, Cc, AdminCc or Owner
-
-    if (defined $args{'TicketObj'}) {
-       if ($args{'TicketObj'}->IsRequestor($self)) {#user is requestor
-           $IsRequestor = 1;
-       }       
-
-       if ($args{'TicketObj'}->IsCc($self)) { #If user is a cc
-           $IsCc = 1;
-       }
-
-       if ($args{'TicketObj'}->IsAdminCc($self)) { #If user is an admin cc
-           $IsAdminCc = 1;
-       }       
-       
-       if ($args{'TicketObj'}->IsOwner($self)) { #If user is an owner
-           $IsOwner = 1;
-       }
-    }
-    
-    if (defined $args{'QueueObj'}) {
-       if ($args{'QueueObj'}->IsCc($self)) { #If user is a cc
-           $IsCc = 1;
-       }
-       if ($args{'QueueObj'}->IsAdminCc($self)) { #If user is an admin cc
-           $IsAdminCc = 1;
-       }
-       
-    } 
-    # }}}
-    
-    # then see whether they have the right for the queue as a member of a metagroup 
-
-    $retval = $self->_HasRight(Scope => 'Queue',
-                                 AppliesTo => $args{'QueueObj'}->Id,
-                                 Right => $args{'Right'},
-                                 IsOwner => $IsOwner,
-                                 IsCc => $IsCc,
-                                 IsAdminCc => $IsAdminCc,
-                                 IsRequestor => $IsRequestor
-                                );
-
-    return ($retval) if (defined $retval);
-
-    #   then we see whether they have the right globally as a member of a metagroup
-    $retval = $self->HasSystemRight( $args{'Right'},
-                                    (IsOwner => $IsOwner,
-                                     IsCc => $IsCc,
-                                     IsAdminCc => $IsAdminCc,
-                                     IsRequestor => $IsRequestor
-                                    ) );
-
-    #If they haven't gotten it by now, they just lose.
-    return ($retval);
-    
+    my %args = (
+        Ticket => undef,
+        @_
+    );
+    return '' unless $self->id && $self->id != RT->Nobody->id
+        && $self->id != RT->SystemUser->id;
+    return 'no email address set'  # loc
+        unless my $email = $self->EmailAddress;
+    return 'email disabled for ticket' # loc
+        if $args{'Ticket'} &&
+            grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo;
+    my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || '';
+    return 'receives daily digests' # loc
+        if $frequency =~ /daily/i;
+    return 'receives weekly digests' # loc
+        if $frequency =~ /weekly/i;
+    return 'email delivery suspended' # loc
+        if $frequency =~ /suspend/i;
+    return '';
 }
 
-# }}}
-  
-# {{{ sub HasSystemRight
+=head2 CanonicalizeEmailAddress ADDRESS
+
+CanonicalizeEmailAddress converts email addresses into canonical form.
+it takes one email address in and returns the proper canonical
+form. You can dump whatever your proper local config is in here.  Note
+that it may be called as a static method; in this case the first argument
+is class name not an object.
+
+=cut
+
+sub CanonicalizeEmailAddress {
+    my $self = shift;
+    my $email = shift;
+    # Example: the following rule would treat all email
+    # coming from a subdomain as coming from second level domain
+    # foo.com
+    if ( my $match   = RT->Config->Get('CanonicalizeEmailAddressMatch') and
+         my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') )
+    {
+        $email =~ s/$match/$replace/gi;
+    }
+    return ($email);
+}
 
-=head2 HasSystemRight
+=head2 CanonicalizeUserInfo HASH of ARGS
 
-takes an array of a single value and a paramhash.
-The single argument is the right being passed in.
-the param hash is some additional data. (IsCc, IsOwner, IsAdminCc and IsRequestor)
+CanonicalizeUserInfo can convert all User->Create options.
+it takes a hashref of all the params sent to User->Create and
+returns that same hash, by default nothing is done.
 
-Returns 1 if this user has the listed 'right'. Returns undef if this user doesn't.
+This function is intended to allow users to have their info looked up via
+an outside source and modified upon creation.
 
 =cut
 
-sub HasSystemRight {
+sub CanonicalizeUserInfo {
     my $self = shift;
-    my $right = shift;
-
-    my %args = ( IsOwner => undef,
-                IsCc => undef,
-                IsAdminCc => undef,
-                IsRequestor => undef,
-                @_);
-    
-    unless (defined $right) {
+    my $args = shift;
+    my $success = 1;
 
-       $RT::Logger->debug("$self RT::User::HasSystemRight was passed in no right.");
-       return(undef);
-    }  
-    return ( $self->_HasRight ( Scope => 'System',
-                               AppliesTo => '0',
-                               Right => $right,
-                               IsOwner => $args{'IsOwner'},
-                               IsCc => $args{'IsCc'},
-                               IsAdminCc => $args{'IsAdminCc'},
-                               IsRequestor => $args{'IsRequestor'},
-                               
-                             )
-          );
-    
+    return ($success);
 }
 
-# }}}
 
-# {{{ sub _HasRight
+=head2 Password and authentication related functions
 
-=head2 sub _HasRight (Right => 'right', Scope => 'scope',  AppliesTo => int, ExtendedPrincipals => SQL)
+=head3 SetRandomPassword
 
-_HasRight is a private helper method for checking a user's rights. It takes
-several options:
+Takes no arguments. Returns a status code and a new password or an error message.
+If the status is 1, the second value returned is the new password.
+If the status is anything else, the new value returned is the error code.
 
-=item Right is a textual right name
+=cut
 
-=item Scope is a textual scope name. (As of July these were Queue, Ticket and System
+sub SetRandomPassword {
+    my $self = shift;
 
-=item AppliesTo is the numerical Id of the object identified in the scope. For tickets, this is the queue #. for queues, this is the queue #
+    unless ( $self->CurrentUserCanModify('Password') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
 
-=item ExtendedPrincipals is an  SQL select clause which assumes that the only
-table in play is ACL.  It's used by HasQueueRight to pass in which 
-metaprincipals apply. Actually, it's probably obsolete. TODO: remove it.
 
-Returns 1 if a matching ACE was found.
+    my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ?  RT->Config->Get('MinimumPasswordLength') : 6);
+    my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ?  RT->Config->Get('MinimumPasswordLength') : 8);
 
-Returns undef if no ACE was found.
+    my $pass = $self->GenerateRandomPassword( $min, $max) ;
 
-=cut
+    # If we have "notify user on
 
+    my ( $val, $msg ) = $self->SetPassword($pass);
 
-sub _HasRight {
-    
-    my $self = shift;
-    my %args = ( Right => undef,
-                Scope => undef,
-                AppliesTo => undef,
-                IsRequestor => undef,
-                IsCc => undef,
-                IsAdminCc => undef,
-                IsOwner => undef,
-                ExtendedPrincipals => undef,
-                @_);
-    
-    if ($self->Disabled) {
-       $RT::Logger->debug ("Disabled User:  ".$self->Name.
-                           " failed access check for ".$args{'Right'}.
-                           " to object ".$args{'Scope'}."/".
-                           $args{'AppliesTo'}."\n");
-       return (undef);
-    }
-    
-    if (!defined $args{'Right'}) {
-       $RT::Logger->debug("_HasRight called without a right\n");
-       return(undef);
-    }
-    elsif (!defined $args{'Scope'}) {
-       $RT::Logger->debug("_HasRight called without a scope\n");
-       return(undef);
-    }
-    elsif (!defined $args{'AppliesTo'}) {
-       $RT::Logger->debug("_HasRight called without an AppliesTo object\n");
-       return(undef);
-    }
-    
-    #If we've cached a win or loss for this lookup say so
-    
-    #TODO Security +++ check to make sure this is complete and right
-    
-    #Construct a hashkey to cache decisions in
-    my ($hashkey);
-    { #it's ugly, but we need to turn off warning, cuz we're joining nulls.
-       local $^W=0;
-       $hashkey =$self->Id .":". join(':',%args);
-    }  
-    
-  # $RT::Logger->debug($hashkey."\n");
-    
-    #Anything older than 10 seconds needs to be rechecked
-    my $cache_timeout = (time - 10);
-    
-    
-    if ((defined $self->{'rights'}{"$hashkey"}) &&
-           ($self->{'rights'}{"$hashkey"} == 1 ) &&
-        (defined $self->{'rights'}{"$hashkey"}{'set'} ) &&
-           ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
-#        $RT::Logger->debug("Cached ACL win for ". 
-#                           $args{'Right'}.$args{'Scope'}.
-#                           $args{'AppliesTo'}."\n");      
-       return ($self->{'rights'}{"$hashkey"});
-    }
-    elsif ((defined $self->{'rights'}{"$hashkey"}) &&
-              ($self->{'rights'}{"$hashkey"} == -1)  &&
-           (defined $self->{'rights'}{"$hashkey"}{'set'}) &&
-              ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
-       
-#      $RT::Logger->debug("Cached ACL loss decision for ". 
-#                         $args{'Right'}.$args{'Scope'}.
-#                         $args{'AppliesTo'}."\n");        
-       
-       return(undef);
-    }
-    
-    
-    my $RightClause = "(RightName = '$args{'Right'}')";
-    my $ScopeClause = "(RightScope = '$args{'Scope'}')";
-    
-    #If an AppliesTo was passed in, we should pay attention to it.
-    #otherwise, none is needed
-    
-    $ScopeClause = "($ScopeClause AND (RightAppliesTo = $args{'AppliesTo'}))"
-      if ($args{'AppliesTo'});
-    
-    
-    # The generic principals clause looks for users with my id
-    # and Rights that apply to _everyone_
-    my $PrincipalsClause = "((PrincipalType = 'User') AND (PrincipalId = ".$self->Id."))";
-    
-    
-    # If the user is the superuser, grant them the damn right ;)
-    my $SuperUserClause = 
-      "(RightName = 'SuperUser') AND (RightScope = 'System') AND (RightAppliesTo = 0)";
-    
-    # If we've been passed in an extended principals clause, we should lump it
-    # on to the existing principals clause. it'll make life easier
-    if ($args{'ExtendedPrincipals'}) {
-       $PrincipalsClause = "(($PrincipalsClause) OR ".
-         "($args{'ExtendedPrincipalsClause'}))";
-    }
-    
-    my $GroupPrincipalsClause = "((ACL.PrincipalType = 'Group') ".
-      "AND (ACL.PrincipalId = Groups.Id) AND (GroupMembers.GroupId = Groups.Id) ".
-     " AND (GroupMembers.UserId = ".$self->Id."))";
-    
-    
-
-
-    # {{{ A bunch of magic statements that make the metagroups listed
-    # work. basically, we if the user falls into the right group,
-    # we add the type of ACL check needed
-    my (@MetaPrincipalsSubClauses, $MetaPrincipalsClause);
-    
-    #The user is always part of the 'Everyone' Group
-    push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'Everyone') AND 
-                                       (PrincipalType = 'Group') AND 
-                                       (Groups.Id = PrincipalId))");
-
-    if ($args{'IsAdminCc'}) {
-       push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'AdminCc') AND 
-                                       (PrincipalType = 'Group') AND 
-                                       (Groups.Id = PrincipalId))");
-    }
-    if ($args{'IsCc'}) {
-       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Cc') AND 
-                                       (PrincipalType = 'Group') AND 
-                                       (Groups.Id = PrincipalId))");
-    }
-    if ($args{'IsRequestor'}) {
-       push (@MetaPrincipalsSubClauses,  " ((Groups.Name = 'Requestor') AND 
-                                       (PrincipalType = 'Group') AND 
-                                       (Groups.Id = PrincipalId))");
-    }
-    if ($args{'IsOwner'}) {
-       
-       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Owner') AND 
-                                       (PrincipalType = 'Group') AND 
-                                       (Groups.Id = PrincipalId))");
-    }
-
-    # }}}
-    
-    my ($GroupRightsQuery, $MetaGroupRightsQuery, $IndividualRightsQuery, $hitcount);
-    
-    # {{{ If there are any metaprincipals to be checked
-    if (@MetaPrincipalsSubClauses) {
-       #chop off the leading or
-       #TODO redo this with an array and a join
-       $MetaPrincipalsClause = join (" OR ", @MetaPrincipalsSubClauses);
-       
-       $MetaGroupRightsQuery =  "SELECT COUNT(ACL.id) FROM ACL, Groups".
-         " WHERE " .
-           " ($ScopeClause) AND ($RightClause) AND ($MetaPrincipalsClause)";
-       
-       # {{{ deal with checking if the user has a right as a member of a metagroup
-
-#      $RT::Logger->debug("Now Trying $MetaGroupRightsQuery\n");       
-       $hitcount = $self->_Handle->FetchResult($MetaGroupRightsQuery);
-       
-       #if there's a match, the right is granted
-       if ($hitcount) {
-           $self->{'rights'}{"$hashkey"}{'set'} = time;
-           $self->{'rights'}{"$hashkey"} = 1;
-           return (1);
-       }
-       
-#      $RT::Logger->debug("No ACL matched MetaGroups query: $MetaGroupRightsQuery\n"); 
-
-       # }}}    
-       
-    }
-    # }}}
-
-    # {{{ deal with checking if the user has a right as a member of a group
-    # This query checks to se whether the user has the right as a member of a
-    # group
-    $GroupRightsQuery = "SELECT COUNT(ACL.id) FROM ACL, GroupMembers, Groups".
-      " WHERE " .
-       " (((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) ".
-         " AND ($GroupPrincipalsClause))";    
-    
-    #  $RT::Logger->debug("Now Trying $GroupRightsQuery\n");   
-    $hitcount = $self->_Handle->FetchResult($GroupRightsQuery);
-    
-    #if there's a match, the right is granted
-    if ($hitcount) {
-       $self->{'rights'}{"$hashkey"}{'set'} = time;
-       $self->{'rights'}{"$hashkey"} = 1;
-       return (1);
-    }
-    
-#    $RT::Logger->debug("No ACL matched $GroupRightsQuery\n"); 
-    
-    # }}}
-
-    # {{{ Check to see whether the user has a right as an individual
-    
-    # This query checks to see whether the current user has the right directly
-    $IndividualRightsQuery = "SELECT COUNT(ACL.id) FROM ACL WHERE ".
-      " ((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) " .
-       " AND ($PrincipalsClause)";
-
-    
-    $hitcount = $self->_Handle->FetchResult($IndividualRightsQuery);
-    
-    if ($hitcount) {
-       $self->{'rights'}{"$hashkey"}{'set'} = time;
-       $self->{'rights'}{"$hashkey"} = 1;
-       return (1);
-    }
-    # }}}
-
-    else { #If the user just doesn't have the right
-       
-#      $RT::Logger->debug("No ACL matched $IndividualRightsQuery\n");
-       
-       #If nothing matched, return 0.
-       $self->{'rights'}{"$hashkey"}{'set'} = time;
-       $self->{'rights'}{"$hashkey"} = -1;
-
-       
-       return (undef);
-    }
-}
-
-# }}}
-
-# {{{ sub CurrentUserCanModify
+    #If we got an error return the error.
+    return ( 0, $msg ) unless ($val);
 
-=head2 CurrentUserCanModify RIGHT
+    #Otherwise, we changed the password, lets return it.
+    return ( 1, $pass );
 
-If the user has rights for this object, either because
-he has 'AdminUsers' or (if he\'s trying to edit himself and the right isn\'t an 
-admin right) 'ModifySelf', return 1. otherwise, return undef.
+}
+
+=head3 ResetPassword
+
+Returns status, [ERROR or new password].  Resets this user's password to
+a randomly generated pronouncable password and emails them, using a
+global template called "PasswordChange".
+
+This function is currently unused in the UI, but available for local scripts.
 
 =cut
 
-sub CurrentUserCanModify {
+sub ResetPassword {
     my $self = shift;
-    my $right = shift;
 
-    if ($self->CurrentUserHasRight('AdminUsers')) {
-       return (1);
+    unless ( $self->CurrentUserCanModify('Password') ) {
+        return ( 0, $self->loc("Permission Denied") );
     }
-    #If the field is marked as an "administrators only" field, 
-    # don\'t let the user touch it.
-    elsif ($self->_Accessible($right, 'admin')) {
-       return(undef);
-    }
-    
-    #If the current user is trying to modify themselves
-    elsif ( ($self->id == $self->CurrentUser->id)  and
-           ($self->CurrentUserHasRight('ModifySelf'))) {
-       return(1);
+    my ( $status, $pass ) = $self->SetRandomPassword();
+
+    unless ($status) {
+        return ( 0, "$pass" );
     }
-    #If we don\'t have a good reason to grant them rights to modify
-    # by now, they lose
-    else {
-       return(undef);
+
+    my $ret = RT::Interface::Email::SendEmailUsingTemplate(
+        To        => $self->EmailAddress,
+        Template  => 'PasswordChange',
+        Arguments => {
+            NewPassword => $pass,
+        },
+    );
+
+    if ($ret) {
+        return ( 1, $self->loc('New password notification sent') );
+    } else {
+        return ( 0, $self->loc('Notification could not be sent') );
     }
-    
-}
 
-# }}}
+}
 
-# {{{ sub CurrentUserHasRight
+=head3 GenerateRandomPassword MIN_LEN and MAX_LEN
 
-=head2 CurrentUserHasRight
-  
-  Takes a single argument. returns 1 if $Self->CurrentUser
-  has the requested right. returns undef otherwise
+Returns a random password between MIN_LEN and MAX_LEN characters long.
 
 =cut
 
-sub CurrentUserHasRight {
-    my $self = shift;
-    my $right = shift;
-    
-    return ($self->CurrentUser->HasSystemRight($right));
+sub GenerateRandomPassword {
+    my $self = shift;   # just to drop it
+    return Text::Password::Pronounceable->generate(@_);
 }
 
-# }}}
+sub SafeSetPassword {
+    my $self = shift;
+    my %args = (
+        Current      => undef,
+        New          => undef,
+        Confirmation => undef,
+        @_,
+    );
+    return (1) unless defined $args{'New'} && length $args{'New'};
+
+    my %cond = $self->CurrentUserRequireToSetPassword;
+
+    unless ( $cond{'CanSet'} ) {
+        return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} );
+    }
 
+    my $error = '';
+    if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) {
+        if ( defined $args{'Current'} && length $args{'Current'} ) {
+            $error = $self->loc("Please enter your current password correctly.");
+        } else {
+            $error = $self->loc("Please enter your current password.");
+        }
+    } elsif ( $args{'New'} ne $args{'Confirmation'} ) {
+        $error = $self->loc("Passwords do not match.");
+    }
 
-# {{{ sub _Set
+    if ( $error ) {
+        $error .= ' '. $self->loc('Password has not been set.');
+        return (0, $error);
+    }
 
-sub _Set {
-  my $self = shift;
-  
-  my %args = (Field => undef,
-             Value => undef,
-             @_
-            );
+    return $self->SetPassword( $args{'New'} );
+}
 
-  # Nobody is allowed to futz with RT_System or Nobody unless they
-  # want to change an email address. For 2.2, neither should have an email address
+=head3 SetPassword
 
-  if ($self->Privileged == 2) {
-    return (0, "Can not modify system users"); 
-  }
-  unless ($self->CurrentUserCanModify($args{'Field'})) {
-      return (0, "Permission Denied");
-  }
+Takes a string. Checks the string's length and sets this user's password
+to that string.
 
+=cut
 
-  
-  #Set the new value
-  my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
-                                    Value=> $args{'Value'});
-  
-    return ($ret, $msg);
-}
+sub SetPassword {
+    my $self     = shift;
+    my $password = shift;
 
-# }}}
+    unless ( $self->CurrentUserCanModify('Password') ) {
+        return ( 0, $self->loc('Password: Permission Denied') );
+    }
 
-# {{{ sub _Value 
+    if ( !$password ) {
+        return ( 0, $self->loc("No password set") );
+    } else {
+        my ($val, $msg) = $self->ValidatePassword($password);
+        return ($val, $msg) if !$val;
+
+        my $new = !$self->HasPassword;
+        $password = $self->_GeneratePassword($password);
+
+        ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password);
+        if ($val) {
+            return ( 1, $self->loc("Password set") ) if $new;
+            return ( 1, $self->loc("Password changed") );
+        } else {
+            return ( $val, $msg );
+        }
+    }
 
-=head2 _Value
+}
 
-Takes the name of a table column.
-Returns its value as a string, if the user passes an ACL check
+sub _GeneratePassword_bcrypt {
+    my $self = shift;
+    my ($password, @rest) = @_;
+
+    my $salt;
+    my $rounds;
+    if (@rest) {
+        # The first split is the number of rounds
+        $rounds = $rest[0];
+
+        # The salt is the first 22 characters, b64 encoded usign the
+        # special bcrypt base64.
+        $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) );
+    } else {
+        $rounds = RT->Config->Get('BcryptCost');
+
+        # Generate a random 16-octet base64 salt
+        $salt = "";
+        $salt .= pack("C", int rand(256)) for 1..16;
+    }
 
-=cut
+    my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({
+        key_nul => 1,
+        cost    => $rounds,
+        salt    => $salt,
+    }, Digest::SHA::sha512( Encode::encode( 'UTF-8', $password) ) );
 
-sub _Value  {
+    return join("!", "", "bcrypt", sprintf("%02d", $rounds),
+                Crypt::Eksblowfish::Bcrypt::en_base64( $salt ).
+                Crypt::Eksblowfish::Bcrypt::en_base64( $hash )
+              );
+}
 
-  my $self = shift;
-  my $field = shift;
-  
-  #If the current user doesn't have ACLs, don't let em at it.  
-  
-  my @PublicFields = qw( Name EmailAddress Organization Disabled
-                        RealName NickName Gecos ExternalAuthId 
-                        AuthSystem ExternalContactInfoId 
-                        ContactInfoSystem );
+sub _GeneratePassword_sha512 {
+    my $self = shift;
+    my ($password, $salt) = @_;
 
-  #if the field is public, return it.
-  if ($self->_Accessible($field, 'public')) {
-      return($self->SUPER::_Value($field));
-      
-  }
-  #If the user wants to see their own values, let them
-  elsif ($self->CurrentUser->Id == $self->Id) {        
-      return($self->SUPER::_Value($field));
-  } 
-  #If the user has the admin users right, return the field
-  elsif ($self->CurrentUserHasRight('AdminUsers')) {
-      return($self->SUPER::_Value($field));
-  }
-  else {
-      return(undef);
-  }    
+    # Generate a 16-character base64 salt
+    unless ($salt) {
+        $salt = "";
+        $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64]
+            for 1..16;
+    }
 
+    my $sha = Digest::SHA->new(512);
+    $sha->add($salt);
+    $sha->add(Encode::encode( 'UTF-8', $password));
+    return join("!", "", "sha512", $salt, $sha->b64digest);
 }
 
-# }}}
+=head3 _GeneratePassword PASSWORD [, SALT]
+
+Returns a string to store in the database.  This string takes the form:
+
+   !method!salt!hash
+
+By default, the method is currently C<bcrypt>.
+
+=cut
+
+sub _GeneratePassword {
+    my $self = shift;
+    return $self->_GeneratePassword_bcrypt(@_);
+}
+
+=head3 HasPassword
+
+Returns true if the user has a valid password, otherwise returns false.
+
+=cut
+
+sub HasPassword {
+    my $self = shift;
+    my $pwd = $self->__Value('Password');
+    return undef if !defined $pwd
+                    || $pwd eq ''
+                    || $pwd eq '*NO-PASSWORD*';
+    return 1;
+}
+
+=head3 IsPassword
+
+Returns true if the passed in value is this user's password.
+Returns undef otherwise.
+
+=cut
+
+sub IsPassword {
+    my $self  = shift;
+    my $value = shift;
+
+    #TODO there isn't any apparent way to legitimately ACL this
+
+    # RT does not allow null passwords
+    if ( ( !defined($value) ) or ( $value eq '' ) ) {
+        return (undef);
+    }
+
+   if ( $self->PrincipalObj->Disabled ) {
+        $RT::Logger->info(
+            "Disabled user " . $self->Name . " tried to log in" );
+        return (undef);
+    }
+
+    unless ($self->HasPassword) {
+        return(undef);
+     }
+
+    my $stored = $self->__Value('Password');
+    if ($stored =~ /^!/) {
+        # If it's a new-style (>= RT 4.0) password, it starts with a '!'
+        my (undef, $method, @rest) = split /!/, $stored;
+        if ($method eq "bcrypt") {
+            return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
+            # Upgrade to a larger number of rounds if necessary
+            return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
+        } elsif ($method eq "sha512") {
+            return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored;
+        } else {
+            $RT::Logger->warn("Unknown hash method $method");
+            return 0;
+        }
+    } elsif (length $stored == 40) {
+        # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
+        my $hash = MIME::Base64::decode_base64($stored);
+        # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
+        my $salt = substr($hash, 0, 4, "");
+        return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5(Encode::encode( "UTF-8", $value))), 0, 26) eq $hash;
+    } elsif (length $stored == 32) {
+        # Hex nonsalted-md5
+        return 0 unless Digest::MD5::md5_hex(Encode::encode( "UTF-8", $value)) eq $stored;
+    } elsif (length $stored == 22) {
+        # Base64 nonsalted-md5
+        return 0 unless Digest::MD5::md5_base64(Encode::encode( "UTF-8", $value)) eq $stored;
+    } elsif (length $stored == 13) {
+        # crypt() output
+        return 0 unless crypt(Encode::encode( "UTF-8", $value), $stored) eq $stored;
+    } else {
+        $RT::Logger->warning("Unknown password form");
+        return 0;
+    }
+
+    # We got here by validating successfully, but with a legacy
+    # password form.  Update to the most recent form.
+    my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self;
+    $obj->_Set(Field => 'Password', Value =>  $self->_GeneratePassword($value) );
+    return 1;
+}
+
+sub CurrentUserRequireToSetPassword {
+    my $self = shift;
+
+    my %res = (
+        CanSet => 1,
+        Reason => '',
+        RequireCurrent => 1,
+    );
+
+    if ( RT->Config->Get('WebRemoteUserAuth')
+        && !RT->Config->Get('WebFallbackToRTLogin')
+    ) {
+        $res{'CanSet'} = 0;
+        $res{'Reason'} = $self->loc("External authentication enabled.");
+    } elsif ( !$self->CurrentUser->HasPassword ) {
+        if ( $self->CurrentUser->id == ($self->id||0) ) {
+            # don't require current password if user has no
+            $res{'RequireCurrent'} = 0;
+        } else {
+            $res{'CanSet'} = 0;
+            $res{'Reason'} = $self->loc("Your password is not set.");
+        }
+    }
+
+    return %res;
+}
+
+=head3 AuthToken
+
+Returns an authentication string associated with the user. This
+string can be used to generate passwordless URLs to integrate
+RT with services and programms like callendar managers, rss
+readers and other.
+
+=cut
+
+sub AuthToken {
+    my $self = shift;
+    my $secret = $self->_Value( AuthToken => @_ );
+    return $secret if $secret;
+
+    $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
+
+    my $tmp = RT::User->new( RT->SystemUser );
+    $tmp->Load( $self->id );
+    my ($status, $msg) = $tmp->SetAuthToken( $secret );
+    unless ( $status ) {
+        $RT::Logger->error( "Couldn't set auth token: $msg" );
+        return undef;
+    }
+    return $secret;
+}
+
+=head3 GenerateAuthToken
+
+Generate a random authentication string for the user.
+
+=cut
+
+sub GenerateAuthToken {
+    my $self = shift;
+    my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
+    return $self->SetAuthToken( $token );
+}
+
+=head3 GenerateAuthString
+
+Takes a string and returns back a hex hash string. Later you can use
+this pair to make sure it's generated by this user using L</ValidateAuthString>
+
+=cut
+
+sub GenerateAuthString {
+    my $self = shift;
+    my $protect = shift;
+
+    my $str = Encode::encode( "UTF-8", $self->AuthToken . $protect );
+
+    return substr(Digest::MD5::md5_hex($str),0,16);
+}
+
+=head3 ValidateAuthString
+
+Takes auth string and protected string. Returns true is protected string
+has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
+
+=cut
+
+sub ValidateAuthString {
+    my $self = shift;
+    my $auth_string = shift;
+    my $protected = shift;
+
+    my $str = Encode::encode( "UTF-8", $self->AuthToken . $protected );
+
+    return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
+}
+
+=head2 SetDisabled
+
+Toggles the user's disabled flag.
+If this flag is
+set, all password checks for this user will fail. All ACL checks for this
+user will fail. The user will appear in no user listings.
+
+=cut
+
+sub SetDisabled {
+    my $self = shift;
+    my $val = shift;
+    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+        return (0, $self->loc('Permission Denied'));
+    }
+
+    $RT::Handle->BeginTransaction();
+    my ($status, $msg) = $self->PrincipalObj->SetDisabled($val);
+    unless ($status) {
+        $RT::Handle->Rollback();
+        $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id));
+        return ($status, $msg);
+    }
+    $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
+
+    $RT::Handle->Commit();
+
+    if ( $val == 1 ) {
+        return (1, $self->loc("User disabled"));
+    } else {
+        return (1, $self->loc("User enabled"));
+    }
+
+}
+
+=head2 Disabled
+
+Returns true if user is disabled or false otherwise
+
+=cut
+
+sub Disabled {
+    my $self = shift;
+    return $self->PrincipalObj->Disabled(@_);
+}
+
+=head2 PrincipalObj
+
+Returns the principal object for this user. returns an empty RT::Principal
+if there's no principal object matching this user.
+The response is cached. PrincipalObj should never ever change.
+
+=cut
+
+sub PrincipalObj {
+    my $self = shift;
+
+    unless ( $self->id ) {
+        $RT::Logger->error("Couldn't get principal for an empty user");
+        return undef;
+    }
+
+    if ( !$self->{_principal_obj} ) {
+
+        my $obj = RT::Principal->new( $self->CurrentUser );
+        $obj->LoadById( $self->id );
+        if (! $obj->id ) {
+            $RT::Logger->crit( 'No principal for user #' . $self->id );
+            return undef;
+        } elsif ( $obj->PrincipalType ne 'User' ) {
+            $RT::Logger->crit(   'User #' . $self->id . ' has principal of ' . $obj->PrincipalType . ' type' );
+            return undef;
+        }
+        $self->{_principal_obj} = $obj;
+    }
+    return $self->{_principal_obj};
+}
+
+
+=head2 PrincipalId
+
+Returns this user's PrincipalId
+
+=cut
+
+sub PrincipalId {
+    my $self = shift;
+    return $self->Id;
+}
+
+=head2 HasGroupRight
+
+Takes a paramhash which can contain
+these items:
+    GroupObj => RT::Group or Group => integer
+    Right => 'Right'
+
+
+Returns 1 if this user has the right specified in the paramhash for the Group
+passed in.
+
+Returns undef if they don't.
+
+=cut
+
+sub HasGroupRight {
+    my $self = shift;
+    my %args = (
+        GroupObj    => undef,
+        Group       => undef,
+        Right       => undef,
+        @_
+    );
+
+
+    if ( defined $args{'Group'} ) {
+        $args{'GroupObj'} = RT::Group->new( $self->CurrentUser );
+        $args{'GroupObj'}->Load( $args{'Group'} );
+    }
+
+    # Validate and load up the GroupId
+    unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) {
+        return undef;
+    }
+
+    # Figure out whether a user has the right we're asking about.
+    my $retval = $self->HasRight(
+        Object => $args{'GroupObj'},
+        Right     => $args{'Right'},
+    );
+
+    return ($retval);
+}
+
+=head2 OwnGroups
+
+Returns a group collection object containing the groups of which this
+user is a member.
+
+=cut
+
+sub OwnGroups {
+    my $self = shift;
+    my $groups = RT::Groups->new($self->CurrentUser);
+    $groups->LimitToUserDefinedGroups;
+    $groups->WithMember(
+        PrincipalId => $self->Id,
+        Recursively => 1
+    );
+    return $groups;
+}
+
+=head2 HasRight
+
+Shim around PrincipalObj->HasRight. See L<RT::Principal>.
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    return $self->PrincipalObj->HasRight(@_);
+}
+
+=head2 CurrentUserCanSee [FIELD]
+
+Returns true if the current user can see the user, based on if it is
+public, ourself, or we have AdminUsers
+
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+    my ($what, $txn) = @_;
+
+    # If it's a public property, fine
+    return 1 if $self->_Accessible( $what, 'public' );
+
+    # Users can see all of their own properties
+    return 1 if defined($self->Id) and $self->CurrentUser->Id == $self->Id;
+
+    # If the user has the admin users right, that's also enough
+    return 1 if $self->CurrentUserHasRight( 'AdminUsers' );
+
+    # Transactions of public properties are visible to users with ShowUserHistory
+    if ($what eq "Transaction" and $self->CurrentUserHasRight( 'ShowUserHistory' )) {
+        my $type = $txn->__Value('Type');
+        my $field = $txn->__Value('Field');
+        return 1 if $type eq "Set" and $self->CurrentUserCanSee($field, $txn);
+
+        # RT::Transaction->CurrentUserCanSee deals with ensuring we meet
+        # the ACLs on CFs, so allow them here
+        return 1 if $type eq "CustomField";
+    }
+
+    return 0;
+}
+
+=head2 CurrentUserCanModify RIGHT
+
+If the user has rights for this object, either because
+he has 'AdminUsers' or (if he's trying to edit himself and the right isn't an
+admin right) 'ModifySelf', return 1. otherwise, return undef.
+
+=cut
+
+sub CurrentUserCanModify {
+    my $self  = shift;
+    my $field = shift;
+
+    if ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+        return (1);
+    }
+
+    #If the field is marked as an "administrators only" field,
+    # don't let the user touch it.
+    elsif ( $self->_Accessible( $field, 'admin' ) ) {
+        return (undef);
+    }
+
+    #If the current user is trying to modify themselves
+    elsif ( ( $self->id == $self->CurrentUser->id )
+        and ( $self->CurrentUser->HasRight(Right => 'ModifySelf', Object => $RT::System) ) )
+    {
+        return (1);
+    }
+
+    #If we don't have a good reason to grant them rights to modify
+    # by now, they lose
+    else {
+        return (undef);
+    }
+
+}
+
+=head2 CurrentUserHasRight
+
+Takes a single argument. returns 1 if $Self->CurrentUser
+has the requested right. returns undef otherwise
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self  = shift;
+    my $right = shift;
+
+    return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) );
+}
+
+sub _PrefName {
+    my $name = shift;
+    if (ref $name) {
+        $name = ref($name).'-'.$name->Id;
+    }
+
+    return 'Pref-'. $name;
+}
+
+=head2 Preferences NAME/OBJ DEFAULT
+
+Obtain user preferences associated with given object or name.
+Returns DEFAULT if no preferences found.  If DEFAULT is a hashref,
+override the entries with user preferences.
+
+=cut
+
+sub Preferences {
+    my $self  = shift;
+    my $name = _PrefName(shift);
+    my $default = shift;
+
+    my ($attr) = $self->Attributes->Named( $name );
+    my $content = $attr ? $attr->Content : undef;
+    unless ( ref $content eq 'HASH' ) {
+        return defined $content ? $content : $default;
+    }
+
+    if (ref $default eq 'HASH') {
+        for (keys %$default) {
+            exists $content->{$_} or $content->{$_} = $default->{$_};
+        }
+    } elsif (defined $default) {
+        $RT::Logger->error("Preferences $name for user #".$self->Id." is hash but default is not");
+    }
+    return $content;
+}
+
+=head2 SetPreferences NAME/OBJ VALUE
+
+Set user preferences associated with given object or name.
+
+=cut
+
+sub SetPreferences {
+    my $self = shift;
+    my $name = _PrefName( shift );
+    my $value = shift;
+
+    return (0, $self->loc("No permission to set preferences"))
+        unless $self->CurrentUserCanModify('Preferences');
+
+    my ($attr) = $self->Attributes->Named( $name );
+    if ( $attr ) {
+        my ($ok, $msg) = $attr->SetContent( $value );
+        return (1, "No updates made")
+            if $msg eq "That is already the current value";
+        return ($ok, $msg);
+    } else {
+        return $self->AddAttribute( Name => $name, Content => $value );
+    }
+}
+
+=head2 DeletePreferences NAME/OBJ VALUE
+
+Delete user preferences associated with given object or name.
+
+=cut
+
+sub DeletePreferences {
+    my $self = shift;
+    my $name = _PrefName( shift );
+
+    return (0, $self->loc("No permission to set preferences"))
+        unless $self->CurrentUserCanModify('Preferences');
+
+    my ($attr) = $self->DeleteAttribute( $name );
+    return (0, $self->loc("Preferences were not found"))
+        unless $attr;
+
+    return 1;
+}
+
+=head2 Stylesheet
+
+Returns a list of valid stylesheets take from preferences.
+
+=cut
+
+sub Stylesheet {
+    my $self = shift;
+
+    my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser);
+
+    if (RT::Interface::Web->ComponentPathIsSafe($style)) {
+        for my $root (RT::Interface::Web->StaticRoots) {
+            if (-d "$root/css/$style") {
+                return $style
+            }
+        }
+    }
+
+    # Fall back to the system stylesheet.
+    return RT->Config->Get('WebDefaultStylesheet');
+}
+
+=head2 WatchedQueues ROLE_LIST
+
+Returns a RT::Queues object containing every queue watched by the user.
+
+Takes a list of roles which is some subset of ('Cc', 'AdminCc').  Defaults to:
+
+$user->WatchedQueues('Cc', 'AdminCc');
+
+=cut
+
+sub WatchedQueues {
+
+    my $self = shift;
+    my @roles = @_ ? @_ : ('Cc', 'AdminCc');
+
+    $RT::Logger->debug('WatcheQueues got user ' . $self->Name);
+
+    my $watched_queues = RT::Queues->new($self->CurrentUser);
+
+    my $group_alias = $watched_queues->Join(
+                                             ALIAS1 => 'main',
+                                             FIELD1 => 'id',
+                                             TABLE2 => 'Groups',
+                                             FIELD2 => 'Instance',
+                                           );
+
+    $watched_queues->Limit(
+                            ALIAS => $group_alias,
+                            FIELD => 'Domain',
+                            VALUE => 'RT::Queue-Role',
+                            ENTRYAGGREGATOR => 'AND',
+                            CASESENSITIVE => 0,
+                          );
+    if (grep { $_ eq 'Cc' } @roles) {
+        $watched_queues->Limit(
+                                SUBCLAUSE => 'LimitToWatchers',
+                                ALIAS => $group_alias,
+                                FIELD => 'Name',
+                                VALUE => 'Cc',
+                                ENTRYAGGREGATOR => 'OR',
+                              );
+    }
+    if (grep { $_ eq 'AdminCc' } @roles) {
+        $watched_queues->Limit(
+                                SUBCLAUSE => 'LimitToWatchers',
+                                ALIAS => $group_alias,
+                                FIELD => 'Name',
+                                VALUE => 'AdminCc',
+                                ENTRYAGGREGATOR => 'OR',
+                              );
+    }
+
+    my $queues_alias = $watched_queues->Join(
+                                              ALIAS1 => $group_alias,
+                                              FIELD1 => 'id',
+                                              TABLE2 => 'CachedGroupMembers',
+                                              FIELD2 => 'GroupId',
+                                            );
+    $watched_queues->Limit(
+                            ALIAS => $queues_alias,
+                            FIELD => 'MemberId',
+                            VALUE => $self->PrincipalId,
+                          );
+    $watched_queues->Limit(
+                            ALIAS => $queues_alias,
+                            FIELD => 'Disabled',
+                            VALUE => 0,
+                          );
+
+
+    $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues");
+
+    return $watched_queues;
+
+}
+
+sub _Set {
+    my $self = shift;
+
+    my %args = (
+        Field => undef,
+        Value => undef,
+    TransactionType   => 'Set',
+    RecordTransaction => 1,
+        @_
+    );
+
+    # Nobody is allowed to futz with RT_System or Nobody
+
+    if ( ($self->Id == RT->SystemUser->Id )  ||
+         ($self->Id == RT->Nobody->Id)) {
+        return ( 0, $self->loc("Can not modify system users") );
+    }
+    unless ( $self->CurrentUserCanModify( $args{'Field'} ) ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my $Old = $self->SUPER::_Value("$args{'Field'}");
+
+    my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
+                      Value => $args{'Value'} );
+
+    #If we can't actually set the field to the value, don't record
+    # a transaction. instead, get out of here.
+    if ( $ret == 0 ) { return ( 0, $msg ); }
+
+    if ( $args{'RecordTransaction'} == 1 ) {
+        if ($args{'Field'} eq "Password") {
+            $args{'Value'} = $Old = '********';
+        }
+        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+                                               Type => $args{'TransactionType'},
+                                               Field     => $args{'Field'},
+                                               NewValue  => $args{'Value'},
+                                               OldValue  => $Old,
+                                               TimeTaken => $args{'TimeTaken'},
+        );
+        return ( $Trans, scalar $TransObj->BriefDescription );
+    } else {
+        return ( $ret, $msg );
+    }
+}
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value {
+
+    my $self  = shift;
+    my $field = shift;
+
+    # Defer to the abstraction above to know if the field can be read
+    return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field);
+    return undef;
+}
+
+=head2 FriendlyName
+
+Return the friendly name
+
+=cut
+
+sub FriendlyName {
+    my $self = shift;
+    return $self->RealName if defined $self->RealName and length $self->RealName;
+    return $self->Name;
+}
+
+=head2 Format
+
+Class or object method.
+
+Returns a string describing a user in the current user's preferred format.
+
+May be invoked in three ways:
+
+    $UserObj->Format;
+    RT::User->Format( User => $UserObj );   # same as above
+    RT::User->Format( Address => $AddressObj, CurrentUser => $CurrentUserObj );
+
+Possible arguments are:
+
+=over
+
+=item User
+
+An L<RT::User> object representing the user to format.  Preferred to Address.
+
+=item Address
+
+An L<Email::Address> object representing the user address to format.  Address
+will be used to lookup an L<RT::User> if possible.
+
+=item CurrentUser
+
+Required when Format is called as a class method with an Address argument.
+Otherwise, this argument is ignored in preference to the CurrentUser of the
+involved L<RT::User> object.
+
+=item Format
+
+Specifies the format to use, overriding any set from the config or current
+user's preferences.
+
+=back
+
+=cut
+
+sub Format {
+    my $self = shift;
+    my %args = (
+        User        => undef,
+        Address     => undef,
+        CurrentUser => undef,
+        Format      => undef,
+        @_
+    );
+
+    if (blessed($self) and $self->id) {
+        @args{"User", "CurrentUser"} = ($self, $self->CurrentUser);
+    }
+    elsif ($args{User} and $args{User}->id) {
+        $args{CurrentUser} = $args{User}->CurrentUser;
+    }
+    elsif ($args{Address} and $args{CurrentUser}) {
+        $args{User} = RT::User->new( $args{CurrentUser} );
+        $args{User}->LoadByEmail( $args{Address}->address );
+        if ($args{User}->id) {
+            delete $args{Address};
+        } else {
+            delete $args{User};
+        }
+    }
+    else {
+        RT->Logger->warning("Invalid arguments to RT::User->Format at @{[join '/', caller]}");
+        return "";
+    }
+
+    $args{Format} ||= RT->Config->Get("UsernameFormat", $args{CurrentUser});
+    $args{Format} =~ s/[^A-Za-z0-9_]+//g;
+
+    my $method    = "_FormatUser" . ucfirst lc $args{Format};
+    my $formatter = $self->can($method);
+
+    unless ($formatter) {
+        RT->Logger->error(
+            "Either system config or user #" . $args{CurrentUser}->id .
+            " picked UsernameFormat $args{Format}, but RT::User->$method doesn't exist"
+        );
+        $formatter = $self->can("_FormatUserRole");
+    }
+    return $formatter->( $self, map { $_ => $args{$_} } qw(User Address) );
+}
+
+sub _FormatUserRole {
+    my $self = shift;
+    my %args = @_;
+
+    my $user = $args{User};
+    return $self->_FormatUserVerbose(@_)
+        unless $user and $user->Privileged;
+
+    my $name = $user->Name;
+    $name .= " (".$user->RealName.")"
+        if $user->RealName and lc $user->RealName ne lc $user->Name;
+    return $name;
+}
+
+sub _FormatUserConcise {
+    my $self = shift;
+    my %args = @_;
+    return $args{User} ? $args{User}->FriendlyName : $args{Address}->address;
+}
+
+sub _FormatUserVerbose {
+    my $self = shift;
+    my %args = @_;
+    my ($user, $address) = @args{"User", "Address"};
+
+    my $email   = '';
+    my $phrase  = '';
+    my $comment = '';
+
+    if ($user) {
+        $email   = $user->EmailAddress || '';
+        $phrase  = $user->RealName  if $user->RealName and lc $user->RealName ne lc $email;
+        $comment = $user->Name      if lc $user->Name ne lc $email;
+    } else {
+        ($email, $phrase, $comment) = (map { $address->$_ } "address", "phrase", "comment");
+    }
+
+    return join " ", grep { $_ } ($phrase || $comment || ''), ($email ? "<$email>" : "");
+}
+
+=head2 PreferredKey
+
+Returns the preferred key of the user. If none is set, then this will query
+GPG and set the preferred key to the maximally trusted key found (and then
+return it). Returns C<undef> if no preferred key can be found.
+
+=cut
+
+sub PreferredKey
+{
+    my $self = shift;
+    return undef unless RT->Config->Get('GnuPG')->{'Enable'};
+
+    if ( ($self->CurrentUser->Id != $self->Id )  &&
+          !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
+          return undef;
+    }
+
+
+
+    my $prefkey = $self->FirstAttribute('PreferredKey');
+    return $prefkey->Content if $prefkey;
+
+    # we don't have a preferred key for this user, so now we must query GPG
+    my %res = RT::Crypt->GetKeysForEncryption($self->EmailAddress);
+    return undef unless defined $res{'info'};
+    my @keys = @{ $res{'info'} };
+    return undef if @keys == 0;
+
+    if (@keys == 1) {
+        $prefkey = $keys[0]->{'Fingerprint'};
+    } else {
+        # prefer the maximally trusted key
+        @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys;
+        $prefkey = $keys[0]->{'Fingerprint'};
+    }
+
+    $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey);
+    return $prefkey;
+}
+
+sub PrivateKey {
+    my $self = shift;
+
+
+    #If the user wants to see their own values, let them.
+    #If the user is an admin, let them.
+    #Otherwwise, don't let them.
+    #
+    if ( ($self->CurrentUser->Id != $self->Id )  &&
+          !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
+          return undef;
+    }
+
+    my $key = $self->FirstAttribute('PrivateKey') or return undef;
+    return $key->Content;
+}
+
+sub SetPrivateKey {
+    my $self = shift;
+    my $key = shift;
+
+    # Users should not be able to change their own PrivateKey values
+    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+        return (0, $self->loc("Permission Denied"));
+    }
+
+    unless ( $key ) {
+        my ($status, $msg) = $self->DeleteAttribute('PrivateKey');
+        unless ( $status ) {
+            $RT::Logger->error( "Couldn't delete attribute: $msg" );
+            return ($status, $self->loc("Couldn't unset private key"));
+        }
+        return ($status, $self->loc("Unset private key"));
+    }
+
+    # check that it's really private key
+    {
+        my %tmp = RT::Crypt->GetKeysForSigning( Signer => $key, Protocol => 'GnuPG' );
+        return (0, $self->loc("No such key or it's not suitable for signing"))
+            if $tmp{'exit_code'} || !$tmp{'info'};
+    }
+
+    my ($status, $msg) = $self->SetAttribute(
+        Name => 'PrivateKey',
+        Content => $key,
+    );
+    return ($status, $self->loc("Couldn't set private key"))
+        unless $status;
+    return ($status, $self->loc("Set private key"));
+}
+
+sub SetLang {
+    my $self = shift;
+    my ($lang) = @_;
+
+    unless ($self->CurrentUserCanModify('Lang')) {
+        return (0, $self->loc("Permission Denied"));
+    }
+
+    # Local hack to cause the result message to be in the _new_ language
+    # if we're updating ourselves
+    $self->CurrentUser->{LangHandle} = RT::I18N->get_handle( $lang )
+        if $self->CurrentUser->id == $self->id;
+    return $self->_Set( Field => 'Lang', Value => $lang );
+}
+
+sub BasicColumns {
+    (
+    [ Name => 'Username' ],
+    [ EmailAddress => 'Email' ],
+    [ RealName => 'Name' ],
+    [ Organization => 'Organization' ],
+    );
+}
+
+=head2 Bookmarks
+
+Returns an unordered list of IDs representing the user's bookmarked tickets.
+
+=cut
+
+sub Bookmarks {
+    my $self = shift;
+    my $bookmarks = $self->FirstAttribute('Bookmarks');
+    return if !$bookmarks;
+
+    $bookmarks = $bookmarks->Content;
+    return if !$bookmarks;
+
+    return keys %$bookmarks;
+}
+
+=head2 HasBookmark TICKET
+
+Returns whether the provided ticket is bookmarked by the user.
+
+=cut
+
+sub HasBookmark {
+    my $self   = shift;
+    my $ticket = shift;
+    my $id     = $ticket->id;
+
+    # maintain bookmarks across merges
+    my @ids = ($id, $ticket->Merged);
+
+    my $bookmarks = $self->FirstAttribute('Bookmarks');
+    $bookmarks = $bookmarks ? $bookmarks->Content : {};
+
+    my @bookmarked = grep { $bookmarks->{ $_ } } @ids;
+    return @bookmarked ? 1 : 0;
+}
+
+=head2 ToggleBookmark TICKET
+
+Toggles whether the provided ticket is bookmarked by the user.
+
+=cut
+
+sub ToggleBookmark {
+    my $self   = shift;
+    my $ticket = shift;
+    my $id     = $ticket->id;
+
+    # maintain bookmarks across merges
+    my @ids = ($id, $ticket->Merged);
+
+    my $bookmarks = $self->FirstAttribute('Bookmarks');
+    $bookmarks = $bookmarks ? $bookmarks->Content : {};
+
+    my $is_bookmarked;
+
+    if ( grep { $bookmarks->{ $_ } } @ids ) {
+        delete $bookmarks->{ $_ } foreach @ids;
+        $is_bookmarked = 0;
+    } else {
+        $bookmarks->{ $id } = 1;
+        $is_bookmarked = 1;
+    }
+
+    $self->SetAttribute(
+        Name    => 'Bookmarks',
+        Content => $bookmarks,
+    );
+
+    return $is_bookmarked;
+}
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+  varchar(200) 'Name'.
+  varbinary(256) 'Password'.
+  varchar(16) 'AuthToken'.
+  text 'Comments'.
+  text 'Signature'.
+  varchar(120) 'EmailAddress'.
+  text 'FreeformContactInfo'.
+  varchar(200) 'Organization'.
+  varchar(120) 'RealName'.
+  varchar(16) 'NickName'.
+  varchar(16) 'Lang'.
+  varchar(16) 'EmailEncoding'.
+  varchar(16) 'WebEncoding'.
+  varchar(100) 'ExternalContactInfoId'.
+  varchar(30) 'ContactInfoSystem'.
+  varchar(100) 'ExternalAuthId'.
+  varchar(30) 'AuthSystem'.
+  varchar(16) 'Gecos'.
+  varchar(30) 'HomePhone'.
+  varchar(30) 'WorkPhone'.
+  varchar(30) 'MobilePhone'.
+  varchar(30) 'PagerPhone'.
+  varchar(200) 'Address1'.
+  varchar(200) 'Address2'.
+  varchar(100) 'City'.
+  varchar(100) 'State'.
+  varchar(16) 'Zip'.
+  varchar(50) 'Country'.
+  varchar(50) 'Timezone'.
+  text 'PGPKey'.
+
+=cut
+
+
+
+
+=head2 id
+
+Returns the current value of id. 
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=head2 Name
+
+Returns the current value of Name. 
+(In the database, Name is stored as varchar(200).)
+
+
+
+=head2 SetName VALUE
+
+
+Set Name to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Name will be stored as a varchar(200).)
+
+
+=cut
+
+
+=head2 Password
+
+Returns the current value of Password. 
+(In the database, Password is stored as varchar(256).)
+
+
+
+=head2 SetPassword VALUE
+
+
+Set Password to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Password will be stored as a varchar(256).)
+
+
+=cut
+
+
+=head2 AuthToken
+
+Returns the current value of AuthToken. 
+(In the database, AuthToken is stored as varchar(16).)
+
+
+
+=head2 SetAuthToken VALUE
+
+
+Set AuthToken to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, AuthToken will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 Comments
+
+Returns the current value of Comments. 
+(In the database, Comments is stored as text.)
+
+
+
+=head2 SetComments VALUE
+
+
+Set Comments to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Comments will be stored as a text.)
+
+
+=cut
+
+
+=head2 Signature
+
+Returns the current value of Signature. 
+(In the database, Signature is stored as text.)
+
+
+
+=head2 SetSignature VALUE
+
+
+Set Signature to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Signature will be stored as a text.)
+
+
+=cut
+
+
+=head2 EmailAddress
+
+Returns the current value of EmailAddress. 
+(In the database, EmailAddress is stored as varchar(120).)
+
+
+
+=head2 SetEmailAddress VALUE
+
+
+Set EmailAddress to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, EmailAddress will be stored as a varchar(120).)
+
+
+=cut
+
+
+=head2 FreeformContactInfo
+
+Returns the current value of FreeformContactInfo. 
+(In the database, FreeformContactInfo is stored as text.)
+
+
+
+=head2 SetFreeformContactInfo VALUE
+
+
+Set FreeformContactInfo to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, FreeformContactInfo will be stored as a text.)
+
+
+=cut
+
+
+=head2 Organization
+
+Returns the current value of Organization. 
+(In the database, Organization is stored as varchar(200).)
+
+
+
+=head2 SetOrganization VALUE
+
+
+Set Organization to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Organization will be stored as a varchar(200).)
+
+
+=cut
+
+
+=head2 RealName
+
+Returns the current value of RealName. 
+(In the database, RealName is stored as varchar(120).)
+
+
+
+=head2 SetRealName VALUE
+
+
+Set RealName to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, RealName will be stored as a varchar(120).)
+
+
+=cut
+
+
+=head2 NickName
+
+Returns the current value of NickName. 
+(In the database, NickName is stored as varchar(16).)
+
+
+
+=head2 SetNickName VALUE
+
+
+Set NickName to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, NickName will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 Lang
+
+Returns the current value of Lang. 
+(In the database, Lang is stored as varchar(16).)
+
+
+
+=head2 SetLang VALUE
+
+
+Set Lang to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Lang will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 EmailEncoding
+
+Returns the current value of EmailEncoding. 
+(In the database, EmailEncoding is stored as varchar(16).)
+
+
+
+=head2 SetEmailEncoding VALUE
+
+
+Set EmailEncoding to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, EmailEncoding will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 WebEncoding
+
+Returns the current value of WebEncoding. 
+(In the database, WebEncoding is stored as varchar(16).)
+
+
+
+=head2 SetWebEncoding VALUE
+
+
+Set WebEncoding to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, WebEncoding will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 ExternalContactInfoId
+
+Returns the current value of ExternalContactInfoId. 
+(In the database, ExternalContactInfoId is stored as varchar(100).)
+
+
+
+=head2 SetExternalContactInfoId VALUE
+
+
+Set ExternalContactInfoId to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ExternalContactInfoId will be stored as a varchar(100).)
+
+
+=cut
+
+
+=head2 ContactInfoSystem
+
+Returns the current value of ContactInfoSystem. 
+(In the database, ContactInfoSystem is stored as varchar(30).)
+
+
+
+=head2 SetContactInfoSystem VALUE
+
+
+Set ContactInfoSystem to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ContactInfoSystem will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 ExternalAuthId
+
+Returns the current value of ExternalAuthId. 
+(In the database, ExternalAuthId is stored as varchar(100).)
+
+
+
+=head2 SetExternalAuthId VALUE
+
+
+Set ExternalAuthId to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ExternalAuthId will be stored as a varchar(100).)
+
+
+=cut
+
+
+=head2 AuthSystem
+
+Returns the current value of AuthSystem. 
+(In the database, AuthSystem is stored as varchar(30).)
+
+
+
+=head2 SetAuthSystem VALUE
+
+
+Set AuthSystem to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, AuthSystem will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 Gecos
+
+Returns the current value of Gecos. 
+(In the database, Gecos is stored as varchar(16).)
+
+
+
+=head2 SetGecos VALUE
+
+
+Set Gecos to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Gecos will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 HomePhone
+
+Returns the current value of HomePhone. 
+(In the database, HomePhone is stored as varchar(30).)
+
+
+
+=head2 SetHomePhone VALUE
+
+
+Set HomePhone to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, HomePhone will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 WorkPhone
+
+Returns the current value of WorkPhone. 
+(In the database, WorkPhone is stored as varchar(30).)
+
+
+
+=head2 SetWorkPhone VALUE
+
+
+Set WorkPhone to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, WorkPhone will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 MobilePhone
+
+Returns the current value of MobilePhone. 
+(In the database, MobilePhone is stored as varchar(30).)
+
+
+
+=head2 SetMobilePhone VALUE
+
+
+Set MobilePhone to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, MobilePhone will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 PagerPhone
+
+Returns the current value of PagerPhone. 
+(In the database, PagerPhone is stored as varchar(30).)
+
+
+
+=head2 SetPagerPhone VALUE
+
+
+Set PagerPhone to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, PagerPhone will be stored as a varchar(30).)
+
+
+=cut
+
+
+=head2 Address1
+
+Returns the current value of Address1. 
+(In the database, Address1 is stored as varchar(200).)
+
+
+
+=head2 SetAddress1 VALUE
+
+
+Set Address1 to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Address1 will be stored as a varchar(200).)
+
+
+=cut
+
+
+=head2 Address2
+
+Returns the current value of Address2. 
+(In the database, Address2 is stored as varchar(200).)
+
+
+
+=head2 SetAddress2 VALUE
+
+
+Set Address2 to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Address2 will be stored as a varchar(200).)
+
+
+=cut
+
+
+=head2 City
+
+Returns the current value of City. 
+(In the database, City is stored as varchar(100).)
+
+
+
+=head2 SetCity VALUE
+
+
+Set City to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, City will be stored as a varchar(100).)
+
+
+=cut
+
+
+=head2 State
+
+Returns the current value of State. 
+(In the database, State is stored as varchar(100).)
+
+
+
+=head2 SetState VALUE
+
+
+Set State to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, State will be stored as a varchar(100).)
+
+
+=cut
+
+
+=head2 Zip
+
+Returns the current value of Zip. 
+(In the database, Zip is stored as varchar(16).)
+
+
+
+=head2 SetZip VALUE
+
+
+Set Zip to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Zip will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 Country
+
+Returns the current value of Country. 
+(In the database, Country is stored as varchar(50).)
+
+
+
+=head2 SetCountry VALUE
+
+
+Set Country to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Country will be stored as a varchar(50).)
+
+
+=cut
+
+
+=head2 Timezone
+
+Returns the current value of Timezone. 
+(In the database, Timezone is stored as varchar(50).)
+
+
+
+=head2 SetTimezone VALUE
+
+
+Set Timezone to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Timezone will be stored as a varchar(50).)
+
+
+=cut
+
+
+=head2 PGPKey
+
+Returns the current value of PGPKey. 
+(In the database, PGPKey is stored as text.)
+
+
+
+=head2 SetPGPKey VALUE
+
+
+Set PGPKey to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, PGPKey will be stored as a text.)
+
+
+=cut
+
+
+=head2 SMIMECertificate
+
+Returns the current value of SMIMECertificate. 
+(In the database, SMIMECertificate is stored as text.)
+
+
+
+=head2 SetSMIMECertificate VALUE
+
+
+Set SMIMECertificate to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, SMIMECertificate will be stored as a text.)
+
+
+=cut
+
+
+=head2 Creator
+
+Returns the current value of Creator. 
+(In the database, Creator is stored as int(11).)
+
+
+=cut
+
+
+=head2 Created
+
+Returns the current value of Created. 
+(In the database, Created is stored as datetime.)
+
+
+=cut
+
+
+=head2 LastUpdatedBy
+
+Returns the current value of LastUpdatedBy. 
+(In the database, LastUpdatedBy is stored as int(11).)
+
+
+=cut
+
+
+=head2 LastUpdated
+
+Returns the current value of LastUpdated. 
+(In the database, LastUpdated is stored as datetime.)
+
+
+=cut
+
+
+# much false laziness w/Ticket.pm.  now with RT 4!
+our %LINKDIRMAP = (
+    MemberOf => { Base => 'MemberOf',
+                  Target => 'HasMember', },
+    RefersTo => { Base => 'RefersTo',
+                Target => 'ReferredToBy', },
+    DependsOn => { Base => 'DependsOn',
+                   Target => 'DependedOnBy', },
+    MergedInto => { Base => 'MergedInto',
+                   Target => 'MergedInto', },
+
+);
+
+sub LINKDIRMAP   { return \%LINKDIRMAP   }
+
+
+=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,
+        @_
+    );
+
+    unless ( $args{'Target'} || $args{'Base'} ) {
+        $RT::Logger->error("Base or Target must be specified\n");
+        return ( 0, $self->loc('Either base or target must be specified') );
+    }
+
+    #check acls
+    my $right = 0;
+    $right++ if $self->CurrentUserHasRight('AdminUsers');
+    if ( !$right && $RT::StrictLinkACL ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+#    # If the other URI is an RT::Ticket, we want to make sure the user
+#    # can modify it too...
+#    my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+#    return (0, $msg) unless $status;
+#    if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+#        $right++;
+#    }
+#    if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
+#         ( $RT::StrictLinkACL && $right < 2 ) )
+#    {
+#        return ( 0, $self->loc("Permission Denied") );
+#    }
+
+    my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+
+    if ( !$val ) {
+        $RT::Logger->debug("Couldn't find that link\n");
+        return ( 0, $Msg );
+    }
+
+    my ($direction, $remote_link);
+
+    if ( $args{'Base'} ) {
+       $remote_link = $args{'Base'};
+       $direction = 'Target';
+    }
+    elsif ( $args{'Target'} ) {
+       $remote_link = $args{'Target'};
+        $direction='Base';
+    }
+
+    if ( $args{'Silent'} ) {
+        return ( $val, $Msg );
+    }
+    else {
+       my $remote_uri = RT::URI->new( $self->CurrentUser );
+       $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
+        );
+
+        if ( $remote_uri->IsLocal ) {
+
+            my $OtherObj = $remote_uri->Object;
+            my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'DeleteLink',
+                                                           Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+                                                                                           : $LINKDIRMAP{$args{'Type'}}->{Target},
+                                                           OldValue => $self->URI,
+                                                           ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
+                                                           TimeTaken => 0 );
+        }
+
+        return ( $Trans, $Msg );
+    }
+}
+
+sub AddLink {
+    my $self = shift;
+    my %args = ( Target => '',
+                 Base   => '',
+                 Type   => '',
+                 Silent => undef,
+                 @_ );
+
+    unless ( $args{'Target'} || $args{'Base'} ) {
+        $RT::Logger->error("Base or Target must be specified\n");
+        return ( 0, $self->loc('Either base or target must be specified') );
+    }
+
+    my $right = 0;
+    $right++ if $self->CurrentUserHasRight('AdminUsers');
+    if ( !$right && $RT::StrictLinkACL ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+#    # If the other URI is an RT::Ticket, we want to make sure the user
+#    # can modify it too...
+#    my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+#    return (0, $msg) unless $status;
+#    if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+#        $right++;
+#    }
+#    if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
+#         ( $RT::StrictLinkACL && $right < 2 ) )
+#    {
+#        return ( 0, $self->loc("Permission Denied") );
+#    }
+
+    return $self->_AddLink(%args);
+}
+
+=head2 _AddLink  
+
+Private non-acled variant of AddLink so that links can be added during create.
+
+=cut
+
+sub _AddLink {
+    my $self = shift;
+    my %args = ( Target => '',
+                 Base   => '',
+                 Type   => '',
+                 Silent => undef,
+                 @_ );
+
+    my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+    return ($val, $msg) if !$val || $exist;
+
+    my ($direction, $remote_link);
+    if ( $args{'Target'} ) {
+        $remote_link  = $args{'Target'};
+        $direction    = 'Base';
+    } elsif ( $args{'Base'} ) {
+        $remote_link  = $args{'Base'};
+        $direction    = 'Target';
+    }
+
+    # Don't write the transaction if we're doing this on create
+    if ( $args{'Silent'} ) {
+        return ( $val, $msg );
+    }
+    else {
+        my $remote_uri = RT::URI->new( $self->CurrentUser );
+       $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 );
+
+        if ( $remote_uri->IsLocal ) {
+
+            my $OtherObj = $remote_uri->Object;
+            my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'AddLink',
+                                                           Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} 
+                                                                                           : $LINKDIRMAP{$args{'Type'}}->{Target},
+                                                           NewValue => $self->URI,
+                                                           ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
+                                                           TimeTaken => 0 );
+        }
+        return ( $val, $Msg );
+    }
+
+}
+
+
+sub _CoreAccessible {
+    {
+     
+        id =>
+        {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+        Name => 
+        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
+        Password => 
+        {read => 1, write => 1, sql_type => 12, length => 256,  is_blob => 0,  is_numeric => 0,  type => 'varchar(256)', default => ''},
+        AuthToken => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        Comments => 
+        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
+        Signature => 
+        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
+        EmailAddress => 
+        {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
+        FreeformContactInfo => 
+        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
+        Organization => 
+        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
+        RealName => 
+        {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
+        NickName => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        Lang => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        EmailEncoding => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        WebEncoding => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        ExternalContactInfoId => 
+        {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
+        ContactInfoSystem => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        ExternalAuthId => 
+        {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
+        AuthSystem => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        Gecos => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        HomePhone => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        WorkPhone => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        MobilePhone => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        PagerPhone => 
+        {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
+        Address1 => 
+        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
+        Address2 => 
+        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
+        City => 
+        {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
+        State => 
+        {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
+        Zip => 
+        {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        Country => 
+        {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
+        Timezone => 
+        {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
+        PGPKey => 
+        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
+        SMIMECertificate =>
+        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
+        Creator => 
+        {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        Created => 
+        {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        LastUpdatedBy => 
+        {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        LastUpdated => 
+        {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+
+ }
+};
+
+sub UID {
+    my $self = shift;
+    return undef unless defined $self->Name;
+    return "@{[ref $self]}-@{[$self->Name]}";
+}
+
+sub FindDependencies {
+    my $self = shift;
+    my ($walker, $deps) = @_;
+
+    $self->SUPER::FindDependencies($walker, $deps);
+
+    # ACL equivalence group
+    my $objs = RT::Groups->new( $self->CurrentUser );
+    $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 );
+    $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
+    $deps->Add( in => $objs );
+
+    # Memberships in SystemInternal groups
+    $objs = RT::GroupMembers->new( $self->CurrentUser );
+    $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id );
+    my $principals = $objs->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'GroupId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    my $groups = $objs->Join(
+        ALIAS1 => $principals,
+        FIELD1 => 'ObjectId',
+        TABLE2 => 'Groups',
+        FIELD2 => 'Id',
+    );
+    $objs->Limit(
+        ALIAS => $groups,
+        FIELD => 'Domain',
+        VALUE => 'SystemInternal',
+        CASESENSITIVE => 0
+    );
+    $deps->Add( in => $objs );
+
+    # XXX: This ignores the myriad of "in" references from the Creator
+    # and LastUpdatedBy columns.
+}
+
+sub __DependsOn {
+    my $self = shift;
+    my %args = (
+        Shredder => undef,
+        Dependencies => undef,
+        @_,
+    );
+    my $deps = $args{'Dependencies'};
+    my $list = [];
+
+# Principal
+    $deps->_PushDependency(
+        BaseObject => $self,
+        Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER,
+        TargetObject => $self->PrincipalObj,
+        Shredder => $args{'Shredder'}
+    );
+
+# ACL equivalence group
+# don't use LoadACLEquivalenceGroup cause it may not exists any more
+    my $objs = RT::Groups->new( $self->CurrentUser );
+    $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 );
+    $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
+    push( @$list, $objs );
+
+# Cleanup user's membership
+    $objs = RT::GroupMembers->new( $self->CurrentUser );
+    $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id );
+    push( @$list, $objs );
+
+    $deps->_PushDependencies(
+        BaseObject => $self,
+        Flags => RT::Shredder::Constants::DEPENDS_ON,
+        TargetObjects => $list,
+        Shredder => $args{'Shredder'}
+    );
+
+# TODO: Almost all objects has Creator, LastUpdatedBy and etc. fields
+# which are references on users(Principal actualy)
+    my @OBJECTS = qw(
+        ACL
+        Articles
+        Attachments
+        Attributes
+        CachedGroupMembers
+        Classes
+        CustomFieldValues
+        CustomFields
+        GroupMembers
+        Groups
+        Links
+        ObjectClasses
+        ObjectCustomFieldValues
+        ObjectCustomFields
+        ObjectScrips
+        Principals
+        Queues
+        ScripActions
+        ScripConditions
+        Scrips
+        Templates
+        Tickets
+        Transactions
+        Users
+    );
+    my @var_objs;
+    foreach( @OBJECTS ) {
+        my $class = "RT::$_";
+        foreach my $method ( qw(Creator LastUpdatedBy) ) {
+            my $objs = $class->new( $self->CurrentUser );
+            next unless $objs->RecordClass->_Accessible( $method => 'read' );
+            $objs->Limit( FIELD => $method, VALUE => $self->id );
+            push @var_objs, $objs;
+        }
+    }
+    $deps->_PushDependencies(
+        BaseObject => $self,
+        Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::VARIABLE,
+        TargetObjects => \@var_objs,
+        Shredder => $args{'Shredder'}
+    );
+
+    return $self->SUPER::__DependsOn( %args );
+}
+
+sub BeforeWipeout {
+    my $self = shift;
+    if( $self->Name =~ /^(RT_System|Nobody)$/ ) {
+        RT::Shredder::Exception::Info->throw('SystemObject');
+    }
+    return $self->SUPER::BeforeWipeout( @_ );
+}
+
+sub Serialize {
+    my $self = shift;
+    return (
+        Disabled => $self->PrincipalObj->Disabled,
+        Principal => $self->PrincipalObj->UID,
+        PrincipalId => $self->PrincipalObj->Id,
+        $self->SUPER::Serialize(@_),
+    );
+}
+
+sub PreInflate {
+    my $class = shift;
+    my ($importer, $uid, $data) = @_;
+
+    my $principal_uid = delete $data->{Principal};
+    my $principal_id  = delete $data->{PrincipalId};
+    my $disabled      = delete $data->{Disabled};
+
+    my $obj = RT::User->new( RT->SystemUser );
+    $obj->LoadByCols( Name => $data->{Name} );
+    $obj->LoadByEmail( $data->{EmailAddress} ) unless $obj->Id;
+    if ($obj->Id) {
+        # User already exists -- merge
+
+        # XXX: We might be merging a privileged user into an unpriv one,
+        # in which case we should probably promote the unpriv user to
+        # being privileged.  Of course, we don't know if the user being
+        # imported is privileged yet, as its group memberships show up
+        # later in the stream...
+        $importer->MergeValues($obj, $data);
+        $importer->SkipTransactions( $uid );
+
+        # Mark both the principal and the user object as resolved
+        $importer->Resolve(
+            $principal_uid,
+            ref($obj->PrincipalObj),
+            $obj->PrincipalObj->Id
+        );
+        $importer->Resolve( $uid => ref($obj) => $obj->Id );
+        return;
+    }
+
+    # Create a principal first, so we know what ID to use
+    my $principal = RT::Principal->new( RT->SystemUser );
+    my ($id) = $principal->Create(
+        PrincipalType => 'User',
+        Disabled => $disabled,
+        ObjectId => 0,
+    );
+
+    # Now we have a principal id, set the id for the user record
+    $data->{id} = $id;
+
+    $importer->Resolve( $principal_uid => ref($principal), $id );
+
+    $importer->Postpone(
+        for => $uid,
+        uid => $principal_uid,
+        column => "ObjectId",
+    );
+
+    return $class->SUPER::PreInflate( $importer, $uid, $data );
+}
+
+sub PostInflate {
+    my $self = shift;
+    RT->InitSystemObjects if $self->Name eq "RT_System";
+}
+
+RT::Base->_ImportOverlays();
+
 
-# }}}
 1;