diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index 9a72c20449470d3c8ce8fa916132e063374fd862..baa7a6351d1a07fe04accbd57c28dfc4e950d8aa 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -169,6 +169,32 @@ class OrgsAjaxAPI extends AjaxController {
         return $resp;
     }
 
+    function importUsers($org_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login Required');
+        elseif (!($org = Organization::lookup($org_id)))
+            Http::response(404, 'No such organization');
+
+        $info = array(
+            'title' => 'Import Users',
+            'action' => "#orgs/$org_id/import-users",
+            'upload_url' => "orgs.php?a=import-users",
+        );
+
+        if ($_POST) {
+            $status = User::importFromPost($_POST['pasted']);
+            if (is_string($status))
+                $info['error'] = $status;
+            else
+                Http::response(201, "{\"count\": $status}");
+        }
+        $info += Format::input($_POST);
+
+        include STAFFINC_DIR . 'templates/user-import.tmpl.php';
+    }
+
     function addOrg() {
 
         $info = array();
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 2193abf85b952d82682095be6709e38b49bbf45c..bad2bb7fe65af4c1bf022f5abdef858d77173f1d 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -248,6 +248,30 @@ class UsersAjaxAPI extends AjaxController {
         include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
     }
 
+    function importUsers() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login Required');
+
+        $info = array(
+            'title' => 'Import Users',
+            'action' => '#users/import',
+            'upload_url' => "users.php?do=import-users",
+        );
+
+        if ($_POST) {
+            $status = User::importFromPost($_POST['pasted']);
+            if (is_string($status))
+                $info['error'] = $status;
+            else
+                Http::response(201, "{\"count\": $status}");
+        }
+        $info += Format::input($_POST);
+
+        include STAFFINC_DIR . 'templates/user-import.tmpl.php';
+    }
+
     function getLookupForm() {
         return self::_lookupform();
     }
diff --git a/include/class.user.php b/include/class.user.php
index e8ed94158018501dc1e19e8cb19cea06ef551ec3..ad599b5d07d817fa5b094edfd1561260b144afcd 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -317,6 +317,128 @@ class User extends UserModel {
         return UserAccount::register($this, $vars, $errors);
     }
 
