Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5adcec567
|
No files matched your search
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
|
||||
@@ -6,6 +6,8 @@ All notable changes are documented here, following [Keep a Changelog](https://ke
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch the optional qbit-email integration to 0.1.0-RC.2 and its published `com.kingsrook.qbits.email.api` package, preserving templates, categories, idempotency keys, linked records and enqueue-status handling. Narrow the email package allowance to the public API.
|
||||
|
||||
- Email through qbit-email 0.1.0-RC.1: new optional dependency and `EmailCollaborationNotifier` (templates `collaboration_mention`, `collaboration_share`, `collaboration_task_reminder`), the default notifier when qbit-email is produced in the instance and no `notifier` is configured; `NoOpNotifier` otherwise. One NOTIFICATION message per recipient, no comment bodies, record labels only when field access allows. New `recipientEmailResolver` config hook; `CollaborationNotification` gains optional `sourceId` and `actorUserId`.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -280,9 +280,9 @@ UI API endpoints are described below. Runtime feature enablement, mirrored WRITE
|
||||
|
||||
## Email through qbit-email
|
||||
|
||||
qbit-email 0.1.0-RC.1 is an optional dependency (`<optional>true</optional>`). When it is on the classpath, produced in the same instance, and `notifier` is not configured, mentions, shares and task notifications are emailed by `EmailCollaborationNotifier` (package `email`, the only class that references qbit-email). Without qbit-email nothing changes: `NoOpNotifier` sends nothing. A configured `notifier` always wins.
|
||||
qbit-email 0.1.0-RC.2 is an optional dependency (`<optional>true</optional>`). The integration uses its published `com.kingsrook.qbits.email.api` package for installation detection, template registration and sending. When it is on the classpath, produced in the same instance, and `notifier` is not configured, mentions, shares and task notifications are emailed by `EmailCollaborationNotifier`. Without qbit-email nothing changes: `NoOpNotifier` sends nothing. A configured `notifier` always wins.
|
||||
|
||||
The parent 2.1.0 guardrail allowlists explicitly include the qbit-email artifact and API package, preserving the parent's existing allowances. The package allowance includes the root because `EmailRuntime.config` lives there; keep all direct email references inside the adapter. Strict UI API route checks are enabled.
|
||||
The guardrail allowlist properties retain the object-storage API and `*-api` artifact allowances and add only the qbit-email artifact and `com.kingsrook.qbits.email.api` package. The current parent 2.0.0 does not execute guardrails; these properties are ready for a parent that does. Tests retain `EmailQBitProducer`/`EmailQBitConfig` only to assemble the host instance; G1 checks production classes and sources.
|
||||
|
||||
- Templates (NOTIFICATION, Markdown, owner `qbit-collaboration`), registered on first send and never overwritten: `collaboration_mention`, `collaboration_share`, `collaboration_task_reminder` (task assignment, reminder and overdue). Follow digests are not emailed.
|
||||
- One message per recipient, idempotency key `collaboration:<mention|share|task>:<event id>:<recipient user id>`, linked to the parent record, sender `qbit-collaboration`. A repeat is a duplicate. The event id is the mention, share, task or assignment row id (task reminders add the due field); `CollaborationNotification` carries it as `sourceId`, with the acting user as `actorUserId`.
|
||||
|
||||
@@ -23,17 +23,13 @@
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
<!-- qbit-email from Maven Central -->
|
||||
<qbit-email.version>0.1.0-RC.1</qbit-email.version>
|
||||
<!-- Optional email integration: only EmailCollaborationNotifier links to its API. -->
|
||||
<!-- EmailRuntime is in the root package; retain the parent's existing allowances. -->
|
||||
<qbit.guardrails.allowedQbitArtifacts>com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:*-api,com.kingsrook.qbits:qbit-email</qbit.guardrails.allowedQbitArtifacts>
|
||||
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email</qbit.guardrails.allowedApiPackages>
|
||||
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
|
||||
</properties>
|
||||
|
||||
<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>
|
||||
|
||||
+18
-19
@@ -30,13 +30,12 @@ import com.kingsrook.qbits.collaboration.notify.CollaborationNotification;
|
||||
import com.kingsrook.qbits.collaboration.notify.CollaborationNotifier;
|
||||
import com.kingsrook.qbits.collaboration.people.PeopleService;
|
||||
import com.kingsrook.qbits.collaboration.security.CollaborationRecordAccessChecker;
|
||||
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.EmailApiStatus;
|
||||
import com.kingsrook.qbits.email.api.EmailApiTemplate;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
@@ -78,7 +77,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
*******************************************************************************/
|
||||
public static boolean isAvailable(QInstance qInstance)
|
||||
{
|
||||
return (EmailRuntime.config(qInstance) != null);
|
||||
return (EmailApi.isInstalled(qInstance));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +90,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
@Override
|
||||
public void notify(CollaborationNotification n)
|
||||
{
|
||||
EmailTemplateDefinition template = templateFor(n.category());
|
||||
EmailApiTemplate template = templateFor(n.category());
|
||||
if(template == null)
|
||||
{
|
||||
return;
|
||||
@@ -104,9 +103,9 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
LOG.info("Collaboration email skipped: no address for recipient", logPair("userId", n.recipientUserId()));
|
||||
return;
|
||||
}
|
||||
EmailTemplates.ensure(template);
|
||||
EmailApi.ensureTemplate(template);
|
||||
String eventId = n.sourceId() != null ? n.sourceId() : n.sourceRecordId();
|
||||
EmailRequest request = new EmailRequest().withTemplateName(template.getName()).withCategory(EmailCategory.NOTIFICATION).withTo(address)
|
||||
EmailApiRequest request = new EmailApiRequest().withTemplateName(template.getName()).withCategory(EmailApiCategory.NOTIFICATION).withTo(address)
|
||||
.withIdempotencyKey("collaboration:" + kind(n.category()) + ":" + eventId + ":" + n.recipientUserId())
|
||||
.withSender(OWNER_QBIT, "notify-" + kind(n.category()))
|
||||
.withVariable("actorName", actorName(n.actorUserId())).withVariable("recordLabel", recordLabel(n));
|
||||
@@ -118,8 +117,8 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
{
|
||||
request.withVariable("link", n.link());
|
||||
}
|
||||
EmailSendResult result = EmailService.send(request);
|
||||
if(result.status() != EmailSendResult.Status.QUEUED)
|
||||
EmailApiResult result = EmailApi.send(request);
|
||||
if(result.status() != EmailApiStatus.QUEUED)
|
||||
{
|
||||
LOG.info("Collaboration email not queued", logPair("userId", n.recipientUserId()), logPair("status", result.status()),
|
||||
logPair("messageId", result.messageId()));
|
||||
@@ -193,7 +192,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
/*******************************************************************************
|
||||
** The shipped template for a category, or null when none exists.
|
||||
*******************************************************************************/
|
||||
static EmailTemplateDefinition templateFor(CollaborationNotification.Category category)
|
||||
static EmailApiTemplate templateFor(CollaborationNotification.Category category)
|
||||
{
|
||||
return (switch(category)
|
||||
{
|
||||
@@ -212,9 +211,9 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
** the template needs no PHI approval because labels are passed only when the
|
||||
** record label fields are fully accessible.
|
||||
*******************************************************************************/
|
||||
private static EmailTemplateDefinition definition(String name, String label, String subject, String body)
|
||||
private static EmailApiTemplate definition(String name, String label, String subject, String body)
|
||||
{
|
||||
return (new EmailTemplateDefinition().withName(name).withLabel(label).withCategory(EmailCategory.NOTIFICATION).withKind("MARKDOWN")
|
||||
return (new EmailApiTemplate().withName(name).withLabel(label).withCategory(EmailApiCategory.NOTIFICATION).withKind("MARKDOWN")
|
||||
.withOwnerQbit(OWNER_QBIT).withSubject(subject).withBody(body)
|
||||
.withVariable("actorName", "Person who acted", "STRING", true, "CONFIDENTIAL")
|
||||
.withVariable("recordLabel", "Record label", "STRING", true, "CONFIDENTIAL")
|
||||
@@ -227,7 +226,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
/*******************************************************************************
|
||||
** Someone mentioned the recipient.
|
||||
*******************************************************************************/
|
||||
static EmailTemplateDefinition mentionTemplate()
|
||||
static EmailApiTemplate mentionTemplate()
|
||||
{
|
||||
return (definition(MENTION_TEMPLATE, "Collaboration mention", "{{actorName}} mentioned you on {{recordLabel}}",
|
||||
"**{{actorName}}** mentioned you on {{recordLabel}}.\n\n" + OPEN_LINK));
|
||||
@@ -238,7 +237,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
/*******************************************************************************
|
||||
** Someone shared a record with the recipient.
|
||||
*******************************************************************************/
|
||||
static EmailTemplateDefinition shareTemplate()
|
||||
static EmailApiTemplate shareTemplate()
|
||||
{
|
||||
return (definition(SHARE_TEMPLATE, "Collaboration share", "{{actorName}} shared {{recordLabel}} with you",
|
||||
"**{{actorName}}** shared {{recordLabel}} with you.\n\n" + OPEN_LINK));
|
||||
@@ -249,7 +248,7 @@ public final class EmailCollaborationNotifier implements CollaborationNotifier
|
||||
/*******************************************************************************
|
||||
** A task assigned to the recipient was assigned, is due, or is overdue.
|
||||
*******************************************************************************/
|
||||
static EmailTemplateDefinition taskReminderTemplate()
|
||||
static EmailApiTemplate taskReminderTemplate()
|
||||
{
|
||||
return (definition(TASK_REMINDER_TEMPLATE, "Collaboration task reminder", "A task on {{recordLabel}} needs your attention",
|
||||
"A task assigned to you on {{recordLabel}} needs your attention.\n\n" + OPEN_LINK));
|
||||
|
||||
@@ -149,7 +149,7 @@ public final class Notifications
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("com.kingsrook.qbits.email.outbox.EmailService");
|
||||
Class.forName("com.kingsrook.qbits.email.api.EmailApi");
|
||||
return (EmailCollaborationNotifier.isAvailable(QContext.getQInstance()));
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
|
||||
+28
-12
@@ -32,16 +32,20 @@ import com.kingsrook.qbits.collaboration.model.CollaborationTables;
|
||||
import com.kingsrook.qbits.collaboration.notify.CollaborationNotification;
|
||||
import com.kingsrook.qbits.collaboration.notify.NoOpNotifier;
|
||||
import com.kingsrook.qbits.collaboration.notify.Notifications;
|
||||
import com.kingsrook.qqq.backend.core.actions.collaboration.UserSessionProviderInterface;
|
||||
import com.kingsrook.qbits.collaboration.sharing.SharingService;
|
||||
import com.kingsrook.qbits.collaboration.tasks.TasksService;
|
||||
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.collaboration.UserSessionProviderInterface;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
@@ -96,25 +100,30 @@ class EmailCollaborationNotifierTest extends PersonalServiceFixture
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the email qbit in the test instance.
|
||||
** Host fixture wiring only: G1 scans production classes and sources. Create
|
||||
** the provider through standard QQQ actions with the documented permissions.
|
||||
*******************************************************************************/
|
||||
private void withEmail() throws Exception
|
||||
{
|
||||
new EmailQBitProducer().withQBitConfig(new EmailQBitConfig().withBackendName("memory").withPublicBaseUrl("https://app.example.org")
|
||||
.withSecretResolver(name -> "k")).produce(instance);
|
||||
QContext.getQSession().withPermissions(EmailPermissions.ALL);
|
||||
SystemWrites.insertOne(EmailTables.PROVIDER, new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP").withValue("host", "127.0.0.1")
|
||||
.withValue("port", 2525).withValue("security", "NONE").withValue("fromAddress", "sender@example.com").withValue("enabled", true).withValue("isDefault", true));
|
||||
QContext.getQSession().withPermissions("email.view", "email.send", "email.manage", "email.admin", "email.hasAccess");
|
||||
QRecord provider = new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP").withValue("host", "127.0.0.1")
|
||||
.withValue("port", 2525).withValue("security", "NONE").withValue("fromAddress", "sender@example.com").withValue("enabled", true).withValue("isDefault", true);
|
||||
assertThat(new InsertAction().execute(new InsertInput("email_provider").withRecords(List.of(provider))).getRecords())
|
||||
.singleElement().satisfies(row -> assertThat(row.getErrors()).isNullOrEmpty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** All outbox messages.
|
||||
** Inspect rendered test content through the standard, permission-checked
|
||||
** table action. The public API deliberately exposes only message metadata.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> messages() throws Exception
|
||||
{
|
||||
return (SystemWrites.query(EmailTables.MESSAGE, new QQueryFilter()));
|
||||
return (new QueryAction().execute(new QueryInput("email_message").withFilter(new QQueryFilter())
|
||||
.withShouldFetchHeavyFields(true).withShouldOmitHiddenFields(false)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +163,14 @@ class EmailCollaborationNotifierTest extends PersonalServiceFixture
|
||||
assertThat(message.getValueString("subject") + message.getValueString("textBody") + message.getValueString("htmlBody")).doesNotContain("secret comment body");
|
||||
assertThat(message.getValueString("htmlBody")).contains("https://app.example.org/app/document/1");
|
||||
});
|
||||
assertThat(SystemWrites.query(EmailTables.TEMPLATE, new QQueryFilter())).extracting(t -> t.getValueString("name")).contains("collaboration_mention");
|
||||
assertThat(EmailApi.getMessageByIdempotencyKey("collaboration:mention:7:bob")).hasValueSatisfying(message ->
|
||||
{
|
||||
assertThat(message.status()).isEqualTo("QUEUED");
|
||||
assertThat(message.category()).isEqualTo(EmailApiCategory.NOTIFICATION);
|
||||
assertThat(message.templateName()).isEqualTo("collaboration_mention");
|
||||
assertThat(message.linkedTableName()).isEqualTo("document");
|
||||
assertThat(message.linkedRecordId()).isEqualTo("1");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +190,7 @@ class EmailCollaborationNotifierTest extends PersonalServiceFixture
|
||||
assertThat(messages()).extracting(m -> m.getValueString("idempotencyKey"))
|
||||
.containsExactlyInAnyOrder("collaboration:share:3:bob", "collaboration:share:3:eve", "collaboration:task:9:remindAt:bob");
|
||||
assertThat(messages()).allSatisfy(m -> assertThat(m.getValueString("toAddresses")).doesNotContain(","));
|
||||
assertThat(SystemWrites.query(EmailTables.TEMPLATE, new QQueryFilter())).extracting(t -> t.getValueString("name"))
|
||||
assertThat(EmailApi.findMessagesByIdempotencyKeyPrefix("collaboration:", 10)).extracting(EmailApiMessage::templateName)
|
||||
.contains("collaboration_share", "collaboration_task_reminder");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user