@littlebearapps/outlook-assistant 3.6.0 → 3.7.1
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 +1 -1
- package/advanced/index.js +5 -5
- package/auth/device-code.js +3 -1
- package/auth/oauth-server.js +4 -4
- package/auth/token-manager.js +2 -2
- package/calendar/list.js +3 -3
- package/categories/index.js +4 -4
- package/contacts/index.js +11 -18
- package/email/conversations.js +19 -17
- package/email/delta.js +1 -1
- package/email/draft.js +6 -8
- package/email/folder-utils.js +1 -1
- package/email/headers.js +10 -10
- package/email/list.js +1 -1
- package/email/mime.js +13 -12
- package/email/search.js +280 -24
- package/email/send.js +1 -1
- package/folder/list.js +1 -1
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/rules/create.js +85 -161
- package/rules/index.js +162 -29
- package/rules/list.js +144 -62
- package/rules/rule-builder.js +284 -0
- package/rules/update.js +213 -0
- package/settings/index.js +10 -22
- package/utils/graph-api.js +1 -1
- package/utils/response-formatter.js +8 -6
- package/utils/safety.js +113 -0
package/rules/update.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update rule functionality
|
|
3
|
+
*/
|
|
4
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
|
+
const { ensureAuthenticated } = require('../auth');
|
|
6
|
+
const { checkRateLimit, formatRuleDryRunPreview } = require('../utils/safety');
|
|
7
|
+
const { getInboxRules } = require('./list');
|
|
8
|
+
const {
|
|
9
|
+
buildConditions,
|
|
10
|
+
buildActions,
|
|
11
|
+
buildExceptions,
|
|
12
|
+
} = require('./rule-builder');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update rule handler — modify an existing rule by name or ID.
|
|
16
|
+
* Conditions, actions, and exceptions use replace semantics (not merge).
|
|
17
|
+
* @param {object} args - Tool arguments
|
|
18
|
+
* @returns {object} - MCP response
|
|
19
|
+
*/
|
|
20
|
+
async function handleUpdateRule(args) {
|
|
21
|
+
const { ruleName, ruleId, name, isEnabled, sequence, dryRun } = args;
|
|
22
|
+
|
|
23
|
+
// Rate limit
|
|
24
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
25
|
+
if (rateLimitError) return rateLimitError;
|
|
26
|
+
|
|
27
|
+
if (!ruleName && !ruleId) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: 'Either ruleName or ruleId is required to identify the rule to update.',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: 'Sequence must be a positive number greater than zero.',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const accessToken = await ensureAuthenticated();
|
|
51
|
+
|
|
52
|
+
// Resolve rule ID
|
|
53
|
+
let resolvedId = ruleId;
|
|
54
|
+
let currentRule = null;
|
|
55
|
+
|
|
56
|
+
const rules = await getInboxRules(accessToken);
|
|
57
|
+
|
|
58
|
+
if (resolvedId) {
|
|
59
|
+
currentRule = rules.find((r) => r.id === resolvedId);
|
|
60
|
+
} else {
|
|
61
|
+
currentRule = rules.find((r) => r.displayName === ruleName);
|
|
62
|
+
if (currentRule) resolvedId = currentRule.id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!currentRule) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Rule "${ruleName || ruleId}" not found.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build PATCH payload — only include fields the user provided
|
|
77
|
+
const patch = {};
|
|
78
|
+
const allWarnings = [];
|
|
79
|
+
|
|
80
|
+
if (name) patch.displayName = name;
|
|
81
|
+
if (isEnabled !== undefined) patch.isEnabled = isEnabled;
|
|
82
|
+
if (sequence) patch.sequence = Math.max(1, Math.floor(sequence));
|
|
83
|
+
|
|
84
|
+
// Build conditions if any condition param was provided
|
|
85
|
+
const { conditions, warnings: condWarnings } = buildConditions(args);
|
|
86
|
+
if (Object.keys(conditions).length > 0) {
|
|
87
|
+
patch.conditions = conditions;
|
|
88
|
+
allWarnings.push(
|
|
89
|
+
'Conditions replaced (not merged). Use dryRun=true to preview before updating.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
allWarnings.push(...condWarnings);
|
|
93
|
+
|
|
94
|
+
// Build actions if any action param was provided
|
|
95
|
+
const { actions, warnings: actWarnings } = await buildActions(
|
|
96
|
+
args,
|
|
97
|
+
accessToken
|
|
98
|
+
);
|
|
99
|
+
if (Object.keys(actions).length > 0) {
|
|
100
|
+
patch.actions = actions;
|
|
101
|
+
allWarnings.push(
|
|
102
|
+
'Actions replaced (not merged). Use dryRun=true to preview before updating.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
allWarnings.push(...actWarnings);
|
|
106
|
+
|
|
107
|
+
// Check for fatal folder-not-found
|
|
108
|
+
const folderNotFound = actWarnings.some((w) => w.includes('not found'));
|
|
109
|
+
if (folderNotFound && Object.keys(actions).length === 0) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build exceptions if any except* param was provided
|
|
121
|
+
const { exceptions, warnings: excWarnings } = buildExceptions(args);
|
|
122
|
+
if (Object.keys(exceptions).length > 0) {
|
|
123
|
+
patch.exceptions = exceptions;
|
|
124
|
+
}
|
|
125
|
+
allWarnings.push(...excWarnings);
|
|
126
|
+
|
|
127
|
+
// Check that something is actually being changed
|
|
128
|
+
if (Object.keys(patch).length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: `No changes specified for rule "${currentRule.displayName}". Provide at least one parameter to update (name, isEnabled, sequence, conditions, actions, or exceptions).`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Dry-run: show before/after preview
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
const updatedRule = { ...currentRule, ...patch };
|
|
142
|
+
const currentPreview = formatRuleDryRunPreview(currentRule);
|
|
143
|
+
const updatedPreview = formatRuleDryRunPreview(updatedRule);
|
|
144
|
+
|
|
145
|
+
let text = `DRY RUN — Update preview for "${currentRule.displayName}" (not applied):\n\n`;
|
|
146
|
+
text += `CURRENT:\n${currentPreview}\n\n`;
|
|
147
|
+
text += `AFTER UPDATE:\n${updatedPreview}`;
|
|
148
|
+
if (allWarnings.length > 0) {
|
|
149
|
+
text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: 'text', text }],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Execute PATCH
|
|
157
|
+
await callGraphAPI(
|
|
158
|
+
accessToken,
|
|
159
|
+
'PATCH',
|
|
160
|
+
`me/mailFolders/inbox/messageRules/${resolvedId}`,
|
|
161
|
+
patch
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const changedFields = Object.keys(patch)
|
|
165
|
+
.map((k) => {
|
|
166
|
+
if (k === 'displayName') return `name → "${patch.displayName}"`;
|
|
167
|
+
if (k === 'isEnabled') {
|
|
168
|
+
return patch.isEnabled ? 'enabled' : 'disabled';
|
|
169
|
+
}
|
|
170
|
+
if (k === 'sequence') return `sequence → ${patch.sequence}`;
|
|
171
|
+
if (k === 'conditions') return 'conditions updated';
|
|
172
|
+
if (k === 'actions') return 'actions updated';
|
|
173
|
+
if (k === 'exceptions') return 'exceptions updated';
|
|
174
|
+
return k;
|
|
175
|
+
})
|
|
176
|
+
.join(', ');
|
|
177
|
+
|
|
178
|
+
let text = `Successfully updated rule "${currentRule.displayName}": ${changedFields}.`;
|
|
179
|
+
// Filter out the replace/merge info warnings from the response since the update is done
|
|
180
|
+
const relevantWarnings = allWarnings.filter(
|
|
181
|
+
(w) => !w.includes('replaced (not merged)')
|
|
182
|
+
);
|
|
183
|
+
if (relevantWarnings.length > 0) {
|
|
184
|
+
text += `\n\nNotes:\n${relevantWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text }],
|
|
189
|
+
};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.message === 'Authentication required') {
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: `Error updating rule: ${error.message}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = handleUpdateRule;
|
package/settings/index.js
CHANGED
|
@@ -56,13 +56,7 @@ function formatAutomaticReplies(settings) {
|
|
|
56
56
|
|
|
57
57
|
const lines = [];
|
|
58
58
|
lines.push(
|
|
59
|
-
`**Status**: ${
|
|
60
|
-
settings.status === 'disabled'
|
|
61
|
-
? '🔴 Disabled'
|
|
62
|
-
: settings.status === 'alwaysEnabled'
|
|
63
|
-
? '🟢 Always Enabled'
|
|
64
|
-
: '📅 Scheduled'
|
|
65
|
-
}`
|
|
59
|
+
`**Status**: ${{ disabled: '🔴 Disabled', alwaysEnabled: '🟢 Always Enabled' }[settings.status] || '📅 Scheduled'}`
|
|
66
60
|
);
|
|
67
61
|
|
|
68
62
|
if (settings.status !== 'disabled') {
|
|
@@ -113,7 +107,7 @@ async function handleGetMailboxSettings(args) {
|
|
|
113
107
|
|
|
114
108
|
const settings = await callGraphAPI(accessToken, 'GET', endpoint);
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
const output = [];
|
|
117
111
|
output.push('# Mailbox Settings\n');
|
|
118
112
|
|
|
119
113
|
if (section && section !== 'all') {
|
|
@@ -287,7 +281,7 @@ async function handleSetAutomaticReplies(args) {
|
|
|
287
281
|
'me/mailboxSettings/automaticRepliesSetting'
|
|
288
282
|
);
|
|
289
283
|
|
|
290
|
-
|
|
284
|
+
const output = [];
|
|
291
285
|
output.push('Automatic replies updated!\n');
|
|
292
286
|
output.push(formatAutomaticReplies(updated));
|
|
293
287
|
|
|
@@ -413,17 +407,11 @@ async function handleSetWorkingHours(args) {
|
|
|
413
407
|
);
|
|
414
408
|
|
|
415
409
|
// Build update
|
|
410
|
+
const padTime = (t) =>
|
|
411
|
+
t.length === 5 ? `${t}:00.0000000` : `${t}.0000000`;
|
|
416
412
|
const workingHours = {
|
|
417
|
-
startTime: startTime
|
|
418
|
-
|
|
419
|
-
? startTime + ':00.0000000'
|
|
420
|
-
: startTime + '.0000000'
|
|
421
|
-
: current.startTime,
|
|
422
|
-
endTime: endTime
|
|
423
|
-
? endTime.length === 5
|
|
424
|
-
? endTime + ':00.0000000'
|
|
425
|
-
: endTime + '.0000000'
|
|
426
|
-
: current.endTime,
|
|
413
|
+
startTime: startTime ? padTime(startTime) : current.startTime,
|
|
414
|
+
endTime: endTime ? padTime(endTime) : current.endTime,
|
|
427
415
|
daysOfWeek: daysOfWeek
|
|
428
416
|
? daysOfWeek.map((d) => d.toLowerCase())
|
|
429
417
|
: current.daysOfWeek,
|
|
@@ -442,7 +430,7 @@ async function handleSetWorkingHours(args) {
|
|
|
442
430
|
'me/mailboxSettings/workingHours'
|
|
443
431
|
);
|
|
444
432
|
|
|
445
|
-
|
|
433
|
+
const output = [];
|
|
446
434
|
output.push('Working hours updated!\n');
|
|
447
435
|
output.push(formatWorkingHours(updated));
|
|
448
436
|
|
|
@@ -491,7 +479,7 @@ async function handleGetAutomaticReplies() {
|
|
|
491
479
|
'me/mailboxSettings/automaticRepliesSetting'
|
|
492
480
|
);
|
|
493
481
|
|
|
494
|
-
|
|
482
|
+
const output = [];
|
|
495
483
|
output.push('# Automatic Replies\n');
|
|
496
484
|
output.push(formatAutomaticReplies(settings));
|
|
497
485
|
|
|
@@ -533,7 +521,7 @@ async function handleGetWorkingHours() {
|
|
|
533
521
|
'me/mailboxSettings/workingHours'
|
|
534
522
|
);
|
|
535
523
|
|
|
536
|
-
|
|
524
|
+
const output = [];
|
|
537
525
|
output.push('# Working Hours\n');
|
|
538
526
|
output.push(formatWorkingHours(settings));
|
|
539
527
|
|
package/utils/graph-api.js
CHANGED
|
@@ -187,7 +187,7 @@ function formatEmailListAsTable(emails, folder, meta = {}) {
|
|
|
187
187
|
const date = formatDateShort(email.receivedDateTime);
|
|
188
188
|
const status = email.isRead ? '📖' : '📬';
|
|
189
189
|
const subject = truncateText(email.subject || '(no subject)', 40);
|
|
190
|
-
const shortId = email.id.substring(0, 20)
|
|
190
|
+
const shortId = `${email.id.substring(0, 20)}...`;
|
|
191
191
|
|
|
192
192
|
output += `| ${i + 1} | ${status} | ${date} | ${truncateText(from, 20)} | ${subject} | \`${shortId}\` |\n`;
|
|
193
193
|
});
|
|
@@ -237,10 +237,12 @@ function formatEmailContent(
|
|
|
237
237
|
|
|
238
238
|
if (verbosity === VERBOSITY.FULL) {
|
|
239
239
|
output += `\n**ID:** \`${email.id}\``;
|
|
240
|
-
if (email.conversationId)
|
|
240
|
+
if (email.conversationId) {
|
|
241
241
|
output += `\n**Conversation ID:** \`${email.conversationId}\``;
|
|
242
|
-
|
|
242
|
+
}
|
|
243
|
+
if (email.internetMessageId) {
|
|
243
244
|
output += `\n**Message-ID:** \`${email.internetMessageId}\``;
|
|
245
|
+
}
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
output += '\n\n---\n\n';
|
|
@@ -457,7 +459,7 @@ function formatRecipients(recipients) {
|
|
|
457
459
|
function truncateText(text, maxLength) {
|
|
458
460
|
if (!text) return '';
|
|
459
461
|
if (text.length <= maxLength) return text;
|
|
460
|
-
return text.substring(0, maxLength - 3)
|
|
462
|
+
return `${text.substring(0, maxLength - 3)}...`;
|
|
461
463
|
}
|
|
462
464
|
|
|
463
465
|
/**
|
|
@@ -490,10 +492,10 @@ function escapeCSV(value) {
|
|
|
490
492
|
const isFormula = formulaChars.some((ch) => str.startsWith(ch));
|
|
491
493
|
|
|
492
494
|
if (isFormula) {
|
|
493
|
-
return
|
|
495
|
+
return `"` + `'${str.replace(/"/g, '""')}"`;
|
|
494
496
|
}
|
|
495
497
|
if (needsQuoting) {
|
|
496
|
-
return
|
|
498
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
497
499
|
}
|
|
498
500
|
return str;
|
|
499
501
|
}
|
package/utils/safety.js
CHANGED
|
@@ -116,8 +116,121 @@ function formatDryRunPreview(emailObject) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Format a dry-run preview for a mail rule (create or update).
|
|
121
|
+
* @param {object} rule - The composed Graph API rule object
|
|
122
|
+
* @returns {string} - Human-readable rule preview text
|
|
123
|
+
*/
|
|
124
|
+
function formatRuleDryRunPreview(rule) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
|
|
127
|
+
lines.push(`Name: ${rule.displayName}`);
|
|
128
|
+
lines.push(`Enabled: ${rule.isEnabled !== false}`);
|
|
129
|
+
if (rule.sequence) lines.push(`Sequence: ${rule.sequence}`);
|
|
130
|
+
|
|
131
|
+
// Conditions
|
|
132
|
+
const cond = rule.conditions || {};
|
|
133
|
+
const condParts = [];
|
|
134
|
+
if (cond.fromAddresses?.length > 0) {
|
|
135
|
+
condParts.push(
|
|
136
|
+
`From: ${cond.fromAddresses.map((a) => a.emailAddress?.address).join(', ')}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (cond.subjectContains?.length > 0) {
|
|
140
|
+
condParts.push(
|
|
141
|
+
`Subject contains (any): "${cond.subjectContains.join('", "')}"`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (cond.bodyContains?.length > 0) {
|
|
145
|
+
condParts.push(`Body contains (any): "${cond.bodyContains.join('", "')}"`);
|
|
146
|
+
}
|
|
147
|
+
if (cond.bodyOrSubjectContains?.length > 0) {
|
|
148
|
+
condParts.push(
|
|
149
|
+
`Body or subject contains (any): "${cond.bodyOrSubjectContains.join('", "')}"`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (cond.senderContains?.length > 0) {
|
|
153
|
+
condParts.push(
|
|
154
|
+
`Sender contains (any): "${cond.senderContains.join('", "')}"`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (cond.recipientContains?.length > 0) {
|
|
158
|
+
condParts.push(
|
|
159
|
+
`Recipient contains (any): "${cond.recipientContains.join('", "')}"`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (cond.sentToAddresses?.length > 0) {
|
|
163
|
+
condParts.push(
|
|
164
|
+
`Sent to: ${cond.sentToAddresses.map((a) => a.emailAddress?.address).join(', ')}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (cond.hasAttachment === true) condParts.push('Has attachment');
|
|
168
|
+
if (cond.importance) condParts.push(`Importance: ${cond.importance}`);
|
|
169
|
+
if (cond.sensitivity) condParts.push(`Sensitivity: ${cond.sensitivity}`);
|
|
170
|
+
if (cond.sentToMe === true) condParts.push('Sent to me');
|
|
171
|
+
if (cond.sentOnlyToMe === true) condParts.push('Sent only to me');
|
|
172
|
+
if (cond.sentCcMe === true) condParts.push('I am in CC');
|
|
173
|
+
if (cond.isAutomaticReply === true) condParts.push('Is automatic reply');
|
|
174
|
+
if (condParts.length > 0) {
|
|
175
|
+
lines.push(`Conditions: ${condParts.join('; ')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Actions
|
|
179
|
+
const act = rule.actions || {};
|
|
180
|
+
const actParts = [];
|
|
181
|
+
if (act.moveToFolder) actParts.push(`Move to folder: ${act.moveToFolder}`);
|
|
182
|
+
if (act.copyToFolder) actParts.push(`Copy to folder: ${act.copyToFolder}`);
|
|
183
|
+
if (act.markAsRead === true) actParts.push('Mark as read');
|
|
184
|
+
if (act.markImportance) {
|
|
185
|
+
actParts.push(`Mark importance: ${act.markImportance}`);
|
|
186
|
+
}
|
|
187
|
+
if (act.forwardTo?.length > 0) {
|
|
188
|
+
actParts.push(
|
|
189
|
+
`Forward to: ${act.forwardTo.map((r) => r.emailAddress?.address).join(', ')}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (act.redirectTo?.length > 0) {
|
|
193
|
+
actParts.push(
|
|
194
|
+
`Redirect to: ${act.redirectTo.map((r) => r.emailAddress?.address).join(', ')}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (act.assignCategories?.length > 0) {
|
|
198
|
+
actParts.push(`Assign categories: ${act.assignCategories.join(', ')}`);
|
|
199
|
+
}
|
|
200
|
+
if (act.stopProcessingRules === true) actParts.push('Stop processing rules');
|
|
201
|
+
if (act.delete === true) actParts.push('Delete (move to Deleted Items)');
|
|
202
|
+
if (actParts.length > 0) {
|
|
203
|
+
lines.push(`Actions: ${actParts.join('; ')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Exceptions
|
|
207
|
+
const exc = rule.exceptions || {};
|
|
208
|
+
const excParts = [];
|
|
209
|
+
if (exc.fromAddresses?.length > 0) {
|
|
210
|
+
excParts.push(
|
|
211
|
+
`From: ${exc.fromAddresses.map((a) => a.emailAddress?.address).join(', ')}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (exc.subjectContains?.length > 0) {
|
|
215
|
+
excParts.push(`Subject contains: "${exc.subjectContains.join('", "')}"`);
|
|
216
|
+
}
|
|
217
|
+
if (exc.senderContains?.length > 0) {
|
|
218
|
+
excParts.push(`Sender contains: "${exc.senderContains.join('", "')}"`);
|
|
219
|
+
}
|
|
220
|
+
if (exc.bodyContains?.length > 0) {
|
|
221
|
+
excParts.push(`Body contains: "${exc.bodyContains.join('", "')}"`);
|
|
222
|
+
}
|
|
223
|
+
if (exc.hasAttachment === true) excParts.push('Has attachment');
|
|
224
|
+
if (excParts.length > 0) {
|
|
225
|
+
lines.push(`Exceptions (rule skipped when): ${excParts.join('; ')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
119
231
|
module.exports = {
|
|
120
232
|
checkRateLimit,
|
|
121
233
|
checkRecipientAllowlist,
|
|
122
234
|
formatDryRunPreview,
|
|
235
|
+
formatRuleDryRunPreview,
|
|
123
236
|
};
|