@cli4ai/gmail 1.0.1 → 1.0.3
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/README.md +112 -113
- package/c4ai.json +1 -1
- package/lib/api.test.ts +287 -0
- package/lib/api.ts +299 -0
- package/lib/attachments.ts +199 -0
- package/lib/drafts.ts +434 -0
- package/lib/labels.ts +198 -0
- package/lib/messages.ts +310 -0
- package/lib/send.ts +331 -0
- package/lib/threads.ts +164 -0
- package/package.json +5 -2
package/lib/api.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
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}. Set GMAIL_CREDENTIALS_PATH in .env`);
|
|
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
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
}
|