@cli4ai/gmail 1.0.2 → 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/lib/drafts.ts DELETED
@@ -1,434 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { getGmail, output, formatDate, encodeBase64Url, extractHeaders, extractBody, type OutputOptions, type MessagePayload } from './api';
4
-
5
- /** Attachment path */
6
- export interface AttachmentPath {
7
- path: string;
8
- mimeType?: string;
9
- }
10
-
11
- /** Draft data */
12
- export interface DraftData {
13
- id: string;
14
- messageId?: string;
15
- threadId?: string;
16
- to: string;
17
- cc?: string | null;
18
- subject: string;
19
- snippet?: string;
20
- body?: string;
21
- }
22
-
23
- /** Draft result */
24
- export interface DraftResult {
25
- ok: boolean;
26
- id?: string;
27
- messageId?: string;
28
- threadId?: string;
29
- to?: string;
30
- subject?: string;
31
- inReplyTo?: string;
32
- deleted?: string;
33
- sentDraft?: string;
34
- }
35
-
36
- /** Draft options */
37
- export interface DraftOptions extends OutputOptions {
38
- cc?: string;
39
- bcc?: string;
40
- attach?: string | string[];
41
- limit?: number;
42
- }
43
-
44
- /**
45
- * Create multipart message with attachment
46
- */
47
- export function createMultipartMessage({ to, cc, bcc, subject, body, inReplyTo, references, attachments = [] }: {
48
- to: string;
49
- cc?: string;
50
- bcc?: string;
51
- subject: string;
52
- body: string;
53
- inReplyTo?: string;
54
- references?: string;
55
- attachments?: AttachmentPath[];
56
- }): string {
57
- const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
58
-
59
- const headers: string[] = [];
60
- headers.push(`To: ${to}`);
61
- if (cc) headers.push(`Cc: ${cc}`);
62
- if (bcc) headers.push(`Bcc: ${bcc}`);
63
- headers.push(`Subject: ${subject}`);
64
- if (inReplyTo) {
65
- headers.push(`In-Reply-To: ${inReplyTo}`);
66
- headers.push(`References: ${references || inReplyTo}`);
67
- }
68
- headers.push('MIME-Version: 1.0');
69
- headers.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
70
-
71
- const parts: string[] = [];
72
-
73
- // Text body part
74
- parts.push(`--${boundary}`);
75
- parts.push('Content-Type: text/plain; charset=utf-8');
76
- parts.push('Content-Transfer-Encoding: 7bit');
77
- parts.push('');
78
- parts.push(body);
79
-
80
- // Attachment parts
81
- for (const att of attachments) {
82
- const filename = path.basename(att.path);
83
- const content = fs.readFileSync(att.path);
84
- const base64Content = content.toString('base64');
85
- const mimeType = att.mimeType || getMimeType(filename);
86
-
87
- parts.push(`--${boundary}`);
88
- parts.push(`Content-Type: ${mimeType}; name="${filename}"`);
89
- parts.push('Content-Transfer-Encoding: base64');
90
- parts.push(`Content-Disposition: attachment; filename="${filename}"`);
91
- parts.push('');
92
- // Split base64 into lines of 76 chars
93
- for (let i = 0; i < base64Content.length; i += 76) {
94
- parts.push(base64Content.slice(i, i + 76));
95
- }
96
- }
97
-
98
- parts.push(`--${boundary}--`);
99
-
100
- const message = headers.join('\r\n') + '\r\n\r\n' + parts.join('\r\n');
101
- return encodeBase64Url(message);
102
- }
103
-
104
- /**
105
- * Simple message without attachments
106
- */
107
- export function createSimpleMessage({ to, cc, bcc, subject, body, inReplyTo, references }: {
108
- to: string;
109
- cc?: string;
110
- bcc?: string;
111
- subject: string;
112
- body: string;
113
- inReplyTo?: string;
114
- references?: string;
115
- }): string {
116
- const lines: string[] = [];
117
- lines.push(`To: ${to}`);
118
- if (cc) lines.push(`Cc: ${cc}`);
119
- if (bcc) lines.push(`Bcc: ${bcc}`);
120
- lines.push(`Subject: ${subject}`);
121
- lines.push('Content-Type: text/plain; charset=utf-8');
122
- if (inReplyTo) {
123
- lines.push(`In-Reply-To: ${inReplyTo}`);
124
- lines.push(`References: ${references || inReplyTo}`);
125
- }
126
- lines.push('');
127
- lines.push(body);
128
- return encodeBase64Url(lines.join('\r\n'));
129
- }
130
-
131
- /**
132
- * Get MIME type from filename
133
- */
134
- function getMimeType(filename: string): string {
135
- const ext = path.extname(filename).toLowerCase();
136
- const mimeTypes: Record<string, string> = {
137
- '.pdf': 'application/pdf',
138
- '.doc': 'application/msword',
139
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
140
- '.xls': 'application/vnd.ms-excel',
141
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
142
- '.png': 'image/png',
143
- '.jpg': 'image/jpeg',
144
- '.jpeg': 'image/jpeg',
145
- '.gif': 'image/gif',
146
- '.txt': 'text/plain',
147
- '.csv': 'text/csv',
148
- '.zip': 'application/zip',
149
- '.json': 'application/json',
150
- '.xml': 'application/xml',
151
- '.html': 'text/html'
152
- };
153
- return mimeTypes[ext] || 'application/octet-stream';
154
- }
155
-
156
- /**
157
- * List all drafts
158
- */
159
- export async function list(options: DraftOptions = {}): Promise<DraftData[]> {
160
- const gmail = await getGmail();
161
- const limit = options.limit || 20;
162
-
163
- const res = await gmail.users.drafts.list({
164
- userId: 'me',
165
- maxResults: limit
166
- });
167
-
168
- if (!res.data.drafts || res.data.drafts.length === 0) {
169
- output([], options);
170
- return [];
171
- }
172
-
173
- // Get details for each draft
174
- const drafts = await Promise.all(
175
- res.data.drafts.map(async (draft) => {
176
- const draftRes = await gmail.users.drafts.get({
177
- userId: 'me',
178
- id: draft.id!,
179
- format: 'metadata',
180
- metadataHeaders: ['To', 'Subject', 'Date']
181
- });
182
-
183
- const headers = extractHeaders(draftRes.data.message?.payload?.headers as Array<{ name: string; value: string }>);
184
-
185
- return {
186
- id: draft.id!,
187
- messageId: draftRes.data.message?.id,
188
- to: headers.to || '(no recipient)',
189
- subject: headers.subject || '(no subject)',
190
- snippet: draftRes.data.message?.snippet || ''
191
- };
192
- })
193
- );
194
-
195
- output(drafts, options);
196
- return drafts;
197
- }
198
-
199
- /**
200
- * Get a draft
201
- */
202
- export async function get(draftId: string, options: OutputOptions = {}): Promise<DraftData> {
203
- const gmail = await getGmail();
204
-
205
- const res = await gmail.users.drafts.get({
206
- userId: 'me',
207
- id: draftId,
208
- format: 'full'
209
- });
210
-
211
- const headers = extractHeaders(res.data.message?.payload?.headers as Array<{ name: string; value: string }>);
212
- const body = extractBody(res.data.message?.payload as MessagePayload);
213
-
214
- const draft: DraftData = {
215
- id: res.data.id!,
216
- messageId: res.data.message?.id,
217
- threadId: res.data.message?.threadId || undefined,
218
- to: headers.to || '',
219
- cc: headers.cc || null,
220
- subject: headers.subject || '(no subject)',
221
- body: body
222
- };
223
-
224
- output(draft, options);
225
- return draft;
226
- }
227
-
228
- /**
229
- * Create a new draft
230
- */
231
- export async function create(to: string, subject: string, body: string, options: DraftOptions = {}): Promise<DraftResult> {
232
- const gmail = await getGmail();
233
-
234
- body = body.replace(/\\n/g, '\n');
235
-
236
- let raw: string;
237
- if (options.attach) {
238
- const attachments = Array.isArray(options.attach) ? options.attach : [options.attach];
239
- const attachmentPaths = attachments.map(a => ({ path: a }));
240
- raw = createMultipartMessage({
241
- to,
242
- cc: options.cc,
243
- bcc: options.bcc,
244
- subject,
245
- body,
246
- attachments: attachmentPaths
247
- });
248
- } else {
249
- raw = createSimpleMessage({
250
- to,
251
- cc: options.cc,
252
- bcc: options.bcc,
253
- subject,
254
- body
255
- });
256
- }
257
-
258
- const res = await gmail.users.drafts.create({
259
- userId: 'me',
260
- requestBody: {
261
- message: { raw }
262
- }
263
- });
264
-
265
- const result: DraftResult = {
266
- ok: true,
267
- id: res.data.id || undefined,
268
- messageId: res.data.message?.id,
269
- to,
270
- subject
271
- };
272
-
273
- output(result, options);
274
- return result;
275
- }
276
-
277
- /**
278
- * Create a draft reply to a message
279
- */
280
- export async function createReply(messageId: string, body: string, options: DraftOptions = {}): Promise<DraftResult> {
281
- const gmail = await getGmail();
282
-
283
- // Get original message
284
- const original = await gmail.users.messages.get({
285
- userId: 'me',
286
- id: messageId,
287
- format: 'metadata',
288
- metadataHeaders: ['From', 'To', 'Subject', 'Message-ID', 'References']
289
- });
290
-
291
- const headers = extractHeaders(original.data.payload?.headers as Array<{ name: string; value: string }>);
292
-
293
- // Determine reply-to address
294
- const replyTo = headers.from!;
295
- let subject = headers.subject || '';
296
- if (!subject.toLowerCase().startsWith('re:')) {
297
- subject = `Re: ${subject}`;
298
- }
299
-
300
- body = body.replace(/\\n/g, '\n');
301
-
302
- let raw: string;
303
- if (options.attach) {
304
- const attachments = Array.isArray(options.attach) ? options.attach : [options.attach];
305
- const attachmentPaths = attachments.map(a => ({ path: a }));
306
- raw = createMultipartMessage({
307
- to: replyTo,
308
- subject,
309
- body,
310
- inReplyTo: headers.message_id,
311
- references: headers.references ? `${headers.references} ${headers.message_id}` : headers.message_id,
312
- attachments: attachmentPaths
313
- });
314
- } else {
315
- raw = createSimpleMessage({
316
- to: replyTo,
317
- subject,
318
- body,
319
- inReplyTo: headers.message_id,
320
- references: headers.references ? `${headers.references} ${headers.message_id}` : headers.message_id
321
- });
322
- }
323
-
324
- const res = await gmail.users.drafts.create({
325
- userId: 'me',
326
- requestBody: {
327
- message: {
328
- raw,
329
- threadId: original.data.threadId || undefined
330
- }
331
- }
332
- });
333
-
334
- const result: DraftResult = {
335
- ok: true,
336
- id: res.data.id || undefined,
337
- messageId: res.data.message?.id,
338
- threadId: original.data.threadId || undefined,
339
- to: replyTo,
340
- subject,
341
- inReplyTo: messageId
342
- };
343
-
344
- output(result, options);
345
- return result;
346
- }
347
-
348
- /**
349
- * Update a draft
350
- */
351
- export async function update(draftId: string, to: string, subject: string, body: string, options: DraftOptions = {}): Promise<DraftResult> {
352
- const gmail = await getGmail();
353
-
354
- body = body.replace(/\\n/g, '\n');
355
-
356
- let raw: string;
357
- if (options.attach) {
358
- const attachments = Array.isArray(options.attach) ? options.attach : [options.attach];
359
- const attachmentPaths = attachments.map(a => ({ path: a }));
360
- raw = createMultipartMessage({
361
- to,
362
- cc: options.cc,
363
- subject,
364
- body,
365
- attachments: attachmentPaths
366
- });
367
- } else {
368
- raw = createSimpleMessage({
369
- to,
370
- cc: options.cc,
371
- subject,
372
- body
373
- });
374
- }
375
-
376
- const res = await gmail.users.drafts.update({
377
- userId: 'me',
378
- id: draftId,
379
- requestBody: {
380
- message: { raw }
381
- }
382
- });
383
-
384
- const result: DraftResult = {
385
- ok: true,
386
- id: res.data.id || undefined,
387
- messageId: res.data.message?.id,
388
- to,
389
- subject
390
- };
391
-
392
- output(result, options);
393
- return result;
394
- }
395
-
396
- /**
397
- * Delete a draft
398
- */
399
- export async function remove(draftId: string, options: OutputOptions = {}): Promise<DraftResult> {
400
- const gmail = await getGmail();
401
-
402
- await gmail.users.drafts.delete({
403
- userId: 'me',
404
- id: draftId
405
- });
406
-
407
- const result: DraftResult = { ok: true, deleted: draftId };
408
- output(result, options);
409
- return result;
410
- }
411
-
412
- /**
413
- * Send a draft
414
- */
415
- export async function send(draftId: string, options: OutputOptions = {}): Promise<DraftResult> {
416
- const gmail = await getGmail();
417
-
418
- const res = await gmail.users.drafts.send({
419
- userId: 'me',
420
- requestBody: {
421
- id: draftId
422
- }
423
- });
424
-
425
- const result: DraftResult = {
426
- ok: true,
427
- id: res.data.id || undefined,
428
- threadId: res.data.threadId || undefined,
429
- sentDraft: draftId
430
- };
431
-
432
- output(result, options);
433
- return result;
434
- }
package/lib/labels.ts DELETED
@@ -1,198 +0,0 @@
1
- import { getGmail, output, type OutputOptions } from './api';
2
-
3
- /** Label data */
4
- export interface LabelData {
5
- id: string;
6
- name: string;
7
- type?: string;
8
- messageListVisibility?: string;
9
- labelListVisibility?: string;
10
- messagesTotal?: number;
11
- messagesUnread?: number;
12
- threadsTotal?: number;
13
- threadsUnread?: number;
14
- }
15
-
16
- /** Label action result */
17
- export interface LabelActionResult {
18
- ok: boolean;
19
- id?: string;
20
- name?: string;
21
- messageId?: string;
22
- labelAdded?: string;
23
- labelRemoved?: string;
24
- deleted?: string;
25
- }
26
-
27
- /**
28
- * List all labels
29
- */
30
- export async function list(options: OutputOptions = {}): Promise<LabelData[]> {
31
- const gmail = await getGmail();
32
-
33
- const res = await gmail.users.labels.list({
34
- userId: 'me'
35
- });
36
-
37
- const labels: LabelData[] = (res.data.labels || []).map(label => ({
38
- id: label.id!,
39
- name: label.name!,
40
- type: label.type || undefined,
41
- messageListVisibility: label.messageListVisibility || undefined,
42
- labelListVisibility: label.labelListVisibility || undefined
43
- }));
44
-
45
- // Sort: system labels first, then user labels
46
- labels.sort((a, b) => {
47
- if (a.type === 'system' && b.type !== 'system') return -1;
48
- if (a.type !== 'system' && b.type === 'system') return 1;
49
- return a.name.localeCompare(b.name);
50
- });
51
-
52
- output(labels, options);
53
- return labels;
54
- }
55
-
56
- /**
57
- * Get label details with counts
58
- */
59
- export async function get(labelId: string, options: OutputOptions = {}): Promise<LabelData> {
60
- const gmail = await getGmail();
61
-
62
- // Resolve label name to ID if needed
63
- const resolvedId = await resolveLabelId(gmail, labelId);
64
-
65
- const res = await gmail.users.labels.get({
66
- userId: 'me',
67
- id: resolvedId
68
- });
69
-
70
- const label: LabelData = {
71
- id: res.data.id!,
72
- name: res.data.name!,
73
- type: res.data.type || undefined,
74
- messagesTotal: res.data.messagesTotal || undefined,
75
- messagesUnread: res.data.messagesUnread || undefined,
76
- threadsTotal: res.data.threadsTotal || undefined,
77
- threadsUnread: res.data.threadsUnread || undefined
78
- };
79
-
80
- output(label, options);
81
- return label;
82
- }
83
-
84
- /**
85
- * Add label to message
86
- */
87
- export async function addToMessage(messageId: string, labelName: string, options: OutputOptions = {}): Promise<LabelActionResult> {
88
- const gmail = await getGmail();
89
-
90
- const labelId = await resolveLabelId(gmail, labelName);
91
-
92
- await gmail.users.messages.modify({
93
- userId: 'me',
94
- id: messageId,
95
- requestBody: {
96
- addLabelIds: [labelId]
97
- }
98
- });
99
-
100
- const result: LabelActionResult = { ok: true, messageId, labelAdded: labelName };
101
- output(result, options);
102
- return result;
103
- }
104
-
105
- /**
106
- * Remove label from message
107
- */
108
- export async function removeFromMessage(messageId: string, labelName: string, options: OutputOptions = {}): Promise<LabelActionResult> {
109
- const gmail = await getGmail();
110
-
111
- const labelId = await resolveLabelId(gmail, labelName);
112
-
113
- await gmail.users.messages.modify({
114
- userId: 'me',
115
- id: messageId,
116
- requestBody: {
117
- removeLabelIds: [labelId]
118
- }
119
- });
120
-
121
- const result: LabelActionResult = { ok: true, messageId, labelRemoved: labelName };
122
- output(result, options);
123
- return result;
124
- }
125
-
126
- /**
127
- * Create a new label
128
- */
129
- export async function create(labelName: string, options: OutputOptions = {}): Promise<LabelActionResult> {
130
- const gmail = await getGmail();
131
-
132
- const res = await gmail.users.labels.create({
133
- userId: 'me',
134
- requestBody: {
135
- name: labelName,
136
- labelListVisibility: 'labelShow',
137
- messageListVisibility: 'show'
138
- }
139
- });
140
-
141
- const result: LabelActionResult = {
142
- ok: true,
143
- id: res.data.id!,
144
- name: res.data.name!
145
- };
146
-
147
- output(result, options);
148
- return result;
149
- }
150
-
151
- /**
152
- * Delete a label
153
- */
154
- export async function remove(labelName: string, options: OutputOptions = {}): Promise<LabelActionResult> {
155
- const gmail = await getGmail();
156
-
157
- const labelId = await resolveLabelId(gmail, labelName);
158
-
159
- // Don't allow deleting system labels
160
- const systemLabels = ['INBOX', 'SENT', 'TRASH', 'SPAM', 'STARRED', 'UNREAD', 'IMPORTANT', 'DRAFT'];
161
- if (labelId.startsWith('CATEGORY_') || systemLabels.includes(labelId)) {
162
- throw new Error(`Cannot delete system label: ${labelName}`);
163
- }
164
-
165
- await gmail.users.labels.delete({
166
- userId: 'me',
167
- id: labelId
168
- });
169
-
170
- const result: LabelActionResult = { ok: true, deleted: labelName };
171
- output(result, options);
172
- return result;
173
- }
174
-
175
- /**
176
- * Resolve label name to ID
177
- */
178
- async function resolveLabelId(gmail: ReturnType<typeof import('googleapis').google.gmail>, labelNameOrId: string): Promise<string> {
179
- // Check if already an ID (system labels or Label_xxx format)
180
- const systemLabels = ['INBOX', 'SENT', 'TRASH', 'SPAM', 'STARRED', 'UNREAD', 'IMPORTANT', 'DRAFT', 'CHAT'];
181
- if (labelNameOrId.startsWith('Label_') ||
182
- labelNameOrId.startsWith('CATEGORY_') ||
183
- systemLabels.includes(labelNameOrId.toUpperCase())) {
184
- return labelNameOrId.toUpperCase();
185
- }
186
-
187
- // Look up by name
188
- const res = await gmail.users.labels.list({ userId: 'me' });
189
- const label = res.data.labels?.find(
190
- l => l.name?.toLowerCase() === labelNameOrId.toLowerCase()
191
- );
192
-
193
- if (!label) {
194
- throw new Error(`Label not found: ${labelNameOrId}`);
195
- }
196
-
197
- return label.id!;
198
- }