inboxctl 0.2.0 → 0.3.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/dist/{chunk-NUN2WRBN.js → chunk-OLL3OA5B.js} +966 -150
- package/dist/chunk-OLL3OA5B.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-NUN2WRBN.js.map +0 -1
|
@@ -8,7 +8,7 @@ var __export = (target, all) => {
|
|
|
8
8
|
// src/mcp/server.ts
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import { z as
|
|
11
|
+
import { z as z4 } from "zod";
|
|
12
12
|
|
|
13
13
|
// src/core/actions/audit.ts
|
|
14
14
|
import { randomUUID } from "crypto";
|
|
@@ -2549,8 +2549,46 @@ async function unsubscribe(options) {
|
|
|
2549
2549
|
};
|
|
2550
2550
|
}
|
|
2551
2551
|
|
|
2552
|
+
// src/core/stats/anomalies.ts
|
|
2553
|
+
import { z } from "zod";
|
|
2554
|
+
|
|
2552
2555
|
// src/core/stats/common.ts
|
|
2553
2556
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2557
|
+
var SYSTEM_LABEL_IDS = [
|
|
2558
|
+
"INBOX",
|
|
2559
|
+
"UNREAD",
|
|
2560
|
+
"STARRED",
|
|
2561
|
+
"IMPORTANT",
|
|
2562
|
+
"SENT",
|
|
2563
|
+
"DRAFT",
|
|
2564
|
+
"TRASH",
|
|
2565
|
+
"SPAM",
|
|
2566
|
+
"ALL_MAIL",
|
|
2567
|
+
"SNOOZED",
|
|
2568
|
+
"CHAT",
|
|
2569
|
+
"CATEGORY_PERSONAL",
|
|
2570
|
+
"CATEGORY_SOCIAL",
|
|
2571
|
+
"CATEGORY_PROMOTIONS",
|
|
2572
|
+
"CATEGORY_UPDATES",
|
|
2573
|
+
"CATEGORY_FORUMS"
|
|
2574
|
+
];
|
|
2575
|
+
var CATEGORY_LABEL_PREFIX = "CATEGORY_";
|
|
2576
|
+
var SYSTEM_LABEL_ID_SET = new Set(SYSTEM_LABEL_IDS);
|
|
2577
|
+
var AUTOMATED_ADDRESS_MARKERS = [
|
|
2578
|
+
"noreply",
|
|
2579
|
+
"no-reply",
|
|
2580
|
+
"no_reply",
|
|
2581
|
+
"newsletter",
|
|
2582
|
+
"notifications",
|
|
2583
|
+
"notification",
|
|
2584
|
+
"mailer",
|
|
2585
|
+
"info@",
|
|
2586
|
+
"hello@",
|
|
2587
|
+
"support@",
|
|
2588
|
+
"marketing",
|
|
2589
|
+
"promo",
|
|
2590
|
+
"updates"
|
|
2591
|
+
];
|
|
2554
2592
|
var SYSTEM_LABEL_NAMES = /* @__PURE__ */ new Map([
|
|
2555
2593
|
["INBOX", "Inbox"],
|
|
2556
2594
|
["UNREAD", "Unread"],
|
|
@@ -2608,6 +2646,14 @@ function getPeriodStart(period = "all", now2 = Date.now()) {
|
|
|
2608
2646
|
function resolveLabelName(labelId) {
|
|
2609
2647
|
return SYSTEM_LABEL_NAMES.get(labelId) || getCachedLabelName(labelId) || labelId;
|
|
2610
2648
|
}
|
|
2649
|
+
function isUserLabel(labelId) {
|
|
2650
|
+
const trimmed = labelId.trim();
|
|
2651
|
+
return trimmed.length > 0 && !SYSTEM_LABEL_ID_SET.has(trimmed) && !trimmed.startsWith(CATEGORY_LABEL_PREFIX);
|
|
2652
|
+
}
|
|
2653
|
+
function isLikelyAutomatedSenderAddress(sender) {
|
|
2654
|
+
const normalized = sender.trim().toLowerCase();
|
|
2655
|
+
return AUTOMATED_ADDRESS_MARKERS.some((marker) => normalized.includes(marker));
|
|
2656
|
+
}
|
|
2611
2657
|
function startOfLocalDay(now2 = Date.now()) {
|
|
2612
2658
|
const date = new Date(now2);
|
|
2613
2659
|
date.setHours(0, 0, 0, 0);
|
|
@@ -2627,28 +2673,6 @@ function startOfLocalMonth(now2 = Date.now()) {
|
|
|
2627
2673
|
return date.getTime();
|
|
2628
2674
|
}
|
|
2629
2675
|
|
|
2630
|
-
// src/core/stats/labels.ts
|
|
2631
|
-
async function getLabelDistribution() {
|
|
2632
|
-
const sqlite = getStatsSqlite();
|
|
2633
|
-
const rows = sqlite.prepare(
|
|
2634
|
-
`
|
|
2635
|
-
SELECT
|
|
2636
|
-
label.value AS labelId,
|
|
2637
|
-
COUNT(*) AS totalMessages,
|
|
2638
|
-
SUM(CASE WHEN e.is_read = 0 THEN 1 ELSE 0 END) AS unreadMessages
|
|
2639
|
-
FROM emails AS e, json_each(e.label_ids) AS label
|
|
2640
|
-
GROUP BY label.value
|
|
2641
|
-
ORDER BY totalMessages DESC, unreadMessages DESC, label.value ASC
|
|
2642
|
-
`
|
|
2643
|
-
).all();
|
|
2644
|
-
return rows.map((row) => ({
|
|
2645
|
-
labelId: row.labelId,
|
|
2646
|
-
labelName: resolveLabelName(row.labelId),
|
|
2647
|
-
totalMessages: row.totalMessages,
|
|
2648
|
-
unreadMessages: row.unreadMessages
|
|
2649
|
-
}));
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
2676
|
// src/core/stats/newsletters.ts
|
|
2653
2677
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2654
2678
|
var KNOWN_NEWSLETTER_LOCAL_PART = /^(newsletter|digest|noreply|no-reply|updates|news)([+._-].*)?$/i;
|
|
@@ -2812,8 +2836,277 @@ async function getNewsletters(options = {}) {
|
|
|
2812
2836
|
return rows.map(mapNewsletterRow);
|
|
2813
2837
|
}
|
|
2814
2838
|
|
|
2815
|
-
// src/core/stats/
|
|
2839
|
+
// src/core/stats/anomalies.ts
|
|
2816
2840
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
2841
|
+
var BULK_LABELS = /* @__PURE__ */ new Set([
|
|
2842
|
+
"newsletter",
|
|
2843
|
+
"newsletters",
|
|
2844
|
+
"promotion",
|
|
2845
|
+
"promotions",
|
|
2846
|
+
"social"
|
|
2847
|
+
]);
|
|
2848
|
+
var reviewCategorizedInputSchema = z.object({
|
|
2849
|
+
since: z.string().min(1).optional(),
|
|
2850
|
+
limit: z.number().int().positive().max(200).optional()
|
|
2851
|
+
}).strict();
|
|
2852
|
+
function toIsoString(value) {
|
|
2853
|
+
if (!value) {
|
|
2854
|
+
return null;
|
|
2855
|
+
}
|
|
2856
|
+
return new Date(value).toISOString();
|
|
2857
|
+
}
|
|
2858
|
+
function parseJsonArray3(raw) {
|
|
2859
|
+
if (!raw) {
|
|
2860
|
+
return [];
|
|
2861
|
+
}
|
|
2862
|
+
try {
|
|
2863
|
+
const parsed = JSON.parse(raw);
|
|
2864
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
2865
|
+
} catch {
|
|
2866
|
+
return [];
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function parseActions(raw) {
|
|
2870
|
+
try {
|
|
2871
|
+
const parsed = JSON.parse(raw);
|
|
2872
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2873
|
+
} catch {
|
|
2874
|
+
return [];
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
function resolveSinceTimestamp(since) {
|
|
2878
|
+
if (!since) {
|
|
2879
|
+
return Date.now() - 7 * DAY_MS2;
|
|
2880
|
+
}
|
|
2881
|
+
const parsed = Date.parse(since);
|
|
2882
|
+
if (Number.isNaN(parsed)) {
|
|
2883
|
+
throw new Error(`Invalid since value: ${since}`);
|
|
2884
|
+
}
|
|
2885
|
+
return parsed;
|
|
2886
|
+
}
|
|
2887
|
+
function isArchived(actions, beforeLabelIds, afterLabelIds) {
|
|
2888
|
+
if (actions.some((action) => action.type === "archive")) {
|
|
2889
|
+
return true;
|
|
2890
|
+
}
|
|
2891
|
+
return beforeLabelIds.includes("INBOX") && !afterLabelIds.includes("INBOX");
|
|
2892
|
+
}
|
|
2893
|
+
function resolveAssignedLabel(actions, beforeLabelIds, afterLabelIds) {
|
|
2894
|
+
const labelAction = actions.find(
|
|
2895
|
+
(action) => action.type === "label" && typeof action.label === "string" && action.label.trim().length > 0
|
|
2896
|
+
);
|
|
2897
|
+
if (labelAction) {
|
|
2898
|
+
return labelAction.label.trim();
|
|
2899
|
+
}
|
|
2900
|
+
const beforeUserLabels = new Set(beforeLabelIds.filter(isUserLabel));
|
|
2901
|
+
const afterUserLabels = afterLabelIds.filter(isUserLabel);
|
|
2902
|
+
const addedLabel = afterUserLabels.find((label) => !beforeUserLabels.has(label));
|
|
2903
|
+
return addedLabel || afterUserLabels[0] || null;
|
|
2904
|
+
}
|
|
2905
|
+
function resolvePrimaryAction(actions) {
|
|
2906
|
+
if (actions.some((action) => action.type === "archive")) {
|
|
2907
|
+
return "archive";
|
|
2908
|
+
}
|
|
2909
|
+
if (actions.some((action) => action.type === "label")) {
|
|
2910
|
+
return "label";
|
|
2911
|
+
}
|
|
2912
|
+
return actions[0]?.type || "unknown";
|
|
2913
|
+
}
|
|
2914
|
+
function summarizeReview(totalReviewed, anomalyCount, highCount, mediumCount) {
|
|
2915
|
+
if (anomalyCount === 0) {
|
|
2916
|
+
return `Reviewed ${totalReviewed} recently categorised emails. Found no potential misclassifications.`;
|
|
2917
|
+
}
|
|
2918
|
+
const severityParts = [];
|
|
2919
|
+
if (highCount > 0) {
|
|
2920
|
+
severityParts.push(`${highCount} high severity`);
|
|
2921
|
+
}
|
|
2922
|
+
if (mediumCount > 0) {
|
|
2923
|
+
severityParts.push(`${mediumCount} medium`);
|
|
2924
|
+
}
|
|
2925
|
+
return `Reviewed ${totalReviewed} recently categorised emails. Found ${anomalyCount} potential misclassifications (${severityParts.join(", ")}).`;
|
|
2926
|
+
}
|
|
2927
|
+
function detectAnomaly(row) {
|
|
2928
|
+
const actions = parseActions(row.appliedActions);
|
|
2929
|
+
const beforeLabelIds = parseJsonArray3(row.beforeLabelIds);
|
|
2930
|
+
const afterLabelIds = parseJsonArray3(row.afterLabelIds);
|
|
2931
|
+
const archived = isArchived(actions, beforeLabelIds, afterLabelIds);
|
|
2932
|
+
const assignedLabel = resolveAssignedLabel(actions, beforeLabelIds, afterLabelIds);
|
|
2933
|
+
const action = resolvePrimaryAction(actions);
|
|
2934
|
+
const totalFromSender = row.totalFromSender ?? 0;
|
|
2935
|
+
const hasNewsletterSignals = Boolean(row.detectionReason) || Boolean(row.listUnsubscribe?.trim());
|
|
2936
|
+
const isBulkLabel = assignedLabel ? BULK_LABELS.has(assignedLabel.toLowerCase()) : false;
|
|
2937
|
+
const automatedSender = isLikelyAutomatedSenderAddress(row.sender || "");
|
|
2938
|
+
const undoAvailable = row.runDryRun !== 1 && row.runStatus !== "undone" && row.runUndoneAt === null && row.itemUndoneAt === null;
|
|
2939
|
+
if (archived && totalFromSender <= 3) {
|
|
2940
|
+
return {
|
|
2941
|
+
emailId: row.emailId,
|
|
2942
|
+
from: row.sender || "",
|
|
2943
|
+
subject: row.subject || "",
|
|
2944
|
+
date: toIsoString(row.date),
|
|
2945
|
+
assignedLabel: assignedLabel || "Unlabeled",
|
|
2946
|
+
action,
|
|
2947
|
+
runId: row.runId,
|
|
2948
|
+
severity: "high",
|
|
2949
|
+
rule: "rare_sender_archived",
|
|
2950
|
+
reason: `Archived email from a rare sender with only ${totalFromSender} total email${totalFromSender === 1 ? "" : "s"}. Rare senders should be reviewed before archiving.`,
|
|
2951
|
+
undoAvailable
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
if (isBulkLabel && !hasNewsletterSignals) {
|
|
2955
|
+
return {
|
|
2956
|
+
emailId: row.emailId,
|
|
2957
|
+
from: row.sender || "",
|
|
2958
|
+
subject: row.subject || "",
|
|
2959
|
+
date: toIsoString(row.date),
|
|
2960
|
+
assignedLabel: assignedLabel || "Unlabeled",
|
|
2961
|
+
action,
|
|
2962
|
+
runId: row.runId,
|
|
2963
|
+
severity: "high",
|
|
2964
|
+
rule: "no_newsletter_signals_as_newsletter",
|
|
2965
|
+
reason: `Labeled as ${assignedLabel} but sender has no List-Unsubscribe header and no newsletter detection signals. Sender has only sent ${totalFromSender} total email${totalFromSender === 1 ? "" : "s"}.`,
|
|
2966
|
+
undoAvailable
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
if (archived && !automatedSender && totalFromSender < 5) {
|
|
2970
|
+
return {
|
|
2971
|
+
emailId: row.emailId,
|
|
2972
|
+
from: row.sender || "",
|
|
2973
|
+
subject: row.subject || "",
|
|
2974
|
+
date: toIsoString(row.date),
|
|
2975
|
+
assignedLabel: assignedLabel || "Unlabeled",
|
|
2976
|
+
action,
|
|
2977
|
+
runId: row.runId,
|
|
2978
|
+
severity: "high",
|
|
2979
|
+
rule: "personal_address_archived",
|
|
2980
|
+
reason: `Archived email from a likely personal sender address with fewer than 5 total emails. This address does not look automated and should stay visible.`,
|
|
2981
|
+
undoAvailable
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
if (isBulkLabel && totalFromSender < 5) {
|
|
2985
|
+
return {
|
|
2986
|
+
emailId: row.emailId,
|
|
2987
|
+
from: row.sender || "",
|
|
2988
|
+
subject: row.subject || "",
|
|
2989
|
+
date: toIsoString(row.date),
|
|
2990
|
+
assignedLabel: assignedLabel || "Unlabeled",
|
|
2991
|
+
action,
|
|
2992
|
+
runId: row.runId,
|
|
2993
|
+
severity: "medium",
|
|
2994
|
+
rule: "low_volume_bulk_label",
|
|
2995
|
+
reason: `Labeled as ${assignedLabel} even though the sender has only ${totalFromSender} total email${totalFromSender === 1 ? "" : "s"}. Bulk labels are safer for higher-volume senders.`,
|
|
2996
|
+
undoAvailable
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
if (archived && totalFromSender === 1) {
|
|
3000
|
+
return {
|
|
3001
|
+
emailId: row.emailId,
|
|
3002
|
+
from: row.sender || "",
|
|
3003
|
+
subject: row.subject || "",
|
|
3004
|
+
date: toIsoString(row.date),
|
|
3005
|
+
assignedLabel: assignedLabel || "Unlabeled",
|
|
3006
|
+
action,
|
|
3007
|
+
runId: row.runId,
|
|
3008
|
+
severity: "medium",
|
|
3009
|
+
rule: "first_time_sender_archived",
|
|
3010
|
+
reason: "Archived an email from a first-time sender. First-time senders are better surfaced for review before cleanup.",
|
|
3011
|
+
undoAvailable
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
async function reviewCategorized(options = {}) {
|
|
3017
|
+
const parsed = reviewCategorizedInputSchema.parse(options);
|
|
3018
|
+
await detectNewsletters();
|
|
3019
|
+
const sqlite = getStatsSqlite();
|
|
3020
|
+
const sinceTimestamp = resolveSinceTimestamp(parsed.since);
|
|
3021
|
+
const limit = Math.min(200, normalizeLimit(parsed.limit, 50));
|
|
3022
|
+
const rows = sqlite.prepare(
|
|
3023
|
+
`
|
|
3024
|
+
SELECT
|
|
3025
|
+
ei.email_id AS emailId,
|
|
3026
|
+
e.from_address AS sender,
|
|
3027
|
+
e.subject AS subject,
|
|
3028
|
+
e.date AS date,
|
|
3029
|
+
e.list_unsubscribe AS listUnsubscribe,
|
|
3030
|
+
ei.before_label_ids AS beforeLabelIds,
|
|
3031
|
+
ei.after_label_ids AS afterLabelIds,
|
|
3032
|
+
ei.applied_actions AS appliedActions,
|
|
3033
|
+
ei.executed_at AS executedAt,
|
|
3034
|
+
ei.undone_at AS itemUndoneAt,
|
|
3035
|
+
ei.run_id AS runId,
|
|
3036
|
+
er.status AS runStatus,
|
|
3037
|
+
er.dry_run AS runDryRun,
|
|
3038
|
+
er.undone_at AS runUndoneAt,
|
|
3039
|
+
ns.detection_reason AS detectionReason,
|
|
3040
|
+
sender_stats.totalFromSender AS totalFromSender
|
|
3041
|
+
FROM execution_items AS ei
|
|
3042
|
+
INNER JOIN emails AS e
|
|
3043
|
+
ON e.id = ei.email_id
|
|
3044
|
+
INNER JOIN execution_runs AS er
|
|
3045
|
+
ON er.id = ei.run_id
|
|
3046
|
+
LEFT JOIN newsletter_senders AS ns
|
|
3047
|
+
ON LOWER(ns.email) = LOWER(e.from_address)
|
|
3048
|
+
LEFT JOIN (
|
|
3049
|
+
SELECT
|
|
3050
|
+
LOWER(from_address) AS senderKey,
|
|
3051
|
+
COUNT(*) AS totalFromSender
|
|
3052
|
+
FROM emails
|
|
3053
|
+
WHERE from_address IS NOT NULL
|
|
3054
|
+
AND TRIM(from_address) <> ''
|
|
3055
|
+
GROUP BY LOWER(from_address)
|
|
3056
|
+
) AS sender_stats
|
|
3057
|
+
ON sender_stats.senderKey = LOWER(e.from_address)
|
|
3058
|
+
WHERE ei.status = 'applied'
|
|
3059
|
+
AND er.status IN ('applied', 'partial')
|
|
3060
|
+
AND COALESCE(er.dry_run, 0) = 0
|
|
3061
|
+
AND er.undone_at IS NULL
|
|
3062
|
+
AND ei.undone_at IS NULL
|
|
3063
|
+
AND COALESCE(ei.executed_at, 0) >= ?
|
|
3064
|
+
ORDER BY COALESCE(ei.executed_at, 0) DESC, ei.email_id ASC
|
|
3065
|
+
`
|
|
3066
|
+
).all(sinceTimestamp);
|
|
3067
|
+
const reviewedRows = rows.filter((row) => {
|
|
3068
|
+
const actions = parseActions(row.appliedActions);
|
|
3069
|
+
const beforeLabelIds = parseJsonArray3(row.beforeLabelIds);
|
|
3070
|
+
const afterLabelIds = parseJsonArray3(row.afterLabelIds);
|
|
3071
|
+
return actions.length > 0 && (resolveAssignedLabel(actions, beforeLabelIds, afterLabelIds) !== null || isArchived(actions, beforeLabelIds, afterLabelIds));
|
|
3072
|
+
});
|
|
3073
|
+
const anomalies = reviewedRows.map((row) => detectAnomaly(row)).filter((anomaly) => anomaly !== null).sort(
|
|
3074
|
+
(left, right) => (left.severity === "high" ? 1 : 0) === (right.severity === "high" ? 1 : 0) ? (right.date || "").localeCompare(left.date || "") || left.emailId.localeCompare(right.emailId) : left.severity === "high" ? -1 : 1
|
|
3075
|
+
);
|
|
3076
|
+
const highCount = anomalies.filter((anomaly) => anomaly.severity === "high").length;
|
|
3077
|
+
const mediumCount = anomalies.filter((anomaly) => anomaly.severity === "medium").length;
|
|
3078
|
+
return {
|
|
3079
|
+
anomalies: anomalies.slice(0, limit),
|
|
3080
|
+
totalReviewed: reviewedRows.length,
|
|
3081
|
+
anomalyCount: anomalies.length,
|
|
3082
|
+
summary: summarizeReview(reviewedRows.length, anomalies.length, highCount, mediumCount)
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/core/stats/labels.ts
|
|
3087
|
+
async function getLabelDistribution() {
|
|
3088
|
+
const sqlite = getStatsSqlite();
|
|
3089
|
+
const rows = sqlite.prepare(
|
|
3090
|
+
`
|
|
3091
|
+
SELECT
|
|
3092
|
+
label.value AS labelId,
|
|
3093
|
+
COUNT(*) AS totalMessages,
|
|
3094
|
+
SUM(CASE WHEN e.is_read = 0 THEN 1 ELSE 0 END) AS unreadMessages
|
|
3095
|
+
FROM emails AS e, json_each(e.label_ids) AS label
|
|
3096
|
+
GROUP BY label.value
|
|
3097
|
+
ORDER BY totalMessages DESC, unreadMessages DESC, label.value ASC
|
|
3098
|
+
`
|
|
3099
|
+
).all();
|
|
3100
|
+
return rows.map((row) => ({
|
|
3101
|
+
labelId: row.labelId,
|
|
3102
|
+
labelName: resolveLabelName(row.labelId),
|
|
3103
|
+
totalMessages: row.totalMessages,
|
|
3104
|
+
unreadMessages: row.unreadMessages
|
|
3105
|
+
}));
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// src/core/stats/noise.ts
|
|
3109
|
+
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
2817
3110
|
var SUGGESTED_CATEGORY_RULES = [
|
|
2818
3111
|
{ category: "Receipts", keywords: ["receipt", "invoice", "payment", "order"] },
|
|
2819
3112
|
{ category: "Shipping", keywords: ["shipping", "tracking", "delivery", "dispatch"] },
|
|
@@ -2822,7 +3115,7 @@ var SUGGESTED_CATEGORY_RULES = [
|
|
|
2822
3115
|
{ category: "Promotions", keywords: ["promo", "offer", "deal", "sale", "marketing"] },
|
|
2823
3116
|
{ category: "Social", keywords: ["linkedin", "facebook", "twitter", "social"] }
|
|
2824
3117
|
];
|
|
2825
|
-
function
|
|
3118
|
+
function toIsoString2(value) {
|
|
2826
3119
|
if (!value) {
|
|
2827
3120
|
return null;
|
|
2828
3121
|
}
|
|
@@ -2861,7 +3154,7 @@ async function getNoiseSenders(options = {}) {
|
|
|
2861
3154
|
const limit = Math.min(50, normalizeLimit(options.limit, 20));
|
|
2862
3155
|
const minNoiseScore = options.minNoiseScore ?? 5;
|
|
2863
3156
|
const activeDays = Math.max(1, Math.floor(options.activeDays ?? 90));
|
|
2864
|
-
const activeSince = Date.now() - activeDays *
|
|
3157
|
+
const activeSince = Date.now() - activeDays * DAY_MS3;
|
|
2865
3158
|
const sortBy = options.sortBy ?? "noise_score";
|
|
2866
3159
|
const rows = sqlite.prepare(
|
|
2867
3160
|
`
|
|
@@ -2920,7 +3213,7 @@ async function getNoiseSenders(options = {}) {
|
|
|
2920
3213
|
unreadRate,
|
|
2921
3214
|
noiseScore,
|
|
2922
3215
|
allTimeNoiseScore,
|
|
2923
|
-
lastSeen:
|
|
3216
|
+
lastSeen: toIsoString2(row.lastSeen),
|
|
2924
3217
|
isNewsletter: row.isNewsletter === 1,
|
|
2925
3218
|
hasUnsubscribeLink: Boolean(unsubscribe2.unsubscribeLink),
|
|
2926
3219
|
unsubscribeLink: unsubscribe2.unsubscribeLink,
|
|
@@ -2930,6 +3223,427 @@ async function getNoiseSenders(options = {}) {
|
|
|
2930
3223
|
return { senders };
|
|
2931
3224
|
}
|
|
2932
3225
|
|
|
3226
|
+
// src/core/stats/query.ts
|
|
3227
|
+
import { z as z2 } from "zod";
|
|
3228
|
+
var QUERY_EMAIL_GROUP_BY_VALUES = [
|
|
3229
|
+
"sender",
|
|
3230
|
+
"domain",
|
|
3231
|
+
"label",
|
|
3232
|
+
"year_month",
|
|
3233
|
+
"year_week",
|
|
3234
|
+
"day_of_week",
|
|
3235
|
+
"is_read",
|
|
3236
|
+
"is_newsletter"
|
|
3237
|
+
];
|
|
3238
|
+
var QUERY_EMAIL_AGGREGATE_VALUES = [
|
|
3239
|
+
"count",
|
|
3240
|
+
"unread_count",
|
|
3241
|
+
"read_count",
|
|
3242
|
+
"unread_rate",
|
|
3243
|
+
"oldest",
|
|
3244
|
+
"newest",
|
|
3245
|
+
"sender_count"
|
|
3246
|
+
];
|
|
3247
|
+
var QUERY_EMAIL_HAVING_FIELDS = [
|
|
3248
|
+
"count",
|
|
3249
|
+
"unread_count",
|
|
3250
|
+
"unread_rate",
|
|
3251
|
+
"sender_count"
|
|
3252
|
+
];
|
|
3253
|
+
var CATEGORY_LABEL_LIKE_PATTERN = `${CATEGORY_LABEL_PREFIX.replace(/_/g, "\\_")}%`;
|
|
3254
|
+
var SYSTEM_LABEL_SQL = SYSTEM_LABEL_IDS.map((label) => `'${label}'`).join(", ");
|
|
3255
|
+
var DOMAIN_SQL = `
|
|
3256
|
+
LOWER(
|
|
3257
|
+
CASE
|
|
3258
|
+
WHEN INSTR(COALESCE(e.from_address, ''), '@') > 0
|
|
3259
|
+
THEN SUBSTR(e.from_address, INSTR(e.from_address, '@') + 1)
|
|
3260
|
+
ELSE ''
|
|
3261
|
+
END
|
|
3262
|
+
)
|
|
3263
|
+
`;
|
|
3264
|
+
var queryEmailsFiltersSchema = z2.object({
|
|
3265
|
+
from: z2.string().optional(),
|
|
3266
|
+
from_contains: z2.string().optional(),
|
|
3267
|
+
domain: z2.string().optional(),
|
|
3268
|
+
domain_contains: z2.string().optional(),
|
|
3269
|
+
subject_contains: z2.string().optional(),
|
|
3270
|
+
date_after: z2.string().optional(),
|
|
3271
|
+
date_before: z2.string().optional(),
|
|
3272
|
+
is_read: z2.boolean().optional(),
|
|
3273
|
+
is_newsletter: z2.boolean().optional(),
|
|
3274
|
+
has_label: z2.boolean().optional(),
|
|
3275
|
+
label: z2.string().optional(),
|
|
3276
|
+
has_unsubscribe: z2.boolean().optional(),
|
|
3277
|
+
min_sender_messages: z2.number().int().positive().optional()
|
|
3278
|
+
}).strict();
|
|
3279
|
+
var havingConditionSchema = z2.object({
|
|
3280
|
+
gte: z2.number().optional(),
|
|
3281
|
+
lte: z2.number().optional()
|
|
3282
|
+
}).strict().refine(
|
|
3283
|
+
(value) => value.gte !== void 0 || value.lte !== void 0,
|
|
3284
|
+
{ message: "Provide at least one of gte or lte." }
|
|
3285
|
+
);
|
|
3286
|
+
var queryEmailsHavingSchema = z2.object({
|
|
3287
|
+
count: havingConditionSchema.optional(),
|
|
3288
|
+
unread_count: havingConditionSchema.optional(),
|
|
3289
|
+
unread_rate: havingConditionSchema.optional(),
|
|
3290
|
+
sender_count: havingConditionSchema.optional()
|
|
3291
|
+
}).strict();
|
|
3292
|
+
var queryEmailsInputSchema = z2.object({
|
|
3293
|
+
filters: queryEmailsFiltersSchema.optional(),
|
|
3294
|
+
group_by: z2.enum(QUERY_EMAIL_GROUP_BY_VALUES).optional(),
|
|
3295
|
+
aggregates: z2.array(z2.enum(QUERY_EMAIL_AGGREGATE_VALUES)).min(1).optional(),
|
|
3296
|
+
having: queryEmailsHavingSchema.optional(),
|
|
3297
|
+
order_by: z2.string().optional(),
|
|
3298
|
+
limit: z2.number().int().positive().max(500).optional()
|
|
3299
|
+
}).strict();
|
|
3300
|
+
var QUERY_EMAILS_FIELD_SCHEMA = {
|
|
3301
|
+
description: "Available fields for the query_emails tool.",
|
|
3302
|
+
filters: {
|
|
3303
|
+
from: { type: "string", description: "Exact sender email (case-insensitive)" },
|
|
3304
|
+
from_contains: { type: "string", description: "Partial match on sender email" },
|
|
3305
|
+
domain: { type: "string", description: "Exact sender domain" },
|
|
3306
|
+
domain_contains: { type: "string", description: "Partial match on sender domain" },
|
|
3307
|
+
subject_contains: { type: "string", description: "Partial match on subject line" },
|
|
3308
|
+
date_after: { type: "string", description: "ISO date \u2014 emails after this date" },
|
|
3309
|
+
date_before: { type: "string", description: "ISO date \u2014 emails before this date" },
|
|
3310
|
+
is_read: { type: "boolean", description: "Filter by read/unread state" },
|
|
3311
|
+
is_newsletter: { type: "boolean", description: "Sender detected as newsletter" },
|
|
3312
|
+
has_label: { type: "boolean", description: "Has any user-applied label" },
|
|
3313
|
+
label: { type: "string", description: "Has this specific label" },
|
|
3314
|
+
has_unsubscribe: { type: "boolean", description: "Has List-Unsubscribe header" },
|
|
3315
|
+
min_sender_messages: { type: "integer", description: "Sender has at least this many total emails" }
|
|
3316
|
+
},
|
|
3317
|
+
group_by: [
|
|
3318
|
+
{ value: "sender", description: "Group by sender email address" },
|
|
3319
|
+
{ value: "domain", description: "Group by sender domain" },
|
|
3320
|
+
{ value: "label", description: "Group by applied label (expands multi-label emails)" },
|
|
3321
|
+
{ value: "year_month", description: "Group by month (YYYY-MM)" },
|
|
3322
|
+
{ value: "year_week", description: "Group by week (YYYY-WNN)" },
|
|
3323
|
+
{ value: "day_of_week", description: "Group by day of week (0=Sunday)" },
|
|
3324
|
+
{ value: "is_read", description: "Group by read/unread state" },
|
|
3325
|
+
{ value: "is_newsletter", description: "Group by newsletter detection" }
|
|
3326
|
+
],
|
|
3327
|
+
aggregates: [
|
|
3328
|
+
{ value: "count", description: "Number of emails" },
|
|
3329
|
+
{ value: "unread_count", description: "Number of unread emails" },
|
|
3330
|
+
{ value: "read_count", description: "Number of read emails" },
|
|
3331
|
+
{ value: "unread_rate", description: "Percentage of emails that are unread" },
|
|
3332
|
+
{ value: "oldest", description: "Earliest email date (ISO string)" },
|
|
3333
|
+
{ value: "newest", description: "Latest email date (ISO string)" },
|
|
3334
|
+
{ value: "sender_count", description: "Count of distinct senders" }
|
|
3335
|
+
],
|
|
3336
|
+
having_fields: [...QUERY_EMAIL_HAVING_FIELDS],
|
|
3337
|
+
example_queries: [
|
|
3338
|
+
{
|
|
3339
|
+
description: "Monthly volume trend for Amazon",
|
|
3340
|
+
query: {
|
|
3341
|
+
filters: { domain_contains: "amazon" },
|
|
3342
|
+
group_by: "year_month",
|
|
3343
|
+
aggregates: ["count", "unread_rate"],
|
|
3344
|
+
order_by: "year_month asc"
|
|
3345
|
+
}
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
description: "Domains with 95%+ unread rate and 50+ emails",
|
|
3349
|
+
query: {
|
|
3350
|
+
group_by: "domain",
|
|
3351
|
+
aggregates: ["count", "unread_rate"],
|
|
3352
|
+
having: { count: { gte: 50 }, unread_rate: { gte: 95 } }
|
|
3353
|
+
}
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
description: "What day of the week gets the most email?",
|
|
3357
|
+
query: {
|
|
3358
|
+
group_by: "day_of_week",
|
|
3359
|
+
aggregates: ["count", "sender_count"]
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
]
|
|
3363
|
+
};
|
|
3364
|
+
var GROUP_BY_SQL_MAP = {
|
|
3365
|
+
sender: "LOWER(COALESCE(e.from_address, ''))",
|
|
3366
|
+
domain: DOMAIN_SQL,
|
|
3367
|
+
label: "CAST(grouped_label.value AS TEXT)",
|
|
3368
|
+
year_month: "STRFTIME('%Y-%m', e.date / 1000, 'unixepoch')",
|
|
3369
|
+
year_week: "STRFTIME('%Y-W%W', e.date / 1000, 'unixepoch')",
|
|
3370
|
+
day_of_week: "CAST(STRFTIME('%w', e.date / 1000, 'unixepoch') AS INTEGER)",
|
|
3371
|
+
is_read: "COALESCE(e.is_read, 0)",
|
|
3372
|
+
is_newsletter: "CASE WHEN ns.email IS NOT NULL THEN 1 ELSE 0 END"
|
|
3373
|
+
};
|
|
3374
|
+
var AGGREGATE_SQL_MAP = {
|
|
3375
|
+
count: "COUNT(*)",
|
|
3376
|
+
unread_count: "SUM(CASE WHEN COALESCE(e.is_read, 0) = 0 THEN 1 ELSE 0 END)",
|
|
3377
|
+
read_count: "SUM(CASE WHEN COALESCE(e.is_read, 0) = 1 THEN 1 ELSE 0 END)",
|
|
3378
|
+
unread_rate: `
|
|
3379
|
+
ROUND(
|
|
3380
|
+
CASE
|
|
3381
|
+
WHEN COUNT(*) = 0 THEN 0
|
|
3382
|
+
ELSE 100.0 * SUM(CASE WHEN COALESCE(e.is_read, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*)
|
|
3383
|
+
END,
|
|
3384
|
+
1
|
|
3385
|
+
)
|
|
3386
|
+
`,
|
|
3387
|
+
oldest: "MIN(e.date)",
|
|
3388
|
+
newest: "MAX(e.date)",
|
|
3389
|
+
sender_count: `
|
|
3390
|
+
COUNT(
|
|
3391
|
+
DISTINCT CASE
|
|
3392
|
+
WHEN e.from_address IS NOT NULL AND TRIM(e.from_address) <> ''
|
|
3393
|
+
THEN LOWER(e.from_address)
|
|
3394
|
+
ELSE NULL
|
|
3395
|
+
END
|
|
3396
|
+
)
|
|
3397
|
+
`
|
|
3398
|
+
};
|
|
3399
|
+
function userLabelPredicate(column) {
|
|
3400
|
+
return `
|
|
3401
|
+
${column} IS NOT NULL
|
|
3402
|
+
AND TRIM(CAST(${column} AS TEXT)) <> ''
|
|
3403
|
+
AND CAST(${column} AS TEXT) NOT IN (${SYSTEM_LABEL_SQL})
|
|
3404
|
+
AND CAST(${column} AS TEXT) NOT LIKE '${CATEGORY_LABEL_LIKE_PATTERN}' ESCAPE '\\'
|
|
3405
|
+
`;
|
|
3406
|
+
}
|
|
3407
|
+
function toIsoString3(value) {
|
|
3408
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
3409
|
+
return null;
|
|
3410
|
+
}
|
|
3411
|
+
return new Date(value).toISOString();
|
|
3412
|
+
}
|
|
3413
|
+
function normalizeGroupValue(groupBy, value) {
|
|
3414
|
+
switch (groupBy) {
|
|
3415
|
+
case "is_read":
|
|
3416
|
+
case "is_newsletter":
|
|
3417
|
+
return Number(value ?? 0) === 1;
|
|
3418
|
+
case "day_of_week":
|
|
3419
|
+
return Number(value ?? 0);
|
|
3420
|
+
case "sender":
|
|
3421
|
+
case "domain":
|
|
3422
|
+
case "label":
|
|
3423
|
+
case "year_month":
|
|
3424
|
+
case "year_week":
|
|
3425
|
+
return typeof value === "string" ? value : value == null ? null : String(value);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
function normalizeAggregateValue(aggregate, value) {
|
|
3429
|
+
switch (aggregate) {
|
|
3430
|
+
case "oldest":
|
|
3431
|
+
case "newest":
|
|
3432
|
+
return toIsoString3(value);
|
|
3433
|
+
case "count":
|
|
3434
|
+
case "unread_count":
|
|
3435
|
+
case "read_count":
|
|
3436
|
+
case "sender_count":
|
|
3437
|
+
return Number(value ?? 0);
|
|
3438
|
+
case "unread_rate":
|
|
3439
|
+
return Number(value ?? 0);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function parseDateFilter(field, value) {
|
|
3443
|
+
const timestamp = Date.parse(value);
|
|
3444
|
+
if (Number.isNaN(timestamp)) {
|
|
3445
|
+
throw new Error(`Invalid ${field} value: ${value}`);
|
|
3446
|
+
}
|
|
3447
|
+
return timestamp;
|
|
3448
|
+
}
|
|
3449
|
+
function resolveAggregates(aggregates) {
|
|
3450
|
+
return Array.from(new Set(aggregates && aggregates.length > 0 ? aggregates : ["count"]));
|
|
3451
|
+
}
|
|
3452
|
+
function resolveOrderBy(orderBy, groupBy, aggregates) {
|
|
3453
|
+
const defaultField = aggregates.includes("count") ? "count" : aggregates[0];
|
|
3454
|
+
const rawValue = (orderBy || `${defaultField} desc`).trim();
|
|
3455
|
+
const match = rawValue.match(/^([a-z_]+)\s+(asc|desc)$/i);
|
|
3456
|
+
if (!match) {
|
|
3457
|
+
throw new Error(`Invalid order_by value: ${rawValue}`);
|
|
3458
|
+
}
|
|
3459
|
+
const [, field, direction] = match;
|
|
3460
|
+
const allowedFields = new Set(aggregates);
|
|
3461
|
+
if (groupBy) {
|
|
3462
|
+
allowedFields.add(groupBy);
|
|
3463
|
+
}
|
|
3464
|
+
if (!allowedFields.has(field)) {
|
|
3465
|
+
throw new Error(`Invalid order_by field: ${field}`);
|
|
3466
|
+
}
|
|
3467
|
+
return `${field} ${direction.toLowerCase()}`;
|
|
3468
|
+
}
|
|
3469
|
+
function buildWhereClauses(filters, groupBy) {
|
|
3470
|
+
const whereParts = [];
|
|
3471
|
+
const params = [];
|
|
3472
|
+
if (filters.from !== void 0) {
|
|
3473
|
+
whereParts.push("LOWER(COALESCE(e.from_address, '')) = LOWER(?)");
|
|
3474
|
+
params.push(filters.from);
|
|
3475
|
+
}
|
|
3476
|
+
if (filters.from_contains !== void 0) {
|
|
3477
|
+
whereParts.push("LOWER(COALESCE(e.from_address, '')) LIKE '%' || LOWER(?) || '%'");
|
|
3478
|
+
params.push(filters.from_contains);
|
|
3479
|
+
}
|
|
3480
|
+
if (filters.domain !== void 0) {
|
|
3481
|
+
whereParts.push(`${DOMAIN_SQL} = LOWER(?)`);
|
|
3482
|
+
params.push(filters.domain);
|
|
3483
|
+
}
|
|
3484
|
+
if (filters.domain_contains !== void 0) {
|
|
3485
|
+
whereParts.push(`${DOMAIN_SQL} LIKE '%' || LOWER(?) || '%'`);
|
|
3486
|
+
params.push(filters.domain_contains);
|
|
3487
|
+
}
|
|
3488
|
+
if (filters.subject_contains !== void 0) {
|
|
3489
|
+
whereParts.push("LOWER(COALESCE(e.subject, '')) LIKE '%' || LOWER(?) || '%'");
|
|
3490
|
+
params.push(filters.subject_contains);
|
|
3491
|
+
}
|
|
3492
|
+
if (filters.date_after !== void 0) {
|
|
3493
|
+
whereParts.push("COALESCE(e.date, 0) >= ?");
|
|
3494
|
+
params.push(parseDateFilter("date_after", filters.date_after));
|
|
3495
|
+
}
|
|
3496
|
+
if (filters.date_before !== void 0) {
|
|
3497
|
+
whereParts.push("COALESCE(e.date, 0) <= ?");
|
|
3498
|
+
params.push(parseDateFilter("date_before", filters.date_before));
|
|
3499
|
+
}
|
|
3500
|
+
if (filters.is_read !== void 0) {
|
|
3501
|
+
whereParts.push("COALESCE(e.is_read, 0) = ?");
|
|
3502
|
+
params.push(filters.is_read ? 1 : 0);
|
|
3503
|
+
}
|
|
3504
|
+
if (filters.is_newsletter !== void 0) {
|
|
3505
|
+
whereParts.push(filters.is_newsletter ? "ns.email IS NOT NULL" : "ns.email IS NULL");
|
|
3506
|
+
}
|
|
3507
|
+
if (filters.has_label !== void 0) {
|
|
3508
|
+
whereParts.push(
|
|
3509
|
+
filters.has_label ? `EXISTS (
|
|
3510
|
+
SELECT 1
|
|
3511
|
+
FROM json_each(COALESCE(e.label_ids, '[]')) AS label_filter
|
|
3512
|
+
WHERE ${userLabelPredicate("label_filter.value")}
|
|
3513
|
+
)` : `NOT EXISTS (
|
|
3514
|
+
SELECT 1
|
|
3515
|
+
FROM json_each(COALESCE(e.label_ids, '[]')) AS label_filter
|
|
3516
|
+
WHERE ${userLabelPredicate("label_filter.value")}
|
|
3517
|
+
)`
|
|
3518
|
+
);
|
|
3519
|
+
}
|
|
3520
|
+
if (filters.label !== void 0) {
|
|
3521
|
+
whereParts.push(`
|
|
3522
|
+
EXISTS (
|
|
3523
|
+
SELECT 1
|
|
3524
|
+
FROM json_each(COALESCE(e.label_ids, '[]')) AS label_filter
|
|
3525
|
+
WHERE LOWER(CAST(label_filter.value AS TEXT)) = LOWER(?)
|
|
3526
|
+
)
|
|
3527
|
+
`);
|
|
3528
|
+
params.push(filters.label);
|
|
3529
|
+
}
|
|
3530
|
+
if (filters.has_unsubscribe !== void 0) {
|
|
3531
|
+
whereParts.push(
|
|
3532
|
+
filters.has_unsubscribe ? "NULLIF(TRIM(e.list_unsubscribe), '') IS NOT NULL" : "(e.list_unsubscribe IS NULL OR TRIM(e.list_unsubscribe) = '')"
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
if (filters.min_sender_messages !== void 0) {
|
|
3536
|
+
whereParts.push("COALESCE(sender_stats.totalFromSender, 0) >= ?");
|
|
3537
|
+
params.push(filters.min_sender_messages);
|
|
3538
|
+
}
|
|
3539
|
+
if (groupBy === "label") {
|
|
3540
|
+
whereParts.push(userLabelPredicate("grouped_label.value"));
|
|
3541
|
+
}
|
|
3542
|
+
return {
|
|
3543
|
+
sql: whereParts.length > 0 ? `WHERE ${whereParts.join("\n AND ")}` : "",
|
|
3544
|
+
params
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
function buildHavingClause(having) {
|
|
3548
|
+
const parts = [];
|
|
3549
|
+
for (const field of QUERY_EMAIL_HAVING_FIELDS) {
|
|
3550
|
+
const condition = having[field];
|
|
3551
|
+
if (!condition) {
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
const expression = AGGREGATE_SQL_MAP[field];
|
|
3555
|
+
if (condition.gte !== void 0) {
|
|
3556
|
+
parts.push(`${expression} >= ${condition.gte}`);
|
|
3557
|
+
}
|
|
3558
|
+
if (condition.lte !== void 0) {
|
|
3559
|
+
parts.push(`${expression} <= ${condition.lte}`);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
return parts.length > 0 ? `HAVING ${parts.join("\n AND ")}` : "";
|
|
3563
|
+
}
|
|
3564
|
+
function normalizeRow(row, groupBy, aggregates) {
|
|
3565
|
+
const normalized = {};
|
|
3566
|
+
if (groupBy) {
|
|
3567
|
+
normalized[groupBy] = normalizeGroupValue(groupBy, row[groupBy]);
|
|
3568
|
+
}
|
|
3569
|
+
for (const aggregate of aggregates) {
|
|
3570
|
+
normalized[aggregate] = normalizeAggregateValue(aggregate, row[aggregate]);
|
|
3571
|
+
}
|
|
3572
|
+
return normalized;
|
|
3573
|
+
}
|
|
3574
|
+
async function queryEmails(options = {}) {
|
|
3575
|
+
const parsed = queryEmailsInputSchema.parse(options);
|
|
3576
|
+
await detectNewsletters();
|
|
3577
|
+
const sqlite = getStatsSqlite();
|
|
3578
|
+
const filters = parsed.filters ?? {};
|
|
3579
|
+
const groupBy = parsed.group_by;
|
|
3580
|
+
const aggregates = resolveAggregates(parsed.aggregates);
|
|
3581
|
+
const having = parsed.having ?? {};
|
|
3582
|
+
const orderBy = resolveOrderBy(parsed.order_by, groupBy, aggregates);
|
|
3583
|
+
const limit = Math.min(500, normalizeLimit(parsed.limit, 50));
|
|
3584
|
+
const { sql: whereSql, params } = buildWhereClauses(filters, groupBy);
|
|
3585
|
+
const havingSql = buildHavingClause(having);
|
|
3586
|
+
const fromSql = [
|
|
3587
|
+
"FROM emails AS e",
|
|
3588
|
+
"LEFT JOIN newsletter_senders AS ns ON LOWER(ns.email) = LOWER(e.from_address)",
|
|
3589
|
+
`LEFT JOIN (
|
|
3590
|
+
SELECT
|
|
3591
|
+
LOWER(from_address) AS senderKey,
|
|
3592
|
+
COUNT(*) AS totalFromSender
|
|
3593
|
+
FROM emails
|
|
3594
|
+
WHERE from_address IS NOT NULL
|
|
3595
|
+
AND TRIM(from_address) <> ''
|
|
3596
|
+
GROUP BY LOWER(from_address)
|
|
3597
|
+
) AS sender_stats ON sender_stats.senderKey = LOWER(e.from_address)`,
|
|
3598
|
+
groupBy === "label" ? "JOIN json_each(COALESCE(e.label_ids, '[]')) AS grouped_label" : ""
|
|
3599
|
+
].filter(Boolean).join("\n");
|
|
3600
|
+
const selectParts = [];
|
|
3601
|
+
if (groupBy) {
|
|
3602
|
+
selectParts.push(`${GROUP_BY_SQL_MAP[groupBy]} AS ${groupBy}`);
|
|
3603
|
+
}
|
|
3604
|
+
for (const aggregate of aggregates) {
|
|
3605
|
+
selectParts.push(`${AGGREGATE_SQL_MAP[aggregate]} AS ${aggregate}`);
|
|
3606
|
+
}
|
|
3607
|
+
const groupBySql = groupBy ? `GROUP BY ${GROUP_BY_SQL_MAP[groupBy]}` : "";
|
|
3608
|
+
const orderBySql = `ORDER BY ${orderBy.split(" ")[0]} ${orderBy.split(" ")[1].toUpperCase()}`;
|
|
3609
|
+
const totalRow = groupBy ? sqlite.prepare(
|
|
3610
|
+
`
|
|
3611
|
+
SELECT COUNT(*) AS totalRows
|
|
3612
|
+
FROM (
|
|
3613
|
+
SELECT 1
|
|
3614
|
+
${fromSql}
|
|
3615
|
+
${whereSql}
|
|
3616
|
+
${groupBySql}
|
|
3617
|
+
${havingSql}
|
|
3618
|
+
) AS grouped_rows
|
|
3619
|
+
`
|
|
3620
|
+
).get(...params) : void 0;
|
|
3621
|
+
const rows = sqlite.prepare(
|
|
3622
|
+
`
|
|
3623
|
+
SELECT
|
|
3624
|
+
${selectParts.join(",\n ")}
|
|
3625
|
+
${fromSql}
|
|
3626
|
+
${whereSql}
|
|
3627
|
+
${groupBySql}
|
|
3628
|
+
${havingSql}
|
|
3629
|
+
${orderBySql}
|
|
3630
|
+
LIMIT ?
|
|
3631
|
+
`
|
|
3632
|
+
).all(...params, limit);
|
|
3633
|
+
return {
|
|
3634
|
+
rows: rows.map((row) => normalizeRow(row, groupBy, aggregates)),
|
|
3635
|
+
totalRows: groupBy ? totalRow?.totalRows ?? 0 : rows.length,
|
|
3636
|
+
query: {
|
|
3637
|
+
filters,
|
|
3638
|
+
group_by: groupBy ?? null,
|
|
3639
|
+
aggregates,
|
|
3640
|
+
having,
|
|
3641
|
+
order_by: orderBy,
|
|
3642
|
+
limit
|
|
3643
|
+
}
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
|
|
2933
3647
|
// src/core/stats/sender.ts
|
|
2934
3648
|
function buildSenderWhereClause(period) {
|
|
2935
3649
|
const whereParts = [
|
|
@@ -3089,7 +3803,7 @@ async function getSenderStats(emailOrDomain) {
|
|
|
3089
3803
|
}
|
|
3090
3804
|
|
|
3091
3805
|
// src/core/stats/uncategorized.ts
|
|
3092
|
-
var
|
|
3806
|
+
var SYSTEM_LABEL_IDS2 = [
|
|
3093
3807
|
"INBOX",
|
|
3094
3808
|
"UNREAD",
|
|
3095
3809
|
"IMPORTANT",
|
|
@@ -3100,13 +3814,13 @@ var SYSTEM_LABEL_IDS = [
|
|
|
3100
3814
|
"STARRED"
|
|
3101
3815
|
];
|
|
3102
3816
|
var CATEGORY_LABEL_PATTERN = "CATEGORY\\_%";
|
|
3103
|
-
function
|
|
3817
|
+
function toIsoString4(value) {
|
|
3104
3818
|
if (!value) {
|
|
3105
3819
|
return null;
|
|
3106
3820
|
}
|
|
3107
3821
|
return new Date(value).toISOString();
|
|
3108
3822
|
}
|
|
3109
|
-
function
|
|
3823
|
+
function parseJsonArray4(raw) {
|
|
3110
3824
|
if (!raw) {
|
|
3111
3825
|
return [];
|
|
3112
3826
|
}
|
|
@@ -3117,7 +3831,7 @@ function parseJsonArray3(raw) {
|
|
|
3117
3831
|
return [];
|
|
3118
3832
|
}
|
|
3119
3833
|
}
|
|
3120
|
-
function
|
|
3834
|
+
function resolveSinceTimestamp2(since) {
|
|
3121
3835
|
if (!since) {
|
|
3122
3836
|
return null;
|
|
3123
3837
|
}
|
|
@@ -3127,6 +3841,50 @@ function resolveSinceTimestamp(since) {
|
|
|
3127
3841
|
}
|
|
3128
3842
|
return parsed;
|
|
3129
3843
|
}
|
|
3844
|
+
function computeConfidence(row) {
|
|
3845
|
+
const signals = [];
|
|
3846
|
+
let score = 0;
|
|
3847
|
+
const hasDefinitiveNewsletterSignal = Boolean(row.listUnsubscribe && row.listUnsubscribe.trim()) || Boolean(row.detectionReason?.includes("list_unsubscribe"));
|
|
3848
|
+
if (row.listUnsubscribe && row.listUnsubscribe.trim()) {
|
|
3849
|
+
signals.push("list_unsubscribe_header");
|
|
3850
|
+
score += 3;
|
|
3851
|
+
}
|
|
3852
|
+
if (row.detectionReason?.includes("list_unsubscribe")) {
|
|
3853
|
+
signals.push("newsletter_list_header");
|
|
3854
|
+
score += 2;
|
|
3855
|
+
}
|
|
3856
|
+
if ((row.totalFromSender ?? 0) >= 20) {
|
|
3857
|
+
signals.push("high_volume_sender");
|
|
3858
|
+
score += 2;
|
|
3859
|
+
} else if ((row.totalFromSender ?? 0) >= 5) {
|
|
3860
|
+
signals.push("moderate_volume_sender");
|
|
3861
|
+
score += 1;
|
|
3862
|
+
}
|
|
3863
|
+
if (row.detectionReason?.includes("known_sender_pattern")) {
|
|
3864
|
+
signals.push("automated_sender_pattern");
|
|
3865
|
+
score += 1;
|
|
3866
|
+
}
|
|
3867
|
+
if (row.detectionReason?.includes("bulk_sender_pattern")) {
|
|
3868
|
+
signals.push("bulk_sender_pattern");
|
|
3869
|
+
score += 1;
|
|
3870
|
+
}
|
|
3871
|
+
if ((row.totalFromSender ?? 0) <= 2 && !hasDefinitiveNewsletterSignal) {
|
|
3872
|
+
signals.push("rare_sender");
|
|
3873
|
+
score -= 3;
|
|
3874
|
+
}
|
|
3875
|
+
if (!row.detectionReason) {
|
|
3876
|
+
signals.push("no_newsletter_signals");
|
|
3877
|
+
score -= 2;
|
|
3878
|
+
}
|
|
3879
|
+
if (!row.detectionReason && !isLikelyAutomatedSenderAddress(row.sender || "")) {
|
|
3880
|
+
signals.push("personal_sender_address");
|
|
3881
|
+
score -= 2;
|
|
3882
|
+
}
|
|
3883
|
+
return {
|
|
3884
|
+
confidence: score >= 3 ? "high" : score >= 0 ? "medium" : "low",
|
|
3885
|
+
signals
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3130
3888
|
function buildWhereClause(options) {
|
|
3131
3889
|
const whereParts = [
|
|
3132
3890
|
`
|
|
@@ -3135,12 +3893,12 @@ function buildWhereClause(options) {
|
|
|
3135
3893
|
FROM json_each(COALESCE(e.label_ids, '[]')) AS label
|
|
3136
3894
|
WHERE label.value IS NOT NULL
|
|
3137
3895
|
AND TRIM(CAST(label.value AS TEXT)) <> ''
|
|
3138
|
-
AND label.value NOT IN (${
|
|
3896
|
+
AND label.value NOT IN (${SYSTEM_LABEL_IDS2.map(() => "?").join(", ")})
|
|
3139
3897
|
AND label.value NOT LIKE ? ESCAPE '\\'
|
|
3140
3898
|
)
|
|
3141
3899
|
`
|
|
3142
3900
|
];
|
|
3143
|
-
const params = [...
|
|
3901
|
+
const params = [...SYSTEM_LABEL_IDS2, CATEGORY_LABEL_PATTERN];
|
|
3144
3902
|
if (options.unreadOnly) {
|
|
3145
3903
|
whereParts.push("COALESCE(e.is_read, 0) = 0");
|
|
3146
3904
|
}
|
|
@@ -3158,7 +3916,7 @@ async function getUncategorizedEmails(options = {}) {
|
|
|
3158
3916
|
const sqlite = getStatsSqlite();
|
|
3159
3917
|
const limit = Math.min(1e3, normalizeLimit(options.limit, 50));
|
|
3160
3918
|
const offset = Math.max(0, Math.floor(options.offset ?? 0));
|
|
3161
|
-
const sinceTimestamp =
|
|
3919
|
+
const sinceTimestamp = resolveSinceTimestamp2(options.since);
|
|
3162
3920
|
const { clause, params } = buildWhereClause({
|
|
3163
3921
|
sinceTimestamp,
|
|
3164
3922
|
unreadOnly: options.unreadOnly ?? false
|
|
@@ -3183,7 +3941,8 @@ async function getUncategorizedEmails(options = {}) {
|
|
|
3183
3941
|
e.is_read AS isRead,
|
|
3184
3942
|
sender_stats.totalFromSender AS totalFromSender,
|
|
3185
3943
|
sender_stats.unreadFromSender AS unreadFromSender,
|
|
3186
|
-
ns.detection_reason AS detectionReason
|
|
3944
|
+
ns.detection_reason AS detectionReason,
|
|
3945
|
+
e.list_unsubscribe AS listUnsubscribe
|
|
3187
3946
|
FROM emails AS e
|
|
3188
3947
|
LEFT JOIN (
|
|
3189
3948
|
SELECT
|
|
@@ -3207,20 +3966,23 @@ async function getUncategorizedEmails(options = {}) {
|
|
|
3207
3966
|
const emails2 = rows.map((row) => {
|
|
3208
3967
|
const totalFromSender = row.totalFromSender ?? 0;
|
|
3209
3968
|
const unreadFromSender = row.unreadFromSender ?? 0;
|
|
3969
|
+
const confidence = computeConfidence(row);
|
|
3210
3970
|
return {
|
|
3211
3971
|
id: row.id,
|
|
3212
3972
|
threadId: row.threadId || "",
|
|
3213
3973
|
from: row.sender || "",
|
|
3214
3974
|
subject: row.subject || "",
|
|
3215
|
-
date:
|
|
3975
|
+
date: toIsoString4(row.date),
|
|
3216
3976
|
snippet: row.snippet || "",
|
|
3217
|
-
labels:
|
|
3977
|
+
labels: parseJsonArray4(row.labelIds),
|
|
3218
3978
|
isRead: row.isRead === 1,
|
|
3219
3979
|
senderContext: {
|
|
3220
3980
|
totalFromSender,
|
|
3221
3981
|
unreadRate: roundPercent(unreadFromSender, totalFromSender),
|
|
3222
3982
|
isNewsletter: Boolean(row.detectionReason),
|
|
3223
|
-
detectionReason: row.detectionReason
|
|
3983
|
+
detectionReason: row.detectionReason,
|
|
3984
|
+
confidence: confidence.confidence,
|
|
3985
|
+
signals: confidence.signals
|
|
3224
3986
|
}
|
|
3225
3987
|
};
|
|
3226
3988
|
});
|
|
@@ -3234,7 +3996,7 @@ async function getUncategorizedEmails(options = {}) {
|
|
|
3234
3996
|
}
|
|
3235
3997
|
|
|
3236
3998
|
// src/core/stats/unsubscribe.ts
|
|
3237
|
-
function
|
|
3999
|
+
function toIsoString5(value) {
|
|
3238
4000
|
if (!value) {
|
|
3239
4001
|
return null;
|
|
3240
4002
|
}
|
|
@@ -3286,8 +4048,8 @@ async function getUnsubscribeSuggestions(options = {}) {
|
|
|
3286
4048
|
unreadCount: row.unreadCount,
|
|
3287
4049
|
unreadRate,
|
|
3288
4050
|
readRate,
|
|
3289
|
-
lastRead:
|
|
3290
|
-
lastReceived:
|
|
4051
|
+
lastRead: toIsoString5(row.lastRead),
|
|
4052
|
+
lastReceived: toIsoString5(row.lastReceived),
|
|
3291
4053
|
unsubscribeLink: unsubscribe2.unsubscribeLink,
|
|
3292
4054
|
unsubscribeMethod: unsubscribe2.unsubscribeMethod,
|
|
3293
4055
|
impactScore: roundImpactScore(row.messageCount, unreadRate),
|
|
@@ -3439,67 +4201,67 @@ import { join as join2 } from "path";
|
|
|
3439
4201
|
import YAML from "yaml";
|
|
3440
4202
|
|
|
3441
4203
|
// src/core/rules/types.ts
|
|
3442
|
-
import { z } from "zod";
|
|
3443
|
-
var RuleNameSchema =
|
|
4204
|
+
import { z as z3 } from "zod";
|
|
4205
|
+
var RuleNameSchema = z3.string().min(1, "Rule name is required").regex(
|
|
3444
4206
|
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
|
|
3445
4207
|
"Rule name must be kebab-case (lowercase letters, numbers, and single hyphens)"
|
|
3446
4208
|
);
|
|
3447
|
-
var RuleFieldSchema =
|
|
3448
|
-
var RegexStringSchema =
|
|
4209
|
+
var RuleFieldSchema = z3.enum(["from", "to", "subject", "snippet", "labels"]);
|
|
4210
|
+
var RegexStringSchema = z3.string().min(1, "Pattern must not be empty").superRefine((value, ctx) => {
|
|
3449
4211
|
try {
|
|
3450
4212
|
new RegExp(value);
|
|
3451
4213
|
} catch (error) {
|
|
3452
4214
|
ctx.addIssue({
|
|
3453
|
-
code:
|
|
4215
|
+
code: z3.ZodIssueCode.custom,
|
|
3454
4216
|
message: `Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`
|
|
3455
4217
|
});
|
|
3456
4218
|
}
|
|
3457
4219
|
});
|
|
3458
|
-
var MatcherSchema =
|
|
4220
|
+
var MatcherSchema = z3.object({
|
|
3459
4221
|
// `snippet` is the only cached free-text matcher in MVP.
|
|
3460
4222
|
field: RuleFieldSchema,
|
|
3461
4223
|
pattern: RegexStringSchema.optional(),
|
|
3462
|
-
contains:
|
|
3463
|
-
values:
|
|
3464
|
-
exclude:
|
|
4224
|
+
contains: z3.array(z3.string().min(1)).min(1).optional(),
|
|
4225
|
+
values: z3.array(z3.string().min(1)).min(1).optional(),
|
|
4226
|
+
exclude: z3.boolean().default(false)
|
|
3465
4227
|
}).strict().superRefine((value, ctx) => {
|
|
3466
4228
|
if (!value.pattern && !value.contains && !value.values) {
|
|
3467
4229
|
ctx.addIssue({
|
|
3468
|
-
code:
|
|
4230
|
+
code: z3.ZodIssueCode.custom,
|
|
3469
4231
|
message: "Matcher must provide at least one of pattern, contains, or values",
|
|
3470
4232
|
path: ["pattern"]
|
|
3471
4233
|
});
|
|
3472
4234
|
}
|
|
3473
4235
|
});
|
|
3474
|
-
var ConditionsSchema =
|
|
3475
|
-
operator:
|
|
3476
|
-
matchers:
|
|
4236
|
+
var ConditionsSchema = z3.object({
|
|
4237
|
+
operator: z3.enum(["AND", "OR"]),
|
|
4238
|
+
matchers: z3.array(MatcherSchema).min(1, "At least one matcher is required")
|
|
3477
4239
|
}).strict();
|
|
3478
|
-
var LabelActionSchema =
|
|
3479
|
-
type:
|
|
3480
|
-
label:
|
|
4240
|
+
var LabelActionSchema = z3.object({
|
|
4241
|
+
type: z3.literal("label"),
|
|
4242
|
+
label: z3.string().min(1, "Label name is required")
|
|
3481
4243
|
});
|
|
3482
|
-
var ArchiveActionSchema =
|
|
3483
|
-
var MarkReadActionSchema =
|
|
3484
|
-
var ForwardActionSchema =
|
|
3485
|
-
type:
|
|
3486
|
-
to:
|
|
4244
|
+
var ArchiveActionSchema = z3.object({ type: z3.literal("archive") });
|
|
4245
|
+
var MarkReadActionSchema = z3.object({ type: z3.literal("mark_read") });
|
|
4246
|
+
var ForwardActionSchema = z3.object({
|
|
4247
|
+
type: z3.literal("forward"),
|
|
4248
|
+
to: z3.string().email("Forward destination must be a valid email address")
|
|
3487
4249
|
});
|
|
3488
|
-
var MarkSpamActionSchema =
|
|
3489
|
-
var ActionSchema =
|
|
4250
|
+
var MarkSpamActionSchema = z3.object({ type: z3.literal("mark_spam") });
|
|
4251
|
+
var ActionSchema = z3.discriminatedUnion("type", [
|
|
3490
4252
|
LabelActionSchema,
|
|
3491
4253
|
ArchiveActionSchema,
|
|
3492
4254
|
MarkReadActionSchema,
|
|
3493
4255
|
ForwardActionSchema,
|
|
3494
4256
|
MarkSpamActionSchema
|
|
3495
4257
|
]);
|
|
3496
|
-
var RuleSchema =
|
|
4258
|
+
var RuleSchema = z3.object({
|
|
3497
4259
|
name: RuleNameSchema,
|
|
3498
|
-
description:
|
|
3499
|
-
enabled:
|
|
3500
|
-
priority:
|
|
4260
|
+
description: z3.string(),
|
|
4261
|
+
enabled: z3.boolean().default(true),
|
|
4262
|
+
priority: z3.number().int().min(0).default(50),
|
|
3501
4263
|
conditions: ConditionsSchema,
|
|
3502
|
-
actions:
|
|
4264
|
+
actions: z3.array(ActionSchema).min(1, "At least one action is required")
|
|
3503
4265
|
}).strict();
|
|
3504
4266
|
|
|
3505
4267
|
// src/core/rules/loader.ts
|
|
@@ -3829,7 +4591,7 @@ function getDatabase4() {
|
|
|
3829
4591
|
const config = loadConfig();
|
|
3830
4592
|
return getSqlite(config.dbPath);
|
|
3831
4593
|
}
|
|
3832
|
-
function
|
|
4594
|
+
function parseJsonArray5(value) {
|
|
3833
4595
|
if (!value) {
|
|
3834
4596
|
return [];
|
|
3835
4597
|
}
|
|
@@ -3846,13 +4608,13 @@ function rowToEmail3(row) {
|
|
|
3846
4608
|
threadId: row.thread_id ?? "",
|
|
3847
4609
|
fromAddress: row.from_address ?? "",
|
|
3848
4610
|
fromName: row.from_name ?? "",
|
|
3849
|
-
toAddresses:
|
|
4611
|
+
toAddresses: parseJsonArray5(row.to_addresses),
|
|
3850
4612
|
subject: row.subject ?? "",
|
|
3851
4613
|
snippet: row.snippet ?? "",
|
|
3852
4614
|
date: row.date ?? 0,
|
|
3853
4615
|
isRead: row.is_read === 1,
|
|
3854
4616
|
isStarred: row.is_starred === 1,
|
|
3855
|
-
labelIds:
|
|
4617
|
+
labelIds: parseJsonArray5(row.label_ids),
|
|
3856
4618
|
sizeEstimate: row.size_estimate ?? 0,
|
|
3857
4619
|
hasAttachments: row.has_attachments === 1,
|
|
3858
4620
|
listUnsubscribe: row.list_unsubscribe
|
|
@@ -4789,8 +5551,8 @@ async function getSyncStatus() {
|
|
|
4789
5551
|
}
|
|
4790
5552
|
|
|
4791
5553
|
// src/mcp/server.ts
|
|
4792
|
-
var
|
|
4793
|
-
var MCP_VERSION = "0.
|
|
5554
|
+
var DAY_MS4 = 24 * 60 * 60 * 1e3;
|
|
5555
|
+
var MCP_VERSION = "0.3.0";
|
|
4794
5556
|
var MCP_TOOLS = [
|
|
4795
5557
|
"search_emails",
|
|
4796
5558
|
"get_email",
|
|
@@ -4809,6 +5571,8 @@ var MCP_TOOLS = [
|
|
|
4809
5571
|
"get_sender_stats",
|
|
4810
5572
|
"get_newsletter_senders",
|
|
4811
5573
|
"get_uncategorized_emails",
|
|
5574
|
+
"review_categorized",
|
|
5575
|
+
"query_emails",
|
|
4812
5576
|
"get_noise_senders",
|
|
4813
5577
|
"get_unsubscribe_suggestions",
|
|
4814
5578
|
"unsubscribe",
|
|
@@ -4826,6 +5590,7 @@ var MCP_RESOURCES = [
|
|
|
4826
5590
|
"inbox://recent",
|
|
4827
5591
|
"inbox://summary",
|
|
4828
5592
|
"inbox://action-log",
|
|
5593
|
+
"schema://query-fields",
|
|
4829
5594
|
"rules://deployed",
|
|
4830
5595
|
"rules://history",
|
|
4831
5596
|
"stats://senders",
|
|
@@ -4947,7 +5712,7 @@ async function buildStartupWarnings() {
|
|
|
4947
5712
|
}
|
|
4948
5713
|
if (!latestSync) {
|
|
4949
5714
|
warnings.push("Inbox cache has not been synced yet. Stats and resources will be empty until `sync_inbox` runs.");
|
|
4950
|
-
} else if (Date.now() - latestSync >
|
|
5715
|
+
} else if (Date.now() - latestSync > DAY_MS4) {
|
|
4951
5716
|
warnings.push("Inbox cache appears stale (last sync older than 24 hours). Call `sync_inbox` if freshness matters.");
|
|
4952
5717
|
}
|
|
4953
5718
|
return warnings;
|
|
@@ -4958,7 +5723,7 @@ async function buildStatsOverview() {
|
|
|
4958
5723
|
topSenders: await getTopSenders({ limit: 10 }),
|
|
4959
5724
|
labelDistribution: (await getLabelDistribution()).slice(0, 10),
|
|
4960
5725
|
dailyVolume: await getVolumeByPeriod("day", {
|
|
4961
|
-
start: Date.now() - 30 *
|
|
5726
|
+
start: Date.now() - 30 * DAY_MS4,
|
|
4962
5727
|
end: Date.now()
|
|
4963
5728
|
})
|
|
4964
5729
|
};
|
|
@@ -4998,9 +5763,9 @@ async function createMcpServer() {
|
|
|
4998
5763
|
{
|
|
4999
5764
|
description: "Search Gmail using Gmail query syntax and return matching email metadata.",
|
|
5000
5765
|
inputSchema: {
|
|
5001
|
-
query:
|
|
5002
|
-
max_results:
|
|
5003
|
-
label:
|
|
5766
|
+
query: z4.string().min(1),
|
|
5767
|
+
max_results: z4.number().int().positive().max(100).optional(),
|
|
5768
|
+
label: z4.string().min(1).optional()
|
|
5004
5769
|
},
|
|
5005
5770
|
annotations: {
|
|
5006
5771
|
readOnlyHint: true
|
|
@@ -5015,7 +5780,7 @@ async function createMcpServer() {
|
|
|
5015
5780
|
{
|
|
5016
5781
|
description: "Fetch a single email with full content by Gmail message ID.",
|
|
5017
5782
|
inputSchema: {
|
|
5018
|
-
email_id:
|
|
5783
|
+
email_id: z4.string().min(1)
|
|
5019
5784
|
},
|
|
5020
5785
|
annotations: {
|
|
5021
5786
|
readOnlyHint: true
|
|
@@ -5028,7 +5793,7 @@ async function createMcpServer() {
|
|
|
5028
5793
|
{
|
|
5029
5794
|
description: "Fetch a full Gmail thread by thread ID.",
|
|
5030
5795
|
inputSchema: {
|
|
5031
|
-
thread_id:
|
|
5796
|
+
thread_id: z4.string().min(1)
|
|
5032
5797
|
},
|
|
5033
5798
|
annotations: {
|
|
5034
5799
|
readOnlyHint: true
|
|
@@ -5041,7 +5806,7 @@ async function createMcpServer() {
|
|
|
5041
5806
|
{
|
|
5042
5807
|
description: "Run inbox sync. Uses incremental sync by default and full sync when requested.",
|
|
5043
5808
|
inputSchema: {
|
|
5044
|
-
full:
|
|
5809
|
+
full: z4.boolean().optional()
|
|
5045
5810
|
},
|
|
5046
5811
|
annotations: {
|
|
5047
5812
|
readOnlyHint: false,
|
|
@@ -5055,7 +5820,7 @@ async function createMcpServer() {
|
|
|
5055
5820
|
{
|
|
5056
5821
|
description: "Archive one or more Gmail messages by removing the INBOX label.",
|
|
5057
5822
|
inputSchema: {
|
|
5058
|
-
email_ids:
|
|
5823
|
+
email_ids: z4.array(z4.string().min(1)).min(1)
|
|
5059
5824
|
},
|
|
5060
5825
|
annotations: {
|
|
5061
5826
|
readOnlyHint: false,
|
|
@@ -5069,9 +5834,9 @@ async function createMcpServer() {
|
|
|
5069
5834
|
{
|
|
5070
5835
|
description: "Add and/or remove Gmail labels on one or more messages.",
|
|
5071
5836
|
inputSchema: {
|
|
5072
|
-
email_ids:
|
|
5073
|
-
add_labels:
|
|
5074
|
-
remove_labels:
|
|
5837
|
+
email_ids: z4.array(z4.string().min(1)).min(1),
|
|
5838
|
+
add_labels: z4.array(z4.string().min(1)).optional(),
|
|
5839
|
+
remove_labels: z4.array(z4.string().min(1)).optional()
|
|
5075
5840
|
},
|
|
5076
5841
|
annotations: {
|
|
5077
5842
|
readOnlyHint: false,
|
|
@@ -5105,8 +5870,8 @@ async function createMcpServer() {
|
|
|
5105
5870
|
{
|
|
5106
5871
|
description: "Mark one or more Gmail messages as read or unread.",
|
|
5107
5872
|
inputSchema: {
|
|
5108
|
-
email_ids:
|
|
5109
|
-
read:
|
|
5873
|
+
email_ids: z4.array(z4.string().min(1)).min(1),
|
|
5874
|
+
read: z4.boolean()
|
|
5110
5875
|
},
|
|
5111
5876
|
annotations: {
|
|
5112
5877
|
readOnlyHint: false,
|
|
@@ -5123,23 +5888,23 @@ async function createMcpServer() {
|
|
|
5123
5888
|
{
|
|
5124
5889
|
description: "Apply grouped inbox actions in one call for faster AI-driven triage and categorization.",
|
|
5125
5890
|
inputSchema: {
|
|
5126
|
-
groups:
|
|
5127
|
-
|
|
5128
|
-
email_ids:
|
|
5129
|
-
actions:
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
type:
|
|
5133
|
-
label:
|
|
5891
|
+
groups: z4.array(
|
|
5892
|
+
z4.object({
|
|
5893
|
+
email_ids: z4.array(z4.string().min(1)).min(1).max(500),
|
|
5894
|
+
actions: z4.array(
|
|
5895
|
+
z4.discriminatedUnion("type", [
|
|
5896
|
+
z4.object({
|
|
5897
|
+
type: z4.literal("label"),
|
|
5898
|
+
label: z4.string().min(1)
|
|
5134
5899
|
}),
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5900
|
+
z4.object({ type: z4.literal("archive") }),
|
|
5901
|
+
z4.object({ type: z4.literal("mark_read") }),
|
|
5902
|
+
z4.object({ type: z4.literal("mark_spam") })
|
|
5138
5903
|
])
|
|
5139
5904
|
).min(1).max(5)
|
|
5140
5905
|
})
|
|
5141
5906
|
).min(1).max(20),
|
|
5142
|
-
dry_run:
|
|
5907
|
+
dry_run: z4.boolean().optional()
|
|
5143
5908
|
},
|
|
5144
5909
|
annotations: {
|
|
5145
5910
|
readOnlyHint: false,
|
|
@@ -5159,8 +5924,8 @@ async function createMcpServer() {
|
|
|
5159
5924
|
{
|
|
5160
5925
|
description: "Forward a Gmail message to another address.",
|
|
5161
5926
|
inputSchema: {
|
|
5162
|
-
email_id:
|
|
5163
|
-
to:
|
|
5927
|
+
email_id: z4.string().min(1),
|
|
5928
|
+
to: z4.string().email()
|
|
5164
5929
|
},
|
|
5165
5930
|
annotations: {
|
|
5166
5931
|
readOnlyHint: false,
|
|
@@ -5174,7 +5939,7 @@ async function createMcpServer() {
|
|
|
5174
5939
|
{
|
|
5175
5940
|
description: "Undo a prior inboxctl action run when the underlying Gmail mutations are reversible.",
|
|
5176
5941
|
inputSchema: {
|
|
5177
|
-
run_id:
|
|
5942
|
+
run_id: z4.string().min(1)
|
|
5178
5943
|
},
|
|
5179
5944
|
annotations: {
|
|
5180
5945
|
readOnlyHint: false,
|
|
@@ -5198,8 +5963,8 @@ async function createMcpServer() {
|
|
|
5198
5963
|
{
|
|
5199
5964
|
description: "Create a Gmail label if it does not already exist.",
|
|
5200
5965
|
inputSchema: {
|
|
5201
|
-
name:
|
|
5202
|
-
color:
|
|
5966
|
+
name: z4.string().min(1),
|
|
5967
|
+
color: z4.string().min(1).optional()
|
|
5203
5968
|
},
|
|
5204
5969
|
annotations: {
|
|
5205
5970
|
readOnlyHint: false,
|
|
@@ -5231,9 +5996,9 @@ async function createMcpServer() {
|
|
|
5231
5996
|
{
|
|
5232
5997
|
description: "Return top senders ranked by cached email volume.",
|
|
5233
5998
|
inputSchema: {
|
|
5234
|
-
limit:
|
|
5235
|
-
min_unread_rate:
|
|
5236
|
-
period:
|
|
5999
|
+
limit: z4.number().int().positive().max(100).optional(),
|
|
6000
|
+
min_unread_rate: z4.number().min(0).max(100).optional(),
|
|
6001
|
+
period: z4.enum(["day", "week", "month", "year", "all"]).optional()
|
|
5237
6002
|
},
|
|
5238
6003
|
annotations: {
|
|
5239
6004
|
readOnlyHint: true
|
|
@@ -5250,7 +6015,7 @@ async function createMcpServer() {
|
|
|
5250
6015
|
{
|
|
5251
6016
|
description: "Return detailed stats for a sender email address or an @domain aggregate.",
|
|
5252
6017
|
inputSchema: {
|
|
5253
|
-
email_or_domain:
|
|
6018
|
+
email_or_domain: z4.string().min(1)
|
|
5254
6019
|
},
|
|
5255
6020
|
annotations: {
|
|
5256
6021
|
readOnlyHint: true
|
|
@@ -5270,8 +6035,8 @@ async function createMcpServer() {
|
|
|
5270
6035
|
{
|
|
5271
6036
|
description: "Return senders that look like newsletters or mailing lists based on cached heuristics.",
|
|
5272
6037
|
inputSchema: {
|
|
5273
|
-
min_messages:
|
|
5274
|
-
min_unread_rate:
|
|
6038
|
+
min_messages: z4.number().int().positive().optional(),
|
|
6039
|
+
min_unread_rate: z4.number().min(0).max(100).optional()
|
|
5275
6040
|
},
|
|
5276
6041
|
annotations: {
|
|
5277
6042
|
readOnlyHint: true
|
|
@@ -5287,10 +6052,10 @@ async function createMcpServer() {
|
|
|
5287
6052
|
{
|
|
5288
6053
|
description: "Return cached emails that have only Gmail system labels and no user-applied organization.",
|
|
5289
6054
|
inputSchema: {
|
|
5290
|
-
limit:
|
|
5291
|
-
offset:
|
|
5292
|
-
unread_only:
|
|
5293
|
-
since:
|
|
6055
|
+
limit: z4.number().int().positive().max(1e3).optional().describe("Max emails to return per page. Default 50. AI clients should start with 50-100 and paginate."),
|
|
6056
|
+
offset: z4.number().int().min(0).optional().describe("Number of results to skip for pagination. Use with totalUncategorized and hasMore."),
|
|
6057
|
+
unread_only: z4.boolean().optional(),
|
|
6058
|
+
since: z4.string().min(1).optional()
|
|
5294
6059
|
},
|
|
5295
6060
|
annotations: {
|
|
5296
6061
|
readOnlyHint: true
|
|
@@ -5303,15 +6068,37 @@ async function createMcpServer() {
|
|
|
5303
6068
|
since
|
|
5304
6069
|
}))
|
|
5305
6070
|
);
|
|
6071
|
+
server.registerTool(
|
|
6072
|
+
"review_categorized",
|
|
6073
|
+
{
|
|
6074
|
+
description: "Scan recently categorized emails for anomalies that suggest a misclassification or over-aggressive archive.",
|
|
6075
|
+
inputSchema: reviewCategorizedInputSchema.shape,
|
|
6076
|
+
annotations: {
|
|
6077
|
+
readOnlyHint: true
|
|
6078
|
+
}
|
|
6079
|
+
},
|
|
6080
|
+
toolHandler(async (args) => reviewCategorized(args))
|
|
6081
|
+
);
|
|
6082
|
+
server.registerTool(
|
|
6083
|
+
"query_emails",
|
|
6084
|
+
{
|
|
6085
|
+
description: "Run structured analytics queries over the cached email dataset using fixed filters, groupings, and aggregates.",
|
|
6086
|
+
inputSchema: queryEmailsInputSchema.shape,
|
|
6087
|
+
annotations: {
|
|
6088
|
+
readOnlyHint: true
|
|
6089
|
+
}
|
|
6090
|
+
},
|
|
6091
|
+
toolHandler(async (args) => queryEmails(args))
|
|
6092
|
+
);
|
|
5306
6093
|
server.registerTool(
|
|
5307
6094
|
"get_noise_senders",
|
|
5308
6095
|
{
|
|
5309
6096
|
description: "Return a focused list of active, high-noise senders worth categorizing, filtering, or unsubscribing.",
|
|
5310
6097
|
inputSchema: {
|
|
5311
|
-
limit:
|
|
5312
|
-
min_noise_score:
|
|
5313
|
-
active_days:
|
|
5314
|
-
sort_by:
|
|
6098
|
+
limit: z4.number().int().positive().max(50).optional(),
|
|
6099
|
+
min_noise_score: z4.number().min(0).optional(),
|
|
6100
|
+
active_days: z4.number().int().positive().optional(),
|
|
6101
|
+
sort_by: z4.enum(["noise_score", "all_time_noise_score", "message_count", "unread_rate"]).optional().describe("Sort order. Default: noise_score. Use all_time_noise_score for lifetime perspective.")
|
|
5315
6102
|
},
|
|
5316
6103
|
annotations: {
|
|
5317
6104
|
readOnlyHint: true
|
|
@@ -5329,9 +6116,9 @@ async function createMcpServer() {
|
|
|
5329
6116
|
{
|
|
5330
6117
|
description: "Return ranked senders with unsubscribe links, sorted by how much inbox noise unsubscribing would remove.",
|
|
5331
6118
|
inputSchema: {
|
|
5332
|
-
limit:
|
|
5333
|
-
min_messages:
|
|
5334
|
-
unread_only_senders:
|
|
6119
|
+
limit: z4.number().int().positive().max(50).optional(),
|
|
6120
|
+
min_messages: z4.number().int().positive().optional(),
|
|
6121
|
+
unread_only_senders: z4.boolean().optional()
|
|
5335
6122
|
},
|
|
5336
6123
|
annotations: {
|
|
5337
6124
|
readOnlyHint: true
|
|
@@ -5348,9 +6135,9 @@ async function createMcpServer() {
|
|
|
5348
6135
|
{
|
|
5349
6136
|
description: "Return the unsubscribe target for a sender and optionally label/archive existing emails in one undoable run.",
|
|
5350
6137
|
inputSchema: {
|
|
5351
|
-
sender_email:
|
|
5352
|
-
also_archive:
|
|
5353
|
-
also_label:
|
|
6138
|
+
sender_email: z4.string().min(1),
|
|
6139
|
+
also_archive: z4.boolean().optional(),
|
|
6140
|
+
also_label: z4.string().min(1).optional()
|
|
5354
6141
|
},
|
|
5355
6142
|
annotations: {
|
|
5356
6143
|
readOnlyHint: false,
|
|
@@ -5368,7 +6155,7 @@ async function createMcpServer() {
|
|
|
5368
6155
|
{
|
|
5369
6156
|
description: "Validate and deploy a rule directly from YAML content.",
|
|
5370
6157
|
inputSchema: {
|
|
5371
|
-
yaml_content:
|
|
6158
|
+
yaml_content: z4.string().min(1)
|
|
5372
6159
|
},
|
|
5373
6160
|
annotations: {
|
|
5374
6161
|
readOnlyHint: false,
|
|
@@ -5385,7 +6172,7 @@ async function createMcpServer() {
|
|
|
5385
6172
|
{
|
|
5386
6173
|
description: "List deployed inboxctl rules and their execution status.",
|
|
5387
6174
|
inputSchema: {
|
|
5388
|
-
enabled_only:
|
|
6175
|
+
enabled_only: z4.boolean().optional()
|
|
5389
6176
|
},
|
|
5390
6177
|
annotations: {
|
|
5391
6178
|
readOnlyHint: true
|
|
@@ -5401,9 +6188,9 @@ async function createMcpServer() {
|
|
|
5401
6188
|
{
|
|
5402
6189
|
description: "Run a deployed rule in dry-run mode by default, or apply it when dry_run is false.",
|
|
5403
6190
|
inputSchema: {
|
|
5404
|
-
rule_name:
|
|
5405
|
-
dry_run:
|
|
5406
|
-
max_emails:
|
|
6191
|
+
rule_name: z4.string().min(1),
|
|
6192
|
+
dry_run: z4.boolean().optional(),
|
|
6193
|
+
max_emails: z4.number().int().positive().max(1e3).optional()
|
|
5407
6194
|
},
|
|
5408
6195
|
annotations: {
|
|
5409
6196
|
readOnlyHint: false,
|
|
@@ -5420,7 +6207,7 @@ async function createMcpServer() {
|
|
|
5420
6207
|
{
|
|
5421
6208
|
description: "Enable a deployed rule by name.",
|
|
5422
6209
|
inputSchema: {
|
|
5423
|
-
rule_name:
|
|
6210
|
+
rule_name: z4.string().min(1)
|
|
5424
6211
|
},
|
|
5425
6212
|
annotations: {
|
|
5426
6213
|
readOnlyHint: false,
|
|
@@ -5434,7 +6221,7 @@ async function createMcpServer() {
|
|
|
5434
6221
|
{
|
|
5435
6222
|
description: "Disable a deployed rule by name.",
|
|
5436
6223
|
inputSchema: {
|
|
5437
|
-
rule_name:
|
|
6224
|
+
rule_name: z4.string().min(1)
|
|
5438
6225
|
},
|
|
5439
6226
|
annotations: {
|
|
5440
6227
|
readOnlyHint: false,
|
|
@@ -5470,6 +6257,15 @@ async function createMcpServer() {
|
|
|
5470
6257
|
},
|
|
5471
6258
|
async (uri) => resourceText(resolveResourceUri(uri, "inbox://action-log"), await buildActionLog())
|
|
5472
6259
|
);
|
|
6260
|
+
server.registerResource(
|
|
6261
|
+
"query-fields",
|
|
6262
|
+
"schema://query-fields",
|
|
6263
|
+
{
|
|
6264
|
+
description: "Field vocabulary, aggregates, and examples for the query_emails analytics tool.",
|
|
6265
|
+
mimeType: "application/json"
|
|
6266
|
+
},
|
|
6267
|
+
async (uri) => resourceText(resolveResourceUri(uri, "schema://query-fields"), QUERY_EMAILS_FIELD_SCHEMA)
|
|
6268
|
+
);
|
|
5473
6269
|
server.registerResource(
|
|
5474
6270
|
"deployed-rules",
|
|
5475
6271
|
"rules://deployed",
|
|
@@ -5529,6 +6325,10 @@ async function createMcpServer() {
|
|
|
5529
6325
|
async () => promptResult(
|
|
5530
6326
|
"Review top senders and recommend cleanup actions.",
|
|
5531
6327
|
[
|
|
6328
|
+
"Step 0 \u2014 Check for past mistakes:",
|
|
6329
|
+
" Call `review_categorized` to see if any recent categorisations look incorrect.",
|
|
6330
|
+
" If anomalies are found, present them first \u2014 fixing past mistakes takes priority over reviewing new senders.",
|
|
6331
|
+
"",
|
|
5532
6332
|
"Step 1 \u2014 Gather data:",
|
|
5533
6333
|
" Use `get_noise_senders` for the most actionable noisy senders.",
|
|
5534
6334
|
" Use `rules://deployed` to check for existing rules covering these senders.",
|
|
@@ -5626,6 +6426,10 @@ async function createMcpServer() {
|
|
|
5626
6426
|
" FYI \u2014 worth knowing about but no action needed",
|
|
5627
6427
|
" NOISE \u2014 bulk, promotional, or irrelevant",
|
|
5628
6428
|
"",
|
|
6429
|
+
"Step 2.5 \u2014 Flag low-confidence items:",
|
|
6430
|
+
' For any email with `confidence: "low"` in `senderContext`, always categorise it as ACTION REQUIRED.',
|
|
6431
|
+
" Better to surface a false positive than bury a real personal or work email.",
|
|
6432
|
+
"",
|
|
5629
6433
|
"Step 3 \u2014 Present findings:",
|
|
5630
6434
|
" List emails grouped by category with: sender, subject, and one-line reason.",
|
|
5631
6435
|
" For NOISE, suggest a label and whether to archive.",
|
|
@@ -5673,6 +6477,14 @@ async function createMcpServer() {
|
|
|
5673
6477
|
" For each group show: count, senders involved, sample subjects.",
|
|
5674
6478
|
" Note confidence level: HIGH (clear pattern), MEDIUM (reasonable guess), LOW (uncertain).",
|
|
5675
6479
|
" Flag any LOW confidence items for the user to decide.",
|
|
6480
|
+
" Present the confidence breakdown: X HIGH (auto-apply), Y MEDIUM (label only), Z LOW (review queue).",
|
|
6481
|
+
" If any LOW confidence emails are present, note why they were flagged from the `signals` array.",
|
|
6482
|
+
"",
|
|
6483
|
+
"Step 3.5 \u2014 Apply confidence gating:",
|
|
6484
|
+
" HIGH confidence \u2014 safe to apply directly (label, mark_read, archive as appropriate).",
|
|
6485
|
+
" MEDIUM confidence \u2014 apply the category label only. Do not archive. Keep the email visible in the inbox.",
|
|
6486
|
+
" LOW confidence \u2014 apply only the label `inboxctl/Review`. Do not archive or mark read.",
|
|
6487
|
+
" These emails need human review before any further action.",
|
|
5676
6488
|
"",
|
|
5677
6489
|
"Step 4 \u2014 Apply with user approval:",
|
|
5678
6490
|
" Create labels for any new categories (use `create_label`).",
|
|
@@ -5688,7 +6500,11 @@ async function createMcpServer() {
|
|
|
5688
6500
|
"Step 6 \u2014 Suggest ongoing rules:",
|
|
5689
6501
|
" For any category with 3+ emails from the same sender, suggest a YAML rule.",
|
|
5690
6502
|
" This prevents the same categorisation from being needed again.",
|
|
5691
|
-
" Use `deploy_rule` after user reviews the YAML."
|
|
6503
|
+
" Use `deploy_rule` after user reviews the YAML.",
|
|
6504
|
+
"",
|
|
6505
|
+
"Step 7 \u2014 Post-categorisation audit:",
|
|
6506
|
+
" After applying actions, call `review_categorized` to check for anomalies.",
|
|
6507
|
+
" If anomalies are found, present them with the option to undo the relevant run."
|
|
5692
6508
|
].join("\n")
|
|
5693
6509
|
)
|
|
5694
6510
|
);
|
|
@@ -5704,7 +6520,7 @@ async function createMcpServer() {
|
|
|
5704
6520
|
{
|
|
5705
6521
|
description: "Get the details of a specific Gmail server-side filter by ID.",
|
|
5706
6522
|
inputSchema: {
|
|
5707
|
-
filter_id:
|
|
6523
|
+
filter_id: z4.string().min(1).describe("Gmail filter ID")
|
|
5708
6524
|
}
|
|
5709
6525
|
},
|
|
5710
6526
|
toolHandler(async ({ filter_id }) => getFilter(filter_id))
|
|
@@ -5714,20 +6530,20 @@ async function createMcpServer() {
|
|
|
5714
6530
|
{
|
|
5715
6531
|
description: "Create a Gmail server-side filter that applies automatically to all future incoming mail. Useful for simple, always-on rules (e.g. 'label all mail from newsletter@x.com and archive it'). At least one criteria field and one action field are required. Gmail does not support updating filters \u2014 to change one, delete it and create a new one. For regex matching, OR conditions, snippet matching, or processing existing mail, use YAML rules instead.",
|
|
5716
6532
|
inputSchema: {
|
|
5717
|
-
from:
|
|
5718
|
-
to:
|
|
5719
|
-
subject:
|
|
5720
|
-
query:
|
|
5721
|
-
negated_query:
|
|
5722
|
-
has_attachment:
|
|
5723
|
-
exclude_chats:
|
|
5724
|
-
size:
|
|
5725
|
-
size_comparison:
|
|
5726
|
-
label:
|
|
5727
|
-
archive:
|
|
5728
|
-
mark_read:
|
|
5729
|
-
star:
|
|
5730
|
-
forward:
|
|
6533
|
+
from: z4.string().optional().describe("Match emails from this address"),
|
|
6534
|
+
to: z4.string().optional().describe("Match emails sent to this address"),
|
|
6535
|
+
subject: z4.string().optional().describe("Match emails with this text in the subject"),
|
|
6536
|
+
query: z4.string().optional().describe("Match using Gmail search syntax (e.g. 'has:attachment')"),
|
|
6537
|
+
negated_query: z4.string().optional().describe("Exclude emails matching this Gmail query"),
|
|
6538
|
+
has_attachment: z4.boolean().optional().describe("Match emails with attachments"),
|
|
6539
|
+
exclude_chats: z4.boolean().optional().describe("Exclude chat messages from matches"),
|
|
6540
|
+
size: z4.number().int().positive().optional().describe("Size threshold in bytes"),
|
|
6541
|
+
size_comparison: z4.enum(["larger", "smaller"]).optional().describe("Use with size: match emails larger or smaller than the threshold"),
|
|
6542
|
+
label: z4.string().optional().describe("Apply this label to matching emails (auto-created if it does not exist)"),
|
|
6543
|
+
archive: z4.boolean().optional().describe("Archive matching emails (remove from inbox)"),
|
|
6544
|
+
mark_read: z4.boolean().optional().describe("Mark matching emails as read"),
|
|
6545
|
+
star: z4.boolean().optional().describe("Star matching emails"),
|
|
6546
|
+
forward: z4.string().email().optional().describe("Forward matching emails to this address (address must be verified in Gmail settings)")
|
|
5731
6547
|
}
|
|
5732
6548
|
},
|
|
5733
6549
|
toolHandler(
|
|
@@ -5754,7 +6570,7 @@ async function createMcpServer() {
|
|
|
5754
6570
|
{
|
|
5755
6571
|
description: "Delete a Gmail server-side filter by ID. The filter stops processing future mail immediately. Already-processed mail is not affected. Use list_filters to find filter IDs.",
|
|
5756
6572
|
inputSchema: {
|
|
5757
|
-
filter_id:
|
|
6573
|
+
filter_id: z4.string().min(1).describe("Gmail filter ID to delete")
|
|
5758
6574
|
}
|
|
5759
6575
|
},
|
|
5760
6576
|
toolHandler(async ({ filter_id }) => {
|
|
@@ -5852,4 +6668,4 @@ export {
|
|
|
5852
6668
|
createMcpServer,
|
|
5853
6669
|
startMcpServer
|
|
5854
6670
|
};
|
|
5855
|
-
//# sourceMappingURL=chunk-
|
|
6671
|
+
//# sourceMappingURL=chunk-OLL3OA5B.js.map
|