From a7ec6f9158820233fab9d1044682b7ede1c24b39 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 2 Oct 2017 16:30:27 +0300 Subject: [PATCH] allow closing all active imap sessions of an user --- docs/api.md | 29 +++++++++++- imap-core/lib/commands/authenticate-plain.js | 3 +- imap-core/lib/commands/login.js | 3 +- imap-core/lib/imap-connection.js | 17 ++++++++ lib/api/users.js | 46 ++++++++++++++++++++ lib/user-handler.js | 30 +++++++++++++ 6 files changed, 125 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index dda20659..f5294f10 100644 --- a/docs/api.md +++ b/docs/api.md @@ -283,7 +283,7 @@ After you have created an user you can use these credentials to log in to the IM ### Update user details -####/users/{user} +#### PUT /users/{user} Updates the properties of an user. Only specify these fields that you want to be updated. @@ -322,6 +322,33 @@ Response for a successful operation: } ``` +### Log out user from all IMAP sessions + +#### PUT /users/{user}/logout + +Forces closing all active IMAP session of an user + +**Parameters** + +- **user** (required) is the ID of the user +- **reason** is an optional message to be sent to the user with logout notification + +**Example** + +``` +curl -XPUT "http://localhost:8080/users/59467f27535f8f0f067ba8e6/logout" -H 'content-type: application/json' -d '{ + "reaosn": "Account was deleted" +}' +``` + +Response for a successful operation: + +```json +{ + "success": true +} +``` + ### Reset user password #### POST /users/{user}/password/reset diff --git a/imap-core/lib/commands/authenticate-plain.js b/imap-core/lib/commands/authenticate-plain.js index 6479d6c3..f7407266 100644 --- a/imap-core/lib/commands/authenticate-plain.js +++ b/imap-core/lib/commands/authenticate-plain.js @@ -119,7 +119,8 @@ function authenticate(connection, token, callback) { username, 'PLAIN' ); - connection.session.user = response.user; + + connection.setUser(response.user); connection.state = 'Authenticated'; callback(null, { diff --git a/imap-core/lib/commands/login.js b/imap-core/lib/commands/login.js index 6a213b06..204b7734 100644 --- a/imap-core/lib/commands/login.js +++ b/imap-core/lib/commands/login.js @@ -112,7 +112,8 @@ module.exports = { username, 'LOGIN' ); - this.session.user = response.user; + + this.setUser(response.user); this.state = 'Authenticated'; callback(null, { diff --git a/imap-core/lib/imap-connection.js b/imap-core/lib/imap-connection.js index 466ec337..ef190227 100644 --- a/imap-core/lib/imap-connection.js +++ b/imap-core/lib/imap-connection.js @@ -10,6 +10,7 @@ const crypto = require('crypto'); const os = require('os'); const EventEmitter = require('events').EventEmitter; const packageInfo = require('../../package'); +const errors = require('../../lib/errors.js'); const SOCKET_TIMEOUT = 30 * 60 * 1000; @@ -86,6 +87,13 @@ class IMAPConnection extends EventEmitter { // increment connection count this._closing = false; this._closed = false; + + this._accountListener = message => { + if (message && message.action === 'LOGOUT') { + this.send('* BYE ' + (message.reason || 'Logout requested')); + this.close(); + } + }; } /** @@ -195,6 +203,8 @@ class IMAPConnection extends EventEmitter { return; } + this._server.notifier.removeListener(this.session, '*', this._accountListener); + this._parser = false; this.state = 'Closed'; @@ -251,6 +261,8 @@ class IMAPConnection extends EventEmitter { return; } + errors.notifyConnection(this.this, err); + this._server.logger.error( { err, @@ -731,6 +743,11 @@ class IMAPConnection extends EventEmitter { return response; } + + setUser(user) { + this.session.user = user; + this._server.notifier.addListener(this.session, '*', this._accountListener); + } } // Expose to the world diff --git a/lib/api/users.js b/lib/api/users.js index a7a36d72..0b76a83d 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -459,6 +459,52 @@ module.exports = (db, server, userHandler) => { }); }); + server.put('/users/:user/logout', (req, res, next) => { + res.charSet('utf-8'); + + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + + reason: Joi.string() + .empty('') + .max(128), + + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) + }); + + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.json({ + error: result.error.message + }); + return next(); + } + + userHandler.logout(result.value.user, result.value.reason || 'Logout requested from API', (err, success) => { + if (err) { + res.json({ + error: err.message + }); + return next(); + } + res.json({ + success + }); + return next(); + }); + }); + server.post('/users/:user/quota/reset', (req, res, next) => { res.charSet('utf-8'); diff --git a/lib/user-handler.js b/lib/user-handler.js index 37b39cf3..f2b597f0 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -1242,6 +1242,36 @@ class UserHandler { this.users.collection('authlog').insertOne(entry, callback); }); } + + logout(user, reason, callback) { + // register this address as the default address for that user + return this.users.collection('users').findOne({ + _id: new ObjectID(user) + }, { + fields: { + _id: true + } + }, (err, userData) => { + if (err) { + log.error('DB', 'DBFAIL logout id=%s error=%s', user, err.message); + err.message = 'Database Error, failed to find user'; + return callback(err); + } + if (!userData) { + return callback(new Error('User not found')); + } + + if (!this.messageHandler || !this.messageHandler.notifier) { + return callback(null, false); + } + + this.messageHandler.notifier.fire(userData._id, '/', { + action: 'LOGOUT', + reason + }); + return callback(null, true); + }); + } } module.exports = UserHandler;