Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceeb134c90
|
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<archunit.version>1.5.1</archunit.version>
|
||||
<jackson.version>2.21.7</jackson.version>
|
||||
<!-- optional email delivery; qbit-email from Maven Central -->
|
||||
<qbit-email.version>0.1.0-RC.1</qbit-email.version>
|
||||
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
|
||||
<!-- live evals call the real API; run them with -Dtest.excludedGroups= -Dgroups=live -->
|
||||
<test.excludedGroups>live</test.excludedGroups>
|
||||
</properties>
|
||||
@@ -37,7 +37,7 @@
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<version>2.1.1</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
|
||||
+11
-13
@@ -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}}")
|
||||
|
||||
+37
-21
@@ -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<QRecord> messages() throws Exception
|
||||
private List<EmailApiMessage> 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<QRecord> messages = messages();
|
||||
List<EmailApiMessage> 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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user