This commit is contained in:
2026-02-27 08:27:43 +00:00
parent 7bf55af5b3
commit bc4c20929c
48 changed files with 4344 additions and 275 deletions
+14 -6
View File
@@ -2,7 +2,7 @@
version: 3
name: email_db
label: Email Database
description: Query, search, aggregate, and manage the user's email database. Use this tool to answer questions about emails, find messages by sender/domain/date/content, get statistics, and delete emails. The database is a local SQLite copy of the user's synced Gmail inbox.
description: Query, search, aggregate, and manage the user's email database. Use this tool to answer questions about emails, find messages by sender/domain/date/content, get statistics, and delete emails. The database is a local SQLite copy of the user's synced Gmail. All actions default to inbox scope — use folder parameter to query other folders like sent, spam, trash, or 'all' for everything.
language: typescript
inputs:
action:
@@ -36,6 +36,10 @@ inputs:
type: string
description: "Attachment content type filter. Full MIME type (e.g. 'image/jpeg') or just the type prefix (e.g. 'image' matches all image types). Used with 'attachments' action."
optional: true
folder:
type: string
description: "Gmail folder/label to scope results. Defaults to 'inbox'. Use 'all' for all folders. Common values: inbox, sent, spam, trash."
optional: true
limit:
type: number
description: "Maximum number of results to return (default 20, max 100)."
@@ -65,9 +69,9 @@ emails (
id TEXT PRIMARY KEY,
integration TEXT, -- source: 'gmail', 'outlook', etc.
email_account TEXT, -- which account: 'user@gmail.com'
from_name TEXT,
from_address TEXT,
from_domain TEXT,
from_name TEXT, -- sender display name
from_address TEXT, -- sender email (lowercase)
from_domain TEXT, -- domain extracted from sender
to_address TEXT,
cc TEXT,
subject TEXT,
@@ -77,7 +81,8 @@ emails (
text_body TEXT,
attachment_count INTEGER,
read INTEGER,
deleted INTEGER
deleted INTEGER,
labels TEXT -- comma-separated label list (e.g. 'INBOX,UNREAD,CATEGORY_UPDATES')
)
attachments (
@@ -91,10 +96,13 @@ attachments (
## Examples
- Search for invoices: `action: "search", search: "invoice"`
- Search for invoices (inbox only): `action: "search", search: "invoice"`
- Search sent emails for invoices: `action: "search", search: "invoice", folder: "sent"`
- Search all folders: `action: "search", search: "invoice", folder: "all"`
- Count emails from a domain: `action: "count", domain: "newsletter.com"`
- Delete all emails from a domain: `action: "delete", domain: "spam.com"`
- Top 10 domains: `action: "domains", limit: 10`
- Stats for spam folder: `action: "stats", folder: "spam"`
- List attachment types: `action: "attachment-types"`
- Find image attachments: `action: "attachments", content_type: "image"`
- Find PDFs from a sender: `action: "attachments", content_type: "application/pdf", sender: "boss@company.com"`
+30 -17
View File
@@ -15,6 +15,7 @@ type Params = {
before?: string;
after?: string;
content_type?: string;
folder?: string;
limit?: number;
};
@@ -72,9 +73,18 @@ function formatRows(rows: Record<string, unknown>[], limit: number): string {
// ── Helpers ──
function buildWhereClause(params: Params): string {
function buildFolderCondition(folder: string | undefined): string | null {
if (!folder || folder === 'all') return null;
return `labels LIKE ${sqlStr(`%${folder}%`)}`;
}
function buildWhereClause(params: Params, defaultFolder?: string): string {
const conditions: string[] = [];
const folder = params.folder ?? defaultFolder;
const folderCond = buildFolderCondition(folder);
if (folderCond) conditions.push(folderCond);
if (params.domain) {
conditions.push(`from_domain = ${sqlStr(params.domain.toLowerCase())}`);
}
@@ -111,20 +121,21 @@ function runQuery(sql: string, limit: number): string {
}
function searchEmails(params: Params, limit: number): string {
const where = withActive(buildWhereClause(params));
const where = withActive(buildWhereClause(params, 'inbox'));
const rows = queryJson(`SELECT id, from_name, from_address, subject, date, snippet, attachment_count FROM emails ${where} ORDER BY date DESC LIMIT ${limit}`);
return formatRows(rows, limit);
}
function getStats(): string {
const totalRows = queryJson('SELECT COUNT(*) as count FROM emails WHERE deleted = 0');
function getStats(params: Params): string {
const folderWhere = withActive(buildWhereClause({ action: 'stats', folder: params.folder }, 'inbox'));
const totalRows = queryJson(`SELECT COUNT(*) as count FROM emails ${folderWhere}`);
const total = (totalRows[0]?.count as number) ?? 0;
if (total === 0) return 'Email database is empty.';
const dateRange = queryJson('SELECT MIN(date) as oldest, MAX(date) as newest FROM emails WHERE deleted = 0');
const topDomains = queryJson('SELECT from_domain, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_domain ORDER BY count DESC LIMIT 10');
const topSenders = queryJson('SELECT from_address, from_name, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_address ORDER BY count DESC LIMIT 10');
const attRows = queryJson('SELECT COUNT(*) as count FROM emails WHERE deleted = 0 AND attachment_count > 0');
const dateRange = queryJson(`SELECT MIN(date) as oldest, MAX(date) as newest FROM emails ${folderWhere}`);
const topDomains = queryJson(`SELECT from_domain, COUNT(*) as count FROM emails ${folderWhere} GROUP BY from_domain ORDER BY count DESC LIMIT 10`);
const topSenders = queryJson(`SELECT from_address, from_name, COUNT(*) as count FROM emails ${folderWhere} GROUP BY from_address ORDER BY count DESC LIMIT 10`);
const attRows = queryJson(`SELECT COUNT(*) as count FROM emails ${folderWhere} AND attachment_count > 0`);
const withAttachments = (attRows[0]?.count as number) ?? 0;
const dr = dateRange[0] ?? {};
@@ -146,7 +157,7 @@ function getStats(): string {
}
function countEmails(params: Params): string {
const where = withActive(buildWhereClause(params));
const where = withActive(buildWhereClause(params, 'inbox'));
const rows = queryJson(`SELECT COUNT(*) as count FROM emails ${where}`);
const count = (rows[0]?.count as number) ?? 0;
@@ -161,16 +172,18 @@ function countEmails(params: Params): string {
return `${count} email${count !== 1 ? 's' : ''}${desc}.`;
}
function listDomains(limit: number): string {
const rows = queryJson(`SELECT from_domain, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_domain ORDER BY count DESC LIMIT ${limit}`);
function listDomains(params: Params, limit: number): string {
const where = withActive(buildWhereClause({ action: 'domains', folder: params.folder }, 'inbox'));
const rows = queryJson(`SELECT from_domain, COUNT(*) as count FROM emails ${where} GROUP BY from_domain ORDER BY count DESC LIMIT ${limit}`);
if (rows.length === 0) return 'No emails in database.';
const lines = rows.map((d, i) => `${i + 1}. ${d.from_domain}${d.count} email${(d.count as number) !== 1 ? 's' : ''}`);
return [`**Sender Domains** (${rows.length}):`, '', ...lines].join('\n');
}
function listSenders(limit: number): string {
const rows = queryJson(`SELECT from_address, from_name, COUNT(*) as count FROM emails WHERE deleted = 0 GROUP BY from_address ORDER BY count DESC LIMIT ${limit}`);
function listSenders(params: Params, limit: number): string {
const where = withActive(buildWhereClause({ action: 'senders', folder: params.folder }, 'inbox'));
const rows = queryJson(`SELECT from_address, from_name, COUNT(*) as count FROM emails ${where} GROUP BY from_address ORDER BY count DESC LIMIT ${limit}`);
if (rows.length === 0) return 'No emails in database.';
const lines = rows.map((s, i) => {
@@ -217,7 +230,7 @@ function searchAttachments(params: Params, limit: number): string {
}
function deleteEmails(params: Params): string {
const where = buildWhereClause(params);
const where = buildWhereClause(params, 'inbox');
if (!where) {
throw new Error('Delete requires at least one filter (domain, sender, before, after, or search).');
}
@@ -254,16 +267,16 @@ export async function execute(_toolCallId: string, params: Params): Promise<Tool
return ok(searchEmails(params, limit));
case 'stats':
return ok(getStats());
return ok(getStats(params));
case 'count':
return ok(countEmails(params));
case 'domains':
return ok(listDomains(limit));
return ok(listDomains(params, limit));
case 'senders':
return ok(listSenders(limit));
return ok(listSenders(params, limit));
case 'attachment-types':
return ok(listAttachmentTypes(limit));