Add ProtonMail bridge, body cache, credentials, SMTP, setup wizard, and
key development notes including the UIDVALIDITY/UIDNEXT imap-proto quirk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove UIDVALIDITY/UIDNEXT from SELECT response to prevent imap-proto
from leaving the tagged SELECT line unconsumed in the buffer, which caused
the subsequent SEARCH command to assert on a stale tag (a122 vs a123).
Also fix empty SEARCH response to omit the trailing space before CRLF.
Remove dead diagnostic code and unused functions throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Passwords are no longer stored in config.toml. Instead:
- New setup wizard (--configure) prompts for credentials on first run
and stores them in the OS keychain (macOS Keychain, GNOME Keyring /
KWallet on Linux, Windows Credential Manager)
- Env-var fallback: TUIMAIL_<KEY> for headless environments
- ProtonMail session token moves from session.json to the keychain
- Config file path moves to {config_dir}/tuimail/config.toml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add proton-bridge as optional dep behind `proton` feature flag
- New proton-bridge/src/lib.rs: pub fn start() spins a background Tokio
thread, pre-binds ports, and signals readiness via mpsc before returning
- src/main.rs: conditionally starts bridge before TUI enters raw mode;
derives effective IMAP/SMTP config via Provider enum
- src/config.rs: add Provider enum, optional imap/smtp, ProtonConfig/
BridgeConfig mirrors, effective_imap/smtp() helpers
- Remove all per-operation eprintln!/println! from imap_server, smtp_server,
and api.rs that fired during TUI operation and corrupted the display
- config.toml.example: unified format covering both imap and proton providers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers setup (config.toml, provider settings, Gmail app passwords),
the split-pane UI, full keyboard reference, compose/reply workflow,
auto-refresh behaviour, and a config field reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Email struct gains from_addr (bare address extracted from From: header)
- r opens compose pre-filled: To = sender, Subject = Re: ..., cursor in Body
- Quoted original shown below a dimmed separator in the Body field; sent as
part of the message body when the user hits Ctrl+S
- Refresh rebound from r → u / F5 to free r for reply
- Status bar updated accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a minimal ESMTP listener (AUTH LOGIN/PLAIN, MAIL FROM, RCPT TO,
DATA, QUIT) that sends via the ProtonMail v4 API (create draft → send).
- ProtonMail internal recipients: Type 1 (encrypt to recipient's ECDH key)
- External recipients: Type 32 PGP/MIME — detached signature in a separate
application/pgp-signature MIME part so the body arrives clean in Gmail.
The MIME entity is wrapped in an OpenPGP-signed message (Signature=1) to
satisfy ProtonMail's mandatory signature check on all external sends.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plain-TCP listener on 0.0.0.0 (handles both localhost and 127.0.0.1).
LOGIN, NOOP, SELECT (reloads inbox), FETCH header+body, SEARCH, STORE,
EXPUNGE (deletes on ProtonMail), LOGOUT.
FETCH body decrypts messages on demand: brief lock for ID lookup, API call
without lock, brief lock again for crypto. RFC 3501 literal format with
exact byte counts for imap-crate compatibility.
Also: update store.expunge() to return (ids, seqs) in descending order for
correct IMAP EXPUNGE response ordering; add chrono for RFC 2822 dates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The key insight from go-proton-api SaltForKey: ProtonMail uses only the
last 31 chars of the bcrypt output as the key passphrase — not the full
60-char string. One line fix, two days of debugging.
Also adds the full crypto layer (crypto.rs): user key unlock, address key
token decryption, and message body decryption via rpgp. Includes SRP auth,
session caching with locked-scope handling, TOTP, and the ProtonMail API
client for inbox listing and message fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements typed async wrappers for the four endpoints tuimail needs:
- list_messages: GET /mail/v4/messages (paged inbox listing)
- get_message: GET /mail/v4/messages/{id} (full message with encrypted body)
- delete_messages: PUT /mail/v4/messages/delete (soft-delete to Trash)
- get_public_keys: GET /core/v4/keys (recipient keys for outbound mail)
All responses decoded through Envelope<T> with Code 1000 check.
main.rs smoke-tests the inbox listing after authentication.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store expires_at (Unix timestamp) in session.json from ExpiresIn response field
- Add is_expired() with 5-minute refresh margin
- Implement POST /auth/v4/refresh flow: tries refresh before falling back to SRP login
- authenticate() now: use cached → refresh if expired → full login if all else fails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert tuimail repo to Cargo workspace with tuimail and proton-bridge members
- Add proton-bridge binary crate with config, SRP 6a, and auth modules
- Implement ProtonMail SRP 6a exactly matching go-srp:
- Little-endian bigints throughout
- expandHash = SHA512(data||0..3) producing 256 bytes
- k, u, M1, M2 all via expandHash with 256-byte normalised inputs
- Password hashing v3/v4: bcrypt($2y$, salt+proton) + expandHash(output||N)
- Authenticate against Proton API (auth/info → auth/v4), verify server proof
- Persist session (UID, access/refresh tokens) to session.json
- Add bridge.toml and session.json to .gitignore (contain credentials/tokens)
- Add PROTON.md with full build plan for the mini-bridge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add smtp.rs with send_email using lettre; supports none/starttls/smtps
- Replace use_tls: bool with TlsMode enum in SmtpConfig for explicit port 465 (SMTPS) support
- Add SMTP_IO_TIMEOUT (15s) for socket I/O and SMTP_WALL_TIMEOUT (30s) covering DNS + connect
- Spawn SMTP send on a dedicated thread so the IMAP worker thread is never blocked
- Update config.toml.example with tls_mode documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Press / to enter search mode; status bar shows query input
- IMAP SEARCH OR SUBJECT/FROM sent in background worker thread
- Results replace inbox list with match count in title
- Navigation and body preview work the same as in regular inbox
- Esc clears search and returns to normal inbox view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compute the max sender width across the loaded emails (capped at 40
chars) and pad each sender field to that width. Long senders are
truncated with an ellipsis. Subject column now starts at a consistent
position regardless of sender name length.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Refresh regression:
- refresh() only loads the latest 50 emails, so if the user scrolled
further via FetchMore their selected email was not in the new list and
selection fell back to index 0. Now preserve emails older than the
refresh batch (previously fetched via FetchMore) by merging them back.
- Also cancel pending debounce on refresh so stale pending fetches can't
overwrite the correct body after selection changes.
- Up-arrow now uses debounce consistently with Down-arrow.
Delete off-by-one:
- IMAP expunge renumbers all messages with seq > deleted seq. The app
was still holding pre-delete sequence numbers, so the next FetchBody
after a delete would retrieve the wrong message. After removing an
email, decrement the seq of every remaining email with seq > deleted.
Cleanup: remove now-unused Inbox.oldest_seq and Inbox.has_older() since
oldest_seq/has_older are now computed from the merged emails list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three improvements:
- Add 15s connect/read/write timeouts to TcpStream so a hung IMAP
server can no longer block the worker thread indefinitely
- Cache tui_markdown rendering: convert Text<'a> to Text<'static> on
first render and reuse across frames, re-parsing only when the body
actually changes
- Debounce FetchBody requests on keyboard navigation: wait 150ms of
inactivity before sending, so rapid scrolling doesn't flood the
worker with stale requests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strip pipe characters from HTML-to-markdown table remnants in email body
- Add bold + arrow prefix (▶) to focused pane title for clear focus indication
- Rename Focus::Preview and all preview_* variables to Message/message_*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strip images, simplify links to just text, remove very long bare URLs,
and collapse excessive blank lines for a cleaner preview pane.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HTML emails are now converted to markdown then rendered with rich formatting
(bold, italic, headings, links) in the preview pane via tui-markdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensure disable_raw_mode and LeaveAlternateScreen run even when the
app returns an error. Also add a panic hook to restore the terminal
on unexpected panics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses Arc<AtomicU32> to track the most recently wanted email sequence
number. The worker skips fetch requests that no longer match, avoiding
wasted network calls when scrolling through emails rapidly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The eager connect() call in the worker thread could hang if the server
was unreachable, preventing the worker from ever processing commands.
Let refresh() handle the initial connection instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Press 'd' to delete the selected email. Removes it from the list
immediately and performs the server-side delete in the background
for a snappy user experience.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a second-pass QP decode to catch =XX sequences that survive
the initial mailparse decoding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fall back to html2text when no text/plain part is available,
converting HTML emails to readable terminal output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetch full raw email and parse MIME structure to find the
text/plain part, removing MIME headers and boundaries from
the preview pane.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After refresh, find the previously selected email by its IMAP
sequence number and keep the highlight on it. Falls back to the
first email if the selected one was deleted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tab toggles focus between inbox list and preview. Focused pane
gets a cyan border. When preview is focused, up/down scrolls the
email body instead of navigating the list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>