flowchart LR
O[Owner] --> L[Launcher and database preflight]
L --> U[32-bit HTA UI]
U --> C[Campaign and contact services]
U --> D[In-app dispatch timer]
C --> M[(Access MDB)]
D --> M
D --> E[Eligibility and pacing policy]
E --> A[SMTP adapter]
A --> P[SMTP server]
The HTA owns campaign editing, preview, approval, pause, reporting, and dispatch. Sending is possible only while the HTA is open and the owner has explicitly started or resumed a campaign. Closing the window starts no further sends. On reopening, all campaigns remain paused until the owner explicitly resumes them. The timer is only a wake-up mechanism; durable database state enforces pacing. A single-instance guard prevents two HTAs from dispatching at once. Keep business rules separate from UI DOM code so they can be tested with a fake clock and SMTP adapter.
The launcher reads DB_PATH, DB_PROVIDER, and DB_BITNESS from the INI, validates them, and starts the configured process architecture. On the current PC, the 32-bit host with Microsoft.ACE.OLEDB.12.0 passed the temporary .mdb create/write/read probe; 64-bit hosts failed. The deployment launcher must therefore run the 32-bit mshta.exe on this PC. It must open the configured .mdb using the configured provider from the same architecture that will host the HTA, read AppMeta.schemaVersion, and fail with a clear error if the provider, file, permissions, or schema is wrong. The temporary diagnostic is useful on another PC but is not a substitute for this startup check. Do not silently fall back to another provider or create a new production database when opening fails.
Delivery per eligible address.nextAllowedAttemptUtc, and pending deliveries. It attempts at most one recipient at a time. Recheck suppression immediately before contacting SMTP.Attempt as Started before the provider call. Record a definitive Accepted or Failed response afterward. If the outcome is unknown after a timeout or crash, mark it Uncertain on recovery and require manual reconciliation. SMTP acceptance does not prove inbox delivery.nextAllowedAttemptUtc = max(now + interval, retryBackoffUntil). Missed slots never accumulate. On app close or restart, no new send begins and no automatic resume occurs.A synchronous COM SMTP call may briefly block the HTA. The feasibility spike must measure that behavior, provide a timeout and visible sending state, and determine whether a small helper process is needed for responsiveness. Any helper must remain bound to the open HTA session and must stop accepting new work when it closes.
| Entity | Essential fields |
|---|---|
| AppMeta | schemaVersion, createdAt, migratedAt |
| Contact | id, emailOriginal, emailKey, businessName, sourceReference, acquiredAt, country, eligibilityBasis |
| Suppression | emailKey, reason, recordedAt, evidenceReference |
| TemplateVersion | id, templateName, version, htmlContent, contentHash, importedAt, validationStatus |
| TemplateImage | id, templateVersionId, originalRelativePath, contentType, contentHash, snapshotBytes, contentId |
| Campaign | id, name, senderKey, fromSnapshot, replyToSnapshot, postalAddressSnapshot, subject, templateVersionId, status, startAtUtc, approvedAt |
| Delivery | id, campaignId, contactId, emailKey, status, attemptCount, lastAttemptAt, providerMessageId, lastErrorCode |
| Attempt | id, deliveryId, startedAtUtc, endedAtUtc, outcome, providerRequestId, sanitizedError |
| DispatchState | senderKey, lastAttemptAtUtc, nextAllowedAttemptUtc |
| OperationalState | id, globalPause, lastOptOutReviewAtUtc, lastConfigHash |
| ProtectedSecret | id, purpose, dpapiCiphertext, createdAt, rotatedAt |
| AuditEvent | id, actor, action, entityId, timestampUtc, detail |
Use ADO parameterized commands for values and transactions for state transitions. Create unique indexes for Contact.emailKey, Suppression.emailKey, and (Delivery.campaignId, Delivery.emailKey). Index due deliveries and attempt history. Use one local MDB and one active HTA instance; do not put the database on a network share. Restrict file permissions to the operator. Back up the MDB, INI, and template sources together while the app is closed, then test restore. The MDB, lock files, backups, and live configuration are excluded from Git.
The owner selects a local .html file. Image references must be relative to that file's folder, such as images/postcard.jpg. On import, resolve each path within that folder, reject traversal and absolute, remote, or data URLs, validate PNG/JPEG/GIF content and size, and snapshot bytes into TemplateImage. The preview uses the copied assets in a restricted context so template scripts cannot run in the privileged HTA. The mail adapter replaces each image reference with a cid: URL and builds an HTML multipart/related message with matching inline image parts. Recipient HTML must contain no file:// or drive paths. General attachments are outside v1.
The SMTP adapter uses the built-in 32-bit Windows PowerShell/.NET Framework System.Net.Mail APIs. A local capability probe completed a certificate-validated STARTTLS 1.3 handshake, and a pickup-folder test produced HTML multipart/related MIME with a CID-linked image. The explicit one-message test uses SmtpClient.EnableSsl and the configured SMTP credentials. Password authentication and received-message rendering still require the owner to enter the password locally and inspect the test inbox. The queued campaign path will use the same MIME construction after template snapshot and dispatch controls are built.
The validated config.ini is the source of truth for system-wide settings described in config.example.ini. The MDB is the source of truth for campaign data and runtime state. The owner may edit INI values directly or through the HTA; the app validates changes before dispatch, records a configuration audit event, and never starts or resumes sending because of a config edit. Do not duplicate an editable setting in both places.
| INI setting group | Meaning | MDB counterpart |
|---|---|---|
SMTP_*, SMTP_SECRET_ID |
Endpoint, auth identity, required TLS/STARTTLS, from and reply addresses, internal test recipient, secret lookup | ProtectedSecret stores DPAPI ciphertext and rotation metadata. |
DB_PATH, DB_PROVIDER, DB_BITNESS |
MDB location, OLE DB provider, and launch architecture | AppMeta stores schema version. |
QUEUE_INTERVAL, SEND_MODE, DISPLAY_TIME_ZONE |
Account-wide minimum gap, allowed sending hours, display time | DispatchState stores pacing; OperationalState stores pause and review state. SEND_MODE=always means any hour while the HTA is open. |
TEMPLATE_ROOT, TEMPLATE_IMAGE_*, TEMPLATE_MESSAGE_MAX_BYTES |
Source folder, inline image mode, supported types, size limits | Template and image versions store approved snapshots. |
OPT_OUT_REVIEW_HOURS, BUSINESS_POSTAL_ADDRESS |
Review reminder and required footer for new campaigns | Review timestamp and campaign footer snapshot. |
SMTP_TLS=on means encryption is mandatory; SMTP_STARTTLS=on means the client must negotiate STARTTLS before authentication or message data. Port 587 is configurable. Invalid combinations, handshake failure, or invalid certificates block sending. QUEUE_INTERVAL=300 means at least 300 seconds between attempts from this account across all campaigns. On interval change, use max(persistedNextAllowedUtc, lastAttemptAtUtc + currentInterval) so a new value cannot shorten the reserved gap.
The HTA starts a local interactive x86 PowerShell setup action. Read-Host -AsSecureString accepts the password without writing it to a command line, file, or log; ConvertFrom-SecureString uses Windows DPAPI for the current user. Only its ciphertext is stored in ProtectedSecret. The sender reads it with ConvertTo-SecureString and passes the secure value to NetworkCredential. Save, load, and rotation passed a disposable-MDB test with synthetic passwords. A backup restored under another Windows identity requires password re-entry. Password setup is allowed while the HTA is closed because it does not send mail.
| Area | Decision to prove in Sprint 0 |
|---|---|
| SMTP | STARTTLS and local inline MIME pass. Verify actual password authentication, received image rendering, size caps, and marketing policy. |
| MDB | Choose migration and backup procedure; verify the real startup gate with the 32-bit HTA and configured file. |
| UI responsiveness | Measure synchronous send duration and choose a bounded helper if the HTA blocks too long. |
| DPAPI | Synthetic password save/load/rotation passes. Verify the owner's password works for a live internal test. |
Powered by TurnKey Linux.