Skip to content

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Trajectory

Кроссплатформенный десктопный почтовый клиент: Tauri v2 (Rust) + React 18 / TypeScript / Tailwind 4 / Motion. Поддерживает Gmail (OAuth2 + Gmail API), Outlook / Office 365 (OAuth2 + Microsoft Graph), любой IMAP + SMTP и POP3 + SMTP.

Быстрый старт

npm install
npm run tauri dev        # десктопное приложение

Запуск UI в браузере без Rust (in-memory mock backend):

npm run dev              # http://localhost:1420 — онбординг
                         # http://localhost:1420/?demo — демо-ящик с письмами

Платформы и обновления

Windows, macOS (Apple Silicon и Intel) и Linux (.deb, .rpm, AppImage). На macOS окно использует родные «светофоры» (src-tauri/tauri.macos.conf.json), закрытое в трей окно возвращается кликом по иконке в Dock.

Для сборки на Linux нужны: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libdbus-1-dev pkg-config.

Автообновление. Ключи и подписи не нужны. Через 10 секунд после запуска и затем каждые 6 часов приложение читает последний опубликованный релиз репозитория Jyni0/Trajectory через GitHub API (src-tauri/src/updater.rs). Если версия в теге (Release-2.8.0 или v2.8.0) новее текущей — предлагает «Update and restart». Проверить вручную: Settings → About → Check for updates.

Установщик выбирается среди файлов релиза по имени (стандартные имена Tauri):

  • Windows — *_x64-setup.exe (или .msi), ставится в пассивном режиме и сам перезапускает приложение;
  • macOS — *_aarch64.dmg / *_x64.dmg: образ монтируется, .app заменяет текущее приложение;
  • Linux — *.AppImage заменяет запущенный AppImage; при установке из .deb / .rpm открывается страница релиза.

Выпуск: поднять version в package.json, src-tauri/tauri.conf.json и src-tauri/Cargo.toml, собрать npm run tauri build и загрузить установщики в новый релиз на GitHub. Сборки для macOS и Linux можно получить workflow .github/workflows/release.yml (запускается вручную, создаёт черновик релиза, ничего не публикует сам).

Структура

src-tauri/                      Rust core
  src/
    main.rs                     точка входа → mail_lib::run()
    lib.rs                      Tauri builder, tray, close-to-tray, регистрация команд
    commands.rs                 IPC-команды (тонкие обёртки над service)
    service.rs                  бизнес-логика: аккаунты, действия, отправка, черновики
    mail_engine.rs              фоновый Tokio sync engine (интервал + запросы из UI)
    db.rs                       SQLite (rusqlite): accounts, folders, emails, attachments, sync_state, tags, заметки
    secrets.rs                  keyring (Keychain / Credential Manager / Secret Service)
    oauth.rs                    OAuth2 PKCE + loopback redirect, refresh, кэш access-токенов
    mime.rs                     MIME-парсер (mail-parser) + санитайзинг HTML (ammonia)
    autodetect.rs               автодетект серверов по домену
    net.rs                      TCP/TLS (rustls + ring)
    providers/                  imap.rs · pop3.rs · smtp.rs · gmail.rs · graph.rs
    tests/                      unit + интеграционные тесты (mock-серверы, wiremock)
src/                            React UI
  components/                   дизайн-система Gravitation: tokens.s.ts + kit/ (Button, IconButton, Input, Field, Combobox, Segmented, Switch, Dialog, ScrollArea, NavItem, Settings…)
  components/layout/            TitleBar (своя рамка окна), Sidebar (выбор аккаунта, папки, теги), Toaster
  components/mail/              EmailList (виртуализация, сортировка), MailReader, HtmlViewer, Composer, Tags (чипы, выбор и редактор тегов)
  components/settings/          SettingsModal, AddAccountPanel
  store/                        Zustand: mailStore, uiStore
  lib/                          api.ts (типизированный IPC), mockBackend.ts, types.ts, compose.ts, format.ts
  hooks/                        useMailEvents (Tauri events), useHotkeys
  tests/                        Vitest + Testing Library
e2e/                            Playwright

Архитектура

  • IPC. src/lib/api.ts описывает интерфейс MailApi. Внутри Tauri он вызывает Rust-команды через invoke, вне Tauri подставляется mockBackend.ts с той же семантикой. Именно на нём работают Vitest, Playwright и превью в браузере.

  • Sync engine. Одна Tokio-задача каждые 30 секунд проверяет «Входящие» всех аккаунтов, а каждые 3 минуты синхронизирует все папки. Открытие папки в UI сразу запрашивает её синхронизацию (sync_folder). Аккаунты синхронизируются параллельно, папки — инкрементально через курсор провайдера в sync_state:

    • IMAP: UIDVALIDITY + последний UID (UID FETCH n:*);
    • POP3: множество UIDL;
    • Gmail: historyId (/history, при 404 — полный листинг);
    • Graph: @odata.deltaLink.

    Новые непрочитанные письма во входящих → событие mail:new, любые изменения → mail:changed, прогресс → sync:status.

  • Действия пользователя (прочитано, флаг, перенос, архив, удаление) сначала применяются к SQLite (UI оптимистичный), затем в фоне реплицируются на сервер. Ошибки приходят событием sync:error.

  • Удаление: письмо уходит в Корзину; удаление из Корзины или Черновиков — окончательное.

  • Отправка: lettre собирает MIME. Для IMAP/POP3 письмо уходит через SMTP, для Gmail/Graph — через их API. Копия кладётся в «Отправленные» (для IMAP также делается APPEND на сервер; дубликаты отсекаются по Message-ID).

Теги и заметки

  • Теги — свои метки с цветом, общие для всех аккаунтов. На письмо можно повесить несколько. Хранятся только на этом устройстве (таблицы tags, email_tags) и на сервер не уходят, поэтому одинаково работают для IMAP, POP3, Gmail и Outlook.
  • Поставить тег можно четырьмя способами: кнопкой «Tags» (t) в окне письма (там же можно создать новый тег), клавишами 1–9 (тег с этим номером в сайдбаре), правым кликом по письму в списке или перетаскиванием письма на тег в сайдбаре.
  • Клик по тегу в сайдбаре показывает все письма с ним в текущем аккаунте. Теги в сайдбаре перетаскиваются для смены порядка; через ⋯ их можно переименовать, перекрасить или удалить.
  • Заметка — личный текст к письму (кнопка «Add note»). Сохраняется на лету и при уходе из поля; если очистить текст, заметка удаляется. Ищется обычным поиском.
  • Сортировка списка: новые/старые первыми, сначала непрочитанные, сначала с флагом, по отправителю, по теме. Выбор запоминается.

Переписки

  • Открытое письмо показывается вместе со всей перепиской: письма, связанные через Message-ID / In-Reply-To, из любых папок (Входящие, Отправленные, архив, метки), кроме Корзины, Спама и Черновиков. Копия одного письма в двух папках показывается один раз.
  • Порядок — от старых к новым. Открытое письмо развернуто, остальные свёрнуты в строку (отправитель, начало текста, дата) и разворачиваются по клику.
  • Цитаты предыдущих писем («On … wrote:» + blockquote, .gmail_quote, строки > … в тексте) в переписке спрятаны под кнопку «•••». Одиночное письмо показывается целиком.

