This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OutlookAddin/.taskmaster/tasks/tasks.json
T
PointStar aa65546fb1 feat(settings): add first-run Settings dialog and ribbon entry
- SettingsDialog (WPF, RTL): General tab (URL/username/API key/locale) +
  About tab; PasswordBox-bound API key, Test Connection probe, Save/Cancel
- SettingsViewModel: loads from SettingsManager + DpapiCredentialStore,
  Test Connection spins a transient HttpClient + EspoCrmClient and reports
  401 vs network failures separately; Save persists then signals dialog
  close
- Ribbon: second "הגדרות" button in Marcus-Law group; "File to EspoCRM"
  now offers to open Settings inline when not configured
- AddInHost.LaunchSettings tears down and rebuilds CRM/filing/retry stack
  so new credentials take effect immediately (CrmClientChanged event)
- Tag .taskmaster/tasks/tasks.json: Task #3 in-review with implementation
  notes (was already locally modified)

Core/UI/Tests green (38 tests passing); VSTO host project still requires
VS Installer Repair to compile locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:37:39 +03:00

131 lines
42 KiB
JSON

{
"master": {
"tasks": [
{
"id": 1,
"title": "Provision Windows CI runner and code-signing infrastructure",
"description": "Set up Windows runner on Coolify with VS Build Tools + VSTO workload, generate self-signed code-signing certificate, configure Gitea Actions runner, and store credentials in Infisical.",
"details": "1. Provision Windows 11 Pro VM on Coolify dev server (4 vCPU/8GB)\n2. Install VS Build Tools 2022 with workloads: VisualStudioExtension + Microsoft.VisualStudio.Workload.Office\n3. Install gitea/act_runner as Windows service, register with gitea.dev.marcus-law.co.il using label 'windows'\n4. Generate self-signed code-signing certificate:\n ```powershell\n $cert = New-SelfSignedCertificate -Subject \"CN=Marcus-Law OutlookAddin\" -Type CodeSigningCert -CertStoreLocation Cert:\\CurrentUser\\My -NotAfter (Get-Date).AddYears(3)\n $thumbprint = $cert.Thumbprint\n $pfxPassword = ConvertTo-SecureString -String \"<generated-password>\" -Force -AsPlainText\n Export-PfxCertificate -Cert $cert -FilePath \"C:\\codesign.pfx\" -Password $pfxPassword\n ```\n5. Export public cert (.cer) for GPO distribution to lawyer PCs\n6. Store in Infisical /outlook-addin (dev env):\n - CODESIGN_THUMBPRINT (string)\n - CODESIGN_PFX_BASE64 (base64-encoded PFX bytes)\n - CODESIGN_PASSWORD (string)\n - PLATFORM_DEV_SSH_KEY (SSH private key for rsync to platform.dev)\n7. Add Gitea Actions secrets referencing Infisical keys\n8. Verify runner by pushing a no-op commit and watching build.yml execute\n9. Document GPO cert deployment in docs/IT-SETUP.md (import to Trusted Publishers + Trusted Root CA on all 10 lawyer machines)",
"testStrategy": "Push a test commit that triggers .gitea/workflows/build.yml. Verify: (1) job runs on windows runner, (2) nuget restore succeeds, (3) msbuild compiles all 4 projects without errors, (4) signtool can access certificate via thumbprint from secrets. Check Gitea Actions logs for green build.",
"priority": "high",
"dependencies": [],
"status": "pending",
"subtasks": []
},
{
"id": 2,
"title": "Implement Core services: EspoCrmClient, CredentialStore, SettingsManager, Logger",
"description": "Build the foundational Core layer services for HTTP communication with EspoCRM, encrypted credential storage via DPAPI, JSON settings persistence, and Serilog file logging.",
"details": "In OutlookAddin.Core project:\n\n1. **EspoCrmClient** (`Services/EspoCrmClient.cs`):\n - Constructor accepts (baseUrl, username, apiKey, ILogger)\n - Auth: `Espo-Authorization` header = base64(username:apiKey)\n - HttpClient with Polly resilience pipeline:\n ```csharp\n var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()\n .AddTimeout(TimeSpan.FromSeconds(15))\n .AddRetry(new RetryStrategyOptions<HttpResponseMessage> {\n MaxRetryAttempts = 3,\n Delay = TimeSpan.FromMilliseconds(500),\n BackoffType = DelayBackoffType.Exponential,\n ShouldHandle = new PredicateBuilder<HttpResponseMessage>()\n .Handle<HttpRequestException>()\n .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)\n })\n .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage> {\n FailureRatio = 0.5,\n MinimumThroughput = 5,\n BreakDuration = TimeSpan.FromSeconds(30)\n })\n .Build();\n ```\n - Methods:\n - `Task<FileEmailResponse> FileMailAsync(FileEmailRequest req)` → POST /api/v1/MailRouter/file\n - `Task<SearchResult> GlobalSearchAsync(string query)` → GET /api/v1/GlobalSearch?q={query}\n - `Task<List<EmailAddressMatch>> EmailAddressSearchAsync(string addr, string? entityType)` → GET /api/v1/EmailAddress/search?q={addr}&entityType={type}\n - `Task<CaseEntity> GetCaseAsync(string id)` → GET /api/v1/Case/{id}\n - `Task<AccountEntity> GetAccountAsync(string id)` → GET /api/v1/Account/{id}\n - `Task<ContactEntity> GetContactAsync(string id)` → GET /api/v1/Contact/{id}\n - `Task<UserInfo> TestConnectionAsync()` → GET /api/v1/App/user\n - Handle 401 (throw UnauthorizedException), 409 (treat as success for filing)\n\n2. **DTOs** (`Models/` folder):\n - FileEmailRequest: { parentType, parentId, subject, from, to, cc, bcc, body, dateSent, messageId, attachments: [{name, mimeType, contents (base64)}] }\n - FileEmailResponse: { id, status }\n - SearchResult: { list: SearchResultGroup[] } where SearchResultGroup: { name (entity type), list: EspoEntityRef[] }\n - EspoEntityRef: { id, name, entityType }\n - EmailAddressMatch, CaseEntity, AccountEntity, ContactEntity, UserInfo\n\n3. **DpapiCredentialStore** (`Services/DpapiCredentialStore.cs`):\n - Save(username, apiKey): encrypt with ProtectedData.Protect (DataProtectionScope.CurrentUser, entropy = SHA256(\"MarcusLaw.OutlookAddin\")), write to %APPDATA%\\MarcusLaw\\OutlookAddin\\creds.dat\n - Load(): decrypt, return (username, apiKey) tuple or null\n - Delete(): remove file\n\n4. **SettingsManager** (`Services/SettingsManager.cs`):\n - Load(): deserialize from %APPDATA%\\MarcusLaw\\OutlookAddin\\settings.json using System.Text.Json\n - Save(Settings): serialize and write\n - Settings model: { EspoCrmUrl, RecentEntities (List<EspoEntityRef>, max 10), WatchedFolders (List<WatchedFolder>), Locale (\"he-IL\"/\"en-US\") }\n\n5. **LoggerFactory** (`Services/LoggerFactory.cs`):\n - Init(): returns ILogger from Serilog.Log.Logger configured with:\n ```csharp\n Log.Logger = new LoggerConfiguration()\n .MinimumLevel.Information()\n .WriteTo.File(\n Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"MarcusLaw\", \"OutlookAddin\", \"logs\", \"addin-.log\"),\n rollingInterval: RollingInterval.Day,\n retainedFileCountLimit: 14,\n outputTemplate: \"{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}\")\n .CreateLogger();\n ```\n\n6. **Unit tests** (`OutlookAddin.Tests/Core/`):\n - EspoCrmClientTests: mock HttpMessageHandler, test happy path + 401 + 409 + network failure + circuit breaker trip\n - DpapiCredentialStoreTests: verify encrypt/decrypt round-trip, file creation\n - SettingsManagerTests: verify JSON serialization with Hebrew strings (RTL test)",
"testStrategy": "Run xUnit tests in OutlookAddin.Tests via `dotnet test`. All tests must pass. Mock HttpMessageHandler to return canned EspoCRM JSON responses. Verify: (1) Polly retries transient failures 3x, (2) circuit breaker opens after 5 consecutive failures, (3) DPAPI round-trip preserves credentials, (4) Settings JSON correctly handles Hebrew UTF-8. Use FluentAssertions for readable assertions.",
"priority": "high",
"dependencies": [
1
],
"status": "pending",
"subtasks": []
},
{
"id": 3,
"title": "Implement Feature 1: File existing mail to Case/Contact",
"description": "Build the primary filing feature: Explorer ribbon button, FileToCaseDialog with live search, FilingService that extracts MailItem properties on STA thread and POSTs to EspoCRM, and category stamping on success.",
"details": "1. **Ribbon XML** (`src/OutlookAddin/Ribbon/ExplorerRibbon.xml`):\n - Add button \"File to EspoCRM\" (Hebrew: \"תייק ל-EspoCRM\") in Mail tab, enabled when 1+ MailItems selected\n - Button callback: `FileToCaseDialog_Click`\n\n2. **FileToCaseDialog** (`src/OutlookAddin.UI/Views/FileToCaseDialog.xaml`):\n - WPF Window, 600x500, FlowDirection=RightToLeft\n - Search TextBox with 250ms debounce (use DispatcherTimer)\n - DataGrid grouped by entity type (Case/Contact/Lead/Account)\n - \"Recent\" ListView sidebar (load from SettingsManager.RecentEntities)\n - \"Create new Contact from sender\" button (if sender email not matched)\n - OK/Cancel buttons\n - ViewModel: `FileToCaseViewModel` (CommunityToolkit.Mvvm source generators)\n - `ObservableCollection<SearchResultGroup> SearchResults`\n - `RelayCommand<string> SearchCommand` → calls EspoCrmClient.GlobalSearchAsync\n - `RelayCommand<EspoEntityRef> SelectCommand` → sets SelectedEntity, closes dialog\n\n3. **FilingService** (`src/OutlookAddin.Core/Services/FilingService.cs`):\n - `Task<FilingResult> FileAsync(List<MailItemRef> items, EspoEntityRef target)`\n - MailItemRef is a DTO with primitives (Subject, SenderEmailAddress, Body, etc.) extracted on STA thread BEFORE passing to FileAsync\n - For each item:\n - Build FileEmailRequest:\n - subject, from, to/cc/bcc (string arrays), body (HTML), dateSent\n - messageId: extract via `mailItem.PropertyAccessor.GetProperty(\"http://schemas.microsoft.com/mapi/proptag/0x1035001F\")` (PR_INTERNET_MESSAGE_ID)\n - Attachments: iterate mailItem.Attachments, SaveAsFile to %TEMP%, read bytes, base64-encode, delete temp file\n - POST to EspoCrmClient.FileMailAsync\n - 200/201: success\n - 409: treat as success (already filed)\n - 401: throw UnauthorizedException → triggers re-auth dialog in UI layer\n - Other error: enqueue in DiskRetryQueue (see below)\n - After all items: marshal back to STA thread (via captured SynchronizationContext), stamp MailItem.Categories += \"Filed: {target.name}\"\n - Create category if not exists: `application.Session.Categories.Add(\"Filed: {name}\", OlCategoryColor.olCategoryColorBlue)`\n - Return FilingResult: { SuccessCount, FailedCount, QueuedCount }\n\n4. **DiskRetryQueue** (`src/OutlookAddin.Core/Services/DiskRetryQueue.cs`):\n - Enqueue(FileEmailRequest, target): serialize to %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\retry\\{guid}.json\n - ProcessQueue(): background task (30s interval), retry all queued items, delete on success\n - Max 3 retries per item, then move to failed/ subfolder\n\n5. **Toast notification** (use Windows.UI.Notifications via Microsoft.Toolkit.Uwp.Notifications NuGet):\n - Show summary: \"Filed 3 of 4 emails. 1 queued for retry.\"\n\n6. **Wire up in ThisAddIn.cs**:\n - Composition root: instantiate FilingService, DiskRetryQueue, start queue processor\n - Ribbon callback: capture selected MailItems on STA, extract primitives, show FileToCaseDialog, on OK call FilingService.FileAsync, show toast\n\n7. **Tests** (`OutlookAddin.Tests/Core/FilingServiceTests.cs`):\n - Mock EspoCrmClient, verify FileMailAsync called with correct payload\n - Test 401 handling (throw UnauthorizedException)\n - Test 409 handling (count as success)\n - Test retry queue enqueue on network failure",
"testStrategy": "Manual: F5 in VS, Outlook launches with add-in. Select 1-2 emails, click \"File to EspoCRM\", search for a case, confirm. Verify: (1) dialog shows search results, (2) email filed successfully, (3) category stamped, (4) toast displayed. Unit tests: verify FilingService logic with mocked HTTP client. Check logs in %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\ for request/response details.",
"priority": "high",
"dependencies": [
2
],
"status": "in-review",
"subtasks": [],
"implementationNotes": "Implemented 2026-05-11 (commits e527aa8, 14ed1f6). Deltas from spec: (1) Used MessageBox for summary instead of UWP toast — simpler, can upgrade later. (2) Tests written under OutlookAddin.Tests/Services/ rather than /Core/. (3) Dialog placed in OutlookAddin.UI/Dialogs/ rather than /Views/. (4) Retry queue keeps items on disk after max attempts (3) instead of moving to failed/ — operator can inspect in retry-queue/ folder. (5) MailItem extraction lives in OutlookAddin/Services/MailItemExtractor.cs (host project, since Outlook interop). Build verified for Core/UI/Tests (38 tests green); host project requires VS Installer Repair to compile (VSTO targets still missing on dev machine). Outstanding: first-run settings dialog for credential entry — host shows \"תוסף עדיין לא מוגדר\" if creds.dat absent."
},
{
"id": 4,
"title": "Implement Feature 2: Reading-pane sidebar with sender matching",
"description": "Build CustomTaskPane that displays matched Case/Contact for the selected email's sender, with live lookup via EspoCRM EmailAddress/search API and in-memory caching.",
"details": "1. **CustomTaskPane registration** (in `ThisAddIn.cs`):\n - In ThisAddIn_Startup, for each Explorer window:\n ```csharp\n var explorer = Globals.ThisAddIn.Application.ActiveExplorer();\n var wpfView = new ReadingPaneView(); // WPF UserControl\n var winFormsHost = new System.Windows.Forms.Integration.ElementHost { Child = wpfView, Dock = DockStyle.Fill };\n var taskPane = CustomTaskPanes.Add(winFormsHost, \"Case Info\", explorer);\n taskPane.DockPosition = Office.MsoCTPDockPosition.msoCTPDockPositionRight;\n taskPane.Width = 320;\n taskPane.Visible = true;\n ```\n\n2. **ReadingPaneView** (`src/OutlookAddin.UI/Views/ReadingPaneView.xaml`):\n - WPF UserControl, FlowDirection=RightToLeft\n - Bound to `ReadingPaneViewModel`\n - States:\n - Loading: ProgressRing\n - Match found: Card with contact name, account name, recent cases (max 5), buttons: \"Open in EspoCRM\" (deep link), \"File to this\", \"File elsewhere\"\n - No match: \"No match found\" + inline search TextBox\n - Error: error message\n\n3. **ReadingPaneViewModel** (`src/OutlookAddin.UI/ViewModels/ReadingPaneViewModel.cs`):\n - `ObservableProperty` for MatchResult (ContactEntity + AccountEntity + List<CaseEntity>)\n - `RelayCommand FileToThisCommand` → triggers FilingService with pre-selected case\n - `RelayCommand FileElsewhereCommand` → opens FileToCaseDialog\n - `RelayCommand OpenInEspoCrmCommand` → `Process.Start(\"https://crm.../#Case/view/{id}\")`\n\n4. **ExplorerSelectionHandler** (`src/OutlookAddin/Handlers/ExplorerSelectionHandler.cs`):\n - Subscribe to Explorer.SelectionChange\n - Debounce 200ms (use System.Threading.Timer)\n - On timer fire: marshal to STA, get Selection.Item(1) as MailItem, extract SenderEmailAddress (primitive string)\n - Pass to MatchingService.LookupAsync on threadpool\n\n5. **MatchingService** (`src/OutlookAddin.Core/Services/MatchingService.cs`):\n - `Task<MatchResult?> LookupAsync(string senderEmail)`:\n 1. Check MemoryCache (key = senderEmail, expiration = 10 min)\n 2. If miss:\n a. GET /api/v1/EmailAddress/search?q={senderEmail}&entityType=Contact\n b. If exactly 1 hit:\n - GET /api/v1/Contact/{id}?select=name,accountId\n - If accountId: GET /api/v1/Account/{accountId}?select=name\n - GET /api/v1/Case?where[linkedWith]=contacts:{id}&maxSize=5&orderBy=createdAt&order=desc\n c. Build MatchResult, cache, return\n 3. If 0 or 2+ hits: return null\n\n6. **Wire up**:\n - In ThisAddIn_Startup: instantiate MatchingService, ExplorerSelectionHandler\n - ExplorerSelectionHandler updates ReadingPaneViewModel on lookup completion\n\n7. **Tests** (`OutlookAddin.Tests/Core/MatchingServiceTests.cs`):\n - Mock EspoCrmClient, verify correct API sequence\n - Test cache hit (2nd call should not hit HTTP)\n - Test 0 matches, 1 match, 2+ matches (ambiguous)",
"testStrategy": "Manual: F5, open Outlook, click on an email in inbox. Verify: (1) sidebar appears on right, (2) after 200ms, displays contact/case info if sender is known, (3) \"Open in EspoCRM\" link works, (4) \"File to this\" triggers filing. Unit tests: verify MatchingService cache behavior, API call sequence. Use Moq to verify MemoryCache.Set called with 10min expiration.",
"priority": "medium",
"dependencies": [
3
],
"status": "pending",
"subtasks": []
},
{
"id": 5,
"title": "Implement Feature 3: Compose from Case (bidirectional)",
"description": "Enable composing emails from Outlook linked to EspoCRM Case (Inspector ribbon button + outlookaddin:// URL protocol), and auto-file on send via ItemSend handler with Sent Items watcher for IMAP latency.",
"details": "1. **Inspector Ribbon** (`src/OutlookAddin/Ribbon/InspectorRibbon.xml`):\n - Button \"Compose from Case\" (Hebrew: \"כתוב מתיק\"), visible only on MailItem compose windows (not read/reply)\n - Callback: `ComposeFromCase_Click`\n\n2. **ComposeFromCaseDialog** (`src/OutlookAddin.UI/Views/ComposeFromCaseDialog.xaml`):\n - Similar to FileToCaseDialog but filter results to entityType=Case only\n - On OK: returns selected CaseEntity (id, name, number, primary contact email)\n\n3. **Compose logic** (in Inspector ribbon callback or URL protocol handler):\n - GET /api/v1/Case/{id}?select=name,number,contactsIds,accountId\n - If contactsIds[0] exists: GET /api/v1/Contact/{contactsIds[0]}?select=emailAddress\n - Set MailItem properties on STA thread:\n ```csharp\n mailItem.To = contactEmail;\n mailItem.Subject = $\"[{caseNumber}] \";\n mailItem.HTMLBody += $\"<hr><p style='direction:rtl;'>תיק: <a href='https://crm.../#{caseId}'>{caseName}</a></p>\";\n ```\n - Stamp UserProperty (custom MAPI property):\n ```csharp\n var prop = mailItem.UserProperties.Add(\"EspoCaseId\", Outlook.OlUserPropertyType.olText, false);\n prop.Value = caseId;\n var guidProp = mailItem.UserProperties.Add(\"OutlookAddinGuid\", Outlook.OlUserPropertyType.olText, false);\n guidProp.Value = Guid.NewGuid().ToString(); // for dedup in Sent Items watcher\n ```\n\n4. **URL protocol handler** (`src/OutlookAddinProtocolHandler/`):\n - New console app project (net48): OutlookAddinProtocolHandler.csproj\n - Entry point parses `outlookaddin://compose?caseId=<id>`\n - Writes JSON message to named pipe `\\\\.\\pipe\\MarcusLaw.OutlookAddin`: `{ \"action\": \"compose\", \"caseId\": \"<id>\" }`\n - Add to OutlookAddin.sln\n\n5. **Named pipe listener** (in `ThisAddIn.cs`):\n - In ThisAddIn_Startup: spawn background thread (or Task.Run loop) with NamedPipeServerStream\n - On message received: marshal to STA, trigger compose flow\n\n6. **Registry setup** (in ClickOnce installer or manual via .reg file):\n - HKCU\\Software\\Classes\\outlookaddin\n - (Default) = \"URL:OutlookAddin Protocol\"\n - URL Protocol = \"\"\n - HKCU\\Software\\Classes\\outlookaddin\\shell\\open\\command\n - (Default) = \"\\\"C:\\\\Users\\\\<user>\\\\AppData\\\\Local\\\\Apps\\\\OutlookAddinProtocolHandler.exe\\\" \\\"%1\\\"\"\n\n7. **ItemSend handler**:\n - Subscribe Application.ItemSend\n - Read MailItem.UserProperties[\"EspoCaseId\"] and [\"OutlookAddinGuid\"]\n - If EspoCaseId present:\n - Let send complete (Cancel = false)\n - Subscribe to Sent Items folder's Items.ItemAdd:\n ```csharp\n var sentItems = application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);\n sentItems.Items.ItemAdd += SentItemsWatcher;\n ```\n - In SentItemsWatcher: check if newly added MailItem has matching OutlookAddinGuid\n - If match found (within 60s timeout): extract primitives, call FilingService.FileAsync\n - Unsubscribe after match or timeout\n\n8. **Tests** (`OutlookAddin.Tests/Integration/`):\n - Mock named pipe message parsing\n - Unit test UserProperty stamping logic (hard to test without real Outlook COM)",
"testStrategy": "Manual: (1) Outlook compose window, click \"Compose from Case\", select a case, verify To/Subject/footer populated and UserProperty stamped. (2) Send email, wait 10s, check if it auto-files to the case (check EspoCRM via API or logs). (3) Test URL protocol: run `start outlookaddin://compose?caseId=<id>` in cmd, verify Outlook compose window opens with case pre-filled. Unit tests: verify UserProperty stamping logic with mocked MailItem (if feasible).",
"priority": "medium",
"dependencies": [
3
],
"status": "pending",
"subtasks": []
},
{
"id": 6,
"title": "Implement Feature 4: Folder auto-sync with confidence-based filing",
"description": "Build folder watching system with TreeView settings UI, per-folder configuration (auto-file mode, confidence threshold), FolderWatcher singleton that subscribes to Items.ItemAdd with static field references to prevent GC, and toast notifications for manual review.",
"details": "1. **SettingsDialog** (`src/OutlookAddin.UI/Views/SettingsDialog.xaml`):\n - TabControl with tabs: General, Folders, About\n - **Folders tab**:\n - TreeView mirroring Outlook folder hierarchy (use Application.Session.Folders recursively)\n - Each node: checkbox (watched or not), if checked:\n - ComboBox: \"Default parent\" (nullable, search via entity picker)\n - ComboBox: \"Mode\" (Auto-file when confident / Notify only)\n - Slider: \"Confidence threshold\" (0.0-1.0, default 0.8)\n - Save button → persist to SettingsManager.Settings.WatchedFolders\n\n2. **WatchedFolder model** (`src/OutlookAddin.Core/Models/WatchedFolder.cs`):\n ```csharp\n public class WatchedFolder {\n public string StoreId { get; set; } // Store.StoreID\n public string FolderPath { get; set; } // e.g., \"\\\\Inbox\\\\Clients\"\n public string? DefaultParentId { get; set; }\n public string? DefaultParentType { get; set; }\n public WatchMode Mode { get; set; } // AutoFile | NotifyOnly\n public double ConfidenceThreshold { get; set; } = 0.8;\n }\n public enum WatchMode { AutoFile, NotifyOnly }\n ```\n\n3. **FolderWatcher singleton** (`src/OutlookAddin/Services/FolderWatcher.cs`):\n - **CRITICAL:** Store Items references in a STATIC field to prevent GC unsubscribe:\n ```csharp\n private static readonly List<Outlook.Items> _itemsRefs = new();\n ```\n - `Initialize(List<WatchedFolder> folders)`:\n - For each folder:\n - Resolve Folder via GetFolderFromPath(StoreId, FolderPath)\n - Get Items collection: `folder.Items`\n - Add to _itemsRefs\n - Subscribe Items.ItemAdd += OnItemAdd\n - `OnItemAdd(object item)`:\n - Cast to MailItem\n - Capture EntryID immediately (string primitive)\n - Check HashSet<string> _processedEntryIds for dedup\n - Queue work to STA-bound TaskScheduler (use TaskScheduler.FromCurrentSynchronizationContext captured at init)\n - Extract SenderEmailAddress\n - Call MatchingService.LookupAsync(sender)\n - If exactly 1 match + confidence >= threshold + mode == AutoFile:\n - Extract MailItem primitives\n - Call FilingService.FileAsync with default parent or matched entity\n - Else:\n - Set MailItem.Categories += \"Needs filing\" (on STA)\n - Show toast: \"New email needs manual filing: {subject}\"\n\n4. **GetFolderFromPath helper**:\n - Recursively traverse Application.Session.Stores[storeId].GetRootFolder().Folders\n - Match by FolderPath (split by \\\\)\n\n5. **Wire up in ThisAddIn.cs**:\n - In ThisAddIn_Startup: instantiate FolderWatcher, load settings, call Initialize\n - On settings change: call FolderWatcher.Reinitialize (unsubscribe old, subscribe new)\n\n6. **Toast notifications** (Microsoft.Toolkit.Uwp.Notifications):\n - Template: \"OutlookAddin: New email in {folderName} needs filing. Subject: {subject}. Click to open.\"\n - Toast click action: activate Outlook and select the MailItem by EntryID\n\n7. **Tests** (`OutlookAddin.Tests/Integration/FolderWatcherTests.cs`):\n - Hard to unit test without real Outlook COM\n - Integration test: verify dedup logic (HashSet), verify ItemAdd callback triggers\n - Mock MatchingService, verify auto-file vs notify logic",
"testStrategy": "Manual: F5, open SettingsDialog → Folders tab, check a folder (e.g., Inbox\\Clients), set mode=Auto-file, threshold=0.8. Send a test email to that folder from a known contact. Verify: (1) MatchingService lookup triggered, (2) if 1 match with confidence >= 0.8, email auto-filed, (3) else, category \"Needs filing\" stamped + toast shown. Check logs for ItemAdd events. Integration test: verify Items reference in static field prevents GC (can't be fully unit tested, rely on manual testing).",
"priority": "medium",
"dependencies": [
4
],
"status": "pending",
"subtasks": []
},
{
"id": 7,
"title": "Implement Hebrew/English localization with RTL theme support",
"description": "Build LocalizationManager with resource files (Strings.he.resx, Strings.en.resx), XAML markup extension for bindings, RTL themes for all WPF views, and Hebrew font stack.",
"details": "1. **Resource files** (`src/OutlookAddin.UI/Resources/`):\n - `Strings.he.resx`: Hebrew strings for all UI text (dialog titles, button labels, error messages, ribbon labels, toast titles)\n - Examples:\n - FileToCaseDialog_Title = \"תייק מייל לתיק\"\n - SearchPlaceholder = \"חפש תיק, לקוח, איש קשר...\"\n - FileButton = \"תייק\"\n - CancelButton = \"ביטול\"\n - ErrorFileEmail = \"שגיאה בתיוק המייל: {0}\"\n - `Strings.en.resx`: English equivalents\n - Set `Strings.resx` as default (fallback to English)\n\n2. **LocalizationManager** (`src/OutlookAddin.UI/Services/LocalizationManager.cs`):\n - Singleton holding current CultureInfo\n - `string Get(string key)`: returns localized string from Strings.resx via ResourceManager\n - `void SetCulture(CultureInfo culture)`: changes current culture, raises PropertyChanged to rebind all UI\n - Default culture: `CultureInfo.CurrentUICulture` (OS default, usually he-IL in Israel)\n - Override via SettingsManager.Settings.Locale (\"he-IL\" / \"en-US\"), requires restart\n\n3. **XAML Markup Extension** (`src/OutlookAddin.UI/Markup/LocExtension.cs`):\n ```csharp\n [MarkupExtensionReturnType(typeof(string))]\n public class LocExtension : MarkupExtension {\n public string Key { get; set; }\n public override object ProvideValue(IServiceProvider serviceProvider) {\n return LocalizationManager.Instance.Get(Key);\n }\n }\n ```\n - Usage in XAML: `<TextBlock Text=\"{l:Loc Key=SearchPlaceholder}\" />`\n\n4. **RTL Themes** (`src/OutlookAddin.UI/Themes/RtlFonts.xaml`):\n - ResourceDictionary with styles:\n ```xaml\n <Style TargetType=\"Window\">\n <Setter Property=\"FlowDirection\" Value=\"RightToLeft\" />\n <Setter Property=\"FontFamily\" Value=\"Segoe UI, David, Frank Ruehl\" />\n </Style>\n <Style TargetType=\"UserControl\">\n <Setter Property=\"FlowDirection\" Value=\"RightToLeft\" />\n <Setter Property=\"FontFamily\" Value=\"Segoe UI, David, Frank Ruehl\" />\n </Style>\n ```\n - Merge in App.xaml (or each view's Resources)\n\n5. **Apply to all views**:\n - FileToCaseDialog.xaml, ComposeFromCaseDialog.xaml, SettingsDialog.xaml, ReadingPaneView.xaml: replace hardcoded strings with `{l:Loc Key=...}`\n - Set `FlowDirection=\"RightToLeft\"` on root element (or inherit from theme)\n\n6. **Ribbon localization**:\n - Ribbon XML supports `getLabel` callback attribute\n - Implement dynamic label callback in ribbon code-behind:\n ```csharp\n public string GetFileToEspoCrmLabel(Office.IRibbonControl control) {\n return LocalizationManager.Instance.Get(\"Ribbon_FileToEspoCrm\");\n }\n ```\n - Add to Strings.resx: `Ribbon_FileToEspoCrm = \"תייק ל-EspoCRM\"` (he), `\"File to EspoCRM\"` (en)\n\n7. **Toast localization**:\n - Wrap toast messages with `LocalizationManager.Get(\"Toast_FilingSuccess\", successCount, queuedCount)`\n - Use String.Format for placeholders\n\n8. **Test both locales**:\n - Add Settings → General tab → \"Language\" ComboBox (he-IL / en-US)\n - On change: SettingsManager.Save, show message \"Restart Outlook to apply\"\n - Manual testing in both locales\n\n9. **Update tests**:\n - Verify Strings.he.resx contains all keys (no missing translations)\n - Unit test LocalizationManager.Get with he-IL and en-US cultures",
"testStrategy": "Manual: F5 with OS locale = he-IL, verify all dialogs, ribbons, toasts display Hebrew text, RTL layout, correct font. Change Settings → Language to en-US, restart Outlook, verify English text, LTR layout. Unit test: set Thread.CurrentThread.CurrentUICulture = new CultureInfo(\"he-IL\"), verify LocalizationManager.Get(\"SearchPlaceholder\") returns Hebrew string. Verify all 4 features (Tasks 3-6) render correctly in Hebrew.",
"priority": "medium",
"dependencies": [
3,
4,
5,
6
],
"status": "pending",
"subtasks": []
},
{
"id": 8,
"title": "Finalize ClickOnce publish pipeline with code signing and automated deployment",
"description": "Complete the .gitea/workflows/build.yml publish job: invoke mage.exe to generate ClickOnce manifests, sign with signtool using cert from Infisical, rsync to platform.dev nginx, post Mattermost notification, and tag first release v1.0.0.",
"details": "1. **Update .gitea/workflows/build.yml publish job** (replace TODOs):\n\n a. **ClickOnce publish step**:\n ```yaml\n - name: Publish ClickOnce\n run: |\n $publishDir = \"publish\"\n $appFiles = \"src/OutlookAddin/bin/Release\"\n \n # Copy binaries to publish/Application Files/OutlookAddin_1_0_0_0/\n $version = \"1.0.0.0\" # TODO: extract from AssemblyInfo or tag\n $versionDir = \"$publishDir/Application Files/OutlookAddin_$($version.Replace('.', '_'))\"\n New-Item -ItemType Directory -Force -Path $versionDir\n Copy-Item -Recurse \"$appFiles/*\" $versionDir\n \n # Create application manifest\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -New Application `\n -ToFile \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -Name \"OutlookAddin\" `\n -Version $version `\n -FromDirectory $versionDir `\n -TrustLevel FullTrust\n \n # Sign application manifest\n $thumb = \"${{ secrets.CODESIGN_THUMBPRINT }}\"\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -Sign \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -CertHash $thumb `\n -TimeStampUri http://timestamp.digicert.com\n \n # Create deployment manifest (.application)\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -New Deployment `\n -ToFile \"$publishDir\\OutlookAddin.application\" `\n -Name \"Marcus-Law Outlook Add-in\" `\n -Version $version `\n -AppManifest \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -Install true `\n -Publisher \"Marcus-Law\" `\n -ProviderUrl \"https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.application\" `\n -UpdateEnabled true `\n -UpdateMode Foreground `\n -UpdateInterval 7 `\n -UpdateUnit days\n \n # Sign deployment manifest\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -Sign \"$publishDir\\OutlookAddin.application\" `\n -CertHash $thumb `\n -TimeStampUri http://timestamp.digicert.com\n \n # Create setup.exe bootstrapper\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x86\\setupbuilder.exe\" `\n -out \"$publishDir\\setup.exe\" `\n -appManifest \"$publishDir\\OutlookAddin.application\" `\n -prereqPackage Microsoft.Net.Framework.4.8\n ```\n\n b. **Upload to platform.dev step**:\n ```yaml\n - name: Upload to platform.dev\n env:\n SSH_KEY: ${{ secrets.PLATFORM_DEV_SSH_KEY }}\n run: |\n # Write SSH key to file\n $sshKeyPath = \"$env:TEMP\\platform_ssh_key\"\n [System.IO.File]::WriteAllBytes($sshKeyPath, [System.Convert]::FromBase64String($env:SSH_KEY))\n \n # rsync via WSL or use WinSCP (if available on runner)\n # Assuming WSL is installed on runner:\n wsl rsync -avz --delete -e \"ssh -i /mnt/c/Users/runneradmin/AppData/Local/Temp/platform_ssh_key -o StrictHostKeyChecking=no\" `\n ./publish/ `\n root@platform.dev.marcus-law.co.il:/var/www/outlook-addin/\n \n Remove-Item $sshKeyPath\n ```\n\n c. **Notify Mattermost step**:\n ```yaml\n - name: Notify Mattermost\n if: success()\n run: |\n # Fetch webhook URL from Infisical /mattermost/WEBHOOK_RELEASES (or hardcode as secret)\n $webhookUrl = \"${{ secrets.MATTERMOST_WEBHOOK_RELEASES }}\"\n $payload = @{\n text = \"✅ OutlookAddin ${{ github.ref_name }} released: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\"\n channel = \"git-verelasim\"\n } | ConvertTo-Json\n Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $payload -ContentType \"application/json\"\n ```\n\n2. **Configure platform.dev nginx**:\n - SSH to platform.dev.marcus-law.co.il\n - Add nginx config `/etc/nginx/sites-available/outlook-addin.conf`:\n ```nginx\n server {\n listen 443 ssl http2;\n server_name platform.dev.marcus-law.co.il;\n \n location /outlook-addin/ {\n alias /var/www/outlook-addin/;\n types {\n application/x-ms-application application;\n application/x-ms-manifest manifest;\n application/octet-stream vsto;\n application/octet-stream deploy;\n }\n add_header Cache-Control \"no-cache, must-revalidate\";\n }\n }\n ```\n - `sudo systemctl reload nginx`\n\n3. **Tag first release**:\n - After publish job succeeds, manually:\n ```bash\n git tag v1.0.0\n git push origin v1.0.0\n ```\n - Or automate in workflow: add step to create GitHub/Gitea release with release notes\n\n4. **Test end-to-end**:\n - On a clean Windows VM (not dev machine), navigate to https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n - Run setup.exe, verify ClickOnce installer prompts for install\n - Verify certificate is trusted (no security warning, because GPO pushed cert to Trusted Publishers)\n - After install, open Outlook, verify add-in loads\n - Check for auto-update: close Outlook, wait 7 days (or manually trigger update check in ClickOnce settings)\n\n5. **Document**:\n - Update docs/IT-SETUP.md with:\n - GPO cert deployment steps (copy .cer to \\\\\\domain\\SYSVOL\\...\\Scripts\\certs\\\\ + Group Policy → Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies → Trusted Publishers + Trusted Root CA)\n - Verify cert installed: `certutil -store -user TrustedPublisher` should list \"Marcus-Law OutlookAddin\"\n\n6. **Add secrets to Gitea Actions**:\n - Go to repo Settings → Secrets\n - Add: CODESIGN_THUMBPRINT, PLATFORM_DEV_SSH_KEY, MATTERMOST_WEBHOOK_RELEASES\n - Values from Infisical `/outlook-addin` and `/mattermost`",
"testStrategy": "1. Push a commit to main branch, verify build job succeeds. 2. Create and push tag v1.0.0, verify publish job runs and completes without errors. 3. Check platform.dev.marcus-law.co.il/outlook-addin/ via browser, verify setup.exe and .application files exist. 4. On a Windows VM with GPO cert, run setup.exe, verify no security warnings, Outlook add-in installs successfully. 5. Check Mattermost #git-verelasim channel for release notification. 6. Logs: check Gitea Actions logs for each step's output, verify signtool and mage.exe succeed.",
"priority": "high",
"dependencies": [
1,
7
],
"status": "pending",
"subtasks": []
},
{
"id": 9,
"title": "Create documentation, coordinate IT rollout, and provide user onboarding",
"description": "Write ONBOARDING.md (Hebrew, lawyer-facing) and IT-SETUP.md (admin), record demo video, coordinate GPO cert push with IT, generate per-user API keys in EspoCRM, distribute install links + credentials, and provide 48h office hours.",
"details": "1. **docs/ONBOARDING.md** (Hebrew):\n - Target audience: lawyers (non-technical)\n - Sections:\n - מה זה התוסף? (what is the add-in)\n - דרישות מערכת (system requirements: Win10/11, Office 2019/2021/M365, .NET 4.8)\n - התקנה:\n 1. לחץ על הלינק: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n 2. אשר את ההתקנה (ClickOnce)\n 3. הפעל מחדש את Outlook\n 4. בהפעלה הראשונה: הזן username + API key (שתקבל במייל)\n - שימוש:\n - **תיוק מייל קיים**: בחר מייל → לחץ \"תייק ל-EspoCRM\" ברצועת הכלים → חפש תיק/לקוח → אשר\n - **פרטי תיק בסרגל צד**: בחר מייל → הסרגל מימין מציג את התיק/לקוח המקושר\n - **כתיבת מייל מתיק**: חלון חדש של מייל → \"כתוב מתיק\" → בחר תיק → המייל יתויק אוטומטית בשליחה\n - **סנכרון תיקיות**: הגדרות → תיקיות → סמן תיקיות לניטור → בחר מצב (אוטומטי/ידני)\n - פתרון בעיות:\n - שגיאת 401: הזן מחדש API key (הגדרות → חיבור)\n - המייל לא התויק: בדוק לוגים ב-%LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\\\ + פנה לתמיכה\n - התוסף לא נטען: בדוק File → Options → Add-ins → Manage COM Add-ins → ודא ש-MarcusLaw.OutlookAddin מסומן\n\n2. **docs/IT-SETUP.md** (admin, English + Hebrew notes):\n - Target audience: IT admin\n - Sections:\n - Code-signing certificate deployment:\n 1. Export public cert (.cer) from codesign.pfx: `openssl pkcs12 -in codesign.pfx -nokeys -out codesign.cer`\n 2. Copy codesign.cer to \\\\\\domain.local\\SYSVOL\\domain.local\\Scripts\\certs\\\\\n 3. Group Policy: Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies:\n - Trusted Publishers: Import codesign.cer\n - Trusted Root Certification Authorities: Import codesign.cer (if self-signed)\n 4. Run `gpupdate /force` on all 10 lawyer machines\n 5. Verify: `certutil -store -user TrustedPublisher` should list \"Marcus-Law OutlookAddin\"\n - EspoCRM API key provisioning:\n 1. Admin → API Users → Create user for each lawyer (username = email prefix, role = Regular User + API User)\n 2. Generate API key per user: User Profile → API Key → Generate\n 3. Send per-user email with:\n - Install link: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n - Username: <email-prefix>\n - API Key: <generated-key>\n - Monitoring:\n - Check Mattermost #תשתיות-ופריסה for deployment notifications\n - Logs on client machines: %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\\\\n - Server logs: EspoCRM data/logs/ (MailRouter endpoint calls)\n\n3. **Demo video** (5 minutes, screen capture):\n - Tools: OBS Studio or Windows Game Bar\n - Script:\n 0:00-0:30 — Intro: \"שלום, זה תוסף EspoCRM ל-Outlook. נראה ארבעת הפיצ'רים העיקריים.\"\n 0:30-1:30 — Feature 1: תיוק מייל קיים (select email, click ribbon, search, file)\n 1:30-2:30 — Feature 2: סרגל צד (select email, show sidebar with case info, click \"File to this\")\n 2:30-3:30 — Feature 3: כתיבת מייל מתיק (compose, \"Compose from Case\", send, auto-file)\n 3:30-4:30 — Feature 4: סנכרון תיקיות (Settings → Folders, check folder, demo auto-file)\n 4:30-5:00 — Outro: \"שאלות? office-hours או תמיכה@marcus-law.co.il\"\n - Upload to internal file server or Mattermost\n\n4. **Coordinate with IT**:\n - Email IT admin (or Mattermost DM) 1 week before rollout:\n - Request GPO cert push to all 10 lawyer machines\n - Provide codesign.cer + docs/IT-SETUP.md\n - Verify cert installed on all machines before sending install links\n\n5. **Generate API keys**:\n - EspoCRM Admin panel:\n - For each lawyer: Administration → Users → create API User (if not exists)\n - Or: per-user API key via User Profile → API → Generate Key\n - Store keys in Infisical `/outlook-addin/users/<email>` (or send via secure 1-time link)\n\n6. **Distribute install link + credentials**:\n - After GPO cert confirmed on all machines:\n - Send individual emails to 10 lawyers:\n Subject: התקנת תוסף EspoCRM ל-Outlook\n Body:\n שלום [שם],\n \n נא להתקין את תוסף EspoCRM ל-Outlook:\n 1. לחץ כאן: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n 2. בהפעלה הראשונה הזן:\n Username: [email-prefix]\n API Key: [key]\n \n מדריך מלא: [link to ONBOARDING.md]\n וידאו הדגמה: [link to demo video]\n \n שעות פתוחות ליום ראשון + שני: 9:00-17:00 ב-#תמיכה-טכנית\n\n7. **Office hours**:\n - Block 48 hours (e.g., Sunday + Monday) for support\n - Announce in Mattermost #תמיכה-טכנית: \"התוסף זמין להתקנה! זמינים לשאלות היום ומחר.\"\n - Prepare FAQ doc with common issues (401 error, add-in not loading, etc.)\n - Monitor logs via FileBrowser or SSH to dev server\n\n8. **Post-rollout**:\n - Collect feedback via Mattermost poll or Google Form\n - Log any bugs as Task Master tasks\n - Plan follow-up release (v1.1.0) with fixes + UX improvements",
"testStrategy": "1. Peer review ONBOARDING.md + IT-SETUP.md with 1-2 lawyers and IT admin, incorporate feedback. 2. Record demo video, send to 2 lawyers for feedback on clarity. 3. Dry-run: install on 1 test machine (not dev machine) using the exact email + install link flow, verify no issues. 4. After GPO cert push, verify on 2 lawyer machines that cert is in TrustedPublisher store. 5. During office hours: track # of installs, # of support requests, # of successful filings (via EspoCRM logs or Mattermost bot). 6. Post-48h: survey lawyers for feedback, measure adoption rate (# active users / 10).",
"priority": "high",
"dependencies": [
8
],
"status": "pending",
"subtasks": []
}
],
"metadata": {
"created": "2026-05-11T09:13:47.068Z",
"updated": "2026-05-11T09:13:47.068Z",
"description": "Tasks for master context"
}
}
}