Makes LibreMail handle mailto: links and email share intents, opening a prefilled compose screen, and lets it appear in the system default-apps list where the platform supports it.
Intent handling
AndroidManifest.xml: intent filters on MainActivity for ACTION_VIEW/ACTION_SENDTO with the mailto scheme (with BROWSABLE, which is also what surfaces LibreMail on the "Open by default" screen), and ACTION_SEND/ACTION_SEND_MULTIPLE for text/plain + message/rfc822.
MainActivity parses the launch intent (and onNewIntent) into a one-shot ComposePrefill, guarded by savedInstanceState == null so a rotation doesn't reopen a duplicate compose.
LibreMailApp navigates to compose from the prefill via a LaunchedEffect.
Parsing
MailtoParser — pure Kotlin RFC 6068 parser (no android.net.Uri, so it's fully JVM-unit-tested): multiple recipients, to/cc/bcc/subject/body, case-insensitive scheme/fields, RFC 3986 percent-decoding. A literal + is preserved (mailto encodes spaces as %20), so user+tag@example.com survives.
IntentComposeParser — turns a mailto: URI or EXTRA_EMAIL/EXTRA_CC/EXTRA_BCC/EXTRA_SUBJECT/EXTRA_TEXT extras into a ComposePrefill.
Compose / routing
Added a Bcc field to the compose form; Routes now carries cc/bcc/body deep-link args feeding ComposeViewModel via SavedStateHandle.
Multi-account: the existing From picker on the compose screen already lets the user choose which account sends.
bcc end-to-end
Bcc was a new concept, so it's threaded fully rather than left as a dead field: OutgoingMessage, SmtpSender (envelope BCC, header stripped), GraphSender (bccRecipients), and outbox + drafts persistence via Room migration v9 → v10 (bccAddresses column, schema 10.json committed).
Default mail app
Android has no public RoleManager email role (unlike SMS/Browser), so becoming the system default is OEM-dependent — hence "where supported." The intent filters make LibreMail appear in the system default-apps / "Open by default" screen where the platform exposes an email association.
Extended SmtpSenderTest (GreenMail: bcc delivered, no Bcc header leaked), GraphSenderTest (bccRecipients payload), and ComposeViewModelTest (prefill from nav args + bcc on send).
Fast CI gate green locally on JDK 21: assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt.
Notes / left to CI
The actual intent-filter/deep-link launch and the Room migration test are instrumented — left to CI/manual (no emulator here).
Known edge: an unusually large shared body is passed through the nav-route arg (same mechanism as the existing to/subject prefill); a multi-hundred-KB text share could risk TransactionTooLargeException on state save. Left uncapped to avoid silently truncating legitimate content.
Closes #25.
## What
Makes LibreMail handle `mailto:` links and email share intents, opening a **prefilled compose** screen, and lets it appear in the system default-apps list where the platform supports it.
### Intent handling
- `AndroidManifest.xml`: intent filters on `MainActivity` for `ACTION_VIEW`/`ACTION_SENDTO` with the `mailto` scheme (with `BROWSABLE`, which is also what surfaces LibreMail on the "Open by default" screen), and `ACTION_SEND`/`ACTION_SEND_MULTIPLE` for `text/plain` + `message/rfc822`.
- `MainActivity` parses the launch intent (and `onNewIntent`) into a one-shot `ComposePrefill`, guarded by `savedInstanceState == null` so a rotation doesn't reopen a duplicate compose.
- `LibreMailApp` navigates to compose from the prefill via a `LaunchedEffect`.
### Parsing
- **`MailtoParser`** — pure Kotlin RFC 6068 parser (no `android.net.Uri`, so it's fully JVM-unit-tested): multiple recipients, `to`/`cc`/`bcc`/`subject`/`body`, case-insensitive scheme/fields, RFC 3986 percent-decoding. A literal `+` is **preserved** (mailto encodes spaces as `%20`), so `user+tag@example.com` survives.
- **`IntentComposeParser`** — turns a `mailto:` URI or `EXTRA_EMAIL`/`EXTRA_CC`/`EXTRA_BCC`/`EXTRA_SUBJECT`/`EXTRA_TEXT` extras into a `ComposePrefill`.
### Compose / routing
- Added a **Bcc** field to the compose form; `Routes` now carries `cc`/`bcc`/`body` deep-link args feeding `ComposeViewModel` via `SavedStateHandle`.
- **Multi-account**: the existing From picker on the compose screen already lets the user choose which account sends.
### bcc end-to-end
Bcc was a new concept, so it's threaded fully rather than left as a dead field: `OutgoingMessage`, `SmtpSender` (envelope BCC, header stripped), `GraphSender` (`bccRecipients`), and outbox + drafts persistence via **Room migration v9 → v10** (`bccAddresses` column, schema `10.json` committed).
### Default mail app
Android has **no public RoleManager email role** (unlike SMS/Browser), so becoming the system default is OEM-dependent — hence "where supported." The intent filters make LibreMail appear in the system default-apps / "Open by default" screen where the platform exposes an email association.
## Tests
- `MailtoParserTest` (JVM): single/multiple recipients, subject, body, cc, bcc, `to` merge, percent-encoding, literal `+`, case-insensitivity, malformed escapes, empty mailto.
- Extended `SmtpSenderTest` (GreenMail: bcc delivered, no `Bcc` header leaked), `GraphSenderTest` (`bccRecipients` payload), and `ComposeViewModelTest` (prefill from nav args + bcc on send).
- Fast CI gate green locally on JDK 21: `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `ktlintCheck` + `detekt`.
## Notes / left to CI
- The actual intent-filter/deep-link **launch** and the Room migration test are instrumented — left to CI/manual (no emulator here).
- Known edge: an unusually large shared `body` is passed through the nav-route arg (same mechanism as the existing `to`/`subject` prefill); a multi-hundred-KB text share could risk `TransactionTooLargeException` on state save. Left uncapped to avoid silently truncating legitimate content.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #25.
What
Makes LibreMail handle
mailto:links and email share intents, opening a prefilled compose screen, and lets it appear in the system default-apps list where the platform supports it.Intent handling
AndroidManifest.xml: intent filters onMainActivityforACTION_VIEW/ACTION_SENDTOwith themailtoscheme (withBROWSABLE, which is also what surfaces LibreMail on the "Open by default" screen), andACTION_SEND/ACTION_SEND_MULTIPLEfortext/plain+message/rfc822.MainActivityparses the launch intent (andonNewIntent) into a one-shotComposePrefill, guarded bysavedInstanceState == nullso a rotation doesn't reopen a duplicate compose.LibreMailAppnavigates to compose from the prefill via aLaunchedEffect.Parsing
MailtoParser— pure Kotlin RFC 6068 parser (noandroid.net.Uri, so it's fully JVM-unit-tested): multiple recipients,to/cc/bcc/subject/body, case-insensitive scheme/fields, RFC 3986 percent-decoding. A literal+is preserved (mailto encodes spaces as%20), souser+tag@example.comsurvives.IntentComposeParser— turns amailto:URI orEXTRA_EMAIL/EXTRA_CC/EXTRA_BCC/EXTRA_SUBJECT/EXTRA_TEXTextras into aComposePrefill.Compose / routing
Routesnow carriescc/bcc/bodydeep-link args feedingComposeViewModelviaSavedStateHandle.bcc end-to-end
Bcc was a new concept, so it's threaded fully rather than left as a dead field:
OutgoingMessage,SmtpSender(envelope BCC, header stripped),GraphSender(bccRecipients), and outbox + drafts persistence via Room migration v9 → v10 (bccAddressescolumn, schema10.jsoncommitted).Default mail app
Android has no public RoleManager email role (unlike SMS/Browser), so becoming the system default is OEM-dependent — hence "where supported." The intent filters make LibreMail appear in the system default-apps / "Open by default" screen where the platform exposes an email association.
Tests
MailtoParserTest(JVM): single/multiple recipients, subject, body, cc, bcc,tomerge, percent-encoding, literal+, case-insensitivity, malformed escapes, empty mailto.SmtpSenderTest(GreenMail: bcc delivered, noBccheader leaked),GraphSenderTest(bccRecipientspayload), andComposeViewModelTest(prefill from nav args + bcc on send).assembleDebug+testDebugUnitTest+lintDebug+ktlintCheck+detekt.Notes / left to CI
bodyis passed through the nav-route arg (same mechanism as the existingto/subjectprefill); a multi-hundred-KB text share could riskTransactionTooLargeExceptionon state save. Left uncapped to avoid silently truncating legitimate content.🤖 Generated with Claude Code