Commit graph

68 commits

Author SHA1 Message Date
Shautvast
11941a5bea doc update2 2026-03-02 16:51:06 +01:00
Shautvast
696c3bec9f doc update 2026-03-02 16:45:37 +01:00
Shautvast
a2d6e72b7a less keychain access 2026-03-02 16:41:19 +01:00
Shautvast
5a3ac2eed9 secured bridge communication 2026-03-02 16:27:46 +01:00
Shautvast
9375b5b96f updated docs5 2026-03-02 15:41:52 +01:00
Shautvast
b64d1411d2 updated docs4 2026-03-02 15:35:49 +01:00
Shautvast
56b27e1692 updated docs3 2026-03-02 15:30:09 +01:00
Shautvast
ef5127e0de updated docs2 2026-03-02 15:28:02 +01:00
Shautvast
1eb4affdb6 updated docs 2026-03-02 15:25:52 +01:00
Shautvast
e2badaa170 update dependencies 2026-03-02 14:58:44 +01:00
Shautvast
3b131fefe8 added license 2026-02-25 20:42:17 +01:00
Shautvast
3f899d0e17 obsolete 2026-02-25 19:44:28 +01:00
Shautvast
bd6b32fbd5 updated docss 2026-02-25 19:43:55 +01:00
Shautvast
45f8355a9e Update CLAUDE.md with current architecture and implementation details
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>
2026-02-25 19:38:16 +01:00
Shautvast
e858236f51 Fix IMAP search tag mismatch and clean up compiler warnings
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>
2026-02-25 17:35:58 +01:00
Shautvast
687e8d028b keep mores links in the message 2026-02-25 17:01:29 +01:00
Shautvast
a709a3a946 implemented safe cache 2026-02-25 16:48:40 +01:00
Shautvast
2c70eb300a Fix setup wizard port prompts showing blank default instead of current value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 10:54:23 +01:00
Shautvast
facb44d561 Store credentials in OS keychain via keyring crate
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>
2026-02-25 10:50:23 +01:00
Shautvast
a8ea1fb5bb replaced println's with tracing calls 2026-02-25 09:04:11 +01:00
Shautvast
2287b08cb5 Integrate proton-bridge in-process and silence verbose logging
- 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>
2026-02-24 22:51:30 +01:00
Shautvast
8826f3bc95 Add USAGE.md user documentation and link from CLAUDE.md
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>
2026-02-24 18:33:50 +01:00
Shautvast
3ce0067f69 Add reply to email (r key)
- 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>
2026-02-24 18:30:54 +01:00
Shautvast
2ef6a0fd24 Add SMTP server (step 7) with PGP/MIME external send
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>
2026-02-24 16:36:38 +01:00
Shautvast
fa1d1a6cb7 Add IMAP server (step 6) — all 9 commands implemented
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>
2026-02-22 19:13:20 +01:00
Shautvast
69fe7994c8 added notes 2026-02-22 18:57:26 +01:00
Shautvast
7916365816 Add message store and refactor main into bridge startup
Step 5: in-memory MessageStore mapping IMAP seq numbers to ProtonMail IDs.
Oldest-first ordering, mark/expunge, range queries, subject/sender search.

Refactor main.rs: extract unlock_key_pool(), build SharedState (Arc<Mutex>),
load initial inbox, then wait for Ctrl-C. Ready to plug in IMAP/SMTP servers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 18:55:32 +01:00
Shautvast
70b2b0348e Decrypt ProtonMail messages end-to-end via key passphrase fix
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>
2026-02-22 15:55:18 +01:00
Shautvast
fc0eef0c31 Add ProtonMail API client (Step 3)
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>
2026-02-20 16:23:42 +01:00
Shautvast
6956fa8c01 manual refactor 2026-02-20 15:29:16 +01:00
Shautvast
dd9651a946 Add session expiry tracking and token refresh to proton-bridge
- 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>
2026-02-20 15:27:47 +01:00
Shautvast
05b6aac692 Add proton-bridge crate: workspace setup and SRP authentication (Step 2)
- 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>
2026-02-19 21:31:10 +01:00
Shautvast
fba2623f15 Add SMTP send with TLS mode support and timeouts
- 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>
2026-02-19 20:07:17 +01:00
Shautvast
7883b35dad Add IMAP search with / keybinding and live results pane
- 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>
2026-02-18 17:01:00 +01:00
Shautvast
c3a7c62214 Align sender and subject columns in inbox list
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>
2026-02-18 16:35:49 +01:00
Shautvast
b6ef2b4508 Fix selection reset on refresh and off-by-one body after delete
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>
2026-02-18 16:12:43 +01:00
Shautvast
12eb683007 Fix intermittent unresponsiveness from slow/hanging IMAP connections
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>
2026-02-18 15:43:45 +01:00
Shautvast
e6ff04a97d Clean up HTML table pipes in message body and improve focus indicators
- 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>
2026-02-18 15:15:43 +01:00
Shautvast
3e647dbe52 Post-process markdown to clean up noisy HTML email output
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>
2026-02-18 13:41:25 +01:00
Shautvast
23f179df24 Replace html2text with fast_html2md + tui-markdown for styled email preview
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>
2026-02-18 13:39:14 +01:00
Shautvast
2214b15f44 Increase html2text wrap width to avoid breaking long URLs
Let Ratatui handle visual line wrapping instead of html2text splitting
lines at 80 columns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:52:52 +01:00
Shautvast
df9d67a7c9 Fix terminal not restoring properly on exit
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>
2026-02-18 09:03:04 +01:00
Shautvast
0eda9045cd Cancel stale body fetch requests when selection changes quickly
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>
2026-02-18 08:59:42 +01:00
Shautvast
7269eca3e9 Fix worker thread blocking startup by deferring IMAP connection
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>
2026-02-18 08:57:57 +01:00
Shautvast
212fd49534 Add delete email with optimistic UI update
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>
2026-02-17 21:57:10 +01:00
Shautvast
e871c1aab8 Clean up leftover quoted-printable artifacts in email body
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>
2026-02-17 21:47:47 +01:00
Shautvast
d0df411c57 Render HTML emails as plain text using html2text
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>
2026-02-17 21:45:44 +01:00
Shautvast
3a2ce88ebf Extract plain text from emails using mailparse
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>
2026-02-17 21:38:46 +01:00
Shautvast
bdb6dce672 Update CLAUDE.md for skim project
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:34:46 +01:00
Shautvast
328a5fa5d2 Rename project to skim
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:33:37 +01:00