diff --git a/connector-common-http/src/main/java/com/redhat/cloud/notifications/connector/http/HttpExceptionProcessor.java b/connector-common-http/src/main/java/com/redhat/cloud/notifications/connector/http/HttpExceptionProcessor.java
index bcacc2116e..ef816d5dcd 100644
--- a/connector-common-http/src/main/java/com/redhat/cloud/notifications/connector/http/HttpExceptionProcessor.java
+++ b/connector-common-http/src/main/java/com/redhat/cloud/notifications/connector/http/HttpExceptionProcessor.java
@@ -9,6 +9,7 @@
import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.client5.http.HttpHostConnectException;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.reactive.ClientWebApplicationException;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
@@ -39,20 +40,10 @@ public class HttpExceptionProcessor extends ExceptionProcessor {
@Override
protected void process(Throwable t, Exchange exchange) {
- if (t instanceof HttpOperationFailedException e) {
- exchange.setProperty(HTTP_STATUS_CODE, e.getStatusCode());
- if (e.getStatusCode() >= 300 && e.getStatusCode() < 400) {
- exchange.setProperty(HTTP_ERROR_TYPE, HTTP_3XX);
- logHttpError(connectorConfig.getServerErrorLogLevel(), e, exchange);
- } else if (e.getStatusCode() >= 400 && e.getStatusCode() < 500 && e.getStatusCode() != SC_TOO_MANY_REQUESTS) {
- exchange.setProperty(HTTP_ERROR_TYPE, HTTP_4XX);
- logHttpError(connectorConfig.getClientErrorLogLevel(), e, exchange);
- } else if (e.getStatusCode() == SC_TOO_MANY_REQUESTS || e.getStatusCode() >= 500) {
- exchange.setProperty(HTTP_ERROR_TYPE, HTTP_5XX);
- logHttpError(connectorConfig.getServerErrorLogLevel(), e, exchange);
- } else {
- logHttpError(ERROR, e, exchange);
- }
+ if (t instanceof ClientWebApplicationException e) {
+ manageReturnedStatusCode(exchange, e.getResponse().getStatus(), e.getResponse().readEntity(String.class));
+ } else if (t instanceof HttpOperationFailedException e) {
+ manageReturnedStatusCode(exchange, e.getStatusCode(), e.getResponseBody());
} else if (t instanceof ConnectTimeoutException) {
exchange.setProperty(HTTP_ERROR_TYPE, CONNECT_TIMEOUT);
} else if (t instanceof SocketTimeoutException) {
@@ -70,7 +61,23 @@ protected void process(Throwable t, Exchange exchange) {
}
}
- private void logHttpError(Logger.Level level, HttpOperationFailedException e, Exchange exchange) {
+ private void manageReturnedStatusCode(Exchange exchange, int statusCode, String responseBody) {
+ exchange.setProperty(HTTP_STATUS_CODE, statusCode);
+ if (statusCode >= 300 && statusCode < 400) {
+ exchange.setProperty(HTTP_ERROR_TYPE, HTTP_3XX);
+ logHttpError(connectorConfig.getServerErrorLogLevel(), statusCode, responseBody, exchange);
+ } else if (statusCode >= 400 && statusCode < 500 && statusCode != SC_TOO_MANY_REQUESTS) {
+ exchange.setProperty(HTTP_ERROR_TYPE, HTTP_4XX);
+ logHttpError(connectorConfig.getClientErrorLogLevel(), statusCode, responseBody, exchange);
+ } else if (statusCode == SC_TOO_MANY_REQUESTS || statusCode >= 500) {
+ exchange.setProperty(HTTP_ERROR_TYPE, HTTP_5XX);
+ logHttpError(connectorConfig.getServerErrorLogLevel(), statusCode, responseBody, exchange);
+ } else {
+ logHttpError(ERROR, statusCode, responseBody, exchange);
+ }
+ }
+
+ private void logHttpError(Logger.Level level, int statusCode, String responseBody, Exchange exchange) {
Log.logf(
level,
HTTP_ERROR_LOG_MSG,
@@ -78,8 +85,8 @@ private void logHttpError(Logger.Level level, HttpOperationFailedException e, Ex
getOrgId(exchange),
getExchangeId(exchange),
getTargetUrl(exchange),
- e.getStatusCode(),
- e.getResponseBody()
+ statusCode,
+ responseBody
);
}
}
diff --git a/connector-email/pom.xml b/connector-email/pom.xml
index 366af3d5ae..6ab8aac64d 100644
--- a/connector-email/pom.xml
+++ b/connector-email/pom.xml
@@ -79,6 +79,13 @@
test
+
+
+ dev.failsafe
+ failsafe
+ ${failsafe.version}
+
+
org.mock-server
@@ -87,6 +94,16 @@
test
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailCloudEventDataExtractor.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailCloudEventDataExtractor.java
index 304c78fc54..0f2616cec6 100644
--- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailCloudEventDataExtractor.java
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailCloudEventDataExtractor.java
@@ -44,6 +44,6 @@ public void extract(final Exchange exchange, final JsonObject cloudEventData) {
exchange.setProperty(ExchangeProperty.EMAIL_RECIPIENTS, emails);
exchange.setProperty(ExchangeProperty.EMAIL_SENDER, emailNotification.emailSender());
- exchange.setProperty(ExchangeProperty.USE_EMAIL_BOP_V1_SSL, emailConnectorConfig.isEnableBopServiceWithSslChecks());
+ exchange.setProperty(ExchangeProperty.USE_SIMPLIFIED_EMAIL_ROUTE, emailConnectorConfig.useSimplifiedEmailRoute(emailNotification.orgId()));
}
}
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailManagementProcessor.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailManagementProcessor.java
new file mode 100644
index 0000000000..9a3bc2026e
--- /dev/null
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailManagementProcessor.java
@@ -0,0 +1,112 @@
+package com.redhat.cloud.notifications.connector.email;
+
+import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig;
+import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty;
+import com.redhat.cloud.notifications.connector.email.model.settings.RecipientSettings;
+import com.redhat.cloud.notifications.connector.email.model.settings.User;
+import com.redhat.cloud.notifications.connector.email.processors.bop.BOPManager;
+import com.redhat.cloud.notifications.connector.email.processors.recipientsresolver.ExternalRecipientsResolver;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import io.quarkus.logging.Log;
+import io.vertx.core.json.JsonObject;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static com.redhat.cloud.notifications.connector.ExchangeProperty.ID;
+import static com.redhat.cloud.notifications.connector.ExchangeProperty.ORG_ID;
+import static com.redhat.cloud.notifications.connector.email.CloudEventHistoryBuilder.TOTAL_RECIPIENTS_KEY;
+import static java.util.stream.Collectors.toSet;
+
+@ApplicationScoped
+public class EmailManagementProcessor implements Processor {
+
+ @Inject
+ EmailConnectorConfig emailConnectorConfig;
+
+ @Inject
+ ExternalRecipientsResolver externalRecipientsResolver;
+
+ @Inject
+ BOPManager bopManager;
+
+ @Inject
+ MeterRegistry meterRegistry;
+
+ static final String BOP_RESPONSE_TIME_METRIC = "email.bop.response.time";
+ static final String RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC = "email.recipients_resolver.response.time";
+
+ @Override
+ public void process(final Exchange exchange) {
+ // fetch recipients
+ Set recipientsList = fetchRecipients(exchange);
+
+ if (recipientsList.isEmpty()) {
+ Log.infof("Skipped Email notification because the recipients list was empty [orgId=$%s, historyId=%s]", exchange.getProperty(ORG_ID, String.class), exchange.getProperty(ID, String.class));
+ } else {
+ // send to bop
+ sendToBop(exchange, recipientsList);
+ }
+ }
+
+ private void sendToBop(Exchange exchange, Set recipientsList) {
+ // split recipient list and send it ot BOP
+ List> packedRecipients = partition(recipientsList, emailConnectorConfig.getMaxRecipientsPerEmail() - 1);
+ final String subject = exchange.getProperty(ExchangeProperty.RENDERED_SUBJECT, String.class);
+ final String body = exchange.getProperty(ExchangeProperty.RENDERED_BODY, String.class);
+ final String sender = exchange.getProperty(ExchangeProperty.EMAIL_SENDER, String.class);
+
+ for (int i = 0; i < packedRecipients.size(); i++) {
+ final Timer.Sample bopResponseTimeMetric = Timer.start(meterRegistry);
+ bopManager.sendToBop(packedRecipients.get(i), subject, body, sender);
+ bopResponseTimeMetric.stop(meterRegistry.timer(BOP_RESPONSE_TIME_METRIC));
+ Log.infof("Sent Email notification %d/%d [orgId=%s, historyId=%s]", i + 1, packedRecipients.size(), exchange.getProperty(ORG_ID, String.class), exchange.getProperty(ID, String.class));
+ }
+ }
+
+ private static List> partition(Set collection, int n) {
+ AtomicInteger counter = new AtomicInteger();
+ return collection.stream()
+ .collect(Collectors.groupingBy(it -> counter.getAndIncrement() / n))
+ .values().stream().collect(Collectors.toList());
+ }
+
+ private Set fetchRecipients(Exchange exchange) {
+ List recipientSettings = exchange.getProperty(ExchangeProperty.RECIPIENT_SETTINGS, List.class);
+ Set subscribers = exchange.getProperty(ExchangeProperty.SUBSCRIBERS, Set.class);
+ Set unsubscribers = exchange.getProperty(ExchangeProperty.UNSUBSCRIBERS, Set.class);
+ JsonObject recipientsAuthorizationCriterion = exchange.getProperty(ExchangeProperty.RECIPIENTS_AUTHORIZATION_CRITERION, JsonObject.class);
+
+ boolean subscribedByDefault = exchange.getProperty(ExchangeProperty.SUBSCRIBED_BY_DEFAULT, boolean.class);
+ final String orgId = exchange.getProperty(ORG_ID, String.class);
+
+ final Timer.Sample recipientsResolverResponseTimeMetric = Timer.start(meterRegistry);
+ Set recipientsList = externalRecipientsResolver.recipientUsers(
+ orgId,
+ Set.copyOf(recipientSettings),
+ subscribers,
+ unsubscribers,
+ subscribedByDefault,
+ recipientsAuthorizationCriterion)
+ .stream().map(User::getEmail).filter(email -> email != null && !email.isBlank()).collect(toSet());
+ recipientsResolverResponseTimeMetric.stop(meterRegistry.timer(RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC));
+
+ Set emails = exchange.getProperty(ExchangeProperty.EMAIL_RECIPIENTS, Set.of(), Set.class);
+ if (emailConnectorConfig.isEmailsInternalOnlyEnabled()) {
+ Set forbiddenEmail = emails.stream().filter(email -> !email.trim().toLowerCase().endsWith("@redhat.com")).collect(Collectors.toSet());
+ if (!forbiddenEmail.isEmpty()) {
+ Log.warnf(" %s emails are forbidden for message historyId: %s ", forbiddenEmail, exchange.getProperty(com.redhat.cloud.notifications.connector.ExchangeProperty.ID, String.class));
+ }
+ emails.removeAll(forbiddenEmail);
+ }
+ recipientsList.addAll(emails);
+ exchange.setProperty(TOTAL_RECIPIENTS_KEY, recipientsList.size());
+ return recipientsList;
+ }
+}
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java
index e1dedb5727..958428ec9e 100644
--- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java
@@ -16,7 +16,6 @@
import org.apache.camel.support.jsse.KeyStoreParameters;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.support.jsse.TrustManagersParameters;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
import java.util.Set;
@@ -24,8 +23,7 @@
import static com.redhat.cloud.notifications.connector.ExchangeProperty.ID;
import static com.redhat.cloud.notifications.connector.ExchangeProperty.ORG_ID;
import static com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty.FILTERED_USERS;
-import static com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty.USE_EMAIL_BOP_V1_SSL;
-import static com.redhat.cloud.notifications.connector.http.SslTrustAllManager.getSslContextParameters;
+import static com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty.USE_SIMPLIFIED_EMAIL_ROUTE;
import static org.apache.camel.LoggingLevel.DEBUG;
import static org.apache.camel.LoggingLevel.INFO;
import static org.apache.camel.builder.endpoint.dsl.HttpEndpointBuilderFactory.HttpEndpointBuilder;
@@ -61,6 +59,9 @@ public class EmailRouteBuilder extends EngineToConnectorRouteBuilder {
@Inject
EmailMetricsProcessor emailMetricsProcessor;
+ @Inject
+ EmailManagementProcessor emailManagementProcessor;
+
/**
* Configures the flow for this connector.
*/
@@ -74,23 +75,27 @@ public void configureRoutes() {
.to(direct(ENTRYPOINT));
}
- /*
- * Prepares the payload accepted by BOP and sends the request to
- * the service.
- */
- final HttpEndpointBuilder bopEndpointV1 = setUpBOPEndpointV1();
-
from(seda(ENGINE_TO_CONNECTOR))
.routeId(emailConnectorConfig.getConnectorName())
- .process(recipientsResolverRequestPreparer)
- .to(RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
- .to(setupRecipientResolverEndpoint())
- .to(RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
- .process(recipientsResolverResponseProcessor)
- .choice().when(shouldSkipEmail())
- .log(INFO, getClass().getName(), "Skipped Email notification because the recipients list was empty [orgId=${exchangeProperty." + ORG_ID + "}, historyId=${exchangeProperty." + ID + "}]")
- .otherwise()
- .to(direct(Routes.SPLIT_AND_SEND))
+ .choice()
+ .when(shouldUseSimplifiedEmailManagement())
+ .process(emailManagementProcessor)
+ .endChoice()
+ .otherwise()
+ .process(recipientsResolverRequestPreparer)
+ .to(RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
+ .to(setupRecipientResolverEndpoint())
+ .to(RECIPIENTS_RESOLVER_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
+ .process(recipientsResolverResponseProcessor)
+ .choice()
+ .when(shouldSkipEmail())
+ .log(INFO, getClass().getName(), "Skipped Email notification because the recipients list was empty [orgId=${exchangeProperty." + ORG_ID + "}, historyId=${exchangeProperty." + ID + "}]")
+ .endChoice()
+ .otherwise()
+ .to(direct(Routes.SPLIT_AND_SEND))
+ .endChoice()
+ .end()
+ .endChoice()
.end()
.to(direct(SUCCESS));
@@ -105,16 +110,10 @@ public void configureRoutes() {
// Clear all the headers that may come from the previous route.
.removeHeaders("*")
.process(this.BOPRequestPreparer)
- .choice().when(shouldUseBopEmailServiceWithSslChecks())
- .log(DEBUG, getClass().getName(), "Sent Email notification [orgId=${exchangeProperty." + ORG_ID + "}, historyId=${exchangeProperty." + ID + "} using regular SSL checks on email service]")
- .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
- .to(emailConnectorConfig.getBopURL())
- .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_STOP)
- .otherwise()
- .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
- .to(bopEndpointV1)
- .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_STOP)
- .end()
+ .log(DEBUG, getClass().getName(), "Sent Email notification [orgId=${exchangeProperty." + ORG_ID + "}, historyId=${exchangeProperty." + ID + "} using regular SSL checks on email service]")
+ .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_START)
+ .to(emailConnectorConfig.getBopURL())
+ .to(BOP_RESPONSE_TIME_METRIC + TIMER_ACTION_STOP)
.log(INFO, getClass().getName(), "Sent Email notification [orgId=${exchangeProperty." + ORG_ID + "}, historyId=${exchangeProperty." + ID + "}]")
.process(emailMetricsProcessor);
}
@@ -123,27 +122,8 @@ private Predicate shouldSkipEmail() {
return exchange -> exchange.getProperty(FILTERED_USERS, Set.class).isEmpty();
}
- private Predicate shouldUseBopEmailServiceWithSslChecks() {
- return exchange -> exchange.getProperty(USE_EMAIL_BOP_V1_SSL, Boolean.class);
- }
-
- /**
- * Creates the endpoint for the BOP service. It makes Apache Camel trust
- * BOP service's certificate.
- * @return the created endpoint.
- */
- protected HttpEndpointBuilder setUpBOPEndpointV1() {
- // Remove the schema from the url to avoid the
- // "ResolveEndpointFailedException", which complaints about specifying
- // the schema twice.
- final String fullURL = this.emailConnectorConfig.getBopURL();
- if (fullURL.startsWith("https")) {
- return https(fullURL.replace("https://", ""))
- .sslContextParameters(getSslContextParameters())
- .x509HostnameVerifier(NoopHostnameVerifier.INSTANCE);
- } else {
- return http(fullURL.replace("http://", ""));
- }
+ private Predicate shouldUseSimplifiedEmailManagement() {
+ return exchange -> exchange.getProperty(USE_SIMPLIFIED_EMAIL_ROUTE, false, Boolean.class);
}
private HttpEndpointBuilder setupRecipientResolverEndpoint() {
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java
index 0965bfedc8..911dca4799 100644
--- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java
@@ -1,6 +1,7 @@
package com.redhat.cloud.notifications.connector.email.config;
import com.redhat.cloud.notifications.connector.http.HttpConnectorConfig;
+import com.redhat.cloud.notifications.unleash.UnleashContextBuilder;
import io.quarkus.runtime.LaunchMode;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@@ -74,13 +75,13 @@ public class EmailConnectorConfig extends HttpConnectorConfig {
@ConfigProperty(name = RECIPIENTS_RESOLVER_TRUST_STORE_TYPE)
Optional recipientsResolverTrustStoreType;
- private String enableBopEmailServiceWithSslChecks;
private String toggleKafkaIncomingHighVolumeTopic;
+ private String toggleUseSimplifiedEmailRoute;
@PostConstruct
void emailConnectorPostConstruct() {
- enableBopEmailServiceWithSslChecks = toggleRegistry.register("enable-bop-service-ssl-checks", true);
toggleKafkaIncomingHighVolumeTopic = toggleRegistry.register("kafka-incoming-high-volume-topic", true);
+ toggleUseSimplifiedEmailRoute = toggleRegistry.register("use-simplified-email-route", true);
}
@Override
@@ -102,7 +103,7 @@ protected Map getLoggedConfiguration() {
config.put(RECIPIENTS_RESOLVER_USER_SERVICE_URL, recipientsResolverServiceURL);
config.put(MAX_RECIPIENTS_PER_EMAIL, maxRecipientsPerEmail);
config.put(NOTIFICATIONS_EMAILS_INTERNAL_ONLY_ENABLED, emailsInternalOnlyEnabled);
- config.put(enableBopEmailServiceWithSslChecks, isEnableBopServiceWithSslChecks());
+ config.put(toggleUseSimplifiedEmailRoute, useSimplifiedEmailRoute(null));
config.put(toggleKafkaIncomingHighVolumeTopic, isIncomingKafkaHighVolumeTopicEnabled());
/*
@@ -182,8 +183,12 @@ public Optional getRecipientsResolverTrustStoreType() {
return recipientsResolverTrustStoreType;
}
- public boolean isEnableBopServiceWithSslChecks() {
- return true;
+ public boolean useSimplifiedEmailRoute(String orgId) {
+ if (unleashEnabled) {
+ return unleash.isEnabled(toggleUseSimplifiedEmailRoute, UnleashContextBuilder.buildUnleashContextWithOrgId(orgId), false);
+ } else {
+ return false;
+ }
}
/**
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java
index b765f9ceb8..ddbb58a217 100644
--- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java
@@ -47,9 +47,9 @@ public class ExchangeProperty {
public static final String RECIPIENTS_SIZE = "recipientsSize";
- public static final String USE_EMAIL_BOP_V1_SSL = "use_email_bop_V1_ssl";
-
public static final String RECIPIENTS_AUTHORIZATION_CRITERION = "recipients_authorization_criterion";
public static final String ADDITIONAL_ERROR_DETAILS = "additionalErrorDetails";
+
+ public static final String USE_SIMPLIFIED_EMAIL_ROUTE = "use_simplified_email_route";
}
diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPManager.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPManager.java
new file mode 100644
index 0000000000..74dac09e8f
--- /dev/null
+++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPManager.java
@@ -0,0 +1,84 @@
+package com.redhat.cloud.notifications.connector.email.processors.bop;
+
+import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig;
+import com.redhat.cloud.notifications.connector.email.model.bop.Email;
+import com.redhat.cloud.notifications.connector.email.model.bop.SendEmailsRequest;
+import dev.failsafe.Failsafe;
+import dev.failsafe.RetryPolicy;
+import dev.failsafe.function.CheckedRunnable;
+import io.quarkus.logging.Log;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+
+@ApplicationScoped
+public class BOPManager {
+
+ public static final String NOTIFICATIONS_BOP_RETRY_MAX_ATTEMPTS = "notifications.bop.retry.max-attempts";
+ public static final String NOTIFICATIONS_BOP_RETRY_INITIAL_BACKOFF = "notifications.bop.retry.initial-backoff";
+ public static final String NOTIFICATIONS_BOP_RETRY_MAX_BACKOFF = "notifications.bop.retry.max-backoff";
+
+ @ConfigProperty(name = NOTIFICATIONS_BOP_RETRY_MAX_ATTEMPTS, defaultValue = "3")
+ int maxRetryAttempts;
+
+ @ConfigProperty(name = NOTIFICATIONS_BOP_RETRY_INITIAL_BACKOFF, defaultValue = "0.1S")
+ Duration initialRetryBackoff;
+
+ @ConfigProperty(name = NOTIFICATIONS_BOP_RETRY_MAX_BACKOFF, defaultValue = "1S")
+ Duration maxRetryBackoff;
+
+ private RetryPolicy