No code change. Confirms that the v1.2.3 updater (HTTP GET the manifest
+ Process.Start the .vsto URL) reaches VSTOInstaller cleanly without
the TrustNotGrantedException that the old ApplicationDeployment.Update
path always threw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log shows TrustNotGrantedException thrown inside CheckForDetailedUpdate
itself (ApplicationTrust.RequestTrust → DetermineTrustCore →
DetermineTrust). The misleading "המשתמש סירב להעניק את ההרשאות"
fires even when the user accepted the trust dialog on install and the
publisher cert is unchanged: ApplicationDeployment.CheckForDetailedUpdate
re-runs the TrustManager evaluation, which can't show a UAC-style
prompt from inside a hosted Outlook process and falls through to a
denial exception.
Bypass the ClickOnce update API entirely:
1. Read CurrentVersion from ApplicationDeployment (this part is fine)
2. HTTP GET the .vsto manifest at UpdateLocation, parse the top-level
<assemblyIdentity> version
3. Compare versions
4. If newer, Process.Start the .vsto URL with UseShellExecute=true so
Windows hands it to VSTOInstaller.exe — which is the only thing that
actually knows how to update a running VSTO add-in correctly
ApplicationDeployment.Update() is also dropped because it has the same
in-process limitation as the trust check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No code change. Bumping to confirm a fresh install from platform.dev
picks up the next tag-driven release cleanly via "בדוק עדכונים".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two %APPDATA% / %LOCALAPPDATA% paths in Settings → אודות are now
hyperlinks. Click to open the folder (creates it lazily if it's not
there yet, so the link works before the first log is written).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing strings only — ClickOnce identity, AssemblyName, namespaces,
DPAPI entropy and on-disk paths all stay MarcusLaw so the 10 lawyers
auto-update without any manual uninstall/reinstall.
What lawyers see change:
- About panel: dark-navy header card (#121d2e from KlearBranding) with
the Klear logo (32KB PNG mirrored from
espocrm-extensions/KlearBranding/files/client/custom/img/logo-klear.png)
and the title "Klear" instead of "Marcus-Law OutlookAddin"
- All MessageBox titles ("Klear" instead of "Marcus-Law OutlookAddin")
- Ribbon group label ("Klear" instead of "Marcus-Law") in both the
Explorer and the new-message Inspector ribbons
- Startup-failure dialog ("תוסף Klear נכשל" instead of "תוסף Marcus-Law")
What stays MarcusLaw (deliberate — Layer 1 only):
- ClickOnce app identity → preserves auto-update across this release
- %APPDATA%\MarcusLaw\…\settings.json + creds.dat → no migration needed
- DpapiCredentialStore EntropySeed → API keys remain decryptable
- Internal namespaces / pipe name / AssemblyCompany attribute
Cleanup:
- Removed the entire Publish/ directory from git. It was the orphaned
manual-release channel under gitea raw/branch/main/Publish/ that no
one actually uses — production installs are served from
platform.dev.marcus-law.co.il via the Gitea Actions pipeline. The
pipeline writes to a local publish/ workspace and uploads a tar.gz
to the generic registry; it never reads or writes the repo's Publish/.
- Wiped the local LegalCRM_1_0_*_0 leftovers from the failed rename
experiment.
Bumped to 1.2.0 — MINOR because the rebrand is a user-visible change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs reported after v1.1.2:
1. False-negative "נכשלו" toast even though the file was on disk. Root
cause from the log: Polly's 15s timeout on /NetworkStorage/action/upload
fires while the server is still writing larger PDFs, so the client sees
TimeoutRejectedException → Outcome.Failed, while the server quietly
finishes the write. The outcome is now decided by listing the target
folder, not by the upload response. If the upload exception fires, the
listing is retried up to 4 times with 2s delays to let the server
finish. Saved if the file appears, Failed only if it never does.
2. The filename TextBox in the attachments list exposed the extension
(.pdf / .docx). Users have to delete it manually when renaming and
risk dropping it. AttachmentRowViewModel now splits FileName into
BaseName (editable) and Extension (frozen at construction). The XAML
binds the TextBox to BaseName and shows the Extension as a muted
adjacent chip. The on-disk filename = BaseName + Extension always.
Bump to 1.1.3.0 for the patch release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DataGrid case picker now shows status via CaseStatusToHebrewConverter
(added PendingHearing / PendingDecision mappings)
- Attachment row replaces the read-only filename TextBlock with a TwoWay
TextBox so users can rename a file before saving (flows through
EspoAttachment.Name into the upload + the on-disk path)
- SaveAttachmentsViewModel.IsLoadingSubfolders gates the folder ComboBox
while EnsureCaseSubfolders / GetCase / ListNetworkStorageFolder run,
so users can't pick a stale "(שורש התיק)" before the real list arrives
- About tab: ResolveDisplayVersion now reads from the host VSTO assembly
(*.OutlookAddin) instead of the UI assembly. The UI csproj is SDK-style
with no <Version> so it always reported 1.0.0.0, which leaked into the
About line on dev installs
- Bump to 1.1.2.0 (csproj + AssemblyInfo) to match the v1.1.2 tag that
will trigger the Gitea Actions auto-publish to platform.dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Versioning policy: MAJOR.MINOR.PATCH stored as MAJOR.MINOR.PATCH.0
in the ClickOnce manifest (it requires 4 components). The trailing
.0 is hidden by SettingsViewModel.ResolveDisplayVersion so the About
tab shows "1.0.7" not "1.0.7.0".
AutoIncrementApplicationRevision is now false; every published change
must bump <ApplicationVersion> and AssemblyVersion/AssemblyFileVersion
together by hand. MAJOR bumps need explicit owner approval.
Previously AssemblyVersion was pinned at 1.0.0.0 while ClickOnce
auto-bumped only ApplicationVersion, so the deployed DLL's
FileVersion never matched the manifest. Now they're locked at 1.0.7.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single-line ListBox with a DataGrid (case number, name,
court case number, status) so users disambiguate same-named cases
(e.g. two "שלומוב זויה" cases). Hydrates each search hit via
GET /Case/{id}?select=id,name,number,status,cCourtCaseNumber in
parallel so columns populate without an extra click. CourtCaseNumber
is mapped to the EspoCRM custom field cCourtCaseNumber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- InstallUrl switched from \192.168.10.97 LAN share to
https://gitea.dev.marcus-law.co.il/.../raw/branch/main/Publish/
so end-users (including remote ones over VPN) can install and
auto-update from the Gitea repo over HTTPS.
- IsWebBootstrapper set to True now that setup.exe fetches over HTTP.
- .gitignore: re-include Publish/** despite the *.vsto/*.application/
*.deploy/*.manifest wildcards so the ClickOnce distribution root
can be tracked.
- First publish of the bundle (version 1.0.0.5); older 1_0_0_2..4
folders deleted before commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Ambiguous lookups (2+ contacts on one email) now dedupe by Contact Id
and fetch each candidate's full record so the picker shows
name + account + email instead of just the bare search payload.
- Per-row "תייק" button on each linked case lets the user pick the
correct case to file to when a contact has multiple cases; bottom
"תייק לתיק הזה" button is hidden in that scenario.
- "→ חזור לרשימת אנשי קשר" link restores the candidate list after
drilling into one of them.
- Contact name + case rows in the sidebar are now hyperlinks that
open the entity in Klear.
- AddInHost.LaunchFileToCaseAsync accepts an optional MatchResult and
auto-files when the sidebar resolved a single linked case, skipping
the manual search dialog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related improvements:
1. About tab → "בדוק עדכונים" button. Calls ClickOnce's
ApplicationDeployment.CurrentDeployment.CheckForDetailedUpdate +
Update on background threads, so the user can pull a new release
immediately without leaving Outlook — no PowerShell, no
CleanOnlineAppCache dance.
Status messages cover the four real outcomes:
• not deployed via ClickOnce (F5 / dev install) → explicit error
• on the latest version → green "already up to date"
• update available → progress text, then "installed — restart
Outlook"
• download / manifest / generic exception → red, with reason
2. Case folder resolution now matches the server. The previous client
logic used Case.name (the lawsuit title, e.g. "מונאס אריאל נ' ביטוח
לאומי") which produced "47-מונאס אריאל נ ביטוח לאומי". But
NetworkStorageIntegration's buildEntityFolderName uses the primary
contact's name from Case.contactsIds[0] → server actually creates
"47-אריאל מונאס" for the same case, so the two diverged and our
uploads went to a phantom folder.
New flow on case selection:
a) EnsureCaseSubfoldersAsync (creates server's canonical folders)
b) GetCase with select=contactsIds + networkStorageFolderPath
c) GetContactAsync on contactsIds[0] → grab Name
d) ResolveCaseFolderPath(case, contactName) → exact match
e) Cache result on vm.ResolvedCaseFolderPath; AddInHost reads it
instead of recomputing.
ResolveCaseFolderPath fallback order is now (1) the integration's
stored path, (2) <number>-<primary contact name>, (3) <number>-<case
name>, (4) the contact / case name alone — matching the server's
buildEntityFolderName logic step by step.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The About tab was always showing "1.0.0" because it read the DLL's
AssemblyVersion, which we never bumped in source — even when the
ClickOnce manifest had been rolled to 1.1.0 by the CI tag.
Read System.Deployment.Application.ApplicationDeployment.CurrentDeployment
.CurrentVersion at runtime when the add-in is running under ClickOnce
(IsNetworkDeployed=true). That's the version the user actually
installed. Fall back to AssemblyVersion when dev-running under F5 (no
ClickOnce context). Adds System.Deployment GAC reference to the UI
project.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server reported successful uploads for the two PDFs into case 6a03166d…
("מונאס אריאל") but the user couldn't find the files on the share. Both
LocalFilesystemClient::uploadFile and WebDavClient::uploadFile return
true only on a real write, so something downstream is silently dropping
the payload — most likely a WebDAV proxy or a basePath mismatch
between what NetworkStorageIntegration thinks the storage is vs. what
the user is browsing.
Add a verification step right after the upload:
1. POST /Attachment → attachmentId
2. POST /NetworkStorage/action/upload → 201 from server
3. GET /NetworkStorage/folderContents?path=<folder>
• If our filename is in the listing → INF "verify present"
• Else → WRN "expected X but NOT in listing. Currently in folder: […]"
The diagnostic line surfaces both halves of the puzzle in a single
log entry, so the next save attempt will tell us in plain text whether
the storage layer is the liar or the path is being normalised
somewhere unexpected.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After reading the NetworkStorageIntegration repo I realized the previous
"save attachments" flow was wrong on every level — Marcus-Law doesn't
use EspoCRM's stock Document/DocumentFolder model. Folders are real
filesystem paths under the network share, named after the Case (format
"<number>-<primary contact name>") with four standard subfolders:
מסמכים, אסמכתאות, התכתבויות, כללי.
Rewire the save pipeline against the integration's real endpoints
(documented in routes.json on the server repo):
Core — three new IEspoCrmClient methods:
- EnsureCaseSubfoldersAsync(caseId)
POST /NetworkStorage/action/createSubfolders
Idempotent — creates case folder + defaults if missing.
- ListNetworkStorageFolderAsync(path)
GET /NetworkStorage/folderContents?path=…
Returns folders + files; we filter to type=folder for the dropdown.
- UploadAttachmentToNetworkStorageAsync(attachmentId, targetPath)
POST /NetworkStorage/action/upload
Moves the staged Attachment binary onto the share at <path>/<name>.
CaseEntity gets a new `networkStorageFolderPath` field (set by the
integration's hook on first Document save; empty until then).
NetworkStorageItem DTO captures the folder-listing rows.
AttachmentSaveService rewritten — no more Document creation, no more
DocumentFolder picker. The new contract is:
SaveAsync(attachments, targetFolderPath)
Where targetFolderPath = "<case folder>[/subfolder]". For each
attachment: upload binary via standard /Attachment, then move via
NetworkStorage upload.
UI — SaveAttachmentsViewModel:
- Subfolder dropdown switched from DocumentFolder objects to strings.
- Static defaults loaded up-front:
"(שורש התיק)" + 4 server-side defaults.
- When the user picks a case:
1) hydrate Case (number + networkStorageFolderPath) via GetCase
2) call EnsureCaseSubfoldersAsync to materialise standard folders
3) listFolder at the case path to merge any custom subfolders into
the dropdown.
- ResolveCaseFolderPath: use Case.networkStorageFolderPath if set,
otherwise compute "<number>-<contact-name>" with a client-side
SanitizeFolderSegment that mirrors LocalFilesystemClient::sanitizeFileName
on the server (geresh/gershayim/quotes removed, illegal chars → hyphen).
AddInHost.LaunchSaveAttachmentsAsync:
- Composes <caseFolder>[/subfolder] from the VM choice.
- Pre-flight ensureCaseSubfolders as a safety net (in case the user
submitted before the dialog's lazy refresh completed).
- Summary MessageBox now shows the resolved storage path instead of a
bare entity name.
Tests: 39 passing. The build is clean across Core / UI / Tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the case-scoped query fails (e.g. tenant rejects parentId in
where), the ViewModel falls back to ListDocumentFoldersAsync — but a
success path with N=0 was silent, leaving the user wondering why the
dropdown stayed empty. Add an INF line so the log shows whether
N folders came back, so 0-folder tenants are obvious from a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs found by reading the log carefully — both shipped in the
initial attachments feature:
1. MailItemExtractor sent every attachment with ContentType = "" and
UploadAttachmentAsync defaulted that to "application/octet-stream".
EspoCRM's attachment upload whitelist (config
attachmentUpload.fileAcceptOptions) does not include octet-stream,
so the server replied:
403 (X-Status-Reason=Not allowed file type.)
Add a GuessMimeType(fileName) helper to MailItemExtractor that maps
common extensions (pdf, doc/docx, xls/xlsx, ppt/pptx, txt, csv,
images, archives, email, audio/video) to canonical MIMEs. The
octet-stream is now only used for truly unknown extensions.
2. 403 was being conflated with 401 in THREE places — EnsureSuccessAsync,
the inline UploadAttachmentAsync handler, and the inline
CreateDocumentAsync handler. Result: a single "Not allowed file
type" rejection threw EspoCrmAuthorizationException, which
AttachmentSaveService treats as a batch-level auth failure and
stops processing remaining files. (Same bug would have killed
FilingService and MatchingService for any non-auth 403 — e.g. an
ACL miss on one record stopping a multi-mail file.)
Only 401 is unambiguously an auth issue. 403 means "you ARE who
you say you are, but this specific operation isn't allowed" —
content rejected / ACL miss / role missing. Treat 403 as a normal
HTTP error: log the body + X-Status-Reason and surface the message
to the user without aborting the batch.
Also include the X-Status-Reason header in the generic
EspoCrmHttpException message everywhere, so any future "fileSizeTooBig"
or similar shows up in the user-visible error and not just the log.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server returned:
400 {"messageTranslation":{"label":"validationFailure",
"data":{"field":"publishDate","type":"required"}}}
Marcus-Law's EspoCRM install marks Document.publishDate as required.
By default the field is optional, but this tenant tightened it. Send
today's date (UTC, YYYY-MM-DD form) so the validation passes — the
user can still edit the published date in EspoCRM later if needed.
Also add the same verbose-error pattern to CreateDocumentAsync that we
already use on UploadAttachmentAsync: when the create returns non-2xx,
log the response body + X-Status-Reason header so any future required-
field mismatch shows up as a single readable line instead of
"שמירת הקבצים נכשלה".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the EspoCRM Attachment API doc (documentation.espocrm.com/development/api/attachment/),
the binary is sent in the "file" field as a data URI. The previous
extra "contents" alias was a legacy convention that strict tenants
reject with HTTP 400 (with an empty body, so the user only ever saw
"שמירת הקבצים נכשלה"). Drop "contents" — match the documented payload:
{
"name": "report.pdf",
"type": "application/pdf",
"role": "Attachment",
"relatedType": "Document",
"field": "file",
"file": "data:application/pdf;base64,..."
}
Folder dropdown: previously case-scoped only. The /Document filter by
parentId returned 400 on this tenant and case-scoped queries can be
fragile across versions. Now: try case-scoped first (so the most-
relevant folders surface); if it comes back empty or errors, fall back
to ListDocumentFoldersAsync (the full tenant list). The user always has
something to choose from, and the "(בלי תיקייה)" sentinel still works
as a "no folder" pick.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for the save-attachments flow:
1. ListFoldersUsedInCaseAsync no longer hits /Case/{id}/documents — that
related-records endpoint requires a Case-to-Document link that
EspoCRM doesn't ship by default, so it returned an empty list on
every install. Switch to a direct Document list filtered by the
polymorphic parent fields:
GET /Document?where[0]=parentType=Case&where[1]=parentId=<id>
That returns all documents linked to the case regardless of how the
parent relationship is wired.
2. UploadAttachmentAsync now:
- Emits BOTH "contents" and "file" payload fields with the
data-URI base64 string. Different EspoCRM versions read from
different keys; including both makes the call portable.
- Logs the request shape (name, type, base64 length) before sending
so server-side rejects are easier to attribute.
- On a non-2xx response, logs the response body AND any
X-Status-Reason header (EspoCRM convention for short error codes
like "fileSizeIsTooLarge") so the user-visible exception finally
includes a useful reason instead of an empty body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The folder dropdown was showing every DocumentFolder in EspoCRM (global
list). The user expected "folders that exist in THIS case" — only the
folders already in use for the chosen case's documents.
Change: when the user picks a case in the SaveAttachments dialog, the
folder list refreshes by walking
GET /Case/{id}/documents?select=id,folderId,folderName&maxSize=200
and deduping the folderId values it finds. EspoCRM denormalizes
folderName onto Document, so no second lookup is needed.
If a case has no documents yet, the dropdown shows just the sentinel
"(בלי תיקייה)" — same default behavior as before, but no more
unrelated folders from other cases.
Files:
- Core: DocumentEntity gets FolderName; IEspoCrmClient and EspoCrmClient
get ListFoldersUsedInCaseAsync(caseId)
- UI: SaveAttachmentsViewModel re-fetches on SelectedCase change
(LoadFoldersForCaseAsync); AddInHost no longer calls the old
LoadFoldersAsync.
Tests: 39 still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual Studio rewrote the legacy VSTO csproj on its next save and
dropped the EmbeddedResource entry for the paperclip icon — without
it the new SaveAttachments ribbon button has no image at runtime and
the file isn't shipped in the assembly. Add it back next to the four
other Klear icons.
The new SaveAttachments button rendered but stayed greyed out. Cause:
getEnabled callbacks are evaluated once when the ribbon loads and the
result is cached — Office only re-asks if you explicitly invalidate
the control. The previous SelectionChange handler invalidated only
FileToEspoCrm.
Collected the selection-dependent button IDs into a static array and
made the SelectionChange handler invalidate all of them. Adding more
context-sensitive buttons in the future is now a one-line edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workflow alongside "תייק ל-Klear": save just the attachments of a
mail to a Case in EspoCRM (no Email entity created), with an optional
DocumentFolder.
Core:
- AttachmentEntity, DocumentEntity, DocumentFolder DTOs
- IEspoCrmClient gets three new endpoints:
UploadAttachmentAsync (POST /Attachment, data-URI base64, role=Attachment)
CreateDocumentAsync (POST /Document, fileId + parent + folderId)
ListDocumentFoldersAsync (GET /DocumentFolder, returns envelope)
- AttachmentSaveService orchestrates per-attachment upload + Document
create, short-circuits on EspoCrmAuthorizationException, aggregates
per-file Saved/Failed/AuthRequired outcomes into AttachmentSaveSummary
UI:
- SaveAttachmentsDialog (RTL, card layout matching FileToCaseDialog):
Case search + folder dropdown + per-attachment checkbox list
- SaveAttachmentsViewModel: debounced GlobalSearch filtered to Case,
optional folder selector (with a "(no folder)" sentinel row),
Attachments collection of AttachmentRowViewModel rows
- AttachmentRowViewModel: IsSelected checkbox + display name + size
computed from base64 length
Host:
- AddInHost.LaunchSaveAttachmentsAsync(MailItem) — extracts on the STA
via MailItemExtractor, fetches folders, shows the dialog (anchored to
Outlook via AttachOwnerToOutlook), runs AttachmentSaveService on OK,
reports a Hebrew MessageBox summary
- AttachmentSaveService field on AddInHost + dispose hook
Ribbon:
- New "שמור קבצים" button in the Marcus-Law group, between Klear and
פרטי תיק. Enabled only when exactly one MailItem is selected and it
has at least one attachment
- klear-attach.png paperclip icon (32x32, blue rounded square),
embedded as a resource and resolved by the existing OnGetImage
callback (new case in the switch)
Tests: 39 still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two quality-of-life tweaks on the sidebar:
1. New CaseStatusToHebrewConverter translates standard EspoCRM Case
statuses (New / Assigned / Pending / Closed / Rejected / Duplicate /
In Progress / On Hold / Resolved / Cancelled / Closed Won / Closed
Lost / Open) to Hebrew. Unknown values pass through unchanged so
custom statuses configured per-tenant still render.
Wired in ReadingPaneView.xaml via Status binding.
2. MatchingService now pulls up to 50 cases per contact instead of 5.
The sidebar's ScrollViewer already handles long lists. 50 is well
below EspoCRM's default page size and large enough for any realistic
attorney/contact relationship. Section header changed from
"תיקים אחרונים" to "תיקים מקושרים" to reflect that it's the
complete list, not just the most recent.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cases lookup for "אברהם מוסטקי" was still failing after the
Case.number fix, with a new exception:
JsonException at $.list[0].createdAt
The JSON value is not in a supported DateTimeOffset format.
EspoCRM serializes datetimes as "2024-01-15 14:30:00" without a timezone
designator, but .NET's built-in DateTimeOffset reader only accepts
strict ISO 8601 with a tz suffix. Adding the FlexibleStringConverter
to CreatedAt is not enough since the target type is DateTimeOffset, not
string.
Solution: a dedicated FlexibleDateTimeOffsetConverter that:
- Accepts ISO 8601 with offset (fast path)
- Accepts "YYYY-MM-DD HH:mm:ss" / "YYYY-MM-DD" / variants, treating
them as UTC
- Accepts Unix epoch numbers
- Accepts null / empty strings
Registered globally on the JsonOptions used by EspoCrmClient, so every
model that exposes a DateTimeOffset? deserializes correctly without
per-property attributes.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced from the addin log:
1) The four Klear ribbon PNGs were not in the assembly manifest
(Available resources listed only the XMLs and Properties.Resources).
The EmbeddedResource entries for Images got dropped from
OutlookAddin.csproj during a previous rebase. Re-add them in the same
spot they had before.
2) MatchingService failed for one of the contacts with:
System.Text.Json.JsonException at $.list[0].number
Cannot get the value of a token type Number as a string.
EspoCRM emits Case.number as a JSON number on installs that use
auto-increment, and as a string when a custom case-code format is
configured. Add a small FlexibleStringConverter that reads either
token type into the same string field, and attach it to
CaseEntity.Number.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous commit the ribbon rendered with NO icons at all.
Cause: loadImage on customUI is the Office-2007 pattern; Office 2010+
silently falls back to no image when the callback signature or timing
isn't exactly what it expects. Some hosts also cache a null result.
Switch to the more reliable per-button pattern:
- Each button declares getImage=OnGetImage (no image= attribute).
- OnGetImage(IRibbonControl control) maps control.Id -> embedded
PNG name and returns a fresh Bitmap.
- If a resource is missing we log it AND the full list of
GetManifestResourceNames() so we can diagnose any future mismatch
from the addin log alone.
Builds clean. PNGs unchanged; csproj <EmbeddedResource> entries are the
same as before. The previous OnLoadImage callback is removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the previous imageMso identifiers with PNG icons painted in the
Klear brand blue (#2563EB):
- klear-file.png — large white K on blue rounded square
- klear-sidebar.png — pane + divider + content lines
- klear-settings.png — three horizontal sliders with dots
- klear-compose.png — envelope with pencil overlay
All four are 32x32 PNGs embedded as resources under
OutlookAddin.Images.*. Ribbon XML now declares `image=<name>.png` and a
shared `loadImage=OnLoadImage` callback resolves each name to a Bitmap
from the assembly's manifest resources.
The ribbons affected:
- TabMail / Marcus-Law: Klear, פרטי תיק, הגדרות (3 buttons)
- TabNewMailMessage / Marcus-Law: כתוב מתיק (1 button)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar identified the contact but showed "(אין תיקים פתוחים)" even
when cases existed for them. The previous query —
GET /Case?where[0][type]=linkedWith&where[0][attribute]=contacts&where[0][value]=…
— came back empty against this EspoCRM build, likely because the link
name on Case differs from the default or the where[] syntax isn't
accepted on this host.
Switch to the contact-side related-records endpoint which is the
canonical EspoCRM way to fetch linked records:
GET /Contact/{contactId}/cases?select=…&orderBy=createdAt&order=desc&maxSize=5
Keep the old /Case?where[] query as a fallback for installs where the
relationship walk fails. Log the chosen path and result count so the
next "no cases shown" report can be diagnosed from the log file alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related polish fixes:
1. Modal dialogs (File-to-Case / Compose-from-Case / Settings) used to
open without an Owner, so they would sink behind other windows and
the user perceived Outlook as "frozen". AttachOwnerToOutlook now
sets WindowInteropHelper.Owner to the foreground HWND (Win32
GetForegroundWindow with MainWindowHandle as fallback) before
ShowDialog. Also sets WindowStartupLocation=CenterOwner and pumps
SetForegroundWindow + Activate on Loaded.
2. Sidebar pane title was "פרטי תיק (EspoCRM)" — renamed to
"Klear · פרטי תיק" to match the new branding.
3. New ribbon button "פרטי תיק" (Marcus-Law group, between Klear and
הגדרות) re-opens the sidebar after the user clicks its X.
Implementation:
- SidebarController.ShowForActiveExplorer() toggles existing pane
Visible back on, or attaches one if missing.
- ExplorerBinding.Pane is now public on the binding for this
reuse.
- AddInHost.ShowSidebar() forwards to the controller.
- ExplorerRibbon.OnShowSidebar wires the ribbon callback.
imageMso = ReadingPaneShow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. New WPF converters (UI.Converters/EntityTypeToHebrewConverter.cs):
- EntityTypeToHebrewConverter maps Case/Contact/Account/Lead/... →
תיק / איש קשר / לקוח / ליד / ...
- EntityTypeToBrushConverter per-type accent brushes
- EntityTypeToIconConverter per-type emoji glyph
2. FileToCaseDialog redesigned:
- Soft slate background (#F8FAFC) with card-style borders + 6px
rounded corners
- Search row turned into a card with a 🔍 leading icon (DockPanel.Dock
respects RTL so it's on the visual right)
- Group headers now show: emoji + Hebrew label (תיק / איש קשר / ...)
+ result count, with a colored stripe per entity type
- Custom ResultItem template: hover + selected backgrounds, rounded
selection
- Recents pane has a header strip and per-row colored chip showing
the Hebrew entity type
- Primary action ("תייק") in solid blue with hover state; secondary
("ביטול") in white with border; tertiary ("+ צור איש קשר") as a
muted link button
3. ComposeFromCaseDialog gets the same card + button styling for
visual consistency.
Builds clean, 39 tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. FileToCaseDialog + ComposeFromCaseDialog: under FlowDirection=RTL,
DockPanel flips Dock="Right" to render at visual left. Switch the
search label to Dock="Left" so it renders on the right (start of
Hebrew reading order), matching where the eye expects it.
2. Replace imageMso values that were either off-topic or rare:
- FileToEspoCrm: MeetingForwardToManager → MoveToFolder
- OpenEspoCrmSettings: ServerPropertiesDialog → FileOptions
- ComposeFromCase: MailMergeStartLetters → NewMailMessage
All three are present in every Outlook version ≥ 2010 and convey the
action better.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the "תייק ל-" / "File to" prefix; the action is described in the
screentip/supertip on hover. Ribbon real estate is tight and the brand
name alone reads cleaner in both locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two combined fixes for "user clicks תייק, nothing happens":
1. EspoEntityRef now also reads "entityName" and "_scope" into
EntityType — different EspoCRM versions emit different field names
from GlobalSearch. Without this the Submit button was clickable (Id
was populated) but GetSelectedFilingTarget bailed out because
EntityType was the empty string.
2. AddInHost.LaunchFileToCaseAsync now logs the dialog close outcome
AND the rejected target shape, and pops a Hebrew MessageBox when
filing is skipped due to missing entityType — instead of silently
returning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>