From 2820795c38922cc507c8a1a5bfb5e4c44e8c7ce0 Mon Sep 17 00:00:00 2001 From: ilyk Date: Thu, 2 Jul 2026 16:32:51 -0700 Subject: [PATCH 01/18] chore: commit real asgard-mail app crate + repo hygiene (WP-0.1) The working tree held the actual GTK4 application (asgard-mail/) as untracked while the git index still described the deleted app/ and simple-gtk-app/ prototype trees. This commits the real codebase so the repository reflects what is actually built. - Add asgard-mail/ (main binary), com.asgard.Mail.metainfo.xml, and the untracked core modules (mail_rules, smart_mailbox, pgp) + ui-components - Remove the obsolete app/ prototype tree (55 files) - Commit Cargo.lock (binary app: lockfile belongs in VCS; also required for the Flatpak offline build) - Fix .gitmodules to an https URL so anonymous clone --recursive works - Untrack logs/, remove debug_output*.txt and Cargo-no-webkit.toml - Move stale root status docs to docs/history/ Part of the production-readiness plan (Phase 0). See EXECUTION_PLAN.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM --- .agent/checkpoints/ACTIVE.json | 45 + ...aned-resize-20260123-212800-completed.json | 79 + .../secrets-cleanup-20260123-completed.json | 36 + .agent/findings/KNOWN.md | 7 + .../P2-07-field-search-implementation.md | 117 + .agent/status.json | 66 + .gitignore | 2 +- .gitmodules | 2 +- Cargo-no-webkit.toml | 101 - Cargo.lock | 970 +++++++-- Cargo.toml | 10 +- app/Cargo.toml | 48 - app/src/app.rs | 364 ---- app/src/dbus_service.rs | 149 -- app/src/email_backend.rs | 589 ----- app/src/main.rs | 126 -- app/src/notifications.rs | 131 -- app/src/theming/asgard.css | 355 --- app/src/theming/mod.rs | 17 - app/src/thread_helpers.rs | 259 --- app/src/tray.rs | 114 - app/src/widgets/mailbox_tree.rs | 274 --- app/src/widgets/message_card.rs | 572 ----- app/src/widgets/message_header.rs | 172 -- app/src/widgets/message_list.rs | 379 ---- app/src/widgets/message_view.rs | 397 ---- app/src/widgets/mod.rs | 15 - app/src/widgets/search_bar.rs | 141 -- app/src/widgets/status_bar.rs | 137 -- app/src/windows/account_wizard.rs | 158 -- app/src/windows/compose_window.rs | 143 -- app/src/windows/main_window.rs | 322 --- app/src/windows/mod.rs | 11 - app/src/windows/preferences_window.rs | 129 -- asgard-mail/.agent/findings/KNOWN.md | 7 + asgard-mail/.agent/status.json | 130 ++ {simple-gtk-app => asgard-mail}/.env.example | 0 {simple-gtk-app => asgard-mail}/Cargo.toml | 10 +- {simple-gtk-app => asgard-mail}/build.rs | 0 ...2026-02-04-timing-instrumentation-added.md | 96 + ...6-02-04-conversation-fetch-optimization.md | 220 ++ ...026-02-04-message-card-widget-reuse-fix.md | 101 + .../2026-02-04-thread-caching-optimization.md | 119 + ...6-02-04-timing-instrumentation-analysis.md | 192 ++ .../examples/pid_query.rs | 0 .../examples/proton_event_streaming.rs | 173 ++ asgard-mail/examples/test_decryption.rs | 235 ++ asgard-mail/examples/test_decryption_env.rs | 227 ++ .../src/backend_bridge.rs | 244 ++- asgard-mail/src/category_detector.rs | 113 + {app => asgard-mail}/src/constants.rs | 5 +- asgard-mail/src/dbus_service.rs | 761 +++++++ .../src/dialogs/account_setup.rs | 110 +- asgard-mail/src/dialogs/compose.rs | 980 +++++++++ asgard-mail/src/dialogs/mail_rules.rs | 615 ++++++ .../src/dialogs/mod.rs | 8 + .../src/dialogs/password_dialog.rs | 0 asgard-mail/src/dialogs/rule_logs.rs | 245 +++ .../src/dialogs/settings.rs | 33 + asgard-mail/src/dialogs/smart_mailbox.rs | 509 +++++ .../src/goa_ffi.rs | 0 .../src/html_render.rs | 0 {simple-gtk-app => asgard-mail}/src/lib.rs | 0 {simple-gtk-app => asgard-mail}/src/main.rs | 1936 ++++++++++++++++- asgard-mail/src/network.rs | 93 + asgard-mail/src/spell_checker.rs | 318 +++ {simple-gtk-app => asgard-mail}/src/style.css | 0 .../src/sync_events.rs | 0 .../src/sync_service.rs | 134 +- .../src/theming/asgard.css | 6 + .../src/theming/mod.rs | 0 .../src/thread_helpers.rs | 0 .../src/threading_test.rs | 0 asgard-mail/src/tray.rs | 176 ++ .../src/widgets/message_card.rs | 276 +++ .../src/widgets/message_header.rs | 0 .../src/widgets/message_list.rs | 0 .../src/widgets/message_view.rs | 0 .../src/widgets/mod.rs | 4 +- asgard-mail/src/widgets/search_bar.rs | 100 + .../src/widgets/secure_webview.rs | 73 +- .../src/widgets/sidebar.rs | 52 +- .../src/widgets/trust.rs | 0 .../src/widgets/trust_panel.rs | 0 assets/asgard-mail.desktop | 4 +- com.asgard.Mail.json | 34 +- com.asgard.Mail.metainfo.xml | 26 + core/Cargo.toml | 6 +- core/src/account.rs | 6 + core/src/error.rs | 9 + core/src/lib.rs | 13 +- core/src/mail_rules.rs | 483 ++++ core/src/pgp.rs | 326 +++ core/src/search/mod.rs | 4 +- core/src/search/simple_index.rs | 567 ++++- core/src/search/tantivy_index.rs | 187 +- core/src/smart_mailbox.rs | 335 +++ core/src/storage/cache.rs | 14 +- core/src/storage/database.rs | 1244 ++++++++++- core/src/storage/migrations.rs | 228 +- core/src/storage/mod.rs | 2 +- core/src/sync/imap_sync.rs | 501 +++-- core/src/sync/mod.rs | 4 +- core/src/sync/smtp_send.rs | 139 +- core/src/sync/sync_manager.rs | 77 +- docs/DBUS_SIGNALS.md | 220 ++ email-backend/src/lib.rs | 13 + .../2026-01-14-proton-decryption-research.md | 201 ++ evidence/solutions/2026-01-13-layout-fix.md | 140 ++ .../2026-02-04-move-to-mailbox-wiring-fix.md | 97 + ...2-17-notification-badges-implementation.md | 97 + oauth/src/gmail_api.rs | 71 + simple-gtk-app/src/constants.rs | 4 - simple-gtk-app/src/dbus_service.rs | 234 -- ui-components/src/account_widget.rs | 28 + ui-components/src/message_widget.rs | 28 + ui-components/src/sync_widget.rs | 28 + 117 files changed, 14009 insertions(+), 6099 deletions(-) create mode 100644 .agent/checkpoints/ACTIVE.json create mode 100644 .agent/checkpoints/archive/paned-resize-20260123-212800-completed.json create mode 100644 .agent/checkpoints/archive/secrets-cleanup-20260123-completed.json create mode 100644 .agent/findings/KNOWN.md create mode 100644 .agent/findings/P2-07-field-search-implementation.md create mode 100644 .agent/status.json delete mode 100644 Cargo-no-webkit.toml delete mode 100644 app/Cargo.toml delete mode 100644 app/src/app.rs delete mode 100644 app/src/dbus_service.rs delete mode 100644 app/src/email_backend.rs delete mode 100644 app/src/main.rs delete mode 100644 app/src/notifications.rs delete mode 100644 app/src/theming/asgard.css delete mode 100644 app/src/theming/mod.rs delete mode 100644 app/src/thread_helpers.rs delete mode 100644 app/src/tray.rs delete mode 100644 app/src/widgets/mailbox_tree.rs delete mode 100644 app/src/widgets/message_card.rs delete mode 100644 app/src/widgets/message_header.rs delete mode 100644 app/src/widgets/message_list.rs delete mode 100644 app/src/widgets/message_view.rs delete mode 100644 app/src/widgets/mod.rs delete mode 100644 app/src/widgets/search_bar.rs delete mode 100644 app/src/widgets/status_bar.rs delete mode 100644 app/src/windows/account_wizard.rs delete mode 100644 app/src/windows/compose_window.rs delete mode 100644 app/src/windows/main_window.rs delete mode 100644 app/src/windows/mod.rs delete mode 100644 app/src/windows/preferences_window.rs create mode 100644 asgard-mail/.agent/findings/KNOWN.md create mode 100644 asgard-mail/.agent/status.json rename {simple-gtk-app => asgard-mail}/.env.example (100%) rename {simple-gtk-app => asgard-mail}/Cargo.toml (85%) rename {simple-gtk-app => asgard-mail}/build.rs (100%) create mode 100644 asgard-mail/evidence/findings/2026-02-04-timing-instrumentation-added.md create mode 100644 asgard-mail/evidence/solutions/2026-02-04-conversation-fetch-optimization.md create mode 100644 asgard-mail/evidence/solutions/2026-02-04-message-card-widget-reuse-fix.md create mode 100644 asgard-mail/evidence/solutions/2026-02-04-thread-caching-optimization.md create mode 100644 asgard-mail/evidence/solutions/2026-02-04-timing-instrumentation-analysis.md rename {simple-gtk-app => asgard-mail}/examples/pid_query.rs (100%) create mode 100644 asgard-mail/examples/proton_event_streaming.rs create mode 100644 asgard-mail/examples/test_decryption.rs create mode 100644 asgard-mail/examples/test_decryption_env.rs rename {simple-gtk-app => asgard-mail}/src/backend_bridge.rs (91%) create mode 100644 asgard-mail/src/category_detector.rs rename {app => asgard-mail}/src/constants.rs (61%) create mode 100644 asgard-mail/src/dbus_service.rs rename {simple-gtk-app => asgard-mail}/src/dialogs/account_setup.rs (87%) create mode 100644 asgard-mail/src/dialogs/compose.rs create mode 100644 asgard-mail/src/dialogs/mail_rules.rs rename {simple-gtk-app => asgard-mail}/src/dialogs/mod.rs (63%) rename {simple-gtk-app => asgard-mail}/src/dialogs/password_dialog.rs (100%) create mode 100644 asgard-mail/src/dialogs/rule_logs.rs rename {simple-gtk-app => asgard-mail}/src/dialogs/settings.rs (91%) create mode 100644 asgard-mail/src/dialogs/smart_mailbox.rs rename {simple-gtk-app => asgard-mail}/src/goa_ffi.rs (100%) rename {simple-gtk-app => asgard-mail}/src/html_render.rs (100%) rename {simple-gtk-app => asgard-mail}/src/lib.rs (100%) rename {simple-gtk-app => asgard-mail}/src/main.rs (71%) create mode 100644 asgard-mail/src/network.rs create mode 100644 asgard-mail/src/spell_checker.rs rename {simple-gtk-app => asgard-mail}/src/style.css (100%) rename {simple-gtk-app => asgard-mail}/src/sync_events.rs (100%) rename {simple-gtk-app => asgard-mail}/src/sync_service.rs (86%) rename {simple-gtk-app => asgard-mail}/src/theming/asgard.css (99%) rename {simple-gtk-app => asgard-mail}/src/theming/mod.rs (100%) rename {simple-gtk-app => asgard-mail}/src/thread_helpers.rs (100%) rename {simple-gtk-app => asgard-mail}/src/threading_test.rs (100%) create mode 100644 asgard-mail/src/tray.rs rename {simple-gtk-app => asgard-mail}/src/widgets/message_card.rs (77%) rename {simple-gtk-app => asgard-mail}/src/widgets/message_header.rs (100%) rename {simple-gtk-app => asgard-mail}/src/widgets/message_list.rs (100%) rename {simple-gtk-app => asgard-mail}/src/widgets/message_view.rs (100%) rename {simple-gtk-app => asgard-mail}/src/widgets/mod.rs (82%) create mode 100644 asgard-mail/src/widgets/search_bar.rs rename {simple-gtk-app => asgard-mail}/src/widgets/secure_webview.rs (84%) rename {simple-gtk-app => asgard-mail}/src/widgets/sidebar.rs (67%) rename {simple-gtk-app => asgard-mail}/src/widgets/trust.rs (100%) rename {simple-gtk-app => asgard-mail}/src/widgets/trust_panel.rs (100%) create mode 100644 com.asgard.Mail.metainfo.xml create mode 100644 core/src/mail_rules.rs create mode 100644 core/src/pgp.rs create mode 100644 core/src/smart_mailbox.rs create mode 100644 docs/DBUS_SIGNALS.md create mode 100644 evidence/findings/2026-01-14-proton-decryption-research.md create mode 100644 evidence/solutions/2026-01-13-layout-fix.md create mode 100644 evidence/solutions/2026-02-04-move-to-mailbox-wiring-fix.md create mode 100644 evidence/solutions/2026-02-17-notification-badges-implementation.md delete mode 100644 simple-gtk-app/src/constants.rs delete mode 100644 simple-gtk-app/src/dbus_service.rs create mode 100644 ui-components/src/account_widget.rs create mode 100644 ui-components/src/message_widget.rs create mode 100644 ui-components/src/sync_widget.rs diff --git a/.agent/checkpoints/ACTIVE.json b/.agent/checkpoints/ACTIVE.json new file mode 100644 index 0000000..55144f2 --- /dev/null +++ b/.agent/checkpoints/ACTIVE.json @@ -0,0 +1,45 @@ +{ + "task_id": "P2-07-field-search", + "task_description": "Implement field-specific search filters supporting from:, to:, subject:, has:attachment prefixes", + "agent": "orchestrator", + "status": "completed", + "created_at": "2026-02-17T12:00:00Z", + "updated_at": "2026-02-17T12:15:00Z", + "progress": { + "current_step": 3, + "total_steps": 3, + "percent": 100 + }, + "completed_steps": [ + { + "step": 1, + "name": "Implement query parser", + "description": "Added ParsedQuery struct and parse_search_query() helper to simple_index.rs" + }, + { + "step": 2, + "name": "Integrate parser into search method", + "description": "Modified search() method to use parsed query for field-specific matching" + }, + { + "step": 3, + "name": "Add tests for field filters", + "description": "Added 10 comprehensive unit tests for from:, to:, subject:, has:attachment parsing and matching" + } + ], + "remaining_steps": [], + "working_data": { + "files_modified": [ + "/home/ilyk/ai-projects/active/product/linux/mailapp/repo/core/src/search/simple_index.rs" + ], + "files_checked_no_changes": [ + "/home/ilyk/ai-projects/active/product/linux/mailapp/repo/asgard-mail/src/widgets/search_bar.rs" + ] + }, + "context_hints": { + "implementation": "Added field-specific prefix parsing on top of existing SearchQuery filtering", + "backward_compatibility": "Plain text queries without prefixes continue to work as before", + "build_status": "cargo build --lib -p asgard-core: SUCCESS" + }, + "notes": "Implementation is complete and backward-compatible. Field-specific filters (from:, to:, subject:, has:attachment) can now be used in search queries." +} diff --git a/.agent/checkpoints/archive/paned-resize-20260123-212800-completed.json b/.agent/checkpoints/archive/paned-resize-20260123-212800-completed.json new file mode 100644 index 0000000..f90b9bb --- /dev/null +++ b/.agent/checkpoints/archive/paned-resize-20260123-212800-completed.json @@ -0,0 +1,79 @@ +{ + "checkpoint_version": "1.0", + "task_id": "paned-resize-20260123-212800", + "agent": "dev-rust", + "task_description": "Fix paned resize behavior between middle (message list) and right (reader) panels", + + "started_at": "2026-01-23T21:28:00Z", + "updated_at": "2026-01-23T21:45:00Z", + "status": "completed", + + "progress": { + "current_phase": "completed", + "current_step": 6, + "total_steps": 6, + "percent_complete": 100 + }, + + "completed_steps": [ + { + "step": 1, + "name": "identify_issue", + "description": "User reported resize handle between center and right panels not working", + "result": "success", + "timestamp": "2026-01-23T21:28:00Z" + }, + { + "step": 2, + "name": "fix_resize_config", + "description": "Changed resize_start_child to true, increased MESSAGE_LIST_MAX to 600", + "result": "success", + "timestamp": "2026-01-23T21:30:00Z" + }, + { + "step": 3, + "name": "fix_overlap_issue", + "description": "Middle pane was covered by left pane - added hexpand to inner_paned, removed hexpand from reader", + "result": "success", + "timestamp": "2026-01-23T21:33:00Z" + }, + { + "step": 4, + "name": "fix_sender_ellipsize", + "description": "Sender labels were preventing shrink - added ellipsize(End) to both sender labels", + "result": "success", + "timestamp": "2026-01-23T21:36:00Z" + }, + { + "step": 5, + "name": "fix_slide_under", + "description": "Pane was sliding under sidebar - fixed by setting width_request on inner_paned = 200 + MESSAGE_VIEW_MIN (550px)", + "result": "success", + "timestamp": "2026-01-23T21:44:00Z" + }, + { + "step": 6, + "name": "verify", + "description": "User confirmed resize works correctly", + "result": "success", + "timestamp": "2026-01-23T21:45:00Z" + } + ], + + "solution_summary": { + "problem": "Resize handle between message list and reader panel not working, panes overlapping and sliding under", + "root_causes": [ + "resize_start_child was false - prevented handle dragging", + "sender labels missing ellipsize - forced minimum width too large", + "inner_paned had no width_request - allowed it to be squeezed causing slide-under" + ], + "fixes_applied": [ + "main.rs:2496-2497 - set resize_start_child(true) and resize_end_child(true)", + "main.rs:2498 - added hexpand(true) to inner_paned", + "main.rs:2500 - added width_request(200 + MESSAGE_VIEW_MIN) to inner_paned", + "main.rs:3593 - added ellipsize(End) to sender_label in message rows", + "main.rs:3732 - added ellipsize(End) to sender_label in sub-rows", + "main.rs:2483 - reduced messages_container width_request to 200" + ] + } +} diff --git a/.agent/checkpoints/archive/secrets-cleanup-20260123-completed.json b/.agent/checkpoints/archive/secrets-cleanup-20260123-completed.json new file mode 100644 index 0000000..de37e92 --- /dev/null +++ b/.agent/checkpoints/archive/secrets-cleanup-20260123-completed.json @@ -0,0 +1,36 @@ +{ + "checkpoint_version": "1.0", + "task_id": "secrets-cleanup-20260123-214500", + "agent": "orchestrator", + "task_description": "Remove Google OAuth secrets from git history and move to secure config", + + "started_at": "2026-01-23T21:45:00Z", + "updated_at": "2026-01-23T21:45:00Z", + "status": "in_progress", + + "identity_reminder": "YOU ARE ORCHESTRATOR. DELEGATE.", + + "progress": { + "current_phase": "planning", + "current_step": 1, + "total_steps": 4, + "percent_complete": 0 + }, + + "pending_steps": [ + {"step": 1, "name": "extract_secrets", "description": "Find and extract OAuth secrets from main.rs"}, + {"step": 2, "name": "secure_storage", "description": "Move secrets to gitignored config file or env vars"}, + {"step": 3, "name": "rewrite_history", "description": "Use git filter-repo to remove secrets from all commits"}, + {"step": 4, "name": "force_push", "description": "Force push cleaned history to remote"} + ], + + "working_data": { + "affected_files": ["simple-gtk-app/src/main.rs"], + "secret_types": ["Google OAuth Client ID", "Google OAuth Client Secret"], + "affected_commits": ["e3d3581", "83e5993"], + "lines_with_secrets": [1247, 1248, 1341, 1342, 1456, 1457, 1554, 1555] + }, + + "recovery_instructions": "Check if secrets have been moved to config, check if git history has been rewritten", + "rollback_instructions": "Secrets are already in git history - no rollback needed for this cleanup" +} diff --git a/.agent/findings/KNOWN.md b/.agent/findings/KNOWN.md new file mode 100644 index 0000000..eae11c7 --- /dev/null +++ b/.agent/findings/KNOWN.md @@ -0,0 +1,7 @@ +# Known Findings Registry + +**Check this file BEFORE reporting any finding as new.** + +## Active Findings + + diff --git a/.agent/findings/P2-07-field-search-implementation.md b/.agent/findings/P2-07-field-search-implementation.md new file mode 100644 index 0000000..5127010 --- /dev/null +++ b/.agent/findings/P2-07-field-search-implementation.md @@ -0,0 +1,117 @@ +# Implementation: Field-Specific Search Filters (P2-07) + +**Completed**: 2026-02-17 +**Task**: Support `from:`, `to:`, `subject:`, `has:attachment` prefixes in search queries + +## Summary + +Implemented field-specific search filter parsing on top of the SimpleSearchIndex to support advanced query syntax. Users can now search using: +- `from:user@example.com` - Filter by sender +- `to:user@example.com` - Filter by recipient +- `subject:keyword` - Filter by subject +- `has:attachment` or `has:attachments` - Filter messages with attachments +- Plain text terms (backward compatible) + +## Implementation Details + +### Files Modified +- `/home/ilyk/ai-projects/active/product/linux/mailapp/repo/core/src/search/simple_index.rs` + +### Changes Made + +1. **ParsedQuery struct** (lines 9-22) + - Data structure to hold parsed field-specific filters + - Fields: `terms`, `from_filter`, `to_filter`, `subject_filter`, `has_attachment` + +2. **parse_search_query() function** (lines 24-60) + - Tokenizes query string by whitespace + - Strips recognized prefixes: `from:`, `to:`, `subject:`, `has:attachment` + - Collects remaining tokens as search terms + - Case-insensitive matching + +3. **Modified search() method** (lines 167-285) + - Parses query at start using `parse_search_query()` + - Applies field-specific filters before general text search + - Handles `has:attachment` prefix as override to existing filter + - Maintains backward compatibility with plain text queries + +### Design Decisions + +1. **Field filters are exact/substring matches** - Uses `contains()` for flexible matching + - `from:alice` matches `alice@example.com` + - `subject:report` matches "Quarterly Report" + +2. **Mixed queries work** - Can combine field filters with plain text + - `from:alice@example.com report` - Emails from Alice containing "report" + - `subject:meeting has:attachment` - Meetings with attachments + +3. **Backward compatible** - Plain text queries unaffected + - Existing queries like `"hello world"` work as before + +## Test Coverage + +Added 10 comprehensive unit tests (lines 454-815): +1. `test_parse_search_query_plain_text` - Plain text parsing +2. `test_parse_search_query_from_filter` - from: prefix parsing +3. `test_parse_search_query_to_filter` - to: prefix parsing +4. `test_parse_search_query_subject_filter` - subject: prefix parsing +5. `test_parse_search_query_has_attachment` - has:attachment parsing +6. `test_parse_search_query_has_attachments` - has:attachments variant +7. `test_parse_search_query_mixed_filters` - Mixed filter queries +8. `test_search_with_from_filter` - from: filter matching +9. `test_search_with_from_filter_no_match` - from: filter no results +10. `test_search_with_subject_filter` - subject: filter matching +11. `test_search_with_to_filter` - to: filter matching +12. `test_search_with_has_attachment_filter` - has:attachment filter matching +13. `test_search_combined_filters` - Multiple filters together + +## Build Status + +``` +cargo build --lib -p asgard-core: SUCCESS +``` + +No breaking changes. Build produces no errors related to search module. + +## Files Checked (No Changes Needed) +- `/home/ilyk/ai-projects/active/product/linux/mailapp/repo/asgard-mail/src/widgets/search_bar.rs` + - Simply passes query string through to search layer + - No UI changes needed + +## Usage Examples + +```rust +// From Alice about "budget" +let query = SearchQuery { + query: "from:alice@example.com budget".to_string(), + ..Default::default() +}; + +// Messages with attachments +let query = SearchQuery { + query: "has:attachment".to_string(), + ..Default::default() +}; + +// Complex query +let query = SearchQuery { + query: "from:alice@example.com to:bob subject:report has:attachment".to_string(), + ..Default::default() +}; +``` + +## Performance Implications + +- Query parsing: O(n) where n = number of whitespace-separated tokens (typically <10) +- Filter application: O(m) where m = number of indexed messages (existing) +- No additional memory overhead beyond ParsedQuery struct (~100 bytes) +- No database changes required + +## Future Enhancements + +Possible extensions (not implemented): +- `before:date` / `after:date` - Date range filtering +- `size:>1MB` - Attachment size filtering +- `is:flagged` / `is:unread` - State-based filtering (already supported via SearchQuery) +- Quoted phrases: `"exact phrase"` +- Negation: `-keyword` or `not:keyword` diff --git a/.agent/status.json b/.agent/status.json new file mode 100644 index 0000000..503966c --- /dev/null +++ b/.agent/status.json @@ -0,0 +1,66 @@ +{ + "updated_at": "2026-02-19T12:56:46.457582", + "orchestrator_pid": 3202884, + "active_agents": { + "fix-p3-01/p3-10-remaining-gaps": { + "type": "dev-rust", + "description": "Fix P3-01/P3-10 remaining gaps", + "prompt_preview": "Fix remaining gaps from three parallel agents that partially completed their work on Asgard Mail (GTK4/libadwaita Rust mail client). There are 3 things to fix:\n\n## 1. Register smart_mailbox module in ", + "started_at": "2026-02-17T15:38:35.176486", + "status": "running", + "tool_uses": 0 + }, + "p3-05:-offline-mode-implementa": { + "type": "dev-rust", + "description": "P3-05: Offline mode implementation", + "prompt_preview": "Implement Offline Mode (P3-05) for Asgard Mail, a GTK4/libadwaita Rust desktop mail client.\n\n## Goal\nQueue sends when offline using gio::NetworkMonitor, attempt delivery when connectivity is restored,", + "started_at": "2026-02-17T15:49:05.748517", + "status": "running", + "tool_uses": 0 + }, + "p3-02:-mail-rules-engine": { + "type": "dev-rust", + "description": "P3-02: Mail rules engine", + "prompt_preview": "Implement Mail Rules (P3-02) for Asgard Mail, a GTK4/libadwaita Rust desktop mail client.\n\n## Goal\nAdd auto-sort/auto-flag rules that run on incoming messages. Create a rules model, database table, an", + "started_at": "2026-02-17T15:52:57.261947", + "status": "running", + "tool_uses": 0 + }, + "validate-gap-closure-features": { + "type": "Explore", + "description": "Validate gap closure features", + "prompt_preview": "Thoroughly verify that all 30 features from the Asgard Mail Gap Closure Plan are implemented in the codebase at /home/ilyk/ai-projects/active/product/linux/mailapp/repo. Check for the presence of each", + "started_at": "2026-02-17T16:18:11.453099", + "status": "running", + "tool_uses": 0 + }, + "fix-21-test-compilation-errors": { + "type": "dev-rust", + "description": "Fix 21 test compilation errors", + "prompt_preview": "Fix all 21 test compilation errors in the asgard-core crate at `/home/ilyk/ai-projects/active/product/linux/mailapp/repo/core`.\n\nRun `cargo test -p asgard-core` first to see all errors. Then fix each ", + "started_at": "2026-02-19T12:53:37.716472", + "status": "running", + "tool_uses": 0 + }, + "update-project-docs": { + "type": "technical-writer", + "description": "Update project docs", + "prompt_preview": "Update 3 documentation files for the Asgard Mail project at `/home/ilyk/ai-projects/active/product/linux/mailapp`. The project just completed a massive \"Gap Closure Plan\" implementing ~30 features acr", + "started_at": "2026-02-19T12:53:43.032368", + "status": "running", + "tool_uses": 0 + }, + "document-stub-vs-wired-feature": { + "type": "Explore", + "description": "Document stub vs wired features", + "prompt_preview": "Analyze the Asgard Mail codebase at `/home/ilyk/ai-projects/active/product/linux/mailapp/repo` to determine which of the 30 gap closure features are fully wired vs UI-only stubs. \n\nFor each feature, c", + "started_at": "2026-02-19T12:54:36.967237", + "status": "running", + "tool_uses": 0 + } + }, + "completed_agents": [], + "current_activity": "Writing: 2026-02-19-gap-closure-feature-wiring-status.md", + "total_tool_uses": 567, + "session_start": "2026-02-17T14:18:44.041568" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index dd1a890..d48e708 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Rust build artifacts target/ -Cargo.lock +# Cargo.lock is committed: this workspace produces a binary app # IDE files .vscode/ diff --git a/.gitmodules b/.gitmodules index 70855e2..8b4c50d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "proton-api-rs"] path = proton-api-rs - url = git@github.com:ilyk/proton-api-rs.git + url = https://github.com/ilyk/proton-api-rs.git diff --git a/Cargo-no-webkit.toml b/Cargo-no-webkit.toml deleted file mode 100644 index 2276b21..0000000 --- a/Cargo-no-webkit.toml +++ /dev/null @@ -1,101 +0,0 @@ -[workspace] -members = [ - "app", - "core", - "oauth", - "ui-components", - "theming", - "crates/pop3", - "minimal-core", - "minimal-app", -] -resolver = "2" - -[workspace.package] -version = "0.1.0" -edition = "2021" -authors = ["Asgard Mail Team"] -description = "A modern, secure email client for GNOME" -license = "MIT" -repository = "https://github.com/asgard/mail" -keywords = ["email", "mail", "gtk4", "gnome", "imap", "smtp"] -categories = ["network-programming", "web-programming"] - -# Common dependencies -[workspace.dependencies] -# Core -tokio = { version = "1.0", features = ["full"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0" -thiserror = "1.0" - -# GTK4 and libadwaita -gtk4 = { version = "0.7", features = ["v4_8"] } -libadwaita = { version = "0.5", features = ["v1_2"] } - -# Email protocols -async-imap = "0.9" -lettre = { version = "0.11", features = ["tokio1-native-tls"] } -native-tls = "0.2" -rustls = "0.21" -oauth2 = "4.4" -mailparse = "0.14" -addr = "0.4" -mime = "0.3" - -# Storage and search -rusqlite = { version = "0.31", features = ["bundled"] } -tantivy = "0.21" -sha2 = "0.10" - -# Crypto and security -keyring = "2.3" -sodiumoxide = "0.2" -rustls-pemfile = "1.0" -time = { version = "0.3", features = ["formatting", "serde"] } -uuid = { version = "1.0", features = ["v4", "serde"] } - -# Networking -reqwest = { version = "0.11", features = ["json", "rustls-tls"] } -url = "2.4" -bytes = "1.0" - -# Async utilities -futures = "0.3" -tokio-stream = "0.1" -tokio-sync = "0.1" -tokio-util = "0.7" -async-trait = "0.1" - -# Data structures -indexmap = "2.0" -dashmap = "5.5" -parking-lot = "0.12" - -# Configuration and paths -directories = "5.0" -config = "0.14" - -# Logging and tracing -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# Serialization -tokio-serde = "0.8" - -# Utilities -regex = "1.10" -base64 = "0.21" -hex = "0.4" - -# Development -tempfile = "3.8" -mockall = "0.12" - -# Development profiles -[profile.dev] -opt-level = 0 - -[profile.release] -opt-level = 3 diff --git a/Cargo.lock b/Cargo.lock index dd54902..82385d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -167,6 +168,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.5.3" @@ -180,6 +190,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "asgard-core" version = "0.1.0" @@ -209,14 +228,17 @@ dependencies = [ "rusqlite", "rustls 0.21.12", "rustls-pemfile", + "sequoia-openpgp", "serde", "serde_json", "sha2", "sodiumoxide", + "tantivy", "tempfile", "thiserror 1.0.69", "time", "tokio", + "tokio-native-tls", "tokio-serde", "tokio-stream", "tokio-sync", @@ -245,25 +267,38 @@ dependencies = [ name = "asgard-mail" version = "0.1.0" dependencies = [ - "anyhow", + "ammonia", "asgard-core", - "async-trait", - "clap", - "futures 0.3.31", + "asgard-oauth", + "dateparser", + "dirs", + "dotenvy", + "email-backend", + "enchant", + "gio", + "glib", + "glib-build-tools", "gtk4", + "html2text", + "keyring", + "ksni", "libadwaita", + "libc", + "log", "notify-rust", "open", + "proton-api-rs", + "pulldown-cmark", "regex", + "reqwest", + "secrecy", "serde", "serde_json", - "thiserror 1.0.69", + "tempfile", "time", "tokio", - "tokio-stream", - "tracing", - "tracing-subscriber", "uuid", + "webkit6", "zbus 3.15.2", "zvariant 3.15.2", ] @@ -707,6 +742,21 @@ dependencies = [ "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitfields" version = "0.12.4" @@ -740,6 +790,15 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "bitpacking" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" +dependencies = [ + "crunchy", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -781,9 +840,9 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] @@ -820,6 +879,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "buffered-reader" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" +dependencies = [ + "libc", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -916,9 +984,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + [[package]] name = "cexpr" version = "0.6.0" @@ -953,12 +1029,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "charset" version = "0.1.5" @@ -1001,6 +1071,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -1179,12 +1250,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1484,6 +1589,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -1496,6 +1611,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch2" version = "0.3.0" @@ -1529,6 +1655,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dsa" version = "0.6.3" @@ -1560,6 +1692,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "eax" version = "0.5.0" @@ -1573,6 +1711,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ecb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" +dependencies = [ + "cipher", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -1689,6 +1836,33 @@ dependencies = [ "serde", ] +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "enchant" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafd018f9ff5933b0cf9d89a095c31125890f2246952749cbaad230b537644db" +dependencies = [ + "enchant-sys", +] + +[[package]] +name = "enchant-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97af59f7a6d8a217695598f23dc12051734322e60b58e5ca2f3a2fffa59ba34f" +dependencies = [ + "pkg-config", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1700,9 +1874,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -1804,6 +1978,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + [[package]] name = "fastrand" version = "1.9.0" @@ -1852,6 +2032,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.8" @@ -1869,6 +2055,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1899,6 +2091,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +[[package]] +name = "fs4" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" +dependencies = [ + "rustix 0.38.44", + "windows-sys 0.52.0", +] + [[package]] name = "funty" version = "2.0.0" @@ -2106,6 +2308,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2398,6 +2609,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -2474,7 +2696,7 @@ dependencies = [ "markup5ever 0.12.1", "tendril", "thiserror 1.0.69", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -2502,6 +2724,12 @@ dependencies = [ "match_token", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.12" @@ -2787,6 +3015,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2853,6 +3084,24 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2882,6 +3131,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -2929,6 +3188,49 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ksni" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b29c089f14ce24c5b25d9bdcb265413b5e0c3df0871823e0d96bd83bc52a24" +dependencies = [ + "futures-util", + "pastey", + "serde", + "tokio", + "zbus 5.13.2", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2972,6 +3274,12 @@ dependencies = [ "webpki-roots 1.0.2", ] +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libadwaita" version = "0.8.0" @@ -3114,6 +3422,21 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + [[package]] name = "mac" version = "0.1.1" @@ -3122,9 +3445,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.6" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" dependencies = [ "cc", "objc2", @@ -3196,12 +3519,31 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.7.6" +name = "measure_time" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] +checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" +dependencies = [ + "instant", + "log", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3219,6 +3561,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" + [[package]] name = "mime" version = "0.3.17" @@ -3289,6 +3637,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + [[package]] name = "native-tls" version = "0.2.14" @@ -3324,19 +3678,6 @@ dependencies = [ "memoffset 0.7.1", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - [[package]] name = "nom" version = "7.1.3" @@ -3358,25 +3699,16 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.11.7" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" dependencies = [ "futures-lite 2.6.1", "log", "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus 5.11.0", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", + "zbus 5.13.2", ] [[package]] @@ -3476,6 +3808,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + [[package]] name = "num_enum" version = "0.7.5" @@ -3529,18 +3871,18 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", @@ -3555,9 +3897,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", "block2", @@ -3599,6 +3941,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oneshot" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -3676,6 +4024,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "ownedbytes" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "p256" version = "0.13.2" @@ -3778,6 +4135,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3805,6 +4168,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pgp" version = "0.16.0" @@ -4194,6 +4567,25 @@ dependencies = [ "psl-types", ] +[[package]] +name = "pulldown-cmark" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" +dependencies = [ + "bitflags 2.9.4", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -4260,6 +4652,36 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -4427,6 +4849,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4678,6 +5110,62 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "sequoia-openpgp" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e858e4e9e48ff079cede92e1b45c942a5466ce9a4e3cc0c2a7e66586a718ef59" +dependencies = [ + "aes", + "aes-gcm", + "anyhow", + "base64 0.22.1", + "block-padding", + "blowfish", + "buffered-reader", + "camellia", + "cast5", + "cfb-mode", + "chrono", + "cipher", + "des", + "digest", + "dsa", + "dyn-clone", + "eax", + "ecb", + "ecdsa", + "ed25519 2.2.3", + "ed25519-dalek", + "getrandom 0.2.16", + "idea", + "idna 1.1.0", + "lalrpop", + "lalrpop-util", + "lazy_static", + "libc", + "md-5", + "memsec", + "num-bigint-dig", + "once_cell", + "p256", + "p384", + "p521", + "rand", + "rand_core", + "regex", + "regex-syntax", + "ripemd", + "rsa", + "sha1collisiondetection", + "sha2", + "thiserror 1.0.69", + "twofish", + "typenum", + "x25519-dalek", + "xxhash-rust", +] + [[package]] name = "serde" version = "1.0.228" @@ -4806,6 +5294,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sha1collisiondetection" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" +dependencies = [ + "const-oid", + "digest", + "generic-array", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4827,15 +5326,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -4873,47 +5363,21 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" -[[package]] -name = "simple-gtk-app" -version = "0.1.0" -dependencies = [ - "ammonia", - "asgard-core", - "asgard-oauth", - "dateparser", - "dirs", - "dotenvy", - "email-backend", - "gio", - "glib", - "glib-build-tools", - "gtk4", - "html2text", - "libadwaita", - "libc", - "log", - "open", - "proton-api-rs", - "regex", - "reqwest", - "secrecy", - "serde", - "serde_json", - "tempfile", - "time", - "tokio", - "uuid", - "webkit6", - "zbus 3.15.2", - "zvariant 3.15.2", -] - [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.11" @@ -5189,6 +5653,147 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tantivy" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96599ea6fccd844fc833fed21d2eecac2e6a7c1afd9e044057391d78b1feb141" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64 0.22.1", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "itertools 0.12.1", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror 1.0.69", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.12.1", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "tantivy-sstable" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" +dependencies = [ + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" +dependencies = [ + "murmurhash32", + "rand_distr", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" +dependencies = [ + "serde", +] + [[package]] name = "tap" version = "1.0.1" @@ -5237,6 +5842,17 @@ dependencies = [ "utf-8", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -5292,15 +5908,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.44" @@ -5334,6 +5941,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -5376,6 +5992,7 @@ dependencies = [ "slab", "socket2 0.6.0", "tokio-macros", + "tracing", "windows-sys 0.59.0", ] @@ -5486,6 +6103,7 @@ checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -5608,32 +6226,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] @@ -5707,6 +6299,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -5764,6 +6362,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5788,12 +6392,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" @@ -6518,6 +7116,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.0" @@ -6566,7 +7170,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.26.4", + "nix", "once_cell", "ordered-stream", "rand", @@ -6585,9 +7189,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.11.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast 0.7.2", "async-executor", @@ -6603,17 +7207,20 @@ dependencies = [ "futures-core", "futures-lite 2.6.1", "hex", - "nix 0.30.1", + "libc", "ordered-stream", + "rustix 1.1.2", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", + "uuid", + "windows-sys 0.61.1", "winnow 0.7.13", - "zbus_macros 5.11.0", - "zbus_names 4.2.0", - "zvariant 5.7.0", + "zbus_macros 5.13.2", + "zbus_names 4.3.1", + "zvariant 5.9.2", ] [[package]] @@ -6632,17 +7239,17 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.106", - "zbus_names 4.2.0", - "zvariant 5.7.0", - "zvariant_utils 3.2.1", + "zbus_names 4.3.1", + "zvariant 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -6658,14 +7265,13 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow 0.7.13", - "zvariant 5.7.0", + "zvariant 5.9.2", ] [[package]] @@ -6768,6 +7374,34 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "3.15.2" @@ -6784,16 +7418,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.7.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", "serde", "winnow 0.7.13", - "zvariant_derive 5.7.0", - "zvariant_utils 3.2.1", + "zvariant_derive 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -6811,15 +7445,15 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.106", - "zvariant_utils 3.2.1", + "zvariant_utils 3.3.0", ] [[package]] @@ -6835,9 +7469,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b40c78c..241f992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "app", "core", # Re-enabled - fixing compilation issues "oauth", "ui-components", @@ -8,7 +7,7 @@ members = [ # "crates/pop3", # Temporarily disabled due to dependency issues "minimal-core", "minimal-app", - "simple-gtk-app", + "asgard-mail", "email-backend", "proton-api-rs", # Forked Proton API client with pure Rust SRP ] @@ -56,10 +55,11 @@ mime = "0.3" rustls = { version = "0.21", features = ["dangerous_configuration"] } rustls-pemfile = "1.0" native-tls = "0.2" +tokio-native-tls = "0.3" # Storage and search rusqlite = { version = "0.29", features = ["bundled"] } -# tantivy = "0.21" # Temporarily disabled due to zstd-safe conflicts +tantivy = "0.22" sha2 = "0.10" directories = "5.0" @@ -67,12 +67,16 @@ directories = "5.0" keyring = "2.3" sodiumoxide = "0.2" # Re-enabled for crypto module libsodium-sys = "0.2" +sequoia-openpgp = { version = "1", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] } # UI Framework gtk4 = { version = "0.10", features = ["v4_8"] } libadwaita = { version = "0.8", features = ["v1_2"] } # webkit2gtk = "0.15" # Disabled - using pure Rust HTML rendering instead +# Spell checking +enchant = "0.3" + # System integration notify-rust = "4.10" open = "5.0" diff --git a/app/Cargo.toml b/app/Cargo.toml deleted file mode 100644 index 7f56fca..0000000 --- a/app/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "asgard-mail" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description = "Asgard Mail - A modern email client for GNOME" - -[[bin]] -name = "asgard-mail" -path = "src/main.rs" - -[dependencies] -# Workspace dependencies -tokio.workspace = true -anyhow.workspace = true -thiserror.workspace = true -tracing.workspace = true -serde.workspace = true -serde_json.workspace = true -time.workspace = true -uuid.workspace = true - -# UI Framework -gtk4.workspace = true -libadwaita.workspace = true - - -# System integration -notify-rust.workspace = true -open.workspace = true - -# Core dependencies -asgard-core = { path = "../core" } -# asgard-oauth = { path = "../oauth" } # Temporarily disabled due to compilation errors - -# Additional dependencies -clap = { version = "4.4", features = ["derive"] } -tokio-stream = "0.1" -futures = "0.3" -async-trait = "0.1" -regex = "1.10" -tracing-subscriber = "0.3" - -# DBus integration -zbus = "3.14" -zvariant = "3.15" diff --git a/app/src/app.rs b/app/src/app.rs deleted file mode 100644 index 6c83776..0000000 --- a/app/src/app.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Main application structure for Asgard Mail - -use crate::windows::MainWindow; -use crate::notifications::NotificationManager; -use crate::dbus_service::AsgardDbusService; -use crate::theming; -use asgard_core::error::AsgardResult; -use asgard_core::config::Config; -use asgard_core::storage::StorageManager; -// use asgard_core::search::TantivySearchIndex; -use asgard_core::sync::SyncManager; -// use asgard_oauth::TokenManager; -use std::sync::Arc; -use tokio::sync::Mutex; -use tracing::{info, error}; -// use gtk4::glib; - -/// Main application structure -pub struct AsgardApp { - /// Application configuration - config: Config, - /// Storage manager - storage: Arc>, - /// Search index - // search_index: Arc>, - /// Sync manager - sync_manager: Arc>, - /// Token manager - // token_manager: TokenManager, - /// Notification manager - notification_manager: NotificationManager, - /// Main window - main_window: MainWindow, - /// Demo mode flag - demo_mode: bool, - /// DBus service - dbus_service: AsgardDbusService, -} - -impl AsgardApp { - /// Create a new Asgard Mail application - pub async fn new(demo_mode: bool, dbus_service: AsgardDbusService) -> AsgardResult { - info!("Initializing Asgard Mail application"); - - // Load configuration - let config = Config::load_from_env(); - config.validate()?; - - // Load CSS theming early - theming::load_css(); - - // Initialize storage - let storage = Arc::new(Mutex::new( - StorageManager::new( - config.database_file_path(), - config.cache_dir(), - ).await? - )); - storage.lock().await.initialize().await?; - - // Initialize search index - // let search_index = Arc::new(Mutex::new( - // TantivySearchIndex::new(config.search_index_dir())? - // )); - - // Initialize sync manager - let sync_manager = Arc::new(Mutex::new( - SyncManager::new( - storage.clone(), - // TODO: Add search index when available - using dummy for now - Arc::new(Mutex::new(asgard_core::search::SimpleSearchIndex::new())), - std::time::Duration::from_secs(config.sync.default_sync_interval), - ) - )); - - // Initialize token manager - // let token_manager = TokenManager::new()?; - - // Initialize notification manager - let notification_manager = NotificationManager::new(&config)?; - - // Initialize main window - let main_window = MainWindow::new( - config.clone(), - storage.clone(), - // search_index.clone(), - sync_manager.clone(), - // token_manager.clone(), - notification_manager.clone(), - demo_mode, - ).await?; - - Ok(Self { - config, - storage, - // search_index, - sync_manager, - // token_manager, - notification_manager, - main_window, - demo_mode, - dbus_service, - }) - } - - /// Run the application - pub async fn run(mut self) -> AsgardResult<()> { - info!("Starting Asgard Mail application"); - - // Set up DBus callback to show window - // Note: GTK widgets are not Send/Sync, so we disable this for now - // let main_window = self.main_window.clone(); - // self.dbus_service.set_show_window_callback(move || { - // let main_window = main_window.clone(); - // glib::MainContext::default().spawn_local(async move { - // main_window.show(); - // }); - // }).await; - - // Set up quit callback on main window - // We'll use a different approach - exit the process directly - let quit_callback = move || { - std::process::exit(0); - }; - self.main_window.set_quit_callback(quit_callback).await; - - // Start background sync - { - let mut sync_manager = self.sync_manager.lock().await; - sync_manager.start_background_sync().await?; - } - - // Show main window - self.main_window.show(); - - // System tray functionality can be added here if needed - - // Run the main event loop - if self.demo_mode { - info!("Running in demo mode"); - self.run_demo_mode().await?; - } else { - self.run_normal_mode().await?; - } - - // Cleanup - info!("Shutting down Asgard Mail"); - self.cleanup().await?; - - // Unregister DBus service - if let Err(e) = self.dbus_service.unregister_service().await { - error!("Failed to unregister DBus service: {}", e); - } - - Ok(()) - } - - /// Run in normal mode - async fn run_normal_mode(&mut self) -> AsgardResult<()> { - // Start GTK main loop - info!("Starting GTK main loop..."); - - // Run GTK main loop in a separate task - let gtk_handle = tokio::task::spawn_blocking(|| { - // gtk4::main(); // Handled by AdwApplication::run() - }); - - // Wait for either GTK to exit or Ctrl+C - tokio::select! { - _ = gtk_handle => { - info!("GTK main loop exited"); - } - _ = tokio::signal::ctrl_c() => { - info!("Received Ctrl+C, shutting down..."); - // gtk4::main_quit(); // Handled by AdwApplication::run() - } - } - - Ok(()) - } - - /// Run in demo mode - async fn run_demo_mode(&mut self) -> AsgardResult<()> { - info!("Demo mode: Creating sample data"); - - // Create demo account - let demo_account = self.create_demo_account().await?; - - // Add demo account to sync manager - { - let sync_manager = self.sync_manager.lock().await; - sync_manager.add_account(demo_account).await?; - } - - // Create demo mailboxes and messages - self.create_demo_data().await?; - - // Run GTK main loop for demo - info!("Starting GTK main loop for demo..."); - - // Run GTK main loop in a separate task - let gtk_handle = tokio::task::spawn_blocking(|| { - // gtk4::main(); // Handled by AdwApplication::run() - }); - - // Wait for either GTK to exit or Ctrl+C - tokio::select! { - _ = gtk_handle => { - info!("GTK main loop exited"); - } - _ = tokio::signal::ctrl_c() => { - info!("Received Ctrl+C, shutting down..."); - // gtk4::main_quit(); // Handled by AdwApplication::run() - } - } - - Ok(()) - } - - /// Create demo account - async fn create_demo_account(&self) -> AsgardResult { - use asgard_core::account::{Account, GmailOAuthConfig}; - - let oauth_config = GmailOAuthConfig { - client_id: "demo-client-id".to_string(), - client_secret: "demo-client-secret".to_string(), - access_token: Some("demo-access-token".to_string()), - refresh_token: Some("demo-refresh-token".to_string()), - token_expires_at: Some(time::OffsetDateTime::now_utc() + time::Duration::hours(1)), - scopes: vec!["https://www.googleapis.com/auth/gmail.readonly".to_string()], - }; - - let account = Account::new_gmail( - "demo@gmail.com".to_string(), - Some("Demo Account".to_string()), - oauth_config, - )?; - - // Store account in database - { - let mut storage = self.storage.lock().await; - storage.database_mut().create_account(&account).await?; - } - - Ok(account) - } - - /// Create demo data - async fn create_demo_data(&self) -> AsgardResult<()> { - use asgard_core::mailbox::Mailbox; - use asgard_core::message::{Message, MessageHeaders, EmailAddress, MessageImportance}; - use std::collections::HashMap; - - // Create demo mailboxes - let inbox = Mailbox::new_inbox(uuid::Uuid::new_v4()); - let sent = Mailbox::new_sent(uuid::Uuid::new_v4(), "Sent".to_string()); - - // Store mailboxes - { - let mut storage = self.storage.lock().await; - storage.database_mut().create_mailbox(&inbox).await?; - storage.database_mut().create_mailbox(&sent).await?; - } - - // Create demo messages - let demo_messages = vec![ - ("Welcome to Asgard Mail", "Welcome to Asgard Mail! This is a demo message to show you how the application works."), - ("Getting Started", "Here are some tips to get started with Asgard Mail..."), - ("Features Overview", "Asgard Mail includes many features like Gmail OAuth, IMAP sync, and more!"), - ]; - - for (subject, body) in &demo_messages { - let headers = MessageHeaders { - message_id: None, - in_reply_to: None, - references: None, - subject: subject.to_string(), - from: vec![EmailAddress { - name: Some("Demo Sender".to_string()), - email: "demo@example.com".to_string(), - }], - to: vec![EmailAddress { - name: None, - email: "demo@gmail.com".to_string(), - }], - cc: vec![], - bcc: vec![], - reply_to: vec![], - date: Some(time::OffsetDateTime::now_utc()), - received_date: Some(time::OffsetDateTime::now_utc()), - importance: MessageImportance::Normal, - custom: HashMap::new(), - }; - - let mut message = Message::new(uuid::Uuid::new_v4(), inbox.id, headers); - - // Add text part - let part = asgard_core::message::MessagePart { - id: "1".to_string(), - part_type: asgard_core::message::MessagePartType::Text, - mime_type: "text/plain".to_string(), - disposition: None, - filename: None, - size: body.len(), - encoding: None, - content_id: None, - content_location: None, - content: Some(body.as_bytes().to_vec()), - children: vec![], - }; - message.add_part(part); - - // Store message - { - let mut storage = self.storage.lock().await; - storage.database_mut().create_message(&message).await?; - } - - // Add to search index - { - // let mut search_index = self.search_index.lock().await; - // search_index.add_message(&message)?; - } - } - - info!("Created demo data: {} mailboxes, {} messages", 2, demo_messages.len()); - Ok(()) - } - - /// Cleanup resources - async fn cleanup(&mut self) -> AsgardResult<()> { - info!("Cleaning up application resources"); - - // Stop background sync - { - let mut sync_manager = self.sync_manager.lock().await; - sync_manager.stop_background_sync().await?; - } - - // Close storage - { - let _storage = self.storage.lock().await; - // storage.close().await?; // TODO: Fix this - need to move out of mutex - } - - Ok(()) - } - - /// Quit the application - pub async fn quit(&mut self) -> AsgardResult<()> { - info!("Quitting Asgard Mail application"); - - // Cleanup resources - self.cleanup().await?; - - // Unregister DBus service - if let Err(e) = self.dbus_service.unregister_service().await { - error!("Failed to unregister DBus service: {}", e); - } - - // Exit the application - std::process::exit(0); - } -} diff --git a/app/src/dbus_service.rs b/app/src/dbus_service.rs deleted file mode 100644 index 0e02f4b..0000000 --- a/app/src/dbus_service.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! DBus service for single-instance application behavior - -use zbus::{Connection, ConnectionBuilder, dbus_interface, Result as ZbusResult}; -use std::sync::Arc; -use tokio::sync::Mutex; -use crate::constants::{DBUS_APP_NAME, DBUS_APP_PATH}; - -/// DBus method names as enum for type safety -#[derive(Debug, Clone, PartialEq)] -pub enum DbusMethod { - Ping, - ShowWindow, - GetPid, -} - -impl DbusMethod { - /// Convert enum to string for DBus interface - pub fn as_str(&self) -> &'static str { - match self { - DbusMethod::Ping => "Ping", - DbusMethod::ShowWindow => "ShowWindow", - DbusMethod::GetPid => "GetPid", - } - } -} - -/// DBus service for Asgard Mail -#[derive(Clone)] -pub struct AsgardDbusService { - /// Connection to DBus - connection: Arc>>, - /// Callback to handle DBus method calls - method_callback: Arc>>>, -} - -impl AsgardDbusService { - /// Create a new DBus service - pub fn new() -> Self { - Self { - connection: Arc::new(Mutex::new(None)), - method_callback: Arc::new(Mutex::new(None)), - } - } - - /// Set the callback to handle DBus method calls - pub async fn set_method_callback(&self, callback: F) - where - F: Fn(DbusMethod) + Send + Sync + 'static, - { - let mut cb = self.method_callback.lock().await; - *cb = Some(Box::new(callback)); - } - - /// Set the show window callback - pub async fn set_show_window_callback(&self, callback: F) - where - F: Fn() + Send + Sync + 'static, - { - let callback_wrapper = move |method| { - if method == DbusMethod::ShowWindow { - callback(); - } - }; - self.set_method_callback(callback_wrapper).await; - } - - /// Register this instance as the DBus service - pub async fn register_service(&self) -> ZbusResult<()> { - println!("Attempting to register DBus service..."); - - let service = AsgardDbusInterface::new(self.method_callback.clone()); - - // Try to claim the name and serve the interface - let connection = ConnectionBuilder::session()? - .name(DBUS_APP_NAME)? - .serve_at(DBUS_APP_PATH, service)? - .build() - .await?; - - println!("Successfully registered Asgard Mail DBus service"); - - // Store the connection - { - let mut conn = self.connection.lock().await; - *conn = Some(connection); - } - - Ok(()) - } - - /// Unregister the DBus service - pub async fn unregister_service(&self) -> ZbusResult<()> { - let mut conn = self.connection.lock().await; - if let Some(connection) = conn.take() { - connection.release_name(DBUS_APP_NAME).await?; - println!("Unregistered Asgard Mail DBus service"); - } - Ok(()) - } -} - -/// DBus interface implementation -pub struct AsgardDbusInterface { - method_callback: Arc>>>, -} - -impl AsgardDbusInterface { - fn new(method_callback: Arc>>>) -> Self { - Self { - method_callback, - } - } -} - -#[dbus_interface(name = "com.asgard.Mail")] -impl AsgardDbusInterface { - /// Ping method to check if service is alive - async fn ping(&self) -> String { - println!("DBus ping received"); - let callback = self.method_callback.lock().await; - if let Some(cb) = callback.as_ref() { - cb(DbusMethod::Ping); - } - "pong".to_string() - } - - /// Show the main window - async fn show_window(&self) { - println!("DBus show_window called"); - - let callback = self.method_callback.lock().await; - if let Some(cb) = callback.as_ref() { - cb(DbusMethod::ShowWindow); - } - } - - /// Get the PID of the current process - async fn get_pid(&self) -> u32 { - let pid = std::process::id(); - println!("DBus get_pid called, returning PID: {}", pid); - - let callback = self.method_callback.lock().await; - if let Some(cb) = callback.as_ref() { - cb(DbusMethod::GetPid); - } - - pid - } -} \ No newline at end of file diff --git a/app/src/email_backend.rs b/app/src/email_backend.rs deleted file mode 100644 index def1e06..0000000 --- a/app/src/email_backend.rs +++ /dev/null @@ -1,589 +0,0 @@ -//! Simplified email backend for Asgard Mail - -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use time::OffsetDateTime; -use uuid::Uuid; - -/// Email account configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmailAccount { - pub id: Uuid, - pub email: String, - pub display_name: String, - pub imap_server: String, - pub imap_port: u16, - pub smtp_server: String, - pub smtp_port: u16, - pub username: String, - pub password: String, - pub use_tls: bool, - pub created_at: OffsetDateTime, -} - -#[derive(Debug, Clone)] -pub struct GnomeEmailAccount { - pub id: String, - pub provider: String, - pub email: String, - pub identity: String, - pub imap_host: String, - pub imap_port: u16, - pub imap_use_ssl: bool, - pub smtp_host: String, - pub smtp_port: u16, - pub smtp_use_tls: bool, -} - -impl EmailAccount { - pub fn new( - email: String, - display_name: String, - imap_server: String, - imap_port: u16, - smtp_server: String, - smtp_port: u16, - username: String, - password: String, - ) -> Self { - Self { - id: Uuid::new_v4(), - email, - display_name, - imap_server, - imap_port, - smtp_server, - smtp_port, - username, - password, - use_tls: true, - created_at: OffsetDateTime::now_utc(), - } - } -} - -/// Email message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmailMessage { - pub id: Uuid, - pub account_id: Uuid, - pub uid: Option, - pub subject: String, - pub from: String, - pub to: Vec, - pub cc: Vec, - pub bcc: Vec, - pub date: OffsetDateTime, - pub body_text: String, - pub body_html: Option, - pub is_read: bool, - pub is_flagged: bool, - pub mailbox: String, - pub size: usize, - - // Threading headers - pub message_id: Option, - pub in_reply_to: Option, - pub references: Vec, - pub server_thread_id: Option, - pub has_attachments: bool, -} - -impl EmailMessage { - pub fn new( - account_id: Uuid, - subject: String, - from: String, - to: Vec, - body_text: String, - mailbox: String, - ) -> Self { - Self { - id: Uuid::new_v4(), - account_id, - uid: None, - subject, - from, - to, - cc: vec![], - bcc: vec![], - date: OffsetDateTime::now_utc(), - body_text, - body_html: None, - is_read: false, - is_flagged: false, - mailbox, - size: 0, - message_id: None, - in_reply_to: None, - references: vec![], - server_thread_id: None, - has_attachments: false, - } - } - -} - -/// Mailbox -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Mailbox { - pub id: Uuid, - pub account_id: Uuid, - pub name: String, - pub display_name: String, - pub message_count: u32, - pub unread_count: u32, -} - -impl Mailbox { - pub fn new(account_id: Uuid, name: String, display_name: String) -> Self { - Self { - id: Uuid::new_v4(), - account_id, - name, - display_name, - message_count: 0, - unread_count: 0, - } - } -} - -/// Email backend manager -pub struct EmailBackend { - accounts: HashMap, - mailboxes: HashMap>, - messages: HashMap>, -} - -impl EmailBackend { - pub fn new() -> Self { - Self { - accounts: HashMap::new(), - mailboxes: HashMap::new(), - messages: HashMap::new(), - } - } - - /// Add an email account - pub fn add_account(&mut self, account: EmailAccount) -> Uuid { - let account_id = account.id; - self.accounts.insert(account_id, account); - self.mailboxes.insert(account_id, vec![]); - self.messages.insert(account_id, vec![]); - account_id - } - - /// Add an account from GNOME Online Accounts - pub fn add_gnome_account(&mut self, gnome_account: GnomeEmailAccount) -> Uuid { - let account_id = Uuid::new_v4(); - let email = gnome_account.email.clone(); - let account = EmailAccount { - id: account_id, - email: gnome_account.email, - display_name: gnome_account.identity, - imap_server: gnome_account.imap_host, - imap_port: gnome_account.imap_port, - smtp_server: gnome_account.smtp_host, - smtp_port: gnome_account.smtp_port, - username: email, // Use email as username - password: "".to_string(), // GOA handles authentication - use_tls: gnome_account.smtp_use_tls, - created_at: OffsetDateTime::now_utc(), - }; - - self.accounts.insert(account_id, account); - self.mailboxes.insert(account_id, vec![]); - self.messages.insert(account_id, vec![]); - - // Create default mailboxes for this account - self.create_default_mailboxes(account_id); - - account_id - } - - /// Get all accounts - pub fn get_accounts(&self) -> Vec<&EmailAccount> { - self.accounts.values().collect() - } - - /// Get mailboxes for an account - pub fn get_mailboxes(&self, account_id: Uuid) -> Vec<&Mailbox> { - self.mailboxes.get(&account_id).map(|v| v.iter().collect()).unwrap_or_default() - } - - /// Get messages for a mailbox - pub fn get_messages(&self, account_id: Uuid, mailbox_name: &str) -> Vec<&EmailMessage> { - self.messages - .get(&account_id) - .map(|messages| { - messages - .iter() - .filter(|msg| msg.mailbox == mailbox_name) - .collect() - }) - .unwrap_or_default() - } - - /// Get a specific message by ID - pub fn get_message(&self, message_id: Uuid) -> Result { - for messages in self.messages.values() { - for message in messages { - if message.id == message_id { - return Ok(message.clone()); - } - } - } - Err("Message not found".to_string()) - } - - /// Create default mailboxes for an account - pub fn create_default_mailboxes(&mut self, account_id: Uuid) { - let mailboxes = vec![ - Mailbox::new(account_id, "INBOX".to_string(), "Inbox".to_string()), - Mailbox::new(account_id, "Sent".to_string(), "Sent".to_string()), - Mailbox::new(account_id, "Drafts".to_string(), "Drafts".to_string()), - Mailbox::new(account_id, "Trash".to_string(), "Trash".to_string()), - Mailbox::new(account_id, "Spam".to_string(), "Spam".to_string()), - ]; - - self.mailboxes.insert(account_id, mailboxes); - } - - /// Add comprehensive test messages for threading demo - pub fn add_sample_messages(&mut self, account_id: Uuid) { - use time::{Date, Month, Time, UtcOffset, OffsetDateTime}; - - // Create default mailboxes first - self.create_default_mailboxes(account_id); - - // ===== COMPREHENSIVE TEST DATA ===== - - // 1. THREAD WITH 3+ MESSAGES (All Read) - // Thread A: Team Discussion - 4 messages, all read - let mut thread_a_msg1 = EmailMessage::new( - account_id, - "Team Meeting Tomorrow".to_string(), - "manager@company.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone(), "team@company.com".to_string()], - "Hi team,\n\nJust a reminder that we have our weekly team meeting tomorrow at 10 AM. Please prepare your status updates and any blockers you're facing.\n\nAgenda:\n- Project status updates\n- Sprint planning\n- Q&A session\n\nSee you all there!\n\nBest,\nManager".to_string(), - "INBOX".to_string(), - ); - thread_a_msg1.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 10).unwrap(), - Time::from_hms(9, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_a_msg1.is_read = true; - thread_a_msg1.message_id = Some("".to_string()); - - let mut thread_a_msg2 = EmailMessage::new( - account_id, - "Re: Team Meeting Tomorrow".to_string(), - "colleague1@company.com".to_string(), - vec!["manager@company.com".to_string(), self.accounts.get(&account_id).unwrap().email.clone()], - "Thanks for the reminder! I'll have my status ready. Quick question - should we bring our laptops for the sprint planning session?\n\nColleague1".to_string(), - "INBOX".to_string(), - ); - thread_a_msg2.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 10).unwrap(), - Time::from_hms(9, 15, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_a_msg2.is_read = true; - thread_a_msg2.message_id = Some("".to_string()); - thread_a_msg2.in_reply_to = Some("".to_string()); - thread_a_msg2.references = vec!["".to_string()]; - - let mut thread_a_msg3 = EmailMessage::new( - account_id, - "Re: Team Meeting Tomorrow".to_string(), - self.accounts.get(&account_id).unwrap().email.clone(), - vec!["manager@company.com".to_string(), "colleague1@company.com".to_string()], - "Yes, please bring laptops for the sprint planning. We'll be using Jira to create tickets and estimate story points.\n\nDemo".to_string(), - "Sent".to_string(), // This is a reply from the user, so it goes to Sent - ); - thread_a_msg3.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 10).unwrap(), - Time::from_hms(9, 30, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_a_msg3.is_read = true; - thread_a_msg3.message_id = Some("".to_string()); - thread_a_msg3.in_reply_to = Some("".to_string()); - thread_a_msg3.references = vec!["".to_string(), "".to_string()]; - - let mut thread_a_msg4 = EmailMessage::new( - account_id, - "Re: Team Meeting Tomorrow".to_string(), - "colleague2@company.com".to_string(), - vec!["manager@company.com".to_string(), self.accounts.get(&account_id).unwrap().email.clone()], - "Perfect! I'll bring mine. Looking forward to the sprint planning session.\n\nColleague2".to_string(), - "INBOX".to_string(), - ); - thread_a_msg4.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 10).unwrap(), - Time::from_hms(9, 45, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_a_msg4.is_read = true; - thread_a_msg4.message_id = Some("".to_string()); - thread_a_msg4.in_reply_to = Some("".to_string()); - thread_a_msg4.references = vec!["".to_string(), "".to_string(), "".to_string()]; - - // 2. THREAD WITH ONLY ONE MESSAGE (Unread) - // Thread B: Single message - unread - let mut thread_b_msg1 = EmailMessage::new( - account_id, - "Weekly Tech Newsletter".to_string(), - "newsletter@tech.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone()], - "This week in tech:\n\n1. New AI developments in machine learning\n2. Latest updates from major tech companies\n3. Open source projects worth checking out\n4. Developer tools and resources\n\nRead the full newsletter on our website.\n\nUnsubscribe | Manage Preferences".to_string(), - "INBOX".to_string(), - ); - thread_b_msg1.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 12).unwrap(), - Time::from_hms(9, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_b_msg1.is_read = false; // Unread - thread_b_msg1.message_id = Some("".to_string()); - - // 3. THREAD WITH UNREAD MESSAGES (Mixed read/unread) - // Thread C: Project Discussion - 3 messages, some unread - let mut thread_c_msg1 = EmailMessage::new( - account_id, - "New Feature Request".to_string(), - "product@company.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone()], - "Hi Demo,\n\nWe have a new feature request from the client. They want to add dark mode support to the application. Can you provide an estimate for this?\n\nRequirements:\n- Toggle between light and dark themes\n- Persist user preference\n- Support for all existing components\n\nLet me know your thoughts!\n\nProduct Manager".to_string(), - "INBOX".to_string(), - ); - thread_c_msg1.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - Time::from_hms(14, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_c_msg1.is_read = true; - thread_c_msg1.message_id = Some("".to_string()); - - let mut thread_c_msg2 = EmailMessage::new( - account_id, - "Re: New Feature Request".to_string(), - self.accounts.get(&account_id).unwrap().email.clone(), - vec!["product@company.com".to_string()], - "Hi Product Manager,\n\nI can definitely implement dark mode support. Based on the requirements, I estimate this would take about 2-3 days of development time.\n\nI'll need to:\n- Create a theme system\n- Update all CSS variables\n- Add user preference storage\n- Test across all components\n\nShould I start working on this?\n\nDemo".to_string(), - "INBOX".to_string(), - ); - thread_c_msg2.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - Time::from_hms(14, 30, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_c_msg2.is_read = true; - thread_c_msg2.message_id = Some("".to_string()); - thread_c_msg2.in_reply_to = Some("".to_string()); - thread_c_msg2.references = vec!["".to_string()]; - - let mut thread_c_msg3 = EmailMessage::new( - account_id, - "Re: New Feature Request".to_string(), - "product@company.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone()], - "Perfect! Yes, please go ahead and start working on it. The client is really excited about this feature.\n\nLet me know if you need any clarification on the requirements.\n\nProduct Manager".to_string(), - "INBOX".to_string(), - ); - thread_c_msg3.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - Time::from_hms(15, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_c_msg3.is_read = false; // Unread - thread_c_msg3.message_id = Some("".to_string()); - thread_c_msg3.in_reply_to = Some("".to_string()); - thread_c_msg3.references = vec!["".to_string(), "".to_string()]; - - // 4. THREAD WITH ALL MESSAGES READ (2 messages) - // Thread D: All read conversation - let mut thread_d_msg1 = EmailMessage::new( - account_id, - "Meeting Notes from Yesterday".to_string(), - "colleague@company.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone()], - "Hi Demo,\n\nHere are the meeting notes from yesterday's client call:\n\n- Client is happy with the current progress\n- They want to add more customization options\n- Next milestone is due in 2 weeks\n- Budget approval for additional features\n\nLet me know if you have any questions!\n\nColleague".to_string(), - "INBOX".to_string(), - ); - thread_d_msg1.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 9).unwrap(), - Time::from_hms(16, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_d_msg1.is_read = true; - thread_d_msg1.message_id = Some("".to_string()); - - let mut thread_d_msg2 = EmailMessage::new( - account_id, - "Re: Meeting Notes from Yesterday".to_string(), - self.accounts.get(&account_id).unwrap().email.clone(), - vec!["colleague@company.com".to_string()], - "Thanks for sharing the notes! The customization options sound interesting. I'll review the requirements and get back to you with any questions.\n\nDemo".to_string(), - "INBOX".to_string(), - ); - thread_d_msg2.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 9).unwrap(), - Time::from_hms(16, 15, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_d_msg2.is_read = true; - thread_d_msg2.message_id = Some("".to_string()); - thread_d_msg2.in_reply_to = Some("".to_string()); - thread_d_msg2.references = vec!["".to_string()]; - - // 5. MESSAGE WITH ATTACHMENT (Single message with attachment) - // Thread E: Document with attachment - let mut thread_e_msg1 = EmailMessage::new( - account_id, - "Contract Review - Please Review Attached Document".to_string(), - "legal@company.com".to_string(), - vec![self.accounts.get(&account_id).unwrap().email.clone(), "team@example.com".to_string()], - "Hi Demo,\n\nPlease review the attached contract document. This is the new service agreement with our client.\n\nKey points to review:\n- Service level agreements\n- Payment terms\n- Intellectual property clauses\n- Termination conditions\n\nPlease provide your feedback by end of week.\n\nLegal Team".to_string(), - "Drafts".to_string(), // Move this to Drafts to demonstrate different mailbox - ); - thread_e_msg1.date = OffsetDateTime::new_in_offset( - Date::from_calendar_date(2024, Month::March, 13).unwrap(), - Time::from_hms(10, 0, 0).unwrap(), - UtcOffset::from_hms(0, 0, 0).unwrap(), - ); - thread_e_msg1.is_read = false; // Unread - thread_e_msg1.message_id = Some("".to_string()); - thread_e_msg1.has_attachments = true; - thread_e_msg1.cc = vec!["cc@example.com".to_string()]; - - // Add all messages to backend - let all_messages = vec![ - thread_a_msg1, thread_a_msg2, thread_a_msg3, thread_a_msg4, // 4-message thread (all read) - thread_b_msg1, // Single message (unread) - thread_c_msg1, thread_c_msg2, thread_c_msg3, // 3-message thread (mixed read/unread) - thread_d_msg1, thread_d_msg2, // 2-message thread (all read) - thread_e_msg1, // Single message with attachment (unread) - ]; - - if let Some(messages) = self.messages.get_mut(&account_id) { - messages.extend(all_messages); - } - - // Update mailbox counts dynamically - self.update_mailbox_counts_dynamically(account_id); - } - - /// Mark message as read - pub fn mark_as_read(&mut self, account_id: Uuid, message_id: Uuid) -> Result<()> { - if let Some(messages) = self.messages.get_mut(&account_id) { - if let Some(message) = messages.iter_mut().find(|m| m.id == message_id) { - message.is_read = true; - } - } - Ok(()) - } - - /// Delete message - pub fn delete_message(&mut self, account_id: Uuid, message_id: Uuid) -> Result<()> { - if let Some(messages) = self.messages.get_mut(&account_id) { - messages.retain(|m| m.id != message_id); - } - Ok(()) - } - - /// Add multiple messages to an account - pub fn add_messages(&mut self, account_id: Uuid, messages: Vec) { - if let Some(account_messages) = self.messages.get_mut(&account_id) { - account_messages.extend(messages); - } - } - - /// Update mailbox counts for an account - pub fn update_mailbox_counts(&mut self, account_id: Uuid, message_count: u32, unread_count: u32) { - if let Some(mailboxes) = self.mailboxes.get_mut(&account_id) { - for mailbox in mailboxes.iter_mut() { - if mailbox.name == "INBOX" { - mailbox.message_count = message_count; - mailbox.unread_count = unread_count; - } - } - } - } - - /// Update mailbox counts dynamically based on actual messages - pub fn update_mailbox_counts_dynamically(&mut self, account_id: Uuid) { - // First, get all messages for this account - let messages = if let Some(account_messages) = self.messages.get(&account_id) { - account_messages.clone() - } else { - return; - }; - - // Then update mailbox counts - if let Some(mailboxes) = self.mailboxes.get_mut(&account_id) { - for mailbox in mailboxes.iter_mut() { - // Count messages in this mailbox - let messages_in_mailbox: Vec<_> = messages.iter() - .filter(|msg| msg.mailbox.to_uppercase() == mailbox.name.to_uppercase()) - .collect(); - mailbox.message_count = messages_in_mailbox.len() as u32; - - // Count unread messages in this mailbox - let unread_count = messages_in_mailbox.iter() - .filter(|msg| !msg.is_read) - .count() as u32; - mailbox.unread_count = unread_count; - } - } - } - - /// Move a message from one mailbox to another - pub fn move_message(&mut self, account_id: Uuid, message_id: Uuid, _from_mailbox: &str, to_mailbox: &str) -> Result<()> { - if let Some(messages) = self.messages.get_mut(&account_id) { - if let Some(message) = messages.iter_mut().find(|m| m.id == message_id) { - message.mailbox = to_mailbox.to_string(); - // Update counts after moving - self.update_mailbox_counts_dynamically(account_id); - Ok(()) - } else { - Err(anyhow::anyhow!("Message not found")) - } - } else { - Err(anyhow::anyhow!("Account not found")) - } - } - - /// Get messages for a specific mailbox (real implementation) - pub fn get_messages_for_mailbox(&self, account_id: Uuid, mailbox_name: &str) -> Vec<&EmailMessage> { - if let Some(messages) = self.messages.get(&account_id) { - messages.iter() - .filter(|msg| msg.mailbox.to_uppercase() == mailbox_name.to_uppercase()) - .collect() - } else { - Vec::new() - } - } - - /// Mark a message as read - pub fn mark_message_as_read(&mut self, account_id: Uuid, message_id: Uuid) -> Result<()> { - if let Some(messages) = self.messages.get_mut(&account_id) { - if let Some(message) = messages.iter_mut().find(|m| m.id == message_id) { - message.is_read = true; - Ok(()) - } else { - Err(anyhow::anyhow!("Message not found")) - } - } else { - Err(anyhow::anyhow!("Account not found")) - } - } -} - -impl Default for EmailBackend { - fn default() -> Self { - Self::new() - } -} diff --git a/app/src/main.rs b/app/src/main.rs deleted file mode 100644 index 830c9ff..0000000 --- a/app/src/main.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Asgard Mail - Main application entry point - -use clap::Parser; -// use gtk4::prelude::*; -// use libadwaita::prelude::*; -use tracing::{info, error}; -use asgard_core::init; - -mod app; -mod windows; -mod widgets; -mod tray; -mod notifications; -mod dbus_service; -mod constants; -mod theming; -mod thread_helpers; -mod email_backend; - -use app::AsgardApp; -use dbus_service::AsgardDbusService; -use constants::{DBUS_APP_NAME, DBUS_INTERFACE_PATH, DBUS_INTERFACE_NAME}; - -/// Command line arguments -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Enable debug mode - #[arg(short, long)] - debug: bool, - - /// Run in demo mode with mock data - #[arg(long)] - demo: bool, - - /// Configuration directory - #[arg(long)] - config_dir: Option, - - /// Cache directory - #[arg(long)] - cache_dir: Option, - - /// Data directory - #[arg(long)] - data_dir: Option, -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Parse command line arguments - let args = Args::parse(); - - // Initialize logging - let log_level = if args.debug { - "debug" - } else { - &std::env::var("ASGARD_MAIL_LOG_LEVEL").unwrap_or_else(|_| "info".to_string()) - }; - - tracing_subscriber::fmt() - .with_max_level(match log_level { - "debug" => tracing::Level::DEBUG, - "info" => tracing::Level::INFO, - "warn" => tracing::Level::WARN, - "error" => tracing::Level::ERROR, - _ => tracing::Level::INFO, - }) - .init(); - - info!("Starting Asgard Mail v{}", env!("CARGO_PKG_VERSION")); - - // Set environment variables if provided - if let Some(config_dir) = args.config_dir { - std::env::set_var("ASGARD_MAIL_CONFIG_DIR", config_dir); - } - if let Some(cache_dir) = args.cache_dir { - std::env::set_var("ASGARD_MAIL_CACHE_DIR", cache_dir); - } - if let Some(data_dir) = args.data_dir { - std::env::set_var("ASGARD_MAIL_DATA_DIR", data_dir); - } - - // Initialize core library - if let Err(e) = init() { - error!("Failed to initialize core library: {}", e); - return Err(e.into()); - } - - // Initialize GTK - gtk4::init()?; - - // Initialize libadwaita - libadwaita::init()?; - - // Try to register this instance as the DBus service - let dbus_service = AsgardDbusService::new(); - - // Try to register the service - if it fails, another instance is already running - match dbus_service.register_service().await { - Ok(_) => { - info!("Successfully registered as the primary instance"); - } - Err(e) => { - error!("Failed to register DBus service (error: {}), assuming another instance is running", e); - info!("Notifying existing instance to show and exiting"); - // Try to notify the existing instance to show - if let Ok(connection) = zbus::Connection::session().await { - let _ = connection.call_method( - Some(DBUS_APP_NAME), - DBUS_INTERFACE_PATH, - Some(DBUS_INTERFACE_NAME), - "ShowWindow", - &(), - ).await; - } - return Ok(()); - } - } - - // Create and run the application - let app = AsgardApp::new(args.demo, dbus_service).await?; - app.run().await?; - - Ok(()) -} diff --git a/app/src/notifications.rs b/app/src/notifications.rs deleted file mode 100644 index 74536d2..0000000 --- a/app/src/notifications.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Notification system for Asgard Mail - -use asgard_core::config::Config; -use asgard_core::error::AsgardResult; -use notify_rust::Notification; -use std::sync::Arc; -use tokio::sync::Mutex; - -/// Notification manager for the application -pub struct NotificationManager { - /// Application configuration - config: Config, - /// Notification state - state: Arc>, -} - -/// Internal notification state -struct NotificationState { - /// Whether notifications are enabled - enabled: bool, - /// Last notification time - last_notification: Option, - /// Notification cooldown - cooldown: std::time::Duration, -} - -impl NotificationManager { - /// Create a new notification manager - pub fn new(config: &Config) -> AsgardResult { - let state = Arc::new(Mutex::new(NotificationState { - enabled: config.notifications.enable_notifications, - last_notification: None, - cooldown: std::time::Duration::from_secs(config.notifications.notification_timeout.into()), - })); - - Ok(Self { - config: config.clone(), - state, - }) - } - - /// Show a notification - pub async fn show_notification(&self, title: &str, body: &str) -> AsgardResult<()> { - let mut state = self.state.lock().await; - - if !state.enabled { - return Ok(()); - } - - // Check cooldown - if let Some(last_time) = state.last_notification { - if last_time.elapsed() < state.cooldown { - return Ok(()); - } - } - - // Create and show notification - let notification = Notification::new() - .summary(title) - .body(body) - .icon("mail-message-new") - .timeout(5000) // 5 seconds - .show(); - - match notification { - Ok(_) => { - state.last_notification = Some(std::time::Instant::now()); - tracing::info!("Notification shown: {}", title); - } - Err(e) => { - tracing::warn!("Failed to show notification: {}", e); - } - } - - Ok(()) - } - - /// Show new email notification - pub async fn show_new_email_notification(&self, sender: &str, subject: &str) -> AsgardResult<()> { - let title = "New Email"; - let body = format!("From: {}\nSubject: {}", sender, subject); - self.show_notification(title, &body).await - } - - /// Show sync notification - pub async fn show_sync_notification(&self, message: &str) -> AsgardResult<()> { - let title = "Email Sync"; - let body = message; - self.show_notification(title, body).await - } - - /// Show error notification - pub async fn show_error_notification(&self, error: &str) -> AsgardResult<()> { - let title = "Error"; - let body = error; - self.show_notification(title, body).await - } - - /// Enable notifications - pub async fn enable(&self) { - let mut state = self.state.lock().await; - state.enabled = true; - } - - /// Disable notifications - pub async fn disable(&self) { - let mut state = self.state.lock().await; - state.enabled = false; - } - - /// Check if notifications are enabled - pub async fn is_enabled(&self) -> bool { - let state = self.state.lock().await; - state.enabled - } - - /// Set notification cooldown - pub async fn set_cooldown(&self, duration: std::time::Duration) { - let mut state = self.state.lock().await; - state.cooldown = duration; - } -} - -impl Clone for NotificationManager { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - state: self.state.clone(), - } - } -} \ No newline at end of file diff --git a/app/src/theming/asgard.css b/app/src/theming/asgard.css deleted file mode 100644 index bef441a..0000000 --- a/app/src/theming/asgard.css +++ /dev/null @@ -1,355 +0,0 @@ -/* Asgard Mail - Apple Mail-inspired styling with libadwaita tokens */ - -/* Ensure opaque surfaces across the app */ -window, .window, .background, .view, .viewer, .content, .pane, .sidebar, .message-list, .message-view { - background-color: @window_bg_color; -} - -/* Header Bar */ -headerbar { - background: transparent; - border: none; - box-shadow: none; -} - -/* Hide any separators used as spacers in the header */ -headerbar separator { - background: transparent; - border: 0; - min-width: 0; - margin: 0; - padding: 0; -} - -/* Make header Paned handles invisible and non-draggable */ -.header-paned > separator { - background: transparent; - border: 0; - min-width: 0; - margin: 0; - padding: 0; -} - -headerbar button.flat { - border-radius: 6px; - padding: 6px; - margin: 2px; -} - -headerbar button.flat:hover { - background-color: alpha(@window_fg_color, 0.1); -} - -/* Sidebar */ -.sidebar { - background-color: @window_bg_color; - border-right: 1px solid alpha(@window_fg_color, 0.1); -} - -.sidebar-section { - padding: 8px 0; -} - -.sidebar-section-title { - font-weight: 600; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: alpha(@window_fg_color, 0.6); - padding: 8px 16px 4px; - margin: 0; -} - -.sidebar-item { - padding: 8px 16px; - margin: 1px 8px; - border-radius: 6px; - transition: background-color 150ms ease; -} - -.sidebar-item:hover { - background-color: alpha(@window_fg_color, 0.08); -} - -.sidebar-item:selected { - background-color: @accent_bg_color; - color: @accent_fg_color; -} - -.sidebar-item:selected:hover { - background-color: alpha(@accent_bg_color, 0.8); -} - -.sidebar-item-icon { - margin-right: 12px; - opacity: 0.7; -} - -.sidebar-item:selected .sidebar-item-icon { - opacity: 1; -} - -/* Message List */ -.message-list { - background-color: @window_bg_color; -} - -.message-row { - padding: 12px 16px; - border-bottom: 1px solid alpha(@window_fg_color, 0.1); - transition: background-color 150ms ease; -} - -.message-row:hover { - background-color: alpha(@window_fg_color, 0.05); -} - -.message-row:selected { - background-color: alpha(@accent_bg_color, 0.2); -} - -.message-row.unread { - font-weight: 600; -} - -.message-sender { - font-size: 14px; - font-weight: 500; - color: @window_fg_color; - margin-bottom: 4px; -} - -.message-subject { - font-size: 13px; - color: @window_fg_color; - margin-bottom: 4px; -} - -.message-preview { - font-size: 12px; - color: alpha(@window_fg_color, 0.7); - line-height: 1.4; -} - -.message-time { - font-size: 11px; - color: alpha(@window_fg_color, 0.6); - text-align: right; -} - -/* Message View */ -.message-view { - background-color: @window_bg_color; - padding: 20px; -} - -.message-header { - border-bottom: 1px solid alpha(@window_fg_color, 0.1); - padding-bottom: 16px; - margin-bottom: 20px; -} - -.message-sender-name { - font-size: 16px; - font-weight: 600; - color: @window_fg_color; - margin-bottom: 8px; -} - -.message-subject-large { - font-size: 18px; - font-weight: 500; - color: @window_fg_color; - margin-bottom: 12px; -} - -.message-metadata { - font-size: 12px; - color: alpha(@window_fg_color, 0.7); - margin-bottom: 8px; -} - -.message-body { - font-size: 14px; - line-height: 1.6; - color: @window_fg_color; -} - -/* Buttons */ -button.flat { - border-radius: 6px; - padding: 8px 12px; - transition: background-color 150ms ease; -} - -button.flat:hover { - background-color: alpha(@window_fg_color, 0.1); -} - -button.flat:active { - background-color: alpha(@window_fg_color, 0.2); -} - -button.suggested-action { - background-color: @accent_bg_color; - color: @accent_fg_color; - border-radius: 6px; - padding: 8px 16px; -} - -button.suggested-action:hover { - background-color: alpha(@accent_bg_color, 0.8); -} - -/* Badges */ -.badge { - background-color: @accent_bg_color; - color: @accent_fg_color; - border-radius: 10px; - padding: 2px 8px; - font-size: 11px; - font-weight: 600; - min-width: 18px; - text-align: center; -} - -.badge.unread { - background-color: @destructive_bg_color; - color: @destructive_fg_color; -} - -/* Status Bar */ -.status-bar { - background-color: alpha(@window_bg_color, 0.95); - border-top: 1px solid alpha(@window_fg_color, 0.1); - padding: 8px 16px; -} - -.status-text { - font-size: 11px; - color: alpha(@window_fg_color, 0.7); -} - -/* Progress Bar */ -progressbar { - background-color: alpha(@window_fg_color, 0.2); - border-radius: 3px; -} - -progressbar > trough > progress { - background-color: @accent_bg_color; - border-radius: 3px; -} - -/* Search Bar */ -.search-bar { - background-color: alpha(@window_bg_color, 0.95); - border-bottom: 1px solid alpha(@window_fg_color, 0.1); - padding: 8px 16px; -} - -.search-entry { - border-radius: 20px; - padding: 8px 16px; - border: 1px solid alpha(@window_fg_color, 0.2); - background-color: @window_bg_color; -} - -.search-entry:focus { - border-color: @accent_bg_color; - box-shadow: 0 0 0 2px alpha(@accent_bg_color, 0.3); -} - -/* Avatar */ -.avatar { - border-radius: 50%; - background-color: @accent_bg_color; - color: @accent_fg_color; - font-weight: 600; - text-align: center; - display: flex; - align-items: center; - justify-content: center; -} - -.avatar36 { - width: 36px; - height: 36px; - font-size: 14px; -} - -.avatar48 { - width: 48px; - height: 48px; - font-size: 18px; -} - -/* Message Card */ -.msg-card { - background-color: @window_bg_color; - border: 1px solid alpha(@window_fg_color, 0.1); - border-radius: 8px; - margin: 8px 0; - overflow: hidden; - transition: box-shadow 150ms ease; -} - -.msg-card:hover { - box-shadow: 0 2px 8px alpha(@window_fg_color, 0.1); -} - -.msg-card.expanded { - box-shadow: 0 4px 16px alpha(@window_fg_color, 0.15); -} - -.msg-header { - padding: 16px; - border-bottom: 1px solid alpha(@window_fg_color, 0.1); - cursor: pointer; -} - -.msg-body { - padding: 16px; -} - -.msg-body.collapsed { - max-height: 60px; -} - -.msg-body.expanded { - max-height: none; -} - -.card-sep { - margin: 0; - opacity: 0.5; -} - -/* Thread indicators */ -.thread-indent { - margin-left: 20px; - border-left: 2px solid alpha(@window_fg_color, 0.2); - padding-left: 16px; -} - -/* Responsive design */ -@media (max-width: 768px) { - .sidebar { - min-width: 200px; - } - - .message-view { - padding: 16px; - } -} - -/* Dark theme adjustments */ -@media (prefers-color-scheme: dark) { - .msg-card { - border-color: alpha(@window_fg_color, 0.2); - } - - .msg-card:hover { - box-shadow: 0 2px 8px alpha(@window_fg_color, 0.2); - } -} \ No newline at end of file diff --git a/app/src/theming/mod.rs b/app/src/theming/mod.rs deleted file mode 100644 index 32679a8..0000000 --- a/app/src/theming/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Theming support for Asgard Mail - -use gtk4::CssProvider; - -pub fn load_css() { - let provider = CssProvider::new(); - provider.load_from_data(include_str!("asgard.css")); - - if let Some(display) = gtk4::gdk::Display::default() { - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - } -} - diff --git a/app/src/thread_helpers.rs b/app/src/thread_helpers.rs deleted file mode 100644 index 22418a6..0000000 --- a/app/src/thread_helpers.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Thread helpers for email threading - -use asgard_core::message::Message; -use std::collections::{HashMap, HashSet}; -use time::OffsetDateTime; - -/// Simple thread representation for demo purposes -#[derive(Debug, Clone)] -pub struct Thread { - pub id: String, - pub subject: String, - pub messages: Vec, - pub last_date: OffsetDateTime, - pub any_unread: bool, - pub last_is_outgoing_reply: bool, - pub has_attachments: bool, -} - -impl Thread { - pub fn new(id: String, subject: String, messages: Vec) -> Self { - let any_unread = messages.iter().any(|m| !m.is_read()); - let has_attachments = messages.iter().any(|m| m.has_attachments()); - let last_date = messages.last().map(|m| m.headers.date.unwrap_or(OffsetDateTime::UNIX_EPOCH)).unwrap_or(OffsetDateTime::UNIX_EPOCH); - let last_is_outgoing_reply = messages.last() - .map(|m| is_outgoing_message(m) && is_reply_message(m)) - .unwrap_or(false); - - Self { - id, - subject, - messages, - last_date, - any_unread, - last_is_outgoing_reply, - has_attachments, - } - } - - /// Get the last message in the thread - pub fn last(&self) -> Option<&Message> { - self.messages.last() - } - - /// Get the number of messages in the thread - pub fn count(&self) -> usize { - self.messages.len() - } - - /// Check if any message in the thread is unread - pub fn any_unread(&self) -> bool { - self.any_unread - } - - /// Check if the thread has attachments - pub fn has_attachments(&self) -> bool { - self.has_attachments - } - - /// Check if the last message is an outgoing reply - pub fn last_is_outgoing_reply(&self) -> bool { - self.last_is_outgoing_reply - } -} - -/// Group messages into threads using JWZ-style threading -pub fn group_into_threads(mut msgs: Vec) -> Vec { - // Sort messages by date (oldest first) - msgs.sort_by_key(|m| m.headers.date.unwrap_or(OffsetDateTime::UNIX_EPOCH)); - - // 1) P1: server thread id (if available) - let (with_tid, without_tid): (Vec<_>, Vec<_>) = - msgs.into_iter().partition(|m| m.headers.message_id.is_some()); - - let mut buckets: HashMap> = HashMap::new(); - for m in with_tid { - if let Some(tid) = &m.headers.message_id { - buckets.entry(tid.clone()).or_default().push(m); - } - } - - // 2) P2: JWZ style threading on the rest - let jwz_threads = jwz_threads(without_tid); - for (tid, v) in jwz_threads { - buckets.entry(tid).or_default().extend(v); - } - - // 3) Normalize each bucket, compute summaries - let mut threads: Vec = buckets.into_iter().map(|(tid, mut v)| { - v.sort_by_key(|m| m.headers.date.unwrap_or(OffsetDateTime::UNIX_EPOCH)); // oldest→newest - let subject = canonical_subject(v.last().map(|m| m.headers.subject.clone()).unwrap_or_default()); - Thread::new(tid, subject, v) - }).collect(); - - // Sort threads by last message time descending for the middle pane - threads.sort_by_key(|t| t.last_date); - threads.reverse(); - threads -} - -// Build nodes for Message-ID and placeholders referenced in References -#[derive(Clone)] -struct Node { - mid: String, - msg: Option, - parent: Option, - children: Vec -} - -/// JWZ-style threading algorithm implementation -fn jwz_threads(messages: Vec) -> HashMap> { - let mut nodes: HashMap = HashMap::new(); - - // pass 1: create nodes - for m in &messages { - if let Some(mid) = &m.headers.message_id { - nodes.entry(mid.clone()).or_insert(Node{ - mid: mid.clone(), - msg: Some(m.clone()), - parent: None, - children: vec![] - }); - } - for r in &m.headers.references { - nodes.entry(r.clone()).or_insert(Node{ - mid: r.clone(), - msg: None, - parent: None, - children: vec![] - }); - } - } - - // pass 2: link using References (parent = last ref) or In-Reply-To - for m in &messages { - if let Some(mid) = &m.headers.message_id { - let parent_id = m.headers.references.as_ref().and_then(|r| r.split_whitespace().last().map(|s| s.to_string())).or_else(|| m.headers.in_reply_to.clone()); - if let Some(pid) = parent_id { - if nodes.contains_key(&pid) { - nodes.get_mut(mid).unwrap().parent = Some(pid.clone()); - nodes.get_mut(&pid).unwrap().children.push(mid.clone()); - } - } - } - } - - // roots are nodes with no parent and with real messages - let mut roots: Vec = nodes.values() - .filter(|n| n.parent.is_none() && n.msg.is_some()) - .map(|n| n.mid.clone()).collect(); - - // collect threads from roots - let mut out: HashMap> = HashMap::new(); - for root in roots.drain(..) { - let tid = root.clone(); // synthetic id: root Message-ID - let mut acc: Vec = Vec::new(); - collect_inorder(&nodes, &root, &mut acc); - out.insert(tid, acc); - } - - // orphans: nodes with a msg but not in any collected thread - let seen: HashSet = out.values().flatten() - .filter_map(|m| m.headers.message_id.clone()).collect(); - for n in nodes.values() { - if let Some(m) = &n.msg { - if !m.headers.message_id.as_ref().map(|x| seen.contains(x)).unwrap_or(false) { - let tid = m.headers.message_id.clone().unwrap_or_else(|| format!("midless-{}", m.id)); - out.entry(tid).or_default().push(m.clone()); - } - } - } - out -} - -/// Recursively collect messages in thread order -fn collect_inorder(nodes: &HashMap, id: &String, acc: &mut Vec) { - if let Some(n) = nodes.get(id) { - if let Some(m) = &n.msg { - acc.push(m.clone()); - } - // order children by Date header if available - let mut childs = n.children.clone(); - childs.sort_by_key(|cid| nodes.get(cid).and_then(|c| c.msg.as_ref()).map(|m| m.headers.date.unwrap_or(OffsetDateTime::UNIX_EPOCH))); - for c in childs { - collect_inorder(nodes, &c, acc); - } - } -} - -/// Get canonical subject for a thread -fn canonical_subject(s: String) -> String { - normalize_subject(&s) -} - -/// Normalize subject for threading -pub fn normalize_subject(raw: &str) -> String { - use regex::Regex; - let mut s = raw.trim().to_string(); - // strip [list] tags - let re_list = Regex::new(r"^\s*\[[^\]]+\]\s*").unwrap(); - s = re_list.replace(&s, "").into_owned(); - // strip reply/forward tokens in many locales - let re_rf = Regex::new(r"^((?i:re|fw|fwd|sv|aw|antw|rv)\s*:\s*)+").unwrap(); - s = re_rf.replace(&s, "").into_owned(); - s.split_whitespace().collect::>().join(" ").to_lowercase() -} - -/// Helper function to format date for display -pub fn format_date_short(date: &time::OffsetDateTime) -> String { - let now = time::OffsetDateTime::now_utc(); - let duration = now - *date; - - if duration.as_seconds_f64() < 60.0 { - "now".to_string() - } else if duration.whole_minutes() < 60 { - format!("{}m ago", duration.whole_minutes()) - } else if duration.whole_hours() < 24 { - format!("{}h ago", duration.whole_hours()) - } else if duration.whole_days() == 1 { - "Yesterday".to_string() - } else if duration.whole_days() < 7 { - // Simple weekday format - match date.weekday() { - time::Weekday::Monday => "Mon".to_string(), - time::Weekday::Tuesday => "Tue".to_string(), - time::Weekday::Wednesday => "Wed".to_string(), - time::Weekday::Thursday => "Thu".to_string(), - time::Weekday::Friday => "Fri".to_string(), - time::Weekday::Saturday => "Sat".to_string(), - time::Weekday::Sunday => "Sun".to_string(), - } - } else if date.year() == now.year() { - format!("{} {}", date.month(), date.day()) - } else { - format!("{}/{}/{}", date.year(), date.month() as u8, date.day()) - } -} - -/// Determine if a message is outgoing based on RFC standards -/// A message is outgoing if: -/// 1. It's in the "Sent" mailbox, OR -/// 2. The sender matches the account's email address -pub fn is_outgoing_message(message: &Message) -> bool { - // Check if message is in Sent mailbox - if message.mailbox_id.to_string().contains("sent") { - return true; - } - - // For demo purposes, check if sender contains the demo email - // In a real implementation, this would check against the account's email - message.headers.from.iter().any(|addr| addr.email.contains("demo@example.com")) -} - -/// Determine if a message is a reply based on RFC standards -/// A message is a reply if it has: -/// 1. In-Reply-To header, OR -/// 2. References header with content -pub fn is_reply_message(message: &Message) -> bool { - message.headers.in_reply_to.is_some() || message.headers.references.as_ref().map_or(false, |r| !r.is_empty()) -} diff --git a/app/src/tray.rs b/app/src/tray.rs deleted file mode 100644 index 88debd5..0000000 --- a/app/src/tray.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! System tray functionality for Asgard Mail - -use asgard_core::config::Config; -use asgard_core::error::AsgardResult; -// use gtk4::prelude::*; -// use gtk4::{Menu, MenuItem, SeparatorMenuItem}; -use std::sync::Arc; -use tokio::sync::Mutex; - -/// System tray for the application -pub struct SystemTray { - /// Application configuration - config: Config, - /// Tray state - state: Arc>, -} - -/// Internal tray state -struct TrayState { - /// Whether tray is enabled - enabled: bool, - /// Whether tray is visible - visible: bool, -} - -impl SystemTray { - /// Create a new system tray - pub fn new(config: &Config) -> AsgardResult { - let state = Arc::new(Mutex::new(TrayState { - enabled: config.ui.show_tray, - visible: false, - })); - - Ok(Self { - config: config.clone(), - state, - }) - } - - /// Start the system tray - pub async fn start(&self) -> AsgardResult<()> { - let mut state = self.state.lock().await; - - if !state.enabled { - return Ok(()); - } - - // For now, just mark as visible - // In a real implementation, this would create the actual system tray - state.visible = true; - - tracing::info!("System tray started"); - Ok(()) - } - - /// Stop the system tray - pub async fn stop(&self) -> AsgardResult<()> { - let mut state = self.state.lock().await; - state.visible = false; - - tracing::info!("System tray stopped"); - Ok(()) - } - - /// Show the system tray - pub async fn show(&self) -> AsgardResult<()> { - let mut state = self.state.lock().await; - if state.enabled { - state.visible = true; - } - Ok(()) - } - - /// Hide the system tray - pub async fn hide(&self) -> AsgardResult<()> { - let mut state = self.state.lock().await; - state.visible = false; - Ok(()) - } - - /// Check if system tray is enabled - pub async fn is_enabled(&self) -> bool { - let state = self.state.lock().await; - state.enabled - } - - /// Check if system tray is visible - pub async fn is_visible(&self) -> bool { - let state = self.state.lock().await; - state.visible - } - - /// Enable system tray - pub async fn enable(&self) { - let mut state = self.state.lock().await; - state.enabled = true; - } - - /// Disable system tray - pub async fn disable(&self) { - let mut state = self.state.lock().await; - state.enabled = false; - state.visible = false; - } -} - -impl Clone for SystemTray { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - state: self.state.clone(), - } - } -} \ No newline at end of file diff --git a/app/src/widgets/mailbox_tree.rs b/app/src/widgets/mailbox_tree.rs deleted file mode 100644 index f1f1a08..0000000 --- a/app/src/widgets/mailbox_tree.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Mailbox tree widget for sidebar navigation - -use gtk4::prelude::*; -use gtk4::{Box as GtkBox, ListBox, ListBoxRow, Label, Orientation, Image, ScrolledWindow}; -// use libadwaita::prelude::*; -// use libadwaita::ActionRow; -use asgard_core::storage::StorageManager; -use std::sync::Arc; -use tokio::sync::Mutex; -use std::collections::HashMap; - -/// Mailbox tree widget for the sidebar -pub struct MailboxTree { - /// Main widget container - pub widget: GtkBox, - /// List box for mailbox items - list_box: ListBox, - /// Storage manager - storage: Arc>, - /// Accordion states (mailbox_name -> expanded) - accordion_states: HashMap, -} - -impl MailboxTree { - /// Create a new mailbox tree widget - pub fn new(storage: Arc>) -> Self { - let widget = GtkBox::new(Orientation::Vertical, 0); - widget.set_size_request(280, -1); - widget.add_css_class("sidebar"); - - let list_box = ListBox::new(); - list_box.set_selection_mode(gtk4::SelectionMode::Single); - list_box.add_css_class("sidebar"); - - let scrolled_window = ScrolledWindow::new(); - scrolled_window.set_child(Some(&list_box)); - scrolled_window.set_policy(gtk4::PolicyType::Never, gtk4::PolicyType::Automatic); - scrolled_window.set_vexpand(true); - scrolled_window.set_hexpand(false); - - widget.append(&scrolled_window); - - Self { - widget, - list_box, - storage, - accordion_states: HashMap::new(), - } - } - - /// Build the mailbox tree with demo data - pub fn build_demo(&self) { - // Clear existing items - while let Some(child) = self.list_box.first_child() { - self.list_box.remove(&child); - } - - // FAVORITES section - self.add_section_header("FAVORITES"); - - // All Inboxes - EXPANDABLE - let all_inboxes_expanded = self.accordion_states.get("ALL_INBOXES").copied().unwrap_or(false); - if let Some((_revealer, _child_container)) = self.add_sidebar_item("All Inboxes", "mail-inbox-symbolic", false, "ALL_INBOXES", true, all_inboxes_expanded) { - // Add account children for All Inboxes - self.add_sidebar_item("Demo Account", "", false, "demo:INBOX", false, false); - } - - self.add_sidebar_item("VIPs", "emblem-favorite-symbolic", false, "VIPS", false, false); - self.add_sidebar_item("Flagged", "flag-symbolic", false, "FLAGGED", false, false); - self.add_sidebar_item("Reminders", "alarm-symbolic", false, "REMINDERS", false, false); - - // All Drafts - EXPANDABLE - let all_drafts_expanded = self.accordion_states.get("ALL_DRAFTS").copied().unwrap_or(false); - if let Some((_revealer2, _child_container2)) = self.add_sidebar_item("All Drafts", "mail-drafts-symbolic", false, "ALL_DRAFTS", true, all_drafts_expanded) { - // Add account children for All Drafts - self.add_sidebar_item("Demo Account", "", false, "demo:DRAFTS", false, false); - } - - // All Sent - EXPANDABLE - let all_sent_expanded = self.accordion_states.get("ALL_SENT").copied().unwrap_or(false); - if let Some((_revealer3, _child_container3)) = self.add_sidebar_item("All Sent", "mail-sent-symbolic", false, "ALL_SENT", true, all_sent_expanded) { - // Add account children for All Sent - self.add_sidebar_item("Demo Account", "", false, "demo:SENT", false, false); - } - - // Individual account sections - self.add_section_header("DEMO ACCOUNT"); - - // Standard mailboxes for the demo account - self.add_sidebar_item("Inbox", "mail-inbox-symbolic", true, "demo:INBOX", false, false); - self.add_sidebar_item("Drafts", "mail-drafts-symbolic", false, "demo:DRAFTS", false, false); - self.add_sidebar_item("Sent", "mail-sent-symbolic", false, "demo:SENT", false, false); - self.add_sidebar_item("Junk", "mail-junk-symbolic", false, "demo:JUNK", false, false); - self.add_sidebar_item("Trash", "user-trash-symbolic", false, "demo:TRASH", false, false); - self.add_sidebar_item("Archive", "mail-archive-symbolic", false, "demo:ARCHIVE", false, false); - } - - fn add_section_header(&self, title: &str) { - let header = Label::new(Some(title)); - header.add_css_class("sidebar-section"); - header.set_xalign(0.0); - header.set_margin_start(12); - header.set_margin_end(12); - header.set_margin_top(8); - header.set_margin_bottom(4); - - let row = ListBoxRow::new(); - row.set_child(Some(&header)); - row.set_selectable(false); - row.set_activatable(false); - self.list_box.append(&row); - } - - fn add_sidebar_item( - &self, - name: &str, - icon_name: &str, - is_selected: bool, - mailbox_name: &str, - expandable: bool, - expanded: bool, - ) -> Option<(gtk4::Revealer, ListBox)> { - let row = ListBoxRow::new(); - row.set_activatable(true); - - let container = GtkBox::new(Orientation::Horizontal, 8); - container.set_margin_start(8); - container.set_margin_end(12); - container.set_margin_top(6); - container.set_margin_bottom(6); - - // Expand symbol (if expandable) - if expandable { - let expand_symbol = Label::new(Some(if expanded { "×" } else { "+" })); - expand_symbol.add_css_class("expand-symbol"); - expand_symbol.set_xalign(0.0); - container.append(&expand_symbol); - } else { - // Empty spacer to maintain alignment when not expandable - let spacer = GtkBox::new(Orientation::Horizontal, 0); - spacer.set_size_request(16, -1); // Same width as expand symbol - container.append(&spacer); - } - - // Icon (only if icon_name is not empty) - if !icon_name.is_empty() { - let icon = Image::from_icon_name(icon_name); - icon.set_icon_size(gtk4::IconSize::Normal); - icon.add_css_class("mailbox-icon"); - container.append(&icon); - } - - // Label - let label = Label::builder() - .label(name) - .xalign(0.0) - .hexpand(true) - .build(); - - container.append(&label); - - // Unread count badge (demo data) - let unread_count = if mailbox_name.contains("INBOX") { 2 } else { 0 }; - if unread_count > 0 { - let badge = Label::builder() - .label(&unread_count.to_string()) - .build(); - badge.add_css_class("count-badge"); - container.append(&badge); - } - - row.set_child(Some(&container)); - self.list_box.append(&row); - - if is_selected { - self.list_box.select_row(Some(&row)); - } - - // Handle expandable functionality - let mut revealer = None; - let mut child_container = None; - - if expandable { - // Create revealer for child items - let revealer_widget = gtk4::Revealer::new(); - revealer_widget.set_reveal_child(expanded); - - // Create a container for child items - let child_list = ListBox::new(); - child_list.add_css_class("sidebar"); - revealer_widget.set_child(Some(&child_list)); - - // Add separate click handlers for expand symbol and label - let expand_symbol = if expandable { - // Find the expand symbol we just added - container.first_child().unwrap().downcast::