diff --git a/include/class.user.php b/include/class.user.php
index 3a5fd1c29e205db10434a235315117b808de74c2..1bef5404b7eb68cec647d2e4864e296f85680b00 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -462,14 +462,22 @@ class User extends UserModel {
             $users[] = $data;
         }
 
+        db_autocommit(false);
+        $error = false;
         foreach ($users as $u) {
             $vars = array_combine($keys, $u);
-            if (!static::fromVars($vars))
-                return sprintf(__('Unable to import user: %s'),
+            if (!static::fromVars($vars)) {
+                $error = sprintf(__('Unable to import user: %s'),
                     print_r($vars, true));
+                break;
+            }
         }
+        if ($error)
+            db_rollback();
+
+        db_autocommit(true);
 
-        return count($users);
+        return $error ?: count($users);
     }
 
     function importFromPost($stuff, $extra=array()) {
@@ -937,6 +945,10 @@ class UserAccount extends UserAccountModel {
         return static::sendUnlockEmail('registration-client') === true;
     }
 
+    function setPassword($new) {
+        $this->set('passwd', Passwd::hash($new));
+    }
+
     protected function sendUnlockEmail($template) {
         global $ost, $cfg;
 
@@ -1021,7 +1033,7 @@ class UserAccount extends UserAccountModel {
         $this->set('username', $vars['username']);
 
         if ($vars['passwd1']) {
-            $this->set('passwd', Passwd::hash($vars['passwd1']));
+            $this->setPassword($vars['passwd1']);
             $this->setStatus(UserAccountStatus::CONFIRMED);
         }
 
diff --git a/include/mysqli.php b/include/mysqli.php
index ed70cd82ef3c5fc8ae0f209759ed9c969f2ceb61..1793f889e262edb157241497f415af96c71da652 100644
--- a/include/mysqli.php
+++ b/include/mysqli.php
@@ -90,6 +90,12 @@ function db_autocommit($enable=true) {
     return $__db->autocommit($enable);
 }
 
+function db_rollback() {
+    global $__db;
+
+    return $__db->rollback();
+}
+
 function db_close() {
     global $__db;
     return @$__db->close();
diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php
index a1647ce3cac8d2be9a57e29ab0fe60fe1eb82ce5..e01e507abc8a677ca2534895b18214dc2de0856a 100644
--- a/setup/cli/modules/class.module.php
+++ b/setup/cli/modules/class.module.php
@@ -39,8 +39,14 @@ class Option {
             $value = null;
         elseif ($value)
             $nargs = 1;
-        if ($this->type == 'int')
+        switch ($this->type) {
+        case 'int':
             $value = (int)$value;
+            break;
+        case 'bool':
+            $value = strcasecmp($value, 'true') === 0 || ((int) $value);
+            break;
+        }
         switch ($this->action) {
             case 'store_true':
                 $value = true;
@@ -156,7 +162,7 @@ class Module {
 
         if ($this->arguments) {
             echo "\nArguments:\n";
-            foreach ($this->arguments as $name=>$help)
+            foreach ($this->arguments as $name=>$help) {
                 $extra = '';
                 if (is_array($help)) {
                     if (isset($help['options']) && is_array($help['options'])) {
@@ -169,6 +175,7 @@ class Module {
                 echo $name . "\n    " . wordwrap(
                     preg_replace('/\s+/', ' ', $help), 76, "\n    ")
                         .$extra."\n";
+            }
         }
 
         if ($this->epilog) {
diff --git a/setup/cli/modules/cron.php b/setup/cli/modules/cron.php
new file mode 100644
index 0000000000000000000000000000000000000000..724c06caca04c3fd2e6870c6896052727a3e8431
--- /dev/null
+++ b/setup/cli/modules/cron.php
@@ -0,0 +1,34 @@
+<?php
+require_once dirname(__file__) . "/class.module.php";
+require_once dirname(__file__) . "/../cli.inc.php";
+
+class CronManager extends Module {
+    var $prologue = 'CLI cron manager for osTicket';
+
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'fetch' => 'Fetch email',
+                'search' => 'Build search index'
+            ),
+        ),
+    );
+
+    function run($args, $options) {
+        Bootstrap::connect();
+        $ost = osTicket::start();
+
+        switch (strtolower($args[0])) {
+        case 'fetch':
+            Cron::MailFetcher();
+            break;
+        case 'search':
+            $ost->searcher->backend->IndexOldStuff();
+            break;
+        }
+    }
+}
+
+Module::register('cron', 'CronManager');
+?>
diff --git a/setup/cli/modules/user.php b/setup/cli/modules/user.php
index 2d6d146a9950a95fda1bd4ad07c4b7a836148368..6fd05f7782eaa1dcf163cb3a3734df08e185ffe7 100644
--- a/setup/cli/modules/user.php
+++ b/setup/cli/modules/user.php
@@ -9,8 +9,12 @@ class UserManager extends Module {
         'action' => array(
             'help' => 'Action to be performed',
             'options' => array(
-                'import' => 'Import users to the system',
-                'export' => 'Export users from the system',
+                'import' => 'Import users from CSV file',
+                'export' => 'Export users from the system to CSV',
+                'activate' => 'Create or activate an account',
+                'lock' => "Lock a user's account",
+                'set-password' => "Set a user's account password",
+                'list' => 'List users based on search criteria',
             ),
         ),
     );
@@ -21,6 +25,26 @@ class UserManager extends Module {
             'help' => 'File or stream to process'),
         'org' => array('-O', '--org', 'metavar'=>'ORGID',
             'help' => 'Set the organization ID on import'),
+
+        'send-mail' => array('-m', '--send-mail',
+            'help' => 'Send the user an email. Used with `activate` and `set-password`',
+            'default' => false, 'action' => 'store_true'),
+
+        'verbose' => array('-v', '--verbose', 'default'=>false,
+            'action'=>'store_true', 'help' => 'Be more verbose'
+        ),
+
+        // -- Search criteria
+        'account' => array('-A', '--account', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on activation status'),
+        'isconfirmed' => array('-C', '--isconfirmed', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on confirmation status'),
+        'islocked' => array('-L', '--islocked', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on locked status'),
+        'email' => array('-E', '--email',
+            'help' => 'Search by email address'),
+        'id' => array('-U', '--id',
+            'help' => 'Search by user id'),
         );
 
     var $stream;
@@ -30,42 +54,164 @@ class UserManager extends Module {
         Bootstrap::connect();
 
         switch ($args['action']) {
-            case 'import':
-                // Properly detect Macintosh style line endings
-                ini_set('auto_detect_line_endings', true);
-
-                if (!$options['file'])
-                    $this->fail('CSV file to import users from is required!');
-                elseif (!($this->stream = fopen($options['file'], 'rb')))
-                    $this->fail("Unable to open input file [{$options['file']}]");
-
-                $extras = array();
-                if ($options['org']) {
-                    if (!($org = Organization::lookup($options['org'])))
-                        $this->fail($options['org'].': Unknown organization ID');
-                    $extras['org_id'] = $options['org'];
+        case 'import':
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+
+            if (!$options['file'])
+                $this->fail('CSV file to import users from is required!');
+            elseif (!($this->stream = fopen($options['file'], 'rb')))
+                $this->fail("Unable to open input file [{$options['file']}]");
+
+            $extras = array();
+            if ($options['org']) {
+                if (!($org = Organization::lookup($options['org'])))
+                    $this->fail($options['org'].': Unknown organization ID');
+                $extras['org_id'] = $options['org'];
+            }
+            $status = User::importCsv($this->stream, $extras);
+            if (is_numeric($status))
+                $this->stderr->write("Successfully imported $status clients\n");
+            else
+                $this->fail($status);
+            break;
+
+        case 'export':
+            $stream = $options['file'] ?: 'php://stdout';
+            if (!($this->stream = fopen($stream, 'c')))
+                $this->fail("Unable to open output file [{$options['file']}]");
+
+            fputcsv($this->stream, array('Name', 'Email'));
+            foreach (User::objects() as $user)
+                fputcsv($this->stream,
+                        array((string) $user->getName(), $user->getEmail()));
+            break;
+
+        case 'activate':
+            $users = $this->getQuerySet($options);
+            foreach ($users as $U) {
+                if ($options['verbose']) {
+                    $this->stderr->write(sprintf(
+                        "Activating %s <%s>\n",
+                        $U->getName(), $U->getDefaultEmail()
+                    ));
                 }
-                $status = User::importCsv($this->stream, $extras);
-                if (is_numeric($status))
-                    $this->stderr->write("Successfully imported $status clients\n");
-                else
-                    $this->fail($status);
+                if (!($account = $U->getAccount())) {
+                    $account = UserAccount::create(array('user' => $U));
+                    $U->account = $account;
+                    $U->save();
+                }
+
+                if ($options['send-mail']) {
+                    global $ost, $cfg;
+                    $ost = osTicket::start();
+                    $cfg = $ost->getConfig();
+
+                    if (($error = $account->sendConfirmEmail()) && $error !== true) {
+                        $this->warn(sprintf('%s: Unable to send email: %s',
+                            $U->getDefaultEmail(), $error->getMessage()
+                        ));
+                    }
+                }
+            }
+
+            break;
+
+        case 'lock':
+            $users = $this->getQuerySet($options);
+            $users->select_related('account');
+            foreach ($users as $U) {
+                if (!($account = $U->getAccount())) {
+                    $this->warn(sprintf(
+                        '%s: User does not have a client account',
+                        $U->getName()
+                    ));
+                }
+                $account->setFlag(UserAccountStatus::LOCKED);
+                $account->save();
+            }
+
+            break;
+
+        case 'list':
+            $users = $this->getQuerySet($options);
+
+            foreach ($users as $U) {
+                $this->stdout->write(sprintf(
+                    "%d %s <%s>%s\n",
+                    $U->id, $U->getName(), $U->getDefaultEmail(),
+                    ($O = $U->getOrganization()) ? " ({$O->getName()})" : ''
+                ));
+            }
+
+            break;
+
+        case 'set-password':
+            $this->stderr->write('Enter new password: ');
+            $ps1 = fgets(STDIN);
+            if (!function_exists('posix_isatty') || !posix_isatty(STDIN)) {
+                $this->stderr->write('Re-enter new password: ');
+                $ps2 = fgets(STDIN);
+
+                if ($ps1 != $ps2)
+                    $this->fail('Passwords do not match');
+            }
+
+            // Account is required
+            $options['account'] = true;
+            $users = $this->getQuerySet($options);
+
+            $updated  = 0;
+            foreach ($users as $U) {
+                $U->account->setPassword($ps1);
+                if ($U->account->save())
+                    $updated++;
+            }
+            $this->stdout->write(sprintf('Updated %d users', $updated));
+            break;
+
+        default:
+            $this->stderr->write('Unknown action!');
+        }
+        @fclose($this->stream);
+    }
+
+    function getQuerySet($options, $requireOne=false) {
+        $users = User::objects();
+        foreach ($options as $O=>$V) {
+            if (!isset($V))
+                continue;
+            switch ($O) {
+            case 'account':
+                $users->filter(array('account__isnull' => !$V));
                 break;
 
-            case 'export':
-                $stream = $options['file'] ?: 'php://stdout';
-                if (!($this->stream = fopen($stream, 'c')))
-                    $this->fail("Unable to open output file [{$options['file']}]");
+            case 'isconfirmed':
+            case 'islocked':
+                $flags = array(
+                    'isconfirmed' => UserAccountStatus::CONFIRMED,
+                    'islocked' => UserAccountStatus::LOCKED,
+                );
+                $Q = new Q(array('account__status__hasbit'=>$flags[$O]));
+                if (!$V)
+                    $Q->negate();
+                $users->filter($Q);
+                break;
 
-                fputcsv($this->stream, array('Name', 'Email'));
-                foreach (User::objects() as $user)
-                    fputcsv($this->stream,
-                            array((string) $user->getName(), $user->getEmail()));
+            case 'org':
+                if (is_numeric($V))
+                    $users->filter(array('org__id'=>$V));
+                else
+                    $users->filter(array('org__name__contains'=>$V));
                 break;
-            default:
-                $this->stderr->write('Unknown action!');
+
+            case 'id':
+                $users->filter(array('id'=>$V));
+                break;
+            }
+
         }
-        @fclose($this->stream);
+        return $users;
     }
 }
 Module::register('user', 'UserManager');