Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b964ab17
|
||
|
|
d2bddc968a |
No files matched your search
@@ -6,6 +6,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `EmailApiReadiness.failedChecks()` and `failed(check)` with `EmailApiReadinessCheck` (NOT_INSTALLED, PROVIDER_MISSING, PROVIDER_INVALID, PHI_PROVIDER, PUBLIC_BASE_URL, UNSUBSCRIBE_KEY, BRAND_POSTAL_ADDRESS, BODY_KEY), so callers react to a specific prerequisite without parsing reason text.
|
||||
- `EmailApi.sendOnce` as the public durable-effect handoff, with API-only outcome/result types, committed enqueue/replay semantics and suppression/transaction refusal parity. Exact idempotency-key and RFC Message-ID lookups return `EmailApiMessage`, which now includes `rfcMessageId` across all lookup routes. Public requests support ordinary caller transactions; `EmailEffects`, `EmailEffectOutcome` and `EmailMessageRef` are deprecated internals. H2 parity, reconciliation, storage-failure and API-boundary coverage; no new dependencies.
|
||||
- Stable `com.kingsrook.qbits.email.api` contract for cross-qbit sends, create-once templates, metadata-only message lookups, enabled sender addresses, default provider selection and secret-safe readiness (including marketing and PHI prerequisites). JDK/QQQ-only signatures, complete request/status mapping tests and a reflection package-boundary guard; no new dependencies.
|
||||
- Documented both supported cross-qbit routes: core-only `SendMessageAction` via `qbitEmail`, and the public API for richer sends and lookups.
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kingsrook.qbits.email.api;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -283,21 +284,24 @@ public final class EmailApi
|
||||
public static EmailApiReadiness readiness(QInstance instance, EmailApiCategory category, boolean phiApproved) throws QException
|
||||
{
|
||||
List<String> reasons = new ArrayList<>();
|
||||
Set<EmailApiReadinessCheck> failed = EnumSet.noneOf(EmailApiReadinessCheck.class);
|
||||
var config = EmailRuntime.config(instance);
|
||||
if(config == null)
|
||||
{
|
||||
return (new EmailApiReadiness(false, List.of("Email qbit is not installed")));
|
||||
return (new EmailApiReadiness(false, List.of("Email qbit is not installed"), Set.of(EmailApiReadinessCheck.NOT_INSTALLED)));
|
||||
}
|
||||
QRecord provider = provider(instance, null);
|
||||
if(provider == null)
|
||||
{
|
||||
reasons.add("An enabled named or default provider is required");
|
||||
failed.add(EmailApiReadinessCheck.PROVIDER_MISSING);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!EmailTableCustomizers.validateProvider(provider, config).isEmpty())
|
||||
{
|
||||
reasons.add("The default provider configuration is invalid");
|
||||
failed.add(EmailApiReadinessCheck.PROVIDER_INVALID);
|
||||
}
|
||||
if(phiApproved)
|
||||
{
|
||||
@@ -308,6 +312,7 @@ public final class EmailApi
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
reasons.add("PHI email needs a PHI-enabled provider with open and click tracking disabled");
|
||||
failed.add(EmailApiReadinessCheck.PHI_PROVIDER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,22 +329,26 @@ public final class EmailApi
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
reasons.add("MARKETING email needs a valid publicBaseUrl");
|
||||
failed.add(EmailApiReadinessCheck.PUBLIC_BASE_URL);
|
||||
}
|
||||
if(!hasSecret(config.getSecretResolver(), "QQQ_EMAIL_UNSUBSCRIBE_KEY"))
|
||||
{
|
||||
reasons.add("MARKETING email needs QQQ_EMAIL_UNSUBSCRIBE_KEY");
|
||||
failed.add(EmailApiReadinessCheck.UNSUBSCRIBE_KEY);
|
||||
}
|
||||
List<QRecord> brands = read(EmailTables.BRAND, EmailStore.equal("isDefault", true).withLimit(2), Set.of("id", "postalAddress"));
|
||||
if(brands.size() != 1 || brands.getFirst().getValueString("postalAddress") == null || brands.getFirst().getValueString("postalAddress").isBlank())
|
||||
{
|
||||
reasons.add("MARKETING email needs a default brand with a postal address");
|
||||
failed.add(EmailApiReadinessCheck.BRAND_POSTAL_ADDRESS);
|
||||
}
|
||||
}
|
||||
if(phiApproved && !hasSecret(config.getSecretResolver(), "QQQ_EMAIL_BODY_KEY"))
|
||||
{
|
||||
reasons.add("PHI email needs QQQ_EMAIL_BODY_KEY");
|
||||
failed.add(EmailApiReadinessCheck.BODY_KEY);
|
||||
}
|
||||
return (new EmailApiReadiness(reasons.isEmpty(), reasons));
|
||||
return (new EmailApiReadiness(reasons.isEmpty(), reasons, failed));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,13 +21,15 @@
|
||||
package com.kingsrook.qbits.email.api;
|
||||
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configuration preflight, not a delivery guarantee. Reasons never contain secret values.
|
||||
*******************************************************************************/
|
||||
public record EmailApiReadiness(boolean ready, List<String> reasons)
|
||||
public record EmailApiReadiness(boolean ready, List<String> reasons, Set<EmailApiReadinessCheck> failedChecks)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Copy configuration reasons.
|
||||
@@ -35,5 +37,16 @@ public record EmailApiReadiness(boolean ready, List<String> reasons)
|
||||
public EmailApiReadiness
|
||||
{
|
||||
reasons = List.copyOf(reasons);
|
||||
failedChecks = failedChecks == null || failedChecks.isEmpty() ? Set.of() : Set.copyOf(EnumSet.copyOf(failedChecks));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a specific prerequisite failed.
|
||||
*******************************************************************************/
|
||||
public boolean failed(EmailApiReadinessCheck check)
|
||||
{
|
||||
return (failedChecks.contains(check));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.email.api;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One readiness prerequisite, so callers can react to specific gaps without
|
||||
** parsing reason text.
|
||||
*******************************************************************************/
|
||||
public enum EmailApiReadinessCheck
|
||||
{
|
||||
NOT_INSTALLED,
|
||||
PROVIDER_MISSING,
|
||||
PROVIDER_INVALID,
|
||||
PHI_PROVIDER,
|
||||
PUBLIC_BASE_URL,
|
||||
UNSUBSCRIBE_KEY,
|
||||
BRAND_POSTAL_ADDRESS,
|
||||
BODY_KEY
|
||||
}
|
||||
@@ -499,11 +499,14 @@ class EmailApiTest
|
||||
void reportsReadinessAndPhiPrerequisites() throws Exception
|
||||
{
|
||||
assertThat(EmailApi.readiness(null, null).reasons()).containsExactly("Email qbit is not installed");
|
||||
assertThat(EmailApi.readiness(null, null).failedChecks()).containsExactly(EmailApiReadinessCheck.NOT_INSTALLED);
|
||||
assertThat(EmailApi.readiness(instance, null).ready()).isTrue();
|
||||
EmailApiReadiness marketing = EmailApi.readiness(instance, EmailApiCategory.MARKETING);
|
||||
assertThat(marketing.ready()).isFalse();
|
||||
assertThat(marketing.reasons()).hasSize(3);
|
||||
assertThat(String.join(";", marketing.reasons())).contains("publicBaseUrl", "QQQ_EMAIL_UNSUBSCRIBE_KEY", "postal address");
|
||||
assertThat(marketing.failedChecks()).containsExactlyInAnyOrder(EmailApiReadinessCheck.PUBLIC_BASE_URL, EmailApiReadinessCheck.UNSUBSCRIBE_KEY, EmailApiReadinessCheck.BRAND_POSTAL_ADDRESS);
|
||||
assertThat(marketing.failed(EmailApiReadinessCheck.PROVIDER_MISSING)).isFalse();
|
||||
EmailQBitConfig config = EmailRuntime.config();
|
||||
config.withPublicBaseUrl("https://app.example.com").withSecretResolver(name -> "synthetic-test-key");
|
||||
QRecord brand = SystemWrites.insertOne(EmailTables.BRAND, new QRecord().withValue("name", "default").withValue("label", "Default").withValue("isDefault", true));
|
||||
@@ -522,6 +525,7 @@ class EmailApiTest
|
||||
EmailApiReadiness phi = EmailApi.readiness(instance, EmailApiCategory.TRANSACTIONAL, true);
|
||||
assertThat(phi.reasons()).hasSize(2);
|
||||
assertThat(String.join(";", phi.reasons())).contains("QQQ_EMAIL_BODY_KEY", "PHI-enabled").doesNotContain("DO_NOT_EXPOSE");
|
||||
assertThat(phi.failedChecks()).containsExactlyInAnyOrder(EmailApiReadinessCheck.BODY_KEY, EmailApiReadinessCheck.PHI_PROVIDER);
|
||||
config.withSecretResolver(name -> " ");
|
||||
assertThat(EmailApi.readiness(instance, EmailApiCategory.SECURITY, true).ready()).isFalse();
|
||||
config.withSecretResolver(name -> "synthetic-test-key");
|
||||
|
||||
Reference in new issue
Block a user