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
+52 -5
View File
@@ -1,17 +1,64 @@
import Layout from 'emailer/emails/layouts/MainLayout.jsx';
import { Container, Text } from '@react-email/components';
import { Container, Text, Link } from '@react-email/components';
type JobEmailData = {
job: { type: string; error?: string };
job: {
type: string;
error?: string;
meta?: {
gmailSyncRecoverable?: boolean;
gmailSyncEmailCount?: number;
gmailSyncIsAuthFail?: boolean;
};
};
};
const GmailSyncRecovery = ({ job }: JobEmailData) => {
const emailCount = job.meta?.gmailSyncEmailCount ?? 0;
const isAuthFail = job.meta?.gmailSyncIsAuthFail;
return (
<>
<Text className="text-base font-semibold text-green-700">Don't worry — your progress is saved.</Text>
{emailCount > 0 && (
<Text className="text-sm text-gray-700">
Your mailbox already has {emailCount.toLocaleString()} emails downloaded. The sync will resume from where it
stopped — it won't re-download emails you already have.
</Text>
)}
<Text className="text-sm text-gray-700">
{isAuthFail
? 'Google temporarily blocked sign-in after hitting bandwidth limits. This usually clears up within a few hours.'
: 'Gmail throttled the connection after downloading too much data at once. This is normal for large mailboxes.'}
</Text>
<Text className="text-sm font-semibold text-gray-800">To resume syncing:</Text>
<Text className="text-sm text-gray-700" style={{ marginTop: 0 }}>
1. Wait a few hours for Google to lift the block{'\n'}
2. Generate a new App Password at{' '}
<Link href="https://myaccount.google.com/apppasswords">myaccount.google.com/apppasswords</Link> (Google revokes
them after repeated auth failures){'\n'}
3. Save the new password in Settings Integrations Google Account{'\n'}
4. Click "Sync Gmail Inbox" to resume only new emails will be downloaded
</Text>
</>
);
};
const Email = ({ job }: JobEmailData) => {
const isRecoverableGmail = job?.type === 'gmail-sync' && job.meta?.gmailSyncRecoverable;
return (
<Layout>
<Container>
<Text className="pt-4 text-2xl">Job Failed</Text>
<Text>Your {job?.type ?? 'unknown'} job has failed.</Text>
{job?.error ? <Text className="text-sm text-gray-600">{job.error}</Text> : null}
<Text className="pt-4 text-2xl">{isRecoverableGmail ? 'Gmail Sync Paused' : 'Job Failed'}</Text>
{isRecoverableGmail ? (
<GmailSyncRecovery job={job} />
) : (
<>
<Text>Your {job?.type ?? 'unknown'} job has failed.</Text>
{job?.error ? <Text className="text-sm text-gray-600">{job.error}</Text> : null}
</>
)}
</Container>
</Layout>
);