Keep template rendering and delivery ownership in email, with caller-scoped permissions and checked replay provenance.
Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
The readiness message was only "Email configuration incomplete" plus a code.
It now names each gap in plain words with the Email app path to fix it, and
notes that transactional email is unaffected by marketing-only gaps. The
failedChecks detail is unchanged; howToFix carries the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Email health tile showed the HealthStatus name (UP/DEGRADED/DOWN/UNKNOWN). It now shows Healthy / Degraded / Down, and "Not checked yet" when no
check has reported, with the check cadence in the context line. The drill to health_check is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The producer read a literal version, so released artifacts reported a stale
or -SNAPSHOT version. qbit-email.properties (version=${project.version}) is
Maven-filtered and the producer loads it; tests assert producer == bundle ==
artifact version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The qbit metadata version is hardcoded (0.1.0), so it cannot be compared with
the filtered bundle version. Check that the bundle resolved a real
x.y.z[-SNAPSHOT|-RC.n] version instead of a literal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The producer test pinned the bundle version to 0.1.0-SNAPSHOT, so the
0.1.0-RC.7 release build failed (bundle and qbit both 0.1.0-RC.6 locally).
Compare the bundle with the produced qbit's version instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On release branches the bundle's version and the producer's differed, so the
bundle test that asserts they match failed, and released bundles recorded
SNAPSHOT as their provenance. help/*.yaml is now filtered:
version: ${project.version}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host-neutral help (deployment-specific facts removed), registered with
HelpBundles, gated by HelpBundleAssertions. QQQ 4.1.0-RC.7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps develop's liquibase-core test dependency next to the Postgres
Testcontainers dependencies, and both changelog entries. H2 tests load
src/test/resources/email-h2.sql (FKs in H2 syntax), never the Postgres DDL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds email-defaults.changelog.sql (non-default neutral brand, only when no brand
exists; tested through real Liquibase), help for the app and every table, a
Setup card on the home, guardrail tests and docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
email_template.ownerQbit was stored but never read. Messages sent from a
template now name its owner qbit as senderQbit unless the request names one,
the built-in secure-message template sets it, and the field is read-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Built on QQQ 4.1.0-RC.7, which has the durable jobs side-effects outbox.
- EmailJobEffectTarget implements core's EffectTarget.
- Delivery hands the email to qbit-email's own outbox with the effect's
idempotency key, so it lands exactly once per key, with the fixed
Message-ID. The result is SENT with the message id, or FAILED when
refused.
- It runs as the job owner (JobSessions.forJob). If the owner can no longer
send, the result is FAILED, never UNKNOWN.
- Reconcile looks the message up by key.
- Email effects through it are IDEMPOTENT_EXTERNAL.
- EmailQBitProducer registers it as target "email" when durable jobs are
enabled (EmailJobEffectTarget.register).
- docs/DURABLE-JOBS.md and the README describe declaring and emitting email
effects.
verify: 301 tests (14 new), checkstyle clean, coverage 95.4% / 100%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Declare a named foreign key for every join and table possible-value field
between the ten email tables, plus suppression.source_message_id. Owned
rows cascade, providers/brands/templates with history are RESTRICT, and
optional pointers (template versions, sender address, suppression sources)
are SET NULL so message retention keeps working. The provider delete check
also covers attempts, events and inbox rows. The upgrade script quarantines
orphans before adding the keys; a Testcontainers guardrail test fails when
a new relation has no key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmailApiReadiness carries failedChecks (EmailApiReadinessCheck) next to
the plain reasons, so callers such as qbit-marketing can tell a missing
unsubscribe key from a missing provider without parsing text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves qqq-orb@0.6.8 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmailApi.sendOnce(request, effectKey) with EmailApiEffectOutcome and
EmailApiEffectResult, lookups by idempotency key and RFC Message-ID, and
rfcMessageId on EmailApiMessage, so other qbits have one public surface.
EmailEffects and its types stay as internal, deprecated delegates; the
durable-jobs guide, README and DESIGN 2.5 point at EmailApi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PhiCanaryEgressTest checks that no five-character window of the canary
appears in any output. The canary's random part was a hex UUID, so a window
could match a SHA-256 body hash or a Message-ID by chance (CI hit "ebf93" in
a body hash). The random part is now mapped from hex to the letters g-v, so a
match can only mean the canary really leaked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
com.kingsrook.qbits.email.api is the stable contract for other qbits:
EmailApi (isInstalled, send, ensureTemplate, message lookups by id and
idempotency-key prefix with metadata only, sender addresses, default
provider, readiness per category) with its own request, result,
template, message and readiness types. A reflection test keeps internal
types out of every public signature. The README and DESIGN 2.5 document
the two supported routes: core SendMessageAction with the qbitEmail
provider for plain mail, and the api package for the rest; internal
packages are not API.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For the durable jobs outbox (QRun-IO/qqq#1191, spec section 2.13):
- Each queued message stores one RFC 5322 Message-ID (email_message.rfc_message_id,
unique), derived from the idempotency key (else a stable per-message seed),
and reuses it on every attempt. SMTP, Postmark, SendGrid and Mailgun send it.
SES receives it in the raw MIME but replaces it; this is documented, and
X-QQQ-Email-Id stays on every transport.
- EmailEffects.send(request, effectKey) hands an email to qbit-email's outbox
exactly once per key: QUEUED_OR_DUPLICATE, or REFUSED (suppressed or
invalid; never retried).
- findByIdempotencyKey / findByRfcMessageId look an email up for a reconcile.
- docs/DURABLE-JOBS.md has the outcome mapping and the binding class to add
when QQQ RC.7 ships (EmailJobEffectTarget implements core's EffectTarget;
no SNAPSHOT dependency now).
- Postgres upgrade SQL for existing installs; checkstyle now also covers test
sources (the repo standard), with the existing violations fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>