Commit Graph
99 Commits
Author SHA1 Message Date
Jason Ross bb3dcd8217 Merge branch 'main' into fix-move-by-role-specialuse 2026-07-01 22:07:18 -05:00
Jason Ross 9f582ecac4 Merge pull request #91 from JMR-dev/feat-fdroid-compliance
chore(fdroid): add F-Droid metadata, license audit, and anti-feature docs
2026-07-01 22:06:38 -05:00
JMR-devandClaude Fable 5 a6436719b1 fix(mail): prefer special-use folder when resolving move-by-role destination
resolveRoleFolder().pick() chose the destination for archive/reportSpam/
trash as the first selectable folder with the matching role, in server
LIST order. A provider's built-in folder (role via an RFC 6154 attribute,
e.g. [Gmail]/Spam via \Junk) and a same-named user folder (role via
roleFromDisplayName) can share a role, so the winner depended on which
one the server happened to LIST first — silently misrouting mail past
the provider's junk training, retention, and auto-purge.

Prefer the server-advertised special-use folder among same-role matches:
maxByOrNull { it.specialUse } picks a specialUse=true folder over
name-derived ones, and, because maxByOrNull returns the first max, keeps
the existing LIST-order behavior when no special-use folder exists.

Closes #58

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:48:46 -05:00
JMR-devandClaude Fable 5 807f2402a5 chore(fdroid): tighten accuracy of CI and KnownVuln wording in audit doc
CI runs ktlint/detekt and the E2E suites (not Android lintDebug), and the
KnownVuln rationale should not imply blanket TLS enforcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:46:06 -05:00
JMR-devandClaude Fable 5 db96134492 chore(fdroid): add F-Droid metadata, license audit, and anti-feature docs
Prepare for F-Droid publication (issue #16):

- docs/fdroid-compliance.md: full dependency license audit (release
  runtime classpath + buildscript classpath — all FOSS, no Play
  Services/Firebase, no non-free Gradle plugins), an anti-feature
  review of actual app behavior (none to declare: debug reporting is
  opt-in/local-only with no endpoint by default, Android Backup is
  gated off by default, Outlook OAuth is optional per-account with a
  public client id), a complete network-surface inventory, and the
  clean-room build verification (assembleRelease succeeds with no
  secrets.properties).
- app/build.gradle.kts: stop embedding AGP's dependency-info block (a
  Google-Play-encrypted dependency list in the APK signing block) in
  APKs/bundles — a known F-Droid inclusion/reproducibility blocker.
- fastlane/metadata/android/en-US/: store listing (title, short/full
  description, changelog for versionCode 1) that F-Droid reads from
  the repo; listing .txt files deliberately carry no license headers.
- docs/fdroid/org.libremail.app.yml: commented template + instructions
  for the eventual fdroiddata build recipe (submission out of scope).
