toga-ai 1.0.621 → 1.0.622
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/email-queue-attachments.md +112 -0
- package/knowledge/1.0/apps/test/INDEX.md +1 -1
- package/knowledge/1.0/apps/test/features/static-no-db-regression-harness.md +15 -1
- package/knowledge/1.0/apps/togadesk/INDEX.md +1 -1
- package/knowledge/1.0/apps/togadesk/features/notifications.md +22 -2
- package/knowledge/1.0/standards/backend-php.md +29 -1
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Cron Execution Monitoring (App_Framework check-in/out → CronJobExecutions)](features/cron-execution-monitoring.md) | `App_Framework::cronInitialization()` / `App_Framework::cronFinished()` (in `library/app/framework.php`) give every 1.0 (`App_`) cron job a check-in/check-out l | library/app/framework.php |
|
|
10
10
|
| [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | Two "View Recommended Services" buttons exist in the TOGa Refresh 2026 SR view: 1. | library/app/model/toga/diagnostic.php, library/app/model/servicerequest.php |
|
|
11
11
|
| [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
|
|
12
|
+
| [App_Email Queued Sending & Attachments (Common.EmailsQueued)](features/email-queue-attachments.md) | `App_Email::send()` can either send **inline** (PHPMailer talks to SES right there) or **queue** the message: `base64(serialize($this))` is inserted into `Commo | library/app/email.php, library/phpmailer/class.phpmailer.php, worker/crons/notifications/infrastructure/send_emails.php, worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php, togadesk/desk/includes/functions.php |
|
|
12
13
|
| [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
|
|
13
14
|
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/api/toga2.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
14
15
|
| [HTTP 500 Error Monitor (App_SystemMonitor_500Error) — and why its \"Error Type\" is not a diagnosis](features/http-500-error-monitor.md) | `App_SystemMonitor_500Error` (`library/app/systemmonitor/500error.php`, title **"HTTP 500 Error Alert"**) is the 1.0 system monitor that watches **`Logs.Api` fo | library/app/systemmonitor/500error.php, worker/crons/infrastructure/system_monitors.php, api2/Controller/Index.php, _underscore/Error.php |
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: App_Email Queued Sending & Attachments (Common.EmailsQueued)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/email.php
|
|
13
|
+
- library/phpmailer/class.phpmailer.php
|
|
14
|
+
- worker/crons/notifications/infrastructure/send_emails.php
|
|
15
|
+
- worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php
|
|
16
|
+
- togadesk/desk/includes/functions.php
|
|
17
|
+
related:
|
|
18
|
+
- ./email-templates.md
|
|
19
|
+
- ../architecture.md
|
|
20
|
+
- ../../togadesk/features/notifications.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`App_Email::send()` can either send **inline** (PHPMailer talks to SES right there) or **queue**
|
|
26
|
+
the message: `base64(serialize($this))` is inserted into `Common.EmailsQueued` and drained later
|
|
27
|
+
by a **different app on a different host** — `worker/crons/notifications/infrastructure/send_emails.php`,
|
|
28
|
+
scheduled `* * * * *`.
|
|
29
|
+
|
|
30
|
+
Two facts dominate everything else here:
|
|
31
|
+
|
|
32
|
+
1. **Attachments used to be queued as filesystem *paths*.** The drain host has no such path, and
|
|
33
|
+
PHPMailer 5 silently swallows an unreadable attachment, so the mail arrived with the body
|
|
34
|
+
intact and the attachment gone — **no error, no log**. Fixed 2026-08-20 (TRUE-80952) by
|
|
35
|
+
embedding the file **bytes** at queue time.
|
|
36
|
+
2. **Queued email does not queue outside production.** `setQueueForSending()` is gated on
|
|
37
|
+
`!App_Registry::inTestMode()`, and every non-prod config has `internal.test_mode = 1`. So on
|
|
38
|
+
alpha/beta/local the message is sent **inline** off local disk and attachments always work —
|
|
39
|
+
any test there is a **false pass**.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
- `App_Email::setQueueForSending($bool)` (`library/app/email.php` ~L161):
|
|
44
|
+
`$this->queueForSending = (($bool && !App_Registry::inTestMode()) ? … : false);`
|
|
45
|
+
- `send()` with queueing on serializes `$this` and inserts into `Common.EmailsQueued`
|
|
46
|
+
(`serializedEmailObject`, LONGTEXT).
|
|
47
|
+
- Before serializing, `App_Email::embedAttachmentsForQueue()` (private, added TRUE-80952) reads
|
|
48
|
+
each attachment into **base64 bytes**. On drain, embedded entries go out via PHPMailer's
|
|
49
|
+
`AddStringAttachment()`; legacy bare-string path entries still use `AddAttachment()`.
|
|
50
|
+
- **Size budget:** `MAX_QUEUED_ATTACHMENT_BYTES = 7340032` (7 MB raw ≈ 9.8 MB base64, under the
|
|
51
|
+
SES 10 MB message cap). Over budget, embedding returns false and `send()` **falls back to an
|
|
52
|
+
inline send** instead of queueing — except when outbound email is disabled, where there is no
|
|
53
|
+
inline fallback and the case is logged.
|
|
54
|
+
- The file read is wrapped in a **local `set_error_handler` → `ErrorException` with a `finally`
|
|
55
|
+
restore**. This is mandatory, not stylistic: in 1.0 any PHP warning routes through
|
|
56
|
+
`App_Error::handleError` → `handleException` → `exit()`, so an unguarded `file_get_contents`
|
|
57
|
+
warning would kill the `send_emails` cron **mid-loop** and strand every queued email behind it.
|
|
58
|
+
`is_file()`/`is_readable()` are not sufficient — the file can vanish between check and read (TOCTOU).
|
|
59
|
+
|
|
60
|
+
## Blast radius (verified 2026-08-20)
|
|
61
|
+
|
|
62
|
+
Only **two** 1.0 call sites both queue *and* attach: TOGa Desk's `sendEmail()`
|
|
63
|
+
(`togadesk/desk/includes/functions.php`) and
|
|
64
|
+
`worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php` (~L44 + L82, bare
|
|
65
|
+
string path — it had the identical bug and is fixed by the same change). worker's other 16
|
|
66
|
+
`setQueueForSending` sites pass `false`; togaview's two queueing sites (`mvc/signup/post.php`,
|
|
67
|
+
`mvc/reset_password_success/post.php`) attach nothing; `tools` and `walmarttechservices` never
|
|
68
|
+
attach. Inline sending and bare-string path attachments are unchanged.
|
|
69
|
+
|
|
70
|
+
## Deploy ordering (asymmetric — and so is rollback)
|
|
71
|
+
|
|
72
|
+
The queue payload format changed, and the queueing app and the draining app deploy separately.
|
|
73
|
+
|
|
74
|
+
- worker **new** + togadesk **old** → togadesk queues paths, new worker still handles paths. Safe.
|
|
75
|
+
- worker **old** + togadesk **new** → togadesk queues `['data' => …]`, the old worker looks for
|
|
76
|
+
`['path']`, finds none, and **drops the attachment** — same symptom, new cause.
|
|
77
|
+
|
|
78
|
+
Therefore: **deploy `worker` before `togadesk`.** Roll back in the inverse order — roll back
|
|
79
|
+
togadesk first, let the queue drain empty, *then* worker.
|
|
80
|
+
|
|
81
|
+
## Gotchas / known issues
|
|
82
|
+
|
|
83
|
+
- **You cannot reproduce or validate a queued-email bug outside production.** `test_mode = 1` on
|
|
84
|
+
alpha, beta and every dev config collapses `setQueueForSending(true)` to `false`. This is the
|
|
85
|
+
most likely reason TRUE-75813 and TRUE-79924 were "verified" and closed while prod stayed
|
|
86
|
+
broken — both corrected the upload path *inside togadesk*, which was already working; the file
|
|
87
|
+
was lost one hop later, at the queue. Locally the queueing app and the drain also share one
|
|
88
|
+
filesystem, so even with queueing forced on the path resolves — you must **delete the source
|
|
89
|
+
file between queue and drain** to simulate the drain host.
|
|
90
|
+
- **PHPMailer 5 hides a missing attachment.** `AddAttachment()`
|
|
91
|
+
(`library/phpmailer/class.phpmailer.php` ~L1358) treats an unreadable path as `STOP_CONTINUE`
|
|
92
|
+
with `$exceptions` off, and sends the message anyway. Absence of an error proves nothing.
|
|
93
|
+
- **The drain cron's exclusion filter is dead code.** `send_emails.php` ~L34 carries
|
|
94
|
+
`AND EmailsQueued.serializedEmailObject NOT LIKE '%App_Email%'`. The column holds base64, in
|
|
95
|
+
which the literal `App_Email` never appears (verified with a PHP round-trip) — the filter
|
|
96
|
+
matches and excludes nothing. **Not changed**: "fixing" it would stop TOGa Desk mail entirely.
|
|
97
|
+
Needs a decision from whoever wrote it before anyone touches it.
|
|
98
|
+
- **OPEN RISK — `max_allowed_packet` on the prod Common cluster is unverified.** Rows can now
|
|
99
|
+
carry ~9.8 MB of base64. The column is LONGTEXT so it is fine, but MySQL 5.7 defaults
|
|
100
|
+
`max_allowed_packet` to 4 MB (8.0: 64 MB). If prod is below the budget the INSERT fails as an
|
|
101
|
+
`App_Query` exception on a live ticket reply — worse than a missing attachment. Check it, and
|
|
102
|
+
lower `MAX_QUEUED_ATTACHMENT_BYTES` beneath it if needed (the oversized path already degrades
|
|
103
|
+
to an inline send, which is the correct behavior).
|
|
104
|
+
- Regression coverage: `test/@Mark/TRUE-80952/test_queued_email_attachments.php` — 26 assertions,
|
|
105
|
+
no DB, no SMTP, confirmed failing against pre-fix HEAD.
|
|
106
|
+
|
|
107
|
+
## Change history
|
|
108
|
+
- 2026-08-20 — TRUE-80952: queued attachments now embed base64 bytes (`embedAttachmentsForQueue()`
|
|
109
|
+
+ `AddStringAttachment`) instead of paths that do not exist on the drain host; added
|
|
110
|
+
`MAX_QUEUED_ATTACHMENT_BYTES` with inline-send fallback and a scoped warning guard around the
|
|
111
|
+
file read; recorded the `test_mode` no-queue trap, the dead drain filter, and the
|
|
112
|
+
worker-before-togadesk deploy order (mhammontree)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
|
|
11
11
|
| [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
|
|
12
12
|
| [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
|
|
13
|
-
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, library/app/database.php, library/app/api/toga2.php |
|
|
13
|
+
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, test/@Mark/TRUE-80952/test_queued_email_attachments.php, library/app/database.php, library/app/api/toga2.php |
|
|
14
14
|
| [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
|
|
15
15
|
| [Talos Knowledge Base Pipeline (Uploader + Processor)](features/talos-kb-pipeline.md) | `team/talos/` holds the two-script web tooling that feeds the **TOGa Talos** (TOGa IQ) AI knowledge bases. | test/team/talos/kb_uploader.php, test/team/talos/kb_processor.php, test/team/talos/kb_processor.ini |
|
|
16
16
|
| [TOGa 2.0 Client Onboarding SQL Generator](features/toga2-client-onboarding-sql.md) | > **Superseded by the browser wizard.** The generation logic here was extracted into the reusable > `OnboardingSqlGenerator` class and wrapped in a local browse | test/team/generate_toga2_onboarding_sql.php |
|
|
@@ -6,10 +6,11 @@ project: Test
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- test/@Mark/AIG/test_multi_email.php
|
|
13
|
+
- test/@Mark/TRUE-80952/test_queued_email_attachments.php
|
|
13
14
|
- library/app/database.php
|
|
14
15
|
- library/app/api/toga2.php
|
|
15
16
|
related:
|
|
@@ -75,6 +76,17 @@ method**, which is a large share of `library`.
|
|
|
75
76
|
entirely. That was out of scope because it changes production code; prefer it when you are
|
|
76
77
|
already editing the method.
|
|
77
78
|
|
|
79
|
+
## Second worked example — private methods via `ReflectionMethod`
|
|
80
|
+
|
|
81
|
+
`test/@Mark/TRUE-80952/test_queued_email_attachments.php` (26 assertions, PHP 7.2, no DB, no SMTP)
|
|
82
|
+
extends the same pattern to a **private** method: it `require_once`s `library/app/email.php`
|
|
83
|
+
directly and invokes `App_Email::embedAttachmentsForQueue()` through `ReflectionMethod` with
|
|
84
|
+
`setAccessible(true)`. It covers the embed + serialize round-trip **with the source file deleted**
|
|
85
|
+
(the real production failure mode), a missing file, a directory path, an empty file, the legacy
|
|
86
|
+
bare-string path form, idempotent re-embed, exact-limit and aggregate-limit budget checks, and
|
|
87
|
+
that the scoped error handler is restored. Confirmed failing against pre-fix HEAD — a 1.0
|
|
88
|
+
regression test is only credible if you have run it against the broken code.
|
|
89
|
+
|
|
78
90
|
## Gotchas / known issues
|
|
79
91
|
|
|
80
92
|
- **A mirrored test proves the mirror, not production.** The drift guard is what makes it
|
|
@@ -102,6 +114,8 @@ already editing the method.
|
|
|
102
114
|
**`worker2` / `_underscore` enforce 8.1** — a helper shared between the two must satisfy 7.2.
|
|
103
115
|
|
|
104
116
|
## Change history
|
|
117
|
+
- 2026-08-20 — added the TRUE-80952 `ReflectionMethod` variant (private-method coverage, source
|
|
118
|
+
file deleted to simulate the queue-drain host) as a second worked example (mhammontree)
|
|
105
119
|
|
|
106
120
|
- 2026-08-12 — Sharpened the 7.2 lint gotcha into a real verification procedure: the default CLI is
|
|
107
121
|
PHP 8.x, so `php -l` there proves nothing; use an actual 7.2 binary, add `-n` if its `php.ini` is
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk ingests support email into tickets through a cron-driven IMAP poller (`crons/tickets.php`) plus a postfix pipe variant (`crons/pipe.php`). | crons/tickets.php, crons/tickets_prod.php, crons/pipe.php, desk/includes/classes/class.ticket.php, vendor/classes/class.imap.php |
|
|
7
7
|
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | The **central** subsystem is TOGa Desk's field-service dispatch domain: repair-order lifecycle, technician scheduling, onsite vs depot service, parts, and shipm | desk/includes/controllers/actions/central/, desk/includes/classes/class.repair.php, desk/includes/classes/class.repairhistory.php, desk/_/browser/datatable/central.php, desk/template/pages/central.php, desk/template/pages/central/view.php, desk/template/modals/central/addTracking.php, library/app/model/togadesk/repairorder.php, library/app/model/togadesk/repairordertracking.php, library/app/api/carrier/ups.php |
|
|
8
8
|
| [Managed Service Order Create Flow (New MSO modal → tickets/add)](features/managed-service-order-create.md) | The **Managed Service Order (MSO) create flow** is how a TOGa Desk user manually creates a managed service order: pick an End User (or a Client Location), choos | desk/includes/controllers/actions/tickets/add.php, desk/template/modals/tickets/addNew.php, desk/template/pages/managed.php, desk/template/pages/getinfo.php, desk/template/footer.php, library/app/model/togadesk/repairorder.php |
|
|
9
|
-
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, crons/tickets.php, crons/tickets_prod.php |
|
|
9
|
+
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, desk/includes/functions.php, desk/includes/classes/class.ticket.php, crons/tickets.php, crons/tickets_prod.php |
|
|
10
10
|
| [Per-Client Hostname Restriction (getRestrictedClient)](features/per-client-host-restriction.md) | TOGa Desk supports **per-client branded URLs** (e.g. | desk/includes/functions.php, desk/template/header.php, _/browser/datatable/tickets, desk/includes/classes/class.ticket.php, ebs/setup_phpini.php, ebs/http_to_https.php, desk/template/modals/kb/viewDocument.php, desk/template/modals/files/aws-view.php, desk/template/modals/files/contract-view.php, template/pages/documents/view.php, template/pages/tasks/view.php |
|
|
11
11
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk exposes a programmatic API at `desk/api/`. | desk/api/index.php, desk/api/resources/tickets.php, desk/api/resources/assets.php, desk/api/resources/authenticate.php, desk/includes/classes/class.apikey.php, desk/includes/functions.php |
|
|
12
12
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | The SMB contracts page (`/desk/?route=toga/smbcontracts&togaClientId=<id>`) edits `TOGA_*.SMBContracts` rows via a modal. | desk/template/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/edit.php |
|
|
@@ -6,13 +6,16 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["ajean"]
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["ajean", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/classes/class.notification.php
|
|
13
|
+
- desk/includes/functions.php
|
|
14
|
+
- desk/includes/classes/class.ticket.php
|
|
13
15
|
- crons/tickets.php
|
|
14
16
|
- crons/tickets_prod.php
|
|
15
17
|
related:
|
|
18
|
+
- 1.0/apps/library/features/email-queue-attachments.md
|
|
16
19
|
- 1.0/apps/togadesk/features/ticket-lifecycle.md
|
|
17
20
|
- 1.0/apps/togadesk/features/email-to-ticket-intake.md
|
|
18
21
|
- 1.0/apps/togadesk/architecture.md
|
|
@@ -55,6 +58,20 @@ nothing if the `STAFF_*` row is absent.
|
|
|
55
58
|
- `TOGaDeskSupport.people_departments` — peopleid ↔ departmentid mapping.
|
|
56
59
|
|
|
57
60
|
## Gotchas / known issues
|
|
61
|
+
- **Outbound attachments were dropped at the email queue, not in TOGa Desk.** `sendEmail()`
|
|
62
|
+
queues via `App_Email` into `Common.EmailsQueued`; until TRUE-80952 attachments were serialized
|
|
63
|
+
as local **paths** that do not exist on the worker host that drains the queue, and PHPMailer 5
|
|
64
|
+
silently sent the mail without them. Two earlier tickets (TRUE-75813, TRUE-79924) "fixed" the
|
|
65
|
+
upload path inside TOGa Desk — which was never the problem — and passed testing because
|
|
66
|
+
**alpha/beta/local run `test_mode = 1`, which disables queueing entirely and sends inline**.
|
|
67
|
+
Never validate an attachment/email change for TOGa Desk on a non-prod environment. See
|
|
68
|
+
[App_Email queued sending & attachments](../../library/features/email-queue-attachments.md).
|
|
69
|
+
- **The AIG branch of `sendEmail()` never attaches anything (open bug, separate from TRUE-80952).**
|
|
70
|
+
`desk/includes/functions.php` ~L961-975 and the AIG arm of `addReply()` in
|
|
71
|
+
`class.ticket.php` build `App_Email_AigTemplates_SendAigSupportTicketReplyNotification` and pass
|
|
72
|
+
`$attachments` only to `logEmail()` — **`addAttachment()` is never called**. Staples Protection
|
|
73
|
+
ticket replies therefore lose attachments for a different reason, and the TRUE-80952 fix does
|
|
74
|
+
not help them. Needs its own ticket.
|
|
58
75
|
- **Silent disable by omission.** A missing `(clientid, type)` row produces no email and no log
|
|
59
76
|
line — diagnose "missing notification" reports by querying `notifications` for that client's
|
|
60
77
|
rows first, before suspecting recipient flags or code.
|
|
@@ -70,6 +87,9 @@ nothing if the `STAFF_*` row is absent.
|
|
|
70
87
|
notification still depends on a `STAFF_NEW_TICKET` row existing for the ticket's client.
|
|
71
88
|
|
|
72
89
|
## Change history
|
|
90
|
+
- 2026-08-20 — TRUE-80952: recorded that outbound attachments were lost at the `App_Email` queue
|
|
91
|
+
(not in TOGa Desk), that non-prod `test_mode` disables queueing and yields false passes, and the
|
|
92
|
+
separate AIG-arm bug where `$attachments` never reaches `addAttachment()` (mhammontree)
|
|
73
93
|
- 2026-06-24 — documented the `notifications` `(clientid, type)` data model and staff-recipient
|
|
74
94
|
resolution; recorded the clientid-178 missing-`STAFF_*`-rows incident (ajean)
|
|
75
95
|
|
|
@@ -5,11 +5,12 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-20
|
|
9
9
|
owners: [jcardinal, rgirish, mhammontree, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/library/architecture.md
|
|
13
|
+
- ../apps/library/features/email-queue-attachments.md
|
|
13
14
|
- ../apps/library/features/error-capture-1-0.md
|
|
14
15
|
- ../../2.0/standards/backend-php.md
|
|
15
16
|
---
|
|
@@ -306,6 +307,14 @@ Two sanctioned patterns, in order of preference:
|
|
|
306
307
|
|
|
307
308
|
Never leave either one in effect beyond the block that needs it.
|
|
308
309
|
|
|
310
|
+
**Worked example — reading a file you are about to serialize (TRUE-80952).**
|
|
311
|
+
`App_Email::embedAttachmentsForQueue()` wraps its `file_get_contents` in a local
|
|
312
|
+
`set_error_handler` → `ErrorException` with a `finally` restore. Guarding with
|
|
313
|
+
`is_file()`/`is_readable()` alone is **not** sufficient: the file can vanish between the
|
|
314
|
+
check and the read (TOCTOU), and the resulting warning would `exit()` the
|
|
315
|
+
`send_emails` cron **mid-loop**, stranding every queued email behind it. Any file read
|
|
316
|
+
inside a loop that processes a queue takes pattern 1.
|
|
317
|
+
|
|
309
318
|
### Start the session before App_Error's fatal handler — and set the handler in code, not INI
|
|
310
319
|
|
|
311
320
|
On EB PHP 8.5 / AL2023, the `.so` extension and cookie flags aside, two rules make Redis
|
|
@@ -501,6 +510,25 @@ Additional verified detail:
|
|
|
501
510
|
> (~L112-123), `clients/prudential/profile.md` (~L75). Prefer the local-handler pattern
|
|
502
511
|
> from the canonical section above for anything new.
|
|
503
512
|
|
|
513
|
+
### Test mode silently disables queued email — non-prod cannot validate a queue bug
|
|
514
|
+
|
|
515
|
+
`App_Email::setQueueForSending()` (`library/app/email.php` ~L161) is gated on
|
|
516
|
+
`!App_Registry::inTestMode()`:
|
|
517
|
+
|
|
518
|
+
$this->queueForSending = (($bool && !App_Registry::inTestMode()) ? … : false);
|
|
519
|
+
|
|
520
|
+
`inTestMode()` is `config['internal']['test_mode'] > 0`, and **every non-prod config is 1**
|
|
521
|
+
(verified for togadesk: prod 0; alpha 1; beta 1; all `config.dev-*.ini` 1). So outside
|
|
522
|
+
production `setQueueForSending(true)` collapses to `false` and the email is sent **inline**,
|
|
523
|
+
off the local filesystem, by the same process that built it. Anything that only breaks on the
|
|
524
|
+
queue path — a serialization gap, a resource the drain host cannot reach — **passes on
|
|
525
|
+
alpha/beta/local**. TRUE-75813 and TRUE-79924 were both verified and closed this way while
|
|
526
|
+
production stayed broken.
|
|
527
|
+
|
|
528
|
+
Rule: a change to queued email is not verified until it is verified in production, or against
|
|
529
|
+
a harness that exercises the serialize → drain boundary with the queueing host's local state
|
|
530
|
+
removed. Treat a non-prod pass on queue code as no evidence at all.
|
|
531
|
+
|
|
504
532
|
## Security Best Practices
|
|
505
533
|
|
|
506
534
|
### Input Validation and Sanitization
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 19 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 25 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
package/package.json
CHANGED