1
0
mirror of https://github.com/mgerb/mywebsite synced 2026-01-13 11:12:47 +00:00

updated bunch of file paths and changed the way posts are loaded

This commit is contained in:
2016-01-05 12:28:04 -06:00
parent 4bb8cae81e
commit 6ab45fe935
13249 changed files with 317868 additions and 2101398 deletions

View File

@@ -0,0 +1,441 @@
'use strict';
var createQueue = require('./message-queue');
var SMTPConnection = require('smtp-connection');
var dns = require('dns');
var net = require('net');
var os = require('os');
var util = require('util');
var packageData = require('../package.json');
var EventEmitter = require('events').EventEmitter;
// Expose to the world
module.exports = function(options) {
return new DirectMailer(options);
};
/**
* Creates a new DirectMailer instance. Provides method 'send' to queue
* outgoing e-mails. The queue is processed in the background.
*
* @constructor
* @param {Object} [options] Optional options object
*/
function DirectMailer(options) {
EventEmitter.call(this);
this._options = options || {};
this._queue = createQueue();
this._started = false;
this._lastId = 0;
// temporary object
var connection = new SMTPConnection({});
this.name = 'SMTP (direct)';
this.version = packageData.version + '[client:' + connection.version + ']';
}
util.inherits(DirectMailer, EventEmitter);
// Adds a dynamic property 'length'
Object.defineProperty(DirectMailer.prototype, 'length', {
get: function() {
return this._queue._instantQueue.length + this._queue._sortedQueue.length;
}
});
/**
* Adds an outgoing message to the queue. Recipient addresses are sorted
* by the receiving domain and for every domain, a copy of the message is queued.
*
* If input is deemed invalid, an error is thrown, so be ready to catch these
* when calling directmail.send(...)
*
* @param {Object} mail Mail object
* @param {Function} callback Callback function
*/
DirectMailer.prototype.send = function(mail, callback) {
var envelope = mail.data.envelope || mail.message.getEnvelope();
var domainEnvelopes = {};
if (!envelope.from) {
return callback(new Error('"From" address missing'));
}
envelope.to = [].concat(envelope.to || []);
if (!envelope.to.length) {
return callback('"Recipients" addresses missing');
}
// We cant't run existing streams more than once so we need to change these
// to buffers. Filenames, URLs etc are not affected for every
// message copy a new file stream will be created
this._clearStreams(mail, function() {
this._formatMessage(mail.message);
envelope.to.forEach(function(recipient) {
recipient = (recipient || '').toString();
var domain = (recipient.split('@').pop() || '').toLowerCase().trim();
if (!domainEnvelopes[domain]) {
domainEnvelopes[domain] = {
from: envelope.from,
to: [recipient]
};
} else if (domainEnvelopes[domain].to.indexOf(recipient) < 0) {
domainEnvelopes[domain].to.push(recipient);
}
});
var returned = 0;
var domains = Object.keys(domainEnvelopes);
var combinedInfo = {
accepted: [],
rejected: [],
pending: [],
errors: [],
envelope: mail.data.envelope || mail.message.getEnvelope()
};
domains.forEach((function(domain) {
var called = false;
var id = ++this._lastId;
var item = {
envelope: domainEnvelopes[domain],
data: mail.data,
message: mail.message,
domain: domain,
id: id,
callback: function(err, info) {
if (called) {
this._log('Callback for #%s already called. Updated values: %s', id, JSON.stringify(err || info));
return;
}
called = true;
returned++;
if (err) {
combinedInfo.errors.push(err);
if (err.recipients) {
combinedInfo.rejected = combinedInfo.rejected.concat(err.recipients || []);
}
} else if (info) {
combinedInfo.accepted = combinedInfo.accepted.concat(info.accepted || []);
combinedInfo.rejected = combinedInfo.rejected.concat(info.rejected || []);
combinedInfo.pending = combinedInfo.pending.concat(info.pending || []);
combinedInfo.messageId = info.messageId;
}
if (returned >= domains.length) {
if (combinedInfo.errors.length === domains.length) {
var error = new Error('Sending failed');
error.errors = combinedInfo.errors;
callback(error);
} else {
callback(null, combinedInfo);
}
}
}.bind(this)
};
this._queue.insert(item);
}).bind(this));
// start send loop if needed
if (!this._started) {
this._started = true;
// do not start the loop before current execution context is finished
setImmediate(this._loop.bind(this));
}
}.bind(this));
};
/**
* Emit a log event
*/
DirectMailer.prototype._log = function( /*str, arg1, arg2, ..., argN*/ ) {
if (!this._options.debug) {
return;
}
var args = Array.prototype.slice.call(arguments);
this.emit('log', {
type: 'direct',
message: util.format.apply(util, args)
});
};
/**
* Looping function to fetch a message from the queue and send it.
*/
DirectMailer.prototype._loop = function() {
// callback is fired when a message is added to the queue
this._queue.get((function(data) {
this._log('Retrieved message #%s from the queue, resolving %s', data.id, data.domain);
// Resolve destination MX server
this._resolveMx(data.domain, (function(err, list) {
if (err) {
this._log('Resolving %s for #%s failed', data.domain, data.id);
this._log(err);
} else if (!list || !list.length) {
this._log('Could not resolve any MX servers for %s', data.domain);
}
if (err || !list || !list.length) {
data.callback(err || new Error('Could not resolve MX for ' + data.domain));
return setImmediate(this._loop.bind(this));
}
// Sort MX list by priority field
list.sort(function(a, b) {
return (a && a.priority || 0) - (b && b.priority || 0);
});
// Use the first server on the list
var exchange = list[0] && list[0].exchange;
this._log('%s resolved to %s for #%s', data.domain, exchange, data.id);
// Try to send the message
this._process(exchange, data, (function(err, response) {
if (err) {
this._log('Failed processing message #%s', data.id);
} else {
this._log('Server responded for #%s: %s', data.id, JSON.stringify(response));
}
if (err) {
if (err.responseCode && err.responseCode >= 500) {
err.domain = data.domain;
err.exchange = exchange;
err.recipients = data.envelope.to;
data.callback(err);
} else {
data.replies = (data.replies || 0) + 1;
if (data.replies <= 5) {
this._queue.insert(data, this._options.retryDelay || data.replies * 15 * 60 * 1000);
this._log('Message #%s requeued', data.id);
data.callback(null, {
pending: {
domain: data.domain,
exchange: exchange,
recipients: data.envelope.to,
response: err.response
}
});
} else {
err.domain = data.domain;
err.exchange = exchange;
err.recipients = data.envelope.to;
data.callback(err);
}
}
} else {
data.callback(null, response);
}
setImmediate(this._loop.bind(this));
}).bind(this));
}).bind(this));
}).bind(this));
};
/**
* Sends a message to provided MX server
*
* @param {String} exchange MX server
* @param {Object} data Message object
* @param {Function} callback Callback to run once the message is either sent or sending fails
*/
DirectMailer.prototype._process = function(exchange, data, callback) {
this._log('Connecting to %s:%s for message #%s', exchange, this._options.port || 25, data.id);
var options = {
host: exchange,
port: this._options.port || 25,
ignoreTLS: true
};
// Add options from DirectMailer options to simplesmtp client
Object.keys(this._options).forEach((function(key) {
options[key] = this._options[key];
}).bind(this));
var connection = new SMTPConnection(options);
var returned = false;
connection.on('log', function(log) {
this.emit('log', log);
}.bind(this));
connection.once('error', function(err) {
if (returned) {
return;
}
returned = true;
return callback(err);
});
var sendMessage = function() {
connection.send(data.envelope, data.message.createReadStream(), function(err, info) {
if (returned) {
return;
}
returned = true;
connection.close();
if (err) {
return callback(err);
}
info.messageId = (data.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
return callback(null, info);
});
};
connection.connect(function() {
if (returned) {
return;
}
sendMessage();
}.bind(this));
};
/**
* Adds additional headers to the outgoing message
*
* @param {Object} message BuildMail message object
*/
DirectMailer.prototype._formatMessage = function(message) {
var hostname = this._resolveHostname(this._options.name);
// set the first header as 'Received:'
message._headers.unshift({
key: 'Received',
value: 'from localhost (127.0.0.1) by ' + hostname + ' with SMTP; ' + Date()
});
};
/**
* Detects stream objects and resolves these to buffers before sending. File paths,
* urls etc. are not affected.
*
* @param {Object} message BuildMail message object
* @param {Function} callback Callback to run
*/
DirectMailer.prototype._clearStreams = function(mail, callback) {
var streamNodes = [];
function walkNode(node) {
if (node.content && typeof node.content.pipe === 'function') {
streamNodes.push(node);
}
if (node.childNodes && node.childNodes.length) {
node.childNodes.forEach(walkNode);
}
}
walkNode(mail.message);
function resolveNodes() {
if (!streamNodes.length) {
return callback();
}
var node = streamNodes.shift();
mail.resolveContent(node, 'content', function(err) {
if (err) {
node.content = new Buffer('<' + err.message + '>');
}
setImmediate(resolveNodes);
});
}
resolveNodes();
};
/**
* Resolves MX server for a domain
*
* @param {String} domain Domain to resolve the MX to
* @param {Function} callback Callback function to run
*/
DirectMailer.prototype._resolveMx = function(domain, callback) {
domain = domain.replace(/[\[\]]/g, '');
// Do not try to resolve the domain name if it is an IP address
if (net.isIP(domain)) {
return callback(null, [{
'priority': 0,
'exchange': domain
}]);
}
dns.resolveMx(domain, function(err, list) {
if (err) {
if (err.code === 'ENODATA') {
// fallback to A
dns.resolve4(domain, function(err, list) {
if (err) {
if (err.code === 'ENODATA') {
// fallback to AAAA
dns.resolve6(domain, function(err, list) {
if (err) {
return callback(err);
}
// return the first resolved Ipv6 with priority 0
return callback(null, [].concat(list || []).map(function(entry) {
return {
'priority': 0,
'exchange': entry
};
}).slice(0, 1));
});
} else {
callback(err);
}
return;
}
// return the first resolved Ipv4 with priority 0
return callback(null, [].concat(list || []).map(function(entry) {
return {
'priority': 0,
'exchange': entry
};
}).slice(0, 1));
});
} else {
callback(err);
}
return;
}
callback(null, list);
});
};
/**
* Resolves current hostname. If resolved name is an IP address, uses 'localhost'.
*
* @param {String} [name] Preferred hostname
* @return {String} Resolved hostname
*/
DirectMailer.prototype._resolveHostname = function(name) {
if (!name || net.isIP(name.replace(/[\[\]]/g, '').trim())) {
name = (os.hostname && os.hostname()) || '';
}
if (!name || net.isIP(name.replace(/[\[\]]/g, '').trim())) {
name = 'localhost';
}
return name.toLowerCase();
};

View File

@@ -0,0 +1,118 @@
'use strict';
// expose to the world
module.exports = function() {
return new MessageQueue();
};
/**
* Creates a queue object
*
* @constructor
*/
function MessageQueue() {
this._instantQueue = [];
this._sortedQueue = [];
this._shiftTimer = null;
this._callbackQueue = [];
}
/**
* Sets a callback to be run when something comes available from the queue
*
* @param {Function} callback Callback function to run with queue element as an argument
*/
MessageQueue.prototype.get = function(callback) {
if (this._instantQueue.length) {
callback(this._instantQueue.pop());
} else {
this._callbackQueue.unshift(callback);
}
};
/**
* Adds an element to the queue. If delay (ms) is set, the data will not be available before
* specified delay has passed. Otherwise the data will be available for processing immediatelly.
*
* @param {Mixed} data Value to be queued
* @param {Number} [delay] If set, delay the availability of the data by {delay} milliseconds
*/
MessageQueue.prototype.insert = function(data, delay) {
var container, added = -1;
if (typeof delay !== 'number') {
this._instantQueue.unshift(data);
this._processInsert();
return true;
} else {
container = {
data: data,
available: Date.now() + delay
};
for (var i = 0, len = this._sortedQueue.length; i < len; i++) {
if (this._sortedQueue[i].available >= container.available) {
this._sortedQueue.splice(i, 0, container);
added = i;
break;
}
}
if (added < 0) {
this._sortedQueue.push(container);
added = 0;
}
if (added === 0) {
this._updateShiftTimer();
}
}
};
/**
* Clears previous timer and creates a new one (if needed) to process the element
* in the queue that needs to be processed first.
*/
MessageQueue.prototype._updateShiftTimer = function() {
var nextShift, now = Date.now();
clearTimeout(this._shiftTimer);
if (!this._sortedQueue.length) {
return;
}
nextShift = this._sortedQueue[0].available;
if (nextShift <= now) {
this._shiftSorted();
} else {
setTimeout(this._shiftSorted.bind(this),
// add +15ms to ensure that data is already available when the timer is fired
this._sortedQueue[0].available - Date.now() + 15);
}
};
/**
* Moves an element from the delayed queue to the immediate queue if an elmenet
* becomes avilable
*/
MessageQueue.prototype._shiftSorted = function() {
var container;
if (!this._sortedQueue.length) {
return;
}
if (this._sortedQueue[0].available <= Date.now()) {
container = this._sortedQueue.shift();
this.insert(container.data);
}
this._updateShiftTimer();
};
/**
* If data from a queue is available and a callback is set, run the callback
* with available data
*/
MessageQueue.prototype._processInsert = function() {
if (this._instantQueue.length && this._callbackQueue.length) {
this._callbackQueue.pop()(this._instantQueue.pop());
}
};