- README.md: F-Droid section pointing at the above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:43:58 -05:00
Jason Ross 0880dd7f54 Merge pull request #80 from JMR-dev/feat-compose-attachment-reminder
feat(compose): prompt before sending when a mentioned attachment is missing
2026-07-01 18:20:57 -05:00
JMR-dev d687f11518 Merge remote-tracking branch 'origin/main' into feat-compose-attachment-reminder
# Conflicts:
#	app/src/androidTest/kotlin/org/libremail/ui/compose/ComposeScreenTest.kt
#	app/src/main/kotlin/org/libremail/ui/compose/ComposeScreen.kt
2026-07-01 18:04:59 -05:00
Jason Ross 0dd6933b13 Merge pull request #82 from JMR-dev/feat-compose-collapsible-cc-bcc
feat(compose): collapse Cc/Bcc into expandable links under the To field
v0.2.0
2026-07-01 17:29:51 -05:00
Jason Ross eeeb838658 Merge branch 'main' into feat-compose-collapsible-cc-bcc 2026-07-01 17:18:15 -05:00
Jason Ross 1828109894 Merge pull request #81 from JMR-dev/feat-richtext-foundation
feat(richtext): parameterized styles, alignment/image/base-style channels, HTML round-trip
2026-07-01 17:15:57 -05:00
Jason Ross bce597b03c Merge branch 'main' into feat-compose-collapsible-cc-bcc 2026-07-01 17:12:43 -05:00
JMR-devandClaude Fable 5 4782b24453 fix(richtext): preserve blank lines between aligned paragraphs; share span merging
Code-review fixes: an all-empty paragraph group (a blank line isolated by an
alignment split) emitted <p></p>, which the parser collapses — it now emits one
<br> per line so blank lines round-trip. The identical span-merge helper that
existed in both the parser and RichTextEditing is now a single shared
mergeSameValueSpans() in RichText.kt, and the private applyBlock/applyLink drop
their never-used default font resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 17:05:14 -05:00
JMR-devandClaude Fable 5 671fca99a2 feat(compose): prompt before sending when a mentioned attachment is missing
Send now scans the subject and body for "attach" and its variants
(word-bounded, case-insensitive). When the text mentions one but the
message carries no attachment, an AlertDialog asks "Need to attach
anything?" — Yes returns to composing and pulses the attach button,
No sends the message as-is, and dismissing cancels the send. (#79)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 17:04:15 -05:00
JMR-devandClaude Fable 5 2cefc4751b feat(richtext): parameterized styles, alignment/image/base-style channels, HTML round-trip
RichStyle becomes a sealed interface (Bold/Italic/Underline/Strikethrough +
FontFamily/FontSize/FontColor/Highlight); RichTextContent gains alignments,
images, and baseStyle channels. The HTML serializer emits merged <span style>
runs, text-align on <p>/<li> (splitting merged paragraphs at alignment
boundaries), <img src="cid:…"> over the visible [image: name] token, and a
single outer <div style> for the base style. The parser is a faithful inverse
and additionally tolerates <del>/<strike>, px font sizes, #rgb colors, and
start/end alignment synonyms; unknown CSS is ignored without dropping text.

hasFormatting() covers every new channel so ComposeViewModel.normalizedHtml()
never silently drops serialized formatting. The editor carries parameterized
style identity via string annotations (libremail:style / libremail:image), maps
alignment onto ParagraphStyle ranges, holds baseStyle in separate field state,
and RichTextEditing.toggleStyle now replaces a different value of the same kind
while styleAt() answers "current value over the selection" for pickers.
ColorSwatchRow is added for the upcoming color/highlight dialogs. No UI change.

Closes #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 16:54:25 -05:00
JMR-devandClaude Fable 5 99ece7b6d7 feat(compose): collapse Cc/Bcc into expandable links under the To field
The Cc and Bcc fields now start collapsed into small left-aligned link
buttons under the To box, freeing about two field heights of vertical
space for the message body. Tapping a link expands it into the regular
input field and focuses it; a field also expands on its own when it
already carries recipients (reply-all/mailto prefill, resumed drafts)
and never re-collapses once shown, so it cannot vanish mid-edit. The
expansion state lives in the UI via rememberSaveable and survives
rotation. Moving the Bcc field also gives it the medium shape every
sibling field already had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 16:20:23 -05:00
JMR-devandClaude Fable 5 f99c2df85f fix(compose): restore Bcc recipients when resuming a draft
saveOrDeleteDraft persists the Bcc line, but the init-block restore
never copied it back, so reopening a draft silently dropped its Bcc
recipients (and re-saving then lost them for good).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 16:20:07 -05:00
Jason Ross f5c9d4c4d2 Merge pull request #57 from JMR-dev/fix-notification-tap-opens-message
fix(notifications): open the tapped message from a new-mail notification
2026-07-01 16:13:46 -05:00
JMR-devandClaude Fable 5 a6ec00d203 fix(notifications): open the tapped message from a new-mail notification
Tapping a new-mail notification only brought the app to the foreground:
the content PendingIntent was a bare launch intent shared by every
notification, and nothing on the activity side handled a message target.

Per-message notifications now carry an explicit open-message intent —
action + id extra + a per-message data URI, so each message keeps its
own PendingIntent under filterEquals instead of all collapsing onto one
FLAG_UPDATE_CURRENT entry. MainActivity parses the id on fresh launch
and in onNewIntent and hands it to the NavHost as pending state (the
pendingCompose handoff pattern) to navigate to the reader. The group
summary keeps the plain open-the-app intent.

Fixes #56

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 15:51:16 -05:00
Jason Ross a5644f4e02 Merge pull request #55 from JMR-dev/chore-preflight-static-analysis
chore(preflight): add ktlintCheck + detekt to the fast gate
2026-07-01 15:38:17 -05:00
JMR-devandClaude Opus 4.8 528cbd94c4 chore(preflight): add ktlintCheck + detekt to the fast gate
CI's "Static analysis" job runs :app:ktlintCheck :app:detekt, which the
local preflight gate did not, so style violations in test/androidTest
source sets (which lintDebug skips) failed the merge gate only after
push. Add both to the /preflight skill and mirror the change in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:28:25 -05:00
Jason Ross 9f7540a2f6 Merge pull request #54 from JMR-dev/fix-drawer-duplicate-folders
fix(mailbox): de-duplicate folder names in the drawer
2026-07-01 15:24:57 -05:00
JMR-devandClaude Opus 4.8 e20981d083 style(test): satisfy ktlint in new folder-label tests
Body expression on the signature line (function-signature) and one
argument per wrapped line (argument-list-wrapping) in the tests added
for drawer folder de-duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:15:24 -05:00
JMR-devandClaude Opus 4.8 0f479b2431 fix(mailbox): de-duplicate folder names in the drawer
The drawer rendered every standard-role folder with a generic friendly
name (e.g. "Drafts") and discarded the server name, so a Gmail account
with both a provider built-in folder and a same-named user folder showed
two identical entries (Drafts, Archive, Spam).

De-duplicate labels provider-agnostically: when 2+ folders would render
the same name, the provider's built-in special folder (identified by RFC
6154 SPECIAL-USE flags, now persisted on the folder cache) gets the
provider name appended ("Archive - Gmail"), a nested user folder gets its
parent location ("Reports (Work)"), and a top-level user folder keeps its
plain name. Only triggers on a real collision, so stock accounts are
unchanged.

