allow configure autoexpunge

This commit is contained in:
Andris Reinman 2017-12-15 11:02:47 +02:00
parent 6c406c11f5
commit 97d004bd53
10 changed files with 566 additions and 536 deletions

View file

@ -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"

View file

@ -9,3 +9,6 @@
redis="redis://127.0.0.1:6379/13"
dbname="wildduck-test"
[imap]
autoExpunge=false

View file

@ -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(

View file

@ -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();
}

View file

@ -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);
}
}
);
});
});
});
});
}
);
}
/**

View file

@ -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);
});
}
);
};

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -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();
});
);
}