Skip to main content

Change Document — PO Email Notification v1.1.0.0

Date: March 16, 2026 Extension: PO - Email Notification Version: 1.1.0.0 (from 1.0.88.5) Publisher: Sataware Technologies


Background

Version 1.0.88.5 of the extension contained hardcoded Azure AD credentials (client ID and client secret) directly in the source code of AzureAdMailLookup.Codeunit.al. This was identified during a code review prior to deployment to production and blocked promotion of the extension.

This version (1.1.0.0) resolves the credential exposure and addresses a set of additional correctness issues identified in the same review: blocking email sends, a missing TableType = Temporary declaration, an unnecessary Commit() in a page trigger, and unhandled HTTP error responses. The changes were developed by Bestway USA's BC development team.


Summary of Changes

#SeverityCategoryDescription
1CriticalSecurityHardcoded Azure AD client ID and secret removed from source
2CriticalCorrectnessTempEmailResults table missing TableType = Temporary
3HighCorrectnessEmail.Send blocks posting transactions on failure; replaced with Email.Enqueue
4HighCorrectnessCommit() in page trigger breaks transactional integrity
5HighCorrectnessHTTP errors in GetAccessToken / SearchEmails were unhandled
6MediumFunctionalityRecipient list split produced empty entries from trailing semicolons
7MediumFunctionalityNotify_Arrived had no missing-recipient guard (unlike Notify_Shipped)
8MediumSecurityOData $filter URL not encoded — & in display names would corrupt the URL
9LowCode qualityHand-rolled ReplaceAll replaced by built-in Text.Replace()
10LowCode qualityDuplicate HTML table header markup extracted to shared procedure
11LowCode qualityUnused PONo parameter removed from BuildHtmlTableFromPostedWRLines_All
12LowCode qualityUnclosed <p> tag in BuildFooter
13MediumObservabilityNo telemetry on happy path — added 28 structured App Insights events covering setup, Graph API, and all three notification procedures

Detailed Changes

1. Hardcoded Credentials Removed (Critical — Security)

Files: AzureAdMailLookup.Codeunit.al (modified), POEmailSetup.table.al (new), POEmailSetup.page.al (new)

Before: The Azure AD client ID and client secret were hardcoded string literals in the GetAccessToken procedure. Any user with read access to the repository or the compiled extension symbols could recover the credentials.

After: Credentials are stored in a new singleton setup table (PO Email Setup, table 80101). The client secret is held in BC's Isolated Storage (DataScope::Module) and accessed only via SecretText — it cannot be read back through any UI or OData endpoint. The GetAccessToken procedure validates that setup is complete before attempting authentication and raises descriptive errors if configuration is missing.

The GetAccessToken return type was also changed from Text to SecretText, and the bearer token is passed to HttpClient.DefaultRequestHeaders.Add via SecretStrSubstNo to prevent the token from appearing in diagnostics or logs.

Setup required post-deploy: Administrators must open the PO Email Setup page and enter the Graph API tenant ID, client ID, and client secret before the email lookup and notifications will function.


2. TempEmailResults Missing TableType = Temporary (Critical — Correctness)

File: TempEmailResults.table.al

Before: The Temp Email Results table had no TableType declaration. Despite being used exclusively as an in-memory buffer (with DeleteAll, Init, Insert, and no Commit), BC would attempt database operations on it, producing unexpected behavior under some transaction states.

After: TableType = Temporary added. The table now correctly exists only in memory for the duration of the calling session. The Commit() call that was in the page trigger to flush the table (change 4 below) is no longer needed.


3. Email.Send Replaced with Email.Enqueue (High — Correctness)

File: POEmailHelper.codeunit.al

Before: All three notification procedures called Email.Send, which executes synchronously in the same transaction as the triggering BC operation (PO release, WR creation, WR posting). If email sending failed — because the scenario was not configured, the SMTP server was unavailable, or a transient error occurred — BC would raise an error and roll back the Purchase Order or Warehouse Receipt operation.

After: A [TryFunction]-wrapped TryEnqueueEmail procedure calls Email.Enqueue instead. Enqueue adds the message to the BC email job queue and returns immediately. Failures are logged to Application Insights with descriptive event IDs (e.g., wr.shipped.enqueue.fail) but do not affect the posting transaction.

Trade-off: Email failures are no longer surfaced to the user in real time. Administrators must monitor App Insights for .enqueue.fail events to detect delivery problems. This is the correct trade-off for a notification system — it is unacceptable for a warehouse receipt to be blocked because an email address was misconfigured.


