added send

This commit is contained in:
Arjun 2026-05-14 21:09:54 +05:30
parent eb4b11a530
commit c0266d4583
6 changed files with 165 additions and 34 deletions

View file

@ -75,7 +75,7 @@ const providerConfigs: ProviderConfig = {
mode: 'static',
},
scopes: [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/calendar.events.readonly',
'https://www.googleapis.com/auth/drive.readonly',
],

View file

@ -26,7 +26,7 @@ const CACHE_DIR = path.join(WorkDir, 'inbox_lists');
}
})();
const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
const REQUIRED_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly';
const REQUIRED_SCOPE = 'https://www.googleapis.com/auth/gmail.modify';
const MAX_THREADS_IN_DIGEST = 10;
const RECENT_BACKFILL_INTERVAL_MS = 15 * 60 * 1000;
const nhm = new NodeHtmlMarkdown();
@ -114,6 +114,7 @@ export interface GmailThreadSnapshot {
sizeBytes?: number;
savedPath: string;
}>;
messageIdHeader?: string;
}>;
}
@ -1158,6 +1159,100 @@ async function performSync() {
}
}
// --- Send Reply ---
export interface SendReplyOptions {
threadId: string;
to: string;
subject: string;
bodyHtml: string;
bodyText: string;
inReplyTo?: string;
references?: string;
}
export interface SendReplyResult {
messageId?: string;
error?: string;
}
function encodeRfc2047(text: string): string {
// Only encode if non-ASCII chars present.
// eslint-disable-next-line no-control-regex
if (/^[\x00-\x7F]*$/.test(text)) return text;
return `=?UTF-8?B?${Buffer.from(text).toString('base64')}?=`;
}
export async function sendThreadReply(opts: SendReplyOptions): Promise<SendReplyResult> {
const auth = await GoogleClientFactory.getClient();
if (!auth) return { error: 'Gmail is not connected.' };
const gmailClient = google.gmail({ version: 'v1', auth });
const userEmail = await getUserEmail(auth);
if (!userEmail) return { error: 'Could not determine your Gmail address.' };
const boundary = `b_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const headers: string[] = [];
headers.push(`From: ${userEmail}`);
headers.push(`To: ${opts.to}`);
headers.push(`Subject: ${encodeRfc2047(opts.subject)}`);
if (opts.inReplyTo) headers.push(`In-Reply-To: ${opts.inReplyTo}`);
if (opts.references) headers.push(`References: ${opts.references}`);
headers.push('MIME-Version: 1.0');
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
const parts: string[] = [];
parts.push(`--${boundary}`);
parts.push('Content-Type: text/plain; charset="UTF-8"');
parts.push('Content-Transfer-Encoding: 7bit');
parts.push('');
parts.push(opts.bodyText);
parts.push('');
parts.push(`--${boundary}`);
parts.push('Content-Type: text/html; charset="UTF-8"');
parts.push('Content-Transfer-Encoding: 7bit');
parts.push('');
parts.push(opts.bodyHtml);
parts.push('');
parts.push(`--${boundary}--`);
const message = `${headers.join('\r\n')}\r\n\r\n${parts.join('\r\n')}`;
const raw = Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
try {
const res = await gmailClient.users.messages.send({
userId: 'me',
requestBody: { raw, threadId: opts.threadId },
});
// Clean up any Gmail-side drafts in this thread.
try {
const drafts = await gmailClient.users.drafts.list({ userId: 'me' });
const matching = (drafts.data.drafts || []).filter(
(d) => d.message?.threadId === opts.threadId && d.id
);
await Promise.all(
matching.map((d) =>
gmailClient.users.drafts.delete({ userId: 'me', id: d.id! })
)
);
} catch (cleanupErr) {
console.warn('[Gmail] Draft cleanup after send failed:', cleanupErr);
}
// Wake the sync loop so the cache picks up the new message.
triggerSync();
return { messageId: res.data.id || undefined };
} catch (err) {
return { error: err instanceof Error ? err.message : String(err) };
}
}
export async function init() {
console.log("Starting Gmail Sync (TS)...");
console.log(`Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`);