|
4 | 4 | import { describe, expect, it } from 'vitest' |
5 | 5 | import { encodeRfc2047 } from './utils' |
6 | 6 |
|
7 | | -/** |
8 | | - * Decode an RFC 2047 encoded header (single or multi-word) back to a string. |
9 | | - */ |
10 | | -function decodeRfc2047(encoded: string): string { |
11 | | - const words = encoded.split(/\r\n\s+/) |
12 | | - return words |
13 | | - .map((word) => { |
14 | | - const match = word.match(/^=\?UTF-8\?B\?(.+)\?=$/) |
15 | | - if (!match) return word |
16 | | - return Buffer.from(match[1], 'base64').toString('utf-8') |
17 | | - }) |
18 | | - .join('') |
19 | | -} |
20 | | - |
21 | 7 | describe('encodeRfc2047', () => { |
22 | 8 | it('returns ASCII text unchanged', () => { |
23 | | - const input = 'Simple ASCII Subject' |
24 | | - expect(encodeRfc2047(input)).toBe(input) |
| 9 | + expect(encodeRfc2047('Simple ASCII Subject')).toBe('Simple ASCII Subject') |
25 | 10 | }) |
26 | 11 |
|
27 | 12 | it('returns empty string unchanged', () => { |
28 | 13 | expect(encodeRfc2047('')).toBe('') |
29 | 14 | }) |
30 | 15 |
|
31 | | - it('encodes short non-ASCII text in a single encoded word', () => { |
32 | | - const input = 'Hello 世界' |
33 | | - const result = encodeRfc2047(input) |
34 | | - expect(result).toMatch(/^=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=$/) |
35 | | - expect(result.length).toBeLessThanOrEqual(75) |
36 | | - expect(decodeRfc2047(result)).toBe(input) |
37 | | - }) |
38 | | - |
39 | | - it('encodes emojis correctly', () => { |
40 | | - const input = 'Time to Stretch! 🧘' |
41 | | - const result = encodeRfc2047(input) |
42 | | - expect(result).toMatch(/^=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=$/) |
43 | | - expect(decodeRfc2047(result)).toBe(input) |
44 | | - }) |
45 | | - |
46 | | - it('splits long non-ASCII text into multiple encoded words', () => { |
47 | | - const input = '今週のミーティングアジェンダについて検討します' |
48 | | - const result = encodeRfc2047(input) |
49 | | - const words = result.split('\r\n ') |
50 | | - words.forEach((word) => { |
51 | | - expect(word.length).toBeLessThanOrEqual(75) |
52 | | - expect(word).toMatch(/^=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=$/) |
53 | | - }) |
54 | | - expect(decodeRfc2047(result)).toBe(input) |
55 | | - }) |
56 | | - |
57 | | - it('handles very long subjects with emojis without splitting characters', () => { |
58 | | - const input = '🎉 '.repeat(30) |
59 | | - const result = encodeRfc2047(input) |
60 | | - const words = result.split('\r\n ') |
61 | | - words.forEach((word) => { |
62 | | - expect(word.length).toBeLessThanOrEqual(75) |
63 | | - expect(word).toMatch(/^=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=$/) |
64 | | - }) |
65 | | - expect(decodeRfc2047(result)).toBe(input) |
66 | | - }) |
67 | | - |
68 | | - it('does not split already-encoded subjects (pure ASCII passthrough)', () => { |
69 | | - const input = '=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=' |
70 | | - const result = encodeRfc2047(input) |
71 | | - expect(result).toBe(input) |
| 16 | + it('encodes emojis as RFC 2047 base64', () => { |
| 17 | + const result = encodeRfc2047('Time to Stretch! 🧘') |
| 18 | + expect(result).toBe('=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=') |
72 | 19 | }) |
73 | 20 |
|
74 | | - it('handles accented characters', () => { |
75 | | - const input = 'Café résumé' |
76 | | - const result = encodeRfc2047(input) |
77 | | - expect(result).toMatch(/^=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=$/) |
78 | | - expect(decodeRfc2047(result)).toBe(input) |
| 21 | + it('round-trips non-ASCII subjects correctly', () => { |
| 22 | + const subjects = ['Hello 世界', 'Café résumé', '🎉🎊🎈 Party!', '今週のミーティング'] |
| 23 | + for (const subject of subjects) { |
| 24 | + const encoded = encodeRfc2047(subject) |
| 25 | + const match = encoded.match(/^=\?UTF-8\?B\?(.+)\?=$/) |
| 26 | + expect(match).not.toBeNull() |
| 27 | + const decoded = Buffer.from(match![1], 'base64').toString('utf-8') |
| 28 | + expect(decoded).toBe(subject) |
| 29 | + } |
79 | 30 | }) |
80 | 31 |
|
81 | | - it('handles mixed ASCII and multi-byte characters in long subjects', () => { |
82 | | - const input = 'Important: 会議の議事録をお送りします - please review by Friday 🙏' |
83 | | - const result = encodeRfc2047(input) |
84 | | - const words = result.split('\r\n ') |
85 | | - words.forEach((word) => { |
86 | | - expect(word.length).toBeLessThanOrEqual(75) |
87 | | - }) |
88 | | - expect(decodeRfc2047(result)).toBe(input) |
| 32 | + it('does not double-encode already-encoded subjects', () => { |
| 33 | + const alreadyEncoded = '=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=' |
| 34 | + expect(encodeRfc2047(alreadyEncoded)).toBe(alreadyEncoded) |
89 | 35 | }) |
90 | 36 | }) |
0 commit comments