Adds a `specialUse` column to the folders table (Room v11 -> v12).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:01:11 -05:00
Jason Ross adcfc7a6dc Merge pull request #52 from JMR-dev/fix/message-list-cursorwindow-overflow
fix(mailbox): project message list to avoid CursorWindow overflow
2026-07-01 13:54:36 -05:00
JMR-devandClaude Opus 4.8 f70bda77d4 fix(mailbox): project message list to avoid CursorWindow overflow
MessageDao.observeAll() ran `SELECT * FROM messages` and returned full
MessageEntity rows — including the potentially large body/isHtml columns —
for every cached message at once. Dragging big HTML bodies through SQLite's
shared ~2 MB CursorWindow overflowed it once enough bodies were cached,
crashing with "Couldn't read row N from CursorWindow" (#51).

Replace it with observeSummaries(), a body-less column projection into a new
lightweight MessageSummary POJO. The list never renders or searches the body,
and the reader already loads it lazily per-message via getById when a message
is opened, so nothing else needs it.

Add a regression test that reads back rows whose bodies exceed the window.

Closes #51

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:41:06 -05:00
Jason Ross 4175b3f440 Merge pull request #50 from JMR-dev/feat/49-onboarding-battery-optimization
feat(onboarding): opt-in to unrestricted battery/background usage
2026-07-01 12:42:52 -05:00
JMR-devandClaude Opus 4.8 59c9f9d27e feat(onboarding): opt-in to unrestricted battery/background usage
Add a guided, F-Droid-safe onboarding step and an Advanced Settings recovery
row that let users move LibreMail to "Unrestricted" battery usage, so IMAP
IDLE push (IdleService) and periodic WorkManager sync aren't throttled or
killed by Doze. Deep-links to the system app-details screen rather than the
restricted REQUEST_IGNORE_BATTERY_OPTIMIZATIONS dialog, so it needs no new
permission and is safe on Play (#17) and F-Droid (#16).

- BatteryPromptDecision: pure, unit-tested gate (supported && !unrestricted && !handled)
- BatteryOptimizationManager: reads isIgnoringBatteryOptimizations, builds the deep-link intent
- Onboarding step shown after the first account is added; skipped when already
  unrestricted or already handled; re-checks status on resume
- Advanced Settings row shows current status and re-opens the system screen
- battery_prompt_handled flag persisted in the settings DataStore (kept out of AppSettings)
- Unit tests for the decision + view model; Espresso E2E for the step

Closes #49

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:31:54 -05:00
Jason Ross 2ce527ad77 Merge pull request #48 from JMR-dev/docs-readme-refresh
docs: refresh README for onboarding/app-password/rich-compose/opt-in features (#20)
2026-07-01 11:16:50 -05:00
JMR-dev c3287b66f4 Merge remote-tracking branch 'origin/main' into docs-readme-refresh 2026-07-01 11:06:22 -05:00
Jason Ross 9ab1765116 Merge pull request #47 from JMR-dev/feat-rich-compose
feat(compose): rich HTML editor, multipart send, and signatures (#23, #36, #37, #38)
2026-07-01 11:06:21 -05:00
JMR-devandClaude Opus 4.8 dde081c4a0 Merge branch 'main' into feat-rich-compose
Renumber the rich-composition schema change onto main's v10 (#43 bcc):
- Migrations.kt: keep MIGRATION_9_10 (bccAddresses) from main; move the rich
  changes (bodyHtml columns + signatures table) into a new MIGRATION_10_11.
- @Database version 10 -> 11; register MIGRATION_10_11; take main's 10.json and
  regenerate 11.json (now carries bccAddresses + bodyHtml + signatures).
- Union OutgoingMessage/ComposeViewModel/Routes (bcc + bodyHtml + signatures +
  reportReview routes); merge both sides' SmtpSender/ComposeViewModel tests.
- Fix MappersHtmlBodyTest positional Draft(...) broken by the inserted bcc field.
Verified: assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:56:12 -05:00
Jason Ross dbe258126b Merge pull request #43 from JMR-dev/feat-mailto-default-app
feat(mailto): handle mailto: links and email share intents (#25)
2026-07-01 10:41:12 -05:00
JMR-devandClaude Opus 4.8 e395e2af1c Merge branch 'main' into feat-mailto-default-app
Resolve LibreMailApp.kt: union the compose function params so mailto prefill
(pendingCompose/onComposeHandled) coexists with onboarding start-gating
(appViewModel) and the crash dialog (startupViewModel); keep LaunchedEffect +
getValue/remember imports. Verified locally: assembleDebug + testDebugUnitTest +
lintDebug + ktlintCheck + detekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:31:56 -05:00
Jason Ross d2b9d990ad Merge pull request #42 from JMR-dev/feat-debug-reporting
feat(reporting): opt-in debug reporting client (capture + review/submit) (#32, #33)
2026-07-01 10:28:45 -05:00
JMR-devandClaude Opus 4.8 291c9a2b4d Merge branch 'main' into feat-debug-reporting
Resolve conflicts from #40/#41/#44:
- build.gradle.kts: keep DEBUG_REPORT_ENDPOINT field + val, take #40's
  outlookRedirectScheme (gmailRedirectScheme was deleted).
- LibreMailApp.kt: function takes BOTH appViewModel (start-dest gating, #44)
  and startupViewModel (crash dialog, #42); use renamed AccountPickerScreen.
- SettingsScreen.kt: keep both the Diagnostics (#42) and Backup (#41) sections.
Verified locally: assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:18:28 -05:00
Jason Ross f47efadfa6 Merge pull request #44 from JMR-dev/feat-onboarding-flow
feat(onboarding): first-run flow, vendor picker, and app-password setup (#26-#31)
2026-07-01 10:10:58 -05:00
JMR-dev 781966e0a0 Merge remote-tracking branch 'origin/main' into feat-onboarding-flow 2026-07-01 09:59:41 -05:00
Jason Ross 085475bf93 Merge pull request #41 from JMR-dev/feat-backup-optin
feat(backup): opt-in Android Backup for settings only (#21)
2026-07-01 09:59:39 -05:00
JMR-dev df5b99aff9 Merge remote-tracking branch 'origin/main' into feat-backup-optin 2026-07-01 09:49:24 -05:00
Jason Ross 4504d34fc6 Merge pull request #40 from JMR-dev/fix-remove-gmail-oauth
refactor(auth): remove dead Gmail OAuth code path (#39)
2026-07-01 09:41:41 -05:00
JMR-devandClaude Opus 4.8 25e1a8b83c test(onboarding): scroll app-password fields/button into view before tapping
Real cause of the API 29-36 E2E timeout (the earlier 5s->15s bump didn't help,
proving it wasn't slowness): AppPasswordSetupScreen is a scrolling Column and the
"Test and add" button sits below the fold on the short default matrix emulator, so
the positional performClick was a silent no-op -> no add -> no navigation -> the
add-another wait never resolved. It passed on API 37 only because that job uses a
taller pixel_2 AVD. performScrollTo() each field + the button before interacting,
matching the existing pattern in SettingsScreenTest. Verified compileDebugAndroid
TestKotlin + ktlintCheck on JDK 21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:27:29 -05:00
JMR-devandClaude Opus 4.8 876539b514 test(onboarding): widen onboarding E2E waitForText timeout to 15s
OnboardingFlowTest passed on the API-37 job but timed out (ComposeTimeoutException
after 5000ms) across the animation-disabled API 29-36 matrix, at the single
async-gated transition: click -> viewModelScope coroutine -> addImapAccount ->
DONE -> LaunchedEffect -> navigate -> AddAnother render. The flow is correct
(green on API 37; ManualSetupScreenTest proves the add-callback path); the 5s cap
was just too tight for that compound step on slower matrix emulators. waitUntil
returns as soon as the text appears, so the happy path is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:41:05 -05:00
JMR-devandClaude Opus 4.8 3ea166607c test(reporting): compile instrumented SettingsScreen test with onReportProblem
The #32/#33 change added a required onReportProblem parameter to SettingsScreen
but left the existing instrumented SettingsScreenTest calling the old signature,
so :app:compileDebugAndroidTestKotlin failed in every E2E job (the local fast
gate never compiles the androidTest variant). Pass onReportProblem = {} in the
test. Verified with :app:compileDebugAndroidTestKotlin on JDK 21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:41:01 -05:00
JMR-devandClaude Opus 4.8 b643c3bb61 docs: refresh README for onboarding/app-password/rich-compose/opt-in features
Bring README in line with the post-batch shipped state (issue #20, folding in
#31's README reconciliation):

- Rewrite the status blurb and feature list to cover the onboarding flow, rich
  compose (HTML + multipart/alternative + signatures), full-history backfill
  with a device-only retention cap, opt-in app lock, mailto/default-app, and
  opt-in local debug reporting.
- Remove the Gmail OAuth setup section and the "no stored passwords for Gmail"
  claim; Gmail/Yahoo/iCloud are now app-password IMAP/SMTP vendors.
- Add an "Accounts and onboarding" section (Outlook OAuth; Gmail/Yahoo/iCloud
  app password with vendor app-password pages + Gmail 2SV note; Other IMAP/SMTP)
  and keep the Outlook OAuth setup section.
- Add a "Privacy and data flow" note: opt-in cache encryption, local
  user-initiated debug reporting (no hosted pipeline), and opt-in Android Backup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:38:27 -05:00
JMR-devandClaude Opus 4.8 bee5737ec4 feat(compose): rich HTML editor, multipart send, and signatures
Bring rich composition to LibreMail (issues #36, #37, #38, and #23).

#36 HTML editor + toolbar
- New pure, JVM-testable rich-text model (`richtext/`): RichTextContent with
  inline styles + links and block markers ("• ", "N. ", "> "), serializing to a
  narrow email-safe HTML subset and back (fromHtml is a faithful inverse).
- Rich editor in ComposeScreen with a bold/italic/underline, bulleted/numbered
  list, block-quote, and link toolbar, backed by AnnotatedString. Unformatted
  text stays plaintext-only (null HTML) so it feels unchanged and is accessible.
- #23: rounded corners on the compose fields/body via MaterialTheme.shapes.

#37 multipart/alternative + reply/forward quoting
- SmtpSender builds multipart/alternative (text/plain + text/html), nested in
  multipart/mixed when there are attachments; GraphSender sends HTML content.
- HtmlToText produces a readable text/plain fallback; ReplyBuilder quotes HTML
  originals as clean blockquotes (tags stripped) without corruption.
- HTML body persists/restores through drafts and the outbox (new nullable
  bodyHtml columns; Room v9->v10 migration + schema).

#38 signatures
- New signatures table (multiple per account, one default) + repository/DAO;
  migration backfills the existing per-account signature as the default.
- Rich signatures reuse the #36 editor; a Signatures management screen (list,
  add/edit/delete, set default) is linked from per-account settings.
- The account's default signature auto-inserts on new compose / reply / forward
  (honoring the enable toggle), placed above the quote, and stays editable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:30:33 -05:00
JMR-devandClaude Opus 4.8 e2e2c36d1d feat(onboarding): first-run flow, vendor picker, and app-password setup
Implements the onboarding epic (#26–#31) as a single feature:

- #26 First-run nav scaffold: gate the start destination on the account
  count (no accounts -> onboarding, else mailbox) via AppViewModel, holding
  render until the count is known so there is no cold-start flash. Onboarding
  is a nested nav graph with a graph-scoped OnboardingViewModel tracking the
  first account added this session. Removes NoAccountState in favour of a
  shared welcome/empty state.
- #28 Provider registry: MailProvider presets for Gmail/Yahoo/iCloud
  (IMAP+SMTP host/port/security, help URL) mirroring Account.outlook, biased
  to STARTTLS where documented; host/port/security unit-tested.
- #27 Vendor picker: AccountPickerScreen replaces the two-button setup screen
  as the single entry point (onboarding + Settings/mailbox "Add account"),
  routing Outlook -> OAuth, Gmail/Yahoo/iCloud -> app-password, Other -> manual.
- #29 App-password guided screen: one reusable screen per provider key with
  explanation + security warning + help link; verifies/persists via
  addImapAccount. ViewModel unit tests cover valid/invalid/failure paths.
- #30 "Add another?" prompt + first-account landing: after each onboarding
  add, offer Yes (back to picker) / No (open the first account's inbox,
  per-account filtered via a MAILBOX account nav arg). Only inside onboarding.
- #31 Instrumented onboarding E2E (welcome -> picker -> app-password add ->
  add-another -> first inbox); managed-device list and CI matrix already in
  lockstep. All new files carry the SPDX header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:13:32 -05:00
JMR-devandClaude Opus 4.8 51c790d6bf feat(mailto): handle mailto: links and email share intents
Add intent filters so LibreMail handles mailto: (ACTION_VIEW / SENDTO)
and email SEND / SEND_MULTIPLE intents, opening a prefilled compose screen.

- MailtoParser: pure RFC 6068 parser (multiple recipients, to/cc/bcc/
  subject/body, percent-encoding; preserves a literal '+'); JVM-tested.
- IntentComposeParser: builds a ComposePrefill from a mailto: URI or the
  EXTRA_EMAIL/CC/BCC/SUBJECT/TEXT share extras.
- MainActivity parses the launch/new intent and hands a one-shot prefill to
  the NavHost (guarded against config-change duplication).
- Compose form gains a Bcc field; Routes carry cc/bcc/body deep-link args.
- bcc wired end-to-end: OutgoingMessage, SMTP + Graph senders, and outbox +
  drafts persistence via Room migration v9 -> v10.
- Multi-account send is served by the existing From picker on compose.

Default mail app: Android exposes no public RoleManager email role, so the
intent filters are what make LibreMail appear on the system "Open by default"
/ default-apps screen where the platform/OEM supports it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:08:06 -05:00
JMR-devandClaude Opus 4.8 d1b0593a8c feat(reporting): opt-in debug reporting client (capture + review/submit)
Implements #32 and #33: a strictly opt-in, F-Droid-safe debug reporting
client. Nothing ever leaves the device unless the user taps Submit.

#32 capture:
- CrashReporter installs a Thread.setDefaultUncaughtExceptionHandler (wired in
  LibreMailApplication) that persists a structured crash record (stack trace +
  app/version/device metadata + recent log ring buffer) locally, then delegates
  to the previous handler. Never auto-sent.
- RingLogBuffer + AppLog: a bounded in-memory, non-PII log ring buffer.
- DiagnosticsCollector assembles a minimal bundle: app version, Android/device,
  a fixed non-PII settings allow-list, and the log buffer.
- ReportStore persists pending reports as JSON files (not Room, so crash-time
  saves are robust and independent of the encrypted/migrating DB).
- "Report a problem" entry point in Settings creates a report on demand.

#33 review & submit:
- ReportReviewScreen shows the full payload verbatim (exactly what would be
  sent), a free-text comment field, and a prominent PII disclaimer, with
  explicit Submit / Discard and Copy / Save-to-file alternatives.
- Submission is user-initiated only: ReportUploadWorker (WorkManager, queue +
  retry, success/failure surfaced) POSTs to BuildConfig.DEBUG_REPORT_ENDPOINT,
  which is EMPTY by default (ingest server #34 is out of scope for this repo).
- On next launch a saved crash report is offered for review via a dialog.

Tests: 23 JVM unit tests covering crash capture + persistence (offered next
launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and
the "nothing sent without Submit" invariant.

Closes #32
Closes #33

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:03:13 -05:00
JMR-devandClaude Opus 4.8 ffbb882227 feat(backup): opt-in Android Backup for settings only
Add an opt-in (off by default) toggle to include app data in system
Android Backup / Auto Backup, gated so only re-creatable user
preferences are ever backed up.

- Flip allowBackup to true and add LibreMailBackupAgent, which enforces
  the runtime opt-in: onFullBackup runs only when the user enables
  "Include settings in Android Backup" (default off), so no data leaves
  the device otherwise. allowBackup is a manifest flag and can't be
  toggled at runtime, hence the agent.
- Rewrite data_extraction_rules.xml (API 31+) and add backup_rules.xml
  (API 29-30) as strict allowlists that back up ONLY the
  libremail_settings DataStore. The Keystore-sealed cache passphrase
  (libremail_dbkey) and the encrypted credentials + mail-cache database
  (libremail.db) are excluded by omission; the cache re-downloads on
  next sync.
- Add includeInBackup preference + setter (nudges BackupManager on
  change) and a "Backup" settings section with F-Droid-honest copy
  (off by default, uses Google infrastructure).
- BackupPolicy is the single source of truth for eligible/excluded
  paths; unit tests cover the toggle default and assert the shipped XML
  resources include only settings and never the secrets/DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:56:18 -05:00
JMR-devandClaude Opus 4.8 657af48052 refactor(auth): remove dead Gmail OAuth code path
Gmail authenticates via app password + preconfigured IMAP/SMTP (decision
in #9), never OAuth, so the Gmail-OAuth implementation was unreachable
dead code. Remove it while keeping Outlook's AppAuth OAuth path intact.

- Delete auth/GmailAuthManager.kt (shared OAuthResult/FreshToken kept).
- Drop AuthType.OAUTH_GMAIL and its exhaustive `when` branch, the
  gmailAuthManager injection, and the SCOPE_GMAIL constant in
  MailConnectionFactory.
- Remove GMAIL_OAUTH_* BuildConfig fields, gmailOAuthClientId, and the
  gmailRedirectScheme val from app/build.gradle.kts.
- Repoint the AppAuth manifestPlaceholders["appAuthRedirectScheme"] to
  the Outlook scheme (org.libremail.outlook). AppAuth's bundled manifest
  now registers that scheme on RedirectUriReceiverActivity, so the app
  manifest's now-redundant Outlook intent-filter is removed; the
  AppCompat theme override (crash fix) is preserved. Verified the merged
  manifest.
- Drop GMAIL_OAUTH_CLIENT_ID from secrets.properties.example.

authType persists as the enum name in a TEXT column, so removing a
constant needs no Room schema change or migration.

Closes #39

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:44:17 -05:00