@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/lib/api.ts DELETED
@@ -1,299 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { google, gmail_v1 } from 'googleapis';
4
- import readline from 'readline';
5
- import type { OAuth2Client } from 'google-auth-library';
6
-
7
- // Load env from root .env
8
- const SCRIPT_DIR = path.dirname(import.meta.dir);
9
- const ENV_FILE = path.join(SCRIPT_DIR, '../.env');
10
- if (fs.existsSync(ENV_FILE)) {
11
- const envContent = fs.readFileSync(ENV_FILE, 'utf-8');
12
- envContent.split('\n').forEach(line => {
13
- const [key, ...vals] = line.split('=');
14
- if (key && !key.startsWith('#')) {
15
- process.env[key.trim()] = vals.join('=').trim();
16
- }
17
- });
18
- }
19
-
20
- // Paths for credentials and token
21
- const CREDENTIALS_PATH = process.env.GMAIL_CREDENTIALS_PATH || path.join(SCRIPT_DIR, 'credentials.json');
22
- const TOKEN_PATH = process.env.GMAIL_TOKEN_PATH || path.join(SCRIPT_DIR, 'token.json');
23
-
24
- // Scopes for Gmail access
25
- const SCOPES = [
26
- 'https://www.googleapis.com/auth/gmail.modify',
27
- 'https://www.googleapis.com/auth/gmail.compose',
28
- 'https://www.googleapis.com/auth/gmail.send'
29
- ];
30
-
31
- let gmailClient: gmail_v1.Gmail | null = null;
32
- let authClient: OAuth2Client | null = null;
33
-
34
- /** Output options */
35
- export interface OutputOptions {
36
- raw?: boolean;
37
- compact?: boolean;
38
- body?: boolean;
39
- fullBody?: boolean;
40
- attach?: string | string[];
41
- cc?: string;
42
- bcc?: string;
43
- limit?: number;
44
- query?: string;
45
- }
46
-
47
- /** Message output data for compact display */
48
- export interface MessageOutputData {
49
- date?: string | null;
50
- from?: string;
51
- subject?: string;
52
- }
53
-
54
- /** Email headers */
55
- export interface EmailHeaders {
56
- from?: string;
57
- to?: string;
58
- cc?: string;
59
- bcc?: string;
60
- subject?: string;
61
- date?: string;
62
- reply_to?: string;
63
- message_id?: string;
64
- references?: string;
65
- }
66
-
67
- /** Message payload */
68
- export interface MessagePayload {
69
- headers?: Array<{ name: string; value: string }>;
70
- body?: { data?: string; size?: number; attachmentId?: string };
71
- parts?: MessagePayload[];
72
- mimeType?: string;
73
- filename?: string;
74
- partId?: string;
75
- }
76
-
77
- /**
78
- * Get new token via OAuth flow
79
- */
80
- async function getNewToken(oAuth2Client: OAuth2Client): Promise<void> {
81
- return new Promise((resolve, reject) => {
82
- const authUrl = oAuth2Client.generateAuthUrl({
83
- access_type: 'offline',
84
- scope: SCOPES,
85
- });
86
- console.error('Authorize this app by visiting this url:');
87
- console.error(authUrl);
88
-
89
- const rl = readline.createInterface({
90
- input: process.stdin,
91
- output: process.stderr,
92
- });
93
-
94
- rl.question('Enter the code from that page here: ', (code: string) => {
95
- rl.close();
96
- oAuth2Client.getToken(code, (err, token) => {
97
- if (err) {
98
- reject(new Error(`Error retrieving access token: ${err.message}`));
99
- return;
100
- }
101
- oAuth2Client.setCredentials(token!);
102
- fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
103
- console.error('Token stored to', TOKEN_PATH);
104
- resolve();
105
- });
106
- });
107
- });
108
- }
109
-
110
- /**
111
- * Authorize and get OAuth2 client
112
- */
113
- export async function authorize(): Promise<OAuth2Client> {
114
- if (authClient) return authClient;
115
-
116
- if (!fs.existsSync(CREDENTIALS_PATH)) {
117
- throw new Error(`Credentials not found at ${CREDENTIALS_PATH}. Run: cli4ai secrets init gmail`);
118
- }
119
-
120
- const content = fs.readFileSync(CREDENTIALS_PATH, 'utf-8');
121
- const credentials = JSON.parse(content);
122
- const { client_secret, client_id, redirect_uris } = credentials.installed || credentials.web;
123
-
124
- const oAuth2Client = new google.auth.OAuth2(
125
- client_id,
126
- client_secret,
127
- redirect_uris[0]
128
- );
129
-
130
- // Try to load existing token
131
- if (fs.existsSync(TOKEN_PATH)) {
132
- const token = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
133
- oAuth2Client.setCredentials(token);
134
-
135
- // Set up automatic token refresh
136
- oAuth2Client.on('tokens', (tokens) => {
137
- const currentToken = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
138
- const newToken = { ...currentToken, ...tokens };
139
- fs.writeFileSync(TOKEN_PATH, JSON.stringify(newToken));
140
- });
141
-
142
- // Try to refresh token if it might be expired
143
- try {
144
- await oAuth2Client.getAccessToken();
145
- } catch (err) {
146
- const error = err as Error;
147
- if (error.message.includes('invalid_grant') || error.message.includes('Token has been expired')) {
148
- console.error('Token expired. Re-authenticating...');
149
- fs.unlinkSync(TOKEN_PATH);
150
- await getNewToken(oAuth2Client);
151
- } else {
152
- throw err;
153
- }
154
- }
155
- } else {
156
- await getNewToken(oAuth2Client);
157
- }
158
-
159
- authClient = oAuth2Client;
160
- return oAuth2Client;
161
- }
162
-
163
- /**
164
- * Get Gmail API client
165
- */
166
- export async function getGmail(): Promise<gmail_v1.Gmail> {
167
- if (gmailClient) return gmailClient;
168
-
169
- const auth = await authorize();
170
- gmailClient = google.gmail({ version: 'v1', auth });
171
- return gmailClient;
172
- }
173
-
174
- /**
175
- * Output data to console
176
- */
177
- export function output(data: unknown, options: OutputOptions = {}): void {
178
- if (options.raw) {
179
- console.log(JSON.stringify(data, null, 2));
180
- } else if (options.compact && Array.isArray(data)) {
181
- data.forEach((item: MessageOutputData) => {
182
- if (item.date && item.from && item.subject) {
183
- const from = item.from.slice(0, 30).padEnd(30);
184
- const subject = item.subject.slice(0, 50);
185
- console.log(`[${item.date}] ${from} ${subject}`);
186
- } else {
187
- console.log(JSON.stringify(item));
188
- }
189
- });
190
- } else {
191
- console.log(JSON.stringify(data, null, 2));
192
- }
193
- }
194
-
195
- /**
196
- * Format date
197
- */
198
- export function formatDate(timestamp: string | number | undefined | null): string | null {
199
- if (!timestamp) return null;
200
- const date = new Date(parseInt(String(timestamp)));
201
- return date.toISOString().replace('T', ' ').slice(0, 16);
202
- }
203
-
204
- /**
205
- * Decode base64url
206
- */
207
- export function decodeBase64Url(str: string | undefined | null): string {
208
- if (!str) return '';
209
- // Replace URL-safe characters and add padding
210
- let normalized = str.replace(/-/g, '+').replace(/_/g, '/');
211
- while (normalized.length % 4) normalized += '=';
212
- return Buffer.from(normalized, 'base64').toString('utf-8');
213
- }
214
-
215
- /**
216
- * Encode to base64url
217
- */
218
- export function encodeBase64Url(str: string): string {
219
- return Buffer.from(str)
220
- .toString('base64')
221
- .replace(/\+/g, '-')
222
- .replace(/\//g, '_')
223
- .replace(/=+$/, '');
224
- }
225
-
226
- /**
227
- * Extract headers from message
228
- */
229
- export function extractHeaders(headers: Array<{ name: string; value: string }> | undefined): EmailHeaders {
230
- const result: EmailHeaders = {};
231
- const headerNames = ['From', 'To', 'Subject', 'Date', 'Cc', 'Bcc', 'Reply-To', 'Message-ID', 'References'];
232
-
233
- for (const header of headers || []) {
234
- if (headerNames.includes(header.name)) {
235
- const key = header.name.toLowerCase().replace('-', '_') as keyof EmailHeaders;
236
- result[key] = header.value;
237
- }
238
- }
239
- return result;
240
- }
241
-
242
- /**
243
- * Extract body from message parts
244
- */
245
- export function extractBody(payload: MessagePayload | undefined): string {
246
- if (!payload) return '';
247
-
248
- // Simple message with direct body
249
- if (payload.body?.data) {
250
- return decodeBase64Url(payload.body.data);
251
- }
252
-
253
- // Multipart message
254
- if (payload.parts) {
255
- // Prefer text/plain, fall back to text/html
256
- for (const part of payload.parts) {
257
- if (part.mimeType === 'text/plain' && part.body?.data) {
258
- return decodeBase64Url(part.body.data);
259
- }
260
- }
261
- for (const part of payload.parts) {
262
- if (part.mimeType === 'text/html' && part.body?.data) {
263
- // Strip HTML tags for plain text
264
- const html = decodeBase64Url(part.body.data);
265
- return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
266
- }
267
- }
268
- // Recursively check nested parts
269
- for (const part of payload.parts) {
270
- if (part.parts) {
271
- const body = extractBody(part);
272
- if (body) return body;
273
- }
274
- }
275
- }
276
-
277
- return '';
278
- }
279
-
280
- /**
281
- * Clean email body for display
282
- */
283
- export function cleanBody(body: string, maxLength?: number | null): string {
284
- if (!body) return '';
285
-
286
- // Remove excessive whitespace
287
- let cleaned = body
288
- .replace(/\r\n/g, '\n')
289
- .replace(/\n{3,}/g, '\n\n')
290
- .replace(/[ \t]+/g, ' ')
291
- .trim();
292
-
293
- // Truncate if needed
294
- if (maxLength && cleaned.length > maxLength) {
295
- cleaned = cleaned.slice(0, maxLength) + '...';
296
- }
297
-
298
- return cleaned;
299
- }
@@ -1,199 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { getGmail, output, decodeBase64Url, type OutputOptions, type MessagePayload } from './api';
4
-
5
- /** Attachment info */
6
- export interface AttachmentInfo {
7
- filename: string;
8
- mimeType?: string;
9
- size: number;
10
- attachmentId?: string | null;
11
- partId?: string;
12
- }
13
-
14
- /** Download result */
15
- export interface DownloadResult {
16
- ok: boolean;
17
- filename: string;
18
- savedTo?: string;
19
- size?: number;
20
- error?: string;
21
- }
22
-
23
- /** Download all result */
24
- export interface DownloadAllResult {
25
- ok: boolean;
26
- message?: string;
27
- downloaded: DownloadResult[];
28
- }
29
-
30
- /**
31
- * Extract attachment info from message payload
32
- */
33
- function extractAttachmentInfo(payload: MessagePayload | undefined, attachments: AttachmentInfo[] = []): AttachmentInfo[] {
34
- if (!payload) return attachments;
35
-
36
- // Check if this part is an attachment
37
- if (payload.filename && payload.filename.length > 0) {
38
- attachments.push({
39
- filename: payload.filename,
40
- mimeType: payload.mimeType,
41
- size: payload.body?.size || 0,
42
- attachmentId: payload.body?.attachmentId || null,
43
- partId: payload.partId
44
- });
45
- }
46
-
47
- // Recursively check parts
48
- if (payload.parts) {
49
- for (const part of payload.parts) {
50
- extractAttachmentInfo(part, attachments);
51
- }
52
- }
53
-
54
- return attachments;
55
- }
56
-
57
- /**
58
- * List attachments in a message
59
- */
60
- export async function list(messageId: string, options: OutputOptions = {}): Promise<AttachmentInfo[]> {
61
- const gmail = await getGmail();
62
-
63
- const res = await gmail.users.messages.get({
64
- userId: 'me',
65
- id: messageId,
66
- format: 'full'
67
- });
68
-
69
- const attachments = extractAttachmentInfo(res.data.payload as MessagePayload);
70
-
71
- output(attachments, options);
72
- return attachments;
73
- }
74
-
75
- /**
76
- * Download an attachment
77
- */
78
- export async function download(messageId: string, filename?: string, outputPath?: string, options: OutputOptions = {}): Promise<DownloadResult> {
79
- const gmail = await getGmail();
80
-
81
- // Get message to find attachment
82
- const res = await gmail.users.messages.get({
83
- userId: 'me',
84
- id: messageId,
85
- format: 'full'
86
- });
87
-
88
- const attachments = extractAttachmentInfo(res.data.payload as MessagePayload);
89
-
90
- // Find the attachment by filename (or get first if not specified)
91
- let attachment: AttachmentInfo | undefined;
92
- if (filename) {
93
- attachment = attachments.find(a =>
94
- a.filename.toLowerCase() === filename.toLowerCase() ||
95
- a.filename.toLowerCase().includes(filename.toLowerCase())
96
- );
97
- } else if (attachments.length === 1) {
98
- attachment = attachments[0];
99
- }
100
-
101
- if (!attachment) {
102
- throw new Error(`Attachment not found: ${filename || '(no attachments)'}`);
103
- }
104
-
105
- if (!attachment.attachmentId) {
106
- throw new Error(`Attachment ${attachment.filename} has no downloadable content`);
107
- }
108
-
109
- // Get the attachment data
110
- const attachmentRes = await gmail.users.messages.attachments.get({
111
- userId: 'me',
112
- messageId: messageId,
113
- id: attachment.attachmentId
114
- });
115
-
116
- // Decode and save
117
- const data = decodeBase64Url(attachmentRes.data.data);
118
- const savePath = outputPath || attachment.filename;
119
-
120
- fs.writeFileSync(savePath, data, 'binary');
121
-
122
- const result: DownloadResult = {
123
- ok: true,
124
- filename: attachment.filename,
125
- savedTo: path.resolve(savePath),
126
- size: attachment.size
127
- };
128
-
129
- output(result, options);
130
- return result;
131
- }
132
-
133
- /**
134
- * Download all attachments from a message
135
- */
136
- export async function downloadAll(messageId: string, outputDir?: string, options: OutputOptions = {}): Promise<DownloadAllResult> {
137
- const gmail = await getGmail();
138
-
139
- // Ensure output directory exists
140
- const dir = outputDir || '.';
141
- if (!fs.existsSync(dir)) {
142
- fs.mkdirSync(dir, { recursive: true });
143
- }
144
-
145
- // Get message
146
- const res = await gmail.users.messages.get({
147
- userId: 'me',
148
- id: messageId,
149
- format: 'full'
150
- });
151
-
152
- const attachments = extractAttachmentInfo(res.data.payload as MessagePayload);
153
-
154
- if (attachments.length === 0) {
155
- const result: DownloadAllResult = { ok: true, message: 'No attachments found', downloaded: [] };
156
- output(result, options);
157
- return result;
158
- }
159
-
160
- const downloaded: DownloadResult[] = [];
161
-
162
- for (const attachment of attachments) {
163
- if (!attachment.attachmentId) continue;
164
-
165
- try {
166
- const attachmentRes = await gmail.users.messages.attachments.get({
167
- userId: 'me',
168
- messageId: messageId,
169
- id: attachment.attachmentId
170
- });
171
-
172
- const data = decodeBase64Url(attachmentRes.data.data);
173
- const savePath = path.join(dir, attachment.filename);
174
-
175
- fs.writeFileSync(savePath, data, 'binary');
176
-
177
- downloaded.push({
178
- ok: true,
179
- filename: attachment.filename,
180
- savedTo: path.resolve(savePath),
181
- size: attachment.size
182
- });
183
- } catch (err) {
184
- downloaded.push({
185
- ok: false,
186
- filename: attachment.filename,
187
- error: (err as Error).message
188
- });
189
- }
190
- }
191
-
192
- const result: DownloadAllResult = {
193
- ok: true,
194
- downloaded
195
- };
196
-
197
- output(result, options);
198
- return result;
199
- }