Логотипы отправителей (BIMI и иконки сайтов)

  • Логотип показывается вместо инициалов, только если письмо прошло DMARC для домена отправителя. Это берётся из верхнего заголовка Authentication-Results, который добавил твой почтовый сервер; нижние заголовки отправитель мог подделать. Без этого правила любой мог бы подставить в From: paypal.com и получить чужой логотип.

  • Политика DMARC домена должна быть quarantine или reject на 100%, как требует спецификация BIMI. Логотип берётся из DNS-записи default._bimi.<домен>; если её нет у поддомена, используется запись родительского домена.

  • Галочка «Verified» ставится, только если сертификат VMC из a=:

    • строится цепочкой до одного из вшитых корней VMC (src-tauri/certs/vmc: DigiCert, GlobalSign, SSL.com; Sectigo выпускает под корнем SSL.com);
    • имеет EKU BIMI и действует сейчас;
    • выдан на этот домен;
    • содержит ровно тот же SVG, что отдаёт l=.

    Тогда показывается логотип из самого сертификата, а значок-галочка стоит перед именем отправителя в списке и в окне письма; в подсказке видно, кто заверил. Без сертификата логотип показывается без галочки.

  • Нет BIMI (так у большинства: Stripe, Anthropic, GitHub…): берётся иконка сайта организации (mail.example.co.uk → https://example.co.uk/) — SVG или самая крупная apple-touch-icon/icon из <head>, иначе /favicon.ico. Тоже только при DMARC pass, всегда без галочки. Файл проверяется по содержимому (PNG/ICO/JPEG/GIF/WebP/SVG без скриптов), ссылки на локальные адреса игнорируются.

  • SVG показывается как <img>, где он не может выполнить скрипт или что-то загрузить. Логотипы со <script> отбрасываются.

  • Результат кэшируется в mail.db на неделю (если логотипа нет — на сутки). Поиск отключается в Settings → General → «Sender logos». При поиске DNS-резолвер и CDN бренда видят твой IP, как и при открытии любой их ссылки.

  • Корни VMC скачаны с AIA-адресов самих CA и сверены по SHA-256 с корнями из живых цепочек eBay, GlobalSign, Sectigo, X и других. При появлении нового корня VMC его нужно добавить в certs/vmc и в bimi.rs.

Хранение данных

Что Где
Пароли IMAP/POP3/SMTP, OAuth refresh-токены, Client ID/секрет своих OAuth-приложений системное хранилище (Windows Credential Manager / macOS Keychain / Secret Service), сервис dev.trajectory.mail
OAuth access-токены только в памяти процесса
Письма, вложения, теги, заметки, кэш логотипов BIMI, настройки аккаунтов (без паролей) SQLite mail.db в папке данных приложения (%APPDATA%\dev.trajectory.mail и т. п.)
Тема, размер сайдбара, сортировка localStorage WebView

mail.db не шифруется — защиту даёт учётная запись ОС (на Windows папка %APPDATA% доступна только пользователю; на Linux/macOS приложение ставит на папку права 0700). Для защиты от кражи диска нужно шифрование диска (BitLocker / FileVault / LUKS). Удалённые письма затираются (PRAGMA secure_delete).

При первом запуске после переименования данные из старой папки dev.singularity.mail и записи keychain старого сервиса автоматически переносятся (переносятся, а не копируются).

Безопасность

  • Пароли и refresh-токены лежат только в системном хранилище (keyring). В SQLite их нет, access-токены хранятся только в памяти.
  • <title> и <noscript> удаляются вместе с текстом, а <style> письма сохраняется — без него вёрстка рассылок разваливается и вылезает скрытый прехедер. Это безопасно: скриптов в окне письма нет, а CSP блокирует любые внешние загрузки из CSS.
  • Входящий HTML проходит санитайзинг дважды: ammonia в Rust и DOMPurify в UI. Рендер идёт в <iframe sandbox> без allow-scripts и с CSP default-src 'none'. Удалённые изображения (трекинг-пиксели) заблокированы, пока пользователь их не разрешит.
  • OAuth: PKCE + проверка state, при обмене токенов редиректы не выполняются.
  • Подключение без шифрования (Security: None) разрешено только к локальным и приватным адресам (localhost, 10/8, 172.16/12, 192.168/16, 100.64/10 — Tailscale, .local/.lan). К публичному серверу пароль открытым текстом не отправится.
  • STARTTLS/STLS обязательны: если сервер их не поддерживает, соединение обрывается, а не продолжается открытым текстом.
  • Вложения сохраняются в «Загрузки» под очищенным именем: без путей, .., имён устройств Windows (CON, NUL…). Существующие файлы никогда не перезаписываются.
  • CSP окна запрещает скрипты со сторонних источников, object, base и form-action.

Настройка OAuth

Проще всего — Settings → Sign-in apps: ввести Client ID (и для Google — секрет desktop-клиента) один раз. Они сохраняются в системном хранилище ключей, в mail.db и в билд не попадают; секрет в UI больше не показывается (только маска GOCSPX-••••••). OAuth-приложение можно создать под отдельным аккаунтом Google/Microsoft — твой почтовый аккаунт только входит в него.

Порядок поиска: Settings → переменные окружения при запуске → значения, «вшитые» при сборке (option_env!). Смена приложения требует заново войти в аккаунты, вошедшие через старое.

Провайдер Где создать Тип клиента Переменные
Google Google Cloud Console → APIs & Services → Credentials. Включить Gmail API Desktop app TRAJECTORY_GOOGLE_CLIENT_ID, TRAJECTORY_GOOGLE_CLIENT_SECRET
Microsoft Entra ID → App registrations Public client / Mobile & desktop, redirect http://localhost TRAJECTORY_MS_CLIENT_ID

Запрашиваемые scopes:

  • Google: gmail.modify, gmail.send, openid, email;
  • Microsoft: offline_access, User.Read, Mail.ReadWrite, Mail.Send.

Тесты

npm run test:rust   # 121 тест: SQLite, BIMI/VMC-репозитории, теги/заметки/сортировка, MIME, mock IMAP/POP3/SMTP, wiremock Gmail/Graph, sync engine
npm test            # 102 теста Vitest: UI-кит, Zustand-сторы, компоненты, теги, интеграция App
npm run test:e2e    # Playwright: онбординг → настройки → аккаунт → список писем → черновик

Mock-серверы IMAP/POP3/SMTP (src-tauri/src/tests/mock_servers.rs) поднимаются внутри процесса на эфемерных портах, поэтому Docker и greenmail не нужны.

Playwright гоняет настоящий UI в Chromium поверх mock backend. Для E2E именно десктопной сборки можно подключить tauri-driver + WebdriverIO: селекторы data-testid из e2e/mail.spec.ts подходят без изменений.

Горячие клавиши

c — написать · r — ответить · j/k — следующее/предыдущее · e — архив · # — удалить · s — флаг · u — прочитано/непрочитано · t — теги письма · 1–9 — поставить/снять тег по номеру · / — поиск · Ctrl+Enter — отправить.

Загрузка Gmail по частям

Gmail считает каждый запрос (скачивание письма — 5 единиц, лимит 15 000 в минуту на пользователя), поэтому:

  • новая папка сначала получает только 50 самых свежих писем; более старые скачиваются пачками по 50, когда список прокручен до конца;
  • запросы одного ящика идут не чаще 10 в секунду; на ответ «слишком много запросов» (429 / 403 Quota exceeded) приложение ждёт 2–32 с и повторяет;
  • уже скачанные письма заново не загружаются, а скачанное до сбоя сохраняется.

IMAP, POP3 и Outlook пока синхронизируют последние 200 писем папки сразу.

Известные ограничения

  • Состояние флагов, изменённое на сервере другим клиентом, для уже скачанных IMAP-писем не подтягивается: для этого нужен CONDSTORE/QRESYNC. Push через IMAP IDLE тоже не реализован, вместо него опрос раз в 2 минуты.
  • Черновики хранятся локально и на сервер не синхронизируются.
  • При пересылке вложения исходного письма не прикрепляются автоматически.
  • После обновления, которое перестало показывать <title> над письмом, при первой синхронизации заново скачиваются последние 200 писем каждой папки: их тела пересобираются, а теги, заметки, флаги и прочитанность сохраняются. У более старых писем в кэше текст заголовка над письмом останется.
  • Теги и заметки привязаны к локальной копии письма. Если сервер выдаст письмо заново под другим идентификатором (например, после переноса другим клиентом), тег с него пропадёт.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages