wip: email sync via imap with status tracking and auto cron

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 04:57:30 +00:00
co-authored by Claude Opus 4.6
parent 5925ac49a1
commit 170bd6d41b
20 changed files with 1929 additions and 373 deletions
@@ -0,0 +1,34 @@
import { getServerIntegration } from 'officerdb';
import { PermanentError } from '../../queue/types';
type TokenRefreshResult = { accessToken: string; expiresAt: number };
export async function refreshGoogleAccessToken(refreshToken: string): Promise<TokenRefreshResult> {
const serverGoogle = await getServerIntegration('google');
const serverConfig = serverGoogle?.config as Record<string, unknown> | undefined;
if (!serverConfig?.clientId || !serverConfig?.clientSecret) {
throw new PermanentError('Google OAuth not configured on server');
}
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: serverConfig.clientId as string,
client_secret: serverConfig.clientSecret as string,
refresh_token: refreshToken,
grant_type: 'refresh_token',
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Google token refresh failed: ${text}`);
}
const data = (await response.json()) as { access_token: string; expires_in: number };
return {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
}
@@ -259,11 +259,16 @@ export const googleCallbackHandler = async (ctx: any) => {
const serverIntegration = await getServerIntegration('google');
// Merge with existing config to preserve fields like imapAppPassword
const existingIntegration = await getUserIntegration(dbUser.id, 'google');
const existingConfig = (existingIntegration?.config as Record<string, unknown>) ?? {};
await upsertUserIntegration({
userId: dbUser.id,
provider: 'google',
serverIntegrationId: serverIntegration?.id,
config: {
...existingConfig,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,