From 962680a98c731cfd704cf5340f99e92ea03a0985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20All=C3=A9tru?= Date: Fri, 27 Sep 2024 23:36:40 +0200 Subject: [PATCH] allow attachment filenames containing semicolons (#13) --- CHANGELOG.md | 3 +++ index.js | 9 +++++--- package.json | 2 +- test/body.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e408b96..aa6ffcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.2.4] - 2024-09-27 +- allow attachment filenames containing semicolons https://github.com/haraka/email-message/issues/12 + ### [1.2.3] - 2024-04-24 - style(es6): replace forEach with for...of diff --git a/index.js b/index.js index 11768cb..897d2f7 100644 --- a/index.js +++ b/index.js @@ -438,17 +438,20 @@ class Body extends events.EventEmitter { } this.ct = ct; + const buildRegex = (key) => new RegExp(`${key}\\s*=\\s*"([^"]+)"|${key}\\s*=\\s*"?([^";]+)"?`, 'i'); + const matchKey = (test, key) => test.match(buildRegex(key))?.filter(item => item); + let match; if (/^(?:text|message)\//i.test(ct) && !/^attachment/i.test(cd)) { this.state = 'body'; } else if (/^multipart\//i.test(ct)) { - match = ct.match(/boundary\s*=\s*"?([^";]+)"?/i); + match = matchKey(ct, 'boundary'); this.boundary = match ? match[1] : ''; this.state = 'multipart_preamble'; } else { - match = cd.match(/name\s*=\s*"?([^";]+)"?/i); + match = matchKey(cd, 'name'); if (!match) { - match = ct.match(/name\s*=\s*"?([^";]+)"?/i); + match = matchKey(ct, 'name'); } const filename = match ? match[1] : ''; this.attachment_stream = exports.createAttachmentStream(this.header); diff --git a/package.json b/package.json index a8857bc..f6f3849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "haraka-email-message", - "version": "1.2.3", + "version": "1.2.4", "description": "Haraka email message", "main": "index.js", "files": [ diff --git a/test/body.js b/test/body.js index cb3ef6b..f2e380a 100644 --- a/test/body.js +++ b/test/body.js @@ -406,4 +406,68 @@ describe('body', function () { done(); }); }); + + describe('attachments', function () { + describe('content-type-name', function () { + it('with-quotes', function (done) { + const body = new Body(); + body.on('attachment_start', (ct, filename) => { + assert.equal(filename, 'aaaa.zip'); + done(); + }); + body.header.parse([ + 'Content-Type: application/zip; name="aaaa.zip"' + ]); + body.parse_start(''); + }); + + it('without-quotes', function (done) { + const body = new Body(); + body.on('attachment_start', (ct, filename) => { + assert.equal(filename, 'aaaa.zip'); + done(); + }); + body.header.parse([ + 'Content-Type: application/zip; name=aaaa.zip' + ]); + body.parse_start(''); + }); + + it('with-quotes-and-semicolons', function (done) { + const body = new Body(); + body.on('attachment_start', (ct, filename) => { + assert.equal(filename, 'aaaa; bbb; cccc.zip'); + done(); + }); + body.header.parse([ + 'Content-Type: application/zip; name="aaaa; bbb; cccc.zip"' + ]); + body.parse_start(''); + }); + + it('with-one-quote-left', function (done) { + const body = new Body(); + body.on('attachment_start', (ct, filename) => { + assert.equal(filename, 'aaaa'); + done(); + }); + body.header.parse([ + 'Content-Type: application/zip; name="aaaa; bbb; cccc.zip' + ]); + body.parse_start(''); + }); + + it('with-one-quote-right', function (done) { + const body = new Body(); + body.on('attachment_start', (ct, filename) => { + assert.equal(filename, 'aaaa'); + done(); + }); + body.header.parse([ + 'Content-Type: application/zip; name=aaaa; bbb; cccc.zip"' + ]); + body.parse_start(''); + }); + }); + }); });