Кроссплатформенный десктопный почтовый клиент: 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. - IMAP: UIDVALIDITY + последний UID (
-
Действия пользователя (прочитано, флаг, перенос, архив, удаление) сначала применяются к 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, строки> …в тексте) в переписке спрятаны под кнопку «•••». Одиночное письмо показывается целиком.
-
Логотип показывается вместо инициалов, только если письмо прошло 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=.
Тогда показывается логотип из самого сертификата, а значок-галочка стоит перед именем отправителя в списке и в окне письма; в подсказке видно, кто заверил. Без сертификата логотип показывается без галочки.
- строится цепочкой до одного из вшитых корней VMC (
-
Нет 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и с CSPdefault-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.
Проще всего — Settings → Sign-in apps: ввести Client ID (и для Google — секрет desktop-клиента) один раз. Они сохраняются в системном хранилище ключей, в mail.db и в билд не попадают; секрет в UI больше не показывается (только маска GOCSPX-••••••). OAuth-приложение можно создать под отдельным аккаунтом Google/Microsoft — твой почтовый аккаунт только входит в него.
Порядок поиска: Settings → переменные окружения при запуске → значения, «вшитые» при сборке (option_env!). Смена приложения требует заново войти в аккаунты, вошедшие через старое.
| Провайдер | Где создать | Тип клиента | Переменные |
|---|---|---|---|
| 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 считает каждый запрос (скачивание письма — 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 писем каждой папки: их тела пересобираются, а теги, заметки, флаги и прочитанность сохраняются. У более старых писем в кэше текст заголовка над письмом останется. - Теги и заметки привязаны к локальной копии письма. Если сервер выдаст письмо заново под другим идентификатором (например, после переноса другим клиентом), тег с него пропадёт.