toga-ai 1.0.621 → 1.0.623
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/knowledge/sessions/2026-08-20-true-80952-queued-attachments-mhammontree.md +88 -0
- 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)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: true-80952-queued-attachments
|
|
4
|
+
title: TRUE-80952 outbound attachments lost at the email queue
|
|
5
|
+
author: mhammontree
|
|
6
|
+
repos: [library, togadesk]
|
|
7
|
+
framework: "1.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-08-20
|
|
11
|
+
updated: 2026-08-20
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: true-80952-queued-attachments
|
|
15
|
+
**Date:** 2026-08-20
|
|
16
|
+
**Project/Repo:** library (1.0 core) + togadesk (1.0 app)
|
|
17
|
+
**Task:** TRUE-80952 — investigate and fix why outbound attachments never arrive from TOGa Desk; root cause found at the email queue hop, fixed in `library/app/email.php`, committed but not deployed or verified in prod.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
|
|
23
|
+
- **Root cause identified and traced end to end.** `App_Email::send()` queues an email as `base64_encode(serialize($this))` into `Common.EmailsQueued` and holds attachments **only as local filesystem PATHS**. The queue is drained by `worker/crons/notifications/infrastructure/send_emails.php` (scheduled `* * * * *`) on a **different host**, where the queueing app's `desk/uploads/` does not exist. PHPMailer 5's `AddAttachment()` (`library/phpmailer/class.phpmailer.php:1358`) catches the unreadable path as `STOP_CONTINUE` and, with `$exceptions` off, **swallows it and sends the email anyway** — body intact, attachment gone, nothing logged. Exact match for the reported symptom.
|
|
24
|
+
- **Confirmed the togadesk side was already correct.** `class.notification.php:86-101` gathers files into `['path'=>..,'name'=>..]` with a `file_exists()` guard and logging; `functions.php:1077-1090` passes them to `addAttachment()`. Both work. The loss is one hop later.
|
|
25
|
+
- **Fix implemented and committed** — `library/app/email.php`, +98/−2, commit `b3a9420d` on branch `TRUE-80952`. New private `embedAttachmentsForQueue()` reads each attachment into base64 bytes before serialization; `send()` delivers embedded entries via `AddStringAttachment()`; new constant `MAX_QUEUED_ATTACHMENT_BYTES = 7340032`.
|
|
26
|
+
- **Regression suite: 26 assertions, all passing on PHP 7.2** — `C:\WWW\test\@Mark\TRUE-80952\test_queued_email_attachments.php`. Standalone (no DB, no SMTP): requires `library/app/email.php` directly and uses `ReflectionMethod` for the private method. The core test **deletes the source file after queueing** to simulate the drain host.
|
|
27
|
+
- **Verified the suite genuinely fails pre-fix** — ran it against `git show HEAD:app/email.php` in a scratch tree; fatals with `ReflectionException: Method App_Email::embedAttachmentsForQueue() does not exist`. It is a real regression test, not one written to the new code.
|
|
28
|
+
- **`php -l` clean on both PHP 7.2 (xampp_7_2_33) and PHP 8 (xampp_8x).** No typed properties introduced; file stays 7.2-compatible.
|
|
29
|
+
- **php-reviewer agent run; both real findings fixed** (see Decisions). Its third point — that the base64 inflation comment said 3/4 — was **wrong**; the comment already reads "~4/3", which is correct. Left as-is.
|
|
30
|
+
- **Empirically disproved the drain cron's exclusion filter.** `send_emails.php:34` has `AND EmailsQueued.serializedEmailObject NOT LIKE '%App_Email%'`. Ran a PHP round-trip: `base64_encode(serialize($appEmailObject))` never contains the literal `App_Email`. The filter matches nothing.
|
|
31
|
+
- **Enumerated the blast radius across all 1.0 apps.** Only **two** call sites both queue and attach: togadesk's `sendEmail()`, and `worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php` (lines 44 + 82, bare-string form — same bug today, fixed by the same change). worker's other 16 `setQueueForSending` sites pass `false`; togaview's two queueing sites (`mvc/signup/post.php`, `mvc/reset_password_success/post.php`) have no attachments; `tools` and `walmarttechservices` never attach.
|
|
32
|
+
- **Confirmed `Common.EmailsQueued.serializedEmailObject` is LONGTEXT** (developer checked) — no `dbchanges` migration needed.
|
|
33
|
+
- **Knowledge captured and PUSHED to `_main`** — 5 docs, index regenerated (392 docs / 30 repos), mirrored to `C:\WWW\.claude\knowledge`.
|
|
34
|
+
|
|
35
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
36
|
+
|
|
37
|
+
- **Testing this on alpha, beta, or a local laptop. It produces a FALSE PASS.** `App_Email::setQueueForSending()` (`library/app/email.php:161`) is gated on `!App_Registry::inTestMode()`:
|
|
38
|
+
`$this->queueForSending = (($bool && !App_Registry::inTestMode()) ? … : false);`
|
|
39
|
+
`inTestMode()` is `config['internal']['test_mode'] > 0`. Enumerated **every** togadesk config: **prod = 0; alpha = 1; beta = 1; and all sixteen `config.dev-*.ini` = 1** (bhavana-laptop, chadwimberly-mactop, davidfranks-laptop, hkurupati, jeffcardinal-alien, jeffcardinal-laptop, jeffcardinal-work, markhammontree-laptop, milindprabhakar-laptop, mkelly, nidhi-laptop, nkeshavamurthy-laptop, nmvenkatesha-laptop, pinalsoni-work, snaredla-laptop, tannercox-laptop). Outside prod the email is sent **inline**, PHPMailer reads the file off local disk, the attachment works, and `embedAttachmentsForQueue()` is **never called**. This is almost certainly why TRUE-75813 and TRUE-79924 were verified and closed while prod stayed broken.
|
|
40
|
+
- **Fixing the upload path inside togadesk.** Three prior attempts did this — TRUE-75813 (`2700c28f`, `d01a21a3`), TRUE-79924 (`7c19371a`), and `4f3789a7` ("Changing the directory path", which corrected `UPLOAD_BASE_DIR` from `__DIR__.'/../uploads'` to `__DIR__.'/../../uploads'`). The path resolves correctly and `desk/uploads` exists. **The path was never the problem.** Do not re-open this line of investigation.
|
|
41
|
+
- **Setting `test_mode = 0` on alpha to force the queue path — rejected, do not do this.** `test_mode` also drives App_Email's redirect to `config['email']['dev_email_to']`. Turning it off means **real email to real recipients** sitting in alpha's ticket data. The safe bypass is assigning the **public** property `$email->queueForSending = true` directly, which skips the setter's gate while leaving test mode on.
|
|
42
|
+
- **Reproducing the bug locally even with queueing forced on.** Locally the queueing app and the drain share one filesystem, so `desk/uploads/...` still resolves and both old and new code pass. The source file must be **deleted between queue and drain** for the test to mean anything.
|
|
43
|
+
- **Writing the PHP test file via a bash heredoc.** `cat > file <<'PHPEOF'` failed with `/usr/bin/bash: -c: line 163: unexpected EOF while looking for matching '`. Used the Write tool instead. Not worth re-attempting through bash.
|
|
44
|
+
|
|
45
|
+
## Not tried yet (candidates for next session)
|
|
46
|
+
|
|
47
|
+
- **Check `max_allowed_packet` on the prod Common cluster** — `SHOW VARIABLES LIKE 'max_allowed_packet'` via the TOGa Database Integration MCP, environment `legacy`. Rows can now carry ~9.8 MB of base64. MySQL 5.7 defaults to 4 MB, 8.0 to 64 MB. If below the budget the INSERT fails and surfaces as an `App_Query` exception on a live ticket reply — worse than a missing attachment.
|
|
48
|
+
- **Write the CLI verification harness** (offered, not written): build an `App_Email` with an attachment, set the public `$email->queueForSending = true` to bypass the test-mode gate, `send()`, **rename the source file**, then unserialize the `EmailsQueued` row and dump `getAttachments()`. Proves both the queue and drain halves, changes no config, sends no mail.
|
|
49
|
+
- **Confirm which git ref `setup_git_libraries.php` clones.** It is fetched from S3 (`asifiles.s3.us-west-2.amazonaws.com/system/setup_git_libraries.php`) by `togadesk/.ebextensions/020_setup_git_libraries.config` and cannot be read locally. If it clones `_main`, the fix needs a PR and merge before any deploy does anything.
|
|
50
|
+
- **Prod verification** — after a ticket reply with an attachment, confirm the fresh `EmailsQueued` row's decoded payload contains `"data"` not `"path"`. This proves the fix independently of delivery, and the queue drains every minute. Use the TOGA Technology Helpdesk client (178), not a real client ticket.
|
|
51
|
+
- **File two follow-up tickets** (both found this session, neither touched): the AIG arm never attaching, and the dead `NOT LIKE '%App_Email%'` filter.
|
|
52
|
+
|
|
53
|
+
## Current file state
|
|
54
|
+
|
|
55
|
+
| File | Status | Notes |
|
|
56
|
+
|------|--------|-------|
|
|
57
|
+
| `C:\WWW\library\app\email.php` | Modified, **committed** | `b3a9420d` on branch `TRUE-80952`, +98/−2. **Not pushed, not merged.** Adds `MAX_QUEUED_ATTACHMENT_BYTES`, `embedAttachmentsForQueue()`, `AddStringAttachment` handling, and the queue/inline fallback. |
|
|
58
|
+
| `C:\WWW\test\@Mark\TRUE-80952\test_queued_email_attachments.php` | New, uncommitted | 26 assertions, passing on 7.2, confirmed failing pre-fix. Test repo is not branched — per-dev folder only. |
|
|
59
|
+
| `knowledge/1.0/apps/library/features/email-queue-attachments.md` | Created, **pushed** | Full writeup: mechanism, failure, budget, warning guard, blast radius, deploy order, dead filter, open packet risk. |
|
|
60
|
+
| `knowledge/1.0/apps/togadesk/features/notifications.md` | Updated, **pushed** | Loss-at-queue + non-prod false-pass gotchas; AIG arm never attaching. Owners unioned `[ajean, mhammontree]`. |
|
|
61
|
+
| `knowledge/1.0/apps/test/features/static-no-db-regression-harness.md` | Updated, **pushed** | Reflection + deleted-source-file worked example. |
|
|
62
|
+
| `knowledge/1.0/standards/backend-php.md` | Updated (⚠ ELEVATED), **pushed** | TRUE-80952 worked example on the warning-exit section; new subsection "Test mode silently disables queued email — non-prod cannot validate a queue bug". |
|
|
63
|
+
| `C:\WWW\togadesk\**` | **Unchanged** | No togadesk code change was needed — it keeps calling `setQueueForSending(true)` and passing paths. |
|
|
64
|
+
| `C:\WWW\worker\**` | **Unchanged** | Drain cron needs no change; it just calls `->send()` on the unserialized object. |
|
|
65
|
+
|
|
66
|
+
## Decisions made
|
|
67
|
+
|
|
68
|
+
- **Option B (embed bytes in `library`) over Option A (togadesk skips the queue when attachments present) or Option C (move uploads to S3).** B fixes the class of bug for every 1.0 app and every queue consumer. A was ~2 lines but leaves all other queued-attachment paths broken and puts SMTP inside a web request. C is correct long-term but far beyond this ticket.
|
|
69
|
+
- **7 MB raw budget (`MAX_QUEUED_ATTACHMENT_BYTES = 7340032`) with an inline-send fallback.** Base64 inflates by 4/3, so ~9.8 MB encoded — just under the SES 10 MB message cap. Over budget, `send()` sends inline rather than queueing something that would be rejected downstream.
|
|
70
|
+
- **Outbound-email-disabled + oversized attachments: log, do not force a send.** There is no inline fallback when sending is disabled, so the email queues with its paths and still loses the attachment on drain. Unfixable, but it must not be silent — silence was the original complaint. Raised by php-reviewer.
|
|
71
|
+
- **Scoped `set_error_handler` (pattern 1 from the 1.0 back-end standard) rather than `App_Error::setThrowExceptionsEnabled(false)`.** Pattern 1 was chosen because the code needs catch-and-continue **per attachment**, not blanket suppression across the whole method. Raised by php-reviewer and it is the most important of its findings: an unguarded `file_get_contents` warning is fatal in 1.0 (`App_Error::handleError` → `handleException` → `exit`) and would kill the `send_emails` cron **mid-loop**, stranding every queued email behind it. `is_file()`/`is_readable()` alone cannot prevent it — TOCTOU.
|
|
72
|
+
- **Deploy `worker` before `togadesk`.** The intermediate states are asymmetric: worker-new + togadesk-old is harmless (togadesk queues paths, worker still handles paths), but togadesk-new + worker-old **drops the attachment** because old drain code looks for `['path']` and an embedded entry has none. Rollback is the inverse: togadesk first, drain the queue empty, then worker.
|
|
73
|
+
- **Did NOT touch `send_emails.php:34`'s dead `NOT LIKE '%App_Email%'` filter.** It excludes nothing today, but "fixing" it to work as apparently intended would **stop TOGa Desk mail from sending entirely**. Needs a decision from whoever wrote it.
|
|
74
|
+
- **Did NOT touch the AIG email arm.** Separate pre-existing bug, separate ticket.
|
|
75
|
+
- **Added `: bool` to the new private method** despite the file having no return types elsewhere — free, correct, and 7.2 supports it. Rejected the reviewer's framing that this was a violation; the coding-style rule covers public/protected only.
|
|
76
|
+
|
|
77
|
+
## Blockers
|
|
78
|
+
|
|
79
|
+
- **This cannot be validated outside production.** Every non-prod environment has `test_mode = 1`, which disables queueing entirely (see "What did NOT work"). Agreed with the developer that TRUE-80952 is a **production deployment and test**. The changes are in the deployment pipeline.
|
|
80
|
+
- **`library` `b3a9420d` is committed but not pushed or merged**, and it is unknown whether prod pulls `_main` or a branch — so it is unconfirmed whether the pipeline will actually carry this fix.
|
|
81
|
+
- **`max_allowed_packet` on the prod Common cluster is unchecked.** This is the one item that could make the deploy worse than the bug.
|
|
82
|
+
|
|
83
|
+
## Exact next step
|
|
84
|
+
|
|
85
|
+
> Run `SHOW VARIABLES LIKE 'max_allowed_packet'` against the prod **Common** cluster (TOGa Database Integration MCP, environment `legacy`) — get developer confirmation first, it is production. If the value is below ~10485760, lower `MAX_QUEUED_ATTACHMENT_BYTES` in `C:\WWW\library\app\email.php` to sit under it, amend/add a commit on branch `TRUE-80952`, and re-run `C:\WWW\test\@Mark\TRUE-80952\test_queued_email_attachments.php` (expect 26 passing) — all **before** the pipeline deploys.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
_Saved by /session-save on 2026-08-20_
|
package/package.json
CHANGED