mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-21 20:18:11 +02:00
added send
This commit is contained in:
parent
c157e9e50d
commit
6f80576f72
7 changed files with 168 additions and 36 deletions
|
|
@ -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',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue