@cli4ai/gmail 1.0.3 → 1.0.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/README.md +75 -75
- package/{c4ai.json → cli4ai.json} +1 -1
- package/package.json +8 -13
- package/lib/api.test.ts +0 -287
- package/lib/api.ts +0 -299
- package/lib/attachments.ts +0 -199
- package/lib/drafts.ts +0 -434
- package/lib/labels.ts +0 -198
- package/lib/messages.ts +0 -310
- package/lib/send.ts +0 -331
- package/lib/threads.ts +0 -164
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @cli4ai/gmail
|
|
2
2
|
|
|
3
|
-
> Official @cli4ai package • https://cli4ai.com • Install
|
|
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
|
|
11
|
-
|
|
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"
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
+
cli4ai run gmail inbox 10 --compact
|
|
148
148
|
|
|
149
149
|
# Find unread from specific sender
|
|
150
|
-
|
|
150
|
+
cli4ai run gmail search "from:important@client.com is:unread"
|
|
151
151
|
|
|
152
152
|
# Read a specific email
|
|
153
|
-
|
|
153
|
+
cli4ai run gmail read 19b176a2f49b681f
|
|
154
154
|
|
|
155
155
|
# View full conversation thread
|
|
156
|
-
|
|
156
|
+
cli4ai run gmail thread 19b176a2f49b681f
|
|
157
157
|
|
|
158
158
|
# Send an email
|
|
159
|
-
|
|
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
|
-
|
|
162
|
+
cli4ai run gmail reply 19b176a2f49b681f "Thanks for the update, looks good!"
|
|
163
163
|
|
|
164
164
|
# Forward an email
|
|
165
|
-
|
|
165
|
+
cli4ai run gmail forward 19b176a2f49b681f "colleague@example.com" "FYI - see below"
|
|
166
166
|
|
|
167
167
|
# Archive old emails
|
|
168
|
-
|
|
168
|
+
cli4ai run gmail archive 19b176a2f49b681f
|
|
169
169
|
|
|
170
170
|
# Label management
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
cli4ai run gmail label 19b176a2f49b681f "Work"
|
|
172
|
+
cli4ai run gmail label-info "INBOX"
|
|
173
173
|
|
|
174
174
|
# Send with attachment
|
|
175
|
-
|
|
175
|
+
cli4ai run gmail send "john@example.com" "Report" "See attached" --attach=./report.pdf
|
|
176
176
|
|
|
177
177
|
# Multiple attachments
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cli4ai/gmail",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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
|
-
"
|
|
13
|
+
"cli4ai",
|
|
14
14
|
"cli",
|
|
15
15
|
"ai-tools",
|
|
16
16
|
"gmail",
|
|
@@ -20,29 +20,24 @@
|
|
|
20
20
|
],
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/
|
|
23
|
+
"url": "https://github.com/cliforai/packages",
|
|
24
24
|
"directory": "packages/gmail"
|
|
25
25
|
},
|
|
26
|
-
"homepage": "https://github.com/
|
|
26
|
+
"homepage": "https://github.com/cliforai/packages/tree/main/packages/gmail",
|
|
27
27
|
"bugs": {
|
|
28
|
-
"url": "https://github.com/
|
|
28
|
+
"url": "https://github.com/cliforai/packages/issues"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"commander": "^14.0.0",
|
|
31
|
+
"googleapis": "^144.0.0",
|
|
33
32
|
"google-auth-library": "^9.0.0",
|
|
34
|
-
"
|
|
33
|
+
"commander": "^14.0.0"
|
|
35
34
|
},
|
|
36
35
|
"files": [
|
|
37
36
|
"run.ts",
|
|
38
|
-
"
|
|
39
|
-
"c4ai.json",
|
|
37
|
+
"cli4ai.json",
|
|
40
38
|
"README.md",
|
|
41
39
|
"LICENSE"
|
|
42
40
|
],
|
|
43
|
-
"scripts": {
|
|
44
|
-
"test": "bun test"
|
|
45
|
-
},
|
|
46
41
|
"publishConfig": {
|
|
47
42
|
"access": "public"
|
|
48
43
|
}
|
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
|
-
});
|