Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b7a20a7d |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
|
||||
+1
-2
@@ -5,8 +5,7 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Changed
|
||||
- Switch the optional qbit-email integration to its published `com.kingsrook.qbits.email.api` package in `0.1.0-RC.2`, preserving templates, categories, idempotency keys, linked records and outcome mapping. Narrow the architecture allowance to that API package while retaining all guardrails.
|
||||
- Email goes through qbit-email when it is produced in the instance (optional dependency, `qbit-email.version` 0.1.0-RC.2): alert channel emails (template `alerts_alert`), notification emails (`alerts_notification`) and digests (`alerts_digest`), all NOTIFICATION, one recipient per message, with idempotency keys and the alert or notification as the linked record. `messagingProviderName` stays as the fallback when qbit-email is absent, and is not required when it is present.
|
||||
- Email goes through qbit-email when it is produced in the instance (optional dependency, `qbit-email.version` 0.1.0-RC.1): alert channel emails (template `alerts_alert`), notification emails (`alerts_notification`) and digests (`alerts_digest`), all NOTIFICATION, one recipient per message, with idempotency keys and the alert or notification as the linked record. `messagingProviderName` stays as the fallback when qbit-email is absent, and is not required when it is present.
|
||||
- Target released QQQ `4.1.0-RC.6`; verify normally with `./mvnw -B verify` (no version override).
|
||||
- Adopt `qbit-build-parent:2.1.0` with architecture guardrail enforcement and strict UI API routes enabled for tests. Dependency, extension contract and stand-in checks run in the normal build.
|
||||
- Add record followers to alert routing through core `RecordFollowers.get(instance, ref, FollowEvent.ALERT)` (#16, QRun-IO/qqq#1164). Deduplicate by user/channel with subscriptions and other record refs; require source-record read access under the follower's session, including record locks. Followers use the existing alert visibility, preferences and suppression pipeline without a dependency on qbit-collaboration. Missing/disabled/failing providers preserve subscription routing.
|
||||
|
||||
@@ -157,7 +157,7 @@ How each one sends:
|
||||
|
||||
## Email through qbit-email
|
||||
|
||||
When qbit-email is produced in the same instance (and on the classpath; it is an optional dependency), alert channel emails, notification emails and daily digests go through the published `com.kingsrook.qbits.email.api.EmailApi` in qbit-email `0.1.0-RC.2` instead of core `SendMessageAction`. Template registration, sends and installation detection use only the public API; templates, categories, idempotency keys, linked records and outcome mapping are unchanged:
|
||||
When qbit-email is produced in the same instance (and on the classpath; it is an optional dependency), alert channel emails, notification emails and daily digests go through `EmailService` instead of core `SendMessageAction`:
|
||||
|
||||
- **Templates**, registered on first use and never overwritten (edit them in the Email app): `alerts_alert` (a channel email for one alert or a batch), `alerts_notification` (one notification) and `alerts_digest` (a list of items, `{{#each items}}`). All are Markdown, category NOTIFICATION, so unsubscribe and suppression apply.
|
||||
- **One recipient per message.** An email list channel sends one message per address.
|
||||
|
||||
@@ -23,15 +23,20 @@
|
||||
<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.2</qbit-email.version>
|
||||
<qbit-email.version>0.1.0-RC.1</qbit-email.version>
|
||||
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
|
||||
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
|
||||
<!-- Optional email integration: only AlertsEmailSender references its API, -->
|
||||
<!-- after classpath and EmailRuntime.config(instance) checks. EmailRuntime -->
|
||||
<!-- lives in the root package; the guardrail accepts package prefixes. -->
|
||||
<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>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.1</version>
|
||||
<version>2.1.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
*******************************************************************************/
|
||||
public final class AlertsEmail
|
||||
{
|
||||
private static final String EMAIL_API_CLASS = "com.kingsrook.qbits.email.api.EmailApi";
|
||||
private static final String EMAIL_SERVICE_CLASS = "com.kingsrook.qbits.email.outbox.EmailService";
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class AlertsEmail
|
||||
}
|
||||
try
|
||||
{
|
||||
Class.forName(EMAIL_API_CLASS);
|
||||
Class.forName(EMAIL_SERVICE_CLASS);
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
{
|
||||
|
||||
@@ -27,11 +27,13 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsAlert;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotification;
|
||||
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.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.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
@@ -40,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
** The one place that names qbit-email classes. Callers reach it only after
|
||||
** {@link AlertsEmail#isActive()}. It registers the alerts templates on first
|
||||
** use (never overwriting people's edits) and sends one message per recipient
|
||||
** through {@code EmailApi}, all in the NOTIFICATION category:
|
||||
** through {@code EmailService}, all in the NOTIFICATION category:
|
||||
** - alerts_alert: an alert channel email (one alert or a batch);
|
||||
** - alerts_notification: one user notification;
|
||||
** - alerts_digest: a user's daily digest of notifications.
|
||||
@@ -94,7 +96,7 @@ public final class AlertsEmailSender
|
||||
*******************************************************************************/
|
||||
static boolean isInstalled(QInstance qInstance)
|
||||
{
|
||||
return (EmailApi.isInstalled(qInstance));
|
||||
return (EmailRuntime.config(qInstance) != null);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +126,7 @@ public final class AlertsEmailSender
|
||||
String idPart = ids.isEmpty() ? "test" : String.join("-", ids.stream().map(String::valueOf).toList());
|
||||
String key = "alerts:alert:" + idPart + ":" + hash(to) + ":" + messageKind + ":" + (now.getEpochSecond() / 60);
|
||||
|
||||
EmailApiRequest request = base(to, process, key)
|
||||
EmailRequest request = base(to, process, key)
|
||||
.withTemplateName(TEMPLATE_ALERT)
|
||||
.withVariable("subject", subject)
|
||||
.withVariable("severity", severity)
|
||||
@@ -136,7 +138,7 @@ public final class AlertsEmailSender
|
||||
}
|
||||
withUrl(request, "link", link);
|
||||
withUrl(request, "settings_url", settingsUrl);
|
||||
return (result(EmailApi.send(request)));
|
||||
return (result(EmailService.send(request)));
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +155,7 @@ public final class AlertsEmailSender
|
||||
public static Result sendNotification(String to, String process, Integer notificationId, Map<String, Serializable> item, String settingsUrl) throws QException
|
||||
{
|
||||
ensure(TEMPLATE_NOTIFICATION);
|
||||
EmailApiRequest request = base(to, process, "alerts:notification:" + notificationId + ":" + hash(to))
|
||||
EmailRequest request = base(to, process, "alerts:notification:" + notificationId + ":" + hash(to))
|
||||
.withTemplateName(TEMPLATE_NOTIFICATION)
|
||||
.withLinkedRecord(AlertsNotification.TABLE_NAME, notificationId);
|
||||
for(Map.Entry<String, Serializable> entry : item.entrySet())
|
||||
@@ -168,7 +170,7 @@ public final class AlertsEmailSender
|
||||
}
|
||||
}
|
||||
withUrl(request, "settings_url", settingsUrl);
|
||||
return (result(EmailApi.send(request)));
|
||||
return (result(EmailService.send(request)));
|
||||
}
|
||||
|
||||
|
||||
@@ -188,13 +190,13 @@ public final class AlertsEmailSender
|
||||
String settingsUrl) throws QException
|
||||
{
|
||||
ensure(TEMPLATE_DIGEST);
|
||||
EmailApiRequest request = base(to, process, "alerts:digest:" + userId + ":" + windowStart)
|
||||
EmailRequest request = base(to, process, "alerts:digest:" + userId + ":" + windowStart)
|
||||
.withTemplateName(TEMPLATE_DIGEST)
|
||||
.withVariable("count", items.size())
|
||||
.withVariable("items", (Serializable) items)
|
||||
.withLinkedRecord(AlertsNotification.TABLE_NAME, firstNotificationId);
|
||||
withUrl(request, "settings_url", settingsUrl);
|
||||
return (result(EmailApi.send(request)));
|
||||
return (result(EmailService.send(request)));
|
||||
}
|
||||
|
||||
|
||||
@@ -202,9 +204,9 @@ public final class AlertsEmailSender
|
||||
/*******************************************************************************
|
||||
** The parts every message has: one recipient, the sender and the key.
|
||||
*******************************************************************************/
|
||||
private static EmailApiRequest base(String to, String process, String idempotencyKey)
|
||||
private static EmailRequest base(String to, String process, String idempotencyKey)
|
||||
{
|
||||
return (new EmailApiRequest().withTo(to).withCategory(EmailApiCategory.NOTIFICATION).withSender(SENDER_QBIT, process).withIdempotencyKey(idempotencyKey));
|
||||
return (new EmailRequest().withTo(to).withCategory(EmailCategory.NOTIFICATION).withSender(SENDER_QBIT, process).withIdempotencyKey(idempotencyKey));
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +215,7 @@ public final class AlertsEmailSender
|
||||
** Pass a URL only when it is absolute http(s); the template schema refuses
|
||||
** relative links, and they are useless in a mail anyway.
|
||||
*******************************************************************************/
|
||||
private static void withUrl(EmailApiRequest request, String name, String url)
|
||||
private static void withUrl(EmailRequest request, String name, String url)
|
||||
{
|
||||
if(url != null && (url.startsWith("https://") || url.startsWith("http://")))
|
||||
{
|
||||
@@ -226,7 +228,7 @@ public final class AlertsEmailSender
|
||||
/*******************************************************************************
|
||||
** Translate qbit-email's result.
|
||||
*******************************************************************************/
|
||||
private static Result result(EmailApiResult sent)
|
||||
private static Result result(EmailSendResult sent)
|
||||
{
|
||||
String detail = sent.reasons() == null || sent.reasons().isEmpty() ? null : String.join("; ", sent.reasons());
|
||||
return (new Result(Status.valueOf(sent.status().name()), sent.messageId(), detail));
|
||||
@@ -264,10 +266,10 @@ public final class AlertsEmailSender
|
||||
*******************************************************************************/
|
||||
private static void ensure(String name) throws QException
|
||||
{
|
||||
EmailApi.ensureTemplate(switch(name)
|
||||
EmailTemplates.ensure(switch(name)
|
||||
{
|
||||
case TEMPLATE_ALERT -> new EmailApiTemplate()
|
||||
.withName(TEMPLATE_ALERT).withLabel("Alerts: alert").withCategory(EmailApiCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
case TEMPLATE_ALERT -> new EmailTemplateDefinition()
|
||||
.withName(TEMPLATE_ALERT).withLabel("Alerts: alert").withCategory(EmailCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
.withSubject("{{subject}}")
|
||||
.withBody("""
|
||||
{{#each items}}**{{this.title}}**
|
||||
@@ -291,8 +293,8 @@ public final class AlertsEmailSender
|
||||
.withVariable("settings_url", "Settings link", "URL", false, null)
|
||||
.withSampleVariables("{\"subject\":\"[ERROR] Export failed\",\"severity\":\"ERROR\",\"kind\":\"RAISED\",\"items\":[{\"title\":\"Export failed\",\"severity\":\"ERROR\","
|
||||
+ "\"typeLabel\":\"Jobs\",\"summary\":\"3 records\"}],\"link\":\"https://example.com/app/alerts_alert/1\",\"settings_url\":\"https://example.com/app/alerts_subscription\"}");
|
||||
case TEMPLATE_NOTIFICATION -> new EmailApiTemplate()
|
||||
.withName(TEMPLATE_NOTIFICATION).withLabel("Alerts: notification").withCategory(EmailApiCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
case TEMPLATE_NOTIFICATION -> new EmailTemplateDefinition()
|
||||
.withName(TEMPLATE_NOTIFICATION).withLabel("Alerts: notification").withCategory(EmailCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
.withSubject("{{title}}")
|
||||
.withBody("""
|
||||
{{#if body}}{{body}}
|
||||
@@ -307,8 +309,8 @@ public final class AlertsEmailSender
|
||||
.withVariable("settings_url", "Settings link", "URL", false, null)
|
||||
.withSampleVariables("{\"title\":\"Export failed\",\"body\":\"The nightly export did not finish.\",\"link\":\"https://example.com/app/widget\",\"linkLabel\":\"Open\","
|
||||
+ "\"settings_url\":\"https://example.com/app/alerts_notification_preference\"}");
|
||||
case TEMPLATE_DIGEST -> new EmailApiTemplate()
|
||||
.withName(TEMPLATE_DIGEST).withLabel("Alerts: daily digest").withCategory(EmailApiCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
case TEMPLATE_DIGEST -> new EmailTemplateDefinition()
|
||||
.withName(TEMPLATE_DIGEST).withLabel("Alerts: daily digest").withCategory(EmailCategory.NOTIFICATION).withKind("MARKDOWN").withOwnerQbit(SENDER_QBIT)
|
||||
.withSubject("{{count}} new notifications")
|
||||
.withBody("""
|
||||
{{#each items}}**{{this.title}}**
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.alerts.AlertsPreferences;
|
||||
import com.kingsrook.qbits.alerts.AlertsQBitConfig;
|
||||
@@ -37,8 +36,8 @@ import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.notifications.NotificationEmailer;
|
||||
import com.kingsrook.qbits.email.EmailQBitConfig;
|
||||
import com.kingsrook.qbits.email.EmailQBitProducer;
|
||||
import com.kingsrook.qbits.email.api.EmailApi;
|
||||
import com.kingsrook.qbits.email.api.EmailApiMessage;
|
||||
import com.kingsrook.qbits.email.EmailRuntime;
|
||||
import com.kingsrook.qbits.email.metadata.EmailTables;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertLink;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationCategory;
|
||||
@@ -65,8 +64,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The email qbit in the same instance, and the alerts config without a
|
||||
** messaging provider (qbit-email makes it unnecessary). G1 permits the
|
||||
** producer and configuration imports in test setup.
|
||||
** messaging provider (qbit-email makes it unnecessary).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void customizeInstance(QInstance qInstance)
|
||||
@@ -118,7 +116,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
*******************************************************************************/
|
||||
private void addProvider() throws Exception
|
||||
{
|
||||
runtime.asSystem(() -> AlertsDb.insert("email_provider", new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP")
|
||||
runtime.asSystem(() -> AlertsDb.insert(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)));
|
||||
}
|
||||
@@ -126,18 +124,12 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Find messages through the API, oldest first. Read the fixture rows with
|
||||
** core QQQ for content assertions: the API deliberately omits bodies,
|
||||
** recipients and sender details from its metadata projection.
|
||||
** The queued messages, oldest first.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> messages() throws QException
|
||||
{
|
||||
List<QRecord> messages = new ArrayList<>();
|
||||
for(EmailApiMessage message : EmailApi.findMessagesByIdempotencyKeyPrefix("alerts:", 100))
|
||||
{
|
||||
messages.add(runtime.asSystem(() -> AlertsDb.byId("email_message", message.id())));
|
||||
}
|
||||
return (messages);
|
||||
return (runtime.asSystem(() -> AlertsDb.query(EmailTables.MESSAGE, new com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter().withOrderBy(
|
||||
new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("id")))));
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +139,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
*******************************************************************************/
|
||||
private String templateName(QRecord message) throws QException
|
||||
{
|
||||
return (EmailApi.getMessage(message.getValueInteger("id")).orElseThrow().templateName());
|
||||
return (runtime.asSystem(() -> AlertsDb.byId(EmailTables.TEMPLATE, message.getValue("templateId"))).getValueString("name"));
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +184,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
{
|
||||
assertThat(AlertsEmail.isActive()).isTrue();
|
||||
assertThat(AlertsEmail.isActive(null)).isFalse();
|
||||
assertThat(EmailApi.isInstalled(qInstance)).isTrue();
|
||||
assertThat(EmailRuntime.config(qInstance)).isNotNull();
|
||||
assertThat(new EmailChannel(new AlertsQBitConfig(), ChannelType.EMAIL).validate(null, null)).isEmpty();
|
||||
assertThat(new NotificationEmailer(runtime).isConfigured()).isTrue();
|
||||
}
|
||||
@@ -209,8 +201,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
{
|
||||
addProvider();
|
||||
EmailChannel channel = new EmailChannel(config, ChannelType.EMAIL);
|
||||
DeliveryResult delivered = channel.deliver(alertMessage(), null, "ops@example.com", null);
|
||||
assertThat(delivered.outcome()).as("Delivery outcome: %s", delivered.error()).isEqualTo(DeliveryResult.Outcome.SENT);
|
||||
assertThat(channel.deliver(alertMessage(), null, "ops@example.com", null).outcome()).isEqualTo(DeliveryResult.Outcome.SENT);
|
||||
|
||||
List<QRecord> messages = messages();
|
||||
assertThat(messages).hasSize(1);
|
||||
@@ -276,7 +267,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
void testSuppressedAddress() throws Exception
|
||||
{
|
||||
addProvider();
|
||||
runtime.asSystem(() -> AlertsDb.insert("email_suppression", new QRecord().withValue("address", "ops@example.com").withValue("scope", "ALL").withValue("reason", "HARD_BOUNCE")));
|
||||
runtime.asSystem(() -> AlertsDb.insert(EmailTables.SUPPRESSION, new QRecord().withValue("address", "ops@example.com").withValue("scope", "ALL").withValue("reason", "HARD_BOUNCE")));
|
||||
DeliveryResult result = new EmailChannel(config, ChannelType.EMAIL).deliver(alertMessage(), null, "ops@example.com", null);
|
||||
assertThat(result.outcome()).isEqualTo(DeliveryResult.Outcome.FAILED);
|
||||
assertThat(result.error()).contains("suppressed");
|
||||
@@ -353,7 +344,7 @@ class EmailQBitSendingTest extends BaseAlertsTest
|
||||
loginAs(ADMIN);
|
||||
QAlerts.notify(new NotificationSignal("Export failed").withSeverity(NotificationSeverity.WARNING).withCategory(NotificationCategory.JOB)
|
||||
.withAudience(NotificationAudience.users("viewer-1")));
|
||||
runtime.asSystem(() -> AlertsDb.insert("email_suppression", new QRecord().withValue("address", "viewer-1@example.com").withValue("scope", "ALL").withValue("reason", "HARD_BOUNCE")));
|
||||
runtime.asSystem(() -> AlertsDb.insert(EmailTables.SUPPRESSION, new QRecord().withValue("address", "viewer-1@example.com").withValue("scope", "ALL").withValue("reason", "HARD_BOUNCE")));
|
||||
NotificationEmailer.Result suppressed = new NotificationEmailer(runtime).run();
|
||||
assertThat(suppressed.skipped()).isEqualTo(1);
|
||||
assertThat(suppressed.sent()).isEqualTo(0);
|
||||
|
||||
@@ -75,7 +75,6 @@ CREATE TABLE email_message (
|
||||
phi_approved BOOLEAN,
|
||||
unsubscribe_url TEXT,
|
||||
idempotency_key VARCHAR(500),
|
||||
rfc_message_id VARCHAR(500),
|
||||
provider_id INTEGER,
|
||||
provider_message_id VARCHAR(500),
|
||||
attempt_count INTEGER,
|
||||
@@ -94,8 +93,7 @@ CREATE TABLE email_message (
|
||||
sender_process VARCHAR(500),
|
||||
suppressed_addresses TEXT,
|
||||
send_after TIMESTAMP,
|
||||
UNIQUE (idempotency_key),
|
||||
UNIQUE (rfc_message_id)
|
||||
UNIQUE (idempotency_key)
|
||||
);
|
||||
|
||||
CREATE TABLE email_message_attempt (
|
||||
|
||||
Reference in new issue
Block a user