Skip to content

Commit b7f7fc8

Browse files
committed
ZMS-47: Fix utf-8 header round-trip in Headers.add()
1 parent b4490c4 commit b7f7fc8

2 files changed

Lines changed: 74 additions & 6 deletions

File tree

lib/headers.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class Headers {
4242
this._parseHeaders();
4343
}
4444
key = this._normalizeHeader(key);
45-
let lines = this.lines.filter(line => line.key === key).map(line => line.line);
45+
let lines = this.lines.filter(line => line.key === key).map(line => this._decodeHeaderValue(line.line));
4646

4747
return lines;
4848
}
@@ -62,7 +62,7 @@ class Headers {
6262
if (!header) {
6363
return '';
6464
}
65-
return ((this.libmime.decodeHeader(header.line) || {}).value || '').toString().trim();
65+
return ((this.libmime.decodeHeader(this._decodeHeaderValue(header.line)) || {}).value || '').toString().trim();
6666
}
6767

6868
getList() {
@@ -174,17 +174,27 @@ class Headers {
174174

175175
lineEnd = lineEnd || '\r\n';
176176

177-
let headers = this.lines.map(line => line.line.replace(/\r?\n/g, lineEnd)).join(lineEnd) + `${lineEnd}${lineEnd}`;
177+
let headers = this.lines
178+
.map(line => this._buildHeaderLine(line.line.replace(/\r?\n/g, lineEnd)))
179+
.reduce((joined, line, idx) => {
180+
if (idx) {
181+
joined.push(Buffer.from(lineEnd, 'binary'));
182+
}
183+
joined.push(line);
184+
return joined;
185+
}, []);
186+
187+
headers.push(Buffer.from(lineEnd + lineEnd, 'binary'));
178188

179189
if (this.mbox) {
180-
headers = this.mbox + lineEnd + headers;
190+
headers.unshift(Buffer.from(this.mbox + lineEnd, 'binary'));
181191
}
182192

183193
if (this.http) {
184-
headers = this.http + lineEnd + headers;
194+
headers.unshift(Buffer.from(this.http + lineEnd, 'binary'));
185195
}
186196

187-
return Buffer.from(headers, 'binary');
197+
return Buffer.concat(headers);
188198
}
189199

190200
_normalizeHeader(key) {
@@ -232,6 +242,20 @@ class Headers {
232242
this.lines = lines;
233243
this.parsed = true;
234244
}
245+
246+
_buildHeaderLine(line) {
247+
let value = this._decodeHeaderValue(line);
248+
return Buffer.from(value, value === line ? 'binary' : 'utf8');
249+
}
250+
251+
_decodeHeaderValue(str) {
252+
if (!str) {
253+
return str;
254+
}
255+
256+
let utf8 = Buffer.from(str, 'binary').toString('utf8');
257+
return utf8.includes('\uFFFD') ? str : utf8;
258+
}
235259
}
236260

237261
// expose to the world

test/headers-test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,47 @@ module.exports['Replace header at relative key index'] = test => {
130130
test.equal(headers.build().toString(), generatedHeaderStr);
131131
test.done();
132132
};
133+
134+
module.exports['Preserve SMTPUTF8 mailbox local part'] = test => {
135+
let value = 'öäöä@wildduck.xn--4caaa.test';
136+
let headers = new Headers();
137+
headers.add('From', value);
138+
139+
test.equal(headers.get('from')[0], `From: ${value}`);
140+
test.equal(headers.getDecoded('from')[0].value, value);
141+
test.equal(headers.build().toString(), `From: ${value}\r\n\r\n`);
142+
test.done();
143+
};
144+
145+
module.exports['Preserve SMTPUTF8 mailbox with UTF-8 domain'] = test => {
146+
let value = 'öäöä@wildduck.äää.test';
147+
let headers = new Headers();
148+
headers.add('From', value);
149+
150+
test.equal(headers.get('from')[0], `From: ${value}`);
151+
test.equal(headers.getDecoded('from')[0].value, value);
152+
test.equal(headers.build().toString(), `From: ${value}\r\n\r\n`);
153+
test.done();
154+
};
155+
156+
module.exports['Preserve UTF-8 display name and mailbox'] = test => {
157+
let value = '"Jöhn Dœ" <öäöä@wildduck.xn--4caaa.test>';
158+
let headers = new Headers();
159+
headers.add('From', value);
160+
161+
test.equal(headers.get('from')[0], `From: ${value}`);
162+
test.equal(headers.getDecoded('from')[0].value, value);
163+
test.equal(headers.build().toString(), `From: ${value}\r\n\r\n`);
164+
test.done();
165+
};
166+
167+
module.exports['Preserve ASCII address round-trip'] = test => {
168+
let value = 'ascii@example.com';
169+
let headers = new Headers();
170+
headers.add('From', value);
171+
172+
test.equal(headers.get('from')[0], `From: ${value}`);
173+
test.equal(headers.getDecoded('from')[0].value, value);
174+
test.equal(headers.build().toString(), `From: ${value}\r\n\r\n`);
175+
test.done();
176+
};

0 commit comments

Comments
 (0)