4. Commit() Removed from Page Trigger (High — Correctness)

File: PurchOrderExt.pageExt.al

Before: After calling MailLookup.SearchEmails(Text, TempResults), the page's lookup trigger called Commit(). This was added to ensure the temp table writes were visible, but it is incorrect — Commit() in a page trigger commits any open transaction in the current session, including unrelated pending changes on the calling page. This could silently commit a partially-edited Purchase Order.

After: Commit() removed. Since TempEmailResults is now correctly declared as TableType = Temporary (change 2), no database writes occur during the search and no commit is required.


5. Unhandled HTTP Errors in Token and Search (High — Correctness)

File: AzureAdMailLookup.Codeunit.al

Before: GetAccessToken and SearchEmails called Client.Post / Client.Get and immediately read the response body without checking whether the HTTP request succeeded. A connection failure or HTTP 4xx/5xx response would produce a misleading JSON parse error ("Invalid JSON response from...") rather than a clear diagnostic.

After: Both procedures now:

  1. Check the return value of Client.Post / Client.Get (which is false on connection failure) and raise a specific error with telemetry.
  2. Check Response.IsSuccessStatusCode() before parsing the body and raise an error showing the HTTP status code.

6. Recipient List Trailing Semicolons (Medium — Functionality)

File: POEmailHelper.codeunit.al

Before: BuildRecipientListFromPO split the "Send E-mail To" field on ; and returned the result directly. A trailing semicolon (common when users select multiple addresses from the lookup) would produce an empty string as the last list entry, which some email clients reject.

After: Each split entry is trimmed and filtered — empty strings are excluded from the returned list.


7. Missing-Recipient Guard in Notify_Arrived (Medium — Functionality)

File: POEmailHelper.codeunit.al

Before: Notify_Arrived_OnWhseReceiptPosted had an early exit if "Send E-mail To" was blank, but did not check whether BuildRecipientListFromPO returned an empty list (which can happen if the field contains only whitespace or semicolons after change 6). The procedure would call EmailMessage.Create with an empty recipient list, producing a silent failure.

After: A recipient-count guard was added matching the existing pattern in Notify_Shipped_OnWhseReceiptCreated, with a Warning-level telemetry event (arrived.norecip) logged when no valid recipients are found.


8. OData URL Not Encoded (Medium — Security / Correctness)

File: AzureAdMailLookup.Codeunit.al

Before: The search text was embedded directly in the OData $filter query string after escaping single quotes. Characters that are reserved in URIs — specifically &, %, +, #, and space — would corrupt the URL, causing Graph API to return a 400 error or silently return unexpected results. A vendor named "A&W" would break the URL into two query string parameters.

After: A UrlEncodeSearchText procedure applies percent-encoding to the five most likely problem characters after OData-escaping the value. % is encoded first to prevent double-encoding.


9–12. Code Quality (Low)

#Change
9ReplaceAll local procedure (hand-rolled O(n²) string replacement) replaced by Text.Replace() — built-in, cleaner, and performs the same function without the manual loop.
10Duplicate <table> / <tr> header HTML extracted to TableHeaderHtml() — all three email templates now share one definition.
11PONo: Code[20] parameter removed from BuildHtmlTableFromPostedWRLines_All — the parameter was never used in the filter; the table already filters by receipt header number.
12BuildFooter unclosed <p> tag fixed — was <a href="...">...</a> with no </p>, now <a href="...">...</a></p>.

13. Comprehensive Telemetry (Medium — Observability)

Files: AzureAdMailLookup.Codeunit.al, POEmailHelper.codeunit.al

Before: The original extension had no telemetry. The v1.1.0.0 review state added telemetry to HTTP error paths only (graph.token.connfail, graph.token.httpfail, graph.search.connfail, graph.search.httpfail). The happy path and all three notification procedures were opaque — there was no way to distinguish a correctly-configured extension from a misconfigured one without reproducing the error interactively.

After: 28 structured App Insights events cover the full lifecycle. The additions beyond HTTP error paths are:

Setup misconfiguration events (AzureAdMailLookup.Codeunit.al): Four events fire before each Error() call in GetAccessToken:

EventSeverityCondition
setup.missingErrorPO Email Setup record does not exist
setup.clientid.missingWarningGraph Client ID field is blank
setup.tenantid.missingWarningGraph Tenant ID field is blank
setup.secret.missingWarningClient secret not saved in Isolated Storage

These are the most likely failure mode immediately after deployment — an administrator deploys the extension but forgets to open the setup page. Without these events, App Insights would show no data at all, which is indistinguishable from "no one has used the extension yet."

Graph API latency tracking: StartTime := CurrentDateTime() is captured before Client.Get(). ElapsedMs := CurrentDateTime() - StartTime is computed after the HTTP call returns. DurationMs is included as a dimension on both graph.search.ok and graph.search.httpfail. This supports KQL latency queries to detect Graph API slowdowns before they affect users.

Zero-result warning (graph.search.noresults): Logged at Warning severity immediately after graph.search.ok when ValueArray.Count() = 0. This is distinct from a search error — the API call succeeded but returned no users. This most commonly indicates a misconfigured User.Read.All permission on the Azure AD app registration.

PO Created notification telemetry (POEmailHelper.codeunit.al): The original Notify_POCreated_OnRelease procedure called SendEmail with no telemetry of its own. All three events (po.created.norecip, po.created.sent, po.created.enqueue.fail) are now logged, matching the pattern established by the Shipped and Arrived procedures. po.created.sent includes RecipientCount and LineCount dimensions.

Enriched success events: wr.shipped.sent and arrived.sent now include RecipientCount and PO dimensions. arrived.nolines Warning added for posted receipts with no item lines (previously a silent exit).


Object Inventory

ObjectIDTypeStatus
AzureAdMailLookup80100CodeunitModified
Temp Email Results80100TableModified — TableType = Temporary added
PO Email Setup80101TableNew
Purch. Order Ext80101Page ExtensionModified — Commit() removed
PO Email Setup80101PageNew
PO Email Helper80103CodeunitModified
PO Email Subscribers80104CodeunitModified — SetLoadFields added to WR line query
PoEmailPermissionSet80100Permission SetModified — new table and page added

File Structure

PO Email Notification/
├── app.json (version 1.1.0.0)
├── README.md
├── docs/
│ ├── CHANGELOG.md
│ └── CHANGE-v1.1.0.0.md (this file)
├── src/
│ ├── codeunits/
│ │ ├── AzureAdMailLookup.Codeunit.al
│ │ ├── POEmailHelper.codeunit.al
│ │ └── POEmailSubscribers.codeunit.al
│ ├── pages/
│ │ └── POEmailSetup.page.al (new)
│ ├── pageExtensions/
│ │ └── PurchOrderExt.pageExt.al
│ ├── permissionset/
│ │ └── PoEmailPermissionSet.permissionset.al
│ └── tables/
│ ├── POEmailSetup.table.al (new)
│ └── TempEmailResults.table.al
└── test/
├── conftest.py
├── pytest.ini
└── test_po_email.py

Deployment Notes

Pre-Deployment

  1. Confirm the "Custom Purchase Emails" email scenario is mapped to an active outbound account in the target environment.
  2. Note the current Azure AD app registration credentials (tenant ID, client ID, and secret) — you will need to enter them in the setup page after deployment.
  3. This version has no data migration requirements. The PO Email Setup record is created automatically when the setup page is first opened.

Post-Deployment

  1. Navigate to PO Email Setup and enter the Graph API credentials.
  2. Click Save Secret to persist the client secret.
  3. Assign PoEmailPermissionSet to all users who need access to the email lookup or setup page.
  4. Test the email lookup on a Purchase Order to confirm connectivity.
  5. Monitor App Insights for any graph.token. or graph.search. error events during the first few days.

Rollback

This version is backward-compatible with the database schema from 1.0.88.5. The new PO Email Setup table is additive. Rolling back to 1.0.88.5 will leave the setup table in place but it will be unused. No data migration is required in either direction.


Known Limitations

  • The recipient semicolon delimiter is fixed — comma-separated addresses are not supported.
  • Graph API results are capped at 50 per search. If a name or email prefix matches more than 50 users, only the first 50 are returned.
  • The Arrived notification fires on all purchase postings that create a receipt (including direct PO posting without a warehouse flow). This is by design and is documented in the subscriber code.
  • Email failures are silent to end users. App Insights monitoring is required to detect enqueue failures.

Testing

See test/test_po_email.py for the automated test suite. Tests are organized into tiers:

  • bc — extension deployment, PO API operations, security and code quality checks (requires BC sandbox credentials)
  • telemetry — App Insights queries to verify email enqueue events (requires az login)

The UAT test plan is in docs/UAT Test Plan - PO Email Notification v1.1.0.0.docx.