diff --git a/config/imap.toml b/config/imap.toml index f2b3cd49..2195e781 100644 --- a/config/imap.toml +++ b/config/imap.toml @@ -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" diff --git a/config/test.toml b/config/test.toml index e0645d63..76ea0644 100644 --- a/config/test.toml +++ b/config/test.toml @@ -9,3 +9,6 @@ redis="redis://127.0.0.1:6379/13" dbname="wildduck-test" + +[imap] + autoExpunge=false diff --git a/imap-core/lib/imap-connection.js b/imap-core/lib/imap-connection.js index 9b8cdbc2..3750689a 100644 --- a/imap-core/lib/imap-connection.js +++ b/imap-core/lib/imap-connection.js @@ -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( diff --git a/imap-core/test/protocol-test.js b/imap-core/test/protocol-test.js index 0496783e..642eca1b 100644 --- a/imap-core/test/protocol-test.js +++ b/imap-core/test/protocol-test.js @@ -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(); } diff --git a/lib/api/submit.js b/lib/api/submit.js index 016074cd..7ccc3200 100644 --- a/lib/api/submit.js +++ b/lib/api/submit.js @@ -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); + } + } + ); + }); }); - }); - }); + } + ); } /** diff --git a/lib/autoreply.js b/lib/autoreply.js index a032917e..fe8e27db 100644 --- a/lib/autoreply.js +++ b/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); + }); + } + ); }; diff --git a/lib/forward.js b/lib/forward.js index 801b3702..a1e187c6 100644 --- a/lib/forward.js +++ b/lib/forward.js @@ -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) { diff --git a/lib/handlers/on-expunge.js b/lib/handlers/on-expunge.js index 7f604a1e..24bc06af 100644 --- a/lib/handlers/on-expunge.js +++ b/lib/handlers/on-expunge.js @@ -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; diff --git a/lib/handlers/on-store.js b/lib/handlers/on-store.js index 7d7f623e..e335629c 100644 --- a/lib/handlers/on-store.js +++ b/lib/handlers/on-store.js @@ -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 diff --git a/lib/maildrop.js b/lib/maildrop.js index 35d1e7ee..0e7ca326 100644 --- a/lib/maildrop.js +++ b/lib/maildrop.js @@ -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(); - }); + ); }