m365-cli 0.1.0

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.
@@ -0,0 +1,353 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import graphClient from '../graph/client.js';
4
+ import { outputMailList, outputMailDetail, outputSendResult, outputAttachmentList, outputAttachmentDownload } from '../utils/output.js';
5
+ import { handleError } from '../utils/error.js';
6
+ import { isTrustedSender, addTrustedSender, removeTrustedSender, listTrustedSenders, getWhitelistFilePath } from '../utils/trusted-senders.js';
7
+
8
+ /**
9
+ * Mail commands
10
+ */
11
+
12
+ /**
13
+ * List emails
14
+ */
15
+ export async function listMails(options) {
16
+ try {
17
+ const { top = 10, folder = 'inbox', json = false } = options;
18
+
19
+ const mails = await graphClient.mail.list({ top, folder });
20
+
21
+ // Mark untrusted senders
22
+ const mailsWithTrustStatus = mails.map(mail => {
23
+ const senderEmail = mail.from?.emailAddress?.address;
24
+ return {
25
+ ...mail,
26
+ isTrusted: senderEmail ? isTrustedSender(senderEmail) : false,
27
+ };
28
+ });
29
+
30
+ outputMailList(mailsWithTrustStatus, { json, top });
31
+ } catch (error) {
32
+ handleError(error, { json: options.json });
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Read email by ID
38
+ */
39
+ export async function readMail(id, options) {
40
+ try {
41
+ const { json = false, force = false } = options;
42
+
43
+ if (!id) {
44
+ throw new Error('Email ID is required');
45
+ }
46
+
47
+ const mail = await graphClient.mail.get(id);
48
+
49
+ // Check whitelist unless --force is used
50
+ const senderEmail = mail.from?.emailAddress?.address;
51
+ const trusted = senderEmail ? isTrustedSender(senderEmail) : false;
52
+
53
+ if (!trusted && !force) {
54
+ // Filter content for untrusted senders
55
+ mail.bodyFiltered = true;
56
+ mail.originalBody = mail.body;
57
+ mail.body = {
58
+ contentType: mail.body?.contentType || 'Text',
59
+ content: '[Content filtered - sender not in trusted senders list]\n\nUse --force to skip whitelist check.',
60
+ };
61
+ }
62
+
63
+ mail.isTrusted = trusted;
64
+
65
+ outputMailDetail(mail, { json });
66
+ } catch (error) {
67
+ handleError(error, { json: options.json });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Send email
73
+ */
74
+ export async function sendMail(to, subject, body, options) {
75
+ try {
76
+ const { attach = [], cc, bcc, json = false } = options;
77
+
78
+ if (!to || !subject || !body) {
79
+ throw new Error('To, subject, and body are required');
80
+ }
81
+
82
+ // Helper function to parse comma-separated emails into recipients array
83
+ const parseRecipients = (emails) => {
84
+ if (!emails) return [];
85
+ return emails
86
+ .split(',')
87
+ .map(email => email.trim())
88
+ .filter(email => email)
89
+ .map(email => ({
90
+ emailAddress: {
91
+ address: email,
92
+ },
93
+ }));
94
+ };
95
+
96
+ // Build message
97
+ const message = {
98
+ subject,
99
+ body: {
100
+ contentType: 'HTML',
101
+ content: body,
102
+ },
103
+ toRecipients: parseRecipients(to),
104
+ };
105
+
106
+ // Add CC recipients if specified
107
+ if (cc) {
108
+ message.ccRecipients = parseRecipients(cc);
109
+ }
110
+
111
+ // Add BCC recipients if specified
112
+ if (bcc) {
113
+ message.bccRecipients = parseRecipients(bcc);
114
+ }
115
+
116
+ // Handle attachments
117
+ if (attach && attach.length > 0) {
118
+ message.attachments = [];
119
+
120
+ let totalSize = 0;
121
+
122
+ for (const filePath of attach) {
123
+ try {
124
+ const fileBuffer = readFileSync(filePath);
125
+ const fileName = filePath.split('/').pop();
126
+ const base64Content = fileBuffer.toString('base64');
127
+
128
+ totalSize += fileBuffer.length;
129
+
130
+ message.attachments.push({
131
+ '@odata.type': '#microsoft.graph.fileAttachment',
132
+ name: fileName,
133
+ contentBytes: base64Content,
134
+ });
135
+ } catch (error) {
136
+ throw new Error(`Failed to read attachment: ${filePath} - ${error.message}`);
137
+ }
138
+ }
139
+
140
+ // Warn about size limit
141
+ if (totalSize > 2360320) { // ~2.25MB
142
+ console.warn('⚠️ Warning: Total attachment size exceeds recommended limit (~2.25MB).');
143
+ console.warn(' Large emails may fail due to Graph API limits.');
144
+ }
145
+ }
146
+
147
+ // Send email
148
+ await graphClient.mail.send(message);
149
+
150
+ const result = {
151
+ status: 'sent',
152
+ to,
153
+ subject,
154
+ attachments: attach.length,
155
+ };
156
+
157
+ // Add CC/BCC info to result
158
+ if (cc) {
159
+ result.cc = cc;
160
+ }
161
+ if (bcc) {
162
+ // Don't show BCC addresses, just count
163
+ result.bccCount = parseRecipients(bcc).length;
164
+ }
165
+
166
+ outputSendResult(result, { json });
167
+ } catch (error) {
168
+ handleError(error, { json: options.json });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Search emails
174
+ */
175
+ export async function searchMails(query, options) {
176
+ try {
177
+ const { top = 10, json = false } = options;
178
+
179
+ if (!query) {
180
+ throw new Error('Search query is required');
181
+ }
182
+
183
+ const mails = await graphClient.mail.search(query, { top });
184
+
185
+ if (!json) {
186
+ console.log(`🔍 Search results for: "${query}"`);
187
+ console.log('');
188
+ }
189
+
190
+ outputMailList(mails, { json, top });
191
+ } catch (error) {
192
+ handleError(error, { json: options.json });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * List attachments for an email
198
+ */
199
+ export async function listAttachments(id, options) {
200
+ try {
201
+ const { json = false } = options;
202
+
203
+ if (!id) {
204
+ throw new Error('Email ID is required');
205
+ }
206
+
207
+ const attachments = await graphClient.mail.attachments(id);
208
+
209
+ outputAttachmentList(attachments, { json, messageId: id });
210
+ } catch (error) {
211
+ handleError(error, { json: options.json });
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Download email attachment
217
+ */
218
+ export async function downloadAttachment(messageId, attachmentId, localPath, options) {
219
+ try {
220
+ const { json = false } = options;
221
+
222
+ if (!messageId || !attachmentId) {
223
+ throw new Error('Message ID and Attachment ID are required');
224
+ }
225
+
226
+ // Get attachment data
227
+ const attachment = await graphClient.mail.downloadAttachment(messageId, attachmentId);
228
+
229
+ if (!attachment.contentBytes) {
230
+ throw new Error('Attachment content not found');
231
+ }
232
+
233
+ // Determine output path
234
+ const fileName = attachment.name || 'attachment';
235
+ const outputPath = localPath || fileName;
236
+
237
+ // Decode base64 content and save
238
+ const buffer = Buffer.from(attachment.contentBytes, 'base64');
239
+ writeFileSync(outputPath, buffer);
240
+
241
+ const result = {
242
+ name: fileName,
243
+ path: outputPath,
244
+ size: buffer.length,
245
+ };
246
+
247
+ outputAttachmentDownload(result, { json });
248
+ } catch (error) {
249
+ handleError(error, { json: options.json });
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Manage trusted senders whitelist
255
+ */
256
+ export async function trustSender(email, options) {
257
+ try {
258
+ const { json = false } = options;
259
+
260
+ if (!email) {
261
+ throw new Error('Email address or domain is required');
262
+ }
263
+
264
+ addTrustedSender(email);
265
+
266
+ const result = {
267
+ action: 'added',
268
+ entry: email,
269
+ path: getWhitelistFilePath(),
270
+ };
271
+
272
+ if (json) {
273
+ console.log(JSON.stringify(result, null, 2));
274
+ } else {
275
+ console.log(`✅ Added to whitelist: ${email}`);
276
+ console.log(` File: ${result.path}`);
277
+ }
278
+ } catch (error) {
279
+ handleError(error, { json: options.json });
280
+ }
281
+ }
282
+
283
+ export async function untrustSender(email, options) {
284
+ try {
285
+ const { json = false } = options;
286
+
287
+ if (!email) {
288
+ throw new Error('Email address or domain is required');
289
+ }
290
+
291
+ removeTrustedSender(email);
292
+
293
+ const result = {
294
+ action: 'removed',
295
+ entry: email,
296
+ path: getWhitelistFilePath(),
297
+ };
298
+
299
+ if (json) {
300
+ console.log(JSON.stringify(result, null, 2));
301
+ } else {
302
+ console.log(`❌ Removed from whitelist: ${email}`);
303
+ console.log(` File: ${result.path}`);
304
+ }
305
+ } catch (error) {
306
+ handleError(error, { json: options.json });
307
+ }
308
+ }
309
+
310
+ export async function showTrustedSenders(options) {
311
+ try {
312
+ const { json = false } = options;
313
+
314
+ const trustedSenders = listTrustedSenders();
315
+
316
+ if (json) {
317
+ console.log(JSON.stringify({ trustedSenders, path: getWhitelistFilePath() }, null, 2));
318
+ } else {
319
+ console.log(`📋 Trusted senders whitelist:`);
320
+ console.log(` File: ${getWhitelistFilePath()}`);
321
+ console.log('');
322
+
323
+ if (trustedSenders.length === 0) {
324
+ console.log(' (empty)');
325
+ } else {
326
+ trustedSenders.forEach(entry => {
327
+ if (entry.startsWith('@')) {
328
+ console.log(` 🌐 ${entry} (domain)`);
329
+ } else {
330
+ console.log(` 📧 ${entry}`);
331
+ }
332
+ });
333
+ }
334
+
335
+ console.log('');
336
+ console.log(` Total: ${trustedSenders.length} entries`);
337
+ }
338
+ } catch (error) {
339
+ handleError(error, { json: options.json });
340
+ }
341
+ }
342
+
343
+ export default {
344
+ list: listMails,
345
+ read: readMail,
346
+ send: sendMail,
347
+ search: searchMails,
348
+ attachments: listAttachments,
349
+ downloadAttachment,
350
+ trust: trustSender,
351
+ untrust: untrustSender,
352
+ trusted: showTrustedSenders,
353
+ };