Skip to content

feat(email): Share ⇒ as email — send a document as the message, in the user's own name - #1222

Merged
rbuergi merged 6 commits into
mainfrom
worktree-agent-ac96ac84d00b1e521
Aug 11, 2026
Merged

rbuergi merged 6 commits into
mainfrom
worktree-agent-ac96ac84d00b1e521

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Why

Asked to email a document, a user had two bad options: send a PDF attachment (the node menu's
send entry was hardcoded to ExportFormat.Pdf with a one-line covering note), or copy-paste — which
loses exactly the parts that matter. And whichever they chose, it went out from a generic portal
address
.

The export also silently drops embedded views. A document can embed a live layout area with
@@("area:OgCard?urls=…"). Every export before this one lost it:

Path What happens to @@(…)
Markdown → PDF (the only path a Markdown node can reach) printed as literal source text
Markdown → DOCX printed as literal source text
Deck → PDF, pixel fidelity empty placeholder div → prints blank

DocumentBuilder builds its own Markdig pipeline (UseAdvancedExtensions().UsePageBreaks()) without
LayoutAreaMarkdownExtension, so the embed is never even recognised as a block.

Share ⇒ as email

One node-menu entry, in the same group as Export to PDF / DOCX — the former "Send to contacts", grown
up rather than a second confusable item.

It comes from the person

Sharing is a personal act, so the mail goes out as the signed-in user via their delegated
personal-assistant credential (EaCredential, Graph /me/sendMail). Recipient sees the person,
replies come back to them, SaveToSentItems so it is in their own Sent Items.

No new consent step: EaGraphAuth.Scopes already requests delegated Mail.Send (alongside
Mail.ReadWrite, Calendars.ReadWrite, offline_access). Connecting the personal assistant grants
it — "connected" and "can send as themselves" are the same condition. Nothing to provision, no admin
consent needed.

The identity is never implicit:

  • IEmailSender gains SendEmail(…, EmailDelivery) and CanSendAsUser(objectId) — both
    default-implemented, so the no-op sender, SMTP senders and test doubles compile and behave
    unchanged.
  • The dialog probes before composing and states the From line.
  • Not connected ⇒ no silent fallback. It offers /auth/ea/connect as the primary action; the
    shared mailbox is only ever an explicitly chosen second option, and then Reply-To is set to the
    user
    so replies still reach a human.
  • Submit re-probes — a credential can lapse between opening the dialog and pressing send — and
    stops rather than substituting a different sender. A failed delegated send surfaces; it is not
    downgraded.

Graph is queried live, never replicated

Graph is the system of record and always current; a mirror buys nothing and leaves personal
correspondence at rest in the mesh. The mesh holds at most an InternetMessageId/ConversationId
reference. The existing inbound mail→agent channel is a separate, deliberate path — untouched.

Embedded areas are resolved server-side

EmailAreaResolver opens each area's synchronization stream — the same one the browser subscribes
to, under the caller's AccessContext — and EmailControlRenderer serializes the settled control
tree to static markup. No browser.

A live area has no completion signal (OgCard emits placeholder cards and fills each in as its
fetch lands), so the tree is snapshotted once quiescent, or at the deadline. Taking the first
emission would export the placeholders.

Built for the Word engine

Outlook on Windows renders through Word. Every rule is a defect seen in a real inbox:

  • per-cell <td>/<th> widths — Word IGNORES <colgroup>;
  • widths proportional to content volume, sqrt-damped with floor/cap so a prose column cannot
    starve the short ones;
  • tables only (no flex/grid); inline CSS only (head styles get stripped);
  • inline <svg> stripped — Word paints a broken-image box, so an omitted icon beats a broken one;
  • absolute URLs — a mail client has no page origin;
  • pictures become cid: inline parts (EmailAttachment.ContentId + IsInline): data: URIs are
    stripped by classic Outlook and remote images sit behind "Download pictures".

Two silent traps fixed here

  1. HtmlAgilityPack caches rendered markup. attribute.Value = x mutates the DOM but not the
    output. Use SetAttributeValue. (Tell: table widths landed, link rewriting didn't.)
  2. Uri.TryCreate("/Some/Page", UriKind.Absolute) returns TRUE on Unix — an implicit file: URI
    — so the obvious "already absolute?" guard ships root-relative links dead in the inbox, while
    returning false on Windows and hiding it. EmailHtmlSanitizer.HasScheme inspects the scheme.

Skill

content/ai/Skill/share-email.md ships with the feature so an agent can drive it and inherits the
constraints: identity rules, the Word-engine HTML rules, live-not-replicated, and the honest
statement that PDF/DOCX still do not render layout areas. Registered in both expected-id test
literals (BuiltInSkillCatalog is not on main yet).

Tests

EmailDocumentExportTest — no mocking; assertions are on the mail a capturing IEmailSender
actually received (it implements the identity overload, so a sender that ignored EmailDelivery
would fail):

  • an embedded OgCard area arrives as a real card, never layout-area;
  • no <script>/<style>/<link>/<svg>/display:grid/flex;
  • every href absolute (regex-asserted);
  • per-cell widths present, prose column wider than the No. column;
  • a data: image becomes a cid: part, a remote image is reported;
  • the sender identity reaches the sender (SendAsUserObjectId), and the shared-mailbox fallback
    carries Reply-To.

Green: EmailDocumentExportTest, SendDocumentToContactsTest, SkillNodeTypeTest,
SkillHarnessImportSourceTest, LocalizationTest, WhatsNewEntryIntegrityTest,
DocumentationLinkIntegrityTest.

Deployment note

Email is live on memex (ns memex): Email__Enabled=true, Graph, shared mailbox
memex@systemorph.com. That mailbox is now the fallback, not the default.

Follow-up

Graph contacts autocomplete (GET /me/people?$search=, live, debounced, no replication) — same
credential, same consent, so it needs no new provisioning. Reply-into-an-existing-conversation
(POST /me/messages/{id}/createReply, letting Graph supply the threading headers) is designed for
in the Skill and comes next.

🤖 Generated with Claude Code

…ws resolved

"Send to contacts" could only ever send a PDF attachment: it exported the node to a
file, attached it, and put a one-line covering note in the body. Asked to email a
document, a user had to copy-paste it — and lost exactly the parts that mattered.

The node menu entry (same block as Export to PDF / DOCX, now "Email this document")
gains a delivery choice. DocumentDelivery.EmailBody runs the SAME node => file
pipeline with a new ExportFormat.Html — Templates/Export/Html — and uses the result
as the email body, prepending the sender's note into it. Attachment delivery keeps
the old behaviour. One entry, not two: the capability absorbs the existing one.

Embedded layout areas now survive the export. A document can embed a live view with
@@(...), and every export so far dropped it: the PDF/DOCX templates parse markdown
with a pipeline that has never heard of the embed syntax, so an embed is printed as
its LITERAL SOURCE TEXT; the pixel path emits the placeholder div a browser would
have filled and prints a blank. EmailAreaResolver opens each area's synchronization
stream server-side under the caller's AccessContext and EmailControlRenderer
serializes the settled control tree to static markup — no browser involved. A live
area has no completion signal (OgCard emits placeholders and fills them in as each
fetch lands), so the snapshot is taken on quiescence, never on the first emission.

The output targets the Word engine Outlook renders with, because each of these is a
defect seen in a real inbox:
  - per-cell <td>/<th> widths, since Word IGNORES <colgroup>;
  - column widths proportional to content volume, sqrt-damped with a floor and cap,
    so a prose column cannot starve the short ones;
  - table layout only (no flex/grid), inline CSS only (head styles get stripped);
  - inline <svg> removed — Word paints it as a broken-image box;
  - absolute URLs, since a mail client has no page origin;
  - pictures converted to cid: inline parts (EmailAttachment.ContentId), because
    data: URIs are stripped by classic Outlook and remote images sit behind
    "Download pictures".

Two traps worth naming, both silent and both fixed here: HtmlAgilityPack caches a
node's rendered markup, so `attribute.Value = x` mutates the DOM without changing
the serialized output (use SetAttributeValue); and on Unix
Uri.TryCreate("/Some/Page", UriKind.Absolute) succeeds as an implicit file: URI, so
the obvious "is it already absolute" guard ships root-relative links dead — while
returning false on Windows, hiding it.

Localized en+de in the Hub catalogs and the byte-identical react vendored copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 11, 2026 15:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an email-first export path that sends a document as email-safe HTML (optionally as the message body), including server-side resolution of embedded layout areas that previously exported as literal @@(...) text or blank placeholders.

Changes:

  • Introduces ExportFormat.Html and an embedded ExportHtml.csx template to render “email-safe” HTML with layout-area embeds resolved.
  • Adds an email HTML pipeline (area resolution, sanitization, table sizing, image cid: inlining) and delivery selection (Attachment vs EmailBody).
  • Updates UI + localization (menu label, send dialog, export dialog hint/button) and adds end-to-end tests + docs/What’s New.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/MeshWeaver.Hosting.Monolith.Test/EmailDocumentExportTest.cs End-to-end tests for emailing a document as HTML body, including embed resolution and mail-safety constraints.
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json Adds/updates English UI strings for “Email this document” and export/email dialogs.
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json Adds/updates German UI strings for the new email/export UX.
src/MeshWeaver.Mesh.Contract/EmailAttachment.cs Extends attachments to support inline ContentId parts (cid:) with IsInline.
src/MeshWeaver.Markdown.Export/Templates/ExportHtml.csx New export script producing email-safe HTML, including embed resolution via the email composer.
src/MeshWeaver.Markdown.Export/MeshWeaver.Markdown.Export.csproj Adds HtmlAgilityPack and embeds the new HTML export template resource.
src/MeshWeaver.Markdown.Export/MarkdownExportTemplates.cs Registers the HTML export template node alongside PDF/DOCX.
src/MeshWeaver.Markdown.Export/Layout/SendDocumentLayoutArea.cs Updates send-to-contacts dialog to support body-vs-attachment delivery and localization.
src/MeshWeaver.Markdown.Export/Layout/MarkdownExportMenuProvider.cs Positions “Email this document” alongside export menu items.
src/MeshWeaver.Markdown.Export/Layout/ExportDocumentLayoutArea.cs Adds an HTML export layout area route and defaulting behavior.
src/MeshWeaver.Markdown.Export/Handlers/SendDocumentDispatch.cs Adds DocumentDelivery option; supports embedding HTML as body with image inlining.
src/MeshWeaver.Markdown.Export/Handlers/ExportDocumentHandler.cs Wires ExportFormat.Html to the correct template.
src/MeshWeaver.Markdown.Export/Email/EmailTableSizer.cs Adds Word-friendly per-cell table width sizing for markdown tables.
src/MeshWeaver.Markdown.Export/Email/EmailStyles.cs Defines inline-CSS tokens used by email renderer output.
src/MeshWeaver.Markdown.Export/Email/EmailNode.cs Provides a small HTML node model for safe serialization (escaping centralized).
src/MeshWeaver.Markdown.Export/Email/EmailImageInliner.cs Converts embedded images into cid: inline parts; reports remote images.
src/MeshWeaver.Markdown.Export/Email/EmailHtmlSanitizer.cs Sanitizes rendered HTML for email clients and absolutizes URLs safely.
src/MeshWeaver.Markdown.Export/Email/EmailHtmlOptions.cs Config knobs for email HTML rendering (base URL, settle/timeout, layout sizes).
src/MeshWeaver.Markdown.Export/Email/EmailDocumentComposer.cs Orchestrates markdown render → area resolve → sanitize → wrap as email-safe document.
src/MeshWeaver.Markdown.Export/Email/EmailControlRenderer.cs Renders settled layout-area control trees into static email HTML markup.
src/MeshWeaver.Markdown.Export/Email/EmailAreaResolver.cs Finds layout-area placeholders and replaces them with rendered static markup.
src/MeshWeaver.Markdown.Export/Configuration/MarkdownExportExtensions.cs Registers the HTML export layout view in the markdown export module.
src/MeshWeaver.Markdown.Export/Configuration/ExportFormat.cs Adds Html format with documentation about email-safe constraints and embed resolution.
src/MeshWeaver.Markdown.Export/Configuration/DocumentExportOptions.cs Adds BaseUrl for absolutizing links/images in HTML email exports.
src/MeshWeaver.Markdown.Export/Configuration/DocumentDelivery.cs New enum for Attachment vs EmailBody delivery semantics.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-11-email-a-document-instead-of-attaching-it.md What’s New entry for the feature.
src/MeshWeaver.Documentation/Data/Architecture/SendingEmail.md Documents the new “send as body” pipeline, constraints, and embed resolution.
src/MeshWeaver.Blazor/Components/ExportDocumentView.razor Updates export dialog for HTML (hint + hides non-applicable paged options); localizes button label.
memex/Memex.Portal.Shared/Email/GraphEmailSender.cs Maps ContentId/IsInline onto Microsoft Graph attachments for inline rendering.
clients/react/src/i18n/strings.en.json Vendored React i18n strings kept in sync with hub localization.
clients/react/src/i18n/strings.de.json Vendored React i18n strings kept in sync with hub localization.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@@ -64,41 +74,63 @@ private static UiControl BuildSendForm(LayoutAreaHost host, string hubPath)
["recipient"] = "",
["email"] = "",
["subject"] = $"Shared with you: {nodeName}",
Comment on lines +145 to +148
if (delivery == DocumentDelivery.EmailBody)
{
var documentHtml = EmailDocumentComposer.PrependIntro(
Encoding.UTF8.GetString(rendered.Content), htmlBody);
Comment on lines +91 to +95
MarkdownControl markdown =>
Observable.Return(new Rendered(
markdown,
EmailNode.Raw(Markdig.Markdown.ToHtml(AsText(markdown.Markdown))))),

Comment on lines +60 to +64
return placeholders
.ToObservable()
.SelectMany(placeholder => RenderPlaceholder(placeholder, hub, options, caller, logger)
.Select(markup => (placeholder, markup)))
.ToList()
@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

707 tests  ±0   707 ✅ ±0   14m 13s ⏱️ + 4m 31s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit f931149. ± Comparison against base commit eb283c4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 291 tests  +56   1 291 ✅ +56   5m 57s ⏱️ -31s
   11 suites ± 0       0 💤 ± 0 
   11 files   ± 0       0 ❌ ± 0 

Results for commit f931149. ± Comparison against base commit eb283c4.

This pull request removes 161 and adds 217 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.ExportDocumentScriptRelayTest ‑ ExportRequest_StartsScriptActivity_AndReturnsBytesOnTerminal
MeshWeaver.Hosting.Monolith.Test.FrameworkStaleInstanceRenderTest ‑ FrameworkStaleInstance_RendersCompiledArea_NotOverlay
MeshWeaver.Hosting.Monolith.Test.GetDataRequestPropagationTest ‑ LocalUpdate_VisibleViaPolledGetDataRequest
MeshWeaver.Hosting.Monolith.Test.HardDeadlineHonoursFactTimeoutTest ‑ NoTestClass_OverridesHardDeadlineBelowItsOwnDeclaredFactTimeout
MeshWeaver.Hosting.Monolith.Test.HardDeadlineHonoursFactTimeoutTest ‑ TestRunningPastTheDefaultDeadline_IsNotKilled_WhenItDeclaresALargerBudget
MeshWeaver.Hosting.Monolith.Test.KernelReplChainingTest ‑ SecondSubmission_SeesFirstSubmissionsVariablesFunctionsAndTypes
MeshWeaver.Hosting.Monolith.Test.LanguageModelSyncedQueryTest ‑ SyncedQuery_AgentsAndModels_FullyPopulated
MeshWeaver.Hosting.Monolith.Test.LanguageModelSyncedQueryTest ‑ SyncedQuery_GetQueryById_ReusesSameUpstreamAcrossCalls
MeshWeaver.Hosting.Monolith.Test.LayoutAreaResubscribeHostAccumulationTest ‑ RepeatedWrites_DoNotStackLayoutAreaHosts_OnTheOwner
MeshWeaver.Hosting.Monolith.Test.LinkedInProfileLayoutAreaTest ‑ LinkedInProfile_NodeType_CompilesAndRendersOverview
…
MeshWeaver.Hosting.Monolith.Test.EmailDocumentExportTest ‑ Absolutize_LeavesAbsoluteAndAnchorLinksAlone
MeshWeaver.Hosting.Monolith.Test.EmailDocumentExportTest ‑ EmailDocument_AsBody_ResolvesEmbeddedLayoutArea_AndIsMailSafe
MeshWeaver.Hosting.Monolith.Test.EmailDocumentExportTest ‑ EmbeddedPicture_BecomesInlineContentIdPart
MeshWeaver.Hosting.Monolith.Test.EmailDocumentExportTest ‑ SharedMailboxFallback_CarriesReplyToTheSender
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ GetStreamAsync_ExistingFile_ReturnsStream
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ GetStreamAsync_NonExistentFile_ReturnsNull
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ ProviderType_ReturnsFileSystem
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ WriteStreamAsync_CreatesDirectory
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ WriteStreamAsync_CreatesFile
MeshWeaver.Hosting.Monolith.Test.FrameworkStaleProactiveRebuildTest ‑ FrameworkStaleNodeType_ProactivelyRebuilds_WithoutInstanceActivation
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 304 tests  ±0   1 294 ✅ ±0   9m 14s ⏱️ - 2m 58s
   11 suites ±0      10 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit f931149. ± Comparison against base commit eb283c4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 037 tests   - 52   2 035 ✅  - 52   7m 39s ⏱️ +33s
   11 suites ± 0       2 💤 ± 0 
   11 files   ± 0       0 ❌ ± 0 

Results for commit f931149. ± Comparison against base commit eb283c4.

This pull request removes 213 and adds 161 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ GetStreamAsync_ExistingFile_ReturnsStream
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ GetStreamAsync_NonExistentFile_ReturnsNull
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ ProviderType_ReturnsFileSystem
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ WriteStreamAsync_CreatesDirectory
MeshWeaver.Hosting.Monolith.Test.FileSystemStreamProviderTests ‑ WriteStreamAsync_CreatesFile
MeshWeaver.Hosting.Monolith.Test.FrameworkStaleProactiveRebuildTest ‑ FrameworkStaleNodeType_ProactivelyRebuilds_WithoutInstanceActivation
MeshWeaver.Hosting.Monolith.Test.GetMeshNodeTimeoutSurfacingTest ‑ NodeThatDoesNotExist_StillEmitsNull_Promptly
MeshWeaver.Hosting.Monolith.Test.GetMeshNodeTimeoutSurfacingTest ‑ ReadTimesOut_MessageCarriesPendingRequestDiagnostics
MeshWeaver.Hosting.Monolith.Test.GetMeshNodeTimeoutSurfacingTest ‑ ReadTimesOut_SurfacesTimeoutException_NotSilentNull
MeshWeaver.Hosting.Monolith.Test.GetMeshNodeTimeoutSurfacingTest ‑ ReadTimesOut_WithEmitNullOptIn_StillEmitsNull
…
MeshWeaver.Hosting.Monolith.Test.ExportDocumentScriptRelayTest ‑ ExportRequest_StartsScriptActivity_AndReturnsBytesOnTerminal
MeshWeaver.Hosting.Monolith.Test.FrameworkStaleInstanceRenderTest ‑ FrameworkStaleInstance_RendersCompiledArea_NotOverlay
MeshWeaver.Hosting.Monolith.Test.GetDataRequestPropagationTest ‑ LocalUpdate_VisibleViaPolledGetDataRequest
MeshWeaver.Hosting.Monolith.Test.HardDeadlineHonoursFactTimeoutTest ‑ NoTestClass_OverridesHardDeadlineBelowItsOwnDeclaredFactTimeout
MeshWeaver.Hosting.Monolith.Test.HardDeadlineHonoursFactTimeoutTest ‑ TestRunningPastTheDefaultDeadline_IsNotKilled_WhenItDeclaresALargerBudget
MeshWeaver.Hosting.Monolith.Test.KernelReplChainingTest ‑ SecondSubmission_SeesFirstSubmissionsVariablesFunctionsAndTypes
MeshWeaver.Hosting.Monolith.Test.LanguageModelSyncedQueryTest ‑ SyncedQuery_AgentsAndModels_FullyPopulated
MeshWeaver.Hosting.Monolith.Test.LanguageModelSyncedQueryTest ‑ SyncedQuery_GetQueryById_ReusesSameUpstreamAcrossCalls
MeshWeaver.Hosting.Monolith.Test.LayoutAreaResubscribeHostAccumulationTest ‑ RepeatedWrites_DoNotStackLayoutAreaHosts_OnTheOwner
MeshWeaver.Hosting.Monolith.Test.LinkedInProfileLayoutAreaTest ‑ LinkedInProfile_NodeType_CompilesAndRendersOverview
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 798 tests  ±0   1 606 ✅ ±0   6m 36s ⏱️ - 1m 28s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit f931149. ± Comparison against base commit eb283c4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   7m 42s ⏱️ +4s
2 083 tests ±0  1 980 ✅ ±0  103 💤 ±0  0 ❌ ±0 
2 301 runs  +1  2 198 ✅ +1  103 💤 ±0  0 ❌ ±0 

Results for commit f931149. ± Comparison against base commit eb283c4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ±0     65 suites  ±0   51m 23s ⏱️ +11s
9 220 tests +4  8 913 ✅ +4  307 💤 ±0  0 ❌ ±0 
9 438 runs  +5  9 131 ✅ +5  307 💤 ±0  0 ❌ ±0 

Results for commit f931149. ± Comparison against base commit eb283c4.

♻️ This comment has been updated with latest results.

…rtal

Sharing a document is a PERSONAL act, so the mail now goes out from the person's own
mailbox using their delegated personal-assistant credential (EaCredential, Graph
/me/sendMail). The recipient sees the person, replies come back to them naturally,
and the message lands in their own Sent Items. Sending a client-facing proposal from
a generic portal address is exactly the surprise this prevents.

No new consent step is needed: EaGraphAuth.Scopes ALREADY requests delegated
Mail.Send alongside Mail.ReadWrite/Calendars.ReadWrite, so connecting the personal
assistant grants it. "Connected" and "can send as themselves" are the same condition.

The identity is never implicit:
  - IEmailSender gains SendEmail(..., EmailDelivery) and CanSendAsUser(objectId),
    both DEFAULT-implemented so existing senders (no-op, SMTP, test doubles) compile
    and behave unchanged.
  - The dialog PROBES CanSendAsUser before composing and states the From line.
  - Not connected ⇒ no silent fallback. It offers /auth/ea/connect; the shared
    mailbox is only ever an explicitly chosen second option, and then Reply-To is set
    to the user so a reply still reaches a human.
  - Submit re-probes at send time — a credential can lapse between opening the dialog
    and pressing send — and stops rather than substituting a different sender.
  - A failed delegated send surfaces; it is NOT downgraded to a shared-mailbox send.

Graph is queried LIVE and never replicated into the mesh: it is the system of record
and always current, so a mirror would buy nothing while leaving personal
correspondence at rest in the mesh. The mesh may hold at most an InternetMessageId /
ConversationId reference. (The existing inbound mail→agent channel is a separate,
deliberate path and is untouched.)

Also ships the /share-email Skill so an agent can drive the capability and inherits
the hard-won constraints: the identity rules, the Word-engine HTML rules, the
live-not-replicated rule, and the honest statement that PDF/DOCX export still does
NOT render embedded layout areas. Registered in both expected-id test literals
(BuiltInSkillCatalog is not on main yet); a malformed skill file would be silently
skipped, so the parse is covered by the passing catalog tests.

Menu entry renamed to the user's own wording, "Share ⇒ as email", en+de, with the
byte-identical react vendored catalogs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rbuergi rbuergi changed the title feat(export): email a document as the message body, with embedded views resolved feat(email): Share ⇒ as email — send a document as the message, in the user's own name Aug 11, 2026
GetDiagnostics_BrokenNodeType_ReturnsErrorStatus asserted "Error" from a SINGLE
GetDiagnostics read taken right after forcing compilation. "Compiling" is a
legitimate intermediate state — Roslyn runs off the calling thread and the Error
status is written when it finishes — so the assert is a race the test loses whenever
the machine is loaded. CI shard 3 saw exactly that: "Expected Error … but found
Compiling", with the CS0246 for ThisTypeDoesNotExist logged moments later, 10.7s
into the test.

It passes locally (1147-test assembly, DOTNET_PROCESSOR_COUNT=4) because a faster
machine finishes the compile before the read — which is what made it a CI-only
failure rather than an obvious one.

Fixed by waiting on the actual condition: poll GetDiagnostics until the status is
terminal, bounded by a Timeout. Polling rather than a stream because GetDiagnostics
is request/response — the shape WritingTests.md prescribes for exactly this case. The
status probe swallows a malformed payload so a transient response keeps the poll
going instead of faulting it.

Not a widened threshold and not a sleep: the assertion is unchanged and still
requires Error: only the "when do we read it" bug is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request Aug 11, 2026
The exception said the Chromium path merely lacked cover pages/TOC/headers. The
harder fact is that Chromium is not in the deployed base image at all
(deploy/base-images/portal-ai/Dockerfile has no browser package - verified), so
removing QuestPDF today would leave the portal with NO PDF renderer rather than a
less capable one. Shipping Chromium in the base image is a deployment decision
with size/attack-surface/build-time consequences and needs to be made on its own
merits, not as a side effect of a licence cleanup.

Records the sequencing (#1222 -> #1234 -> base-image decision -> removal) and
dates the entry, so the exception reads as tracked debt with a blocker rather
than an approval that quietly became permanent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rbuergi and others added 3 commits August 11, 2026 19:44
…84d00b1e521

# Conflicts:
#	test/MeshWeaver.AI.Test/SkillHarnessImportSourceTest.cs
#	test/MeshWeaver.AI.Test/SkillNodeTypeTest.cs
… the diff

The built-in-catalog assertion compares two 21-entry collections, and the collection
assertion truncates BOTH sides after ten entries. A one-skill difference therefore
prints two identical-looking prefixes ending in "…" — in the CI log AND in the trx —
so the failure reports only that two collections differ, which is the information you
already had. Diagnosing a CI-only mismatch from that is impossible: the artifacts
cannot tell you which id is missing.

AssertMatches now reports the set difference by name (missing / unexpected / both
counts / the full shipped set) and points at the mechanism — a skill ships by its FILE
via the glob in MeshWeaver.AI.csproj — so the next occurrence is a one-line diagnosis
rather than an archaeology session.

Also compares ORDINALLY. The previous OrderBy(x => x) is culture-sensitive, and a
culture-sensitive sort treats the hyphen in og-card / share-email / pull-request as
ignorable punctuation, which makes the expected ORDER depend on the machine's locale.
(Verified not to be the current failure — the ids sort identically both ways and under
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 — but a latent locale dependency in a shared
expectation is worth removing while here.)

The assertion is not weakened: exact set and exact order are still required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…84d00b1e521

# Conflicts:
#	test/MeshWeaver.AI.Test/BuiltInSkillCatalog.cs
#	test/MeshWeaver.AI.Test/SkillHarnessImportSourceTest.cs
#	test/MeshWeaver.AI.Test/SkillNodeTypeTest.cs
@rbuergi
rbuergi merged commit dad8615 into main Aug 11, 2026
25 of 26 checks passed
rbuergi added a commit that referenced this pull request Aug 11, 2026
Conflict resolution: accept main's deletion of BuiltInSkillCatalog.cs and its
file-derived skill expectations (#1225) — the share-email id I had added to that
literal is now derived from content/ai/Skill/*.md, so nothing to carry over.
@rbuergi
rbuergi deleted the worktree-agent-ac96ac84d00b1e521 branch August 19, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants