mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-28 10:47:45 +08:00
Added support for PGP encrypting cleartext messages
This commit is contained in:
parent
2b7d5a5fe9
commit
0c96919679
5 changed files with 437 additions and 236 deletions
|
@ -5,6 +5,7 @@ const Joi = require('joi');
|
|||
const MongoPaging = require('mongo-cursor-pagination');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const tools = require('../tools');
|
||||
const openpgp = require('openpgp');
|
||||
|
||||
module.exports = (db, server, userHandler) => {
|
||||
server.get({ name: 'users', path: '/users' }, (req, res, next) => {
|
||||
|
@ -138,6 +139,9 @@ module.exports = (db, server, userHandler) => {
|
|||
recipients: Joi.number().min(0).default(0),
|
||||
forwards: Joi.number().min(0).default(0),
|
||||
|
||||
pubKey: Joi.string().empty('').trim().regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
|
||||
encryptMessages: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).default(false),
|
||||
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
|
@ -167,21 +171,33 @@ module.exports = (db, server, userHandler) => {
|
|||
result.value.forward = forward;
|
||||
}
|
||||
|
||||
userHandler.create(result.value, (err, id) => {
|
||||
if ('pubKey' in req.params && !result.value.pubKey) {
|
||||
result.value.pubKey = '';
|
||||
}
|
||||
|
||||
checkPubKey(result.value.pubKey, err => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message,
|
||||
username: result.value.username
|
||||
error: 'PGP key validation failed. ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
userHandler.create(result.value, (err, id) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message,
|
||||
username: result.value.username
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: !!id,
|
||||
id
|
||||
res.json({
|
||||
success: !!id,
|
||||
id
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -256,6 +272,9 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
enabled2fa: userData.enabled2fa,
|
||||
|
||||
encryptMessages: userData.encryptMessages,
|
||||
pubKey: userData.pubKey,
|
||||
|
||||
forward: userData.forward,
|
||||
targetUrl: userData.targetUrl,
|
||||
|
||||
|
@ -300,6 +319,9 @@ module.exports = (db, server, userHandler) => {
|
|||
forward: Joi.string().empty('').email(),
|
||||
targetUrl: Joi.string().empty('').max(256),
|
||||
|
||||
pubKey: Joi.string().empty('').trim().regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
|
||||
encryptMessages: Joi.boolean().empty('').truthy(['Y', 'true', 'yes', 1]),
|
||||
|
||||
retention: Joi.number().min(0),
|
||||
quota: Joi.number().min(0),
|
||||
recipients: Joi.number().min(0),
|
||||
|
@ -345,17 +367,30 @@ module.exports = (db, server, userHandler) => {
|
|||
result.value.name = '';
|
||||
}
|
||||
|
||||
userHandler.update(user, result.value, (err, success) => {
|
||||
if (!result.value.pubKey && 'pubKey' in req.params) {
|
||||
result.value.pubKey = '';
|
||||
}
|
||||
|
||||
checkPubKey(result.value.pubKey, err => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
error: 'PGP key validation failed. ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
res.json({
|
||||
success
|
||||
|
||||
userHandler.update(user, result.value, (err, success) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
res.json({
|
||||
success
|
||||
});
|
||||
return next();
|
||||
});
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -472,3 +507,32 @@ module.exports = (db, server, userHandler) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
function checkPubKey(pubKey, done) {
|
||||
if (!pubKey) {
|
||||
return done();
|
||||
}
|
||||
|
||||
// try to encrypt something with that key
|
||||
let armored;
|
||||
try {
|
||||
armored = openpgp.key.readArmored(pubKey).keys;
|
||||
} catch (E) {
|
||||
return done(E);
|
||||
}
|
||||
|
||||
openpgp
|
||||
.encrypt({
|
||||
data: 'Hello, World!',
|
||||
publicKeys: armored
|
||||
})
|
||||
.then(ciphertext => {
|
||||
if (/^-----BEGIN PGP MESSAGE/.test(ciphertext.data)) {
|
||||
// everything checks out
|
||||
return done();
|
||||
}
|
||||
|
||||
return done(new Error('Unexpected message'));
|
||||
})
|
||||
.catch(err => done(err));
|
||||
}
|
||||
|
|
|
@ -9,11 +9,14 @@ const libmime = require('libmime');
|
|||
const counters = require('./counters');
|
||||
const consts = require('./consts');
|
||||
const tools = require('./tools');
|
||||
const openpgp = require('openpgp');
|
||||
const parseDate = require('../imap-core/lib/parse-date');
|
||||
|
||||
// index only the following headers for SEARCH
|
||||
const INDEXED_HEADERS = ['to', 'cc', 'subject', 'from', 'sender', 'reply-to', 'message-id', 'thread-index', 'x-rspamd-spam', 'x-spam-status'];
|
||||
|
||||
openpgp.config.commentstring = 'Plaintext message encrypted by Wild Duck Mail Server';
|
||||
|
||||
class MessageHandler {
|
||||
constructor(options) {
|
||||
this.database = options.database;
|
||||
|
@ -1159,6 +1162,126 @@ class MessageHandler {
|
|||
processNext();
|
||||
});
|
||||
}
|
||||
|
||||
encryptMessage(pubKey, raw, callback) {
|
||||
if (!pubKey) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let lastBytes = [];
|
||||
let headerEnd = raw.length;
|
||||
let headerLength = 0;
|
||||
|
||||
// split the message into header and body
|
||||
for (let i = 0, len = raw.length; i < len; i++) {
|
||||
lastBytes.unshift(raw[i]);
|
||||
if (lastBytes.length > 10) {
|
||||
lastBytes.length = 4;
|
||||
}
|
||||
if (lastBytes.length < 2) {
|
||||
continue;
|
||||
}
|
||||
let pos = 0;
|
||||
if (lastBytes[pos] !== 0x0a) {
|
||||
continue;
|
||||
}
|
||||
pos++;
|
||||
if (lastBytes[pos] === 0x0d) {
|
||||
pos++;
|
||||
}
|
||||
if (lastBytes[pos] !== 0x0a) {
|
||||
continue;
|
||||
}
|
||||
pos++;
|
||||
if (lastBytes[pos] === 0x0d) {
|
||||
pos++;
|
||||
}
|
||||
// we have a match!'
|
||||
headerEnd = i + 1 - pos;
|
||||
headerLength = pos;
|
||||
break;
|
||||
}
|
||||
|
||||
let header = raw.slice(0, headerEnd);
|
||||
let breaker = headerLength ? raw.slice(headerEnd, headerEnd + headerLength) : new Buffer(0);
|
||||
let body = headerEnd + headerLength < raw.length ? raw.slice(headerEnd + headerLength) : new Buffer(0);
|
||||
|
||||
// modify headers
|
||||
let headers = [];
|
||||
let bodyHeaders = [];
|
||||
let lastHeader = false;
|
||||
let boundary = 'nm_' + crypto.randomBytes(14).toString('hex');
|
||||
|
||||
let headerLines = header.toString('binary').split('\r\n');
|
||||
// use for, so we could escape from it if needed
|
||||
for (let i = 0, len = headerLines.length; i < len; i++) {
|
||||
let line = headerLines[i];
|
||||
if (!i || !lastHeader || !/^\s/.test(line)) {
|
||||
lastHeader = [line];
|
||||
if (/^content-type:/i.test(line)) {
|
||||
let parts = line.split(':');
|
||||
let value = parts.slice(1).join(':');
|
||||
if (value.split(';').shift().trim().toLowerCase() === 'multipart/encrypted') {
|
||||
// message is already encrypted, do nothing
|
||||
return callback(null, false);
|
||||
}
|
||||
bodyHeaders.push(lastHeader);
|
||||
} else if (/^content-transfer-encoding:/i.test(line)) {
|
||||
bodyHeaders.push(lastHeader);
|
||||
} else {
|
||||
headers.push(lastHeader);
|
||||
}
|
||||
} else {
|
||||
lastHeader.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
headers.push(['Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";'], [' boundary="' + boundary + '"']);
|
||||
|
||||
headers.push(['Content-Description: OpenPGP encrypted message']);
|
||||
headers.push(['Content-Transfer-Encoding: 7bit']);
|
||||
|
||||
headers = Buffer.from(headers.map(line => line.join('\r\n')).join('\r\n'), 'binary');
|
||||
bodyHeaders = Buffer.from(bodyHeaders.map(line => line.join('\r\n')).join('\r\n'), 'binary');
|
||||
|
||||
openpgp
|
||||
.encrypt({
|
||||
data: Buffer.concat([Buffer.from(bodyHeaders + '\r\n\r\n'), body]),
|
||||
publicKeys: openpgp.key.readArmored(pubKey).keys
|
||||
})
|
||||
.then(ciphertext => {
|
||||
let text =
|
||||
'This is an OpenPGP/MIME encrypted message\r\n\r\n' +
|
||||
'--' +
|
||||
boundary +
|
||||
'\r\n' +
|
||||
'Content-Type: application/pgp-encrypted\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
'Version: 1\r\n' +
|
||||
'\r\n' +
|
||||
'--' +
|
||||
boundary +
|
||||
'\r\n' +
|
||||
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
|
||||
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
ciphertext.data +
|
||||
'\r\n--' +
|
||||
boundary +
|
||||
'--\r\n';
|
||||
|
||||
callback(null, Buffer.concat([headers, breaker, Buffer.from(text)]));
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
// encryption failed, keep message as is
|
||||
callback(null, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageHandler;
|
||||
|
|
|
@ -373,8 +373,12 @@ class UserHandler {
|
|||
forwards: data.forwards || 0,
|
||||
|
||||
// autoreply status
|
||||
// off by default, can be changed later by user through the API
|
||||
autoreply: false,
|
||||
|
||||
pubKey: data.pubKey || '',
|
||||
encryptMessages: !!data.encryptMessages,
|
||||
|
||||
// default retention for user mailboxes
|
||||
retention: data.retention || 0,
|
||||
|
||||
|
|
449
lmtp.js
449
lmtp.js
|
@ -73,7 +73,9 @@ const serverOptions = {
|
|||
forwards: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
autoreply: true
|
||||
autoreply: true,
|
||||
encryptMessages: true,
|
||||
pubKey: true
|
||||
}
|
||||
}, (err, user) => {
|
||||
if (err) {
|
||||
|
@ -148,252 +150,259 @@ const serverOptions = {
|
|||
chunklen += header.length;
|
||||
|
||||
let raw = Buffer.concat(chunks, chunklen);
|
||||
let prepared = messageHandler.prepareMessage({
|
||||
raw
|
||||
});
|
||||
let maildata = messageHandler.indexer.getMaildata(prepared.id, prepared.mimeTree);
|
||||
|
||||
// default flags are empty
|
||||
let flags = [];
|
||||
|
||||
// default mailbox target is INBOX
|
||||
let mailboxQueryKey = 'path';
|
||||
let mailboxQueryValue = 'INBOX';
|
||||
|
||||
db.database.collection('filters').find({ user: user._id }).sort({ _id: 1 }).toArray((err, filters) => {
|
||||
if (err) {
|
||||
// ignore, as filtering is not so important
|
||||
messageHandler.encryptMessage(user.encryptMessages ? user.pubKey : false, raw, (err, encrypted) => {
|
||||
if (!err && encrypted) {
|
||||
raw = encrypted;
|
||||
}
|
||||
// append generic spam header check to the filters
|
||||
filters = (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
|
||||
}
|
||||
}
|
||||
: []
|
||||
);
|
||||
|
||||
let forwardTargets = new Set();
|
||||
let forwardTargetUrls = new Set();
|
||||
let matchingFilters = [];
|
||||
let filterActions = new Map();
|
||||
let prepared = messageHandler.prepareMessage({
|
||||
raw
|
||||
});
|
||||
let maildata = messageHandler.indexer.getMaildata(prepared.id, prepared.mimeTree);
|
||||
|
||||
filters
|
||||
// apply all filters to the message
|
||||
.map(filter => checkFilter(filter, prepared, maildata))
|
||||
// remove all unmatched filters
|
||||
.filter(filter => filter)
|
||||
// apply filter actions
|
||||
.forEach(filter => {
|
||||
matchingFilters.push(filter.id);
|
||||
// default flags are empty
|
||||
let flags = [];
|
||||
|
||||
// apply matching filter
|
||||
if (!filterActions) {
|
||||
filterActions = filter.action;
|
||||
} else {
|
||||
Object.keys(filter.action).forEach(key => {
|
||||
if (key === 'forward') {
|
||||
forwardTargets.add(filter.action[key]);
|
||||
return;
|
||||
}
|
||||
// default mailbox target is INBOX
|
||||
let mailboxQueryKey = 'path';
|
||||
let mailboxQueryValue = 'INBOX';
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let forwardMessage = done => {
|
||||
if (user.forward && !filterActions.get('delete')) {
|
||||
// forward to default recipient only if the message is not deleted
|
||||
forwardTargets.add(user.forward);
|
||||
}
|
||||
|
||||
if (user.targetUrl && !filterActions.get('delete')) {
|
||||
// forward to default URL only if the message is not deleted
|
||||
forwardTargetUrls.add(user.targetUrl);
|
||||
}
|
||||
|
||||
// never forward messages marked as spam
|
||||
if ((!forwardTargets.size && !forwardTargetUrls.size) || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
forward(
|
||||
{
|
||||
user,
|
||||
sender,
|
||||
recipient,
|
||||
|
||||
forward: forwardTargets.size ? Array.from(forwardTargets) : false,
|
||||
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
|
||||
|
||||
chunks
|
||||
},
|
||||
done
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let sendAutoreply = done => {
|
||||
// never reply to messages marked as spam
|
||||
if (!sender || !user.autoreply || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
autoreply(
|
||||
{
|
||||
user,
|
||||
sender,
|
||||
recipient,
|
||||
chunks,
|
||||
messageHandler
|
||||
},
|
||||
done
|
||||
);
|
||||
};
|
||||
|
||||
forwardMessage((err, id) => {
|
||||
db.database.collection('filters').find({ user: user._id }).sort({ _id: 1 }).toArray((err, filters) => {
|
||||
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
|
||||
);
|
||||
} 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(',')
|
||||
);
|
||||
// ignore, as filtering is not so important
|
||||
}
|
||||
// append generic spam header check to the filters
|
||||
filters = (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
|
||||
}
|
||||
}
|
||||
: []
|
||||
);
|
||||
|
||||
sendAutoreply((err, id) => {
|
||||
if (err) {
|
||||
log.error('LMTP', '%s AUTOREPLYFAIL from=%s to=%s error=%s', prepared.id.toString(), '<>', sender, err.message);
|
||||
} else if (id) {
|
||||
log.silly('LMTP', '%s AUTOREPLYOK id=%s from=%s to=%s', prepared.id.toString(), id, '<>', sender);
|
||||
}
|
||||
let forwardTargets = new Set();
|
||||
let forwardTargetUrls = new Set();
|
||||
let matchingFilters = [];
|
||||
let filterActions = new Map();
|
||||
|
||||
if (filterActions.get('delete')) {
|
||||
// nothing to do with the message, just continue
|
||||
responses.push({
|
||||
user,
|
||||
response: 'Message dropped by policy as ' + prepared.id.toString()
|
||||
});
|
||||
prepared = false;
|
||||
maildata = false;
|
||||
return storeNext();
|
||||
}
|
||||
filters
|
||||
// apply all filters to the message
|
||||
.map(filter => checkFilter(filter, prepared, maildata))
|
||||
// remove all unmatched filters
|
||||
.filter(filter => filter)
|
||||
// apply filter actions
|
||||
.forEach(filter => {
|
||||
matchingFilters.push(filter.id);
|
||||
|
||||
// apply filter results to the message
|
||||
filterActions.forEach((value, key) => {
|
||||
switch (key) {
|
||||
case 'spam':
|
||||
if (value > 0) {
|
||||
// positive value is spam
|
||||
mailboxQueryKey = 'specialUse';
|
||||
mailboxQueryValue = '\\Junk';
|
||||
// apply matching filter
|
||||
if (!filterActions) {
|
||||
filterActions = filter.action;
|
||||
} else {
|
||||
Object.keys(filter.action).forEach(key => {
|
||||
if (key === 'forward') {
|
||||
forwardTargets.add(filter.action[key]);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'seen':
|
||||
if (value) {
|
||||
flags.push('\\Seen');
|
||||
|
||||
if (key === 'targetUrl') {
|
||||
forwardTargetUrls.add(filter.action[key]);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'flag':
|
||||
if (value) {
|
||||
flags.push('\\Flagged');
|
||||
|
||||
// if a previous filter already has set a value then do not touch it
|
||||
if (!filterActions.has(key)) {
|
||||
filterActions.set(key, filter.action[key]);
|
||||
}
|
||||
break;
|
||||
case 'mailbox':
|
||||
if (value) {
|
||||
// positive value is spam
|
||||
mailboxQueryKey = 'mailbox';
|
||||
mailboxQueryValue = value;
|
||||
}
|
||||
break;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let messageOptions = {
|
||||
user: (user && user._id) || user,
|
||||
[mailboxQueryKey]: mailboxQueryValue,
|
||||
let forwardMessage = done => {
|
||||
if (user.forward && !filterActions.get('delete')) {
|
||||
// forward to default recipient only if the message is not deleted
|
||||
forwardTargets.add(user.forward);
|
||||
}
|
||||
|
||||
prepared,
|
||||
maildata,
|
||||
if (user.targetUrl && !filterActions.get('delete')) {
|
||||
// forward to default URL only if the message is not deleted
|
||||
forwardTargetUrls.add(user.targetUrl);
|
||||
}
|
||||
|
||||
meta: {
|
||||
source: 'LMTP',
|
||||
from: sender,
|
||||
to: recipient,
|
||||
origin: session.remoteAddress,
|
||||
originhost: session.clientHostname,
|
||||
transhost: session.hostNameAppearsAs,
|
||||
transtype: session.transmissionType,
|
||||
time: Date.now()
|
||||
},
|
||||
// never forward messages marked as spam
|
||||
if ((!forwardTargets.size && !forwardTargetUrls.size) || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
filters: matchingFilters,
|
||||
// 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();
|
||||
}
|
||||
|
||||
date: false,
|
||||
flags,
|
||||
forward(
|
||||
{
|
||||
user,
|
||||
sender,
|
||||
recipient,
|
||||
|
||||
// if similar message exists, then skip
|
||||
skipExisting: true
|
||||
};
|
||||
forward: forwardTargets.size ? Array.from(forwardTargets) : false,
|
||||
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
|
||||
|
||||
messageHandler.add(messageOptions, (err, inserted, info) => {
|
||||
// remove Delivered-To
|
||||
chunks.shift();
|
||||
chunklen -= header.length;
|
||||
chunks
|
||||
},
|
||||
done
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// push to response list
|
||||
responses.push({
|
||||
let sendAutoreply = done => {
|
||||
// never reply to messages marked as spam
|
||||
if (!sender || !user.autoreply || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
autoreply(
|
||||
{
|
||||
user,
|
||||
response: err ? err : 'Message stored as ' + info.id.toString()
|
||||
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
|
||||
);
|
||||
} 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(',')
|
||||
);
|
||||
}
|
||||
|
||||
sendAutoreply((err, id) => {
|
||||
if (err) {
|
||||
log.error('LMTP', '%s AUTOREPLYFAIL from=%s to=%s error=%s', prepared.id.toString(), '<>', sender, err.message);
|
||||
} else if (id) {
|
||||
log.silly('LMTP', '%s AUTOREPLYOK id=%s from=%s to=%s', prepared.id.toString(), id, '<>', sender);
|
||||
}
|
||||
|
||||
if (filterActions.get('delete')) {
|
||||
// nothing to do with the message, just continue
|
||||
responses.push({
|
||||
user,
|
||||
response: 'Message dropped by policy as ' + prepared.id.toString()
|
||||
});
|
||||
prepared = false;
|
||||
maildata = false;
|
||||
return storeNext();
|
||||
}
|
||||
|
||||
// apply filter results to the message
|
||||
filterActions.forEach((value, key) => {
|
||||
switch (key) {
|
||||
case 'spam':
|
||||
if (value > 0) {
|
||||
// positive value is spam
|
||||
mailboxQueryKey = 'specialUse';
|
||||
mailboxQueryValue = '\\Junk';
|
||||
}
|
||||
break;
|
||||
case 'seen':
|
||||
if (value) {
|
||||
flags.push('\\Seen');
|
||||
}
|
||||
break;
|
||||
case 'flag':
|
||||
if (value) {
|
||||
flags.push('\\Flagged');
|
||||
}
|
||||
break;
|
||||
case 'mailbox':
|
||||
if (value) {
|
||||
// positive value is spam
|
||||
mailboxQueryKey = 'mailbox';
|
||||
mailboxQueryValue = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
storeNext();
|
||||
let messageOptions = {
|
||||
user: (user && user._id) || user,
|
||||
[mailboxQueryKey]: mailboxQueryValue,
|
||||
|
||||
prepared,
|
||||
maildata,
|
||||
|
||||
meta: {
|
||||
source: 'LMTP',
|
||||
from: sender,
|
||||
to: recipient,
|
||||
origin: session.remoteAddress,
|
||||
originhost: session.clientHostname,
|
||||
transhost: session.hostNameAppearsAs,
|
||||
transtype: session.transmissionType,
|
||||
time: Date.now()
|
||||
},
|
||||
|
||||
filters: matchingFilters,
|
||||
|
||||
date: false,
|
||||
flags,
|
||||
|
||||
// if similar message exists, then skip
|
||||
skipExisting: true
|
||||
};
|
||||
|
||||
messageHandler.add(messageOptions, (err, inserted, info) => {
|
||||
// remove Delivered-To
|
||||
chunks.shift();
|
||||
chunklen -= header.length;
|
||||
|
||||
// push to response list
|
||||
responses.push({
|
||||
user,
|
||||
response: err ? err : 'Message stored as ' + info.id.toString()
|
||||
});
|
||||
|
||||
storeNext();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
"grunt-mocha-test": "^0.13.2",
|
||||
"grunt-shell-spawn": "^0.3.10",
|
||||
"grunt-wait": "^0.1.0",
|
||||
"icedfrisby": "^1.2.0",
|
||||
"mocha": "^3.4.2"
|
||||
"icedfrisby": "^1.3.0",
|
||||
"mocha": "^3.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"addressparser": "^1.0.1",
|
||||
|
@ -30,7 +30,7 @@
|
|||
"html-to-text": "^3.3.0",
|
||||
"iconv-lite": "^0.4.18",
|
||||
"joi": "^10.6.0",
|
||||
"js-yaml": "^3.9.0",
|
||||
"js-yaml": "^3.9.1",
|
||||
"libbase64": "^0.2.0",
|
||||
"libmime": "^3.1.0",
|
||||
"libqp": "^1.1.0",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"node-redis-scripty": "0.0.5",
|
||||
"nodemailer": "^4.0.1",
|
||||
"npmlog": "^4.1.2",
|
||||
"openpgp": "^2.5.8",
|
||||
"qrcode": "^0.9.0",
|
||||
"redfour": "^1.0.2",
|
||||
"redis": "^2.7.1",
|
||||
|
|
Loading…
Reference in a new issue