diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6
new file mode 100644
index 00000000000..6628a6fa726
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6
@@ -0,0 +1,34 @@
+import computed from 'ember-addons/ember-computed-decorators';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+
+export default Ember.Controller.extend({
+  users: null,
+  groupId: null,
+  saving: false,
+
+  @computed('saving', 'users', 'groupId')
+  buttonDisabled(saving, users, groupId) {
+    return saving || !groupId || !users || !users.length;
+  },
+
+  actions: {
+    addToGroup() {
+      if (this.get('saving')) { return; }
+
+      const users = this.get('users').split("\n")
+                                      .uniq()
+                                      .reject(x => x.length === 0);
+
+      this.set('saving', true);
+      Discourse.ajax('/admin/groups/bulk', {
+        data: { users, group_id: this.get('groupId') },
+        method: 'PUT'
+      }).then(() => {
+        this.transitionToRoute('adminGroups.bulkComplete');
+      }).catch(popupAjaxError).finally(() => {
+        this.set('saving', false);
+      });
+
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6
new file mode 100644
index 00000000000..8d9554556f8
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6
@@ -0,0 +1,13 @@
+import Group from 'discourse/models/group';
+
+export default Ember.Route.extend({
+  model() {
+    return Group.findAll().then(groups => {
+      return groups.filter(g => !g.get('automatic'));
+    });
+  },
+
+  setupController(controller, groups) {
+    controller.setProperties({ groups, groupId: null, users: null });
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
index e01d0f8f0d6..dd758556c60 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -49,6 +49,8 @@ export default {
     });
 
     this.resource('adminGroups', { path: '/groups' }, function() {
+      this.route('bulk');
+      this.route('bulkComplete', { path: 'bulk-complete' });
       this.resource('adminGroupsType', { path: '/:type' }, function() {
         this.resource('adminGroup', { path: '/:name' });
       });
diff --git a/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs b/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs
new file mode 100644
index 00000000000..51eb3e4394b
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs
@@ -0,0 +1 @@
+<p>{{i18n "admin.groups.bulk_complete"}}</p>
diff --git a/app/assets/javascripts/admin/templates/groups-bulk.hbs b/app/assets/javascripts/admin/templates/groups-bulk.hbs
new file mode 100644
index 00000000000..baf3a63cda0
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/groups-bulk.hbs
@@ -0,0 +1,19 @@
+<div class='groups-bulk'>
+  <p>{{i18n "admin.groups.bulk_paste"}}</p>
+
+  <div class='control'>
+    {{textarea value=users class="paste-users"}}
+  </div>
+
+  <div class='control'>
+    {{combo-box content=groups valueAttribute="id" value=groupId none="admin.groups.bulk_select"}}
+  </div>
+
+  <div class='control'>
+    {{d-button disabled=buttonDisabled
+               class="btn-primary"
+               action="addToGroup"
+               icon="plus"
+               label="admin.groups.bulk"}}
+  </div>
+</div>
diff --git a/app/assets/javascripts/admin/templates/groups.hbs b/app/assets/javascripts/admin/templates/groups.hbs
index 2d767c38442..aa7d9213ca8 100644
--- a/app/assets/javascripts/admin/templates/groups.hbs
+++ b/app/assets/javascripts/admin/templates/groups.hbs
@@ -1,6 +1,7 @@
 {{#admin-nav}}
   {{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
   {{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
+  {{nav-item route='adminGroups.bulk' label='admin.groups.bulk'}}
 {{/admin-nav}}
 
 <div class="admin-container">
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index 60396122e88..63c01ffed80 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -215,6 +215,11 @@ td.flaggers td {
   }
 }
 
+.paste-users {
+  width: 400px;
+  height: 150px;
+}
+
 .groups, .badges {
   .form-horizontal {
     label {
@@ -1015,6 +1020,11 @@ table.api-keys {
   }
 }
 
+.groups-bulk {
+  .control {
+    margin-bottom: 1em;
+  }
+}
 
 .commits-widget {
   border: solid 1px dark-light-diff($primary, $secondary, 90%, -60%);
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 767657f1e18..67e8d86966d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -19,6 +19,42 @@ class Admin::GroupsController < Admin::AdminController
     render nothing: true
   end
 
+  def bulk
+    render nothing: true
+  end
+
+  def bulk_perform
+    group = Group.find(params[:group_id].to_i)
+    if group.present?
+      users = (params[:users] || []).map {|u| u.downcase}
+      user_ids = User.where("username_lower in (:users) OR email IN (:users)", users: users).pluck(:id)
+
+      if user_ids.present?
+        Group.exec_sql("INSERT INTO group_users
+                                    (group_id, user_id, created_at, updated_at)
+                       SELECT #{group.id},
+                              u.id,
+                              CURRENT_TIMESTAMP,
+                              CURRENT_TIMESTAMP
+                       FROM users AS u
+                       WHERE u.id IN (#{user_ids.join(', ')})
+                         AND NOT EXISTS(SELECT 1 FROM group_users AS gu
+                                        WHERE gu.user_id = u.id AND
+                                              gu.group_id = #{group.id})")
+
+        if group.primary_group?
+          User.where(id: user_ids).update_all(primary_group_id: group.id)
+        end
+
+        if group.title.present?
+          User.where(id: user_ids).update_all(title: group.title)
+        end
+      end
+    end
+
+    render json: success_json
+  end
+
   def create
     group = Group.new
 
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index af5d2e6e09a..6eff38ce549 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1949,6 +1949,10 @@ en:
         add: "Add"
         add_members: "Add members"
         custom: "Custom"
+        bulk_complete: "The users have been added to the group."
+        bulk: "Bulk Add to Group"
+        bulk_paste: "Paste a list of usernames or emails, one per line:"
+        bulk_select: "(select a group)"
         automatic: "Automatic"
         automatic_membership_email_domains: "Users who register with an email domain that exactly matches one in this list will be automatically added to this group:"
         automatic_membership_retroactive: "Apply the same email domain rule to add existing registered users"
diff --git a/config/routes.rb b/config/routes.rb
index cc55476b0ab..0af5799a22d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -60,6 +60,9 @@ Discourse::Application.routes.draw do
     resources :groups, constraints: AdminConstraint.new do
       collection do
         post "refresh_automatic_groups" => "groups#refresh_automatic_groups"
+        get 'bulk'
+        get 'bulk-complete' => 'groups#bulk'
+        put 'bulk' => 'groups#bulk_perform'
       end
       member do
         put "members" => "groups#add_members"
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 75bc9ddac12..e7c2233ec14 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -36,6 +36,26 @@ describe Admin::GroupsController do
 
   end
 
+  context ".bulk" do
+    it "can assign users to a group by email or username" do
+      group = Fabricate(:group, name: "test", primary_group: true, title: 'WAT')
+      user = Fabricate(:user)
+      user2 = Fabricate(:user)
+
+      xhr :put, :bulk_perform, group_id: group.id, users: [user.username.upcase, user2.email, 'doesnt_exist']
+
+      expect(response).to be_success
+
+      user.reload
+      expect(user.primary_group).to eq(group)
+      expect(user.title).to eq("WAT")
+
+      user2.reload
+      expect(user2.primary_group).to eq(group)
+
+    end
+  end
+
   context ".create" do
 
     it "strip spaces on the group name" do