Showing preview only (2,608K chars total). Download the full file or copy to clipboard to get everything.
Repository: avihaymenahem/velo
Branch: main
Commit: ec47a7a5095b
Files: 470
Total size: 2.4 MB
Directory structure:
gitextract_6tjjzw1_/
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── docs.yml
│ │ └── feature_request.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── packaging.yml
│ ├── release-please.yml
│ ├── release.yml
│ └── update-homebrew.yml
├── .gitignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── com.velomail.app.desktop
├── com.velomail.app.metainfo.xml
├── com.velomail.app.yml
├── docs/
│ ├── architecture.md
│ ├── development.md
│ └── keyboard-shortcuts.md
├── index.html
├── landing/
│ ├── .gitignore
│ ├── README.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── public/
│ │ ├── og-image.html
│ │ ├── robots.txt
│ │ ├── screenshots/
│ │ │ └── .gitkeep
│ │ └── sitemap.xml
│ ├── src/
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── components/
│ │ │ ├── CtaFooter.tsx
│ │ │ ├── Features.tsx
│ │ │ ├── Hero.tsx
│ │ │ ├── Navbar.tsx
│ │ │ ├── OpenSource.tsx
│ │ │ ├── ProductShowcase.tsx
│ │ │ ├── WhyVelo.tsx
│ │ │ └── mockups/
│ │ │ ├── AiMockup.tsx
│ │ │ ├── AppMockup.tsx
│ │ │ ├── MultiProviderMockup.tsx
│ │ │ └── SplitInboxMockup.tsx
│ │ ├── index.css
│ │ └── main.tsx
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wrangler.jsonc
├── package.json
├── release-please-config.json
├── splashscreen.html
├── src/
│ ├── App.tsx
│ ├── ComposerWindow.tsx
│ ├── ThreadWindow.tsx
│ ├── components/
│ │ ├── accounts/
│ │ │ ├── AccountSwitcher.test.tsx
│ │ │ ├── AccountSwitcher.tsx
│ │ │ ├── AddAccount.tsx
│ │ │ ├── AddCalDavAccount.tsx
│ │ │ ├── AddImapAccount.tsx
│ │ │ ├── SetupClientId.test.tsx
│ │ │ └── SetupClientId.tsx
│ │ ├── attachments/
│ │ │ ├── AttachmentGridItem.tsx
│ │ │ ├── AttachmentLibrary.test.tsx
│ │ │ ├── AttachmentLibrary.tsx
│ │ │ └── AttachmentListItem.tsx
│ │ ├── calendar/
│ │ │ ├── CalendarList.test.tsx
│ │ │ ├── CalendarList.tsx
│ │ │ ├── CalendarPage.tsx
│ │ │ ├── CalendarReauthBanner.tsx
│ │ │ ├── CalendarToolbar.tsx
│ │ │ ├── DayView.tsx
│ │ │ ├── EventCard.tsx
│ │ │ ├── EventCreateModal.tsx
│ │ │ ├── EventDetailModal.tsx
│ │ │ ├── MonthView.tsx
│ │ │ └── WeekView.tsx
│ │ ├── composer/
│ │ │ ├── AddressInput.test.tsx
│ │ │ ├── AddressInput.tsx
│ │ │ ├── AiAssistPanel.tsx
│ │ │ ├── AttachmentPicker.tsx
│ │ │ ├── Composer.tsx
│ │ │ ├── EditorToolbar.tsx
│ │ │ ├── FromSelector.tsx
│ │ │ ├── ScheduleSendDialog.tsx
│ │ │ ├── SignatureSelector.tsx
│ │ │ ├── TemplatePicker.tsx
│ │ │ ├── UndoSendToast.tsx
│ │ │ └── scheduleSendPresets.test.ts
│ │ ├── dnd/
│ │ │ ├── DndProvider.test.ts
│ │ │ └── DndProvider.tsx
│ │ ├── email/
│ │ │ ├── ActionBar.tsx
│ │ │ ├── AttachmentList.test.tsx
│ │ │ ├── AttachmentList.tsx
│ │ │ ├── AuthBadge.test.tsx
│ │ │ ├── AuthBadge.tsx
│ │ │ ├── AuthWarningBanner.test.tsx
│ │ │ ├── AuthWarningBanner.tsx
│ │ │ ├── CategoryTabs.test.tsx
│ │ │ ├── CategoryTabs.tsx
│ │ │ ├── ContactSidebar.test.tsx
│ │ │ ├── ContactSidebar.tsx
│ │ │ ├── EmailRenderer.test.tsx
│ │ │ ├── EmailRenderer.tsx
│ │ │ ├── FollowUpDialog.tsx
│ │ │ ├── InlineAttachmentPreview.test.tsx
│ │ │ ├── InlineAttachmentPreview.tsx
│ │ │ ├── InlineReply.tsx
│ │ │ ├── LinkConfirmDialog.tsx
│ │ │ ├── MessageItem.test.tsx
│ │ │ ├── MessageItem.tsx
│ │ │ ├── MoveToFolderDialog.test.tsx
│ │ │ ├── MoveToFolderDialog.tsx
│ │ │ ├── PhishingBanner.tsx
│ │ │ ├── RawMessageModal.test.tsx
│ │ │ ├── RawMessageModal.tsx
│ │ │ ├── SmartReplySuggestions.tsx
│ │ │ ├── SnoozeDialog.tsx
│ │ │ ├── ThreadCard.test.tsx
│ │ │ ├── ThreadCard.tsx
│ │ │ ├── ThreadSummary.tsx
│ │ │ └── ThreadView.tsx
│ │ ├── help/
│ │ │ ├── HelpCard.tsx
│ │ │ ├── HelpCardGrid.tsx
│ │ │ ├── HelpPage.tsx
│ │ │ ├── HelpSearchBar.tsx
│ │ │ ├── HelpSidebar.tsx
│ │ │ ├── HelpTooltip.tsx
│ │ │ └── helpContentSearch.test.ts
│ │ ├── labels/
│ │ │ └── LabelForm.tsx
│ │ ├── layout/
│ │ │ ├── EmailList.tsx
│ │ │ ├── MailLayout.tsx
│ │ │ ├── ReadingPane.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ └── TitleBar.tsx
│ │ ├── search/
│ │ │ ├── AskInbox.tsx
│ │ │ ├── CommandPalette.tsx
│ │ │ ├── SearchBar.tsx
│ │ │ └── ShortcutsHelp.tsx
│ │ ├── settings/
│ │ │ ├── CalDavSettings.tsx
│ │ │ ├── ContactEditor.tsx
│ │ │ ├── FilterEditor.tsx
│ │ │ ├── LabelEditor.test.tsx
│ │ │ ├── LabelEditor.tsx
│ │ │ ├── QuickStepEditor.tsx
│ │ │ ├── SettingsPage.tsx
│ │ │ ├── SignatureEditor.test.tsx
│ │ │ ├── SignatureEditor.tsx
│ │ │ ├── SmartFolderEditor.tsx
│ │ │ ├── SmartLabelEditor.test.tsx
│ │ │ ├── SmartLabelEditor.tsx
│ │ │ ├── SubscriptionManager.tsx
│ │ │ └── TemplateEditor.tsx
│ │ ├── tasks/
│ │ │ ├── AiTaskExtractDialog.tsx
│ │ │ ├── TaskItem.test.tsx
│ │ │ ├── TaskItem.tsx
│ │ │ ├── TaskQuickAdd.tsx
│ │ │ ├── TaskSidebar.tsx
│ │ │ └── TasksPage.tsx
│ │ └── ui/
│ │ ├── Button.test.tsx
│ │ ├── Button.tsx
│ │ ├── ConfirmDialog.test.tsx
│ │ ├── ConfirmDialog.tsx
│ │ ├── ContextMenu.test.tsx
│ │ ├── ContextMenu.tsx
│ │ ├── ContextMenuPortal.tsx
│ │ ├── DateTimePickerDialog.test.tsx
│ │ ├── DateTimePickerDialog.tsx
│ │ ├── EmptyState.tsx
│ │ ├── ErrorBoundary.test.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── InputDialog.test.tsx
│ │ ├── InputDialog.tsx
│ │ ├── Modal.test.tsx
│ │ ├── Modal.tsx
│ │ ├── OfflineBanner.tsx
│ │ ├── Skeleton.tsx
│ │ ├── TextField.test.tsx
│ │ ├── TextField.tsx
│ │ ├── UpdateToast.test.tsx
│ │ ├── UpdateToast.tsx
│ │ └── illustrations/
│ │ ├── GenericEmptyIllustration.tsx
│ │ ├── InboxClearIllustration.tsx
│ │ ├── NoAccountIllustration.tsx
│ │ ├── NoSearchResultsIllustration.tsx
│ │ ├── ReadingPaneIllustration.tsx
│ │ └── index.ts
│ ├── config/
│ │ └── tauriConfig.test.ts
│ ├── constants/
│ │ ├── helpContent.test.ts
│ │ ├── helpContent.ts
│ │ ├── shortcuts.test.ts
│ │ ├── shortcuts.ts
│ │ ├── themes.test.ts
│ │ └── themes.ts
│ ├── hooks/
│ │ ├── useClickOutside.ts
│ │ ├── useContextMenu.ts
│ │ ├── useKeyboardShortcuts.test.ts
│ │ ├── useKeyboardShortcuts.ts
│ │ ├── useRouteNavigation.test.ts
│ │ └── useRouteNavigation.ts
│ ├── main.tsx
│ ├── router/
│ │ ├── index.ts
│ │ ├── navigate.test.ts
│ │ ├── navigate.ts
│ │ └── routeTree.tsx
│ ├── services/
│ │ ├── ai/
│ │ │ ├── aiService.test.ts
│ │ │ ├── aiService.ts
│ │ │ ├── askInbox.ts
│ │ │ ├── categorizationManager.ts
│ │ │ ├── errors.ts
│ │ │ ├── prompts.ts
│ │ │ ├── providerFactory.test.ts
│ │ │ ├── providerFactory.ts
│ │ │ ├── providerManager.test.ts
│ │ │ ├── providerManager.ts
│ │ │ ├── providers/
│ │ │ │ ├── claudeProvider.ts
│ │ │ │ ├── copilotProvider.test.ts
│ │ │ │ ├── copilotProvider.ts
│ │ │ │ ├── geminiProvider.ts
│ │ │ │ ├── ollamaProvider.test.ts
│ │ │ │ ├── ollamaProvider.ts
│ │ │ │ └── openaiProvider.ts
│ │ │ ├── taskExtraction.test.ts
│ │ │ ├── taskExtraction.ts
│ │ │ ├── types.ts
│ │ │ ├── writingStyleService.test.ts
│ │ │ └── writingStyleService.ts
│ │ ├── attachments/
│ │ │ ├── cacheManager.test.ts
│ │ │ ├── cacheManager.ts
│ │ │ ├── preCacheManager.test.ts
│ │ │ └── preCacheManager.ts
│ │ ├── backgroundCheckers.test.ts
│ │ ├── backgroundCheckers.ts
│ │ ├── badgeManager.ts
│ │ ├── bundles/
│ │ │ └── bundleManager.ts
│ │ ├── calendar/
│ │ │ ├── autoDiscovery.test.ts
│ │ │ ├── autoDiscovery.ts
│ │ │ ├── caldavProvider.test.ts
│ │ │ ├── caldavProvider.ts
│ │ │ ├── googleCalendarProvider.test.ts
│ │ │ ├── googleCalendarProvider.ts
│ │ │ ├── icalHelper.test.ts
│ │ │ ├── icalHelper.ts
│ │ │ ├── providerFactory.test.ts
│ │ │ ├── providerFactory.ts
│ │ │ └── types.ts
│ │ ├── categorization/
│ │ │ ├── backfillService.test.ts
│ │ │ ├── backfillService.ts
│ │ │ ├── ruleEngine.test.ts
│ │ │ └── ruleEngine.ts
│ │ ├── composer/
│ │ │ ├── draftAutoSave.test.ts
│ │ │ └── draftAutoSave.ts
│ │ ├── contacts/
│ │ │ └── gravatar.ts
│ │ ├── db/
│ │ │ ├── accounts.test.ts
│ │ │ ├── accounts.ts
│ │ │ ├── aiCache.ts
│ │ │ ├── attachments.test.ts
│ │ │ ├── attachments.ts
│ │ │ ├── bundleRules.test.ts
│ │ │ ├── bundleRules.ts
│ │ │ ├── calendarEvents.test.ts
│ │ │ ├── calendarEvents.ts
│ │ │ ├── calendars.test.ts
│ │ │ ├── calendars.ts
│ │ │ ├── connection.test.ts
│ │ │ ├── connection.ts
│ │ │ ├── contacts.test.ts
│ │ │ ├── contacts.ts
│ │ │ ├── filters.ts
│ │ │ ├── folderSyncState.test.ts
│ │ │ ├── folderSyncState.ts
│ │ │ ├── followUpReminders.ts
│ │ │ ├── imageAllowlist.test.ts
│ │ │ ├── imageAllowlist.ts
│ │ │ ├── labels.test.ts
│ │ │ ├── labels.ts
│ │ │ ├── linkScanResults.ts
│ │ │ ├── localDrafts.test.ts
│ │ │ ├── localDrafts.ts
│ │ │ ├── messages.test.ts
│ │ │ ├── messages.ts
│ │ │ ├── migrations.test.ts
│ │ │ ├── migrations.ts
│ │ │ ├── notificationVips.ts
│ │ │ ├── pendingOperations.test.ts
│ │ │ ├── pendingOperations.ts
│ │ │ ├── phishingAllowlist.ts
│ │ │ ├── quickSteps.test.ts
│ │ │ ├── quickSteps.ts
│ │ │ ├── scheduledEmails.ts
│ │ │ ├── search.ts
│ │ │ ├── sendAsAliases.test.ts
│ │ │ ├── sendAsAliases.ts
│ │ │ ├── settings.ts
│ │ │ ├── signatures.ts
│ │ │ ├── smartFolders.test.ts
│ │ │ ├── smartFolders.ts
│ │ │ ├── smartLabelRules.test.ts
│ │ │ ├── smartLabelRules.ts
│ │ │ ├── tasks.test.ts
│ │ │ ├── tasks.ts
│ │ │ ├── templates.ts
│ │ │ ├── threadCategories.ts
│ │ │ ├── threads.test.ts
│ │ │ ├── threads.ts
│ │ │ ├── writingStyleProfiles.test.ts
│ │ │ └── writingStyleProfiles.ts
│ │ ├── deepLinkHandler.ts
│ │ ├── email/
│ │ │ ├── gmailProvider.test.ts
│ │ │ ├── gmailProvider.ts
│ │ │ ├── imapSmtpProvider.test.ts
│ │ │ ├── imapSmtpProvider.ts
│ │ │ ├── providerFactory.test.ts
│ │ │ ├── providerFactory.ts
│ │ │ └── types.ts
│ │ ├── emailActions.test.ts
│ │ ├── emailActions.ts
│ │ ├── filters/
│ │ │ ├── filterEngine.test.ts
│ │ │ └── filterEngine.ts
│ │ ├── followup/
│ │ │ └── followupManager.ts
│ │ ├── globalShortcut.ts
│ │ ├── gmail/
│ │ │ ├── auth.test.ts
│ │ │ ├── auth.ts
│ │ │ ├── authParser.test.ts
│ │ │ ├── authParser.ts
│ │ │ ├── client.test.ts
│ │ │ ├── client.ts
│ │ │ ├── draftDeletion.test.ts
│ │ │ ├── draftDeletion.ts
│ │ │ ├── messageParser.test.ts
│ │ │ ├── messageParser.ts
│ │ │ ├── sendAs.test.ts
│ │ │ ├── sendAs.ts
│ │ │ ├── sync.test.ts
│ │ │ ├── sync.ts
│ │ │ ├── syncManager.test.ts
│ │ │ ├── syncManager.ts
│ │ │ └── tokenManager.ts
│ │ ├── google/
│ │ │ └── calendar.ts
│ │ ├── imap/
│ │ │ ├── autoDiscovery.test.ts
│ │ │ ├── autoDiscovery.ts
│ │ │ ├── folderMapper.test.ts
│ │ │ ├── folderMapper.ts
│ │ │ ├── imapConfigBuilder.test.ts
│ │ │ ├── imapConfigBuilder.ts
│ │ │ ├── imapSync.test.ts
│ │ │ ├── imapSync.ts
│ │ │ ├── messageHelper.test.ts
│ │ │ ├── messageHelper.ts
│ │ │ ├── tauriCommands.test.ts
│ │ │ └── tauriCommands.ts
│ │ ├── notifications/
│ │ │ └── notificationManager.ts
│ │ ├── oauth/
│ │ │ ├── oauthFlow.test.ts
│ │ │ ├── oauthFlow.ts
│ │ │ ├── oauthTokenManager.test.ts
│ │ │ ├── oauthTokenManager.ts
│ │ │ ├── providers.test.ts
│ │ │ └── providers.ts
│ │ ├── phishing/
│ │ │ └── phishingScanner.ts
│ │ ├── queue/
│ │ │ ├── queueProcessor.test.ts
│ │ │ └── queueProcessor.ts
│ │ ├── quickSteps/
│ │ │ ├── defaults.ts
│ │ │ ├── executor.test.ts
│ │ │ ├── executor.ts
│ │ │ └── types.ts
│ │ ├── search/
│ │ │ ├── searchParser.test.ts
│ │ │ ├── searchParser.ts
│ │ │ ├── searchQueryBuilder.test.ts
│ │ │ ├── searchQueryBuilder.ts
│ │ │ ├── smartFolderQuery.test.ts
│ │ │ └── smartFolderQuery.ts
│ │ ├── smartLabels/
│ │ │ ├── backfillService.test.ts
│ │ │ ├── backfillService.ts
│ │ │ ├── smartLabelManager.test.ts
│ │ │ ├── smartLabelManager.ts
│ │ │ ├── smartLabelService.test.ts
│ │ │ └── smartLabelService.ts
│ │ ├── snooze/
│ │ │ ├── scheduledSendManager.ts
│ │ │ └── snoozeManager.ts
│ │ ├── tasks/
│ │ │ ├── taskManager.test.ts
│ │ │ └── taskManager.ts
│ │ ├── threading/
│ │ │ ├── threadBuilder.test.ts
│ │ │ └── threadBuilder.ts
│ │ ├── unsubscribe/
│ │ │ └── unsubscribeManager.ts
│ │ ├── updateManager.test.ts
│ │ └── updateManager.ts
│ ├── stores/
│ │ ├── accountStore.test.ts
│ │ ├── accountStore.ts
│ │ ├── composerStore.test.ts
│ │ ├── composerStore.ts
│ │ ├── contextMenuStore.test.ts
│ │ ├── contextMenuStore.ts
│ │ ├── labelStore.test.ts
│ │ ├── labelStore.ts
│ │ ├── shortcutStore.ts
│ │ ├── smartFolderStore.test.ts
│ │ ├── smartFolderStore.ts
│ │ ├── taskStore.test.ts
│ │ ├── taskStore.ts
│ │ ├── threadStore.test.ts
│ │ ├── threadStore.ts
│ │ ├── uiStore.test.ts
│ │ └── uiStore.ts
│ ├── styles/
│ │ └── globals.css
│ ├── test/
│ │ ├── mocks/
│ │ │ ├── db.mock.ts
│ │ │ ├── entities.mock.ts
│ │ │ ├── index.ts
│ │ │ ├── services.mock.ts
│ │ │ ├── stores.mock.ts
│ │ │ └── tauri.mock.ts
│ │ └── setup.ts
│ ├── utils/
│ │ ├── crypto.test.ts
│ │ ├── crypto.ts
│ │ ├── date.ts
│ │ ├── emailBuilder.test.ts
│ │ ├── emailBuilder.ts
│ │ ├── emailUtils.test.ts
│ │ ├── emailUtils.ts
│ │ ├── fileTypeHelpers.test.ts
│ │ ├── fileTypeHelpers.ts
│ │ ├── fileUtils.test.ts
│ │ ├── fileUtils.ts
│ │ ├── imageBlocker.test.ts
│ │ ├── imageBlocker.ts
│ │ ├── imageResize.ts
│ │ ├── mailtoParser.test.ts
│ │ ├── mailtoParser.ts
│ │ ├── networkErrors.test.ts
│ │ ├── networkErrors.ts
│ │ ├── noReply.test.ts
│ │ ├── noReply.ts
│ │ ├── phishingDetector.test.ts
│ │ ├── phishingDetector.ts
│ │ ├── resolveFromAddress.test.ts
│ │ ├── resolveFromAddress.ts
│ │ ├── sanitize.test.ts
│ │ ├── sanitize.ts
│ │ ├── templateVariables.test.ts
│ │ ├── templateVariables.ts
│ │ ├── timestamp.test.ts
│ │ └── timestamp.ts
│ └── vite-env.d.ts
├── src-tauri/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── Entitlements.plist
│ ├── build.rs
│ ├── capabilities/
│ │ └── default.json
│ ├── icons/
│ │ ├── android/
│ │ │ ├── mipmap-anydpi-v26/
│ │ │ │ └── ic_launcher.xml
│ │ │ └── values/
│ │ │ └── ic_launcher_background.xml
│ │ └── icon.icns
│ ├── src/
│ │ ├── commands.rs
│ │ ├── imap/
│ │ │ ├── client.rs
│ │ │ ├── mod.rs
│ │ │ └── types.rs
│ │ ├── lib.rs
│ │ ├── main.rs
│ │ ├── oauth.rs
│ │ └── smtp/
│ │ ├── client.rs
│ │ ├── mod.rs
│ │ └── types.rs
│ └── tauri.conf.json
├── tsconfig.json
├── velo.spec
├── vite.config.ts
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @avihaymenahem
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a bug or unexpected behavior
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Description
description: What happened?
placeholder: A clear description of the bug
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: How can we reproduce this issue?
placeholder: |
1. Go to '...'
2. Click on '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What did you expect to happen?
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: App version
description: Found in Settings or the title bar
placeholder: e.g. 0.3.14
validations:
required: false
- type: dropdown
id: account-type
attributes:
label: Account type
options:
- Gmail API
- IMAP/SMTP
- Both
- N/A
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots / logs
description: If applicable, add screenshots or paste error logs
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Any other information that might help
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/docs.yml
================================================
name: Documentation Improvement
description: Suggest improvements to documentation
labels: ["documentation"]
body:
- type: textarea
id: improvement
attributes:
label: What needs improvement
description: Which docs are missing, unclear, or outdated?
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Suggested changes
description: How should the documentation be improved?
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Any other context or references
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a new feature or improvement
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Problem / motivation
description: What problem does this solve? Why do you want this?
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: Describe the solution you'd like
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any alternative solutions or features you've considered
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Any other context, mockups, or screenshots
validations:
required: false
================================================
FILE: .github/pull_request_template.md
================================================
## Summary
<!-- Brief description of what this PR does and why -->
## Changes
<!-- List the key changes -->
-
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Enhancement (improving existing feature)
- [ ] Refactor (no behavior change)
- [ ] Documentation
- [ ] CI/Build
## Testing
- [ ] Existing tests pass (`npm run test`)
- [ ] New tests added (if applicable)
- [ ] Manually tested
## Screenshots
<!-- If applicable, add screenshots -->
================================================
FILE: .github/workflows/packaging.yml
================================================
name: Build & Package
on:
workflow_call:
inputs:
tag_name:
description: "Release tag name"
type: string
required: true
permissions:
contents: write
jobs:
build-flatpak:
name: Build Flatpak
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Flatpak and Builder
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
- name: Setup Flatpak remote and install SDK
run: |
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.gnome.Platform/x86_64/46
flatpak --user install -y flathub org.gnome.Sdk/x86_64/46
flatpak --user install -y flathub org.freedesktop.Sdk.Extension.node20/x86_64/23.08
- name: Build Flatpak bundle
run: |
flatpak-builder --user --force-clean --repo=repo build-dir com.velomail.app.yml
flatpak build-bundle repo velo.flatpak com.velomail.app
- name: Upload Flatpak to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ inputs.tag_name }}" velo.flatpak --clobber
build-srpm:
name: Build SRPM
runs-on: ubuntu-latest
container: fedora:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install RPM build tools
run: dnf install -y rpm-build spectool rpmdevtools jq gh
- name: Get version
run: echo "APP_VERSION=$(jq -r .version src-tauri/tauri.conf.json)" >> $GITHUB_ENV
- name: Create source tarball
run: |
tar \
--exclude='.git' \
--exclude='.github' \
--exclude='*.flatpak' \
--exclude='*.tar.gz' \
--transform "s/^\./velo-${{ env.APP_VERSION }}/" \
-czf "/tmp/velo-${{ env.APP_VERSION }}.tar.gz" .
- name: Set up RPM build environment
run: |
rpmdev-setuptree
# Ensure spec version matches the actual release version
sed -i "s/^%global app_version.*/%global app_version ${{ env.APP_VERSION }}/" velo.spec
cp velo.spec ~/rpmbuild/SPECS/
cp "/tmp/velo-${{ env.APP_VERSION }}.tar.gz" ~/rpmbuild/SOURCES/
- name: Build Source RPM
run: rpmbuild -bs ~/rpmbuild/SPECS/velo.spec
- name: Upload SRPM to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ inputs.tag_name }}" ~/rpmbuild/SRPMS/*.src.rpm --clobber --repo ${{ github.repository }}
================================================
FILE: .github/workflows/release-please.yml
================================================
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
release_id: ${{ steps.release.outputs.id }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
build-and-release:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
uses: ./.github/workflows/release.yml
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
release_id: ${{ needs.release-please.outputs.release_id }}
secrets: inherit
packaging:
needs: [release-please, build-and-release]
if: needs.release-please.outputs.release_created == 'true'
uses: ./.github/workflows/packaging.yml
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
secrets: inherit
update-homebrew:
needs: [release-please, build-and-release]
if: needs.release-please.outputs.release_created == 'true'
uses: ./.github/workflows/update-homebrew.yml
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
secrets: inherit
================================================
FILE: .github/workflows/release.yml
================================================
name: Build & Release
on:
workflow_call:
inputs:
tag_name:
description: "Release tag (e.g. velo-v0.4.3)"
type: string
required: false
release_id:
description: "Existing GitHub release ID to upload assets to"
type: string
required: false
workflow_dispatch:
release:
types: [created]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test
build:
needs: test
strategy:
fail-fast: false
matrix:
include:
- platform: ubuntu-22.04
args: ""
- platform: windows-latest
args: ""
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev
- run: npm ci
- name: Build and upload to existing release
if: inputs.release_id
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
releaseId: ${{ inputs.release_id }}
updaterJsonKeepUniversal: true
args: ${{ matrix.args }}
- name: Build and create release
if: ${{ !inputs.release_id }}
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Velo v__VERSION__"
releaseBody: "See the assets below to download for your platform."
releaseDraft: false
prerelease: false
updaterJsonKeepUniversal: true
args: ${{ matrix.args }}
build-macos:
needs: test
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
- run: npm ci
- name: Check if Apple signing is configured
id: signing-check
run: |
if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ]; then
echo "has_signing=true" >> "$GITHUB_OUTPUT"
else
echo "has_signing=false" >> "$GITHUB_OUTPUT"
fi
- name: Import Apple signing certificate
if: steps.signing-check.outputs.has_signing == 'true'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Build and upload (signed + notarized)
if: steps.signing-check.outputs.has_signing == 'true' && inputs.release_id
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
releaseId: ${{ inputs.release_id }}
updaterJsonKeepUniversal: true
args: --target universal-apple-darwin
- name: Build and release (signed + notarized)
if: steps.signing-check.outputs.has_signing == 'true' && !inputs.release_id
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Velo v__VERSION__"
releaseBody: "See the assets below to download for your platform."
releaseDraft: false
prerelease: false
updaterJsonKeepUniversal: true
args: --target universal-apple-darwin
- name: Build and upload (unsigned)
if: steps.signing-check.outputs.has_signing != 'true' && inputs.release_id
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
releaseId: ${{ inputs.release_id }}
updaterJsonKeepUniversal: true
args: --target universal-apple-darwin
- name: Build and release (unsigned)
if: steps.signing-check.outputs.has_signing != 'true' && !inputs.release_id
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Velo v__VERSION__"
releaseBody: |
See the assets below to download for your platform.
> **macOS users**: This build is unsigned. After downloading, run:
> ```
> xattr -cr /Applications/Velo.app
> ```
> Or right-click the app and select "Open" on first launch.
releaseDraft: false
prerelease: false
updaterJsonKeepUniversal: true
args: --target universal-apple-darwin
- name: Clean up keychain
if: always() && steps.signing-check.outputs.has_signing == 'true'
run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
================================================
FILE: .github/workflows/update-homebrew.yml
================================================
name: Update Homebrew Tap
on:
workflow_call:
inputs:
tag_name:
description: "Release tag name (e.g. v0.5.0)"
type: string
required: true
workflow_dispatch:
inputs:
version:
description: "Version to sync (e.g. 0.3.14). Defaults to latest release."
required: false
jobs:
update-homebrew:
runs-on: macos-latest
steps:
- name: Get version
id: version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ -n "${{ inputs.tag_name }}" ]; then
# Called from release-please workflow — tag_name is the full tag (e.g. velo-v0.4.12)
TAG="${{ inputs.tag_name }}"
elif [ -n "${{ inputs.version }}" ]; then
# Manual dispatch with a version — look up the actual tag from releases
TAG=$(gh release list --repo ${{ github.repository }} --json tagName -q "[.[] | select(.tagName | test(\"${{ inputs.version }}\"))][0].tagName")
if [ -z "$TAG" ]; then
TAG="velo-v${{ inputs.version }}"
fi
else
# Manual dispatch without version — use latest release
TAG=$(gh release view --repo ${{ github.repository }} --json tagName -q '.tagName')
fi
BARE_VERSION="${TAG##*v}"
echo "version=${BARE_VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Using version: ${BARE_VERSION} (tag: ${TAG})"
- name: Wait for DMG and compute SHA256
id: sha
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="${{ steps.version.outputs.tag }}"
DMG_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/Velo_${VERSION}_universal.dmg"
echo "Downloading DMG from: ${DMG_URL}"
for i in 1 2 3 4 5; do
HTTP_CODE=$(curl -sL -o Velo.dmg -w "%{http_code}" "$DMG_URL")
if [ "$HTTP_CODE" = "200" ]; then
echo "Download succeeded (attempt $i)"
break
fi
echo "Attempt $i failed (HTTP $HTTP_CODE), retrying in 60s..."
rm -f Velo.dmg
if [ "$i" = "5" ]; then
echo "::error::DMG not available after 5 attempts"
exit 1
fi
sleep 60
done
SHA256=$(shasum -a 256 Velo.dmg | awk '{print $1}')
echo "SHA256: ${SHA256}"
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
- name: Checkout tap repository
uses: actions/checkout@v4
with:
repository: avihaymenahem/homebrew-velo
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-velo
- name: Update cask definition
run: |
VERSION="${{ steps.version.outputs.version }}"
SHA256="${{ steps.sha.outputs.sha256 }}"
cat > homebrew-velo/Casks/velo.rb << CASK_EOF
cask "velo" do
version "${VERSION}"
sha256 "${SHA256}"
url "https://github.com/avihaymenahem/velo/releases/download/velo-v#{version}/Velo_#{version}_universal.dmg",
verified: "github.com/avihaymenahem/velo/"
name "Velo"
desc "Fast, beautiful desktop email client"
homepage "https://github.com/avihaymenahem/velo"
livecheck do
url :url
strategy :github_latest
end
app "Velo.app"
caveats <<~EOS
If the app is not notarized, macOS may block it on first launch.
To allow it, right-click Velo.app and select "Open", or run:
xattr -cr /Applications/Velo.app
EOS
zap trash: [
"~/Library/Application Support/com.velomail.app",
"~/Library/Caches/com.velomail.app",
"~/Library/Preferences/com.velomail.app.plist",
"~/Library/Saved Application State/com.velomail.app.savedState",
"~/Library/WebKit/com.velomail.app",
]
end
CASK_EOF
- name: Commit and push to tap
working-directory: homebrew-velo
run: |
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/velo.rb
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Update velo to ${VERSION}"
git push
================================================
FILE: .gitignore
================================================
# Dependencies
node_modules/
# Build outputs
dist/
build/
test-buildroot/
.flatpak-builder/
# Tauri
src-tauri/target/
src-tauri/gen/schemas
# Environment variables & secrets
.env
.env.*
!.env.example
# Database files
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini
nul
# IDE / Editor
.vscode/
.idea/
*.swp
*.swo
*.swn
*~
.project
.classpath
.settings/
# Claude Code — ignore local settings but track shared skills
.claude/*
!.claude/skills/
# Debug files
debug-*.mjs
# Tauri signing keys
*.key
*.pem
*.p12
*.pfx
*.cert
*.jks
# Coverage
coverage/
# Temporary files
*.tmp
*.temp
.cache/
# Packaging Artifacts
build-dir/
repo/
velo-*.tar.gz
*.flatpak
*.rpm
*.src.rpm
================================================
FILE: .release-please-manifest.json
================================================
{
".": "0.4.21"
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [0.4.21](https://github.com/avihaymenahem/velo/compare/velo-v0.4.20...velo-v0.4.21) (2026-02-27)
### Bug Fixes
* improve IMAP sync error handling and reliability ([29ce210](https://github.com/avihaymenahem/velo/commit/29ce210b78c1dccaf0cdef02f1342dcd14f0aedf))
## [0.4.20](https://github.com/avihaymenahem/velo/compare/velo-v0.4.19...velo-v0.4.20) (2026-02-26)
### Bug Fixes
* add Escape key to close inline reply editor ([386b403](https://github.com/avihaymenahem/velo/commit/386b40303e5dece542eb2617e485e352cc3f5c07))
* resolve SQLite transaction errors during IMAP initial sync ([6044f42](https://github.com/avihaymenahem/velo/commit/6044f429581f6c2142cc536f1eb6299347bfdbeb)), closes [#192](https://github.com/avihaymenahem/velo/issues/192)
## [0.4.19](https://github.com/avihaymenahem/velo/compare/velo-v0.4.18...velo-v0.4.19) (2026-02-25)
### Features
* chunked IMAP sync with lightweight UID search and batched transactions ([7440215](https://github.com/avihaymenahem/velo/commit/7440215fe1bf923afc666486ec2c999ed1e5c266))
### Bug Fixes
* allow optional space after colon in search operators ([d1e9495](https://github.com/avihaymenahem/velo/commit/d1e9495ec5efa247406941d0b5ebfec55d699927))
## [0.4.18](https://github.com/avihaymenahem/velo/compare/velo-v0.4.17...velo-v0.4.18) (2026-02-24)
### Features
* auto-advance to next thread after removal actions ([520ea01](https://github.com/avihaymenahem/velo/commit/520ea01ab78bbd7a8cc8fa019246fe4a7d181034))
### Bug Fixes
* use background-image instead of background shorthand in dark mode ([9107b50](https://github.com/avihaymenahem/velo/commit/9107b5081c37082469decc47b178fcd7c15540fb)), closes [#168](https://github.com/avihaymenahem/velo/issues/168)
## [0.4.17](https://github.com/avihaymenahem/velo/compare/velo-v0.4.16...velo-v0.4.17) (2026-02-23)
### Features
* add GitHub Copilot (GitHub Models) as 5th AI provider ([9b8e162](https://github.com/avihaymenahem/velo/commit/9b8e1628d9cd784bb3e1a5d3a310724e198ce1cd))
* add Move to Folder/Label shortcut (V key) ([751aeaa](https://github.com/avihaymenahem/velo/commit/751aeaa4b98002ebdc99156ce76a256786ccf042))
### Bug Fixes
* use server-side IMAP SINCE date filter to prevent sync timeouts on large folders ([99d9301](https://github.com/avihaymenahem/velo/commit/99d9301f836b24b2917b1aae05980073a86f4f3d)), closes [#147](https://github.com/avihaymenahem/velo/issues/147)
## [0.4.16](https://github.com/avihaymenahem/velo/compare/velo-v0.4.15...velo-v0.4.16) (2026-02-22)
### Features
* add model selection dropdowns for AI providers ([#158](https://github.com/avihaymenahem/velo/issues/158)) ([74244ca](https://github.com/avihaymenahem/velo/commit/74244caf5c0072272abad7c3e7481eb1674eb2ef))
### Bug Fixes
* add reduce motion setting to prevent animated background strobe on some Windows GPUs ([981f2b5](https://github.com/avihaymenahem/velo/commit/981f2b51aabf95e7335f08ef8ce7c0f4ec9b0ca7)), closes [#156](https://github.com/avihaymenahem/velo/issues/156)
* reduce IMAP sync connection storm with single-connection folder sync ([6b90b7a](https://github.com/avihaymenahem/velo/commit/6b90b7a1bfa0a2a048de6b0746acbf01511eb9cb)), closes [#147](https://github.com/avihaymenahem/velo/issues/147)
## [0.4.15](https://github.com/avihaymenahem/velo/compare/velo-v0.4.14...velo-v0.4.15) (2026-02-21)
### Bug Fixes
* smart folder unread count SQL error and sync progress visibility ([7c2eb4e](https://github.com/avihaymenahem/velo/commit/7c2eb4edb6fa2d14f847d194e86fe48d3ee94ee0))
## [0.4.14](https://github.com/avihaymenahem/velo/compare/velo-v0.4.13...velo-v0.4.14) (2026-02-21)
### Features
* accept self-signed certificates for IMAP/SMTP ([#148](https://github.com/avihaymenahem/velo/issues/148)) ([a5f7cec](https://github.com/avihaymenahem/velo/commit/a5f7cec2d8a4bd2701acd96a36fd62c8ac00c93a))
### Bug Fixes
* add --repo flag to gh release upload in SRPM job ([5b863c0](https://github.com/avihaymenahem/velo/commit/5b863c0048a49635b560d921dacbc04ef96b6a15))
* add TCP timeouts and keepalive to IMAP client ([#147](https://github.com/avihaymenahem/velo/issues/147)) ([a77b474](https://github.com/avihaymenahem/velo/commit/a77b474bcc3f59abf49e5c67665cffdb7459058d))
* resolve local AI (Ollama/LMStudio) connection failures ([adfc09f](https://github.com/avihaymenahem/velo/commit/adfc09f6900ab40c11b73767a24fad07d97547c2)), closes [#145](https://github.com/avihaymenahem/velo/issues/145)
## [0.4.13](https://github.com/avihaymenahem/velo/compare/velo-v0.4.12...velo-v0.4.13) (2026-02-21)
### Bug Fixes
* align release pipeline version sync for SRPM and Homebrew ([ebf21ff](https://github.com/avihaymenahem/velo/commit/ebf21ffe3f22bbbaeeb9d8e598df876f23c8c34f))
## [0.4.12](https://github.com/avihaymenahem/velo/compare/velo-v0.4.11...velo-v0.4.12) (2026-02-21)
### Features
* consolidate release pipeline — packaging and homebrew on release only ([7e4ac8c](https://github.com/avihaymenahem/velo/commit/7e4ac8cc40da62c8d23716b4f5c21fea27e263c3))
* pass releaseId from release-please to tauri-action ([9587dfd](https://github.com/avihaymenahem/velo/commit/9587dfdd1eae8d2b3364c93ddb07533087246cd9))
### Bug Fixes
* move release-please annotation to own line in RPM spec ([134746f](https://github.com/avihaymenahem/velo/commit/134746f1c5c5d209d609bec9c8376fe688f6d0d0))
* update velo.spec version to 0.4.11 and fix release-please annotation ([d1d08b2](https://github.com/avihaymenahem/velo/commit/d1d08b2ee6951c71fb6ae7d8bcfceadff465e827))
## [0.4.11](https://github.com/avihaymenahem/velo/compare/velo-v0.4.10...velo-v0.4.11) (2026-02-21)
### Features
* add Flatpak and RPM packaging for Linux distribution ([95c1e29](https://github.com/avihaymenahem/velo/commit/95c1e2954a465982c3feec8d90bbe1aee8fb8c86))
* parallelize Gmail sync and add 429 rate limit retry ([ff3580b](https://github.com/avihaymenahem/velo/commit/ff3580b29807c844a81cb79586168700c84c1dc3))
### Bug Fixes
* align test files — remove stale mocks, add cleanup, fix brittle assertions ([4acf9e3](https://github.com/avihaymenahem/velo/commit/4acf9e3343e377a989f80bc26bd650f988e5bf47))
* use Tauri native fetch for local AI to bypass CORS ([6e84ab2](https://github.com/avihaymenahem/velo/commit/6e84ab2884c261db0ed0a4fec6d223295355a7dc)), closes [#127](https://github.com/avihaymenahem/velo/issues/127)
## [0.4.10](https://github.com/avihaymenahem/velo/compare/velo-v0.4.9...velo-v0.4.10) (2026-02-20)
### Features
* add AI smart labels for automatic email labeling ([986a7ae](https://github.com/avihaymenahem/velo/commit/986a7aef3f13171f0a0cebd8f523aa67a7cb34f5))
* add attachment library, keyboard shortcut, and update docs ([b69f042](https://github.com/avihaymenahem/velo/commit/b69f042e74b42ba4680ee60730959e7de08e6dc7))
* add sidebar nav item reordering and visibility customization ([3f96837](https://github.com/avihaymenahem/velo/commit/3f96837dfeaf65647889633d297766b6e5be079c))
### Bug Fixes
* resolve context menu bugs on attachment preview and submenu opening ([f1d26b9](https://github.com/avihaymenahem/velo/commit/f1d26b97410a596f8562e175470dddf9eafba433))
## [0.4.9](https://github.com/avihaymenahem/velo/compare/velo-v0.4.8...velo-v0.4.9) (2026-02-20)
### Bug Fixes
* resolve IMAP attachment fetching and display ([2c40b51](https://github.com/avihaymenahem/velo/commit/2c40b51d87a7c83de6204c170ab057bc11efc08e)), closes [#124](https://github.com/avihaymenahem/velo/issues/124)
## [0.4.8](https://github.com/avihaymenahem/velo/compare/velo-v0.4.7...velo-v0.4.8) (2026-02-20)
### Bug Fixes
* save IMAP/SMTP sent messages to local DB and Sent folder ([3133ee9](https://github.com/avihaymenahem/velo/commit/3133ee9b24324cd2e6e2098a8e66ad48d6cccbe0)), closes [#121](https://github.com/avihaymenahem/velo/issues/121)
## [0.4.7](https://github.com/avihaymenahem/velo/compare/velo-v0.4.6...velo-v0.4.7) (2026-02-20)
### Features
* add local AI support via Ollama and LMStudio ([1cee002](https://github.com/avihaymenahem/velo/commit/1cee00291df37c46ba2d46a95346152a6ac7dc1f)), closes [#98](https://github.com/avihaymenahem/velo/issues/98)
## [0.4.6](https://github.com/avihaymenahem/velo/compare/velo-v0.4.5...velo-v0.4.6) (2026-02-20)
### Features
* add CalDAV calendar integration for IMAP and standalone accounts ([08e05ff](https://github.com/avihaymenahem/velo/commit/08e05ff571652c73cce6261a3c5f875a6a013e9a)), closes [#113](https://github.com/avihaymenahem/velo/issues/113)
## [0.4.5](https://github.com/avihaymenahem/velo/compare/velo-v0.4.4...velo-v0.4.5) (2026-02-20)
### Bug Fixes
* attachments not showing in attachment list ([fdf8c75](https://github.com/avihaymenahem/velo/commit/fdf8c75ed5d42e29fdd90e96c88b2b33a90d48b4))
## [0.4.4](https://github.com/avihaymenahem/velo/compare/velo-v0.4.3...velo-v0.4.4) (2026-02-19)
### Bug Fixes
* **ci:** fix version parsing in standalone update-homebrew workflow ([41b3390](https://github.com/avihaymenahem/velo/commit/41b3390652b6f2055c7cb523a2153d6d4359b069))
* **ci:** remove invalid makeLatest input and fix Homebrew update skip ([236e81b](https://github.com/avihaymenahem/velo/commit/236e81ba33b95a134bd7852840809039c24561c0))
## [0.4.3](https://github.com/avihaymenahem/velo/compare/velo-v0.4.2...velo-v0.4.3) (2026-02-19)
### Features
* **sync:** add per-folder sync via F5 shortcut and sidebar context menu ([d11c642](https://github.com/avihaymenahem/velo/commit/d11c642013ed538aaad67f56158e6d9ba37695e9)), closes [#101](https://github.com/avihaymenahem/velo/issues/101)
### Bug Fixes
* **ci:** auto-sync Homebrew tap when workflow files change ([2958a35](https://github.com/avihaymenahem/velo/commit/2958a35a2ac01c29bdf5f3e3ec9c359a5bf131dd))
* **ci:** fix Homebrew cask 404 and deprecation warning ([b39d402](https://github.com/avihaymenahem/velo/commit/b39d402bd36f3415c25ecb160dc4c5ec92d67195))
* **ci:** verify DMG exists before updating Homebrew cask ([2cdc3d2](https://github.com/avihaymenahem/velo/commit/2cdc3d2fd3e54f5c5dcb99d1c8fe92fe59305861))
* **sync:** clear sync spinner on velo-sync-done event instead of promise ([a502f04](https://github.com/avihaymenahem/velo/commit/a502f040969f8dc4ba29ecacc057aec26c184e6f))
## [0.4.2](https://github.com/avihaymenahem/velo/compare/velo-v0.4.1...velo-v0.4.2) (2026-02-19)
### Features
* **signatures:** add HTML source editor toggle and sanitize signature output ([e1ca851](https://github.com/avihaymenahem/velo/commit/e1ca8512dc5f54278d64cda0f1fc8721f97a525d)), closes [#99](https://github.com/avihaymenahem/velo/issues/99)
### Bug Fixes
* **attachments:** use EmailProvider for IMAP attachment preview and download ([228ca5e](https://github.com/avihaymenahem/velo/commit/228ca5e86be56e080c3a109acbdd07e63c63bdd4)), closes [#100](https://github.com/avihaymenahem/velo/issues/100)
* **settings:** use Tauri OS plugin for reliable platform detection ([07b6890](https://github.com/avihaymenahem/velo/commit/07b6890f9a7daeba666414ccf7b66c2e626902a2))
## [0.4.1](https://github.com/avihaymenahem/velo/compare/velo-v0.4.0...velo-v0.4.1) (2026-02-18)
### Features
* **nav:** add arrow key navigation between messages in thread view ([efd213d](https://github.com/avihaymenahem/velo/commit/efd213d2f0420852be2432e7ef09a1c12231f110))
* **nav:** add arrow key navigation in email list and thread view ([e87c712](https://github.com/avihaymenahem/velo/commit/e87c712a284cee6918f21042764ca90119e8cbb1))
* **nav:** add arrow key navigation in email list with auto-scroll ([9f4b0d8](https://github.com/avihaymenahem/velo/commit/9f4b0d826100492dc781bab6c48b4e0e5ba191af))
### Bug Fixes
* **popout:** set active account in thread pop-out window ([ae60695](https://github.com/avihaymenahem/velo/commit/ae606950a8c1692a5c935d4ea60d384d1093e7e0))
* **test:** update HelpPage test for 14 categories (added tasks) ([ca97b65](https://github.com/avihaymenahem/velo/commit/ca97b656290781f1d81d944e57445a6f1158f287))
* **ui:** replace loading text with skeleton animation and fix platform detection ([02eda9f](https://github.com/avihaymenahem/velo/commit/02eda9fd35f7272222aa4c5e9f28661230bc754b))
## [0.4.0](https://github.com/avihaymenahem/velo/compare/velo-v0.3.19...velo-v0.4.0) (2026-02-18)
### ⚠ BREAKING CHANGES
* Migration 18 adds 3 new database tables (writing_style_profiles, tasks, task_tags) and 2 new default settings. The migration runner now wraps each migration in a transaction. The taskStore is the 9th Zustand store and is initialized on app startup. These changes require a fresh app restart to run the new migration.
### Features
* add AI auto-draft replies with writing style learning and full task manager ([c75dfc5](https://github.com/avihaymenahem/velo/commit/c75dfc5b3cf7b08abc9c8a9c15018dc480413516))
* **ui:** highlight spam threads with dimmed red background ([5766ecb](https://github.com/avihaymenahem/velo/commit/5766ecbc72ea5e121c486d2f21fd7a40a3cd2179))
### Bug Fixes
* create placeholder thread before message insert during IMAP sync ([6c2d013](https://github.com/avihaymenahem/velo/commit/6c2d0135a6b3683dfbce4075a032b9df12ed699a)), closes [#89](https://github.com/avihaymenahem/velo/issues/89)
## [0.3.19](https://github.com/avihaymenahem/velo/compare/velo-v0.3.18...velo-v0.3.19) (2026-02-18)
### Features
* add auto-update via Tauri updater plugin ([7ac2362](https://github.com/avihaymenahem/velo/commit/7ac2362c3ef1c9e9f628fd2232cd16f8ccfc194b))
## [0.3.18](https://github.com/avihaymenahem/velo/compare/velo-v0.3.17...velo-v0.3.18) (2026-02-18)
### Bug Fixes
* resolve nested button warning and 204 response parsing ([e44f063](https://github.com/avihaymenahem/velo/commit/e44f063927b179444711771e87923343b6599a26))
### Performance Improvements
* memoize calendar event buckets, filter descriptions, and contact search ([3eb6042](https://github.com/avihaymenahem/velo/commit/3eb60425bcff8e60a9fc34e23e2abe6f77fdce09))
* optimize rendering, store subscriptions, and DB queries ([0fd4d8c](https://github.com/avihaymenahem/velo/commit/0fd4d8c784a326f30f334cbf4ace46cd7347677e))
* pre-parse filter JSON and lazy load route components ([33440b7](https://github.com/avihaymenahem/velo/commit/33440b7ed272ac04adbb3186f5d81f77f1e45dec))
## [0.3.17](https://github.com/avihaymenahem/velo/compare/velo-v0.3.16...velo-v0.3.17) (2026-02-18)
### Bug Fixes
* guard against undefined payload in parseIdToken ([120b0d7](https://github.com/avihaymenahem/velo/commit/120b0d7668791773a976b192c45c5e20bedfbcba))
* handle missing router context in pop-out thread windows ([b484d86](https://github.com/avihaymenahem/velo/commit/b484d86e7b68b7950c432b9d077b5258ed8fdb15))
## [0.3.16](https://github.com/avihaymenahem/velo/compare/velo-v0.3.15...velo-v0.3.16) (2026-02-18)
### Features
* add Microsoft OAuth2 support for Outlook/Hotmail/Live accounts ([019a5e2](https://github.com/avihaymenahem/velo/commit/019a5e241dc558d6eb384efc5b6e9880643d7383))
## [0.3.15](https://github.com/avihaymenahem/velo/compare/velo-v0.3.14...velo-v0.3.15) (2026-02-18)
### Features
* add standalone workflow to manually sync homebrew tap ([5a33e67](https://github.com/avihaymenahem/velo/commit/5a33e6707175bfa13a443c2e2489e6f40996ee7b))
### Bug Fixes
* allow homebrew tap update on workflow_dispatch triggers ([c31ddc8](https://github.com/avihaymenahem/velo/commit/c31ddc86c022005d1aa02ea9f6e828a39e2bff46))
* only show sync status bar for initial syncs, not delta syncs ([b925610](https://github.com/avihaymenahem/velo/commit/b9256103b9f9f07bb2573f4e539607cbab024e96))
* prevent IMAP sync OOM on large mailboxes and surface sync errors ([61ebc6e](https://github.com/avihaymenahem/velo/commit/61ebc6ef7b1993c2a15f8c0c022657b275fa62c2)), closes [#74](https://github.com/avihaymenahem/velo/issues/74) [#76](https://github.com/avihaymenahem/velo/issues/76)
## [0.3.14](https://github.com/avihaymenahem/velo/compare/velo-v0.3.13...velo-v0.3.14) (2026-02-17)
### Features
* prioritize new account sync to eliminate 20-30s delay ([49bce0f](https://github.com/avihaymenahem/velo/commit/49bce0fc8227d75923642cef26700c13504ee046))
## [0.3.13](https://github.com/avihaymenahem/velo/compare/velo-v0.3.12...velo-v0.3.13) (2026-02-17)
### Features
* add About page to settings ([fa03431](https://github.com/avihaymenahem/velo/commit/fa03431f091a3f84d78eab1122267c35fdd8c722))
* add Homebrew tap auto-update to release workflow ([4a817b0](https://github.com/avihaymenahem/velo/commit/4a817b0dba3bba3b8d4650e3c1a3b57a9f0a72f0))
* add View Source option to message context menu ([c657b0f](https://github.com/avihaymenahem/velo/commit/c657b0f798d70bda0436acbd0ea435afd3f84b63))
* optimize IMAP delta sync with single-connection batch check ([0a62b73](https://github.com/avihaymenahem/velo/commit/0a62b7363c6c7d34592781a711eb8695b8e5ed52))
## [0.3.12](https://github.com/avihaymenahem/velo/compare/velo-v0.3.11...velo-v0.3.12) (2026-02-16)
### Bug Fixes
* starred threads not appearing in Starred folder ([a03db9f](https://github.com/avihaymenahem/velo/commit/a03db9f4877988d7d979980f750ff5daf63bc052))
## [0.3.11](https://github.com/avihaymenahem/velo/compare/velo-v0.3.10...velo-v0.3.11) (2026-02-16)
### Bug Fixes
* IMAP emails not displaying in UI after sync ([18521cf](https://github.com/avihaymenahem/velo/commit/18521cf2cbcb87f75cab25cff21dba9876fb0e31))
* IMAP fetch fallback for servers incompatible with async-imap ([fcc7a45](https://github.com/avihaymenahem/velo/commit/fcc7a45f52e2fe04595d40c0c34926adca5678b4))
* IMAP trash not working for servers with non-standard folder names ([b6cf2c6](https://github.com/avihaymenahem/velo/commit/b6cf2c6d3aae86fa261fd3b20d938ff8c16f36a9))
## [0.3.10](https://github.com/avihaymenahem/velo/compare/velo-v0.3.9...velo-v0.3.10) (2026-02-16)
### Bug Fixes
* IMAP messages downloaded but not stored in database ([1c28a8e](https://github.com/avihaymenahem/velo/commit/1c28a8e7c3e55dfdd3197ba2011e7b82025767f5)), closes [#39](https://github.com/avihaymenahem/velo/issues/39)
## [0.3.9](https://github.com/avihaymenahem/velo/compare/velo-v0.3.8...velo-v0.3.9) (2026-02-16)
### Bug Fixes
* decode IMAP folder names from modified UTF-7 and use real UIDs for sync ([19a919e](https://github.com/avihaymenahem/velo/commit/19a919eece270efaa0751e8d74b42dca6e6f4f54))
## [0.3.8](https://github.com/avihaymenahem/velo/compare/velo-v0.3.7...velo-v0.3.8) (2026-02-16)
### Bug Fixes
* add appdata read/write permissions for Tauri FS baseDir operations ([f9750de](https://github.com/avihaymenahem/velo/commit/f9750de942535e3c245fcfd86b034446bfb37233))
## [0.3.7](https://github.com/avihaymenahem/velo/compare/velo-v0.3.6...velo-v0.3.7) (2026-02-16)
### Bug Fixes
* use baseDir option for Tauri FS operations to resolve scope errors ([7b463dc](https://github.com/avihaymenahem/velo/commit/7b463dcba326e45c59ac5d2d47b967d05591384a))
## [0.3.6](https://github.com/avihaymenahem/velo/compare/velo-v0.3.5...velo-v0.3.6) (2026-02-16)
### Bug Fixes
* resolve nested button warnings, TipTap duplicate extensions, FS scope, and CI type errors ([65c0028](https://github.com/avihaymenahem/velo/commit/65c0028e03315fc7150a1882ed0775344ec345fd))
## [0.3.5](https://github.com/avihaymenahem/velo/compare/velo-v0.3.4...velo-v0.3.5) (2026-02-16)
### Bug Fixes
* add missing path separator in attachment cache directory ([de4355b](https://github.com/avihaymenahem/velo/commit/de4355b799abf316cb4ee729d22c6f03138174f2))
* call sep() as function, not use as string ([b65888b](https://github.com/avihaymenahem/velo/commit/b65888b70578c767a330ec13087c38f66880bda5))
* use join() for paths and hash long attachment IDs for filenames ([d01dd79](https://github.com/avihaymenahem/velo/commit/d01dd794dbe02ef0820bc293e7af39bc37deaa45))
## [0.3.4](https://github.com/avihaymenahem/velo/compare/velo-v0.3.3...velo-v0.3.4) (2026-02-16)
### Bug Fixes
* suppress notifications for muted threads in deltaSync ([4d21334](https://github.com/avihaymenahem/velo/commit/4d21334efc8d2e6d078173fad28c76f1bd1fcc46))
* wire phishing sensitivity setting and improve brand impersonation detection ([e063c9d](https://github.com/avihaymenahem/velo/commit/e063c9df676dea3757357bebc092e48cbc181513))
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Development — starts Tauri app with Vite dev server (port 1420)
npm run tauri dev
# Build production app
npm run tauri build
# Vite dev server only (no Tauri)
npm run dev
# Run all tests (single run)
npm run test
# Run tests in watch mode
npm run test:watch
# Run a single test file
npx vitest run src/stores/uiStore.test.ts
# Type-check only (no emit)
npx tsc --noEmit
# Rust backend only (from src-tauri/)
cargo build
cargo test
```
## Architecture
Tauri v2 desktop app: Rust backend + React 19 frontend communicating via Tauri IPC.
### Three-layer data flow
1. **Rust backend** (`src-tauri/`): System tray, minimize-to-tray (hide on close), splash screen, OAuth localhost server (port 17248, PKCE), single-instance enforcement, autostart support, IMAP/SMTP client modules. Tauri commands: `start_oauth_server`, `close_splashscreen`, `set_tray_tooltip`, `open_devtools`, plus 11 IMAP commands (`imap_test_connection`, `imap_list_folders`, `imap_fetch_messages`, `imap_fetch_new_uids`, `imap_fetch_message_body`, `imap_set_flags`, `imap_move_messages`, `imap_delete_messages`, `imap_get_folder_status`, `imap_fetch_attachment`, `imap_append_message`) and 2 SMTP commands (`smtp_send_email`, `smtp_test_connection`). Rust IMAP uses `async-imap` + `mail-parser`, SMTP uses `lettre`. Plugins: sql (SQLite), notification, opener, log, dialog, fs, http, single-instance, autostart, deep-link (`mailto:` scheme), global-shortcut. Windows-specific: sets AUMID for proper notification identity.
2. **Service layer** (`src/services/`): All business logic. Plain async functions (not classes, except `GmailClient`).
- `db/` — SQLite queries via `getDb()` singleton from `connection.ts`. Version-tracked migrations in `migrations.ts`. FTS5 full-text search on messages (trigram tokenizer). 32 service files covering accounts, messages, threads, labels, contacts, filters, templates, signatures, attachments, scheduled emails, image allowlist, search, settings, AI cache, bundle rules, calendar events, follow-up reminders, notification VIPs, thread categories, send-as aliases, smart folders, quick steps, link scan results, phishing allowlist, folder sync state, and smart label rules.
- `email/` — `EmailProvider` abstraction unifying Gmail API and IMAP/SMTP behind a single interface. `providerFactory.ts` returns appropriate provider based on `account.provider` field ("gmail_api" or "imap"). `gmailProvider.ts` wraps existing GmailClient. `imapSmtpProvider.ts` delegates to Rust IMAP/SMTP Tauri commands.
- `gmail/` — `GmailClient` class auto-refreshes tokens 5min before expiry, retries on 401. `tokenManager.ts` caches clients per account in a Map. `syncManager.ts` orchestrates sync (60s interval) for both Gmail and IMAP accounts via the EmailProvider abstraction. `sync.ts` does initial sync (365 days, configurable via `sync_period_days` setting) and delta sync via Gmail History API; falls back to full sync if history expired (~30 days). `authParser.ts` parses SPF/DKIM/DMARC from `Authentication-Results` headers. `sendAs.ts` fetches send-as aliases from Gmail API.
- `imap/` — IMAP-specific services. `tauriCommands.ts` wraps Rust IMAP Tauri commands. `imapSync.ts` orchestrates IMAP initial sync (batch fetch, 50 messages/batch) and delta sync via UIDVALIDITY/last_uid tracking. `folderMapper.ts` maps IMAP folders (special-use flags + well-known names) to Gmail-style labels. `autoDiscovery.ts` provides pre-configured server settings for 7 major providers (Outlook, Yahoo, iCloud, AOL, Zoho, FastMail, GMX). `imapConfigBuilder.ts` builds IMAP/SMTP configs from account records. `messageHelper.ts` handles IMAP message utilities.
- `threading/` — JWZ threading algorithm (`threadBuilder.ts`) for grouping IMAP messages into conversation threads using Message-ID, References, and In-Reply-To headers. Supports incremental threading, phantom containers for missing references, and subject-based merging.
- `ai/` — `aiService.ts` provides thread summaries, smart replies, AI compose, text transform, auto-categorization, smart label classification, and task extraction. `providerManager.ts` manages three providers (`providers/claudeProvider.ts`, `providers/openaiProvider.ts`, `providers/geminiProvider.ts`). `askInbox.ts` enables natural language inbox queries. `categorizationManager.ts` auto-sorts threads into Primary/Updates/Promotions/Social/Newsletters. `writingStyleService.ts` analyzes user writing style from sent emails and generates auto-draft replies. `taskExtraction.ts` extracts tasks from email threads via AI. `errors.ts` and `types.ts` define shared AI types. Results cached locally via `db/aiCache.ts`.
- `google/` — `calendar.ts` handles Google Calendar API (list calendars, fetch events, create events, token refresh).
- `composer/` — `draftAutoSave.ts` auto-saves drafts every 3 seconds (debounced). Watches composer state changes via Zustand subscribe.
- `search/` — `searchParser.ts` parses Gmail-style operators (`from:`, `to:`, `subject:`, `has:attachment`, `is:unread/read/starred`, `before:`, `after:`, `label:`). `searchQueryBuilder.ts` builds SQL queries from parsed operators.
- `filters/` — `filterEngine.ts` auto-applies filters to incoming messages during sync. Criteria use AND logic (case-insensitive substring matching). Actions: applyLabel, archive, trash, star, markRead.
- `categorization/` — `ruleEngine.ts` applies rule-based categorization (pattern matching on sender/subject) before falling back to AI.
- `snooze/` — Background interval checkers for snooze unsnooze and scheduled sends.
- `followup/` — `followupManager.ts` checks for follow-up reminders (threads with no reply after user-set delay).
- `bundles/` — `bundleManager.ts` manages newsletter bundling with delivery schedules.
- `notifications/` — `notificationManager.ts` provides OS notifications via tauri-plugin-notification with VIP sender filtering.
- `contacts/` — `gravatar.ts` fetches Gravatar profile images for contacts.
- `attachments/` — `cacheManager.ts` handles local attachment caching with size limits. `preCacheManager.ts` background pre-caches recent small attachments (<5MB, 7 days) every 15 minutes.
- `unsubscribe/` — `unsubscribeManager.ts` handles one-click unsubscribe (RFC 8058 List-Unsubscribe-Post and mailto: fallback).
- `quickSteps/` — Custom action chain executor with 18 action types. `executor.ts` runs action sequences on threads. `defaults.ts` provides preset templates. `types.ts` defines action chain schema.
- `queue/` — `queueProcessor.ts` processes offline operation queue every 30s. Compacts redundant ops, retries with exponential backoff (60s→300s→900s→3600s), marks permanently failed ops.
- `tasks/` — `taskManager.ts` handles recurring task logic: `parseRecurrenceRule`, `calculateNextOccurrence` (daily/weekly/monthly/yearly), `handleRecurringTaskCompletion` (completes current, creates next).
- `smartLabels/` — AI-powered auto-labeling. `smartLabelService.ts` two-phase matching (criteria fast path + AI classification). `smartLabelManager.ts` sync integration orchestrator. `backfillService.ts` batch-applies to existing inbox emails.
- Root-level services: `emailActions.ts` (centralized offline-aware email action service — optimistic UI, local DB updates, offline queueing), `badgeManager.ts` (taskbar badge count), `deepLinkHandler.ts` (`mailto:` protocol handling), `globalShortcut.ts` (system-wide compose shortcut).
3. **UI layer** (`src/components/`, `src/stores/`): Nine Zustand stores (`uiStore`, `accountStore`, `threadStore`, `composerStore`, `labelStore`, `contextMenuStore`, `shortcutStore`, `smartFolderStore`, `taskStore`) — simple synchronous state, no middleware. Components subscribe directly via hooks.
### Component organization
14 groups, ~94 component files:
- `layout/` — Sidebar, EmailList, ReadingPane, TitleBar
- `email/` — ThreadView, ThreadCard, MessageItem, EmailRenderer, ActionBar, AttachmentList, SnoozeDialog, ContactSidebar, FollowUpDialog, InlineAttachmentPreview, InlineReply, SmartReplySuggestions, ThreadSummary, AuthBadge, AuthWarningBanner, PhishingBanner, LinkConfirmDialog, CategoryTabs, MoveToFolderDialog
- `composer/` — Composer (TipTap v3 rich text editor), AddressInput, EditorToolbar, AttachmentPicker, ScheduleSendDialog, SignatureSelector, TemplatePicker, UndoSendToast, AiAssistPanel, FromSelector
- `search/` — CommandPalette, SearchBar, ShortcutsHelp, AskInbox
- `settings/` — SettingsPage, FilterEditor, LabelEditor, SignatureEditor, TemplateEditor, ContactEditor, SubscriptionManager, QuickStepEditor, SmartFolderEditor
- `accounts/` — AddAccount, AddImapAccount, AccountSwitcher, SetupClientId
- `calendar/` — CalendarPage, CalendarReauthBanner, CalendarToolbar, DayView, WeekView, MonthView, EventCard, EventCreateModal
- `attachments/` — AttachmentLibrary, AttachmentGridItem, AttachmentListItem
- `tasks/` — TasksPage, TaskItem, TaskQuickAdd, TaskSidebar, AiTaskExtractDialog
- `help/` — HelpPage, HelpSidebar, HelpSearchBar, HelpCard, HelpCardGrid, HelpTooltip
- `labels/` — LabelForm
- `dnd/` — DndProvider (@dnd-kit drag-and-drop: threads → sidebar labels)
- `ui/` — EmptyState, Skeleton, ContextMenu, ContextMenuPortal, OfflineBanner, illustrations/ (InboxClearIllustration, NoAccountIllustration, NoSearchResultsIllustration, ReadingPaneIllustration, GenericEmptyIllustration)
### Multi-window support
Thread pop-out windows via `ThreadWindow.tsx`. Entry point in `main.tsx` checks URL params (`?thread=...&account=...`) to render `<ThreadWindow />` or `<App />`. Window label format: `thread-{threadId}`. Tauri capabilities allow `thread-*` wildcard. Default size: 800x700. Splash screen window (400x300, no decorations, always on top) shown during initialization.
### Startup sequence (App.tsx)
1. `runMigrations()`
2. Restore persisted settings: theme, color theme, sidebar, contact sidebar, reading pane position, read filter, email list width, email density, default reply mode, mark-as-read behavior, send & archive, font scale, inbox view mode, phishing detection, sidebar nav config
3. Load custom keyboard shortcuts (`shortcutStore.loadKeyMap()`)
4. `getAllAccounts()` → `initializeClients()` (Gmail API clients) / create IMAP providers → `fetchSendAsAliases()` per Gmail account
5. `startBackgroundSync()` (60s interval), `backfillUncategorizedThreads()`
6. `startSnoozeChecker()` + `startScheduledSendChecker()` + `startFollowUpChecker()` + `startBundleChecker()` (60s intervals) + `startQueueProcessor()` (30s) + `startPreCacheManager()` (15min)
7. Initialize network status detection (`online`/`offline` window events → `uiStore.setOnline()`, triggers queue flush on reconnect)
8. `initNotifications()` (request OS permission)
9. `initGlobalShortcut()` (system-wide compose shortcut)
10. `initDeepLinkHandler()` (`mailto:` protocol)
11. `updateBadgeCount()` (taskbar badge)
12. `close_splashscreen` → show main window
13. Cleanup on unmount: stop all background checkers (including queue processor, pre-cache manager), unregister shortcuts, deep link handler
### Cross-component communication
Custom window events: `velo-sync-done`, `velo-toggle-command-palette`, `velo-toggle-shortcuts-help`, `velo-toggle-ask-inbox`, `velo-move-to-folder`. Tray emits `tray-check-mail` via Tauri event system. `single-instance-args` event for deep link forwarding.
### Keyboard shortcuts
`useKeyboardShortcuts` hook in App.tsx — Superhuman-style keys. Skips when input/textarea/contentEditable is focused. Supports two-key sequences (only `g` prefix currently) with 1s timeout via refs. Shortcut definitions in `src/constants/shortcuts.ts`. Customizable via `shortcutStore` (persisted to SQLite settings).
| Key | Action |
|-----|--------|
| `j` / `k` | Navigate threads down/up |
| `o` / `Enter` | Open thread |
| `e` | Archive |
| `s` | Star/unstar |
| `p` | Pin/unpin |
| `m` | Mute/unmute thread |
| `c` | Compose new email |
| `r` | Reply |
| `a` | Reply all |
| `f` | Forward |
| `u` | Unsubscribe |
| `t` | Create task from email (AI) |
| `v` | Move to folder/label |
| `#` / `Delete` / `Backspace` | Trash (permanent delete if already in trash) |
| `!` | Report spam / Not spam (context-aware) |
| `/` or `Ctrl+K` | Command palette / search |
| `?` | Shortcuts help |
| `Escape` | Close composer → clear multi-select → deselect thread (hierarchical) |
| `Ctrl+Shift+E` | Toggle sidebar |
| `Ctrl+Enter` | Send email (in composer) |
| `Ctrl+A` | Select all threads |
| `Ctrl+Shift+A` | Select all threads from current position |
| `g` then `i` | Go to Inbox |
| `g` then `s` | Go to Starred |
| `g` then `t` | Go to Sent |
| `g` then `d` | Go to Drafts |
| `g` then `p` | Go to Primary |
| `g` then `u` | Go to Updates |
| `g` then `o` | Go to Promotions |
| `g` then `c` | Go to Social |
| `g` then `n` | Go to Newsletters |
| `g` then `k` | Go to Tasks |
| `g` then `a` | Go to Attachments |
Multi-select: click to toggle, Shift+click for range. All keyboard actions work on multi-selected threads.
## Styling
Tailwind CSS v4 — uses `@import "tailwindcss"`, `@theme {}` for custom properties, and `@custom-variant dark` in `src/styles/globals.css`. Dark mode toggles via `<html class="dark">` which swaps CSS custom properties. Font scaling via `font-scale-{small|default|large|xlarge}` classes on `<html>`.
**Semantic color tokens**: `bg-bg-primary/secondary/tertiary/hover/selected`, `text-text-primary/secondary/tertiary`, `border-border-primary/secondary`, `bg-accent/accent-hover/accent-light`, `bg-danger/warning/success`, `bg-sidebar-bg`, `text-sidebar-text`.
**Glass effects**: `.glass-panel`, `.glass-modal`, `.glass-backdrop` utility classes with blur and shadow properties.
**Color themes**: 8 accent color presets (Indigo, Rose, Emerald, Amber, Sky, Violet, Orange, Slate) defined in `src/constants/themes.ts`. Each has light & dark variants. Applied via CSS custom properties, independent of light/dark mode.
**Background**: Animated gradient blobs (5 blobs with radial gradients, keyframe animations). Light mode uses blue→purple→pink→orange→cyan gradient; dark mode uses darker blues/purples.
**Icons**: `lucide-react` icon library.
## Testing
Vitest + jsdom. Setup file: `src/test/setup.ts` (imports `@testing-library/jest-dom/vitest`). Config: `globals: true` (no imports needed for `describe`, `it`, `expect`). Tests are colocated with source files (e.g., `uiStore.test.ts` next to `uiStore.ts`). Zustand test pattern: `useStore.setState()` in beforeEach, assert via `.getState()`.
132 test files across stores (8), services (70), utils (14), components (32), constants (3), router (1), hooks (2), and config (1).
## Database
SQLite via Tauri SQL plugin. 19 migrations (version-tracked in `_migrations` table, transactional). Custom `splitStatements()` handles BEGIN...END blocks in triggers.
Key tables (37 total): `accounts` (with `provider` "gmail_api"|"imap", IMAP/SMTP host/port/security fields, `auth_method`, encrypted `imap_password`, optional `imap_username`), `messages` (with FTS5 index `messages_fts`, `auth_results`, `message_id_header`, `references_header`, `in_reply_to_header`, `imap_uid`, `imap_folder`), `threads` (with `is_pinned`, `is_muted`), `thread_labels`, `labels` (with `imap_folder_path`, `imap_special_use`), `contacts` (frequency-ranked for autocomplete, with `first_contacted_at`), `attachments` (with `cached_at`, `cache_size`, `imap_part_id`), `filter_rules` (criteria/actions as JSON), `scheduled_emails` (status: pending/sent/failed), `templates` (with optional keyboard shortcut), `signatures`, `image_allowlist`, `settings` (key-value store), `ai_cache`, `thread_categories`, `calendar_events`, `follow_up_reminders`, `notification_vips`, `unsubscribe_actions`, `bundle_rules`, `bundled_threads`, `send_as_aliases`, `smart_folders`, `link_scan_results`, `phishing_allowlist`, `quick_steps`, `folder_sync_state` (IMAP UIDVALIDITY/last_uid/modseq tracking per folder), `pending_operations` (offline action queue with retry/backoff), `local_drafts` (offline draft persistence), `writing_style_profiles` (AI writing style per account), `tasks` (full task management with priorities, subtasks, recurrence), `task_tags` (custom task tag colors), `smart_label_rules` (AI auto-labeling rules with optional criteria), `_migrations`.
## Key Gotchas
- **Tauri SQL plugin config**: `preload` in tauri.conf.json must be an array `["sqlite:velo.db"]` — NOT an object/map
- **Tauri Emitter trait**: Must `use tauri::Emitter;` to call `.emit()` on windows
- **Tauri capabilities**: Any new plugin needs explicit permissions added to `src-tauri/capabilities/default.json`. Windows allow `"main"`, `"splashscreen"`, and `"thread-*"` wildcard
- **Tauri window config**: Custom titlebar — macOS uses `titleBarStyle: "Overlay"`, Windows/Linux removes decorations programmatically in Rust setup. 1200x800 default, 800x600 minimum. Splash screen: 400x300, no decorations, center, always on top
- **Single instance**: `tauri-plugin-single-instance` must be first plugin registered. Forwards args for deep linking
- **Minimize-to-tray**: Use `.on_window_event()` on the Builder, not `window.on_window_event()`
- **Windows WebView2**: `Chrome_WidgetWin_0` error on close is benign — ignore it
- **Windows AUMID**: Set explicitly in Rust for proper notification identity (`com.velomail.app`)
- **OAuth (Gmail)**: Localhost server tries ports 17248-17251. PKCE flow, no client secret. Client ID stored in SQLite settings table, configured by user in Settings
- **IMAP message IDs**: Format is `imap-{accountId}-{folder}-{uid}` — not the RFC Message-ID header
- **IMAP security mapping**: UI shows "SSL/TLS", "STARTTLS", "None" but config stores "ssl", "starttls", "none"
- **IMAP UIDVALIDITY**: If UIDVALIDITY changes on a folder, all cached UIDs are invalid — triggers full resync of that folder
- **IMAP folders vs labels**: IMAP has no native labels; folders are mapped to Gmail-style labels via `folderMapper.ts` using special-use flags and well-known name matching
- **IMAP passwords**: Encrypted with AES-256-GCM in SQLite (same crypto as OAuth tokens)
- **IMAP username**: Optional `imap_username` column on accounts — when set, used as login username for IMAP/SMTP instead of email. Falls back to email when null
- **IMAP auto-discovery**: Pre-configured for Outlook/Hotmail, Yahoo, iCloud, AOL, Zoho, FastMail, GMX; other providers require manual server entry
- **Provider abstraction**: All sync/send operations go through `EmailProvider` interface — use `getEmailProvider(account)` from `providerFactory.ts`, never call Gmail or IMAP APIs directly from components
- **Offline mode**: All email modify operations (archive, trash, star, read, send, labels, drafts) go through `emailActions.ts` which applies optimistic UI updates, local DB changes, and queues operations when offline. Never call `getGmailClient()` directly for modify operations — use the convenience wrappers (`archiveThread`, `trashThread`, `starThread`, etc.). Queue processor runs every 30s, compacts redundant ops, uses exponential backoff retries. Conflict detection in delta sync skips threads with pending local ops
- **Network detection**: `uiStore.isOnline` tracks connectivity via `navigator.onLine` + window `online`/`offline` events. Queue flush triggers automatically on reconnect
- **CSP**: Allows connections to googleapis.com, anthropic.com, openai.com, generativelanguage.googleapis.com, gravatar.com, googleusercontent.com
- **TypeScript strict mode**: `noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexedAccess` are all enabled. Target ES2021, bundler module resolution, `moduleDetection: "force"`
- **Path alias**: `@/*` maps to `src/*`
- **Email HTML rendering**: DOMPurify sanitization, rendered in sandboxed iframe (`allow-same-origin` only). Strips remote images by default (uses `data-blocked-src` attributes), allowlist per sender
- **Thread deletion**: Two-stage — first trash, then permanent delete from DB if already in trash
- **Snooze**: Removes INBOX label and adds SNOOZED label (not just a flag)
- **Draft auto-save**: 3-second debounce, not configurable
- **Gmail History API**: Expires after ~30 days, triggers automatic full sync fallback
- **Vite HMR**: Uses port 1421 when `TAURI_DEV_HOST` is set
- **Vite build**: Multi-page — `index.html` (main app) + `splashscreen.html`
- **Filter engine**: AND logic for criteria, merges actions when multiple filters match same message
- **AI providers**: API keys stored in SQLite settings table. Provider selected per-feature in settings. Results cached in `ai_cache` table
- **Deep links**: `mailto:` scheme registered via tauri-plugin-deep-link. Opens compose window with pre-filled recipient
- **Autostart**: Uses `--hidden` flag to start minimized to tray
- **Phishing detection**: 10 heuristic rules (IP URLs, homograph, suspicious TLDs, URL shorteners, display/href mismatch, suspicious paths, brand impersonation, dangerous protocols, free email impostor, subdomain spoofing). Sensitivity configurable (low/default/high). Results cached in `link_scan_results`
- **Auth display**: SPF/DKIM/DMARC parsed from `Authentication-Results` header. Aggregate verdict: pass/fail/warning/unknown. Stored in `messages.auth_results` column
- **Mute threads**: Sets `is_muted` flag, auto-archives. Muted threads suppressed from notifications during delta sync
- **Send-as aliases**: Fetched from Gmail `/settings/sendAs` API on account init (Gmail only). `FromSelector` shown in composer when account has multiple aliases
- **Smart folders**: Saved search queries with dynamic tokens (`__LAST_7_DAYS__`, `__LAST_30_DAYS__`, `__TODAY__`). Managed via `smartFolderStore`
- **Quick steps**: Custom action chains with 18 action types. Executor in `services/quickSteps/executor.ts`
- **Split inbox**: Category tabs (Primary/Updates/Promotions/Social/Newsletters) with backfill service for existing threads
- **Help page**: In-app help at `/help/$topic` with 13 categories, searchable cards, and contextual `HelpTooltip` component. All content in `src/constants/helpContent.ts`. After adding a new feature, run `/document-feature` to add its help card
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Velo
Thank you for your interest in contributing to Velo! This guide will help you get started.
## Getting Started
### Prerequisites
- [Node.js](https://nodejs.org/) v18+
- [Rust](https://www.rust-lang.org/tools/install)
- [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/)
### Setup
```bash
git clone https://github.com/avihaymenahem/velo.git
cd velo
npm install
npm run tauri dev
```
## Development Workflow
1. **Fork** the repository and create a branch from `main`
2. **Make your changes** -- see the sections below for guidelines
3. **Run tests** before submitting: `npm run test`
4. **Type-check** your code: `npx tsc --noEmit`
5. **Open a pull request** against `main`
## Project Structure
| Directory | Purpose |
|-----------|---------|
| `src-tauri/` | Rust backend (Tauri commands, plugins, system tray) |
| `src/components/` | React UI components (12 groups) |
| `src/stores/` | Zustand state stores |
| `src/services/` | Business logic (email, AI, sync, DB) |
| `src/services/db/` | SQLite query functions and migrations |
| `src/hooks/` | Custom React hooks |
| `src/constants/` | Static data (shortcuts, themes, help content) |
| `src/utils/` | Shared utility functions |
For detailed architecture, see [docs/architecture.md](docs/architecture.md).
## Code Guidelines
### TypeScript / React
- **Strict mode** is enabled (`noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexedAccess`)
- **Avoid `useEffect`** where possible -- prefer derived state, event handlers, `useMemo`, or refs
- **Tailwind CSS v4** for all styling -- use semantic color tokens (`bg-bg-primary`, `text-text-primary`, etc.)
- **Icons** from `lucide-react`
- **Path alias**: use `@/` to import from `src/`
### Rust
- Backend code lives in `src-tauri/src/`
- New Tauri commands must be registered in `src-tauri/src/lib.rs`
- New plugins need permissions added to `src-tauri/capabilities/default.json`
### Database
- Migrations go in `src/services/db/migrations.ts` -- always add to the end of the array
- Never modify existing migrations; only append new ones
- Query functions go in `src/services/db/` as separate files
### Testing
- Write tests for new code -- tests are colocated with source files (e.g., `foo.test.ts` next to `foo.ts`)
- Run the full suite with `npm run test`
- Run a single file with `npx vitest run <path>`
- We use **Vitest** with `globals: true` (no need to import `describe`, `it`, `expect`)
### Commit Messages
We use [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning:
```
feat: add snooze presets to context menu
fix: prevent duplicate sync on reconnect
docs: update keyboard shortcuts reference
refactor: extract email parser into separate module
test: add coverage for filter engine AND logic
chore: bump tauri to v2.10
```
## Pull Requests
- Fill out the [PR template](.github/pull_request_template.md)
- Keep PRs focused -- one feature or fix per PR
- Include screenshots for UI changes
- Ensure all tests pass and there are no type errors
## Reporting Bugs
Use the [bug report template](https://github.com/avihaymenahem/velo/issues/new?template=bug_report.yml) on GitHub Issues. Include:
- Steps to reproduce
- Expected vs. actual behavior
- OS and Velo version
- Screenshots or logs if applicable
## Feature Requests
Use the [feature request template](https://github.com/avihaymenahem/velo/issues/new?template=feature_request.yml) on GitHub Issues.
## License
By contributing, you agree that your contributions will be licensed under the [Apache-2.0 License](LICENSE).
## Packaging
This project uses GitHub Actions to build Flatpak and RPM packages for Linux distributions. To test these builds locally, follow the instructions below.
### Testing the Flatpak Build
These steps guide you through building the Flatpak package locally using `flatpak-builder`.
1. **Install Dependencies**
You need `flatpak` and `flatpak-builder`.
* **On Fedora:**
```bash
sudo dnf install flatpak flatpak-builder
```
* **On Debian/Ubuntu:**
```bash
sudo apt install flatpak flatpak-builder
```
2. **Install the Build Runtimes**
The build requires the GNOME 46 SDK and the Node.js extension. Rust is installed via rustup during the build, so no Rust SDK extension is needed.
```bash
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y flathub \
org.gnome.Platform/x86_64/46 \
org.gnome.Sdk/x86_64/46 \
org.freedesktop.Sdk.Extension.node20/x86_64/23.08
```
3. **Build and Install the Application**
Run the npm script from the root of the project. This will compile the application and install it directly for the current user using `flatpak-builder`.
```bash
npm run flatpak
```
4. **Test the Local Build**
You can now run the application directly.
```bash
flatpak run com.velomail.app
```
### Building and Testing the RPM Locally
You can build the RPM directly using Tauri's built-in bundler.
1. **Install Build Dependencies**
Ensure you have the necessary system dependencies installed:
```bash
sudo dnf install webkit2gtk4.1-devel openssl-devel libappindicator-gtk3-devel librsvg2-devel rpm-build
```
2. **Build the RPM**
Run the Tauri build command specifying `rpm` as the bundle target:
```bash
npx tauri build -b rpm
```
3. **Test the Local Build**
The compiled RPM will be located in the `src-tauri/target/release/bundle/rpm/` directory. Install it to test:
```bash
sudo dnf install src-tauri/target/release/bundle/rpm/*.rpm
```
### Pushing to COPR
To publish a new release to a Fedora COPR repository using the `velo.spec` file:
1. **Install RPM Tools**
```bash
sudo dnf install rpmdevtools copr-cli
rpmdev-setuptree
```
2. **Create a Source Tarball and SRPM**
Create a source tarball that matches the version in `velo.spec`, then build the SRPM.
```bash
VERSION=$(grep -oP '(?<=^%global app_version ).*' velo.spec)
tar --exclude='.git' --transform "s/^\./velo-${VERSION}/" -czf "velo-${VERSION}.tar.gz" .
cp "velo-${VERSION}.tar.gz" ~/rpmbuild/SOURCES/
cp velo.spec ~/rpmbuild/SPECS/
rpmbuild -bs ~/rpmbuild/SPECS/velo.spec
```
3. **Upload to COPR**
Submit the generated SRPM to your COPR project.
```bash
copr build your-username/velo ~/rpmbuild/SRPMS/velo-${VERSION}-1.*.src.rpm
```
*Note: Because our RPM build runs `npm ci` and Cargo, ensure **"Enable network in buildroot"** is turned on in your COPR project settings.*
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided alongside the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please also get an
"applicable patent or copyright grant" from each Contributor.
Copyright 2025 Velo Mail
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
<p align="center">
<img src="assets/icon.png?v1" alt="Velo" width="200" height="200" style="border-radius: 24px;" />
</p>
<h1 align="center">Velo</h1>
<p align="center">
<strong>Email at the speed of thought.</strong>
</p>
<p align="center">
A blazing-fast, keyboard-first desktop email client built with Tauri, React, and Rust.<br />
Local-first. Privacy-focused. AI-powered.
</p>
<p align="center">
<a href="#features">Features</a> •
<a href="#installation">Installation</a> •
<a href="docs/keyboard-shortcuts.md">Shortcuts</a> •
<a href="docs/architecture.md">Architecture</a> •
<a href="docs/development.md">Development</a> •
<a href="CONTRIBUTING.md">Contributing</a>
</p>
---
<p align="center">
<img width="1920" height="1032" alt="Screenshot 2026-02-17 223320" src="https://github.com/user-attachments/assets/dd096d15-4c1e-438c-99f9-c38b50a8a437" />
</p>
---
## Why Velo?
Most email clients are slow, bloated, or send your data to someone else's server. Velo is different:
- **Local-first** -- Your emails live in a local SQLite database. No middleman servers. Read your mail offline.
- **Keyboard-driven** -- Superhuman-inspired shortcuts let you fly through your inbox without touching the mouse.
- **AI-enhanced** -- Summarize threads, generate replies, and search your inbox in natural language -- with your choice of AI provider.
- **Native performance** -- Rust backend via Tauri v2. Small binary, low memory, instant startup.
- **Private by default** -- Remote images blocked, HTML sanitized, emails rendered in sandboxed iframes. Your data stays on your machine.
---
## Features
### Email
- Multi-account support: Gmail (API) and IMAP/SMTP (Outlook, Yahoo, iCloud, Fastmail, and more) with instant switching
- Threaded conversations with collapsible messages
- Full-text search with Gmail-style operators (`from:`, `to:`, `subject:`, `has:attachment`, `label:`, etc.)
- Command palette (`/` or `Ctrl+K`) for quick actions
- Drag-and-drop labels, multi-select, pin threads, mute threads, context menus
- Split inbox with category tabs (Primary, Updates, Promotions, Social, Newsletters)
- Inline reply, contact sidebar with Gravatar
### Composer
- TipTap v3 rich text editor (bold, italic, lists, code, links, images)
- Undo send, schedule send, auto-save drafts
- Multiple signatures, reusable templates with variables
- Send-as email aliases with from-address selector
- Drag-and-drop attachments with inline preview
- Frequency-ranked contact autocomplete
### Smart Inbox
- Snooze threads with presets or custom date/time
- Filters to auto-label, archive, trash, star, or mark read
- AI + rule-based auto-categorization (Primary, Updates, Promotions, Social, Newsletters)
- One-click unsubscribe (RFC 8058) and subscription manager
- Newsletter bundling with delivery schedules
- Smart folders / saved searches with dynamic query tokens
- Quick steps -- custom action chains for batch thread processing
- Follow-up reminders when you haven't received a reply
### AI
Three providers with selectable models -- choose one or mix and match:
| Provider | Models |
|----------|--------|
| **Anthropic Claude** | Haiku 4.5, Sonnet 4, Opus 4 |
| **OpenAI** | GPT-4o Mini, GPT-4o, GPT-4.1 Nano, GPT-4.1 Mini, GPT-4.1 |
| **Google Gemini** | 2.5 Flash, 2.5 Pro |
Thread summaries, smart reply suggestions, AI compose & reply, text transform (improve/shorten/formalize), Ask My Inbox (natural language search). Pick which model to use per provider in Settings. All results cached locally.
### Calendar
Google Calendar sync with month, week, and day views. Create events without leaving Velo.
### UI & Design
- Glassmorphism with animated gradient background
- Dark / light / system theme with 8 accent color presets
- Flexible reading pane (right, bottom, hidden), resizable panels
- Configurable density and font scaling
- Pop-out thread windows, custom titlebar, splash screen
- System tray with taskbar badge count
### Privacy & Security
- OAuth PKCE for Gmail -- no client secret, no backend servers
- Encrypted password/app-password storage for IMAP accounts (AES-256-GCM)
- Remote image blocking with per-sender allowlist
- Phishing link detection with 10 heuristic scoring rules
- SPF/DKIM/DMARC authentication display with badges and warnings
- DOMPurify + sandboxed iframe rendering
- AES-256-GCM encrypted token storage
### System Integration
- `mailto:` deep links, global compose shortcut
- Autostart (hidden in tray), single instance
- [Customizable keyboard shortcuts](docs/keyboard-shortcuts.md)
---
## Installation
Download the latest release for your platform:
**[Download Velo](https://github.com/avihaymenahem/velo/releases/latest)** -- Windows `.msi` / `.exe` • macOS `.dmg` • Linux `.deb` / `.AppImage`
No build tools or programming knowledge required -- just download, install, and run.
### Account setup
**Gmail:** Create OAuth credentials in [Google Cloud Console](https://console.cloud.google.com/) (enable Gmail API + Calendar API), then enter your Client ID in Velo's Settings. No client secret needed (PKCE).
**IMAP/SMTP:** Click "Add IMAP Account" in the account switcher. Enter your email and password -- Velo auto-discovers server settings for popular providers (Outlook, Yahoo, iCloud, Fastmail, etc.). For other providers, enter IMAP/SMTP server details manually. No Google Cloud project needed.
**AI (optional):** Add an API key for [Anthropic](https://console.anthropic.com/), [OpenAI](https://platform.openai.com/), or [Google Gemini](https://aistudio.google.com/) in Settings. Then select which model to use for each provider.
### Building from source
For developers who want to build Velo themselves or contribute:
```bash
git clone https://github.com/avihaymenahem/velo.git
cd velo
npm install
npm run tauri dev
```
**Prerequisites:** [Node.js](https://nodejs.org/) v18+, [Rust](https://www.rust-lang.org/tools/install), [Tauri v2 deps](https://v2.tauri.app/start/prerequisites/)
See [Development Guide](docs/development.md) for all commands, testing, and build instructions.
---
## Tech Stack
| | |
|--|--|
| **Framework** | Tauri v2 (Rust) + React 19 + TypeScript |
| **Styling** | Tailwind CSS v4 |
| **State** | Zustand 5 (8 stores) |
| **Editor** | TipTap v3 |
| **Email** | Gmail API, IMAP/SMTP (via async-imap + lettre in Rust) |
| **Database** | SQLite + FTS5 (33 tables) |
| **AI** | Claude, GPT, Gemini |
| **Testing** | Vitest + Testing Library |
See [Architecture](docs/architecture.md) for detailed design, data flow, and project structure.
---
## Building
```bash
npm run tauri build
```
**Windows** `.msi` / `.exe` • **macOS** `.dmg` / `.app` • **Linux** `.deb` / `.AppImage`
---
## License
[Apache-2.0](LICENSE)
---
<p align="center">
Built with Rust and React.<br />
Made by <a href="https://github.com/avihaymenahem">Avihay</a>.
</p>
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
| Version | Supported |
|---------|-----------|
| Latest release | Yes |
| Older releases | No |
We only provide security fixes for the latest release. Please keep Velo up to date.
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them via email to **security@velomail.app** (or by opening a [private security advisory](https://github.com/avihaymenahem/velo/security/advisories/new) on GitHub).
Include as much of the following as possible:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
You should receive an acknowledgment within **48 hours**. We will work with you to understand the issue and coordinate a fix before any public disclosure.
## Security Model
### Local-First Architecture
Velo is a desktop application. Your emails, tokens, and settings are stored locally in a SQLite database on your machine. There are no Velo-operated backend servers.
### Authentication & Credentials
- **Gmail**: OAuth 2.0 with PKCE -- no client secret stored. Tokens are encrypted with AES-256-GCM before being saved to the local database.
- **IMAP/SMTP**: Passwords and app passwords are encrypted with AES-256-GCM in the local SQLite database.
- **AI API keys**: Stored in the local SQLite settings table. Keys are sent directly to the respective provider (Anthropic, OpenAI, Google) over HTTPS -- never to any Velo server.
### Email Rendering
- HTML emails are sanitized with **DOMPurify** and rendered in a **sandboxed iframe** (`allow-same-origin` only -- no scripts)
- Remote images are **blocked by default** and replaced with placeholders. Users can allowlist specific senders.
- Phishing detection uses 10 heuristic rules to flag suspicious links before you click them
### Network
- All API connections use HTTPS
- Content Security Policy restricts network requests to known domains (googleapis.com, anthropic.com, openai.com, generativelanguage.googleapis.com, gravatar.com, googleusercontent.com)
- No telemetry, analytics, or tracking
### Dependencies
- Frontend: React, Tailwind CSS, TipTap, Zustand, DOMPurify, lucide-react
- Backend: Tauri v2 (Rust), async-imap, lettre, mail-parser
- We monitor dependencies for known vulnerabilities and update regularly
## Scope
The following are **in scope** for security reports:
- Authentication bypass or token leakage
- Credential exposure (OAuth tokens, IMAP passwords, API keys)
- Cross-site scripting (XSS) via email content escaping the sandbox
- Remote code execution
- SQL injection in the local SQLite database
- Insecure data storage or encryption weaknesses
- Phishing detection bypasses
The following are **out of scope**:
- Vulnerabilities requiring physical access to the user's machine (local SQLite is not encrypted at rest by design -- the OS protects user files)
- Denial of service against the local application
- Issues in third-party dependencies with no demonstrated impact on Velo
- Social engineering attacks
## Disclosure Policy
- We follow coordinated disclosure. Please allow us reasonable time (typically 90 days) to address the issue before public disclosure.
- We will credit reporters in release notes unless anonymity is requested.
================================================
FILE: com.velomail.app.desktop
================================================
[Desktop Entry]
Name=Velo
Comment=Fast, beautiful desktop email client
Exec=velo
Icon=com.velomail.app
Type=Application
Categories=Network;Email;
StartupWMClass=Velo
StartupNotify=true
================================================
FILE: com.velomail.app.metainfo.xml
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<component type="desktop-application">
<id>com.velomail.app</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>
<name>Velo</name>
<summary>Fast, beautiful desktop email client</summary>
<description>
<p>Velo is a fast and beautiful desktop email client, built with modern web technologies.</p>
</description>
<url type="homepage">https://github.com/avihaymenahem/velo</url>
<url type="bugtracker">https://github.com/avihaymenahem/velo/issues</url>
<launchable type="desktop-id">com.velomail.app.desktop</launchable>
<releases>
<release version="0.4.21" date="2026-02-20" /> <!-- x-release-please-version -->
</releases>
<developer id="app.velomail">
<name>Velo Team</name>
</developer>
<content_rating type="oars-1.1" />
<icons>
<icon type="cached" width="128" height="128">/app/share/icons/hicolor/128x128/apps/com.velomail.app.png</icon>
<icon type="cached" width="256" height="256">/app/share/icons/hicolor/256x256/apps/com.velomail.app.png</icon>
</icons>
</component>
================================================
FILE: com.velomail.app.yml
================================================
app-id: com.velomail.app
runtime: org.gnome.Platform
runtime-version: "46"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.node20
command: velo
build-options:
append-path: /usr/lib/sdk/node20/bin
env:
CARGO_HOME: /run/build/velo/flatpak-cargo
RUSTUP_HOME: /run/build/velo/flatpak-rustup
LD_LIBRARY_PATH: /app/lib
PKG_CONFIG_PATH: /app/lib/pkgconfig
build-args:
- --share=network
modules:
- name: velo
buildsystem: simple
build-commands:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
- npm ci
- export PATH="$CARGO_HOME/bin:$PATH" && npx tauri build --no-bundle
- install -Dm755 src-tauri/target/release/velo /app/bin/velo
- install -Dm644 src-tauri/icons/32x32.png /app/share/icons/hicolor/32x32/apps/com.velomail.app.png
- install -Dm644 src-tauri/icons/128x128.png /app/share/icons/hicolor/128x128/apps/com.velomail.app.png
- install -Dm644 src-tauri/icons/128x128@2x.png /app/share/icons/hicolor/256x256/apps/com.velomail.app.png
- install -Dm644 com.velomail.app.desktop /app/share/applications/com.velomail.app.desktop
- install -Dm644 com.velomail.app.metainfo.xml /app/share/metainfo/com.velomail.app.metainfo.xml
sources:
- type: dir
path: .
skip:
- src-tauri/target
- node_modules
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --share=network
- --device=dri
- --filesystem=xdg-download:rw
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.freedesktop.portal.Desktop # File picker, open/save dialogs
- --talk-name=org.kde.StatusNotifierWatcher # D-Bus StatusNotifierItem for KSNI
================================================
FILE: docs/architecture.md
================================================
# Architecture
Velo follows a **three-layer architecture** with clear separation of concerns.
```
+--------------------------+
| React 19 + Zustand | UI Layer
| Components + 9 Stores | (TypeScript)
+--------------------------+
| Service Layer | Business Logic
| Email Provider / Gmail / | (TypeScript)
| IMAP / DB / AI / Sync / |
| Calendar / Bundles / |
| Filters / Notifications |
+--------------------------+
| Tauri v2 + Rust | Native Layer
| System Tray / OAuth / | (Rust)
| SQLite / Notifications / |
| Deep Links / Autostart |
+--------------------------+
```
## Tech Stack
| Layer | Technology |
|-------|-----------|
| **Framework** | [Tauri v2](https://v2.tauri.app/) |
| **Frontend** | React 19, TypeScript, Zustand 5 |
| **Styling** | Tailwind CSS v4 |
| **Editor** | TipTap v3 |
| **Backend** | Rust |
| **Database** | SQLite (via tauri-plugin-sql) |
| **Search** | FTS5 with trigram tokenizer |
| **AI** | Anthropic Claude, OpenAI GPT, Google Gemini (user-selectable models per provider) |
| **Icons** | Lucide React |
| **Drag & Drop** | @dnd-kit |
| **Testing** | Vitest + Testing Library |
## Data Flow
1. **Sync** -- Background sync every 60s. Gmail accounts use Gmail History API (delta sync, falls back to full sync if history expires ~30 days). IMAP accounts use UIDVALIDITY/last_uid tracking for efficient delta sync.
2. **Storage** -- All messages, threads, labels, contacts, calendar events, and AI results stored in local SQLite (34 tables) with FTS5 full-text indexing.
3. **State** -- Eight Zustand stores manage UI state. No middleware, no persistence needed -- ephemeral state rebuilds from SQLite on startup.
4. **Rendering** -- Email HTML is sanitized with DOMPurify and rendered in sandboxed iframes. Remote images blocked by default.
5. **Background services** -- Seven interval checkers run continuously: sync (60s), snooze (60s), scheduled send (60s), follow-up reminders (60s), newsletter bundles (60s), offline queue processor (30s), and attachment pre-cache (15min).
6. **Security** -- Phishing link detection scores message links with 10 heuristic rules. SPF/DKIM/DMARC authentication headers parsed and displayed as badges.
## Project Structure
```
velo/
├── src/
│ ├── components/ # React components (14 groups, ~94 files)
│ │ ├── layout/ # Sidebar, EmailList, ReadingPane, TitleBar
│ │ ├── email/ # ThreadView, MessageItem, EmailRenderer,
│ │ │ # ContactSidebar, SmartReplySuggestions,
│ │ │ # InlineReply, ThreadSummary, FollowUpDialog,
│ │ │ # AuthBadge, AuthWarningBanner, PhishingBanner,
│ │ │ # LinkConfirmDialog, CategoryTabs
│ │ ├── composer/ # Composer, AddressInput, EditorToolbar,
│ │ │ # AiAssistPanel, ScheduleSendDialog, FromSelector
│ │ ├── search/ # CommandPalette, SearchBar, ShortcutsHelp, AskInbox
│ │ ├── settings/ # SettingsPage, FilterEditor, LabelEditor,
│ │ │ # SubscriptionManager, ContactEditor,
│ │ │ # QuickStepEditor, SmartFolderEditor
│ │ ├── accounts/ # AddAccount, AddImapAccount, AccountSwitcher, SetupClientId
│ │ ├── calendar/ # CalendarPage, MonthView, WeekView, DayView,
│ │ │ # EventCard, EventCreateModal
│ │ ├── attachments/ # AttachmentLibrary, AttachmentGridItem, AttachmentListItem
│ │ ├── tasks/ # TasksPage, TaskItem, TaskSidebar, TaskQuickAdd,
│ │ │ # AiTaskExtractDialog
│ │ ├── help/ # HelpPage, HelpSidebar, HelpSearchBar,
│ │ │ # HelpCard, HelpCardGrid, HelpTooltip
│ │ ├── labels/ # LabelForm
│ │ ├── dnd/ # DndProvider (drag threads → sidebar labels)
│ │ └── ui/ # EmptyState, Skeleton, ContextMenu, OfflineBanner, illustrations/
│ ├── services/ # Business logic layer
│ │ ├── db/ # SQLite queries (29 files), migrations, FTS5
│ │ ├── email/ # EmailProvider abstraction, providerFactory,
│ │ │ # gmailProvider, imapSmtpProvider
│ │ ├── gmail/ # GmailClient, tokenManager, syncManager
│ │ ├── imap/ # IMAP sync, folder mapper, auto-discovery,
│ │ │ # config builder, Tauri command wrappers
│ │ ├── threading/ # JWZ threading engine for IMAP conversations
│ │ ├── ai/ # AI service, 3 providers, categorization, Ask Inbox,
│ │ │ # writing style analysis, auto-drafts, task extraction
│ │ ├── google/ # Google Calendar API
│ │ ├── composer/ # Draft auto-save
│ │ ├── search/ # Query parser, SQL builder
│ │ ├── filters/ # Auto-apply filter engine
│ │ ├── categorization/ # Rule-based categorization engine
│ │ ├── snooze/ # Snooze & scheduled send checkers
│ │ ├── followup/ # Follow-up reminder checker
│ │ ├── bundles/ # Newsletter bundle manager
│ │ ├── notifications/ # OS notification manager
│ │ ├── contacts/ # Gravatar integration
│ │ ├── attachments/ # Attachment cache manager, pre-cache manager
│ │ ├── unsubscribe/ # One-click unsubscribe (RFC 8058)
│ │ ├── quickSteps/ # Quick step executor, types, defaults
│ │ ├── queue/ # Offline queue processor
│ │ ├── tasks/ # Task recurrence manager
│ │ ├── emailActions.ts # Centralized email action service (offline-aware)
│ │ ├── badgeManager.ts # Taskbar badge count
│ │ ├── deepLinkHandler.ts # mailto: protocol handler
│ │ └── globalShortcut.ts # System-wide compose shortcut
│ ├── stores/ # Zustand stores (9): ui, account, thread,
│ │ # composer, label, contextMenu, shortcut, smartFolder, task
│ ├── hooks/ # useKeyboardShortcuts, useClickOutside, useContextMenu
│ ├── utils/ # crypto, date, emailBuilder, sanitize, imageBlocker,
│ │ # mailtoParser, fileUtils, templateVariables, noReply
│ ├── constants/ # Keyboard shortcuts, color themes, help content
│ └── styles/ # Tailwind CSS v4 globals
├── src-tauri/
│ ├── src/ # Rust backend (tray, OAuth, splash, single-instance,
│ │ │ # IMAP client, SMTP client, Tauri commands)
│ ├── capabilities/ # Tauri v2 permissions
│ └── icons/ # App icons (all platforms)
├── docs/ # Documentation
├── package.json
├── CLAUDE.md # AI coding assistant context
└── README.md
```
## Rust Backend
The Rust layer (`src-tauri/src/`) handles system integration and performance-critical email protocol operations. It provides:
- **System tray** -- Show/hide, check mail, quit menu
- **OAuth server** -- Localhost PKCE server on port 17248
- **IMAP client** (`imap/`) -- Full IMAP protocol via `async-imap` + `mail-parser`. Supports TLS/STARTTLS/plain, XOAuth2 auth. Operations: FETCH, STORE, MOVE, DELETE, APPEND, LIST, STATUS
- **SMTP client** (`smtp/`) -- Email sending via `lettre`. Supports TLS/STARTTLS/plain. Parses RFC 2822 envelopes
- **Splash screen** -- Shown during initialization, closed when ready
- **Single instance** -- Prevents duplicate app windows, forwards deep link args
- **Minimize to tray** -- Hides on close instead of quitting
- **Custom titlebar** -- Overlay on macOS, frameless on Windows/Linux
- **Windows AUMID** -- Set for proper notification identity
**Tauri commands:** `start_oauth_server`, `close_splashscreen`, `set_tray_tooltip`, `open_devtools`, 11 IMAP commands (`imap_test_connection`, `imap_list_folders`, `imap_fetch_messages`, etc.), 2 SMTP commands (`smtp_send_email`, `smtp_test_connection`)
**Plugins (13):** sql, notification, opener, log, dialog, fs, http, single-instance, autostart, deep-link, global-shortcut
**Rust dependencies (IMAP/SMTP):** `async-imap`, `tokio-native-tls`, `mail-parser`, `lettre`
## Service Layer
All business logic lives in `src/services/` as plain async functions (except `GmailClient` class). Email operations use the `EmailProvider` abstraction — all sync/send flows go through `providerFactory.ts` which returns the appropriate provider (Gmail API or IMAP/SMTP) based on the account type.
| Service | Description |
|---------|-------------|
| `db/` | SQLite queries (29 files), migrations, FTS5 search |
| `email/` | EmailProvider abstraction, provider factory, Gmail/IMAP adapters |
| `gmail/` | Gmail client, token management, sync engine |
| `imap/` | IMAP sync, folder-to-label mapping, auto-discovery, Tauri command wrappers |
| `threading/` | JWZ threading algorithm for IMAP message grouping |
| `ai/` | AI service with 3 providers (selectable models), categorization, Ask Inbox, writing style analysis, auto-drafts, task extraction |
| `google/` | Google Calendar API |
| `composer/` | Draft auto-save (3s debounce) |
| `search/` | Gmail-style query parser, SQL builder |
| `filters/` | Auto-apply filter engine (AND logic) |
| `categorization/` | Rule-based categorization before AI fallback |
| `snooze/` | Snooze & scheduled send background checkers |
| `followup/` | Follow-up reminder checker |
| `bundles/` | Newsletter bundling with delivery schedules |
| `notifications/` | OS notifications with VIP filtering |
| `contacts/` | Gravatar integration |
| `attachments/` | Local attachment caching, pre-cache recent attachments |
| `unsubscribe/` | One-click unsubscribe (RFC 8058) |
| `quickSteps/` | Custom action chains with executor engine |
| `queue/` | Offline queue processor with exponential backoff |
| `tasks/` | Task recurrence manager |
| `smartLabels/` | AI-powered auto-labeling with two-phase matching (criteria + AI) |
**Root-level services:** `emailActions.ts` (centralized offline-aware email actions), `badgeManager.ts` (taskbar badge), `deepLinkHandler.ts` (mailto: protocol), `globalShortcut.ts` (system-wide compose)
## UI Layer
Nine Zustand stores manage ephemeral UI state:
| Store | Purpose |
|-------|---------|
| `uiStore` | Theme, sidebar, sidebar nav config, reading pane, density, font scale, selections, online status, pending ops count |
| `accountStore` | Account list, active account |
| `threadStore` | Thread list, selected thread, loading state |
| `composerStore` | Compose state, recipients, body, attachments |
| `labelStore` | Label list, label operations |
| `contextMenuStore` | Right-click context menu state |
| `shortcutStore` | Custom keyboard shortcut bindings |
| `smartFolderStore` | Saved searches with dynamic query tokens |
| `taskStore` | Task list, filters, grouping, thread tasks, incomplete count |
## Database
SQLite via Tauri SQL plugin. 19 migrations, 35 tables total.
Key tables: `accounts` (with `provider`, IMAP/SMTP fields), `messages` (with FTS5 index, `auth_results`, IMAP headers, `imap_uid`, `imap_folder`), `threads` (with `is_pinned`, `is_muted`), `thread_labels`, `labels` (with `imap_folder_path`, `imap_special_use`), `contacts`, `attachments` (with `imap_part_id`), `filter_rules`, `scheduled_emails`, `templates`, `signatures`, `image_allowlist`, `settings`, `ai_cache`, `thread_categories`, `calendar_events`, `follow_up_reminders`, `notification_vips`, `unsubscribe_actions`, `bundle_rules`, `bundled_threads`, `send_as_aliases`, `smart_folders`, `link_scan_results`, `phishing_allowlist`, `quick_steps`, `folder_sync_state` (IMAP sync tracking), `pending_operations` (offline action queue), `local_drafts` (offline draft persistence), `writing_style_profiles` (AI writing style per account), `tasks` (full task management with priorities, subtasks, recurrence), `task_tags` (custom task tag colors), `smart_label_rules` (AI-powered auto-labeling rules).
## Startup Sequence
1. Run database migrations
2. Restore persisted settings (theme, sidebar, density, font scale, reading pane, etc.)
3. Load custom keyboard shortcuts
4. Initialize email providers for all accounts (Gmail API clients + IMAP providers), sync send-as aliases for Gmail accounts
5. Start background sync (60s interval), backfill uncategorized threads
6. Start background checkers (snooze, scheduled send, follow-up, bundles, queue processor, attachment pre-cache)
7. Initialize network status detection (online/offline listeners)
8. Initialize OS notifications
9. Register global compose shortcut
10. Initialize deep link handler (`mailto:`)
11. Update taskbar badge count
12. Close splash screen, show main window
## Packaging & Distribution
Velo supports standard Linux distribution formats via automated and local build processes:
- **RPM & COPR**: Native RPM generation is integrated via Tauri's bundler (`tauri build -b rpm`), making it trivial to build and test RPMs locally or publish SRPMs to Fedora COPR.
- **Flatpak**: A Flatpak manifest (`com.velomail.app.yml`) defines the sandbox environment, leveraging the GNOME 46 runtime and Rust/Node.js SDK extensions. Local builds are streamlined via an npm script (`npm run flatpak`) which uses `flatpak-builder` while excluding host-specific artifacts to ensure reproducible sandboxed builds.
================================================
FILE: docs/development.md
================================================
# Development
## Prerequisites
- [Node.js](https://nodejs.org/) (v18+)
- [Rust](https://www.rust-lang.org/tools/install) (latest stable)
- Tauri v2 system dependencies ([see Tauri docs](https://v2.tauri.app/start/prerequisites/))
## Commands
```bash
# Start Tauri dev (frontend + backend)
npm run tauri dev
# Vite dev server only (no Tauri)
npm run dev
# Run tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run a specific test file
npx vitest run src/stores/uiStore.test.ts
# Type-check
npx tsc --noEmit
# Build for production
npm run tauri build
# Rust only (from src-tauri/)
cd src-tauri && cargo build
```
## Testing
- **Framework:** Vitest + jsdom
- **Setup:** `src/test/setup.ts` (imports `@testing-library/jest-dom/vitest`)
- **Config:** `globals: true` -- no imports needed for `describe`, `it`, `expect`
- **Location:** Tests are colocated with source files (e.g., `uiStore.test.ts` next to `uiStore.ts`)
- **Count:** 130 test files across stores (8), services (70), utils (14), components (31), constants (3), router (1), hooks (2), and config (1)
### Zustand test pattern
```ts
beforeEach(() => {
useStore.setState(initialState);
});
it('does something', () => {
useStore.getState().someAction();
expect(useStore.getState().value).toBe(expected);
});
```
## Building
```bash
# Build for your current platform
npm run tauri build
```
Produces native installers:
- **Windows** -- `.msi` / `.exe`
- **macOS** -- `.dmg` / `.app`
- **Linux** -- `.deb` / `.AppImage`
## Email Account Setup
### Gmail (OAuth)
Velo connects directly to Gmail via OAuth. You need your own Google Cloud credentials:
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project (or use an existing one)
3. Enable the **Gmail API** and **Google Calendar API**
4. Create OAuth 2.0 credentials (Desktop application)
5. In Velo's Settings, enter your Client ID
> Velo uses PKCE flow -- no client secret is required.
### IMAP/SMTP
For non-Gmail providers (Outlook, Yahoo, iCloud, Fastmail, etc.):
1. Click the account switcher in the sidebar → **Add IMAP Account**
2. Enter your email address and password (or app-password)
3. Velo auto-discovers server settings for well-known providers
4. For other providers, enter IMAP/SMTP host, port, and security manually
5. Test connection, then save
> No Google Cloud project or Client ID needed. Passwords are encrypted with AES-256-GCM in the local database. Some providers (e.g., Gmail, Yahoo) require an app-specific password instead of your main password.
## AI Setup (Optional)
To enable AI features, add your API key for one or more providers in Settings:
- **Anthropic Claude** -- [Get API key](https://console.anthropic.com/) -- Haiku 4.5 (default), Sonnet 4, Opus 4
- **OpenAI** -- [Get API key](https://platform.openai.com/) -- GPT-4o Mini (default), GPT-4o, GPT-4.1 series
- **Google Gemini** -- [Get API key](https://aistudio.google.com/) -- 2.5 Flash (default), 2.5 Pro
After adding an API key, select which model to use for each provider in Settings > AI.
================================================
FILE: docs/keyboard-shortcuts.md
================================================
# Keyboard Shortcuts
Velo is designed to be used entirely from the keyboard. All shortcuts are customizable in Settings.
## Navigation
| Key | Action |
|-----|--------|
| `j` / `k` | Next / previous thread |
| `o` or `Enter` | Open thread |
| `Escape` | Close composer / clear selection / deselect |
| `g` then `i` | Go to Inbox |
| `g` then `s` | Go to Starred |
| `g` then `t` | Go to Sent |
| `g` then `d` | Go to Drafts |
| `g` then `p` | Go to Primary |
| `g` then `u` | Go to Updates |
| `g` then `o` | Go to Promotions |
| `g` then `c` | Go to Social |
| `g` then `n` | Go to Newsletters |
| `g` then `k` | Go to Tasks |
| `g` then `a` | Go to Attachments |
## Actions
| Key | Action |
|-----|--------|
| `c` | Compose new email |
| `r` | Reply |
| `a` | Reply all |
| `f` | Forward |
| `e` | Archive |
| `s` | Star / unstar |
| `p` | Pin / unpin |
| `m` | Mute / unmute thread |
| `#` | Trash (permanent delete if already in trash) |
| `!` | Spam / not spam |
| `u` | Unsubscribe |
| `t` | Create task from email (AI) |
| `v` | Move to folder/label |
| `Ctrl+Enter` | Send email |
| `Ctrl+A` | Select all threads |
| `Ctrl+Shift+A` | Select all from current position |
## In-thread
| Key | Action |
|-----|--------|
| `↓` (Arrow Down) | Next message in thread |
| `↑` (Arrow Up) | Previous message in thread |
## App
| Key | Action |
|-----|--------|
| `/` or `Ctrl+K` | Command palette |
| `?` | Keyboard shortcuts help |
| `i` | Ask Inbox (AI) |
| `F5` | Sync current folder |
| `Ctrl+Shift+E` | Toggle sidebar |
## Multi-select
- **Click** a thread to toggle its selection
- **Shift+click** to select a range
- All keyboard actions (archive, trash, star, etc.) apply to the entire selection
## Two-key sequences
Velo supports Vim-style two-key sequences. Press the first key, then the second within 1 second:
- `g` is the only prefix key currently
- If the second key isn't pressed in time, the sequence resets
## Customization
All shortcuts can be rebound in **Settings > Keyboard Shortcuts**. Custom bindings are persisted to the local database and restored on startup. Shortcut definitions live in `src/constants/shortcuts.ts`.
================================================
FILE: index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Velo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: landing/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: landing/README.md
================================================
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
================================================
FILE: landing/eslint.config.js
================================================
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
================================================
FILE: landing/index.html
================================================
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta -->
<title>Velo — The email client you'd build for yourself</title>
<meta name="description" content="Free, open-source desktop email client. Keyboard-first, AI-powered, and completely private. Supports Gmail, Outlook, Yahoo, iCloud, and any IMAP provider. Available for Windows, macOS & Linux." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#09090b" />
<meta name="author" content="Velo" />
<meta name="robots" content="index, follow" />
<meta name="keywords" content="email client, desktop email, open source email, AI email, keyboard shortcuts email, privacy email, Gmail client, IMAP email client, Outlook email client, Tauri app, free email client" />
<link rel="canonical" href="https://velomail.app/" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://velomail.app/" />
<meta property="og:title" content="Velo — The email client you'd build for yourself" />
<meta property="og:description" content="Free, open-source desktop email client. Keyboard-first, AI-powered, and completely private. Supports Gmail, Outlook, Yahoo, iCloud & any IMAP provider. Windows, macOS & Linux." />
<meta property="og:image" content="https://velomail.app/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Velo — The email client you'd build for yourself" />
<meta property="og:site_name" content="Velo" />
<meta property="og:locale" content="en_US" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://velomail.app/" />
<meta name="twitter:title" content="Velo — The email client you'd build for yourself" />
<meta name="twitter:description" content="Free, open-source desktop email client. Keyboard-first, AI-powered, and completely private. Supports Gmail, Outlook, Yahoo, iCloud & any IMAP provider." />
<meta name="twitter:image" content="https://velomail.app/og-image.png" />
<meta name="twitter:image:alt" content="Velo — The email client you'd build for yourself" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<!-- Structured Data: SoftwareApplication -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Velo",
"description": "Free, open-source desktop email client. Keyboard-first, AI-powered, and completely private. Built with Tauri for Windows, macOS & Linux.",
"url": "https://velomail.app",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Windows, macOS, Linux",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"softwareVersion": "1.0",
"author": {
"@type": "Organization",
"name": "Velo",
"url": "https://velomail.app"
},
"license": "https://github.com/avihaymenahem/velo/blob/main/LICENSE",
"downloadUrl": "https://github.com/avihaymenahem/velo/releases",
"screenshot": "https://velomail.app/og-image.png",
"featureList": "Multi-provider support (Gmail API + IMAP/SMTP), AI-powered thread summaries, Smart replies, 30+ keyboard shortcuts, Phishing detection, Split inbox categories, Local-first SQLite database, Dark & light themes, Google Calendar integration",
"isAccessibleForFree": true
}
</script>
<!-- Structured Data: WebSite -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Velo",
"url": "https://velomail.app",
"description": "Free, open-source desktop email client"
}
</script>
<!-- Structured Data: FAQPage -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Is Velo free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, Velo is completely free and open source. No subscriptions, no premium tiers, no hidden costs."
}
},
{
"@type": "Question",
"name": "What email providers does Velo support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Velo supports Gmail via the Gmail REST API with OAuth, plus any IMAP/SMTP provider including Outlook, Yahoo, iCloud, Fastmail, Zoho, and more."
}
},
{
"@type": "Question",
"name": "Does Velo store my data in the cloud?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Velo is local-first — all data is stored in a SQLite database on your machine. No analytics, no tracking, no cloud storage."
}
},
{
"@type": "Question",
"name": "What platforms does Velo support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Velo runs on Windows, macOS, and Linux as a native desktop application built with Tauri v2."
}
}
]
}
</script>
</head>
<body class="bg-[#09090b] text-[#fafafa] antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: landing/package.json
================================================
{
"name": "landing",
"private": true,
"license": "Apache-2.0",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.1.18",
"framer-motion": "^12.34.0",
"lenis": "^1.3.17",
"lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
}
================================================
FILE: landing/public/og-image.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800;900&display=swap');
body {
width: 1200px;
height: 630px;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #0A0A0F 0%, #151030 35%, #1a0a2e 65%, #0f172a 100%);
color: #E8E8F0;
overflow: hidden;
position: relative;
}
.blob-1 {
position: absolute;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(99,102,241,0.18) 0%, transparent 70%);
filter: blur(80px);
top: -100px;
left: -50px;
}
.blob-2 {
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(124,58,237,0.14) 0%, transparent 70%);
filter: blur(80px);
bottom: -80px;
right: -30px;
}
.content {
position: relative;
z-index: 1;
padding: 60px 80px;
height: 100%;
display: flex;
flex-direction: column;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
}
.logo-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #4F46E5, #7C3AED);
display: flex;
align-items: center;
justify-content: center;
}
.logo-text {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
.headline {
font-size: 72px;
font-weight: 800;
letter-spacing: -3px;
line-height: 1.05;
margin-bottom: 20px;
}
.headline .gradient {
background: linear-gradient(135deg, #E8E8F0 0%, #818CF8 50%, #7C3AED 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subheadline {
font-size: 22px;
font-weight: 400;
color: #9898B0;
margin-bottom: auto;
}
.stats {
display: flex;
gap: 32px;
margin-bottom: 12px;
}
.stat {
display: flex;
align-items: baseline;
gap: 6px;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: #818CF8;
}
.stat-label {
font-size: 14px;
color: #686880;
font-weight: 500;
}
.domain {
font-size: 18px;
font-weight: 600;
color: #6366F1;
}
/* Mockup */
.mockup {
position: absolute;
right: 40px;
top: 120px;
width: 400px;
height: 380px;
border-radius: 16px;
background: #12121C;
border: 1px solid rgba(255,255,255,0.06);
overflow: hidden;
box-shadow: 0 24px 80px rgba(0,0,0,0.5);
}
.mockup-titlebar {
height: 36px;
background: #16161F;
display: flex;
align-items: center;
padding: 0 14px;
gap: 6px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255,255,255,0.08);
}
.mockup-row {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.mockup-row:first-of-type {
background: rgba(99,102,241,0.04);
}
.mockup-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(99,102,241,0.15);
flex-shrink: 0;
}
.mockup-lines {
flex: 1;
}
.mockup-line {
height: 8px;
border-radius: 4px;
background: rgba(255,255,255,0.08);
margin-bottom: 6px;
}
.mockup-line:last-child {
margin-bottom: 0;
width: 70%;
height: 6px;
background: rgba(255,255,255,0.04);
}
.mockup-line.short { width: 50%; }
.mockup-line.medium { width: 65%; }
</style>
</head>
<body>
<div class="blob-1"></div>
<div class="blob-2"></div>
<div class="content">
<div class="logo">
<div class="logo-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16v16H4z"/>
<path d="M4 4l8 8 8-8"/>
</svg>
</div>
<div class="logo-text">Velo</div>
</div>
<div class="headline">
Email at the<br>
<span class="gradient">speed of thought</span>
</div>
<div class="subheadline">AI-powered, keyboard-first, privacy-focused desktop email.</div>
<div class="stats">
<div class="stat"><span class="stat-value">122+</span><span class="stat-label">Features</span></div>
<div class="stat"><span class="stat-value">30+</span><span class="stat-label">Shortcuts</span></div>
<div class="stat"><span class="stat-value">3</span><span class="stat-label">AI Providers</span></div>
<div class="stat"><span class="stat-value">0</span><span class="stat-label">Tracking</span></div>
</div>
<div class="domain">velomail.app</div>
</div>
<div class="mockup">
<div class="mockup-titlebar">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="mockup-row">
<div class="mockup-avatar"></div>
<div class="mockup-lines">
<div class="mockup-line short"></div>
<div class="mockup-line"></div>
</div>
</div>
<div class="mockup-row">
<div class="mockup-avatar" style="background:rgba(124,58,237,0.12)"></div>
<div class="mockup-lines">
<div class="mockup-line medium"></div>
<div class="mockup-line"></div>
</div>
</div>
<div class="mockup-row">
<div class="mockup-avatar" style="background:rgba(99,102,241,0.1)"></div>
<div class="mockup-lines">
<div class="mockup-line"></div>
<div class="mockup-line short"></div>
</div>
</div>
<div class="mockup-row">
<div class="mockup-avatar" style="background:rgba(99,102,241,0.08)"></div>
<div class="mockup-lines">
<div class="mockup-line short"></div>
<div class="mockup-line medium"></div>
</div>
</div>
<div class="mockup-row">
<div class="mockup-avatar" style="background:rgba(124,58,237,0.08)"></div>
<div class="mockup-lines">
<div class="mockup-line medium"></div>
<div class="mockup-line"></div>
</div>
</div>
</div>
</body>
</html>
================================================
FILE: landing/public/robots.txt
================================================
User-agent: *
Allow: /
Sitemap: https://velomail.app/sitemap.xml
================================================
FILE: landing/public/screenshots/.gitkeep
================================================
================================================
FILE: landing/public/sitemap.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://velomail.app/</loc>
<lastmod>2026-02-13</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
================================================
FILE: landing/src/App.css
================================================
/* Intentionally empty — all styles in index.css and Tailwind */
================================================
FILE: landing/src/App.tsx
================================================
import { useEffect } from 'react'
import Lenis from 'lenis'
import { Navbar } from './components/Navbar'
import { Hero } from './components/Hero'
import { WhyVelo } from './components/WhyVelo'
import { ProductShowcase } from './components/ProductShowcase'
import { Features } from './components/Features'
import { OpenSource } from './components/OpenSource'
import { CtaFooter } from './components/CtaFooter'
import './App.css'
function App() {
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
})
function raf(time: number) {
lenis.raf(time)
requestAnimationFrame(raf)
}
requestAnimationFrame(raf)
return () => lenis.destroy()
}, [])
return (
<div className="relative min-h-screen">
<Navbar />
<main>
<Hero />
<WhyVelo />
<ProductShowcase />
<Features />
<OpenSource />
<CtaFooter />
</main>
</div>
)
}
export default App
================================================
FILE: landing/src/components/CtaFooter.tsx
================================================
import { motion } from 'framer-motion'
import { Download } from 'lucide-react'
export function CtaFooter() {
return (
<section id="download" className="relative">
{/* CTA */}
<div className="relative py-24 md:py-32 px-6 overflow-hidden">
{/* Background glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[700px] h-[400px] bg-accent/[0.06] rounded-full blur-[100px] pointer-events-none" />
<div className="relative max-w-3xl mx-auto text-center">
<motion.h2
className="text-3xl md:text-4xl lg:text-5xl font-light tracking-tight mb-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7 }}
>
<span className="gradient-text">Try Velo</span>
<span className="text-text-primary"> today</span>
</motion.h2>
<motion.p
className="text-text-secondary text-lg max-w-md mx-auto mb-10 leading-relaxed"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.05 }}
>
Free, open source, and ready in two minutes.
</motion.p>
<motion.div
className="mb-8"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<a
href="https://github.com/avihaymenahem/velo/releases"
target="_blank"
rel="noopener noreferrer"
className="btn-primary"
>
<Download size={17} />
Download for Free
</a>
</motion.div>
<motion.div
className="flex items-center justify-center gap-6 text-sm text-text-muted"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<span>Windows</span>
<span className="w-1 h-1 rounded-full bg-text-muted" />
<span>macOS</span>
<span className="w-1 h-1 rounded-full bg-text-muted" />
<span>Linux</span>
</motion.div>
</div>
</div>
{/* Footer */}
<footer className="border-t border-border py-8 px-6">
<div className="max-w-5xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2">
<img src="/logo-white.svg" alt="Velo" className="w-5 h-5 rounded" />
<span className="text-sm text-text-muted">Velo</span>
</div>
<div className="flex items-center gap-6 text-sm text-text-muted">
<a href="https://github.com/avihaymenahem/velo" target="_blank" rel="noopener noreferrer" className="hover:text-text-secondary transition-colors no-underline">
GitHub
</a>
<a href="https://github.com/avihaymenahem/velo/releases" target="_blank" rel="noopener noreferrer" className="hover:text-text-secondary transition-colors no-underline">
Releases
</a>
<a href="mailto:info@velomail.app" className="hover:text-text-secondary transition-colors no-underline">
Contact
</a>
<a href="https://github.com/avihaymenahem/velo/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" className="hover:text-text-secondary transition-colors no-underline">
Apache 2.0
</a>
</div>
</div>
</footer>
</section>
)
}
================================================
FILE: landing/src/components/Features.tsx
================================================
import { motion } from 'framer-motion'
import {
Zap, Search, Clock, Send, Bell, Calendar,
Filter, Layers, GripVertical, PenTool, Shield, Palette,
CheckSquare, PenSquare,
} from 'lucide-react'
const FEATURES = [
{
icon: Zap,
title: 'Quick Steps',
description: '18 action types to automate repetitive workflows. Archive, label, reply, forward — chain actions into one-click sequences.',
},
{
icon: Search,
title: 'Command palette',
description: 'Gmail-style search operators (from:, has:attachment, before:) with fuzzy matching and instant results across all accounts.',
},
{
icon: Clock,
title: 'Snooze & schedule',
description: 'Snooze threads to resurface later. Schedule emails to send at the perfect time. Background checkers handle the rest.',
},
{
icon: Send,
title: 'Undo send',
description: 'Configurable delay window after hitting send. Cancel a message before it actually leaves your outbox.',
},
{
icon: Bell,
title: 'Smart notifications',
description: 'OS-native notifications filtered by VIP senders. Only get alerted for the people who matter.',
},
{
icon: Calendar,
title: 'Calendar integration',
description: 'Google Calendar built in — view events, create meetings, and manage your schedule without switching apps.',
},
{
icon: Filter,
title: 'Filters & rules',
description: 'Auto-apply labels, archive, star, or mark as read. AND-logic criteria with action merging when multiple filters match.',
},
{
icon: Layers,
title: 'Newsletter bundles',
description: 'Group newsletter senders into bundles with delivery schedules. Read them on your terms, not theirs.',
},
{
icon: GripVertical,
title: 'Drag & drop',
description: 'Drag threads onto sidebar labels to organize instantly. Multi-select and bulk operations for power users.',
},
{
icon: PenTool,
title: 'Rich composer',
description: 'TipTap editor with formatting, templates, signatures, attachments, and draft auto-save every 3 seconds.',
},
{
icon: Shield,
title: 'Phishing detection',
description: '10 heuristic rules — homograph attacks, URL shorteners, display mismatch, brand impersonation. Configurable sensitivity.',
},
{
icon: PenSquare,
title: 'AI auto-drafts',
description: 'Reply and the editor is pre-filled with an AI-generated draft that matches your writing style. Learned from your sent emails.',
},
{
icon: CheckSquare,
title: 'Task manager',
description: 'Full task management with priorities, due dates, subtasks, recurring tasks, and AI extraction from emails. Press t to turn any email into a task.',
},
{
icon: Palette,
title: 'Themes & density',
description: '8 accent colors, light & dark mode, 4 density levels, adjustable font scaling. Make it yours.',
},
]
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: { duration: 0.5, delay: i * 0.06 },
}),
}
export function Features() {
return (
<section className="relative py-24 md:py-32 px-6 dot-grid">
{/* Fade edges */}
<div className="absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-bg-primary to-transparent pointer-events-none" />
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-bg-primary to-transparent pointer-events-none" />
<div className="relative max-w-5xl mx-auto">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-light tracking-tight mb-4">
<span className="gradient-text">Everything</span>
<span className="text-text-primary"> you'd expect, and more</span>
</h2>
<p className="text-text-secondary text-lg max-w-xl mx-auto">
130+ features built for people who live in their inbox.
</p>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{FEATURES.map((feature, i) => (
<motion.div
key={feature.title}
className="group rounded-xl border border-white/[0.04] p-5 transition-all duration-300 hover:border-white/[0.08] hover:bg-white/[0.02]"
custom={i}
variants={cardVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<div className="flex items-start gap-3.5">
<div className="w-9 h-9 rounded-lg bg-accent/10 border border-accent/15 flex items-center justify-center flex-shrink-0 transition-colors duration-300 group-hover:bg-accent/15 group-hover:border-accent/25">
<feature.icon size={17} className="text-accent" strokeWidth={1.5} />
</div>
<div className="min-w-0">
<h3 className="text-[15px] font-medium text-text-primary mb-1">{feature.title}</h3>
<p className="text-[13px] text-text-secondary leading-relaxed">{feature.description}</p>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}
================================================
FILE: landing/src/components/Hero.tsx
================================================
import { motion } from 'framer-motion'
import { Download, Github } from 'lucide-react'
import { AppMockup } from './mockups/AppMockup'
export function Hero() {
return (
<section className="relative pt-32 pb-16 md:pt-44 md:pb-24 px-6 overflow-hidden">
{/* Background glow */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-accent/[0.07] rounded-full blur-[120px] pointer-events-none" />
<div className="relative max-w-5xl mx-auto">
{/* Label */}
<motion.div
className="flex justify-center mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="inline-flex items-center gap-2.5 px-4 py-2 rounded-full border border-white/[0.06] bg-white/[0.03]">
<img src="/logo-white.svg" alt="Velo" className="h-4 w-auto" />
<span className="text-sm text-text-secondary">Open source desktop email client</span>
</div>
</motion.div>
{/* Headline */}
<motion.h1
className="text-center text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-light leading-[1.1] tracking-tight mb-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.05 }}
>
<span className="text-text-primary">The email client </span>
<br />
<span className="gradient-text">you'd build for yourself</span>
</motion.h1>
{/* Subline */}
<motion.p
className="text-center text-lg md:text-xl text-text-secondary max-w-xl mx-auto mb-10 leading-relaxed"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
Keyboard-first, AI-powered, and completely private.
<br className="hidden sm:block" />
Free forever because it's open source.
</motion.p>
{/* CTAs */}
<motion.div
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-20 md:mb-28"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.15 }}
>
<a href="https://github.com/avihaymenahem/velo/releases" target="_blank" rel="noopener noreferrer" className="btn-primary">
<Download size={17} />
Download for Free
</a>
<a href="https://github.com/avihaymenahem/velo" target="_blank" rel="noopener noreferrer" className="btn-secondary">
<Github size={16} />
View on GitHub
</a>
</motion.div>
{/* App mockup with glow behind */}
<motion.div
className="relative"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.25, ease: [0.16, 1, 0.3, 1] }}
>
{/* Glow behind mockup */}
<div className="absolute -inset-4 bg-accent/[0.06] rounded-2xl blur-[60px] pointer-events-none" />
<div className="relative mockup-hover">
<AppMockup />
</div>
</motion.div>
</div>
</section>
)
}
================================================
FILE: landing/src/components/Navbar.tsx
================================================
import { useState, useCallback } from 'react'
import { motion, useScroll, useMotionValueEvent } from 'framer-motion'
import { Menu, X, Github } from 'lucide-react'
const NAV_LINKS = [
{ label: 'Features', href: '#features' },
{ label: 'Open Source', href: '#open-source' },
{ label: 'Download', href: '#download' },
]
export function Navbar() {
const [isScrolled, setIsScrolled] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const { scrollY } = useScroll()
useMotionValueEvent(scrollY, 'change', (latest) => {
setIsScrolled(latest > 50)
})
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault()
const el = document.querySelector(href)
el?.scrollIntoView({ behavior: 'smooth' })
setMobileOpen(false)
}, [])
return (
<motion.header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? 'nav-blur' : ''
}`}
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
<nav className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
<a href="#" className="flex items-center gap-2.5 text-text-primary no-underline">
<img src="/logo-white.svg" alt="Velo" className="w-7 h-7 rounded-md" />
<span className="font-semibold text-lg tracking-tight">Velo</span>
</a>
<div className="hidden md:flex items-center gap-8">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
onClick={(e) => handleNavClick(e, link.href)}
className="text-sm text-text-secondary hover:text-text-primary transition-colors duration-200 no-underline"
>
{link.label}
</a>
))}
</div>
<div className="hidden md:flex items-center gap-3">
<a
href="https://github.com/avihaymenahem/velo"
target="_blank"
rel="noopener noreferrer"
className="btn-secondary !py-2 !px-4 !text-sm"
>
<Github size={15} />
GitHub
</a>
<a
href="https://github.com/avihaymenahem/velo/releases"
target="_blank"
rel="noopener noreferrer"
className="btn-primary !py-2 !px-4 !text-sm"
>
Download
</a>
</div>
<button
className="md:hidden p-2 text-text-secondary hover:text-text-primary transition-colors bg-transparent border-none cursor-pointer"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
{mobileOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</nav>
{mobileOpen && (
<motion.div
className="md:hidden nav-blur border-t border-border px-6 py-4"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<div className="flex flex-col gap-4">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
onClick={(e) => handleNavClick(e, link.href)}
className="text-text-secondary hover:text-text-primary transition-colors py-1 no-underline"
>
{link.label}
</a>
))}
<a href="https://github.com/avihaymenahem/velo" target="_blank" rel="noopener noreferrer" className="btn-secondary text-sm !py-2.5 mt-2 justify-center">
<Github size={16} />
GitHub
</a>
<a href="https://github.com/avihaymenahem/velo/releases" target="_blank" rel="noopener noreferrer" className="btn-primary text-sm !py-2.5 justify-center">
Download
</a>
</div>
</motion.div>
)}
</motion.header>
)
}
================================================
FILE: landing/src/components/OpenSource.tsx
================================================
import { motion } from 'framer-motion'
import { Github, Code2, EyeOff, HardDrive, Lock } from 'lucide-react'
const TRUST_SIGNALS = [
{ icon: Code2, label: 'Open source' },
{ icon: EyeOff, label: 'No tracking' },
{ icon: HardDrive, label: 'Local database' },
{ icon: Lock, label: 'AES-256 encryption' },
]
export function OpenSource() {
return (
<section id="open-source" className="relative py-24 md:py-32 px-6 overflow-hidden">
{/* Background glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-accent/[0.05] rounded-full blur-[100px] pointer-events-none" />
<div className="relative max-w-3xl mx-auto text-center">
<motion.h2
className="text-3xl md:text-4xl lg:text-5xl font-light tracking-tight mb-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7 }}
>
<span className="gradient-text">Open source</span>
<span className="text-text-primary"> and free forever</span>
</motion.h2>
<motion.p
className="text-text-secondary text-lg max-w-xl mx-auto mb-12 leading-relaxed"
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.05 }}
>
Every line of code is public, licensed under Apache 2.0. No telemetry, no data collection, no cloud dependency. Your email stays on your machine.
</motion.p>
<motion.div
className="flex flex-wrap items-center justify-center gap-8 md:gap-12 mb-12"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
>
{TRUST_SIGNALS.map((signal) => (
<div key={signal.label} className="flex items-center gap-2.5 text-text-secondary">
<div className="w-8 h-8 rounded-lg bg-accent/10 border border-accent/20 flex items-center justify-center">
<signal.icon size={15} className="text-accent" strokeWidth={1.5} />
</div>
<span className="text-sm">{signal.label}</span>
</div>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<a
href="https://github.com/avihaymenahem/velo"
target="_blank"
rel="noopener noreferrer"
className="btn-secondary"
>
<Github size={16} />
Star on GitHub
</a>
</motion.div>
</div>
</section>
)
}
================================================
FILE: landing/src/components/ProductShowcase.tsx
================================================
import { type ReactNode } from 'react'
import { motion } from 'framer-motion'
import { SplitInboxMockup } from './mockups/SplitInboxMockup'
import { AiMockup } from './mockups/AiMockup'
import { MultiProviderMockup } from './mockups/MultiProviderMockup'
const FEATURES: { title: string; description: string; mockup: ReactNode }[] = [
{
title: 'Split inbox',
description:
'Emails are auto-sorted into Primary, Updates, Promotions, Social, and Newsletters. Rule-based categorization with AI fallback keeps your inbox organized without manual effort.',
mockup: <SplitInboxMockup />,
},
{
title: 'AI assistant',
description:
'Choose from Claude, GPT, or Gemini. Get thread summaries, smart reply suggestions, auto-drafted replies that match your writing style, AI task extraction, and natural-language inbox search — all running through your own API key.',
mockup: <AiMockup />,
},
{
title: 'Multi-provider',
description:
'Connect Gmail, Outlook, Yahoo, iCloud, Fastmail, or any IMAP server. Auto-discovery for major providers. Manage all your accounts from a single unified inbox.',
mockup: <MultiProviderMockup />,
},
]
export function ProductShowcase() {
return (
<section className="py-24 md:py-32 px-6">
<div className="max-w-5xl mx-auto flex flex-col gap-32 md:gap-40">
{FEATURES.map((feature, i) => {
const reversed = i % 2 !== 0
return (
<motion.div
key={feature.title}
className={`flex flex-col gap-10 md:gap-16 items-center ${
reversed ? 'md:flex-row-reverse' : 'md:flex-row'
}`}
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
>
{/* Text */}
<div className="md:w-5/12 flex-shrink-0">
<h3 className="text-2xl md:text-3xl font-light tracking-tight mb-4">
<span className="gradient-text">{feature.title}</span>
</h3>
<p className="text-text-secondary leading-relaxed">
{feature.description}
</p>
</div>
{/* Mockup */}
<div className="md:w-7/12 w-full mockup-hover">
{feature.mockup}
</div>
</motion.div>
)
})}
</div>
</section>
)
}
================================================
FILE: landing/src/components/WhyVelo.tsx
================================================
import { motion } from 'framer-motion'
import { Keyboard, Brain, ShieldCheck } from 'lucide-react'
const DIFFERENTIATORS = [
{
icon: Keyboard,
title: 'Keyboard-first',
description:
'30+ shortcuts, two-key sequences, and a command palette. Navigate your entire inbox without touching the mouse. Fully customizable.',
},
{
icon: Brain,
title: 'AI on your terms',
description:
'Claude, GPT, or Gemini — pick your provider, use your own API key. Summaries, smart replies, and auto-categorization. Your data never leaves your machine.',
},
{
icon: ShieldCheck,
title: 'Local-first privacy',
description:
'Everything stored in a local SQLite database. No cloud, no tracking, no analytics. Built-in phishing detection and AES-256 encryption.',
},
]
const cardVariants = {
hidden: { opacity: 0, y: 24 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: { duration: 0.6, delay: i * 0.15, ease: [0.16, 1, 0.3, 1] as [number, number, number, number] },
}),
}
export function WhyVelo() {
return (
<section id="features" className="relative py-24 md:py-32 px-6 dot-grid">
{/* Subtle top/bottom fade to blend the dot grid */}
<div className="absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-bg-primary to-transparent pointer-events-none" />
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-bg-primary to-transparent pointer-events-none" />
<div className="relative max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
{DIFFERENTIATORS.map((item, i) => (
<motion.div
key={item.title}
className="group rounded-xl border border-transparent p-6 transition-colors duration-300 hover:border-white/[0.06] hover:bg-white/[0.02]"
custom={i}
variants={cardVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<div className="w-10 h-10 rounded-lg bg-accent/10 border border-accent/20 flex items-center justify-center mb-4 transition-colors duration-300 group-hover:bg-accent/15 group-hover:border-accent/30">
<item.icon size={20} className="text-accent" strokeWidth={1.5} />
</div>
<h3 className="text-lg font-medium text-text-primary mb-2">{item.title}</h3>
<p className="text-text-secondary leading-relaxed text-[15px]">{item.description}</p>
</motion.div>
))}
</div>
</section>
)
}
================================================
FILE: landing/src/components/mockups/AiMockup.tsx
================================================
/** AI assistant mockup showing thread summary, smart replies, and inline reply with AI assist */
import { Sparkles, RefreshCw, Wand2, Send } from 'lucide-react'
export function AiMockup() {
return (
<div className="rounded-xl border border-white/[0.08] bg-[#0e0e11] overflow-hidden shadow-2xl shadow-black/50">
{/* Thread header */}
<div className="px-4 py-3 border-b border-white/[0.06]">
<div className="text-[13px] text-zinc-200 font-medium">Re: Partnership proposal — Acme Corp</div>
<div className="flex items-center gap-2 mt-1">
<div className="w-5 h-5 rounded-full bg-emerald-500/20 flex items-center justify-center text-[8px] text-emerald-400 font-medium">J</div>
<span className="text-[11px] text-zinc-400">Julia Martinez</span>
<span className="text-[10px] text-zinc-700">· 3 messages</span>
</div>
</div>
{/* AI Summary */}
<div className="mx-3 mt-3 p-3 rounded-lg bg-indigo-500/5 border border-indigo-500/15">
<div className="flex items-center gap-1.5 mb-2">
<Sparkles size={12} className="text-indigo-400" />
<span className="text-[11px] text-indigo-400 font-medium">AI Summary</span>
<button className="ml-auto p-0.5 rounded text-zinc-600 hover:text-zinc-400">
<RefreshCw size={10} />
</button>
</div>
<p className="text-[11px] text-zinc-400 leading-relaxed">
Julia proposes a partnership with Acme Corp for Q2. Key terms: 15% revenue share, 12-month commitment, joint marketing campaign. She's requesting a meeting next week to discuss details.
</p>
</div>
{/* Message preview */}
<div className="px-4 py-3">
<p className="text-[11px] text-zinc-500 leading-relaxed">
Hi Alex,<br /><br />
Following up on our conversation at the conference. I'd love to schedule a call to discuss the partnership terms in detail. Would Thursday or Friday work for you?<br /><br />
I've attached the proposed agreement for your review.<br /><br />
Best,<br />Julia
</p>
</div>
{/* Smart replies */}
<div className="px-3 pb-3 border-t border-white/[0.06] pt-2.5">
<div className="flex items-center gap-1.5 mb-2">
<Sparkles size={10} className="text-indigo-400" />
<span className="text-[10px] text-indigo-400 font-medium">Quick Replies</span>
</div>
<div className="flex flex-wrap gap-1.5">
{[
'Thursday works! Let\'s meet at 2pm.',
'I\'ll review the agreement and get back to you.',
'Can we also invite the legal team?',
].map((reply) => (
<button key={reply} className="px-2.5 py-1.5 rounded-full border border-white/[0.08] bg-white/[0.02] text-[10px] text-zinc-400 hover:border-indigo-500/30 transition-colors">
{reply}
</button>
))}
</div>
</div>
{/* Inline reply composer with AI assist */}
<div className="mx-3 mb-3 rounded-lg border border-white/[0.06] bg-white/[0.02]">
<div className="px-3 py-2.5">
<p className="text-[11px] text-zinc-300 leading-relaxed">
Hi Julia,<br /><br />
Thursday at 2pm works perfectly. I'll review the proposed agreement before our call so we can dive right into the details.<br /><br />
Would it be alright if I include our legal counsel? It would help streamline the process.
</p>
</div>
<div className="flex items-center gap-2 px-3 py-2 border-t border-white/[0.04]">
<button className="flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-indigo-500/10 border border-indigo-500/20 text-[10px] text-indigo-400 font-medium">
<Wand2 size={10} />
AI Assist
</button>
<div className="flex-1" />
<button className="flex items-center gap-1.5 px-3 py-1 rounded-md bg-indigo-500 text-white text-[10px] font-medium">
<Send size={9} />
Send
</button>
</div>
</div>
</div>
)
}
================================================
FILE: landing/src/components/mockups/AppMockup.tsx
================================================
/** Full app layout mockup for the hero section — sidebar + email list + reading pane */
import {
Inbox, Send, FileText, Trash2, Archive, Star, AlertTriangle,
Keyboard, Brain, Sparkles, Paperclip, ChevronDown, Search,
Settings, HelpCircle, PanelLeftClose, Plus,
} from 'lucide-react'
const SIDEBAR_ITEMS = [
{ icon: Inbox, label: 'Inbox', count: 12, active: true },
{ icon: Star, label: 'Starred', count: 3 },
{ icon: Send, label: 'Sent' },
{ icon: FileText, label: 'Drafts', count: 1 },
{ icon: Archive, label: 'Archive' },
{ icon: AlertTriangle, label: 'Spam' },
{ icon: Trash2, label: 'Trash' },
]
const LABELS = [
{ name: 'Work', color: '#6366F1' },
{ name: 'Personal', color: '#34D399' },
{ name: 'Finance', color: '#FBBF24' },
]
const THREADS = [
{ sender: 'Alex Chen', subject: 'Q1 product roadmap review', snippet: 'Hey team, here are the updated milestones for...', time: '10:32 AM', unread: true, avatar: 'A', starred: true },
{ sender: 'Sarah Kim', subject: 'Design system updates', snippet: 'The new component library is ready for review...', time: '9:15 AM', unread: true, avatar: 'S', category: 'Updates' },
{ sender: 'GitHub', subject: 'PR #142 merged successfully', snippet: 'Your pull request has been merged into main...', time: '8:45 AM', unread: false, avatar: 'G', category: 'Updates' },
{ sender: 'David Park', subject: 'Re: Weekend plans', snippet: 'Sounds great! Let me check my calendar and...', time: 'Yesterday', unread: false, avatar: 'D' },
{ sender: 'Stripe', subject: 'Your monthly invoice', snippet: 'Your invoice for January 2026 is ready...', time: 'Yesterday', unread: false, avatar: 'S', category: 'Promotions' },
{ sender: 'Maria Lopez', subject: 'Client presentation feedback', snippet: 'Great job on the deck! A few suggestions...', time: 'Yesterday', unread: false, avatar: 'M', attachment: true },
{ sender: 'Newsletter', subject: 'This Week in Tech', snippet: 'The biggest stories in technology this week...', time: 'Jan 28', unread: false, avatar: 'N', category: 'Newsletters' },
]
const MESSAGE_BODY = `Hi team,
I've put together the updated roadmap for Q1. Here are the key milestones:
1. Component library v2 — Feb 15
2. API redesign rollout — Mar 1
3. Mobile app beta — Mar 20
Let me know your thoughts on the timeline. I think we can hit all three if we prioritize the API work first.
Best,
Alex`
export function AppMockup() {
return (
<div className="rounded-xl border border-white/[0.08] bg-[#09090b] overflow-hidden shadow-2xl shadow-black/50">
{/* Title bar */}
<div className="flex items-center h-8 px-3 bg-[#0f0f12] border-b border-white/[0.06]">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-[#ff5f57]" />
<div className="w-2.5 h-2.5 rounded-full bg-[#febc2e]" />
<div className="w-2.5 h-2.5 rounded-full bg-[#28c840]" />
</div>
<span className="text-[10px] text-zinc-500 mx-auto">Velo</span>
</div>
<div className="flex h-[420px] md:h-[480px]">
{/* Sidebar */}
<div className="hidden sm:flex w-48 flex-col border-r border-white/[0.06] bg-[#0c0c0f] py-2">
{/* Account */}
<div className="flex items-center gap-2 px-3 py-2 mx-2 rounded-lg hover:bg-white/[0.03]">
<div className="w-7 h-7 rounded-full bg-indigo-500/20 flex items-center justify-center text-[11px] text-indigo-400 font-medium">A</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] text-zinc-300 font-medium truncate">Alex Chen</div>
<div className="text-[9px] text-zinc-600 truncate">alex@company.com</div>
</div>
<ChevronDown size={10} className="text-zinc-600" />
</div>
{/* Compose */}
<button className="mx-3 mt-2 mb-1 py-1.5 rounded-lg bg-indigo-500 text-white text-[11px] font-medium flex items-center justify-center gap-1.5">
<Plus size={12} /> Compose
</button>
{/* Nav items */}
<div className="mt-1 px-2 flex flex-col gap-0.5">
{SIDEBAR_ITEMS.map((item) => (
<div
key={item.label}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-[11px] ${
item.active
? 'bg-indigo-500/10 text-indigo-400 font-medium'
: 'text-zinc-500 hover:bg-white/[0.03]'
}`}
>
<item.icon size={13} />
<span className="flex-1">{item.label}</span>
{item.count && (
<span className={`text-[9px] px-1.5 rounded-full ${
item.active ? 'bg-indigo-500/15 text-indigo-400' : 'bg-zinc-800 text-zinc-500'
}`}>{item.count}</span>
)}
</div>
))}
</div>
{/* Labels */}
<div className="mt-3 px-4">
<div className="text-[9px] text-zinc-600 uppercase tracking-wider font-medium mb-1.5">Labels</div>
{LABELS.map((label) => (
<div key={label.name} className="flex items-center gap-2 py-1 text-[11px] text-zinc-500">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: label.color }} />
{label.name}
</div>
))}
</div>
{/* Bottom */}
<div className="mt-auto px-2 flex items-center gap-1 border-t border-white/[0.04] pt-2">
<button className="p-1.5 rounded-md text-zinc-600 hover:bg-white/[0.03]"><Settings size={12} /></button>
<button className="p-1.5 rounded-md text-zinc-600 hover:bg-white/[0.03]"><HelpCircle size={12} /></button>
<div className="flex-1" />
<button className="p-1.5 rounded-md text-zinc-600 hover:bg-white/[0.03]"><PanelLeftClose size={12} /></button>
</div>
</div>
{/* Email list */}
<div className="w-full sm:w-64 md:w-72 flex-shrink-0 border-r border-white/[0.06] bg-[#0e0e11] flex flex-col">
{/* Search */}
<div className="px-3 py-2 border-b border-white/[0.06]">
<div className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-white/[0.03] border border-white/[0.06]">
<Search size={12} className="text-zinc-600" />
<span className="text-[11px] text-zinc-600">Search emails...</span>
<span className="ml-auto text-[9px] text-zinc-700 bg-zinc-800/50 rounded px-1 py-0.5">/</span>
</div>
</div>
{/* Thread list */}
<div className="flex-1 overflow-hidden">
{THREADS.map((thread, i) => (
<div
key={i}
className={`px-3 py-2.5 border-b border-white/[0.04] cursor-pointer ${
i === 0 ? 'bg-white/[0.04]' : 'hover:bg-white/[0.02]'
}`}
>
<div className="flex items-center gap-2">
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-medium flex-shrink-0 ${
thread.unread ? 'bg-indigo-500 text-white' : 'bg-zinc-800 text-zinc-500'
}`}>{thread.avatar}</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<span className={`text-[11px] truncate ${thread.unread ? 'text-zinc-200 font-semibold' : 'text-zinc-500'}`}>{thread.sender}</span>
<span className="text-[9px] text-zinc-600 ml-2 flex-shrink-0">{thread.time}</span>
</div>
<div className={`text-[11px] truncate ${thread.unread ? 'text-zinc-300' : 'text-zinc-600'}`}>{thread.subject}</div>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] text-zinc-700 truncate flex-1">{thread.snippet}</span>
{thread.starred && <Star size={9} className="text-amber-400 fill-amber-400 flex-shrink-0" />}
{thread.attachment && <Paperclip size={9} className="text-zinc-600 flex-shrink-0" />}
{thread.category && (
<span className={`text-[8px] px-1 rounded flex-shrink-0 ${
thread.category === 'Updates' ? 'bg-yellow-500/10 text-yellow-500' :
thread.category === 'Promotions' ? 'bg-emerald-500/10 text-emerald-500' :
'bg-orange-500/10 text-orange-500'
}`}>{thread.category}</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Reading pane */}
<div className="hidden md:flex flex-1 flex-col bg-[#0b0b0e]">
{/* Action bar */}
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-white/[0.06]">
{[Archive, Trash2, Star, Keyboard, Brain].map((Icon, i) => (
<button key={i} className="p-1.5 rounded-md text-zinc-600 hover:bg-white/[0.04]">
<Icon size={13} />
</button>
))}
</div>
{/* Thread header */}
<div className="px-4 py-3 border-b border-white/[0.06]">
<h3 className="text-[13px] text-zinc-200 font-medium">Q1 product roadmap review</h3>
<div className="flex items-center gap-2 mt-1">
<div className="w-5 h-5 rounded-full bg-indigo-500/20 flex items-center justify-center text-[8px] text-indigo-400 font-medium">A</div>
<span className="text-[11px] text-zinc-400">Alex Chen</span>
<span className="text-[9px] text-zinc-600">to me</span>
<span className="text-[9px] text-zinc-700 ml-auto">10:32 AM</span>
</div>
</div>
{/* AI Summary */}
<div className="mx-3 mt-2 p-2.5 rounded-lg bg-indigo-500/5 border border-indigo-500/15">
<div className="flex items-center gap-1.5 mb-1.5">
<Sparkles size={11} className="text-indigo-400" />
<span className="text-[10px] text-indigo-400 font-medium">AI Summary</span>
</div>
<p className="text-[10px] text-zinc-400 leading-relaxed">
Alex shares the Q1 roadmap with 3 milestones: component library v2 (Feb 15), API redesign (Mar 1), and mobile beta (Mar 20). Suggests prioritizing API work.
</p>
</div>
{/* Message body */}
<div className="flex-1 px-4 py-3 overflow-hidden">
<pre className="text-[11px] text-zinc-400 leading-relaxed whitespace-pre-wrap font-sans">{MESSAGE_BODY}</pre>
</div>
{/* Smart replies */}
<div className="px-3 pb-2.5">
<div className="flex items-center gap-1.5 mb-1.5">
<Sparkles size={10} className="text-indigo-400" />
<span className="text-[9px] text-indigo-400 font-medium">Quick Replies</span>
</div>
<div className="flex flex-wrap gap-1.5">
{['Looks good, let\'s proceed!', 'Can we discuss the timeline?', 'I have a few concerns'].map((reply) => (
<button key={reply} className="px-2.5 py-1 rounded-full border border-white/[0.08] bg-white/[0.02] text-[9px] text-zinc-400 hover:border-indigo-500/30">
{reply}
</button>
))}
</div>
</div>
</div>
</div>
</div>
)
}
================================================
FILE: landing/src/components/mockups/MultiProviderMockup.tsx
================================================
/** Multi-provider mockup showing account switcher + provider logos */
import { Check, ChevronDown, Plus, Mail, Globe } from 'lucide-react'
const ACCOUNTS = [
{ name: 'Alex Chen', email: 'alex@company.com', provider: 'Gmail', avatar: 'A', color: 'bg-indigo-500/20 text-indigo-400', active: true },
{ name: 'Alex Chen', email: 'alex.chen@outlook.com', provider: 'Outlook', avatar: 'A', color: 'bg-sky-500/20 text-sky-400' },
{ name: 'Alex Personal', email: 'alex@icloud.com', provider: 'iCloud', avatar: 'A', color: 'bg-zinc-500/20 text-zinc-400' },
]
const PROVIDERS = [
{ name: 'Gmail', desc: 'Google OAuth', icon: Mail },
{ name: 'Outlook', desc: 'IMAP/SMTP', icon: Mail },
{ name: 'Yahoo', desc: 'IMAP/SMTP', icon: Mail },
{ name: 'iCloud', desc: 'IMAP/SMTP', icon: Mail },
{ name: 'Fastmail', desc: 'IMAP/SMTP', icon: Mail },
{ name: 'Any IMAP', desc: 'Custom server', icon: Globe },
]
export function MultiProviderMockup() {
return (
<div className="rounded-xl border border-white/[0.08] bg-[#0e0e11] overflow-hidden shadow-2xl shadow-black/50">
<div className="flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x divide-white/[0.06]">
{/* Left: Account switcher */}
<div className="flex-1">
{/* Current account */}
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-white/[0.06]">
<div className="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center text-[12px] text-indigo-400 font-medium">A</div>
<div className="min-w-0 flex-1">
<div className="text-[12px] text-zinc-200 font-medium">Alex Chen</div>
<div className="text-[10px] text-zinc-600">alex@company.com</div>
</div>
<ChevronDown size={14} className="text-zinc-600" />
</div>
{/* Account list */}
<div className="p-2">
<div className="text-[9px] text-zinc-600 uppercase tracking-wider font-medium px-2 py-1.5">Accounts</div>
{ACCOUNTS.map((account) => (
<div
key={account.email}
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-lg ${
account.active ? 'bg-indigo-500/8' : 'hover:bg-white/[0.03]'
}`}
>
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-medium ${account.color}`}>
{account.avatar}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-zinc-300 truncate">{account.name}</span>
<span className={`text-[8px] px-1.5 py-0.5 rounded-full ${
account.provider === 'Gmail' ? 'bg-red-500/10 text-red-400' :
account.provider === 'Outlook' ? 'bg-sky-500/10 text-sky-400' :
'bg-zinc-700/50 text-zinc-500'
}`}>{account.provider}</span>
</div>
<div className="text-[10px] text-zinc-600 truncate">{account.email}</div>
</div>
{account.active && <Check size={12} className="text-indigo-400 flex-shrink-0" />}
</div>
))}
{/* Add account */}
<div className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg hover:bg-white/[0.03] mt-1 border-t border-white/[0.04] pt-3">
<div className="w-7 h-7 rounded-full bg-zinc-800 flex items-center justify-center">
<Plus size={12} className="text-zinc-500" />
</div>
<span className="text-[11px] text-zinc-500">Add account</span>
</div>
</div>
</div>
{/* Right: Supported providers */}
<div className="w-full md:w-56 bg-[#0c0c0f]">
<div className="px-4 py-3 border-b border-white/[0.06]">
<div className="text-[12px] text-zinc-300 font-medium">Supported providers</div>
<div className="text-[10px] text-zinc-600 mt-0.5">Auto-discovery for major providers</div>
</div>
<div className="p-2">
{PROVIDERS.map((provider) => (
<div key={provider.name} className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg hover:bg-white/[0.03]">
<div className="w-7 h-7 rounded-lg bg-white/[0.04] border border-white/[0.06] flex items-center justify-center">
<provider.icon size={13} className="text-zinc-500" />
</div>
<div>
<div className="text-[11px] text-zinc-400">{provider.name}</div>
<div className="text-[9px] text-zinc-700">{provider.desc}</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
================================================
FILE: landing/src/components/mockups/SplitInboxMockup.tsx
================================================
/** Split inbox mockup showing category tabs + categorized threads */
import { Inbox, Bell, Tag, Users, Newspaper, Paperclip, Star } from 'lucide-react'
const TABS = [
{ icon: Inbox, label: 'Primary', count: 8, active: true },
{ icon: Bell, label: 'Updates', count: 3 },
{ icon: Tag, label: 'Promotions', count: 5 },
{ icon: Users, label: 'Social', count: 2 },
{ icon: Newspaper, label: 'Newsletters' },
]
const THREADS = [
{ sender: 'Alex Chen', subject: 'Q1 product roadmap review', snippet: 'Hey team, here are the updated milestones for next quarter...', time: '10:32 AM', unread: true, avatar: 'A', starred: true },
{ sender: 'Sarah Kim', subject: 'Design review meeting notes', snippet: 'Here are the action items from today\'s design review...', time: '9:15 AM', unread: true, avatar: 'S' },
{ sender: 'David Park', subject: 'Re: API integration spec', snippet: 'I\'ve reviewed the spec and have a few questions about the...', time: '8:45 AM', unread: true, avatar: 'D', attachment: true },
{ sender: 'Maria Lopez', subject: 'Client onboarding checklist', snippet: 'The updated checklist is attached. Please review before...', time: 'Yesterday', unread: false, avatar: 'M', attachment: true },
{ sender: 'James Wilson', subject: 'Budget approval for Q2', snippet: 'Hi team, I need approval for the following budget items...', time: 'Yesterday', unread: false, avatar: 'J' },
{ sender: 'Emily Zhang', subject: 'New hire orientation schedule', snippet: 'Welcome aboard! Here\'s the schedule for your first week...', time: 'Jan 28', unread: false, avatar: 'E' },
]
export function SplitInboxMockup() {
return (
<div className="rounded-xl border border-white/[0.08] bg-[#0e0e11] overflow-hidden shadow-2xl shadow-black/50">
{/* Category tabs */}
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-white/[0.06] overflow-x-auto">
{TABS.map((tab) => (
<button
key={tab.label}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] font-medium whitespace-nowrap transition-colors ${
tab.active
? 'text-indigo-400 bg-indigo-500/10'
: 'text-zinc-600 hover:text-zinc-400 hover:bg-white/[0.03]'
}`}
>
<tab.icon size={12} />
{tab.label}
{tab.count && (
<span className={`text-[9px] px-1.5 rounded-full ${
tab.active ? 'bg-indigo-500/20 text-indigo-400' : 'bg-zinc-800 text-zinc-600'
}`}>{tab.count}</span>
)}
</button>
))}
</div>
{/* Thread list */}
<div>
{THREADS.map((thread, i) => (
<div
key={i}
className={`px-4 py-3 border-b border-white/[0.04] ${
i === 0 ? 'bg-white/[0.03]' : ''
}`}
>
<div className="flex items-start gap-2.5">
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-medium flex-shrink-0 mt-0.5 ${
thread.unread ? 'bg-indigo-500 text-white' : 'bg-zinc-800 text-zinc-500'
}`}>{thread.avatar}</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<span className={`text-[12px] ${thread.unread ? 'text-zinc-200 font-semibold' : 'text-zinc-500'}`}>{thread.sender}</span>
<span className="text-[10px] text-zinc-600 ml-2 flex-shrink-0">{thread.time}</span>
</div>
<div className={`text-[12px] truncate mt-0.5 ${thread.unread ? 'text-zinc-300' : 'text-zinc-600'}`}>{thread.subject}</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] text-zinc-700 truncate">{thread.snippet}</span>
{thread.starred && <Star size={10} className="text-amber-400 fill-amber-400 flex-shrink-0" />}
{thread.attachment && <Paperclip size={10} className="text-zinc-600 flex-shrink-0" />}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
}
================================================
FILE: landing/src/index.css
================================================
@import "tailwindcss";
@theme {
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--color-bg-primary: #09090b;
--color-bg-secondary: #18181b;
--color-text-primary: #fafafa;
--color-text-secondary: #a1a1aa;
--color-text-muted: #71717a;
--color-accent: #6366F1;
--color-accent-hover: #818CF8;
--color-border: rgba(255, 255, 255, 0.08);
}
/* ── Base ── */
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--font-sans);
overflow-x: hidden;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
::selection {
background-color: rgba(99, 102, 241, 0.3);
color: #fff;
}
/* ── Scrollbar ── */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* ── Dot grid background ── */
.dot-grid {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
}
/* ── Gradient text ── */
.gradient-text {
background: linear-gradient(135deg, #e0e7ff 0%, #818CF8 50%, #6366F1 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ── Primary Button ── */
.btn-primary {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--color-accent);
border-radius: 10px;
padding: 12px 28px;
font-weight: 500;
font-size: 15px;
color: white;
cursor: pointer;
border: none;
text-decoration: none;
transition: all 0.25s ease;
}
.btn-primary::before {
content: '';
position: absolute;
inset: -1px;
border-radius: 11px;
background: var(--color-accent);
z-index: -1;
filter: blur(16px);
opacity: 0.35;
transition: opacity 0.25s ease;
}
.btn-primary:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
box-shadow: 0 4px 24px rgba(99, 102, 241, 0.25);
}
.btn-primary:hover::before {
opacity: 0.5;
}
.btn-primary:active {
transform: translateY(0);
}
/* ── Secondary Button ── */
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
font-weight: 500;
font-size: 15px;
color: var(--color-text-secondary);
background: transparent;
border: 1px solid var(--color-border);
border-radius: 10px;
cursor: pointer;
text-decoration: none;
transition: all 0.25s ease;
}
.btn-secondary:hover {
color: var(--color-text-primary);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.btn-secondary:active {
transform: translateY(0);
}
/* ── Nav ── */
.nav-blur {
backdrop-filter: blur(16px) saturate(180%);
background: rgba(9, 9, 11, 0.85);
border-bottom: 1px solid var(--color-border);
}
/* ── Mockup hover lift ── */
.mockup-hover {
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s ease;
}
.mockup-hover:hover {
transform: translateY(-4px);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.4),
0 0 80px rgba(99, 102, 241, 0.08);
}
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
================================================
FILE: landing/src/main.tsx
================================================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
================================================
FILE: landing/tsconfig.app.json
================================================
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
================================================
FILE: landing/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: landing/tsconfig.node.json
================================================
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: landing/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
================================================
FILE: landing/wrangler.jsonc
================================================
{
"name": "velomail",
"compatibility_date": "2026-02-13",
"assets": {
"directory": "./dist"
}
}
================================================
FILE: package.json
================================================
{
"name": "velo",
"version": "0.4.21",
"private": true,
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"tauri": "tauri",
"dev:landing": "npm run dev --prefix landing",
"flatpak": "flatpak-builder --force-clean --user --install --install-deps-from=flathub build-dir com.velomail.app.yml"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"@dnd-kit/core": "^6.3.1",
"@google/generative-ai": "^0.24.1",
"@tanstack/react-router": "^1.159.5",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-autostart": "^2.0.0",
"@tauri-apps/plugin-deep-link": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
"@tauri-apps/plugin-http": "^2.5.7",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-sql": "^2.3.2",
"@tauri-apps/plugin-updater": "^2.10.0",
"@tiptap/extension-color": "^3.19.0",
"@tiptap/extension-highlight": "^3.19.0",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-placeholder": "^3.19.0",
"@tiptap/extension-text-align": "^3.19.0",
"@tiptap/extension-text-style": "^3.19.0",
"@tiptap/extension-underline": "^3.19.0",
"@tiptap/pm": "^3.19.0",
"@tiptap/react": "^3.19.0",
"@tiptap/starter-kit": "^3.19.0",
"dompurify": "^3.3.1",
"lucide-react": "^0.563.0",
"openai": "^6.21.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-transition-group": "^4.4.5",
"tsdav": "^2.1.8",
"zustand": "^5.0.11"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/cli": "^2.10.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/dompurify": "^3.0.5",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@types/react-transition-group": "^4.4.12",
"@vitejs/plugin-react": "^5.1.3",
"jsdom": "^28.0.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}
================================================
FILE: release-please-config.json
================================================
{
"packages": {
".": {
"release-type": "node",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"extra-files": [
{
"type": "json",
"path": "src-tauri/tauri.conf.json",
"jsonpath": "$.version"
},
{
"type": "generic",
"path": "src-tauri/Cargo.toml"
},
{
"type": "generic",
"path": "com.velomail.app.metainfo.xml"
},
{
"type": "generic",
"path": "velo.spec"
}
]
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
================================================
FILE: splashscreen.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Velo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0c1222 0%, #151030 35%, #1a0a2e 65%, #0f172a 100%);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.logo {
width: 180px;
opacity: 0;
animation: fadeIn 0.6s ease-out 0.1s forwards;
}
.loader {
display: flex;
gap: 6px;
margin-top: 32px;
opacity: 0;
animation: fadeIn 0.6s ease-out 0.4s forwards;
}
.loader span {
width: 5px;
height: 5px;
border-radius: 50%;
background: #818cf8;
animation: pulse 1.4s ease-in-out infinite;
}
.loader span:nth-child(2) { animation-delay: 0.2s; }
.loader span:nth-child(3) { animation-delay: 0.4s; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
</style>
</head>
<body>
<!-- Velo logo — inline SVG, no external dependencies -->
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 431 97.455" fill="none">
<g>
<path d="M107.905 65.616c-2.3 1-4.4 1.6-6.8 2.4l-2.6 4.8c-7.6.7-11.2-10.9-13.6-16.5L74.105 30.916c-1.6-3.8-4.3-10.4-6.3-13.9-1.3-2.2-2.8-4
gitextract_6tjjzw1_/ ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── docs.yml │ │ └── feature_request.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── packaging.yml │ ├── release-please.yml │ ├── release.yml │ └── update-homebrew.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── com.velomail.app.desktop ├── com.velomail.app.metainfo.xml ├── com.velomail.app.yml ├── docs/ │ ├── architecture.md │ ├── development.md │ └── keyboard-shortcuts.md ├── index.html ├── landing/ │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public/ │ │ ├── og-image.html │ │ ├── robots.txt │ │ ├── screenshots/ │ │ │ └── .gitkeep │ │ └── sitemap.xml │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── components/ │ │ │ ├── CtaFooter.tsx │ │ │ ├── Features.tsx │ │ │ ├── Hero.tsx │ │ │ ├── Navbar.tsx │ │ │ ├── OpenSource.tsx │ │ │ ├── ProductShowcase.tsx │ │ │ ├── WhyVelo.tsx │ │ │ └── mockups/ │ │ │ ├── AiMockup.tsx │ │ │ ├── AppMockup.tsx │ │ │ ├── MultiProviderMockup.tsx │ │ │ └── SplitInboxMockup.tsx │ │ ├── index.css │ │ └── main.tsx │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── wrangler.jsonc ├── package.json ├── release-please-config.json ├── splashscreen.html ├── src/ │ ├── App.tsx │ ├── ComposerWindow.tsx │ ├── ThreadWindow.tsx │ ├── components/ │ │ ├── accounts/ │ │ │ ├── AccountSwitcher.test.tsx │ │ │ ├── AccountSwitcher.tsx │ │ │ ├── AddAccount.tsx │ │ │ ├── AddCalDavAccount.tsx │ │ │ ├── AddImapAccount.tsx │ │ │ ├── SetupClientId.test.tsx │ │ │ └── SetupClientId.tsx │ │ ├── attachments/ │ │ │ ├── AttachmentGridItem.tsx │ │ │ ├── AttachmentLibrary.test.tsx │ │ │ ├── AttachmentLibrary.tsx │ │ │ └── AttachmentListItem.tsx │ │ ├── calendar/ │ │ │ ├── CalendarList.test.tsx │ │ │ ├── CalendarList.tsx │ │ │ ├── CalendarPage.tsx │ │ │ ├── CalendarReauthBanner.tsx │ │ │ ├── CalendarToolbar.tsx │ │ │ ├── DayView.tsx │ │ │ ├── EventCard.tsx │ │ │ ├── EventCreateModal.tsx │ │ │ ├── EventDetailModal.tsx │ │ │ ├── MonthView.tsx │ │ │ └── WeekView.tsx │ │ ├── composer/ │ │ │ ├── AddressInput.test.tsx │ │ │ ├── AddressInput.tsx │ │ │ ├── AiAssistPanel.tsx │ │ │ ├── AttachmentPicker.tsx │ │ │ ├── Composer.tsx │ │ │ ├── EditorToolbar.tsx │ │ │ ├── FromSelector.tsx │ │ │ ├── ScheduleSendDialog.tsx │ │ │ ├── SignatureSelector.tsx │ │ │ ├── TemplatePicker.tsx │ │ │ ├── UndoSendToast.tsx │ │ │ └── scheduleSendPresets.test.ts │ │ ├── dnd/ │ │ │ ├── DndProvider.test.ts │ │ │ └── DndProvider.tsx │ │ ├── email/ │ │ │ ├── ActionBar.tsx │ │ │ ├── AttachmentList.test.tsx │ │ │ ├── AttachmentList.tsx │ │ │ ├── AuthBadge.test.tsx │ │ │ ├── AuthBadge.tsx │ │ │ ├── AuthWarningBanner.test.tsx │ │ │ ├── AuthWarningBanner.tsx │ │ │ ├── CategoryTabs.test.tsx │ │ │ ├── CategoryTabs.tsx │ │ │ ├── ContactSidebar.test.tsx │ │ │ ├── ContactSidebar.tsx │ │ │ ├── EmailRenderer.test.tsx │ │ │ ├── EmailRenderer.tsx │ │ │ ├── FollowUpDialog.tsx │ │ │ ├── InlineAttachmentPreview.test.tsx │ │ │ ├── InlineAttachmentPreview.tsx │ │ │ ├── InlineReply.tsx │ │ │ ├── LinkConfirmDialog.tsx │ │ │ ├── MessageItem.test.tsx │ │ │ ├── MessageItem.tsx │ │ │ ├── MoveToFolderDialog.test.tsx │ │ │ ├── MoveToFolderDialog.tsx │ │ │ ├── PhishingBanner.tsx │ │ │ ├── RawMessageModal.test.tsx │ │ │ ├── RawMessageModal.tsx │ │ │ ├── SmartReplySuggestions.tsx │ │ │ ├── SnoozeDialog.tsx │ │ │ ├── ThreadCard.test.tsx │ │ │ ├── ThreadCard.tsx │ │ │ ├── ThreadSummary.tsx │ │ │ └── ThreadView.tsx │ │ ├── help/ │ │ │ ├── HelpCard.tsx │ │ │ ├── HelpCardGrid.tsx │ │ │ ├── HelpPage.tsx │ │ │ ├── HelpSearchBar.tsx │ │ │ ├── HelpSidebar.tsx │ │ │ ├── HelpTooltip.tsx │ │ │ └── helpContentSearch.test.ts │ │ ├── labels/ │ │ │ └── LabelForm.tsx │ │ ├── layout/ │ │ │ ├── EmailList.tsx │ │ │ ├── MailLayout.tsx │ │ │ ├── ReadingPane.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── TitleBar.tsx │ │ ├── search/ │ │ │ ├── AskInbox.tsx │ │ │ ├── CommandPalette.tsx │ │ │ ├── SearchBar.tsx │ │ │ └── ShortcutsHelp.tsx │ │ ├── settings/ │ │ │ ├── CalDavSettings.tsx │ │ │ ├── ContactEditor.tsx │ │ │ ├── FilterEditor.tsx │ │ │ ├── LabelEditor.test.tsx │ │ │ ├── LabelEditor.tsx │ │ │ ├── QuickStepEditor.tsx │ │ │ ├── SettingsPage.tsx │ │ │ ├── SignatureEditor.test.tsx │ │ │ ├── SignatureEditor.tsx │ │ │ ├── SmartFolderEditor.tsx │ │ │ ├── SmartLabelEditor.test.tsx │ │ │ ├── SmartLabelEditor.tsx │ │ │ ├── SubscriptionManager.tsx │ │ │ └── TemplateEditor.tsx │ │ ├── tasks/ │ │ │ ├── AiTaskExtractDialog.tsx │ │ │ ├── TaskItem.test.tsx │ │ │ ├── TaskItem.tsx │ │ │ ├── TaskQuickAdd.tsx │ │ │ ├── TaskSidebar.tsx │ │ │ └── TasksPage.tsx │ │ └── ui/ │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ ├── ConfirmDialog.test.tsx │ │ ├── ConfirmDialog.tsx │ │ ├── ContextMenu.test.tsx │ │ ├── ContextMenu.tsx │ │ ├── ContextMenuPortal.tsx │ │ ├── DateTimePickerDialog.test.tsx │ │ ├── DateTimePickerDialog.tsx │ │ ├── EmptyState.tsx │ │ ├── ErrorBoundary.test.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── InputDialog.test.tsx │ │ ├── InputDialog.tsx │ │ ├── Modal.test.tsx │ │ ├── Modal.tsx │ │ ├── OfflineBanner.tsx │ │ ├── Skeleton.tsx │ │ ├── TextField.test.tsx │ │ ├── TextField.tsx │ │ ├── UpdateToast.test.tsx │ │ ├── UpdateToast.tsx │ │ └── illustrations/ │ │ ├── GenericEmptyIllustration.tsx │ │ ├── InboxClearIllustration.tsx │ │ ├── NoAccountIllustration.tsx │ │ ├── NoSearchResultsIllustration.tsx │ │ ├── ReadingPaneIllustration.tsx │ │ └── index.ts │ ├── config/ │ │ └── tauriConfig.test.ts │ ├── constants/ │ │ ├── helpContent.test.ts │ │ ├── helpContent.ts │ │ ├── shortcuts.test.ts │ │ ├── shortcuts.ts │ │ ├── themes.test.ts │ │ └── themes.ts │ ├── hooks/ │ │ ├── useClickOutside.ts │ │ ├── useContextMenu.ts │ │ ├── useKeyboardShortcuts.test.ts │ │ ├── useKeyboardShortcuts.ts │ │ ├── useRouteNavigation.test.ts │ │ └── useRouteNavigation.ts │ ├── main.tsx │ ├── router/ │ │ ├── index.ts │ │ ├── navigate.test.ts │ │ ├── navigate.ts │ │ └── routeTree.tsx │ ├── services/ │ │ ├── ai/ │ │ │ ├── aiService.test.ts │ │ │ ├── aiService.ts │ │ │ ├── askInbox.ts │ │ │ ├── categorizationManager.ts │ │ │ ├── errors.ts │ │ │ ├── prompts.ts │ │ │ ├── providerFactory.test.ts │ │ │ ├── providerFactory.ts │ │ │ ├── providerManager.test.ts │ │ │ ├── providerManager.ts │ │ │ ├── providers/ │ │ │ │ ├── claudeProvider.ts │ │ │ │ ├── copilotProvider.test.ts │ │ │ │ ├── copilotProvider.ts │ │ │ │ ├── geminiProvider.ts │ │ │ │ ├── ollamaProvider.test.ts │ │ │ │ ├── ollamaProvider.ts │ │ │ │ └── openaiProvider.ts │ │ │ ├── taskExtraction.test.ts │ │ │ ├── taskExtraction.ts │ │ │ ├── types.ts │ │ │ ├── writingStyleService.test.ts │ │ │ └── writingStyleService.ts │ │ ├── attachments/ │ │ │ ├── cacheManager.test.ts │ │ │ ├── cacheManager.ts │ │ │ ├── preCacheManager.test.ts │ │ │ └── preCacheManager.ts │ │ ├── backgroundCheckers.test.ts │ │ ├── backgroundCheckers.ts │ │ ├── badgeManager.ts │ │ ├── bundles/ │ │ │ └── bundleManager.ts │ │ ├── calendar/ │ │ │ ├── autoDiscovery.test.ts │ │ │ ├── autoDiscovery.ts │ │ │ ├── caldavProvider.test.ts │ │ │ ├── caldavProvider.ts │ │ │ ├── googleCalendarProvider.test.ts │ │ │ ├── googleCalendarProvider.ts │ │ │ ├── icalHelper.test.ts │ │ │ ├── icalHelper.ts │ │ │ ├── providerFactory.test.ts │ │ │ ├── providerFactory.ts │ │ │ └── types.ts │ │ ├── categorization/ │ │ │ ├── backfillService.test.ts │ │ │ ├── backfillService.ts │ │ │ ├── ruleEngine.test.ts │ │ │ └── ruleEngine.ts │ │ ├── composer/ │ │ │ ├── draftAutoSave.test.ts │ │ │ └── draftAutoSave.ts │ │ ├── contacts/ │ │ │ └── gravatar.ts │ │ ├── db/ │ │ │ ├── accounts.test.ts │ │ │ ├── accounts.ts │ │ │ ├── aiCache.ts │ │ │ ├── attachments.test.ts │ │ │ ├── attachments.ts │ │ │ ├── bundleRules.test.ts │ │ │ ├── bundleRules.ts │ │ │ ├── calendarEvents.test.ts │ │ │ ├── calendarEvents.ts │ │ │ ├── calendars.test.ts │ │ │ ├── calendars.ts │ │ │ ├── connection.test.ts │ │ │ ├── connection.ts │ │ │ ├── contacts.test.ts │ │ │ ├── contacts.ts │ │ │ ├── filters.ts │ │ │ ├── folderSyncState.test.ts │ │ │ ├── folderSyncState.ts │ │ │ ├── followUpReminders.ts │ │ │ ├── imageAllowlist.test.ts │ │ │ ├── imageAllowlist.ts │ │ │ ├── labels.test.ts │ │ │ ├── labels.ts │ │ │ ├── linkScanResults.ts │ │ │ ├── localDrafts.test.ts │ │ │ ├── localDrafts.ts │ │ │ ├── messages.test.ts │ │ │ ├── messages.ts │ │ │ ├── migrations.test.ts │ │ │ ├── migrations.ts │ │ │ ├── notificationVips.ts │ │ │ ├── pendingOperations.test.ts │ │ │ ├── pendingOperations.ts │ │ │ ├── phishingAllowlist.ts │ │ │ ├── quickSteps.test.ts │ │ │ ├── quickSteps.ts │ │ │ ├── scheduledEmails.ts │ │ │ ├── search.ts │ │ │ ├── sendAsAliases.test.ts │ │ │ ├── sendAsAliases.ts │ │ │ ├── settings.ts │ │ │ ├── signatures.ts │ │ │ ├── smartFolders.test.ts │ │ │ ├── smartFolders.ts │ │ │ ├── smartLabelRules.test.ts │ │ │ ├── smartLabelRules.ts │ │ │ ├── tasks.test.ts │ │ │ ├── tasks.ts │ │ │ ├── templates.ts │ │ │ ├── threadCategories.ts │ │ │ ├── threads.test.ts │ │ │ ├── threads.ts │ │ │ ├── writingStyleProfiles.test.ts │ │ │ └── writingStyleProfiles.ts │ │ ├── deepLinkHandler.ts │ │ ├── email/ │ │ │ ├── gmailProvider.test.ts │ │ │ ├── gmailProvider.ts │ │ │ ├── imapSmtpProvider.test.ts │ │ │ ├── imapSmtpProvider.ts │ │ │ ├── providerFactory.test.ts │ │ │ ├── providerFactory.ts │ │ │ └── types.ts │ │ ├── emailActions.test.ts │ │ ├── emailActions.ts │ │ ├── filters/ │ │ │ ├── filterEngine.test.ts │ │ │ └── filterEngine.ts │ │ ├── followup/ │ │ │ └── followupManager.ts │ │ ├── globalShortcut.ts │ │ ├── gmail/ │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── authParser.test.ts │ │ │ ├── authParser.ts │ │ │ ├── client.test.ts │ │ │ ├── client.ts │ │ │ ├── draftDeletion.test.ts │ │ │ ├── draftDeletion.ts │ │ │ ├── messageParser.test.ts │ │ │ ├── messageParser.ts │ │ │ ├── sendAs.test.ts │ │ │ ├── sendAs.ts │ │ │ ├── sync.test.ts │ │ │ ├── sync.ts │ │ │ ├── syncManager.test.ts │ │ │ ├── syncManager.ts │ │ │ └── tokenManager.ts │ │ ├── google/ │ │ │ └── calendar.ts │ │ ├── imap/ │ │ │ ├── autoDiscovery.test.ts │ │ │ ├── autoDiscovery.ts │ │ │ ├── folderMapper.test.ts │ │ │ ├── folderMapper.ts │ │ │ ├── imapConfigBuilder.test.ts │ │ │ ├── imapConfigBuilder.ts │ │ │ ├── imapSync.test.ts │ │ │ ├── imapSync.ts │ │ │ ├── messageHelper.test.ts │ │ │ ├── messageHelper.ts │ │ │ ├── tauriCommands.test.ts │ │ │ └── tauriCommands.ts │ │ ├── notifications/ │ │ │ └── notificationManager.ts │ │ ├── oauth/ │ │ │ ├── oauthFlow.test.ts │ │ │ ├── oauthFlow.ts │ │ │ ├── oauthTokenManager.test.ts │ │ │ ├── oauthTokenManager.ts │ │ │ ├── providers.test.ts │ │ │ └── providers.ts │ │ ├── phishing/ │ │ │ └── phishingScanner.ts │ │ ├── queue/ │ │ │ ├── queueProcessor.test.ts │ │ │ └── queueProcessor.ts │ │ ├── quickSteps/ │ │ │ ├── defaults.ts │ │ │ ├── executor.test.ts │ │ │ ├── executor.ts │ │ │ └── types.ts │ │ ├── search/ │ │ │ ├── searchParser.test.ts │ │ │ ├── searchParser.ts │ │ │ ├── searchQueryBuilder.test.ts │ │ │ ├── searchQueryBuilder.ts │ │ │ ├── smartFolderQuery.test.ts │ │ │ └── smartFolderQuery.ts │ │ ├── smartLabels/ │ │ │ ├── backfillService.test.ts │ │ │ ├── backfillService.ts │ │ │ ├── smartLabelManager.test.ts │ │ │ ├── smartLabelManager.ts │ │ │ ├── smartLabelService.test.ts │ │ │ └── smartLabelService.ts │ │ ├── snooze/ │ │ │ ├── scheduledSendManager.ts │ │ │ └── snoozeManager.ts │ │ ├── tasks/ │ │ │ ├── taskManager.test.ts │ │ │ └── taskManager.ts │ │ ├── threading/ │ │ │ ├── threadBuilder.test.ts │ │ │ └── threadBuilder.ts │ │ ├── unsubscribe/ │ │ │ └── unsubscribeManager.ts │ │ ├── updateManager.test.ts │ │ └── updateManager.ts │ ├── stores/ │ │ ├── accountStore.test.ts │ │ ├── accountStore.ts │ │ ├── composerStore.test.ts │ │ ├── composerStore.ts │ │ ├── contextMenuStore.test.ts │ │ ├── contextMenuStore.ts │ │ ├── labelStore.test.ts │ │ ├── labelStore.ts │ │ ├── shortcutStore.ts │ │ ├── smartFolderStore.test.ts │ │ ├── smartFolderStore.ts │ │ ├── taskStore.test.ts │ │ ├── taskStore.ts │ │ ├── threadStore.test.ts │ │ ├── threadStore.ts │ │ ├── uiStore.test.ts │ │ └── uiStore.ts │ ├── styles/ │ │ └── globals.css │ ├── test/ │ │ ├── mocks/ │ │ │ ├── db.mock.ts │ │ │ ├── entities.mock.ts │ │ │ ├── index.ts │ │ │ ├── services.mock.ts │ │ │ ├── stores.mock.ts │ │ │ └── tauri.mock.ts │ │ └── setup.ts │ ├── utils/ │ │ ├── crypto.test.ts │ │ ├── crypto.ts │ │ ├── date.ts │ │ ├── emailBuilder.test.ts │ │ ├── emailBuilder.ts │ │ ├── emailUtils.test.ts │ │ ├── emailUtils.ts │ │ ├── fileTypeHelpers.test.ts │ │ ├── fileTypeHelpers.ts │ │ ├── fileUtils.test.ts │ │ ├── fileUtils.ts │ │ ├── imageBlocker.test.ts │ │ ├── imageBlocker.ts │ │ ├── imageResize.ts │ │ ├── mailtoParser.test.ts │ │ ├── mailtoParser.ts │ │ ├── networkErrors.test.ts │ │ ├── networkErrors.ts │ │ ├── noReply.test.ts │ │ ├── noReply.ts │ │ ├── phishingDetector.test.ts │ │ ├── phishingDetector.ts │ │ ├── resolveFromAddress.test.ts │ │ ├── resolveFromAddress.ts │ │ ├── sanitize.test.ts │ │ ├── sanitize.ts │ │ ├── templateVariables.test.ts │ │ ├── templateVariables.ts │ │ ├── timestamp.test.ts │ │ └── timestamp.ts │ └── vite-env.d.ts ├── src-tauri/ │ ├── .gitignore │ ├── Cargo.toml │ ├── Entitlements.plist │ ├── build.rs │ ├── capabilities/ │ │ └── default.json │ ├── icons/ │ │ ├── android/ │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ └── values/ │ │ │ └── ic_launcher_background.xml │ │ └── icon.icns │ ├── src/ │ │ ├── commands.rs │ │ ├── imap/ │ │ │ ├── client.rs │ │ │ ├── mod.rs │ │ │ └── types.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── oauth.rs │ │ └── smtp/ │ │ ├── client.rs │ │ ├── mod.rs │ │ └── types.rs │ └── tauri.conf.json ├── tsconfig.json ├── velo.spec ├── vite.config.ts └── vitest.config.ts
SYMBOL INDEX (1475 symbols across 308 files)
FILE: landing/src/App.tsx
function App (line 12) | function App() {
FILE: landing/src/components/CtaFooter.tsx
function CtaFooter (line 4) | function CtaFooter() {
FILE: landing/src/components/Features.tsx
constant FEATURES (line 8) | const FEATURES = [
function Features (line 90) | function Features() {
FILE: landing/src/components/Hero.tsx
function Hero (line 5) | function Hero() {
FILE: landing/src/components/Navbar.tsx
constant NAV_LINKS (line 5) | const NAV_LINKS = [
function Navbar (line 11) | function Navbar() {
FILE: landing/src/components/OpenSource.tsx
constant TRUST_SIGNALS (line 4) | const TRUST_SIGNALS = [
function OpenSource (line 11) | function OpenSource() {
FILE: landing/src/components/ProductShowcase.tsx
constant FEATURES (line 7) | const FEATURES: { title: string; description: string; mockup: ReactNode ...
function ProductShowcase (line 28) | function ProductShowcase() {
FILE: landing/src/components/WhyVelo.tsx
constant DIFFERENTIATORS (line 4) | const DIFFERENTIATORS = [
function WhyVelo (line 34) | function WhyVelo() {
FILE: landing/src/components/mockups/AiMockup.tsx
function AiMockup (line 4) | function AiMockup() {
FILE: landing/src/components/mockups/AppMockup.tsx
constant SIDEBAR_ITEMS (line 8) | const SIDEBAR_ITEMS = [
constant LABELS (line 18) | const LABELS = [
constant THREADS (line 24) | const THREADS = [
constant MESSAGE_BODY (line 34) | const MESSAGE_BODY = `Hi team,
function AppMockup (line 47) | function AppMockup() {
FILE: landing/src/components/mockups/MultiProviderMockup.tsx
constant ACCOUNTS (line 4) | const ACCOUNTS = [
constant PROVIDERS (line 10) | const PROVIDERS = [
function MultiProviderMockup (line 19) | function MultiProviderMockup() {
FILE: landing/src/components/mockups/SplitInboxMockup.tsx
constant TABS (line 4) | const TABS = [
constant THREADS (line 12) | const THREADS = [
function SplitInboxMockup (line 21) | function SplitInboxMockup() {
FILE: src-tauri/build.rs
function main (line 1) | fn main() {
FILE: src-tauri/src/commands.rs
function imap_test_connection (line 12) | pub async fn imap_test_connection(config: ImapConfig) -> Result<String, ...
function imap_list_folders (line 17) | pub async fn imap_list_folders(config: ImapConfig) -> Result<Vec<ImapFol...
function imap_fetch_messages (line 25) | pub async fn imap_fetch_messages(
function imap_fetch_new_uids (line 57) | pub async fn imap_fetch_new_uids(
function imap_search_all_uids (line 69) | pub async fn imap_search_all_uids(
function imap_fetch_message_body (line 80) | pub async fn imap_fetch_message_body(
function imap_fetch_raw_message (line 92) | pub async fn imap_fetch_raw_message(
function imap_set_flags (line 104) | pub async fn imap_set_flags(
function imap_move_messages (line 148) | pub async fn imap_move_messages(
function imap_delete_messages (line 172) | pub async fn imap_delete_messages(
function imap_get_folder_status (line 195) | pub async fn imap_get_folder_status(
function imap_fetch_attachment (line 206) | pub async fn imap_fetch_attachment(
function imap_append_message (line 219) | pub async fn imap_append_message(
function base64url_decode (line 236) | fn base64url_decode(input: &str) -> Result<Vec<u8>, String> {
function imap_search_folder (line 245) | pub async fn imap_search_folder(
function imap_sync_folder (line 257) | pub async fn imap_sync_folder(
function imap_raw_fetch_diagnostic (line 270) | pub async fn imap_raw_fetch_diagnostic(
function imap_delta_check (line 279) | pub async fn imap_delta_check(
function smtp_send_email (line 292) | pub async fn smtp_send_email(
function smtp_test_connection (line 300) | pub async fn smtp_test_connection(config: SmtpConfig) -> Result<SmtpSend...
FILE: src-tauri/src/imap/client.rs
constant TCP_CONNECT_TIMEOUT (line 14) | const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
constant TLS_HANDSHAKE_TIMEOUT (line 15) | const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
constant AUTH_TIMEOUT (line 16) | const AUTH_TIMEOUT: Duration = Duration::from_secs(30);
constant IMAP_CMD_TIMEOUT (line 17) | const IMAP_CMD_TIMEOUT: Duration = Duration::from_secs(30);
constant IMAP_FETCH_TIMEOUT (line 18) | const IMAP_FETCH_TIMEOUT: Duration = Duration::from_secs(120);
constant IMAP_SEARCH_TIMEOUT (line 19) | const IMAP_SEARCH_TIMEOUT: Duration = Duration::from_secs(60);
constant OVERALL_CONNECT_TIMEOUT (line 20) | const OVERALL_CONNECT_TIMEOUT: Duration = Duration::from_secs(60);
function configure_tcp_socket (line 23) | fn configure_tcp_socket(stream: &TcpStream) {
type XOAuth2 (line 41) | struct XOAuth2 {
method new (line 46) | fn new(user: &str, access_token: &str) -> Self {
type Response (line 56) | type Response = Vec<u8>;
method process (line 57) | fn process(&mut self, _challenge: &[u8]) -> Self::Response {
type ImapStream (line 68) | pub(crate) enum ImapStream {
method poll_read (line 74) | fn poll_read(
method poll_write (line 87) | fn poll_write(
method poll_flush (line 98) | fn poll_flush(
method poll_shutdown (line 108) | fn poll_shutdown(
method fmt (line 120) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function build_tls_connector (line 132) | fn build_tls_connector(accept_invalid_certs: bool) -> Result<native_tls:...
type ImapSession (line 143) | type ImapSession = Session<ImapStream>;
function connect (line 151) | pub async fn connect(config: &ImapConfig) -> Result<ImapSession, String> {
function connect_inner (line 160) | async fn connect_inner(config: &ImapConfig) -> Result<ImapSession, Strin...
function list_folders (line 177) | pub async fn list_folders(session: &mut ImapSession) -> Result<Vec<ImapF...
function fetch_messages (line 231) | pub async fn fetch_messages(
function fetch_message_body (line 326) | pub async fn fetch_message_body(
function fetch_new_uids (line 370) | pub async fn fetch_new_uids(
function search_all_uids (line 394) | pub async fn search_all_uids(
function set_flags (line 417) | pub async fn set_flags(
function move_messages (line 445) | pub async fn move_messages(
function delete_messages (line 494) | pub async fn delete_messages(
function append_message (line 530) | pub async fn append_message(
function get_folder_status (line 543) | pub async fn get_folder_status(
function fetch_attachment (line 570) | pub async fn fetch_attachment(
function fetch_raw_message (line 644) | pub async fn fetch_raw_message(
function delta_check_folders (line 685) | pub async fn delta_check_folders(
function search_folder (line 750) | pub async fn search_folder(
function sync_folder (line 802) | pub async fn sync_folder(
function test_connection (line 911) | pub async fn test_connection(config: &ImapConfig) -> Result<String, Stri...
function raw_fetch_messages (line 939) | pub async fn raw_fetch_messages(
function raw_fetch_diagnostic (line 1044) | pub async fn raw_fetch_diagnostic(
type RawFetchedMessage (line 1109) | struct RawFetchedMessage {
function raw_connect_starttls (line 1119) | async fn raw_connect_starttls(config: &ImapConfig) -> Result<ImapStream,...
function raw_send_and_wait (line 1156) | async fn raw_send_and_wait(
function parse_untagged_number (line 1192) | fn parse_untagged_number(line: &str, keyword: &str) -> Option<u32> {
function extract_bracket_number (line 1203) | fn extract_bracket_number(line: &str, keyword: &str) -> Option<u32> {
function raw_parse_fetch_responses (line 1223) | async fn raw_parse_fetch_responses(
function extract_fetch_uid (line 1305) | fn extract_fetch_uid(line: &str) -> Option<u32> {
function extract_flags_from_fetch (line 1314) | fn extract_flags_from_fetch(line: &str) -> String {
function extract_internal_date (line 1327) | fn extract_internal_date(line: &str) -> Option<i64> {
function parse_imap_date (line 1337) | fn parse_imap_date(s: &str) -> Option<i64> {
function is_leap_year (line 1388) | fn is_leap_year(y: i64) -> bool {
function extract_literal_size (line 1393) | fn extract_literal_size(line: &str) -> Option<usize> {
function connect_stream (line 1405) | async fn connect_stream(config: &ImapConfig) -> Result<ImapStream, Strin...
function connect_starttls (line 1451) | async fn connect_starttls(config: &ImapConfig) -> Result<ImapSession, St...
function authenticate (line 1516) | async fn authenticate(
function detect_special_use (line 1536) | fn detect_special_use(name: &async_imap::types::Name) -> Option<String> {
function parse_message (line 1578) | fn parse_message(
function build_imap_section_map (line 1742) | fn build_imap_section_map(message: &mail_parser::Message) -> std::collec...
function extract_header_text (line 1785) | fn extract_header_text(hv: Option<&mail_parser::HeaderValue>) -> Option<...
function extract_first_address (line 1796) | fn extract_first_address(
function format_address_list (line 1814) | fn format_address_list(addr: Option<&mail_parser::Address>) -> Option<St...
FILE: src-tauri/src/imap/types.rs
type ImapConfig (line 4) | pub struct ImapConfig {
type ImapFolder (line 16) | pub struct ImapFolder {
type ImapMessage (line 27) | pub struct ImapMessage {
type ImapAttachment (line 55) | pub struct ImapAttachment {
type ImapFolderStatus (line 65) | pub struct ImapFolderStatus {
type ImapFetchResult (line 74) | pub struct ImapFetchResult {
type ImapFolderSyncResult (line 80) | pub struct ImapFolderSyncResult {
type ImapFolderSearchResult (line 87) | pub struct ImapFolderSearchResult {
type DeltaCheckRequest (line 93) | pub struct DeltaCheckRequest {
type DeltaCheckResult (line 100) | pub struct DeltaCheckResult {
FILE: src-tauri/src/lib.rs
function close_splashscreen (line 15) | fn close_splashscreen(app: tauri::AppHandle) {
function set_tray_tooltip (line 26) | fn set_tray_tooltip(app: tauri::AppHandle, tooltip: String) -> Result<()...
function open_devtools (line 44) | fn open_devtools(app: tauri::AppHandle) {
function run (line 51) | pub fn run() {
FILE: src-tauri/src/main.rs
function main (line 4) | fn main() {
FILE: src-tauri/src/oauth.rs
type OAuthResult (line 8) | pub struct OAuthResult {
function start_oauth_server (line 16) | pub async fn start_oauth_server(port: u16, state: String) -> Result<OAut...
function parse_auth_code_and_state (line 88) | fn parse_auth_code_and_state(request: &str) -> Result<(String, String), ...
function parse_query_string (line 114) | fn parse_query_string(path: &str) -> HashMap<String, String> {
function urlencoding_decode (line 127) | fn urlencoding_decode(s: &str) -> String {
type TokenExchangeResult (line 153) | pub struct TokenExchangeResult {
function oauth_exchange_token (line 164) | pub async fn oauth_exchange_token(
function oauth_refresh_token (line 215) | pub async fn oauth_refresh_token(
FILE: src-tauri/src/smtp/client.rs
function decode_base64url (line 13) | fn decode_base64url(input: &str) -> Result<Vec<u8>, String> {
function build_transport (line 20) | fn build_transport(
function extract_envelope (line 88) | fn extract_envelope(raw: &[u8]) -> Result<lettre::address::Envelope, Str...
function send_raw_email (line 150) | pub async fn send_raw_email(
function test_connection (line 169) | pub async fn test_connection(config: &SmtpConfig) -> Result<SmtpSendResu...
function test_decode_base64url_valid (line 191) | fn test_decode_base64url_valid() {
function test_decode_base64url_invalid (line 199) | fn test_decode_base64url_invalid() {
function test_extract_envelope_valid (line 206) | fn test_extract_envelope_valid() {
function test_extract_envelope_no_from (line 215) | fn test_extract_envelope_no_from() {
function test_extract_envelope_no_recipients (line 223) | fn test_extract_envelope_no_recipients() {
function test_extract_envelope_with_bcc (line 231) | fn test_extract_envelope_with_bcc() {
FILE: src-tauri/src/smtp/types.rs
type SmtpConfig (line 4) | pub struct SmtpConfig {
type SmtpSendResult (line 16) | pub struct SmtpSendResult {
FILE: src/App.tsx
function useRouterSyncBridge (line 84) | function useRouterSyncBridge() {
function App (line 97) | function App() {
FILE: src/ComposerWindow.tsx
function ComposerWindow (line 15) | function ComposerWindow() {
FILE: src/ThreadWindow.tsx
function ThreadWindow (line 16) | function ThreadWindow() {
FILE: src/components/accounts/AccountSwitcher.tsx
type AccountSwitcherProps (line 6) | interface AccountSwitcherProps {
function AccountSwitcher (line 11) | function AccountSwitcher({
function ActiveAvatar (line 144) | function ActiveAvatar({ account }: { account: Account | undefined }) {
function AccountAvatarSmall (line 172) | function AccountAvatarSmall({
FILE: src/components/accounts/AddAccount.tsx
type AddAccountProps (line 13) | interface AddAccountProps {
type View (line 18) | type View = "select-provider" | "gmail" | "imap" | "caldav";
function AddAccount (line 20) | function AddAccount({ onClose, onSuccess }: AddAccountProps) {
FILE: src/components/accounts/AddCalDavAccount.tsx
type AddCalDavAccountProps (line 16) | interface AddCalDavAccountProps {
type Step (line 22) | type Step = "basic" | "server" | "test" | "done";
function AddCalDavAccount (line 24) | function AddCalDavAccount({ onClose, onSuccess, onBack }: AddCalDavAccou...
FILE: src/components/accounts/AddImapAccount.tsx
type AddImapAccountProps (line 27) | interface AddImapAccountProps {
type Step (line 33) | type Step = "basic" | "imap" | "smtp" | "test";
type AuthMode (line 34) | type AuthMode = "password" | "oauth2";
type FormState (line 36) | interface FormState {
type TestStatus (line 101) | interface TestStatus {
function mapSecurity (line 113) | function mapSecurity(security: string): string {
function AddImapAccount (line 118) | function AddImapAccount({
FILE: src/components/accounts/SetupClientId.tsx
type SetupClientIdProps (line 5) | interface SetupClientIdProps {
function SetupClientId (line 10) | function SetupClientId({ onComplete, onCancel }: SetupClientIdProps) {
FILE: src/components/attachments/AttachmentGridItem.tsx
type AttachmentGridItemProps (line 5) | interface AttachmentGridItemProps {
function formatRelativeDate (line 12) | function formatRelativeDate(timestamp: number | null): string {
function AttachmentGridItem (line 26) | function AttachmentGridItem({ attachment, onPreview, onDownload, onJumpT...
FILE: src/components/attachments/AttachmentLibrary.tsx
type TypeFilter (line 20) | type TypeFilter = "all" | "images" | "pdfs" | "documents" | "spreadsheet...
type DateFilter (line 21) | type DateFilter = "all" | "today" | "week" | "month" | "year";
type SizeFilter (line 22) | type SizeFilter = "all" | "small" | "medium" | "large";
type ViewMode (line 23) | type ViewMode = "grid" | "list";
constant TYPE_OPTIONS (line 25) | const TYPE_OPTIONS: { value: TypeFilter; label: string }[] = [
constant DATE_OPTIONS (line 35) | const DATE_OPTIONS: { value: DateFilter; label: string }[] = [
constant SIZE_OPTIONS (line 43) | const SIZE_OPTIONS: { value: SizeFilter; label: string }[] = [
function matchesType (line 50) | function matchesType(att: AttachmentWithContext, filter: TypeFilter): bo...
function matchesDate (line 65) | function matchesDate(att: AttachmentWithContext, filter: DateFilter): bo...
function matchesSize (line 77) | function matchesSize(att: AttachmentWithContext, filter: SizeFilter): bo...
function AttachmentLibrary (line 87) | function AttachmentLibrary() {
FILE: src/components/attachments/AttachmentListItem.tsx
type AttachmentListItemProps (line 5) | interface AttachmentListItemProps {
function formatShortDate (line 12) | function formatShortDate(timestamp: number | null): string {
function AttachmentListItem (line 21) | function AttachmentListItem({ attachment, onPreview, onDownload, onJumpT...
FILE: src/components/calendar/CalendarList.test.tsx
function makeCalendar (line 6) | function makeCalendar(overrides: Partial<DbCalendar> = {}): DbCalendar {
FILE: src/components/calendar/CalendarList.tsx
type CalendarListProps (line 3) | interface CalendarListProps {
function CalendarList (line 8) | function CalendarList({ calendars, onVisibilityChange }: CalendarListPro...
FILE: src/components/calendar/CalendarPage.tsx
function CalendarPage (line 16) | function CalendarPage() {
function upsertCalendarEventFromProvider (line 375) | async function upsertCalendarEventFromProvider(
FILE: src/components/calendar/CalendarReauthBanner.tsx
type CalendarReauthBannerProps (line 5) | interface CalendarReauthBannerProps {
function CalendarReauthBanner (line 11) | function CalendarReauthBanner({ accountId, email, onReauthSuccess }: Cal...
FILE: src/components/calendar/CalendarToolbar.tsx
type CalendarView (line 3) | type CalendarView = "day" | "week" | "month";
type CalendarToolbarProps (line 5) | interface CalendarToolbarProps {
function CalendarToolbar (line 17) | function CalendarToolbar({
function formatTitle (line 93) | function formatTitle(date: Date, view: CalendarView): string {
FILE: src/components/calendar/DayView.tsx
type DayViewProps (line 4) | interface DayViewProps {
constant HOURS (line 10) | const HOURS = Array.from({ length: 24 }, (_, i) => i);
function DayView (line 12) | function DayView({ currentDate, events, onEventClick }: DayViewProps) {
FILE: src/components/calendar/EventCard.tsx
type EventCardProps (line 3) | interface EventCardProps {
function EventCard (line 9) | function EventCard({ event, compact, onClick }: EventCardProps) {
FILE: src/components/calendar/EventCreateModal.tsx
type EventCreateModalProps (line 7) | interface EventCreateModalProps {
function EventCreateModal (line 20) | function EventCreateModal({ calendars, onClose, onCreate }: EventCreateM...
function getDefaultStart (line 130) | function getDefaultStart(): string {
function getDefaultEnd (line 137) | function getDefaultEnd(): string {
function toLocalISOString (line 144) | function toLocalISOString(date: Date): string {
FILE: src/components/calendar/EventDetailModal.tsx
type EventDetailModalProps (line 11) | interface EventDetailModalProps {
function EventDetailModal (line 19) | function EventDetailModal({ event, calendars, accountId, onClose, onUpda...
function toLocalISOString (line 229) | function toLocalISOString(date: Date): string {
FILE: src/components/calendar/MonthView.tsx
type MonthViewProps (line 5) | interface MonthViewProps {
constant DAY_NAMES (line 11) | const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
function MonthView (line 13) | function MonthView({ currentDate, events, onEventClick }: MonthViewProps) {
FILE: src/components/calendar/WeekView.tsx
type WeekViewProps (line 4) | interface WeekViewProps {
constant HOURS (line 10) | const HOURS = Array.from({ length: 24 }, (_, i) => i);
constant DAY_NAMES (line 11) | const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
function WeekView (line 13) | function WeekView({ currentDate, events, onEventClick }: WeekViewProps) {
FILE: src/components/composer/AddressInput.tsx
type AddressInputProps (line 4) | interface AddressInputProps {
function AddressInput (line 11) | function AddressInput({
FILE: src/components/composer/AiAssistPanel.tsx
type AiAssistPanelProps (line 13) | interface AiAssistPanelProps {
function AiAssistPanel (line 19) | function AiAssistPanel({ editor, isReplyMode, threadMessages }: AiAssist...
function QuickAction (line 159) | function QuickAction({
FILE: src/components/composer/AttachmentPicker.tsx
constant MAX_TOTAL_SIZE (line 7) | const MAX_TOTAL_SIZE = 24 * 1024 * 1024;
function AttachmentPicker (line 9) | function AttachmentPicker() {
FILE: src/components/composer/Composer.tsx
function Composer (line 35) | function Composer() {
FILE: src/components/composer/EditorToolbar.tsx
type EditorToolbarProps (line 6) | interface EditorToolbarProps {
function EditorToolbar (line 12) | function EditorToolbar({ editor, onToggleAiAssist, aiAssistOpen }: Edito...
FILE: src/components/composer/FromSelector.tsx
type FromSelectorProps (line 3) | interface FromSelectorProps {
function FromSelector (line 13) | function FromSelector({ aliases, selectedEmail, onChange }: FromSelector...
FILE: src/components/composer/ScheduleSendDialog.tsx
type ScheduleSendDialogProps (line 3) | interface ScheduleSendDialogProps {
function getSchedulePresets (line 8) | function getSchedulePresets(): { label: string; detail: string; timestam...
function ScheduleSendDialog (line 48) | function ScheduleSendDialog({ onSchedule, onClose }: ScheduleSendDialogP...
FILE: src/components/composer/SignatureSelector.tsx
function SignatureSelector (line 9) | function SignatureSelector() {
FILE: src/components/composer/TemplatePicker.tsx
type TemplatePickerProps (line 8) | interface TemplatePickerProps {
function TemplatePicker (line 12) | function TemplatePicker({ editor }: TemplatePickerProps) {
FILE: src/components/composer/UndoSendToast.tsx
constant UNDO_DELAY_SECONDS (line 5) | const UNDO_DELAY_SECONDS = 5;
function UndoSendToast (line 7) | function UndoSendToast() {
FILE: src/components/dnd/DndProvider.tsx
constant LABEL_MAP (line 16) | const LABEL_MAP: Record<string, string> = {
type DragData (line 27) | interface DragData {
function resolveLabelChange (line 36) | function resolveLabelChange(
type DndProviderProps (line 63) | interface DndProviderProps {
function DndProvider (line 67) | function DndProvider({ children }: DndProviderProps) {
FILE: src/components/email/ActionBar.tsx
type ActionBarProps (line 18) | interface ActionBarProps {
function Separator (line 35) | function Separator() {
function ActionBar (line 39) | function ActionBar({ thread, messages, noReply, defaultReplyMode = "repl...
FILE: src/components/email/AttachmentList.tsx
function dedup (line 11) | function dedup(attachments: DbAttachment[]): DbAttachment[] {
type AttachmentListProps (line 21) | interface AttachmentListProps {
function AttachmentList (line 28) | function AttachmentList({ accountId, messageId, attachments, referencedC...
function AttachmentPreview (line 81) | function AttachmentPreview({
function TextPreview (line 250) | function TextPreview({ url }: { url: string }) {
FILE: src/components/email/AuthBadge.test.tsx
function makeAuthResults (line 6) | function makeAuthResults(aggregate: AuthResult["aggregate"]): string {
FILE: src/components/email/AuthBadge.tsx
type AuthBadgeProps (line 5) | interface AuthBadgeProps {
function AuthBadge (line 9) | function AuthBadge({ authResults }: AuthBadgeProps) {
FILE: src/components/email/AuthWarningBanner.test.tsx
function makeAuthResults (line 6) | function makeAuthResults(aggregate: AuthResult["aggregate"]): string {
FILE: src/components/email/AuthWarningBanner.tsx
type AuthWarningBannerProps (line 4) | interface AuthWarningBannerProps {
function AuthWarningBanner (line 10) | function AuthWarningBanner({ authResults, senderAddress, onDismiss }: Au...
FILE: src/components/email/CategoryTabs.test.tsx
method observe (line 12) | observe() {}
method unobserve (line 13) | unobserve() {}
method disconnect (line 14) | disconnect() {}
FILE: src/components/email/CategoryTabs.tsx
type CategoryTabsProps (line 5) | interface CategoryTabsProps {
constant CATEGORY_ICONS (line 11) | const CATEGORY_ICONS: Record<string, LucideIcon> = {
function CategoryTabs (line 19) | function CategoryTabs({ activeCategory, onCategoryChange, unreadCounts }...
FILE: src/components/email/ContactSidebar.tsx
type ContactSidebarProps (line 22) | interface ContactSidebarProps {
function ContactSidebar (line 29) | function ContactSidebar({ email, name, accountId, onClose }: ContactSide...
FILE: src/components/email/EmailRenderer.test.tsx
class MockResizeObserver (line 33) | class MockResizeObserver {
method observe (line 34) | observe() {}
method unobserve (line 35) | unobserve() {}
method disconnect (line 36) | disconnect() {}
function makeAttachment (line 40) | function makeAttachment(overrides: Partial<DbAttachment> = {}): DbAttach...
FILE: src/components/email/EmailRenderer.tsx
type EmailRendererProps (line 10) | interface EmailRendererProps {
function EmailRenderer (line 21) | function EmailRenderer({
FILE: src/components/email/FollowUpDialog.tsx
type FollowUpDialogProps (line 3) | interface FollowUpDialogProps {
function getFollowUpPresets (line 9) | function getFollowUpPresets(): { label: string; timestamp: number }[] {
function FollowUpDialog (line 40) | function FollowUpDialog({ isOpen = true, onSetReminder, onClose }: Follo...
FILE: src/components/email/InlineAttachmentPreview.test.tsx
class MockIntersectionObserver (line 13) | class MockIntersectionObserver {
method constructor (line 14) | constructor(callback: IntersectionObserverCallback) {
FILE: src/components/email/InlineAttachmentPreview.tsx
function dedup (line 8) | function dedup(attachments: DbAttachment[]): DbAttachment[] {
type InlineAttachmentPreviewProps (line 18) | interface InlineAttachmentPreviewProps {
function InlineAttachmentPreview (line 26) | function InlineAttachmentPreview({
function ImageThumbnail (line 91) | function ImageThumbnail({
FILE: src/components/email/InlineReply.tsx
type ReplyMode (line 23) | type ReplyMode = "reply" | "replyAll" | "forward";
type InlineReplyProps (line 25) | interface InlineReplyProps {
function InlineReply (line 33) | function InlineReply({ thread, messages, accountId, noReply, onSent }: I...
FILE: src/components/email/LinkConfirmDialog.tsx
type LinkConfirmDialogProps (line 5) | interface LinkConfirmDialogProps {
function LinkConfirmDialog (line 11) | function LinkConfirmDialog({ linkAnalysis, onCancel, onConfirm }: LinkCo...
FILE: src/components/email/MessageItem.test.tsx
function makeMessage (line 28) | function makeMessage(overrides: Partial<DbMessage> = {}): DbMessage {
FILE: src/components/email/MessageItem.tsx
type MessageItemProps (line 12) | interface MessageItemProps {
function parseUnsubscribeUrl (line 172) | function parseUnsubscribeUrl(header: string): string | null {
function UnsubscribeLink (line 181) | function UnsubscribeLink({
FILE: src/components/email/MoveToFolderDialog.tsx
type MoveToFolderDialogProps (line 24) | interface MoveToFolderDialogProps {
type Destination (line 30) | interface Destination {
constant SYSTEM_DESTINATIONS (line 39) | const SYSTEM_DESTINATIONS: Destination[] = [
function MoveToFolderDialog (line 46) | function MoveToFolderDialog({
FILE: src/components/email/PhishingBanner.tsx
type PhishingBannerProps (line 4) | interface PhishingBannerProps {
function PhishingBanner (line 9) | function PhishingBanner({ scanResult, onTrustSender }: PhishingBannerPro...
FILE: src/components/email/RawMessageModal.tsx
type RawMessageModalProps (line 6) | interface RawMessageModalProps {
function RawMessageModal (line 13) | function RawMessageModal({
FILE: src/components/email/SmartReplySuggestions.tsx
type SmartReplySuggestionsProps (line 9) | interface SmartReplySuggestionsProps {
function SmartReplySuggestions (line 16) | function SmartReplySuggestions({ threadId, accountId, messages, noReply ...
FILE: src/components/email/SnoozeDialog.tsx
type SnoozeDialogProps (line 3) | interface SnoozeDialogProps {
function getSnoozePresets (line 9) | function getSnoozePresets(): { label: string; timestamp: number }[] {
function SnoozeDialog (line 47) | function SnoozeDialog({ isOpen = true, onSnooze, onClose }: SnoozeDialog...
FILE: src/components/email/ThreadCard.test.tsx
function makeThread (line 36) | function makeThread(overrides: Partial<Thread> = {}): Thread {
FILE: src/components/email/ThreadCard.tsx
constant CATEGORY_COLORS (line 11) | const CATEGORY_COLORS: Record<string, string> = {
type ThreadCardProps (line 18) | interface ThreadCardProps {
FILE: src/components/email/ThreadSummary.tsx
type ThreadSummaryProps (line 8) | interface ThreadSummaryProps {
function ThreadSummary (line 14) | function ThreadSummary({ threadId, accountId, messages }: ThreadSummaryP...
FILE: src/components/email/ThreadView.tsx
type ThreadViewProps (line 26) | interface ThreadViewProps {
function handlePopOut (line 30) | async function handlePopOut(thread: Thread) {
function ThreadView (line 60) | function ThreadView({ thread }: ThreadViewProps) {
function buildQuote (line 518) | function buildQuote(msg: DbMessage): string {
function buildForwardQuote (line 527) | function buildForwardQuote(msg: DbMessage): string {
FILE: src/components/help/HelpCard.tsx
type HelpCardProps (line 5) | interface HelpCardProps {
function HelpCard (line 11) | function HelpCard({ card, isExpanded, onToggle }: HelpCardProps) {
FILE: src/components/help/HelpCardGrid.tsx
type HelpCardGridProps (line 4) | interface HelpCardGridProps {
function HelpCardGrid (line 10) | function HelpCardGrid({ cards, expandedCardId, onToggleCard }: HelpCardG...
FILE: src/components/help/HelpPage.tsx
function HelpPage (line 10) | function HelpPage() {
FILE: src/components/help/HelpSearchBar.tsx
type HelpSearchBarProps (line 3) | interface HelpSearchBarProps {
function HelpSearchBar (line 8) | function HelpSearchBar({ query, onChange }: HelpSearchBarProps) {
FILE: src/components/help/HelpSidebar.tsx
type HelpSidebarProps (line 4) | interface HelpSidebarProps {
function HelpSidebar (line 8) | function HelpSidebar({ activeTopic }: HelpSidebarProps) {
FILE: src/components/help/HelpTooltip.tsx
type HelpTooltipProps (line 7) | interface HelpTooltipProps {
function HelpTooltip (line 12) | function HelpTooltip({ contextId, size = 14 }: HelpTooltipProps) {
FILE: src/components/help/helpContentSearch.test.ts
function filterCards (line 10) | function filterCards(query: string) {
FILE: src/components/labels/LabelForm.tsx
constant GMAIL_LABEL_COLORS (line 6) | const GMAIL_LABEL_COLORS: { bg: string; fg: string }[] = [
type LabelFormProps (line 33) | interface LabelFormProps {
function LabelForm (line 40) | function LabelForm({ accountId, label, onDone, variant = "settings" }: L...
FILE: src/components/layout/EmailList.tsx
constant PAGE_SIZE (line 33) | const PAGE_SIZE = 50;
constant LABEL_MAP (line 36) | const LABEL_MAP: Record<string, string> = {
function EmailList (line 47) | function EmailList({ width, listRef }: { width?: number; listRef?: React...
function EmptyStateForContext (line 703) | function EmptyStateForContext({
FILE: src/components/layout/MailLayout.tsx
function ResizableEmailLayout (line 7) | function ResizableEmailLayout() {
function MailLayout (line 52) | function MailLayout() {
FILE: src/components/layout/ReadingPane.tsx
function ReadingPane (line 7) | function ReadingPane() {
FILE: src/components/layout/Sidebar.tsx
type SidebarProps (line 47) | interface SidebarProps {
constant ALL_NAV_ITEMS (line 52) | const ALL_NAV_ITEMS: { id: string; label: string; icon: LucideIcon }[] = [
constant CATEGORY_ITEMS (line 68) | const CATEGORY_ITEMS: { id: string; label: string; icon: LucideIcon }[] = [
function DroppableNavItem (line 76) | function DroppableNavItem({
function DroppableLabelItem (line 116) | function DroppableLabelItem({
constant SMART_FOLDER_ICON_MAP (line 191) | const SMART_FOLDER_ICON_MAP: Record<string, LucideIcon> = {
function getSmartFolderIcon (line 202) | function getSmartFolderIcon(iconName: string): LucideIcon {
constant LABELS_COLLAPSED_COUNT (line 206) | const LABELS_COLLAPSED_COUNT = 3;
function Sidebar (line 208) | function Sidebar({ collapsed, onAddAccount }: SidebarProps) {
function PendingOpsIndicator (line 665) | function PendingOpsIndicator({ collapsed }: { collapsed: boolean }) {
FILE: src/components/layout/TitleBar.tsx
function TitleBar (line 7) | function TitleBar() {
FILE: src/components/search/AskInbox.tsx
type AskInboxProps (line 8) | interface AskInboxProps {
function AskInbox (line 13) | function AskInbox({ isOpen, onClose }: AskInboxProps) {
FILE: src/components/search/CommandPalette.tsx
type Command (line 12) | interface Command {
type CommandPaletteProps (line 20) | interface CommandPaletteProps {
function CommandPalette (line 25) | function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
FILE: src/components/search/SearchBar.tsx
function SearchBar (line 9) | function SearchBar() {
FILE: src/components/search/ShortcutsHelp.tsx
type ShortcutsHelpProps (line 5) | interface ShortcutsHelpProps {
function ShortcutsHelp (line 10) | function ShortcutsHelp({ isOpen, onClose }: ShortcutsHelpProps) {
FILE: src/components/settings/CalDavSettings.tsx
type CalDavSettingsProps (line 9) | interface CalDavSettingsProps {
function CalDavSettings (line 14) | function CalDavSettings({ account, onSaved }: CalDavSettingsProps) {
FILE: src/components/settings/ContactEditor.tsx
function ContactEditor (line 10) | function ContactEditor() {
FILE: src/components/settings/FilterEditor.tsx
function FilterEditor (line 16) | function FilterEditor() {
FILE: src/components/settings/LabelEditor.test.tsx
function setStoreWithLabels (line 14) | function setStoreWithLabels(labels: { id: string; accountId: string; nam...
FILE: src/components/settings/LabelEditor.tsx
function LabelEditor (line 7) | function LabelEditor() {
FILE: src/components/settings/QuickStepEditor.tsx
function describeActions (line 20) | function describeActions(actionsJson: string): string {
function QuickStepEditor (line 37) | function QuickStepEditor() {
FILE: src/components/settings/SettingsPage.tsx
type SettingsTab (line 64) | type SettingsTab = "general" | "notifications" | "composing" | "mail-rul...
function SettingsPage (line 78) | function SettingsPage() {
function SendAsAliasesSection (line 1408) | function SendAsAliasesSection() {
function SyncOfflineSection (line 1493) | function SyncOfflineSection() {
function DeveloperTab (line 1577) | function DeveloperTab() {
function AboutTab (line 1728) | function AboutTab() {
function InfoRow (line 1824) | function InfoRow({ label, value }: { label: string; value: string }) {
function ShortcutsTab (line 1833) | function ShortcutsTab() {
function ImapCalDavSection (line 1990) | function ImapCalDavSection() {
function CalDavSettingsInline (line 2019) | function CalDavSettingsInline({ account, onSaved }: { account: import("@...
function SidebarNavEditor (line 2031) | function SidebarNavEditor() {
function Section (line 2142) | function Section({
function SettingRow (line 2159) | function SettingRow({
constant DAY_NAMES (line 2174) | const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
function BundleSettings (line 2176) | function BundleSettings() {
function ToggleRow (line 2290) | function ToggleRow({
FILE: src/components/settings/SignatureEditor.tsx
function SignatureEditor (line 18) | function SignatureEditor() {
FILE: src/components/settings/SmartFolderEditor.tsx
function SmartFolderEditor (line 13) | function SmartFolderEditor() {
FILE: src/components/settings/SmartLabelEditor.tsx
function SmartLabelEditor (line 16) | function SmartLabelEditor() {
FILE: src/components/settings/SubscriptionManager.tsx
function SubscriptionManager (line 12) | function SubscriptionManager() {
FILE: src/components/settings/TemplateEditor.tsx
function TemplateEditor (line 18) | function TemplateEditor() {
function InsertVariableDropdown (line 199) | function InsertVariableDropdown({ onInsert }: { onInsert: (variable: str...
FILE: src/components/tasks/AiTaskExtractDialog.tsx
constant PRIORITY_OPTIONS (line 9) | const PRIORITY_OPTIONS: { value: TaskPriority; label: string; color: str...
type AiTaskExtractDialogProps (line 17) | interface AiTaskExtractDialogProps {
function AiTaskExtractDialog (line 25) | function AiTaskExtractDialog({
FILE: src/components/tasks/TaskItem.test.tsx
function makeTask (line 5) | function makeTask(overrides: Partial<DbTask> = {}): DbTask {
FILE: src/components/tasks/TaskItem.tsx
constant PRIORITY_COLORS (line 14) | const PRIORITY_COLORS: Record<TaskPriority, string> = {
constant PRIORITY_DOT_COLORS (line 22) | const PRIORITY_DOT_COLORS: Record<TaskPriority, string> = {
function formatDueDate (line 30) | function formatDueDate(timestamp: number): string {
function getDueDateColor (line 44) | function getDueDateColor(timestamp: number): string {
type TaskItemProps (line 52) | interface TaskItemProps {
function TaskItem (line 62) | function TaskItem({
FILE: src/components/tasks/TaskQuickAdd.tsx
type TaskQuickAddProps (line 4) | interface TaskQuickAddProps {
function TaskQuickAdd (line 9) | function TaskQuickAdd({ onAdd, placeholder = "Add a task..." }: TaskQuic...
FILE: src/components/tasks/TaskSidebar.tsx
type TaskSidebarProps (line 19) | interface TaskSidebarProps {
function TaskSidebar (line 24) | function TaskSidebar({ accountId, threadId }: TaskSidebarProps) {
FILE: src/components/tasks/TasksPage.tsx
constant PRIORITY_ORDER (line 25) | const PRIORITY_ORDER: Record<TaskPriority, number> = {
function TasksPage (line 33) | function TasksPage() {
FILE: src/components/ui/Button.tsx
type ButtonProps (line 3) | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
function Button (line 12) | function Button({
FILE: src/components/ui/ConfirmDialog.tsx
type ConfirmDialogProps (line 5) | interface ConfirmDialogProps {
function ConfirmDialog (line 17) | function ConfirmDialog({
FILE: src/components/ui/ContextMenu.tsx
type ContextMenuItem (line 6) | interface ContextMenuItem {
type ContextMenuProps (line 19) | interface ContextMenuProps {
function ContextMenu (line 25) | function ContextMenu({ items, position, onClose }: ContextMenuProps) {
function Submenu (line 269) | function Submenu({
FILE: src/components/ui/ContextMenuPortal.tsx
function buildQuote (line 46) | function buildQuote(msg: { from_name: string | null; from_address: strin...
function buildForwardQuote (line 54) | function buildForwardQuote(msg: { from_name: string | null; from_address...
function ContextMenuPortal (line 59) | function ContextMenuPortal() {
function SidebarLabelMenu (line 119) | function SidebarLabelMenu({
function SidebarNavMenu (line 165) | function SidebarNavMenu({
function ThreadMenu (line 195) | function ThreadMenu({
function MessageMenu (line 599) | function MessageMenu({
FILE: src/components/ui/DateTimePickerDialog.tsx
type Preset (line 5) | interface Preset {
type DateTimePickerDialogProps (line 13) | interface DateTimePickerDialogProps {
function DateTimePickerDialog (line 24) | function DateTimePickerDialog({
FILE: src/components/ui/EmptyState.tsx
type EmptyStateProps (line 4) | type EmptyStateProps = {
function EmptyState (line 12) | function EmptyState({ title, subtitle, ...rest }: EmptyStateProps) {
FILE: src/components/ui/ErrorBoundary.test.tsx
function ThrowingComponent (line 5) | function ThrowingComponent({ message }: { message: string }) {
function GoodComponent (line 10) | function GoodComponent() {
function MaybeThrow (line 83) | function MaybeThrow() {
FILE: src/components/ui/ErrorBoundary.tsx
type ErrorBoundaryProps (line 3) | interface ErrorBoundaryProps {
type ErrorBoundaryState (line 9) | interface ErrorBoundaryState {
class ErrorBoundary (line 14) | class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryS...
method constructor (line 15) | constructor(props: ErrorBoundaryProps) {
method getDerivedStateFromError (line 20) | static getDerivedStateFromError(error: Error): ErrorBoundaryState {
method componentDidCatch (line 24) | componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
method render (line 28) | render(): ReactNode {
FILE: src/components/ui/InputDialog.tsx
type InputField (line 5) | interface InputField {
type InputDialogProps (line 13) | interface InputDialogProps {
function InputDialog (line 22) | function InputDialog({
FILE: src/components/ui/Modal.tsx
type ModalProps (line 5) | interface ModalProps {
function Modal (line 19) | function Modal({
FILE: src/components/ui/OfflineBanner.tsx
function OfflineBanner (line 4) | function OfflineBanner() {
FILE: src/components/ui/Skeleton.tsx
function ThreadCardSkeleton (line 1) | function ThreadCardSkeleton() {
function EmailListSkeleton (line 19) | function EmailListSkeleton({ count = 8 }: { count?: number }) {
function MessageSkeleton (line 29) | function MessageSkeleton() {
FILE: src/components/ui/TextField.tsx
type TextFieldProps (line 3) | interface TextFieldProps extends Omit<InputHTMLAttributes<HTMLInputEleme...
FILE: src/components/ui/UpdateToast.tsx
function UpdateToast (line 9) | function UpdateToast() {
FILE: src/components/ui/illustrations/GenericEmptyIllustration.tsx
type Props (line 1) | interface Props {
function GenericEmptyIllustration (line 6) | function GenericEmptyIllustration({ size = 140, className }: Props) {
FILE: src/components/ui/illustrations/InboxClearIllustration.tsx
type Props (line 1) | interface Props {
function InboxClearIllustration (line 6) | function InboxClearIllustration({ size = 140, className }: Props) {
FILE: src/components/ui/illustrations/NoAccountIllustration.tsx
type Props (line 1) | interface Props {
function NoAccountIllustration (line 6) | function NoAccountIllustration({ size = 140, className }: Props) {
FILE: src/components/ui/illustrations/NoSearchResultsIllustration.tsx
type Props (line 1) | interface Props {
function NoSearchResultsIllustration (line 6) | function NoSearchResultsIllustration({ size = 140, className }: Props) {
FILE: src/components/ui/illustrations/ReadingPaneIllustration.tsx
type Props (line 1) | interface Props {
function ReadingPaneIllustration (line 6) | function ReadingPaneIllustration({ size = 140, className }: Props) {
FILE: src/constants/helpContent.test.ts
constant VALID_SETTINGS_TABS (line 9) | const VALID_SETTINGS_TABS = [
FILE: src/constants/helpContent.ts
type HelpTip (line 69) | interface HelpTip {
type HelpCard (line 74) | interface HelpCard {
type HelpCategory (line 84) | interface HelpCategory {
type ContextualTip (line 91) | interface ContextualTip {
constant VALID_SETTINGS_TABS (line 99) | const VALID_SETTINGS_TABS = [
type SettingsTabId (line 104) | type SettingsTabId = (typeof VALID_SETTINGS_TABS)[number];
constant HELP_CATEGORIES (line 108) | const HELP_CATEGORIES: HelpCategory[] = [
constant CONTEXTUAL_TIPS (line 1277) | const CONTEXTUAL_TIPS: Record<string, ContextualTip> = {
function getAllCards (line 1328) | function getAllCards(): (HelpCard & { categoryId: string; categoryLabel:...
function getCategoryById (line 1339) | function getCategoryById(id: string): HelpCategory | undefined {
FILE: src/constants/shortcuts.ts
type ShortcutItem (line 1) | interface ShortcutItem {
type ShortcutCategory (line 7) | interface ShortcutCategory {
constant SHORTCUTS (line 12) | const SHORTCUTS: ShortcutCategory[] = [
function getDefaultKeyMap (line 62) | function getDefaultKeyMap(): Record<string, string> {
FILE: src/constants/themes.ts
type ColorThemeId (line 1) | type ColorThemeId =
type ThemeColors (line 11) | interface ThemeColors {
type ColorTheme (line 19) | interface ColorTheme {
constant COLOR_THEMES (line 27) | const COLOR_THEMES: ColorTheme[] = [
constant DEFAULT_COLOR_THEME (line 182) | const DEFAULT_COLOR_THEME: ColorThemeId = "indigo";
function getThemeById (line 184) | function getThemeById(id: string): ColorTheme {
FILE: src/hooks/useClickOutside.ts
function useClickOutside (line 3) | function useClickOutside(
FILE: src/hooks/useContextMenu.ts
function useContextMenu (line 8) | function useContextMenu(
FILE: src/hooks/useKeyboardShortcuts.ts
function matchesKey (line 22) | function matchesKey(binding: string, e: KeyboardEvent): boolean {
function buildReverseMap (line 45) | function buildReverseMap(keyMap: Record<string, string>): {
function getCachedReverseMap (line 73) | function getCachedReverseMap(keyMap: Record<string, string>): ReturnType...
function useKeyboardShortcuts (line 84) | function useKeyboardShortcuts() {
function executeAction (line 201) | async function executeAction(actionId: string): Promise<void> {
FILE: src/hooks/useRouteNavigation.test.ts
function setMatches (line 21) | function setMatches(
FILE: src/hooks/useRouteNavigation.ts
function useMatchesSafe (line 7) | function useMatchesSafe() {
function useActiveLabel (line 19) | function useActiveLabel(): string {
function useSelectedThreadId (line 47) | function useSelectedThreadId(): string | null {
function useActiveCategory (line 61) | function useActiveCategory(): string {
function useSearchQuery (line 75) | function useSearchQuery(): string {
FILE: src/main.tsx
function Root (line 13) | function Root() {
FILE: src/router/index.ts
type Register (line 14) | interface Register {
FILE: src/router/navigate.test.ts
method state (line 13) | get state() {
FILE: src/router/navigate.ts
constant SYSTEM_LABELS (line 4) | const SYSTEM_LABELS = new Set([
function navigateToLabel (line 12) | function navigateToLabel(
function navigateToThread (line 94) | function navigateToThread(threadId: string): void {
function navigateToSettings (line 141) | function navigateToSettings(tab = "general"): void {
function navigateToHelp (line 148) | function navigateToHelp(topic = "getting-started"): void {
function navigateBack (line 155) | function navigateBack(): void {
function getActiveLabel (line 197) | function getActiveLabel(): string {
function getSelectedThreadId (line 231) | function getSelectedThreadId(): string | null {
FILE: src/router/routeTree.tsx
constant VALID_CATEGORIES (line 19) | const VALID_CATEGORIES = ["Primary", "Updates", "Promotions", "Social", ...
type MailSearch (line 21) | type MailSearch = {
function validateMailSearch (line 26) | function validateMailSearch(search: Record<string, unknown>): MailSearch {
function MailPage (line 53) | function MailPage() {
function SettingsTabPage (line 61) | function SettingsTabPage() {
function CalendarPageWrapper (line 71) | function CalendarPageWrapper() {
function HelpPageWrapper (line 81) | function HelpPageWrapper() {
function AttachmentLibraryWrapper (line 150) | function AttachmentLibraryWrapper() {
function TasksPageWrapper (line 167) | function TasksPageWrapper() {
FILE: src/services/ai/aiService.ts
function callAi (line 19) | async function callAi(systemPrompt: string, userContent: string): Promis...
function formatMessageForSummary (line 36) | function formatMessageForSummary(msg: DbMessage): string {
function summarizeThread (line 49) | async function summarizeThread(
function composeFromPrompt (line 68) | async function composeFromPrompt(instructions: string): Promise<string> {
function generateReply (line 72) | async function generateReply(
type TransformType (line 83) | type TransformType = "improve" | "shorten" | "formalize";
function transformText (line 85) | async function transformText(
function generateSmartReplies (line 97) | async function generateSmartReplies(
function askInbox (line 146) | async function askInbox(
constant VALID_CATEGORIES (line 155) | const VALID_CATEGORIES = new Set(["Primary", "Updates", "Promotions", "S...
function categorizeThreads (line 157) | async function categorizeThreads(
function classifyThreadsBySmartLabels (line 185) | async function classifyThreadsBySmartLabels(
function extractTaskFromThread (line 227) | async function extractTaskFromThread(
function testConnection (line 238) | async function testConnection(): Promise<boolean> {
FILE: src/services/ai/askInbox.ts
function extractSearchTerms (line 8) | function extractSearchTerms(question: string): string {
type AskInboxResult (line 32) | interface AskInboxResult {
function askMyInbox (line 41) | async function askMyInbox(
FILE: src/services/ai/categorizationManager.ts
function categorizeNewThreads (line 9) | async function categorizeNewThreads(accountId: string): Promise<void> {
FILE: src/services/ai/errors.ts
type AiErrorCode (line 1) | type AiErrorCode =
class AiError (line 7) | class AiError extends Error {
method constructor (line 10) | constructor(code: AiErrorCode, message: string) {
FILE: src/services/ai/prompts.ts
constant SUMMARIZE_PROMPT (line 1) | const SUMMARIZE_PROMPT = `You are summarizing an email thread. Each mess...
constant COMPOSE_PROMPT (line 12) | const COMPOSE_PROMPT = `Write an email based on the following instructio...
constant REPLY_PROMPT (line 14) | const REPLY_PROMPT = `Write a reply to this email thread. Consider the f...
constant IMPROVE_PROMPT (line 18) | const IMPROVE_PROMPT = `Improve the following email text. Make it cleare...
constant SHORTEN_PROMPT (line 20) | const SHORTEN_PROMPT = `Make the following email text more concise while...
constant FORMALIZE_PROMPT (line 22) | const FORMALIZE_PROMPT = `Rewrite the following email text in a more for...
constant SMART_REPLY_PROMPT (line 24) | const SMART_REPLY_PROMPT = `Generate exactly 3 short email reply options...
constant ASK_INBOX_PROMPT (line 35) | const ASK_INBOX_PROMPT = `You are an AI assistant that answers questions...
constant CATEGORIZE_PROMPT (line 46) | const CATEGORIZE_PROMPT = `Categorize each email thread into exactly ONE...
constant WRITING_STYLE_ANALYSIS_PROMPT (line 60) | const WRITING_STYLE_ANALYSIS_PROMPT = `Analyze the writing style of the ...
constant AUTO_DRAFT_REPLY_PROMPT (line 71) | const AUTO_DRAFT_REPLY_PROMPT = `Generate a complete email reply draft f...
constant SMART_LABEL_PROMPT (line 84) | const SMART_LABEL_PROMPT = `Classify each email thread against a set of ...
constant EXTRACT_TASK_PROMPT (line 100) | const EXTRACT_TASK_PROMPT = `Extract an actionable task from the followi...
FILE: src/services/ai/providerFactory.ts
function createProviderFactory (line 5) | function createProviderFactory<TClient>(
FILE: src/services/ai/providerManager.ts
constant API_KEY_SETTINGS (line 11) | const API_KEY_SETTINGS: Record<Exclude<AiProvider, "ollama">, string> = {
function getActiveProviderName (line 20) | async function getActiveProviderName(): Promise<AiProvider> {
function getActiveProvider (line 26) | async function getActiveProvider(): Promise<AiProviderClient> {
function isAiAvailable (line 77) | async function isAiAvailable(): Promise<boolean> {
function clearProviderClients (line 96) | function clearProviderClients(): void {
FILE: src/services/ai/providers/claudeProvider.ts
function createClaudeProvider (line 9) | function createClaudeProvider(apiKey: string, model: string): AiProvider...
function clearClaudeProvider (line 40) | function clearClaudeProvider(): void {
FILE: src/services/ai/providers/copilotProvider.ts
function createCopilotProvider (line 15) | function createCopilotProvider(apiKey: string, model: string): AiProvide...
function clearCopilotProvider (line 47) | function clearCopilotProvider(): void {
FILE: src/services/ai/providers/geminiProvider.ts
function createGeminiProvider (line 9) | function createGeminiProvider(apiKey: string, modelId: string): AiProvid...
function clearGeminiProvider (line 37) | function clearGeminiProvider(): void {
FILE: src/services/ai/providers/ollamaProvider.ts
function getClient (line 8) | function getClient(serverUrl: string, model: string): OpenAI {
function createOllamaProvider (line 22) | function createOllamaProvider(serverUrl: string, model: string): AiProvi...
function clearOllamaProvider (line 54) | function clearOllamaProvider(): void {
FILE: src/services/ai/providers/openaiProvider.ts
function createOpenAIProvider (line 9) | function createOpenAIProvider(apiKey: string, model: string): AiProvider...
function clearOpenAIProvider (line 41) | function clearOpenAIProvider(): void {
FILE: src/services/ai/taskExtraction.test.ts
function makeMessage (line 10) | function makeMessage(overrides: Partial<DbMessage> = {}): DbMessage {
FILE: src/services/ai/taskExtraction.ts
type ExtractedTask (line 5) | interface ExtractedTask {
constant VALID_PRIORITIES (line 12) | const VALID_PRIORITIES = new Set<TaskPriority>(["none", "low", "medium",...
function extractTask (line 17) | async function extractTask(
FILE: src/services/ai/types.ts
type AiProvider (line 1) | type AiProvider = "claude" | "openai" | "gemini" | "ollama" | "copilot";
type AiCompletionRequest (line 3) | interface AiCompletionRequest {
type AiProviderClient (line 9) | interface AiProviderClient {
constant DEFAULT_MODELS (line 14) | const DEFAULT_MODELS: Record<AiProvider, string> = {
type ModelOption (line 22) | interface ModelOption {
constant PROVIDER_MODELS (line 27) | const PROVIDER_MODELS: Record<Exclude<AiProvider, "ollama">, ModelOption...
constant MODEL_SETTINGS (line 53) | const MODEL_SETTINGS: Record<Exclude<AiProvider, "ollama">, string> = {
FILE: src/services/ai/writingStyleService.test.ts
function makeSentMessage (line 50) | function makeSentMessage(overrides: Partial<DbMessage> = {}): DbMessage {
FILE: src/services/ai/writingStyleService.ts
function callAi (line 14) | async function callAi(systemPrompt: string, userContent: string): Promis...
function analyzeWritingStyle (line 34) | async function analyzeWritingStyle(samples: DbMessage[]): Promise<string> {
function getOrCreateStyleProfile (line 48) | async function getOrCreateStyleProfile(accountId: string): Promise<strin...
function refreshWritingStyle (line 73) | async function refreshWritingStyle(accountId: string): Promise<string | ...
function formatThreadForDraft (line 78) | function formatThreadForDraft(messages: DbMessage[]): string {
type AutoDraftMode (line 95) | type AutoDraftMode = "reply" | "replyAll";
function generateAutoDraft (line 101) | async function generateAutoDraft(
function regenerateAutoDraft (line 138) | async function regenerateAutoDraft(
function isAutoDraftEnabled (line 152) | async function isAutoDraftEnabled(): Promise<boolean> {
FILE: src/services/attachments/cacheManager.ts
constant CACHE_DIR (line 4) | const CACHE_DIR = "attachment_cache";
function hashFileName (line 6) | function hashFileName(id: string): string {
function cacheAttachment (line 20) | async function cacheAttachment(
function loadCachedAttachment (line 53) | async function loadCachedAttachment(
function getCacheSize (line 64) | async function getCacheSize(): Promise<number> {
function evictOldestCached (line 72) | async function evictOldestCached(): Promise<void> {
function clearAllCache (line 107) | async function clearAllCache(): Promise<void> {
FILE: src/services/attachments/preCacheManager.test.ts
function runPreCache (line 42) | async function runPreCache() {
FILE: src/services/attachments/preCacheManager.ts
constant MAX_ATTACHMENT_SIZE (line 8) | const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
constant RECENT_DAYS (line 9) | const RECENT_DAYS = 7;
constant BATCH_LIMIT (line 10) | const BATCH_LIMIT = 20;
function preCacheRecent (line 14) | async function preCacheRecent(): Promise<void> {
function startPreCacheManager (line 73) | function startPreCacheManager(): void {
function stopPreCacheManager (line 79) | function stopPreCacheManager(): void {
FILE: src/services/backgroundCheckers.ts
type BackgroundChecker (line 5) | interface BackgroundChecker {
function createBackgroundChecker (line 10) | function createBackgroundChecker(
FILE: src/services/badgeManager.ts
function updateBadgeCount (line 7) | async function updateBadgeCount(): Promise<void> {
FILE: src/services/bundles/bundleManager.ts
function isDeliveryTime (line 15) | function isDeliveryTime(schedule: DeliverySchedule): boolean {
function checkBundleDelivery (line 30) | async function checkBundleDelivery(): Promise<void> {
FILE: src/services/calendar/autoDiscovery.ts
type CalDavPreset (line 1) | interface CalDavPreset {
constant PRESETS (line 8) | const PRESETS: CalDavPreset[] = [
type CalDavDiscoveryResult (line 41) | interface CalDavDiscoveryResult {
function discoverCalDavSettings (line 52) | async function discoverCalDavSettings(email: string): Promise<CalDavDisc...
function tryWellKnownDiscovery (line 95) | async function tryWellKnownDiscovery(domain: string): Promise<string | n...
function tryNextcloudDiscovery (line 124) | async function tryNextcloudDiscovery(domain: string): Promise<string | n...
function testCalDavConnection (line 142) | async function testCalDavConnection(
FILE: src/services/calendar/caldavProvider.test.ts
constant MOCK_ICAL_DATA (line 3) | const MOCK_ICAL_DATA =
constant MOCK_ICAL_DATA_2 (line 6) | const MOCK_ICAL_DATA_2 =
FILE: src/services/calendar/caldavProvider.ts
class CalDAVProvider (line 14) | class CalDAVProvider implements CalendarProvider {
method constructor (line 18) | constructor(readonly accountId: string) {}
method getClient (line 20) | private async getClient(): Promise<DAVClient> {
method listCalendars (line 45) | async listCalendars(): Promise<CalendarInfo[]> {
method fetchEvents (line 57) | async fetchEvents(calendarRemoteId: string, timeMin: string, timeMax: ...
method createEvent (line 77) | async createEvent(calendarRemoteId: string, event: CreateEventInput): ...
method updateEvent (line 93) | async updateEvent(
method deleteEvent (line 139) | async deleteEvent(_calendarRemoteId: string, remoteEventId: string, et...
method syncEvents (line 154) | async syncEvents(calendarRemoteId: string, _syncToken?: string): Promi...
method testConnection (line 185) | async testConnection(): Promise<{ success: boolean; message: string }> {
function extractCalendarColor (line 201) | function extractCalendarColor(cal: DAVCalendar): string | null {
FILE: src/services/calendar/googleCalendarProvider.test.ts
constant CALENDAR_API_BASE (line 8) | const CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
function createMockClient (line 10) | function createMockClient() {
FILE: src/services/calendar/googleCalendarProvider.ts
constant CALENDAR_API_BASE (line 13) | const CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
type GoogleCalendarListItem (line 15) | interface GoogleCalendarListItem {
type GoogleCalendarListResponse (line 23) | interface GoogleCalendarListResponse {
type GoogleCalendarEvent (line 27) | interface GoogleCalendarEvent {
type GoogleEventListResponse (line 42) | interface GoogleEventListResponse {
class GoogleCalendarProvider (line 48) | class GoogleCalendarProvider implements CalendarProvider {
method constructor (line 51) | constructor(readonly accountId: string) {}
method getClient (line 53) | private async getClient(): Promise<GmailClient> {
method listCalendars (line 57) | async listCalendars(): Promise<CalendarInfo[]> {
method fetchEvents (line 70) | async fetchEvents(calendarRemoteId: string, timeMin: string, timeMax: ...
method createEvent (line 86) | async createEvent(calendarRemoteId: string, event: CreateEventInput): ...
method updateEvent (line 117) | async updateEvent(calendarRemoteId: string, remoteEventId: string, eve...
method deleteEvent (line 146) | async deleteEvent(calendarRemoteId: string, remoteEventId: string): Pr...
method syncEvents (line 154) | async syncEvents(calendarRemoteId: string, syncToken?: string): Promis...
method testConnection (line 213) | async testConnection(): Promise<{ success: boolean; message: string }> {
function mapGoogleEvent (line 223) | function mapGoogleEvent(event: GoogleCalendarEvent): CalendarEventData {
FILE: src/services/calendar/icalHelper.ts
function generateVEvent (line 6) | function generateVEvent(event: CreateEventInput | UpdateEventInput, uid?...
function parseVEvent (line 56) | function parseVEvent(icalData: string, href?: string): CalendarEventData {
function unfoldLines (line 146) | function unfoldLines(icalData: string): string[] {
function formatDateTimeUTC (line 151) | function formatDateTimeUTC(date: Date): string {
function formatDateOnly (line 155) | function formatDateOnly(date: Date): string {
function escapeICalText (line 162) | function escapeICalText(text: string): string {
function unescapeICalText (line 170) | function unescapeICalText(text: string): string {
function parseICalDateTime (line 178) | function parseICalDateTime(value: string, isAllDay: boolean): number {
FILE: src/services/calendar/providerFactory.test.ts
class GoogleCalendarProvider (line 14) | class GoogleCalendarProvider {
method constructor (line 17) | constructor(accountId: string) {
class CalDAVProvider (line 25) | class CalDAVProvider {
method constructor (line 28) | constructor(accountId: string) {
FILE: src/services/calendar/providerFactory.ts
function getCalendarProvider (line 12) | async function getCalendarProvider(accountId: string): Promise<CalendarP...
function hasCalendarSupport (line 47) | async function hasCalendarSupport(accountId: string): Promise<boolean> {
function removeCalendarProvider (line 57) | function removeCalendarProvider(accountId: string): void {
function clearAllCalendarProviders (line 61) | function clearAllCalendarProviders(): void {
FILE: src/services/calendar/types.ts
type CalendarProviderType (line 1) | type CalendarProviderType = "google_api" | "caldav";
type CalendarInfo (line 3) | interface CalendarInfo {
type CalendarEventData (line 10) | interface CalendarEventData {
type CreateEventInput (line 27) | interface CreateEventInput {
type UpdateEventInput (line 37) | interface UpdateEventInput {
type CalendarSyncResult (line 46) | interface CalendarSyncResult {
type CalendarProvider (line 54) | interface CalendarProvider {
FILE: src/services/categorization/backfillService.ts
function backfillUncategorizedThreads (line 14) | async function backfillUncategorizedThreads(
FILE: src/services/categorization/ruleEngine.test.ts
function input (line 3) | function input(overrides: Partial<CategorizationInput> = {}): Categoriza...
FILE: src/services/categorization/ruleEngine.ts
type CategorizationInput (line 3) | interface CategorizationInput {
constant SOCIAL_DOMAINS (line 9) | const SOCIAL_DOMAINS = new Set([
constant NEWSLETTER_DOMAINS (line 27) | const NEWSLETTER_DOMAINS = new Set([
constant PROMO_PREFIXES (line 44) | const PROMO_PREFIXES = new Set([
constant UPDATE_PREFIXES (line 58) | const UPDATE_PREFIXES = new Set([
function getDomain (line 78) | function getDomain(email: string): string | null {
function getLocalPart (line 84) | function getLocalPart(email: string): string | null {
function categorizeByRules (line 99) | function categorizeByRules(input: CategorizationInput): ThreadCategory {
FILE: src/services/composer/draftAutoSave.ts
constant DEBOUNCE_MS (line 10) | const DEBOUNCE_MS = 3000;
function saveDraft (line 12) | async function saveDraft(): Promise<void> {
function scheduleSave (line 60) | function scheduleSave(): void {
function startAutoSave (line 68) | function startAutoSave(accountId: string): void {
function stopAutoSave (line 94) | function stopAutoSave(): void {
FILE: src/services/contacts/gravatar.ts
function md5 (line 8) | function md5(input: string): string {
function getGravatarUrl (line 94) | function getGravatarUrl(email: string): string {
function fetchAndCacheGravatarUrl (line 99) | async function fetchAndCacheGravatarUrl(email: string): Promise<string |...
FILE: src/services/db/accounts.ts
type DbAccount (line 4) | interface DbAccount {
function decryptAccountTokens (line 39) | async function decryptAccountTokens(account: DbAccount): Promise<DbAccou...
function getAllAccounts (line 78) | async function getAllAccounts(): Promise<DbAccount[]> {
function getAccount (line 86) | async function getAccount(id: string): Promise<DbAccount | null> {
function getAccountByEmail (line 94) | async function getAccountByEmail(
function insertAccount (line 104) | async function insertAccount(account: {
function updateAccountTokens (line 131) | async function updateAccountTokens(
function updateAccountSyncState (line 144) | async function updateAccountSyncState(
function clearAccountHistoryId (line 155) | async function clearAccountHistoryId(id: string): Promise<void> {
function updateAccountAllTokens (line 163) | async function updateAccountAllTokens(
function deleteAccount (line 178) | async function deleteAccount(id: string): Promise<void> {
function insertImapAccount (line 183) | async function insertImapAccount(account: {
function insertCalDavAccount (line 223) | async function insertCalDavAccount(account: {
function updateAccountCalDav (line 251) | async function updateAccountCalDav(
function insertOAuthImapAccount (line 280) | async function insertOAuthImapAccount(account: {
FILE: src/services/db/aiCache.ts
type AiCacheEntry (line 3) | interface AiCacheEntry {
function getAiCache (line 12) | async function getAiCache(
function setAiCache (line 25) | async function setAiCache(
function deleteAiCache (line 42) | async function deleteAiCache(
FILE: src/services/db/attachments.ts
type DbAttachment (line 3) | interface DbAttachment {
function upsertAttachment (line 16) | async function upsertAttachment(att: {
type AttachmentWithContext (line 48) | interface AttachmentWithContext {
function getAttachmentsForAccount (line 66) | async function getAttachmentsForAccount(
type AttachmentSender (line 83) | interface AttachmentSender {
function getAttachmentSenders (line 89) | async function getAttachmentSenders(
function getAttachmentsForMessage (line 105) | async function getAttachmentsForMessage(
FILE: src/services/db/bundleRules.ts
type DeliverySchedule (line 4) | interface DeliverySchedule {
type DbBundleRule (line 10) | interface DbBundleRule {
type DbBundledThread (line 21) | interface DbBundledThread {
function getBundleRules (line 28) | async function getBundleRules(accountId: string): Promise<DbBundleRule[]> {
function getBundleRule (line 36) | async function getBundleRule(
function setBundleRule (line 46) | async function setBundleRule(
function holdThread (line 64) | async function holdThread(
function isThreadHeld (line 80) | async function isThreadHeld(
function getHeldThreadIds (line 91) | async function getHeldThreadIds(
function releaseHeldThreads (line 103) | async function releaseHeldThreads(
function updateLastDelivered (line 115) | async function updateLastDelivered(
function getBundleSummary (line 127) | async function getBundleSummary(
function getBundleSummaries (line 162) | async function getBundleSummaries(
function getNextDeliveryTime (line 209) | function getNextDeliveryTime(schedule: DeliverySchedule): number {
FILE: src/services/db/calendarEvents.ts
type DbCalendarEvent (line 3) | interface DbCalendarEvent {
function upsertCalendarEvent (line 26) | async function upsertCalendarEvent(event: {
function getCalendarEventsInRange (line 65) | async function getCalendarEventsInRange(
function getCalendarEventsInRangeMulti (line 79) | async function getCalendarEventsInRangeMulti(
function deleteEventsForCalendar (line 99) | async function deleteEventsForCalendar(calendarId: string): Promise<void> {
function getEventByRemoteId (line 104) | async function getEventByRemoteId(
function deleteEventByRemoteId (line 114) | async function deleteEventByRemoteId(
function deleteCalendarEvent (line 125) | async function deleteCalendarEvent(eventId: string): Promise<void> {
FILE: src/services/db/calendars.test.ts
constant MOCK_UUID (line 35) | const MOCK_UUID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
function makeCal (line 232) | function makeCal(overrides: Partial<DbCalendar> = {}): DbCalendar {
FILE: src/services/db/calendars.ts
type DbCalendar (line 3) | interface DbCalendar {
function upsertCalendar (line 18) | async function upsertCalendar(calendar: {
function getCalendarsForAccount (line 43) | async function getCalendarsForAccount(accountId: string): Promise<DbCale...
function getVisibleCalendars (line 51) | async function getVisibleCalendars(accountId: string): Promise<DbCalenda...
function setCalendarVisibility (line 59) | async function setCalendarVisibility(calendarId: string, visible: boolea...
function updateCalendarSyncToken (line 67) | async function updateCalendarSyncToken(
function deleteCalendarsForAccount (line 79) | async function deleteCalendarsForAccount(accountId: string): Promise<voi...
function getCalendarById (line 84) | async function getCalendarById(calendarId: string): Promise<DbCalendar |...
FILE: src/services/db/connection.ts
function getDb (line 5) | async function getDb(): Promise<Database> {
function buildDynamicUpdate (line 16) | function buildDynamicUpdate(
function withTransaction (line 48) | async function withTransaction(fn: (db: Database) => Promise<void>): Pro...
function selectFirstBy (line 87) | async function selectFirstBy<T>(
function existsBy (line 99) | async function existsBy(
function boolToInt (line 111) | function boolToInt(value: boolean | undefined | null): number {
FILE: src/services/db/contacts.ts
type DbContact (line 4) | interface DbContact {
type ContactAttachment (line 14) | interface ContactAttachment {
type SameDomainContact (line 21) | interface SameDomainContact {
function searchContacts (line 30) | async function searchContacts(
function getAllContacts (line 48) | async function getAllContacts(
function updateContact (line 64) | async function updateContact(
function deleteContact (line 78) | async function deleteContact(id: string): Promise<void> {
function upsertContact (line 86) | async function upsertContact(
function getContactByEmail (line 104) | async function getContactByEmail(
type ContactStats (line 113) | interface ContactStats {
function getContactStats (line 119) | async function getContactStats(
function getRecentThreadsWithContact (line 136) | async function getRecentThreadsWithContact(
function updateContactAvatar (line 152) | async function updateContactAvatar(
function updateContactNotes (line 166) | async function updateContactNotes(
function getAttachmentsFromContact (line 180) | async function getAttachmentsFromContact(
constant PUBLIC_DOMAINS (line 196) | const PUBLIC_DOMAINS = new Set([
function getContactsFromSameDomain (line 207) | async function getContactsFromSameDomain(
function getLatestAuthResult (line 231) | async function getLatestAuthResult(
FILE: src/services/db/filters.ts
type FilterCriteria (line 3) | interface FilterCriteria {
type FilterActions (line 11) | interface FilterActions {
type DbFilterRule (line 19) | interface DbFilterRule {
function getFiltersForAccount (line 30) | async function getFiltersForAccount(
function getEnabledFiltersForAccount (line 40) | async function getEnabledFiltersForAccount(
function insertFilter (line 50) | async function insertFilter(filter: {
function updateFilter (line 73) | async function updateFilter(
function deleteFilter (line 95) | async function deleteFilter(id: string): Promise<void> {
FILE: src/services/db/folderSyncState.ts
type FolderSyncState (line 3) | interface FolderSyncState {
function getFolderSyncState (line 12) | async function getFolderSyncState(
function upsertFolderSyncState (line 22) | async function upsertFolderSyncState(
function deleteFolderSyncState (line 42) | async function deleteFolderSyncState(
function clearAllFolderSyncStates (line 53) | async function clearAllFolderSyncStates(
function getAllFolderSyncStates (line 63) | async function getAllFolderSyncStates(
FILE: src/services/db/followUpReminders.ts
type DbFollowUpReminder (line 4) | interface DbFollowUpReminder {
function insertFollowUpReminder (line 14) | async function insertFollowUpReminder(
function getPendingFollowUpReminders (line 31) | async function getPendingFollowUpReminders(): Promise<DbFollowUpReminder...
function getFollowUpForThread (line 40) | async function getFollowUpForThread(
function updateFollowUpStatus (line 50) | async function updateFollowUpStatus(
function cancelFollowUpForThread (line 61) | async function cancelFollowUpForThread(
function getActiveFollowUpThreadIds (line 72) | async function getActiveFollowUpThreadIds(
FILE: src/services/db/imageAllowlist.ts
function isAllowlisted (line 4) | async function isAllowlisted(
function getAllowlistedSenders (line 19) | async function getAllowlistedSenders(
function addToAllowlist (line 34) | async function addToAllowlist(
function removeFromAllowlist (line 46) | async function removeFromAllowlist(
type AllowlistEntry (line 57) | interface AllowlistEntry {
function getAllowlistForAccount (line 64) | async function getAllowlistForAccount(
FILE: src/services/db/labels.ts
type DbLabel (line 3) | interface DbLabel {
function getLabelsForAccount (line 16) | async function getLabelsForAccount(
function upsertLabel (line 26) | async function upsertLabel(label: {
function deleteLabelsForAccount (line 57) | async function deleteLabelsForAccount(
function deleteLabel (line 64) | async function deleteLabel(
function updateLabelSortOrder (line 75) | async function updateLabelSortOrder(
FILE: src/services/db/linkScanResults.ts
function getCachedScanResult (line 3) | async function getCachedScanResult(
function cacheScanResult (line 15) | async function cacheScanResult(
function deleteScanResults (line 27) | async function deleteScanResults(accountId: string): Promise<void> {
FILE: src/services/db/localDrafts.ts
type LocalDraft (line 3) | interface LocalDraft {
function upsertLocalDraft (line 22) | async function upsertLocalDraft(draft: {
function getLocalDraft (line 65) | async function getLocalDraft(id: string): Promise<LocalDraft | null> {
function getUnsyncedDrafts (line 74) | async function getUnsyncedDrafts(
function markDraftSynced (line 84) | async function markDraftSynced(
function deleteLocalDraft (line 95) | async function deleteLocalDraft(id: string): Promise<void> {
FILE: src/services/db/messages.ts
type DbMessage (line 3) | interface DbMessage {
function getMessagesForThread (line 33) | async function getMessagesForThread(
function upsertMessage (line 44) | async function upsertMessage(msg: {
function deleteMessage (line 119) | async function deleteMessage(
function updateMessageThreadIds (line 130) | async function updateMessageThreadIds(
function deleteAllMessagesForAccount (line 147) | async function deleteAllMessagesForAccount(
function getRecentSentMessages (line 161) | async function getRecentSentMessages(
FILE: src/services/db/migrations.test.ts
function splitStatements (line 4) | function splitStatements(sql: string): string[] {
FILE: src/services/db/migrations.ts
constant MIGRATIONS (line 3) | const MIGRATIONS = [
function splitStatements (line 784) | function splitStatements(sql: string): string[] {
function runMigrations (line 825) | async function runMigrations(): Promise<void> {
FILE: src/services/db/notificationVips.ts
type NotificationVip (line 4) | interface NotificationVip {
function getVipSenders (line 12) | async function getVipSenders(accountId: string): Promise<Set<string>> {
function getAllVipSenders (line 21) | async function getAllVipSenders(accountId: string): Promise<Notification...
function addVipSender (line 29) | async function addVipSender(
function removeVipSender (line 42) | async function removeVipSender(
function isVipSender (line 53) | async function isVipSender(
FILE: src/services/db/pendingOperations.ts
type PendingOperation (line 3) | interface PendingOperation {
function enqueuePendingOperation (line 17) | async function enqueuePendingOperation(
function getPendingOperations (line 33) | async function getPendingOperations(
function updateOperationStatus (line 57) | async function updateOperationStatus(
function deleteOperation (line 69) | async function deleteOperation(id: string): Promise<void> {
constant BACKOFF_SCHEDULE (line 74) | const BACKOFF_SCHEDULE = [60, 300, 900, 3600];
function incrementRetry (line 76) | async function incrementRetry(id: string): Promise<void> {
function getPendingOpsCount (line 104) | async function getPendingOpsCount(accountId?: string): Promise<number> {
function getFailedOpsCount (line 119) | async function getFailedOpsCount(accountId?: string): Promise<number> {
function getPendingOpsForResource (line 134) | async function getPendingOpsForResource(
function compactQueue (line 147) | async function compactQueue(accountId?: string): Promise<number> {
function clearFailedOperations (line 231) | async function clearFailedOperations(accountId?: string): Promise<void> {
function retryFailedOperations (line 243) | async function retryFailedOperations(accountId?: string): Promise<void> {
FILE: src/services/db/phishingAllowlist.ts
function isPhishingAllowlisted (line 4) | async function isPhishingAllowlisted(
function addToPhishingAllowlist (line 16) | async function addToPhishingAllowlist(
function removeFromPhishingAllowlist (line 28) | async function removeFromPhishingAllowlist(
function getPhishingAllowlist (line 39) | async function getPhishingAllowlist(
FILE: src/services/db/quickSteps.ts
type DbQuickStep (line 4) | interface DbQuickStep {
function getQuickStepsForAccount (line 18) | async function getQuickStepsForAccount(
function getEnabledQuickStepsForAccount (line 28) | async function getEnabledQuickStepsForAccount(
function insertQuickStep (line 38) | async function insertQuickStep(step: {
function updateQuickStep (line 67) | async function updateQuickStep(
function deleteQuickStep (line 95) | async function deleteQuickStep(id: string): Promise<void> {
function reorderQuickSteps (line 100) | async function reorderQuickSteps(
FILE: src/services/db/scheduledEmails.ts
type DbScheduledEmail (line 4) | interface DbScheduledEmail {
function getPendingScheduledEmails (line 21) | async function getPendingScheduledEmails(): Promise<DbScheduledEmail[]> {
function getScheduledEmailsForAccount (line 30) | async function getScheduledEmailsForAccount(
function insertScheduledEmail (line 40) | async function insertScheduledEmail(email: {
function updateScheduledEmailStatus (line 74) | async function updateScheduledEmailStatus(
function deleteScheduledEmail (line 85) | async function deleteScheduledEmail(id: string): Promise<void> {
FILE: src/services/db/search.ts
type SearchResult (line 5) | interface SearchResult {
function searchMessages (line 21) | async function searchMessages(
FILE: src/services/db/sendAsAliases.ts
type DbSendAsAlias (line 3) | interface DbSendAsAlias {
type SendAsAlias (line 17) | interface SendAsAlias {
function mapDbAlias (line 30) | function mapDbAlias(db: DbSendAsAlias): SendAsAlias {
function getAliasesForAccount (line 45) | async function getAliasesForAccount(
function upsertAlias (line 55) | async function upsertAlias(alias: {
function getDefaultAlias (line 96) | async function getDefaultAlias(
function setDefaultAlias (line 113) | async function setDefaultAlias(
function deleteAlias (line 130) | async function deleteAlias(id: string): Promise<void> {
FILE: src/services/db/settings.ts
function getSetting (line 4) | async function getSetting(key: string): Promise<string | null> {
function setSetting (line 13) | async function setSetting(key: string, value: string): Promise<void> {
function getAllSettings (line 21) | async function getAllSettings(): Promise<Record<string, string>> {
function getSecureSetting (line 33) | async function getSecureSetting(key: string): Promise<string | null> {
function setSecureSetting (line 51) | async function setSecureSetting(key: string, value: string): Promise<voi...
FILE: src/services/db/signatures.ts
type DbSignature (line 3) | interface DbSignature {
function getSignaturesForAccount (line 12) | async function getSignaturesForAccount(
function getDefaultSignature (line 22) | async function getDefaultSignature(
function insertSignature (line 31) | async function insertSignature(sig: {
function updateSignature (line 55) | async function updateSignature(
function deleteSignature (line 86) | async function deleteSignature(id: string): Promise<void> {
FILE: src/services/db/smartFolders.ts
type DbSmartFolder (line 3) | interface DbSmartFolder {
function getSmartFolders (line 18) | async function getSmartFolders(
function getSmartFolderById (line 33) | async function getSmartFolderById(
function insertSmartFolder (line 42) | async function insertSmartFolder(folder: {
function updateSmartFolder (line 65) | async function updateSmartFolder(
function deleteSmartFolder (line 82) | async function deleteSmartFolder(id: string): Promise<void> {
function updateSmartFolderSortOrder (line 87) | async function updateSmartFolderSortOrder(
FILE: src/services/db/smartLabelRules.ts
type DbSmartLabelRule (line 4) | interface DbSmartLabelRule {
function getSmartLabelRulesForAccount (line 15) | async function getSmartLabelRulesForAccount(
function getEnabledSmartLabelRules (line 25) | async function getEnabledSmartLabelRules(
function insertSmartLabelRule (line 35) | async function insertSmartLabelRule(rule: {
function updateSmartLabelRule (line 58) | async function updateSmartLabelRule(
function deleteSmartLabelRule (line 81) | async function deleteSmartLabelRule(id: string): Promise<void> {
FILE: src/services/db/tasks.ts
type TaskPriority (line 3) | type TaskPriority = "none" | "low" | "medium" | "high" | "urgent";
type DbTask (line 5) | interface DbTask {
type DbTaskTag (line 25) | interface DbTaskTag {
function getTasksForAccount (line 33) | async function getTasksForAccount(
function getTaskById (line 52) | async function getTaskById(id: string): Promise<DbTask | null> {
function getTasksForThread (line 61) | async function getTasksForThread(
function getSubtasks (line 73) | async function getSubtasks(parentId: string): Promise<DbTask[]> {
function insertTask (line 81) | async function insertTask(task: {
function updateTask (line 118) | async function updateTask(
function deleteTask (line 176) | async function deleteTask(id: string): Promise<void> {
function completeTask (line 181) | async function completeTask(id: string): Promise<void> {
function uncompleteTask (line 189) | async function uncompleteTask(id: string): Promise<void> {
function reorderTasks (line 197) | async function reorderTasks(
function getIncompleteTaskCount (line 209) | async function getIncompleteTaskCount(
function getTaskTags (line 220) | async function getTaskTags(
function upsertTaskTag (line 230) | async function upsertTaskTag(
function deleteTaskTag (line 244) | async function deleteTaskTag(
FILE: src/services/db/templates.ts
type DbTemplate (line 3) | interface DbTemplate {
function getTemplatesForAccount (line 17) | async function getTemplatesForAccount(
function insertTemplate (line 27) | async function insertTemplate(tmpl: {
function updateTemplate (line 43) | async function updateTemplate(
function deleteTemplate (line 60) | async function deleteTemplate(id: string): Promise<void> {
FILE: src/services/db/threadCategories.ts
type ThreadCategory (line 3) | type ThreadCategory = "Primary" | "Updates" | "Promotions" | "Social" | ...
constant ALL_CATEGORIES (line 5) | const ALL_CATEGORIES: ThreadCategory[] = [
type DbThreadCategory (line 13) | interface DbThreadCategory {
function getThreadCategory (line 20) | async function getThreadCategory(
function getThreadCategoryWithManual (line 32) | async function getThreadCategoryWithManual(
function getRecentRuleCategorizedThreadIds (line 45) | async function getRecentRuleCategorizedThreadIds(
function getCategoriesForThreads (line 64) | async function getCategoriesForThreads(
function setThreadCategory (line 87) | async function setThreadCategory(
function setThreadCategoriesBatch (line 103) | async function setThreadCategoriesBatch(
function getCategoryUnreadCounts (line 121) | async function getCategoryUnreadCounts(
function getUncategorizedInboxThreadIds (line 142) | async function getUncategorizedInboxThreadIds(
FILE: src/services/db/threads.ts
type DbThread (line 3) | interface DbThread {
function getThreadsForAccount (line 22) | async function getThreadsForAccount(
function getThreadsForCategory (line 52) | async function getThreadsForCategory(
function upsertThread (line 88) | async function upsertThread(thread: {
function setThreadLabels (line 122) | async function setThreadLabels(
function getThreadLabelIds (line 142) | async function getThreadLabelIds(
function getThreadById (line 154) | async function getThreadById(
function getThreadCountForAccount (line 170) | async function getThreadCountForAccount(accountId: string): Promise<numb...
function getUnreadInboxCount (line 179) | async function getUnreadInboxCount(): Promise<number> {
function deleteThread (line 189) | async function deleteThread(
function deleteAllThreadsForAccount (line 200) | async function deleteAllThreadsForAccount(
function pinThread (line 210) | async function pinThread(
function unpinThread (line 221) | async function unpinThread(
function muteThread (line 232) | async function muteThread(
function unmuteThread (line 243) | async function unmuteThread(
function getMutedThreadIds (line 254) | async function getMutedThreadIds(
FILE: src/services/db/writingStyleProfiles.ts
type DbWritingStyleProfile (line 3) | interface DbWritingStyleProfile {
function getWritingStyleProfile (line 12) | async function getWritingStyleProfile(
function upsertWritingStyleProfile (line 23) | async function upsertWritingStyleProfile(
function deleteWritingStyleProfile (line 39) | async function deleteWritingStyleProfile(
FILE: src/services/deepLinkHandler.ts
function handleUrl (line 8) | async function handleUrl(url: string): Promise<void> {
function initDeepLinkHandler (line 31) | async function initDeepLinkHandler(): Promise<() => void> {
FILE: src/services/email/gmailProvider.ts
constant GMAIL_SPECIAL_USE (line 6) | const GMAIL_SPECIAL_USE: Record<string, string | null> = {
class GmailApiProvider (line 27) | class GmailApiProvider implements EmailProvider {
method constructor (line 32) | constructor(accountId: string, client: GmailClient) {
method listFolders (line 37) | async listFolders(): Promise<EmailFolder[]> {
method createFolder (line 54) | async createFolder(name: string, _parentPath?: string): Promise<EmailF...
method deleteFolder (line 69) | async deleteFolder(path: string): Promise<void> {
method renameFolder (line 74) | async renameFolder(path: string, newName: string): Promise<void> {
method initialSync (line 78) | async initialSync(
method deltaSync (line 92) | async deltaSync(syncToken: string): Promise<SyncResult> {
method fetchMessage (line 127) | async fetchMessage(messageId: string): Promise<ParsedMessage> {
method fetchAttachment (line 132) | async fetchAttachment(
method fetchRawMessage (line 140) | async fetchRawMessage(messageId: string): Promise<string> {
method archive (line 147) | async archive(threadId: string, _messageIds: string[]): Promise<void> {
method trash (line 151) | async trash(threadId: string, _messageIds: string[]): Promise<void> {
method permanentDelete (line 155) | async permanentDelete(
method markRead (line 162) | async markRead(
method star (line 174) | async star(
method spam (line 186) | async spam(
method moveToFolder (line 198) | async moveToFolder(
method addLabel (line 206) | async addLabel(threadId: string, labelId: string): Promise<void> {
method removeLabel (line 210) | async removeLabel(threadId: string, labelId: string): Promise<void> {
method sendMessage (line 214) | async sendMessage(
method createDraft (line 222) | async createDraft(
method updateDraft (line 230) | async updateDraft(
method deleteDraft (line 243) | async deleteDraft(draftId: string): Promise<void> {
method testConnection (line 247) | async testConnection(): Promise<{ success: boolean; message: string }> {
method getProfile (line 263) | async getProfile(): Promise<{ email: string; name?: string }> {
FILE: src/services/email/imapSmtpProvider.ts
function base64UrlDecode (line 30) | function base64UrlDecode(input: string): string {
function parseBasicHeaders (line 46) | function parseBasicHeaders(raw: string): Map<string, string> {
function extractSnippet (line 69) | function extractSnippet(raw: string, maxLen = 200): string {
class ImapSmtpProvider (line 105) | class ImapSmtpProvider implements EmailProvider {
method constructor (line 112) | constructor(accountId: string) {
method getAccount (line 116) | private async getAccount(): Promise<DbAccount> {
method getImapConfig (line 124) | private async getImapConfig(): Promise<ImapConfig> {
method getSmtpConfig (line 137) | private async getSmtpConfig(): Promise<SmtpConfig> {
method clearConfigCache (line 152) | clearConfigCache(): void {
method listFolders (line 159) | async listFolders(): Promise<EmailFolder[]> {
method createFolder (line 179) | async createFolder(
method deleteFolder (line 189) | async deleteFolder(_path: string): Promise<void> {
method renameFolder (line 196) | async renameFolder(_path: string, _newName: string): Promise<void> {
method initialSync (line 205) | async initialSync(
method deltaSync (line 214) | async deltaSync(_syncToken: string): Promise<SyncResult> {
method fetchMessage (line 220) | async fetchMessage(messageId: string): Promise<ParsedMessage> {
method fetchAttachment (line 240) | async fetchAttachment(
method fetchRawMessage (line 255) | async fetchRawMessage(messageId: string): Promise<string> {
method archive (line 268) | async archive(
method trash (line 283) | async trash(
method permanentDelete (line 298) | async permanentDelete(
method markRead (line 310) | async markRead(
method star (line 323) | async star(
method spam (line 336) | async spam(
method moveToFolder (line 353) | async moveToFolder(
method addLabel (line 367) | async addLabel(
method removeLabel (line 380) | async removeLabel(
method sendMessage (line 393) | async sendMessage(
method saveSentMessageLocally (line 434) | private async saveSentMessageLocally(
method createDraft (line 513) | async createDraft(
method updateDraft (line 528) | async updateDraft(
method deleteDraft (line 543) | async deleteDraft(draftId: string): Promise<void> {
method testConnection (line 562) | async testConnection(): Promise<{ success: boolean; message: string }> {
method getProfile (line 593) | async getProfile(): Promise<{ email: string; name?: string }> {
method groupByFolder (line 609) | private groupByFolder(messageIds: string[]): Map<string, number[]> {
method parseImapMessageId (line 636) | private parseImapMessageId(
FILE: src/services/email/providerFactory.ts
function getEmailProvider (line 13) | async function getEmailProvider(
function removeProvider (line 39) | function removeProvider(accountId: string): void {
function invalidateProviderConfig (line 48) | function invalidateProviderConfig(accountId: string): void {
function clearAllProviders (line 58) | function clearAllProviders(): void {
FILE: src/services/email/types.ts
type AccountProvider (line 3) | type AccountProvider = "gmail_api" | "imap" | "caldav";
type EmailFolder (line 5) | interface EmailFolder {
type SyncResult (line 16) | interface SyncResult {
type EmailProvider (line 26) | interface EmailProvider {
FILE: src/services/emailActions.ts
type EmailAction (line 13) | type EmailAction =
type ActionResult (line 65) | interface ActionResult {
function getNextThreadId (line 76) | function getNextThreadId(currentId: string): string | null {
function applyOptimisticUpdate (line 91) | function applyOptimisticUpdate(action: EmailAction): void {
function revertOptimisticUpdate (line 123) | function revertOptimisticUpdate(action: EmailAction): void {
function applyLocalDbUpdate (line 143) | async function applyLocalDbUpdate(
function getResourceId (line 236) | function getResourceId(action: EmailAction): string {
function actionToParams (line 242) | function actionToParams(action: EmailAction): Record<string, unknown> {
function executeViaProvider (line 248) | async function executeViaProvider(
function executeEmailAction (line 303) | async function executeEmailAction(
function executeQueuedAction (line 357) | async function executeQueuedAction(
function archiveThread (line 370) | function archiveThread(
function trashThread (line 382) | function trashThread(
function permanentDeleteThread (line 394) | function permanentDeleteThread(
function markThreadRead (line 406) | function markThreadRead(
function starThread (line 420) | function starThread(
function spamThread (line 434) | function spamThread(
function moveThread (line 448) | function moveThread(
function addThreadLabel (line 462) | function addThreadLabel(
function removeThreadLabel (line 474) | function removeThreadLabel(
function sendEmail (line 486) | async function sendEmail(
function createDraft (line 505) | function createDraft(
function updateDraft (line 517) | function updateDraft(
function deleteDraft (line 531) | function deleteDraft(
FILE: src/services/filters/filterEngine.ts
function messageMatchesFilter (line 10) | function messageMatchesFilter(
type FilterResult (line 41) | interface FilterResult {
function computeFilterActions (line 51) | function computeFilterActions(actions: FilterActions): FilterResult {
function applyFiltersToMessages (line 84) | async function applyFiltersToMessages(
FILE: src/services/followup/followupManager.ts
function checkFollowUpReminders (line 14) | async function checkFollowUpReminders(): Promise<void> {
FILE: src/services/globalShortcut.ts
constant DEFAULT_SHORTCUT (line 6) | const DEFAULT_SHORTCUT = "CmdOrCtrl+Shift+M";
function handleComposeShortcut (line 9) | async function handleComposeShortcut(): Promise<void> {
function initGlobalShortcut (line 18) | async function initGlobalShortcut(): Promise<void> {
function registerComposeShortcut (line 33) | async function registerComposeShortcut(shortcut: string): Promise<void> {
function unregisterComposeShortcut (line 47) | async function unregisterComposeShortcut(): Promise<void> {
function getCurrentShortcut (line 58) | function getCurrentShortcut(): string | null {
FILE: src/services/gmail/auth.ts
constant GOOGLE_AUTH_URL (line 4) | const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
constant GOOGLE_TOKEN_URL (line 5) | const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
constant OAUTH_CALLBACK_PORT (line 6) | const OAUTH_CALLBACK_PORT = 17248;
constant SCOPES (line 8) | const SCOPES = [
type OAuthServerResult (line 19) | interface OAuthServerResult {
type TokenResponse (line 24) | interface TokenResponse {
type UserInfo (line 32) | interface UserInfo {
function generateCodeVerifier (line 38) | function generateCodeVerifier(): string {
function generateCodeChallenge (line 44) | async function generateCodeChallenge(verifier: string): Promise<string> {
function base64UrlEncode (line 51) | function base64UrlEncode(bytes: Uint8Array): string {
function startOAuthFlow (line 70) | async function startOAuthFlow(
function exchangeCodeForTokens (line 137) | async function exchangeCodeForTokens(
function refreshAccessToken (line 170) | async function refreshAccessToken(
function fetchUserInfo (line 196) | async function fetchUserInfo(accessToken: string): Promise<UserInfo> {
FILE: src/services/gmail/authParser.test.ts
function makeHeaders (line 4) | function makeHeaders(
FILE: src/services/gmail/authParser.ts
type AuthVerdict (line 1) | interface AuthVerdict {
type AuthResult (line 6) | interface AuthResult {
function parseVerdict (line 17) | function parseVerdict(headerValue: string, mechanism: string): AuthVerdi...
function parseReceivedSpf (line 37) | function parseReceivedSpf(headerValue: string): AuthVerdict | null {
function unknownVerdict (line 47) | function unknownVerdict(): AuthVerdict {
function computeAggregate (line 59) | function computeAggregate(
function parseAuthenticationResults (line 109) | function parseAuthenticationResults(
FILE: src/services/gmail/client.ts
constant GMAIL_API_BASE (line 6) | const GMAIL_API_BASE = "https://www.googleapis.com/gmail/v1";
constant MAX_RETRY_ATTEMPTS (line 7) | const MAX_RETRY_ATTEMPTS = 3;
constant INITIAL_BACKOFF_MS (line 8) | const INITIAL_BACKOFF_MS = 1000;
type TokenInfo (line 10) | interface TokenInfo {
class GmailClient (line 19) | class GmailClient {
method constructor (line 26) | constructor(accountId: string, clientId: string, tokenInfo: TokenInfo,...
method getValidToken (line 33) | private async getValidToken(): Promise<string> {
method refreshToken (line 48) | private async refreshToken(): Promise<void> {
method fetchWithRetry (line 76) | private async fetchWithRetry(
method request (line 97) | async request<T>(
method getProfile (line 149) | async getProfile(): Promise<{ emailAddress: string; messagesTotal: num...
method listLabels (line 153) | async listLabels(): Promise<{ labels: GmailLabel[] }> {
method listThreads (line 157) | async listThreads(params: {
method getThread (line 172) | async getThread(threadId: string, format: "full" | "metadata" | "minim...
method getMessage (line 176) | async getMessage(messageId: string, format: "full" | "metadata" | "min...
method modifyThread (line 180) | async modifyThread(threadId: string, addLabelIds?: string[], removeLab...
method getHistory (line 187) | async getHistory(
method createLabel (line 209) | async createLabel(name: string, color?: { textColor: string; backgroun...
method updateLabel (line 225) | async updateLabel(labelId: string, updates: { name?: string; color?: {...
method deleteLabel (line 238) | async deleteLabel(labelId: string): Promise<void> {
method deleteThread (line 254) | async deleteThread(threadId: string): Promise<void> {
method sendMessage (line 270) | async sendMessage(raw: string, threadId?: string): Promise<GmailMessag...
method getAttachment (line 283) | async getAttachment(messageId: string, attachmentId: string): Promise<...
method createDraft (line 290) | async createDraft(raw: string, threadId?: string): Promise<{ id: strin...
method updateDraft (line 302) | async updateDraft(draftId: string, raw: string, threadId?: string): Pr...
method deleteDraft (line 314) | async deleteDraft(draftId: string): Promise<void> {
method listDrafts (line 321) | async listDrafts(): Promise<{ id: string; message: { id: string; threa...
type GmailLabel (line 328) | interface GmailLabel {
type GmailThreadStub (line 341) | interface GmailThreadStub {
type GmailThread (line 347) | interface GmailThread {
type GmailMessage (line 353) | interface GmailMessage {
type GmailMessagePart (line 364) | interface GmailMessagePart {
type GmailHeader (line 373) | interface GmailHeader {
type GmailHistoryItem (line 378) | interface GmailHistoryItem {
FILE: src/services/gmail/draftDeletion.test.ts
function createMockClient (line 10) | function createMockClient(drafts: { id: string; message: { id: string; t...
FILE: src/services/gmail/draftDeletion.ts
function deleteDraftsForThread (line 9) | async function deleteDraftsForThread(
FILE: src/services/gmail/messageParser.ts
type ParsedAttachment (line 4) | interface ParsedAttachment {
type ParsedMessage (line 13) | interface ParsedMessage {
function parseGmailMessage (line 39) | function parseGmailMessage(msg: GmailMessage): ParsedMessage {
function getHeader (line 76) | function getHeader(headers: GmailHeader[], name: string): string | null {
function parseEmailAddress (line 83) | function parseEmailAddress(raw: string | null): {
function extractBody (line 101) | function extractBody(
function extractAttachments (line 119) | function extractAttachments(part: GmailMessagePart): ParsedAttachment[] {
function collectAttachments (line 125) | function collectAttachments(part: GmailMessagePart, results: ParsedAttac...
function decodeBase64Url (line 157) | function decodeBase64Url(data: string): string {
FILE: src/services/gmail/sendAs.ts
type GmailSendAsEntry (line 4) | interface GmailSendAsEntry {
type GmailSendAsResponse (line 14) | interface GmailSendAsResponse {
function fetchSendAsAliases (line 21) | async function fetchSendAsAliases(
FILE: src/services/gmail/sync.test.ts
function createMockClient (line 82) | function createMockClient(historyItems: unknown[]): GmailClient {
FILE: src/services/gmail/sync.ts
function loadAutoArchiveCategories (line 16) | async function loadAutoArchiveCategories(): Promise<Set<string>> {
type SyncProgress (line 22) | interface SyncProgress {
type SyncProgressCallback (line 28) | type SyncProgressCallback = (progress: SyncProgress) => void;
function processAndStoreThread (line 34) | async function processAndStoreThread(
function syncLabels (line 157) | async function syncLabels(
function initialSync (line 177) | async function initialSync(
function parallelLimit (line 266) | async function parallelLimit<T>(
function deltaSync (line 288) | async function deltaSync(
FILE: src/services/gmail/syncManager.test.ts
function makeGmailAccount (line 68) | function makeGmailAccount(id: string, historyId: string | null = null) {
FILE: src/services/gmail/syncManager.ts
constant SYNC_INTERVAL_MS (line 14) | const SYNC_INTERVAL_MS = 60_000;
function mapImapPhase (line 17) | function mapImapPhase(phase: string): "labels" | "threads" | "messages" ...
type SyncStatusCallback (line 29) | type SyncStatusCallback = (
function onSyncStatus (line 38) | function onSyncStatus(cb: SyncStatusCallback): () => void {
function syncGmailAccount (line 48) | async function syncGmailAccount(accountId: string): Promise<void> {
function syncImapAccount (line 85) | async function syncImapAccount(accountId: string): Promise<void> {
function syncCalendarForAccount (line 138) | async function syncCalendarForAccount(accountId: string): Promise<void> {
function syncAccountInternal (line 212) | async function syncAccountInternal(accountId: string): Promise<void> {
function runSync (line 253) | async function runSync(accountIds: string[]): Promise<void> {
function syncAccount (line 285) | async function syncAccount(accountId: string): Promise<void> {
function startBackgroundSync (line 295) | function startBackgroundSync(accountIds: string[], skipImmediateSync = f...
function stopBackgroundSync (line 312) | function stopBackgroundSync(): void {
function triggerSync (line 323) | async function triggerSync(accountIds: string[]): Promise<void> {
function forceFullSync (line 331) | async function forceFullSync(accountIds: string[]): Promise<void> {
function resyncAccount (line 343) | async function resyncAccount(accountId: string): Promise<void> {
FILE: src/services/gmail/tokenManager.ts
function getGmailClient (line 14) | async function getGmailClient(
function removeClient (line 43) | function removeClient(accountId: string): void {
function getClientId (line 50) | async function getClientId(): Promise<string> {
function getClientSecret (line 61) | async function getClientSecret(): Promise<string | undefined> {
function initializeClients (line 69) | async function initializeClients(): Promise<void> {
function reauthorizeAccount (line 91) | async function reauthorizeAccount(
FILE: src/services/google/calendar.ts
constant CALENDAR_API_BASE (line 3) | const CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
type CalendarEvent (line 5) | interface CalendarEvent {
type EventListResponse (line 19) | interface EventListResponse {
function listCalendarEvents (line 24) | async function listCalendarEvents(
function createCalendarEvent (line 42) | async function createCalendarEvent(
function deleteCalendarEvent (line 60) | async function deleteCalendarEvent(
FILE: src/services/imap/autoDiscovery.ts
type SecurityType (line 1) | type SecurityType = "ssl" | "starttls" | "none";
type AuthMethod (line 2) | type AuthMethod = "password" | "oauth2";
type ServerSettings (line 4) | interface ServerSettings {
type WellKnownProvider (line 13) | interface WellKnownProvider {
function extractDomain (line 161) | function extractDomain(email: string): string | null {
type WellKnownProviderResult (line 168) | interface WellKnownProviderResult {
function findWellKnownProvider (line 179) | function findWellKnownProvider(
function guessServerSettings (line 199) | function guessServerSettings(domain: string): ServerSettings {
function discoverSettings (line 215) | function discoverSettings(email: string): WellKnownProviderResult | null {
function getDefaultSmtpPort (line 231) | function getDefaultSmtpPort(security: SecurityType): number {
function getDefaultImapPort (line 245) | function getDefaultImapPort(security: SecurityType): number {
FILE: src/services/imap/folderMapper.ts
constant SPECIAL_USE_MAP (line 7) | const SPECIAL_USE_MAP: Record<string, { labelId: string; labelName: stri...
constant FOLDER_NAME_MAP (line 23) | const FOLDER_NAME_MAP: Record<string, string> = {
type FolderLabelMapping (line 55) | interface FolderLabelMapping {
function mapFolderToLabel (line 66) | function mapFolderToLabel(folder: ImapFolder): FolderLabelMapping {
function getLabelsForMessage (line 100) | function getLabelsForMessage(
function syncFoldersToLabels (line 127) | async function syncFoldersToLabels(
function getSyncableFolders (line 156) | function getSyncableFolders(folders: ImapFolder[]): ImapFolder[] {
FILE: src/services/imap/imapConfigBuilder.ts
function mapSecurity (line 8) | function mapSecurity(security: string | null): "tls" | "starttls" | "non...
function mapAuthMethod (line 20) | function mapAuthMethod(method: string | null): "password" | "oauth2" {
function buildImapConfig (line 32) | function buildImapConfig(
function buildSmtpConfig (line 64) | function buildSmtpConfig(
FILE: src/services/imap/imapSync.test.ts
function setupFolderWithMessages (line 274) | function setupFolderWithMessages(folder: string, messages: ReturnType<ty...
FILE: src/services/imap/imapSync.ts
constant BATCH_SIZE (line 39) | const BATCH_SIZE = 50;
constant CHUNK_SIZE (line 41) | const CHUNK_SIZE = 200;
constant THREAD_BATCH_SIZE (line 43) | const THREAD_BATCH_SIZE = 100;
constant CIRCUIT_BREAKER_THRESHOLD (line 50) | const CIRCUIT_BREAKER_THRESHOLD = 3;
constant CIRCUIT_BREAKER_DELAY_MS (line 52) | const CIRCUIT_BREAKER_DELAY_MS = 15_000;
constant CIRCUIT_BREAKER_MAX_FAILURES (line 54) | const CIRCUIT_BREAKER_MAX_FAILURES = 5;
constant INTER_FOLDER_DELAY_MS (line 56) | const INTER_FOLDER_DELAY_MS = 1_000;
function isConnectionError (line 58) | function isConnectionError(err: unknown): boolean {
function delay (line 72) | function delay(ms: number): Promise<void> {
constant IMAP_MONTH_NAMES (line 80) | const IMAP_MONTH_NAMES = [
function formatImapDate (line 88) | function formatImapDate(date: Date): string {
function computeSinceDate (line 100) | function computeSinceDate(daysBack: number): string {
type ImapSyncProgress (line 110) | interface ImapSyncProgress {
type ImapSyncProgressCallback (line 117) | type ImapSyncProgressCallback = (progress: ImapSyncProgress) => void;
function syntheticMessageId (line 126) | function syntheticMessageId(accountId: string, folder: string, uid: numb...
function imapMessageToParsedMessage (line 134) | function imapMessageToParsedMessage(
function storeThreadsAndMessages (line 207) | async function storeThreadsAndMessages(
function fetchMessagesInBatches (line 355) | async function fetchMessagesInBatches(
function imapInitialSync (line 390) | async function imapInitialSync(
function imapDeltaSync (line 827) | async function imapDeltaSync(accountId: string, daysBack = 365): Promise...
FILE: src/services/imap/messageHelper.ts
type ImapMessageInfo (line 4) | interface ImapMessageInfo {
function getImapUidsForMessages (line 13) | async function getImapUidsForMessages(
function groupMessagesByFolder (line 42) | function groupMessagesByFolder(
constant SPECIAL_USE_TO_LABEL_ID (line 60) | const SPECIAL_USE_TO_LABEL_ID: Record<string, string> = {
function findSpecialFolder (line 72) | async function findSpecialFolder(
function securityToConfigType (line 108) | function securityToConfigType(
function updateMessageImapFolder (line 126) | async function updateMessageImapFolder(
FILE: src/services/imap/tauriCommands.ts
type ImapConfig (line 5) | interface ImapConfig {
type ImapFolder (line 15) | interface ImapFolder {
type ImapMessage (line 25) | interface ImapMessage {
type ImapAttachment (line 52) | interface ImapAttachment {
type ImapFolderStatus (line 61) | interface ImapFolderStatus {
type ImapFetchResult (line 69) | interface ImapFetchResult {
type ImapFolderSearchResult (line 76) | interface ImapFolderSearchResult {
type ImapFolderSyncResult (line 83) | interface ImapFolderSyncResult {
type DeltaCheckRequest (line 91) | interface DeltaCheckRequest {
type DeltaCheckResult (line 97) | interface DeltaCheckResult {
type SmtpConfig (line 106) | interface SmtpConfig {
type SmtpSendResult (line 116) | interface SmtpSendResult {
function imapTestConnection (line 127) | async function imapTestConnection(config: ImapConfig): Promise<string> {
function imapListFolders (line 134) | async function imapListFolders(config: ImapConfig): Promise<ImapFolder[]> {
function imapFetchMessages (line 142) | async function imapFetchMessages(
function imapFetchNewUids (line 153) | async function imapFetchNewUids(
function imapSearchAllUids (line 165) | async function imapSearchAllUids(
function imapFetchMessageBody (line 175) | async function imapFetchMessageBody(
function imapSetFlags (line 188) | async function imapSetFlags(
function imapMoveMessages (line 202) | async function imapMoveMessages(
function imapDeleteMessages (line 214) | async function imapDeleteMessages(
function imapAppendMessage (line 227) | async function imapAppendMessage(
function imapGetFolderStatus (line 239) | async function imapGetFolderStatus(
function imapFetchAttachment (line 250) | async function imapFetchAttachment(
function imapFetchRawMessage (line 263) | async function imapFetchRawMessage(
function imapDeltaCheck (line 275) | async function imapDeltaCheck(
function imapSyncFolder (line 287) | async function imapSyncFolder(
function imapSearchFolder (line 301) | async function imapSearchFolder(
function imapRawFetchDiagnostic (line 312) | async function imapRawFetchDiagnostic(
function smtpSendEmail (line 326) | async function smtpSendEmail(
function smtpTestConnection (line 336) | async function smtpTestConnection(config: SmtpConfig): Promise<SmtpSendR...
FILE: src/services/notifications/notificationManager.ts
type NotificationContext (line 17) | interface NotificationContext {
function showAndFocusMainWindow (line 27) | async function showAndFocusMainWindow(): Promise<void> {
function initNotifications (line 38) | async function initNotifications(): Promise<void> {
function queueNewEmailNotification (line 112) | function queueNewEmailNotification(
function shouldNotifyForMessage (line 153) | function shouldNotifyForMessage(
function notifyFollowUpDue (line 169) | function notifyFollowUpDue(
function notifySnoozeReturn (line 188) | function notifySnoozeReturn(subject: string): void {
FILE: src/services/oauth/oauthFlow.test.ts
function makeIdToken (line 128) | function makeIdToken(payload: Record<string, unknown>): string {
FILE: src/services/oauth/oauthFlow.ts
constant OAUTH_CALLBACK_PORT (line 5) | const OAUTH_CALLBACK_PORT = 17248;
type OAuthServerResult (line 7) | interface OAuthServerResult {
type TokenResponse (line 12) | interface TokenResponse {
type ProviderUserInfo (line 21) | interface ProviderUserInfo {
function generateCodeVerifier (line 27) | function generateCodeVerifier(): string {
function generateCodeChallenge (line 33) | async function generateCodeChallenge(verifier: string): Promise<string> {
function base64UrlEncode (line 40) | function base64UrlEncode(bytes: Uint8Array): string {
function startProviderOAuthFlow (line 59) | async function startProviderOAuthFlow(
function exchangeCode (line 122) | async function exchangeCode(
function refreshProviderToken (line 145) | async function refreshProviderToken(
function parseIdToken (line 161) | function parseIdToken(idToken: string): Record<string, unknown> {
function fetchUserInfo (line 168) | async function fetchUserInfo(
FILE: src/services/oauth/oauthTokenManager.ts
constant REFRESH_BUFFER_MS (line 7) | const REFRESH_BUFFER_MS = 5 * 60 * 1000;
function ensureFreshToken (line 17) | async function ensureFreshToken(account: DbAccount): Promise<string> {
FILE: src/services/oauth/providers.ts
type OAuthProviderConfig (line 1) | interface OAuthProviderConfig {
function getOAuthProvider (line 42) | function getOAuthProvider(id: string): OAuthProviderConfig | null {
function getAllOAuthProviders (line 46) | function getAllOAuthProviders(): OAuthProviderConfig[] {
FILE: src/services/phishing/phishingScanner.ts
function scanMessageLinks (line 17) | async function scanMessageLinks(
FILE: src/services/queue/queueProcessor.ts
constant BATCH_SIZE (line 14) | const BATCH_SIZE = 50;
function processQueue (line 18) | async function processQueue(): Promise<void> {
function updatePendingCount (line 60) | async function updatePendingCount(): Promise<void> {
function startQueueProcessor (line 65) | function startQueueProcessor(): void {
function stopQueueProcessor (line 71) | function stopQueueProcessor(): void {
function triggerQueueFlush (line 80) | async function triggerQueueFlush(): Promise<void> {
FILE: src/services/quickSteps/defaults.ts
constant DEFAULT_QUICK_STEPS (line 4) | const DEFAULT_QUICK_STEPS: {
function seedDefaultQuickSteps (line 29) | async function seedDefaultQuickSteps(
FILE: src/services/quickSteps/executor.ts
function executeSingleAction (line 16) | async function executeSingleAction(
function executeQuickStep (line 151) | async function executeQuickStep(
FILE: src/services/quickSteps/types.ts
type QuickStepActionType (line 1) | type QuickStepActionType =
type QuickStepAction (line 20) | interface QuickStepAction {
type QuickStep (line 30) | interface QuickStep {
type QuickStepExecutionResult (line 44) | interface QuickStepExecutionResult {
constant ACTION_TYPE_METADATA (line 52) | const ACTION_TYPE_METADATA: {
FILE: src/services/search/searchParser.ts
type ParsedSearchQuery (line 7) | interface ParsedSearchQuery {
constant OPERATOR_REGEX (line 21) | const OPERATOR_REGEX = /(?:^|\s)(from|to|subject|has|is|before|after|lab...
function parseDateToTimestamp (line 27) | function parseDateToTimestamp(dateStr: string): number | undefined {
function parseSearchQuery (line 40) | function parseSearchQuery(input: string): ParsedSearchQuery {
function hasSearchOperators (line 117) | function hasSearchOperators(query: string): boolean {
FILE: src/services/search/searchQueryBuilder.ts
type BuiltQuery (line 3) | interface BuiltQuery {
function buildSearchQuery (line 12) | function buildSearchQuery(
FILE: src/services/search/smartFolderQuery.ts
function resolveQueryTokens (line 12) | function resolveQueryTokens(query: string): string {
function getSmartFolderSearchQuery (line 47) | function getSmartFolderSearchQuery(
function getSmartFolderUnreadCount (line 61) | function getSmartFolderUnreadCount(
type SmartFolderRow (line 84) | interface SmartFolderRow {
function mapSmartFolderRows (line 99) | async function mapSmartFolderRows(rows: SmartFolderRow[]): Promise<Threa...
FILE: src/services/smartLabels/backfillService.ts
type BackfillRow (line 6) | interface BackfillRow {
function backfillSmartLabels (line 23) | async function backfillSmartLabels(
FILE: src/services/smartLabels/smartLabelManager.test.ts
function makeMessage (line 16) | function makeMessage(threadId = "t1"): ParsedMessage {
FILE: src/services/smartLabels/smartLabelManager.ts
function applySmartLabelsToMessages (line 9) | async function applySmartLabelsToMessages(
FILE: src/services/smartLabels/smartLabelService.test.ts
function makeMessage (line 21) | function makeMessage(overrides: Partial<ParsedMessage> = {}): ParsedMess...
FILE: src/services/smartLabels/smartLabelService.ts
type SmartLabelMatch (line 7) | interface SmartLabelMatch {
function matchSmartLabels (line 17) | async function matchSmartLabels(
FILE: src/services/snooze/scheduledSendManager.ts
function checkScheduledEmails (line 13) | async function checkScheduledEmails(): Promise<void> {
FILE: src/services/snooze/snoozeManager.ts
function checkSnoozedThreads (line 10) | async function checkSnoozedThreads(): Promise<void> {
function snoozeThread (line 47) | async function snoozeThread(
FILE: src/services/tasks/taskManager.ts
type RecurrenceRule (line 3) | interface RecurrenceRule {
function parseRecurrenceRule (line 12) | function parseRecurrenceRule(json: string | null): RecurrenceRule | null {
function calculateNextOccurrence (line 24) | function calculateNextOccurrence(
function handleRecurringTaskCompletion (line 53) | async function handleRecurringTaskCompletion(
FILE: src/services/threading/threadBuilder.ts
type ThreadableMessage (line 9) | interface ThreadableMessage {
type ThreadGroup (line 18) | interface ThreadGroup {
type Container (line 28) | interface Container {
function normalizeSubject (line 43) | function normalizeSubject(subject: string | null): string {
function parseReferences (line 74) | function parseReferences(references: string | null): string[] {
function djb2Hash (line 108) | function djb2Hash(str: string): string {
function generateThreadId (line 121) | function generateThreadId(rootMessageId: string): string {
function createContainer (line 129) | function createContainer(messageId: string): Container {
function isAncestor (line 142) | function isAncestor(container: Container, ancestor: Container): boolean {
function unlinkFromParent (line 154) | function unlinkFromParent(child: Container): void {
function linkParentChild (line 164) | function linkParentChild(parent: Container, child: Container): void {
function buildThreads (line 183) | function buildThreads(messages: ThreadableMessage[]): ThreadGroup[] {
function collectMessages (line 318) | function collectMessages(
function getSubjectForContainer (line 338) | function getSubjectForContainer(container: Container): string | null {
function updateThreads (line 352) | function updateThreads(
FILE: src/services/unsubscribe/unsubscribeManager.ts
type ParsedUnsubscribe (line 7) | interface ParsedUnsubscribe {
type SubscriptionEntry (line 13) | interface SubscriptionEntry {
function parseUnsubscribeHeaders (line 26) | function parseUnsubscribeHeaders(
function executeUnsubscribe (line 47) | async function executeUnsubscribe(
function recordUnsubscribeAction (line 129) | async function recordUnsubscribeAction(
function getSubscriptions (line 153) | async function getSubscriptions(accountId: string): Promise<Subscription...
function getUnsubscribeStatus (line 176) | async function getUnsubscribeStatus(
FILE: src/services/updateManager.ts
type UpdateInfo (line 4) | interface UpdateInfo {
type UpdateCallback (line 9) | type UpdateCallback = (update: UpdateInfo) => void;
function performCheck (line 15) | async function performCheck(): Promise<void> {
constant FOUR_HOURS (line 27) | const FOUR_HOURS = 4 * 60 * 60 * 1000;
function startUpdateChecker (line 29) | function startUpdateChecker(): void {
function stopUpdateChecker (line 35) | function stopUpdateChecker(): void {
function checkForUpdateNow (line 40) | async function checkForUpdateNow(): Promise<UpdateInfo | null> {
function installUpdate (line 45) | async function installUpdate(): Promise<void> {
function getAvailableUpdate (line 55) | function getAvailableUpdate(): UpdateInfo | null {
function setUpdateCallback (line 59) | function setUpdateCallback(cb: UpdateCallback | null): void {
function _resetForTesting (line 64) | function _resetForTesting(): void {
FILE: src/stores/accountStore.ts
type Account (line 4) | interface Account {
type AccountState (line 13) | interface AccountState {
FILE: src/stores/composerStore.ts
type ComposerMode (line 3) | type ComposerMode = "new" | "reply" | "replyAll" | "forward";
type ComposerViewMode (line 4) | type ComposerViewMode = "modal" | "fullpage";
type ComposerAttachment (line 6) | interface ComposerAttachment {
type ComposerState (line 15) | interface ComposerState {
FILE: src/stores/contextMenuStore.ts
type ContextMenuType (line 3) | type ContextMenuType = "sidebarLabel" | "sidebarNav" | "thread" | "messa...
type ContextMenuState (line 5) | interface ContextMenuState {
FILE: src/stores/labelStore.ts
type Label (line 6) | interface Label {
constant SYSTEM_LABEL_IDS (line 17) | const SYSTEM_LABEL_IDS = new Set([
constant CATEGORY_PREFIX (line 30) | const CATEGORY_PREFIX = "CATEGORY_";
function isSystemLabel (line 32) | function isSystemLabel(id: string): boolean {
type LabelState (line 36) | interface LabelState {
FILE: src/stores/shortcutStore.ts
type ShortcutState (line 5) | interface ShortcutState {
constant SETTINGS_KEY (line 18) | const SETTINGS_KEY = "custom_shortcuts";
function persistKeyMap (line 20) | function persistKeyMap(customKeys: Record<string, string>) {
FILE: src/stores/smartFolderStore.ts
type SmartFolder (line 12) | interface SmartFolder {
function mapDbFolder (line 23) | function mapDbFolder(db: DbSmartFolder): SmartFolder {
type SmartFolderState (line 36) | interface SmartFolderState {
FILE: src/stores/taskStore.test.ts
function makeTask (line 4) | function makeTask(overrides: Partial<DbTask> = {}): DbTask {
FILE: src/stores/taskStore.ts
type TaskGroupBy (line 4) | type TaskGroupBy = "none" | "priority" | "dueDate" | "tag";
type TaskFilterStatus (line 5) | type TaskFilterStatus = "all" | "incomplete" | "completed";
type TaskState (line 7) | interface TaskState {
FILE: src/stores/threadStore.ts
type Thread (line 3) | interface Thread {
type ThreadState (line 20) | interface ThreadState {
FILE: src/stores/uiStore.ts
type Theme (line 5) | type Theme = "light" | "dark" | "system";
type ReadingPanePosition (line 6) | type ReadingPanePosition = "right" | "bottom" | "hidden";
type ReadFilter (line 7) | type ReadFilter = "all" | "read" | "unread";
type EmailDensity (line 8) | type EmailDensity = "compact" | "default" | "spacious";
type DefaultReplyMode (line 9) | type DefaultReplyMode = "reply" | "replyAll";
type MarkAsReadBehavior (line 10) | type MarkAsReadBehavior = "instant" | "2s" | "manual";
type FontScale (line 11) | type FontScale = "small" | "default" | "large" | "xlarge";
type InboxViewMode (line 12) | type InboxViewMode = "unified" | "split";
type SidebarNavItem (line 14) | interface SidebarNavItem {
type UIState (line 19) | interface UIState {
FILE: src/test/mocks/db.mock.ts
function createMockDb (line 3) | function createMockDb() {
FILE: src/test/mocks/entities.mock.ts
function createMockParsedMessage (line 15) | function createMockParsedMessage(
function createMockGmailMessage (line 46) | function createMockGmailMessage(
function createMockGmailAccount (line 92) | function createMockGmailAccount(
function createMockImapAccount (line 132) | function createMockImapAccount(
function createMockDbAccount (line 172) | function createMockDbAccount(
function createMockImapMessage (line 212) | function createMockImapMessage(
function createMockImapFolder (line 244) | function createMockImapFolder(
function createMockImapConfig (line 260) | function createMockImapConfig(
function createMockImapFolderStatus (line 274) | function createMockImapFolderStatus(
function createMockImapFetchResult (line 287) | function createMockImapFetchResult(
function createMockImapFolderSyncResult (line 300) | function createMockImapFolderSyncResult(
function createMockQuickStep (line 314) | function createMockQuickStep(
function createMockSendAsAlias (line 333) | function createMockSendAsAlias(
FILE: src/test/mocks/services.mock.ts
function createMockGmailClient (line 4) | function createMockGmailClient(
function createMockEmailProvider (line 30) | function createMockEmailProvider(
function createMockAiProvider (line 52) | function createMockAiProvider(response = "ai response") {
function createMockFetchResponse (line 62) | function createMockFetchResponse(
FILE: src/test/mocks/stores.mock.ts
function createMockUIStoreState (line 3) | function createMockUIStoreState(overrides: Record<string, unknown> = {}) {
function createMockThreadStoreState (line 11) | function createMockThreadStoreState(
function createMockAccountStoreState (line 23) | function createMockAccountStoreState(
FILE: src/test/mocks/tauri.mock.ts
function createMockTauriFs (line 7) | function createMockTauriFs() {
function createMockTauriPath (line 30) | function createMockTauriPath() {
FILE: src/utils/crypto.ts
constant KEY_FILE_NAME (line 9) | const KEY_FILE_NAME = "velo.key";
constant ALGORITHM (line 10) | const ALGORITHM = "AES-GCM";
constant KEY_LENGTH (line 11) | const KEY_LENGTH = 256;
constant IV_LENGTH (line 12) | const IV_LENGTH = 12;
constant FS_OPTIONS (line 13) | const FS_OPTIONS = { baseDir: BaseDirectory.AppData };
function base64Encode (line 17) | function base64Encode(bytes: Uint8Array): string {
function base64Decode (line 25) | function base64Decode(str: string): Uint8Array {
function ensureAppDataDir (line 34) | async function ensureAppDataDir(): Promise<void> {
function asBufferSource (line 45) | function asBufferSource(arr: Uint8Array): BufferSource {
function getOrCreateKey (line 49) | async function getOrCreateKey(): Promise<CryptoKey> {
function encryptValue (line 81) | async function encryptValue(plaintext: string): Promise<string> {
function decryptValue (line 103) | async function decryptValue(encrypted: string): Promise<string> {
function isEncrypted (line 131) | function isEncrypted(value: string): boolean {
FILE: src/utils/date.ts
function formatRelativeDate (line 4) | function formatRelativeDate(timestamp: number): string {
function formatFullDate (line 49) | function formatFullDate(timestamp: number): string {
function isSameDay (line 61) | function isSameDay(a: Date, b: Date): boolean {
FILE: src/utils/emailBuilder.test.ts
function decodeBase64Url (line 155) | function decodeBase64Url(encoded: string): string {
FILE: src/utils/emailBuilder.ts
type EmailAttachment (line 4) | interface EmailAttachment {
type EmailDraft (line 10) | interface EmailDraft {
function base64UrlEncode (line 23) | function base64UrlEncode(str: string): string {
function htmlToPlainText (line 35) | function htmlToPlainText(html: string): string {
function buildAlternativePart (line 47) | function buildAlternativePart(boundary: string, htmlBody: string): strin...
type InlineImage (line 67) | interface InlineImage {
function extractInlineImages (line 77) | function extractInlineImages(html: string): { html: string; images: Inli...
function generateMessageId (line 93) | function generateMessageId(from: string): string {
function buildRawEmail (line 100) | function buildRawEmail(draft: EmailDraft): string {
FILE: src/utils/emailUtils.ts
function normalizeEmail (line 5) | function normalizeEmail(email: string): string {
FILE: src/utils/fileTypeHelpers.ts
function formatFileSize (line 1) | function formatFileSize(bytes: number): string {
function isImage (line 7) | function isImage(mimeType: string | null): boolean {
function isPdf (line 11) | function isPdf(mimeType: string | null, filename?: string | null): boole...
function isText (line 17) | function isText(mimeType: string | null): boolean {
function canPreview (line 22) | function canPreview(mimeType: string | null, filename: string | null): b...
function isDocument (line 26) | function isDocument(mimeType: string | null, filename?: string | null): ...
function isSpreadsheet (line 34) | function isSpreadsheet(mimeType: string | null, filename?: string | null...
function isArchive (line 42) | function isArchive(mimeType: string | null): boolean {
function getFileIcon (line 47) | function getFileIcon(mimeType: string | null): string {
FILE: src/utils/fileUtils.test.ts
class FailingFileReader (line 40) | class FailingFileReader {
method readAsDataURL (line 46) | readAsDataURL() {
FILE: src/utils/fileUtils.ts
function readFileAsBase64 (line 4) | function readFileAsBase64(file: File): Promise<string> {
FILE: src/utils/imageBlocker.ts
function stripRemoteImages (line 10) | function stripRemoteImages(html: string): string {
function restoreRemoteImages (line 29) | function restoreRemoteImages(html: string): string {
function hasBlockedImages (line 39) | function hasBlockedImages(html: string): boolean {
FILE: src/utils/imageResize.ts
function resizeImageBlob (line 1) | async function resizeImageBlob(
FILE: src/utils/mailtoParser.ts
type MailtoFields (line 1) | interface MailtoFields {
function parseMailtoUrl (line 9) | function parseMailtoUrl(url: string): MailtoFields {
FILE: src/utils/networkErrors.ts
type ErrorType (line 1) | type ErrorType = "network" | "auth" | "quota" | "server" | "permanent";
type ClassifiedError (line 3) | interface ClassifiedError {
constant NETWORK_PATTERNS (line 9) | const NETWORK_PATTERNS = [
constant AUTH_PATTERNS (line 28) | const AUTH_PATTERNS = [
function classifyError (line 36) | function classifyError(error: unknown): ClassifiedError {
function formatSyncError (line 87) | function formatSyncError(rawError: string): string {
FILE: src/utils/noReply.ts
constant NO_REPLY_PATTERNS (line 1) | const NO_REPLY_PATTERNS = [
function isNoReplyAddress (line 12) | function isNoReplyAddress(address: string | null | undefined): boolean {
FILE: src/utils/phishingDetector.ts
type TriggeredRule (line 10) | interface TriggeredRule {
type LinkAnalysis (line 17) | interface LinkAnalysis {
type MessageScanResult (line 25) | interface MessageScanResult {
constant SUSPICIOUS_TLDS_TIER1 (line 36) | const SUSPICIOUS_TLDS_TIER1 = new Set([
constant SUSPICIOUS_TLDS_TIER2 (line 39) | const SUSPICIOUS_TLDS_TIER2 = new Set([
constant SUSPICIOUS_TLDS_TIER3 (line 42) | const SUSPICIOUS_TLDS_TIER3 = new Set([
constant URL_SHORTENERS (line 46) | const URL_SHORTENERS = new Set([
constant SUSPICIOUS_PATH_KEYWORDS (line 50) | const SUSPICIOUS_PATH_KEYWORDS = [
constant DANGEROUS_PROTOCOLS (line 55) | const DANGEROUS_PROTOCOLS = new Set(["data:", "javascript:", "vbscript:"...
constant IMPERSONATED_BRANDS (line 57) | const IMPERSONATED_BRANDS = [
constant MAX_LINKS (line 62) | const MAX_LINKS = 200;
function getRiskLevel (line 66) | function getRiskLevel(score: number): "safe" | "low" | "medium" | "high" {
function checkIpAddress (line 75) | function checkIpAddress(hostname: string): TriggeredRule | null {
function checkHomograph (line 88) | function checkHomograph(hostname: string): TriggeredRule | null {
function checkSuspiciousTld (line 102) | function checkSuspiciousTld(hostname: string): TriggeredRule | null {
function getRegistrableDomain (line 138) | function getRegistrableDomain(hostname: string): string {
function checkDisplayHrefMismatch (line 147) | function checkDisplayHrefMismatch(url: string, displayText: string): Tri...
function checkExcessiveSubdomains (line 190) | function checkExcessiveSubdomains(hostname: string): TriggeredRule | null {
function checkUrlShortener (line 203) | function checkUrlShortener(hostname: string): TriggeredRule | null {
function checkSuspiciousPathKeywords (line 216) | function checkSuspiciousPathKeywords(pathname: string, search: string): ...
function checkDangerousProtocol (line 230) | function checkDangerousProtocol(url: string): TriggeredRule | null {
function checkUrlObfuscation (line 245) | function checkUrlObfuscation(url: string, _hostname: string): TriggeredR...
function checkBrandImpersonation (line 295) | function checkBrandImpersonation(hostname: string, pathname: string): Tr...
function analyzeLink (line 326) | function analyzeLink(url: string, displayText: string): LinkAnalysis {
function scanLinksInHtml (line 411) | function scanLinksInHtml(html: string): LinkAnalysis[] {
type PhishingSensitivity (line 451) | type PhishingSensitivity = "low" | "default" | "high";
constant SENSITIVITY_THRESHOLDS (line 454) | const SENSITIVITY_THRESHOLDS: Record<PhishingSensitivity, { scoreThresho...
function scanMessage (line 460) | function scanMessage(messageId: string, html: string | null, sensitivity...
FILE: src/utils/resolveFromAddress.ts
function resolveFromAddress (line 13) | function resolveFromAddress(
FILE: src/utils/sanitize.ts
function escapeHtml (line 3) | function escapeHtml(str: string): string {
function sanitizeHtml (line 11) | function sanitizeHtml(html: string): string {
FILE: src/utils/templateVariables.ts
type VariableContext (line 4) | interface VariableContext {
type TemplateVariable (line 12) | interface TemplateVariable {
constant TEMPLATE_VARIABLES (line 17) | const TEMPLATE_VARIABLES: TemplateVariable[] = [
function splitName (line 28) | function splitName(fullName: string | undefined): { first: string; last:...
function resolveRecipientName (line 40) | async function resolveRecipientName(ctx: VariableContext): Promise<strin...
function interpolateVariables (line 55) | async function interpolateVariables(
function interpolateVariablesSync (line 97) | function interpolateVariablesSync(
FILE: src/utils/timestamp.ts
function getCurrentUnixTimestamp (line 4) | function getCurrentUnixTimestamp(): number {
Condensed preview — 470 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,665K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 17,
"preview": "* @avihaymenahem\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1689,
"preview": "name: Bug Report\ndescription: Report a bug or unexpected behavior\nlabels: [\"bug\"]\nbody:\n - type: textarea\n id: descr"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/docs.yml",
"chars": 672,
"preview": "name: Documentation Improvement\ndescription: Suggest improvements to documentation\nlabels: [\"documentation\"]\nbody:\n - t"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 915,
"preview": "name: Feature Request\ndescription: Suggest a new feature or improvement\nlabels: [\"enhancement\"]\nbody:\n - type: textarea"
},
{
"path": ".github/pull_request_template.md",
"chars": 451,
"preview": "## Summary\n<!-- Brief description of what this PR does and why -->\n\n## Changes\n<!-- List the key changes -->\n-\n\n## Type "
},
{
"path": ".github/workflows/packaging.yml",
"chars": 2680,
"preview": "name: Build & Package\n\non:\n workflow_call:\n inputs:\n tag_name:\n description: \"Release tag name\"\n "
},
{
"path": ".github/workflows/release-please.yml",
"chars": 1335,
"preview": "name: Release Please\n\non:\n push:\n branches:\n - main\n\npermissions:\n contents: write\n pull-requests: write\n\njob"
},
{
"path": ".github/workflows/release.yml",
"chars": 8021,
"preview": "name: Build & Release\n\non:\n workflow_call:\n inputs:\n tag_name:\n description: \"Release tag (e.g. velo-v0."
},
{
"path": ".github/workflows/update-homebrew.yml",
"chars": 4584,
"preview": "name: Update Homebrew Tap\n\non:\n workflow_call:\n inputs:\n tag_name:\n description: \"Release tag name (e.g."
},
{
"path": ".gitignore",
"chars": 767,
"preview": "# Dependencies\nnode_modules/\n\n# Build outputs\ndist/\nbuild/\ntest-buildroot/\n.flatpak-builder/\n\n# Tauri\nsrc-tauri/target/\n"
},
{
"path": ".release-please-manifest.json",
"chars": 20,
"preview": "{\n \".\": \"0.4.21\"\n}\n"
},
{
"path": "CHANGELOG.md",
"chars": 20130,
"preview": "# Changelog\n\n## [0.4.21](https://github.com/avihaymenahem/velo/compare/velo-v0.4.20...velo-v0.4.21) (2026-02-27)\n\n\n### B"
},
{
"path": "CLAUDE.md",
"chars": 22311,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "CONTRIBUTING.md",
"chars": 6704,
"preview": "# Contributing to Velo\n\nThank you for your interest in contributing to Velo! This guide will help you get started.\n\n## G"
},
{
"path": "LICENSE",
"chars": 11225,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 7088,
"preview": "<p align=\"center\">\n <img src=\"assets/icon.png?v1\" alt=\"Velo\" width=\"200\" height=\"200\" style=\"border-radius: 24px;\" />\n<"
},
{
"path": "SECURITY.md",
"chars": 3310,
"preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported |\n|---------|-----------|\n| Latest release | Yes |\n| Old"
},
{
"path": "com.velomail.app.desktop",
"chars": 185,
"preview": "[Desktop Entry]\nName=Velo\nComment=Fast, beautiful desktop email client\nExec=velo\nIcon=com.velomail.app\nType=Application\n"
},
{
"path": "com.velomail.app.metainfo.xml",
"chars": 1111,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<component type=\"desktop-application\">\n <id>com.velomail.app</id>\n <metadata_l"
},
{
"path": "com.velomail.app.yml",
"chars": 1775,
"preview": "app-id: com.velomail.app\nruntime: org.gnome.Platform\nruntime-version: \"46\"\nsdk: org.gnome.Sdk\nsdk-extensions:\n - org.fr"
},
{
"path": "docs/architecture.md",
"chars": 13398,
"preview": "# Architecture\n\nVelo follows a **three-layer architecture** with clear separation of concerns.\n\n```\n+-------------------"
},
{
"path": "docs/development.md",
"chars": 3076,
"preview": "# Development\n\n## Prerequisites\n\n- [Node.js](https://nodejs.org/) (v18+)\n- [Rust](https://www.rust-lang.org/tools/instal"
},
{
"path": "docs/keyboard-shortcuts.md",
"chars": 2158,
"preview": "# Keyboard Shortcuts\n\nVelo is designed to be used entirely from the keyboard. All shortcuts are customizable in Settings"
},
{
"path": "index.html",
"chars": 291,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "landing/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "landing/README.md",
"chars": 2555,
"preview": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLin"
},
{
"path": "landing/eslint.config.js",
"chars": 616,
"preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
},
{
"path": "landing/index.html",
"chars": 6192,
"preview": "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n\n <!-- Primary Meta -->\n <titl"
},
{
"path": "landing/package.json",
"chars": 934,
"preview": "{\n \"name\": \"landing\",\n \"private\": true,\n \"license\": \"Apache-2.0\",\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"script"
},
{
"path": "landing/public/og-image.html",
"chars": 6590,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; "
},
{
"path": "landing/public/robots.txt",
"chars": 66,
"preview": "User-agent: *\nAllow: /\n\nSitemap: https://velomail.app/sitemap.xml\n"
},
{
"path": "landing/public/screenshots/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "landing/public/sitemap.xml",
"chars": 263,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n <url>\n <loc>htt"
},
{
"path": "landing/src/App.css",
"chars": 65,
"preview": "/* Intentionally empty — all styles in index.css and Tailwind */\n"
},
{
"path": "landing/src/App.tsx",
"chars": 1033,
"preview": "import { useEffect } from 'react'\nimport Lenis from 'lenis'\nimport { Navbar } from './components/Navbar'\nimport { Hero }"
},
{
"path": "landing/src/components/CtaFooter.tsx",
"chars": 3860,
"preview": "import { motion } from 'framer-motion'\nimport { Download } from 'lucide-react'\n\nexport function CtaFooter() {\n return ("
},
{
"path": "landing/src/components/Features.tsx",
"chars": 5436,
"preview": "import { motion } from 'framer-motion'\nimport {\n Zap, Search, Clock, Send, Bell, Calendar,\n Filter, Layers, GripVertic"
},
{
"path": "landing/src/components/Hero.tsx",
"chars": 3314,
"preview": "import { motion } from 'framer-motion'\nimport { Download, Github } from 'lucide-react'\nimport { AppMockup } from './mock"
},
{
"path": "landing/src/components/Navbar.tsx",
"chars": 4036,
"preview": "import { useState, useCallback } from 'react'\nimport { motion, useScroll, useMotionValueEvent } from 'framer-motion'\nimp"
},
{
"path": "landing/src/components/OpenSource.tsx",
"chars": 2911,
"preview": "import { motion } from 'framer-motion'\nimport { Github, Code2, EyeOff, HardDrive, Lock } from 'lucide-react'\n\nconst TRUS"
},
{
"path": "landing/src/components/ProductShowcase.tsx",
"chars": 2544,
"preview": "import { type ReactNode } from 'react'\nimport { motion } from 'framer-motion'\nimport { SplitInboxMockup } from './mockup"
},
{
"path": "landing/src/components/WhyVelo.tsx",
"chars": 2545,
"preview": "import { motion } from 'framer-motion'\nimport { Keyboard, Brain, ShieldCheck } from 'lucide-react'\n\nconst DIFFERENTIATOR"
},
{
"path": "landing/src/components/mockups/AiMockup.tsx",
"chars": 4152,
"preview": "/** AI assistant mockup showing thread summary, smart replies, and inline reply with AI assist */\nimport { Sparkles, Ref"
},
{
"path": "landing/src/components/mockups/AppMockup.tsx",
"chars": 11684,
"preview": "/** Full app layout mockup for the hero section — sidebar + email list + reading pane */\nimport {\n Inbox, Send, FileTex"
},
{
"path": "landing/src/components/mockups/MultiProviderMockup.tsx",
"chars": 4925,
"preview": "/** Multi-provider mockup showing account switcher + provider logos */\nimport { Check, ChevronDown, Plus, Mail, Globe } "
},
{
"path": "landing/src/components/mockups/SplitInboxMockup.tsx",
"chars": 4219,
"preview": "/** Split inbox mockup showing category tabs + categorized threads */\nimport { Inbox, Bell, Tag, Users, Newspaper, Paper"
},
{
"path": "landing/src/index.css",
"chars": 3402,
"preview": "@import \"tailwindcss\";\n\n@theme {\n --font-sans: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-ser"
},
{
"path": "landing/src/main.tsx",
"chars": 230,
"preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport App from '."
},
{
"path": "landing/tsconfig.app.json",
"chars": 732,
"preview": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n \"target\": \"ES2022\",\n"
},
{
"path": "landing/tsconfig.json",
"chars": 119,
"preview": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n"
},
{
"path": "landing/tsconfig.node.json",
"chars": 653,
"preview": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n \"target\": \"ES2023\","
},
{
"path": "landing/vite.config.ts",
"chars": 192,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'"
},
{
"path": "landing/wrangler.jsonc",
"chars": 108,
"preview": "{\n \"name\": \"velomail\",\n \"compatibility_date\": \"2026-02-13\",\n \"assets\": {\n \"directory\": \"./dist\"\n }\n}\n"
},
{
"path": "package.json",
"chars": 2378,
"preview": "{\n \"name\": \"velo\",\n \"version\": \"0.4.21\",\n \"private\": true,\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"scripts\""
},
{
"path": "release-please-config.json",
"chars": 683,
"preview": "{\n \"packages\": {\n \".\": {\n \"release-type\": \"node\",\n \"bump-minor-pre-major\": true,\n \"bump-patch-for-min"
},
{
"path": "splashscreen.html",
"chars": 5784,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, "
},
{
"path": "src/App.tsx",
"chars": 23018,
"preview": "import { useEffect, useState, useCallback, useRef } from \"react\";\nimport { Outlet } from \"@tanstack/react-router\";\nimpor"
},
{
"path": "src/ComposerWindow.tsx",
"chars": 6898,
"preview": "import { useEffect, useState } from \"react\";\nimport { Composer } from \"./components/composer/Composer\";\nimport { UndoSen"
},
{
"path": "src/ThreadWindow.tsx",
"chars": 6990,
"preview": "import { useEffect, useState } from \"react\";\nimport { ThreadView } from \"./components/email/ThreadView\";\nimport { Compos"
},
{
"path": "src/components/accounts/AccountSwitcher.test.tsx",
"chars": 3540,
"preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/r"
},
{
"path": "src/components/accounts/AccountSwitcher.tsx",
"chars": 6872,
"preview": "import { useState, useRef, useCallback } from \"react\";\nimport { useAccountStore, type Account } from \"@/stores/accountSt"
},
{
"path": "src/components/accounts/AddAccount.tsx",
"chars": 9238,
"preview": "import { useState } from \"react\";\nimport { Mail, Calendar } from \"lucide-react\";\nimport { startOAuthFlow } from \"@/servi"
},
{
"path": "src/components/accounts/AddCalDavAccount.tsx",
"chars": 10354,
"preview": "import { useState, useCallback } from \"react\";\nimport {\n ArrowLeft,\n ArrowRight,\n CheckCircle2,\n XCircle,\n Loader2,"
},
{
"path": "src/components/accounts/AddImapAccount.tsx",
"chars": 30664,
"preview": "import { useState, useCallback } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport {\n ArrowLeft,\n Ar"
},
{
"path": "src/components/accounts/SetupClientId.test.tsx",
"chars": 2701,
"preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\ni"
},
{
"path": "src/components/accounts/SetupClientId.tsx",
"chars": 3277,
"preview": "import { useState } from \"react\";\nimport { setSetting, setSecureSetting } from \"@/services/db/settings\";\nimport { Modal "
},
{
"path": "src/components/attachments/AttachmentGridItem.tsx",
"chars": 3323,
"preview": "import { Download, Eye, ExternalLink } from \"lucide-react\";\nimport { formatFileSize, getFileIcon, canPreview } from \"@/u"
},
{
"path": "src/components/attachments/AttachmentLibrary.test.tsx",
"chars": 5100,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-libra"
},
{
"path": "src/components/attachments/AttachmentLibrary.tsx",
"chars": 12981,
"preview": "import { useState, useEffect, useMemo, useCallback, useRef } from \"react\";\nimport { save } from \"@tauri-apps/plugin-dial"
},
{
"path": "src/components/attachments/AttachmentListItem.tsx",
"chars": 2809,
"preview": "import { Download, Eye, ExternalLink } from \"lucide-react\";\nimport { formatFileSize, getFileIcon, canPreview } from \"@/u"
},
{
"path": "src/components/calendar/CalendarList.test.tsx",
"chars": 3888,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { vi } from \"vitest\";\nimport { CalendarList }"
},
{
"path": "src/components/calendar/CalendarList.tsx",
"chars": 1997,
"preview": "import type { DbCalendar } from \"@/services/db/calendars\";\n\ninterface CalendarListProps {\n calendars: DbCalendar[];\n o"
},
{
"path": "src/components/calendar/CalendarPage.tsx",
"chars": 13196,
"preview": "import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useAccountStore } from \"@/stores/accountStore"
},
{
"path": "src/components/calendar/CalendarReauthBanner.tsx",
"chars": 2031,
"preview": "import { useState } from \"react\";\nimport { AlertTriangle, Loader2 } from \"lucide-react\";\nimport { reauthorizeAccount } f"
},
{
"path": "src/components/calendar/CalendarToolbar.tsx",
"chars": 3902,
"preview": "import { ChevronLeft, ChevronRight, Plus, CalendarDays } from \"lucide-react\";\n\nexport type CalendarView = \"day\" | \"week\""
},
{
"path": "src/components/calendar/DayView.tsx",
"chars": 3611,
"preview": "import { useMemo } from \"react\";\nimport type { DbCalendarEvent } from \"@/services/db/calendarEvents\";\n\ninterface DayView"
},
{
"path": "src/components/calendar/EventCard.tsx",
"chars": 1439,
"preview": "import type { DbCalendarEvent } from \"@/services/db/calendarEvents\";\n\ninterface EventCardProps {\n event: DbCalendarEven"
},
{
"path": "src/components/calendar/EventCreateModal.tsx",
"chars": 4620,
"preview": "import { useState, useCallback } from \"react\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Modal } from \"@"
},
{
"path": "src/components/calendar/EventDetailModal.tsx",
"chars": 8428,
"preview": "import { useState, useCallback } from \"react\";\nimport { MapPin, Clock, User, Pencil, Trash2 } from \"lucide-react\";\nimpor"
},
{
"path": "src/components/calendar/MonthView.tsx",
"chars": 3407,
"preview": "import { useMemo } from \"react\";\nimport type { DbCalendarEvent } from \"@/services/db/calendarEvents\";\nimport { EventCard"
},
{
"path": "src/components/calendar/WeekView.tsx",
"chars": 5181,
"preview": "import { useMemo } from \"react\";\nimport type { DbCalendarEvent } from \"@/services/db/calendarEvents\";\n\ninterface WeekVie"
},
{
"path": "src/components/composer/AddressInput.test.tsx",
"chars": 2841,
"preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { render, fireEvent } from \"@testing-li"
},
{
"path": "src/components/composer/AddressInput.tsx",
"chars": 5412,
"preview": "import { useState, useRef, useCallback, useEffect } from \"react\";\nimport { searchContacts, type DbContact } from \"@/serv"
},
{
"path": "src/components/composer/AiAssistPanel.tsx",
"chars": 5621,
"preview": "import { useState, useEffect } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\nimport { Wand2, Sparkles, Arro"
},
{
"path": "src/components/composer/AttachmentPicker.tsx",
"chars": 2887,
"preview": "import { useRef } from \"react\";\nimport { Paperclip, X } from \"lucide-react\";\nimport { useComposerStore, type ComposerAtt"
},
{
"path": "src/components/composer/Composer.tsx",
"chars": 22658,
"preview": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { CSSTransition } from \"react-transition-group\""
},
{
"path": "src/components/composer/EditorToolbar.tsx",
"chars": 4535,
"preview": "import { useRef, useState } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\nimport { InputDialog } from \"@/co"
},
{
"path": "src/components/composer/FromSelector.tsx",
"chars": 1214,
"preview": "import type { SendAsAlias } from \"@/services/db/sendAsAliases\";\n\ninterface FromSelectorProps {\n aliases: SendAsAlias[];"
},
{
"path": "src/components/composer/ScheduleSendDialog.tsx",
"chars": 1983,
"preview": "import { DateTimePickerDialog } from \"@/components/ui/DateTimePickerDialog\";\n\ninterface ScheduleSendDialogProps {\n onSc"
},
{
"path": "src/components/composer/SignatureSelector.tsx",
"chars": 1720,
"preview": "import { useState, useEffect } from \"react\";\nimport { useComposerStore } from \"@/stores/composerStore\";\nimport { useAcco"
},
{
"path": "src/components/composer/TemplatePicker.tsx",
"chars": 2863,
"preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { FileText, ChevronDown } from \"lucide-react\";\n"
},
{
"path": "src/components/composer/UndoSendToast.tsx",
"chars": 1402,
"preview": "import { useRef } from \"react\";\nimport { CSSTransition } from \"react-transition-group\";\nimport { useComposerStore } from"
},
{
"path": "src/components/composer/scheduleSendPresets.test.ts",
"chars": 2469,
"preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\n\ndescribe(\"ScheduleSendDialog presets\", () => "
},
{
"path": "src/components/dnd/DndProvider.test.ts",
"chars": 1992,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { resolveLabelChange } from \"./DndProvider\";\n\ndescribe(\"resolveLab"
},
{
"path": "src/components/dnd/DndProvider.tsx",
"chars": 3836,
"preview": "import { useState, type ReactNode } from \"react\";\nimport {\n DndContext,\n PointerSensor,\n useSensor,\n useSensors,\n D"
},
{
"path": "src/components/email/ActionBar.tsx",
"chars": 13761,
"preview": "import { useState, useEffect } from \"react\";\nimport type { Thread } from \"@/stores/threadStore\";\nimport { useThreadStore"
},
{
"path": "src/components/email/AttachmentList.test.tsx",
"chars": 8941,
"preview": "import { render, screen, waitFor, fireEvent } from \"@testing-library/react\";\nimport { AttachmentList } from \"./Attachmen"
},
{
"path": "src/components/email/AttachmentList.tsx",
"chars": 9191,
"preview": "import { useState, useCallback, useRef, useEffect } from \"react\";\nimport { save } from \"@tauri-apps/plugin-dialog\";\nimpo"
},
{
"path": "src/components/email/AuthBadge.test.tsx",
"chars": 2211,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { AuthBad"
},
{
"path": "src/components/email/AuthBadge.tsx",
"chars": 2141,
"preview": "import { useState } from \"react\";\nimport { ShieldCheck, ShieldAlert, ShieldX, ShieldQuestion } from \"lucide-react\";\nimpo"
},
{
"path": "src/components/email/AuthWarningBanner.test.tsx",
"chars": 1755,
"preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\ni"
},
{
"path": "src/components/email/AuthWarningBanner.tsx",
"chars": 1407,
"preview": "import { ShieldX, X } from \"lucide-react\";\nimport type { AuthResult } from \"@/services/gmail/authParser\";\n\ninterface Aut"
},
{
"path": "src/components/email/CategoryTabs.test.tsx",
"chars": 2721,
"preview": "import { describe, it, expect, vi, beforeEach, beforeAll } from \"vitest\";\nimport { render, screen, fireEvent } from \"@te"
},
{
"path": "src/components/email/CategoryTabs.tsx",
"chars": 3939,
"preview": "import { useEffect, useLayoutEffect, useCallback, useRef, useState } from \"react\";\nimport { Inbox, Bell, Tag, Users, New"
},
{
"path": "src/components/email/ContactSidebar.test.tsx",
"chars": 6102,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@test"
},
{
"path": "src/components/email/ContactSidebar.tsx",
"chars": 17695,
"preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport {\n Mail, Clock, X, Send, Copy, Star, UserPlus,"
},
{
"path": "src/components/email/EmailRenderer.test.tsx",
"chars": 5318,
"preview": "import { render, waitFor } from \"@testing-library/react\";\nimport { EmailRenderer } from \"./EmailRenderer\";\nimport type {"
},
{
"path": "src/components/email/EmailRenderer.tsx",
"chars": 7863,
"preview": "import { useRef, useCallback, useLayoutEffect, useMemo, useState, useEffect } from \"react\";\nimport { ImageOff } from \"lu"
},
{
"path": "src/components/email/FollowUpDialog.tsx",
"chars": 1484,
"preview": "import { DateTimePickerDialog } from \"@/components/ui/DateTimePickerDialog\";\n\ninterface FollowUpDialogProps {\n isOpen?:"
},
{
"path": "src/components/email/InlineAttachmentPreview.test.tsx",
"chars": 6091,
"preview": "import { render, screen, waitFor } from \"@testing-library/react\";\nimport { InlineAttachmentPreview } from \"./InlineAttac"
},
{
"path": "src/components/email/InlineAttachmentPreview.tsx",
"chars": 5966,
"preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport type { DbAttachment } from \"@/services/db/attac"
},
{
"path": "src/components/email/InlineReply.tsx",
"chars": 15750,
"preview": "import { useState, useCallback, useEffect, useRef } from \"react\";\nimport { useEditor, EditorContent } from \"@tiptap/reac"
},
{
"path": "src/components/email/LinkConfirmDialog.tsx",
"chars": 4037,
"preview": "import { ShieldAlert, ExternalLink } from \"lucide-react\";\nimport { Modal } from \"@/components/ui/Modal\";\nimport type { L"
},
{
"path": "src/components/email/MessageItem.test.tsx",
"chars": 4272,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, act } from \"@testing-library/rea"
},
{
"path": "src/components/email/MessageItem.tsx",
"chars": 8115,
"preview": "import { memo, useState, useRef, useEffect, useMemo, forwardRef } from \"react\";\nimport { formatFullDate } from \"@/utils/"
},
{
"path": "src/components/email/MoveToFolderDialog.test.tsx",
"chars": 7660,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-libra"
},
{
"path": "src/components/email/MoveToFolderDialog.tsx",
"chars": 9175,
"preview": "import { useState, useRef, useCallback, useMemo } from \"react\";\nimport { CSSTransition } from \"react-transition-group\";\n"
},
{
"path": "src/components/email/PhishingBanner.tsx",
"chars": 1641,
"preview": "import { ShieldAlert } from \"lucide-react\";\nimport type { MessageScanResult } from \"@/utils/phishingDetector\";\n\ninterfac"
},
{
"path": "src/components/email/RawMessageModal.test.tsx",
"chars": 3851,
"preview": "import { render, screen, waitFor, fireEvent } from \"@testing-library/react\";\nimport { RawMessageModal } from \"./RawMessa"
},
{
"path": "src/components/email/RawMessageModal.tsx",
"chars": 3348,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { Modal } from \"@/components/ui/Modal\";\nimport { getEma"
},
{
"path": "src/components/email/SmartReplySuggestions.tsx",
"chars": 4229,
"preview": "import { useState, useCallback, useRef, useEffect } from \"react\";\nimport { Sparkles, RefreshCw } from \"lucide-react\";\nim"
},
{
"path": "src/components/email/SnoozeDialog.tsx",
"chars": 1822,
"preview": "import { DateTimePickerDialog } from \"@/components/ui/DateTimePickerDialog\";\n\ninterface SnoozeDialogProps {\n isOpen?: b"
},
{
"path": "src/components/email/ThreadCard.test.tsx",
"chars": 2887,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
},
{
"path": "src/components/email/ThreadCard.tsx",
"chars": 6935,
"preview": "import { memo, useMemo } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport type { Thread } from \"@/stor"
},
{
"path": "src/components/email/ThreadSummary.tsx",
"chars": 3821,
"preview": "import { useState, useCallback, useRef, useEffect } from \"react\";\nimport { Sparkles, ChevronDown, ChevronUp, RefreshCw }"
},
{
"path": "src/components/email/ThreadView.tsx",
"chars": 19710,
"preview": "import { useEffect, useState, useRef, useCallback } from \"react\";\nimport { MessageItem } from \"./MessageItem\";\nimport { "
},
{
"path": "src/components/help/HelpCard.tsx",
"chars": 3040,
"preview": "import { ChevronRight } from \"lucide-react\";\nimport { navigateToSettings } from \"@/router/navigate\";\nimport type { HelpC"
},
{
"path": "src/components/help/HelpCardGrid.tsx",
"chars": 624,
"preview": "import { HelpCard } from \"./HelpCard\";\nimport type { HelpCard as HelpCardData } from \"@/constants/helpContent\";\n\ninterfa"
},
{
"path": "src/components/help/HelpPage.tsx",
"chars": 4679,
"preview": "import { useState, useMemo } from \"react\";\nimport { useParams } from \"@tanstack/react-router\";\nimport { ArrowLeft, Searc"
},
{
"path": "src/components/help/HelpSearchBar.tsx",
"chars": 1046,
"preview": "import { Search, X } from \"lucide-react\";\n\ninterface HelpSearchBarProps {\n query: string;\n onChange: (query: string) ="
},
{
"path": "src/components/help/HelpSidebar.tsx",
"chars": 1068,
"preview": "import { HELP_CATEGORIES } from \"@/constants/helpContent\";\nimport { navigateToHelp } from \"@/router/navigate\";\n\ninterfac"
},
{
"path": "src/components/help/HelpTooltip.tsx",
"chars": 2427,
"preview": "import { useState, useRef, useCallback, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Hel"
},
{
"path": "src/components/help/helpContentSearch.test.ts",
"chars": 4000,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { HELP_CATEGORIES, getAllCards, getCategoryById } from \"@/constant"
},
{
"path": "src/components/labels/LabelForm.tsx",
"chars": 5968,
"preview": "import { useState, useCallback, useRef, useEffect } from \"react\";\nimport { X } from \"lucide-react\";\nimport { useLabelSto"
},
{
"path": "src/components/layout/EmailList.tsx",
"chars": 31063,
"preview": "import { useEffect, useCallback, useMemo, useRef, useState } from \"react\";\nimport { CSSTransition } from \"react-transiti"
},
{
"path": "src/components/layout/MailLayout.tsx",
"chars": 2630,
"preview": "import { useCallback, useRef } from \"react\";\nimport { EmailList } from \"./EmailList\";\nimport { ReadingPane } from \"./Rea"
},
{
"path": "src/components/layout/ReadingPane.tsx",
"chars": 877,
"preview": "import { ThreadView } from \"../email/ThreadView\";\nimport { useThreadStore } from \"@/stores/threadStore\";\nimport { useSel"
},
{
"path": "src/components/layout/Sidebar.tsx",
"chars": 25451,
"preview": "import { useEffect, useState, useCallback, useMemo } from \"react\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport "
},
{
"path": "src/components/layout/TitleBar.tsx",
"chars": 2490,
"preview": "import { useState, useEffect } from \"react\";\nimport { getCurrentWindow } from \"@tauri-apps/api/window\";\nimport { Minus, "
},
{
"path": "src/components/search/AskInbox.tsx",
"chars": 6488,
"preview": "import { useState, useRef, useCallback } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Sparkles, X, S"
},
{
"path": "src/components/search/CommandPalette.tsx",
"chars": 9300,
"preview": "import { useState, useRef, useCallback, useEffect, useMemo } from \"react\";\nimport { CSSTransition } from \"react-transiti"
},
{
"path": "src/components/search/SearchBar.tsx",
"chars": 3770,
"preview": "import { useState, useRef, useCallback } from \"react\";\nimport { searchMessages } from \"@/services/db/search\";\nimport { u"
},
{
"path": "src/components/search/ShortcutsHelp.tsx",
"chars": 1433,
"preview": "import { SHORTCUTS } from \"@/constants/shortcuts\";\nimport { useShortcutStore } from \"@/stores/shortcutStore\";\nimport { M"
},
{
"path": "src/components/settings/CalDavSettings.tsx",
"chars": 4990,
"preview": "import { useState, useCallback, useEffect } from \"react\";\nimport { Loader2, CheckCircle2, XCircle } from \"lucide-react\";"
},
{
"path": "src/components/settings/ContactEditor.tsx",
"chars": 5429,
"preview": "import { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { Search, Pencil, Trash2, Check, X } from \"luc"
},
{
"path": "src/components/settings/FilterEditor.tsx",
"chars": 12278,
"preview": "import { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { Trash2, Pencil } from \"lucide-react\";\nimport"
},
{
"path": "src/components/settings/LabelEditor.test.tsx",
"chars": 7814,
"preview": "import { describe, it, expect, beforeEach, vi } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@test"
},
{
"path": "src/components/settings/LabelEditor.tsx",
"chars": 5782,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { Trash2, Pencil, ChevronUp, ChevronDown, X } from \"luc"
},
{
"path": "src/components/settings/QuickStepEditor.tsx",
"chars": 15330,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { Trash2, Pencil, Plus, GripVertical, ChevronDown } fro"
},
{
"path": "src/components/settings/SettingsPage.tsx",
"chars": 103547,
"preview": "import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useParams } from \"@tanstack/react-router\";\nim"
},
{
"path": "src/components/settings/SignatureEditor.test.tsx",
"chars": 6544,
"preview": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport { vi, describe, it, expect, beforeEa"
},
{
"path": "src/components/settings/SignatureEditor.tsx",
"chars": 7525,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimpo"
},
{
"path": "src/components/settings/SmartFolderEditor.tsx",
"chars": 7048,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { Trash2, Pencil } from \"lucide-react\";\nimport { useAcc"
},
{
"path": "src/components/settings/SmartLabelEditor.test.tsx",
"chars": 7592,
"preview": "import { describe, it, expect, beforeEach, vi } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@test"
},
{
"path": "src/components/settings/SmartLabelEditor.tsx",
"chars": 11822,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { Trash2, Pencil, ChevronDown, ChevronUp, Loader2 } fro"
},
{
"path": "src/components/settings/SubscriptionManager.tsx",
"chars": 7633,
"preview": "import { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { useAccountStore } from \"@/stores/accountStor"
},
{
"path": "src/components/settings/TemplateEditor.tsx",
"chars": 8203,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimpo"
},
{
"path": "src/components/tasks/AiTaskExtractDialog.tsx",
"chars": 7983,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { X, Loader2, Sparkles, Calendar, Flag } from \"lucide-r"
},
{
"path": "src/components/tasks/TaskItem.test.tsx",
"chars": 2206,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { TaskItem } from \"./TaskItem\";\nimport type {"
},
{
"path": "src/components/tasks/TaskItem.tsx",
"chars": 6376,
"preview": "import { useState, useCallback } from \"react\";\nimport {\n Circle,\n CheckCircle2,\n ChevronRight,\n ChevronDown,\n Trash"
},
{
"path": "src/components/tasks/TaskQuickAdd.tsx",
"chars": 1145,
"preview": "import { useState, useCallback, useRef } from \"react\";\nimport { Plus } from \"lucide-react\";\n\ninterface TaskQuickAddProps"
},
{
"path": "src/components/tasks/TaskSidebar.tsx",
"chars": 5013,
"preview": "import { useState, useEffect, useCallback } from \"react\";\nimport { X, ExternalLink } from \"lucide-react\";\nimport { useTa"
},
{
"path": "src/components/tasks/TasksPage.tsx",
"chars": 12005,
"preview": "import { useState, useEffect, useCallback, useMemo } from \"react\";\nimport {\n CheckSquare,\n Search,\n Trash2,\n CheckCi"
},
{
"path": "src/components/ui/Button.test.tsx",
"chars": 3508,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { Button } from \"./Button\";\n\ndescribe(\"Button"
},
{
"path": "src/components/ui/Button.tsx",
"chars": 1342,
"preview": "import { type ButtonHTMLAttributes, type ReactNode, type Ref } from \"react\";\n\ninterface ButtonProps extends ButtonHTMLAt"
},
{
"path": "src/components/ui/ConfirmDialog.test.tsx",
"chars": 2845,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { ConfirmDialog } from \"./ConfirmDialog\";\n\nde"
},
{
"path": "src/components/ui/ConfirmDialog.tsx",
"chars": 1680,
"preview": "import { type ReactNode, useEffect, useRef } from \"react\";\nimport { Modal } from \"./Modal\";\nimport { Button } from \"./Bu"
},
{
"path": "src/components/ui/ContextMenu.test.tsx",
"chars": 6009,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-libra"
},
{
"path": "src/components/ui/ContextMenu.tsx",
"chars": 11238,
"preview": "import { useEffect, useRef, useState, useCallback } from \"react\";\nimport { useClickOutside } from \"@/hooks/useClickOutsi"
},
{
"path": "src/components/ui/ContextMenuPortal.tsx",
"chars": 22369,
"preview": "import { useState, useEffect } from \"react\";\nimport { ContextMenu, type ContextMenuItem } from \"./ContextMenu\";\nimport {"
},
{
"path": "src/components/ui/DateTimePickerDialog.test.tsx",
"chars": 5918,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { DateTimePickerDialog } from \"./DateTimePick"
},
{
"path": "src/components/ui/DateTimePickerDialog.tsx",
"chars": 2905,
"preview": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Modal } from \"@/components/u"
},
{
"path": "src/components/ui/EmptyState.tsx",
"chars": 929,
"preview": "import type { LucideIcon } from \"lucide-react\";\nimport type { ComponentType } from \"react\";\n\ntype EmptyStateProps = {\n "
},
{
"path": "src/components/ui/ErrorBoundary.test.tsx",
"chars": 3573,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\n\n//"
},
{
"path": "src/components/ui/ErrorBoundary.tsx",
"chars": 1540,
"preview": "import { Component, type ErrorInfo, type ReactNode } from \"react\";\n\ninterface ErrorBoundaryProps {\n children: ReactNode"
},
{
"path": "src/components/ui/InputDialog.test.tsx",
"chars": 3775,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { InputDialog } from \"./InputDialog\";\n\ndescri"
},
{
"path": "src/components/ui/InputDialog.tsx",
"chars": 2797,
"preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { Modal } from \"./Modal\";\nimport { Button } fro"
},
{
"path": "src/components/ui/Modal.test.tsx",
"chars": 3693,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { Modal } from \"./Modal\";\n\ndescribe(\"Modal\", "
},
{
"path": "src/components/ui/Modal.tsx",
"chars": 2136,
"preview": "import { type ReactNode, useEffect, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CSSTransit"
},
{
"path": "src/components/ui/OfflineBanner.tsx",
"chars": 488,
"preview": "import { useUIStore } from \"@/stores/uiStore\";\nimport { WifiOff } from \"lucide-react\";\n\nexport function OfflineBanner() "
},
{
"path": "src/components/ui/Skeleton.tsx",
"chars": 1611,
"preview": "export function ThreadCardSkeleton() {\n return (\n <div className=\"px-4 py-3 border-b border-border-secondary animate"
},
{
"path": "src/components/ui/TextField.test.tsx",
"chars": 3673,
"preview": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport { TextField } from \"./TextField\";\n\ndescribe(\""
},
{
"path": "src/components/ui/TextField.tsx",
"chars": 1085,
"preview": "import { type InputHTMLAttributes, forwardRef } from \"react\";\n\ninterface TextFieldProps extends Omit<InputHTMLAttributes"
},
{
"path": "src/components/ui/UpdateToast.test.tsx",
"chars": 2972,
"preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor, act } from \""
},
{
"path": "src/components/ui/UpdateToast.tsx",
"chars": 2192,
"preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { CSSTransition } from \"react-transition-group\""
},
{
"path": "src/components/ui/illustrations/GenericEmptyIllustration.tsx",
"chars": 2163,
"preview": "interface Props {\n size?: number;\n className?: string;\n}\n\nexport function GenericEmptyIllustration({ size = 140, class"
},
{
"path": "src/components/ui/illustrations/InboxClearIllustration.tsx",
"chars": 1986,
"preview": "interface Props {\n size?: number;\n className?: string;\n}\n\nexport function InboxClearIllustration({ size = 140, classNa"
},
{
"path": "src/components/ui/illustrations/NoAccountIllustration.tsx",
"chars": 1714,
"preview": "interface Props {\n size?: number;\n className?: string;\n}\n\nexport function NoAccountIllustration({ size = 140, classNam"
},
{
"path": "src/components/ui/illustrations/NoSearchResultsIllustration.tsx",
"chars": 2004,
"preview": "interface Props {\n size?: number;\n className?: string;\n}\n\nexport function NoSearchResultsIllustration({ size = 140, cl"
},
{
"path": "src/components/ui/illustrations/ReadingPaneIllustration.tsx",
"chars": 2070,
"preview": "interface Props {\n size?: number;\n className?: string;\n}\n\nexport function ReadingPaneIllustration({ size = 140, classN"
},
{
"path": "src/components/ui/illustrations/index.ts",
"chars": 349,
"preview": "export { InboxClearIllustration } from \"./InboxClearIllustration\";\nexport { NoSearchResultsIllustration } from \"./NoSear"
},
{
"path": "src/config/tauriConfig.test.ts",
"chars": 600,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\n\ndescr"
},
{
"path": "src/constants/helpContent.test.ts",
"chars": 2536,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n HELP_CATEGORIES,\n CONTEXTUAL_TIPS,\n getAllCards,\n getCatego"
},
{
"path": "src/constants/helpContent.ts",
"chars": 79416,
"preview": "import type { LucideIcon } from \"lucide-react\";\nimport {\n Mail,\n PenLine,\n Search,\n Tag,\n Clock,\n Sparkles,\n News"
},
{
"path": "src/constants/shortcuts.test.ts",
"chars": 1646,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { SHORTCUTS, getDefaultKeyMap } from \"./shortcuts\";\n\ndescribe(\"SHO"
},
{
"path": "src/constants/shortcuts.ts",
"chars": 3244,
"preview": "export interface ShortcutItem {\n id: string;\n keys: string; // default key binding\n desc: string;\n}\n\nexport interface"
},
{
"path": "src/constants/themes.test.ts",
"chars": 1333,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n COLOR_THEMES,\n DEFAULT_COLOR_THEME,\n getThemeById,\n} from \"."
},
{
"path": "src/constants/themes.ts",
"chars": 4143,
"preview": "export type ColorThemeId =\n | \"indigo\"\n | \"rose\"\n | \"emerald\"\n | \"amber\"\n | \"sky\"\n | \"violet\"\n | \"orange\"\n | \"sl"
},
{
"path": "src/hooks/useClickOutside.ts",
"chars": 458,
"preview": "import { useEffect, type RefObject } from \"react\";\n\nexport function useClickOutside(\n ref: RefObject<HTMLElement | null"
}
]
// ... and 270 more files (download for full content)
About this extraction
This page contains the full source code of the avihaymenahem/velo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 470 files (2.4 MB), approximately 652.1k tokens, and a symbol index with 1475 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.