Permalink
Browse files

Merge pull request #168 from baudehlo/mailbody_modifier

Bannering support
  • Loading branch information...
2 parents 844b391 + 7ee5a96 commit 6ee4a6fd9c0d211b0e4434d3604f42047fe1f888 @baudehlo committed Apr 19, 2012
Showing with 202 additions and 46 deletions.
  1. +6 −0 docs/Transaction.md
  2. +134 −19 mailbody.js
  3. +2 −22 mailheader.js
  4. +19 −5 transaction.js
  5. +41 −0 utils.js
View
6 docs/Transaction.md
@@ -74,3 +74,9 @@ Note that in the `start` event, you can set per-attachment events via:
body.on('attachment_data', cb)
body.on('attachment_end', cb)
+
+* transaction.set_banner(text, html)
+
+Sets a banner to be added to the end of the email. If the html part is not
+given (optional) then the text part will have each line ending replaced with
+`<br/>` when being inserted into HTML parts.
View
153 mailbody.js
@@ -2,6 +2,7 @@
// Mail Body Parser
var logger = require('./logger');
var Header = require('./mailheader').Header;
+var utils = require('./utils');
var events = require('events');
var util = require('util');
var Iconv = require('./mailheader').Iconv;
@@ -11,7 +12,8 @@ var buf_siz = 65536;
function Body (header, options) {
this.header = header || new Header();
this.header_lines = [];
- this.options = options;
+ this.is_html = false;
+ this.options = options || {};
this.bodytext = '';
this.body_text_encoded = '';
this.children = []; // if multipart
@@ -24,14 +26,14 @@ util.inherits(Body, events.EventEmitter);
exports.Body = Body;
Body.prototype.parse_more = function (line) {
- this["parse_" + this.state](line);
+ return this["parse_" + this.state](line);
}
Body.prototype.parse_child = function (line) {
// check for MIME boundary
if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) {
- this.children[this.children.length -1].parse_end(line);
+ line = this.children[this.children.length -1].parse_end(line);
if (this.children[this.children.length -1].state === 'attachment') {
var child = this.children[this.children.length - 1];
@@ -56,10 +58,10 @@ Body.prototype.parse_child = function (line) {
this.children.push(bod);
bod.state = 'headers';
}
- return;
+ return line;
}
// Pass data into last child
- this.children[this.children.length - 1].parse_more(line);
+ return this.children[this.children.length - 1].parse_more(line);
}
Body.prototype.parse_headers = function (line) {
@@ -72,12 +74,17 @@ Body.prototype.parse_headers = function (line) {
else {
this.header_lines.push(line);
}
+ return line;
}
Body.prototype.parse_start = function (line) {
var ct = this.header.get_decoded('content-type') || 'text/plain';
var enc = this.header.get_decoded('content-transfer-encoding') || '8bit';
var cd = this.header.get_decoded('content-disposition') || '';
+
+ if (/text\/html/i.test(ct)) {
+ this.is_html = true;
+ }
if (!enc.match(/^base64|quoted-printable|[78]bit$/i)) {
logger.logerror("Invalid CTE on email: " + enc + ", using 8bit");
@@ -108,21 +115,124 @@ Body.prototype.parse_start = function (line) {
this.state = 'attachment';
}
- this["parse_" + this.state](line);
+ return this["parse_" + this.state](line);
+}
+
+function _get_html_insert_position (buf) {
+ // TODO: consider re-writing this to go backwards from the end
+ for (var i=0,l=buf.length; i<l; i++) {
+ if (buf[i] === 60 && buf[i+1] === 47) { // found: "</"
+ if ( (buf[i+2] === 98 || buf[i+2] === 66) && // "b" or "B"
+ (buf[i+3] === 111 || buf[i+3] === 79) && // "o" or "O"
+ (buf[i+4] === 100 || buf[i+4] === 68) && // "d" or "D"
+ (buf[i+5] === 121 || buf[i+5] === 89) && // "y" or "Y"
+ buf[i+6] === 62)
+ {
+ // matched </body>
+ return i;
+ }
+ if ( (buf[i+2] === 104 || buf[i+2] === 72) && // "h" or "H"
+ (buf[i+3] === 116 || buf[i+3] === 84) && // "t" or "T"
+ (buf[i+4] === 109 || buf[i+4] === 77) && // "m" or "M"
+ (buf[i+5] === 108 || buf[i+5] === 76) && // "l" or "L"
+ buf[i+6] === 62)
+ {
+ // matched </html>
+ return i;
+ }
+ }
+ }
+ return buf.length - 1; // default is at the end
}
Body.prototype.parse_end = function (line) {
+ if (!line) {
+ line = '';
+ }
// ignore these lines - but we could store somewhere I guess.
- if (this.body_text_encoded.length) {
+ if (this.body_text_encoded.length && this.bodytext.length === 0) {
var buf = this.decode_function(this.body_text_encoded);
- if (Iconv) {
- var ct = this.header.get_decoded('content-type') || 'text/plain';
- var enc = 'UTF-8';
- var matches = /\bcharset\s*=\s*(?:\"|3D|')?([\w_\-]*)(?:\"|3D|')?/.exec(ct);
- if (matches) {
- enc = matches[1];
+
+ var ct = this.header.get_decoded('content-type') || 'text/plain';
+ var enc = 'UTF-8';
+ var matches = /\bcharset\s*=\s*(?:\"|3D|')?([\w_\-]*)(?:\"|3D|')?/.exec(ct);
+ if (matches) {
+ enc = matches[1];
+ }
+ this.body_encoding = enc;
+
+ if (this.options.banner && /^text\//i.test(ct)) {
+ // up until this point we've returned '' for line, so now we insert
+ // the banner and return the whole lot as one line, re-encoded using
+ // whatever encoding scheme we had to use to decode it in the first
+ // place.
+
+ // First we convert the banner to the same encoding as the body
+ var banner_str = this.options.banner[this.is_html ? 1 : 0];
+ var banner_buf = null;
+ if (Iconv) {
+ try {
+ var converter = new Iconv("UTF-8", enc + "//IGNORE");
+ banner_buf = converter.convert(banner_str);
+ }
+ catch (err) {
+ logger.logerror("iconv conversion of banner to " + enc + " failed: " + err);
+ }
+ }
+
+ if (!banner_buf) {
+ banner_buf = new Buffer(banner_str);
}
- this.body_encoding = enc;
+
+ // Allocate a new buffer: (6 or 1 is <p>...</p> vs \n...\n - correct that if you change those!)
+ var new_buf = new Buffer(buf.length + banner_buf.length + (this.is_html ? 6 : 1));
+
+ // Now we find where to insert it and combine it with the original buf:
+ if (this.is_html) {
+ var insert_pos = _get_html_insert_position(buf);
+
+ // copy start of buf into new_buf
+ buf.copy(new_buf, 0, 0, insert_pos);
+
+ // add in <p>
+ new_buf[insert_pos++] = 60;
+ new_buf[insert_pos++] = 80;
+ new_buf[insert_pos++] = 62;
+
+ // copy all of banner into new_buf
+ banner_buf.copy(new_buf, insert_pos);
+
+ new_buf[banner_buf.length + insert_pos++] = 60;
+ new_buf[banner_buf.length + insert_pos++] = 47;
+ new_buf[banner_buf.length + insert_pos++] = 80;
+ new_buf[banner_buf.length + insert_pos++] = 62;
+
+ // copy remainder of buf into new_buf, if there is buf remaining
+ if (buf.length > (insert_pos - 6)) {
+ buf.copy(new_buf, insert_pos + banner_buf.length, insert_pos - 7);
+ }
+ }
+ else {
+ buf.copy(new_buf);
+ new_buf[buf.length] = 10; // \n
+ banner_buf.copy(new_buf, buf.length + 1);
+ new_buf[buf.length + banner_buf.length + 1] = 10; // \n
+ }
+
+ // Now convert back to base_64 or QP if required:
+ if (this.decode_function === this.decode_qp) {
+ line = utils.encode_qp(new_buf.toString("binary")) + "\n" + line;
+ }
+ else if (this.decode_function === this.decode_base64) {
+ line = new_buf.toString("base64").replace(/(.{1,76})/g, "$1\n") + line;
+ }
+ else {
+ line = new_buf.toString("binary") + line; // "binary" is deprecated, lets hope this works...
+ }
+ }
+
+ // Now convert the buffer to UTF-8 to store in this.bodytext
+ if (Iconv) {
if (/UTF-?8/i.test(enc)) {
this.bodytext = buf.toString();
}
@@ -142,20 +252,24 @@ Body.prototype.parse_end = function (line) {
this.body_encoding = 'no_iconv';
this.bodytext = buf.toString();
}
+
// delete this.body_text_encoded;
}
+ return line;
}
Body.prototype.parse_body = function (line) {
this.body_text_encoded += line;
+ if (this.options.banner)
+ return '';
+ return line;
}
Body.prototype.parse_multipart_preamble = function (line) {
if (this.boundary) {
if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) {
if (line.substr(this.boundary.length + 2, 2) === '--') {
// end
- return;
}
else {
// next section
@@ -166,25 +280,25 @@ Body.prototype.parse_multipart_preamble = function (line) {
this.children.push(bod);
bod.state = 'headers';
this.state = 'child';
- return;
}
+ return line;
}
}
this.body_text_encoded += line;
+ return line;
}
Body.prototype.parse_attachment = function (line) {
if (this.boundary) {
if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) {
if (line.substr(this.boundary.length + 2, 2) === '--') {
// end
- return;
}
else {
// next section
this.state = 'headers';
- return;
}
+ return line;
}
}
@@ -215,9 +329,10 @@ Body.prototype.parse_attachment = function (line) {
buf.copy(this.buf, this.buf_fill);
this.buf_fill += buf.length;
}
+ return line;
}
-Body.prototype.decode_qp = require('./mailheader').decode_qp;
+Body.prototype.decode_qp = utils.decode_qp;
Body.prototype.decode_base64 = function (line) {
return new Buffer(line, "base64");
View
24 mailheader.js
@@ -1,6 +1,7 @@
"use strict";
// An RFC 2822 email header parser
var logger = require('./logger');
+var utils = require('./utils');
var Iconv;
try { Iconv = require('iconv').Iconv }
catch (err) {
@@ -16,27 +17,6 @@ function Header (options) {
exports.Header = Header;
exports.Iconv = Iconv;
-exports.decode_qp = function (line) {
- if (! /=/.test(line)) {
- // this may be a pointless optimisation...
- return new Buffer(line);
- }
- line = line.replace(/=\n/mg, '');
- var buf = new Buffer(line.length);
- var pos = 0;
- for (var i=0,l=line.length; i < l; i++) {
- if (line[i] === '=') {
- i++;
- buf[pos] = parseInt(line[i] + line[i+1], 16);
- i++;
- }
- else {
- buf[pos] = line.charCodeAt(i);
- }
- pos++;
- }
- return buf.slice(0, pos);
-}
Header.prototype.parse = function (lines) {
for (var i=0,l=lines.length; i < l; i++) {
@@ -69,7 +49,7 @@ function _decode_header (matched, encoding, cte, data) {
switch(cte) {
case 'Q':
- data = exports.decode_qp(data.replace(/_/g, ' '));
+ data = utils.decode_qp(data.replace(/_/g, ' '));
break;
case 'B':
data = new Buffer(data, "base64");
View
24 transaction.js
@@ -13,6 +13,7 @@ function Transaction() {
this.mail_from = null;
this.rcpt_to = [];
this.data_lines = [];
+ this.banner = null;
this.data_bytes = 0;
this.header_pos = 0;
this.parse_body = false;
@@ -37,18 +38,23 @@ Transaction.prototype.add_data = function(line) {
this.header.parse(this.data_lines);
this.header_pos = this.data_lines.length;
if (this.parse_body) {
- this.body = this.body || new body.Body(this.header);
+ this.body = this.body || new body.Body(this.header, {"banner": this.banner});
}
}
else if (this.header_pos && this.parse_body) {
- this.body.parse_more(line);
+ line = this.body.parse_more(line);
+ }
+ if (line.length) {
+ this.data_lines.push(line);
}
- this.data_lines.push(line);
};
Transaction.prototype.end_data = function() {
if (this.header_pos && this.parse_body) {
- this.body.parse_end();
+ var data = this.body.parse_end();
+ if (data.length) {
+ this.data_lines.push(data);
+ }
}
}
@@ -70,10 +76,18 @@ Transaction.prototype.remove_header = function (key) {
Transaction.prototype.attachment_hooks = function (start, data, end) {
this.parse_body = 1;
- this.body = this.body || new body.Body(this.header);
+ this.body = this.body || new body.Body(this.header, {"banner": this.banner});
this.body.on('attachment_start', start);
if (data)
this.body.on('attachment_data', data);
if (end)
this.body.on('attachment_end', end);
};
+
+Transaction.prototype.set_banner = function (text, html) {
+ this.parse_body = true;
+ if (!html) {
+ html = text.replace(/\n/g, '<br/>\n');
+ }
+ this.banner = [text, html];
+}
View
41 utils.js
@@ -78,3 +78,44 @@ exports.date_to_str = function (d) {
_pad(d.getHours(),2) + ':' + _pad(d.getMinutes(),2) + ':' + _pad(d.getSeconds(),2) +
' ' + d.toString().match(/\sGMT([+-]\d+)/)[1];
}
+
+exports.decode_qp = function (line) {
+ if (! /=/.test(line)) {
+ // this may be a pointless optimisation...
+ return new Buffer(line);
+ }
+ line = line.replace(/=\n/mg, '');
+ var buf = new Buffer(line.length);
+ var pos = 0;
+ for (var i=0,l=line.length; i < l; i++) {
+ if (line[i] === '=') {
+ i++;
+ buf[pos] = parseInt(line[i] + line[i+1], 16);
+ i++;
+ }
+ else {
+ buf[pos] = line.charCodeAt(i);
+ }
+ pos++;
+ }
+ return buf.slice(0, pos);
+}
+
+function _char_to_qp (ch) {
+ return "=" + _pad(ch.charCodeAt(0).toString(16).toUpperCase(), 2);
+}
+// Shameless attempt to copy from Perl's MIME::QuotedPrint::Perl code.
+exports.encode_qp = function (str) {
+ var broken_lines = '';
+ str = str.replace(/([^\ \t\n!"#\$%&'()*+,\-.\/0-9:;<>?\@A-Z\[\\\]^_`a-z{|}~])/g, function (orig, p1) {
+ return _char_to_qp(p1);
+ }).replace(/([ \t]+)$/gm, function (orig, p1) {
+ return p1.split('').map(_char_to_qp).join('');
+ }).replace(/([\s\S]*?^[^\n]{73}(?:[^=\n]{2}(?![^=\n]{0,1}$)|[^=\n](?![^=\n]{0,2}$)|(?![^=\n]{0,3}$)))/gm,
+ function (orig, p1) {
+ broken_lines += p1 + "=\n";
+ return '';
+ });
+ return broken_lines + str;
+}
+

0 comments on commit 6ee4a6f

Please sign in to comment.