galleon
Version:
A badass SMTP mail server built on Node to make your life simpler.
243 lines (207 loc) • 8.41 kB
JavaScript
/* -- Modules -- */
// Essential
var eventEmmiter = require('events').EventEmitter;
var util = require("util");
// Core
var outbound = require('./outbound');
// Foundations
var colors = require('colors'); // Better looking error handling
var moment = require('moment');
var _ = require('lodash');
/* -- ------- -- */
// GLOBALS
var queueUpdate, queueStart, queueAdd;
colors.setTheme({
silly: 'rainbow',
input: 'grey',
verbose: 'cyan',
prompt: 'grey',
success: 'green',
data: 'grey',
info: 'cyan',
warn: 'yellow',
debug: 'grey',
bgWhite: 'bgWhite',
bold: 'bold',
error: 'red'
});
/* Initiate outbound queue. */
var Queue = function (environment, callback) {
console.log("Queue created".success);
this.environment = environment;
eventEmmiter.call(this);
}
util.inherits(Queue, eventEmmiter);
queueStart = function queueStart(environment, databaseConnection) {
console.log("Queue started".success);
var maxConcurrent = 50;
var outbox = databaseConnection.collections.queue;
outbox.count({ state: 'transit' }).exec(function (error, count) {
if (error) return console.log(colors.error(error));
console.log(colors.info(count + " mails in transit"));
// Bit of a callback hell here
if ((count <= maxConcurrent) || (count === undefined)) {
outbox.find().where({ or: [{ state: 'pending' }, { state: 'denied' }] }).limit(Math.abs(maxConcurrent - count)).exec(function (error, models) {
if (error) return console.log(colors.error(error));
console.log(colors.info(models.length + " mails found in queue"));
// TIMED FILTER FOR ATTEMPTED EMAILS
models = _.filter(models, function (mail) {
/*
FILTERS OUT EMAILS BASED ON:
STATE - WHEN STATE IS NOT DENIED EMAIL IS KEPT
TIME - WHEN LAST ATTEMPT IS ( n+1 * 2 minutes ) IN THE PAST EMAIL IS KEPT
OTHERWISE EMAIL IS REMOVED FROM THE ARRAY
*/
if (mail.state !== 'denied') return true;
if (moment().isAfter(moment(mail.schedule.attempted).add((mail.attempts || 1) * 2, 'minutes'))) {
return true;
} else return false;
});
_.forEach(models, function (mail) {
outbox.update({ eID: mail.eID }, { state: 'transit' }).exec(function (error, mail) {
if (error) console.log(error.error);
var mail = mail[0]; // Update returns and Array
var OUTBOUND = new outbound(environment);
OUTBOUND.send({
from: mail.sender,
to: mail.to,
subject: mail.subject,
text: mail.text,
html: mail.html,
attachments: mail.attachments || []
}, OUTBOUND_TRACKER(databaseConnection, outbox, mail, maxConcurrent));
});
});
});
}
});
}
// OUTBOUND_TRACKER Prevents clogs in the outbound process by implementing a timeout (essentially assuming a sent was failed after 60 seconds)
/// Timeout should be assumed from the max number of concurrent sends (a ratio of 60seconds:10concurrent should be ideal)
var OUTBOUND_TRACKER = function OUTBOUND_TRACKER(databaseConnection, outbox, mail, maxConcurrent) {
var OUTBOUND_ERROR = function OUTBOUND_ERROR(error) {
///* SET MAX ATTEMPTS SET TO 5 AFTER QUEUE IS CRONNED
var MAX_ATTEMPTS = 1;
if (error) console.log("OUTBOUND_ERROR", error);
// UPDATE DENIED/FAILED ITEM & INCREMENT ATTEMPTS
outbox.update({ eID: mail.eID }, {
state: (mail.attempts >= MAX_ATTEMPTS) ? 'failed' : 'denied', attempts: ++mail.attempts, schedule: {
attempted: moment().toISOString(),
scheduled: mail.scheduled || moment().toISOString()
}
}).exec(function (error, _mail) {
if (error) console.log("OUTBOUND_ERROR->UPDATE", error);
else console.log((mail.attempts > MAX_ATTEMPTS) ? "OUTBOUND_ERROR->FAILED" : "OUTBOUND_ERROR->DENIED", _.first(_mail).eID, _.first(_mail).subject);
});
/// - FAILURE NOTICE - ///
if (mail.attempts >= MAX_ATTEMPTS) {
console.log("OUTBOUND_ERROR->FAILURE-NOTICE", mail.eID);
var notice = "The following email has been denied by the receiver.<br>Attempts have been made to deliver this email but have failed in multiple occasions. Please try again.<hr><br> Reason of denial: " + JSON.stringify(error) + "<br> Receiver: " + mail.to + "<br> Subject: " + mail.subject + "<br> Date: " + JSON.stringify(mail.stamp) + "<br><hr><br>" + mail.html;
databaseConnection.collections.mail.create({
association: mail.sender,
sender: mail.sender,
receiver: mail.to,
to: mail.to,
stamp: { sent: (new Date()), received: (new Date()) },
subject: "Failure Notice: " + mail.subject,
text: notice,
html: notice || "Failure Notice",
read: false,
trash: false,
dkim: "pass",
spf: "pass",
spam: false,
spamScore: 0,
// STRING ENUM: ['pending', 'approved', 'denied']
state: 'approved'
}, function (error, mail) {
if (error) console.log("OUTBOUND_ERROR->FAILURE-NOTICE-DENIED", error);
})
}
/// - -------------- - ///
}
// ALIAS FOR DEBUGGING
var OUTBOUND_TIMEDOUT = OUTBOUND_ERROR;
// TIMEOUT*
var local_timeout = setTimeout(OUTBOUND_TIMEDOUT, (maxConcurrent) ? Math.round(maxConcurrent * 6000) : 60000);
/* REQUIRED IMPROVEMENTS */
return function OUTBOUND_SENT(error, response) {
clearTimeout(local_timeout);
if (error) {
OUTBOUND_ERROR(error);
} else {
outbox.update({ eID: mail.eID }, { state: 'sent' }).limit(1).exec(function (error, mail) {
var mail = _.first(mail);
// Move from Queue to Mailbox
databaseConnection.collections.mail.create({
association: /(?:"?([^"]*)"?\s)?(?:<?(.+@[^>]+)>?)/.exec(mail.sender)[2],
sender: mail.sender,
receiver: mail.to,
to: mail.to,
stamp: { sent: new Date(), received: new Date() },
subject: mail.subject,
text: mail.text,
html: mail.html,
read: false,
trash: false,
sent: true,
spam: false,
spamScore: 0,
attachments: mail.attachments || [],
// STRING ENUM: ['draft', 'pending', 'approved', 'denied']
state: 'approved'
}, function (error, model) {
if (error) console.log(error);
if (!error) console.log("Message " + mail.subject + " sent");
})
});
}
}
}
queueAdd = function queueAdd(databaseConnection, mail, options, callback) {
var _this = this;
// Humane programming
if ((options.constructor !== Object) && (!callback)) callback = options;
var queue = {
association: mail.association,
sender: mail.from,
to: mail.to,
schedule: { attempted: moment().toISOString(), scheduled: moment().toISOString() },
attempts: 0,
subject: mail.subject,
text: mail.text || "",
html: mail.html || "",
// !IMPORTANT: Attachments should not exists here since it will override previous uploads
//attachments: mail.attachments || [],
// STRING ENUM: ['draft', 'pending', 'transit', 'sent', 'denied', 'failed']
state: (mail.draft) ? 'draft' : 'pending'
};
// Load queue modules
_this.environment.modulator.launch('queue', queue, function (error, _queue) {
if (_queue !== undefined) queue = _queue;
queueUpdate = function queueUpdate(error, model) {
if (error) console.log(colors.error(error));
// Start queue
queueStart(_this.environment, databaseConnection);
_this.emit('queued', error, model, databaseConnection);
if (callback) callback(error, model);
}
//
/// This section differentiates between deferred (draft) and immediate email
if (mail.remove && mail.id) {
// REMOVES DRAFT
databaseConnection.collections.queue.destroy({ eID: mail.id, association: queue.association });
} else if (!!mail.id) {
// UPDATES DRAFT (if ID is provided)
databaseConnection.collections.queue.update({ eID: mail.id, association: queue.association }, queue, queueUpdate)
} else {
// CREATES DRAFT/PENDING EMAIL
databaseConnection.collections.queue.create(queue, queueUpdate);
}
///
//
})
};
Queue.prototype.start = queueStart;
Queue.prototype.add = queueAdd;
module.exports = Queue;