@cli4ai/gmail 1.0.4 → 1.0.6

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/send.ts ADDED
@@ -0,0 +1,331 @@
1
+ import { getGmail, output, encodeBase64Url, extractHeaders, extractBody, type OutputOptions, type MessagePayload } from './api';
2
+ import { createMultipartMessage, createSimpleMessage } from './drafts';
3
+
4
+ /** Send result */
5
+ export interface SendResult {
6
+ ok: boolean;
7
+ id?: string;
8
+ threadId?: string;
9
+ to?: string;
10
+ subject?: string;
11
+ inReplyTo?: string;
12
+ forwardedFrom?: string;
13
+ }
14
+
15
+ /** Send options */
16
+ export interface SendOptions extends OutputOptions {
17
+ cc?: string;
18
+ bcc?: string;
19
+ attach?: string | string[];
20
+ }
21
+
22
+ /**
23
+ * Create raw email message
24
+ */
25
+ function createRawMessage({ to, cc, bcc, subject, body, inReplyTo, references }: {
26
+ to: string;
27
+ cc?: string;
28
+ bcc?: string;
29
+ subject: string;
30
+ body: string;
31
+ inReplyTo?: string;
32
+ references?: string;
33
+ }): string {
34
+ const lines: string[] = [];
35
+
36
+ lines.push(`To: ${to}`);
37
+ if (cc) lines.push(`Cc: ${cc}`);
38
+ if (bcc) lines.push(`Bcc: ${bcc}`);
39
+ lines.push(`Subject: ${subject}`);
40
+ lines.push('Content-Type: text/plain; charset=utf-8');
41
+
42
+ if (inReplyTo) {
43
+ lines.push(`In-Reply-To: ${inReplyTo}`);
44
+ lines.push(`References: ${references || inReplyTo}`);
45
+ }
46
+
47
+ lines.push('');
48
+ lines.push(body);
49
+
50
+ return encodeBase64Url(lines.join('\r\n'));
51
+ }
52
+
53
+ /**
54
+ * Send a new email
55
+ */
56
+ export async function send(to: string, subject: string, body: string, options: SendOptions = {}): Promise<SendResult> {
57
+ const gmail = await getGmail();
58
+
59
+ // Process newlines in body
60
+ body = body.replace(/\\n/g, '\n');
61
+
62
+ let raw: string;
63
+ if (options.attach) {
64
+ const attachments = Array.isArray(options.attach) ? options.attach : [options.attach];
65
+ const attachmentPaths = attachments.map(a => ({ path: a }));
66
+ raw = createMultipartMessage({
67
+ to,
68
+ cc: options.cc,
69
+ bcc: options.bcc,
70
+ subject,
71
+ body,
72
+ attachments: attachmentPaths
73
+ });
74
+ } else {
75
+ raw = createRawMessage({
76
+ to,
77
+ cc: options.cc,
78
+ bcc: options.bcc,
79
+ subject,
80
+ body
81
+ });
82
+ }
83
+
84
+ const res = await gmail.users.messages.send({
85
+ userId: 'me',
86
+ requestBody: { raw }
87
+ });
88
+
89
+ const result: SendResult = {
90
+ ok: true,
91
+ id: res.data.id || undefined,
92
+ threadId: res.data.threadId || undefined,
93
+ to,
94
+ subject
95
+ };
96
+
97
+ output(result, options);
98
+ return result;
99
+ }
100
+
101
+ /**
102
+ * Reply to a message
103
+ */
104
+ export async function reply(messageId: string, body: string, options: SendOptions = {}): Promise<SendResult> {
105
+ const gmail = await getGmail();
106
+
107
+ // Get original message to extract headers
108
+ const original = await gmail.users.messages.get({
109
+ userId: 'me',
110
+ id: messageId,
111
+ format: 'metadata',
112
+ metadataHeaders: ['From', 'To', 'Subject', 'Message-ID', 'References']
113
+ });
114
+
115
+ const headers = extractHeaders(original.data.payload?.headers as Array<{ name: string; value: string }>);
116
+
117
+ // Determine who to reply to
118
+ const replyTo = headers.from!;
119
+ let subject = headers.subject || '';
120
+ if (!subject.toLowerCase().startsWith('re:')) {
121
+ subject = `Re: ${subject}`;
122
+ }
123
+
124
+ // Process newlines in body
125
+ body = body.replace(/\\n/g, '\n');
126
+
127
+ let raw: string;
128
+ if (options.attach) {
129
+ const attachments = Array.isArray(options.attach) ? options.attach : [options.attach];
130
+ const attachmentPaths = attachments.map(a => ({ path: a }));
131
+ raw = createMultipartMessage({
132
+ to: replyTo,
133
+ subject,
134
+ body,
135
+ inReplyTo: headers.message_id,
136
+ references: headers.references ? `${headers.references} ${headers.message_id}` : headers.message_id,
137
+ attachments: attachmentPaths
138
+ });
139
+ } else {
140
+ raw = createRawMessage({
141
+ to: replyTo,
142
+ subject,
143
+ body,
144
+ inReplyTo: headers.message_id,
145
+ references: headers.references ? `${headers.references} ${headers.message_id}` : headers.message_id
146
+ });
147
+ }
148
+
149
+ const res = await gmail.users.messages.send({
150
+ userId: 'me',
151
+ requestBody: {
152
+ raw,
153
+ threadId: original.data.threadId || undefined
154
+ }
155
+ });
156
+
157
+ const result: SendResult = {
158
+ ok: true,
159
+ id: res.data.id || undefined,
160
+ threadId: res.data.threadId || undefined,
161
+ to: replyTo,
162
+ subject,
163
+ inReplyTo: messageId
164
+ };
165
+
166
+ output(result, options);
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Reply all
172
+ */
173
+ export async function replyAll(messageId: string, body: string, options: SendOptions = {}): Promise<SendResult> {
174
+ const gmail = await getGmail();
175
+
176
+ // Get original message
177
+ const original = await gmail.users.messages.get({
178
+ userId: 'me',
179
+ id: messageId,
180
+ format: 'metadata',
181
+ metadataHeaders: ['From', 'To', 'Cc', 'Subject', 'Message-ID', 'References']
182
+ });
183
+
184
+ const headers = extractHeaders(original.data.payload?.headers as Array<{ name: string; value: string }>);
185
+
186
+ // Get my email address
187
+ const profile = await gmail.users.getProfile({ userId: 'me' });
188
+ const myEmail = profile.data.emailAddress!;
189
+
190
+ // Build recipient list (original From + all To/Cc, excluding myself)
191
+ const allRecipients = new Set<string>();
192
+ allRecipients.add(headers.from!);
193
+
194
+ if (headers.to) {
195
+ headers.to.split(',').forEach(email => allRecipients.add(email.trim()));
196
+ }
197
+ if (headers.cc) {
198
+ headers.cc.split(',').forEach(email => allRecipients.add(email.trim()));
199
+ }
200
+
201
+ // Remove myself from recipients
202
+ const recipients = Array.from(allRecipients).filter(
203
+ email => !email.toLowerCase().includes(myEmail.toLowerCase())
204
+ );
205
+
206
+ let subject = headers.subject || '';
207
+ if (!subject.toLowerCase().startsWith('re:')) {
208
+ subject = `Re: ${subject}`;
209
+ }
210
+
211
+ body = body.replace(/\\n/g, '\n');
212
+
213
+ const raw = createRawMessage({
214
+ to: recipients.join(', '),
215
+ subject,
216
+ body,
217
+ inReplyTo: headers.message_id,
218
+ references: headers.references ? `${headers.references} ${headers.message_id}` : headers.message_id
219
+ });
220
+
221
+ const res = await gmail.users.messages.send({
222
+ userId: 'me',
223
+ requestBody: {
224
+ raw,
225
+ threadId: original.data.threadId || undefined
226
+ }
227
+ });
228
+
229
+ const result: SendResult = {
230
+ ok: true,
231
+ id: res.data.id || undefined,
232
+ threadId: res.data.threadId || undefined,
233
+ to: recipients.join(', '),
234
+ subject,
235
+ inReplyTo: messageId
236
+ };
237
+
238
+ output(result, options);
239
+ return result;
240
+ }
241
+
242
+ /**
243
+ * Forward a message
244
+ */
245
+ export async function forward(messageId: string, to: string, body?: string, options: OutputOptions = {}): Promise<SendResult> {
246
+ const gmail = await getGmail();
247
+
248
+ // Get original message with full body
249
+ const original = await gmail.users.messages.get({
250
+ userId: 'me',
251
+ id: messageId,
252
+ format: 'full'
253
+ });
254
+
255
+ const headers = extractHeaders(original.data.payload?.headers as Array<{ name: string; value: string }>);
256
+ const originalBody = extractBody(original.data.payload as MessagePayload);
257
+
258
+ let subject = headers.subject || '';
259
+ if (!subject.toLowerCase().startsWith('fwd:')) {
260
+ subject = `Fwd: ${subject}`;
261
+ }
262
+
263
+ // Build forwarded message body
264
+ const forwardedContent = [
265
+ body ? body.replace(/\\n/g, '\n') : '',
266
+ '',
267
+ '---------- Forwarded message ----------',
268
+ `From: ${headers.from}`,
269
+ `Date: ${headers.date}`,
270
+ `Subject: ${headers.subject}`,
271
+ `To: ${headers.to}`,
272
+ '',
273
+ originalBody
274
+ ].join('\n');
275
+
276
+ const raw = createRawMessage({
277
+ to,
278
+ subject,
279
+ body: forwardedContent
280
+ });
281
+
282
+ const res = await gmail.users.messages.send({
283
+ userId: 'me',
284
+ requestBody: { raw }
285
+ });
286
+
287
+ const result: SendResult = {
288
+ ok: true,
289
+ id: res.data.id || undefined,
290
+ threadId: res.data.threadId || undefined,
291
+ to,
292
+ subject,
293
+ forwardedFrom: messageId
294
+ };
295
+
296
+ output(result, options);
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * Create draft
302
+ */
303
+ export async function draft(to: string, subject: string, body: string, options: SendOptions = {}): Promise<SendResult> {
304
+ const gmail = await getGmail();
305
+
306
+ body = body.replace(/\\n/g, '\n');
307
+
308
+ const raw = createRawMessage({
309
+ to,
310
+ cc: options.cc,
311
+ subject,
312
+ body
313
+ });
314
+
315
+ const res = await gmail.users.drafts.create({
316
+ userId: 'me',
317
+ requestBody: {
318
+ message: { raw }
319
+ }
320
+ });
321
+
322
+ const result: SendResult = {
323
+ ok: true,
324
+ id: res.data.id || undefined,
325
+ to,
326
+ subject
327
+ };
328
+
329
+ output(result, options);
330
+ return result;
331
+ }
package/lib/threads.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { getGmail, output, formatDate, extractHeaders, extractBody, cleanBody, type OutputOptions, type MessagePayload } from './api';
2
+
3
+ /** Thread message */
4
+ export interface ThreadMessage {
5
+ id: string;
6
+ date: string | null;
7
+ from: string;
8
+ to: string;
9
+ subject: string;
10
+ body: string;
11
+ }
12
+
13
+ /** Thread data */
14
+ export interface ThreadData {
15
+ id: string;
16
+ subject: string;
17
+ messageCount: number;
18
+ messages: ThreadMessage[];
19
+ }
20
+
21
+ /** Thread summary */
22
+ export interface ThreadSummary {
23
+ id: string;
24
+ subject: string;
25
+ from: string;
26
+ lastFrom: string;
27
+ date: string | null;
28
+ messageCount: number;
29
+ snippet: string;
30
+ }
31
+
32
+ /** Thread action result */
33
+ export interface ThreadActionResult {
34
+ ok: boolean;
35
+ archived?: string;
36
+ trashed?: string;
37
+ }
38
+
39
+ /** Thread options */
40
+ export interface ThreadOptions extends OutputOptions {
41
+ fullBody?: boolean;
42
+ limit?: number;
43
+ query?: string;
44
+ }
45
+
46
+ /**
47
+ * Get full thread (conversation)
48
+ */
49
+ export async function get(threadId: string, options: ThreadOptions = {}): Promise<ThreadData> {
50
+ const gmail = await getGmail();
51
+
52
+ const res = await gmail.users.threads.get({
53
+ userId: 'me',
54
+ id: threadId,
55
+ format: 'full'
56
+ });
57
+
58
+ const messages: ThreadMessage[] = (res.data.messages || []).map(msg => {
59
+ const headers = extractHeaders(msg.payload?.headers as Array<{ name: string; value: string }>);
60
+ return {
61
+ id: msg.id!,
62
+ date: formatDate(msg.internalDate),
63
+ from: headers.from || '',
64
+ to: headers.to || '',
65
+ subject: headers.subject || '(no subject)',
66
+ body: cleanBody(extractBody(msg.payload as MessagePayload), options.fullBody ? null : 2000)
67
+ };
68
+ });
69
+
70
+ const thread: ThreadData = {
71
+ id: res.data.id!,
72
+ subject: messages[0]?.subject || '(no subject)',
73
+ messageCount: messages.length,
74
+ messages: messages
75
+ };
76
+
77
+ output(thread, options);
78
+ return thread;
79
+ }
80
+
81
+ /**
82
+ * List recent threads
83
+ */
84
+ export async function list(options: ThreadOptions = {}): Promise<ThreadSummary[]> {
85
+ const gmail = await getGmail();
86
+ const limit = options.limit || 20;
87
+ const query = options.query || '';
88
+
89
+ const res = await gmail.users.threads.list({
90
+ userId: 'me',
91
+ maxResults: limit,
92
+ q: query
93
+ });
94
+
95
+ if (!res.data.threads || res.data.threads.length === 0) {
96
+ output([], options);
97
+ return [];
98
+ }
99
+
100
+ // Get summary of each thread
101
+ const threads = await Promise.all(
102
+ res.data.threads.map(async (thread) => {
103
+ const threadData = await gmail.users.threads.get({
104
+ userId: 'me',
105
+ id: thread.id!,
106
+ format: 'metadata',
107
+ metadataHeaders: ['From', 'Subject', 'Date']
108
+ });
109
+
110
+ const firstMsg = threadData.data.messages?.[0];
111
+ const lastMsg = threadData.data.messages?.[threadData.data.messages.length - 1];
112
+ const firstHeaders = extractHeaders(firstMsg?.payload?.headers as Array<{ name: string; value: string }>);
113
+ const lastHeaders = extractHeaders(lastMsg?.payload?.headers as Array<{ name: string; value: string }>);
114
+
115
+ return {
116
+ id: thread.id!,
117
+ subject: firstHeaders.subject || '(no subject)',
118
+ from: firstHeaders.from || '',
119
+ lastFrom: lastHeaders.from || '',
120
+ date: formatDate(lastMsg?.internalDate),
121
+ messageCount: threadData.data.messages?.length || 0,
122
+ snippet: thread.snippet || ''
123
+ };
124
+ })
125
+ );
126
+
127
+ output(threads, options);
128
+ return threads;
129
+ }
130
+
131
+ /**
132
+ * Archive entire thread
133
+ */
134
+ export async function archive(threadId: string, options: OutputOptions = {}): Promise<ThreadActionResult> {
135
+ const gmail = await getGmail();
136
+
137
+ await gmail.users.threads.modify({
138
+ userId: 'me',
139
+ id: threadId,
140
+ requestBody: {
141
+ removeLabelIds: ['INBOX']
142
+ }
143
+ });
144
+
145
+ const result: ThreadActionResult = { ok: true, archived: threadId };
146
+ output(result, options);
147
+ return result;
148
+ }
149
+
150
+ /**
151
+ * Trash entire thread
152
+ */
153
+ export async function trash(threadId: string, options: OutputOptions = {}): Promise<ThreadActionResult> {
154
+ const gmail = await getGmail();
155
+
156
+ await gmail.users.threads.trash({
157
+ userId: 'me',
158
+ id: threadId
159
+ });
160
+
161
+ const result: ThreadActionResult = { ok: true, trashed: threadId };
162
+ output(result, options);
163
+ return result;
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli4ai/gmail",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Gmail CLI tool for messages, threads, and drafts",
5
5
  "author": "cliforai",
6
6
  "license": "MIT",
@@ -28,6 +28,7 @@
28
28
  "url": "https://github.com/cliforai/packages/issues"
29
29
  },
30
30
  "dependencies": {
31
+ "@cli4ai/lib": "^1.0.2",
31
32
  "googleapis": "^144.0.0",
32
33
  "google-auth-library": "^9.0.0",
33
34
  "commander": "^14.0.0"
@@ -35,6 +36,7 @@
35
36
  "files": [
36
37
  "run.ts",
37
38
  "cli4ai.json",
39
+ "lib/**/*",
38
40
  "README.md",
39
41
  "LICENSE"
40
42
  ],