Added login rate limiting. Updated flags in batches. Do not index too long header values

v1.0.11
This commit is contained in:
Andris Reinman 2017-04-05 11:39:42 +03:00
parent 66146e6680
commit c9b9442de6
13 changed files with 329 additions and 300 deletions

View file

@ -452,17 +452,13 @@ Response message includes the following fields
- **headers** is an array that lists all headers of the message. A header is an object:
- **key** is the lowercase key of the header
- **value** is the header value in unicode (all encoded values are decoded to utf-8)
- **value** is the header value in unicode (all encoded values are decoded to utf-8). The value is capped at around 800 characters.
- **date** is the receive date (not header Date: field)
- **mailbox** is the id of the mailbox this messages belongs to
- **flags** is an array of IMAP flags for this message
- **text** is the plaintext version of the message (derived from html if not present in message source)
- **html** is the HTML version of the message (derived from plaintext if not present in message source)
- **html** is the HTML version of the message (derived from plaintext if not present in message source). It is an array of strings, each array element corresponds to different MIME node and might have its own html header
- **attachments** is an array of attachment objects. Attachments can be shared between messages.
@ -513,7 +509,7 @@ The response for successful operation should look like this:
"date": "2017-04-03T10:34:43.007Z",
"flags": ["\\Seen"],
"text": "Hello world!",
"html": "<p>Hello world!</p>",
"html": ["<p>Hello world!</p>"],
"attachments": [
{
"id": "58e2254289cccb742fd6c015",

View file

@ -29,13 +29,14 @@ module.exports = {
imap: {
port: 9993,
host: '127.0.0.1',
// If certificate path is not defined, use built-in self-signed certs
//key: '/path/to/server/key.pem'
//cert: '/path/to/server/cert.pem'
secure: true,
// Max size for messages uploaded via APPEND
maxMB: 5,
// default quota storage in MB (can be overriden per user)
maxStorage: 100
maxStorage: 1000
},
lmtp: {

View file

@ -1,98 +0,0 @@
# imap-core
Node.js module to create custom IMAP servers.
This is something I have used mostly for client work as the lower level dependency for some specific applications that serve IMAP. I don't have any such clients right no so I published the code if anyone finds it useful. I removed all proprietary code developed for clients, the module is only about the lower level protocol usage and does not contain any actual server logic.
You can see an example implementation of an IMAP server from the [example script](examples/index.js). Most of the code is inherited from the Hoodiecrow test-IMAP server module but this module can be used for asynchronous data access while in Hoodiecrow everything was synchronous (storage was an in-memory object that was accessed and updated synchronously).
## Demo
Install dependencies
npm install
Run the example
node examples/index.js
Connect to the server on port 9993
openssl s_client -crlf -connect localhost:9993
Once connected use testuser:pass to log in
< * OK test ready
> A LOGIN testuser pass
< A OK testuser authenticated
## IMAP extension support
This project is going to support only selected extensions, that are minimally required.
## Sequence number handling
Sequence numbers are handled automatically, no need to do this in the application side you only need to keep count of the incrementing UID's. All sequence number based operations are converted to use UID values instead.
## Handling large input
Unfortunately input handling for a single command is not stream based, so everything sent to the server is loaded into memory before being processed. Literal size can be limited though and in this case the server refuses to process literals bigger than configured size.
## SEARCH query
Search query is provided as a tree structure.
Possible SEARCH terms
- Array a list of AND terms
- **or** - in the form of `{key: 'or', value: [terms]}` where _terms_ is a list of OR terms
- **not** - inverts another term. In the form of `{key: 'not', value: term}` where _term_ is the term that must be inverted
- **flag** - describes a flag term. In the form of `{key: 'flag', value: 'term', exists: bool}` where _term_ is the flag name to look for and _bool_ indicates if the flag must be bresent (_true_) or missing (_false_)
- **header** - describes a header value. Header key is a case insensitive exact match (eg. 'X-Foo' matches header 'X-Foo:' but not 'X-Fooz:'). Header value is a partial match. In the form of `{key: 'header', header: 'keyterm', value: 'valueterm'}` where _keyterm_ is the header key name and _valueterm_ is the value of the header. If value is empty then the query acts as boolean, if header key is present, then it matches, otherwise it does not match
- **uid** - is a an array of UID values (numbers)
- **all** - if present then indicates that all messages should match
- **internaldate** - operates on the date the message was received. Date value is day based, so timezone and time should be discarded. In the form of `{key: 'internaldate', operator: 'op', value: 'val'}` where _op_ is one of '<', '=', '>=' and _val_ is a date string
- **date** - operates on the date listed in the massage _Date:_ header. Date value is day based, so timezone and time should be discarded. In the form of `{key: 'date', operator: 'op', value: 'val'}` where _op_ is one of '<', '=', '>=' and _val_ is a date string
- **body** - looks for a partial match in the message BODY (does not match header fields). In the form of `{key: 'body', value: 'term'}` where _term_ is the partial match to look for
- **text** - looks for a partial match in the entire message, including the body and headers. In the form of `{key: 'text', value: 'term'}` where _term_ is the partial match to look for
- **size** - matches message size. In the form of `{key: 'size', value: num, operator: 'op'}` where _op_ is one of '<', '=', '>' and _num_ is the size of the message
- **charset** - sets the charset to be used in the text fields. Can be ignored as everything should be UTF-8 by default
## Currently implemented RFC3501 commands
- **APPEND**
- **CAPABILITY**
- **CHECK**
- **CLOSE**
- **COPY**
- **CREATE**
- **DELETE**
- **EXPUNGE**
- **FETCH**
- **LIST**
- **LOGIN**
- **LOGOUT**
- **LSUB**
- **NOOP**
- **RENAME**
- **SEARCH**
- **SELECT**
- **STARTTLS**
- **STATUS**
- **STORE**
- **SUBSCRIBE**
- **UID COPY**
- **UID STORE**
- **UNSUBSCRIBE**
Extensions
- **Conditional STORE** rfc4551 and **ENABLE**
- **Special-Use Mailboxes** rfc6154
- **ID extension** rfc2971
- **IDLE command** rfc2177
- **NAMESPACE** rfc2342 (hard coded single user namespace)
- **UNSELECT** rfc3691
- **AUTHENTICATE PLAIN** and **SASL-IR**
Unlike the Hoodiecrow project you can not enable or disable extensions, everything is as it is.

View file

@ -52,12 +52,14 @@ module.exports = {
let readNext = () => {
let chunk;
while ((chunk = this.writeStream.read()) !== null) {
if (this._deflate.write(chunk) === false) {
if (this._deflate && this._deflate.write(chunk) === false) {
return this._deflate.once('drain', readNext);
}
}
// flush data to socket
this._deflate.flush();
if (this._deflate) {
this._deflate.flush();
}
};
this.writeStream.on('readable', readNext);

View file

@ -41,6 +41,9 @@ module.exports = {
}, this.session, (err, response) => {
if (err) {
if (err.response) {
return callback(null, err);
}
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this.id, username, 'LOGIN', err.message);
return callback(err);
}

View file

@ -102,6 +102,9 @@ module.exports = {
flags[i] = flags[i].toLowerCase().replace(/^\\./, c => c.toUpperCase());
}
}
if (flags[i].length > 255) {
return callback(new Error('Too long value for a flag'));
}
}
// keep only unique flags

View file

@ -20,7 +20,7 @@ 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('[%s] S: <pipe message stream to socket>', this.connection.id);
this.connection._server.logger.debug('[%s] S: %s<pipe message stream to socket>', this.connection.id, obj.description || '');
obj.pipe(this.connection[!this.connection.compression ? '_socket' : '_deflate'], {
end: false
});

View file

@ -252,7 +252,7 @@ class Indexer {
let response = {
attachments: [],
text: '',
html: ''
html: []
};
let htmlContent = [];
@ -445,7 +445,7 @@ class Indexer {
return match;
});
response.html = htmlContent.filter(str => str.trim()).map(updateCidLinks).join('\n').trim();
response.html = htmlContent.filter(str => str.trim()).map(updateCidLinks);
response.text = textContent.filter(str => str.trim()).map(updateCidLinks).join('\n').trim();
callback(null, response);

301
imap.js
View file

@ -1,6 +1,7 @@
'use strict';
const log = require('npmlog');
const util = require('util');
const config = require('config');
const IMAPServerModule = require('./imap-core');
const IMAPServer = IMAPServerModule.IMAPServer;
@ -11,11 +12,15 @@ const ObjectID = require('mongodb').ObjectID;
const Indexer = require('./imap-core/lib/indexer/indexer');
const imapTools = require('./imap-core/lib/imap-tools');
const fs = require('fs');
const rateLimiter = require('rolling-rate-limiter');
const setupIndexes = require('./indexes.json');
const MessageHandler = require('./lib/message-handler');
const db = require('./lib/db');
const packageData = require('./package.json');
// home many modifications to cache before writing
const BULK_BATCH_SIZE = 150;
// Setup server
const serverOptions = {
secure: config.imap.secure,
@ -52,25 +57,37 @@ let messageHandler;
server.onAuth = function (login, session, callback) {
let username = (login.username || '').toString().trim();
db.database.collection('users').findOne({
username
}, (err, user) => {
// rate limit authentication attempts per username/source IP
server.loginLimiter(username + ':' + session.remoteAddress, (err, timeLeft) => {
if (err) {
return callback(err);
}
if (!user) {
return callback();
if (timeLeft) {
let err = new Error('Too many logins, try again later');
err.response = 'NO';
return callback(err);
}
if (!bcrypt.compareSync(login.password, user.password)) {
return callback();
}
callback(null, {
user: {
id: user._id,
username
db.database.collection('users').findOne({
username
}, (err, user) => {
if (err) {
return callback(err);
}
if (!user) {
return callback();
}
if (!bcrypt.compareSync(login.password, user.password)) {
return callback();
}
callback(null, {
user: {
id: user._id,
username
}
});
});
});
@ -352,9 +369,7 @@ server.onStatus = function (path, session, callback) {
}
db.database.collection('messages').find({
mailbox: mailbox._id,
flags: {
$ne: '\\Seen'
}
seen: false
}).count((err, unseen) => {
if (err) {
return callback(err);
@ -440,7 +455,7 @@ server.updateMailboxFlags = function (mailbox, update, callback) {
}
// found some new flags not yet set for mailbox
// FIXME: Should we send unsolicited FLAGS and PERMANENTFLAGS notifications?
// FIXME: Should we send unsolicited FLAGS and PERMANENTFLAGS notifications? Probably not
return db.database.collection('mailboxes').findOneAndUpdate({
_id: mailbox._id
}, {
@ -466,12 +481,28 @@ server.onStore = function (path, update, session, callback) {
return callback(null, 'NONEXISTENT');
}
let cursor = db.database.collection('messages').find({
let query = {
mailbox: mailbox._id,
uid: {
$in: update.messages
}
}).project({
};
if (update.unchangedSince) {
query = {
mailbox: mailbox._id,
modseq: {
$lte: update.unchangedSince
},
uid: {
$in: update.messages
}
};
}
let cursor = db.database.collection('messages').
find(query).
project({
_id: true,
uid: true,
flags: true
@ -479,20 +510,26 @@ server.onStore = function (path, update, session, callback) {
['uid', 1]
]);
let updateEntries = [];
let notifyEntries = [];
let done = (...args) => {
if (notifyEntries.length) {
let entries = notifyEntries;
notifyEntries = [];
setImmediate(() => this.notifier.addEntries(session.user.id, path, entries, () => {
this.notifier.fire(session.user.id, path);
if (args[0]) { // first argument is an error
return callback(...args);
} else {
server.updateMailboxFlags(mailbox, update, () => callback(...args));
}
}));
return;
if (updateEntries.length) {
return db.database.collection('messages').bulkWrite(updateEntries, {
ordered: false,
w: 1
}, () => {
updateEntries = [];
this.notifier.addEntries(session.user.id, path, notifyEntries, () => {
notifyEntries = [];
this.notifier.fire(session.user.id, path);
if (args[0]) { // first argument is an error
return callback(...args);
} else {
server.updateMailboxFlags(mailbox, update, () => callback(...args));
}
});
});
}
this.notifier.fire(session.user.id, path);
if (args[0]) { // first argument is an error
@ -537,7 +574,6 @@ server.onStore = function (path, update, session, callback) {
flagsupdate = {
$set: {
flags: message.flags,
seen: message.flags.includes('\\Seen'),
flagged: message.flags.includes('\\Flagged'),
deleted: message.flags.includes('\\Deleted')
@ -645,31 +681,42 @@ server.onStore = function (path, update, session, callback) {
}
if (updated) {
db.database.collection('messages').findOneAndUpdate({
_id: message._id
}, flagsupdate, {}, err => {
if (err) {
return cursor.close(() => done(err));
}
notifyEntries.push({
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags,
message: message._id
});
if (notifyEntries.length > 100) {
// emit notifications in batches of 100
let entries = notifyEntries;
notifyEntries = [];
setImmediate(() => this.notifier.addEntries(session.user.id, path, entries, processNext));
return;
} else {
setImmediate(() => processNext());
updateEntries.push({
updateOne: {
filter: {
_id: message._id
},
update: flagsupdate
}
});
notifyEntries.push({
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags,
message: message._id
});
if (updateEntries.length >= BULK_BATCH_SIZE) {
return db.database.collection('messages').bulkWrite(updateEntries, {
ordered: false,
w: 1
}, err => {
updateEntries = [];
if (err) {
return cursor.close(() => done(err));
}
this.notifier.addEntries(session.user.id, path, notifyEntries, () => {
notifyEntries = [];
this.notifier.fire(session.user.id, path);
processNext();
});
});
} else {
processNext();
}
} else {
processNext();
}
@ -696,7 +743,7 @@ server.onExpunge = function (path, update, session, callback) {
let cursor = db.database.collection('messages').find({
mailbox: mailbox._id,
flags: '\\Deleted'
deleted: true
}).project({
_id: true,
uid: true,
@ -863,6 +910,8 @@ server.onCopy = function (path, update, session, callback) {
let sourceId = message._id;
// Copying is not done in bulk to minimize risk of going out of sync with incremental UIDs
sourceUid.unshift(message.uid);
db.database.collection('mailboxes').findOneAndUpdate({
_id: target._id
@ -926,9 +975,7 @@ server.onCopy = function (path, update, session, callback) {
});
});
};
processNext();
});
});
};
@ -1099,24 +1146,57 @@ server.onFetch = function (path, options, session, callback) {
};
if (options.changedSince) {
query.modseq = {
$gt: options.changedSince
query = {
mailbox: mailbox._id,
modseq: {
$gt: options.changedSince
},
uid: {
$in: options.messages
}
};
}
let cursor = db.database.collection('messages').find(query).project(projection).sort([
let isUpdated = false;
let updateEntries = [];
let notifyEntries = [];
let done = (...args) => {
if (updateEntries.length) {
return db.database.collection('messages').bulkWrite(updateEntries, {
ordered: false,
w: 1
}, () => {
updateEntries = [];
this.notifier.addEntries(session.user.id, path, notifyEntries, () => {
notifyEntries = [];
this.notifier.fire(session.user.id, path);
return callback(...args);
});
});
}
if (isUpdated) {
this.notifier.fire(session.user.id, path);
}
return callback(...args);
};
let cursor = db.database.collection('messages').
find(query).
project(projection).
sort([
['uid', 1]
]);
let rowCount = 0;
let processNext = () => {
cursor.next((err, message) => {
if (err) {
return callback(err);
return done(err);
}
if (!message) {
return cursor.close(() => {
this.notifier.fire(session.user.id, path);
return callback(null, true);
done(null, true);
});
}
@ -1135,43 +1215,67 @@ server.onFetch = function (path, options, session, callback) {
})
}));
stream.description = util.format('* FETCH #%s uid=%s size=%sB ', ++rowCount, message.uid, message.size);
stream.on('error', err => {
session.socket.write('INTERNAL ERROR\n');
session.socket.destroy(); // ended up in erroneus state, kill the connection to abort
return cursor.close(() => callback(err));
return cursor.close(() => done(err));
});
// send formatted response to socket
session.writeStream.write(stream, () => {
if (!options.markAsSeen || message.flags.includes('\\Seen')) {
return processNext();
}
if (!markAsSeen) {
return processNext();
}
this.logger.debug('[%s] UPDATE FLAGS for "%s"', session.id, message.uid);
db.database.collection('messages').findOneAndUpdate({
_id: message._id
}, {
$addToSet: {
flags: '\\Seen'
isUpdated = true;
updateEntries.push({
updateOne: {
filter: {
_id: message._id
},
update: {
$addToSet: {
flags: '\\Seen'
},
$set: {
seen: true
}
}
}
}, {}, err => {
if (err) {
return cursor.close(() => callback(err));
}
this.notifier.addEntries(session.user.id, path, {
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags,
message: message._id
}, processNext);
});
notifyEntries.push({
command: 'FETCH',
ignore: session.id,
uid: message.uid,
flags: message.flags,
message: message._id
});
if (updateEntries.length >= BULK_BATCH_SIZE) {
return db.database.collection('messages').bulkWrite(updateEntries, {
ordered: false,
w: 1
}, err => {
updateEntries = [];
if (err) {
return cursor.close(() => done(err));
}
this.notifier.addEntries(session.user.id, path, notifyEntries, () => {
notifyEntries = [];
this.notifier.fire(session.user.id, path);
processNext();
});
});
} else {
processNext();
}
});
});
};
@ -1469,7 +1573,8 @@ server.onSearch = function (path, options, session, callback) {
});
}
let cursor = db.database.collection('messages').find(query).
let cursor = db.database.collection('messages').
find(query).
project({
uid: true,
modseq: true
@ -1491,26 +1596,12 @@ server.onSearch = function (path, options, session, callback) {
}));
}
//if (message.raw) {
// message.raw = message.raw.toString();
//}
//session.matchSearchQuery(message, options.query, (err, match) => {
// if (err) {
// return cursor.close(() => callback(err));
// }
//if (match && highestModseq < message.modseq) {
if (highestModseq < message.modseq) {
highestModseq = message.modseq;
}
//if (match) {
uidList.push(message.uid);
//}
processNext();
//});
});
};
@ -1590,6 +1681,14 @@ module.exports = done => {
database: db.database
});
server.loginLimiter = rateLimiter({
redis: db.redis,
namespace: 'UserLoginLimiter',
// allow 100 login attempts per minute
interval: 60 * 1000,
maxInInterval: 100
});
let started = false;
server.on('error', err => {

View file

@ -60,11 +60,17 @@
"user": 1
}
}, {
"name": "mailbox_uid_modseq",
"name": "mailbox_ui",
"key": {
"mailbox": 1,
"uid": 1,
"modseq": 1
"uid": 1
}
}, {
"name": "mailbox_modseq_uid",
"key": {
"mailbox": 1,
"modseq": 1,
"uid": 1
}
}, {
"name": "newer_first",
@ -109,12 +115,6 @@
"headers.key": 1,
"headers.value": 1
}
}, {
"name": "has_attachments",
"key": {
"mailbox": 1,
"hasAttachments": 1
}
}, {
"name": "fulltext",
"key": {
@ -139,6 +139,12 @@
"mailbox": 1,
"flagged": 1
}
}, {
"name": "has_attachments",
"key": {
"mailbox": 1,
"hasAttachments": 1
}
}]
}, {
"collection": "attachment.files",

View file

@ -1,10 +1,13 @@
'use strict';
const config = require('config');
const tools = require('./tools');
const mongodb = require('mongodb');
const redis = require('redis');
const MongoClient = mongodb.MongoClient;
module.exports.database = false;
module.exports.redis = false;
module.exports.connect = callback => {
MongoClient.connect(config.mongo, (err, database) => {
@ -12,6 +15,7 @@ module.exports.connect = callback => {
return callback(err);
}
module.exports.database = database;
module.exports.redis = redis.createClient(tools.redisConfig(config.redis));
callback(null, database);
});
};

View file

@ -8,8 +8,6 @@ const Indexer = require('../imap-core/lib/indexer/indexer');
const ImapNotifier = require('./imap-notifier');
const tools = require('./tools');
const libmime = require('libmime');
const createDOMPurify = require('dompurify');
const jsdom = require('jsdom');
class MessageHandler {
@ -54,20 +52,6 @@ class MessageHandler {
});
}
cleanHtml(html) {
let win = jsdom.jsdom('', {
features: {
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
ProcessExternalResources: false // do not execute JS within script blocks
}
}).defaultView;
let domPurify = createDOMPurify(win);
return domPurify.sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: true // allow cid: images
});
}
add(options, callback) {
let id = new ObjectID();
@ -91,6 +75,21 @@ class MessageHandler {
} catch (E) {
// ignore
}
// trim long values as mongodb indexed fields can not be too long
if (Buffer.byteLength(key, 'utf-8') >= 255) {
key = Buffer.from(key).slice(0, 255).toString();
key = key.substr(0, key.length - 4);
}
if (Buffer.byteLength(value, 'utf-8') >= 880) {
// value exceeds MongoDB max indexed value length
value = Buffer.from(value).slice(0, 880).toString();
// remove last 4 chars to be sure we do not have any incomplete unicode sequences
value = value.substr(0, value.length - 4);
}
return {
key,
value
@ -107,7 +106,75 @@ class MessageHandler {
return callback(err);
}
// Another server might be waiting for the lock like this.
let internaldate = options.date && new Date(options.date) || new Date();
let headerdate = mimeTree.parsedHeader.date && new Date(mimeTree.parsedHeader.date) || false;
let flags = [].concat(options.flags || []);
if (!headerdate || headerdate.toString() === 'Invalid Date') {
headerdate = internaldate;
}
// prepare message object
let message = {
_id: id,
internaldate,
headerdate,
flags,
size,
meta: options.meta || {},
headers,
mimeTree,
envelope,
bodystructure,
messageId,
// use boolean for more common flags
seen: flags.includes('\\Seen'),
flagged: flags.includes('\\Flagged'),
deleted: flags.includes('\\Deleted')
};
if (maildata.attachments && maildata.attachments.length) {
message.attachments = maildata.attachments;
message.hasAttachments = true;
} else {
message.hasAttachments = false;
}
let maxTextLength = 300 * 1024;
if (maildata.text) {
message.text = maildata.text.replace(/\r\n/g, '\n').trim();
message.text = message.text.length <= maxTextLength ? message.text : message.text.substr(0, maxTextLength);
message.intro = message.text.replace(/\s+/g, ' ').trim();
if (message.intro.length > 128) {
message.intro = message.intro.substr(0, 128) + '…';
}
}
if (maildata.html && maildata.html.length) {
let htmlSize = 0;
message.html = maildata.html.map(html => {
if (htmlSize >= maxTextLength || !html) {
return '';
}
if (htmlSize + Buffer.byteLength(html) <= maxTextLength) {
htmlSize += Buffer.byteLength(html);
return html;
}
html = html.substr(0, htmlSize + Buffer.byteLength(html) - maxTextLength);
htmlSize += Buffer.byteLength(html);
return html;
}).filter(html => html);
}
// Another server might be waiting for the lock
this.redlock.waitAcquireLock(mailbox._id.toString(), 30 * 1000, 10 * 1000, (err, lock) => {
if (err) {
return callback(err);
@ -166,64 +233,11 @@ class MessageHandler {
let mailbox = item.value;
let internaldate = options.date && new Date(options.date) || new Date();
let headerdate = mimeTree.parsedHeader.date && new Date(mimeTree.parsedHeader.date) || false;
let flags = [].concat(options.flags || []);
if (!headerdate || headerdate.toString() === 'Invalid Date') {
headerdate = internaldate;
}
let message = {
_id: id,
mailbox: mailbox._id,
user: mailbox.user,
uid: mailbox.uidNext,
modseq: mailbox.modifyIndex + 1,
internaldate,
headerdate,
flags,
size,
meta: options.meta || {},
headers,
mimeTree,
envelope,
bodystructure,
messageId,
// use boolean for more common flags
seen: flags.includes('\\Seen'),
flagged: flags.includes('\\Flagged'),
deleted: flags.includes('\\Deleted')
};
if (maildata.attachments && maildata.attachments.length) {
message.attachments = maildata.attachments;
message.hasAttachments = true;
} else {
message.hasAttachments = false;
}
// use mailparser to parse plaintext and html content
let maxTextLength = 200 * 1024;
if (maildata.text) {
message.text = maildata.text.replace(/\r\n/g, '\n').trim();
message.text = message.text.length <= maxTextLength ? message.text : message.text.substr(0, maxTextLength);
message.intro = message.text.replace(/\s+/g, ' ').trim();
if (message.intro.length > 256) {
message.intro = message.intro.substr(0, 256) + '…';
}
}
if (maildata.html) {
message.html = this.cleanHtml(maildata.html.replace(/\r\n/g, '\n')).trim();
message.html = message.html.length <= maxTextLength ? message.html : message.html.substr(0, maxTextLength);
}
// updated message object by setting mailbox specific values
message.mailbox = mailbox._id;
message.user = mailbox.user;
message.uid = mailbox.uidNext;
message.modseq = mailbox.modifyIndex + 1;
this.database.collection('messages').insertOne(message, err => {
if (err) {

View file

@ -1,6 +1,6 @@
{
"name": "wildduck",
"version": "1.0.10",
"version": "1.0.11",
"description": "IMAP server built with Node.js and MongoDB",
"main": "server.js",
"scripts": {
@ -22,12 +22,10 @@
"bcryptjs": "^2.4.3",
"clone": "^2.1.1",
"config": "^1.25.1",
"dompurify": "^0.8.5",
"grid-fs": "^1.0.1",
"html-to-text": "^3.2.0",
"iconv-lite": "^0.4.15",
"joi": "^10.3.4",
"jsdom": "^9.12.0",
"joi": "^10.4.1",
"libbase64": "^0.1.0",
"libmime": "^3.1.0",
"libqp": "^1.1.0",
@ -38,6 +36,7 @@
"redfour": "^1.0.0",
"redis": "^2.7.1",
"restify": "^4.3.0",
"rolling-rate-limiter": "^0.1.4",
"smtp-server": "^2.0.3",
"toml": "^2.3.2",
"utf7": "^1.0.2",