├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .env.example ├── .github ├── CODEOWNERS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.yml ├── PULL_REQUEST_TEMPLATE.md ├── TRANSLATION.md └── workflows │ ├── ci.yml │ ├── deploy-to-prod-command.yml │ ├── lingo-dev.yml │ └── sync-production.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── MCP.md ├── README.md ├── ROADMAP.md ├── apps ├── mail │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── login │ │ │ │ ├── error-message.tsx │ │ │ │ ├── login-client.tsx │ │ │ │ └── page.tsx │ │ │ └── zero │ │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── (full-width) │ │ │ ├── about.tsx │ │ │ ├── contributors.tsx │ │ │ ├── layout.tsx │ │ │ ├── pricing.tsx │ │ │ ├── privacy.tsx │ │ │ └── terms.tsx │ │ ├── (routes) │ │ │ ├── developer │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── mail │ │ │ │ ├── [folder] │ │ │ │ │ └── page.tsx │ │ │ │ ├── compose │ │ │ │ │ └── page.tsx │ │ │ │ ├── create │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── under-construction │ │ │ │ │ └── [path] │ │ │ │ │ ├── back-button.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── settings │ │ │ │ ├── [...settings] │ │ │ │ │ └── page.tsx │ │ │ │ ├── appearance │ │ │ │ │ └── page.tsx │ │ │ │ ├── connections │ │ │ │ │ └── page.tsx │ │ │ │ ├── danger-zone │ │ │ │ │ └── page.tsx │ │ │ │ ├── general │ │ │ │ │ └── page.tsx │ │ │ │ ├── labels │ │ │ │ │ ├── colors.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── notifications │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── privacy │ │ │ │ │ └── page.tsx │ │ │ │ ├── security │ │ │ │ │ └── page.tsx │ │ │ │ ├── shortcuts │ │ │ │ │ ├── hotkey-recorder.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── signatures │ │ │ │ │ └── page.tsx │ │ │ └── toast-test.tsx │ │ ├── entry.client.tsx │ │ ├── entry.server.tsx │ │ ├── globals.css │ │ ├── home │ │ │ └── page.tsx │ │ ├── mailto-handler.ts │ │ ├── meta-files │ │ │ ├── manifest.ts │ │ │ ├── microsoft-identity-association.json.ts │ │ │ ├── microsoft-identity-association.ts │ │ │ └── not-found.ts │ │ ├── og-api │ │ │ ├── create.tsx │ │ │ └── home.tsx │ │ ├── page.tsx │ │ ├── root.tsx │ │ └── routes.ts │ ├── components.json │ ├── components │ │ ├── ai-toggle-button.tsx │ │ ├── connection │ │ │ └── add.tsx │ │ ├── context │ │ │ ├── command-palette-context.tsx │ │ │ ├── label-sidebar-context.tsx │ │ │ ├── sidebar-context.tsx │ │ │ └── thread-context.tsx │ │ ├── cookies │ │ │ └── cookie-trigger.tsx │ │ ├── create │ │ │ ├── ai-chat.tsx │ │ │ ├── ai-textarea.tsx │ │ │ ├── create-email.tsx │ │ │ ├── editor-autocomplete.ts │ │ │ ├── editor-buttons.tsx │ │ │ ├── editor-menu.tsx │ │ │ ├── editor.colors.tsx │ │ │ ├── editor.link-selector.tsx │ │ │ ├── editor.node-selector.tsx │ │ │ ├── editor.text-buttons.tsx │ │ │ ├── editor.tsx │ │ │ ├── email-composer.tsx │ │ │ ├── email-phrases.ts │ │ │ ├── extensions.ts │ │ │ ├── ghost-text.css │ │ │ ├── image-upload.ts │ │ │ ├── prosemirror.css │ │ │ ├── selectors │ │ │ │ ├── link-selector.tsx │ │ │ │ ├── math-selector.tsx │ │ │ │ ├── node-selector.tsx │ │ │ │ └── text-buttons.tsx │ │ │ ├── signature-display.tsx │ │ │ ├── slash-command.tsx │ │ │ ├── uploaded-file-icon.tsx │ │ │ └── voice.tsx │ │ ├── home │ │ │ ├── HomeContent.tsx │ │ │ ├── footer.tsx │ │ │ └── pixelated-bg.tsx │ │ ├── icons │ │ │ ├── animated │ │ │ │ ├── moon.tsx │ │ │ │ ├── square-pen.tsx │ │ │ │ └── sun.tsx │ │ │ └── icons.tsx │ │ ├── labels │ │ │ └── label-dialog.tsx │ │ ├── magicui │ │ │ └── file-tree.tsx │ │ ├── mail │ │ │ ├── attachment-dialog.tsx │ │ │ ├── attachments-accordion.tsx │ │ │ ├── data.tsx │ │ │ ├── mail-display.tsx │ │ │ ├── mail-iframe.tsx │ │ │ ├── mail-list.tsx │ │ │ ├── mail-quick-actions.tsx │ │ │ ├── mail-skeleton.tsx │ │ │ ├── mail.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── navbar.tsx │ │ │ ├── note-panel.tsx │ │ │ ├── optimistic-thread-state.tsx │ │ │ ├── render-labels.tsx │ │ │ ├── reply-composer.tsx │ │ │ ├── search-bar.tsx │ │ │ ├── signature-preview.tsx │ │ │ ├── thread-display.tsx │ │ │ ├── thread-subject.tsx │ │ │ └── use-mail.ts │ │ ├── motion-primitives │ │ │ └── text-effect.tsx │ │ ├── navbar.tsx │ │ ├── navigation.tsx │ │ ├── onboarding.tsx │ │ ├── party.tsx │ │ ├── pricing │ │ │ ├── comparision.tsx │ │ │ └── pricing-card.tsx │ │ ├── providers │ │ │ ├── editor-provider.tsx │ │ │ └── hotkey-provider-wrapper.tsx │ │ ├── responsive-modal.tsx │ │ ├── settings │ │ │ └── settings-card.tsx │ │ ├── theme │ │ │ ├── mode-toggle.tsx │ │ │ ├── sidebar-theme-switcher.tsx │ │ │ ├── theme-switcher.tsx │ │ │ └── theme-toggle.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── ai-sidebar.tsx │ │ │ ├── animated-number.tsx │ │ │ ├── app-sidebar.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command-menu.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── envelop.tsx │ │ │ ├── form.tsx │ │ │ ├── gauge.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── nav-user.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── page-header.tsx │ │ │ ├── popover.tsx │ │ │ ├── pricing-dialog.tsx │ │ │ ├── pricing-switch.tsx │ │ │ ├── progress.tsx │ │ │ ├── prompts-dialog.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── recursive-folder.tsx │ │ │ ├── resizable.tsx │ │ │ ├── responsive-modal.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── settings-content.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar-labels.tsx │ │ │ ├── sidebar-toggle.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── spinner.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── text-shimmer.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── user │ │ │ └── user-button.tsx │ ├── config │ │ ├── navigation.ts │ │ └── shortcuts.ts │ ├── eslint.config.ts │ ├── hooks │ │ ├── driver │ │ │ ├── use-delete.ts │ │ │ └── use-move-to.ts │ │ ├── ui │ │ │ └── use-background-queue.ts │ │ ├── use-billing.ts │ │ ├── use-compose-editor.ts │ │ ├── use-connections.ts │ │ ├── use-copy-to-clipboard.ts │ │ ├── use-debounce.ts │ │ ├── use-drafts.ts │ │ ├── use-email-aliases.ts │ │ ├── use-geo-location.ts │ │ ├── use-hot-key.ts │ │ ├── use-image-loading.ts │ │ ├── use-labels.ts │ │ ├── use-mail-navigation.ts │ │ ├── use-media-query.ts │ │ ├── use-mobile.tsx │ │ ├── use-notes.tsx │ │ ├── use-open-compose-modal.ts │ │ ├── use-optimistic-actions.ts │ │ ├── use-previous.ts │ │ ├── use-search-value.ts │ │ ├── use-settings.ts │ │ ├── use-stats.ts │ │ ├── use-summary.ts │ │ └── use-threads.ts │ ├── i18n │ │ ├── config.ts │ │ └── request.ts │ ├── instrument.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-proxy.ts │ │ ├── constants.tsx │ │ ├── countries.ts │ │ ├── email-utils.client.tsx │ │ ├── email-utils.ts │ │ ├── filter.ts │ │ ├── hotkeys │ │ │ ├── compose-hotkeys.tsx │ │ │ ├── global-hotkeys.tsx │ │ │ ├── mail-list-hotkeys.tsx │ │ │ ├── navigation-hotkeys.tsx │ │ │ ├── thread-display-hotkeys.tsx │ │ │ └── use-hotkey-utils.ts │ │ ├── label-colors.ts │ │ ├── notes-utils.ts │ │ ├── posthog-provider.tsx │ │ ├── prompts.ts │ │ ├── redis.ts │ │ ├── sanitize-tip-tap-html.tsx │ │ ├── schemas.ts │ │ ├── site-config.ts │ │ ├── thread-actions.ts │ │ ├── timezones.ts │ │ ├── trpc.server.ts │ │ └── utils.ts │ ├── locales │ │ ├── ar.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fa.json │ │ ├── fr.json │ │ ├── hi.json │ │ ├── hu.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── lv.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt.json │ │ ├── ru.json │ │ ├── tr.json │ │ ├── vi.json │ │ ├── zh_CN.json │ │ └── zh_TW.json │ ├── middleware.ts │ ├── package.json │ ├── providers │ │ ├── client-providers.tsx │ │ ├── query-provider.tsx │ │ └── server-providers.tsx │ ├── public │ │ ├── adam.jpg │ │ ├── ahmet.jpg │ │ ├── ai-chat.png │ │ ├── ai-summary.png │ │ ├── ai.svg │ │ ├── assets │ │ │ ├── attachment-icons │ │ │ │ ├── audio.svg │ │ │ │ ├── csv.svg │ │ │ │ ├── excel.svg │ │ │ │ ├── figma.svg │ │ │ │ ├── file.svg │ │ │ │ ├── html.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── powerpoint.svg │ │ │ │ ├── video.svg │ │ │ │ ├── word.svg │ │ │ │ └── zip.svg │ │ │ ├── m0 rounded edges.png │ │ │ ├── m0 rounded edges.svg │ │ │ ├── m0 w lines rounded edges.png │ │ │ ├── m0 w lines rounded edges.svg │ │ │ ├── m0 w lines.png │ │ │ ├── m0 w lines.svg │ │ │ ├── m0.png │ │ │ ├── m0.svg │ │ │ ├── mail.svg │ │ │ ├── mail0.io - Text + logo w lines.png │ │ │ ├── mail0.io - Text + logo w lines.svg │ │ │ ├── mail0.io - Text w lines.png │ │ │ ├── mail0.io - Text w lines.svg │ │ │ └── rocket.svg │ │ ├── black-icon.svg │ │ ├── claude.png │ │ ├── compose.png │ │ ├── dudu.jpg │ │ ├── email-preview.png │ │ ├── empty-state-light.svg │ │ ├── empty-state.svg │ │ ├── favicon.ico │ │ ├── fonts │ │ │ └── geist │ │ │ │ ├── Geist-Black.ttf │ │ │ │ ├── Geist-Bold.ttf │ │ │ │ ├── Geist-ExtraBold.ttf │ │ │ │ ├── Geist-ExtraLight.ttf │ │ │ │ ├── Geist-Light.ttf │ │ │ │ ├── Geist-Medium.ttf │ │ │ │ ├── Geist-Regular.ttf │ │ │ │ ├── Geist-SemiBold.ttf │ │ │ │ └── Geist-Thin.ttf │ │ ├── gradient.svg │ │ ├── homepage-image.png │ │ ├── icons-pwa │ │ │ ├── icon-180.png │ │ │ ├── icon-192.png │ │ │ └── icon-512.png │ │ ├── lock.svg │ │ ├── mail-list.png │ │ ├── mail-pixel.svg │ │ ├── mail.svg │ │ ├── netflix.svg │ │ ├── nizzy.jpg │ │ ├── onboarding │ │ │ ├── coming-soon.png │ │ │ ├── get-started.png │ │ │ ├── ready.png │ │ │ ├── step1.gif │ │ │ ├── step2.gif │ │ │ └── step3.gif │ │ ├── openai.png │ │ ├── opened-mail.svg │ │ ├── pixel.svg │ │ ├── pricing-gradient.png │ │ ├── purple-gradient.png │ │ ├── purple-zap.svg │ │ ├── ryan.jpg │ │ ├── search.png │ │ ├── small-pixel.png │ │ ├── snooze-home.png │ │ ├── star-home.png │ │ ├── star.svg │ │ ├── stripe.svg │ │ ├── verified-home.png │ │ ├── white-icon.svg │ │ ├── yc-small.svg │ │ ├── yc.svg │ │ └── zap.svg │ ├── react-router.config.ts │ ├── store │ │ ├── backgroundQueue.ts │ │ ├── draftStates.ts │ │ └── optimistic-updates.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── types │ │ ├── index.ts │ │ ├── speech-recognition.d.ts │ │ ├── tailwind.d.ts │ │ └── tools.ts │ ├── vite.config.ts │ ├── worker.ts │ └── wrangler.jsonc └── server │ ├── drizzle.config.ts │ ├── eslint.config.ts │ ├── package.json │ ├── src │ ├── ctx.ts │ ├── db │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 0000_fine_steel_serpent.sql │ │ │ ├── 0001_greedy_darkhawk.sql │ │ │ ├── 0002_flimsy_nightshade.sql │ │ │ ├── 0003_purple_kylun.sql │ │ │ ├── 0004_quiet_grey_gargoyle.sql │ │ │ ├── 0005_mature_lady_deathstrike.sql │ │ │ ├── 0006_small_unicorn.sql │ │ │ ├── 0007_tense_wrecking_crew.sql │ │ │ ├── 0008_freezing_hydra.sql │ │ │ ├── 0009_boring_big_bertha.sql │ │ │ ├── 0010_dry_hemingway.sql │ │ │ ├── 0011_huge_newton_destine.sql │ │ │ ├── 0012_even_johnny_storm.sql │ │ │ ├── 0013_calm_timeslip.sql │ │ │ ├── 0014_cuddly_energizer.sql │ │ │ ├── 0015_minor_mister_sinister.sql │ │ │ ├── 0016_neat_ogun.sql │ │ │ ├── 0017_bouncy_shotgun.sql │ │ │ ├── 0018_far_lady_mastermind.sql │ │ │ ├── 0019_mean_war_machine.sql │ │ │ ├── 0020_bright_gladiator.sql │ │ │ ├── 0021_outgoing_mariko_yashida.sql │ │ │ ├── 0022_round_violations.sql │ │ │ ├── 0023_narrow_maria_hill.sql │ │ │ ├── 0024_familiar_wiccan.sql │ │ │ ├── 0025_far_echo.sql │ │ │ ├── 0025_nervous_paper_doll.sql │ │ │ ├── 0026_smooth_norrin_radd.sql │ │ │ ├── 0027_vengeful_golden_guardian.sql │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ ├── 0001_snapshot.json │ │ │ │ ├── 0002_snapshot.json │ │ │ │ ├── 0003_snapshot.json │ │ │ │ ├── 0004_snapshot.json │ │ │ │ ├── 0005_snapshot.json │ │ │ │ ├── 0006_snapshot.json │ │ │ │ ├── 0007_snapshot.json │ │ │ │ ├── 0008_snapshot.json │ │ │ │ ├── 0009_snapshot.json │ │ │ │ ├── 0010_snapshot.json │ │ │ │ ├── 0011_snapshot.json │ │ │ │ ├── 0012_snapshot.json │ │ │ │ ├── 0013_snapshot.json │ │ │ │ ├── 0014_snapshot.json │ │ │ │ ├── 0015_snapshot.json │ │ │ │ ├── 0016_snapshot.json │ │ │ │ ├── 0017_snapshot.json │ │ │ │ ├── 0018_snapshot.json │ │ │ │ ├── 0019_snapshot.json │ │ │ │ ├── 0020_snapshot.json │ │ │ │ ├── 0021_snapshot.json │ │ │ │ ├── 0022_snapshot.json │ │ │ │ ├── 0023_snapshot.json │ │ │ │ ├── 0024_snapshot.json │ │ │ │ ├── 0025_snapshot.json │ │ │ │ ├── 0026_snapshot.json │ │ │ │ ├── 0027_snapshot.json │ │ │ │ └── _journal.json │ │ └── schema.ts │ ├── lib │ │ ├── auth-providers.ts │ │ ├── auth.ts │ │ ├── brain.fallback.prompts.ts │ │ ├── brain.ts │ │ ├── cookies.ts │ │ ├── driver │ │ │ ├── google-label-color-map.ts │ │ │ ├── google.ts │ │ │ ├── index.ts │ │ │ ├── microsoft.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── email-utils.ts │ │ ├── filter.ts │ │ ├── notes-manager.ts │ │ ├── party.ts │ │ ├── prompts.ts │ │ ├── sanitize-tip-tap-html.ts │ │ ├── schemas.ts │ │ ├── server-utils.ts │ │ ├── services.ts │ │ ├── shortcuts.ts │ │ ├── timezones.ts │ │ └── utils.ts │ ├── main.ts │ ├── overrides.d.ts │ ├── routes │ │ ├── agent │ │ │ ├── shared.ts │ │ │ ├── tools.ts │ │ │ └── utils.ts │ │ ├── autumn.ts │ │ └── chat.ts │ ├── services │ │ └── writing-style-service.ts │ ├── trpc │ │ ├── index.ts │ │ ├── routes │ │ │ ├── ai │ │ │ │ ├── compose.ts │ │ │ │ ├── index.ts │ │ │ │ └── search.ts │ │ │ ├── brain.ts │ │ │ ├── connections.ts │ │ │ ├── cookies.ts │ │ │ ├── drafts.ts │ │ │ ├── label.ts │ │ │ ├── mail.ts │ │ │ ├── notes.ts │ │ │ ├── settings.ts │ │ │ ├── shortcut.ts │ │ │ └── user.ts │ │ └── trpc.ts │ └── types.ts │ ├── tsconfig.json │ └── wrangler.jsonc ├── crowdin.yml ├── docker-compose.db.yaml ├── docker-compose.prod.yaml ├── docker ├── app │ └── Dockerfile └── db │ └── Dockerfile ├── eslint.config.mjs ├── i18n.json ├── i18n.lock ├── package.json ├── packages ├── cli │ ├── package.json │ ├── src │ │ ├── cli.ts │ │ ├── commands │ │ │ ├── fix-env.ts │ │ │ ├── index.ts │ │ │ ├── reinstall-node-modules.ts │ │ │ └── sync.ts │ │ └── utils.ts │ └── tsconfig.json ├── eslint-config │ ├── config.ts │ ├── package.json │ └── tsconfig.json ├── tailwind-config │ ├── package.json │ ├── tailwind.config.ts │ └── tsconfig.json └── tsconfig │ ├── base.json │ └── package.json ├── patches └── novel.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── better-auth.png ├── coderabbit.png ├── drizzle-orm.png └── vercel.png ├── scripts ├── README.md ├── docker │ ├── entrypoint.sh │ └── replace-placeholder.sh ├── package.json ├── run.ts ├── seed-style │ ├── seeder.ts │ └── styles │ │ ├── concise_emails.json │ │ ├── friendly_emails.json │ │ ├── genz_emails.json │ │ ├── persuasive_emails.json │ │ └── professional_emails.json ├── send-emails │ └── index.ts └── tsconfig.json ├── tsconfig.json └── turbo.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu as the base image 2 | FROM ubuntu:latest 3 | 4 | # Avoid prompts from apt 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | # Install basic dependencies 8 | RUN apt-get update && apt-get install -y \ 9 | git \ 10 | curl \ 11 | wget \ 12 | build-essential \ 13 | pkg-config \ 14 | zip \ 15 | unzip \ 16 | zsh \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | # Create workspace directory 20 | RUN mkdir -p /workspaces 21 | 22 | # Set working directory 23 | WORKDIR /workspaces 24 | 25 | # Install zsh 26 | RUN apt-get update && apt-get install -y zsh 27 | 28 | # install Oh My Zsh 29 | RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended 30 | 31 | # Bun and pnpm installation 32 | RUN curl -fsSL https://bun.sh/install | bash 33 | RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash - 34 | 35 | # Register Bun in bashrc and zshrc 36 | RUN echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc 37 | RUN echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc 38 | 39 | # Install NVM and Node.js 40 | ENV NVM_DIR=/root/.nvm 41 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash 42 | 43 | # Install Node.js and set it up using a shell 44 | RUN . $NVM_DIR/nvm.sh && \ 45 | nvm install --lts && \ 46 | nvm use --lts && \ 47 | nvm alias default 'lts/*' && \ 48 | npm install -g bun 49 | 50 | # Register Nvm in bashrc and zshrc 51 | RUN echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.bashrc 52 | RUN echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc 53 | RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.bashrc 54 | RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.zshrc 55 | 56 | # Switch back to dialog for any ad-hoc use of apt-get 57 | ENV DEBIAN_FRONTEND=dialog -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose 3 | { 4 | "name": "0.email Dev Container", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": "." 8 | }, 9 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 10 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 11 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 12 | 13 | // Features to add to the dev container 14 | "features": { 15 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 16 | }, 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | "forwardPorts": [], 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "ms-azuretools.vscode-docker", 26 | "eamodio.gitlens", 27 | "streetsidesoftware.code-spell-checker" 28 | ], 29 | "settings": {} 30 | } 31 | }, 32 | 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | "postCreateCommand": "zsh" 35 | } 36 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /apps/*/node_modules 4 | /packages/*/node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | bun-debug.log* 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # ide 44 | .idea 45 | .vscode 46 | .turbo 47 | i18n.cache 48 | apps/mail/scripts.ts 49 | .gitignore 50 | .husky 51 | .github 52 | .devcontainer 53 | .env.example 54 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_PUBLIC_APP_URL=http://localhost:3000 2 | VITE_PUBLIC_BACKEND_URL=http://localhost:8787 3 | 4 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/zerodotemail" 5 | 6 | # Change this to a random string, use `openssl rand -hex 32` to generate a 32 character string 7 | BETTER_AUTH_SECRET=my-better-auth-secret 8 | BETTER_AUTH_URL=http://localhost:3000 9 | 10 | COOKIE_DOMAIN="localhost" 11 | 12 | # Change to your project's client ID and secret, these work with localhost:8787 13 | GOOGLE_CLIENT_ID= 14 | GOOGLE_CLIENT_SECRET= 15 | 16 | # Upstash/Local Redis Instance 17 | REDIS_URL="http://localhost:8079" 18 | REDIS_TOKEN="upstash-local-token" 19 | 20 | # Resend API Key 21 | RESEND_API_KEY= 22 | 23 | # OpenAI API Key 24 | OPENAI_API_KEY= 25 | PERPLEXITY_API_KEY= 26 | 27 | #AI PROMPT 28 | AI_SYSTEM_PROMPT="" 29 | 30 | NODE_ENV="development" 31 | 32 | AUTUMN_SECRET_KEY= 33 | 34 | TWILIO_ACCOUNT_SID= 35 | TWILIO_AUTH_TOKEN= 36 | TWILIO_PHONE_NUMBER= -------------------------------------------------------------------------------- /.github/CODEOWNERS.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mail-0/Zero/13bae8fdcb0324339a08b911c2c55b439ff39fb6/.github/CODEOWNERS.md -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mail-0/Zero/13bae8fdcb0324339a08b911c2c55b439ff39fb6/.github/CODE_OF_CONDUCT.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nizzyabi] 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | autofix: 12 | timeout-minutes: 10 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Code 🛎 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup pnpm 🌟 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Setup Node 📦 22 | uses: actions/setup-node@v4 23 | 24 | - name: Install dependencies 📦 25 | run: pnpm install 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-prod-command.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Production Command 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | deploy-to-production: 14 | runs-on: ubuntu-latest 15 | name: Merge Staging to Production 16 | if: github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'production-deploy') && startsWith(github.event.comment.body, '/deploy') && github.event.comment.author_association == 'MEMBER' 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Rebase the main branch on staging 24 | id: rebase 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | git fetch origin staging 29 | git checkout main 30 | git rebase staging 31 | echo "rebase_status=$?" >> $GITHUB_OUTPUT 32 | 33 | - name: Error if rebase was not successful 34 | if: ${{ steps.rebase.outputs.rebase_status != 0 }} 35 | uses: mshick/add-pr-comment@v2 36 | with: 37 | message: | 38 | Failed to rebase staging on main, please rebase manually and run the command again. 39 | 40 | - name: Push changes if rebase was successful 41 | if: ${{ steps.rebase.outputs.rebase_status == 0 }} 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: git push --force-with-lease origin main 45 | -------------------------------------------------------------------------------- /.github/workflows/lingo-dev.yml: -------------------------------------------------------------------------------- 1 | name: lingo-dev 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | workflow_dispatch: 8 | inputs: 9 | skip_localization: 10 | description: 'Skip Lingo.dev step' 11 | type: 'boolean' 12 | default: false 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | main: 24 | timeout-minutes: 15 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout Code 🛎 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup pnpm 🌟 31 | uses: pnpm/action-setup@v4 32 | 33 | - name: Setup Node 📦 34 | uses: actions/setup-node@v4 35 | 36 | - name: Install dependencies 📦 37 | run: pnpm install 38 | 39 | - name: Run Lingo.dev Localization 🌐 40 | if: ${{ !inputs.skip_localization }} 41 | uses: lingodotdev/lingo.dev@main 42 | env: 43 | GH_TOKEN: ${{ github.token }} 44 | with: 45 | api-key: ${{ secrets.LINGODOTDEV_API_KEY }} 46 | pull-request: true 47 | -------------------------------------------------------------------------------- /.github/workflows/sync-production.yml: -------------------------------------------------------------------------------- 1 | name: Sync Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | 12 | jobs: 13 | sync-branches: 14 | runs-on: ubuntu-latest 15 | name: Syncing branches 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | 26 | - name: Opening pull request 27 | id: pull 28 | uses: JDTX0/branch-sync@v1.5.1 29 | with: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | FROM_BRANCH: 'staging' 32 | TO_BRANCH: 'main' 33 | PULL_REQUEST_TITLE: 'Deploy to production (Automated)' 34 | CONTENT_COMPARISON: true 35 | PULL_REQUEST_BODY: | 36 | This is an automated pull request to deploy the staging branch to production. 37 | Please review the pull request and comment `/deploy` to merge this PR and deploy to production. 38 | LABELS: '["production-deploy"]' 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | .next 18 | out 19 | 20 | # production 21 | build 22 | dist 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | bun-debug.log* 34 | # env files (can opt-in for committing if needed) 35 | .env 36 | .dev.vars 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | 45 | # ide 46 | .idea 47 | .vscode 48 | .turbo 49 | i18n.cache 50 | apps/mail/scripts.ts 51 | 52 | .wrangler 53 | worker-configuration.d.ts 54 | 55 | .dev.vars.* 56 | .react-router 57 | 58 | # devcontainer 59 | .pnpm-store 60 | tsx-0/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # don't show warnings if package versions don't match 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | save-exact=true 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | bun.lockb 2 | node_modules 3 | .next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "plugins": ["prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zero Email 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MCP.md: -------------------------------------------------------------------------------- 1 | ### Zero MCP 2 | 3 | ## Capabilties 4 | 5 | Zero MCP provides the following capabilities: 6 | 7 | ### Email Management 8 | 9 | - Get email threads by ID 10 | - List emails in specific folders 11 | - Create and send new emails 12 | - Create email drafts 13 | - Send existing drafts 14 | - Delete emails 15 | - Mark emails as read/unread 16 | - Modify email labels 17 | - Bulk delete emails 18 | - Bulk archive emails 19 | 20 | ### Label Management 21 | 22 | - Get all user labels 23 | - Create new custom labels with custom colors 24 | - Delete existing labels 25 | 26 | ### AI-Powered Features 27 | 28 | - Compose emails with AI assistance 29 | - Ask questions about mailbox content 30 | - Ask questions about specific email threads 31 | - Web search using Perplexity AI 32 | 33 | ### Search and Organization 34 | 35 | - Search emails with custom queries 36 | - Filter emails by labels 37 | - Manage email organization through labels 38 | - Archive and trash management 39 | 40 | ## How to use? 41 | 42 | You can connecto ZeroMCP using two methods: 43 | 44 | 1. Better Auth session token 45 | 2. OAuth (Coming soon) 46 | 47 | ## Better Auth session token 48 | 49 | Copy the session cookie from your browser cookies and place it into the Authorization header. You can copy the entire cookie field used in Zero webapp and it will work. Or you can use the format: `better-auth-{env}.session_token={value}`. 50 | Replace `env` with `dev` for local development, `value` is your session token. 51 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # 0.email Roadmap 🛤️ 2 | 3 | This document outlines the development roadmap for 0.email. Our vision is to create a powerful, user-friendly, and privacy-focused email experience. 4 | 5 | ## Current Development Focus 6 | 7 | ### 1. Core Email Connectivity 8 | 9 | - Connect main email providers 10 | - Support for multiple email accounts 11 | - Unified inbox experience 12 | 13 | ### 2. Email Usage Improvements 14 | 15 | - AI-powered email assistance 16 | - Advanced drag-and-drop tools 17 | - Customizable keyboard shortcuts 18 | - Performance optimization 19 | - Enhanced search capabilities 20 | - Deep customization options 21 | 22 | ### 3. Infrastructure 23 | 24 | - Domain management 25 | - Optimized email client 26 | - Self-hosting capabilities 27 | 28 | ## Development Priorities 29 | 30 | 1. Building a robust foundation for email management 31 | 2. Implementing user-requested features 32 | 3. Ensuring seamless integration with existing email providers 33 | 4. Maintaining high performance and reliability 34 | 35 | ## Contributing 36 | 37 | We welcome community input and contributions to help shape these features and priorities. If you have suggestions or would like to contribute, please: 38 | 39 | 1. Open an issue to discuss new feature ideas 40 | 2. Submit pull requests for improvements 41 | 3. Join discussions in existing issues 42 | 43 | ## Timeline 44 | 45 | This roadmap is a living document and will be updated as development progresses and priorities evolve based on community feedback and technological advances. 46 | 47 | --- 48 | 49 | Last updated: Feb 2025 50 | -------------------------------------------------------------------------------- /apps/mail/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | output.json 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | bun-debug.log* 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # ide 44 | .idea 45 | 46 | # Sentry Config File 47 | .env.sentry-build-plugin 48 | 49 | # Sentry Config File 50 | .sentryclirc 51 | -------------------------------------------------------------------------------- /apps/mail/app/(auth)/login/error-message.tsx: -------------------------------------------------------------------------------- 1 | import { TriangleAlert } from 'lucide-react'; 2 | import { useTranslations } from 'use-intl'; 3 | import { useQueryState } from 'nuqs'; 4 | import { useEffect } from 'react'; 5 | import { toast } from 'sonner'; 6 | 7 | const errorMessages = ['required_scopes_missing'] as const; 8 | 9 | const errorToasts = ['early_access_required', 'unauthorized'] as const; 10 | 11 | type ErrorToast = (typeof errorToasts)[number]; 12 | type ErrorMessage = (typeof errorMessages)[number]; 13 | 14 | const isErrorToast = (error: string): error is (typeof errorToasts)[number] => 15 | errorToasts.includes(error as ErrorToast); 16 | 17 | const isErrorMessage = (error: string): error is (typeof errorMessages)[number] => 18 | errorMessages.includes(error as ErrorMessage); 19 | 20 | const ErrorMessage = () => { 21 | const [error] = useQueryState('error'); 22 | const t = useTranslations(); 23 | 24 | useEffect(() => { 25 | if (error && isErrorToast(error)) { 26 | toast.error(t(`errorMessages.${error}`)); 27 | } 28 | }); 29 | 30 | if (!error || !isErrorMessage(error)) { 31 | return null; 32 | } 33 | 34 | return ( 35 |
36 |
37 | 38 |

39 | {t(`errorMessages.${error}`)} 40 |

41 |
42 |
43 | ); 44 | }; 45 | 46 | export default ErrorMessage; 47 | -------------------------------------------------------------------------------- /apps/mail/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { authProviders, customProviders, isProviderEnabled } from '@zero/server/auth-providers'; 2 | import { LoginClient } from './login-client'; 3 | import { useLoaderData } from 'react-router'; 4 | import { env } from 'cloudflare:workers'; 5 | 6 | export function loader() { 7 | const isProd = !import.meta.env.DEV; 8 | 9 | const authProviderStatus = authProviders(env as unknown as Record).map( 10 | (provider) => { 11 | const envVarStatus = 12 | provider.envVarInfo?.map((envVar) => { 13 | const envVarName = envVar.name as keyof typeof env; 14 | return { 15 | name: envVar.name, 16 | set: !!env[envVarName], 17 | source: envVar.source, 18 | defaultValue: envVar.defaultValue, 19 | }; 20 | }) || []; 21 | 22 | return { 23 | id: provider.id, 24 | name: provider.name, 25 | enabled: isProviderEnabled(provider, env as unknown as Record), 26 | required: provider.required, 27 | envVarInfo: provider.envVarInfo, 28 | envVarStatus, 29 | }; 30 | }, 31 | ); 32 | 33 | const customProviderStatus = customProviders.map((provider) => { 34 | return { 35 | id: provider.id, 36 | name: provider.name, 37 | enabled: true, 38 | isCustom: provider.isCustom, 39 | customRedirectPath: provider.customRedirectPath, 40 | envVarStatus: [], 41 | }; 42 | }); 43 | 44 | const allProviders = [...customProviderStatus, ...authProviderStatus]; 45 | 46 | return { 47 | allProviders, 48 | isProd, 49 | }; 50 | } 51 | 52 | export default function LoginPage() { 53 | const { allProviders, isProd } = useLoaderData(); 54 | 55 | return ( 56 |
57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/mail/app/(full-width)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router'; 2 | 3 | export default function FullWidthLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper'; 2 | import { CommandPaletteProvider } from '@/components/context/command-palette-context'; 3 | import { Outlet } from 'react-router'; 4 | 5 | export default function Layout() { 6 | return ( 7 | 8 | 9 |
10 | 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/compose/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogDescription, 5 | DialogTitle, 6 | DialogTrigger, 7 | } from '@/components/ui/dialog'; 8 | import { CreateEmail } from '@/components/create/create-email'; 9 | import { authProxy } from '@/lib/auth-proxy'; 10 | import { useLoaderData } from 'react-router'; 11 | import type { Route } from './+types/page'; 12 | 13 | export async function loader({ request }: Route.LoaderArgs) { 14 | const session = await authProxy.api.getSession({ headers: request.headers }); 15 | if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`); 16 | const url = new URL(request.url); 17 | if (url.searchParams.get('to')?.startsWith('mailto:')) { 18 | return Response.redirect( 19 | `${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose/handle-mailto?mailto=${encodeURIComponent(url.searchParams.get('to') ?? '')}`, 20 | ); 21 | } 22 | 23 | return Object.fromEntries(url.searchParams.entries()) as { 24 | to?: string; 25 | subject?: string; 26 | body?: string; 27 | draftId?: string; 28 | cc?: string; 29 | bcc?: string; 30 | }; 31 | } 32 | 33 | export default function ComposePage() { 34 | const params = useLoaderData(); 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { authProxy } from '@/lib/auth-proxy'; 2 | import type { Route } from './+types/page'; 3 | 4 | export async function loader({ request }: Route.LoaderArgs) { 5 | const session = await authProxy.api.getSession({ headers: request.headers }); 6 | if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`); 7 | 8 | const url = new URL(request.url); 9 | const params = Object.fromEntries(url.searchParams.entries()) as { 10 | to?: string; 11 | subject?: string; 12 | body?: string; 13 | }; 14 | const toParam = params.to || 'someone@someone.com'; 15 | return Response.redirect( 16 | `${import.meta.env.VITE_PUBLIC_APP_URL}/mail/inbox?isComposeOpen=true&to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`, 17 | ); 18 | } 19 | 20 | // export async function generateMetadata({ searchParams }: any) { 21 | // // Need to await searchParams in Next.js 15+ 22 | // const params = await searchParams; 23 | 24 | // const toParam = params.to || 'someone'; 25 | 26 | // // Create common metadata properties 27 | // const title = `Email ${toParam} on Zero`; 28 | // const description = 'Zero - The future of email is here'; 29 | // const imageUrl = `/og-api/create?to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`; 30 | 31 | // // Create metadata object 32 | // return { 33 | // title, 34 | // description, 35 | // openGraph: { 36 | // title, 37 | // description, 38 | // images: [imageUrl], 39 | // }, 40 | // twitter: { 41 | // card: 'summary_large_image', 42 | // title, 43 | // description, 44 | // images: [imageUrl], 45 | // }, 46 | // }; 47 | // } 48 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper'; 2 | import { OnboardingWrapper } from '@/components/onboarding'; 3 | import { NotificationProvider } from '@/components/party'; 4 | import { AppSidebar } from '@/components/ui/app-sidebar'; 5 | import { Outlet, useLoaderData } from 'react-router'; 6 | import type { Route } from './+types/layout'; 7 | 8 | export async function loader({ request }: Route.LoaderArgs) { 9 | return { 10 | headers: Object.fromEntries(request.headers.entries()), 11 | }; 12 | } 13 | 14 | export default function MailLayout() { 15 | const { headers } = useLoaderData(); 16 | return ( 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router'; 2 | 3 | export function loader() { 4 | throw redirect(`/mail/inbox`); 5 | } 6 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/under-construction/[path]/back-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { ArrowLeft } from 'lucide-react'; 3 | 4 | export default function BackButton() { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/mail/under-construction/[path]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarToggle } from '@/components/ui/sidebar-toggle'; 2 | import { Construction } from 'lucide-react'; 3 | import BackButton from './back-button'; 4 | import { use } from 'react'; 5 | 6 | interface UnderConstructionProps { 7 | params: Promise<{ 8 | path: string; 9 | }>; 10 | } 11 | 12 | export default function UnderConstruction({ params }: UnderConstructionProps) { 13 | const resolvedParams = use(params); 14 | // Decode the path parameter 15 | const decodedPath = decodeURIComponent(resolvedParams.path); 16 | 17 | return ( 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |

Under Construction

27 |

28 | The {decodedPath} page is currently under construction. 29 | Check back soon! 30 |

31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/settings/[...settings]/page.tsx: -------------------------------------------------------------------------------- 1 | import NotificationsPage from '../notifications/page'; 2 | import ConnectionsPage from '../connections/page'; 3 | import AppearancePage from '../appearance/page'; 4 | import ShortcutsPage from '../shortcuts/page'; 5 | import SecurityPage from '../security/page'; 6 | import { useTranslations } from 'use-intl'; 7 | import GeneralPage from '../general/page'; 8 | import { useParams } from 'react-router'; 9 | import LabelsPage from '../labels/page'; 10 | 11 | const settingsPages: Record = { 12 | general: GeneralPage, 13 | connections: ConnectionsPage, 14 | security: SecurityPage, 15 | appearance: AppearancePage, 16 | shortcuts: ShortcutsPage, 17 | notifications: NotificationsPage, 18 | labels: LabelsPage, 19 | }; 20 | 21 | export default function SettingsPage() { 22 | const params = useParams(); 23 | const section = params.settings?.[0] || 'general'; 24 | const t = useTranslations(); 25 | 26 | const SettingsComponent = settingsPages[section]; 27 | 28 | if (!SettingsComponent) { 29 | return
{t('pages.error.settingsNotFound')}
; 30 | } 31 | 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/settings/labels/colors.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = [ 2 | '#FFFFFF', // White 3 | '#000000', // Black 4 | '#0000FF', // Blue 5 | '#FF0000', // Red 6 | '#FFFF00', // Yellow 7 | '#FFA500', // Orange 8 | '#800080', // Purple 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsLayoutContent } from '@/components/ui/settings-content'; 2 | import { Outlet } from 'react-router'; 3 | 4 | export default function SettingsLayout() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/mail/app/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router'; 2 | 3 | export function loader() { 4 | throw redirect(`/settings/general`); 5 | } 6 | -------------------------------------------------------------------------------- /apps/mail/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import '../instrument'; 2 | 3 | import { startTransition, StrictMode } from 'react'; 4 | import { HydratedRouter } from 'react-router/dom'; 5 | import { hydrateRoot } from 'react-dom/client'; 6 | import * as Sentry from '@sentry/react'; 7 | 8 | startTransition(() => { 9 | hydrateRoot( 10 | document, 11 | 12 | 13 | , 14 | { 15 | onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { 16 | console.warn('Uncaught error', error, errorInfo.componentStack); 17 | }), 18 | // Callback called when React catches an error in an ErrorBoundary. 19 | onCaughtError: Sentry.reactErrorHandler(), 20 | // Callback called when React automatically recovers from errors. 21 | onRecoverableError: Sentry.reactErrorHandler(), 22 | }, 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/mail/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from 'react-router'; 2 | import { renderToReadableStream } from 'react-dom/server'; 3 | import { ServerRouter } from 'react-router'; 4 | import { isbot } from 'isbot'; 5 | 6 | export default async function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | routerContext: EntryContext, 11 | _loadContext: AppLoadContext, 12 | ) { 13 | let shellRendered = false; 14 | const userAgent = request.headers.get('user-agent'); 15 | 16 | const body = await renderToReadableStream( 17 | , 18 | { 19 | onError(error: unknown) { 20 | responseStatusCode = 500; 21 | // Log streaming rendering errors from inside the shell. Don't log 22 | // errors encountered during initial shell rendering since they'll 23 | // reject and get logged in handleDocumentRequest. 24 | if (shellRendered) { 25 | console.error(error); 26 | } 27 | }, 28 | }, 29 | ); 30 | shellRendered = true; 31 | 32 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding 33 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation 34 | if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set('Content-Type', 'text/html'); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /apps/mail/app/home/page.tsx: -------------------------------------------------------------------------------- 1 | import HomeContent from '@/components/home/HomeContent'; 2 | 3 | export default function HomeRoute() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/mail/app/meta-files/manifest.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return Response.json({ 3 | name: 'Zero', 4 | short_name: '0', 5 | description: 'Zero - the first open source email app that puts your privacy and safety first.', 6 | scope: '/', 7 | start_url: '/mail/inbox', 8 | display: 'standalone', 9 | background_color: '#000', 10 | theme_color: '#fff', 11 | icons: [ 12 | { 13 | src: '/icons-pwa/icon-512.png', 14 | sizes: '512x512', 15 | type: 'image/png', 16 | purpose: 'any maskable', 17 | }, 18 | { 19 | src: '/icons-pwa/icon-192.png', 20 | sizes: '192x192', 21 | type: 'image/png', 22 | purpose: 'any maskable', 23 | }, 24 | { 25 | src: '/icons-pwa/icon-180.png', 26 | sizes: '180x180', 27 | type: 'image/png', 28 | }, 29 | ], 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /apps/mail/app/meta-files/microsoft-identity-association.json.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return Response.json({ 3 | associatedApplications: [ 4 | { 5 | applicationId: 'ecf043a0-41bb-4c89-bd31-7e3f272f8e3c', 6 | }, 7 | ], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /apps/mail/app/meta-files/microsoft-identity-association.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return Response.json({ 3 | associatedApplications: [ 4 | { 5 | applicationId: '80b11343-e52c-4969-81e8-faebfed78a67', 6 | }, 7 | ], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /apps/mail/app/meta-files/not-found.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | throw new Response('Not Found', { status: 404 }); 3 | } 4 | 5 | export default function NotFound() { 6 | return null; 7 | } 8 | -------------------------------------------------------------------------------- /apps/mail/app/page.tsx: -------------------------------------------------------------------------------- 1 | import HomeContent from '@/components/home/HomeContent'; 2 | import { authProxy } from '@/lib/auth-proxy'; 3 | import type { Route } from './+types/page'; 4 | import { redirect } from 'react-router'; 5 | 6 | export async function loader({ request }: Route.LoaderArgs) { 7 | const session = await authProxy.api.getSession({ headers: request.headers }); 8 | if (session?.user.id) throw redirect('/mail/inbox'); 9 | return null; 10 | } 11 | 12 | export default function Home() { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /apps/mail/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/mail/components/ai-toggle-button.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip'; 2 | import { useAISidebar } from './ui/ai-sidebar'; 3 | import { Button } from './ui/button'; 4 | 5 | // AI Toggle Button Component 6 | const AIToggleButton = () => { 7 | const { toggleOpen: toggleAISidebar, open: isSidebarOpen } = useAISidebar(); 8 | 9 | return ( 10 | !isSidebarOpen && ( 11 |
12 | 13 | 14 | 42 | 43 | Toggle AI Assistant 44 | 45 |
46 | ) 47 | ); 48 | }; 49 | 50 | export default AIToggleButton; 51 | -------------------------------------------------------------------------------- /apps/mail/components/cookies/cookie-trigger.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Cookie } from 'lucide-react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface CookieTriggerProps { 6 | variant?: 'link' | 'button' | 'prominent' | 'icon'; 7 | className?: string; 8 | children?: React.ReactNode; 9 | onClick?: () => void; 10 | } 11 | 12 | export function CookieTrigger({ 13 | variant = 'button', 14 | className, 15 | children, 16 | onClick, 17 | }: CookieTriggerProps) { 18 | const variants = { 19 | link: 'text-primary underline-offset-4 hover:underline p-0 h-auto', 20 | button: 'bg-primary text-primary-foreground hover:bg-primary/90', 21 | prominent: 'b text-white font-medium px-6', 22 | icon: 'h-9 w-9 rounded-full border-zinc-800 dark:text-black text-white shadow-lg bg-black dark:bg-white relative top-2 left-2', 23 | }; 24 | 25 | return ( 26 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/mail/components/create/ai-textarea.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import React from 'react'; 3 | 4 | interface TextareaProps extends React.TextareaHTMLAttributes {} 5 | 6 | const AITextarea = React.forwardRef( 7 | ({ className, ...props }, ref) => { 8 | return ( 9 | 19 | ); 20 | }, 21 | ); 22 | 23 | AITextarea.displayName = 'AITextarea'; 24 | 25 | export { AITextarea }; 26 | -------------------------------------------------------------------------------- /apps/mail/components/create/editor-menu.tsx: -------------------------------------------------------------------------------- 1 | import { EditorBubble, useEditor } from 'novel'; 2 | import { removeAIHighlight } from 'novel'; 3 | 4 | import { type ReactNode, useEffect } from 'react'; 5 | 6 | interface EditorMenuProps { 7 | children: ReactNode; 8 | open: boolean; 9 | onOpenChange: (open: boolean) => void; 10 | } 11 | 12 | export default function EditorMenu({ children, open, onOpenChange }: EditorMenuProps) { 13 | const { editor } = useEditor(); 14 | 15 | useEffect(() => { 16 | if (!editor) return; 17 | if (!open) removeAIHighlight(editor); 18 | }, [open]); 19 | 20 | return ( 21 | { 25 | onOpenChange(false); 26 | editor?.chain().unsetHighlight().run(); 27 | }, 28 | }} 29 | className="border-muted bg-background flex w-fit max-w-[90vw] overflow-hidden rounded-md border shadow-xl" 30 | > 31 | {!open && children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/mail/components/create/image-upload.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mail-0/Zero/13bae8fdcb0324339a08b911c2c55b439ff39fb6/apps/mail/components/create/image-upload.ts -------------------------------------------------------------------------------- /apps/mail/components/create/selectors/math-selector.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { SigmaIcon } from 'lucide-react'; 3 | import { useEditor } from 'novel'; 4 | import { cn } from '@/lib/utils'; 5 | 6 | export const MathSelector = () => { 7 | const { editor } = useEditor(); 8 | 9 | if (!editor) return null; 10 | 11 | return ( 12 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/mail/components/create/selectors/text-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { MessageSquare, FileText, Edit } from 'lucide-react'; 2 | import type { SelectorItem } from './node-selector'; 3 | import { EditorBubbleItem, useEditor } from 'novel'; 4 | import { Button } from '@/components/ui/button'; 5 | import { cn } from '@/lib/utils'; 6 | 7 | export const TextButtons = () => { 8 | const { editor } = useEditor(); 9 | if (!editor) return null; 10 | 11 | // Define AI action handlers 12 | const handleChatWithAI = () => { 13 | // Get selected text 14 | const selection = editor.state.selection; 15 | const selectedText = selection.empty 16 | ? '' 17 | : editor.state.doc.textBetween(selection.from, selection.to); 18 | 19 | console.log('Chat with AI about:', selectedText); 20 | // Implement chat with AI functionality 21 | }; 22 | 23 | const items = [ 24 | { 25 | name: 'chat-with-zero', 26 | label: 'Chat with Zero', 27 | action: handleChatWithAI, 28 | useImage: true, 29 | imageSrc: '/ai.svg', 30 | }, 31 | ]; 32 | 33 | return ( 34 |
35 | {items.map((item) => ( 36 | { 39 | item.action(); 40 | }} 41 | > 42 | 57 | 58 | ))} 59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /apps/mail/components/mail/nav-main.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'use-intl'; 2 | import React, { useState } from 'react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | const NavMain: React.FC = () => { 6 | const t = useTranslations(); 7 | const [state, setState] = useState<'collapsed' | 'expanded'>('collapsed'); 8 | 9 | const handleToggle = () => { 10 | setState((prevState) => (prevState === 'collapsed' ? 'expanded' : 'collapsed')); 11 | }; 12 | 13 | return
{/* Rest of the component code */}
; 14 | }; 15 | 16 | export default NavMain; 17 | -------------------------------------------------------------------------------- /apps/mail/components/mail/thread-subject.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; 2 | import { cn } from '@/lib/utils'; 3 | import { useRef } from 'react'; 4 | 5 | interface ThreadSubjectProps { 6 | subject?: string; 7 | } 8 | 9 | export default function ThreadSubject({ subject }: ThreadSubjectProps) { 10 | const textRef = useRef(null); 11 | const subjectContent = subject || '(no subject)'; 12 | 13 | return ( 14 |
15 | 22 | {subjectContent.trim()} 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/mail/components/mail/use-mail.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai'; 2 | 3 | export type Config = { 4 | selected: string | null; 5 | bulkSelected: string[]; 6 | replyComposerOpen: boolean; 7 | replyAllComposerOpen: boolean; 8 | forwardComposerOpen: boolean; 9 | showImages: boolean; 10 | }; 11 | 12 | const configAtom = atom({ 13 | selected: null, 14 | bulkSelected: [], 15 | replyComposerOpen: false, 16 | replyAllComposerOpen: false, 17 | forwardComposerOpen: false, 18 | showImages: false, 19 | }); 20 | 21 | export function useMail() { 22 | return useAtom(configAtom); 23 | } 24 | 25 | export const clearBulkSelectionAtom = atom(null, (get, set) => { 26 | const current = get(configAtom); 27 | set(configAtom, { ...current, bulkSelected: [] }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/mail/components/providers/editor-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | import { Editor } from '@tiptap/react'; 3 | 4 | interface EditorContextType { 5 | editor: Editor | null; 6 | setEditor: (editor: Editor | null) => void; 7 | } 8 | 9 | export const EditorContext = createContext({ 10 | editor: null, 11 | setEditor: () => {}, 12 | }); 13 | 14 | export function EditorProvider({ children }: { children: React.ReactNode }) { 15 | const [editor, setEditor] = useState(null); 16 | 17 | return {children}; 18 | } 19 | 20 | export function useEditor() { 21 | return useContext(EditorContext); 22 | } 23 | -------------------------------------------------------------------------------- /apps/mail/components/providers/hotkey-provider-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ThreadDisplayHotkeys } from '@/lib/hotkeys/thread-display-hotkeys'; 2 | import { NavigationHotkeys } from '@/lib/hotkeys/navigation-hotkeys'; 3 | import { MailListHotkeys } from '@/lib/hotkeys/mail-list-hotkeys'; 4 | import { ComposeHotkeys } from '@/lib/hotkeys/compose-hotkeys'; 5 | import { GlobalHotkeys } from '@/lib/hotkeys/global-hotkeys'; 6 | import { HotkeysProvider } from 'react-hotkeys-hook'; 7 | import React from 'react'; 8 | 9 | interface HotkeyProviderWrapperProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | export function HotkeyProviderWrapper({ children }: HotkeyProviderWrapperProps) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/mail/components/responsive-modal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogHeader, 5 | DialogDescription, 6 | DialogTitle, 7 | } from '@/components/ui/dialog'; 8 | import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; 9 | import { VisuallyHidden } from 'radix-ui'; 10 | import { type ReactElement } from 'react'; 11 | // import { useMedia } from 'react-use'; 12 | 13 | type ResponsiveModalProps = { 14 | children: React.ReactNode; 15 | open: boolean; 16 | onOpenChange: (open: boolean) => void; 17 | }; 18 | 19 | export default function ResponsiveModal({ 20 | children, 21 | open, 22 | onOpenChange, 23 | }: ResponsiveModalProps): ReactElement { 24 | const isDesktop = true; 25 | 26 | if (isDesktop) { 27 | return ( 28 | 29 | 30 | 31 | Title 32 | Modal content 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | ); 40 | } 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | Title 48 | 49 | 50 |
51 | {children} 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/mail/components/settings/settings-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 2 | import { PricingDialog } from '../ui/pricing-dialog'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface SettingsCardProps extends React.HTMLAttributes { 6 | title: string; 7 | description?: string; 8 | children: React.ReactNode; 9 | footer?: React.ReactNode; 10 | action?: React.ReactNode; 11 | } 12 | 13 | export function SettingsCard({ 14 | title, 15 | description, 16 | children, 17 | footer, 18 | action, 19 | className, 20 | }: SettingsCardProps) { 21 | return ( 22 | 28 | 29 |
30 | {title} 31 | {description && {description}} 32 |
33 | {action &&
{action}
} 34 |
35 | {children} 36 | {footer &&
{footer}
} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/mail/components/theme/mode-toggle.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mail-0/Zero/13bae8fdcb0324339a08b911c2c55b439ff39fb6/apps/mail/components/theme/mode-toggle.tsx -------------------------------------------------------------------------------- /apps/mail/components/theme/sidebar-theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | 3 | import { MoonIcon } from '../icons/animated/moon'; 4 | import { SunIcon } from '../icons/animated/sun'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | export function SidebarThemeSwitch() { 8 | const [isRendered, setIsRendered] = useState(false); 9 | const { theme, resolvedTheme, setTheme } = useTheme(); 10 | 11 | // Prevents hydration error 12 | useEffect(() => setIsRendered(true), []); 13 | 14 | async function handleThemeToggle() { 15 | const newTheme = theme === 'dark' ? 'light' : 'dark'; 16 | 17 | function update() { 18 | setTheme(newTheme); 19 | } 20 | 21 | if (document.startViewTransition && newTheme !== resolvedTheme) { 22 | document.documentElement.style.viewTransitionName = 'theme-transition'; 23 | await document.startViewTransition(update).finished; 24 | document.documentElement.style.viewTransitionName = ''; 25 | } else { 26 | update(); 27 | } 28 | } 29 | 30 | if (!isRendered) return null; 31 | 32 | return ( 33 |
34 | {theme === 'dark' ? : } 35 |

App Theme

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/mail/components/theme/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { MoonIcon } from '@/components/icons/animated/moon'; 2 | import { SunIcon } from '@/components/icons/animated/sun'; 3 | import { useEffect, useState } from 'react'; 4 | import { useTheme } from 'next-themes'; 5 | 6 | interface ThemeToggleProps { 7 | className?: string; 8 | showLabel?: boolean; 9 | } 10 | 11 | export function ThemeToggle({ className = '', showLabel = false }: ThemeToggleProps) { 12 | const [isRendered, setIsRendered] = useState(false); 13 | const { theme, resolvedTheme, setTheme } = useTheme(); 14 | 15 | useEffect(() => setIsRendered(true), []); 16 | 17 | async function handleThemeToggle() { 18 | const newTheme = theme === 'dark' ? 'light' : 'dark'; 19 | 20 | function update() { 21 | setTheme(newTheme); 22 | } 23 | 24 | if (document.startViewTransition && newTheme !== resolvedTheme) { 25 | document.documentElement.style.viewTransitionName = 'theme-transition'; 26 | await document.startViewTransition(update).finished; 27 | document.documentElement.style.viewTransitionName = ''; 28 | } else { 29 | update(); 30 | } 31 | } 32 | 33 | if (!isRendered) return null; 34 | 35 | return ( 36 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/mail/components/ui/animated-number.tsx: -------------------------------------------------------------------------------- 1 | import { motion, type SpringOptions, useSpring, useTransform } from 'motion/react'; 2 | import { useEffect } from 'react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | export type AnimatedNumberProps = { 6 | value: number; 7 | className?: string; 8 | springOptions?: SpringOptions; 9 | as?: React.ElementType; 10 | }; 11 | 12 | export function AnimatedNumber({ 13 | value, 14 | className, 15 | springOptions, 16 | as = 'span', 17 | }: AnimatedNumberProps) { 18 | const MotionComponent = motion.create(as); 19 | 20 | const spring = useSpring(value, springOptions); 21 | const display = useTransform(spring, (current) => Math.round(current).toLocaleString()); 22 | 23 | useEffect(() => { 24 | spring.set(value); 25 | }, [spring, value]); 26 | 27 | return {display}; 28 | } 29 | -------------------------------------------------------------------------------- /apps/mail/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar as AvatarPrimitive } from 'radix-ui'; 2 | import { cn } from '@/lib/utils'; 3 | import * as React from 'react'; 4 | 5 | const Avatar = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 14 | )); 15 | Avatar.displayName = AvatarPrimitive.Root.displayName; 16 | 17 | const AvatarImage = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 26 | )); 27 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 28 | 29 | const AvatarFallback = React.forwardRef< 30 | React.ElementRef, 31 | React.ComponentPropsWithoutRef 32 | >(({ className, ...props }, ref) => ( 33 | 38 | )); 39 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 40 | 41 | export { Avatar, AvatarImage, AvatarFallback }; 42 | -------------------------------------------------------------------------------- /apps/mail/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'border-transparent bg-primary text-primary-foreground ', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | important: 'border-0 text-amber-700 dark:text-amber-500 ', 18 | promotions: 'border-0 text-red-700 dark:text-red-500 ', 19 | personal: 'border-0 text-green-700 dark:text-green-500 ', 20 | updates: 'border-0 text-purple-700 dark:text-purple-500 ', 21 | forums: 'border-transparent text-blue-500 dark:text-blue-400', 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: 'default', 26 | }, 27 | }, 28 | ); 29 | 30 | export interface BadgeProps 31 | extends React.HTMLAttributes, 32 | VariantProps {} 33 | 34 | function Badge({ className, variant, ...props }: BadgeProps) { 35 | return
; 36 | } 37 | 38 | export { Badge, badgeVariants }; 39 | -------------------------------------------------------------------------------- /apps/mail/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as CheckboxPrimitive } from 'radix-ui'; 2 | import { Check } from 'lucide-react'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 20 | 21 | 22 | 23 | )); 24 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 25 | 26 | export { Checkbox }; 27 | -------------------------------------------------------------------------------- /apps/mail/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsible as CollapsiblePrimitive } from 'radix-ui'; 2 | 3 | const Collapsible = CollapsiblePrimitive.Root; 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 10 | -------------------------------------------------------------------------------- /apps/mail/components/ui/command-menu.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mail-0/Zero/13bae8fdcb0324339a08b911c2c55b439ff39fb6/apps/mail/components/ui/command-menu.tsx -------------------------------------------------------------------------------- /apps/mail/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /apps/mail/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import { Label as LabelPrimitive } from 'radix-ui'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & VariantProps 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | Label.displayName = LabelPrimitive.Root.displayName; 18 | 19 | export { Label }; 20 | -------------------------------------------------------------------------------- /apps/mail/components/ui/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import React from 'react'; 3 | 4 | interface PageHeaderProps extends React.HTMLAttributes {} 5 | 6 | const PageHeader = React.forwardRef( 7 | ({ className, children, ...props }, ref) => { 8 | return ( 9 |
14 | {children} 15 |
16 | ); 17 | }, 18 | ); 19 | PageHeader.displayName = 'PageHeader'; 20 | 21 | interface PageHeaderTitleProps extends React.HTMLAttributes {} 22 | 23 | const PageHeaderTitle = React.forwardRef( 24 | ({ className, children, ...props }, ref) => { 25 | return ( 26 |

27 | {children} 28 |

29 | ); 30 | }, 31 | ); 32 | PageHeaderTitle.displayName = 'PageHeaderTitle'; 33 | 34 | interface PageHeaderDescriptionProps extends React.HTMLAttributes {} 35 | 36 | const PageHeaderDescription = React.forwardRef( 37 | ({ className, children, ...props }, ref) => { 38 | return ( 39 |

40 | {children} 41 |

42 | ); 43 | }, 44 | ); 45 | PageHeaderDescription.displayName = 'PageHeaderDescription'; 46 | 47 | // Attach subcomponents to PageHeader 48 | // @ts-expect-error, fix types 49 | PageHeader.Title = PageHeaderTitle; 50 | // @ts-expect-error, fix types 51 | PageHeader.Description = PageHeaderDescription; 52 | 53 | export { PageHeader }; 54 | -------------------------------------------------------------------------------- /apps/mail/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import { Popover as PopoverPrimitive } from 'radix-ui'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverTrigger, PopoverContent }; 30 | -------------------------------------------------------------------------------- /apps/mail/components/ui/pricing-switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as SwitchPrimitives } from 'radix-ui'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const PricingSwitch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | PricingSwitch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { PricingSwitch }; 28 | -------------------------------------------------------------------------------- /apps/mail/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import { Progress as ProgressPrimitive } from 'radix-ui'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 15 | 19 | 20 | )); 21 | Progress.displayName = ProgressPrimitive.Root.displayName; 22 | 23 | export { Progress }; 24 | -------------------------------------------------------------------------------- /apps/mail/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'; 2 | import { Circle } from 'lucide-react'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const RadioGroup = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => { 11 | return ; 12 | }); 13 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 14 | 15 | const RadioGroupItem = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => { 19 | return ( 20 | 28 | 29 | 30 | 31 | 32 | ); 33 | }); 34 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 35 | 36 | export { RadioGroup, RadioGroupItem }; 37 | -------------------------------------------------------------------------------- /apps/mail/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import * as ResizablePrimitive from 'react-resizable-panels'; 2 | import { GripVertical } from 'lucide-react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const ResizablePanelGroup = ({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ); 15 | 16 | const ResizablePanel = ResizablePrimitive.Panel; 17 | 18 | const ResizableHandle = ({ 19 | withHandle, 20 | className, 21 | ...props 22 | }: React.ComponentProps & { 23 | withHandle?: boolean; 24 | }) => ( 25 | div]:rotate-90', 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {withHandle && ( 33 |
34 | 35 |
36 | )} 37 |
38 | ); 39 | 40 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 41 | -------------------------------------------------------------------------------- /apps/mail/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import { Separator as SeparatorPrimitive } from 'radix-ui'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( 10 | 21 | )); 22 | Separator.displayName = SeparatorPrimitive.Root.displayName; 23 | 24 | export { Separator }; 25 | -------------------------------------------------------------------------------- /apps/mail/components/ui/settings-content.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarToggle } from '@/components/ui/sidebar-toggle'; 2 | import { AppSidebar } from '@/components/ui/app-sidebar'; 3 | import { ScrollArea } from '@/components/ui/scroll-area'; 4 | 5 | export function SettingsLayoutContent({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 | 9 |
10 |
11 |
12 | 13 |
14 | 15 |
{children}
16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/mail/components/ui/sidebar-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'react'; 2 | 3 | import { type SidebarTrigger, useSidebar } from '@/components/ui/sidebar'; 4 | import { Button } from '@/components/ui/button'; 5 | import { PanelLeftOpen } from '../icons/icons'; 6 | import { cn } from '@/lib/utils'; 7 | 8 | export function SidebarToggle({ className }: ComponentProps) { 9 | const { toggleSidebar } = useSidebar(); 10 | 11 | return ( 12 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/mail/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
; 5 | } 6 | 7 | export { Skeleton }; 8 | -------------------------------------------------------------------------------- /apps/mail/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'motion/react'; 2 | 3 | interface SpinnerProps { 4 | size?: number; 5 | color?: string; 6 | } 7 | 8 | export const Spinner = ({ size = 24, color = 'currentColor' }: SpinnerProps) => { 9 | return ( 10 | 23 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /apps/mail/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as SwitchPrimitives } from 'radix-ui'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /apps/mail/components/ui/text-shimmer.tsx: -------------------------------------------------------------------------------- 1 | // Source: https://motion-primitives.com/docs/text-shimmer 2 | 3 | import React, { useMemo, type JSX } from 'react'; 4 | import { motion } from 'motion/react'; 5 | import { cn } from '@/lib/utils'; 6 | 7 | export type TextShimmerProps = { 8 | children: string; 9 | as?: React.ElementType; 10 | className?: string; 11 | duration?: number; 12 | spread?: number; 13 | }; 14 | 15 | export function TextShimmer({ 16 | children, 17 | as: Component = 'p', 18 | className, 19 | duration = 2, 20 | spread = 2, 21 | }: TextShimmerProps) { 22 | const MotionComponent = motion.create(Component as keyof JSX.IntrinsicElements); 23 | 24 | const dynamicSpread = useMemo(() => { 25 | return children.length * spread; 26 | }, [children, spread]); 27 | 28 | return ( 29 | 51 | {children} 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /apps/mail/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Textarea = React.forwardRef>( 6 | ({ className, ...props }, ref) => { 7 | return ( 8 |