@cli4ai/gmail 1.0.3 → 1.0.5

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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @cli4ai/gmail
2
2
 
3
- > Official @cli4ai package • https://cli4ai.com • Install c4ai: `npm i -g c4ai`
3
+ > Official @cli4ai package • https://cli4ai.com • Install cli4ai: `npm i -g cli4ai`
4
4
 
5
5
  Full-featured Gmail CLI via Google API. Fast search, send/reply/forward, drafts, attachments, and labels.
6
6
 
7
7
  ## Setup
8
8
 
9
9
  ```bash
10
- npm i -g c4ai
11
- c4ai add -g gmail
10
+ npm i -g cli4ai
11
+ cli4ai add -g gmail
12
12
  ```
13
13
 
14
14
  ### 1) Google Cloud OAuth
@@ -26,7 +26,7 @@ Set `GMAIL_CREDENTIALS_PATH` to the downloaded JSON (optional: `GMAIL_TOKEN_PATH
26
26
  Example:
27
27
 
28
28
  ```bash
29
- GMAIL_CREDENTIALS_PATH="$HOME/Downloads/credentials.json" c4ai run gmail inbox
29
+ GMAIL_CREDENTIALS_PATH="$HOME/Downloads/credentials.json" cli4ai run gmail inbox
30
30
  ```
31
31
 
32
32
  On first run, the tool prints an authorization URL and prompts for the code, then stores a token at `GMAIL_TOKEN_PATH` (default: `token.json` inside the installed package directory).
@@ -35,60 +35,60 @@ On first run, the tool prints an authorization URL and prompts for the code, the
35
35
 
36
36
  ### Reading
37
37
  ```bash
38
- c4ai run gmail inbox [limit] # Recent inbox messages (default: 20)
39
- c4ai run gmail unread [limit] # Unread messages only
40
- c4ai run gmail search <query> [limit] # Search with Gmail syntax
41
- c4ai run gmail read <id> # Read full message
42
- c4ai run gmail thread <id> # Full conversation thread
43
- c4ai run gmail threads [limit] [query] # List recent threads
38
+ cli4ai run gmail inbox [limit] # Recent inbox messages (default: 20)
39
+ cli4ai run gmail unread [limit] # Unread messages only
40
+ cli4ai run gmail search <query> [limit] # Search with Gmail syntax
41
+ cli4ai run gmail read <id> # Read full message
42
+ cli4ai run gmail thread <id> # Full conversation thread
43
+ cli4ai run gmail threads [limit] [query] # List recent threads
44
44
  ```
45
45
 
46
46
  ### Actions
47
47
  ```bash
48
- c4ai run gmail archive <id> # Archive (remove from inbox)
49
- c4ai run gmail trash <id> # Move to trash
50
- c4ai run gmail untrash <id> # Restore from trash
51
- c4ai run gmail star <id> # Star message
52
- c4ai run gmail unstar <id> # Unstar message
53
- c4ai run gmail markread <id> # Mark as read
54
- c4ai run gmail markunread <id> # Mark as unread
48
+ cli4ai run gmail archive <id> # Archive (remove from inbox)
49
+ cli4ai run gmail trash <id> # Move to trash
50
+ cli4ai run gmail untrash <id> # Restore from trash
51
+ cli4ai run gmail star <id> # Star message
52
+ cli4ai run gmail unstar <id> # Unstar message
53
+ cli4ai run gmail markread <id> # Mark as read
54
+ cli4ai run gmail markunread <id> # Mark as unread
55
55
  ```
56
56
 
57
57
  ### Sending
58
58
  ```bash
59
- c4ai run gmail send <to> <subject> <body> # Send new email
60
- c4ai run gmail reply <id> <body> # Reply to message
61
- c4ai run gmail replyall <id> <body> # Reply to all
62
- c4ai run gmail forward <id> <to> [body] # Forward message
63
- c4ai run gmail draft <to> <subject> <body> # Create draft (legacy)
59
+ cli4ai run gmail send <to> <subject> <body> # Send new email
60
+ cli4ai run gmail reply <id> <body> # Reply to message
61
+ cli4ai run gmail replyall <id> <body> # Reply to all
62
+ cli4ai run gmail forward <id> <to> [body] # Forward message
63
+ cli4ai run gmail draft <to> <subject> <body> # Create draft (legacy)
64
64
  ```
65
65
 
66
66
  ### Drafts
67
67
  ```bash
68
- c4ai run gmail drafts # List all drafts
69
- c4ai run gmail draft-get <id> # Get draft content
70
- c4ai run gmail draft-create <to> <subj> <body> # Create new draft
71
- c4ai run gmail draft-reply <msg-id> <body> # Create draft reply to message
72
- c4ai run gmail draft-update <id> <to> <s> <b> # Update existing draft
73
- c4ai run gmail draft-delete <id> # Delete draft
74
- c4ai run gmail draft-send <id> # Send draft
68
+ cli4ai run gmail drafts # List all drafts
69
+ cli4ai run gmail draft-get <id> # Get draft content
70
+ cli4ai run gmail draft-create <to> <subj> <body> # Create new draft
71
+ cli4ai run gmail draft-reply <msg-id> <body> # Create draft reply to message
72
+ cli4ai run gmail draft-update <id> <to> <s> <b> # Update existing draft
73
+ cli4ai run gmail draft-delete <id> # Delete draft
74
+ cli4ai run gmail draft-send <id> # Send draft
75
75
  ```
76
76
 
77
77
  ### Attachments
78
78
  ```bash
79
- c4ai run gmail attachments <id> # List attachments in message
80
- c4ai run gmail download <id> [filename] [out] # Download specific attachment
81
- c4ai run gmail download-all <id> [dir] # Download all attachments
79
+ cli4ai run gmail attachments <id> # List attachments in message
80
+ cli4ai run gmail download <id> [filename] [out] # Download specific attachment
81
+ cli4ai run gmail download-all <id> [dir] # Download all attachments
82
82
  ```
83
83
 
84
84
  ### Labels
85
85
  ```bash
86
- c4ai run gmail labels # List all labels
87
- c4ai run gmail label <id> <label> # Add label to message
88
- c4ai run gmail unlabel <id> <label> # Remove label
89
- c4ai run gmail label-info <label> # Get label with counts
90
- c4ai run gmail label-create <name> # Create new label
91
- c4ai run gmail label-delete <name> # Delete label
86
+ cli4ai run gmail labels # List all labels
87
+ cli4ai run gmail label <id> <label> # Add label to message
88
+ cli4ai run gmail unlabel <id> <label> # Remove label
89
+ cli4ai run gmail label-info <label> # Get label with counts
90
+ cli4ai run gmail label-create <name> # Create new label
91
+ cli4ai run gmail label-delete <name> # Delete label
92
92
  ```
93
93
 
94
94
  ## Output Flags
@@ -110,83 +110,83 @@ The search command uses Gmail's powerful search syntax:
110
110
 
111
111
  ```bash
112
112
  # By sender/recipient
113
- c4ai run gmail search "from:boss@company.com"
114
- c4ai run gmail search "to:me"
115
- c4ai run gmail search "cc:team@company.com"
113
+ cli4ai run gmail search "from:boss@company.com"
114
+ cli4ai run gmail search "to:me"
115
+ cli4ai run gmail search "cc:team@company.com"
116
116
 
117
117
  # By status
118
- c4ai run gmail search "is:unread"
119
- c4ai run gmail search "is:starred"
120
- c4ai run gmail search "is:important"
118
+ cli4ai run gmail search "is:unread"
119
+ cli4ai run gmail search "is:starred"
120
+ cli4ai run gmail search "is:important"
121
121
 
122
122
  # By content
123
- c4ai run gmail search "subject:meeting"
124
- c4ai run gmail search "has:attachment"
125
- c4ai run gmail search "filename:pdf"
123
+ cli4ai run gmail search "subject:meeting"
124
+ cli4ai run gmail search "has:attachment"
125
+ cli4ai run gmail search "filename:pdf"
126
126
 
127
127
  # By date
128
- c4ai run gmail search "after:2024/12/01"
129
- c4ai run gmail search "before:2024/12/31"
130
- c4ai run gmail search "newer_than:7d"
131
- c4ai run gmail search "older_than:1m"
128
+ cli4ai run gmail search "after:2024/12/01"
129
+ cli4ai run gmail search "before:2024/12/31"
130
+ cli4ai run gmail search "newer_than:7d"
131
+ cli4ai run gmail search "older_than:1m"
132
132
 
133
133
  # By label/location
134
- c4ai run gmail search "label:work"
135
- c4ai run gmail search "in:inbox"
136
- c4ai run gmail search "in:sent"
134
+ cli4ai run gmail search "label:work"
135
+ cli4ai run gmail search "in:inbox"
136
+ cli4ai run gmail search "in:sent"
137
137
 
138
138
  # Combine queries
139
- c4ai run gmail search "from:boss@company.com is:unread newer_than:7d"
140
- c4ai run gmail search "has:attachment larger:5M"
139
+ cli4ai run gmail search "from:boss@company.com is:unread newer_than:7d"
140
+ cli4ai run gmail search "has:attachment larger:5M"
141
141
  ```
142
142
 
143
143
  ## Examples
144
144
 
145
145
  ```bash
146
146
  # Quick inbox scan
147
- c4ai run gmail inbox 10 --compact
147
+ cli4ai run gmail inbox 10 --compact
148
148
 
149
149
  # Find unread from specific sender
150
- c4ai run gmail search "from:important@client.com is:unread"
150
+ cli4ai run gmail search "from:important@client.com is:unread"
151
151
 
152
152
  # Read a specific email
153
- c4ai run gmail read 19b176a2f49b681f
153
+ cli4ai run gmail read 19b176a2f49b681f
154
154
 
155
155
  # View full conversation thread
156
- c4ai run gmail thread 19b176a2f49b681f
156
+ cli4ai run gmail thread 19b176a2f49b681f
157
157
 
158
158
  # Send an email
159
- c4ai run gmail send "john@example.com" "Meeting tomorrow" "Hi John,\n\nAre we still on for tomorrow?\n\nThanks"
159
+ cli4ai run gmail send "john@example.com" "Meeting tomorrow" "Hi John,\n\nAre we still on for tomorrow?\n\nThanks"
160
160
 
161
161
  # Reply to an email
162
- c4ai run gmail reply 19b176a2f49b681f "Thanks for the update, looks good!"
162
+ cli4ai run gmail reply 19b176a2f49b681f "Thanks for the update, looks good!"
163
163
 
164
164
  # Forward an email
165
- c4ai run gmail forward 19b176a2f49b681f "colleague@example.com" "FYI - see below"
165
+ cli4ai run gmail forward 19b176a2f49b681f "colleague@example.com" "FYI - see below"
166
166
 
167
167
  # Archive old emails
168
- c4ai run gmail archive 19b176a2f49b681f
168
+ cli4ai run gmail archive 19b176a2f49b681f
169
169
 
170
170
  # Label management
171
- c4ai run gmail label 19b176a2f49b681f "Work"
172
- c4ai run gmail label-info "INBOX"
171
+ cli4ai run gmail label 19b176a2f49b681f "Work"
172
+ cli4ai run gmail label-info "INBOX"
173
173
 
174
174
  # Send with attachment
175
- c4ai run gmail send "john@example.com" "Report" "See attached" --attach=./report.pdf
175
+ cli4ai run gmail send "john@example.com" "Report" "See attached" --attach=./report.pdf
176
176
 
177
177
  # Multiple attachments
178
- c4ai run gmail send "john@example.com" "Files" "Here are the files" --attach=./a.pdf --attach=./b.pdf
178
+ cli4ai run gmail send "john@example.com" "Files" "Here are the files" --attach=./a.pdf --attach=./b.pdf
179
179
 
180
180
  # List and download attachments
181
- c4ai run gmail attachments 19b176a2f49b681f
182
- c4ai run gmail download 19b176a2f49b681f "report.pdf" ./downloads/report.pdf
183
- c4ai run gmail download-all 19b176a2f49b681f ./downloads/
181
+ cli4ai run gmail attachments 19b176a2f49b681f
182
+ cli4ai run gmail download 19b176a2f49b681f "report.pdf" ./downloads/report.pdf
183
+ cli4ai run gmail download-all 19b176a2f49b681f ./downloads/
184
184
 
185
185
  # Draft management
186
- c4ai run gmail drafts
187
- c4ai run gmail draft-create "john@example.com" "Draft" "Will finish later"
188
- c4ai run gmail draft-reply 19b176a2f49b681f "Thanks, need to think about this"
189
- c4ai run gmail draft-send r123456789
186
+ cli4ai run gmail drafts
187
+ cli4ai run gmail draft-create "john@example.com" "Draft" "Will finish later"
188
+ cli4ai run gmail draft-reply 19b176a2f49b681f "Thanks, need to think about this"
189
+ cli4ai run gmail draft-send r123456789
190
190
  ```
191
191
 
192
192
  ## Project Structure
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmail",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Gmail CLI tool for messages, threads, and drafts",
5
5
  "author": "cliforai",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli4ai/gmail",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Gmail CLI tool for messages, threads, and drafts",
5
5
  "author": "cliforai",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "type": "module",
12
12
  "keywords": [
13
- "c4ai",
13
+ "cli4ai",
14
14
  "cli",
15
15
  "ai-tools",
16
16
  "gmail",
@@ -20,29 +20,25 @@
20
20
  ],
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/cli4ai/packages",
23
+ "url": "https://github.com/cliforai/packages",
24
24
  "directory": "packages/gmail"
25
25
  },
26
- "homepage": "https://github.com/cli4ai/packages/tree/main/packages/gmail",
26
+ "homepage": "https://github.com/cliforai/packages/tree/main/packages/gmail",
27
27
  "bugs": {
28
- "url": "https://github.com/cli4ai/packages/issues"
28
+ "url": "https://github.com/cliforai/packages/issues"
29
29
  },
30
30
  "dependencies": {
31
- "@cli4ai/lib": "^1.0.0",
32
- "commander": "^14.0.0",
31
+ "@cli4ai/lib": "^1.0.2",
32
+ "googleapis": "^144.0.0",
33
33
  "google-auth-library": "^9.0.0",
34
- "googleapis": "^140.0.0"
34
+ "commander": "^14.0.0"
35
35
  },
36
36
  "files": [
37
37
  "run.ts",
38
- "lib/**",
39
- "c4ai.json",
38
+ "cli4ai.json",
40
39
  "README.md",
41
40
  "LICENSE"
42
41
  ],
43
- "scripts": {
44
- "test": "bun test"
45
- },
46
42
  "publishConfig": {
47
43
  "access": "public"
48
44
  }
package/lib/api.test.ts DELETED
@@ -1,287 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from 'bun:test';
2
- import {
3
- formatDate,
4
- decodeBase64Url,
5
- encodeBase64Url,
6
- extractHeaders,
7
- extractBody,
8
- cleanBody,
9
- output,
10
- type MessagePayload,
11
- type EmailHeaders
12
- } from './api';
13
-
14
- describe('gmail/lib/api', () => {
15
- describe('formatDate', () => {
16
- test('should format timestamp to readable date', () => {
17
- // 1704067200000 = 2024-01-01 00:00:00 UTC
18
- const result = formatDate('1704067200000');
19
- expect(result).toBe('2024-01-01 00:00');
20
- });
21
-
22
- test('should handle timestamp as number', () => {
23
- const result = formatDate(1704067200000);
24
- expect(result).toBe('2024-01-01 00:00');
25
- });
26
-
27
- test('should return null for undefined', () => {
28
- expect(formatDate(undefined)).toBeNull();
29
- });
30
-
31
- test('should return null for null', () => {
32
- expect(formatDate(null)).toBeNull();
33
- });
34
- });
35
-
36
- describe('decodeBase64Url', () => {
37
- test('should decode base64url encoded string', () => {
38
- // 'Hello World' in base64url
39
- const encoded = 'SGVsbG8gV29ybGQ';
40
- const result = decodeBase64Url(encoded);
41
- expect(result).toBe('Hello World');
42
- });
43
-
44
- test('should handle URL-safe characters', () => {
45
- // Contains - and _ instead of + and /
46
- const encoded = 'PDw_Pz4-'; // <<??>>
47
- const result = decodeBase64Url(encoded);
48
- expect(result).toBe('<<??>>');
49
- });
50
-
51
- test('should handle missing padding', () => {
52
- // Without = padding
53
- const encoded = 'SGVsbG8';
54
- const result = decodeBase64Url(encoded);
55
- expect(result).toBe('Hello');
56
- });
57
-
58
- test('should return empty string for undefined', () => {
59
- expect(decodeBase64Url(undefined)).toBe('');
60
- });
61
-
62
- test('should return empty string for null', () => {
63
- expect(decodeBase64Url(null)).toBe('');
64
- });
65
-
66
- test('should return empty string for empty string', () => {
67
- expect(decodeBase64Url('')).toBe('');
68
- });
69
- });
70
-
71
- describe('encodeBase64Url', () => {
72
- test('should encode string to base64url', () => {
73
- const result = encodeBase64Url('Hello World');
74
- expect(result).toBe('SGVsbG8gV29ybGQ');
75
- });
76
-
77
- test('should replace + with -', () => {
78
- // A string that would produce + in standard base64
79
- const result = encodeBase64Url('<<??>>');
80
- expect(result).not.toContain('+');
81
- expect(result).toContain('-');
82
- });
83
-
84
- test('should replace / with _', () => {
85
- // A string that would produce / in standard base64
86
- const result = encodeBase64Url('<<??>>');
87
- expect(result).not.toContain('/');
88
- });
89
-
90
- test('should remove padding', () => {
91
- const result = encodeBase64Url('Hello');
92
- expect(result).not.toContain('=');
93
- });
94
-
95
- test('should be reversible', () => {
96
- const original = 'Test message with special chars: éà';
97
- const encoded = encodeBase64Url(original);
98
- const decoded = decodeBase64Url(encoded);
99
- expect(decoded).toBe(original);
100
- });
101
- });
102
-
103
- describe('extractHeaders', () => {
104
- test('should extract email headers', () => {
105
- const headers = [
106
- { name: 'From', value: 'sender@example.com' },
107
- { name: 'To', value: 'recipient@example.com' },
108
- { name: 'Subject', value: 'Test Subject' },
109
- { name: 'Date', value: '2024-01-01' }
110
- ];
111
- const result = extractHeaders(headers);
112
- expect(result.from).toBe('sender@example.com');
113
- expect(result.to).toBe('recipient@example.com');
114
- expect(result.subject).toBe('Test Subject');
115
- expect(result.date).toBe('2024-01-01');
116
- });
117
-
118
- test('should handle Message-ID header', () => {
119
- const headers = [
120
- { name: 'Message-ID', value: '<123@example.com>' }
121
- ];
122
- const result = extractHeaders(headers);
123
- expect(result.message_id).toBe('<123@example.com>');
124
- });
125
-
126
- test('should handle Reply-To header', () => {
127
- const headers = [
128
- { name: 'Reply-To', value: 'reply@example.com' }
129
- ];
130
- const result = extractHeaders(headers);
131
- expect(result.reply_to).toBe('reply@example.com');
132
- });
133
-
134
- test('should ignore unknown headers', () => {
135
- const headers = [
136
- { name: 'X-Custom-Header', value: 'custom value' },
137
- { name: 'From', value: 'sender@example.com' }
138
- ];
139
- const result = extractHeaders(headers);
140
- expect(result.from).toBe('sender@example.com');
141
- expect(Object.keys(result)).not.toContain('x_custom_header');
142
- });
143
-
144
- test('should return empty object for undefined headers', () => {
145
- const result = extractHeaders(undefined);
146
- expect(result).toEqual({});
147
- });
148
-
149
- test('should return empty object for empty array', () => {
150
- const result = extractHeaders([]);
151
- expect(result).toEqual({});
152
- });
153
- });
154
-
155
- describe('extractBody', () => {
156
- test('should extract body from simple message', () => {
157
- const payload: MessagePayload = {
158
- body: { data: 'SGVsbG8gV29ybGQ' } // 'Hello World'
159
- };
160
- const result = extractBody(payload);
161
- expect(result).toBe('Hello World');
162
- });
163
-
164
- test('should prefer text/plain from multipart', () => {
165
- const payload: MessagePayload = {
166
- parts: [
167
- { mimeType: 'text/html', body: { data: 'PGh0bWw-SGVsbG88L2h0bWw-' } },
168
- { mimeType: 'text/plain', body: { data: 'SGVsbG8' } }
169
- ]
170
- };
171
- const result = extractBody(payload);
172
- expect(result).toBe('Hello');
173
- });
174
-
175
- test('should fall back to text/html if no text/plain', () => {
176
- const payload: MessagePayload = {
177
- parts: [
178
- { mimeType: 'text/html', body: { data: 'PGI-SGVsbG88L2I-' } } // <b>Hello</b>
179
- ]
180
- };
181
- const result = extractBody(payload);
182
- // HTML tags should be stripped
183
- expect(result).toContain('Hello');
184
- });
185
-
186
- test('should handle nested parts', () => {
187
- const payload: MessagePayload = {
188
- parts: [
189
- {
190
- mimeType: 'multipart/alternative',
191
- parts: [
192
- { mimeType: 'text/plain', body: { data: 'SGVsbG8' } }
193
- ]
194
- }
195
- ]
196
- };
197
- const result = extractBody(payload);
198
- expect(result).toBe('Hello');
199
- });
200
-
201
- test('should return empty string for undefined payload', () => {
202
- expect(extractBody(undefined)).toBe('');
203
- });
204
-
205
- test('should return empty string for empty payload', () => {
206
- expect(extractBody({})).toBe('');
207
- });
208
- });
209
-
210
- describe('cleanBody', () => {
211
- test('should remove excessive whitespace', () => {
212
- const body = 'Hello World';
213
- const result = cleanBody(body);
214
- expect(result).toBe('Hello World');
215
- });
216
-
217
- test('should normalize line breaks', () => {
218
- const body = 'Hello\r\nWorld';
219
- const result = cleanBody(body);
220
- expect(result).toBe('Hello\nWorld');
221
- });
222
-
223
- test('should collapse multiple newlines', () => {
224
- const body = 'Hello\n\n\n\nWorld';
225
- const result = cleanBody(body);
226
- expect(result).toBe('Hello\n\nWorld');
227
- });
228
-
229
- test('should trim whitespace', () => {
230
- const body = ' Hello World ';
231
- const result = cleanBody(body);
232
- expect(result).toBe('Hello World');
233
- });
234
-
235
- test('should truncate to maxLength', () => {
236
- const body = 'Hello World This is a long message';
237
- const result = cleanBody(body, 10);
238
- expect(result).toBe('Hello Worl...');
239
- });
240
-
241
- test('should not truncate when maxLength is null', () => {
242
- const body = 'Hello World';
243
- const result = cleanBody(body, null);
244
- expect(result).toBe('Hello World');
245
- });
246
-
247
- test('should return empty string for empty input', () => {
248
- expect(cleanBody('')).toBe('');
249
- });
250
- });
251
-
252
- describe('output', () => {
253
- let consoleSpy: ReturnType<typeof spyOn>;
254
- let logs: string[];
255
-
256
- beforeEach(() => {
257
- logs = [];
258
- consoleSpy = spyOn(console, 'log').mockImplementation((msg: string) => {
259
- logs.push(msg);
260
- });
261
- });
262
-
263
- afterEach(() => {
264
- consoleSpy.mockRestore();
265
- });
266
-
267
- test('should output JSON by default', () => {
268
- output({ test: 'data' });
269
- expect(logs[0]).toContain('"test": "data"');
270
- });
271
-
272
- test('should output raw JSON when raw option is true', () => {
273
- output({ test: 'data' }, { raw: true });
274
- expect(logs[0]).toContain('"test": "data"');
275
- });
276
-
277
- test('should output compact format for message arrays', () => {
278
- const messages = [
279
- { date: '2024-01-01', from: 'sender@test.com', subject: 'Test Subject' }
280
- ];
281
- output(messages, { compact: true });
282
- expect(logs[0]).toContain('[2024-01-01]');
283
- expect(logs[0]).toContain('sender@test.com');
284
- expect(logs[0]).toContain('Test Subject');
285
- });
286
- });
287
- });