w-email 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/w-email.umd.js +2 -2
- package/dist/w-email.umd.js.map +1 -1
- package/docs/WEmail.mjs.html +389 -156
- package/docs/global.html +88 -17
- package/docs/index.html +1 -1
- package/package.json +4 -3
- package/src/WEmail.mjs +388 -155
- package/test/api-WEmail-smtp.test.mjs +388 -0
- package/test/tools/fakeSmtp.mjs +310 -0
- package/test/tools/genCert.mjs +101 -0
- package/test/unit-WEmail-opt.test.mjs +316 -0
- package/test/all.test.mjs +0 -10
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import tls from 'tls'
|
|
5
|
+
import crypto from 'crypto'
|
|
6
|
+
import { simpleParser } from 'mailparser'
|
|
7
|
+
import WEmail from '../src/WEmail.mjs'
|
|
8
|
+
import { startFakeSmtp, getClosedPort } from './tools/fakeSmtp.mjs'
|
|
9
|
+
import genCert from './tools/genCert.mjs'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
let fdTmp = path.resolve('./test/_tmp/api-WEmail-smtp')
|
|
13
|
+
let fpAtt = path.join(fdTmp, 'att.txt')
|
|
14
|
+
let fpAttZh = path.join(fdTmp, '附件.txt')
|
|
15
|
+
let fpBin = path.join(fdTmp, 'b.bin')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
let sha1 = (b) => {
|
|
19
|
+
return crypto.createHash('sha1').update(b).digest('hex')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
let sleep = (ms) => {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
setTimeout(resolve, ms)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
let settle = (p) => {
|
|
31
|
+
return p.then((value) => {
|
|
32
|
+
return { state: 'fulfilled', value }
|
|
33
|
+
}, (value) => {
|
|
34
|
+
return { state: 'rejected', value }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
let base = {
|
|
40
|
+
srcName: 'test name',
|
|
41
|
+
srcEmail: 'sender@example.com',
|
|
42
|
+
srcPW: 'pw-123',
|
|
43
|
+
emTitle: 'test title',
|
|
44
|
+
emContent: '<div>test content</div>',
|
|
45
|
+
toEmails: 'to1@example.com',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
//send, 啟動假SMTP並以WEmail寄送, 回傳結果、各連線紀錄、含信件之連線與解析後信件
|
|
50
|
+
async function send(opt, srvOpt = {}) {
|
|
51
|
+
let srv = await startFakeSmtp(srvOpt)
|
|
52
|
+
let r
|
|
53
|
+
try {
|
|
54
|
+
r = await settle(new WEmail({ srcHost: '127.0.0.1', srcPort: srv.port, ...opt }))
|
|
55
|
+
await sleep(100) //待客戶端送出QUIT
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await srv.close()
|
|
59
|
+
}
|
|
60
|
+
let sess = srv.sessions.find((s) => s.data !== null) || srv.sessions[0] || null
|
|
61
|
+
let mail = sess && sess.data !== null ? await simpleParser(sess.data) : null
|
|
62
|
+
return { r, sessions: srv.sessions, sess, mail }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
describe('api-WEmail-smtp', function() {
|
|
67
|
+
//spec(src/WEmail.mjs JSDoc): 以srcEmail與srcPW登入srcHost:srcPort寄信; 寄件人顯示名稱為srcName; 收件人、副本、密件為toEmails、toEmailsCC、toEmailsBCC;
|
|
68
|
+
//主旨為emTitle, 內容以html寄送emContent, 附件為emAttachments; resolve回傳郵件伺服器回應字串(部分收件人被拒仍為resolve);
|
|
69
|
+
//寄送失敗時reject Error物件(通常帶code); 輸入檢核失敗時不連線
|
|
70
|
+
|
|
71
|
+
before(function() {
|
|
72
|
+
fs.mkdirSync(fdTmp, { recursive: true })
|
|
73
|
+
fs.writeFileSync(fpAtt, 'hello att 測試附件\nline2\n', 'utf8')
|
|
74
|
+
fs.writeFileSync(fpAttZh, '中文檔名附件內容\n', 'utf8')
|
|
75
|
+
fs.writeFileSync(fpBin, Buffer.from(Array.from({ length: 1024 }, (_, i) => (i * 37) % 256)))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
after(function() {
|
|
79
|
+
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('寄送內容', function() {
|
|
83
|
+
|
|
84
|
+
it('A01: 基本寄送, resolve伺服器回應, 以srcEmail登入, 信封與標頭正確', async function() {
|
|
85
|
+
let { r, sess, mail } = await send(base)
|
|
86
|
+
assert.strict.deepEqual(r, { state: 'fulfilled', value: '250 2.0.0 OK queued' }) //spec: resolve回傳郵件伺服器回應字串
|
|
87
|
+
assert.strict.equal(sess.auth.user, 'sender@example.com')
|
|
88
|
+
assert.strict.equal(sess.auth.pass, 'pw-123')
|
|
89
|
+
assert.strict.equal(sess.mailFrom, 'sender@example.com')
|
|
90
|
+
assert.strict.deepEqual(sess.rcptTo, ['to1@example.com'])
|
|
91
|
+
assert.strict.deepEqual(mail.from.value, [{ address: 'sender@example.com', name: 'test name' }])
|
|
92
|
+
assert.strict.deepEqual(mail.to.value, [{ address: 'to1@example.com', name: '' }])
|
|
93
|
+
assert.strict.equal(mail.subject, 'test title')
|
|
94
|
+
assert.strict.equal(mail.html, '<div>test content</div>')
|
|
95
|
+
assert.strict.deepEqual(mail.attachments, [])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('A02: 真實呼叫端形狀(w-web-sso server/srEmail.mjs), 中文寄件名、主旨與內容', async function() {
|
|
99
|
+
let { r, sess, mail } = await send({
|
|
100
|
+
...base,
|
|
101
|
+
srcName: 'SSO之寄信系統',
|
|
102
|
+
emTitle: '使用者登入通知',
|
|
103
|
+
emContent: '使用者已於時間ooo裝置xxx登入',
|
|
104
|
+
toEmails: ['to1@example.com'],
|
|
105
|
+
})
|
|
106
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
107
|
+
assert.strict.deepEqual(sess.rcptTo, ['to1@example.com'])
|
|
108
|
+
assert.strict.deepEqual(mail.from.value, [{ address: 'sender@example.com', name: 'SSO之寄信系統' }])
|
|
109
|
+
assert.strict.equal(mail.subject, '使用者登入通知')
|
|
110
|
+
assert.strict.equal(mail.html, '使用者已於時間ooo裝置xxx登入')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('A09-A14/NB1/NB2/NB7/NB8/NB9/NB10: 寄件人名稱含特殊字元時原樣保留, 寄件地址仍為srcEmail', async function() {
|
|
114
|
+
let names = ['Acme, Inc.', 'He said "hi"', 'System (test)', 'evil@attacker.com', 'A <x@y.com> B', '系統 通知 🚀', 'Team: Ops', 'a;b', 'a"b', 'C:\\dir\\x', 'a < b', 'a b\tc']
|
|
115
|
+
for (let srcName of names) {
|
|
116
|
+
let { r, sess, mail } = await send({ ...base, srcName })
|
|
117
|
+
assert.strict.equal(r.state, 'fulfilled', srcName)
|
|
118
|
+
assert.strict.equal(sess.mailFrom, 'sender@example.com', srcName) //spec: 寄件地址為srcEmail
|
|
119
|
+
assert.strict.deepEqual(mail.from.value, [{ address: 'sender@example.com', name: srcName }], srcName) //spec: 寄件人顯示名稱為srcName
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('N4/NB3/NB4/NB6: 寄件人名稱之換行轉為空白並去除前後空白, 不產生Bcc標頭', async function() {
|
|
124
|
+
let exps = [
|
|
125
|
+
['a\r\nBcc: evil@example.com', 'a Bcc: evil@example.com'],
|
|
126
|
+
['a\r\nb', 'a b'],
|
|
127
|
+
[' test name ', 'test name'],
|
|
128
|
+
[' ', ''],
|
|
129
|
+
]
|
|
130
|
+
for (let [srcName, name] of exps) {
|
|
131
|
+
let { r, sess, mail } = await send({ ...base, srcName })
|
|
132
|
+
assert.strict.equal(r.state, 'fulfilled', srcName)
|
|
133
|
+
assert.strict.deepEqual(mail.from.value, [{ address: 'sender@example.com', name }], srcName)
|
|
134
|
+
assert.strict.deepEqual(sess.rcptTo, ['to1@example.com'], srcName)
|
|
135
|
+
assert.strict.equal(mail.headers.has('bcc'), false, srcName)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('N0/N1/N5: srcName未給定或為null時顯示名稱為system by w-email, 空字串時不顯示名稱', async function() {
|
|
140
|
+
let exps = [
|
|
141
|
+
[undefined, 'system by w-email'],
|
|
142
|
+
[null, 'system by w-email'],
|
|
143
|
+
['', ''],
|
|
144
|
+
]
|
|
145
|
+
for (let [srcName, name] of exps) {
|
|
146
|
+
let { r, mail } = await send({ ...base, srcName })
|
|
147
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
148
|
+
assert.strict.deepEqual(mail.from.value, [{ address: 'sender@example.com', name }], String(srcName)) //spec: 預設'system by w-email', 空字串則不顯示名稱
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('S0/S1/S4/K0/C1: emTitle與emContent未給定、為null或空字串時, 信件無主旨與內容', async function() {
|
|
153
|
+
for (let v of [undefined, null, '']) {
|
|
154
|
+
let { r, mail } = await send({ ...base, emTitle: v, emContent: v })
|
|
155
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
156
|
+
assert.strict.equal(mail.subject || '', '', String(v)) //spec: emTitle預設''
|
|
157
|
+
assert.strict.equal(mail.html || '', '', String(v)) //spec: emContent預設''
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('N2/S2/C2/C4/WB1: 文字欄位之數字轉為字串, emContent可為Buffer', async function() {
|
|
162
|
+
let r1 = await send({ ...base, srcName: 123, emTitle: 456, emContent: 789, srcPW: 0 })
|
|
163
|
+
assert.strict.equal(r1.r.state, 'fulfilled')
|
|
164
|
+
assert.strict.deepEqual(r1.mail.from.value, [{ address: 'sender@example.com', name: '123' }])
|
|
165
|
+
assert.strict.equal(r1.mail.subject, '456')
|
|
166
|
+
assert.strict.equal(r1.mail.html, '789')
|
|
167
|
+
assert.strict.equal(r1.sess.auth.pass, '0')
|
|
168
|
+
let r2 = await send({ ...base, emContent: Buffer.from('<p>buffer body</p>', 'utf8') })
|
|
169
|
+
assert.strict.equal(r2.r.state, 'fulfilled')
|
|
170
|
+
assert.strict.equal(r2.mail.html, '<p>buffer body</p>')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('收件人', function() {
|
|
176
|
+
|
|
177
|
+
it('A05/A06: 收件人、副本、密件皆在信封內, 標頭不含Bcc', async function() {
|
|
178
|
+
let { r, sess, mail } = await send({
|
|
179
|
+
...base,
|
|
180
|
+
toEmails: ['to1@example.com', 'to2@example.com'],
|
|
181
|
+
toEmailsCC: 'cc1@example.com',
|
|
182
|
+
toEmailsBCC: ['bcc1@example.com', 'bcc2@example.com'],
|
|
183
|
+
})
|
|
184
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
185
|
+
assert.strict.deepEqual(sess.rcptTo, ['to1@example.com', 'to2@example.com', 'cc1@example.com', 'bcc1@example.com', 'bcc2@example.com'])
|
|
186
|
+
assert.strict.deepEqual(mail.to.value.map((v) => v.address), ['to1@example.com', 'to2@example.com'])
|
|
187
|
+
assert.strict.deepEqual(mail.cc.value.map((v) => v.address), ['cc1@example.com'])
|
|
188
|
+
assert.strict.equal(mail.headers.has('bcc'), false) //spec: 密件副本不得出現於信件標頭
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('T9: 只有密件收件人時可寄送', async function() {
|
|
192
|
+
let { r, sess, mail } = await send({ ...base, toEmails: '', toEmailsBCC: 'bcc1@example.com' })
|
|
193
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
194
|
+
assert.strict.deepEqual(sess.rcptTo, ['bcc1@example.com'])
|
|
195
|
+
assert.strict.equal(mail.headers.has('bcc'), false)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('T4/T5/T8/A30/T12: 收件人逐筆去除前後空白, 略過null與空字串', async function() {
|
|
199
|
+
let { r, sess, mail } = await send({
|
|
200
|
+
...base,
|
|
201
|
+
toEmails: [' a@example.com ', null, '', ' To1@Example.COM '],
|
|
202
|
+
toEmailsCC: '',
|
|
203
|
+
toEmailsBCC: null,
|
|
204
|
+
})
|
|
205
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
206
|
+
assert.strict.deepEqual(sess.rcptTo, ['a@example.com', 'To1@example.com']) //網域由nodemailer轉小寫
|
|
207
|
+
assert.strict.deepEqual(mail.to.value.map((v) => v.address), ['a@example.com', 'To1@example.com'])
|
|
208
|
+
assert.strict.equal(mail.cc, undefined)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('A29/E2: 國際化網域轉為punycode寄送, srcEmail去除前後空白後寄送與登入', async function() {
|
|
212
|
+
let { r, sess } = await send({ ...base, srcEmail: ' sender@example.com ', toEmails: 'user@例子.測試' })
|
|
213
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
214
|
+
assert.strict.deepEqual(sess.rcptTo, ['user@xn--fsqu00a.xn--g6w251d']) //spec: 國際化網域轉為punycode
|
|
215
|
+
let header = sess.data.toString('utf8').split('\r\n\r\n')[0]
|
|
216
|
+
assert.strict.match(header, /^To: user@xn--fsqu00a\.xn--g6w251d$/m) //標頭與信封一致; mailparser解析時會把punycode轉回Unicode, 故以原始標頭斷言
|
|
217
|
+
assert.strict.equal(sess.mailFrom, 'sender@example.com')
|
|
218
|
+
assert.strict.equal(sess.auth.user, 'sender@example.com')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('A33: 收件人檢核失敗時不建立連線', async function() {
|
|
222
|
+
let { r, sessions } = await send({ ...base, toEmails: 'x@example.com\r\nRCPT TO:<evil@example.com>' })
|
|
223
|
+
assert.strict.deepEqual(r, { state: 'rejected', value: 'toEmails is not email' }) //spec: 輸入檢核失敗時為錯誤訊息字串
|
|
224
|
+
assert.strict.equal(sessions.length, 0) //spec: 不連線即reject
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('附件', function() {
|
|
230
|
+
|
|
231
|
+
it('A19/A20/A22: 附件以path(含中文檔名與二進位內容)或content給予, 檔名與內容正確', async function() {
|
|
232
|
+
let { r, mail } = await send({
|
|
233
|
+
...base,
|
|
234
|
+
emAttachments: [
|
|
235
|
+
{ filename: 'att.txt', path: fpAtt },
|
|
236
|
+
{ filename: '附件.txt', path: fpAttZh },
|
|
237
|
+
{ filename: 'b.bin', path: fpBin },
|
|
238
|
+
{ filename: 'inline.txt', content: '文字內容' },
|
|
239
|
+
],
|
|
240
|
+
})
|
|
241
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
242
|
+
assert.strict.deepEqual(mail.attachments.map((a) => [a.filename, sha1(a.content)]), [
|
|
243
|
+
['att.txt', sha1(fs.readFileSync(fpAtt))],
|
|
244
|
+
['附件.txt', sha1(fs.readFileSync(fpAttZh))],
|
|
245
|
+
['b.bin', sha1(fs.readFileSync(fpBin))],
|
|
246
|
+
['inline.txt', sha1(Buffer.from('文字內容', 'utf8'))],
|
|
247
|
+
])
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('M1/M13/M14/MB1-MB3/M5/MB9/M8/R08b: 附件為falsy時無附件, 陣列內falsy略過, raw與filename為false可寄送', async function() {
|
|
251
|
+
for (let emAttachments of [null, false, '', 0]) {
|
|
252
|
+
let { r, mail } = await send({ ...base, emAttachments })
|
|
253
|
+
assert.strict.equal(r.state, 'fulfilled', String(emAttachments))
|
|
254
|
+
assert.strict.deepEqual(mail.attachments, [], String(emAttachments))
|
|
255
|
+
}
|
|
256
|
+
let { r, mail } = await send({
|
|
257
|
+
...base,
|
|
258
|
+
emAttachments: [
|
|
259
|
+
null,
|
|
260
|
+
'',
|
|
261
|
+
{ filename: 'n.txt', content: 'n' },
|
|
262
|
+
{ raw: 'Content-Type: text/plain; name="r.txt"\r\nContent-Disposition: attachment; filename="r.txt"\r\n\r\nraw-body' },
|
|
263
|
+
{ filename: false, content: 'no-name' },
|
|
264
|
+
],
|
|
265
|
+
})
|
|
266
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
267
|
+
assert.strict.deepEqual(mail.attachments.map((a) => a.content.toString('utf8')), ['n', 'raw-body', 'no-name'])
|
|
268
|
+
assert.strict.deepEqual(mail.attachments.map((a) => a.filename), ['n.txt', 'r.txt', undefined])
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('A23: 附件path不存在, reject Error', async function() {
|
|
272
|
+
let { r } = await send({ ...base, emAttachments: { filename: 'x.txt', path: path.join(fdTmp, 'not-exist.txt') } })
|
|
273
|
+
assert.strict.equal(r.state, 'rejected')
|
|
274
|
+
assert.strict.ok(r.value instanceof Error)
|
|
275
|
+
assert.strict.equal(r.value.code, 'ESTREAM')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('伺服器回應與錯誤', function() {
|
|
281
|
+
|
|
282
|
+
it('B01: 伺服器拒絕登入, reject Error EAUTH', async function() {
|
|
283
|
+
let { r } = await send(base, { auth: 'reject' })
|
|
284
|
+
assert.strict.ok(r.value instanceof Error)
|
|
285
|
+
assert.strict.equal(r.value.code, 'EAUTH')
|
|
286
|
+
assert.strict.equal(r.value.responseCode, 535)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('B02: 唯一收件人被拒, reject Error EENVELOPE', async function() {
|
|
290
|
+
let { r } = await send(base, { rcptReject: ['to1@example.com'] })
|
|
291
|
+
assert.strict.ok(r.value instanceof Error)
|
|
292
|
+
assert.strict.equal(r.value.code, 'EENVELOPE')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('B03: 兩位收件人其一被拒, 仍resolve', async function() {
|
|
296
|
+
let { r, sess } = await send({ ...base, toEmails: ['a@example.com', 'b@example.com'] }, { rcptReject: ['b@example.com'] })
|
|
297
|
+
assert.strict.deepEqual(r, { state: 'fulfilled', value: '250 2.0.0 OK queued' }) //spec: 部分收件人被伺服器拒收時仍為resolve
|
|
298
|
+
assert.strict.deepEqual(sess.rcptTo, ['a@example.com', 'b@example.com'])
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('B04: 信件內容被拒, reject Error EMESSAGE', async function() {
|
|
302
|
+
let { r } = await send(base, { dataReject: true })
|
|
303
|
+
assert.strict.ok(r.value instanceof Error)
|
|
304
|
+
assert.strict.equal(r.value.code, 'EMESSAGE')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('B05: 連不上郵件伺服器, reject Error ESOCKET', async function() {
|
|
308
|
+
let port = await getClosedPort()
|
|
309
|
+
let r = await settle(new WEmail({ ...base, srcHost: '127.0.0.1', srcPort: port }))
|
|
310
|
+
assert.strict.ok(r.value instanceof Error)
|
|
311
|
+
assert.strict.equal(r.value.code, 'ESOCKET')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('B06/B07/B08: 伺服器於連線時、MAIL FROM時或信件傳送中斷線, reject Error ECONNECTION', async function() {
|
|
315
|
+
for (let dropAt of ['connect', 'mailfrom', 'middata']) {
|
|
316
|
+
let { r } = await send(base, { dropAt })
|
|
317
|
+
assert.strict.ok(r.value instanceof Error, dropAt)
|
|
318
|
+
assert.strict.equal(r.value.code, 'ECONNECTION', dropAt)
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('B09: 伺服器僅提供XOAUTH2, reject Error EAUTH', async function() {
|
|
323
|
+
let { r } = await send(base, { auth: 'xoauth2only' })
|
|
324
|
+
assert.strict.ok(r.value instanceof Error)
|
|
325
|
+
assert.strict.equal(r.value.code, 'EAUTH')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('W1/B10: 伺服器未要求登入且srcPW為空字串, 不登入即寄送', async function() {
|
|
329
|
+
let { r, sess } = await send({ ...base, srcPW: '' }, { auth: 'none' })
|
|
330
|
+
assert.strict.equal(r.state, 'fulfilled') //spec: 郵件伺服器未要求登入時srcPW可為空字串
|
|
331
|
+
assert.strict.equal(sess.auth, null)
|
|
332
|
+
assert.strict.deepEqual(sess.rcptTo, ['to1@example.com'])
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('STARTTLS', function() {
|
|
338
|
+
//spec: 465以外之連接埠於伺服器支援時使用STARTTLS; 伺服器未宣告STARTTLS時以明文連線與登入
|
|
339
|
+
//測試憑證於執行時以Node內建crypto產生(test/tools/genCert.mjs), 並暫時加入預設信任憑證, 每案後還原
|
|
340
|
+
|
|
341
|
+
let cert = null
|
|
342
|
+
let caOri = null
|
|
343
|
+
|
|
344
|
+
before(function() {
|
|
345
|
+
if (typeof tls.setDefaultCACertificates !== 'function' || typeof tls.getCACertificates !== 'function') {
|
|
346
|
+
this.skip() //Node版本不支援暫時信任憑證
|
|
347
|
+
}
|
|
348
|
+
cert = genCert()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
afterEach(function() {
|
|
352
|
+
if (caOri !== null) {
|
|
353
|
+
tls.setDefaultCACertificates(caOri)
|
|
354
|
+
caOri = null
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('TLS1: 伺服器宣告STARTTLS且憑證受信任, 升級TLS後才登入並寄送', async function() {
|
|
359
|
+
caOri = tls.getCACertificates('default')
|
|
360
|
+
tls.setDefaultCACertificates([...caOri, cert.cert])
|
|
361
|
+
let { r, sess, mail } = await send(base, { tls: cert })
|
|
362
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
363
|
+
assert.strict.equal(sess.tls, true)
|
|
364
|
+
let iTls = sess.cmds.indexOf('STARTTLS')
|
|
365
|
+
let iAuth = sess.cmds.findIndex((c) => c.startsWith('AUTH '))
|
|
366
|
+
assert.strict.ok(iTls >= 0 && iAuth > iTls) //帳密於升級TLS後才送出
|
|
367
|
+
assert.strict.equal(sess.auth.pass, 'pw-123')
|
|
368
|
+
assert.strict.equal(mail.subject, 'test title')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('TLS2: 憑證不受信任時reject Error ESOCKET, 且未送出帳密', async function() {
|
|
372
|
+
let { r, sess } = await send(base, { tls: cert })
|
|
373
|
+
assert.strict.ok(r.value instanceof Error)
|
|
374
|
+
assert.strict.equal(r.value.code, 'ESOCKET')
|
|
375
|
+
assert.strict.equal(sess.auth, null)
|
|
376
|
+
assert.strict.equal(sess.cmds.includes('STARTTLS'), true)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('TLS3: 伺服器未宣告STARTTLS時以明文登入(已知限制)', async function() {
|
|
380
|
+
let { r, sess } = await send(base)
|
|
381
|
+
assert.strict.equal(r.state, 'fulfilled')
|
|
382
|
+
assert.strict.equal(sess.tls, false)
|
|
383
|
+
assert.strict.equal(sess.auth.pass, 'pw-123')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
})
|