replace imap gmail sync with mbsync + maildir import, app password UI

- rewrite gmail-sync handler: mbsync downloads to local Maildir, then import to sqlite
- add app password field to google integration config and API
- gmail sync section independent from oauth in settings UI
- live mbsync progress streaming to job status
- recoverable failure email with instructions for overquota/auth errors
- sync meta persisted to job on failure for richer notifications

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent 927267e041
commit 583580eb10
6 changed files with 472 additions and 256 deletions
@@ -126,9 +126,28 @@ integrationsRouter.get('/google/status', async (ctx) => {
connected: !!connConfig?.accessToken,
email: connConfig?.email ?? null,
picture: connConfig?.picture ?? null,
hasAppPassword: !!connConfig?.imapAppPassword,
});
});
integrationsRouter.put('/google/app-password', async (ctx) => {
const user = ctx.get('user');
const body = ctx.get('body') as { appPassword?: string };
if (!body.appPassword) throw BAD_REQUEST('Missing appPassword');
const connection = await getUserIntegration(user.id, 'google');
const connConfig = (connection?.config as Record<string, unknown>) ?? {};
await upsertUserIntegration({
userId: user.id,
provider: 'google',
serverIntegrationId: connection?.serverIntegrationId ?? undefined,
config: { ...connConfig, imapAppPassword: body.appPassword },
});
return ctx.json({ ok: true });
});
integrationsRouter.delete('/google/connection', async (ctx) => {
const user = ctx.get('user');
const connection = await getUserIntegration(user.id, 'google');