zele 0.3.17 → 0.3.21
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/README.md +81 -12
- package/dist/api-utils.d.ts +10 -0
- package/dist/api-utils.js +14 -0
- package/dist/api-utils.js.map +1 -1
- package/dist/cli-types.d.ts +4 -0
- package/dist/cli-types.js +6 -0
- package/dist/cli-types.js.map +1 -0
- package/dist/cli.js +1 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.d.ts +2 -2
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.d.ts +2 -2
- package/dist/commands/auth-cmd.js +58 -52
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.d.ts +2 -2
- package/dist/commands/calendar.js +13 -14
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.d.ts +2 -2
- package/dist/commands/draft.js +62 -15
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/filter.d.ts +2 -2
- package/dist/commands/filter.js.map +1 -1
- package/dist/commands/label.d.ts +2 -2
- package/dist/commands/label.js +5 -6
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail-actions.d.ts +2 -2
- package/dist/commands/mail-actions.js +290 -1
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.d.ts +2 -2
- package/dist/commands/mail.js +50 -10
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/profile.d.ts +2 -2
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/watch.d.ts +2 -2
- package/dist/commands/watch.js +2 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/gmail-client.d.ts +59 -3
- package/dist/gmail-client.js +119 -5
- package/dist/gmail-client.js.map +1 -1
- package/dist/imap-smtp-client.d.ts +75 -4
- package/dist/imap-smtp-client.js +131 -7
- package/dist/imap-smtp-client.js.map +1 -1
- package/dist/unsubscribe.d.ts +76 -0
- package/dist/unsubscribe.js +224 -0
- package/dist/unsubscribe.js.map +1 -0
- package/package.json +3 -2
- package/skills/zele/SKILL.md +32 -124
- package/src/api-utils.ts +14 -0
- package/src/cli-types.ts +8 -0
- package/src/cli.ts +2 -7
- package/src/commands/attachment.ts +2 -2
- package/src/commands/auth-cmd.ts +66 -56
- package/src/commands/calendar.ts +15 -16
- package/src/commands/draft.ts +71 -17
- package/src/commands/filter.ts +2 -2
- package/src/commands/label.ts +7 -8
- package/src/commands/mail-actions.ts +315 -4
- package/src/commands/mail.ts +54 -12
- package/src/commands/profile.ts +2 -2
- package/src/commands/watch.ts +4 -4
- package/src/gmail-client.ts +193 -6
- package/src/imap-smtp-client.ts +186 -7
- package/src/unsubscribe.test.ts +487 -0
- package/src/unsubscribe.ts +255 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// Tests for List-Unsubscribe / List-Unsubscribe-Post parsing and planning.
|
|
2
|
+
// Covers RFC 2369 header parsing, RFC 6068 mailto: decoding, and RFC 8058
|
|
3
|
+
// one-click detection. All tests are pure: no network, no mocks, no fixtures.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
parseListUnsubscribeEntries,
|
|
8
|
+
parseMailto,
|
|
9
|
+
parseListUnsubscribePost,
|
|
10
|
+
planUnsubscribe,
|
|
11
|
+
hasUnsubscribeMechanism,
|
|
12
|
+
hasOneClickUnsubscribe,
|
|
13
|
+
} from './unsubscribe.js'
|
|
14
|
+
|
|
15
|
+
describe('parseListUnsubscribeEntries', () => {
|
|
16
|
+
test('single mailto', () => {
|
|
17
|
+
expect(parseListUnsubscribeEntries('<mailto:unsub@example.com>')).toMatchInlineSnapshot(`
|
|
18
|
+
[
|
|
19
|
+
"mailto:unsub@example.com",
|
|
20
|
+
]
|
|
21
|
+
`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('single https', () => {
|
|
25
|
+
expect(parseListUnsubscribeEntries('<https://example.com/u/abc>')).toMatchInlineSnapshot(`
|
|
26
|
+
[
|
|
27
|
+
"https://example.com/u/abc",
|
|
28
|
+
]
|
|
29
|
+
`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('RFC 8058 §8.2 complex example', () => {
|
|
33
|
+
const header = `<mailto:listrequest@example.com?subject=unsubscribe>,
|
|
34
|
+
<https://example.com/unsubscribe.html?opaque=123456789>`
|
|
35
|
+
expect(parseListUnsubscribeEntries(header)).toMatchInlineSnapshot(`
|
|
36
|
+
[
|
|
37
|
+
"mailto:listrequest@example.com?subject=unsubscribe",
|
|
38
|
+
"https://example.com/unsubscribe.html?opaque=123456789",
|
|
39
|
+
]
|
|
40
|
+
`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('header with internal whitespace is tolerated', () => {
|
|
44
|
+
const header = '<mailto:\n a@b.com\n >, <https://c.example/x>'
|
|
45
|
+
expect(parseListUnsubscribeEntries(header)).toMatchInlineSnapshot(`
|
|
46
|
+
[
|
|
47
|
+
"mailto:a@b.com",
|
|
48
|
+
"https://c.example/x",
|
|
49
|
+
]
|
|
50
|
+
`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('malformed entries without angle brackets are dropped', () => {
|
|
54
|
+
expect(parseListUnsubscribeEntries('mailto:a@b.com, <https://c/x>')).toMatchInlineSnapshot(`
|
|
55
|
+
[
|
|
56
|
+
"https://c/x",
|
|
57
|
+
]
|
|
58
|
+
`)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('empty header returns empty', () => {
|
|
62
|
+
expect(parseListUnsubscribeEntries('')).toMatchInlineSnapshot(`[]`)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('comma inside mailto query does not split', () => {
|
|
66
|
+
const header = '<mailto:a@b.com?cc=x@y.com,z@w.com&subject=bye>'
|
|
67
|
+
expect(parseListUnsubscribeEntries(header)).toMatchInlineSnapshot(`
|
|
68
|
+
[
|
|
69
|
+
"mailto:a@b.com?cc=x@y.com,z@w.com&subject=bye",
|
|
70
|
+
]
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('parseMailto', () => {
|
|
76
|
+
test('plain mailto', () => {
|
|
77
|
+
expect(parseMailto('mailto:unsub@example.com')).toMatchInlineSnapshot(`
|
|
78
|
+
{
|
|
79
|
+
"to": "unsub@example.com",
|
|
80
|
+
}
|
|
81
|
+
`)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('mailto with subject', () => {
|
|
85
|
+
expect(parseMailto('mailto:list@x.com?subject=unsubscribe')).toMatchInlineSnapshot(`
|
|
86
|
+
{
|
|
87
|
+
"subject": "unsubscribe",
|
|
88
|
+
"to": "list@x.com",
|
|
89
|
+
}
|
|
90
|
+
`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('mailto with percent-encoded subject and body', () => {
|
|
94
|
+
expect(
|
|
95
|
+
parseMailto('mailto:list@x.com?subject=please%20remove&body=unsubscribe%20me'),
|
|
96
|
+
).toMatchInlineSnapshot(`
|
|
97
|
+
{
|
|
98
|
+
"body": "unsubscribe me",
|
|
99
|
+
"subject": "please remove",
|
|
100
|
+
"to": "list@x.com",
|
|
101
|
+
}
|
|
102
|
+
`)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('mailto with cc list', () => {
|
|
106
|
+
expect(parseMailto('mailto:a@b.com?cc=c@d.com,e@f.com')).toMatchInlineSnapshot(`
|
|
107
|
+
{
|
|
108
|
+
"cc": [
|
|
109
|
+
"c@d.com",
|
|
110
|
+
"e@f.com",
|
|
111
|
+
],
|
|
112
|
+
"to": "a@b.com",
|
|
113
|
+
}
|
|
114
|
+
`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('mailto preserves + literally (RFC 6068 is not form-urlencoded)', () => {
|
|
118
|
+
// `+` must NOT become space — this matters for plus-addressing and for
|
|
119
|
+
// subjects containing "C++" etc.
|
|
120
|
+
expect(
|
|
121
|
+
parseMailto('mailto:list@x.com?subject=please+remove&cc=foo+tag@x.com'),
|
|
122
|
+
).toMatchInlineSnapshot(`
|
|
123
|
+
{
|
|
124
|
+
"cc": [
|
|
125
|
+
"foo+tag@x.com",
|
|
126
|
+
],
|
|
127
|
+
"subject": "please+remove",
|
|
128
|
+
"to": "list@x.com",
|
|
129
|
+
}
|
|
130
|
+
`)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('mailto strips CRLF from headers', () => {
|
|
134
|
+
// Defense-in-depth: strip \r and \n from to, subject, cc. Body is left
|
|
135
|
+
// alone because it legitimately contains newlines.
|
|
136
|
+
expect(
|
|
137
|
+
parseMailto('mailto:a@b.com?subject=hi%0D%0ABcc:%20evil@c.com&body=line1%0Aline2'),
|
|
138
|
+
).toMatchInlineSnapshot(`
|
|
139
|
+
{
|
|
140
|
+
"body": "line1
|
|
141
|
+
line2",
|
|
142
|
+
"subject": "hiBcc: evil@c.com",
|
|
143
|
+
"to": "a@b.com",
|
|
144
|
+
}
|
|
145
|
+
`)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('non-mailto returns null', () => {
|
|
149
|
+
expect(parseMailto('https://example.com')).toMatchInlineSnapshot(`null`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('mailto without target returns null', () => {
|
|
153
|
+
expect(parseMailto('mailto:')).toMatchInlineSnapshot(`null`)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('parseListUnsubscribePost', () => {
|
|
158
|
+
test('canonical value', () => {
|
|
159
|
+
expect(parseListUnsubscribePost('List-Unsubscribe=One-Click')).toMatchInlineSnapshot(`true`)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('extra whitespace tolerated', () => {
|
|
163
|
+
expect(parseListUnsubscribePost(' List-Unsubscribe = One-Click ')).toMatchInlineSnapshot(
|
|
164
|
+
`true`,
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('case insensitive', () => {
|
|
169
|
+
expect(parseListUnsubscribePost('list-unsubscribe=one-click')).toMatchInlineSnapshot(`true`)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('wrong value', () => {
|
|
173
|
+
expect(parseListUnsubscribePost('List-Unsubscribe=Two-Click')).toMatchInlineSnapshot(`false`)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('undefined', () => {
|
|
177
|
+
expect(parseListUnsubscribePost(undefined)).toMatchInlineSnapshot(`false`)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('planUnsubscribe', () => {
|
|
182
|
+
test('RFC 8058 §8.1 simple one-click', () => {
|
|
183
|
+
const plan = planUnsubscribe({
|
|
184
|
+
listUnsubscribe: '<https://example.com/unsubscribe/opaquepart>',
|
|
185
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
186
|
+
dkimAuthentic: true,
|
|
187
|
+
})
|
|
188
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
189
|
+
{
|
|
190
|
+
"dkimAuthentic": true,
|
|
191
|
+
"hasOneClick": true,
|
|
192
|
+
"mechanisms": [
|
|
193
|
+
{
|
|
194
|
+
"kind": "one-click",
|
|
195
|
+
"url": "https://example.com/unsubscribe/opaquepart",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
"warnings": [],
|
|
199
|
+
}
|
|
200
|
+
`)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('RFC 8058 §8.2 complex: one-click + mailto', () => {
|
|
204
|
+
const plan = planUnsubscribe({
|
|
205
|
+
listUnsubscribe: `<mailto:listrequest@example.com?subject=unsubscribe>,
|
|
206
|
+
<https://example.com/unsubscribe.html?opaque=123456789>`,
|
|
207
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
208
|
+
dkimAuthentic: true,
|
|
209
|
+
})
|
|
210
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
211
|
+
{
|
|
212
|
+
"dkimAuthentic": true,
|
|
213
|
+
"hasOneClick": true,
|
|
214
|
+
"mechanisms": [
|
|
215
|
+
{
|
|
216
|
+
"kind": "one-click",
|
|
217
|
+
"url": "https://example.com/unsubscribe.html?opaque=123456789",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"kind": "mailto",
|
|
221
|
+
"mailto": {
|
|
222
|
+
"subject": "unsubscribe",
|
|
223
|
+
"to": "listrequest@example.com",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
"warnings": [],
|
|
228
|
+
}
|
|
229
|
+
`)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('mailto only (no one-click)', () => {
|
|
233
|
+
const plan = planUnsubscribe({
|
|
234
|
+
listUnsubscribe: '<mailto:unsub@list.example.com?subject=unsubscribe>',
|
|
235
|
+
listUnsubscribePost: undefined,
|
|
236
|
+
dkimAuthentic: true,
|
|
237
|
+
})
|
|
238
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
239
|
+
{
|
|
240
|
+
"dkimAuthentic": true,
|
|
241
|
+
"hasOneClick": false,
|
|
242
|
+
"mechanisms": [
|
|
243
|
+
{
|
|
244
|
+
"kind": "mailto",
|
|
245
|
+
"mailto": {
|
|
246
|
+
"subject": "unsubscribe",
|
|
247
|
+
"to": "unsub@list.example.com",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
"warnings": [],
|
|
252
|
+
}
|
|
253
|
+
`)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('legacy https landing page only (no Post header)', () => {
|
|
257
|
+
const plan = planUnsubscribe({
|
|
258
|
+
listUnsubscribe: '<https://example.com/unsubscribe?id=abc>',
|
|
259
|
+
listUnsubscribePost: undefined,
|
|
260
|
+
dkimAuthentic: true,
|
|
261
|
+
})
|
|
262
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
263
|
+
{
|
|
264
|
+
"dkimAuthentic": true,
|
|
265
|
+
"hasOneClick": false,
|
|
266
|
+
"mechanisms": [
|
|
267
|
+
{
|
|
268
|
+
"kind": "url",
|
|
269
|
+
"url": "https://example.com/unsubscribe?id=abc",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
"warnings": [],
|
|
273
|
+
}
|
|
274
|
+
`)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('no headers at all → empty plan', () => {
|
|
278
|
+
const plan = planUnsubscribe({
|
|
279
|
+
listUnsubscribe: undefined,
|
|
280
|
+
listUnsubscribePost: undefined,
|
|
281
|
+
dkimAuthentic: true,
|
|
282
|
+
})
|
|
283
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
284
|
+
{
|
|
285
|
+
"dkimAuthentic": true,
|
|
286
|
+
"hasOneClick": false,
|
|
287
|
+
"mechanisms": [],
|
|
288
|
+
"warnings": [],
|
|
289
|
+
}
|
|
290
|
+
`)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('DKIM failed with one-click emits warning', () => {
|
|
294
|
+
const plan = planUnsubscribe({
|
|
295
|
+
listUnsubscribe: '<https://example.com/u/abc>',
|
|
296
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
297
|
+
dkimAuthentic: false,
|
|
298
|
+
})
|
|
299
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
300
|
+
{
|
|
301
|
+
"dkimAuthentic": false,
|
|
302
|
+
"hasOneClick": true,
|
|
303
|
+
"mechanisms": [
|
|
304
|
+
{
|
|
305
|
+
"kind": "one-click",
|
|
306
|
+
"url": "https://example.com/u/abc",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
"warnings": [
|
|
310
|
+
"DKIM did not pass; one-click may be spoofed by an attacker",
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
`)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('DKIM unknown with one-click emits warning', () => {
|
|
317
|
+
const plan = planUnsubscribe({
|
|
318
|
+
listUnsubscribe: '<https://example.com/u/abc>',
|
|
319
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
320
|
+
dkimAuthentic: null,
|
|
321
|
+
})
|
|
322
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
323
|
+
{
|
|
324
|
+
"dkimAuthentic": null,
|
|
325
|
+
"hasOneClick": true,
|
|
326
|
+
"mechanisms": [
|
|
327
|
+
{
|
|
328
|
+
"kind": "one-click",
|
|
329
|
+
"url": "https://example.com/u/abc",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
"warnings": [
|
|
333
|
+
"DKIM status unknown (no authentication info on this message)",
|
|
334
|
+
],
|
|
335
|
+
}
|
|
336
|
+
`)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('one-click Post header present but only http URL (no https)', () => {
|
|
340
|
+
const plan = planUnsubscribe({
|
|
341
|
+
listUnsubscribe: '<http://example.com/u/abc>',
|
|
342
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
343
|
+
dkimAuthentic: true,
|
|
344
|
+
})
|
|
345
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
346
|
+
{
|
|
347
|
+
"dkimAuthentic": true,
|
|
348
|
+
"hasOneClick": false,
|
|
349
|
+
"mechanisms": [
|
|
350
|
+
{
|
|
351
|
+
"kind": "url",
|
|
352
|
+
"url": "http://example.com/u/abc",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
"warnings": [
|
|
356
|
+
"List-Unsubscribe-Post is present but no https URL (only http); RFC 8058 requires https",
|
|
357
|
+
],
|
|
358
|
+
}
|
|
359
|
+
`)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('one-click with multiple https URLs (all are candidates)', () => {
|
|
363
|
+
const plan = planUnsubscribe({
|
|
364
|
+
listUnsubscribe: '<https://a.example/u>, <https://b.example/u>',
|
|
365
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click',
|
|
366
|
+
dkimAuthentic: true,
|
|
367
|
+
})
|
|
368
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
369
|
+
{
|
|
370
|
+
"dkimAuthentic": true,
|
|
371
|
+
"hasOneClick": true,
|
|
372
|
+
"mechanisms": [
|
|
373
|
+
{
|
|
374
|
+
"kind": "one-click",
|
|
375
|
+
"url": "https://a.example/u",
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"kind": "one-click",
|
|
379
|
+
"url": "https://b.example/u",
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
"warnings": [],
|
|
383
|
+
}
|
|
384
|
+
`)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('legacy fallback preserves sender order: https before mailto', () => {
|
|
388
|
+
// Without List-Unsubscribe-Post the sender's declared order wins
|
|
389
|
+
// (RFC 2369 §2 left-to-right preference). Make sure we emit https
|
|
390
|
+
// first when it comes first in the header.
|
|
391
|
+
const plan = planUnsubscribe({
|
|
392
|
+
listUnsubscribe: '<https://x.example/u>, <mailto:y@z.example>',
|
|
393
|
+
listUnsubscribePost: undefined,
|
|
394
|
+
dkimAuthentic: true,
|
|
395
|
+
})
|
|
396
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
397
|
+
{
|
|
398
|
+
"dkimAuthentic": true,
|
|
399
|
+
"hasOneClick": false,
|
|
400
|
+
"mechanisms": [
|
|
401
|
+
{
|
|
402
|
+
"kind": "url",
|
|
403
|
+
"url": "https://x.example/u",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"kind": "mailto",
|
|
407
|
+
"mailto": {
|
|
408
|
+
"to": "y@z.example",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
"warnings": [],
|
|
413
|
+
}
|
|
414
|
+
`)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('legacy fallback preserves sender order: mailto before https', () => {
|
|
418
|
+
const plan = planUnsubscribe({
|
|
419
|
+
listUnsubscribe: '<mailto:y@z.example>, <https://x.example/u>',
|
|
420
|
+
listUnsubscribePost: undefined,
|
|
421
|
+
dkimAuthentic: true,
|
|
422
|
+
})
|
|
423
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
424
|
+
{
|
|
425
|
+
"dkimAuthentic": true,
|
|
426
|
+
"hasOneClick": false,
|
|
427
|
+
"mechanisms": [
|
|
428
|
+
{
|
|
429
|
+
"kind": "mailto",
|
|
430
|
+
"mailto": {
|
|
431
|
+
"to": "y@z.example",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"kind": "url",
|
|
436
|
+
"url": "https://x.example/u",
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
"warnings": [],
|
|
440
|
+
}
|
|
441
|
+
`)
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
describe('hasUnsubscribeMechanism', () => {
|
|
446
|
+
test('null or empty returns false', () => {
|
|
447
|
+
expect(hasUnsubscribeMechanism(null)).toMatchInlineSnapshot(`false`)
|
|
448
|
+
expect(hasUnsubscribeMechanism(undefined)).toMatchInlineSnapshot(`false`)
|
|
449
|
+
expect(hasUnsubscribeMechanism('')).toMatchInlineSnapshot(`false`)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('mailto: is usable', () => {
|
|
453
|
+
expect(hasUnsubscribeMechanism('<mailto:u@x.com>')).toMatchInlineSnapshot(`true`)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('https: is usable', () => {
|
|
457
|
+
expect(hasUnsubscribeMechanism('<https://x.com/u>')).toMatchInlineSnapshot(`true`)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('garbage with no usable scheme returns false', () => {
|
|
461
|
+
expect(hasUnsubscribeMechanism('<ftp://x.com/u>')).toMatchInlineSnapshot(`false`)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
describe('hasOneClickUnsubscribe', () => {
|
|
466
|
+
test('both headers with https URL', () => {
|
|
467
|
+
expect(
|
|
468
|
+
hasOneClickUnsubscribe('<https://x.com/u>', 'List-Unsubscribe=One-Click'),
|
|
469
|
+
).toMatchInlineSnapshot(`true`)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('missing Post header', () => {
|
|
473
|
+
expect(hasOneClickUnsubscribe('<https://x.com/u>', null)).toMatchInlineSnapshot(`false`)
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test('missing https URL in List-Unsubscribe', () => {
|
|
477
|
+
expect(
|
|
478
|
+
hasOneClickUnsubscribe('<mailto:u@x.com>', 'List-Unsubscribe=One-Click'),
|
|
479
|
+
).toMatchInlineSnapshot(`false`)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
test('http: (non-https) does not qualify', () => {
|
|
483
|
+
expect(
|
|
484
|
+
hasOneClickUnsubscribe('<http://x.com/u>', 'List-Unsubscribe=One-Click'),
|
|
485
|
+
).toMatchInlineSnapshot(`false`)
|
|
486
|
+
})
|
|
487
|
+
})
|