+    static function importCsv($stream, $defaults=array()) {
+        //Read the header (if any)
+        $headers = array('name' => 'Full Name', 'email' => 'Email Address');
+        $uform = UserForm::getUserForm();
+        $all_fields = $uform->getFields();
+        $named_fields = array();
+        $has_header = true;
+        foreach ($all_fields as $f)
+            if ($f->get('name'))
+                $named_fields[] = $f;
+
+        if (($data = fgetcsv($stream, 1000, ","))) {
+            if (Validator::is_email($data[1])) {
+                $has_header = false; // We don't have an header!
+            }
+            else {
+                $headers = array();
+                foreach ($data as $h) {
+                    $found = false;
+                    foreach ($all_fields as $f) {
+                        if (in_array(mb_strtolower($h), array(
+                                mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
+                            $found = true;
+                            if (!$f->get('name'))
+                                return $h.': Field must have `variable` set to be imported';
+                            $headers[$f->get('name')] = $f->get('label');
+                            break;
+                        }
+                    }
+                    if (!$found) {
+                        $has_header = false;
+                        if (count($data) == count($named_fields)) {
+                            // Number of fields in the user form matches the number
+                            // of fields in the data. Assume things line up
+                            $headers = array();
+                            foreach ($named_fields as $f)
+                                $headers[$f->get('name')] = $f->get('label');
+                            break;
+                        }
+                        else {
+                            return $h.': Unable to map header to a user field';
+                        }
+                    }
+                }
+            }
+        }
+        else
+            return 'Whoops. Perhaps you meant to send some CSV records';
+
+        // 'name' and 'email' MUST be in the headers
+        if (!isset($headers['name']) || !isset($headers['email']))
+            return 'CSV file must include `name` and `email` columns';
+
+        if (!$has_header)
+            fseek($stream, 0);
+
+        $users = $fields = $keys = array();
+        foreach ($headers as $h => $label) {
+            if (!($f = $uform->getField($h)))
+                continue;
+
+            $name = $keys[] = $f->get('name');
+            $fields[$name] = $f->getImpl();
+        }
+
+        // Add default fields (org_id, etc).
+        foreach ($defaults as $key => $val) {
+            // Don't apply defaults which are also being imported
+            if (isset($header[$key]))
+                unset($defaults[$key]);
+            $keys[] = $key;
+        }
+
+        while (($data = fgetcsv($stream, 1000, ",")) !== false) {
+            if (!count($data))
+                // Skip empty rows
+                continue;
+            elseif (count($data) != count($headers))
+                return 'Bad data. Expected: '.implode(', ', $headers);
+            // Validate according to field configuration
+            $i = 0;
+            foreach ($headers as $h => $label) {
+                $f = $fields[$h];
+                $T = $f->parse($data[$i]);
+                if ($f->validateEntry($T) && $f->errors())
+                    return $label.': Invalid data: '.implode(', ', $f->errors());
+                // Convert to database format
+                $data[$i] = $f->to_database($T);
+                $i++;
+            }
+            // Add default fields
+            foreach ($defaults as $key => $val)
+                $data[] = $val;
+
+            $users[] = $data;
+        }
+
+        foreach ($users as $u) {
+            $vars = array_combine($keys, $u);
+            if (!static::fromVars($vars))
+                return 'Unable to import user: '.print_r($vars, true);
+        }
+
+        return count($users);
+    }
+
+    function importFromPost($stuff, $extra=array()) {
+        if (is_array($stuff) && !$stuff['error']) {
+            $stream = fopen($stuff['tmp_name'], 'r');
+        }
+        elseif ($stuff) {
+            $stream = fopen('php://temp', 'w+');
+            fwrite($stream, $stuff);
+            rewind($stream);
+        }
+        else {
+            return 'Unable to parse submitted users';
+        }
+
+        return User::importCsv($stream, $extra);
+    }
+
     function updateInfo($vars, &$errors) {
 
         $valid = true;
diff --git a/include/staff/templates/user-import.tmpl.php b/include/staff/templates/user-import.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..581f1cb48b3c6e191d14c99df80b7995deb16b95
--- /dev/null
+++ b/include/staff/templates/user-import.tmpl.php
@@ -0,0 +1,85 @@
+<div id="the-lookup-form">
+<h3><?php echo $info['title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} ?>
+<ul class="tabs">
+    <li><a href="#copy-paste" class="active"
+        ><i class="icon-edit"></i>&nbsp;Copy Paste</a></li>
+    <li><a href="#upload"
+        ><i class="icon-fixed-width icon-cloud-upload"></i>&nbsp;Upload</a></li>
+</ul>
+<form action="<?php echo $info['action']; ?>" method="post" enctype="multipart/form-data"
+    onsubmit="javascript:
+    if ($(this).find('[name=import]').val()) {
+        $(this).attr('action', '<?php echo $info['upload_url']; ?>');
+        $(document).unbind('submit.dialog');
+    }">
+<?php echo csrf_token();
+if ($org_id) { ?>
+    <input type="hidden" name="id" value="<?php echo $org_id; ?>"/>
+<?php } ?>
+
+<div class="tab_content" id="copy-paste" style="margin:5px;">
+<h2 style="margin-bottom:10px">Name and Email</h2>
+<p>
+Enter one name and email address per line.<br/>
+<em>To import more other fields, use the Upload tab.</em>
+</p>
+<textarea name="pasted" style="display:block;width:100%;height:8em"
+    placeholder="e.g. John Doe, john.doe@osticket.com">
+<?php echo $info['pasted']; ?>
+</textarea>
+</div>
+
+<div class="tab_content" id="upload" style="display:none;margin:5px;">
+<h2 style="margin-bottom:10px">Import a CSV File</h2>
+<p>
+<em>Use the columns shown in the table below. To add more fields, visit the
+Admin Panel -&gt; Manage -&gt; Forms -&gt; <?php echo
+UserForm::getUserForm()->get('title'); ?> page to edit the available fields.
+Only fields with `variable` defined can be imported.</em>
+</p>
+<table class="list"><tr>
+<?php
+    $fields = array();
+    $data = array(
+        array('name' => 'John Doe', 'email' => 'john.doe@osticket.com')
+    );
+    foreach (UserForm::getUserForm()->getFields() as $f)
+        if ($f->get('name'))
+            $fields[] = $f->get('name');
+    foreach ($fields as $f) { ?>
+            <th><?php echo mb_convert_case($f, MB_CASE_TITLE); ?></th>
+<?php } ?>
+</tr>
+<?php
+    foreach ($data as $d) {
+        foreach ($fields as $f) {
+            ?><td><?php
+            if (isset($d[$f])) echo $d[$f];
+            ?></td><?php
+        }
+    } ?>
+</tr></table>
+<br/>
+<input type="file" name="import"/>
+</div>
+    <hr>
+    <p class="full-width">
+        <span class="buttons" style="float:left">
+            <input type="reset" value="Reset">
+            <input type="button" name="cancel" class="close"  value="Cancel">
+        </span>
+        <span class="buttons" style="float:right">
+            <input type="submit" value="Import Users">
+        </span>
+     </p>
+</form>
diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php
index f0be96907b6a7d52333c989a274ed86073cf573e..2e864633b561db33fca6262feccf28e6dc9ea8be 100644
--- a/include/staff/templates/users.tmpl.php
+++ b/include/staff/templates/users.tmpl.php
@@ -55,7 +55,11 @@ else
 ?>
 <div style="width:700px; float:left;"><b><?php echo $showing; ?></b></div>
 <div style="float:right;text-align:right;padding-right:5px;">
-    <b><a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="Icon newstaff add-user">Add New User</a></b></div>
+    <b><a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="Icon newstaff add-user">Add User</a></b>
+    |
+    <b><a href="#orgs/<?php echo $org->getId(); ?>/import-users" class="add-user">
+    <i class="icon-cloud-upload icon-large"></i> Import</a></b>
+</div>
 <div class="clear"></div>
 <br/>
 <?php
@@ -117,10 +121,12 @@ endif;
 $(function() {
     $(document).on('click', 'a.add-user', function(e) {
         e.preventDefault();
-        $.userLookup('ajax.php/orgs/<?php echo $org->getId(); ?>/add-user', function (user) {
-            window.location.href = 'orgs.php?id=<?php echo $org->getId(); ?>'
-         });
-
+        $.userLookup('ajax.php/' + $(this).attr('href').substr(1), function (user) {
+            if (user && user.id)
+                window.location.href = 'orgs.php?id=<?php echo $org->getId(); ?>'
+            else
+              $.pjax({url: window.location.href, container: '#content'})
+        });
         return false;
      });
 });
diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php
index 0439698f0863368fea95201d82cc424b9948a9a0..97e5862fdceff9ed155cfd43f0727c0168cb2c93 100644
--- a/include/staff/users.inc.php
+++ b/include/staff/users.inc.php
@@ -83,7 +83,10 @@ $query="$select $from $where GROUP BY user.id ORDER BY $order_by LIMIT ".$pageNa
     </form>
  </div>
  <div style="float:right;text-align:right;padding-right:5px;">
-    <b><a href="#users/add" class="Icon newstaff add-user">Add New User</a></b></div>
+    <b><a href="#users/add" class="Icon newstaff popup-dialog">Add User</a></b>
+    |
+    <b><a href="#users/import" class="popup-dialog"><i class="icon-cloud-upload icon-large"></i> Import</a></b>
+</div>
 <div class="clear"></div>
 <?php
 $showing = $search ? 'Search Results: ' : '';
@@ -167,10 +170,13 @@ $(function() {
         property: "/bin/true"
     });
 
-    $(document).on('click', 'a.add-user', function(e) {
+    $(document).on('click', 'a.popup-dialog', function(e) {
         e.preventDefault();
-        $.userLookup('ajax.php/users/add', function (user) {
-            window.location.href = 'users.php?id='+user.id;
+        $.userLookup('ajax.php/' + $(this).attr('href').substr(1), function (user) {
+            if (user && user.id)
+                window.location.href = 'users.php?id='+user.id;
+            else
+              $.pjax({url: window.location.href, container: '#content'})
          });
 
         return false;
diff --git a/scp/ajax.php b/scp/ajax.php
index d0830bdf84cf81116154d9caf9f29fd0b1272d28..fada24ab53278dfda7acf32ab6c93c4afa59769d 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -78,6 +78,7 @@ $dispatcher = patterns('',
         url_get('^/lookup/form$', 'getLookupForm'),
         url_post('^/lookup/form$', 'addUser'),
         url_get('^/add$', 'addUser'),
+        url('^/import$', 'importUsers'),
         url_get('^/select$', 'selectUser'),
         url_get('^/select/(?P<id>\d+)$', 'selectUser'),
         url_get('^/select/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteUser'),
@@ -107,6 +108,7 @@ $dispatcher = patterns('',
         url_get('^/(?P<id>\d+)/add-user(?:/(?P<userid>\d+))?$', 'addUser'),
         url_get('^/(?P<id>\d+)/add-user(?:/auth:(?P<userid>.+))?$', 'addUser', array(true)),
         url_post('^/(?P<id>\d+)/add-user$', 'addUser'),
+        url('^/(?P<id>\d+)/import-users$', 'importUsers'),
         url_get('^/(?P<id>\d+)/delete$', 'delete'),
         url_delete('^/(?P<id>\d+)/delete$', 'delete')
     )),
diff --git a/scp/orgs.php b/scp/orgs.php
index 0ecb8d1892cbed3f27610139a5a6fb0d4e3a53af..bfdc267aeacd56772f5226b49ec9f4c57e543e71 100644
--- a/scp/orgs.php
+++ b/scp/orgs.php
@@ -14,9 +14,24 @@
 **********************************************************************/
 require('staff.inc.php');
 $org = null;
-if ($_REQUEST['id'])
-    $org = Organization::lookup($_REQUEST['id']);
+if ($_REQUEST['id'] || $_REQUEST['org_id'])
+    $org = Organization::lookup($_REQUEST['org_id'] ?: $_REQUEST['id']);
 
+if ($_POST) {
+    switch ($_REQUEST['a']) {
+    case 'import-users':
+        if (!$org) {
+            $errors['err'] = 'Organization ID must be specified for import';
+            break;
+        }
+        $status = User::importFromPost($_FILES['import'] ?: $_POST['pasted'],
+            array('org_id'=>$org->getId()));
+        if (is_numeric($status))
+            $msg = "Successfully imported $status clients";
+        else
+            $errors['err'] = $status;
+    }
+}
 
 $page = $org? 'org-view.inc.php' : 'orgs.inc.php';
 $nav->setTabActive('users');
diff --git a/scp/users.php b/scp/users.php
index 3020a91f19fceb60517d817382d28a1cdfb1e2e2..547d278ac52e5b3d91e7edd06c6602805d25b725 100644
--- a/scp/users.php
+++ b/scp/users.php
@@ -18,7 +18,7 @@ if ($_REQUEST['id'] && !($user=User::lookup($_REQUEST['id'])))
     $errors['err'] = 'Unknown or invalid user ID.';
 
 if ($_POST) {
-    switch(strtolower($_POST['do'])) {
+    switch(strtolower($_REQUEST['do'])) {
         case 'update':
             if (!$user) {
                 $errors['err']='Unknown or invalid user.';
@@ -66,6 +66,13 @@ if ($_POST) {
                 $errors['err'] = "Coming soon!";
             }
             break;
+        case 'import-users':
+            $status = User::importFromPost($_FILES['import'] ?: $_POST['pasted']);
+            if (is_numeric($status))
+                $msg = "Successfully imported $status clients";
+            else
+                $errors['err'] = $status;
+            break;
         default:
             $errors['err'] = 'Unknown action/command';
             break;