mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-09-20 07:16:05 +08:00
Use prettier for formatting
This commit is contained in:
parent
40b36fed53
commit
08a4cdde0a
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"extends": "nodemailer"
|
||||
"extends": "nodemailer",
|
||||
"fix": false
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (grunt) {
|
||||
|
||||
module.exports = function(grunt) {
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
eslint: {
|
||||
|
|
775
api.js
775
api.js
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
module.exports = {
|
||||
handler(command, callback) {
|
||||
|
||||
let capabilities = [];
|
||||
|
||||
if (!this.secure) {
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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]':
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ module.exports = {
|
|||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// fixed structre
|
||||
this.send('* NAMESPACE (("" "/")) NIL NIL');
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' : '')
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 || {};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, '=');
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])) {
|
||||
|
|
|
@ -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
56
imap-core/test/client.js
Normal 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();
|
24
imap-core/test/fixtures/chunks.js
vendored
24
imap-core/test/fixtures/chunks.js
vendored
File diff suppressed because one or more lines are too long
2024
imap-core/test/fixtures/mimetorture.js
vendored
2024
imap-core/test/fixtures/mimetorture.js
vendored
File diff suppressed because it is too large
Load diff
101
imap-core/test/fixtures/mimetree.js
vendored
101
imap-core/test/fixtures/mimetree.js
vendored
|
@ -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))';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
100
lib/autoreply.js
100
lib/autoreply.js
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
173
lmtp.js
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
181
pop3.js
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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', ' ## ## ## ## ## ## ## ## ## ## ## ## ## ##');
|
||||
|
|
14
worker.js
14
worker.js
|
@ -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 => {
|
||||
|
|
Loading…
Reference in a new issue