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 ## 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 ================================================ 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 `` or ``. 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 `` which swaps CSS custom properties. Font scaling via `font-scale-{small|default|large|xlarge}` classes on ``. **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 ` - 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 ================================================

Velo

Velo

Email at the speed of thought.

A blazing-fast, keyboard-first desktop email client built with Tauri, React, and Rust.
Local-first. Privacy-focused. AI-powered.

Features  •   Installation  •   Shortcuts  •   Architecture  •   Development  •   Contributing

---

Screenshot 2026-02-17 223320

--- ## 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) ---

Built with Rust and React.
Made by Avihay.

================================================ 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 ================================================ com.velomail.app CC0-1.0 Apache-2.0 Velo Fast, beautiful desktop email client

Velo is a fast and beautiful desktop email client, built with modern web technologies.

https://github.com/avihaymenahem/velo https://github.com/avihaymenahem/velo/issues com.velomail.app.desktop Velo Team /app/share/icons/hicolor/128x128/apps/com.velomail.app.png /app/share/icons/hicolor/256x256/apps/com.velomail.app.png
================================================ 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 ================================================ Velo
================================================ 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 ================================================ Velo — The email client you'd build for yourself
================================================ 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 ================================================
Email at the
speed of thought
AI-powered, keyboard-first, privacy-focused desktop email.
122+Features
30+Shortcuts
3AI Providers
0Tracking
velomail.app
================================================ FILE: landing/public/robots.txt ================================================ User-agent: * Allow: / Sitemap: https://velomail.app/sitemap.xml ================================================ FILE: landing/public/screenshots/.gitkeep ================================================ ================================================ FILE: landing/public/sitemap.xml ================================================ https://velomail.app/ 2026-02-13 weekly 1.0 ================================================ 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 (
) } export default App ================================================ FILE: landing/src/components/CtaFooter.tsx ================================================ import { motion } from 'framer-motion' import { Download } from 'lucide-react' export function CtaFooter() { return (
{/* CTA */}
{/* Background glow */}
Try Velo today Free, open source, and ready in two minutes. Download for Free Windows macOS Linux
{/* Footer */}
) } ================================================ 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 (
{/* Fade edges */}

Everything you'd expect, and more

130+ features built for people who live in their inbox.

{FEATURES.map((feature, i) => (

{feature.title}

{feature.description}

))}
) } ================================================ 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 (
{/* Background glow */}
{/* Label */}
Velo Open source desktop email client
{/* Headline */} The email client
you'd build for yourself
{/* Subline */} Keyboard-first, AI-powered, and completely private.
Free forever because it's open source.
{/* CTAs */} Download for Free View on GitHub {/* App mockup with glow behind */} {/* Glow behind mockup */}
) } ================================================ 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, href: string) => { e.preventDefault() const el = document.querySelector(href) el?.scrollIntoView({ behavior: 'smooth' }) setMobileOpen(false) }, []) return ( {mobileOpen && ( )} ) } ================================================ 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 (
{/* Background glow */}
Open source and free forever 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. {TRUST_SIGNALS.map((signal) => (
{signal.label}
))}
Star on GitHub
) } ================================================ 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: , }, { 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: , }, { 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: , }, ] export function ProductShowcase() { return (
{FEATURES.map((feature, i) => { const reversed = i % 2 !== 0 return ( {/* Text */}

{feature.title}

{feature.description}

{/* Mockup */}
{feature.mockup}
) })}
) } ================================================ 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 (
{/* Subtle top/bottom fade to blend the dot grid */}
{DIFFERENTIATORS.map((item, i) => (

{item.title}

{item.description}

))}
) } ================================================ 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 (
{/* Thread header */}
Re: Partnership proposal — Acme Corp
J
Julia Martinez · 3 messages
{/* AI Summary */}
AI Summary

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.

{/* Message preview */}

Hi Alex,

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?

I've attached the proposed agreement for your review.

Best,
Julia

{/* Smart replies */}
Quick Replies
{[ '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) => ( ))}
{/* Inline reply composer with AI assist */}

Hi Julia,

Thursday at 2pm works perfectly. I'll review the proposed agreement before our call so we can dive right into the details.

Would it be alright if I include our legal counsel? It would help streamline the process.

) } ================================================ 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 (
{/* Title bar */}
Velo
{/* Sidebar */}
{/* Account */}
A
Alex Chen
alex@company.com
{/* Compose */} {/* Nav items */}
{SIDEBAR_ITEMS.map((item) => (
{item.label} {item.count && ( {item.count} )}
))}
{/* Labels */}
Labels
{LABELS.map((label) => (
{label.name}
))}
{/* Bottom */}
{/* Email list */}
{/* Search */}
Search emails... /
{/* Thread list */}
{THREADS.map((thread, i) => (
{thread.avatar}
{thread.sender} {thread.time}
{thread.subject}
{thread.snippet} {thread.starred && } {thread.attachment && } {thread.category && ( {thread.category} )}
))}
{/* Reading pane */}
{/* Action bar */}
{[Archive, Trash2, Star, Keyboard, Brain].map((Icon, i) => ( ))}
{/* Thread header */}

Q1 product roadmap review

A
Alex Chen to me 10:32 AM
{/* AI Summary */}
AI Summary

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.

{/* Message body */}
{MESSAGE_BODY}
{/* Smart replies */}
Quick Replies
{['Looks good, let\'s proceed!', 'Can we discuss the timeline?', 'I have a few concerns'].map((reply) => ( ))}
) } ================================================ 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 (
{/* Left: Account switcher */}
{/* Current account */}
A
Alex Chen
alex@company.com
{/* Account list */}
Accounts
{ACCOUNTS.map((account) => (
{account.avatar}
{account.name} {account.provider}
{account.email}
{account.active && }
))} {/* Add account */}
Add account
{/* Right: Supported providers */}
Supported providers
Auto-discovery for major providers
{PROVIDERS.map((provider) => (
{provider.name}
{provider.desc}
))}
) } ================================================ 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 (
{/* Category tabs */}
{TABS.map((tab) => ( ))}
{/* Thread list */}
{THREADS.map((thread, i) => (
{thread.avatar}
{thread.sender} {thread.time}
{thread.subject}
{thread.snippet} {thread.starred && } {thread.attachment && }
))}
) } ================================================ 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( , ) ================================================ 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 ================================================ Velo
================================================ FILE: src/App.tsx ================================================ import { useEffect, useState, useCallback, useRef } from "react"; import { Outlet } from "@tanstack/react-router"; import { Sidebar } from "./components/layout/Sidebar"; import { AddAccount } from "./components/accounts/AddAccount"; import { Composer } from "./components/composer/Composer"; import { UndoSendToast } from "./components/composer/UndoSendToast"; import { CommandPalette } from "./components/search/CommandPalette"; import { ShortcutsHelp } from "./components/search/ShortcutsHelp"; import { AskInbox } from "./components/search/AskInbox"; import { useUIStore } from "./stores/uiStore"; import { useAccountStore } from "./stores/accountStore"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import { runMigrations } from "./services/db/migrations"; import { getAllAccounts } from "./services/db/accounts"; import { getSetting } from "./services/db/settings"; import { startBackgroundSync, stopBackgroundSync, syncAccount, triggerSync, onSyncStatus, } from "./services/gmail/syncManager"; import { initializeClients } from "./services/gmail/tokenManager"; import { startSnoozeChecker, stopSnoozeChecker, } from "./services/snooze/snoozeManager"; import { startScheduledSendChecker, stopScheduledSendChecker, } from "./services/snooze/scheduledSendManager"; import { startFollowUpChecker, stopFollowUpChecker, } from "./services/followup/followupManager"; import { startBundleChecker, stopBundleChecker, } from "./services/bundles/bundleManager"; import { initNotifications } from "./services/notifications/notificationManager"; import { initGlobalShortcut, unregisterComposeShortcut, } from "./services/globalShortcut"; import { initDeepLinkHandler } from "./services/deepLinkHandler"; import { updateBadgeCount } from "./services/badgeManager"; import { startQueueProcessor, stopQueueProcessor, triggerQueueFlush, } from "./services/queue/queueProcessor"; import { startPreCacheManager, stopPreCacheManager, } from "./services/attachments/preCacheManager"; import { startUpdateChecker, stopUpdateChecker, } from "./services/updateManager"; import { fetchSendAsAliases } from "./services/gmail/sendAs"; import { getGmailClient } from "./services/gmail/tokenManager"; import { invoke } from "@tauri-apps/api/core"; import { DndProvider } from "./components/dnd/DndProvider"; import { TitleBar } from "./components/layout/TitleBar"; import { useShortcutStore } from "./stores/shortcutStore"; import { getIncompleteTaskCount } from "./services/db/tasks"; import { useTaskStore } from "./stores/taskStore"; import { ContextMenuPortal } from "./components/ui/ContextMenuPortal"; import { MoveToFolderDialog } from "./components/email/MoveToFolderDialog"; import { OfflineBanner } from "./components/ui/OfflineBanner"; import { UpdateToast } from "./components/ui/UpdateToast"; import { ErrorBoundary } from "./components/ui/ErrorBoundary"; import { formatSyncError } from "./utils/networkErrors"; import { getThemeById, COLOR_THEMES } from "./constants/themes"; import type { ColorThemeId } from "./constants/themes"; import { router } from "./router"; import { getSelectedThreadId } from "./router/navigate"; /** * Sync bridge: subscribes to router state changes and writes the selected * thread ID to the threadStore so that range-select and other multi-select * logic can use it as an anchor. */ function useRouterSyncBridge() { useEffect(() => { return router.subscribe("onResolved", () => { const threadId = getSelectedThreadId(); if (useThreadStore.getState().selectedThreadId !== threadId) { useThreadStore.getState().selectThread(threadId); } }); }, []); } import { useThreadStore } from "./stores/threadStore"; export default function App() { const theme = useUIStore((s) => s.theme); const fontScale = useUIStore((s) => s.fontScale); const colorTheme = useUIStore((s) => s.colorTheme); const reduceMotion = useUIStore((s) => s.reduceMotion); const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed); const [showAddAccount, setShowAddAccount] = useState(false); const [initialized, setInitialized] = useState(false); const [syncStatus, setSyncStatus] = useState(null); const [showCommandPalette, setShowCommandPalette] = useState(false); const [showShortcutsHelp, setShowShortcutsHelp] = useState(false); const [showAskInbox, setShowAskInbox] = useState(false); const [moveToFolderState, setMoveToFolderState] = useState<{ open: boolean; threadIds: string[] }>({ open: false, threadIds: [] }); const deepLinkCleanupRef = useRef<(() => void) | undefined>(undefined); // Sync bridge: router state → Zustand stores (temporary) useRouterSyncBridge(); // Register global keyboard shortcuts useKeyboardShortcuts(); // Network status detection useEffect(() => { const { setOnline } = useUIStore.getState(); setOnline(navigator.onLine); const handleOnline = () => { setOnline(true); triggerQueueFlush(); const accounts = useAccountStore.getState().accounts; const activeIds = accounts.filter((a) => a.isActive).map((a) => a.id); if (activeIds.length > 0) triggerSync(activeIds); }; const handleOffline = () => setOnline(false); window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, []); // Suppress default browser context menu globally (Tauri app should feel native) // Elements with data-native-context-menu opt out so the browser menu is available useEffect(() => { const handler = (e: MouseEvent) => { if ((e.target as HTMLElement).closest?.("[data-native-context-menu]")) return; e.preventDefault(); }; document.addEventListener("contextmenu", handler); return () => document.removeEventListener("contextmenu", handler); }, []); // Listen for command palette / shortcuts help toggle events useEffect(() => { const togglePalette = () => setShowCommandPalette((p) => !p); const toggleHelp = () => setShowShortcutsHelp((p) => !p); const toggleAskInbox = () => setShowAskInbox((p) => !p); const handleMoveToFolder = (e: Event) => { const detail = (e as CustomEvent<{ threadIds: string[] }>).detail; setMoveToFolderState({ open: true, threadIds: detail.threadIds }); }; window.addEventListener("velo-toggle-command-palette", togglePalette); window.addEventListener("velo-toggle-shortcuts-help", toggleHelp); window.addEventListener("velo-toggle-ask-inbox", toggleAskInbox); window.addEventListener("velo-move-to-folder", handleMoveToFolder); return () => { window.removeEventListener("velo-toggle-command-palette", togglePalette); window.removeEventListener("velo-toggle-shortcuts-help", toggleHelp); window.removeEventListener("velo-toggle-ask-inbox", toggleAskInbox); window.removeEventListener("velo-move-to-folder", handleMoveToFolder); }; }, []); // Listen for tray "Check for Mail" button useEffect(() => { let unlisten: (() => void) | undefined; import("@tauri-apps/api/event").then(({ listen }) => { listen("tray-check-mail", () => { const accounts = useAccountStore.getState().accounts; const activeIds = accounts.filter((a) => a.isActive).map((a) => a.id); if (activeIds.length > 0) { triggerSync(activeIds); } }).then((fn) => { unlisten = fn; }); }); return () => { unlisten?.(); }; }, []); // Initialize database, load accounts, start sync useEffect(() => { async function init() { try { await runMigrations(); const ui = useUIStore.getState(); // Restore persisted theme const savedTheme = await getSetting("theme"); if (savedTheme === "light" || savedTheme === "dark" || savedTheme === "system") { ui.setTheme(savedTheme); } // Restore persisted sidebar state const savedSidebar = await getSetting("sidebar_collapsed"); if (savedSidebar === "true") { ui.setSidebarCollapsed(true); } // Restore contact sidebar visibility const savedContactSidebar = await getSetting("contact_sidebar_visible"); if (savedContactSidebar === "false") { ui.setContactSidebarVisible(false); } // Restore reading pane position const savedPanePos = await getSetting("reading_pane_position"); if (savedPanePos === "right" || savedPanePos === "bottom" || savedPanePos === "hidden") { ui.setReadingPanePosition(savedPanePos); } // Restore read filter const savedReadFilter = await getSetting("read_filter"); if (savedReadFilter === "all" || savedReadFilter === "read" || savedReadFilter === "unread") { ui.setReadFilter(savedReadFilter); } // Restore email list width const savedListWidth = await getSetting("email_list_width"); if (savedListWidth) { const w = parseInt(savedListWidth, 10); if (w >= 240 && w <= 800) ui.setEmailListWidth(w); } // Restore email density const savedDensity = await getSetting("email_density"); if (savedDensity === "compact" || savedDensity === "default" || savedDensity === "spacious") { ui.setEmailDensity(savedDensity); } // Restore default reply mode const savedReplyMode = await getSetting("default_reply_mode"); if (savedReplyMode === "reply" || savedReplyMode === "replyAll") { ui.setDefaultReplyMode(savedReplyMode); } // Restore mark-as-read behavior const savedMarkRead = await getSetting("mark_as_read_behavior"); if (savedMarkRead === "instant" || savedMarkRead === "2s" || savedMarkRead === "manual") { ui.setMarkAsReadBehavior(savedMarkRead); } // Restore send and archive const savedSendArchive = await getSetting("send_and_archive"); if (savedSendArchive === "true") { ui.setSendAndArchive(true); } // Restore font scale const savedFontScale = await getSetting("font_size"); if (savedFontScale === "small" || savedFontScale === "default" || savedFontScale === "large" || savedFontScale === "xlarge") { ui.setFontScale(savedFontScale); } // Restore color theme const savedColorTheme = await getSetting("color_theme"); if (savedColorTheme && COLOR_THEMES.some((t) => t.id === savedColorTheme)) { ui.setColorTheme(savedColorTheme as ColorThemeId); } // Restore inbox view mode const savedViewMode = await getSetting("inbox_view_mode"); if (savedViewMode === "unified" || savedViewMode === "split") { ui.setInboxViewMode(savedViewMode); } // Restore reduce motion preference const savedReduceMotion = await getSetting("reduce_motion"); if (savedReduceMotion === "true") { ui.setReduceMotion(true); } // Restore task sidebar visibility const savedTaskSidebar = await getSetting("task_sidebar_visible"); if (savedTaskSidebar === "true") { ui.setTaskSidebarVisible(true); } // Restore sidebar nav config const savedNavConfig = await getSetting("sidebar_nav_config"); if (savedNavConfig) { try { const parsed = JSON.parse(savedNavConfig); if (Array.isArray(parsed)) ui.restoreSidebarNavConfig(parsed); } catch { /* ignore malformed JSON */ } } // Load custom keyboard shortcuts await useShortcutStore.getState().loadKeyMap(); const dbAccounts = await getAllAccounts(); const mapped = dbAccounts.map((a) => ({ id: a.id, email: a.email, displayName: a.display_name, avatarUrl: a.avatar_url, isActive: a.is_active === 1, provider: a.provider, })); const savedAccountId = await getSetting("active_account_id"); useAccountStore.getState().setAccounts(mapped, savedAccountId); // Initialize Gmail clients for existing accounts await initializeClients(); // Fetch send-as aliases for each active email account (skip CalDAV-only) const activeIds = mapped.filter((a) => a.isActive).map((a) => a.id); const emailAccountIds = mapped.filter((a) => a.isActive && a.provider !== "caldav").map((a) => a.id); for (const accountId of emailAccountIds) { try { const client = await getGmailClient(accountId); await fetchSendAsAliases(client, accountId); } catch (err) { console.warn(`Failed to fetch send-as aliases for ${accountId}:`, err); } } // Start background sync for active accounts if (activeIds.length > 0) { startBackgroundSync(activeIds); } // Start snooze, scheduled send, follow-up, bundle, and queue checkers startSnoozeChecker(); startScheduledSendChecker(); startFollowUpChecker(); startBundleChecker(); startQueueProcessor(); startPreCacheManager(); // Initialize notifications await initNotifications(); // Initialize global compose shortcut await initGlobalShortcut(); // Initialize deep link handler deepLinkCleanupRef.current = await initDeepLinkHandler(); // Initial badge count await updateBadgeCount(); // Load initial task count const activeAcct = useAccountStore.getState().activeAccountId; if (activeAcct) { const count = await getIncompleteTaskCount(activeAcct); useTaskStore.getState().setIncompleteCount(count); } // Start auto-update checker startUpdateChecker(); } catch (err) { console.error("Failed to initialize:", err); } setInitialized(true); invoke("close_splashscreen").catch(() => {}); } init(); return () => { stopBackgroundSync(); stopSnoozeChecker(); stopScheduledSendChecker(); stopFollowUpChecker(); stopBundleChecker(); stopQueueProcessor(); stopPreCacheManager(); stopUpdateChecker(); unregisterComposeShortcut(); deepLinkCleanupRef.current?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps -- store setters are stable references }, []); // Listen for sync status updates const backfillDoneRef = useRef(false); useEffect(() => { const unsub = onSyncStatus((accountId, status, progress, error) => { if (status === "syncing") { if (progress) { if (progress.phase === "messages") { setSyncStatus( `Syncing: ${progress.current}/${progress.total} messages`, ); } else if (progress.phase === "labels") { setSyncStatus("Syncing labels..."); } else if (progress.phase === "threads") { setSyncStatus(`Building threads... (${progress.current}/${progress.total})`); } } else { setSyncStatus("Syncing..."); } } else if (status === "done") { setSyncStatus("Sync complete"); setTimeout(() => setSyncStatus(null), 2_000); window.dispatchEvent(new Event("velo-sync-done")); updateBadgeCount(); // Backfill uncategorized threads after first successful sync if (!backfillDoneRef.current) { backfillDoneRef.current = true; import("./services/categorization/backfillService") .then(({ backfillUncategorizedThreads }) => backfillUncategorizedThreads(accountId)) .catch((err) => console.error("Backfill error:", err)); } } else if (status === "error") { setSyncStatus(error ? `Sync failed: ${formatSyncError(error)}` : "Sync failed"); // Still dispatch sync-done so the UI refreshes with any partially stored data window.dispatchEvent(new Event("velo-sync-done")); // Auto-clear the error after 8 seconds setTimeout(() => setSyncStatus(null), 8_000); } }); return unsub; }, []); // Sync theme class to element useEffect(() => { const root = document.documentElement; if (theme === "dark") { root.classList.add("dark"); } else if (theme === "light") { root.classList.remove("dark"); } else { const mq = window.matchMedia("(prefers-color-scheme: dark)"); const apply = () => { if (mq.matches) { root.classList.add("dark"); } else { root.classList.remove("dark"); } }; apply(); mq.addEventListener("change", apply); return () => mq.removeEventListener("change", apply); } }, [theme]); // Sync font-scale class to element useEffect(() => { const root = document.documentElement; root.classList.remove("font-scale-small", "font-scale-default", "font-scale-large", "font-scale-xlarge"); root.classList.add(`font-scale-${fontScale}`); }, [fontScale]); // Sync reduce-motion class to element useEffect(() => { const root = document.documentElement; root.classList.toggle("reduce-motion", reduceMotion); }, [reduceMotion]); // Apply color theme CSS custom properties to useEffect(() => { const root = document.documentElement; const props = ["--color-accent", "--color-accent-hover", "--color-accent-light", "--color-bg-selected", "--color-sidebar-active"]; const apply = () => { if (colorTheme === "indigo") { // Default theme — remove inline overrides, let CSS handle it for (const p of props) root.style.removeProperty(p); return; } const themeData = getThemeById(colorTheme); const isDark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const colors = isDark ? themeData.dark : themeData.light; root.style.setProperty("--color-accent", colors.accent); root.style.setProperty("--color-accent-hover", colors.accentHover); root.style.setProperty("--color-accent-light", colors.accentLight); root.style.setProperty("--color-bg-selected", colors.bgSelected); root.style.setProperty("--color-sidebar-active", colors.sidebarActive); }; apply(); if (theme === "system") { const mq = window.matchMedia("(prefers-color-scheme: dark)"); mq.addEventListener("change", apply); return () => mq.removeEventListener("change", apply); } }, [colorTheme, theme]); const handleAddAccountSuccess = useCallback(async () => { setShowAddAccount(false); const dbAccounts = await getAllAccounts(); const mapped = dbAccounts.map((a) => ({ id: a.id, email: a.email, displayName: a.display_name, avatarUrl: a.avatar_url, isActive: a.is_active === 1, provider: a.provider, })); useAccountStore.getState().setAccounts(mapped); // Re-initialize clients for the new account await initializeClients(); const newest = mapped[mapped.length - 1]; if (newest) { // Sync the new account immediately — before restarting the background // timer so it doesn't queue behind delta syncs for existing accounts. syncAccount(newest.id); // Fetch send-as aliases in the background (non-blocking, skip CalDAV-only accounts) if (newest.provider !== "caldav") { getGmailClient(newest.id) .then((client) => fetchSendAsAliases(client, newest.id)) .catch((err) => console.warn(`Failed to fetch send-as aliases for new account:`, err)); } } // Restart background sync for all accounts, but skip the immediate run // since we already triggered the new account's sync above. const activeIds = mapped.filter((a) => a.isActive).map((a) => a.id); startBackgroundSync(activeIds, true); }, []); if (!initialized) { return (
Loading your inbox...
); } return (
{/* Animated gradient blobs for glassmorphism effect */}