diff --git a/settings/js/users/deleteHandler.js b/settings/js/users/deleteHandler.js
index d4736d88701f8fa6ac4455ed656820c9720290ae..c89a844044e69d78507eb70bf8fdcc895959575b 100644
--- a/settings/js/users/deleteHandler.js
+++ b/settings/js/users/deleteHandler.js
@@ -34,6 +34,16 @@ function DeleteHandler(endpoint, paramID, markCallback, removeCallback) {
 	this.notificationPlaceholder = '%oid';
 }
 
+/**
+ * Number of milliseconds after which the operation is performed.
+ */
+DeleteHandler.TIMEOUT_MS = 7000;
+
+/**
+ * Timer after which the action will be performed anyway.
+ */
+DeleteHandler.prototype._timeout = null;
+
 /**
  * The function to be called after successfully marking the object for deletion
  * @callback markCallback
@@ -72,7 +82,9 @@ DeleteHandler.prototype.setNotification = function(notifier, dataID, message, un
 
 	var dh = this;
 
-	$('#notification').on('click', '.undo', function () {
+	$('#notification')
+		.off('click.deleteHandler_' + dataID)
+		.on('click.deleteHandler_' + dataID, '.undo', function () {
 		if ($('#notification').data(dh.notificationDataID)) {
 			var oid = dh.oidToDelete;
 			dh.cancel();
@@ -116,18 +128,36 @@ DeleteHandler.prototype.hideNotification = function() {
  */
 DeleteHandler.prototype.mark = function(oid) {
 	if(this.oidToDelete !== false) {
-		this.deleteEntry();
+		// passing true to avoid hiding the notification
+		// twice and causing the second notification
+		// to disappear immediately
+		this.deleteEntry(true);
 	}
 	this.oidToDelete = oid;
 	this.canceled = false;
 	this.markCallback(oid);
 	this.showNotification();
+	if (this._timeout) {
+		clearTimeout(this._timeout);
+		this._timeout = null;
+	}
+	if (DeleteHandler.TIMEOUT_MS > 0) {
+		this._timeout = window.setTimeout(
+				_.bind(this.deleteEntry, this),
+			   	DeleteHandler.TIMEOUT_MS
+		);
+	}
 };
 
 /**
  * cancels a delete operation
  */
 DeleteHandler.prototype.cancel = function() {
+	if (this._timeout) {
+		clearTimeout(this._timeout);
+		this._timeout = null;
+	}
+
 	this.canceled = true;
 	this.oidToDelete = false;
 };
