m365-cli 0.1.3 → 0.1.4
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/config/default.json +1 -0
- package/package.json +6 -2
- package/src/commands/mail.js +37 -3
- package/src/graph/client.js +15 -0
- package/src/utils/trusted-senders.js +16 -7
package/config/default.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"https://graph.microsoft.com/Calendars.ReadWrite",
|
|
8
8
|
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
9
9
|
"https://graph.microsoft.com/Sites.ReadWrite.All",
|
|
10
|
+
"https://graph.microsoft.com/User.Read",
|
|
10
11
|
"offline_access"
|
|
11
12
|
],
|
|
12
13
|
"graphApiUrl": "https://graph.microsoft.com/v1.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "m365-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"m365": "./bin/m365.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"test:run": "vitest run",
|
|
18
19
|
"link": "npm link",
|
|
19
20
|
"unlink": "npm unlink -g m365-cli"
|
|
20
21
|
},
|
|
@@ -42,5 +43,8 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"commander": "^12.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"vitest": "^4.0.18"
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/commands/mail.js
CHANGED
|
@@ -5,6 +5,26 @@ import { outputMailList, outputMailDetail, outputSendResult, outputAttachmentLis
|
|
|
5
5
|
import { handleError } from '../utils/error.js';
|
|
6
6
|
import { isTrustedSender, addTrustedSender, removeTrustedSender, listTrustedSenders, getWhitelistFilePath } from '../utils/trusted-senders.js';
|
|
7
7
|
|
|
8
|
+
// Cache for current user's email
|
|
9
|
+
let currentUserEmailCache = null;
|
|
10
|
+
let currentUserEmailWarningShown = false;
|
|
11
|
+
|
|
12
|
+
async function getCurrentUserEmail() {
|
|
13
|
+
if (currentUserEmailCache) return currentUserEmailCache;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const user = await graphClient.getCurrentUser();
|
|
17
|
+
currentUserEmailCache = user.mail || user.userPrincipalName || null;
|
|
18
|
+
return currentUserEmailCache;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (!currentUserEmailWarningShown) {
|
|
21
|
+
console.error('Warning: Failed to get current user email (need User.Read permission):', error.message);
|
|
22
|
+
currentUserEmailWarningShown = true;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
29
|
* Mail commands
|
|
10
30
|
*/
|
|
@@ -18,12 +38,14 @@ export async function listMails(options) {
|
|
|
18
38
|
|
|
19
39
|
const mails = await graphClient.mail.list({ top, folder });
|
|
20
40
|
|
|
41
|
+
const currentUserEmail = await getCurrentUserEmail();
|
|
42
|
+
|
|
21
43
|
// Mark untrusted senders
|
|
22
44
|
const mailsWithTrustStatus = mails.map(mail => {
|
|
23
45
|
const senderEmail = mail.from?.emailAddress?.address;
|
|
24
46
|
return {
|
|
25
47
|
...mail,
|
|
26
|
-
isTrusted: senderEmail ? isTrustedSender(senderEmail) : false,
|
|
48
|
+
isTrusted: senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false,
|
|
27
49
|
};
|
|
28
50
|
});
|
|
29
51
|
|
|
@@ -46,9 +68,11 @@ export async function readMail(id, options) {
|
|
|
46
68
|
|
|
47
69
|
const mail = await graphClient.mail.get(id);
|
|
48
70
|
|
|
71
|
+
const currentUserEmail = await getCurrentUserEmail();
|
|
72
|
+
|
|
49
73
|
// Check whitelist unless --force is used
|
|
50
74
|
const senderEmail = mail.from?.emailAddress?.address;
|
|
51
|
-
const trusted = senderEmail ? isTrustedSender(senderEmail) : false;
|
|
75
|
+
const trusted = senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false;
|
|
52
76
|
|
|
53
77
|
if (!trusted && !force) {
|
|
54
78
|
// Filter content for untrusted senders
|
|
@@ -182,12 +206,22 @@ export async function searchMails(query, options) {
|
|
|
182
206
|
|
|
183
207
|
const mails = await graphClient.mail.search(query, { top });
|
|
184
208
|
|
|
209
|
+
const currentUserEmail = await getCurrentUserEmail();
|
|
210
|
+
|
|
211
|
+
const mailsWithTrustStatus = mails.map(mail => {
|
|
212
|
+
const senderEmail = mail.from?.emailAddress?.address;
|
|
213
|
+
return {
|
|
214
|
+
...mail,
|
|
215
|
+
isTrusted: senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
185
219
|
if (!json) {
|
|
186
220
|
console.log(`🔍 Search results for: "${query}"`);
|
|
187
221
|
console.log('');
|
|
188
222
|
}
|
|
189
223
|
|
|
190
|
-
outputMailList(
|
|
224
|
+
outputMailList(mailsWithTrustStatus, { json, top });
|
|
191
225
|
} catch (error) {
|
|
192
226
|
handleError(error, { json: options.json });
|
|
193
227
|
}
|
package/src/graph/client.js
CHANGED
|
@@ -103,6 +103,21 @@ class GraphClient {
|
|
|
103
103
|
async delete(endpoint, options = {}) {
|
|
104
104
|
return this.request(endpoint, { ...options, method: 'DELETE' });
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get current user profile
|
|
109
|
+
*/
|
|
110
|
+
async getCurrentUser() {
|
|
111
|
+
return this.get('/me', {
|
|
112
|
+
queryParams: { '$select': 'id,displayName,mail,userPrincipalName' },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mail endpoints
|
|
118
|
+
*/
|
|
119
|
+
mail = {
|
|
120
|
+
}
|
|
106
121
|
|
|
107
122
|
/**
|
|
108
123
|
* Mail endpoints
|
|
@@ -50,18 +50,27 @@ export function loadTrustedSenders() {
|
|
|
50
50
|
/**
|
|
51
51
|
* Check if a sender is trusted
|
|
52
52
|
* @param {string} senderEmail - Email address to check
|
|
53
|
+
* @param {string} [currentUserEmail] - Current user's email address (to trust own emails)
|
|
53
54
|
* @returns {boolean} True if sender is trusted
|
|
54
55
|
*/
|
|
55
|
-
export function isTrustedSender(senderEmail) {
|
|
56
|
+
export function isTrustedSender(senderEmail, currentUserEmail) {
|
|
56
57
|
if (!senderEmail) {
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Check if sender is the current user (own emails are trusted)
|
|
62
|
+
if (currentUserEmail) {
|
|
63
|
+
const normalizedSender = senderEmail.toLowerCase().trim();
|
|
64
|
+
const normalizedCurrentUser = currentUserEmail.toLowerCase().trim();
|
|
65
|
+
if (normalizedSender === normalizedCurrentUser) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
// Handle Exchange DN format (internal mail)
|
|
61
71
|
// These are formatted like: /O=EXCHANGELABS/OU=.../CN=RECIPIENTS/CN=...
|
|
62
72
|
if (senderEmail.startsWith('/O=EXCHANGELABS') || senderEmail.startsWith('/O=EXCHANGE')) {
|
|
63
73
|
// Internal organization mail - consider trusted
|
|
64
|
-
// In a production environment, you might want to be more selective
|
|
65
74
|
return true;
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -71,7 +80,7 @@ export function isTrustedSender(senderEmail) {
|
|
|
71
80
|
for (const entry of trustedSenders) {
|
|
72
81
|
const normalized = entry.toLowerCase();
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
// Domain match (e.g., @example.com)
|
|
75
84
|
if (normalized.startsWith('@')) {
|
|
76
85
|
const domain = normalized.substring(1);
|
|
77
86
|
if (normalizedEmail.endsWith(`@${domain}`)) {
|
|
@@ -117,10 +126,10 @@ export function addTrustedSender(email) {
|
|
|
117
126
|
writeFileSync(path, readFileSync(path, 'utf-8') + line, 'utf-8');
|
|
118
127
|
} else {
|
|
119
128
|
// Create new file with header
|
|
120
|
-
const header = `# M365 Trusted Senders Whitelist
|
|
121
|
-
# One email address or domain per line
|
|
122
|
-
# Lines starting with @ match entire domains (e.g. @example.com)
|
|
123
|
-
# Senders not in this list will have their email body filtered out
|
|
129
|
+
const header = `# M365 Trusted Senders Whitelist
|
|
130
|
+
# One email address or domain per line
|
|
131
|
+
# Lines starting with @ match entire domains (e.g. @example.com)
|
|
132
|
+
# Senders not in this list will have their email body filtered out
|
|
124
133
|
|
|
125
134
|
`;
|
|
126
135
|
writeFileSync(path, header + email + '\n', 'utf-8');
|