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,190 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ /**
6
+ * Trusted senders whitelist manager
7
+ * Protects against phishing by filtering email content from untrusted senders
8
+ */
9
+
10
+ // Whitelist file paths (check in order)
11
+ const WHITELIST_PATHS = [
12
+ join(homedir(), '.m365-cli/trusted-senders.txt'),
13
+ ];
14
+
15
+ /**
16
+ * Get the active whitelist file path
17
+ */
18
+ function getWhitelistPath() {
19
+ // Return first existing path
20
+ for (const path of WHITELIST_PATHS) {
21
+ if (existsSync(path)) {
22
+ return path;
23
+ }
24
+ }
25
+
26
+ // Default to second path if none exist
27
+ return WHITELIST_PATHS[1];
28
+ }
29
+
30
+ /**
31
+ * Load trusted senders from file
32
+ * @returns {Array<string>} List of trusted email addresses and domains
33
+ */
34
+ export function loadTrustedSenders() {
35
+ const path = getWhitelistPath();
36
+
37
+ if (!existsSync(path)) {
38
+ return [];
39
+ }
40
+
41
+ try {
42
+ const content = readFileSync(path, 'utf-8');
43
+ return content
44
+ .split('\n')
45
+ .map(line => line.trim())
46
+ .filter(line => line && !line.startsWith('#')); // Skip comments and empty lines
47
+ } catch (error) {
48
+ console.error(`Warning: Failed to read whitelist from ${path}:`, error.message);
49
+ return [];
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check if a sender is trusted
55
+ * @param {string} senderEmail - Email address to check
56
+ * @returns {boolean} True if sender is trusted
57
+ */
58
+ export function isTrustedSender(senderEmail) {
59
+ if (!senderEmail) {
60
+ return false;
61
+ }
62
+
63
+ // Handle Exchange DN format (internal mail)
64
+ // These are formatted like: /O=EXCHANGELABS/OU=.../CN=RECIPIENTS/CN=...
65
+ if (senderEmail.startsWith('/O=EXCHANGELABS') || senderEmail.startsWith('/O=EXCHANGE')) {
66
+ // Internal organization mail - consider trusted
67
+ // In a production environment, you might want to be more selective
68
+ return true;
69
+ }
70
+
71
+ const trustedSenders = loadTrustedSenders();
72
+ const normalizedEmail = senderEmail.toLowerCase().trim();
73
+
74
+ for (const entry of trustedSenders) {
75
+ const normalized = entry.toLowerCase();
76
+
77
+ // Domain match (e.g., @example.com)
78
+ if (normalized.startsWith('@')) {
79
+ const domain = normalized.substring(1);
80
+ if (normalizedEmail.endsWith(`@${domain}`)) {
81
+ return true;
82
+ }
83
+ }
84
+ // Exact email match
85
+ else if (normalized === normalizedEmail) {
86
+ return true;
87
+ }
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Add a sender to the whitelist
95
+ * @param {string} email - Email address or domain to trust
96
+ */
97
+ export function addTrustedSender(email) {
98
+ const path = getWhitelistPath();
99
+ const trustedSenders = loadTrustedSenders();
100
+
101
+ // Normalize input
102
+ const normalized = email.toLowerCase().trim();
103
+
104
+ // Check if already trusted
105
+ if (trustedSenders.some(entry => entry.toLowerCase() === normalized)) {
106
+ throw new Error(`Already trusted: ${email}`);
107
+ }
108
+
109
+ // Ensure directory exists
110
+ const dir = dirname(path);
111
+ if (!existsSync(dir)) {
112
+ mkdirSync(dir, { recursive: true });
113
+ }
114
+
115
+ // Append to file
116
+ const line = `\n${email}`;
117
+
118
+ try {
119
+ if (existsSync(path)) {
120
+ writeFileSync(path, readFileSync(path, 'utf-8') + line, 'utf-8');
121
+ } else {
122
+ // Create new file with header
123
+ const header = `# M365 Trusted Senders Whitelist\n
124
+ # One email address or domain per line\n
125
+ # Lines starting with @ match entire domains (e.g. @example.com)\n
126
+ # Senders not in this list will have their email body filtered out\n
127
+
128
+ `;
129
+ writeFileSync(path, header + email + '\n', 'utf-8');
130
+ }
131
+ } catch (error) {
132
+ throw new Error(`Failed to add trusted sender: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Remove a sender from the whitelist
138
+ * @param {string} email - Email address or domain to untrust
139
+ */
140
+ export function removeTrustedSender(email) {
141
+ const path = getWhitelistPath();
142
+
143
+ if (!existsSync(path)) {
144
+ throw new Error('Whitelist file does not exist');
145
+ }
146
+
147
+ const trustedSenders = loadTrustedSenders();
148
+ const normalized = email.toLowerCase().trim();
149
+
150
+ // Find matching entry (case-insensitive)
151
+ const matchingEntry = trustedSenders.find(
152
+ entry => entry.toLowerCase() === normalized
153
+ );
154
+
155
+ if (!matchingEntry) {
156
+ throw new Error(`Not in whitelist: ${email}`);
157
+ }
158
+
159
+ try {
160
+ // Read full content
161
+ const content = readFileSync(path, 'utf-8');
162
+
163
+ // Remove the matching line
164
+ const lines = content.split('\n');
165
+ const filtered = lines.filter(line => {
166
+ const trimmed = line.trim();
167
+ return trimmed !== matchingEntry;
168
+ });
169
+
170
+ writeFileSync(path, filtered.join('\n'), 'utf-8');
171
+ } catch (error) {
172
+ throw new Error(`Failed to remove trusted sender: ${error.message}`);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * List all trusted senders
178
+ * @returns {Array<string>} List of trusted entries
179
+ */
180
+ export function listTrustedSenders() {
181
+ return loadTrustedSenders();
182
+ }
183
+
184
+ /**
185
+ * Get whitelist file path (for display purposes)
186
+ * @returns {string} Path to whitelist file
187
+ */
188
+ export function getWhitelistFilePath() {
189
+ return getWhitelistPath();
190
+ }