@cli4ai/gmail 1.0.1 → 1.0.3
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 +112 -113
- package/c4ai.json +1 -1
- package/lib/api.test.ts +287 -0
- package/lib/api.ts +299 -0
- package/lib/attachments.ts +199 -0
- package/lib/drafts.ts +434 -0
- package/lib/labels.ts +198 -0
- package/lib/messages.ts +310 -0
- package/lib/send.ts +331 -0
- package/lib/threads.ts +164 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,65 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cli4ai/gmail
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Official @cli4ai package • https://cli4ai.com • Install c4ai: `npm i -g c4ai`
|
|
4
|
+
|
|
5
|
+
Full-featured Gmail CLI via Google API. Fast search, send/reply/forward, drafts, attachments, and labels.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g c4ai
|
|
11
|
+
c4ai add -g gmail
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### 1) Google Cloud OAuth
|
|
15
|
+
|
|
16
|
+
1. Go to https://console.cloud.google.com
|
|
17
|
+
2. Create a project (or use an existing one)
|
|
18
|
+
3. Enable the Gmail API
|
|
19
|
+
4. Create OAuth 2.0 credentials (Desktop app recommended)
|
|
20
|
+
5. Download the OAuth client JSON
|
|
21
|
+
|
|
22
|
+
### 2) Point the tool at your credentials
|
|
23
|
+
|
|
24
|
+
Set `GMAIL_CREDENTIALS_PATH` to the downloaded JSON (optional: `GMAIL_TOKEN_PATH`).
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
GMAIL_CREDENTIALS_PATH="$HOME/Downloads/credentials.json" c4ai run gmail inbox
|
|
30
|
+
```
|
|
31
|
+
|
|
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).
|
|
4
33
|
|
|
5
34
|
## Commands
|
|
6
35
|
|
|
7
36
|
### Reading
|
|
8
37
|
```bash
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
15
44
|
```
|
|
16
45
|
|
|
17
46
|
### Actions
|
|
18
47
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
55
|
```
|
|
27
56
|
|
|
28
57
|
### Sending
|
|
29
58
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
35
64
|
```
|
|
36
65
|
|
|
37
66
|
### Drafts
|
|
38
67
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
75
|
```
|
|
47
76
|
|
|
48
77
|
### Attachments
|
|
49
78
|
```bash
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
82
|
```
|
|
54
83
|
|
|
55
84
|
### Labels
|
|
56
85
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
92
|
```
|
|
64
93
|
|
|
65
94
|
## Output Flags
|
|
@@ -81,130 +110,100 @@ The search command uses Gmail's powerful search syntax:
|
|
|
81
110
|
|
|
82
111
|
```bash
|
|
83
112
|
# By sender/recipient
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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"
|
|
87
116
|
|
|
88
117
|
# By status
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
118
|
+
c4ai run gmail search "is:unread"
|
|
119
|
+
c4ai run gmail search "is:starred"
|
|
120
|
+
c4ai run gmail search "is:important"
|
|
92
121
|
|
|
93
122
|
# By content
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
c4ai run gmail search "subject:meeting"
|
|
124
|
+
c4ai run gmail search "has:attachment"
|
|
125
|
+
c4ai run gmail search "filename:pdf"
|
|
97
126
|
|
|
98
127
|
# By date
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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"
|
|
103
132
|
|
|
104
133
|
# By label/location
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
c4ai run gmail search "label:work"
|
|
135
|
+
c4ai run gmail search "in:inbox"
|
|
136
|
+
c4ai run gmail search "in:sent"
|
|
108
137
|
|
|
109
138
|
# Combine queries
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
c4ai run gmail search "from:boss@company.com is:unread newer_than:7d"
|
|
140
|
+
c4ai run gmail search "has:attachment larger:5M"
|
|
112
141
|
```
|
|
113
142
|
|
|
114
143
|
## Examples
|
|
115
144
|
|
|
116
145
|
```bash
|
|
117
146
|
# Quick inbox scan
|
|
118
|
-
|
|
147
|
+
c4ai run gmail inbox 10 --compact
|
|
119
148
|
|
|
120
149
|
# Find unread from specific sender
|
|
121
|
-
|
|
150
|
+
c4ai run gmail search "from:important@client.com is:unread"
|
|
122
151
|
|
|
123
152
|
# Read a specific email
|
|
124
|
-
|
|
153
|
+
c4ai run gmail read 19b176a2f49b681f
|
|
125
154
|
|
|
126
155
|
# View full conversation thread
|
|
127
|
-
|
|
156
|
+
c4ai run gmail thread 19b176a2f49b681f
|
|
128
157
|
|
|
129
158
|
# Send an email
|
|
130
|
-
|
|
159
|
+
c4ai run gmail send "john@example.com" "Meeting tomorrow" "Hi John,\n\nAre we still on for tomorrow?\n\nThanks"
|
|
131
160
|
|
|
132
161
|
# Reply to an email
|
|
133
|
-
|
|
162
|
+
c4ai run gmail reply 19b176a2f49b681f "Thanks for the update, looks good!"
|
|
134
163
|
|
|
135
164
|
# Forward an email
|
|
136
|
-
|
|
165
|
+
c4ai run gmail forward 19b176a2f49b681f "colleague@example.com" "FYI - see below"
|
|
137
166
|
|
|
138
167
|
# Archive old emails
|
|
139
|
-
|
|
168
|
+
c4ai run gmail archive 19b176a2f49b681f
|
|
140
169
|
|
|
141
170
|
# Label management
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
c4ai run gmail label 19b176a2f49b681f "Work"
|
|
172
|
+
c4ai run gmail label-info "INBOX"
|
|
144
173
|
|
|
145
174
|
# Send with attachment
|
|
146
|
-
|
|
175
|
+
c4ai run gmail send "john@example.com" "Report" "See attached" --attach=./report.pdf
|
|
147
176
|
|
|
148
177
|
# Multiple attachments
|
|
149
|
-
|
|
178
|
+
c4ai run gmail send "john@example.com" "Files" "Here are the files" --attach=./a.pdf --attach=./b.pdf
|
|
150
179
|
|
|
151
180
|
# List and download attachments
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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/
|
|
155
184
|
|
|
156
185
|
# Draft management
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Setup
|
|
164
|
-
|
|
165
|
-
### 1. Google Cloud Project
|
|
166
|
-
|
|
167
|
-
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
|
168
|
-
2. Create a project (or use existing)
|
|
169
|
-
3. Enable Gmail API
|
|
170
|
-
4. Create OAuth 2.0 credentials (Desktop app type)
|
|
171
|
-
5. Download `credentials.json`
|
|
172
|
-
|
|
173
|
-
### 2. Place Credentials
|
|
174
|
-
|
|
175
|
-
Put `credentials.json` in the `gmail/` directory, or set path in `.env`:
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
GMAIL_CREDENTIALS_PATH=/path/to/credentials.json
|
|
179
|
-
GMAIL_TOKEN_PATH=/path/to/token.json
|
|
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
|
|
180
190
|
```
|
|
181
191
|
|
|
182
|
-
### 3. First Run
|
|
183
|
-
|
|
184
|
-
On first run, you'll be prompted to authorize:
|
|
185
|
-
|
|
186
|
-
1. Visit the OAuth URL shown
|
|
187
|
-
2. Sign in with your Google account
|
|
188
|
-
3. Copy the authorization code
|
|
189
|
-
4. Paste it in the terminal
|
|
190
|
-
|
|
191
|
-
Token is saved and auto-refreshes.
|
|
192
|
-
|
|
193
192
|
## Project Structure
|
|
194
193
|
|
|
195
194
|
```
|
|
196
195
|
gmail/
|
|
197
|
-
├── run.
|
|
196
|
+
├── run.ts # CLI entry point
|
|
198
197
|
├── lib/
|
|
199
|
-
│ ├── api.
|
|
200
|
-
│ ├── messages.
|
|
201
|
-
│ ├── threads.
|
|
202
|
-
│ ├── send.
|
|
203
|
-
│ ├── drafts.
|
|
204
|
-
│ ├── attachments.
|
|
205
|
-
│ └── labels.
|
|
206
|
-
├── credentials.json
|
|
207
|
-
└── token.json
|
|
198
|
+
│ ├── api.ts # Gmail API client + OAuth
|
|
199
|
+
│ ├── messages.ts # inbox, unread, search, read, archive...
|
|
200
|
+
│ ├── threads.ts # Thread operations
|
|
201
|
+
│ ├── send.ts # send, reply, forward (with attachment support)
|
|
202
|
+
│ ├── drafts.ts # Draft management, reply drafts
|
|
203
|
+
│ ├── attachments.ts # List, download attachments
|
|
204
|
+
│ └── labels.ts # Label management
|
|
205
|
+
├── credentials.json # OAuth credentials (gitignored)
|
|
206
|
+
└── token.json # Auth token (gitignored)
|
|
208
207
|
```
|
|
209
208
|
|
|
210
209
|
## Required Scopes
|
package/c4ai.json
CHANGED
package/lib/api.test.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
});
|