@cli4ai/gmail 1.0.11 → 1.0.13
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 +3 -3
- package/dist/lib/api.d.ts +91 -0
- package/dist/lib/api.js +240 -0
- package/dist/lib/attachments.d.ts +35 -0
- package/dist/lib/attachments.js +148 -0
- package/dist/lib/drafts.d.ts +89 -0
- package/dist/lib/drafts.js +334 -0
- package/dist/lib/labels.d.ts +47 -0
- package/dist/lib/labels.js +143 -0
- package/dist/lib/messages.d.ts +77 -0
- package/dist/lib/messages.js +223 -0
- package/dist/lib/send.d.ts +37 -0
- package/dist/lib/send.js +259 -0
- package/dist/lib/threads.d.ts +55 -0
- package/dist/lib/threads.js +101 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +260 -0
- package/package.json +13 -5
- package/run.ts +0 -316
package/cli4ai.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gmail",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Gmail CLI tool for messages, threads, and drafts",
|
|
5
5
|
"author": "cliforai",
|
|
6
|
-
"license": "
|
|
7
|
-
"entry": "run.
|
|
6
|
+
"license": "BUSL-1.1",
|
|
7
|
+
"entry": "dist/run.js",
|
|
8
8
|
"runtime": "node",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"gmail",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { gmail_v1 } from 'googleapis';
|
|
2
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
3
|
+
/** Output options */
|
|
4
|
+
export interface OutputOptions {
|
|
5
|
+
raw?: boolean;
|
|
6
|
+
compact?: boolean;
|
|
7
|
+
body?: boolean;
|
|
8
|
+
fullBody?: boolean;
|
|
9
|
+
attach?: string | string[];
|
|
10
|
+
cc?: string;
|
|
11
|
+
bcc?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
query?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Message output data for compact display */
|
|
16
|
+
export interface MessageOutputData {
|
|
17
|
+
date?: string | null;
|
|
18
|
+
from?: string;
|
|
19
|
+
subject?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Email headers */
|
|
22
|
+
export interface EmailHeaders {
|
|
23
|
+
from?: string;
|
|
24
|
+
to?: string;
|
|
25
|
+
cc?: string;
|
|
26
|
+
bcc?: string;
|
|
27
|
+
subject?: string;
|
|
28
|
+
date?: string;
|
|
29
|
+
reply_to?: string;
|
|
30
|
+
message_id?: string;
|
|
31
|
+
references?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Message payload */
|
|
34
|
+
export interface MessagePayload {
|
|
35
|
+
headers?: Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
value: string;
|
|
38
|
+
}>;
|
|
39
|
+
body?: {
|
|
40
|
+
data?: string;
|
|
41
|
+
size?: number;
|
|
42
|
+
attachmentId?: string;
|
|
43
|
+
};
|
|
44
|
+
parts?: MessagePayload[];
|
|
45
|
+
mimeType?: string;
|
|
46
|
+
filename?: string;
|
|
47
|
+
partId?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authorize and get OAuth2 client
|
|
51
|
+
*/
|
|
52
|
+
export declare function authorize(): Promise<OAuth2Client>;
|
|
53
|
+
/**
|
|
54
|
+
* Get Gmail API client
|
|
55
|
+
*/
|
|
56
|
+
export declare function getGmail(): Promise<gmail_v1.Gmail>;
|
|
57
|
+
/**
|
|
58
|
+
* Output data to console
|
|
59
|
+
*/
|
|
60
|
+
export declare function output(data: unknown, options?: OutputOptions): void;
|
|
61
|
+
/**
|
|
62
|
+
* Format date
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatDate(timestamp: string | number | undefined | null): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Decode base64url to Buffer (for binary attachments)
|
|
67
|
+
*/
|
|
68
|
+
export declare function decodeBase64Url(str: string | undefined | null): Buffer;
|
|
69
|
+
/**
|
|
70
|
+
* Decode base64url to UTF-8 string (for text content)
|
|
71
|
+
*/
|
|
72
|
+
export declare function decodeBase64UrlToString(str: string | undefined | null): string;
|
|
73
|
+
/**
|
|
74
|
+
* Encode to base64url
|
|
75
|
+
*/
|
|
76
|
+
export declare function encodeBase64Url(str: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Extract headers from message
|
|
79
|
+
*/
|
|
80
|
+
export declare function extractHeaders(headers: Array<{
|
|
81
|
+
name: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}> | undefined): EmailHeaders;
|
|
84
|
+
/**
|
|
85
|
+
* Extract body from message parts
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractBody(payload: MessagePayload | undefined): string;
|
|
88
|
+
/**
|
|
89
|
+
* Clean email body for display
|
|
90
|
+
*/
|
|
91
|
+
export declare function cleanBody(body: string, maxLength?: number | null): string;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
// SECURITY: Store credentials in user's home directory, not in node_modules
|
|
7
|
+
const GMAIL_CONFIG_DIR = path.join(homedir(), '.cli4ai', 'gmail');
|
|
8
|
+
// Paths for credentials and token (env vars override for flexibility)
|
|
9
|
+
const CREDENTIALS_PATH = process.env.GMAIL_CREDENTIALS_PATH || path.join(GMAIL_CONFIG_DIR, 'credentials.json');
|
|
10
|
+
const TOKEN_PATH = process.env.GMAIL_TOKEN_PATH || path.join(GMAIL_CONFIG_DIR, 'token.json');
|
|
11
|
+
/**
|
|
12
|
+
* Ensure the gmail config directory exists with secure permissions
|
|
13
|
+
*/
|
|
14
|
+
function ensureConfigDir() {
|
|
15
|
+
if (!fs.existsSync(GMAIL_CONFIG_DIR)) {
|
|
16
|
+
fs.mkdirSync(GMAIL_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Scopes for Gmail access
|
|
20
|
+
const SCOPES = [
|
|
21
|
+
'https://www.googleapis.com/auth/gmail.modify',
|
|
22
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
23
|
+
'https://www.googleapis.com/auth/gmail.send'
|
|
24
|
+
];
|
|
25
|
+
let gmailClient = null;
|
|
26
|
+
let authClient = null;
|
|
27
|
+
/**
|
|
28
|
+
* Get new token via OAuth flow
|
|
29
|
+
*/
|
|
30
|
+
async function getNewToken(oAuth2Client) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const authUrl = oAuth2Client.generateAuthUrl({
|
|
33
|
+
access_type: 'offline',
|
|
34
|
+
scope: SCOPES,
|
|
35
|
+
});
|
|
36
|
+
console.error('Authorize this app by visiting this url:');
|
|
37
|
+
console.error(authUrl);
|
|
38
|
+
const rl = readline.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stderr,
|
|
41
|
+
});
|
|
42
|
+
rl.question('Enter the code from that page here: ', (code) => {
|
|
43
|
+
rl.close();
|
|
44
|
+
oAuth2Client.getToken(code, (err, token) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
reject(new Error(`Error retrieving access token: ${err.message}`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
oAuth2Client.setCredentials(token);
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(token), { mode: 0o600 });
|
|
52
|
+
console.error('Token stored to', TOKEN_PATH);
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Authorize and get OAuth2 client
|
|
60
|
+
*/
|
|
61
|
+
export async function authorize() {
|
|
62
|
+
if (authClient)
|
|
63
|
+
return authClient;
|
|
64
|
+
if (!fs.existsSync(CREDENTIALS_PATH)) {
|
|
65
|
+
throw new Error(`Credentials not found at ${CREDENTIALS_PATH}. Run: cli4ai secrets init gmail`);
|
|
66
|
+
}
|
|
67
|
+
const content = fs.readFileSync(CREDENTIALS_PATH, 'utf-8');
|
|
68
|
+
const credentials = JSON.parse(content);
|
|
69
|
+
const { client_secret, client_id, redirect_uris } = credentials.installed || credentials.web;
|
|
70
|
+
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
|
71
|
+
// Try to load existing token
|
|
72
|
+
if (fs.existsSync(TOKEN_PATH)) {
|
|
73
|
+
const token = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
|
|
74
|
+
oAuth2Client.setCredentials(token);
|
|
75
|
+
// Set up automatic token refresh
|
|
76
|
+
oAuth2Client.on('tokens', (tokens) => {
|
|
77
|
+
const currentToken = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
|
|
78
|
+
const newToken = { ...currentToken, ...tokens };
|
|
79
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(newToken), { mode: 0o600 });
|
|
80
|
+
});
|
|
81
|
+
// Try to refresh token if it might be expired
|
|
82
|
+
try {
|
|
83
|
+
await oAuth2Client.getAccessToken();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const error = err;
|
|
87
|
+
if (error.message.includes('invalid_grant') || error.message.includes('Token has been expired')) {
|
|
88
|
+
console.error('Token expired. Re-authenticating...');
|
|
89
|
+
fs.unlinkSync(TOKEN_PATH);
|
|
90
|
+
await getNewToken(oAuth2Client);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await getNewToken(oAuth2Client);
|
|
99
|
+
}
|
|
100
|
+
authClient = oAuth2Client;
|
|
101
|
+
return oAuth2Client;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get Gmail API client
|
|
105
|
+
*/
|
|
106
|
+
export async function getGmail() {
|
|
107
|
+
if (gmailClient)
|
|
108
|
+
return gmailClient;
|
|
109
|
+
const auth = await authorize();
|
|
110
|
+
gmailClient = google.gmail({ version: 'v1', auth });
|
|
111
|
+
return gmailClient;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Output data to console
|
|
115
|
+
*/
|
|
116
|
+
export function output(data, options = {}) {
|
|
117
|
+
if (options.raw) {
|
|
118
|
+
console.log(JSON.stringify(data, null, 2));
|
|
119
|
+
}
|
|
120
|
+
else if (options.compact && Array.isArray(data)) {
|
|
121
|
+
data.forEach((item) => {
|
|
122
|
+
if (item.date && item.from && item.subject) {
|
|
123
|
+
const from = item.from.slice(0, 30).padEnd(30);
|
|
124
|
+
const subject = item.subject.slice(0, 50);
|
|
125
|
+
console.log(`[${item.date}] ${from} ${subject}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(JSON.stringify(item));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(JSON.stringify(data, null, 2));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format date
|
|
138
|
+
*/
|
|
139
|
+
export function formatDate(timestamp) {
|
|
140
|
+
if (!timestamp)
|
|
141
|
+
return null;
|
|
142
|
+
const date = new Date(parseInt(String(timestamp)));
|
|
143
|
+
return date.toISOString().replace('T', ' ').slice(0, 16);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Decode base64url to Buffer (for binary attachments)
|
|
147
|
+
*/
|
|
148
|
+
export function decodeBase64Url(str) {
|
|
149
|
+
if (!str)
|
|
150
|
+
return Buffer.alloc(0);
|
|
151
|
+
// Replace URL-safe characters and add padding
|
|
152
|
+
let normalized = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
153
|
+
while (normalized.length % 4)
|
|
154
|
+
normalized += '=';
|
|
155
|
+
return Buffer.from(normalized, 'base64');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Decode base64url to UTF-8 string (for text content)
|
|
159
|
+
*/
|
|
160
|
+
export function decodeBase64UrlToString(str) {
|
|
161
|
+
return decodeBase64Url(str).toString('utf-8');
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Encode to base64url
|
|
165
|
+
*/
|
|
166
|
+
export function encodeBase64Url(str) {
|
|
167
|
+
return Buffer.from(str)
|
|
168
|
+
.toString('base64')
|
|
169
|
+
.replace(/\+/g, '-')
|
|
170
|
+
.replace(/\//g, '_')
|
|
171
|
+
.replace(/=+$/, '');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extract headers from message
|
|
175
|
+
*/
|
|
176
|
+
export function extractHeaders(headers) {
|
|
177
|
+
const result = {};
|
|
178
|
+
const headerNames = ['From', 'To', 'Subject', 'Date', 'Cc', 'Bcc', 'Reply-To', 'Message-ID', 'References'];
|
|
179
|
+
for (const header of headers || []) {
|
|
180
|
+
if (headerNames.includes(header.name)) {
|
|
181
|
+
const key = header.name.toLowerCase().replace('-', '_');
|
|
182
|
+
result[key] = header.value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Extract body from message parts
|
|
189
|
+
*/
|
|
190
|
+
export function extractBody(payload) {
|
|
191
|
+
if (!payload)
|
|
192
|
+
return '';
|
|
193
|
+
// Simple message with direct body
|
|
194
|
+
if (payload.body?.data) {
|
|
195
|
+
return decodeBase64UrlToString(payload.body.data);
|
|
196
|
+
}
|
|
197
|
+
// Multipart message
|
|
198
|
+
if (payload.parts) {
|
|
199
|
+
// Prefer text/plain, fall back to text/html
|
|
200
|
+
for (const part of payload.parts) {
|
|
201
|
+
if (part.mimeType === 'text/plain' && part.body?.data) {
|
|
202
|
+
return decodeBase64UrlToString(part.body.data);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const part of payload.parts) {
|
|
206
|
+
if (part.mimeType === 'text/html' && part.body?.data) {
|
|
207
|
+
// Strip HTML tags for plain text
|
|
208
|
+
const html = decodeBase64UrlToString(part.body.data);
|
|
209
|
+
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Recursively check nested parts
|
|
213
|
+
for (const part of payload.parts) {
|
|
214
|
+
if (part.parts) {
|
|
215
|
+
const body = extractBody(part);
|
|
216
|
+
if (body)
|
|
217
|
+
return body;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Clean email body for display
|
|
225
|
+
*/
|
|
226
|
+
export function cleanBody(body, maxLength) {
|
|
227
|
+
if (!body)
|
|
228
|
+
return '';
|
|
229
|
+
// Remove excessive whitespace
|
|
230
|
+
let cleaned = body
|
|
231
|
+
.replace(/\r\n/g, '\n')
|
|
232
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
233
|
+
.replace(/[ \t]+/g, ' ')
|
|
234
|
+
.trim();
|
|
235
|
+
// Truncate if needed
|
|
236
|
+
if (maxLength && cleaned.length > maxLength) {
|
|
237
|
+
cleaned = cleaned.slice(0, maxLength) + '...';
|
|
238
|
+
}
|
|
239
|
+
return cleaned;
|
|
240
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type OutputOptions } from './api.js';
|
|
2
|
+
/** Attachment info */
|
|
3
|
+
export interface AttachmentInfo {
|
|
4
|
+
filename: string;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
size: number;
|
|
7
|
+
attachmentId?: string | null;
|
|
8
|
+
partId?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Download result */
|
|
11
|
+
export interface DownloadResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
filename: string;
|
|
14
|
+
savedTo?: string;
|
|
15
|
+
size?: number;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Download all result */
|
|
19
|
+
export interface DownloadAllResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
message?: string;
|
|
22
|
+
downloaded: DownloadResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* List attachments in a message
|
|
26
|
+
*/
|
|
27
|
+
export declare function list(messageId: string, options?: OutputOptions): Promise<AttachmentInfo[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Download an attachment
|
|
30
|
+
*/
|
|
31
|
+
export declare function download(messageId: string, filename?: string, outputPath?: string, options?: OutputOptions): Promise<DownloadResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Download all attachments from a message
|
|
34
|
+
*/
|
|
35
|
+
export declare function downloadAll(messageId: string, outputDir?: string, options?: OutputOptions): Promise<DownloadAllResult>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getGmail, output, decodeBase64Url } from './api.js';
|
|
4
|
+
/**
|
|
5
|
+
* Extract attachment info from message payload
|
|
6
|
+
*/
|
|
7
|
+
function extractAttachmentInfo(payload, attachments = []) {
|
|
8
|
+
if (!payload)
|
|
9
|
+
return attachments;
|
|
10
|
+
// Check if this part is an attachment
|
|
11
|
+
if (payload.filename && payload.filename.length > 0) {
|
|
12
|
+
attachments.push({
|
|
13
|
+
filename: payload.filename,
|
|
14
|
+
mimeType: payload.mimeType,
|
|
15
|
+
size: payload.body?.size || 0,
|
|
16
|
+
attachmentId: payload.body?.attachmentId || null,
|
|
17
|
+
partId: payload.partId
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Recursively check parts
|
|
21
|
+
if (payload.parts) {
|
|
22
|
+
for (const part of payload.parts) {
|
|
23
|
+
extractAttachmentInfo(part, attachments);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return attachments;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List attachments in a message
|
|
30
|
+
*/
|
|
31
|
+
export async function list(messageId, options = {}) {
|
|
32
|
+
const gmail = await getGmail();
|
|
33
|
+
const res = await gmail.users.messages.get({
|
|
34
|
+
userId: 'me',
|
|
35
|
+
id: messageId,
|
|
36
|
+
format: 'full'
|
|
37
|
+
});
|
|
38
|
+
const attachments = extractAttachmentInfo(res.data.payload);
|
|
39
|
+
output(attachments, options);
|
|
40
|
+
return attachments;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Download an attachment
|
|
44
|
+
*/
|
|
45
|
+
export async function download(messageId, filename, outputPath, options = {}) {
|
|
46
|
+
const gmail = await getGmail();
|
|
47
|
+
// Get message to find attachment
|
|
48
|
+
const res = await gmail.users.messages.get({
|
|
49
|
+
userId: 'me',
|
|
50
|
+
id: messageId,
|
|
51
|
+
format: 'full'
|
|
52
|
+
});
|
|
53
|
+
const attachments = extractAttachmentInfo(res.data.payload);
|
|
54
|
+
// Find the attachment by filename (or get first if not specified)
|
|
55
|
+
let attachment;
|
|
56
|
+
if (filename) {
|
|
57
|
+
attachment = attachments.find(a => a.filename.toLowerCase() === filename.toLowerCase() ||
|
|
58
|
+
a.filename.toLowerCase().includes(filename.toLowerCase()));
|
|
59
|
+
}
|
|
60
|
+
else if (attachments.length === 1) {
|
|
61
|
+
attachment = attachments[0];
|
|
62
|
+
}
|
|
63
|
+
if (!attachment) {
|
|
64
|
+
throw new Error(`Attachment not found: ${filename || '(no attachments)'}`);
|
|
65
|
+
}
|
|
66
|
+
if (!attachment.attachmentId) {
|
|
67
|
+
throw new Error(`Attachment ${attachment.filename} has no downloadable content`);
|
|
68
|
+
}
|
|
69
|
+
// Get the attachment data
|
|
70
|
+
const attachmentRes = await gmail.users.messages.attachments.get({
|
|
71
|
+
userId: 'me',
|
|
72
|
+
messageId: messageId,
|
|
73
|
+
id: attachment.attachmentId
|
|
74
|
+
});
|
|
75
|
+
// Decode and save
|
|
76
|
+
const data = decodeBase64Url(attachmentRes.data.data);
|
|
77
|
+
// SECURITY: Sanitize filename to prevent path traversal
|
|
78
|
+
const safeFilename = path.basename(attachment.filename);
|
|
79
|
+
const savePath = outputPath || safeFilename;
|
|
80
|
+
fs.writeFileSync(savePath, data);
|
|
81
|
+
const result = {
|
|
82
|
+
ok: true,
|
|
83
|
+
filename: attachment.filename,
|
|
84
|
+
savedTo: path.resolve(savePath),
|
|
85
|
+
size: attachment.size
|
|
86
|
+
};
|
|
87
|
+
output(result, options);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Download all attachments from a message
|
|
92
|
+
*/
|
|
93
|
+
export async function downloadAll(messageId, outputDir, options = {}) {
|
|
94
|
+
const gmail = await getGmail();
|
|
95
|
+
// Ensure output directory exists
|
|
96
|
+
const dir = outputDir || '.';
|
|
97
|
+
if (!fs.existsSync(dir)) {
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
// Get message
|
|
101
|
+
const res = await gmail.users.messages.get({
|
|
102
|
+
userId: 'me',
|
|
103
|
+
id: messageId,
|
|
104
|
+
format: 'full'
|
|
105
|
+
});
|
|
106
|
+
const attachments = extractAttachmentInfo(res.data.payload);
|
|
107
|
+
if (attachments.length === 0) {
|
|
108
|
+
const result = { ok: true, message: 'No attachments found', downloaded: [] };
|
|
109
|
+
output(result, options);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
const downloaded = [];
|
|
113
|
+
for (const attachment of attachments) {
|
|
114
|
+
if (!attachment.attachmentId)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const attachmentRes = await gmail.users.messages.attachments.get({
|
|
118
|
+
userId: 'me',
|
|
119
|
+
messageId: messageId,
|
|
120
|
+
id: attachment.attachmentId
|
|
121
|
+
});
|
|
122
|
+
const data = decodeBase64Url(attachmentRes.data.data);
|
|
123
|
+
// SECURITY: Sanitize filename to prevent path traversal
|
|
124
|
+
const safeFilename = path.basename(attachment.filename);
|
|
125
|
+
const savePath = path.join(dir, safeFilename);
|
|
126
|
+
fs.writeFileSync(savePath, data);
|
|
127
|
+
downloaded.push({
|
|
128
|
+
ok: true,
|
|
129
|
+
filename: attachment.filename,
|
|
130
|
+
savedTo: path.resolve(savePath),
|
|
131
|
+
size: attachment.size
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
downloaded.push({
|
|
136
|
+
ok: false,
|
|
137
|
+
filename: attachment.filename,
|
|
138
|
+
error: err.message
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const result = {
|
|
143
|
+
ok: true,
|
|
144
|
+
downloaded
|
|
145
|
+
};
|
|
146
|
+
output(result, options);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type OutputOptions } from './api.js';
|
|
2
|
+
/** Attachment path */
|
|
3
|
+
export interface AttachmentPath {
|
|
4
|
+
path: string;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Draft data */
|
|
8
|
+
export interface DraftData {
|
|
9
|
+
id: string;
|
|
10
|
+
messageId?: string;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
to: string;
|
|
13
|
+
cc?: string | null;
|
|
14
|
+
subject: string;
|
|
15
|
+
snippet?: string;
|
|
16
|
+
body?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Draft result */
|
|
19
|
+
export interface DraftResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
id?: string;
|
|
22
|
+
messageId?: string;
|
|
23
|
+
threadId?: string;
|
|
24
|
+
to?: string;
|
|
25
|
+
subject?: string;
|
|
26
|
+
inReplyTo?: string;
|
|
27
|
+
deleted?: string;
|
|
28
|
+
sentDraft?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Draft options */
|
|
31
|
+
export interface DraftOptions extends OutputOptions {
|
|
32
|
+
cc?: string;
|
|
33
|
+
bcc?: string;
|
|
34
|
+
attach?: string | string[];
|
|
35
|
+
limit?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create multipart message with attachment
|
|
39
|
+
*/
|
|
40
|
+
export declare function createMultipartMessage({ to, cc, bcc, subject, body, inReplyTo, references, attachments }: {
|
|
41
|
+
to: string;
|
|
42
|
+
cc?: string;
|
|
43
|
+
bcc?: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
body: string;
|
|
46
|
+
inReplyTo?: string;
|
|
47
|
+
references?: string;
|
|
48
|
+
attachments?: AttachmentPath[];
|
|
49
|
+
}): string;
|
|
50
|
+
/**
|
|
51
|
+
* Simple message without attachments
|
|
52
|
+
*/
|
|
53
|
+
export declare function createSimpleMessage({ to, cc, bcc, subject, body, inReplyTo, references }: {
|
|
54
|
+
to: string;
|
|
55
|
+
cc?: string;
|
|
56
|
+
bcc?: string;
|
|
57
|
+
subject: string;
|
|
58
|
+
body: string;
|
|
59
|
+
inReplyTo?: string;
|
|
60
|
+
references?: string;
|
|
61
|
+
}): string;
|
|
62
|
+
/**
|
|
63
|
+
* List all drafts
|
|
64
|
+
*/
|
|
65
|
+
export declare function list(options?: DraftOptions): Promise<DraftData[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Get a draft
|
|
68
|
+
*/
|
|
69
|
+
export declare function get(draftId: string, options?: OutputOptions): Promise<DraftData>;
|
|
70
|
+
/**
|
|
71
|
+
* Create a new draft
|
|
72
|
+
*/
|
|
73
|
+
export declare function create(to: string, subject: string, body: string, options?: DraftOptions): Promise<DraftResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Create a draft reply to a message
|
|
76
|
+
*/
|
|
77
|
+
export declare function createReply(messageId: string, body: string, options?: DraftOptions): Promise<DraftResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Update a draft
|
|
80
|
+
*/
|
|
81
|
+
export declare function update(draftId: string, to: string, subject: string, body: string, options?: DraftOptions): Promise<DraftResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Delete a draft
|
|
84
|
+
*/
|
|
85
|
+
export declare function remove(draftId: string, options?: OutputOptions): Promise<DraftResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Send a draft
|
|
88
|
+
*/
|
|
89
|
+
export declare function send(draftId: string, options?: OutputOptions): Promise<DraftResult>;
|