mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-05 04:34:42 +08:00
allow configure autoexpunge
This commit is contained in:
parent
6c406c11f5
commit
97d004bd53
10 changed files with 566 additions and 536 deletions
|
@ -49,6 +49,9 @@ ignoredHosts=[]
|
|||
#secure=false
|
||||
#ignoreSTARTTLS=true
|
||||
|
||||
# If true then EXPUNGE is called after a message gets a \Deleted flag set
|
||||
autoExpunge=true
|
||||
|
||||
[setup]
|
||||
# Public configuration for IMAP
|
||||
hostname="localhost"
|
||||
|
|
|
@ -9,3 +9,6 @@
|
|||
redis="redis://127.0.0.1:6379/13"
|
||||
|
||||
dbname="wildduck-test"
|
||||
|
||||
[imap]
|
||||
autoExpunge=false
|
||||
|
|
|
@ -477,11 +477,11 @@ class IMAPConnection extends EventEmitter {
|
|||
|
||||
conn._listenerData.lock = true;
|
||||
conn._server.notifier.getUpdates(selectedMailbox, conn.selected.modifyIndex, (err, updates) => {
|
||||
conn._listenerData.lock = false;
|
||||
if (conn._listenerData.cleared) {
|
||||
if (!conn._listenerData || conn._listenerData.cleared) {
|
||||
// already logged out
|
||||
return;
|
||||
}
|
||||
conn._listenerData.lock = false;
|
||||
|
||||
if (err) {
|
||||
conn.logger.info(
|
||||
|
|
|
@ -1089,8 +1089,8 @@ describe('IMAP Protocol integration tests', function() {
|
|||
|
||||
describe('EXPUNGE', function() {
|
||||
// EXPUNGE is a NO OP with autoexpunge
|
||||
it.skip('should automatically expunge messages', function(done) {
|
||||
let cmds = ['T1 LOGIN testuser pass', 'T2 SELECT INBOX', 'T3 STORE 2:* +FLAGS (\\Deleted)', 'SLEEP', 'T4 EXPUNGE', 'T6 LOGOUT'];
|
||||
it('should expunge all deleted messages', function(done) {
|
||||
let cmds = ['T1 LOGIN testuser pass', 'T2 SELECT INBOX', 'T3 STORE 2:* +FLAGS (\\Deleted)', 'T4 EXPUNGE', 'T6 LOGOUT'];
|
||||
|
||||
testClient(
|
||||
{
|
||||
|
@ -1100,7 +1100,6 @@ describe('IMAP Protocol integration tests', function() {
|
|||
},
|
||||
function(resp) {
|
||||
resp = resp.toString();
|
||||
console.log(resp);
|
||||
expect(resp.match(/^\* \d+ EXPUNGE/gm).length).to.equal(5);
|
||||
expect(/^T4 OK/m.test(resp)).to.be.true;
|
||||
done();
|
||||
|
@ -1111,8 +1110,8 @@ describe('IMAP Protocol integration tests', function() {
|
|||
|
||||
describe('UID EXPUNGE', function() {
|
||||
// UID EXPUNGE is a NO OP with autoexpunge
|
||||
it.skip('should automatically expunge messages', function(done) {
|
||||
let cmds = ['T1 LOGIN testuser pass', 'T2 SELECT INBOX', 'T3 STORE 1:* +FLAGS (\\Deleted)', 'SLEEP', 'T4 UID EXPUNGE 50', 'T5 LOGOUT'];
|
||||
it('should expunge specific messages', function(done) {
|
||||
let cmds = ['T1 LOGIN testuser pass', 'T2 SELECT INBOX', 'T3 STORE 1:* +FLAGS (\\Deleted)', 'T4 UID EXPUNGE 103,105', 'T5 LOGOUT'];
|
||||
|
||||
testClient(
|
||||
{
|
||||
|
@ -1122,8 +1121,9 @@ describe('IMAP Protocol integration tests', function() {
|
|||
},
|
||||
function(resp) {
|
||||
resp = resp.toString();
|
||||
expect(resp.match(/^\* \d+ EXPUNGE/gm).length).to.equal(1);
|
||||
expect(resp.match(/^\* \d+ EXPUNGE/gm).length).to.equal(2);
|
||||
expect(resp.match(/^\* 3 EXPUNGE/gm).length).to.equal(1);
|
||||
expect(resp.match(/^\* 4 EXPUNGE/gm).length).to.equal(1);
|
||||
expect(/^T4 OK/m.test(resp)).to.be.true;
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -28,376 +28,382 @@ module.exports = (db, server, messageHandler) => {
|
|||
function sendMessage(options, callback) {
|
||||
let user = options.user;
|
||||
|
||||
db.users.collection('users').findOne({ _id: user },
|
||||
{
|
||||
fields: {
|
||||
username: true,
|
||||
name: true,
|
||||
address: true,
|
||||
quota: true,
|
||||
storageUsed: true,
|
||||
recipients: true,
|
||||
encryptMessages: true,
|
||||
pubKey: true,
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
(err, userData) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
db.users.collection('users').findOne(
|
||||
{ _id: user },
|
||||
{
|
||||
fields: {
|
||||
username: true,
|
||||
name: true,
|
||||
address: true,
|
||||
quota: true,
|
||||
storageUsed: true,
|
||||
recipients: true,
|
||||
encryptMessages: true,
|
||||
pubKey: true,
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
(err, userData) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
err = new Error('This user does not exist');
|
||||
err.code = 'ERRMISSINGUSER';
|
||||
return callback();
|
||||
}
|
||||
if (!userData) {
|
||||
err = new Error('This user does not exist');
|
||||
err.code = 'ERRMISSINGUSER';
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (userData.disabled) {
|
||||
err = new Error('User account is disabled');
|
||||
err.code = 'ERRDISABLEDUSER';
|
||||
return callback(err);
|
||||
}
|
||||
if (userData.disabled) {
|
||||
err = new Error('User account is disabled');
|
||||
err.code = 'ERRDISABLEDUSER';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let overQuota = Number(userData.quota || config.maxStorage * 1024 * 1024) - userData.storageUsed <= 0;
|
||||
userData.recipients = userData.recipients || config.maxRecipients;
|
||||
let overQuota = Number(userData.quota || config.maxStorage * 1024 * 1024) - userData.storageUsed <= 0;
|
||||
userData.recipients = userData.recipients || config.maxRecipients;
|
||||
|
||||
db.users
|
||||
.collection('addresses')
|
||||
.find({ user: userData._id }, { address: true, addrview: true })
|
||||
.toArray((err, addresses) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let getReferencedMessage = done => {
|
||||
if (!options.reference) {
|
||||
return done(null, false);
|
||||
}
|
||||
let query = {};
|
||||
if (typeof options.reference === 'object') {
|
||||
query.mailbox = options.reference.mailbox;
|
||||
query.uid = options.reference.id;
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
query.user = user;
|
||||
db.users.collection('messages').findOne(query,
|
||||
{
|
||||
fields: {
|
||||
'mimeTree.parsedHeader': true,
|
||||
thread: true
|
||||
}
|
||||
},
|
||||
(err, messageData) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let headers = (messageData && messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
||||
let subject = headers.subject || '';
|
||||
try {
|
||||
subject = libmime.decodeWords(subject).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
|
||||
if (!/^\w+: /.test(subject)) {
|
||||
subject = ((options.reference.action === 'forward' ? 'Fwd' : 'Re') + ': ' + subject).trim();
|
||||
}
|
||||
|
||||
let sender = headers['reply-to'] || headers.from || headers.sender;
|
||||
let replyTo = [];
|
||||
let replyCc = [];
|
||||
let uniqueRecipients = new Set();
|
||||
|
||||
let checkAddress = (target, addr) => {
|
||||
let address = tools.normalizeAddress(addr.address);
|
||||
if (!inAddressList(addresses, address) && !uniqueRecipients.has(address)) {
|
||||
uniqueRecipients.add(address);
|
||||
if (addr.name) {
|
||||
try {
|
||||
addr.name = libmime.decodeWords(addr.name).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
}
|
||||
target.push(addr);
|
||||
}
|
||||
};
|
||||
|
||||
if (sender && sender.address) {
|
||||
checkAddress(replyTo, sender);
|
||||
}
|
||||
|
||||
if (options.reference.action === 'replyAll') {
|
||||
[].concat(headers.to || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyTo, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
[].concat(headers.cc || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyCc, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
}
|
||||
|
||||
let messageId = (headers['message-id'] || '').trim();
|
||||
let references = (headers.references || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(' ')
|
||||
.filter(mid => mid);
|
||||
|
||||
if (messageId && !references.includes(messageId)) {
|
||||
references.unshift(messageId);
|
||||
}
|
||||
if (references.length > 50) {
|
||||
references = references.slice(0, 50);
|
||||
}
|
||||
|
||||
return done(null, {
|
||||
replyTo,
|
||||
replyCc,
|
||||
inReplyTo: messageId,
|
||||
references: references.join(' '),
|
||||
subject,
|
||||
thread: messageData.thread
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getReferencedMessage((err, referenceData) => {
|
||||
db.users
|
||||
.collection('addresses')
|
||||
.find({ user: userData._id }, { address: true, addrview: true })
|
||||
.toArray((err, addresses) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let envelope = options.envelope;
|
||||
|
||||
if (!envelope) {
|
||||
envelope = {
|
||||
from: options.from,
|
||||
to: []
|
||||
};
|
||||
}
|
||||
|
||||
if (!envelope.from) {
|
||||
if (options.from) {
|
||||
envelope.from = options.from;
|
||||
let getReferencedMessage = done => {
|
||||
if (!options.reference) {
|
||||
return done(null, false);
|
||||
}
|
||||
let query = {};
|
||||
if (typeof options.reference === 'object') {
|
||||
query.mailbox = options.reference.mailbox;
|
||||
query.uid = options.reference.id;
|
||||
} else {
|
||||
options.from = envelope.from = {
|
||||
name: userData.name || '',
|
||||
address: userData.address
|
||||
};
|
||||
return done(null, false);
|
||||
}
|
||||
}
|
||||
query.user = user;
|
||||
db.users.collection('messages').findOne(
|
||||
query,
|
||||
{
|
||||
fields: {
|
||||
'mimeTree.parsedHeader': true,
|
||||
thread: true
|
||||
}
|
||||
},
|
||||
(err, messageData) => {
|
||||
if (err) {
|
||||
err.code = 'ERRDB';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
options.from = options.from || envelope.from;
|
||||
let headers = (messageData && messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
||||
let subject = headers.subject || '';
|
||||
try {
|
||||
subject = libmime.decodeWords(subject).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
|
||||
if (!inAddressList(addresses, tools.normalizeAddress(options.from.address))) {
|
||||
options.from.address = userData.address;
|
||||
}
|
||||
if (!/^\w+: /.test(subject)) {
|
||||
subject = ((options.reference.action === 'forward' ? 'Fwd' : 'Re') + ': ' + subject).trim();
|
||||
}
|
||||
|
||||
if (!inAddressList(addresses, tools.normalizeAddress(envelope.from.address))) {
|
||||
envelope.from.address = userData.address;
|
||||
}
|
||||
let sender = headers['reply-to'] || headers.from || headers.sender;
|
||||
let replyTo = [];
|
||||
let replyCc = [];
|
||||
let uniqueRecipients = new Set();
|
||||
|
||||
if (!envelope.to.length) {
|
||||
envelope.to = envelope.to
|
||||
.concat(options.to || [])
|
||||
.concat(options.cc || [])
|
||||
.concat(options.bcc || []);
|
||||
if (!envelope.to.length && referenceData && ['reply', 'replyAll'].includes(options.reference.action)) {
|
||||
envelope.to = envelope.to.concat(referenceData.replyTo || []).concat(referenceData.replyCc || []);
|
||||
options.to = referenceData.replyTo;
|
||||
options.cc = referenceData.replyCc;
|
||||
}
|
||||
}
|
||||
let checkAddress = (target, addr) => {
|
||||
let address = tools.normalizeAddress(addr.address);
|
||||
if (!inAddressList(addresses, address) && !uniqueRecipients.has(address)) {
|
||||
uniqueRecipients.add(address);
|
||||
if (addr.name) {
|
||||
try {
|
||||
addr.name = libmime.decodeWords(addr.name).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
}
|
||||
target.push(addr);
|
||||
}
|
||||
};
|
||||
|
||||
let extraHeaders = [];
|
||||
if (referenceData) {
|
||||
if (['reply', 'replyAll'].includes(options.reference.action)) {
|
||||
extraHeaders.push({ key: 'In-Reply-To', value: referenceData.inReplyTo });
|
||||
}
|
||||
extraHeaders.push({ key: 'References', value: referenceData.references });
|
||||
}
|
||||
if (sender && sender.address) {
|
||||
checkAddress(replyTo, sender);
|
||||
}
|
||||
|
||||
envelope.to = envelope.to.filter(addr => !inAddressList(addresses, tools.normalizeAddress(addr.address)));
|
||||
if (options.reference.action === 'replyAll') {
|
||||
[].concat(headers.to || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyTo, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
[].concat(headers.cc || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyCc, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
}
|
||||
|
||||
let now = new Date();
|
||||
let sendTime = options.sendTime;
|
||||
if (!sendTime || sendTime < now) {
|
||||
sendTime = now;
|
||||
}
|
||||
let messageId = (headers['message-id'] || '').trim();
|
||||
let references = (headers.references || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(' ')
|
||||
.filter(mid => mid);
|
||||
|
||||
let data = {
|
||||
envelope,
|
||||
from: options.from,
|
||||
date: sendTime,
|
||||
to: options.to || [],
|
||||
cc: options.cc || [],
|
||||
bcc: options.bcc || [],
|
||||
subject: options.subject || (referenceData && referenceData.subject) || '',
|
||||
text: options.text || '',
|
||||
html: options.html || '',
|
||||
headers: extraHeaders.concat(options.headers || []),
|
||||
attachments: options.attachments || []
|
||||
if (messageId && !references.includes(messageId)) {
|
||||
references.unshift(messageId);
|
||||
}
|
||||
if (references.length > 50) {
|
||||
references = references.slice(0, 50);
|
||||
}
|
||||
|
||||
return done(null, {
|
||||
replyTo,
|
||||
replyCc,
|
||||
inReplyTo: messageId,
|
||||
references: references.join(' '),
|
||||
subject,
|
||||
thread: messageData.thread
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let compiler = new MailComposer(data);
|
||||
let compiled = compiler.compile();
|
||||
let collector = new StreamCollect();
|
||||
let compiledEnvelope = compiled.getEnvelope();
|
||||
getReferencedMessage((err, referenceData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
messageHandler.counters.ttlcounter(
|
||||
'wdr:' + userData._id.toString(),
|
||||
compiledEnvelope.to.length,
|
||||
userData.recipients,
|
||||
false,
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
err.code = 'ERRREDIS';
|
||||
return callback(err);
|
||||
}
|
||||
let envelope = options.envelope;
|
||||
|
||||
let success = result.success;
|
||||
let sent = result.value;
|
||||
let ttl = result.ttl;
|
||||
if (!envelope) {
|
||||
envelope = {
|
||||
from: options.from,
|
||||
to: []
|
||||
};
|
||||
}
|
||||
|
||||
let ttlHuman = false;
|
||||
if (ttl) {
|
||||
if (ttl < 60) {
|
||||
ttlHuman = ttl + ' seconds';
|
||||
} else if (ttl < 3600) {
|
||||
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
||||
} else {
|
||||
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, userData.recipients, ttl);
|
||||
let err = new Error('You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : ''));
|
||||
err.code = 'ERRSENDINGLIMIT';
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
|
||||
let messageId = new ObjectID();
|
||||
let message = maildrop(
|
||||
{
|
||||
parentId: messageId,
|
||||
reason: 'submit',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
sendTime
|
||||
},
|
||||
(err, ...args) => {
|
||||
if (err || !args[0]) {
|
||||
if (err) {
|
||||
err.code = err.code || 'ERRCOMPOSE';
|
||||
}
|
||||
return callback(err, ...args);
|
||||
}
|
||||
|
||||
let outbound = args[0].id;
|
||||
|
||||
if (overQuota) {
|
||||
log.info('API', 'STOREFAIL user=%s error=%s', user, 'Over quota');
|
||||
return callback(null, {
|
||||
id: false,
|
||||
mailbox: false,
|
||||
queueId: outbound,
|
||||
overQuota: true
|
||||
});
|
||||
}
|
||||
|
||||
// Checks if the message needs to be encrypted before storing it
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages ? userData.pubKey : false,
|
||||
{ chunks: collector.chunks, chunklen: collector.chunklen },
|
||||
(err, encrypted) => {
|
||||
let raw = false;
|
||||
if (!err && encrypted) {
|
||||
// message was encrypted, so use the result instead of raw
|
||||
raw = encrypted;
|
||||
}
|
||||
|
||||
let messageOptions = {
|
||||
user: userData._id,
|
||||
specialUse: '\\Sent',
|
||||
|
||||
outbound,
|
||||
|
||||
meta: {
|
||||
source: 'API',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
origin: options.ip,
|
||||
sess: options.sess,
|
||||
time: new Date()
|
||||
},
|
||||
|
||||
date: false,
|
||||
flags: ['\\Seen'],
|
||||
|
||||
// if similar message exists, then skip
|
||||
skipExisting: true
|
||||
};
|
||||
|
||||
if (raw) {
|
||||
messageOptions.raw = raw;
|
||||
} else {
|
||||
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
||||
}
|
||||
|
||||
messageHandler.add(messageOptions, (err, success, info) => {
|
||||
if (err) {
|
||||
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
|
||||
err.code = 'SUBMITFAIL';
|
||||
return callback(err);
|
||||
} else if (!info) {
|
||||
log.info('API', 'SUBMITSKIP user=%s message=already exists', user);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
if (message) {
|
||||
let stream = compiled.createReadStream();
|
||||
stream.once('error', err => message.emit('error', err));
|
||||
stream
|
||||
//aa
|
||||
.pipe(collector)
|
||||
//bb
|
||||
.pipe(message);
|
||||
if (!envelope.from) {
|
||||
if (options.from) {
|
||||
envelope.from = options.from;
|
||||
} else {
|
||||
options.from = envelope.from = {
|
||||
name: userData.name || '',
|
||||
address: userData.address
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
options.from = options.from || envelope.from;
|
||||
|
||||
if (!inAddressList(addresses, tools.normalizeAddress(options.from.address))) {
|
||||
options.from.address = userData.address;
|
||||
}
|
||||
|
||||
if (!inAddressList(addresses, tools.normalizeAddress(envelope.from.address))) {
|
||||
envelope.from.address = userData.address;
|
||||
}
|
||||
|
||||
if (!envelope.to.length) {
|
||||
envelope.to = envelope.to
|
||||
.concat(options.to || [])
|
||||
.concat(options.cc || [])
|
||||
.concat(options.bcc || []);
|
||||
if (!envelope.to.length && referenceData && ['reply', 'replyAll'].includes(options.reference.action)) {
|
||||
envelope.to = envelope.to.concat(referenceData.replyTo || []).concat(referenceData.replyCc || []);
|
||||
options.to = referenceData.replyTo;
|
||||
options.cc = referenceData.replyCc;
|
||||
}
|
||||
}
|
||||
|
||||
let extraHeaders = [];
|
||||
if (referenceData) {
|
||||
if (['reply', 'replyAll'].includes(options.reference.action)) {
|
||||
extraHeaders.push({ key: 'In-Reply-To', value: referenceData.inReplyTo });
|
||||
}
|
||||
extraHeaders.push({ key: 'References', value: referenceData.references });
|
||||
}
|
||||
|
||||
envelope.to = envelope.to.filter(addr => !inAddressList(addresses, tools.normalizeAddress(addr.address)));
|
||||
|
||||
let now = new Date();
|
||||
let sendTime = options.sendTime;
|
||||
if (!sendTime || sendTime < now) {
|
||||
sendTime = now;
|
||||
}
|
||||
|
||||
let data = {
|
||||
envelope,
|
||||
from: options.from,
|
||||
date: sendTime,
|
||||
to: options.to || [],
|
||||
cc: options.cc || [],
|
||||
bcc: options.bcc || [],
|
||||
subject: options.subject || (referenceData && referenceData.subject) || '',
|
||||
text: options.text || '',
|
||||
html: options.html || '',
|
||||
headers: extraHeaders.concat(options.headers || []),
|
||||
attachments: options.attachments || []
|
||||
};
|
||||
|
||||
let compiler = new MailComposer(data);
|
||||
let compiled = compiler.compile();
|
||||
let collector = new StreamCollect();
|
||||
let compiledEnvelope = compiled.getEnvelope();
|
||||
|
||||
messageHandler.counters.ttlcounter(
|
||||
'wdr:' + userData._id.toString(),
|
||||
compiledEnvelope.to.length,
|
||||
userData.recipients,
|
||||
false,
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
err.code = 'ERRREDIS';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let success = result.success;
|
||||
let sent = result.value;
|
||||
let ttl = result.ttl;
|
||||
|
||||
let ttlHuman = false;
|
||||
if (ttl) {
|
||||
if (ttl < 60) {
|
||||
ttlHuman = ttl + ' seconds';
|
||||
} else if (ttl < 3600) {
|
||||
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
||||
} else {
|
||||
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, userData.recipients, ttl);
|
||||
let err = new Error(
|
||||
'You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : '')
|
||||
);
|
||||
err.code = 'ERRSENDINGLIMIT';
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
|
||||
let messageId = new ObjectID();
|
||||
let message = maildrop(
|
||||
{
|
||||
parentId: messageId,
|
||||
reason: 'submit',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
sendTime
|
||||
},
|
||||
(err, ...args) => {
|
||||
if (err || !args[0]) {
|
||||
if (err) {
|
||||
err.code = err.code || 'ERRCOMPOSE';
|
||||
}
|
||||
return callback(err, ...args);
|
||||
}
|
||||
|
||||
let outbound = args[0].id;
|
||||
|
||||
if (overQuota) {
|
||||
log.info('API', 'STOREFAIL user=%s error=%s', user, 'Over quota');
|
||||
return callback(null, {
|
||||
id: false,
|
||||
mailbox: false,
|
||||
queueId: outbound,
|
||||
overQuota: true
|
||||
});
|
||||
}
|
||||
|
||||
// Checks if the message needs to be encrypted before storing it
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages ? userData.pubKey : false,
|
||||
{ chunks: collector.chunks, chunklen: collector.chunklen },
|
||||
(err, encrypted) => {
|
||||
let raw = false;
|
||||
if (!err && encrypted) {
|
||||
// message was encrypted, so use the result instead of raw
|
||||
raw = encrypted;
|
||||
}
|
||||
|
||||
let messageOptions = {
|
||||
user: userData._id,
|
||||
specialUse: '\\Sent',
|
||||
|
||||
outbound,
|
||||
|
||||
meta: {
|
||||
source: 'API',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
origin: options.ip,
|
||||
sess: options.sess,
|
||||
time: new Date()
|
||||
},
|
||||
|
||||
date: false,
|
||||
flags: ['\\Seen'],
|
||||
|
||||
// if similar message exists, then skip
|
||||
skipExisting: true
|
||||
};
|
||||
|
||||
if (raw) {
|
||||
messageOptions.raw = raw;
|
||||
} else {
|
||||
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
||||
}
|
||||
|
||||
messageHandler.add(messageOptions, (err, success, info) => {
|
||||
if (err) {
|
||||
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
|
||||
err.code = 'SUBMITFAIL';
|
||||
return callback(err);
|
||||
} else if (!info) {
|
||||
log.info('API', 'SUBMITSKIP user=%s message=already exists', user);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
if (message) {
|
||||
let stream = compiled.createReadStream();
|
||||
stream.once('error', err => message.emit('error', err));
|
||||
stream
|
||||
//aa
|
||||
.pipe(collector)
|
||||
//bb
|
||||
.pipe(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
278
lib/autoreply.js
278
lib/autoreply.js
|
@ -13,161 +13,165 @@ module.exports = (options, callback) => {
|
|||
}
|
||||
|
||||
let curtime = new Date();
|
||||
db.database.collection('autoreplies').findOne({
|
||||
user: options.userData._id,
|
||||
start: {
|
||||
$lte: curtime
|
||||
db.database.collection('autoreplies').findOne(
|
||||
{
|
||||
user: options.userData._id,
|
||||
start: {
|
||||
$lte: curtime
|
||||
},
|
||||
end: {
|
||||
$gte: curtime
|
||||
}
|
||||
},
|
||||
end: {
|
||||
$gte: curtime
|
||||
}
|
||||
},
|
||||
(err, autoreply) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!autoreply || !autoreply.status) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
// step 1. check if recipient is valid (non special address)
|
||||
// step 2. check if recipient not in cache list
|
||||
// step 3. parse headers, check if not automatic message
|
||||
// step 4. prepare message with special headers (in-reply-to, references, Auto-Submitted)
|
||||
|
||||
let messageHeaders = false;
|
||||
let messageSplitter = new MessageSplitter();
|
||||
|
||||
messageSplitter.once('headers', headers => {
|
||||
messageHeaders = headers;
|
||||
|
||||
let autoSubmitted = headers.getFirst('Auto-Submitted');
|
||||
if (autoSubmitted && autoSubmitted.toLowerCase() !== 'no') {
|
||||
// skip automatic messages
|
||||
return callback(null, false);
|
||||
(err, autoreply) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let precedence = headers.getFirst('Precedence');
|
||||
if (precedence && ['list', 'junk', 'bulk'].includes(precedence.toLowerCase())) {
|
||||
return callback(null, false);
|
||||
}
|
||||
let listUnsubscribe = headers.getFirst('List-Unsubscribe');
|
||||
if (listUnsubscribe) {
|
||||
return callback(null, false);
|
||||
}
|
||||
let suppressAutoresponse = headers.getFirst('X-Auto-Response-Suppress');
|
||||
if (suppressAutoresponse && /OOF|AutoReply/i.test(suppressAutoresponse)) {
|
||||
|
||||
if (!autoreply || !autoreply.status) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.redis
|
||||
.multi()
|
||||
// delete all old entries
|
||||
.zremrangebyscore('war:' + autoreply._id, '-inf', Date.now() - consts.MAX_AUTOREPLY_INTERVAL)
|
||||
// add new entry if not present
|
||||
.zadd('war:' + autoreply._id, 'NX', Date.now(), options.sender)
|
||||
// if no-one touches this key from now, then delete after max interval has passed
|
||||
.expire('war:' + autoreply._id, consts.MAX_AUTOREPLY_INTERVAL)
|
||||
.exec((err, result) => {
|
||||
if (err) {
|
||||
errors.notify(err, { userId: options.userData._id });
|
||||
return callback(null, false);
|
||||
}
|
||||
// step 1. check if recipient is valid (non special address)
|
||||
// step 2. check if recipient not in cache list
|
||||
// step 3. parse headers, check if not automatic message
|
||||
// step 4. prepare message with special headers (in-reply-to, references, Auto-Submitted)
|
||||
|
||||
if (!result || !result[1] || !result[1][1]) {
|
||||
// already responded
|
||||
return callback(null, false);
|
||||
}
|
||||
let messageHeaders = false;
|
||||
let messageSplitter = new MessageSplitter();
|
||||
|
||||
// check limiting counters
|
||||
options.messageHandler.counters.ttlcounter('wda:' + options.userData._id, 1, consts.MAX_AUTOREPLIES, false, (err, result) => {
|
||||
if (err || !result.success) {
|
||||
messageSplitter.once('headers', headers => {
|
||||
messageHeaders = headers;
|
||||
|
||||
let autoSubmitted = headers.getFirst('Auto-Submitted');
|
||||
if (autoSubmitted && autoSubmitted.toLowerCase() !== 'no') {
|
||||
// skip automatic messages
|
||||
return callback(null, false);
|
||||
}
|
||||
let precedence = headers.getFirst('Precedence');
|
||||
if (precedence && ['list', 'junk', 'bulk'].includes(precedence.toLowerCase())) {
|
||||
return callback(null, false);
|
||||
}
|
||||
let listUnsubscribe = headers.getFirst('List-Unsubscribe');
|
||||
if (listUnsubscribe) {
|
||||
return callback(null, false);
|
||||
}
|
||||
let suppressAutoresponse = headers.getFirst('X-Auto-Response-Suppress');
|
||||
if (suppressAutoresponse && /OOF|AutoReply/i.test(suppressAutoresponse)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.redis
|
||||
.multi()
|
||||
// delete all old entries
|
||||
.zremrangebyscore('war:' + autoreply._id, '-inf', Date.now() - consts.MAX_AUTOREPLY_INTERVAL)
|
||||
// add new entry if not present
|
||||
.zadd('war:' + autoreply._id, 'NX', Date.now(), options.sender)
|
||||
// if no-one touches this key from now, then delete after max interval has passed
|
||||
.expire('war:' + autoreply._id, consts.MAX_AUTOREPLY_INTERVAL)
|
||||
.exec((err, result) => {
|
||||
if (err) {
|
||||
errors.notify(err, { userId: options.userData._id });
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let data = {
|
||||
envelope: {
|
||||
from: '',
|
||||
to: options.sender
|
||||
},
|
||||
from: {
|
||||
name: options.userData.name,
|
||||
address: options.recipient
|
||||
},
|
||||
to: options.sender,
|
||||
subject: autoreply.subject
|
||||
? 'Auto: ' + autoreply.subject
|
||||
: {
|
||||
prepared: true,
|
||||
value: 'Auto: Re: ' + headers.getFirst('Subject')
|
||||
},
|
||||
headers: {
|
||||
'Auto-Submitted': 'auto-replied',
|
||||
'X-WD-Autoreply-For': (options.parentId || '').toString()
|
||||
},
|
||||
inReplyTo: headers.getFirst('Message-ID'),
|
||||
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
|
||||
text: autoreply.text,
|
||||
html: autoreply.html
|
||||
};
|
||||
if (!result || !result[1] || !result[1][1]) {
|
||||
// already responded
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let compiler = new MailComposer(data);
|
||||
let message = maildrop(
|
||||
{
|
||||
parentId: options.parentId,
|
||||
reason: 'autoreply',
|
||||
from: '',
|
||||
// check limiting counters
|
||||
options.messageHandler.counters.ttlcounter('wda:' + options.userData._id, 1, consts.MAX_AUTOREPLIES, false, (err, result) => {
|
||||
if (err || !result.success) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let data = {
|
||||
envelope: {
|
||||
from: '',
|
||||
to: options.sender
|
||||
},
|
||||
from: {
|
||||
name: options.userData.name,
|
||||
address: options.recipient
|
||||
},
|
||||
to: options.sender,
|
||||
interface: 'autoreplies'
|
||||
},
|
||||
(err, ...args) => {
|
||||
if (err || !args[0]) {
|
||||
if (err) {
|
||||
err.code = err.code || 'ERRCOMPOSE';
|
||||
}
|
||||
return callback(err, ...args);
|
||||
}
|
||||
db.database.collection('messagelog').insertOne({
|
||||
id: args[0].id,
|
||||
messageId: args[0].messageId,
|
||||
subject: autoreply.subject
|
||||
? 'Auto: ' + autoreply.subject
|
||||
: {
|
||||
prepared: true,
|
||||
value: 'Auto: Re: ' + headers.getFirst('Subject')
|
||||
},
|
||||
headers: {
|
||||
'Auto-Submitted': 'auto-replied',
|
||||
'X-WD-Autoreply-For': (options.parentId || '').toString()
|
||||
},
|
||||
inReplyTo: headers.getFirst('Message-ID'),
|
||||
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
|
||||
text: autoreply.text,
|
||||
html: autoreply.html
|
||||
};
|
||||
|
||||
let compiler = new MailComposer(data);
|
||||
let message = maildrop(
|
||||
{
|
||||
parentId: options.parentId,
|
||||
action: 'AUTOREPLY',
|
||||
reason: 'autoreply',
|
||||
from: '',
|
||||
to: options.sender,
|
||||
created: new Date()
|
||||
interface: 'autoreplies'
|
||||
},
|
||||
() => callback(err, args && args[0].id));
|
||||
(err, ...args) => {
|
||||
if (err || !args[0]) {
|
||||
if (err) {
|
||||
err.code = err.code || 'ERRCOMPOSE';
|
||||
}
|
||||
return callback(err, ...args);
|
||||
}
|
||||
db.database.collection('messagelog').insertOne(
|
||||
{
|
||||
id: args[0].id,
|
||||
messageId: args[0].messageId,
|
||||
parentId: options.parentId,
|
||||
action: 'AUTOREPLY',
|
||||
from: '',
|
||||
to: options.sender,
|
||||
created: new Date()
|
||||
},
|
||||
() => callback(err, args && args[0].id)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (message) {
|
||||
compiler
|
||||
.compile()
|
||||
.createReadStream()
|
||||
.pipe(message);
|
||||
}
|
||||
);
|
||||
|
||||
if (message) {
|
||||
compiler
|
||||
.compile()
|
||||
.createReadStream()
|
||||
.pipe(message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
messageSplitter.on('error', () => false);
|
||||
messageSplitter.on('data', () => false);
|
||||
messageSplitter.on('end', () => false);
|
||||
messageSplitter.on('error', () => false);
|
||||
messageSplitter.on('data', () => false);
|
||||
messageSplitter.on('end', () => false);
|
||||
|
||||
setImmediate(() => {
|
||||
let pos = 0;
|
||||
let writeNextChunk = () => {
|
||||
if (messageHeaders || pos >= options.chunks.length) {
|
||||
return messageSplitter.end();
|
||||
}
|
||||
let chunk = options.chunks[pos++];
|
||||
if (!messageSplitter.write(chunk)) {
|
||||
return messageSplitter.once('drain', writeNextChunk);
|
||||
} else {
|
||||
setImmediate(writeNextChunk);
|
||||
}
|
||||
};
|
||||
setImmediate(writeNextChunk);
|
||||
});
|
||||
});
|
||||
setImmediate(() => {
|
||||
let pos = 0;
|
||||
let writeNextChunk = () => {
|
||||
if (messageHeaders || pos >= options.chunks.length) {
|
||||
return messageSplitter.end();
|
||||
}
|
||||
let chunk = options.chunks[pos++];
|
||||
if (!messageSplitter.write(chunk)) {
|
||||
return messageSplitter.once('drain', writeNextChunk);
|
||||
} else {
|
||||
setImmediate(writeNextChunk);
|
||||
}
|
||||
};
|
||||
setImmediate(writeNextChunk);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,17 +28,19 @@ module.exports = (options, callback) => {
|
|||
}
|
||||
return callback(err, ...args);
|
||||
}
|
||||
db.database.collection('messagelog').insertOne({
|
||||
id: args[0].id,
|
||||
messageId: args[0].messageId,
|
||||
action: 'FORWARD',
|
||||
parentId: options.parentId,
|
||||
from: options.sender,
|
||||
to: options.recipient,
|
||||
targets: options.targets,
|
||||
created: new Date()
|
||||
},
|
||||
() => callback(err, args && args[0] && args[0].id));
|
||||
db.database.collection('messagelog').insertOne(
|
||||
{
|
||||
id: args[0].id,
|
||||
messageId: args[0].messageId,
|
||||
action: 'FORWARD',
|
||||
parentId: options.parentId,
|
||||
from: options.sender,
|
||||
to: options.recipient,
|
||||
targets: options.targets,
|
||||
created: new Date()
|
||||
},
|
||||
() => callback(err, args && args[0] && args[0].id)
|
||||
);
|
||||
});
|
||||
if (message) {
|
||||
if (options.stream) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const db = require('../db');
|
||||
const tools = require('../tools');
|
||||
|
||||
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
|
||||
module.exports = (server, messageHandler) => (mailbox, update, session, callback) => {
|
||||
|
@ -25,18 +26,24 @@ module.exports = (server, messageHandler) => (mailbox, update, session, callback
|
|||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let query = {
|
||||
user: session.user.id,
|
||||
mailbox: mailboxData._id,
|
||||
undeleted: false,
|
||||
// uid is part of the sharding key so we need it somehow represented in the query
|
||||
uid: {}
|
||||
};
|
||||
|
||||
if (update.isUid) {
|
||||
query.uid = tools.checkRangeQuery(update.messages);
|
||||
} else {
|
||||
query.uid.$gt = 0;
|
||||
query.uid.$lt = mailboxData.uidNext;
|
||||
}
|
||||
|
||||
let cursor = db.database
|
||||
.collection('messages')
|
||||
.find({
|
||||
user: session.user.id,
|
||||
mailbox: mailboxData._id,
|
||||
undeleted: false,
|
||||
// uid is part of the sharding key so we need it somehow represented in the query
|
||||
uid: {
|
||||
$gt: 0,
|
||||
$lt: mailboxData.uidNext
|
||||
}
|
||||
})
|
||||
.find(query)
|
||||
.sort([['uid', 1]]);
|
||||
|
||||
let deletedMessages = 0;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('wild-config');
|
||||
const imapTools = require('../../imap-core/lib/imap-tools');
|
||||
const db = require('../db');
|
||||
const tools = require('../tools');
|
||||
|
@ -114,7 +115,7 @@ module.exports = server => (mailbox, update, session, callback) => {
|
|||
// first argument is an error
|
||||
return callback(...args);
|
||||
} else {
|
||||
if (shouldExpunge) {
|
||||
if (config.imap.autoExpunge && shouldExpunge) {
|
||||
// shcedule EXPUNGE command for current folder
|
||||
let expungeOptions = {
|
||||
// create new temporary session so it would not mix with the active one
|
||||
|
|
|
@ -279,18 +279,20 @@ module.exports = (options, callback) => {
|
|||
documents.push(delivery);
|
||||
}
|
||||
|
||||
db.senderDb.collection(config.sender.collection).insertMany(documents,
|
||||
{
|
||||
w: 1,
|
||||
ordered: false
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
db.senderDb.collection(config.sender.collection).insertMany(
|
||||
documents,
|
||||
{
|
||||
w: 1,
|
||||
ordered: false
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, envelope);
|
||||
});
|
||||
callback(null, envelope);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -352,19 +354,21 @@ function removeMessage(id, callback) {
|
|||
}
|
||||
|
||||
function setMeta(id, data, callback) {
|
||||
db.senderDb.collection(config.sender.gfs + '.files').findOneAndUpdate({
|
||||
filename: 'message ' + id
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'metadata.data': data
|
||||
db.senderDb.collection(config.sender.gfs + '.files').findOneAndUpdate(
|
||||
{
|
||||
filename: 'message ' + id
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'metadata.data': data
|
||||
}
|
||||
},
|
||||
{},
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
},
|
||||
{},
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue