'use strict'; const MailComposer = require('nodemailer/lib/mail-composer'); const MessageSplitter = require('./message-splitter'); const errors = require('./errors'); const { SettingsHandler } = require('./settings-handler'); async function autoreply(options, autoreplyData) { if (!options.sender || /mailer-daemon|no-?reply/gi.test(options.sender)) { return 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(); let settingsHandler = new SettingsHandler({ db: options.db.database }); let maxAutoreplyInterval = await settingsHandler.get('const:autoreply:interval', {}); return new Promise((resolve, reject) => { messageSplitter.once('headers', headers => { messageHeaders = headers; let autoSubmitted = headers.getFirst('Auto-Submitted'); if (autoSubmitted && autoSubmitted.toLowerCase() !== 'no') { // skip automatic messages return resolve(false); } let precedence = headers.getFirst('Precedence'); if (precedence && ['list', 'junk', 'bulk'].includes(precedence.toLowerCase())) { return resolve(false); } let listUnsubscribe = headers.getFirst('List-Unsubscribe'); if (listUnsubscribe) { return resolve(false); } let suppressAutoresponse = headers.getFirst('X-Auto-Response-Suppress'); if (suppressAutoresponse && /OOF|AutoReply/i.test(suppressAutoresponse)) { return resolve(false); } options.db.redis .multi() // delete all old entries .zremrangebyscore('war:' + autoreplyData._id, '-inf', Date.now() - maxAutoreplyInterval) // add new entry if not present .zadd('war:' + autoreplyData._id, 'NX', Date.now(), options.sender) // if no-one touches this key from now, then delete after max interval has passed .expire('war:' + autoreplyData._id, maxAutoreplyInterval) .exec((err, result) => { if (err) { errors.notify(err, { userId: autoreplyData._id }); return resolve(false); } if (!result || !result[1] || !result[1][1]) { // already responded return resolve(false); } // check limiting counters options.messageHandler.counters.ttlcounter('wda:' + autoreplyData._id, 1, maxAutoreplyInterval, false, (err, result) => { if (err || !result.success) { return resolve(false); } let inReplyTo = Buffer.from(headers.getFirst('Message-ID'), 'binary').toString(); let data = { envelope: { from: '', to: options.sender }, from: { name: autoreplyData.name || (options.userData && options.userData.name), address: options.recipient }, to: options.sender, subject: (autoreplyData.subject && 'Auto: ' + autoreplyData.subject) || { prepared: true, value: 'Auto: Re: ' + Buffer.from(headers.getFirst('Subject'), 'binary').toString() }, headers: { 'Auto-Submitted': 'auto-replied', 'X-WD-Autoreply-For': (options.parentId || options.queueId).toString() }, inReplyTo, references: (inReplyTo + ' ' + Buffer.from(headers.getFirst('References'), 'binary').toString()).trim(), text: autoreplyData.text, html: autoreplyData.html }; let compiler = new MailComposer(data); let message = options.maildrop.push( { parentId: options.parentId, reason: 'autoreply', from: '', to: options.sender, interface: 'autoreplies' }, (err, ...args) => { if (err || !args[0]) { if (err) { err.responseCode = 500; err.code = err.code || 'ERRCOMPOSE'; return reject(err); } return resolve(false); } let logentry = { id: args[0].id, messageId: args[0].messageId, action: 'AUTOREPLY', from: '', to: options.sender, sender: options.recipient, created: new Date() }; if (options.parentId) { logentry.parentId = options.parentId; } if (options.queueId) { logentry.queueId = options.queueId; } return resolve(args && args[0] && args[0].id); } ); if (message) { compiler.compile().createReadStream().pipe(message); } }); }); }); messageSplitter.on('error', err => reject(err)); 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); }); }); } module.exports = autoreply;