Use prettier for formatting

This commit is contained in:
Andris Reinman 2017-06-03 09:51:58 +03:00
parent 40b36fed53
commit 08a4cdde0a
80 changed files with 7681 additions and 6043 deletions

View file

@ -1,3 +1,4 @@
{
"extends": "nodemailer"
"extends": "nodemailer",
"fix": false
}

View file

@ -1,7 +1,6 @@
'use strict';
module.exports = function (grunt) {
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
eslint: {

775
api.js
View file

@ -25,12 +25,14 @@ let messageHandler;
let userHandler;
server.use(restify.queryParser());
server.use(restify.bodyParser({
maxBodySize: 0,
mapParams: true,
mapFiles: false,
overrideParams: false
}));
server.use(
restify.bodyParser({
maxBodySize: 0,
mapParams: true,
mapFiles: false,
overrideParams: false
})
);
server.post('/user/create', (req, res, next) => {
res.charSet('utf-8');
@ -41,15 +43,19 @@ server.post('/user/create', (req, res, next) => {
quota: Joi.number().default(config.maxStorage * (1024 * 1024))
});
const result = Joi.validate({
username: req.params.username,
password: req.params.password,
quota: req.params.quota
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.params.username,
password: req.params.password,
quota: req.params.quota
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -88,15 +94,19 @@ server.post('/user/address/create', (req, res, next) => {
let address = req.params.address;
let main = req.params.main;
const result = Joi.validate({
username,
address: (address || '').replace(/[\u0080-\uFFFF]/g, 'x'),
main
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username,
address: (address || '').replace(/[\u0080-\uFFFF]/g, 'x'),
main
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -179,13 +189,18 @@ server.post('/user/address/create', (req, res, next) => {
if (!userData.address || main) {
// register this address as the default address for that user
return db.database.collection('users').findOneAndUpdate({
_id: userData._id
}, {
$set: {
address
}
}, {}, done);
return db.database.collection('users').findOneAndUpdate(
{
_id: userData._id
},
{
$set: {
address
}
},
{},
done
);
}
done();
@ -204,16 +219,20 @@ server.post('/user/quota', (req, res, next) => {
forwards: Joi.number().min(0).max(1000000).optional()
});
const result = Joi.validate({
username: req.params.username,
quota: req.params.quota,
recipients: req.params.recipients,
forwards: req.params.forwards
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.params.username,
quota: req.params.quota,
recipients: req.params.recipients,
forwards: req.params.forwards
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -286,13 +305,17 @@ server.post('/user/quota/reset', (req, res, next) => {
username: Joi.string().alphanum().lowercase().min(3).max(30).required()
});
const result = Joi.validate({
username: req.params.username
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.params.username
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -322,46 +345,34 @@ server.post('/user/quota/reset', (req, res, next) => {
return next();
}
// calculate mailbox size by aggregating the size's of all messages
db.database.collection('messages').aggregate([{
$match: {
user: user._id
}
}, {
$group: {
_id: {
user: '$user'
db.database
.collection('messages')
.aggregate(
[
{
$match: {
user: user._id
}
},
storageUsed: {
$sum: '$size'
{
$group: {
_id: {
user: '$user'
},
storageUsed: {
$sum: '$size'
}
}
}
],
{
cursor: {
batchSize: 1
}
}
}], {
cursor: {
batchSize: 1
}
}).toArray((err, result) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
username
});
return next();
}
let storageUsed = result && result[0] && result[0].storageUsed || 0;
// update quota counter
db.database.collection('users').findOneAndUpdate({
_id: user._id
}, {
$set: {
storageUsed: Number(storageUsed) || 0
}
}, {
returnOriginal: false
}, (err, result) => {
)
.toArray((err, result) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
@ -370,23 +381,43 @@ server.post('/user/quota/reset', (req, res, next) => {
return next();
}
if (!result || !result.value) {
let storageUsed = (result && result[0] && result[0].storageUsed) || 0;
// update quota counter
db.database.collection('users').findOneAndUpdate({
_id: user._id
}, {
$set: {
storageUsed: Number(storageUsed) || 0
}
}, {
returnOriginal: false
}, (err, result) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
username
});
return next();
}
if (!result || !result.value) {
res.json({
error: 'This user does not exist',
username
});
return next();
}
res.json({
error: 'This user does not exist',
username
success: true,
username,
previousStorageUsed: user.storageUsed,
storageUsed: Number(result.value.storageUsed) || 0
});
return next();
}
res.json({
success: true,
username,
previousStorageUsed: user.storageUsed,
storageUsed: Number(result.value.storageUsed) || 0
});
return next();
});
});
});
});
@ -398,14 +429,18 @@ server.post('/user/password', (req, res, next) => {
password: Joi.string().min(3).max(100).required()
});
const result = Joi.validate({
username: req.params.username,
password: req.params.password
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.params.username,
password: req.params.password
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -456,13 +491,17 @@ server.get('/user', (req, res, next) => {
username: Joi.string().alphanum().lowercase().min(3).max(30).required()
});
const result = Joi.validate({
username: req.query.username
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.query.username
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -491,70 +530,75 @@ server.get('/user', (req, res, next) => {
return next();
}
db.database.collection('addresses').find({
user: userData._id
}).sort({
address: 1
}).toArray((err, addresses) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
username
});
return next();
}
if (!addresses) {
addresses = [];
}
db.redis.multi().
get('wdr:' + userData._id.toString()).
ttl('wdr:' + userData._id.toString()).
get('wdf:' + userData._id.toString()).
ttl('wdf:' + userData._id.toString()).
exec((err, result) => {
db.database
.collection('addresses')
.find({
user: userData._id
})
.sort({
address: 1
})
.toArray((err, addresses) => {
if (err) {
// ignore
res.json({
error: 'MongoDB Error: ' + err.message,
username
});
return next();
}
let recipients = Number(userData.recipients) || 0;
let forwards = Number(userData.forwards) || 0;
let recipientsSent = Number(result && result[0]) || 0;
let recipientsTtl = Number(result && result[1]) || 0;
if (!addresses) {
addresses = [];
}
let forwardsSent = Number(result && result[2]) || 0;
let forwardsTtl = Number(result && result[3]) || 0;
db.redis
.multi()
.get('wdr:' + userData._id.toString())
.ttl('wdr:' + userData._id.toString())
.get('wdf:' + userData._id.toString())
.ttl('wdf:' + userData._id.toString())
.exec((err, result) => {
if (err) {
// ignore
}
let recipients = Number(userData.recipients) || 0;
let forwards = Number(userData.forwards) || 0;
res.json({
success: true,
username,
let recipientsSent = Number(result && result[0]) || 0;
let recipientsTtl = Number(result && result[1]) || 0;
quota: Number(userData.quota) || config.maxStorage * 1024 * 1024,
storageUsed: Math.max(Number(userData.storageUsed) || 0, 0),
let forwardsSent = Number(result && result[2]) || 0;
let forwardsTtl = Number(result && result[3]) || 0;
recipients,
recipientsSent,
res.json({
success: true,
username,
forwards,
forwardsSent,
quota: Number(userData.quota) || config.maxStorage * 1024 * 1024,
storageUsed: Math.max(Number(userData.storageUsed) || 0, 0),
recipientsLimited: recipients ? recipients <= recipientsSent : false,
recipientsTtl: recipientsTtl >= 0 ? recipientsTtl : false,
recipients,
recipientsSent,
forwardsLimited: forwards ? forwards <= forwardsSent : false,
forwardsTtl: forwardsTtl >= 0 ? forwardsTtl : false,
forwards,
forwardsSent,
addresses: addresses.map(address => ({
id: address._id.toString(),
address: address.address,
main: address.address === userData.address,
created: address.created
}))
});
return next();
recipientsLimited: recipients ? recipients <= recipientsSent : false,
recipientsTtl: recipientsTtl >= 0 ? recipientsTtl : false,
forwardsLimited: forwards ? forwards <= forwardsSent : false,
forwardsTtl: forwardsTtl >= 0 ? forwardsTtl : false,
addresses: addresses.map(address => ({
id: address._id.toString(),
address: address.address,
main: address.address === userData.address,
created: address.created
}))
});
return next();
});
});
});
});
});
@ -565,13 +609,17 @@ server.get('/user/mailboxes', (req, res, next) => {
username: Joi.string().alphanum().lowercase().min(3).max(30).required()
});
const result = Joi.validate({
username: req.query.username
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
username: req.query.username
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -600,53 +648,58 @@ server.get('/user/mailboxes', (req, res, next) => {
return next();
}
db.database.collection('mailboxes').find({
user: userData._id
}).toArray((err, mailboxes) => {
if (err) {
db.database
.collection('mailboxes')
.find({
user: userData._id
})
.toArray((err, mailboxes) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
username
});
return next();
}
if (!mailboxes) {
mailboxes = [];
}
let priority = {
Inbox: 1,
Sent: 2,
Junk: 3,
Trash: 4
};
res.json({
error: 'MongoDB Error: ' + err.message,
username
success: true,
username,
mailboxes: mailboxes
.map(mailbox => ({
id: mailbox._id.toString(),
path: mailbox.path,
special: mailbox.path === 'INBOX' ? 'Inbox' : mailbox.specialUse ? mailbox.specialUse.replace(/^\\/, '') : false
}))
.sort((a, b) => {
if (a.special && !b.special) {
return -1;
}
if (b.special && !a.special) {
return 1;
}
if (a.special && b.special) {
return (priority[a.special] || 5) - (priority[b.special] || 5);
}
return a.path.localeCompare(b.path);
})
});
return next();
}
if (!mailboxes) {
mailboxes = [];
}
let priority = {
Inbox: 1,
Sent: 2,
Junk: 3,
Trash: 4
};
res.json({
success: true,
username,
mailboxes: mailboxes.map(mailbox => ({
id: mailbox._id.toString(),
path: mailbox.path,
special: mailbox.path === 'INBOX' ? 'Inbox' : (mailbox.specialUse ? mailbox.specialUse.replace(/^\\/, '') : false)
})).sort((a, b) => {
if (a.special && !b.special) {
return -1;
}
if (b.special && !a.special) {
return 1;
}
if (a.special && b.special) {
return (priority[a.special] || 5) - (priority[b.special] || 5);
}
return a.path.localeCompare(b.path);
})
});
return next();
});
});
});
@ -662,16 +715,20 @@ server.get('/mailbox/:id', (req, res, next) => {
size: Joi.number().min(1).max(50).default(20)
});
const result = Joi.validate({
id: req.params.id,
before: req.params.before,
after: req.params.after,
size: req.params.size
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
id: req.params.id,
before: req.params.before,
after: req.params.after,
size: req.params.size
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -707,9 +764,7 @@ server.get('/mailbox/:id', (req, res, next) => {
mailbox: mailbox._id
};
let reverse = false;
let sort = [
['uid', -1]
];
let sort = [['uid', -1]];
if (req.params.before) {
query.uid = {
@ -719,9 +774,7 @@ server.get('/mailbox/:id', (req, res, next) => {
query.uid = {
$gt: after
};
sort = [
['uid', 1]
];
sort = [['uid', 1]];
reverse = true;
}
@ -731,9 +784,7 @@ server.get('/mailbox/:id', (req, res, next) => {
fields: {
uid: true
},
sort: [
['uid', -1]
]
sort: [['uid', -1]]
}, (err, entry) => {
if (err) {
res.json({
@ -765,9 +816,7 @@ server.get('/mailbox/:id', (req, res, next) => {
fields: {
uid: true
},
sort: [
['uid', 1]
]
sort: [['uid', 1]]
}, (err, entry) => {
if (err) {
res.json({
@ -786,71 +835,76 @@ server.get('/mailbox/:id', (req, res, next) => {
let oldest = entry.uid;
db.database.collection('messages').find(query, {
uid: true,
mailbox: true,
idate: true,
headers: true,
ha: true,
intro: true
}).sort(sort).limit(size).toArray((err, messages) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
id
});
return next();
}
if (reverse) {
messages = messages.reverse();
}
let nextPage = false;
let prevPage = false;
if (messages.length) {
if (after || before) {
prevPage = messages[0].uid;
if (prevPage >= newest) {
prevPage = false;
}
}
if (messages.length >= size) {
nextPage = messages[messages.length - 1].uid;
if (nextPage < oldest) {
nextPage = false;
}
}
}
res.json({
success: true,
mailbox: {
id: mailbox._id,
path: mailbox.path
},
next: nextPage ? '/mailbox/' + id + '?before=' + nextPage + '&size=' + size : false,
prev: prevPage ? '/mailbox/' + id + '?after=' + prevPage + '&size=' + size : false,
messages: messages.map(message => {
let response = {
id: message._id,
date: message.idate,
ha: message.ha,
intro: message.intro
};
message.headers.forEach(entry => {
if (['subject', 'from', 'to', 'cc', 'bcc'].includes(entry.key)) {
response[entry.key] = entry.value;
}
db.database
.collection('messages')
.find(query, {
uid: true,
mailbox: true,
idate: true,
headers: true,
ha: true,
intro: true
})
.sort(sort)
.limit(size)
.toArray((err, messages) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
id
});
return response;
})
});
return next();
}
return next();
});
if (reverse) {
messages = messages.reverse();
}
let nextPage = false;
let prevPage = false;
if (messages.length) {
if (after || before) {
prevPage = messages[0].uid;
if (prevPage >= newest) {
prevPage = false;
}
}
if (messages.length >= size) {
nextPage = messages[messages.length - 1].uid;
if (nextPage < oldest) {
nextPage = false;
}
}
}
res.json({
success: true,
mailbox: {
id: mailbox._id,
path: mailbox.path
},
next: nextPage ? '/mailbox/' + id + '?before=' + nextPage + '&size=' + size : false,
prev: prevPage ? '/mailbox/' + id + '?after=' + prevPage + '&size=' + size : false,
messages: messages.map(message => {
let response = {
id: message._id,
date: message.idate,
ha: message.ha,
intro: message.intro
};
message.headers.forEach(entry => {
if (['subject', 'from', 'to', 'cc', 'bcc'].includes(entry.key)) {
response[entry.key] = entry.value;
}
});
return response;
})
});
return next();
});
});
});
});
@ -864,14 +918,18 @@ server.get('/message/:id', (req, res, next) => {
mailbox: Joi.string().hex().lowercase().length(24).optional()
});
const result = Joi.validate({
id: req.params.id,
mailbox: req.params.mailbox
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
id: req.params.id,
mailbox: req.params.mailbox
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -941,14 +999,18 @@ server.get('/message/:id/raw', (req, res, next) => {
mailbox: Joi.string().hex().lowercase().length(24).optional()
});
const result = Joi.validate({
id: req.params.id,
mailbox: req.params.mailbox
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
id: req.params.id,
mailbox: req.params.mailbox
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -1012,14 +1074,18 @@ server.get('/message/:message/attachment/:attachment', (req, res, next) => {
attachment: Joi.string().regex(/^ATT\d+$/i).uppercase().required()
});
const result = Joi.validate({
message: req.params.message,
attachment: req.params.attachment
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
message: req.params.message,
attachment: req.params.attachment
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -1112,13 +1178,17 @@ server.get('/attachment/:attachment', (req, res, next) => {
attachment: Joi.string().hex().lowercase().length(24).required()
});
const result = Joi.validate({
attachment: req.params.attachment
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
attachment: req.params.attachment
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -1173,14 +1243,18 @@ server.del('/message/:id', (req, res, next) => {
mailbox: Joi.string().hex().lowercase().length(24).optional()
});
const result = Joi.validate({
id: req.params.id,
mailbox: req.params.mailbox
}, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
const result = Joi.validate(
{
id: req.params.id,
mailbox: req.params.mailbox
},
schema,
{
abortEarly: false,
convert: true,
allowUnknown: true
}
);
if (result.error) {
res.json({
@ -1200,23 +1274,26 @@ server.del('/message/:id', (req, res, next) => {
query.mailbox = new ObjectID(mailbox);
}
messageHandler.del({
query
}, (err, success) => {
if (err) {
messageHandler.del(
{
query
},
(err, success) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
id
});
return next();
}
res.json({
error: 'MongoDB Error: ' + err.message,
success,
id
});
return next();
}
res.json({
success,
id
});
return next();
});
);
});
module.exports = done => {

View file

@ -1,6 +1,6 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: ['Authenticated', 'Selected'],
@ -10,24 +10,28 @@ module.exports = {
// which does not yet take into account the appended message
disableNotifications: true,
schema: [{
name: 'mailbox',
type: 'string'
}, {
name: 'flags',
type: 'array',
optional: true
}, {
name: 'datetime',
type: 'string',
optional: true
}, {
name: 'message',
type: 'literal'
}],
schema: [
{
name: 'mailbox',
type: 'string'
},
{
name: 'flags',
type: 'array',
optional: true
},
{
name: 'datetime',
type: 'string',
optional: true
},
{
name: 'message',
type: 'literal'
}
],
handler(command, callback) {
// Check if APPEND method is set
if (typeof this._server.onAppend !== 'function') {
return callback(null, {
@ -45,12 +49,12 @@ module.exports = {
if (command.attributes.length === 2) {
flags = command.attributes[0] || [];
internaldate = command.attributes[1] && command.attributes[1].value || '';
internaldate = (command.attributes[1] && command.attributes[1].value) || '';
} else if (command.attributes.length === 1) {
if (Array.isArray(command.attributes[0])) {
flags = command.attributes[0];
} else {
internaldate = command.attributes[0] && command.attributes[0].value || '';
internaldate = (command.attributes[0] && command.attributes[0].value) || '';
}
}
@ -94,20 +98,25 @@ module.exports = {
return true;
});
this._server.onAppend(mailbox, flags, internaldate, new Buffer(typeof message.value === 'string' ? message.value : (message.value || '').toString(), 'binary'), this.session, (err, success, info) => {
this._server.onAppend(
mailbox,
flags,
internaldate,
new Buffer(typeof message.value === 'string' ? message.value : (message.value || '').toString(), 'binary'),
this.session,
(err, success, info) => {
if (err) {
return callback(err);
}
if (err) {
return callback(err);
let code = typeof success === 'string' ? success.toUpperCase() : 'APPENDUID ' + info.uidValidity + ' ' + info.uid;
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
}
let code = typeof success === 'string' ? success.toUpperCase() : 'APPENDUID ' + info.uidValidity + ' ' + info.uid;
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
});
);
}
};

View file

@ -3,15 +3,16 @@
module.exports = {
state: 'Not Authenticated',
schema: [{
name: 'token',
type: 'string',
optional: true
}],
schema: [
{
name: 'token',
type: 'string',
optional: true
}
],
handler(command, callback, next) {
let token = (command.attributes && command.attributes[0] && command.attributes[0].value || '').toString().trim();
let token = ((command.attributes && command.attributes[0] && command.attributes[0].value) || '').toString().trim();
if (!this.secure && !this._server.options.ignoreSTARTTLS) {
// Only allow authentication using TLS
@ -57,52 +58,74 @@ function authenticate(connection, token, callback) {
let password = (data[2] || '').toString().trim();
// Do auth
connection._server.onAuth({
method: 'PLAIN',
username,
password
}, connection.session, (err, response) => {
if (err) {
connection._server.logger.info({
err,
tnx: 'auth',
username,
method: 'PLAIN',
action: 'fail',
cid: connection.id
}, '[%s] Authentication error for %s using %s\n%s', connection.id, username, 'PLAIN', err.message);
return callback(err);
}
connection._server.onAuth(
{
method: 'PLAIN',
username,
password
},
connection.session,
(err, response) => {
if (err) {
connection._server.logger.info(
{
err,
tnx: 'auth',
username,
method: 'PLAIN',
action: 'fail',
cid: connection.id
},
'[%s] Authentication error for %s using %s\n%s',
connection.id,
username,
'PLAIN',
err.message
);
return callback(err);
}
if (!response || !response.user) {
connection._server.logger.info({
tnx: 'auth',
if (!response || !response.user) {
connection._server.logger.info(
{
tnx: 'auth',
username,
method: 'PLAIN',
action: 'fail',
cid: connection.id
},
'[%s] Authentication failed for %s using %s',
connection.id,
username,
'PLAIN'
);
return callback(null, {
response: 'NO',
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
});
}
connection._server.logger.info(
{
tnx: 'auth',
username,
method: 'PLAIN',
action: 'success',
cid: connection.id
},
'[%s] %s authenticated using %s',
connection.id,
username,
method: 'PLAIN',
action: 'fail',
cid: connection.id
}, '[%s] Authentication failed for %s using %s', connection.id, username, 'PLAIN');
return callback(null, {
response: 'NO',
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
'PLAIN'
);
connection.session.user = response.user;
connection.state = 'Authenticated';
callback(null, {
response: 'OK',
message: new Buffer(username + ' authenticated').toString('binary')
});
}
connection._server.logger.info({
tnx: 'auth',
username,
method: 'PLAIN',
action: 'success',
cid: connection.id
}, '[%s] %s authenticated using %s', connection.id, username, 'PLAIN');
connection.session.user = response.user;
connection.state = 'Authenticated';
callback(null, {
response: 'OK',
message: new Buffer(username + ' authenticated').toString('binary')
});
});
);
}

View file

@ -2,7 +2,6 @@
module.exports = {
handler(command, callback) {
let capabilities = [];
if (!this.secure) {

View file

@ -4,7 +4,6 @@ module.exports = {
state: 'Selected',
handler(command, callback) {
// Check if EXPUNGE method is set
if (typeof this._server.onExpunge !== 'function') {
return callback(null, {
@ -28,15 +27,20 @@ module.exports = {
this.state = 'Authenticated';
this.updateNotificationListener(() => {
this._server.onExpunge(mailbox, {
isUid: false,
silent: true
}, this.session, () => {
// don't care if expunging succeeded, the mailbox is now closed anyway
callback(null, {
response: 'OK'
});
});
this._server.onExpunge(
mailbox,
{
isUid: false,
silent: true
},
this.session,
() => {
// don't care if expunging succeeded, the mailbox is now closed anyway
callback(null, {
response: 'OK'
});
}
);
});
}
};

View file

@ -6,14 +6,15 @@ const zlib = require('zlib');
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mechanism',
type: 'string'
}],
schema: [
{
name: 'mechanism',
type: 'string'
}
],
handler(command, callback) {
let mechanism = (command.attributes[0] && command.attributes[0].value || '').toString().toUpperCase().trim();
let mechanism = ((command.attributes[0] && command.attributes[0].value) || '').toString().toUpperCase().trim();
if (!mechanism) {
return callback(null, {
@ -38,20 +39,30 @@ module.exports = {
this._inflate = zlib.createInflateRaw();
this._deflate.once('error', err => {
this._server.logger.debug({
err,
tnx: 'deflate',
cid: this.id
}, '[%s] Deflate error %s', this.id, err.message);
this._server.logger.debug(
{
err,
tnx: 'deflate',
cid: this.id
},
'[%s] Deflate error %s',
this.id,
err.message
);
this.close();
});
this._inflate.once('error', err => {
this._server.logger.debug({
err,
tnx: 'inflate',
cid: this.id
}, '[%s] Inflate error %s', this.id, err.message);
this._server.logger.debug(
{
err,
tnx: 'inflate',
cid: this.id
},
'[%s] Inflate error %s',
this.id,
err.message
);
this.close();
});

View file

@ -1,20 +1,22 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
schema: [{
name: 'range',
type: 'sequence'
}, {
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'range',
type: 'sequence'
},
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let cmd = (command.command || '').toString().toUpperCase();
// Check if COPY method is set
@ -25,8 +27,8 @@ module.exports = {
});
}
let range = command.attributes[0] && command.attributes[0].value || '';
let path = Buffer.from(command.attributes[1] && command.attributes[1].value || '', 'binary').toString();
let range = (command.attributes[0] && command.attributes[0].value) || '';
let path = Buffer.from((command.attributes[1] && command.attributes[1].value) || '', 'binary').toString();
let mailbox = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled);
if (!mailbox) {
@ -39,21 +41,27 @@ module.exports = {
let messages = imapTools.getMessageRange(this.selected.uidList, range, cmd === 'UID COPY');
this._server.onCopy(this.selected.mailbox, {
destination: mailbox,
messages
}, this.session, (err, success, info) => {
if (err) {
return callback(err);
this._server.onCopy(
this.selected.mailbox,
{
destination: mailbox,
messages
},
this.session,
(err, success, info) => {
if (err) {
return callback(err);
}
let code = typeof success === 'string'
? success.toUpperCase()
: 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid);
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
}
let code = typeof success === 'string' ? success.toUpperCase() : 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid);
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
});
);
}
};

View file

@ -1,21 +1,22 @@
'use strict';
let imapTools = require('../imap-tools');
let utf7 = require('utf7').imap;
const imapTools = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag CREATE "mailbox"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let mailbox = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let mailbox = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
if (!this.acceptUTF8Enabled) {
// decode before normalizing to uncover stuff like ending / etc.
@ -68,8 +69,6 @@ module.exports = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
}
};

View file

@ -1,20 +1,21 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
// tag DELETE "mailbox"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let mailbox = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let mailbox = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
mailbox = imapTools.normalizeMailbox(mailbox, !this.acceptUTF8Enabled);
// Check if DELETE method is set

View file

@ -8,12 +8,12 @@ module.exports = {
let enabled = [];
command.attributes.map(attr => {
if ((attr && attr.value || '').toString().toUpperCase() === 'CONDSTORE') {
if (((attr && attr.value) || '').toString().toUpperCase() === 'CONDSTORE') {
this.condstoreEnabled = true;
enabled.push('CONDSTORE');
}
if ((attr && attr.value || '').toString().toUpperCase() === 'UTF8=ACCEPT') {
if (((attr && attr.value) || '').toString().toUpperCase() === 'UTF8=ACCEPT') {
this.acceptUTF8Enabled = true;
enabled.push('UTF8=ACCEPT');
}

View file

@ -4,7 +4,6 @@ module.exports = {
state: 'Selected',
handler(command, callback) {
// Check if EXPUNGE method is set
if (typeof this._server.onExpunge !== 'function') {
return callback(null, {
@ -20,17 +19,22 @@ module.exports = {
});
}
this._server.onExpunge(this.selected.mailbox, {
isUid: false
}, this.session, (err, success) => {
if (err) {
return callback(err);
}
this._server.onExpunge(
this.selected.mailbox,
{
isUid: false
},
this.session,
(err, success) => {
if (err) {
return callback(err);
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
}
);
}
};

View file

@ -1,7 +1,7 @@
'use strict';
let imapTools = require('../imap-tools');
let imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
const imapHandler = require('../handler/imap-handler');
/*
@ -16,20 +16,23 @@ module.exports = {
state: 'Selected',
disableNotifications: true,
schema: [{
name: 'range',
type: 'sequence'
}, {
name: 'data',
type: 'mixed'
}, {
name: 'extensions',
type: 'array',
optional: true
}],
schema: [
{
name: 'range',
type: 'sequence'
},
{
name: 'data',
type: 'mixed'
},
{
name: 'extensions',
type: 'array',
optional: true
}
],
handler(command, callback) {
// Check if FETCH method is set
if (typeof this._server.onFetch !== 'function') {
return callback(null, {
@ -39,7 +42,7 @@ module.exports = {
}
let isUid = (command.command || '').toString().toUpperCase() === 'UID FETCH' ? true : false;
let range = command.attributes[0] && command.attributes[0].value || '';
let range = (command.attributes[0] && command.attributes[0].value) || '';
if (!imapTools.validateSequnce(range)) {
return callback(new Error('Invalid sequence set for ' + command.command));
}
@ -53,7 +56,7 @@ module.exports = {
let query = [];
let params = [].concat(command.attributes[1] || []);
let extensions = [].concat(command.attributes[2] || []).map(val => (val && val.value));
let extensions = [].concat(command.attributes[2] || []).map(val => val && val.value);
if (extensions.length) {
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'CHANGEDSINCE' || isNaN(extensions[1])) {
@ -87,7 +90,7 @@ module.exports = {
}
// checks conditions does the messages need to be marked as seen, is the full body needed etc.
for (i = 0, len = params.length; i < len; i++) {
for ((i = 0), (len = params.length); i < len; i++) {
param = params[i];
if (!param || (typeof param !== 'string' && param.type !== 'ATOM')) {
return callback(new Error('Invalid message data item name for ' + command.command));
@ -161,7 +164,7 @@ module.exports = {
let getFieldName = field => (field.value || '').toString().toLowerCase();
// compose query object from parsed IMAP command
for (i = 0, len = params.length; i < len; i++) {
for ((i = 0), (len = params.length); i < len; i++) {
param = params[i];
let item = {
query: imapHandler.compiler({
@ -221,42 +224,52 @@ module.exports = {
query.push(item);
}
this._server.logger.debug({
tnx: 'fetch',
cid: this.id
}, '[%s] FETCH: %s', this.id, JSON.stringify({
metadataOnly: !!metadataOnly,
markAsSeen: !!markAsSeen,
messages: messages.length,
query,
changedSince,
isUid
}));
this._server.logger.debug(
{
tnx: 'fetch',
cid: this.id
},
'[%s] FETCH: %s',
this.id,
JSON.stringify({
metadataOnly: !!metadataOnly,
markAsSeen: !!markAsSeen,
messages: messages.length,
query,
changedSince,
isUid
})
);
this._server.onFetch(this.selected.mailbox, {
metadataOnly: !!metadataOnly,
markAsSeen: !!markAsSeen,
messages,
query,
changedSince,
isUid
}, this.session, (err, success) => {
if (err) {
return callback(err);
this._server.onFetch(
this.selected.mailbox,
{
metadataOnly: !!metadataOnly,
markAsSeen: !!markAsSeen,
messages,
query,
changedSince,
isUid
},
this.session,
(err, success) => {
if (err) {
return callback(err);
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
);
}
};
function checkSchema(schema, item) {
let i, len;
if (Array.isArray(schema)) {
for (i = 0, len = schema.length; i < len; i++) {
for ((i = 0), (len = schema.length); i < len; i++) {
if (checkSchema(schema[i], item)) {
return true;
}
@ -272,7 +285,6 @@ function checkSchema(schema, item) {
}
if (typeof schema === 'object' && schema) {
// check.type
switch (Object.prototype.toString.call(schema.type)) {
case '[object RegExp]':

View file

@ -7,14 +7,15 @@ let imapHandler = require('../handler/imap-handler');
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'quotaroot',
type: 'string'
}],
schema: [
{
name: 'quotaroot',
type: 'string'
}
],
handler(command, callback) {
let quotaRoot = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let quotaRoot = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
if (typeof this._server.onGetQuota !== 'function') {
return callback(null, {
@ -36,20 +37,29 @@ module.exports = {
}
// * QUOTA "" (STORAGE 220676 15728640)
this.send(imapHandler.compiler({
tag: '*',
command: 'QUOTA',
attributes: [data.root || '', [{
type: 'atom',
value: 'STORAGE'
}, {
type: 'atom',
value: String(Math.ceil((Number(data.storageUsed) || 0) / 1024))
}, {
type: 'atom',
value: String(Math.ceil((Number(data.quota) || 0) / 1024))
}]]
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'QUOTA',
attributes: [
data.root || '',
[
{
type: 'atom',
value: 'STORAGE'
},
{
type: 'atom',
value: String(Math.ceil((Number(data.storageUsed) || 0) / 1024))
},
{
type: 'atom',
value: String(Math.ceil((Number(data.quota) || 0) / 1024))
}
]
]
})
);
callback(null, {
response: 'OK',

View file

@ -1,8 +1,8 @@
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
let utf7 = require('utf7').imap;
const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag SELECT "mailbox"
// tag EXAMINE "mailbox"
@ -10,14 +10,15 @@ let utf7 = require('utf7').imap;
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let path = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let path = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let mailbox = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled);
if (typeof this._server.onGetQuota !== 'function') {
@ -54,27 +55,38 @@ module.exports = {
}
// * QUOTAROOT INBOX ""
this.send(imapHandler.compiler({
tag: '*',
command: 'QUOTAROOT',
attributes: [path, data.root || '']
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'QUOTAROOT',
attributes: [path, data.root || '']
})
);
// * QUOTA "" (STORAGE 220676 15728640)
this.send(imapHandler.compiler({
tag: '*',
command: 'QUOTA',
attributes: [data.root || '', [{
type: 'atom',
value: 'STORAGE'
}, {
type: 'atom',
value: String(Math.ceil((Number(data.storageUsed) || 0) / 1024))
}, {
type: 'atom',
value: String(Math.ceil((Number(data.quota) || 0) / 1024))
}]]
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'QUOTA',
attributes: [
data.root || '',
[
{
type: 'atom',
value: 'STORAGE'
},
{
type: 'atom',
value: String(Math.ceil((Number(data.storageUsed) || 0) / 1024))
},
{
type: 'atom',
value: String(Math.ceil((Number(data.quota) || 0) / 1024))
}
]
]
})
);
callback(null, {
response: 'OK',

View file

@ -1,15 +1,17 @@
'use strict';
let packageInfo = require('../../../package');
let imapHandler = require('../handler/imap-handler');
const packageInfo = require('../../../package');
const imapHandler = require('../handler/imap-handler');
let allowedKeys = ['name', 'version', 'os', 'os-version', 'vendor', 'support-url', 'address', 'date', 'command', 'arguments', 'environment'];
const allowedKeys = ['name', 'version', 'os', 'os-version', 'vendor', 'support-url', 'address', 'date', 'command', 'arguments', 'environment'];
module.exports = {
schema: [{
name: 'id',
type: ['null', 'array']
}],
schema: [
{
name: 'id',
type: ['null', 'array']
}
],
handler(command, callback) {
let clientId = {};
let serverId = {};
@ -41,18 +43,27 @@ module.exports = {
}
});
this._server.logger.info({
tnx: 'id',
cid: this.id
}, '[%s] Client identification data received', this.id);
Object.keys(clientId).
sort((a, b) => (allowedKeys.indexOf(a) - allowedKeys.indexOf(b))).
forEach(key => {
this._server.logger.info({
this._server.logger.info(
{
tnx: 'id',
cid: this.id
}, '[%s] %s%s: %s', this.id, key, new Array(maxKeyLen - key.length + 1).join(' '), clientId[key]);
},
'[%s] Client identification data received',
this.id
);
Object.keys(clientId).sort((a, b) => allowedKeys.indexOf(a) - allowedKeys.indexOf(b)).forEach(key => {
this._server.logger.info(
{
tnx: 'id',
cid: this.id
},
'[%s] %s%s: %s',
this.id,
key,
new Array(maxKeyLen - key.length + 1).join(' '),
clientId[key]
);
});
}
@ -70,14 +81,18 @@ module.exports = {
});
}
this.send(imapHandler.compiler({
tag: '*',
command: 'ID',
attributes: serverIdList.length ? [serverIdList] : {
type: 'atom',
value: 'NIL'
}
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'ID',
attributes: serverIdList.length
? [serverIdList]
: {
type: 'atom',
value: 'NIL'
}
})
);
callback(null, {
response: 'OK'

View file

@ -4,7 +4,6 @@ module.exports = {
state: ['Authenticated', 'Selected'],
handler(command, callback, next) {
let idleTimeout = setTimeout(() => {
if (typeof this._server.onIdleEnd === 'function') {
this._server.onIdleEnd(this.selected && this.selected.mailbox, this.session);

View file

@ -1,36 +1,41 @@
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
let utf7 = require('utf7').imap;
const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag LIST (SPECIAL-USE) "" "%" RETURN (SPECIAL-USE)
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'selection',
type: ['array'],
optional: true
}, {
name: 'reference',
type: 'string'
}, {
name: 'mailbox',
type: 'string'
}, {
name: 'return',
type: 'atom',
optional: true
}, {
name: 'return',
type: 'array',
optional: true
}],
schema: [
{
name: 'selection',
type: ['array'],
optional: true
},
{
name: 'reference',
type: 'string'
},
{
name: 'mailbox',
type: 'string'
},
{
name: 'return',
type: 'atom',
optional: true
},
{
name: 'return',
type: 'array',
optional: true
}
],
handler(command, callback) {
let filterSpecialUseFolders = false;
let filterSpecialUseFlags = false;
let reference;
@ -41,7 +46,11 @@ module.exports = {
// (SPECIAL-USE)
if (Array.isArray(command.attributes[0])) {
if (command.attributes[0].length) {
if (command.attributes[0].length === 1 && command.attributes[0][0].type === 'ATOM' && command.attributes[0][0].value.toUpperCase() === 'SPECIAL-USE') {
if (
command.attributes[0].length === 1 &&
command.attributes[0][0].type === 'ATOM' &&
command.attributes[0][0].value.toUpperCase() === 'SPECIAL-USE'
) {
filterSpecialUseFolders = true;
} else {
return callback(new Error('Invalid argument provided for LIST'));
@ -51,18 +60,23 @@ module.exports = {
}
// ""
reference = Buffer.from(command.attributes[arrPos] && command.attributes[arrPos].value || '', 'binary').toString();
reference = Buffer.from((command.attributes[arrPos] && command.attributes[arrPos].value) || '', 'binary').toString();
arrPos++;
// "%"
mailbox = Buffer.from(command.attributes[arrPos] && command.attributes[arrPos].value || '', 'binary').toString();
mailbox = Buffer.from((command.attributes[arrPos] && command.attributes[arrPos].value) || '', 'binary').toString();
arrPos++;
// RETURN (SPECIAL-USE)
if (arrPos < command.attributes.length) {
if (command.attributes[arrPos].type === 'ATOM' && command.attributes[arrPos].value.toUpperCase() === 'RETURN') {
arrPos++;
if (Array.isArray(command.attributes[arrPos]) && command.attributes[arrPos].length === 1 && command.attributes[arrPos][0].type === 'ATOM' && command.attributes[arrPos][0].value.toUpperCase() === 'SPECIAL-USE') {
if (
Array.isArray(command.attributes[arrPos]) &&
command.attributes[arrPos].length === 1 &&
command.attributes[arrPos][0].type === 'ATOM' &&
command.attributes[arrPos][0].value.toUpperCase() === 'SPECIAL-USE'
) {
filterSpecialUseFlags = true;
} else {
return callback(new Error('Invalid argument provided for LIST'));
@ -110,10 +124,12 @@ module.exports = {
flags = flags.concat(folder.specialUse || []);
response.attributes.push(flags.map(flag => ({
type: 'atom',
value: flag
})));
response.attributes.push(
flags.map(flag => ({
type: 'atom',
value: flag
}))
);
response.attributes.push('/');
let path = folder.path;
@ -130,7 +146,6 @@ module.exports = {
callback(null, {
response: 'OK'
});
};
if (!mailbox && !filterSpecialUseFlags) {
@ -139,10 +154,14 @@ module.exports = {
tag: '*',
command: 'LIST',
attributes: [
[{
type: 'atom',
value: '\\Noselect'
}], '/', '/'
[
{
type: 'atom',
value: '\\Noselect'
}
],
'/',
'/'
]
};
this.send(imapHandler.compiler(response));

View file

@ -3,16 +3,18 @@
module.exports = {
state: 'Not Authenticated',
schema: [{
name: 'username',
type: 'string'
}, {
name: 'password',
type: 'string'
}],
schema: [
{
name: 'username',
type: 'string'
},
{
name: 'password',
type: 'string'
}
],
handler(command, callback) {
let username = Buffer.from((command.attributes[0].value || '').toString().trim(), 'binary').toString();
let password = Buffer.from((command.attributes[1].value || '').toString().trim(), 'binary').toString();
@ -26,13 +28,19 @@ module.exports = {
// Check if authentication method is set
if (typeof this._server.onAuth !== 'function') {
this._server.logger.info({
tnx: 'auth',
this._server.logger.info(
{
tnx: 'auth',
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
},
'[%s] Authentication failed for %s using %s',
this.id,
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
}, '[%s] Authentication failed for %s using %s', this.id, username, 'LOGIN');
'LOGIN'
);
return callback(null, {
response: 'NO',
message: 'Authentication not implemented'
@ -40,57 +48,78 @@ module.exports = {
}
// Do auth
this._server.onAuth({
method: 'LOGIN',
username,
password
}, this.session, (err, response) => {
if (err) {
if (err.response) {
return callback(null, err);
this._server.onAuth(
{
method: 'LOGIN',
username,
password
},
this.session,
(err, response) => {
if (err) {
if (err.response) {
return callback(null, err);
}
this._server.logger.info(
{
err,
tnx: 'auth',
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
},
'[%s] Authentication error for %s using %s\n%s',
this.id,
username,
'LOGIN',
err.message
);
return callback(err);
}
this._server.logger.info({
err,
tnx: 'auth',
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
}, '[%s] Authentication error for %s using %s\n%s', this.id, username, 'LOGIN', err.message);
return callback(err);
}
if (!response || !response.user) {
this._server.logger.info({
tnx: 'auth',
if (!response || !response.user) {
this._server.logger.info(
{
tnx: 'auth',
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
},
'[%s] Authentication failed for %s using %s',
this.id,
username,
'LOGIN'
);
return callback(null, {
response: 'NO',
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
});
}
this._server.logger.info(
{
tnx: 'auth',
username,
method: 'LOGIN',
action: 'success',
cid: this.id
},
'[%s] %s authenticated using %s',
this.id,
username,
method: 'LOGIN',
action: 'fail',
cid: this.id
}, '[%s] Authentication failed for %s using %s', this.id, username, 'LOGIN');
return callback(null, {
response: 'NO',
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
'LOGIN'
);
this.session.user = response.user;
this.state = 'Authenticated';
callback(null, {
response: 'OK',
message: new Buffer(username + ' authenticated').toString('binary')
});
}
this._server.logger.info({
tnx: 'auth',
username,
method: 'LOGIN',
action: 'success',
cid: this.id
}, '[%s] %s authenticated using %s', this.id, username, 'LOGIN');
this.session.user = response.user;
this.state = 'Authenticated';
callback(null, {
response: 'OK',
message: new Buffer(username + ' authenticated').toString('binary')
});
});
);
}
};

View file

@ -1,12 +1,14 @@
'use strict';
let quotes = [
const quotes = [
'All dreams are but another reality. Never forget...',
'Oh boy, oh boy, oh boy...',
'Cut the dramatics, would yeh, and follow me!',
'Oh ho ho ho, duck hunters is da cwaziest peoples! Ha ha ha.',
'Well, that makes sense. Send a bird to catch a cat!',
'Piccobello!'
'Piccobello!',
'No more Mr. Nice Duck!',
'Not bad for a duck from outer space.'
];
module.exports = {

View file

@ -1,26 +1,28 @@
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
let utf7 = require('utf7').imap;
const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag LSUB "" "%"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'reference',
type: 'string'
}, {
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'reference',
type: 'string'
},
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let reference = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let mailbox = Buffer.from(command.attributes[1] && command.attributes[1].value || '', 'binary').toString();
let reference = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let mailbox = Buffer.from((command.attributes[1] && command.attributes[1].value) || '', 'binary').toString();
// Check if LIST method is set
if (typeof this._server.onLsub !== 'function') {
@ -33,7 +35,6 @@ module.exports = {
let query = imapTools.normalizeMailbox(reference + mailbox, !this.acceptUTF8Enabled);
let lsubResponse = (err, list) => {
if (err) {
return callback(err);
}
@ -46,7 +47,7 @@ module.exports = {
let path = folder.path;
if (!this.acceptUTF8Enabled) {
path = utf7.encode(path);
}else{
} else {
path = Buffer.from(path);
}
@ -58,7 +59,8 @@ module.exports = {
type: 'atom',
value: flag
})),
'/', path
'/',
path
]
};
@ -68,7 +70,6 @@ module.exports = {
callback(null, {
response: 'OK'
});
};
if (!mailbox) {

View file

@ -1,20 +1,22 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
schema: [{
name: 'range',
type: 'sequence'
}, {
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'range',
type: 'sequence'
},
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let cmd = (command.command || '').toString().toUpperCase();
// Check if MOVE method is set
@ -25,8 +27,8 @@ module.exports = {
});
}
let range = command.attributes[0] && command.attributes[0].value || '';
let path = Buffer.from(command.attributes[1] && command.attributes[1].value || '', 'binary').toString();
let range = (command.attributes[0] && command.attributes[0].value) || '';
let path = Buffer.from((command.attributes[1] && command.attributes[1].value) || '', 'binary').toString();
let mailbox = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled);
if (!mailbox) {
@ -39,20 +41,27 @@ module.exports = {
let messages = imapTools.getMessageRange(this.selected.uidList, range, cmd === 'UID MOVE');
this._server.onMove(this.selected.mailbox, {
destination: mailbox,
messages
}, this.session, (err, success, info) => {
if (err) {
return callback(err);
this._server.onMove(
this.selected.mailbox,
{
destination: mailbox,
messages
},
this.session,
(err, success, info) => {
if (err) {
return callback(err);
}
let code = typeof success === 'string'
? success.toUpperCase()
: 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid);
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
}
let code = typeof success === 'string' ? success.toUpperCase() : 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid);
callback(null, {
response: success === true ? 'OK' : 'NO',
code
});
});
);
}
};

View file

@ -4,7 +4,6 @@ module.exports = {
state: ['Authenticated', 'Selected'],
handler(command, callback) {
// fixed structre
this.send('* NAMESPACE (("" "/")) NIL NIL');

View file

@ -1,24 +1,26 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
// tag RENAME "mailbox"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}, {
name: 'newname',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
},
{
name: 'newname',
type: 'string'
}
],
handler(command, callback) {
let mailbox = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let newname = Buffer.from(command.attributes[1] && command.attributes[1].value || '', 'binary').toString();
let mailbox = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let newname = Buffer.from((command.attributes[1] && command.attributes[1].value) || '', 'binary').toString();
// Check if RENAME method is set
if (typeof this._server.onRename !== 'function') {
@ -75,8 +77,6 @@ module.exports = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
}
};

View file

@ -1,7 +1,7 @@
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
@ -9,7 +9,6 @@ module.exports = {
schema: false, // recursive, can't predefine
handler(command, callback) {
// Check if SEARCH method is set
if (typeof this._server.onSearch !== 'function') {
return callback(null, {
@ -44,73 +43,78 @@ module.exports = {
this.condstoreEnabled = this.selected.condstoreEnabled = true;
}
this._server.onSearch(this.selected.mailbox, {
query: parsed.query,
terms: parsed.terms,
isUid
}, this.session, (err, results) => {
if (err) {
return callback(err);
}
this._server.onSearch(
this.selected.mailbox,
{
query: parsed.query,
terms: parsed.terms,
isUid
},
this.session,
(err, results) => {
if (err) {
return callback(err);
}
let matches = results.uidList;
let matches = results.uidList;
if (typeof matches === 'string') {
return callback(null, {
response: 'NO',
code: matches.toUpperCase()
});
}
if (typeof matches === 'string') {
return callback(null, {
response: 'NO',
code: matches.toUpperCase()
});
}
let response = {
tag: '*',
command: 'SEARCH',
attributes: []
};
let response = {
tag: '*',
command: 'SEARCH',
attributes: []
};
if (Array.isArray(matches) && matches.length) {
matches.sort((a, b) => (a - b));
if (Array.isArray(matches) && matches.length) {
matches.sort((a, b) => a - b);
matches.forEach(nr => {
let seq;
matches.forEach(nr => {
let seq;
if (!isUid) {
seq = this.selected.uidList.indexOf(nr) + 1;
if (seq) {
if (!isUid) {
seq = this.selected.uidList.indexOf(nr) + 1;
if (seq) {
response.attributes.push({
type: 'atom',
value: String(seq)
});
}
} else {
response.attributes.push({
type: 'atom',
value: String(seq)
value: String(nr)
});
}
} else {
response.attributes.push({
});
}
// append (MODSEQ 123) for queries that include MODSEQ criteria
if (results.highestModseq && parsed.terms.indexOf('modseq') >= 0) {
response.attributes.push([
{
type: 'atom',
value: String(nr)
});
}
value: 'MODSEQ'
},
{
type: 'atom',
value: String(results.highestModseq)
}
]);
}
this.send(imapHandler.compiler(response));
return callback(null, {
response: 'OK'
});
}
// append (MODSEQ 123) for queries that include MODSEQ criteria
if (results.highestModseq && parsed.terms.indexOf('modseq') >= 0) {
response.attributes.push(
[{
type: 'atom',
value: 'MODSEQ'
}, {
type: 'atom',
value: String(results.highestModseq)
}]
);
}
this.send(imapHandler.compiler(response));
return callback(null, {
response: 'OK'
});
});
);
},
parseQueryTerms // expose for testing
@ -189,7 +193,6 @@ function parseQueryTerms(terms, uidList) {
};
switch (response.key) {
case 'not':
// make sure not is not an array, instead return several 'not' expressions
response = [].concat(curTerm[1] || []).map(val => ({
@ -266,7 +269,7 @@ function normalizeTerm(term, mapping) {
if (result[0] === 'flag') {
flags = [];
result.forEach((val, i) => {
if (i && (i % 2 !== 0)) {
if (i && i % 2 !== 0) {
flags.push({
key: 'flag',
value: val,

View file

@ -1,7 +1,7 @@
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
// tag SELECT "mailbox"
// tag EXAMINE "mailbox"
@ -9,23 +9,23 @@ let imapTools = require('../imap-tools');
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}, {
name: 'extensions',
type: 'array',
optional: true
}],
schema: [
{
name: 'mailbox',
type: 'string'
},
{
name: 'extensions',
type: 'array',
optional: true
}
],
handler(command, callback) {
let path = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let path = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let mailbox = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled);
let extensions = [].
concat(command.attributes[1] || []).
map(attr => (attr && attr.value || '').toString().toUpperCase());
let extensions = [].concat(command.attributes[1] || []).map(attr => ((attr && attr.value) || '').toString().toUpperCase());
// Is CONDSTORE found from the optional arguments list?
if (extensions.indexOf('CONDSTORE') >= 0) {
@ -77,61 +77,78 @@ module.exports = {
let flagList = imapTools.systemFlagsFormatted.concat(folder.flags || []);
// * FLAGS (\Answered \Flagged \Draft \Deleted \Seen)
this.send(imapHandler.compiler({
tag: '*',
command: 'FLAGS',
attributes: [
flagList.map(flag => ({
type: 'atom',
value: flag
}))
]
}));
// * OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen \*)] Flags permitted
this.send(imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [{
type: 'section',
section: [
// unrelated comment to enforce eslint-happy indentation
{
type: 'atom',
value: 'PERMANENTFLAGS'
},
this.send(
imapHandler.compiler({
tag: '*',
command: 'FLAGS',
attributes: [
flagList.map(flag => ({
type: 'atom',
value: flag
})).concat({
type: 'text',
value: '\\*'
})
}))
]
}, {
type: 'text',
value: 'Flags permitted'
}]
}));
})
);
// * OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen \*)] Flags permitted
this.send(
imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [
{
type: 'section',
section: [
// unrelated comment to enforce eslint-happy indentation
{
type: 'atom',
value: 'PERMANENTFLAGS'
},
flagList
.map(flag => ({
type: 'atom',
value: flag
}))
.concat({
type: 'text',
value: '\\*'
})
]
},
{
type: 'text',
value: 'Flags permitted'
}
]
})
);
// * OK [UIDVALIDITY 123] UIDs valid
this.send(imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [{
type: 'section',
section: [{
type: 'atom',
value: 'UIDVALIDITY'
}, {
type: 'atom',
value: String(Number(folder.uidValidity) || 1)
}]
}, {
type: 'text',
value: 'UIDs valid'
}]
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [
{
type: 'section',
section: [
{
type: 'atom',
value: 'UIDVALIDITY'
},
{
type: 'atom',
value: String(Number(folder.uidValidity) || 1)
}
]
},
{
type: 'text',
value: 'UIDs valid'
}
]
})
);
// * 0 EXISTS
this.send('* ' + folder.uidList.length + ' EXISTS');
@ -141,43 +158,59 @@ module.exports = {
// * OK [HIGHESTMODSEQ 123]
if ('modifyIndex' in folder && Number(folder.modifyIndex)) {
this.send(imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [{
type: 'section',
section: [{
type: 'atom',
value: 'HIGHESTMODSEQ'
}, {
type: 'atom',
value: String(Number(folder.modifyIndex) || 0)
}]
}, {
type: 'text',
value: 'Highest'
}]
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [
{
type: 'section',
section: [
{
type: 'atom',
value: 'HIGHESTMODSEQ'
},
{
type: 'atom',
value: String(Number(folder.modifyIndex) || 0)
}
]
},
{
type: 'text',
value: 'Highest'
}
]
})
);
}
// * OK [UIDNEXT 1] Predicted next UID
this.send(imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [{
type: 'section',
section: [{
type: 'atom',
value: 'UIDNEXT'
}, {
type: 'atom',
value: String(Number(folder.uidNext) || 1)
}]
}, {
type: 'text',
value: 'Predicted next UID'
}]
}));
this.send(
imapHandler.compiler({
tag: '*',
command: 'OK',
attributes: [
{
type: 'section',
section: [
{
type: 'atom',
value: 'UIDNEXT'
},
{
type: 'atom',
value: String(Number(folder.uidNext) || 1)
}
]
},
{
type: 'text',
value: 'Predicted next UID'
}
]
})
);
// start listening for EXPUNGE, EXISTS and FETCH FLAGS notifications
this.updateNotificationListener(() => {
@ -187,7 +220,6 @@ module.exports = {
message: command.command + ' completed' + (this.selected.condstoreEnabled ? ', CONDSTORE is now enabled' : '')
});
});
});
}
};

View file

@ -3,13 +3,16 @@
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'quotaroot',
type: 'string'
}, {
name: 'limits',
type: 'array'
}],
schema: [
{
name: 'quotaroot',
type: 'string'
},
{
name: 'limits',
type: 'array'
}
],
handler(command, callback) {
callback(null, {

View file

@ -2,10 +2,10 @@
// openssl s_client -starttls imap -crlf -connect localhost:1143
let tls = require('tls');
let tlsOptions = require('../tls-options');
const tls = require('tls');
const tlsOptions = require('../tls-options');
let SOCKET_TIMEOUT = 30 * 60 * 1000;
const SOCKET_TIMEOUT = 30 * 60 * 1000;
module.exports = {
handler(command, callback) {
@ -63,10 +63,14 @@ function upgrade(connection) {
connection._socket = secureSocket;
connection._upgrading = false;
connection._server.logger.info({
tnx: 'starttls',
cid: connection.id
}, '[%s] Connection upgraded to TLS', connection.id);
connection._server.logger.info(
{
tnx: 'starttls',
cid: connection.id
},
'[%s] Connection upgraded to TLS',
connection.id
);
connection._socket.pipe(connection._parser);
connection.writeStream.pipe(connection._socket);
});

View file

@ -1,24 +1,26 @@
'use strict';
let imapTools = require('../imap-tools');
let imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools');
const imapHandler = require('../handler/imap-handler');
// tag STATUS "mailbox" (UNSEEN UIDNEXT)
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}, {
name: 'query',
type: 'array'
}],
schema: [
{
name: 'mailbox',
type: 'string'
},
{
name: 'query',
type: 'array'
}
],
handler(command, callback) {
let mailbox = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let mailbox = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let query = command.attributes[1] && command.attributes[1];
let statusElements = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN', 'HIGHESTMODSEQ'];
@ -59,7 +61,7 @@ module.exports = {
// check if only known status items are used
for (let i = 0, len = query.length; i < len; i++) {
statusItem = (query[i] && query[i].value || '').toString().toUpperCase();
statusItem = ((query[i] && query[i].value) || '').toString().toUpperCase();
if (statusElements.indexOf(statusItem) < 0) {
return callback(null, {
response: 'BAD',
@ -128,8 +130,6 @@ module.exports = {
callback(null, {
response: 'OK'
});
});
}
};

View file

@ -1,28 +1,32 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
disableNotifications: true,
schema: [{
name: 'range',
type: 'sequence'
}, {
name: 'extensions',
type: 'array',
optional: true
}, {
name: 'action',
type: 'string'
}, {
name: 'flags',
type: 'array'
}],
schema: [
{
name: 'range',
type: 'sequence'
},
{
name: 'extensions',
type: 'array',
optional: true
},
{
name: 'action',
type: 'string'
},
{
name: 'flags',
type: 'array'
}
],
handler(command, callback) {
// Check if STORE method is set
if (typeof this._server.onStore !== 'function') {
return callback(null, {
@ -40,24 +44,20 @@ module.exports = {
}
let type = 'flags'; // currently hard coded, in the future might support other values as well, eg. X-GM-LABELS
let range = command.attributes[0] && command.attributes[0].value || '';
let range = (command.attributes[0] && command.attributes[0].value) || '';
// if arguments include extenstions at index 1, then length is 4, otherwise 3
let pos = command.attributes.length === 4 ? 1 : 0;
let action = (command.attributes[pos + 1] && command.attributes[pos + 1].value || '').toString().toUpperCase();
let action = ((command.attributes[pos + 1] && command.attributes[pos + 1].value) || '').toString().toUpperCase();
let flags = [].
concat(command.attributes[pos + 2] || []).
map(flag => (flag && flag.value || '').toString());
let flags = [].concat(command.attributes[pos + 2] || []).map(flag => ((flag && flag.value) || '').toString());
let unchangedSince = 0;
let silent = false;
// extensions are available as the optional argument at index 1
let extensions = !pos ? [] : [].
concat(command.attributes[pos] || []).
map(val => (val && val.value));
let extensions = !pos ? [] : [].concat(command.attributes[pos] || []).map(val => val && val.value);
if (extensions.length) {
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'UNCHANGEDSINCE' || isNaN(extensions[1])) {
@ -117,56 +117,61 @@ module.exports = {
let messages = imapTools.getMessageRange(this.selected.uidList, range, false);
this._server.onStore(this.selected.mailbox, {
value: flags,
action,
type,
silent,
messages,
unchangedSince
}, this.session, (err, success, modified) => {
if (err) {
return callback(err);
}
this._server.onStore(
this.selected.mailbox,
{
value: flags,
action,
type,
silent,
messages,
unchangedSince
},
this.session,
(err, success, modified) => {
if (err) {
return callback(err);
}
// STORE returns MODIFIED as sequence numbers, so convert UIDs to sequence list
if (modified && modified.length) {
modified = modified.
map(uid => this.selected.uidList.indexOf(uid) + 1).
filter(seq =>
// ensure that deleted items (eg seq=0) do not end up in the list
seq > 0
);
}
// STORE returns MODIFIED as sequence numbers, so convert UIDs to sequence list
if (modified && modified.length) {
modified = modified.map(uid => this.selected.uidList.indexOf(uid) + 1).filter(
seq =>
// ensure that deleted items (eg seq=0) do not end up in the list
seq > 0
);
}
let message = success === true ? 'STORE completed' : false;
if (modified && modified.length) {
message = 'Conditional STORE failed';
} else if (message && unchangedSince) {
message = 'Conditional STORE completed';
}
let message = success === true ? 'STORE completed' : false;
if (modified && modified.length) {
message = 'Conditional STORE failed';
} else if (message && unchangedSince) {
message = 'Conditional STORE completed';
}
let response = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : (modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false),
message
};
let response = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string'
? success.toUpperCase()
: modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false,
message
};
// check if only messages that exist are referenced
if (!this._server.options.allowStoreExpunged && success === true && !silent && messages.length) {
for (let i = this.selected.notifications.length - 1; i >= 0; i--) {
if (this.selected.notifications[i].command === 'EXPUNGE' && messages.indexOf(this.selected.notifications[i].uid) >= 0) {
response = {
response: 'NO',
message: 'Some of the messages no longer exist'
};
break;
// check if only messages that exist are referenced
if (!this._server.options.allowStoreExpunged && success === true && !silent && messages.length) {
for (let i = this.selected.notifications.length - 1; i >= 0; i--) {
if (this.selected.notifications[i].command === 'EXPUNGE' && messages.indexOf(this.selected.notifications[i].uid) >= 0) {
response = {
response: 'NO',
message: 'Some of the messages no longer exist'
};
break;
}
}
}
callback(null, response);
}
callback(null, response);
});
);
}
};

View file

@ -1,20 +1,21 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
// tag SUBSCRIBE "mailbox"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let path = Buffer.from(command.attributes[0] && command.attributes[0].value || '', 'binary').toString();
let path = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
let mailbox = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled);
// Check if SUBSCRIBE method is set
@ -48,8 +49,6 @@ module.exports = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
}
};

View file

@ -1,17 +1,18 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
schema: [{
name: 'range',
type: 'sequence'
}],
schema: [
{
name: 'range',
type: 'sequence'
}
],
handler(command, callback) {
// Check if EXPUNGE method is set
if (typeof this._server.onExpunge !== 'function') {
return callback(null, {
@ -27,24 +28,29 @@ module.exports = {
});
}
let range = command.attributes[0] && command.attributes[0].value || '';
let range = (command.attributes[0] && command.attributes[0].value) || '';
if (!imapTools.validateSequnce(range)) {
return callback(new Error('Invalid sequence set for UID EXPUNGE'));
}
let messages = imapTools.getMessageRange(this.selected.uidList, range, true);
this._server.onExpunge(this.selected.mailbox, {
isUid: true,
messages
}, this.session, (err, success) => {
if (err) {
return callback(err);
}
this._server.onExpunge(
this.selected.mailbox,
{
isUid: true,
messages
},
this.session,
(err, success) => {
if (err) {
return callback(err);
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
}
);
}
};

View file

@ -1,27 +1,31 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
schema: [{
name: 'range',
type: 'sequence'
}, {
name: 'extensions',
type: 'array',
optional: true
}, {
name: 'action',
type: 'string'
}, {
name: 'flags',
type: 'array'
}],
schema: [
{
name: 'range',
type: 'sequence'
},
{
name: 'extensions',
type: 'array',
optional: true
},
{
name: 'action',
type: 'string'
},
{
name: 'flags',
type: 'array'
}
],
handler(command, callback) {
// Check if STORE method is set
if (typeof this._server.onStore !== 'function') {
return callback(null, {
@ -31,24 +35,20 @@ module.exports = {
}
let type = 'flags'; // currently hard coded, in the future might support other values as well, eg. X-GM-LABELS
let range = command.attributes[0] && command.attributes[0].value || '';
let range = (command.attributes[0] && command.attributes[0].value) || '';
// if arguments include extenstions at index 1, then length is 4, otherwise 3
let pos = command.attributes.length === 4 ? 1 : 0;
let action = (command.attributes[pos + 1] && command.attributes[pos + 1].value || '').toString().toUpperCase();
let action = ((command.attributes[pos + 1] && command.attributes[pos + 1].value) || '').toString().toUpperCase();
let flags = [].
concat(command.attributes[pos + 2] || []).
map(flag => (flag && flag.value || '').toString());
let flags = [].concat(command.attributes[pos + 2] || []).map(flag => ((flag && flag.value) || '').toString());
let unchangedSince = 0;
let silent = false;
// extensions are available as the optional argument at index 1
let extensions = !pos ? [] : [].
concat(command.attributes[pos] || []).
map(val => (val && val.value));
let extensions = !pos ? [] : [].concat(command.attributes[pos] || []).map(val => val && val.value);
if (extensions.length) {
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'UNCHANGEDSINCE' || isNaN(extensions[1])) {
@ -105,32 +105,38 @@ module.exports = {
let messages = imapTools.getMessageRange(this.selected.uidList, range, true);
this._server.onStore(this.selected.mailbox, {
isUid: true,
value: flags,
action,
type,
silent,
messages,
unchangedSince
}, this.session, (err, success, modified) => {
if (err) {
return callback(err);
this._server.onStore(
this.selected.mailbox,
{
isUid: true,
value: flags,
action,
type,
silent,
messages,
unchangedSince
},
this.session,
(err, success, modified) => {
if (err) {
return callback(err);
}
let message = success === true ? 'UID STORE completed' : false;
if (modified && modified.length) {
message = 'Conditional UID STORE failed';
} else if (message && unchangedSince) {
message = 'Conditional UID STORE completed';
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string'
? success.toUpperCase()
: modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false,
message
});
}
let message = success === true ? 'UID STORE completed' : false;
if (modified && modified.length) {
message = 'Conditional UID STORE failed';
} else if (message && unchangedSince) {
message = 'Conditional UID STORE completed';
}
callback(null, {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : (modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false),
message
});
});
);
}
};

View file

@ -1,20 +1,21 @@
'use strict';
let imapTools = require('../imap-tools');
const imapTools = require('../imap-tools');
// tag UNSUBSCRIBE "mailbox"
module.exports = {
state: ['Authenticated', 'Selected'],
schema: [{
name: 'mailbox',
type: 'string'
}],
schema: [
{
name: 'mailbox',
type: 'string'
}
],
handler(command, callback) {
let mailbox = imapTools.normalizeMailbox(command.attributes[0] && command.attributes[0].value || '', !this.acceptUTF8Enabled);
let mailbox = imapTools.normalizeMailbox((command.attributes[0] && command.attributes[0].value) || '', !this.acceptUTF8Enabled);
// Check if UNSUBSCRIBE method is set
if (typeof this._server.onUnsubscribe !== 'function') {
@ -48,8 +49,6 @@ module.exports = {
response: success === true ? 'OK' : 'NO',
code: typeof success === 'string' ? success.toUpperCase() : false
});
});
}
};

View file

@ -2,15 +2,15 @@
'use strict';
let imapFormalSyntax = require('./imap-formal-syntax');
let streams = require('stream');
let PassThrough = streams.PassThrough;
let LengthLimiter = require('../length-limiter');
const imapFormalSyntax = require('./imap-formal-syntax');
const streams = require('stream');
const PassThrough = streams.PassThrough;
const LengthLimiter = require('../length-limiter');
/**
* Compiles an input object into a streamed IMAP response
*/
module.exports = function (response, isLogging) {
module.exports = function(response, isLogging) {
let output = new PassThrough();
let resp = (response.tag || '') + (response.command ? ' ' + response.command : '');
@ -22,7 +22,7 @@ module.exports = function (response, isLogging) {
let queue = [];
let ended = false;
let emit = function (stream, expectedLength, startFrom, maxLength) {
let emit = function(stream, expectedLength, startFrom, maxLength) {
expectedLength = expectedLength || 0;
startFrom = startFrom || 0;
maxLength = maxLength || 0;
@ -92,7 +92,7 @@ module.exports = function (response, isLogging) {
}
};
let walk = function (node, callback) {
let walk = function(node, callback) {
if (lastType === 'LITERAL' || (['(', '<', '['].indexOf((resp || lr).substr(-1)) < 0 && (resp || lr).length)) {
resp += ' ';
}
@ -145,43 +145,42 @@ module.exports = function (response, isLogging) {
}
switch (node.type.toUpperCase()) {
case 'LITERAL':
{
let nval = node.value;
case 'LITERAL': {
let nval = node.value;
if (typeof nval === 'number') {
nval = nval.toString();
if (typeof nval === 'number') {
nval = nval.toString();
}
let len;
if (nval && typeof nval.pipe === 'function') {
len = node.expectedLength || 0;
if (node.startFrom) {
len -= node.startFrom;
}
if (node.maxLength) {
len = Math.min(len, node.maxLength);
}
} else {
len = (nval || '').toString().length;
}
let len;
if (isLogging) {
resp += '"(* ' + len + 'B literal *)"';
} else {
resp += '{' + len + '}\r\n';
emit();
if (nval && typeof nval.pipe === 'function') {
len = node.expectedLength || 0;
if (node.startFrom) {
len -= node.startFrom;
}
if (node.maxLength) {
len = Math.min(len, node.maxLength);
}
//value is a stream object
emit(nval, node.expectedLength, node.startFrom, node.maxLength);
} else {
len = (nval || '').toString().length;
resp = (nval || '').toString('binary');
}
if (isLogging) {
resp += '"(* ' + len + 'B literal *)"';
} else {
resp += '{' + len + '}\r\n';
emit();
if (nval && typeof nval.pipe === 'function') {
//value is a stream object
emit(nval, node.expectedLength, node.startFrom, node.maxLength);
} else {
resp = (nval || '').toString('binary');
}
}
break;
}
break;
}
case 'STRING':
if (isLogging && node.value.length > 20) {
resp += '"(* ' + node.value.length + 'B string *)"';
@ -195,44 +194,43 @@ module.exports = function (response, isLogging) {
break;
case 'NUMBER':
resp += (node.value || 0);
resp += node.value || 0;
break;
case 'ATOM':
case 'SECTION':
{
val = (node.value || '').toString('binary');
case 'SECTION': {
val = (node.value || '').toString('binary');
if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
val = JSON.stringify(val);
if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
val = JSON.stringify(val);
}
resp += val;
let finalize = () => {
if (node.partial) {
resp += '<' + node.partial.join('.') + '>';
}
setImmediate(callback);
};
resp += val;
if (node.section) {
resp += '[';
let finalize = () => {
if (node.partial) {
resp += '<' + node.partial.join('.') + '>';
let pos = 0;
let next = () => {
if (pos >= node.section.length) {
resp += ']';
return setImmediate(finalize);
}
setImmediate(callback);
walk(node.section[pos++], next);
};
if (node.section) {
resp += '[';
let pos = 0;
let next = () => {
if (pos >= node.section.length) {
resp += ']';
return setImmediate(finalize);
}
walk(node.section[pos++], next);
};
return setImmediate(next);
}
return finalize();
return setImmediate(next);
}
return finalize();
}
}
setImmediate(callback);
};

View file

@ -2,18 +2,17 @@
'use strict';
let imapFormalSyntax = require('./imap-formal-syntax');
const imapFormalSyntax = require('./imap-formal-syntax');
/**
* Compiles an input object into
*/
module.exports = function (response, asArray, isLogging) {
module.exports = function(response, asArray, isLogging) {
let respParts = [];
let resp = (response.tag || '') + (response.command ? ' ' + response.command : '');
let val;
let lastType;
let walk = function (node) {
let walk = function(node) {
if (lastType === 'LITERAL' || (['(', '<', '['].indexOf(resp.substr(-1)) < 0 && resp.length)) {
resp += ' ';
}
@ -85,7 +84,7 @@ module.exports = function (response, asArray, isLogging) {
break;
case 'NUMBER':
resp += (node.value || 0);
resp += node.value || 0;
break;
case 'ATOM':
@ -108,7 +107,6 @@ module.exports = function (response, asArray, isLogging) {
}
break;
}
};
[].concat(response.attributes || []).forEach(walk);

View file

@ -24,121 +24,119 @@ function excludeChars(source, exclude) {
}
module.exports = {
CHAR: function () {
let value = expandRange(0x01, 0x7F);
this.CHAR = function () {
CHAR: function() {
let value = expandRange(0x01, 0x7f);
this.CHAR = function() {
return value;
};
return value;
},
CHAR8: function () {
let value = expandRange(0x01, 0xFF);
this.CHAR8 = function () {
CHAR8: function() {
let value = expandRange(0x01, 0xff);
this.CHAR8 = function() {
return value;
};
return value;
},
SP: function () {
SP: function() {
return ' ';
},
CTL: function () {
let value = expandRange(0x00, 0x1F) + '\x7F';
this.CTL = function () {
CTL: function() {
let value = expandRange(0x00, 0x1f) + '\x7F';
this.CTL = function() {
return value;
};
return value;
},
DQUOTE: function () {
DQUOTE: function() {
return '"';
},
ALPHA: function () {
let value = expandRange(0x41, 0x5A) + expandRange(0x61, 0x7A);
this.ALPHA = function () {
ALPHA: function() {
let value = expandRange(0x41, 0x5a) + expandRange(0x61, 0x7a);
this.ALPHA = function() {
return value;
};
return value;
},
DIGIT: function () {
let value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7A);
this.DIGIT = function () {
DIGIT: function() {
let value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7a);
this.DIGIT = function() {
return value;
};
return value;
},
'ATOM-CHAR': function () {
'ATOM-CHAR': function() {
let value = excludeChars(this.CHAR(), this['atom-specials']());
this['ATOM-CHAR'] = function () {
this['ATOM-CHAR'] = function() {
return value;
};
return value;
},
'ASTRING-CHAR': function () {
'ASTRING-CHAR': function() {
let value = this['ATOM-CHAR']() + this['resp-specials']();
this['ASTRING-CHAR'] = function () {
this['ASTRING-CHAR'] = function() {
return value;
};
return value;
},
'TEXT-CHAR': function () {
'TEXT-CHAR': function() {
let value = excludeChars(this.CHAR(), '\r\n');
this['TEXT-CHAR'] = function () {
this['TEXT-CHAR'] = function() {
return value;
};
return value;
},
'atom-specials': function () {
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() +
this['quoted-specials']() + this['resp-specials']();
this['atom-specials'] = function () {
'atom-specials': function() {
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + this['quoted-specials']() + this['resp-specials']();
this['atom-specials'] = function() {
return value;
};
return value;
},
'list-wildcards': function () {
'list-wildcards': function() {
return '%' + '*';
},
'quoted-specials': function () {
'quoted-specials': function() {
let value = this.DQUOTE() + '\\';
this['quoted-specials'] = function () {
this['quoted-specials'] = function() {
return value;
};
return value;
},
'resp-specials': function () {
'resp-specials': function() {
return ']';
},
tag: function () {
tag: function() {
let value = excludeChars(this['ASTRING-CHAR'](), '+');
this.tag = function () {
this.tag = function() {
return value;
};
return value;
},
command: function () {
command: function() {
let value = this.ALPHA() + this.DIGIT();
this.command = function () {
this.command = function() {
return value;
};
return value;
},
verify: function (str, allowedChars) {
verify: function(str, allowedChars) {
for (let i = 0, len = str.length; i < len; i++) {
if (allowedChars.indexOf(str.charAt(i)) < 0) {
return i;

View file

@ -1,8 +1,8 @@
'use strict';
let parser = require('./imap-parser');
let compiler = require('./imap-compiler');
let compileStream = require('./imap-compile-stream');
const parser = require('./imap-parser');
const compiler = require('./imap-compiler');
const compileStream = require('./imap-compile-stream');
module.exports = {
parser,

View file

@ -2,10 +2,9 @@
'use strict';
let imapFormalSyntax = require('./imap-formal-syntax');
const imapFormalSyntax = require('./imap-formal-syntax');
class TokenParser {
constructor(parent, startPos, str, options) {
this.str = (str || '').toString();
this.options = options || {};
@ -22,10 +21,9 @@ class TokenParser {
}
getAttributes() {
let attributes = [],
branch = attributes;
let attributes = [], branch = attributes;
let walk = function (node) {
let walk = function(node) {
let curBranch = branch;
let elm;
let partial;
@ -114,24 +112,22 @@ class TokenParser {
}
processString() {
let chr, i, len,
checkSP = function () {
let chr,
i,
len,
checkSP = function() {
// jump to the next non whitespace pos
while (this.str.charAt(i + 1) === ' ') {
i++;
}
}.bind(this);
for (i = 0, len = this.str.length; i < len; i++) {
for ((i = 0), (len = this.str.length); i < len; i++) {
chr = this.str.charAt(i);
switch (this.state) {
case 'NORMAL':
switch (chr) {
// DQUOTE starts a new string
case '"':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
@ -139,15 +135,13 @@ class TokenParser {
this.state = 'STRING';
this.currentNode.closed = false;
break;
// ( starts a new list
// ( starts a new list
case '(':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LIST';
this.currentNode.closed = false;
break;
// ) closes a list
// ) closes a list
case ')':
if (this.currentNode.type !== 'LIST') {
throw new Error('Unexpected list terminator ) at position ' + (this.pos + i));
@ -159,8 +153,7 @@ class TokenParser {
checkSP();
break;
// ] closes section group
// ] closes section group
case ']':
if (this.currentNode.type !== 'SECTION') {
throw new Error('Unexpected section terminator ] at position ' + (this.pos + i));
@ -170,8 +163,7 @@ class TokenParser {
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// < starts a new partial
// < starts a new partial
case '<':
if (this.str.charAt(i - 1) !== ']') {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
@ -185,16 +177,14 @@ class TokenParser {
this.currentNode.closed = false;
}
break;
// { starts a new literal
// { starts a new literal
case '{':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LITERAL';
this.state = 'LITERAL';
this.currentNode.closed = false;
break;
// ( starts a new sequence
// ( starts a new sequence
case '*':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SEQUENCE';
@ -202,13 +192,11 @@ class TokenParser {
this.currentNode.closed = false;
this.state = 'SEQUENCE';
break;
// normally a space should never occur
// normally a space should never occur
case ' ':
// just ignore
break;
// [ starts section
// [ starts section
case '[':
// If it is the *first* element after response command, then process as a response argument list
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(this.parent.command.toUpperCase()) >= 0 && this.currentNode === this.tree) {
@ -241,8 +229,7 @@ class TokenParser {
// jump i to the ']'
i = this.str.indexOf(']', i + 10);
this.currentNode.endPos = this.pos + i - 1;
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos,
this.currentNode.endPos - this.pos + 1);
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
this.currentNode = this.currentNode.parentNode;
// close out the SECTION
@ -253,7 +240,7 @@ class TokenParser {
break;
}
/* falls through */
/* falls through */
default:
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
// Allow \ as the first char for atom to support system flags
@ -271,7 +258,6 @@ class TokenParser {
break;
case 'ATOM':
// space finishes an atom
if (chr === ' ') {
this.currentNode.endPos = this.pos + i - 1;
@ -283,10 +269,7 @@ class TokenParser {
//
if (
this.currentNode.parentNode &&
(
(chr === ')' && this.currentNode.parentNode.type === 'LIST') ||
(chr === ']' && this.currentNode.parentNode.type === 'SECTION')
)
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
) {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
@ -335,7 +318,6 @@ class TokenParser {
break;
case 'STRING':
// DQUOTE ends the string sequence
if (chr === '"') {
this.currentNode.endPos = this.pos + i;
@ -466,9 +448,7 @@ class TokenParser {
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
break;
} else if (this.currentNode.parentNode &&
chr === ']' &&
this.currentNode.parentNode.type === 'SECTION') {
} else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
@ -509,12 +489,9 @@ class TokenParser {
}
}
}
}
class ParserInstance {
constructor(input, options) {
this.input = (input || '').toString();
this.options = options || {};
@ -604,7 +581,7 @@ class ParserInstance {
}
}
module.exports = function (command, options) {
module.exports = function(command, options) {
let parser, response = {};
options = options || {};

View file

@ -1,10 +1,10 @@
'use strict';
let imapHandler = require('./handler/imap-handler');
const imapHandler = require('./handler/imap-handler');
const MAX_MESSAGE_SIZE = 1 * 1024 * 1024;
let commands = new Map([
const commands = new Map([
/*eslint-disable global-require*/
// require must normally be on top of the module
['NOOP', require('./commands/noop')],
@ -51,7 +51,6 @@ let commands = new Map([
]);
class IMAPCommand {
constructor(connection) {
this.connection = connection;
this.payload = '';
@ -87,7 +86,6 @@ class IMAPCommand {
}
if (command.literal) {
// check if the literal size is in acceptable bounds
if (isNaN(command.expecting) || isNaN(command.expecting) < 0 || command.expecting > Number.MAX_SAFE_INTEGER) {
this.connection.send(this.tag + ' BAD Invalid literal size');
@ -99,12 +97,17 @@ class IMAPCommand {
// Allow large literals for selected commands only
(!['APPEND'].includes(this.command) && command.expecting > 1024) ||
// Deny all literals bigger than maxMessage
command.expecting > maxAllowed) {
this.connection._server.logger.debug({
tnx: 'client',
cid: this.connection.id
}, '[%s] C:', this.connection.id, this.payload);
command.expecting > maxAllowed
) {
this.connection._server.logger.debug(
{
tnx: 'client',
cid: this.connection.id
},
'[%s] C:',
this.connection.id,
this.payload
);
this.payload = ''; // reset payload
@ -153,21 +156,31 @@ class IMAPCommand {
// check if the payload needs to be directod to a preset handler
if (typeof this.connection._nextHandler === 'function') {
this.connection._server.logger.debug({
tnx: 'client',
cid: this.connection.id
}, '[%s] C: <%s bytes of data>', this.connection.id, this.payload && this.payload.length || 0);
this.connection._server.logger.debug(
{
tnx: 'client',
cid: this.connection.id
},
'[%s] C: <%s bytes of data>',
this.connection.id,
(this.payload && this.payload.length) || 0
);
return this.connection._nextHandler(this.payload, next);
}
try {
this.parsed = imapHandler.parser(this.payload);
} catch (E) {
this.connection._server.logger.debug({
err: E,
tnx: 'client',
cid: this.connection.id
}, '[%s] C:', this.connection.id, this.payload);
this.connection._server.logger.debug(
{
err: E,
tnx: 'client',
cid: this.connection.id
},
'[%s] C:',
this.connection.id,
this.payload
);
this.connection.send(this.tag + ' BAD ' + E.message);
return next();
}
@ -182,10 +195,15 @@ class IMAPCommand {
});
}
this.connection._server.logger.debug({
tnx: 'client',
cid: this.connection.id
}, '[%s] C:', this.connection.id, imapHandler.compiler(this.parsed, false, true));
this.connection._server.logger.debug(
{
tnx: 'client',
cid: this.connection.id
},
'[%s] C:',
this.connection.id,
imapHandler.compiler(this.parsed, false, true)
);
this.validateCommand(this.parsed, handler, err => {
if (err) {
@ -194,34 +212,46 @@ class IMAPCommand {
}
if (typeof handler.handler === 'function') {
handler.handler.call(this.connection, this.parsed, (err, response) => {
if (err) {
this.connection.send(this.tag + ' ' + (err.response || 'BAD') + ' ' + err.message);
return next(err);
}
handler.handler.call(
this.connection,
this.parsed,
(err, response) => {
if (err) {
this.connection.send(this.tag + ' ' + (err.response || 'BAD') + ' ' + err.message);
return next(err);
}
// send EXPUNGE, EXISTS etc queued notices
this.sendNotifications(handler, () => {
// send EXPUNGE, EXISTS etc queued notices
this.sendNotifications(handler, () => {
// send command ready response
this.connection.writeStream.write({
tag: this.tag,
command: response.response,
attributes: []
.concat(
response.code
? {
type: 'SECTION',
section: [
{
type: 'TEXT',
value: response.code
}
]
}
: []
)
.concat({
type: 'TEXT',
value: response.message || this.command + ' completed'
})
});
// send command ready response
this.connection.writeStream.write({
tag: this.tag,
command: response.response,
attributes: [].concat(response.code ? {
type: 'SECTION',
section: [{
type: 'TEXT',
value: response.code
}]
} : []).concat({
type: 'TEXT',
value: response.message || this.command + ' completed'
})
next();
});
next();
});
}, next);
},
next
);
} else {
this.connection.send(this.tag + ' NO Not implemented: ' + this.command);
return next();
@ -262,13 +292,12 @@ class IMAPCommand {
}
// Deny commands with too little arguments
if ((parsed.attributes && parsed.attributes.length || 0) < minArgs) {
if (((parsed.attributes && parsed.attributes.length) || 0) < minArgs) {
return callback(new Error('Not enough arguments provided'));
}
callback();
}
}
module.exports.IMAPCommand = IMAPCommand;

View file

@ -1,10 +1,9 @@
'use strict';
let imapHandler = require('./handler/imap-handler');
let Transform = require('stream').Transform;
const imapHandler = require('./handler/imap-handler');
const Transform = require('stream').Transform;
class IMAPComposer extends Transform {
constructor(options) {
super();
Transform.call(this, {
@ -20,10 +19,15 @@ class IMAPComposer extends Transform {
if (typeof obj.pipe === 'function') {
// pipe stream to socket and wait until it finishes before continuing
this.connection._server.logger.debug({
tnx: 'pipeout',
cid: this.connection.id
}, '[%s] S: %s<pipe message stream to socket>', this.connection.id, obj.description || '');
this.connection._server.logger.debug(
{
tnx: 'pipeout',
cid: this.connection.id
},
'[%s] S: %s<pipe message stream to socket>',
this.connection.id,
obj.description || ''
);
obj.pipe(this.connection[!this.connection.compression ? '_socket' : '_deflate'], {
end: false
});
@ -37,10 +41,15 @@ class IMAPComposer extends Transform {
let compiled = imapHandler.compiler(obj);
this.connection._server.logger.debug({
tnx: 'send',
cid: this.connection.id
}, '[%s] S:', this.connection.id, compiled);
this.connection._server.logger.debug(
{
tnx: 'send',
cid: this.connection.id
},
'[%s] S:',
this.connection.id,
compiled
);
this.push(new Buffer(compiled + '\r\n', 'binary'));
done();
@ -49,7 +58,6 @@ class IMAPComposer extends Transform {
_flush(done) {
done();
}
}
module.exports.IMAPComposer = IMAPComposer;

View file

@ -1,15 +1,15 @@
'use strict';
let IMAPStream = require('./imap-stream').IMAPStream;
let IMAPCommand = require('./imap-command').IMAPCommand;
let IMAPComposer = require('./imap-composer').IMAPComposer;
let imapTools = require('./imap-tools');
let search = require('./search');
let dns = require('dns');
let crypto = require('crypto');
let os = require('os');
let EventEmitter = require('events').EventEmitter;
let packageInfo = require('../../package');
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');
const SOCKET_TIMEOUT = 30 * 60 * 1000;
@ -21,7 +21,6 @@ const SOCKET_TIMEOUT = 30 * 60 * 1000;
* @param {Object} socket Socket instance
*/
class IMAPConnection extends EventEmitter {
constructor(server, socket) {
super();
@ -89,7 +88,6 @@ class IMAPConnection extends EventEmitter {
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
@ -100,20 +98,30 @@ class IMAPConnection extends EventEmitter {
// 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
dns.reverse(this.remoteAddress, (err, hostnames) => { // eslint-disable-line handle-callback-err
dns.reverse(this.remoteAddress, (err, hostnames) => {
if (err) {
//ignore, no big deal
}
// eslint-disable-line handle-callback-err
if (this._closing || this._closed) {
return;
}
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this.clientHostname = (hostnames && hostnames.shift()) || '[' + this.remoteAddress + ']';
this._startSession();
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');
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');
});
}
@ -126,14 +134,19 @@ class IMAPConnection extends EventEmitter {
send(payload, callback) {
if (this._socket && this._socket.writable) {
this[!this.compression ? '_socket' : '_deflate'].write(payload + '\r\n', 'binary', callback);
if(this.compression){
if (this.compression) {
// make sure we transmit the message immediatelly
this._deflate.flush();
}
this._server.logger.debug({
tnx: 'send',
cid: this.id
}, '[%s] S:', this.id, payload);
this._server.logger.debug(
{
tnx: 'send',
cid: this.id
},
'[%s] S:',
this.id,
payload
);
}
}
@ -168,10 +181,14 @@ class IMAPConnection extends EventEmitter {
* @event
*/
_onEnd() {
this._server.logger.info({
tnx: 'close',
cid: this.id
}, '[%s] Connection END', this.id);
this._server.logger.info(
{
tnx: 'close',
cid: this.id
},
'[%s] Connection END',
this.id
);
if (!this._closed) {
this._onClose();
}
@ -181,7 +198,7 @@ class IMAPConnection extends EventEmitter {
* Fired when the socket is closed
* @event
*/
_onClose( /* hadError */ ) {
_onClose(/* hadError */) {
if (this._closed) {
return;
}
@ -216,10 +233,15 @@ class IMAPConnection extends EventEmitter {
this._closed = true;
this._closing = false;
this._server.logger.info({
tnx: 'close',
cid: this.id
}, '[%s] Connection closed to %s', this.id, this.clientHostname);
this._server.logger.info(
{
tnx: 'close',
cid: this.id
},
'[%s] Connection closed to %s',
this.id,
this.clientHostname
);
}
/**
@ -234,10 +256,15 @@ class IMAPConnection extends EventEmitter {
return;
}
this._server.logger.error({
err,
cid: this.id
}, '[%s] %s', this.id, err.message);
this._server.logger.error(
{
err,
cid: this.id
},
'[%s] %s',
this.id,
err.message
);
this.emit('error', err);
}
@ -247,10 +274,14 @@ class IMAPConnection extends EventEmitter {
* @event
*/
_onTimeout() {
this._server.logger.info({
tnx: 'connection',
cid: this.id
}, '[%s] Connection TIMEOUT', this.id);
this._server.logger.info(
{
tnx: 'connection',
cid: this.id
},
'[%s] Connection TIMEOUT',
this.id
);
if (this.idling) {
return; // ignore timeouts when IDLEing
}
@ -291,7 +322,6 @@ class IMAPConnection extends EventEmitter {
*/
_startSession() {
this.session = {
id: this.id,
selected: this.selected,
@ -331,7 +361,7 @@ class IMAPConnection extends EventEmitter {
}
let cleared = false;
let listenerData = this._listenerData = {
let listenerData = (this._listenerData = {
mailbox: this.selected.mailbox,
lock: false,
clear: () => {
@ -376,11 +406,16 @@ class IMAPConnection extends EventEmitter {
listenerData.lock = false;
if (err) {
this._server.logger.info({
err,
tnx: 'updates',
cid: this.id
}, '[%s] Notification Error: %s', this.id, err.message);
this._server.logger.info(
{
err,
tnx: 'updates',
cid: this.id
},
'[%s] Notification Error: %s',
this.id,
err.message
);
return;
}
@ -407,7 +442,7 @@ class IMAPConnection extends EventEmitter {
}
});
}
};
});
this._server.notifier.addListener(this.session, this._listenerData.mailbox, this._listenerData.callback);
@ -424,10 +459,15 @@ class IMAPConnection extends EventEmitter {
let existsResponse;
// show notifications
this._server.logger.info({
tnx: 'notifications',
cid: this.id
}, '[%s] Pending notifications: %s', this.id, this.selected.notifications.length);
this._server.logger.info(
{
tnx: 'notifications',
cid: this.id
},
'[%s] Pending notifications: %s',
this.id,
this.selected.notifications.length
);
// find UIDs that are both added and removed
let added = new Set(); // added UIDs
@ -475,23 +515,31 @@ class IMAPConnection extends EventEmitter {
this.selected.modifyIndex = update.modseq;
}
this._server.logger.info({
tnx: 'notifications',
cid: this.id
}, '[%s] Processing notification: %s', this.id, JSON.stringify(update));
this._server.logger.info(
{
tnx: 'notifications',
cid: this.id
},
'[%s] Processing notification: %s',
this.id,
JSON.stringify(update)
);
if (update.ignore === this.id) {
continue; // skip this
}
this._server.logger.info({
tnx: 'notifications',
cid: this.id
}, '[%s] UIDS: %s', this.id, this.selected.uidList.length);
this._server.logger.info(
{
tnx: 'notifications',
cid: this.id
},
'[%s] UIDS: %s',
this.id,
this.selected.uidList.length
);
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
@ -500,27 +548,32 @@ class IMAPConnection extends EventEmitter {
break;
case 'EXPUNGE':
{
let seq = (this.selected.uidList || []).indexOf(update.uid);
this._server.logger.info({
case 'EXPUNGE': {
let seq = (this.selected.uidList || []).indexOf(update.uid);
this._server.logger.info(
{
tnx: 'expunge',
cid: this.id
}, '[%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
}
break;
},
'[%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
}
case 'FETCH':
this.writeStream.write(this.formatResponse('FETCH', update.uid, {
flags: update.flags,
modseq: this.selected.condstoreEnabled && update.modseq || false
}));
break;
}
case 'FETCH':
this.writeStream.write(
this.formatResponse('FETCH', update.uid, {
flags: update.flags,
modseq: (this.selected.condstoreEnabled && update.modseq) || false
})
);
break;
}
@ -536,10 +589,12 @@ class IMAPConnection extends EventEmitter {
this.writeStream.write({
tag: '*',
command: String(this.selected.uidList.length),
attributes: [{
type: 'atom',
value: 'EXISTS'
}]
attributes: [
{
type: 'atom',
value: 'EXISTS'
}
]
});
}
@ -573,10 +628,12 @@ class IMAPConnection extends EventEmitter {
let response = {
tag: '*',
command: String(seq),
attributes: [{
type: 'atom',
value: command
}]
attributes: [
{
type: 'atom',
value: command
}
]
};
if (data) {
@ -586,11 +643,12 @@ class IMAPConnection extends EventEmitter {
data.query.forEach((item, i) => {
response.attributes[1].push(item.original);
if (['flags', 'modseq'].indexOf(item.item) >= 0) {
response.attributes[1].
push([].concat(data.values[i] || []).map(value => ({
type: 'ATOM',
value: (value || value === 0 ? value : '').toString()
})));
response.attributes[1].push(
[].concat(data.values[i] || []).map(value => ({
type: 'ATOM',
value: (value || value === 0 ? value : '').toString()
}))
);
} else if (Object.prototype.toString.call(data.values[i]) === '[object Date]') {
response.attributes[1].push({
type: 'ATOM',
@ -618,7 +676,7 @@ class IMAPConnection extends EventEmitter {
} else {
response.attributes[1].push({
type: 'ATOM',
value: (data.values[i]).toString()
value: data.values[i].toString()
});
}
});
@ -633,24 +691,35 @@ class IMAPConnection extends EventEmitter {
switch (key) {
case 'FLAGS':
value = [].concat(value || []).map(flag => (flag && flag.value ? flag : {
type: 'ATOM',
value: flag
}));
value = [].concat(value || []).map(
flag =>
flag && flag.value
? flag
: {
type: 'ATOM',
value: flag
}
);
break;
case 'UID':
value = value && value.value ? value : {
type: 'ATOM',
value: (value || '0').toString()
};
value = value && value.value
? value
: {
type: 'ATOM',
value: (value || '0').toString()
};
break;
case 'MODSEQ':
value = [].concat(value && value.value ? value : {
type: 'ATOM',
value: (value || '0').toString()
});
value = [].concat(
value && value.value
? value
: {
type: 'ATOM',
value: (value || '0').toString()
}
);
break;
}
@ -666,7 +735,6 @@ class IMAPConnection extends EventEmitter {
return response;
}
}
// Expose to the world

View file

@ -17,7 +17,6 @@ const CLOSE_TIMEOUT = 1 * 1000; // how much to wait until pending connections ar
* @param {Object} options Connection and IMAP optionsž
*/
class IMAPServer extends EventEmitter {
constructor(options) {
super();
@ -77,17 +76,28 @@ class IMAPServer extends EventEmitter {
// close active connections
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
this.logger.info(
{
tnx: 'close'
},
'Server closing with %s pending connection%s, waiting %s seconds before terminating',
connections,
connections !== 1 ? 's' : '',
timeout / 1000
);
}
this._closeTimeout = setTimeout(() => {
connections = this.connections.size;
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.logger.info(
{
tnx: 'close'
},
'Closing %s pending connection%s to close the server',
connections,
connections !== 1 ? 's' : ''
);
this.connections.forEach(connection => {
connection.send('* BYE System shutdown');
@ -105,7 +115,6 @@ class IMAPServer extends EventEmitter {
* @returns {Object} Bunyan logger instance
*/
_createDefaultLogger() {
let logger = {
_print: (...args) => {
let level = args.shift();
@ -117,10 +126,7 @@ class IMAPServer extends EventEmitter {
message = args[0];
}
console.log('[%s] %s: %s', // eslint-disable-line no-console
new Date().toISOString().substr(0, 19).replace(/T/, ' '),
level.toUpperCase(),
message);
console.log('[%s] %s: %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), level.toUpperCase(), message); // eslint-disable-line no-console
}
};
@ -159,7 +165,8 @@ class IMAPServer extends EventEmitter {
'%sIMAP Server listening on %s:%s',
this.options.secure ? 'Secure ' : '',
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
address.port);
address.port
);
}
/**
@ -168,9 +175,12 @@ class IMAPServer extends EventEmitter {
* @event
*/
_onClose() {
this.logger.info({
tnx: 'closed'
}, 'IMAP Server closed');
this.logger.info(
{
tnx: 'closed'
},
'IMAP Server closed'
);
this.emit('close');
}
@ -182,7 +192,6 @@ class IMAPServer extends EventEmitter {
_onError(err) {
this.emit('error', err);
}
}
// Expose to the world

View file

@ -1,8 +1,8 @@
'use strict';
let stream = require('stream');
let Writable = stream.Writable;
let PassThrough = stream.PassThrough;
const stream = require('stream');
const Writable = stream.Writable;
const PassThrough = stream.PassThrough;
/**
* Incoming IMAP stream parser. Detects and emits command payloads.
@ -14,7 +14,6 @@ let PassThrough = stream.PassThrough;
* @param {Object} [options] Optional Stream options object
*/
class IMAPStream extends Writable {
constructor(options) {
// init Writable
super();
@ -34,11 +33,10 @@ class IMAPStream extends Writable {
this.on('finish', this._flushData.bind(this));
}
/**
* Placeholder command handler. Override this with your own.
*/
oncommand( /* command, callback */ ) {
oncommand(/* command, callback */) {
throw new Error('Command handler is not set');
}
@ -75,7 +73,6 @@ class IMAPStream extends Writable {
// Handle literal mode where we know how many bytes to expect before switching back to
// normal line based mode. All the data we receive is pumped to a passthrough stream
if (this._expecting > 0) {
if (data.length - pos <= 0) {
return done();
}
@ -118,38 +115,44 @@ class IMAPStream extends Writable {
if (!isNaN(match[1])) {
this._literal = new PassThrough();
this.oncommand({
value: line,
final: false,
expecting: this._expecting,
literal: this._literal,
this.oncommand(
{
value: line,
final: false,
expecting: this._expecting,
literal: this._literal,
// called once the stream has been processed
readyCallback: () => {
let next = this._literalReady;
if (typeof next === 'function') {
this._literalReady = false;
next();
} else {
this._literalReady = true;
// called once the stream has been processed
readyCallback: () => {
let next = this._literalReady;
if (typeof next === 'function') {
this._literalReady = false;
next();
} else {
this._literalReady = true;
}
}
},
err => {
if (err) {
this._expecting = 0;
this._literal = false;
this._literalReady = false;
}
setImmediate(this._readValue.bind(this, regex, data, pos, done));
}
}, err => {
if (err) {
this._expecting = 0;
this._literal = false;
this._literalReady = false;
}
setImmediate(this._readValue.bind(this, regex, data, pos, done));
});
);
return;
}
}
this.oncommand({
value: line,
final: true
}, this._readValue.bind(this, regex, data, pos, done));
this.oncommand(
{
value: line,
final: true
},
this._readValue.bind(this, regex, data, pos, done)
);
}
/**
@ -163,9 +166,7 @@ class IMAPStream extends Writable {
this.oncommand(new Buffer(line, 'binary'));
}
}
}
// Expose to the world
module.exports.IMAPStream = IMAPStream;

View file

@ -9,12 +9,15 @@ module.exports.systemFlagsFormatted = ['\\Answered', '\\Flagged', '\\Draft', '\\
module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen'];
module.exports.fetchSchema = {
body: [true, {
type: /^(\d+\.)*(CONTENT|HEADER|HEADER\.FIELDS|HEADER\.FIELDS\.NOT|TEXT|MIME|\d+)$/i,
headers: /^(\d+\.)*(HEADER\.FIELDS|HEADER\.FIELDS\.NOT)$/i,
startFrom: 'optional',
maxLength: 'optional'
}],
body: [
true,
{
type: /^(\d+\.)*(CONTENT|HEADER|HEADER\.FIELDS|HEADER\.FIELDS\.NOT|TEXT|MIME|\d+)$/i,
headers: /^(\d+\.)*(HEADER\.FIELDS|HEADER\.FIELDS\.NOT)$/i,
startFrom: 'optional',
maxLength: 'optional'
}
],
bodystructure: true,
envelope: true,
flags: true,
@ -42,10 +45,7 @@ module.exports.searchSchema = {
header: ['string', 'string'],
keyword: ['string'],
larger: ['number'],
modseq: [
['string', 'string', 'number'],
['number']
],
modseq: [['string', 'string', 'number'], ['number']],
new: true,
not: ['expression'],
old: true,
@ -195,11 +195,11 @@ module.exports.searchMapping = {
* @param {range} range Sequence range, eg "1,2,3:7"
* @returns {Boolean} True if the string looks like a sequence range
*/
module.exports.validateSequnce = function (range) {
module.exports.validateSequnce = function(range) {
return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range));
};
module.exports.normalizeMailbox = function (mailbox, utf7Encoded) {
module.exports.normalizeMailbox = function(mailbox, utf7Encoded) {
if (!mailbox) {
return '';
}
@ -222,7 +222,7 @@ module.exports.normalizeMailbox = function (mailbox, utf7Encoded) {
return mailbox;
};
module.exports.generateFolderListing = function (folders, skipHierarchy) {
module.exports.generateFolderListing = function(folders, skipHierarchy) {
let items = new Map();
let parents = [];
@ -328,10 +328,11 @@ module.exports.generateFolderListing = function (folders, skipHierarchy) {
return result;
};
module.exports.filterFolders = function (folders, query) {
module.exports.filterFolders = function(folders, query) {
query = query
// remove excess * and %
.replace(/\*\*+/g, '*').replace(/%%+/g, '%')
.replace(/\*\*+/g, '*')
.replace(/%%+/g, '%')
// escape special characters
.replace(/([\\^$+?!.():=\[\]|,\-])/g, '\\$1')
// setup *
@ -344,7 +345,7 @@ module.exports.filterFolders = function (folders, query) {
return folders.filter(folder => !!regex.test(folder.path));
};
module.exports.getMessageRange = function (uidList, range, isUid) {
module.exports.getMessageRange = function(uidList, range, isUid) {
range = (range || '').toString();
let result = [];
@ -364,7 +365,7 @@ module.exports.getMessageRange = function (uidList, range, isUid) {
}
from = Number(from) || 1;
to = to.pop() || from;
to = Number(to === '*' && total || to) || from;
to = Number((to === '*' && total) || to) || from;
if (nr >= Math.min(from, to) && nr <= Math.max(from, to)) {
return true;
@ -373,13 +374,13 @@ module.exports.getMessageRange = function (uidList, range, isUid) {
return false;
};
for (i = 0, len = uidList.length; i < len; i++) {
for ((i = 0), (len = uidList.length); i < len; i++) {
if (uidList[i] > maxUid) {
maxUid = uidList[i];
}
}
for (i = 0, len = uidList.length; i < len; i++) {
for ((i = 0), (len = uidList.length); i < len; i++) {
uid = uidList[i] || 1;
if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
result.push(uidList[i]);
@ -389,7 +390,7 @@ module.exports.getMessageRange = function (uidList, range, isUid) {
return result;
};
module.exports.packMessageRange = function (uidList) {
module.exports.packMessageRange = function(uidList) {
if (!Array.isArray(uidList)) {
uidList = [].concat(uidList || []);
}
@ -398,12 +399,10 @@ module.exports.packMessageRange = function (uidList) {
return '';
}
uidList.sort((a, b) => (a - b));
uidList.sort((a, b) => a - b);
let last = uidList[uidList.length - 1];
let result = [
[last]
];
let result = [[last]];
for (let i = uidList.length - 2; i >= 0; i--) {
if (uidList[i] === uidList[i + 1] - 1) {
result[0].unshift(uidList[i]);
@ -428,11 +427,9 @@ module.exports.packMessageRange = function (uidList) {
* @param {Date} date Date object to parse
* @returns {String} Internaldate formatted date
*/
module.exports.formatInternalDate = function (date) {
module.exports.formatInternalDate = function(date) {
let day = date.getUTCDate(),
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
][date.getUTCMonth()],
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getUTCMonth()],
year = date.getUTCFullYear(),
hour = date.getUTCHours(),
minute = date.getUTCMinutes(),
@ -441,11 +438,29 @@ module.exports.formatInternalDate = function (date) {
tzHours = Math.abs(Math.floor(tz / 60)),
tzMins = Math.abs(tz) - tzHours * 60;
return (day < 10 ? '0' : '') + day + '-' + month + '-' + year + ' ' +
(hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') +
minute + ':' + (second < 10 ? '0' : '') + second + ' ' +
(tz > 0 ? '-' : '+') + (tzHours < 10 ? '0' : '') + tzHours +
(tzMins < 10 ? '0' : '') + tzMins;
return (
(day < 10 ? '0' : '') +
day +
'-' +
month +
'-' +
year +
' ' +
(hour < 10 ? '0' : '') +
hour +
':' +
(minute < 10 ? '0' : '') +
minute +
':' +
(second < 10 ? '0' : '') +
second +
' ' +
(tz > 0 ? '-' : '+') +
(tzHours < 10 ? '0' : '') +
tzHours +
(tzMins < 10 ? '0' : '') +
tzMins
);
};
/**
@ -470,8 +485,7 @@ module.exports.formatInternalDate = function (date) {
* @param {Object} options Options for the indexer
* @returns {Array} Resolved responses
*/
module.exports.getQueryResponse = function (query, message, options) {
module.exports.getQueryResponse = function(query, message, options) {
options = options || {};
// for optimization purposes try to use cached mimeTree etc. if available
@ -485,7 +499,6 @@ module.exports.getQueryResponse = function (query, message, options) {
query.forEach(item => {
let value = '';
switch (item.item) {
case 'uid':
value = message.uid;
break;
@ -505,39 +518,38 @@ module.exports.getQueryResponse = function (query, message, options) {
value = message.idate;
break;
case 'bodystructure':
{
if (message.bodystructure) {
value = message.bodystructure;
} else {
if (!mimeTree) {
mimeTree = indexer.parseMimeTree(message.raw);
}
value = indexer.getBodyStructure(mimeTree);
case 'bodystructure': {
if (message.bodystructure) {
value = message.bodystructure;
} else {
if (!mimeTree) {
mimeTree = indexer.parseMimeTree(message.raw);
}
let walk = arr => {
arr.forEach((entry, i) => {
if (Array.isArray(entry)) {
return walk(entry);
}
if (!entry || typeof entry !== 'object') {
return;
}
let val = entry;
if (!Buffer.isBuffer(val) && val.buffer) {
val = val.buffer;
}
arr[i] = libmime.encodeWords(val.toString(), false, Infinity);
});
};
if (!options.acceptUTF8Enabled) {
walk(value);
}
break;
value = indexer.getBodyStructure(mimeTree);
}
let walk = arr => {
arr.forEach((entry, i) => {
if (Array.isArray(entry)) {
return walk(entry);
}
if (!entry || typeof entry !== 'object') {
return;
}
let val = entry;
if (!Buffer.isBuffer(val) && val.buffer) {
val = val.buffer;
}
arr[i] = libmime.encodeWords(val.toString(), false, Infinity);
});
};
if (!options.acceptUTF8Enabled) {
walk(value);
}
break;
}
case 'envelope':
if (message.envelope) {
value = message.envelope;
@ -662,7 +674,7 @@ module.exports.getQueryResponse = function (query, message, options) {
// If start+length is larger than available value length, then do not return the length value
// Instead of BODY[]<10.20> return BODY[]<10> which means that the response is from offset 10 to the end
if (item.original.partial.length === 2 && (item.partial.maxLength - item.partial.startFrom > len)) {
if (item.original.partial.length === 2 && item.partial.maxLength - item.partial.startFrom > len) {
item.original.partial.pop();
}
}

View file

@ -80,13 +80,13 @@ class Indexer {
next();
};
root = false;
if (node.size || node.attachmentId) {
if (!root) {
if (!node.boundary) {
append(false, true); // force newline
}
size += node.size;
}
root = false;
if (node.boundary) {
append('--' + node.boundary);

View file

@ -123,7 +123,7 @@ class MIMEParser {
node.message = parse(node.body.join(''));
}
node.lineCount = node.body.length;
node.lineCount = node.body.length ? node.body.length - 1 : 0;
node.body = Buffer.from(
node.body
.join('')
@ -241,13 +241,13 @@ class MIMEParser {
*/
parseValueParams(headerValue) {
let data = {
value: '',
type: '',
subtype: '',
params: {}
},
match,
processEncodedWords = {};
value: '',
type: '',
subtype: '',
params: {}
};
let match;
let processEncodedWords = {};
(headerValue || '').split(';').forEach((part, i) => {
let key, value;
@ -287,7 +287,7 @@ class MIMEParser {
let charset = '';
let value = '';
processEncodedWords[key].forEach(val => {
let parts = val.split('\'');
let parts = val.split("'"); // eslint-disable-line quotes
charset = charset || parts.shift();
value += (parts.pop() || '').replace(/%/g, '=');
});

View file

@ -1,7 +1,7 @@
'use strict';
let streams = require('stream');
let Transform = streams.Transform;
const streams = require('stream');
const Transform = streams.Transform;
// make sure that a stream piped to this transform stream
// always emits a fixed amounts of bytes. Either by truncating

View file

@ -1,11 +1,11 @@
'use strict';
const Indexer = require('./indexer/indexer');
let indexer = new Indexer();
const indexer = new Indexer();
module.exports.matchSearchQuery = matchSearchQuery;
let queryHandlers = {
const queryHandlers = {
// always matches
all(message, query, callback) {
return callback(null, true);
@ -57,9 +57,13 @@ let queryHandlers = {
// matches message body
body(message, query, callback) {
let data = indexer.getContents(message.mimeTree, {
type: 'text'
}, true);
let data = indexer.getContents(
message.mimeTree,
{
type: 'text'
},
true
);
let resolveData = next => {
if (data.type !== 'stream') {
@ -94,9 +98,13 @@ let queryHandlers = {
// matches message source
text(message, query, callback) {
let data = indexer.getContents(message.mimeTree, {
type: 'content'
}, true);
let data = indexer.getContents(
message.mimeTree,
{
type: 'content'
},
true
);
let resolveData = next => {
if (data.type !== 'stream') {
@ -160,7 +168,7 @@ let queryHandlers = {
mimeTree = indexer.parseMimeTree(message.raw || '');
}
let headers = (mimeTree.header || []);
let headers = mimeTree.header || [];
let header = query.header;
let term = (query.value || '').toString().toLowerCase();
let key, value, parts;
@ -169,7 +177,7 @@ let queryHandlers = {
parts = headers[i].split(':');
key = (parts.shift() || '').trim().toLowerCase();
value = (parts.join(':') || '');
value = parts.join(':') || '';
if (key === header && (!term || value.toLowerCase().indexOf(term) >= 0)) {
return callback(null, true);
@ -212,7 +220,6 @@ function getShortDate(date) {
* @returns {Boolean} Term matched (true) or not (false)
*/
function matchSearchTerm(message, query, callback) {
if (Array.isArray(query)) {
// AND, all terms need to match
return matchSearchQuery(message, query, callback);
@ -224,28 +231,27 @@ function matchSearchTerm(message, query, callback) {
}
switch (query.key) {
case 'or':
{
// OR, only single match needed
let checked = 0;
let checkNext = () => {
if (checked >= query.value.length) {
return callback(null, false);
case 'or': {
// OR, only single match needed
let checked = 0;
let checkNext = () => {
if (checked >= query.value.length) {
return callback(null, false);
}
let term = query.value[checked++];
matchSearchTerm(message, term, (err, match) => {
if (err) {
return callback(err);
}
let term = query.value[checked++];
matchSearchTerm(message, term, (err, match) => {
if (err) {
return callback(err);
}
if (match) {
return callback(null, true);
}
setImmediate(checkNext);
});
};
return setImmediate(checkNext);
}
/*
if (match) {
return callback(null, true);
}
setImmediate(checkNext);
});
};
return setImmediate(checkNext);
}
/*
// OR, only single match needed
for (let i = query.value.length - 1; i >= 0; i--) {
if (matchSearchTerm(message, query.value[i])) {

View file

@ -3,7 +3,7 @@
// Expose to the world
module.exports = getTLSOptions;
let tlsDefaults = {
const tlsDefaults = {
key: '-----BEGIN RSA PRIVATE KEY-----\n' +
'MIIEpAIBAAKCAQEA6Z5Qqhw+oWfhtEiMHE32Ht94mwTBpAfjt3vPpX8M7DMCTwHs\n' +
'1xcXvQ4lQ3rwreDTOWdoJeEEy7gMxXqH0jw0WfBx+8IIJU69xstOyT7FRFDvA1yT\n' +

56
imap-core/test/client.js Normal file
View file

@ -0,0 +1,56 @@
/* eslint no-console:0 */
'use strict';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const config = require('config');
const BrowserBox = require('browserbox');
const client = new BrowserBox('localhost', config.imap.port, {
useSecureTransport: config.imap.secure,
auth: {
user: 'testuser',
pass: 'secretpass'
},
id: {
name: 'My Client',
version: '0.1'
},
tls: {
rejectUnauthorized: false
}
});
client.onerror = function(err) {
console.log(err);
process.exit(1);
};
client.onauth = function() {
client.upload('INBOX', 'from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz\r\n', false, err => {
if (err) {
console.log(err);
return process.exit(1);
}
client.selectMailbox('INBOX', (err, mailbox) => {
if (err) {
console.log(err);
return process.exit(1);
}
console.log(mailbox);
client.listMessages(mailbox.exists, ['BODY.PEEK[]', 'BODYSTRUCTURE'], (err, data) => {
if (err) {
console.log(err);
return process.exit(1);
}
console.log('<<<%s>>>', data[0]['body[]']);
return process.exit(0);
});
});
});
};
client.connect();

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
'use strict';
module.exports.rfc822 = '' +
module.exports.rfc822 =
'' +
'Subject: test\ r\ n ' +
'Content-type: multipart/mixed; boundary=abc\r\n' +
'\r\n' +
@ -15,40 +16,41 @@ module.exports.rfc822 = '' +
'--abc--\r\n';
module.exports.mimetree = {
childNodes: [{
header: ['Content-Type: text/plain'],
parsedHeader: {
'content-type': {
value: 'text/plain',
type: 'text',
subtype: 'plain',
params: {}
}
childNodes: [
{
header: ['Content-Type: text/plain'],
parsedHeader: {
'content-type': {
value: 'text/plain',
type: 'text',
subtype: 'plain',
params: {}
}
},
body: 'Hello world!',
multipart: false,
boundary: false,
lineCount: 1,
size: 12
},
body: 'Hello world!',
multipart: false,
boundary: false,
lineCount: 1,
size: 12
}, {
header: ['Content-Type: image/png'],
parsedHeader: {
'content-type': {
value: 'image/png',
type: 'image',
subtype: 'png',
params: {}
}
},
body: 'BinaryContent',
multipart: false,
boundary: false,
lineCount: 1,
size: 13
}],
header: ['Subject: test',
'Content-type: multipart/mixed; boundary=abc'
{
header: ['Content-Type: image/png'],
parsedHeader: {
'content-type': {
value: 'image/png',
type: 'image',
subtype: 'png',
params: {}
}
},
body: 'BinaryContent',
multipart: false,
boundary: false,
lineCount: 1,
size: 13
}
],
header: ['Subject: test', 'Content-type: multipart/mixed; boundary=abc'],
parsedHeader: {
'content-type': {
value: 'multipart/mixed',
@ -70,35 +72,14 @@ module.exports.mimetree = {
};
module.exports.bodystructure = [
['text',
'plain',
null,
null,
null,
'7bit',
12,
1,
null,
null,
null,
null
],
['image',
'png',
null,
null,
null,
'7bit',
13,
null,
null,
null,
null
],
'mixed', ['boundary', 'abc'],
['text', 'plain', null, null, null, '7bit', 12, 1, null, null, null, null],
['image', 'png', null, null, null, '7bit', 13, null, null, null, null],
'mixed',
['boundary', 'abc'],
null,
null,
null
];
module.exports.command = '* FETCH (BODYSTRUCTURE (("text" "plain" NIL NIL NIL "7bit" 12 1 NIL NIL NIL NIL) ("image" "png" NIL NIL NIL "7bit" 13 NIL NIL NIL NIL) "mixed" ("boundary" "abc") NIL NIL NIL))';
module.exports.command =
'* FETCH (BODYSTRUCTURE (("text" "plain" NIL NIL NIL "7bit" 12 1 NIL NIL NIL NIL) ("image" "png" NIL NIL NIL "7bit" 13 NIL NIL NIL NIL) "mixed" ("boundary" "abc") NIL NIL NIL))';

View file

@ -2,19 +2,18 @@
'use strict';
let chai = require('chai');
let imapHandler = require('../lib/handler/imap-handler');
let PassThrough = require('stream').PassThrough;
let crypto = require('crypto');
let expect = chai.expect;
const chai = require('chai');
const imapHandler = require('../lib/handler/imap-handler');
const PassThrough = require('stream').PassThrough;
const crypto = require('crypto');
const expect = chai.expect;
chai.config.includeStack = true;
describe('IMAP Command Compile Stream', function () {
describe('#compile', function () {
it('should compile correctly', function (done) {
let command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
describe('IMAP Command Compile Stream', function() {
describe('#compile', function() {
it('should compile correctly', function(done) {
let command =
'* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
parsed = imapHandler.parser(command, {
allowUntagged: true
});
@ -27,18 +26,18 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('Types', function () {
describe('Types', function() {
let parsed;
beforeEach(function () {
beforeEach(function() {
parsed = {
tag: '*',
command: 'CMD'
};
});
describe('No attributes', function () {
it('should compile correctly', function (done) {
describe('No attributes', function() {
it('should compile correctly', function(done) {
let command = '* CMD';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
@ -49,12 +48,14 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('TEXT', function () {
it('should compile correctly', function (done) {
parsed.attributes = [{
type: 'TEXT',
value: 'Tere tere!'
}];
describe('TEXT', function() {
it('should compile correctly', function(done) {
parsed.attributes = [
{
type: 'TEXT',
value: 'Tere tere!'
}
];
let command = '* CMD Tere tere!';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
expect(err).to.not.exist;
@ -64,15 +65,19 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('SECTION', function () {
it('should compile correctly', function (done) {
parsed.attributes = [{
type: 'SECTION',
section: [{
type: 'ATOM',
value: 'ALERT'
}]
}];
describe('SECTION', function() {
it('should compile correctly', function(done) {
parsed.attributes = [
{
type: 'SECTION',
section: [
{
type: 'ATOM',
value: 'ALERT'
}
]
}
];
let command = '* CMD [ALERT]';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
expect(err).to.not.exist;
@ -82,18 +87,22 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('ATOM', function () {
it('should compile correctly', function (done) {
parsed.attributes = [{
type: 'ATOM',
value: 'ALERT'
}, {
type: 'ATOM',
value: '\\ALERT'
}, {
type: 'ATOM',
value: 'NO ALERT'
}];
describe('ATOM', function() {
it('should compile correctly', function(done) {
parsed.attributes = [
{
type: 'ATOM',
value: 'ALERT'
},
{
type: 'ATOM',
value: '\\ALERT'
},
{
type: 'ATOM',
value: 'NO ALERT'
}
];
let command = '* CMD ALERT \\ALERT "NO ALERT"';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
expect(err).to.not.exist;
@ -103,12 +112,14 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('SEQUENCE', function () {
it('should compile correctly', function (done) {
parsed.attributes = [{
type: 'SEQUENCE',
value: '*:4,5,6'
}];
describe('SEQUENCE', function() {
it('should compile correctly', function(done) {
parsed.attributes = [
{
type: 'SEQUENCE',
value: '*:4,5,6'
}
];
let command = '* CMD *:4,5,6';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
expect(err).to.not.exist;
@ -118,12 +129,9 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('NIL', function () {
it('should compile correctly', function (done) {
parsed.attributes = [
null,
null
];
describe('NIL', function() {
it('should compile correctly', function(done) {
parsed.attributes = [null, null];
let command = '* CMD NIL NIL';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
@ -134,8 +142,8 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('TEXT', function () {
it('should compile correctly', function (done) {
describe('TEXT', function() {
it('should compile correctly', function(done) {
parsed.attributes = [
// keep indentation
{
@ -154,7 +162,7 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('should keep short strings', function (done) {
it('should keep short strings', function(done) {
parsed.attributes = [
// keep indentation
{
@ -172,7 +180,7 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('should hide strings', function (done) {
it('should hide strings', function(done) {
parsed.attributes = [
// keep indentation
{
@ -191,7 +199,7 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('should hide long strings', function (done) {
it('should hide long strings', function(done) {
parsed.attributes = [
// keep indentation
{
@ -210,12 +218,13 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('No Command', function () {
it('should compile correctly', function (done) {
describe('No Command', function() {
it('should compile correctly', function(done) {
parsed = {
tag: '*',
attributes: [
1, {
1,
{
type: 'ATOM',
value: 'EXPUNGE'
}
@ -230,8 +239,8 @@ describe('IMAP Command Compile Stream', function () {
});
});
});
describe('Literal', function () {
it('shoud return as text', function (done) {
describe('Literal', function() {
it('shoud return as text', function(done) {
let parsed = {
tag: '*',
command: 'CMD',
@ -253,15 +262,18 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('should compile correctly without tag and command', function (done) {
it('should compile correctly without tag and command', function(done) {
let parsed = {
attributes: [{
type: 'LITERAL',
value: 'Tere tere!'
}, {
type: 'LITERAL',
value: 'Vana kere'
}]
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
}
]
};
let command = '{10}\r\nTere tere! {9}\r\nVana kere';
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
@ -271,7 +283,7 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('shoud return byte length', function (done) {
it('shoud return byte length', function(done) {
let parsed = {
tag: '*',
command: 'CMD',
@ -293,33 +305,40 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('should mix with other values', function (done) {
it('should mix with other values', function(done) {
let s = new PassThrough();
let str = 'abc'.repeat(100);
let parsed = {
attributes: [{
type: 'ATOM',
value: 'BODY'
}, {
type: 'LITERAL',
value: 'Tere tere!'
}, {
type: 'ATOM',
value: 'BODY'
}, {
type: 'LITERAL',
//value: str
value: s,
expectedLength: str.length
}, {
type: 'ATOM',
value: 'UID'
}, {
type: 'ATOM',
value: '12345'
}]
attributes: [
{
type: 'ATOM',
value: 'BODY'
},
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'ATOM',
value: 'BODY'
},
{
type: 'LITERAL',
//value: str
value: s,
expectedLength: str.length
},
{
type: 'ATOM',
value: 'UID'
},
{
type: 'ATOM',
value: '12345'
}
]
};
setImmediate(function(){
setImmediate(function() {
s.end(str);
});
let command = 'BODY {10}\r\nTere tere! BODY {' + str.length + '}\r\n' + str + ' UID 12345';
@ -330,7 +349,7 @@ describe('IMAP Command Compile Stream', function () {
});
});
it('shoud pipe literal streams', function (done) {
it('shoud pipe literal streams', function(done) {
let stream1 = new PassThrough();
let stream2 = new PassThrough();
let stream3 = new PassThrough();
@ -342,19 +361,23 @@ describe('IMAP Command Compile Stream', function () {
{
type: 'LITERAL',
value: 'Tere tere!'
}, {
},
{
type: 'LITERAL',
expectedLength: 5,
value: stream1
},
'Vana kere', {
'Vana kere',
{
type: 'LITERAL',
expectedLength: 7,
value: stream2
}, {
},
{
type: 'LITERAL',
value: 'Kuidas laheb?'
}, {
},
{
type: 'LITERAL',
expectedLength: 5,
value: stream3
@ -380,7 +403,7 @@ describe('IMAP Command Compile Stream', function () {
}, 100);
});
it('shoud pipe limited literal streams', function (done) {
it('shoud pipe limited literal streams', function(done) {
let stream1 = new PassThrough();
let stream2 = new PassThrough();
let stream3 = new PassThrough();
@ -392,22 +415,26 @@ describe('IMAP Command Compile Stream', function () {
{
type: 'LITERAL',
value: 'Tere tere!'
}, {
},
{
type: 'LITERAL',
expectedLength: 5,
value: stream1,
startFrom: 2,
maxLength: 2
},
'Vana kere', {
'Vana kere',
{
type: 'LITERAL',
expectedLength: 7,
value: stream2,
startFrom: 2
}, {
},
{
type: 'LITERAL',
value: 'Kuidas laheb?'
}, {
},
{
type: 'LITERAL',
expectedLength: 7,
value: stream3,
@ -435,7 +462,7 @@ describe('IMAP Command Compile Stream', function () {
}, 100);
});
it('shoud pipe errors for literal streams', function (done) {
it('shoud pipe errors for literal streams', function(done) {
let stream1 = new PassThrough();
let parsed = {
tag: '*',
@ -445,7 +472,8 @@ describe('IMAP Command Compile Stream', function () {
{
type: 'LITERAL',
value: 'Tere tere!'
}, {
},
{
type: 'LITERAL',
expectedLength: 5,
value: stream1
@ -465,10 +493,10 @@ describe('IMAP Command Compile Stream', function () {
});
});
describe('#LengthLimiter', function () {
describe('#LengthLimiter', function() {
this.timeout(10000); //eslint-disable-line no-invalid-this
it('should emit exact length', function (done) {
it('should emit exact length', function(done) {
let len = 1024;
let limiter = new imapHandler.compileStream.LengthLimiter(len);
let expected = 'X'.repeat(len);
@ -495,7 +523,7 @@ describe('IMAP Command Compile Stream', function () {
setTimeout(emitter, 100);
});
it('should truncate output', function (done) {
it('should truncate output', function(done) {
let len = 1024;
let limiter = new imapHandler.compileStream.LengthLimiter(len - 100);
let expected = 'X'.repeat(len - 100);
@ -522,7 +550,7 @@ describe('IMAP Command Compile Stream', function () {
setTimeout(emitter, 100);
});
it('should skip output', function (done) {
it('should skip output', function(done) {
let len = 1024;
let limiter = new imapHandler.compileStream.LengthLimiter(len - 100, false, 30);
let expected = 'X'.repeat(len - 100 - 30);
@ -549,7 +577,7 @@ describe('IMAP Command Compile Stream', function () {
setTimeout(emitter, 100);
});
it('should pad output', function (done) {
it('should pad output', function(done) {
let len = 1024;
let limiter = new imapHandler.compileStream.LengthLimiter(len + 100);
let expected = 'X'.repeat(len) + ' '.repeat(100);
@ -576,7 +604,7 @@ describe('IMAP Command Compile Stream', function () {
setTimeout(emitter, 100);
});
it('should pipe to several streams', function (done) {
it('should pipe to several streams', function(done) {
let len = 1024;
let start = 30;
let margin = 200;
@ -586,7 +614,7 @@ describe('IMAP Command Compile Stream', function () {
let expected1 = input.substr(start, len - margin - start);
let expected2 = input.substr(len - margin);
limiter.on('done', function (remainder) {
limiter.on('done', function(remainder) {
stream.unpipe(limiter);
if (remainder) {
stream.unshift(remainder);
@ -617,7 +645,6 @@ describe('IMAP Command Compile Stream', function () {
} else {
setImmediate(emitter);
}
};
stream.pipe(limiter);

View file

@ -2,15 +2,16 @@
'use strict';
let chai = require('chai');
let imapHandler = require('../lib/handler/imap-handler');
let expect = chai.expect;
const chai = require('chai');
const imapHandler = require('../lib/handler/imap-handler');
const expect = chai.expect;
chai.config.includeStack = true;
describe('IMAP Command Compiler', function () {
describe('#compile', function () {
it('should compile correctly', function () {
let command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
describe('IMAP Command Compiler', function() {
describe('#compile', function() {
it('should compile correctly', function() {
let command =
'* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
parsed = imapHandler.parser(command, {
allowUntagged: true
}),
@ -20,85 +21,93 @@ describe('IMAP Command Compiler', function () {
});
});
describe('Types', function () {
describe('Types', function() {
let parsed;
beforeEach(function () {
beforeEach(function() {
parsed = {
tag: '*',
command: 'CMD'
};
});
describe('No attributes', function () {
it('should compile correctly', function () {
describe('No attributes', function() {
it('should compile correctly', function() {
expect(imapHandler.compiler(parsed)).to.equal('* CMD');
});
});
describe('TEXT', function () {
it('should compile correctly', function () {
parsed.attributes = [{
type: 'TEXT',
value: 'Tere tere!'
}];
describe('TEXT', function() {
it('should compile correctly', function() {
parsed.attributes = [
{
type: 'TEXT',
value: 'Tere tere!'
}
];
expect(imapHandler.compiler(parsed)).to.equal('* CMD Tere tere!');
});
});
describe('SECTION', function () {
it('should compile correctly', function () {
parsed.attributes = [{
type: 'SECTION',
section: [{
type: 'ATOM',
value: 'ALERT'
}]
}];
describe('SECTION', function() {
it('should compile correctly', function() {
parsed.attributes = [
{
type: 'SECTION',
section: [
{
type: 'ATOM',
value: 'ALERT'
}
]
}
];
expect(imapHandler.compiler(parsed)).to.equal('* CMD [ALERT]');
});
});
describe('ATOM', function () {
it('should compile correctly', function () {
parsed.attributes = [{
type: 'ATOM',
value: 'ALERT'
}, {
type: 'ATOM',
value: '\\ALERT'
}, {
type: 'ATOM',
value: 'NO ALERT'
}];
describe('ATOM', function() {
it('should compile correctly', function() {
parsed.attributes = [
{
type: 'ATOM',
value: 'ALERT'
},
{
type: 'ATOM',
value: '\\ALERT'
},
{
type: 'ATOM',
value: 'NO ALERT'
}
];
expect(imapHandler.compiler(parsed)).to.equal('* CMD ALERT \\ALERT "NO ALERT"');
});
});
describe('SEQUENCE', function () {
it('should compile correctly', function () {
parsed.attributes = [{
type: 'SEQUENCE',
value: '*:4,5,6'
}];
describe('SEQUENCE', function() {
it('should compile correctly', function() {
parsed.attributes = [
{
type: 'SEQUENCE',
value: '*:4,5,6'
}
];
expect(imapHandler.compiler(parsed)).to.equal('* CMD *:4,5,6');
});
});
describe('NIL', function () {
it('should compile correctly', function () {
parsed.attributes = [
null,
null
];
describe('NIL', function() {
it('should compile correctly', function() {
parsed.attributes = [null, null];
expect(imapHandler.compiler(parsed)).to.equal('* CMD NIL NIL');
});
});
describe('TEXT', function () {
it('should compile correctly', function () {
describe('TEXT', function() {
it('should compile correctly', function() {
parsed.attributes = [
// keep indentation
{
@ -110,10 +119,9 @@ describe('IMAP Command Compiler', function () {
];
expect(imapHandler.compiler(parsed)).to.equal('* CMD "Tere tere!" "Vana kere"');
});
it('should keep short strings', function () {
it('should keep short strings', function() {
parsed.attributes = [
// keep indentation
{
@ -126,7 +134,7 @@ describe('IMAP Command Compiler', function () {
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "Tere tere!" "Vana kere"');
});
it('should hide strings', function () {
it('should hide strings', function() {
parsed.attributes = [
// keep indentation
{
@ -140,7 +148,7 @@ describe('IMAP Command Compiler', function () {
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* value hidden *)" "Vana kere"');
});
it('should hide long strings', function () {
it('should hide long strings', function() {
parsed.attributes = [
// keep indentation
{
@ -154,12 +162,13 @@ describe('IMAP Command Compiler', function () {
});
});
describe('No Command', function () {
it('should compile correctly', function () {
describe('No Command', function() {
it('should compile correctly', function() {
parsed = {
tag: '*',
attributes: [
1, {
1,
{
type: 'ATOM',
value: 'EXPUNGE'
}
@ -169,8 +178,8 @@ describe('IMAP Command Compiler', function () {
expect(imapHandler.compiler(parsed)).to.equal('* 1 EXPUNGE');
});
});
describe('Literal', function () {
it('shoud return as text', function () {
describe('Literal', function() {
it('shoud return as text', function() {
let parsed = {
tag: '*',
command: 'CMD',
@ -187,22 +196,25 @@ describe('IMAP Command Compiler', function () {
expect(imapHandler.compiler(parsed)).to.equal('* CMD {10}\r\nTere tere! "Vana kere"');
});
it('should return as an array text 1', function () {
it('should return as an array text 1', function() {
let parsed = {
tag: '*',
command: 'CMD',
attributes: [{
type: 'LITERAL',
value: 'Tere tere!'
}, {
type: 'LITERAL',
value: 'Vana kere'
}]
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
}
]
};
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']);
});
it('should return as an array text 2', function () {
it('should return as an array text 2', function() {
let parsed = {
tag: '*',
command: 'CMD',
@ -211,7 +223,8 @@ describe('IMAP Command Compiler', function () {
{
type: 'LITERAL',
value: 'Tere tere!'
}, {
},
{
type: 'LITERAL',
value: 'Vana kere'
},
@ -221,20 +234,23 @@ describe('IMAP Command Compiler', function () {
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere "zzz"']);
});
it('should compile correctly without tag and command', function () {
it('should compile correctly without tag and command', function() {
let parsed = {
attributes: [{
type: 'LITERAL',
value: 'Tere tere!'
}, {
type: 'LITERAL',
value: 'Vana kere'
}]
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
}
]
};
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['{10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']);
});
it('shoud return byte length', function () {
it('shoud return byte length', function() {
let parsed = {
tag: '*',
command: 'CMD',

View file

@ -3,19 +3,19 @@
'use strict';
let chai = require('chai');
let expect = chai.expect;
const chai = require('chai');
const expect = chai.expect;
//let http = require('http');
let fs = require('fs');
let Indexer = require('../lib/indexer/indexer');
let indexer = new Indexer();
const fs = require('fs');
const Indexer = require('../lib/indexer/indexer');
const indexer = new Indexer();
chai.config.includeStack = true;
//const HTTP_PORT = 9998;
let fixtures = {
const fixtures = {
simple: {
eml: fs.readFileSync(__dirname + '/fixtures/simple.eml'),
tree: require('./fixtures/simple.json')
@ -26,8 +26,8 @@ let fixtures = {
}
};
describe('#parseMimeTree', function () {
it('should parse mime message', function (done) {
describe('#parseMimeTree', function() {
it('should parse mime message', function(done) {
let parsed = indexer.parseMimeTree(fixtures.simple.eml);
//expect(parsed).to.deep.equal(fixtures.simple.tree);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,93 +7,104 @@ const fs = require('fs');
const parseMimeTree = require('../lib/indexer/parse-mime-tree');
const imapHandler = require('../lib/handler/imap-handler');
module.exports = function (options) {
module.exports = function(options) {
// This example uses global folders and subscriptions
let folders = new Map();
let subscriptions = new WeakSet();
[{
path: 'INBOX',
uidValidity: 123,
uidNext: 70,
modifyIndex: 5000,
messages: [{
uid: 45,
flags: [],
modseq: 100,
idate: new Date('14-Sep-2013 21:22:28 -0300'),
mimeTree: parseMimeTree(new Buffer('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz'))
}, {
uid: 49,
flags: ['\\Seen'],
idate: new Date(),
modseq: 5000,
mimeTree: parseMimeTree(fs.readFileSync(__dirname + '/fixtures/ryan_finnie_mime_torture.eml'))
}, {
uid: 50,
flags: ['\\Seen'],
modseq: 45,
idate: new Date(),
mimeTree: parseMimeTree('MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@tr.ee\r\n' +
'Content-Type: multipart/mixed;\r\n' +
' boundary=\'----mailcomposer-?=_1-1328088797399\'\r\n' +
'Message-Id: <testmessage-for-bug>;\r\n' +
'\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: message/rfc822\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@pangalink.net\r\n' +
'In-Reply-To: <test1>\r\n' +
'\r\n' +
'Hello world 1!\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: message/rfc822\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@pangalink.net\r\n' +
'\r\n' +
'Hello world 2!\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: text/html; charset=utf-8\r\n' +
'Content-Transfer-Encoding: quoted-printable\r\n' +
'\r\n' +
'<b>Hello world 3!</b>\r\n' +
'------mailcomposer-?=_1-1328088797399--')
}, {
uid: 52,
flags: [],
modseq: 4,
idate: new Date(),
mimeTree: parseMimeTree('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nHello World!')
}, {
uid: 53,
flags: [],
modseq: 5,
idate: new Date()
}, {
uid: 60,
flags: [],
modseq: 6,
idate: new Date()
}],
journal: []
}, {
path: '[Gmail]/Sent Mail',
specialUse: '\\Sent',
uidValidity: 123,
uidNext: 90,
modifyIndex: 1,
messages: [],
journal: []
}].forEach(folder => {
[
{
path: 'INBOX',
uidValidity: 123,
uidNext: 70,
modifyIndex: 5000,
messages: [
{
uid: 45,
flags: [],
modseq: 100,
idate: new Date('14-Sep-2013 21:22:28 -0300'),
mimeTree: parseMimeTree(new Buffer('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz\r\n'))
},
{
uid: 49,
flags: ['\\Seen'],
idate: new Date(),
modseq: 5000,
mimeTree: parseMimeTree(fs.readFileSync(__dirname + '/fixtures/ryan_finnie_mime_torture.eml'))
},
{
uid: 50,
flags: ['\\Seen'],
modseq: 45,
idate: new Date(),
mimeTree: parseMimeTree(
'MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@tr.ee\r\n' +
'Content-Type: multipart/mixed;\r\n' +
' boundary=\'----mailcomposer-?=_1-1328088797399\'\r\n' +
'Message-Id: <testmessage-for-bug>;\r\n' +
'\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: message/rfc822\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@pangalink.net\r\n' +
'In-Reply-To: <test1>\r\n' +
'\r\n' +
'Hello world 1!\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: message/rfc822\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'MIME-Version: 1.0\r\n' +
'From: andris@kreata.ee\r\n' +
'To: andris@pangalink.net\r\n' +
'\r\n' +
'Hello world 2!\r\n' +
'------mailcomposer-?=_1-1328088797399\r\n' +
'Content-Type: text/html; charset=utf-8\r\n' +
'Content-Transfer-Encoding: quoted-printable\r\n' +
'\r\n' +
'<b>Hello world 3!</b>\r\n' +
'------mailcomposer-?=_1-1328088797399--\r\n'
)
},
{
uid: 52,
flags: [],
modseq: 4,
idate: new Date(),
mimeTree: parseMimeTree('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nHello World!\r\n')
},
{
uid: 53,
flags: [],
modseq: 5,
idate: new Date()
},
{
uid: 60,
flags: [],
modseq: 6,
idate: new Date()
}
],
journal: []
},
{
path: '[Gmail]/Sent Mail',
specialUse: '\\Sent',
uidValidity: 123,
uidNext: 90,
modifyIndex: 1,
messages: [],
journal: []
}
].forEach(folder => {
folders.set(folder.path, folder);
subscriptions.add(folder);
});
@ -113,7 +124,7 @@ module.exports = function (options) {
console.log('SERVER ERR\n%s', err.stack); // eslint-disable-line no-console
});
server.onAuth = function (login, session, callback) {
server.onAuth = function(login, session, callback) {
if (login.username !== 'testuser' || login.password !== 'pass') {
return callback();
}
@ -128,7 +139,7 @@ module.exports = function (options) {
// LIST "" "*"
// Returns all folders, query is informational
// folders is either an Array or a Map
server.onList = function (query, session, callback) {
server.onList = function(query, session, callback) {
this.logger.debug('[%s] LIST for "%s"', session.id, query);
callback(null, folders);
@ -137,7 +148,7 @@ module.exports = function (options) {
// LSUB "" "*"
// Returns all subscribed folders, query is informational
// folders is either an Array or a Map
server.onLsub = function (query, session, callback) {
server.onLsub = function(query, session, callback) {
this.logger.debug('[%s] LSUB for "%s"', session.id, query);
let subscribed = [];
@ -151,7 +162,7 @@ module.exports = function (options) {
};
// SUBSCRIBE "path/to/mailbox"
server.onSubscribe = function (mailbox, session, callback) {
server.onSubscribe = function(mailbox, session, callback) {
this.logger.debug('[%s] SUBSCRIBE to "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -163,7 +174,7 @@ module.exports = function (options) {
};
// UNSUBSCRIBE "path/to/mailbox"
server.onUnsubscribe = function (mailbox, session, callback) {
server.onUnsubscribe = function(mailbox, session, callback) {
this.logger.debug('[%s] UNSUBSCRIBE from "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -175,7 +186,7 @@ module.exports = function (options) {
};
// CREATE "path/to/mailbox"
server.onCreate = function (mailbox, session, callback) {
server.onCreate = function(mailbox, session, callback) {
this.logger.debug('[%s] CREATE "%s"', session.id, mailbox);
if (folders.has(mailbox)) {
@ -197,7 +208,7 @@ module.exports = function (options) {
// RENAME "path/to/mailbox" "new/path"
// NB! RENAME affects child and hierarchy mailboxes as well, this example does not do this
server.onRename = function (mailbox, newname, session, callback) {
server.onRename = function(mailbox, newname, session, callback) {
this.logger.debug('[%s] RENAME "%s" to "%s"', session.id, mailbox, newname);
if (!folders.has(mailbox)) {
@ -218,7 +229,7 @@ module.exports = function (options) {
};
// DELETE "path/to/mailbox"
server.onDelete = function (mailbox, session, callback) {
server.onDelete = function(mailbox, session, callback) {
this.logger.debug('[%s] DELETE "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -235,7 +246,7 @@ module.exports = function (options) {
};
// SELECT/EXAMINE
server.onOpen = function (mailbox, session, callback) {
server.onOpen = function(mailbox, session, callback) {
this.logger.debug('[%s] Opening "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -254,7 +265,7 @@ module.exports = function (options) {
};
// STATUS (X Y X)
server.onStatus = function (mailbox, session, callback) {
server.onStatus = function(mailbox, session, callback) {
this.logger.debug('[%s] Requested status for "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -273,20 +284,20 @@ module.exports = function (options) {
};
// APPEND mailbox (flags) date message
server.onAppend = function (mailbox, flags, date, raw, session, callback) {
server.onAppend = function(mailbox, flags, date, raw, session, callback) {
this.logger.debug('[%s] Appending message to "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
return callback(null, 'TRYCREATE');
}
date = date && new Date(date) || new Date();
date = (date && new Date(date)) || new Date();
let folder = folders.get(mailbox);
let message = {
uid: folder.uidNext++,
modseq: ++folder.modifyIndex,
date: date && new Date(date) || new Date(),
date: (date && new Date(date)) || new Date(),
mimeTree: parseMimeTree(raw),
flags
};
@ -294,21 +305,26 @@ module.exports = function (options) {
folder.messages.push(message);
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
this.notifier.addEntries(session.user.username, mailbox, {
command: 'EXISTS',
uid: message.uid
}, () => {
this.notifier.fire(session.user.username, mailbox);
return callback(null, true, {
uidValidity: folder.uidValidity,
this.notifier.addEntries(
session.user.username,
mailbox,
{
command: 'EXISTS',
uid: message.uid
});
});
},
() => {
this.notifier.fire(session.user.username, mailbox);
return callback(null, true, {
uidValidity: folder.uidValidity,
uid: message.uid
});
}
);
};
// STORE / UID STORE, updates flags for selected UIDs
server.onStore = function (mailbox, update, session, callback) {
server.onStore = function(mailbox, update, session, callback) {
this.logger.debug('[%s] Updating messages in "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -342,8 +358,7 @@ module.exports = function (options) {
switch (update.action) {
case 'set':
// check if update set matches current or is different
if (message.flags.length !== update.value.length ||
update.value.filter(flag => message.flags.indexOf(flag) < 0).length) {
if (message.flags.length !== update.value.length || update.value.filter(flag => message.flags.indexOf(flag) < 0).length) {
updated = true;
}
// set flags
@ -351,13 +366,15 @@ module.exports = function (options) {
break;
case 'add':
message.flags = message.flags.concat(update.value.filter(flag => {
if (message.flags.indexOf(flag) < 0) {
updated = true;
return true;
}
return false;
}));
message.flags = message.flags.concat(
update.value.filter(flag => {
if (message.flags.indexOf(flag) < 0) {
updated = true;
return true;
}
return false;
})
);
break;
case 'remove':
@ -377,19 +394,26 @@ module.exports = function (options) {
// Only show response if not silent or modseq is required
if (!update.silent || condstoreEnabled) {
session.writeStream.write(session.formatResponse('FETCH', message.uid, {
uid: update.isUid ? message.uid : false,
flags: update.silent ? false : message.flags,
modseq: condstoreEnabled ? message.modseq : false
}));
session.writeStream.write(
session.formatResponse('FETCH', message.uid, {
uid: update.isUid ? message.uid : false,
flags: update.silent ? false : message.flags,
modseq: condstoreEnabled ? message.modseq : false
})
);
}
this.notifier.addEntries(session.user.username, mailbox, {
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags
}, processMessages);
this.notifier.addEntries(
session.user.username,
mailbox,
{
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags
},
processMessages
);
} else {
processMessages();
}
@ -399,7 +423,7 @@ module.exports = function (options) {
};
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
server.onExpunge = function (mailbox, update, session, callback) {
server.onExpunge = function(mailbox, update, session, callback) {
this.logger.debug('[%s] Deleting messages from "%s"', session.id, mailbox);
if (!folders.has(mailbox)) {
@ -412,18 +436,16 @@ module.exports = function (options) {
for (i = folder.messages.length - 1; i >= 0; i--) {
if (
(
(update.isUid && update.messages.indexOf(folder.messages[i].uid) >= 0) ||
!update.isUid
) && folder.messages[i].flags.indexOf('\\Deleted') >= 0) {
((update.isUid && update.messages.indexOf(folder.messages[i].uid) >= 0) || !update.isUid) &&
folder.messages[i].flags.indexOf('\\Deleted') >= 0
) {
deleted.unshift(folder.messages[i].uid);
folder.messages.splice(i, 1);
}
}
let entries = [];
for (i = 0, len = deleted.length; i < len; i++) {
for ((i = 0), (len = deleted.length); i < len; i++) {
entries.push({
command: 'EXPUNGE',
ignore: session.id,
@ -441,7 +463,7 @@ module.exports = function (options) {
};
// COPY / UID COPY sequence mailbox
server.onCopy = function (mailbox, update, session, callback) {
server.onCopy = function(mailbox, update, session, callback) {
this.logger.debug('[%s] Copying messages from "%s" to "%s"', session.id, mailbox, update.destination);
if (!folders.has(mailbox)) {
@ -468,7 +490,7 @@ module.exports = function (options) {
}
}
for (i = 0, len = messages.length; i < len; i++) {
for ((i = 0), (len = messages.length); i < len; i++) {
messages[i].uid = destinationFolder.uidNext++;
destinationUid.push(messages[i].uid);
destinationFolder.messages.push(messages[i]);
@ -492,7 +514,7 @@ module.exports = function (options) {
};
// sends results to socket
server.onFetch = function (mailbox, options, session, callback) {
server.onFetch = function(mailbox, options, session, callback) {
this.logger.debug('[%s] Requested FETCH for "%s"', session.id, mailbox);
this.logger.debug('[%s] FETCH: %s', session.id, JSON.stringify(options.query));
if (!folders.has(mailbox)) {
@ -519,7 +541,6 @@ module.exports = function (options) {
flags: message.flags
});
}
});
}
@ -541,10 +562,12 @@ module.exports = function (options) {
return setImmediate(processMessage);
}
let stream = imapHandler.compileStream(session.formatResponse('FETCH', message.uid, {
query: options.query,
values: session.getQueryResponse(options.query, message)
}));
let stream = imapHandler.compileStream(
session.formatResponse('FETCH', message.uid, {
query: options.query,
values: session.getQueryResponse(options.query, message)
})
);
// send formatted response to socket
session.writeStream.write(stream, () => {
@ -557,7 +580,7 @@ module.exports = function (options) {
};
// returns an array of matching UID values and the highest modseq of matching messages
server.onSearch = function (mailbox, options, session, callback) {
server.onSearch = function(mailbox, options, session, callback) {
if (!folders.has(mailbox)) {
return callback(null, 'NONEXISTENT');
}

View file

@ -2,21 +2,21 @@
'use strict';
let imapTools = require('../lib/imap-tools');
let chai = require('chai');
let expect = chai.expect;
const imapTools = require('../lib/imap-tools');
const chai = require('chai');
const expect = chai.expect;
chai.config.includeStack = true;
describe('#packMessageRange', function () {
it('should return as is', function () {
describe('#packMessageRange', function() {
it('should return as is', function() {
expect(imapTools.packMessageRange([1, 3, 5, 9])).to.equal('1,3,5,9');
});
it('should return a range', function () {
it('should return a range', function() {
expect(imapTools.packMessageRange([1, 2, 3, 4])).to.equal('1:4');
});
it('should return mixed ranges', function () {
it('should return mixed ranges', function() {
expect(imapTools.packMessageRange([1, 3, 4, 6, 8, 9, 10, 11, 13])).to.equal('1,3:4,6,8:11,13');
});
});

View file

@ -41,59 +41,65 @@ module.exports = (options, callback) => {
return callback(null, false);
}
db.redis.multi().
// delete all old entries
zremrangebyscore('war:' + options.user._id, '-inf', Date.now() - MAX_AUTOREPLY_INTERVAL).
// add enw entry if not present
zadd('war:' + options.user._id, 'NX', Date.now(), options.sender).
exec((err, response) => {
if (err) {
return callback(null, false);
}
if (!response || !response[1]) {
// already responded
return callback(null, false);
}
// check limiting counters
options.messageHandler.counters.ttlcounter('wda:' + options.user._id, 1, 2000, (err, result) => {
if (err || !result.success) {
db.redis
.multi()
// delete all old entries
.zremrangebyscore('war:' + options.user._id, '-inf', Date.now() - MAX_AUTOREPLY_INTERVAL)
// add enw entry if not present
.zadd('war:' + options.user._id, 'NX', Date.now(), options.sender)
.exec((err, response) => {
if (err) {
return callback(null, false);
}
let data = {
envelope: {
from: '',
to: options.sender
},
from: {
name: options.user.name,
address: options.recipient
},
to: options.sender,
subject: options.user.autoreply.subject ? 'Auto: ' + options.user.autoreply.subject : {
prepared: true,
value: 'Auto: Re: ' + headers.getFirst('Subject')
},
headers: {
'Auto-Submitted': 'auto-replied'
},
inReplyTo: headers.getFirst('Message-ID'),
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
text: options.user.autoreply.message
};
if (!response || !response[1]) {
// already responded
return callback(null, false);
}
let compiler = new MailComposer(data);
let message = maildrop({
from: '',
to: options.sender,
interface: 'autoreply'
}, callback);
// check limiting counters
options.messageHandler.counters.ttlcounter('wda:' + options.user._id, 1, 2000, (err, result) => {
if (err || !result.success) {
return callback(null, false);
}
compiler.compile().createReadStream().pipe(message);
let data = {
envelope: {
from: '',
to: options.sender
},
from: {
name: options.user.name,
address: options.recipient
},
to: options.sender,
subject: options.user.autoreply.subject
? 'Auto: ' + options.user.autoreply.subject
: {
prepared: true,
value: 'Auto: Re: ' + headers.getFirst('Subject')
},
headers: {
'Auto-Submitted': 'auto-replied'
},
inReplyTo: headers.getFirst('Message-ID'),
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
text: options.user.autoreply.message
};
let compiler = new MailComposer(data);
let message = maildrop(
{
from: '',
to: options.sender,
interface: 'autoreply'
},
callback
);
compiler.compile().createReadStream().pipe(message);
});
});
});
});
messageSplitter.on('error', () => false);
messageSplitter.on('data', () => false);

View file

@ -36,9 +36,9 @@ module.exports = redis => {
return callback(err);
}
return callback(null, {
success: !!(res && res[0] || 0),
value: res && res[1] || 0,
ttl: res && res[2] || 0
success: !!((res && res[0]) || 0),
value: (res && res[1]) || 0,
ttl: (res && res[2]) || 0
});
});
});

View file

@ -21,14 +21,13 @@ class DkimStream extends Transform {
// find next remainder
let nextRemainder = '';
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
// If we get another chunk that does not match this description then we can restore the previously processed data
let state = 'file';
for (let i = chunk.length - 1; i >= 0; i--) {
let c = chunk[i];
if (state === 'file' && (c === 0x0A || c === 0x0D)) {
if (state === 'file' && (c === 0x0a || c === 0x0d)) {
// do nothing, found \n or \r at the end of chunk, stil end of file
} else if (state === 'file' && (c === 0x09 || c === 0x20)) {
// switch to line ending mode, this is the last non-empty line
@ -47,8 +46,10 @@ class DkimStream extends Transform {
if (i === 0) {
// reached to the beginning of the chunk, check if it is still about the ending
// and if the remainder also matches
if ((state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))) {
if (
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
) {
// keep everything
this.remainder += chunk.toString('binary');
return;
@ -74,11 +75,11 @@ class DkimStream extends Transform {
if (chunk && !needsFixing) {
// check if we even need to change anything
for (let i = 0, len = chunk.length; i < len; i++) {
if (i && chunk[i] === 0x0A && chunk[i - 1] !== 0x0D) {
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
// missing \r before \n
needsFixing = true;
break;
} else if (i && chunk[i] === 0x0D && chunk[i - 1] === 0x20) {
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
// trailing WSP found
needsFixing = true;
break;
@ -97,9 +98,10 @@ class DkimStream extends Transform {
if (needsFixing) {
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
this.remainder = nextRemainder;
bodyStr = bodyStr.replace(/\r?\n/g, '\n') // use js line endings
.replace(/[ \t]*$/mg, '') // remove line endings, rtrim
.replace(/[ \t]+/mg, ' ') // single spaces
bodyStr = bodyStr
.replace(/\r?\n/g, '\n') // use js line endings
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim
.replace(/[ \t]+/gm, ' ') // single spaces
.replace(/\n/g, '\r\n'); // restore rfc822 line endings
chunk = Buffer.from(bodyStr, 'binary');
} else if (nextRemainder) {

View file

@ -8,16 +8,19 @@ module.exports = (options, callback) => {
return callback(null, false);
}
let message = maildrop({
from: options.sender,
to: options.recipient,
let message = maildrop(
{
from: options.sender,
to: options.recipient,
forward: options.forward,
http: !!options.targetUrl,
targeUrl: options.targetUrl,
forward: options.forward,
http: !!options.targetUrl,
targeUrl: options.targetUrl,
interface: 'forwarder'
}, callback);
interface: 'forwarder'
},
callback
);
setImmediate(() => {
let pos = 0;

View file

@ -8,7 +8,6 @@ const redis = require('redis');
const log = require('npmlog');
class ImapNotifier extends EventEmitter {
constructor(options) {
super();
@ -149,12 +148,14 @@ class ImapNotifier extends EventEmitter {
user = false;
}
let mailboxQuery = mailbox ? {
_id: mailbox._id
} : {
user,
path
};
let mailboxQuery = mailbox
? {
_id: mailbox._id
}
: {
user,
path
};
if (updated.length) {
// provision new modseq value
@ -268,14 +269,16 @@ class ImapNotifier extends EventEmitter {
if (!mailbox) {
return callback(null, 'NONEXISTENT');
}
this.database.collection('journal').find({
mailbox: mailbox._id,
modseq: {
$gt: modifyIndex
}
}).sort([
['modseq', 1]
]).toArray(callback);
this.database
.collection('journal')
.find({
mailbox: mailbox._id,
modseq: {
$gt: modifyIndex
}
})
.sort([['modseq', 1]])
.toArray(callback);
});
}
}

View file

@ -36,12 +36,15 @@ function parseAddressList(headers, key, withNames) {
}
function parseAddressses(headerList, withNames) {
let map = convertAddresses(headerList.map(address => {
if (typeof address === 'string') {
address = addressparser(address);
}
return address;
}), withNames);
let map = convertAddresses(
headerList.map(address => {
if (typeof address === 'string') {
address = addressparser(address);
}
return address;
}),
withNames
);
return Array.from(map).map(entry => entry[1]);
}
@ -183,7 +186,6 @@ module.exports = (options, callback) => {
messageSplitter.once('error', err => dkimStream.emit('error', err));
store(id, dkimStream, err => {
if (err) {
return callback(err);
}
@ -197,7 +199,6 @@ module.exports = (options, callback) => {
let date = new Date();
for (let i = 0, len = deliveries.length; i < len; i++) {
let recipient = deliveries[i];
let deliveryZone = options.zone || config.sender.zone || 'default';
let recipientDomain = recipient.to.substr(recipient.to.lastIndexOf('@') + 1).replace(/[\[\]]/g, '');
@ -230,8 +231,7 @@ module.exports = (options, callback) => {
documents.push(delivery);
}
db.senderDb.collection(config.sender.collection).
insertMany(documents, {
db.senderDb.collection(config.sender.collection).insertMany(documents, {
w: 1,
ordered: false
}, err => {
@ -249,9 +249,11 @@ module.exports = (options, callback) => {
};
function store(id, stream, callback) {
gridstore = gridstore || new GridFSBucket(db.senderDb, {
bucketName: config.sender.gfs
});
gridstore =
gridstore ||
new GridFSBucket(db.senderDb, {
bucketName: config.sender.gfs
});
let returned = false;
let store = gridstore.openUploadStream('message ' + id, {

View file

@ -61,15 +61,15 @@ class MessageSplitter extends Transform {
} else {
chr = data[i - lblen];
}
if (chr === 0x0A && i) {
if (chr === 0x0a && i) {
let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
if (pr1 === 0x0A) {
if (pr1 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
} else if (pr1 === 0x0D && pr2 === 0x0A) {
} else if (pr1 === 0x0d && pr2 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;

View file

@ -27,11 +27,15 @@ class POP3Connection extends EventEmitter {
init() {
this._setListeners();
this._resetSession();
this._server.logger.info({
tnx: 'connection',
cid: this._id,
host: this.remoteAddress
}, 'Connection from %s', this.remoteAddress);
this._server.logger.info(
{
tnx: 'connection',
cid: this._id,
host: this.remoteAddress
},
'Connection from %s',
this.remoteAddress
);
this.send('+OK WDPop ready for requests from ' + this.remoteAddress);
}
@ -51,11 +55,15 @@ class POP3Connection extends EventEmitter {
payload = payload.join('\r\n') + '\r\n.';
}
this._server.logger.debug({
tnx: 'send',
cid: this._id,
host: this.remoteAddress
}, 'S:', (payload.length < 128 ? payload : payload.substr(0, 128) + '... +' + (payload.length - 128) + ' B').replace(/\r?\n/g, '\\n'));
this._server.logger.debug(
{
tnx: 'send',
cid: this._id,
host: this.remoteAddress
},
'S:',
(payload.length < 128 ? payload : payload.substr(0, 128) + '... +' + (payload.length - 128) + ' B').replace(/\r?\n/g, '\\n')
);
this.write(payload + '\r\n');
}
@ -77,7 +85,7 @@ class POP3Connection extends EventEmitter {
* Fired when the socket is closed
* @event
*/
_onClose( /* hadError */ ) {
_onClose(/* hadError */) {
if (this._closed) {
return;
}
@ -89,12 +97,16 @@ class POP3Connection extends EventEmitter {
this._closed = true;
this._closing = false;
this._server.logger.info({
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.session.user && this.session.user.username
}, 'Connection closed to %s', this.remoteAddress);
this._server.logger.info(
{
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.session.user && this.session.user.username
},
'Connection closed to %s',
this.remoteAddress
);
this.emit('close');
}
@ -110,11 +122,15 @@ class POP3Connection extends EventEmitter {
return this.close(); // mark connection as 'closing'
}
this._server.logger.error({
err,
tnx: 'error',
user: this.session.user && this.session.user.username
}, '%s', err.message);
this._server.logger.error(
{
err,
tnx: 'error',
user: this.session.user && this.session.user.username
},
'%s',
err.message
);
this.emit('error', err);
}
@ -176,19 +192,27 @@ class POP3Connection extends EventEmitter {
if (typeof this._nextHandler === 'function') {
let handler = this._nextHandler;
this._nextHandler = null;
this._server.logger.debug({
tnx: 'receive',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'C: <%s bytes of continue data>', Buffer.byteLength(line));
this._server.logger.debug(
{
tnx: 'receive',
cid: this._id,
user: this.session.user && this.session.user.username
},
'C: <%s bytes of continue data>',
Buffer.byteLength(line)
);
return handler(line, err => {
if (err) {
this._server.logger.info({
err,
tnx: '+',
cid: this._id,
host: this.remoteAddress
}, 'Error processing continue data. %s', err.message);
this._server.logger.info(
{
err,
tnx: '+',
cid: this._id,
host: this.remoteAddress
},
'Error processing continue data. %s',
err.message
);
this.send('-ERR ' + err.message);
this.close();
} else {
@ -205,22 +229,31 @@ class POP3Connection extends EventEmitter {
if (/^(PASS|AUTH PLAIN)\s+[^\s]+/i.test(line)) {
logLine = logLine.replace(/[^\s]+$/, '*hidden*');
}
this._server.logger.debug({
tnx: 'receive',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'C:', logLine);
this._server.logger.debug(
{
tnx: 'receive',
cid: this._id,
user: this.session.user && this.session.user.username
},
'C:',
logLine
);
if (typeof this['command_' + command] === 'function') {
this['command_' + command](args, err => {
if (err) {
this._server.logger.info({
err,
tnx: 'command',
this._server.logger.info(
{
err,
tnx: 'command',
command,
cid: this._id,
host: this.remoteAddress
},
'Error running %s. %s',
command,
cid: this._id,
host: this.remoteAddress
}, 'Error running %s. %s', command, err.message);
err.message
);
this.send('-ERR ' + err.message);
this.close();
} else {
@ -282,49 +315,68 @@ class POP3Connection extends EventEmitter {
let password = args;
this.session.user = false;
this._server.onAuth({
method: 'USER',
username,
password
}, this.session, (err, response) => {
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'USER',
user: username
}, 'Authentication error for %s using %s. %s', username, 'USER', err.message);
return next(err);
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'USER',
user: username
}, 'Authentication failed for %s using %s', username, 'USER');
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
this._server.onAuth(
{
method: 'USER',
user: username
}, '%s authenticated using %s', username, 'USER');
this.session.user = response.user;
this.openMailbox(err => {
username,
password
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'USER',
user: username
},
'Authentication error for %s using %s. %s',
username,
'USER',
err.message
);
return next(err);
}
next();
});
});
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'USER',
user: username
},
'Authentication failed for %s using %s',
username,
'USER'
);
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'USER',
user: username
},
'%s authenticated using %s',
username,
'USER'
);
this.session.user = response.user;
this.openMailbox(err => {
if (err) {
return next(err);
}
next();
});
}
);
}
command_AUTH(args, next) {
@ -392,10 +444,14 @@ class POP3Connection extends EventEmitter {
return finish();
}
this._server.onUpdate({
deleted,
seen
}, this.session, finish);
this._server.onUpdate(
{
deleted,
seen
},
this.session,
finish
);
}
// https://tools.ietf.org/html/rfc1939#page-6
@ -433,14 +489,11 @@ class POP3Connection extends EventEmitter {
if (index) {
this.send('+OK ' + index + ' ' + this.session.listing.messages[index - 1].size);
} else {
this.send(
['+OK ' + this.session.listing.count + ' ' + this.session.listing.size]
.concat(
this.session.listing.messages
.filter(message => !message.popped)
.map((message, i) => (i + 1) + ' ' + message.size)
));
['+OK ' + this.session.listing.count + ' ' + this.session.listing.size].concat(
this.session.listing.messages.filter(message => !message.popped).map((message, i) => i + 1 + ' ' + message.size)
)
);
}
return next();
@ -470,13 +523,7 @@ class POP3Connection extends EventEmitter {
if (index) {
this.send('+OK ' + index + ' ' + this.session.listing.messages[index - 1].id);
} else {
this.send(
['+OK']
.concat(
this.session.listing.messages
.filter(message => !message.popped)
.map((message, i) => (i + 1) + ' ' + message.id)
));
this.send(['+OK'].concat(this.session.listing.messages.filter(message => !message.popped).map((message, i) => i + 1 + ' ' + message.id)));
}
return next();
@ -538,7 +585,15 @@ class POP3Connection extends EventEmitter {
this.session.listing.count += count;
this.session.listing.size += size;
this.send('+OK maildrop has ' + this.session.listing.count + ' message' + (this.session.listing.count !== 1 ? 's' : '') + ' (' + this.session.listing.size + ' octets)');
this.send(
'+OK maildrop has ' +
this.session.listing.count +
' message' +
(this.session.listing.count !== 1 ? 's' : '') +
' (' +
this.session.listing.size +
' octets)'
);
return next();
}
@ -706,60 +761,84 @@ class POP3Connection extends EventEmitter {
let username = credentials[1] || credentials[0] || '';
let password = credentials[2] || '';
this._server.onAuth({
method: 'PLAIN',
username,
password
}, this.session, (err, response) => {
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'PLAIN',
user: username
}, 'Authentication error for %s using %s. %s', username, 'PLAIN', err.message);
return next(err);
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'PLAIN',
user: username
}, 'Authentication failed for %s using %s', username, 'PLAIN');
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
this._server.onAuth(
{
method: 'PLAIN',
user: username
}, '%s authenticated using %s', username, 'PLAIN');
this.session.user = response.user;
this.openMailbox(err => {
username,
password
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'PLAIN',
user: username
},
'Authentication error for %s using %s. %s',
username,
'PLAIN',
err.message
);
return next(err);
}
next();
});
});
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'PLAIN',
user: username
},
'Authentication failed for %s using %s',
username,
'PLAIN'
);
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
},
'%s authenticated using %s',
username,
'PLAIN'
);
this.session.user = response.user;
this.openMailbox(err => {
if (err) {
return next(err);
}
next();
});
}
);
}
openMailbox(next) {
this._server.onListMessages(this.session, (err, listing) => {
if (err) {
this._server.logger.info({
err,
tnx: 'listerr',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'Failed listing messages for %s. %s', this.session.user.username, err.message);
this._server.logger.info(
{
err,
tnx: 'listerr',
cid: this._id,
user: this.session.user && this.session.user.username
},
'Failed listing messages for %s. %s',
this.session.user.username,
err.message
);
return next(err);
}

View file

@ -41,8 +41,7 @@ class POP3Server extends EventEmitter {
component: this.options.component || 'pop3-server'
});
this.server = (this.options.secure ? tls : net)
.createServer(this.options, socket => this._onConnect(socket));
this.server = (this.options.secure ? tls : net).createServer(this.options, socket => this._onConnect(socket));
this._setListeners();
}
@ -73,7 +72,8 @@ class POP3Server extends EventEmitter {
this.options.secure ? 'Secure ' : '',
'POP3',
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
address.port);
address.port
);
}
/**
@ -82,9 +82,12 @@ class POP3Server extends EventEmitter {
* @event
*/
_onClose() {
this.logger.info({
tnx: 'closed'
}, 'POP3 Server closed');
this.logger.info(
{
tnx: 'closed'
},
'POP3 Server closed'
);
this.emit('close');
}
@ -124,17 +127,28 @@ class POP3Server extends EventEmitter {
// close active connections
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
this.logger.info(
{
tnx: 'close'
},
'Server closing with %s pending connection%s, waiting %s seconds before terminating',
connections,
connections !== 1 ? 's' : '',
timeout / 1000
);
}
this._closeTimeout = setTimeout(() => {
connections = this.connections.size;
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.logger.info(
{
tnx: 'close'
},
'Closing %s pending connection%s to close the server',
connections,
connections !== 1 ? 's' : ''
);
this.connections.forEach(connection => {
connection.close();

View file

@ -40,7 +40,6 @@ function redisConfig(defaultConfig) {
});
if (!response.hasOwnProperty('retry_strategy')) {
response.retry_strategy = options => {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with a individual error
return new Error('The server refused the connection');

View file

@ -71,7 +71,6 @@ class UserHandler {
user: true
}
}, (err, addressData) => {
if (err) {
return callback(err);
}
@ -110,11 +109,12 @@ class UserHandler {
// try master password
if (bcrypt.compareSync(password, userData.password || '')) {
meta.scope = 'master';
this.redis.multi().
zadd('wl:' + userData._id.toString(), Date.now(), JSON.stringify(meta)).
zremrangebyscore('wl:' + userData._id.toString(), '-INF', Date.now() - (10 * 24 * 3600 * 1000)).
expire('wl:' + userData._id.toString(), 10 * 24 * 3600).
exec(() => false);
this.redis
.multi()
.zadd('wl:' + userData._id.toString(), Date.now(), JSON.stringify(meta))
.zremrangebyscore('wl:' + userData._id.toString(), '-INF', Date.now() - 10 * 24 * 3600 * 1000)
.expire('wl:' + userData._id.toString(), 10 * 24 * 3600)
.exec(() => false);
return callback(null, {
user: userData._id,
@ -134,13 +134,13 @@ class UserHandler {
for (let i = 0; i < userData.asp.length; i++) {
let asp = userData.asp[i];
if (bcrypt.compareSync(password, asp.password || '')) {
meta.scope = asp.id.toString();
this.redis.multi().
zadd('wl:' + userData._id.toString(), Date.now(), JSON.stringify(meta)).
zremrangebyscore('wl:' + userData._id.toString(), '-INF', Date.now() - (10 * 24 * 3600 * 1000)).
expire('wl:' + userData._id.toString(), 10 * 24 * 3600).
exec(() => false);
this.redis
.multi()
.zadd('wl:' + userData._id.toString(), Date.now(), JSON.stringify(meta))
.zremrangebyscore('wl:' + userData._id.toString(), '-INF', Date.now() - 10 * 24 * 3600 * 1000)
.expire('wl:' + userData._id.toString(), 10 * 24 * 3600)
.exec(() => false);
return callback(null, {
user: userData._id,
@ -323,7 +323,6 @@ class UserHandler {
seed: true
}
}, (err, entry) => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', username, err.message);
return callback(new Error('Database Error, failed to check user'));
@ -539,26 +538,32 @@ class UserHandler {
return callback(null, userData._id);
});
});
}
getMailboxes(language) {
let translation = mailboxTranslations.hasOwnProperty(language) ? mailboxTranslations[language] : mailboxTranslations.en;
let defaultMailboxes = [{
path: 'INBOX'
}, {
specialUse: '\\Sent'
}, {
specialUse: '\\Trash'
}, {
specialUse: '\\Drafts'
}, {
specialUse: '\\Junk'
}, {
specialUse: '\\Archive'
}];
let defaultMailboxes = [
{
path: 'INBOX'
},
{
specialUse: '\\Sent'
},
{
specialUse: '\\Trash'
},
{
specialUse: '\\Drafts'
},
{
specialUse: '\\Junk'
},
{
specialUse: '\\Archive'
}
];
let uidValidity = Math.floor(Date.now() / 1000);

173
lmtp.js
View file

@ -15,7 +15,6 @@ const fs = require('fs');
let messageHandler;
const serverOptions = {
lmtp: true,
// log to console
@ -42,7 +41,6 @@ const serverOptions = {
disabledCommands: ['AUTH'],
onMailFrom(address, session, callback) {
// reset session entries
session.users = [];
@ -120,9 +118,8 @@ const serverOptions = {
});
stream.once('end', () => {
let spamHeader = config.spamHeader && config.spamHeader.toLowerCase();
let sender = tools.normalizeAddress(session.envelope.mailFrom && session.envelope.mailFrom.address || '');
let sender = tools.normalizeAddress((session.envelope.mailFrom && session.envelope.mailFrom.address) || '');
let responses = [];
let users = session.users;
let stored = 0;
@ -164,55 +161,59 @@ const serverOptions = {
let mailboxQueryKey = 'path';
let mailboxQueryValue = 'INBOX';
let filters = (user.filters || []).concat(spamHeader ? {
id: 'SPAM',
query: {
headers: {
[spamHeader]: 'Yes'
let filters = (user.filters || []).concat(
spamHeader
? {
id: 'SPAM',
query: {
headers: {
[spamHeader]: 'Yes'
}
},
action: {
// only applies if any other filter does not already mark message as spam or ham
spam: true
}
}
},
action: {
// only applies if any other filter does not already mark message as spam or ham
spam: true
}
} : []);
: []
);
let forwardTargets = new Set();
let forwardTargetUrls = new Set();
let matchingFilters = [];
let filterActions = new Map();
filters.
// apply all filters to the message
map(filter => checkFilter(filter, prepared, maildata)).
// remove all unmatched filers
filter(filter => filter).
// apply filter actions
forEach(filter => {
matchingFilters.push(filter.id);
filters
// apply all filters to the message
.map(filter => checkFilter(filter, prepared, maildata))
// remove all unmatched filers
.filter(filter => filter)
// apply filter actions
.forEach(filter => {
matchingFilters.push(filter.id);
// apply matching filter
if (!filterActions) {
filterActions = filter.action;
} else {
Object.keys(filter.action).forEach(key => {
if (key === 'forward') {
forwardTargets.add(filter.action[key]);
return;
}
// apply matching filter
if (!filterActions) {
filterActions = filter.action;
} else {
Object.keys(filter.action).forEach(key => {
if (key === 'forward') {
forwardTargets.add(filter.action[key]);
return;
}
if (key === 'targetUrl') {
forwardTargetUrls.add(filter.action[key]);
return;
}
if (key === 'targetUrl') {
forwardTargetUrls.add(filter.action[key]);
return;
}
// if a previous filter already has set a value then do not touch it
if (!filterActions.has(key)) {
filterActions.set(key, filter.action[key]);
}
});
}
});
// if a previous filter already has set a value then do not touch it
if (!filterActions.has(key)) {
filterActions.set(key, filter.action[key]);
}
});
}
});
let forwardMessage = done => {
if (user.forward && !filterActions.get('delete')) {
@ -231,26 +232,34 @@ const serverOptions = {
}
// check limiting counters
messageHandler.counters.ttlcounter('wdf:' + user._id.toString(), forwardTargets.size + forwardTargetUrls.size, user.forwards, (err, result) => {
if (err) {
// failed checks
log.error('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + user._id.toString(), err.message);
} else if (!result.success) {
log.silly('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + user._id.toString(), 'Precondition failed');
return done();
messageHandler.counters.ttlcounter(
'wdf:' + user._id.toString(),
forwardTargets.size + forwardTargetUrls.size,
user.forwards,
(err, result) => {
if (err) {
// failed checks
log.error('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + user._id.toString(), err.message);
} else if (!result.success) {
log.silly('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + user._id.toString(), 'Precondition failed');
return done();
}
forward(
{
user,
sender,
recipient,
forward: forwardTargets.size ? Array.from(forwardTargets) : false,
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
chunks
},
done
);
}
forward({
user,
sender,
recipient,
forward: forwardTargets.size ? Array.from(forwardTargets) : false,
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
chunks
}, done);
});
);
};
let sendAutoreply = done => {
@ -259,20 +268,39 @@ const serverOptions = {
return setImmediate(done);
}
autoreply({
user,
sender,
recipient,
chunks,
messageHandler
}, done);
autoreply(
{
user,
sender,
recipient,
chunks,
messageHandler
},
done
);
};
forwardMessage((err, id) => {
if (err) {
log.error('LMTP', '%s FRWRDFAIL from=%s to=%s target=%s error=%s', prepared.id.toString(), sender, recipient, Array.from(forwardTargets).concat(forwardTargetUrls).join(','), err.message);
log.error(
'LMTP',
'%s FRWRDFAIL from=%s to=%s target=%s error=%s',
prepared.id.toString(),
sender,
recipient,
Array.from(forwardTargets).concat(forwardTargetUrls).join(','),
err.message
);
} else if (id) {
log.silly('LMTP', '%s FRWRDOK id=%s from=%s to=%s target=%s', prepared.id.toString(), id, sender, recipient, Array.from(forwardTargets).concat(forwardTargetUrls).join(','));
log.silly(
'LMTP',
'%s FRWRDOK id=%s from=%s to=%s target=%s',
prepared.id.toString(),
id,
sender,
recipient,
Array.from(forwardTargets).concat(forwardTargetUrls).join(',')
);
}
sendAutoreply((err, id) => {
@ -324,7 +352,7 @@ const serverOptions = {
});
let messageOptions = {
user: user && user._id || user,
user: (user && user._id) || user,
[mailboxQueryKey]: mailboxQueryValue,
prepared,
@ -351,7 +379,6 @@ const serverOptions = {
};
messageHandler.add(messageOptions, (err, inserted, info) => {
// remove Delivered-To
chunks.shift();
chunklen -= header.length;

View file

@ -33,7 +33,7 @@ if (config.syslog && syslog) {
log.on('log.warn', data => syslog.warn(...logger(data)));
case 'error':
log.on('log.error', data => syslog.error(...logger(data)));
/* eslint-enable no-fallthrough */
/* eslint-enable no-fallthrough */
}
log.level = 'silent'; // disable normal log stream

View file

@ -10,7 +10,8 @@
"author": "Andris Reinman",
"license": "EUPL-1.1",
"devDependencies": {
"chai": "^3.5.0",
"browserbox": "^0.9.1",
"chai": "^4.0.1",
"eslint-config-nodemailer": "^1.0.0",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
@ -25,12 +26,12 @@
"generate-password": "^1.3.0",
"html-to-text": "^3.3.0",
"iconv-lite": "^0.4.17",
"joi": "^10.5.0",
"joi": "^10.5.2",
"libbase64": "^0.1.0",
"libmime": "^3.1.0",
"libqp": "^1.1.0",
"mailsplit": "^4.0.2",
"mongodb": "^2.2.27",
"mongodb": "^2.2.28",
"node-redis-scripty": "0.0.5",
"nodemailer": "^4.0.1",
"npmlog": "^4.1.0",

181
pop3.js
View file

@ -36,29 +36,34 @@ const serverOptions = {
},
onAuth(auth, session, callback) {
userHandler.authenticate(auth.username, auth.password, {
protocol: 'POP3',
ip: session.remoteAddress
}, (err, result) => {
if (err) {
return callback(err);
}
if (!result) {
return callback();
}
if (result.scope === 'master' && result.enabled2fa) {
// master password not allowed if 2fa is enabled!
return callback();
}
callback(null, {
user: {
id: result.user,
username: result.username
userHandler.authenticate(
auth.username,
auth.password,
{
protocol: 'POP3',
ip: session.remoteAddress
},
(err, result) => {
if (err) {
return callback(err);
}
});
});
if (!result) {
return callback();
}
if (result.scope === 'master' && result.enabled2fa) {
// master password not allowed if 2fa is enabled!
return callback();
}
callback(null, {
user: {
id: result.user,
username: result.username
}
});
}
);
},
onListMessages(session, callback) {
@ -67,7 +72,6 @@ const serverOptions = {
user: session.user.id,
path: 'INBOX'
}, (err, mailbox) => {
if (err) {
return callback(err);
}
@ -78,37 +82,41 @@ const serverOptions = {
session.user.mailbox = mailbox._id;
db.database.collection('messages').find({
mailbox: mailbox._id
}).project({
uid: true,
size: true,
// required to decide if we need to update flags after RETR
flags: true,
seen: true
}).sort([
['uid', -1]
]).limit(config.pop3.maxMessages || MAX_MESSAGES).toArray((err, messages) => {
if (err) {
return callback(err);
}
db.database
.collection('messages')
.find({
mailbox: mailbox._id
})
.project({
uid: true,
size: true,
// required to decide if we need to update flags after RETR
flags: true,
seen: true
})
.sort([['uid', -1]])
.limit(config.pop3.maxMessages || MAX_MESSAGES)
.toArray((err, messages) => {
if (err) {
return callback(err);
}
return callback(null, {
messages: messages.
// showolder first
reverse().
// compose message objects
map(message => ({
id: message._id.toString(),
uid: message.uid,
size: message.size,
flags: message.flags,
seen: message.seen
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
return callback(null, {
messages: messages
// showolder first
.reverse()
// compose message objects
.map(message => ({
id: message._id.toString(),
uid: message.uid,
size: message.size,
flags: message.flags,
seen: message.seen
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
});
});
});
});
},
@ -136,7 +144,6 @@ const serverOptions = {
},
onUpdate(update, session, callback) {
let handleSeen = next => {
if (update.seen && update.seen.length) {
return markAsSeen(session, update.seen, next);
@ -193,25 +200,28 @@ function trashMessages(session, messages, callback) {
return callback(new Error('Trash mailbox not found for user'));
}
messageHandler.move({
user: session.user.id,
// folder to move messages from
source: {
mailbox: session.user.mailbox
},
// folder to move messages to
destination: trashMailbox,
// list of UIDs to move
messages: messages.map(message => message.uid),
messageHandler.move(
{
user: session.user.id,
// folder to move messages from
source: {
mailbox: session.user.mailbox
},
// folder to move messages to
destination: trashMailbox,
// list of UIDs to move
messages: messages.map(message => message.uid),
// add \Seen flags to deleted messages
markAsSeen: true
}, (err, success, meta) => {
if (err) {
return callback(err);
// add \Seen flags to deleted messages
markAsSeen: true
},
(err, success, meta) => {
if (err) {
return callback(err);
}
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
}
callback(null, success && meta && meta.destinationUid && meta.destinationUid.length || 0);
});
);
});
}
@ -259,19 +269,24 @@ function markAsSeen(session, messages, callback) {
if (err) {
return callback(err);
}
messageHandler.notifier.addEntries(mailboxData, false, messages.map(message => {
let result = {
command: 'FETCH',
uid: message.uid,
flags: message.flags.concat('\\Seen'),
message: new ObjectID(message.id),
modseq: mailboxData.modifyIndex
};
return result;
}), () => {
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
callback(null, messages.length);
});
messageHandler.notifier.addEntries(
mailboxData,
false,
messages.map(message => {
let result = {
command: 'FETCH',
uid: message.uid,
flags: message.flags.concat('\\Seen'),
message: new ObjectID(message.id),
modseq: mailboxData.modifyIndex
};
return result;
}),
() => {
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
callback(null, messages.length);
}
);
});
});
}

View file

@ -4,14 +4,14 @@
process.env.UV_THREADPOOL_SIZE = 16;
let config = require('config');
let log = require('npmlog');
let packageData = require('./package.json');
const config = require('config');
const log = require('npmlog');
const packageData = require('./package.json');
log.level = config.log.level;
require('./logger');
let printLogo = () => {
const printLogo = () => {
log.info('App', '');
log.info('App', ' ## ## ###### ## ##### ##### ## ## #### ## ##');
log.info('App', ' ## ## ## ## ## ## ## ## ## ## ## ## ## ##');

View file

@ -1,12 +1,12 @@
'use strict';
let config = require('config');
let log = require('npmlog');
let imap = require('./imap');
let pop3 = require('./pop3');
let lmtp = require('./lmtp');
let api = require('./api');
let db = require('./lib/db');
const config = require('config');
const log = require('npmlog');
const imap = require('./imap');
const pop3 = require('./pop3');
const lmtp = require('./lmtp');
const api = require('./api');
const db = require('./lib/db');
// Initialize database connection
db.connect(err => {