@@ -137,22 +167,31 @@ DeleteHandler.prototype.cancel = function() {
  * initialized by mark(). On error, it will show a message via
  * OC.dialogs.alert. On success, a callback is fired so that the client can
  * update the web interface accordingly.
+ *
+ * @param {boolean} [keepNotification] true to keep the notification, false to hide
+ * it, defaults to false
  */
-DeleteHandler.prototype.deleteEntry = function() {
+DeleteHandler.prototype.deleteEntry = function(keepNotification) {
 	if(this.canceled || this.oidToDelete === false) {
 		return false;
 	}
 
 	var dh = this;
-	if($('#notification').data(this.notificationDataID) === true) {
+	if(!keepNotification && $('#notification').data(this.notificationDataID) === true) {
 		dh.hideNotification();
 	}
 
+	if (this._timeout) {
+		clearTimeout(this._timeout);
+		this._timeout = null;
+	}
+
 	var payload = {};
 	payload[dh.ajaxParamID] = dh.oidToDelete;
 	$.ajax({
 		type: 'POST',
 		url: OC.filePath('settings', 'ajax', dh.ajaxEndpoint),
+		// FIXME: do not use synchronous ajax calls as they block the browser !
 		async: false,
 		data: payload,
 		success: function (result) {
diff --git a/settings/tests/js/users/deleteHandlerSpec.js b/settings/tests/js/users/deleteHandlerSpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b6328be801196c48590ef30c8b551faac3cb6ab
--- /dev/null
+++ b/settings/tests/js/users/deleteHandlerSpec.js
@@ -0,0 +1,185 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+describe('DeleteHandler tests', function() {
+	var showNotificationSpy;
+	var hideNotificationSpy;
+	var clock;
+	var removeCallback;
+	var markCallback;
+	var undoCallback;
+
+	function init(markCallback, removeCallback, undoCallback) {
+		var handler = new DeleteHandler('dummyendpoint.php', 'paramid', markCallback, removeCallback);
+		handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry', undoCallback);
+		return handler;
+	}
+
+	beforeEach(function() {
+		showNotificationSpy = sinon.spy(OC.Notification, 'showHtml');
+		hideNotificationSpy = sinon.spy(OC.Notification, 'hide');
+		clock = sinon.useFakeTimers();
+		removeCallback = sinon.stub();
+		markCallback = sinon.stub();
+		undoCallback = sinon.stub();
+
+		$('#testArea').append('<div id="notification"></div>');
+	});
+	afterEach(function() {
+		showNotificationSpy.restore();
+		hideNotificationSpy.restore();
+		clock.restore();
+	});
+	it('shows a notification when marking for delete', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+
+		expect(showNotificationSpy.calledOnce).toEqual(true);
+		expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry');
+
+		expect(markCallback.calledOnce).toEqual(true);
+		expect(markCallback.getCall(0).args[0]).toEqual('some_uid');
+		expect(removeCallback.notCalled).toEqual(true);
+		expect(undoCallback.notCalled).toEqual(true);
+
+		expect(fakeServer.requests.length).toEqual(0);
+	});
+	it('deletes first entry and reshows notification on second delete', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+
+		expect(showNotificationSpy.calledOnce).toEqual(true);
+		expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry');
+		showNotificationSpy.reset();
+
+		handler.mark('some_other_uid');
+
+		expect(hideNotificationSpy.calledOnce).toEqual(true);
+		expect(showNotificationSpy.calledOnce).toEqual(true);
+		expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_other_uid entry');
+
+		expect(markCallback.calledTwice).toEqual(true);
+		expect(markCallback.getCall(0).args[0]).toEqual('some_uid');
+		expect(markCallback.getCall(1).args[0]).toEqual('some_other_uid');
+		expect(removeCallback.notCalled).toEqual(true);
+		expect(undoCallback.notCalled).toEqual(true);
+
+		// previous one was delete
+		expect(fakeServer.requests.length).toEqual(1);
+		var	request = fakeServer.requests[0];
+		expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
+	});
+	it('automatically deletes after timeout', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+
+		clock.tick(5000);
+		// nothing happens yet
+		expect(fakeServer.requests.length).toEqual(0);
+
+		clock.tick(3000);
+		expect(fakeServer.requests.length).toEqual(1);
+		var	request = fakeServer.requests[0];
+		expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
+	});
+	it('deletes when deleteEntry is called', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+
+		handler.deleteEntry();
+		expect(fakeServer.requests.length).toEqual(1);
+		var	request = fakeServer.requests[0];
+		expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
+	});
+	it('cancels deletion when undo is clicked', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback);
+		handler.mark('some_uid');
+		$('#notification .undo').click();
+
+		expect(undoCallback.calledOnce).toEqual(true);
+
+		// timer was cancelled
+		clock.tick(10000);
+		expect(fakeServer.requests.length).toEqual(0);
+	});
+	it('cancels deletion when cancel method is called', function() {
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback);
+		handler.mark('some_uid');
+		handler.cancel();
+
+		// not sure why, seems to be by design
+		expect(undoCallback.notCalled).toEqual(true);
+
+		// timer was cancelled
+		clock.tick(10000);
+		expect(fakeServer.requests.length).toEqual(0);
+	});
+	it('calls removeCallback after successful server side deletion', function() {
+		fakeServer.respondWith(/\/index\.php\/settings\/ajax\/dummyendpoint.php/, [
+			200,
+			{ 'Content-Type': 'application/json' },
+			JSON.stringify({status: 'success'})
+		]);
+
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+		handler.deleteEntry();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		var request = fakeServer.requests[0];
+		var query = OC.parseQueryString(request.requestBody);
+		expect(query.paramid).toEqual('some_uid');
+
+		expect(removeCallback.calledOnce).toEqual(true);
+		expect(undoCallback.notCalled).toEqual(true);
+		expect(removeCallback.getCall(0).args[0]).toEqual('some_uid');
+	});
+	it('calls undoCallback and shows alert after failed server side deletion', function() {
+		// stub t to avoid extra calls
+		var tStub = sinon.stub(window, 't').returns('text');
+		fakeServer.respondWith(/\/index\.php\/settings\/ajax\/dummyendpoint.php/, [
+			200,
+			{ 'Content-Type': 'application/json' },
+			JSON.stringify({status: 'error', data: {message: 'test error'}})
+		]);
+
+		var alertDialogStub = sinon.stub(OC.dialogs, 'alert');
+		var handler = init(markCallback, removeCallback, undoCallback);
+		handler.mark('some_uid');
+		handler.deleteEntry();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		var request = fakeServer.requests[0];
+		var query = OC.parseQueryString(request.requestBody);
+		expect(query.paramid).toEqual('some_uid');
+
+		expect(removeCallback.notCalled).toEqual(true);
+		expect(undoCallback.calledOnce).toEqual(true);
+		expect(undoCallback.getCall(0).args[0]).toEqual('some_uid');
+
+		expect(alertDialogStub.calledOnce);
+
+		alertDialogStub.restore();
+		tStub.restore();
+	});
+});
diff --git a/tests/karma.config.js b/tests/karma.config.js
index 14a0d7e8464fd5b4683332e8b6e6c7ec1297bbf2..357fcf3f122fac912f8d9a370d0530c9f3d8a562 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -66,7 +66,15 @@ module.exports = function(config) {
 					'apps/files_external/js/mountsfilelist.js'
 				],
 				testFiles: ['apps/files_external/tests/js/*.js']
-			}];
+			},
+			{
+				name: 'settings',
+				srcFiles: [
+					'settings/js/users/deleteHandler.js'
+				],
+				testFiles: ['settings/tests/js/users/deleteHandlerSpec.js']
+			}
+		];
 	}
 
 	// respect NOCOVERAGE env variable