2017-03-06 05:45:50 +08:00
|
|
|
'use strict';
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
const IMAPStream = require('./imap-stream').IMAPStream;
|
|
|
|
const IMAPCommand = require('./imap-command').IMAPCommand;
|
|
|
|
const IMAPComposer = require('./imap-composer').IMAPComposer;
|
|
|
|
const imapTools = require('./imap-tools');
|
|
|
|
const search = require('./search');
|
|
|
|
const dns = require('dns');
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const os = require('os');
|
|
|
|
const EventEmitter = require('events').EventEmitter;
|
|
|
|
const packageInfo = require('../../package');
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
const SOCKET_TIMEOUT = 30 * 60 * 1000;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a handler for new socket
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param {Object} server Server instance
|
|
|
|
* @param {Object} socket Socket instance
|
|
|
|
*/
|
|
|
|
class IMAPConnection extends EventEmitter {
|
|
|
|
constructor(server, socket) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
// Random session ID, used for logging
|
|
|
|
this.id = crypto.randomBytes(9).toString('base64');
|
|
|
|
|
2017-04-04 21:35:56 +08:00
|
|
|
this.compression = false;
|
|
|
|
this._deflate = false;
|
|
|
|
this._inflate = false;
|
|
|
|
|
2017-03-06 05:45:50 +08:00
|
|
|
this._server = server;
|
|
|
|
this._socket = socket;
|
|
|
|
|
|
|
|
this.writeStream = new IMAPComposer({
|
|
|
|
connection: this
|
|
|
|
});
|
|
|
|
this.writeStream.pipe(this._socket);
|
|
|
|
this.writeStream.on('error', this._onError.bind(this));
|
|
|
|
|
|
|
|
// session data (envelope, user etc.)
|
|
|
|
this.session = false;
|
|
|
|
|
|
|
|
// If true then the connection is currently being upgraded to TLS
|
|
|
|
this._upgrading = false;
|
|
|
|
|
|
|
|
// Parser instance for the incoming stream
|
|
|
|
this._parser = new IMAPStream();
|
|
|
|
|
|
|
|
// Set handler for incoming commands
|
|
|
|
this._parser.oncommand = this._onCommand.bind(this);
|
|
|
|
|
|
|
|
// Manage multi part command
|
|
|
|
this._currentCommand = false;
|
|
|
|
|
|
|
|
// If set, then data payload is not executed as a command but as an argument for this function
|
|
|
|
this._nextHandler = false;
|
|
|
|
|
|
|
|
// If true, then the connection is using TLS
|
|
|
|
this.secure = !!this._server.options.secure;
|
|
|
|
|
|
|
|
// Store remote address for later usage
|
|
|
|
this.remoteAddress = this._socket.remoteAddress;
|
|
|
|
|
|
|
|
// Server hostname for the greegins
|
2017-07-12 02:38:23 +08:00
|
|
|
this.name = (this._server.options.name || os.hostname()).toLowerCase();
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
this.state = 'Not Authenticated';
|
|
|
|
|
|
|
|
this._listenerData = false;
|
|
|
|
|
|
|
|
// selected mailbox metadata
|
|
|
|
this.selected = false;
|
|
|
|
|
|
|
|
// ignore timeouts if true
|
|
|
|
this.idling = false;
|
|
|
|
|
|
|
|
// indicates if CONDSTORE is enabled for the session
|
|
|
|
this.condstoreEnabled = false;
|
|
|
|
|
|
|
|
// Resolved hostname for remote IP address
|
|
|
|
this.clientHostname = false;
|
|
|
|
|
|
|
|
// increment connection count
|
|
|
|
this._closing = false;
|
|
|
|
this._closed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
|
|
|
|
* is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
|
|
|
|
*/
|
|
|
|
init() {
|
|
|
|
// Setup event handlers for the socket
|
|
|
|
this._setListeners();
|
|
|
|
|
|
|
|
// Resolve hostname for the remote IP
|
|
|
|
// we do not care for errors as we consider the ip as unresolved in this case, no big deal
|
2017-06-03 14:51:58 +08:00
|
|
|
dns.reverse(this.remoteAddress, (err, hostnames) => {
|
|
|
|
if (err) {
|
|
|
|
//ignore, no big deal
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-line handle-callback-err
|
2017-03-06 05:45:50 +08:00
|
|
|
if (this._closing || this._closed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this.clientHostname = (hostnames && hostnames.shift()) || '[' + this.remoteAddress + ']';
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
this._startSession();
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'connect',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Connection from %s',
|
|
|
|
this.id,
|
|
|
|
this.clientHostname
|
|
|
|
);
|
|
|
|
this.send('* OK ' + ((this._server.options.id && this._server.options.id.name) || packageInfo.name) + ' ready');
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send data to socket
|
|
|
|
*
|
|
|
|
* @param {Number} code Response code
|
|
|
|
* @param {String|Array} data If data is Array, send a multi-line response
|
|
|
|
*/
|
|
|
|
send(payload, callback) {
|
|
|
|
if (this._socket && this._socket.writable) {
|
2017-04-04 22:09:39 +08:00
|
|
|
this[!this.compression ? '_socket' : '_deflate'].write(payload + '\r\n', 'binary', callback);
|
2017-06-03 14:51:58 +08:00
|
|
|
if (this.compression) {
|
2017-05-16 18:35:29 +08:00
|
|
|
// make sure we transmit the message immediatelly
|
|
|
|
this._deflate.flush();
|
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.debug(
|
|
|
|
{
|
|
|
|
tnx: 'send',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] S:',
|
|
|
|
this.id,
|
|
|
|
payload
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close socket
|
|
|
|
*/
|
|
|
|
close() {
|
|
|
|
if (!this._socket.destroyed && this._socket.writable) {
|
|
|
|
this._socket.end();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.connections.delete(this);
|
|
|
|
|
|
|
|
this._closing = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// PRIVATE METHODS
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Setup socket event handlers
|
|
|
|
*/
|
|
|
|
_setListeners() {
|
|
|
|
this._socket.on('close', this._onClose.bind(this));
|
|
|
|
this._socket.on('end', this._onEnd.bind(this));
|
|
|
|
this._socket.on('error', this._onError.bind(this));
|
|
|
|
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, this._onTimeout.bind(this));
|
|
|
|
this._socket.pipe(this._parser);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when the socket is closed
|
|
|
|
* @event
|
|
|
|
*/
|
|
|
|
_onEnd() {
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'close',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Connection END',
|
|
|
|
this.id
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
if (!this._closed) {
|
|
|
|
this._onClose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when the socket is closed
|
|
|
|
* @event
|
|
|
|
*/
|
2017-06-03 14:51:58 +08:00
|
|
|
_onClose(/* hadError */) {
|
2017-03-06 05:45:50 +08:00
|
|
|
if (this._closed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._parser = false;
|
|
|
|
|
|
|
|
this.state = 'Closed';
|
|
|
|
|
|
|
|
if (this._dataStream) {
|
|
|
|
this._dataStream.unpipe();
|
|
|
|
this._dataStream = null;
|
|
|
|
}
|
|
|
|
|
2017-04-04 21:35:56 +08:00
|
|
|
if (this._deflate) {
|
|
|
|
this._deflate = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._inflate) {
|
|
|
|
this._inflate = null;
|
|
|
|
}
|
|
|
|
|
2017-03-06 05:45:50 +08:00
|
|
|
if (this._listenerData) {
|
|
|
|
this._listenerData.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.connections.delete(this);
|
|
|
|
|
|
|
|
if (this._closed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._closed = true;
|
|
|
|
this._closing = false;
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'close',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Connection closed to %s',
|
|
|
|
this.id,
|
|
|
|
this.clientHostname
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when an error occurs with the socket
|
|
|
|
*
|
|
|
|
* @event
|
|
|
|
* @param {Error} err Error object
|
|
|
|
*/
|
|
|
|
_onError(err) {
|
2017-08-09 02:14:04 +08:00
|
|
|
if (err.processed) {
|
|
|
|
return;
|
|
|
|
}
|
2017-03-06 05:45:50 +08:00
|
|
|
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
|
|
|
this.close(); // mark connection as 'closing'
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.error(
|
|
|
|
{
|
|
|
|
err,
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] %s',
|
|
|
|
this.id,
|
|
|
|
err.message
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
this.emit('error', err);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when socket timeouts. Closes connection
|
|
|
|
*
|
|
|
|
* @event
|
|
|
|
*/
|
|
|
|
_onTimeout() {
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'connection',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Connection TIMEOUT',
|
|
|
|
this.id
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
if (this.idling) {
|
|
|
|
return; // ignore timeouts when IDLEing
|
|
|
|
}
|
|
|
|
this.send('* BYE Idle timeout, closing connection');
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a selected command is available and ivokes it
|
|
|
|
*
|
|
|
|
* @param {Buffer} command Single line of data from the client
|
|
|
|
* @param {Function} callback Callback to run once the command is processed
|
|
|
|
*/
|
|
|
|
_onCommand(command, callback) {
|
|
|
|
let currentCommand = this._currentCommand;
|
|
|
|
|
|
|
|
callback = callback || (() => false);
|
|
|
|
|
|
|
|
if (this._upgrading) {
|
|
|
|
// ignore any commands before TLS upgrade is finished
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!currentCommand) {
|
|
|
|
this._currentCommand = currentCommand = new IMAPCommand(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!command.final) {
|
|
|
|
currentCommand.append(command, callback);
|
|
|
|
} else {
|
|
|
|
this._currentCommand = false;
|
|
|
|
currentCommand.end(command, callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets up a new session
|
|
|
|
*/
|
|
|
|
_startSession() {
|
|
|
|
this.session = {
|
|
|
|
id: this.id,
|
|
|
|
|
|
|
|
selected: this.selected,
|
|
|
|
|
|
|
|
remoteAddress: this.remoteAddress,
|
|
|
|
clientHostname: this.clientHostname,
|
|
|
|
writeStream: this.writeStream,
|
|
|
|
socket: this._socket,
|
|
|
|
|
|
|
|
formatResponse: this.formatResponse.bind(this),
|
|
|
|
getQueryResponse: imapTools.getQueryResponse,
|
2017-03-21 06:07:23 +08:00
|
|
|
matchSearchQuery: search.matchSearchQuery,
|
2017-03-06 05:45:50 +08:00
|
|
|
|
2017-03-21 06:07:23 +08:00
|
|
|
isUTF8Enabled: () => this.acceptUTF8Enabled
|
2017-03-06 05:45:50 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets up notification listener from upstream
|
|
|
|
*
|
|
|
|
* @param {Function} done Called once listeners are updated
|
|
|
|
*/
|
|
|
|
updateNotificationListener(done) {
|
|
|
|
if (this._listenerData) {
|
|
|
|
if (!this.selected || this._listenerData.mailbox !== this.selected.mailbox) {
|
|
|
|
// registered against some mailbox, unregister from it
|
|
|
|
this._listenerData.clear();
|
|
|
|
} else if (this._listenerData.mailbox === this.selected.mailbox) {
|
|
|
|
// already registered
|
|
|
|
return done();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.selected) {
|
|
|
|
this._listenerData = false;
|
|
|
|
return done();
|
|
|
|
}
|
|
|
|
|
|
|
|
let cleared = false;
|
2017-06-03 14:51:58 +08:00
|
|
|
let listenerData = (this._listenerData = {
|
2017-03-06 05:45:50 +08:00
|
|
|
mailbox: this.selected.mailbox,
|
|
|
|
lock: false,
|
|
|
|
clear: () => {
|
|
|
|
this._server.notifier.removeListener(this.session, listenerData.mailbox, listenerData.callback);
|
|
|
|
if (listenerData === this._listenerData) {
|
|
|
|
this._listenerData = false;
|
|
|
|
}
|
|
|
|
listenerData = false;
|
|
|
|
cleared = true;
|
|
|
|
},
|
|
|
|
callback: message => {
|
|
|
|
if (message) {
|
|
|
|
if (this.selected && message.action === 'DELETE' && message.mailbox === this.selected.mailbox) {
|
|
|
|
this.send('* BYE Selected mailbox was deleted, have to disconnect');
|
|
|
|
this.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (listenerData.lock) {
|
|
|
|
// race condition, do not allow fetching data before previous fetch is finished
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cleared) {
|
|
|
|
// some kind of a race condition, just ignore
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if not selected anymore, remove itself
|
|
|
|
if (this.state !== 'Selected' || !this.selected) {
|
|
|
|
listenerData.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
listenerData.lock = true;
|
|
|
|
this._server.notifier.getUpdates(this.session, this._listenerData.mailbox, this.selected.modifyIndex, (err, updates) => {
|
|
|
|
if (cleared) {
|
|
|
|
// client probably switched mailboxes while processing, just ignore all results
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
listenerData.lock = false;
|
|
|
|
|
|
|
|
if (err) {
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
err,
|
|
|
|
tnx: 'updates',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Notification Error: %s',
|
|
|
|
this.id,
|
|
|
|
err.message
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if not selected anymore, remove itself
|
|
|
|
if (this.state !== 'Selected' || !this.selected) {
|
|
|
|
listenerData.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!updates || !updates.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// store new incremental modify index
|
|
|
|
if (updates[updates.length - 1].modseq > this.selected.modifyIndex) {
|
|
|
|
this.selected.modifyIndex = updates[updates.length - 1].modseq;
|
|
|
|
}
|
|
|
|
|
|
|
|
// append received notifications to the list
|
|
|
|
this.selected.notifications = this.selected.notifications.concat(updates);
|
|
|
|
if (this.idling) {
|
|
|
|
// when idling emit notifications immediatelly
|
|
|
|
this.emitNotifications();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
});
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
this._server.notifier.addListener(this.session, this._listenerData.mailbox, this._listenerData.callback);
|
|
|
|
|
|
|
|
return done();
|
|
|
|
}
|
|
|
|
|
|
|
|
// send notifications to client
|
|
|
|
emitNotifications() {
|
|
|
|
if (this.state !== 'Selected' || !this.selected || !this.selected.notifications.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let changed = false;
|
|
|
|
let existsResponse;
|
|
|
|
|
|
|
|
// show notifications
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'notifications',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Pending notifications: %s',
|
|
|
|
this.id,
|
|
|
|
this.selected.notifications.length
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
// find UIDs that are both added and removed
|
|
|
|
let added = new Set(); // added UIDs
|
|
|
|
let removed = new Set(); // removed UIDs
|
|
|
|
let skip = new Set(); // UIDs that are removed before ever seen
|
|
|
|
|
|
|
|
for (let i = 0, len = this.selected.notifications.length; i < len; i++) {
|
|
|
|
let update = this.selected.notifications[i];
|
|
|
|
if (update.command === 'EXISTS') {
|
|
|
|
added.add(update.uid);
|
|
|
|
} else if (update.command === 'EXPUNGE') {
|
|
|
|
removed.add(update.uid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removed.forEach(uid => {
|
|
|
|
if (added.has(uid)) {
|
|
|
|
skip.add(uid);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// filter multiple FETCH calls, only keep latest, otherwise might mess up MODSEQ responses
|
|
|
|
let fetches = new Set();
|
|
|
|
for (let i = this.selected.notifications.length - 1; i >= 0; i--) {
|
|
|
|
let update = this.selected.notifications[i];
|
|
|
|
if (update.command === 'FETCH') {
|
|
|
|
// skip multiple flag updates and updates for removed or newly added messages
|
|
|
|
if (fetches.has(update.uid) || added.has(update.uid) || removed.has(update.uid)) {
|
|
|
|
this.selected.notifications.splice(i, 1);
|
|
|
|
} else {
|
|
|
|
fetches.add(update.uid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0, len = this.selected.notifications.length; i < len; i++) {
|
|
|
|
let update = this.selected.notifications[i];
|
|
|
|
|
|
|
|
// skip unnecessary entries that are already removed
|
|
|
|
if (skip.has(update.uid)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (update.modseq > this.selected.modifyIndex) {
|
|
|
|
this.selected.modifyIndex = update.modseq;
|
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'notifications',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] Processing notification: %s',
|
|
|
|
this.id,
|
|
|
|
JSON.stringify(update)
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
if (update.ignore === this.id) {
|
|
|
|
continue; // skip this
|
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
this._server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'notifications',
|
|
|
|
cid: this.id
|
|
|
|
},
|
|
|
|
'[%s] UIDS: %s',
|
|
|
|
this.id,
|
|
|
|
this.selected.uidList.length
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
switch (update.command) {
|
|
|
|
case 'EXISTS':
|
|
|
|
// Generate the response but do not send it yet (EXIST response generation is needed to modify the UID list)
|
|
|
|
// This way we can accumulate consecutive EXISTS responses into single one as
|
|
|
|
// only the last one actually matters to the client
|
|
|
|
existsResponse = this.formatResponse('EXISTS', update.uid);
|
|
|
|
changed = false;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
case 'EXPUNGE': {
|
|
|
|
let seq = (this.selected.uidList || []).indexOf(update.uid);
|
|
|
|
this._server.logger.info(
|
|
|
|
{
|
2017-04-13 16:35:39 +08:00
|
|
|
tnx: 'expunge',
|
|
|
|
cid: this.id
|
2017-06-03 14:51:58 +08:00
|
|
|
},
|
|
|
|
'[%s] EXPUNGE %s',
|
|
|
|
this.id,
|
|
|
|
seq
|
|
|
|
);
|
|
|
|
if (seq >= 0) {
|
|
|
|
let output = this.formatResponse('EXPUNGE', update.uid);
|
|
|
|
this.writeStream.write(output);
|
|
|
|
changed = true; // if no more EXISTS after this, then generate an additional EXISTS
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'FETCH':
|
|
|
|
this.writeStream.write(
|
|
|
|
this.formatResponse('FETCH', update.uid, {
|
|
|
|
flags: update.flags,
|
|
|
|
modseq: (this.selected.condstoreEnabled && update.modseq) || false
|
|
|
|
})
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-30 18:25:42 +08:00
|
|
|
if (existsResponse && !changed) {
|
2017-03-06 05:45:50 +08:00
|
|
|
// send cached EXISTS response
|
|
|
|
this.writeStream.write(existsResponse);
|
|
|
|
existsResponse = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
this.writeStream.write({
|
|
|
|
tag: '*',
|
|
|
|
command: String(this.selected.uidList.length),
|
2017-06-03 14:51:58 +08:00
|
|
|
attributes: [
|
|
|
|
{
|
|
|
|
type: 'atom',
|
|
|
|
value: 'EXISTS'
|
|
|
|
}
|
|
|
|
]
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear queue
|
|
|
|
this.selected.notifications = [];
|
|
|
|
|
|
|
|
if (typeof this._server.onNotifications === 'function') {
|
|
|
|
setImmediate(this._server.onNotifications.bind(this._server, this.selected.mailbox, this.selected.modifyIndex, this.session));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
formatResponse(command, uid, data) {
|
|
|
|
command = command.toUpperCase();
|
|
|
|
let seq;
|
|
|
|
|
|
|
|
if (command === 'EXISTS') {
|
|
|
|
this.selected.uidList.push(uid);
|
|
|
|
seq = this.selected.uidList.length;
|
|
|
|
} else {
|
|
|
|
seq = (this.selected.uidList || []).indexOf(uid);
|
|
|
|
if (seq < 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
seq++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (command === 'EXPUNGE') {
|
|
|
|
this.selected.uidList.splice(seq - 1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
tag: '*',
|
|
|
|
command: String(seq),
|
2017-06-03 14:51:58 +08:00
|
|
|
attributes: [
|
|
|
|
{
|
|
|
|
type: 'atom',
|
|
|
|
value: command
|
|
|
|
}
|
|
|
|
]
|
2017-03-06 05:45:50 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
response.attributes.push([]);
|
|
|
|
if ('query' in data) {
|
|
|
|
// Response for FETCH command
|
|
|
|
data.query.forEach((item, i) => {
|
|
|
|
response.attributes[1].push(item.original);
|
|
|
|
if (['flags', 'modseq'].indexOf(item.item) >= 0) {
|
2017-06-03 14:51:58 +08:00
|
|
|
response.attributes[1].push(
|
|
|
|
[].concat(data.values[i] || []).map(value => ({
|
|
|
|
type: 'ATOM',
|
|
|
|
value: (value || value === 0 ? value : '').toString()
|
|
|
|
}))
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
} else if (Object.prototype.toString.call(data.values[i]) === '[object Date]') {
|
|
|
|
response.attributes[1].push({
|
|
|
|
type: 'ATOM',
|
|
|
|
value: imapTools.formatInternalDate(data.values[i])
|
|
|
|
});
|
|
|
|
} else if (Array.isArray(data.values[i])) {
|
|
|
|
response.attributes[1].push(data.values[i]);
|
|
|
|
} else if (item.isLiteral) {
|
|
|
|
if (data.values[i] && data.values[i].type === 'stream') {
|
|
|
|
response.attributes[1].push({
|
|
|
|
type: 'LITERAL',
|
|
|
|
value: data.values[i].value,
|
|
|
|
expectedLength: data.values[i].expectedLength,
|
|
|
|
startFrom: data.values[i].startFrom,
|
|
|
|
maxLength: data.values[i].maxLength
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
response.attributes[1].push({
|
|
|
|
type: 'LITERAL',
|
|
|
|
value: data.values[i]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (data.values[i] === '') {
|
|
|
|
response.attributes[1].push(data.values[i]);
|
|
|
|
} else {
|
|
|
|
response.attributes[1].push({
|
|
|
|
type: 'ATOM',
|
2017-06-03 14:51:58 +08:00
|
|
|
value: data.values[i].toString()
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Notification response
|
|
|
|
Object.keys(data).forEach(key => {
|
|
|
|
let value = data[key];
|
|
|
|
key = key.toUpperCase();
|
|
|
|
if (!value) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
case 'FLAGS':
|
2017-06-03 14:51:58 +08:00
|
|
|
value = [].concat(value || []).map(
|
|
|
|
flag =>
|
|
|
|
flag && flag.value
|
|
|
|
? flag
|
|
|
|
: {
|
|
|
|
type: 'ATOM',
|
|
|
|
value: flag
|
|
|
|
}
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'UID':
|
2017-07-12 02:38:23 +08:00
|
|
|
value =
|
|
|
|
value && value.value
|
|
|
|
? value
|
|
|
|
: {
|
|
|
|
type: 'ATOM',
|
|
|
|
value: (value || '0').toString()
|
|
|
|
};
|
2017-03-06 05:45:50 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'MODSEQ':
|
2017-06-03 14:51:58 +08:00
|
|
|
value = [].concat(
|
|
|
|
value && value.value
|
|
|
|
? value
|
|
|
|
: {
|
|
|
|
type: 'ATOM',
|
|
|
|
value: (value || '0').toString()
|
|
|
|
}
|
|
|
|
);
|
2017-03-06 05:45:50 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
response.attributes[1].push({
|
|
|
|
type: 'ATOM',
|
|
|
|
value: key
|
|
|
|
});
|
|
|
|
|
|
|
|
response.attributes[1].push(value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expose to the world
|
|
|
|
module.exports.IMAPConnection = IMAPConnection;
|