From ceeb134c9020a031e00e2b829e994476d4371917 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 14 Oct 2026 10:20:42 -0500 Subject: [PATCH] refactor(email): use qbit-email's public api package; no G1 allow-list Moves the optional email integration from qbit-email internals to com.kingsrook.qbits.email.api (qbit-email 0.1.0-RC.2) with identical behaviour, and moves to qbit-build-parent 2.1.1, whose default G1 allow-lists include qbit-email's API (QRun-IO/qbit-bom#12), so the per-repo allow-lists are gone. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + README.md | 2 +- pom.xml | 4 +- .../qbits/assistant/email/AssistantEmail.java | 2 +- .../assistant/email/AssistantEmailSender.java | 24 ++++---- .../assistant/email/AssistantEmailTest.java | 58 ++++++++++++------- 6 files changed, 53 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d354ed..65400dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Changed +- Switch scheduled-result email delivery to qbit-email 0.1.0-RC.2's published `com.kingsrook.qbits.email.api` package, preserving templates, categories, idempotency keys, linked records and status mapping. - Add optional qbit-email 0.1.0-RC.1 delivery of a scheduled task's result (`AssistantEmail.sendScheduledResult`): template `assistant_scheduled_result`, link-only secure message for results above PUBLIC using core's `DataClassification`, owner-recipient hook, opt-in and `emailExternalApproved` gates. Scheduled-task wiring remains a separate integration step. ## Unreleased - Provider API extraction (#30) diff --git a/README.md b/README.md index 94ec3a4..47d7136 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,7 @@ Apache 2.0. When the optional [qbit-email](https://github.com/QRun-IO/qbit-email) is on the classpath **and** produced in the instance, a finished scheduled-task run can be emailed to its owner. Without it nothing changes: `AssistantEmail.sendScheduledResult` returns `NOT_AVAILABLE`. The dependency is `optional`; every reference to it is in `AssistantEmailSender`. -This integration uses qbit-email **0.1.0-RC.1**. Scheduled tasks (P-g) are available, but the runner does not yet wire email delivery. Hosts can call `AssistantEmail.sendScheduledResult(qInstance, config, new ScheduledResultNotice(...))` after a run, supplying its owner, classification, absolute run link and explicit opt-in as `emailResult`. Persisting a task-level email opt-in and wiring the runner remain a separate integration step. +This integration uses qbit-email **0.1.0-RC.2** through its published `com.kingsrook.qbits.email.api` package. Scheduled tasks (P-g) are available, but the runner does not yet wire email delivery. Hosts can call `AssistantEmail.sendScheduledResult(qInstance, config, new ScheduledResultNotice(...))` after a run, supplying its owner, classification, absolute run link and explicit opt-in as `emailResult`. Persisting a task-level email opt-in and wiring the runner remain a separate integration step. Gates, in order (each returns a status and queues nothing): 1. the task opted in (`NOT_REQUESTED`); diff --git a/pom.xml b/pom.xml index 759d533..f9380bb 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ 1.5.1 2.21.7 - 0.1.0-RC.1 + 0.1.0-RC.2 live @@ -37,7 +37,7 @@ com.kingsrook qbit-build-parent - 2.0.0 + 2.1.1 com.kingsrook.qbits diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmail.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmail.java index 1c9ec13..f33cdfd 100644 --- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmail.java +++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmail.java @@ -55,7 +55,7 @@ public final class AssistantEmail { try { - Class.forName("com.kingsrook.qbits.email.outbox.EmailService"); + Class.forName("com.kingsrook.qbits.email.api.EmailApi"); } catch(ClassNotFoundException | LinkageError e) { diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmailSender.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmailSender.java index 181c9d6..51b6e27 100644 --- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmailSender.java +++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/email/AssistantEmailSender.java @@ -24,13 +24,11 @@ package com.kingsrook.qbits.assistant.email; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qbits.email.EmailRuntime; -import com.kingsrook.qbits.email.outbox.EmailCategory; -import com.kingsrook.qbits.email.outbox.EmailRequest; -import com.kingsrook.qbits.email.outbox.EmailSendResult; -import com.kingsrook.qbits.email.outbox.EmailService; -import com.kingsrook.qbits.email.template.EmailTemplateDefinition; -import com.kingsrook.qbits.email.template.EmailTemplates; +import com.kingsrook.qbits.email.api.EmailApi; +import com.kingsrook.qbits.email.api.EmailApiCategory; +import com.kingsrook.qbits.email.api.EmailApiRequest; +import com.kingsrook.qbits.email.api.EmailApiResult; +import com.kingsrook.qbits.email.api.EmailApiTemplate; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -66,7 +64,7 @@ final class AssistantEmailSender *******************************************************************************/ static boolean isInstalled(QInstance qInstance) { - return (EmailRuntime.config(qInstance) != null); + return (EmailApi.isInstalled(qInstance)); } @@ -79,7 +77,7 @@ final class AssistantEmailSender { boolean linkOnly = notice.classification() != null && notice.classification().isAtLeast(DataClassification.CONFIDENTIAL); - EmailRequest request = new EmailRequest().withTo(recipient) + EmailApiRequest request = new EmailApiRequest().withTo(recipient) .withIdempotencyKey("assistant:scheduled:" + notice.runId()) .withLinkedRecord(notice.runTable(), notice.runId()) .withSender(SENDER_QBIT, SENDER_PROCESS); @@ -100,7 +98,7 @@ final class AssistantEmailSender } request.withVariables(variables); - return (map(EmailService.send(request))); + return (map(EmailApi.send(request))); } @@ -123,7 +121,7 @@ final class AssistantEmailSender /******************************************************************************* ** Outbox result to the qbit's own vocabulary. *******************************************************************************/ - private static ScheduledEmailStatus map(EmailSendResult result) + private static ScheduledEmailStatus map(EmailApiResult result) { return (switch(result.status()) { @@ -141,10 +139,10 @@ final class AssistantEmailSender *******************************************************************************/ static void ensureTemplate() throws QException { - EmailTemplates.ensure(new EmailTemplateDefinition() + EmailApi.ensureTemplate(new EmailApiTemplate() .withName(TEMPLATE_NAME) .withLabel("Assistant scheduled task result") - .withCategory(EmailCategory.NOTIFICATION) + .withCategory(EmailApiCategory.NOTIFICATION) .withKind("MARKDOWN") .withOwnerQbit(SENDER_QBIT) .withSubject("Assistant task result: {{taskName}}") diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/email/AssistantEmailTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/email/AssistantEmailTest.java index 3a498c7..a81fda1 100644 --- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/email/AssistantEmailTest.java +++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/email/AssistantEmailTest.java @@ -26,14 +26,15 @@ import java.util.List; import com.kingsrook.qbits.assistant.AssistantQBitConfig; import com.kingsrook.qbits.email.EmailQBitConfig; import com.kingsrook.qbits.email.EmailQBitProducer; -import com.kingsrook.qbits.email.metadata.EmailTables; -import com.kingsrook.qbits.email.permissions.EmailPermissions; -import com.kingsrook.qbits.email.support.SystemWrites; +import com.kingsrook.qbits.email.api.EmailApi; +import com.kingsrook.qbits.email.api.EmailApiCategory; +import com.kingsrook.qbits.email.api.EmailApiMessage; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -101,8 +102,8 @@ class AssistantEmailTest } if(withEmail) { - QContext.init(qInstance, new QSession().withPermissions(EmailPermissions.ALL)); - new InsertAction().execute(new InsertInput(EmailTables.PROVIDER).withRecords(List.of(new QRecord().withValue("name", "relay").withValue("label", "Relay") + QContext.init(qInstance, new QSession().withPermissions(List.of("email.view", "email.send", "email.manage", "email.admin", "email.hasAccess"))); + new InsertAction().execute(new InsertInput("email_provider").withRecords(List.of(new QRecord().withValue("name", "relay").withValue("label", "Relay") .withValue("kind", "SMTP").withValue("host", "127.0.0.1").withValue("port", 25).withValue("security", "NONE") .withValue("fromAddress", "sender@example.com").withValue("enabled", true).withValue("isDefault", true)))); } @@ -125,9 +126,20 @@ class AssistantEmailTest /******************************************************************************* ** *******************************************************************************/ - private List messages() throws Exception + private List messages() throws Exception { - return (SystemWrites.query(EmailTables.MESSAGE, new QQueryFilter())); + return (EmailApi.findMessagesByIdempotencyKeyPrefix("assistant:scheduled:", 100)); + } + + + + /******************************************************************************* + ** Inspect the test outbox through core as the authorized email administrator. + ** The public email API deliberately excludes bodies and recipient addresses. + *******************************************************************************/ + private QRecord storedMessage() throws Exception + { + return (new QueryAction().execute(new QueryInput("email_message")).getRecords().getFirst()); } @@ -142,18 +154,20 @@ class AssistantEmailTest assertThat(AssistantEmail.isAvailable(instance)).isTrue(); assertThat(AssistantEmail.sendScheduledResult(instance, config, notice(7, "Three items need attention.", null, true))).isEqualTo(ScheduledEmailStatus.QUEUED); - List messages = messages(); + List messages = messages(); assertThat(messages).hasSize(1); - QRecord message = messages.get(0); - assertThat(message.getValueString("category")).isEqualTo("NOTIFICATION"); + EmailApiMessage metadata = messages.getFirst(); + assertThat(metadata.category()).isEqualTo(EmailApiCategory.NOTIFICATION); + assertThat(metadata.idempotencyKey()).isEqualTo("assistant:scheduled:7"); + assertThat(metadata.linkedTableName()).isEqualTo(RUN_TABLE); + assertThat(metadata.linkedRecordId()).isEqualTo("7"); + assertThat(metadata.templateName()).isEqualTo("assistant_scheduled_result"); + QRecord message = storedMessage(); assertThat(message.getValueString("toAddresses")).isEqualTo("owner@example.com"); - assertThat(message.getValueString("idempotencyKey")).isEqualTo("assistant:scheduled:7"); - assertThat(message.getValueString("linkedTableName")).isEqualTo(RUN_TABLE); - assertThat(message.getValueString("linkedRecordId")).isEqualTo("7"); assertThat(message.getValueString("senderQbit")).isEqualTo("assistant"); assertThat(message.getValueString("subject")).contains("Weekly review"); assertThat(message.getValueString("htmlBody")).contains("Three items need attention.").contains("https://app.example.com/app/assistant_scheduled_run/7"); - QRecord template = SystemWrites.query(EmailTables.TEMPLATE, new QQueryFilter()).stream().filter(t -> "assistant_scheduled_result".equals(t.getValueString("name"))).findFirst().orElseThrow(); + QRecord template = new QueryAction().execute(new QueryInput("email_template")).getRecords().stream().filter(t -> "assistant_scheduled_result".equals(t.getValueString("name"))).findFirst().orElseThrow(); assertThat(template.getValueString("category")).isEqualTo("NOTIFICATION"); assertThat(template.getValueString("ownerQbit")).isEqualTo("assistant"); assertThat(message.getValueInteger("templateId")).isEqualTo(template.getValueInteger("id")); @@ -173,13 +187,15 @@ class AssistantEmailTest { assertThat(AssistantEmail.sendScheduledResult(instance, config, notice(8, "Patient Jane Doe has diabetes", DataClassification.PHI, true))).isEqualTo(ScheduledEmailStatus.QUEUED); - QRecord message = messages().get(0); - assertThat(message.getValueString("category")).isEqualTo("SECURITY"); + EmailApiMessage metadata = messages().getFirst(); + assertThat(metadata.category()).isEqualTo(EmailApiCategory.SECURITY); + assertThat(metadata.idempotencyKey()).isEqualTo("assistant:scheduled:8"); + assertThat(metadata.linkedRecordId()).isEqualTo("8"); + assertThat(metadata.templateName()).isEqualTo("email_secure_message"); + QRecord message = storedMessage(); assertThat(message.getValueString("htmlBody")).contains("https://app.example.com/app/assistant_scheduled_run/8").doesNotContain("Jane Doe").doesNotContain("Weekly review"); assertThat(message.getValueString("textBody")).doesNotContain("Jane Doe"); assertThat(message.getValueString("subject")).doesNotContain("Weekly review"); - assertThat(message.getValueString("idempotencyKey")).isEqualTo("assistant:scheduled:8"); - assertThat(message.getValueString("linkedRecordId")).isEqualTo("8"); } @@ -201,7 +217,7 @@ class AssistantEmailTest config.withEmailRecipientResolver(id -> "user-17".equals(id) ? " user17@example.com " : null); assertThat(AssistantEmail.sendScheduledResult(instance, config, noAddress)).isEqualTo(ScheduledEmailStatus.QUEUED); - assertThat(messages().get(0).getValueString("toAddresses")).isEqualTo("user17@example.com"); + assertThat(storedMessage().getValueString("toAddresses")).isEqualTo("user17@example.com"); } @@ -230,7 +246,7 @@ class AssistantEmailTest assertThat(AssistantEmailSender.summary("x".repeat(1000))).hasSize(280).endsWith("..."); AssistantEmail.sendScheduledResult(instance, config, notice(5, "y".repeat(2000), null, true)); - assertThat(messages().get(0).getValueString("htmlBody")).doesNotContain("y".repeat(281)); + assertThat(storedMessage().getValueString("htmlBody")).doesNotContain("y".repeat(281)); } -- 2.54.0