Commit Graph

22 Commits

Author SHA1 Message Date
PointStar 41a39ce2ac fix(matching): /EmailAddress/search returns a bare array, not {total,list}
The sidebar lookup was raising
  System.Text.Json.JsonException: The JSON value could not be converted
  to MarcusLaw.OutlookAddin.Core.Models.SearchResult
because EspoCRM's /EmailAddress/search endpoint returns
[{id, entityType, emailAddress, name}, ...] (and on some hosted builds
{entityId, entityName, ...}), not the GlobalSearch-style envelope.

- IEspoCrmClient.EmailAddressSearchAsync now returns
  Task<List<EmailAddressMatch>>.
- EspoCrmClient parses the bare array, with a fallback that tolerates
  {"list":[...]} wrapping if some plugin reshapes the response.
- EmailAddressMatch accepts both the {id, entityType} and the newer
  {entityId, entityName} field naming.
- MatchingService updated for the new contract.

The GlobalSearch endpoint (FileToCaseDialog search) still uses the
{total, list} envelope and is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:37:09 +03:00
PointStar 484e908026 fix(addin-host): treat API key alone as configured (don't require username)
After the previous fix made Username optional in the Settings dialog and
the credential store, TryInitializeCrm still bailed out when Username
was blank — leaving the add-in in "not configured" state even though
the user had saved a valid API key.

Only ApiKey is required now. The check matches the new contract used by
EspoCrmClient (X-Api-Key alone is sufficient for API-User credentials).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:31:47 +03:00
PointStar b6e4e38833 fix(creds): store empty username when not supplied (was saving "(api-key)" placeholder)
When the user left Username blank and clicked Save, the dialog persisted
the literal string "(api-key)" as the username. On reopen, that value
loaded back and the next Test Connection / save would send a poisoned
Espo-Authorization Basic header — base64("(api-key)":<apiKey>) — which
the server correctly rejects with 401, overshadowing the valid
X-Api-Key header.

Fix:
- DpapiCredentialStore.Save accepts a null/empty username and stores it
  as an empty string. ApiKey is still mandatory.
- SettingsViewModel passes null when the field is blank instead of the
  placeholder.
- EspoCrmClient already skips the Espo-Authorization header when
  username is blank — combined with this fix, no fallback header is
  sent for API-only credentials.

Tests: added Save_AcceptsEmptyUsername; updated Save_RejectsBlankInput
→ Save_RejectsBlankApiKey to match the new contract. 39/39 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:28:37 +03:00
PointStar 891b9d01ae fix(auth): send X-Api-Key header — Espo-Authorization alone was wrong for API users
EspoCRM uses TWO different auth schemes:
  * X-Api-Key: <apiKey>            — for API-type users (no password)
  * Espo-Authorization: base64(u:p) — for regular users with passwords

The PRD-derived constructor only set Espo-Authorization with a fake
base64(username:apiKey) which the server rejects with 401 for API users.

Fix:
- Always send X-Api-Key when an apiKey is provided.
- Also send Espo-Authorization Basic-style header when a username is
  supplied, so regular-user credentials still work without code changes.
- Username is now optional (constructor + Settings dialog). API key is
  sufficient on its own; Settings credential store falls back to
  "(api-key)" as the recorded username for logging when blank.

Tests updated:
- FileMailAsync_AddsBothApiKeyAndBasicAuthHeaders — asserts X-Api-Key
  is the primary header and Espo-Authorization remains the fallback.
- Constructor_RejectsBlankCredentials — empty username now allowed,
  empty apiKey / baseUrl still throw.

All 38 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:22:23 +03:00
PointStar 661e0cb598 fix(tls): enable TLS 1.2/1.3 on AddIn startup — fix SecureChannelFailure
Symptom: "Could not create SSL/TLS secure channel" on every CRM request
(GlobalSearch, MatchingService, TestConnection). TCP and DNS succeed
fine — the failure is at the schannel handshake stage.

Root cause: .NET Framework 4.8 inherits ServicePointManager.SecurityProtocol
from the host process. In Outlook 2019/M365 that ends up as SSL3/Tls1
which modern HTTPS servers reject.

Fix: in AddInHost ctor, OR Tls12 + Tls13 into SecurityProtocol before
any HttpClient is created. Tls13 (value 12288) is wrapped in try/catch
for pre-Win10-1809 systems.

Also surface WebExceptionStatus values in the Settings "Test connection"
dialog (SecureChannelFailure / TrustFailure / NameResolutionFailure /
ConnectFailure / Timeout) so future schannel issues report a clear
Hebrew explanation instead of "One or more errors occurred".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:15:42 +03:00
PointStar 40c3839278 fix(settings): surface real reason behind "Test connection" failures
The catch-all in TestConnectionAsync was just showing
"שגיאת חיבור: One or more errors occurred." which is useless. Walk the
InnerException chain and translate the underlying SocketError /
TaskCanceled / AuthenticationException into a specific Hebrew message:

- HostNotFound  → "השרת {host} לא נמצא ב-DNS"
- ConnectionRefused → "החיבור ל-{host}:{port} נדחה"
- TimedOut      → "חיבור פג זמן"
- HostUnreachable / NetworkUnreachable → check VPN/network
- AuthenticationException → bad TLS cert
- TaskCanceled  → 15s timeout
- otherwise     → "{OuterType}: {OuterMsg} — {InnerType}: {InnerMsg}"

Full exception (with stack) keeps going to the addin log via
_logger.Warning, so the user can still file a diagnostic bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:06:43 +03:00
PointStar 3bedb2cb06 fix(sidebar): wrap ElementHost in WinForms UserControl + cast Explorer.Close event
Two VSTO-only build errors:

1. CustomTaskPanes.Add() requires a System.Windows.Forms.UserControl.
   ElementHost derives from Control, not UserControl. Wrap it inside a
   thin UserControl so the COM adapter is happy.

2. Outlook.Explorer exposes BOTH a Close() method and a Close event with
   the same name; the C# compiler resolves "explorer.Close += handler"
   to the method group. Cast through ExplorerEvents_10_Event to bind
   the event explicitly.

Core/UI build clean. Host project still requires the VSTO build targets
to compile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:00:18 +03:00
PointStar 69af5b6ac3 feat(i18n): Task #7 — Hebrew/English localization infrastructure
- Strings.resx (Hebrew, default) + Strings.en-US.resx (English override)
  cover every dialog/sidebar/ribbon string, plus filing summary format
  templates (`Msg_FilingSummary_*`)
- LocalizationManager singleton wraps ResourceManager with Get() and
  Format() helpers; SetCulture honors AppSettings.Locale
- LocExtension XAML markup ({l:Loc Key}) for bindings
- Themes/RtlFonts.xaml — global RightToLeft FlowDirection and Hebrew
  font stack (Segoe UI / David / Frank Ruehl / Arial)
- AddInHost calls LocalizationManager.SetCulture(Settings.Locale) before
  any UI thread reads a resource

Existing dialogs keep their hardcoded Hebrew for v1 — those are already
in the correct (he-IL) language. Future iterations can switch to
{l:Loc …} bindings; the resource files already enumerate every string so
the migration is mechanical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:52:59 +03:00
PointStar a05b8114d4 feat(folders): Task #6 — per-folder auto-sync with confidence gate
Host:
- FolderResolver: walks Outlook Stores/Folders by stored StoreId +
  backslash-separated FolderPath; RCWs released at each hop
- FolderWatcher: rooted Items references in a Subscription list
  (preventing the GC from silently dropping the COM event sink),
  HashSet<EntryID> dedup, marshals category stamping back to the WPF
  dispatcher. Per-folder mode: AutoFile when MatchingService returns a
  confident unique match AND confidence ≥ ConfidenceThreshold; otherwise
  stamps "Needs filing"
- FolderTreeBuilder: one-shot STA traversal (max depth 4, max 500 entries)
  into Core-side FolderTreeNode list

UI:
- FolderTreeNode in Core for cross-project transport
- FolderRowViewModel: per-row IsWatched / Mode / ConfidenceThreshold
- SettingsViewModel.LoadFolderTree(tree) merges existing
  AppSettings.WatchedFolders into the loaded list
- Save now writes WatchedFolders alongside URL/creds
- SettingsDialog: new "תיקיות" tab with a DataGrid (checkbox + mode combo
  per folder)

AddInHost:
- Builds FolderResolver + FolderWatcher inside TryInitializeCrm, applies
  Settings.WatchedFolders on startup and after Save
- LaunchSettings: invokes FolderTreeBuilder, passes the flat list to the
  ViewModel, and re-applies the watch list after the dialog closes

Core/UI/Tests green (38 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:51:03 +03:00
PointStar 0964439441 feat(compose): Task #5 — Compose from Case + URL protocol + auto-file
UI:
- ComposeFromCaseDialog (RTL, search-then-pick a single Case via
  GlobalSearch + entityType=Case filter)
- ComposeFromCaseViewModel — debounced GlobalSearch, returns chosen
  EspoEntityRef

Host:
- ComposeService.ApplyCaseToMailAsync:
  * fetches Case + primary Contact
  * stamps UserProperties EspoCaseId, EspoCaseName, OutlookAddinGuid
  * pre-fills To/Subject and appends an RTL footer with a CRM deep link
- ItemSend hook (in AddInHost.HookItemSend): when the user sends a
  tagged compose, register guid → target with SentItemsWatcher
- SentItemsWatcher: rooted Items reference (prevents GC), 30s expiry
  sweep, 2-min default timeout per pending tag, MailMatched event
- AddInHost.OnSentMailMatched: extracts the sent MailItem, files via
  FilingService, stamps "Filed: {name}" category
- LaunchComposeFromCaseAsync (Inspector ribbon) and
  LaunchComposeFromCaseByIdAsync (URL protocol) entry points
- NamedPipeListener (\.\pipe\MarcusLaw.OutlookAddin) — listens for
  "COMPOSE <caseId>" lines, dispatches on WPF Dispatcher

Ribbon:
- InspectorRibbon.xml — "כתוב מתיק" button on Mail Compose ribbon, hidden
  for received/read mail via getVisible
- ExplorerRibbon class now serves both Explorer and Inspector ribbons
  (single IRibbonExtensibility, dispatches by ribbonID)

URL protocol:
- New OutlookAddinProtocolHandler console project (net48 WinExe):
  parses outlookaddin://compose?caseId=<id>, connects to named pipe,
  writes "COMPOSE <id>\n"
- tools/install-protocol.ps1 + uninstall-protocol.ps1 register the
  scheme under HKCU\Software\Classes\outlookaddin

Core/UI/Tests + protocol handler all build; 38 unit tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:47:36 +03:00
PointStar 2368764251 feat(sidebar): Task #4 — reading-pane sidebar with sender matching
Core:
- IEspoCrmClient.ListCasesForContactAsync (paged where[]=linkedWith query)
- EspoListResponse<T> generic envelope for /list endpoints
- IMatchingService + MatchingService: EmailAddress/search → Contact →
  Account → recent 5 Cases. MemoryCache 10-min TTL; caches null and
  ambiguous results too. Returns null for 0 matches, IsAmbiguous=true
  for 2+, populated MatchResult for exactly 1.
- MatchResult DTO with Contact / Account / RecentCases / IsAmbiguous

UI:
- ReadingPaneView (WPF UserControl, RTL): 6 states — Empty / Loading /
  Match / NoMatch / Ambiguous / Error — switched via Visibility bindings
- ReadingPaneViewModel: state machine, FileRequested + OpenInBrowserRequested
  events, Reset()/LoadAsync() with cancellation chain (every selection
  cancels the previous lookup)

Host:
- ExplorerSelectionHandler: subscribes to Outlook.Explorer.SelectionChange,
  200ms debounce via Timer, dedupes by last sender, marshals to WPF
  dispatcher before reading COM state
- SidebarController: attaches a CustomTaskPane per Explorer
  (ElementHost wrapping ReadingPaneView), tracks bindings by COM identity,
  unhooks on Explorer.Close. Sidebar defers attach until CRM client is
  configured (subscribes to CrmClientChanged)
- AddInHost: MatchingService field, MemoryCache shared across lookups,
  InitializeSidebar(taskPanes) invoked from ThisAddIn_Startup

Core/UI/Tests green (38 tests passing); VSTO host still needs VS
Installer Repair locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:42:40 +03:00
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
chaim 923a0b657f fix(host): enable AutoGenerateBindingRedirects for System.Text.Json deps
VSTO csproj template disables binding redirects, but System.Text.Json's
transitive deps (System.Text.Encodings.Web, System.Runtime.CompilerServices.Unsafe,
System.Memory) ship as out-of-band NuGet packs whose versions diverge
from .NET Framework 4.8's facade assemblies. Without redirects, the
JIT loads the wrong Unsafe.dll for DefaultJavaScriptEncoder's static
init, throwing FileLoadException wrapped in TypeInitializationException
when SettingsManager.JsonOptions is touched.

GenerateBindingRedirectsOutputType=true ensures the generated
{MarcusLaw.OutlookAddin.dll}.config goes alongside the assembly so
the VSTO loader actually sees it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:36:54 +00:00
chaim e8265400f8 fix(host): copy Core's NuGet refs (System.Text.Json, ProtectedData, Polly, Caching)
Legacy VSTO csproj doesn't follow PackageReferences transitively from
ProjectReferences — at runtime the host's bin\ ends up without the
satellite DLLs that Core depends on. JIT'ing SettingsManager throws
a type-initializer exception because JavaScriptEncoder.UnsafeRelaxedJsonEscaping
(in System.Text.Encodings.Web, transitive of System.Text.Json) is missing.

Pull all four runtime NuGet packs into the host directly. Versions
mirror Core.csproj exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:31:40 +00:00
chaim daf3c422ca fix(host): reference System.Net.Http for HttpClient in legacy VSTO csproj
The legacy VSTO csproj doesn't include System.Net.Http in its default
references. Adding it so HttpClient resolves in AddInHost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:29:42 +00:00
chaim 12e0013f28 fix(host): pass HttpClient to EspoCrmClient ctor + dispose on shutdown
The composition root was calling EspoCrmClient(baseUrl, user, key, logger)
which doesn't match the actual ctor signature
EspoCrmClient(HttpClient, string, string, string, ILogger). One missing
HttpClient argument = one compile error blocking the host build.

- AddInHost now owns a single HttpClient (Timeout=30s), passed to
  EspoCrmClient on init, disposed alongside the retry processor and
  Serilog on shutdown.
- baseUrl.ToString() satisfies the string parameter; the Uri.TryCreate
  upstream still validates the URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:26:25 +00:00
chaim 9bac4025e8 fix(host): set LangVersion=latest + Nullable=enable on VSTO csproj
The legacy VSTO csproj defaults to C# 7.3, but the host code (e.g.
AddInHost, MailItemExtractor) uses nullable reference types — which
require C# 8.0+. Building fails with ~12x
"Feature 'nullable reference types' is not available in C# 7.3".

Sync with the SDK-style Core/UI/Tests projects that already declare
LangVersion=latest + Nullable=enable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:23:58 +00:00
PointStar 14ed1f6142 feat(filing): wire Feature 1 host (ribbon, extractor, retry processor)
OutlookAddin host project:
- ExplorerRibbon (IRibbonExtensibility + ExplorerRibbon.xml embedded): adds
  "תייק ל-EspoCRM" button to Mail Home tab, enabled only when 1+ MailItems
  selected; invalidates on Explorer.SelectionChange
- MailItemExtractor: STA-side conversion of Outlook.MailItem to MailEnvelope
  primitives (subject, from, to/cc/bcc, body, PR_INTERNET_MESSAGE_ID,
  attachments base64), with COM RCW release on each iteration
- RetryQueueProcessor: 30s background drain, max 3 attempts per item,
  401 short-circuits the loop, items abandoned (not deleted) after max
- AddInHost: composition root — Logger, SettingsManager, DpapiCredentialStore,
  EspoCrmClient, FilingService, RetryQueueProcessor. LaunchFileToCaseAsync
  reads selection, extracts, opens dialog, files, stamps "Filed: {name}"
  category on success, promotes target into Settings.RecentEntities (max 10),
  shows summary MessageBox.
- ThisAddIn: instantiate AddInHost in Startup; CreateRibbonExtensibilityObject
  returns ExplorerRibbon

VSTO targets are missing on this machine — host project not built locally.
Core/UI/Tests still green (38 tests passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:11:36 +03:00
PointStar e527aa80af feat(filing): scaffold Task #3 Feature 1 — file mail to Case/Contact
Core layer:
- FilingService: orchestrates batch filing with 401-aborts-batch, 409-treats-as-success, transient errors fall to DiskRetryQueue
- DiskRetryQueue: atomic write (tmp + rename), Hebrew UTF-8 round-trip, idempotent remove
- Models: MailEnvelope (STA-extracted primitives), FilingTarget, FilingOutcome enum, FilingItemResult, FilingBatchSummary, RetryQueueItem
- EspoCrmClient.CreateContactAsync for "create contact from sender" flow

UI layer:
- FileToCaseDialog (WPF, RTL, Hebrew labels) with grouped search results + recent sidebar
- FileToCaseViewModel: 250ms debounced GlobalSearch, create-contact-from-sender command, DialogResult signaling

Tests (38 passing):
- FilingServiceTests: happy path, 409, 401 aborts batch, transient enqueues, queue failure → Failed, target propagation, null guards
- DiskRetryQueueTests: id assignment, Hebrew round-trip, corrupt-file skip, idempotent remove, atomic overwrite

VSTO host project (OutlookAddin.csproj) regenerated; ThisAddIn wiring in follow-up commits.
gitignore: exclude .claude/settings.local.json (per-machine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:04:10 +03:00
PointStar f563dd17dd feat(core): implement EspoCrmClient, DPAPI store, settings, logger (Task #2)
- EspoCrmClient: typed REST wrapper with Polly v8 pipeline
  (Timeout 15s -> Retry 3 exp+jitter -> CircuitBreaker 0.5/5/30s).
  Auth via Espo-Authorization header (base64 username:apiKey).
  Treats 409 on /MailRouter/file as success (already-filed dedup),
  401/403 raise EspoCrmAuthorizationException for re-auth UI.
- DTOs in Models/ matching MailRouter v1.2.0 contract from
  PROJECT-BRIEFING.md (FileEmailRequest now includes replyTo + isHtml,
  attachments use contentType+contentBase64).
- DpapiCredentialStore (Security/): ProtectedData.Protect with
  CurrentUser scope, SHA256("MarcusLaw.OutlookAddin.v1") entropy.
- SettingsManager + AppSettings + WatchedFolder (Configuration/):
  System.Text.Json with UnsafeRelaxedJsonEscaping for Hebrew strings,
  UTF-8 without BOM, recovers to defaults on missing/corrupt file.
- LoggerFactory (Logging/): Serilog daily-rolling File sink with
  14-day retention.

24 xUnit tests covering happy/4xx/5xx/auth/Hebrew round-trips all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:27:05 +03:00
PointStar 75505fbbff chore: remove broken VSTO host stub pending VS template regeneration
The hand-written legacy csproj at src/OutlookAddin/ is marked Unsupported
by VS 2022. Per PROJECT-BRIEFING.md (steps 5-11), the host project must
be regenerated from the VSTO template inside Visual Studio with the
Office workload — there is no CLI/dotnet template equivalent.

Removing now so the next VS session starts from a clean slate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:26:45 +03:00
chaim cf10db00a3 chore: scaffold Outlook VSTO Add-in for EspoCRM
Initial repo scaffold for a Windows VSTO/COM Outlook Add-in (C#/.NET FW 4.8)
that integrates Outlook desktop with EspoCRM via the existing MailRouter
v1.2.0 endpoint (POST /api/v1/MailRouter/file).

Includes:
- Solution + 4 csproj projects (host, Core, UI, Tests) under src/
- VSTO host project stub (ThisAddIn.cs, requires VS 2022 + Office workload to fully resolve)
- Core/UI/Tests as SDK-style net48 projects (NuGet: Polly, System.Text.Json,
  Serilog, CommunityToolkit.Mvvm, xUnit, Moq, FluentAssertions)
- .gitea/workflows/build.yml for Windows runner CI (publish/sign placeholders)
- docs/ARCHITECTURE.md (component diagram, threading model, per-feature flow)
- docs/ONBOARDING.md (Hebrew lawyer-facing install guide)

Server-side already in place: MailRouter v1.2.0 — see plan
~/.claude/plans/resilient-sauteeing-feather.md for the 6-week implementation
plan.

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