added send

This commit is contained in:
Arjun 2026-05-14 21:09:54 +05:30
parent c157e9e50d
commit 6f80576f72
7 changed files with 168 additions and 36 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

@ -134,7 +134,7 @@ async function publishCalendarSyncEvent(
// Configuration
const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
const LOOKBACK_DAYS = 7;
const REQUIRED_SCOPES = [
'https://www.googleapis.com/auth/calendar.events.readonly',

View file

@ -25,8 +25,8 @@ const CACHE_DIR = path.join(WorkDir, 'inbox_lists');
console.warn('[Gmail] Cache directory migration failed:', err);
}
})();
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
const REQUIRED_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly';
const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
const REQUIRED_SCOPE = 'https://www.googleapis.com/auth/gmail.modify';
const MAX_THREADS_IN_DIGEST = 10;
const nhm = new NodeHtmlMarkdown();
@ -107,6 +107,7 @@ export interface GmailThreadSnapshot {
bodyHtml?: string;
unread?: boolean;
bodyHeight?: number;
messageIdHeader?: string;
}>;
}
@ -465,6 +466,7 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
bodyHtml,
unread: msg.labelIds?.includes('UNREAD') ?? false,
bodyHeight: msg.id ? heightCarryover.get(msg.id) : undefined,
messageIdHeader: headerValue(headers, 'Message-ID') || headerValue(headers, 'Message-Id') || undefined,
isDraft,
};
}));
@ -911,6 +913,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.`);