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.
- package/README.md +683 -0
- package/bin/m365.js +489 -0
- package/config/default.json +18 -0
- package/package.json +36 -0
- package/src/auth/device-flow.js +154 -0
- package/src/auth/token-manager.js +237 -0
- package/src/commands/calendar.js +279 -0
- package/src/commands/mail.js +353 -0
- package/src/commands/onedrive.js +423 -0
- package/src/commands/sharepoint.js +312 -0
- package/src/graph/client.js +875 -0
- package/src/utils/config.js +60 -0
- package/src/utils/error.js +114 -0
- package/src/utils/output.js +850 -0
- package/src/utils/trusted-senders.js +190 -0
|
@@ -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
|
+
};
|