mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-07 21:55:25 +08:00
when submitting message structures then extract embedded attachments from HTML before doing validation checks
This commit is contained in:
parent
8a7a46f050
commit
1e1417cedb
7 changed files with 158 additions and 97 deletions
|
@ -6,7 +6,8 @@ services:
|
|||
node_js:
|
||||
- 10
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
notifications:
|
||||
email:
|
||||
- andris@kreata.ee
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
module.exports = function(grunt) {
|
||||
module.exports = function (grunt) {
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
eslint: {
|
||||
|
@ -22,7 +22,7 @@ module.exports = function(grunt) {
|
|||
wait: {
|
||||
server: {
|
||||
options: {
|
||||
delay: 7 * 1000
|
||||
delay: 12 * 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,8 +4,6 @@ const config = require('wild-config');
|
|||
const log = require('npmlog');
|
||||
const libmime = require('libmime');
|
||||
const Joi = require('joi');
|
||||
const uuid = require('uuid');
|
||||
const os = require('os');
|
||||
const MongoPaging = require('mongo-cursor-pagination');
|
||||
const addressparser = require('nodemailer/lib/addressparser');
|
||||
const MailComposer = require('nodemailer/lib/mail-composer');
|
||||
|
@ -20,6 +18,7 @@ const Maildropper = require('../maildropper');
|
|||
const util = require('util');
|
||||
const roles = require('../roles');
|
||||
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
|
||||
const { preprocessAttachments } = require('../data-url');
|
||||
|
||||
module.exports = (db, server, messageHandler, userHandler, storageHandler) => {
|
||||
let maildrop = new Maildropper({
|
||||
|
@ -1576,6 +1575,9 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => {
|
|||
req.params.raw = req.body;
|
||||
}
|
||||
|
||||
// do this before validation so we would not end up with too large html values
|
||||
preprocessAttachments(req.params);
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
|
@ -1738,25 +1740,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => {
|
|||
keepBcc: true
|
||||
};
|
||||
|
||||
if (data.html && typeof data.html === 'string') {
|
||||
let fromAddress = (data.from && data.from.address).toString() || os.hostname();
|
||||
let cids = new Map();
|
||||
data.html = data.html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
if (cids.has(dataUri)) {
|
||||
return prefix + 'cid:' + cids.get(dataUri);
|
||||
}
|
||||
let cid = uuid.v4() + '-attachments@' + fromAddress.split('@').pop();
|
||||
data.attachments.push(
|
||||
processDataUrl({
|
||||
path: dataUri,
|
||||
cid
|
||||
})
|
||||
);
|
||||
cids.set(dataUri, cid);
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
}
|
||||
|
||||
// ensure plaintext content if html is provided
|
||||
if (data.html && !data.text) {
|
||||
try {
|
||||
|
@ -3069,28 +3052,3 @@ function parseAddresses(data) {
|
|||
walk([].concat(data || []));
|
||||
return Array.from(addresses);
|
||||
}
|
||||
|
||||
function processDataUrl(element) {
|
||||
let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
|
||||
if (!parts) {
|
||||
return element;
|
||||
}
|
||||
|
||||
element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));
|
||||
|
||||
if ('path' in element) {
|
||||
element.path = false;
|
||||
}
|
||||
|
||||
if ('href' in element) {
|
||||
element.href = false;
|
||||
}
|
||||
|
||||
parts[1].split(';').forEach(item => {
|
||||
if (/^\w+\/[^/]+$/i.test(item)) {
|
||||
element.contentType = element.contentType || item.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
const config = require('wild-config');
|
||||
const log = require('npmlog');
|
||||
const libmime = require('libmime');
|
||||
const uuid = require('uuid');
|
||||
const os = require('os');
|
||||
const util = require('util');
|
||||
const MailComposer = require('nodemailer/lib/mail-composer');
|
||||
const htmlToText = require('html-to-text');
|
||||
|
@ -15,6 +13,7 @@ const Maildropper = require('../maildropper');
|
|||
const roles = require('../roles');
|
||||
const Transform = require('stream').Transform;
|
||||
const { sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
|
||||
const { preprocessAttachments } = require('../data-url');
|
||||
|
||||
class StreamCollect extends Transform {
|
||||
constructor() {
|
||||
|
@ -383,25 +382,6 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
disableUrlAccess: true
|
||||
};
|
||||
|
||||
if (data.html && typeof data.html === 'string') {
|
||||
let fromAddress = (data.from && data.from.address).toString() || os.hostname();
|
||||
let cids = new Map();
|
||||
data.html = data.html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
if (cids.has(dataUri)) {
|
||||
return prefix + 'cid:' + cids.get(dataUri);
|
||||
}
|
||||
let cid = uuid.v4() + '-attachments@' + fromAddress.split('@').pop();
|
||||
data.attachments.push(
|
||||
processDataUrl({
|
||||
path: dataUri,
|
||||
cid
|
||||
})
|
||||
);
|
||||
cids.set(dataUri, cid);
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
}
|
||||
|
||||
// ensure plaintext content if html is provided
|
||||
if (data.html && !data.text) {
|
||||
try {
|
||||
|
@ -723,6 +703,9 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
// extract embedded attachments from HTML
|
||||
preprocessAttachments(req.params);
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
|
@ -772,28 +755,3 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
})
|
||||
);
|
||||
};
|
||||
|
||||
function processDataUrl(element) {
|
||||
let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
|
||||
if (!parts) {
|
||||
return element;
|
||||
}
|
||||
|
||||
element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));
|
||||
|
||||
if ('path' in element) {
|
||||
element.path = false;
|
||||
}
|
||||
|
||||
if ('href' in element) {
|
||||
element.href = false;
|
||||
}
|
||||
|
||||
parts[1].split(';').forEach(item => {
|
||||
if (/^\w+\/[^/]+$/i.test(item)) {
|
||||
element.contentType = element.contentType || item.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
|
74
lib/data-url.js
Normal file
74
lib/data-url.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
const uuid = require('uuid');
|
||||
|
||||
function processDataUrl(element, useBase64) {
|
||||
let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
|
||||
if (!parts) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (useBase64) {
|
||||
element.content = /\bbase64$/i.test(parts[1]) ? parts[2] : Buffer.from(decodeURIComponent(parts[2])).toString('base64');
|
||||
element.encoding = 'base64';
|
||||
} else {
|
||||
element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));
|
||||
}
|
||||
|
||||
if ('path' in element) {
|
||||
delete element.path;
|
||||
}
|
||||
|
||||
if ('href' in element) {
|
||||
delete element.href;
|
||||
}
|
||||
|
||||
parts[1].split(';').forEach(item => {
|
||||
if (/^\w+\/[^/]+$/i.test(item)) {
|
||||
element.contentType = element.contentType || item.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts attachments from html field
|
||||
* @param {Object} data Parsed data object from client
|
||||
*/
|
||||
function preprocessAttachments(data) {
|
||||
let hostname = data.from && data.from.address && typeof data.from.address === 'string' ? data.from.address.split('@').pop() : os.hostname();
|
||||
|
||||
if (data.html && typeof data.html === 'string' && data.html.length < 12 * 1024 * 1024) {
|
||||
let attachments = [];
|
||||
let cids = new Map();
|
||||
|
||||
data.html = data.html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
if (cids.has(dataUri)) {
|
||||
return prefix + 'cid:' + cids.get(dataUri);
|
||||
}
|
||||
let cid = uuid.v4() + '-attachments@' + hostname;
|
||||
attachments.push(
|
||||
processDataUrl(
|
||||
{
|
||||
path: dataUri,
|
||||
cid
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
cids.set(dataUri, cid);
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
|
||||
if (attachments.length) {
|
||||
data.attachments = [].concat(data.attachments || []).concat(attachments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processDataUrl,
|
||||
preprocessAttachments
|
||||
};
|
|
@ -19,7 +19,7 @@
|
|||
"ajv": "6.12.6",
|
||||
"chai": "4.2.0",
|
||||
"docsify-cli": "4.4.2",
|
||||
"eslint": "7.13.0",
|
||||
"eslint": "7.14.0",
|
||||
"eslint-config-nodemailer": "1.2.0",
|
||||
"eslint-config-prettier": "6.15.0",
|
||||
"grunt": "1.3.0",
|
||||
|
@ -41,7 +41,7 @@
|
|||
"axios": "0.21.0",
|
||||
"base32.js": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "3.18.1",
|
||||
"bull": "3.20.0",
|
||||
"gelf": "2.0.1",
|
||||
"generate-password": "1.5.1",
|
||||
"he": "1.2.0",
|
||||
|
|
|
@ -453,4 +453,74 @@ describe('API tests', function () {
|
|||
expect(response.body.success).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('message', () => {
|
||||
before(async () => {
|
||||
const response = await server.get(`/users/${userId}/mailboxes`).expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
inbox = response.body.results.find(result => result.path === 'INBOX');
|
||||
expect(inbox).to.exist;
|
||||
inbox = inbox.id;
|
||||
});
|
||||
|
||||
it('should POST /users/:user/mailboxes/:mailbox/messages with text and html', async () => {
|
||||
const message = {
|
||||
from: {
|
||||
name: 'test tester',
|
||||
address: 'testuser@example.com'
|
||||
},
|
||||
subject: 'hello world',
|
||||
text: 'Hello hello world!',
|
||||
html: '<p>Hello hello world!</p>'
|
||||
};
|
||||
const response = await server.post(`/users/${userId}/mailboxes/${inbox}/messages`).send(message).expect(200);
|
||||
|
||||
expect(response.body.success).to.be.true;
|
||||
expect(response.body.message.id).to.be.gt(0);
|
||||
|
||||
const messageDataResponse = await server.get(`/users/${userId}/mailboxes/${inbox}/messages/${response.body.message.id}`);
|
||||
expect(response.body.success).to.be.true;
|
||||
|
||||
const messageData = messageDataResponse.body;
|
||||
expect(messageData.subject).to.equal(message.subject);
|
||||
expect(messageData.html[0]).to.equal(message.html);
|
||||
expect(messageData.attachments).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should POST /users/:user/mailboxes/:mailbox/messages with embedded attachment', async () => {
|
||||
const message = {
|
||||
from: {
|
||||
name: 'test tester',
|
||||
address: 'testuser@example.com'
|
||||
},
|
||||
subject: 'hello world',
|
||||
text: 'Hello hello world!',
|
||||
html:
|
||||
'<p>Hello hello world! <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" /></p>'
|
||||
};
|
||||
const response = await server.post(`/users/${userId}/mailboxes/${inbox}/messages`).send(message);
|
||||
|
||||
expect(response.body.success).to.be.true;
|
||||
expect(response.body.message.id).to.be.gt(0);
|
||||
|
||||
const messageDataResponse = await server.get(`/users/${userId}/mailboxes/${inbox}/messages/${response.body.message.id}`);
|
||||
expect(response.body.success).to.be.true;
|
||||
|
||||
const messageData = messageDataResponse.body;
|
||||
expect(messageData.subject).to.equal(message.subject);
|
||||
expect(messageData.html[0]).to.equal('<p>Hello hello world! <img src="attachment:ATT00001" alt="Red dot" /></p>');
|
||||
expect(messageData.attachments).to.deep.equal([
|
||||
{
|
||||
contentType: 'image/png',
|
||||
disposition: 'attachment',
|
||||
filename: 'attachment-1.png',
|
||||
id: 'ATT00001',
|
||||
related: true,
|
||||
size: 118,
|
||||
sizeKb: 1,
|
||||
transferEncoding: 'base64'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue