Skip to content

Commit 18707c8

Browse files
author
Guðmundur Gunnarsson
committed
Added isEncoded option to createChild which prevents build from encoding node content.
1 parent 5ab00b7 commit 18707c8

8 files changed

+1494
-1558
lines changed

src/builder-unit.js

+608
Large diffs are not rendered by default.

src/builder.js

+489
Large diffs are not rendered by default.

src/emailjs-mime-builder.js

-730
This file was deleted.

src/utils-unit.js

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/* eslint-disable no-unused-expressions */
2+
3+
import {
4+
convertAddresses,
5+
generateBoundary,
6+
parseAddresses,
7+
normalizeHeaderKey,
8+
escapeHeaderArgument,
9+
encodeHeaderValue,
10+
buildHeaderValue,
11+
isPlainText
12+
} from './utils'
13+
14+
describe('#convertAddresses', function () {
15+
it('should convert address object to a string', function () {
16+
expect(convertAddresses([{
17+
name: 'Jõgeva Ants',
18+
address: 'ants@jõgeva.ee'
19+
}, {
20+
name: 'Composers',
21+
group: [{
22+
address: '[email protected]',
23+
name: 'Bach, Sebastian'
24+
}, {
25+
address: '[email protected]',
26+
name: 'Mozzie'
27+
}]
28+
}])).to.equal('=?UTF-8?Q?J=C3=B5geva_Ants?= <[email protected]>, Composers:"Bach, Sebastian" <[email protected]>, Mozzie <[email protected]>;')
29+
})
30+
31+
it('should keep ascii name as is', function () {
32+
expect(convertAddresses([{
33+
name: 'O\'Vigala Sass',
34+
address: '[email protected]'
35+
}])).to.equal('O\'Vigala Sass <[email protected]>')
36+
})
37+
38+
it('should include name in quotes for special symbols', function () {
39+
expect(convertAddresses([{
40+
name: 'Sass, Vigala',
41+
address: '[email protected]'
42+
}])).to.equal('"Sass, Vigala" <[email protected]>')
43+
})
44+
45+
it('should escape quotes', function () {
46+
expect(convertAddresses([{
47+
name: '"Vigala Sass"',
48+
address: '[email protected]'
49+
}])).to.equal('"\\"Vigala Sass\\"" <[email protected]>')
50+
})
51+
52+
it('should mime encode unicode names', function () {
53+
expect(convertAddresses([{
54+
name: '"Jõgeva Sass"',
55+
address: '[email protected]'
56+
}])).to.equal('=?UTF-8?Q?=22J=C3=B5geva_Sass=22?= <[email protected]>')
57+
})
58+
})
59+
60+
describe('isPlainText', function () {
61+
it('should return true', function () {
62+
expect(isPlainText('az09\t\r\n~!?')).to.be.true
63+
})
64+
65+
it('should return false on low bits', function () {
66+
expect(isPlainText('az09\n\x08!?')).to.be.false
67+
})
68+
69+
it('should return false on high bits', function () {
70+
expect(isPlainText('az09\nõ!?')).to.be.false
71+
})
72+
})
73+
74+
describe('generateBoundary ', function () {
75+
it('should genereate boundary string', function () {
76+
const nodeId = 'abc'
77+
const rootBoundary = 'def'
78+
expect(generateBoundary(nodeId, rootBoundary)).to.equal('----sinikael-?=_abc-def')
79+
})
80+
})
81+
82+
describe('parseAddresses', function () {
83+
it('should normalize header key', function () {
84+
expect(parseAddresses('test [email protected]')).to.deep.equal([{
85+
address: '[email protected]',
86+
name: 'test'
87+
}])
88+
89+
expect(parseAddresses(['test [email protected]'])).to.deep.equal([{
90+
address: '[email protected]',
91+
name: 'test'
92+
}])
93+
94+
expect(parseAddresses([
95+
96+
])).to.deep.equal([{
97+
address: '[email protected]',
98+
name: 'test'
99+
}])
100+
101+
expect(parseAddresses([{
102+
address: '[email protected]',
103+
name: 'test'
104+
}])).to.deep.equal([{
105+
address: '[email protected]',
106+
name: 'test'
107+
}])
108+
})
109+
})
110+
111+
describe('normalizeHeaderKey', function () {
112+
it('should normalize header key', function () {
113+
expect(normalizeHeaderKey('key')).to.equal('Key')
114+
expect(normalizeHeaderKey('mime-vERSION')).to.equal('MIME-Version')
115+
expect(normalizeHeaderKey('-a-long-name')).to.equal('-A-Long-Name')
116+
})
117+
})
118+
119+
describe('escapeHeaderArgument', function () {
120+
it('should return original value if possible', function () {
121+
expect(escapeHeaderArgument('abc')).to.equal('abc')
122+
})
123+
124+
it('should use quotes', function () {
125+
expect(escapeHeaderArgument('abc "tere"')).to.equal('"abc \\"tere\\""')
126+
})
127+
})
128+
129+
describe('encodeHeaderValue', function () {
130+
it('should do noting if possible', function () {
131+
expect(encodeHeaderValue('x-my', 'test value')).to.equal('test value')
132+
})
133+
134+
it('should encode non ascii characters', function () {
135+
expect(encodeHeaderValue('x-my', 'test jõgeva value')).to.equal('test =?UTF-8?B?asO1Z2V2YQ==?= value')
136+
})
137+
138+
it('should format references', function () {
139+
expect(encodeHeaderValue('references', 'abc def')).to.equal('<abc> <def>')
140+
expect(encodeHeaderValue('references', ['abc', 'def'])).to.equal('<abc> <def>')
141+
})
142+
143+
it('should format message-id', function () {
144+
expect(encodeHeaderValue('message-id', 'abc')).to.equal('<abc>')
145+
})
146+
147+
it('should format addresses', function () {
148+
expect(encodeHeaderValue('from', {
149+
name: 'the safewithme testuser',
150+
address: 'safewithme.testuser@jõgeva.com'
151+
})).to.equal('the safewithme testuser <[email protected]>')
152+
})
153+
})
154+
155+
describe('buildHeaderValue', function () {
156+
it('should build header value', function () {
157+
expect(buildHeaderValue({
158+
value: 'test'
159+
})).to.equal('test')
160+
})
161+
it('should build header value with params', function () {
162+
expect(buildHeaderValue({
163+
value: 'test',
164+
params: {
165+
a: 'b'
166+
}
167+
})).to.equal('test; a=b')
168+
})
169+
it('should build header value with empty params', function () {
170+
expect(buildHeaderValue({
171+
value: 'test',
172+
params: {
173+
a: ';'
174+
}
175+
})).to.equal('test; a=";"')
176+
})
177+
it('should build header value with quotes in params', function () {
178+
expect(buildHeaderValue({
179+
value: 'test',
180+
params: {
181+
a: ';"'
182+
}
183+
})).to.equal('test; a=";\\""')
184+
})
185+
it('should build header value with multiple params', function () {
186+
expect(buildHeaderValue({
187+
value: 'test',
188+
params: {
189+
a: 'b',
190+
c: 'd'
191+
}
192+
})).to.equal('test; a=b; c=d')
193+
})
194+
})

src/utils.js

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/* eslint-disable node/no-deprecated-api */
2+
/* eslint-disable no-control-regex */
3+
4+
import { flatten } from 'ramda'
5+
import parseAddress from 'emailjs-addressparser'
6+
import {
7+
mimeWordsEncode,
8+
mimeWordEncode,
9+
continuationEncode
10+
} from 'emailjs-mime-codec'
11+
import { toASCII } from 'punycode'
12+
13+
/**
14+
* If needed, mime encodes the name part
15+
*
16+
* @param {String} name Name part of an address
17+
* @returns {String} Mime word encoded string if needed
18+
*/
19+
function encodeAddressName (name) {
20+
if (!/^[\w ']*$/.test(name)) {
21+
if (/^[\x20-\x7e]*$/.test(name)) {
22+
return '"' + name.replace(/([\\"])/g, '\\$1') + '"'
23+
} else {
24+
return mimeWordEncode(name, 'Q')
25+
}
26+
}
27+
return name
28+
}
29+
30+
/**
31+
* Checks if a value is plaintext string (uses only printable 7bit chars)
32+
*
33+
* @param {String} value String to be tested
34+
* @returns {Boolean} true if it is a plaintext string
35+
*/
36+
export function isPlainText (value) {
37+
return !(typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value))
38+
}
39+
40+
/**
41+
* Rebuilds address object using punycode and other adjustments
42+
*
43+
* @param {Array} addresses An array of address objects
44+
* @param {Array} [uniqueList] An array to be populated with addresses
45+
* @return {String} address string
46+
*/
47+
export function convertAddresses (addresses = [], uniqueList = []) {
48+
var values = []
49+
50+
;[].concat(addresses).forEach(address => {
51+
if (address.address) {
52+
address.address = address.address
53+
.replace(/^.*?(?=@)/, user => mimeWordsEncode(user, 'Q'))
54+
.replace(/@.+$/, domain => '@' + toASCII(domain.substr(1)))
55+
56+
if (!address.name) {
57+
values.push(address.address)
58+
} else if (address.name) {
59+
values.push(encodeAddressName(address.name) + ' <' + address.address + '>')
60+
}
61+
62+
if (uniqueList.indexOf(address.address) < 0) {
63+
uniqueList.push(address.address)
64+
}
65+
} else if (address.group) {
66+
values.push(encodeAddressName(address.name) + ':' + (address.group.length ? convertAddresses(address.group, uniqueList) : '').trim() + ';')
67+
}
68+
})
69+
70+
return values.join(', ')
71+
}
72+
73+
/**
74+
* Parses addresses. Takes in a single address or an array or an
75+
* array of address arrays (eg. To: [[first group], [second group],...])
76+
*
77+
* @param {Mixed} addresses Addresses to be parsed
78+
* @return {Array} An array of address objects
79+
*/
80+
export function parseAddresses (addresses = []) {
81+
return flatten([].concat(addresses).map((address) => {
82+
if (address && address.address) {
83+
address = convertAddresses(address)
84+
}
85+
return parseAddress(address)
86+
}))
87+
}
88+
89+
/**
90+
* Encodes a header value for use in the generated rfc2822 email.
91+
*
92+
* @param {String} key Header key
93+
* @param {String} value Header value
94+
*/
95+
export function encodeHeaderValue (key, value = '') {
96+
key = normalizeHeaderKey(key)
97+
98+
switch (key) {
99+
case 'From':
100+
case 'Sender':
101+
case 'To':
102+
case 'Cc':
103+
case 'Bcc':
104+
case 'Reply-To':
105+
return convertAddresses(parseAddresses(value))
106+
107+
case 'Message-Id':
108+
case 'In-Reply-To':
109+
case 'Content-Id':
110+
value = value.replace(/\r?\n|\r/g, ' ')
111+
112+
if (value.charAt(0) !== '<') {
113+
value = '<' + value
114+
}
115+
116+
if (value.charAt(value.length - 1) !== '>') {
117+
value = value + '>'
118+
}
119+
return value
120+
121+
case 'References':
122+
value = [].concat.apply([], [].concat(value).map((elm = '') => elm
123+
.replace(/\r?\n|\r/g, ' ')
124+
.trim()
125+
.replace(/<[^>]*>/g, str => str.replace(/\s/g, ''))
126+
.split(/\s+/)
127+
)).map(function (elm) {
128+
if (elm.charAt(0) !== '<') {
129+
elm = '<' + elm
130+
}
131+
if (elm.charAt(elm.length - 1) !== '>') {
132+
elm = elm + '>'
133+
}
134+
return elm
135+
})
136+
137+
return value.join(' ').trim()
138+
139+
default:
140+
return mimeWordsEncode((value || '').toString().replace(/\r?\n|\r/g, ' '), 'B')
141+
}
142+
}
143+
144+
/**
145+
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
146+
*
147+
* @param {String} key Key to be normalized
148+
* @return {String} key in Camel-Case form
149+
*/
150+
export function normalizeHeaderKey (key = '') {
151+
return key.replace(/\r?\n|\r/g, ' ') // no newlines in keys
152+
.trim().toLowerCase()
153+
.replace(/^MIME\b|^[a-z]|-[a-z]/ig, c => c.toUpperCase()) // use uppercase words, except MIME
154+
}
155+
156+
/**
157+
* Generates a multipart boundary value
158+
*
159+
* @return {String} boundary value
160+
*/
161+
export function generateBoundary (nodeId, baseBoundary) {
162+
return '----sinikael-?=_' + nodeId + '-' + baseBoundary
163+
}
164+
165+
/**
166+
* Escapes a header argument value (eg. boundary value for content type),
167+
* adds surrounding quotes if needed
168+
*
169+
* @param {String} value Header argument value
170+
* @return {String} escaped and quoted (if needed) argument value
171+
*/
172+
export function escapeHeaderArgument (value) {
173+
if (value.match(/[\s'"\\;/=]|^-/g)) {
174+
return '"' + value.replace(/(["\\])/g, '\\$1') + '"'
175+
} else {
176+
return value
177+
}
178+
}
179+
180+
/**
181+
* Joins parsed header value together as 'value; param1=value1; param2=value2'
182+
*
183+
* @param {Object} structured Parsed header value
184+
* @return {String} joined header value
185+
*/
186+
export function buildHeaderValue (structured) {
187+
var paramsArray = []
188+
189+
Object.keys(structured.params || {}).forEach(param => {
190+
// filename might include unicode characters so it is a special case
191+
if (param === 'filename') {
192+
continuationEncode(param, structured.params[param], 50).forEach(function (encodedParam) {
193+
// continuation encoded strings are always escaped, so no need to use enclosing quotes
194+
// in fact using quotes might end up with invalid filenames in some clients
195+
paramsArray.push(encodedParam.key + '=' + encodedParam.value)
196+
})
197+
} else {
198+
paramsArray.push(param + '=' + escapeHeaderArgument(structured.params[param]))
199+
}
200+
})
201+
202+
return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '')
203+
}

0 commit comments

Comments
 (0)