@cli4ai/gmail 1.0.7 → 1.0.9

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/cli4ai.json CHANGED
@@ -1,38 +1,271 @@
1
1
  {
2
2
  "name": "gmail",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Gmail CLI tool for messages, threads, and drafts",
5
5
  "author": "cliforai",
6
6
  "license": "MIT",
7
7
  "entry": "run.ts",
8
8
  "runtime": "bun",
9
- "keywords": ["gmail", "email", "google", "mail"],
9
+ "keywords": [
10
+ "gmail",
11
+ "email",
12
+ "google",
13
+ "mail"
14
+ ],
10
15
  "commands": {
11
- "inbox": { "description": "Recent inbox messages", "args": [{ "name": "limit", "required": false }] },
12
- "unread": { "description": "Unread messages only", "args": [{ "name": "limit", "required": false }] },
13
- "search": { "description": "Search with Gmail syntax", "args": [{ "name": "query", "required": true }, { "name": "limit", "required": false }] },
14
- "read": { "description": "Read full message", "args": [{ "name": "id", "required": true }] },
15
- "archive": { "description": "Archive message", "args": [{ "name": "id", "required": true }] },
16
- "trash": { "description": "Move to trash", "args": [{ "name": "id", "required": true }] },
17
- "untrash": { "description": "Remove from trash", "args": [{ "name": "id", "required": true }] },
18
- "star": { "description": "Star message", "args": [{ "name": "id", "required": true }] },
19
- "unstar": { "description": "Unstar message", "args": [{ "name": "id", "required": true }] },
20
- "markread": { "description": "Mark as read", "args": [{ "name": "id", "required": true }] },
21
- "markunread": { "description": "Mark as unread", "args": [{ "name": "id", "required": true }] },
22
- "thread": { "description": "Full conversation thread", "args": [{ "name": "id", "required": true }] },
23
- "threads": { "description": "List recent threads", "args": [{ "name": "limit", "required": false }] },
24
- "send": { "description": "Send new email", "args": [{ "name": "to", "required": true }, { "name": "subject", "required": true }, { "name": "body", "required": true }] },
25
- "reply": { "description": "Reply to message", "args": [{ "name": "id", "required": true }, { "name": "body", "required": true }] },
26
- "replyall": { "description": "Reply all", "args": [{ "name": "id", "required": true }, { "name": "body", "required": true }] },
27
- "forward": { "description": "Forward message", "args": [{ "name": "id", "required": true }, { "name": "to", "required": true }] },
28
- "draft": { "description": "Create draft", "args": [{ "name": "to", "required": true }, { "name": "subject", "required": true }, { "name": "body", "required": true }] },
29
- "labels": { "description": "List all labels" },
30
- "label": { "description": "Add label to message", "args": [{ "name": "id", "required": true }, { "name": "label", "required": true }] },
31
- "unlabel": { "description": "Remove label from message", "args": [{ "name": "id", "required": true }, { "name": "label", "required": true }] },
32
- "attachments": { "description": "List attachments in message", "args": [{ "name": "id", "required": true }] },
33
- "download": { "description": "Download attachment", "args": [{ "name": "id", "required": true }] },
34
- "drafts": { "description": "List all drafts" },
35
- "draft-send": { "description": "Send draft", "args": [{ "name": "id", "required": true }] }
16
+ "inbox": {
17
+ "description": "Recent inbox messages",
18
+ "args": [
19
+ {
20
+ "name": "limit",
21
+ "required": false
22
+ }
23
+ ]
24
+ },
25
+ "unread": {
26
+ "description": "Unread messages only",
27
+ "args": [
28
+ {
29
+ "name": "limit",
30
+ "required": false
31
+ }
32
+ ]
33
+ },
34
+ "search": {
35
+ "description": "Search with Gmail syntax",
36
+ "args": [
37
+ {
38
+ "name": "query",
39
+ "required": true
40
+ },
41
+ {
42
+ "name": "limit",
43
+ "required": false
44
+ }
45
+ ]
46
+ },
47
+ "read": {
48
+ "description": "Read full message",
49
+ "args": [
50
+ {
51
+ "name": "id",
52
+ "required": true
53
+ }
54
+ ]
55
+ },
56
+ "archive": {
57
+ "description": "Archive message",
58
+ "args": [
59
+ {
60
+ "name": "id",
61
+ "required": true
62
+ }
63
+ ]
64
+ },
65
+ "trash": {
66
+ "description": "Move to trash",
67
+ "args": [
68
+ {
69
+ "name": "id",
70
+ "required": true
71
+ }
72
+ ]
73
+ },
74
+ "untrash": {
75
+ "description": "Remove from trash",
76
+ "args": [
77
+ {
78
+ "name": "id",
79
+ "required": true
80
+ }
81
+ ]
82
+ },
83
+ "star": {
84
+ "description": "Star message",
85
+ "args": [
86
+ {
87
+ "name": "id",
88
+ "required": true
89
+ }
90
+ ]
91
+ },
92
+ "unstar": {
93
+ "description": "Unstar message",
94
+ "args": [
95
+ {
96
+ "name": "id",
97
+ "required": true
98
+ }
99
+ ]
100
+ },
101
+ "markread": {
102
+ "description": "Mark as read",
103
+ "args": [
104
+ {
105
+ "name": "id",
106
+ "required": true
107
+ }
108
+ ]
109
+ },
110
+ "markunread": {
111
+ "description": "Mark as unread",
112
+ "args": [
113
+ {
114
+ "name": "id",
115
+ "required": true
116
+ }
117
+ ]
118
+ },
119
+ "thread": {
120
+ "description": "Full conversation thread",
121
+ "args": [
122
+ {
123
+ "name": "id",
124
+ "required": true
125
+ }
126
+ ]
127
+ },
128
+ "threads": {
129
+ "description": "List recent threads",
130
+ "args": [
131
+ {
132
+ "name": "limit",
133
+ "required": false
134
+ }
135
+ ]
136
+ },
137
+ "send": {
138
+ "description": "Send new email",
139
+ "args": [
140
+ {
141
+ "name": "to",
142
+ "required": true
143
+ },
144
+ {
145
+ "name": "subject",
146
+ "required": true
147
+ },
148
+ {
149
+ "name": "body",
150
+ "required": true
151
+ }
152
+ ]
153
+ },
154
+ "reply": {
155
+ "description": "Reply to message",
156
+ "args": [
157
+ {
158
+ "name": "id",
159
+ "required": true
160
+ },
161
+ {
162
+ "name": "body",
163
+ "required": true
164
+ }
165
+ ]
166
+ },
167
+ "replyall": {
168
+ "description": "Reply all",
169
+ "args": [
170
+ {
171
+ "name": "id",
172
+ "required": true
173
+ },
174
+ {
175
+ "name": "body",
176
+ "required": true
177
+ }
178
+ ]
179
+ },
180
+ "forward": {
181
+ "description": "Forward message",
182
+ "args": [
183
+ {
184
+ "name": "id",
185
+ "required": true
186
+ },
187
+ {
188
+ "name": "to",
189
+ "required": true
190
+ }
191
+ ]
192
+ },
193
+ "draft": {
194
+ "description": "Create draft",
195
+ "args": [
196
+ {
197
+ "name": "to",
198
+ "required": true
199
+ },
200
+ {
201
+ "name": "subject",
202
+ "required": true
203
+ },
204
+ {
205
+ "name": "body",
206
+ "required": true
207
+ }
208
+ ]
209
+ },
210
+ "labels": {
211
+ "description": "List all labels"
212
+ },
213
+ "label": {
214
+ "description": "Add label to message",
215
+ "args": [
216
+ {
217
+ "name": "id",
218
+ "required": true
219
+ },
220
+ {
221
+ "name": "label",
222
+ "required": true
223
+ }
224
+ ]
225
+ },
226
+ "unlabel": {
227
+ "description": "Remove label from message",
228
+ "args": [
229
+ {
230
+ "name": "id",
231
+ "required": true
232
+ },
233
+ {
234
+ "name": "label",
235
+ "required": true
236
+ }
237
+ ]
238
+ },
239
+ "attachments": {
240
+ "description": "List attachments in message",
241
+ "args": [
242
+ {
243
+ "name": "id",
244
+ "required": true
245
+ }
246
+ ]
247
+ },
248
+ "download": {
249
+ "description": "Download attachment",
250
+ "args": [
251
+ {
252
+ "name": "id",
253
+ "required": true
254
+ }
255
+ ]
256
+ },
257
+ "drafts": {
258
+ "description": "List all drafts"
259
+ },
260
+ "draft-send": {
261
+ "description": "Send draft",
262
+ "args": [
263
+ {
264
+ "name": "id",
265
+ "required": true
266
+ }
267
+ ]
268
+ }
36
269
  },
37
270
  "dependencies": {
38
271
  "googleapis": "^144.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli4ai/gmail",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Gmail CLI tool for messages, threads, and drafts",
5
5
  "author": "cliforai",
6
6
  "license": "MIT",
@@ -28,7 +28,6 @@
28
28
  "url": "https://github.com/cliforai/packages/issues"
29
29
  },
30
30
  "dependencies": {
31
- "@cli4ai/lib": "^1.0.2",
32
31
  "googleapis": "^144.0.0",
33
32
  "google-auth-library": "^9.0.0",
34
33
  "commander": "^14.0.0"
@@ -36,7 +35,6 @@
36
35
  "files": [
37
36
  "run.ts",
38
37
  "cli4ai.json",
39
- "lib/**/*",
40
38
  "README.md",
41
39
  "LICENSE"
42
40
  ],
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
- });