This is an automated email from the ASF dual-hosted git repository.
quantranhong1999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new a92d8cea18 JAMES-4216 Allow HTML templating for overquota emails
(#3097)
a92d8cea18 is described below
commit a92d8cea1865abc99d57f6a867e2f616894a0606
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Jul 27 10:59:17 2026 +0200
JAMES-4216 Allow HTML templating for overquota emails (#3097)
---
.../servers/partials/configure/listeners.adoc | 5 +
.../mailing/QuotaMailingListenerConfiguration.java | 71 ++++++++++++--
.../mailing/subscribers/QuotaThresholdNotice.java | 33 ++++++-
.../templates/QuotaThresholdMailHtmlBody.mustache | 22 +++++
.../QuotaMailingListenerConfigurationTest.java | 64 ++++++++++++-
.../QuotaThresholdMailingIntegrationTest.java | 30 ++++++
.../subscribers/QuotaThresholdNoticeTest.java | 102 +++++++++++++++++++++
.../src/test/resources/templates/html1.mustache | 1 +
.../src/test/resources/templates/html2.mustache | 1 +
9 files changed, 315 insertions(+), 14 deletions(-)
diff --git a/docs/modules/servers/partials/configure/listeners.adoc
b/docs/modules/servers/partials/configure/listeners.adoc
index f4b9d657ef..0328c20132 100644
--- a/docs/modules/servers/partials/configure/listeners.adoc
+++ b/docs/modules/servers/partials/configure/listeners.adoc
@@ -99,6 +99,11 @@ different occupation thresholds.
| bodyTemplate
| Mustache template for rendering the body of the warning email.
+| htmlBodyTemplate
+| Optional. Mustache template for rendering an HTML body of the warning email.
When specified, the email is sent as a
+`multipart/alternative` message combining the plain text body (from
`bodyTemplate`) and this HTML body. It is exposed the
+same rendering variables as `bodyTemplate`.
+
| thresholds
| Floating number between 0 and 1 representing the threshold of quota
occupation from which a mail should be sent.
Configuring several thresholds is supported.
diff --git
a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
index e10a8adb0d..60102d4e62 100644
---
a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
+++
b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
@@ -45,6 +45,7 @@ public class QuotaMailingListenerConfiguration {
interface XmlKeys {
String SUBJECT_TEMPLATE = "subjectTemplate";
String BODY_TEMPLATE = "bodyTemplate";
+ String HTML_BODY_TEMPLATE = "htmlBodyTemplate";
String GRACE_PERIOD = "gracePeriod";
String THRESHOLDS = "thresholds.threshold";
String THRESHOLD_VALUE = "value";
@@ -56,6 +57,7 @@ public class QuotaMailingListenerConfiguration {
.addThresholds(readThresholds(config))
.subjectTemplate(readSubjectTemplate(config))
.bodyTemplate(readBodyTemplate(config))
+ .htmlBodyTemplate(readHtmlBodyTemplate(config))
.gracePeriod(readGracePeriod(config))
.name(readName(config))
.build();
@@ -73,6 +75,10 @@ public class QuotaMailingListenerConfiguration {
return Optional.ofNullable(config.getString(XmlKeys.BODY_TEMPLATE,
null));
}
+ private static Optional<String>
readHtmlBodyTemplate(HierarchicalConfiguration<ImmutableNode> config) {
+ return
Optional.ofNullable(config.getString(XmlKeys.HTML_BODY_TEMPLATE, null));
+ }
+
private static Optional<Duration>
readGracePeriod(HierarchicalConfiguration<ImmutableNode> config) {
return Optional.ofNullable(config.getString(XmlKeys.GRACE_PERIOD,
null))
.map(string -> DurationParser.parse(string, ChronoUnit.DAYS));
@@ -85,7 +91,8 @@ public class QuotaMailingListenerConfiguration {
node.getDouble(XmlKeys.THRESHOLD_VALUE),
RenderingInformation.from(
Optional.ofNullable(node.getString(XmlKeys.BODY_TEMPLATE)),
-
Optional.ofNullable(node.getString(XmlKeys.SUBJECT_TEMPLATE)))))
+
Optional.ofNullable(node.getString(XmlKeys.SUBJECT_TEMPLATE)),
+
Optional.ofNullable(node.getString(XmlKeys.HTML_BODY_TEMPLATE)))))
.collect(ImmutableMap.toImmutableMap(
pair -> new QuotaThreshold(pair.getLeft()),
Pair::getRight));
@@ -94,22 +101,34 @@ public class QuotaMailingListenerConfiguration {
public static class RenderingInformation {
private final Optional<String> bodyTemplate;
private final Optional<String> subjectTemplate;
+ private final Optional<String> htmlBodyTemplate;
public static RenderingInformation from(Optional<String> bodyTemplate,
Optional<String> subjectTemplate) {
+ return from(bodyTemplate, subjectTemplate, Optional.empty());
+ }
+
+ public static RenderingInformation from(Optional<String> bodyTemplate,
Optional<String> subjectTemplate, Optional<String> htmlBodyTemplate) {
return new RenderingInformation(
bodyTemplate,
- subjectTemplate);
+ subjectTemplate,
+ htmlBodyTemplate);
}
public static RenderingInformation from(String bodyTemplate, String
subjectTemplate) {
return from(Optional.of(bodyTemplate),
Optional.of(subjectTemplate));
}
- private RenderingInformation(Optional<String> bodyTemplate,
Optional<String> subjectTemplate) {
+ public static RenderingInformation from(String bodyTemplate, String
subjectTemplate, String htmlBodyTemplate) {
+ return from(Optional.of(bodyTemplate),
Optional.of(subjectTemplate), Optional.of(htmlBodyTemplate));
+ }
+
+ private RenderingInformation(Optional<String> bodyTemplate,
Optional<String> subjectTemplate, Optional<String> htmlBodyTemplate) {
Preconditions.checkArgument(!bodyTemplate.equals(Optional.of("")),
"Pass a non empty bodyTemplate");
Preconditions.checkArgument(!subjectTemplate.equals(Optional.of("")), "Pass a
non empty subjectTemplate");
+
Preconditions.checkArgument(!htmlBodyTemplate.equals(Optional.of("")), "Pass a
non empty htmlBodyTemplate");
this.bodyTemplate = bodyTemplate;
this.subjectTemplate = subjectTemplate;
+ this.htmlBodyTemplate = htmlBodyTemplate;
}
public Optional<String> getBodyTemplate() {
@@ -120,20 +139,25 @@ public class QuotaMailingListenerConfiguration {
return subjectTemplate;
}
+ public Optional<String> getHtmlBodyTemplate() {
+ return htmlBodyTemplate;
+ }
+
@Override
public final boolean equals(Object o) {
if (o instanceof RenderingInformation) {
RenderingInformation that = (RenderingInformation) o;
return Objects.equals(this.bodyTemplate, that.bodyTemplate)
- && Objects.equals(this.subjectTemplate,
that.subjectTemplate);
+ && Objects.equals(this.subjectTemplate,
that.subjectTemplate)
+ && Objects.equals(this.htmlBodyTemplate,
that.htmlBodyTemplate);
}
return false;
}
@Override
public final int hashCode() {
- return Objects.hash(bodyTemplate, subjectTemplate);
+ return Objects.hash(bodyTemplate, subjectTemplate,
htmlBodyTemplate);
}
@Override
@@ -141,6 +165,7 @@ public class QuotaMailingListenerConfiguration {
return MoreObjects.toStringHelper(this)
.add("bodyTemplate", bodyTemplate)
.add("subjectTemplate", subjectTemplate)
+ .add("htmlBodyTemplate", htmlBodyTemplate)
.toString();
}
}
@@ -151,6 +176,7 @@ public class QuotaMailingListenerConfiguration {
private Optional<Duration> gradePeriod;
private Optional<String> bodyTemplate;
private Optional<String> subjectTemplate;
+ private Optional<String> htmlBodyTemplate;
private Optional<String> name;
private Builder() {
@@ -159,6 +185,7 @@ public class QuotaMailingListenerConfiguration {
gradePeriod = Optional.empty();
bodyTemplate = Optional.empty();
subjectTemplate = Optional.empty();
+ htmlBodyTemplate = Optional.empty();
name = Optional.empty();
}
@@ -206,11 +233,22 @@ public class QuotaMailingListenerConfiguration {
return this;
}
+ public Builder htmlBodyTemplate(String htmlBodyTemplate) {
+
Preconditions.checkArgument(!Strings.isNullOrEmpty(htmlBodyTemplate), "Pass a
non null/empty htmlBodyTemplate");
+ this.htmlBodyTemplate = Optional.of(htmlBodyTemplate);
+ return this;
+ }
+
public Builder bodyTemplate(Optional<String> bodyTemplate) {
bodyTemplate.ifPresent(this::bodyTemplate);
return this;
}
+ public Builder htmlBodyTemplate(Optional<String> htmlBodyTemplate) {
+ htmlBodyTemplate.ifPresent(this::htmlBodyTemplate);
+ return this;
+ }
+
public Builder subjectTemplate(Optional<String> subjectTemplate) {
subjectTemplate.ifPresent(this::subjectTemplate);
return this;
@@ -239,12 +277,15 @@ public class QuotaMailingListenerConfiguration {
gradePeriod.orElse(DEFAULT_GRACE_PERIOD),
bodyTemplate,
subjectTemplate,
+ htmlBodyTemplate,
name.orElse(DEFAULT_NAME));
}
}
public static final String DEFAULT_BODY_TEMPLATE =
FileSystem.CLASSPATH_PROTOCOL + "//templates/QuotaThresholdMailBody.mustache";
public static final String DEFAULT_SUBJECT_TEMPLATE =
FileSystem.CLASSPATH_PROTOCOL +
"//templates/QuotaThresholdMailSubject.mustache";
+ /** Not applied by default: HTML rendering needs to be explicitly opted
in. */
+ public static final String SAMPLE_HTML_BODY_TEMPLATE =
FileSystem.CLASSPATH_PROTOCOL +
"//templates/QuotaThresholdMailHtmlBody.mustache";
public static final RenderingInformation DEFAULT_RENDERING_INFORMATION =
RenderingInformation.from(Optional.empty(), Optional.empty());
public static final Duration DEFAULT_GRACE_PERIOD = Duration.ofDays(1);
private static final String DEFAULT_NAME = "default";
@@ -263,15 +304,18 @@ public class QuotaMailingListenerConfiguration {
private final Duration gracePeriod;
private final Optional<String> bodyTemplate;
private final Optional<String> subjectTemplate;
+ private final Optional<String> htmlBodyTemplate;
private final String name;
private QuotaMailingListenerConfiguration(ImmutableMap<QuotaThreshold,
RenderingInformation> toRenderingInformation,
- QuotaThresholds thresholds,
Duration gracePeriod, Optional<String> bodyTemplate, Optional<String>
subjectTemplate, String name) {
+ QuotaThresholds thresholds,
Duration gracePeriod, Optional<String> bodyTemplate, Optional<String>
subjectTemplate,
+ Optional<String>
htmlBodyTemplate, String name) {
this.toRenderingInformation = toRenderingInformation;
this.thresholds = thresholds;
this.gracePeriod = gracePeriod;
this.bodyTemplate = bodyTemplate;
this.subjectTemplate = subjectTemplate;
+ this.htmlBodyTemplate = htmlBodyTemplate;
this.name = name;
}
@@ -301,6 +345,17 @@ public class QuotaMailingListenerConfiguration {
.orElse(DEFAULT_SUBJECT_TEMPLATE);
}
+ /**
+ * @return the template to be rendered as a text/html alternative part, or
empty when HTML rendering is not opted in.
+ */
+ public Optional<String> getHtmlBodyTemplate(QuotaThreshold quotaThreshold)
{
+ return Optional
+ .ofNullable(
+ toRenderingInformation.get(quotaThreshold))
+ .flatMap(RenderingInformation::getHtmlBodyTemplate)
+ .or(() -> htmlBodyTemplate);
+ }
+
public String getName() {
return name;
}
@@ -315,6 +370,7 @@ public class QuotaMailingListenerConfiguration {
&& Objects.equals(this.gracePeriod, that.gracePeriod)
&& Objects.equals(this.subjectTemplate, that.subjectTemplate)
&& Objects.equals(this.bodyTemplate, that.bodyTemplate)
+ && Objects.equals(this.htmlBodyTemplate, that.htmlBodyTemplate)
&& Objects.equals(this.name, that.name);
}
return false;
@@ -322,7 +378,7 @@ public class QuotaMailingListenerConfiguration {
@Override
public final int hashCode() {
- return Objects.hash(toRenderingInformation, thresholds,
subjectTemplate, bodyTemplate, gracePeriod, name);
+ return Objects.hash(toRenderingInformation, thresholds,
subjectTemplate, bodyTemplate, htmlBodyTemplate, gracePeriod, name);
}
@Override
@@ -332,6 +388,7 @@ public class QuotaMailingListenerConfiguration {
.add("thresholds", thresholds)
.add("bodyTemplate", bodyTemplate)
.add("subjectTemplate", subjectTemplate)
+ .add("htmlBodyTemplate", htmlBodyTemplate)
.add("gracePeriod", gracePeriod)
.add("name", name)
.toString();
diff --git
a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
index 919883c29d..ced3ec6cc2 100644
---
a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
+++
b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
@@ -32,6 +32,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
+import jakarta.mail.MessagingException;
+
import org.apache.commons.io.IOUtils;
import org.apache.james.core.builder.MimeMessageBuilder;
import org.apache.james.core.quota.QuotaCountLimit;
@@ -46,6 +48,7 @@ import org.apache.james.mailbox.quota.model.QuotaThreshold;
import org.apache.james.mailbox.quota.model.QuotaThresholdChange;
import org.apache.james.util.SizeFormat;
+import com.github.fge.lambdas.Throwing;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
@@ -53,6 +56,8 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
public class QuotaThresholdNotice {
+ private static final String ALTERNATIVE_SUB_TYPE = "alternative";
+ private static final String TEXT_HTML_UTF8_TYPE = "text/html;
charset=UTF-8";
public static class Builder {
private Optional<QuotaThreshold> countThreshold;
@@ -135,10 +140,22 @@ public class QuotaThresholdNotice {
this.configuration = configuration;
}
- public MimeMessageBuilder generateMimeMessage(FileSystem fileSystem)
throws IOException {
- return MimeMessageBuilder.mimeMessageBuilder()
- .setSubject(generateSubject(fileSystem))
- .setText(generateReport(fileSystem));
+ public MimeMessageBuilder generateMimeMessage(FileSystem fileSystem)
throws IOException, MessagingException {
+ MimeMessageBuilder mimeMessageBuilder =
MimeMessageBuilder.mimeMessageBuilder()
+ .setSubject(generateSubject(fileSystem));
+
+ Optional<String> htmlReport = generateHtmlReport(fileSystem);
+ if (htmlReport.isEmpty()) {
+ return mimeMessageBuilder.setText(generateReport(fileSystem));
+ }
+ return
mimeMessageBuilder.setContent(MimeMessageBuilder.multipartBuilder()
+ .subType(ALTERNATIVE_SUB_TYPE)
+ .addBody(MimeMessageBuilder.bodyPartBuilder()
+ .data(generateReport(fileSystem))
+ .type(MimeMessageBuilder.DEFAULT_TEXT_PLAIN_UTF8_TYPE))
+ .addBody(MimeMessageBuilder.bodyPartBuilder()
+ .data(htmlReport.get())
+ .type(TEXT_HTML_UTF8_TYPE)));
}
@VisibleForTesting
@@ -153,6 +170,12 @@ public class QuotaThresholdNotice {
configuration.getBodyTemplate(mostSignificantThreshold()));
}
+ @VisibleForTesting
+ Optional<String> generateHtmlReport(FileSystem fileSystem) throws
IOException {
+ return configuration.getHtmlBodyTemplate(mostSignificantThreshold())
+ .map(Throwing.function((String template) ->
renderTemplate(fileSystem, template)).sneakyThrow());
+ }
+
private QuotaThreshold mostSignificantThreshold() {
return Stream.of(countThreshold, sizeThreshold)
.flatMap(Optional::stream)
@@ -162,7 +185,7 @@ public class QuotaThresholdNotice {
private String renderTemplate(FileSystem fileSystem, String template)
throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new
ByteArrayOutputStream();
- Writer writer = new OutputStreamWriter(byteArrayOutputStream)) {
+ Writer writer = new OutputStreamWriter(byteArrayOutputStream,
StandardCharsets.UTF_8)) {
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(getPatternReader(fileSystem,
template), "example");
diff --git
a/mailbox/plugin/quota-mailing/src/main/resources/templates/QuotaThresholdMailHtmlBody.mustache
b/mailbox/plugin/quota-mailing/src/main/resources/templates/QuotaThresholdMailHtmlBody.mustache
new file mode 100644
index 0000000000..a1cecfc6a3
--- /dev/null
+++
b/mailbox/plugin/quota-mailing/src/main/resources/templates/QuotaThresholdMailHtmlBody.mustache
@@ -0,0 +1,22 @@
+<html>
+<head>
+ <meta charset="UTF-8"/>
+</head>
+<body style="font-family: Helvetica, Arial, sans-serif; color: #333333;">
+<p>You receive this email because you recently exceeded a threshold related to
the quotas of your email account.</p>
+{{#hasExceededSizeThreshold}}
+<p>
+ You currently occupy more than <strong>{{sizeThreshold}} %</strong> of the
total size allocated to you.<br/>
+ You currently occupy <strong>{{usedSize}}</strong>{{#hasSizeLimit}} on a
total of <strong>{{limitSize}}</strong> allocated to you{{/hasSizeLimit}}.
+</p>
+{{/hasExceededSizeThreshold}}
+{{#hasExceededCountThreshold}}
+<p>
+ You currently occupy more than <strong>{{countThreshold}} %</strong> of
the total message count allocated to you.<br/>
+ You currently have <strong>{{usedCount}}</strong>
messages{{#hasCountLimit}} on a total of <strong>{{limitCount}}</strong>
allowed for you{{/hasCountLimit}}.
+</p>
+{{/hasExceededCountThreshold}}
+<p>You need to be aware that actions leading to exceeded quotas will be
denied. This will result in a degraded service.</p>
+<p>To mitigate this issue you might reach your administrator in order to
increase your configured quota. You might also delete some non important
emails.</p>
+</body>
+</html>
diff --git
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfigurationTest.java
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfigurationTest.java
index 919c2c7e02..46ecf2e851 100644
---
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfigurationTest.java
+++
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfigurationTest.java
@@ -44,6 +44,9 @@ public class QuotaMailingListenerConfigurationTest {
private static final String OTHER_BODY_TEMPLATE = "other_body.mustache";
private static final String YET_ANOTHER_SUBJECT_TEMPLATE =
"yet_another_sbj.mustache";
private static final String YET_ANOTHER_BODY_TEMPLATE =
"yet_another_body.mustache";
+ private static final String HTML_BODY_TEMPLATE = "html_body.mustache";
+ private static final String OTHER_HTML_BODY_TEMPLATE =
"other_html_body.mustache";
+ private static final String YET_ANOTHER_HTML_BODY_TEMPLATE =
"yet_another_html_body.mustache";
@Test
public void shouldMatchBeanContract() {
@@ -60,15 +63,18 @@ public class QuotaMailingListenerConfigurationTest {
" <value>0.85</value>" +
" <subjectTemplate>" + SUBJECT_TEMPLATE +
"</subjectTemplate>\n" +
" <bodyTemplate>" + BODY_TEMPLATE + "</bodyTemplate>\n" +
+ " <htmlBodyTemplate>" + HTML_BODY_TEMPLATE +
"</htmlBodyTemplate>\n" +
" </threshold>\n" +
" <threshold>\n" +
" <value>0.98</value>\n" +
" <subjectTemplate>" + OTHER_SUBJECT_TEMPLATE +
"</subjectTemplate>\n" +
" <bodyTemplate>" + OTHER_BODY_TEMPLATE +
"</bodyTemplate>\n" +
+ " <htmlBodyTemplate>" + OTHER_HTML_BODY_TEMPLATE +
"</htmlBodyTemplate>\n" +
" </threshold>\n" +
" </thresholds>\n" +
" <subjectTemplate>" + YET_ANOTHER_SUBJECT_TEMPLATE +
"</subjectTemplate>\n" +
" <bodyTemplate>" + YET_ANOTHER_BODY_TEMPLATE +
"</bodyTemplate>\n" +
+ " <htmlBodyTemplate>" + YET_ANOTHER_HTML_BODY_TEMPLATE +
"</htmlBodyTemplate>\n" +
" <gracePeriod>3 days</gracePeriod>\n" +
" <name>listener-name</name>\n" +
"</configuration>"));
@@ -78,12 +84,13 @@ public class QuotaMailingListenerConfigurationTest {
assertThat(result)
.isEqualTo(QuotaMailingListenerConfiguration.builder()
.addThreshold(new QuotaThreshold(0.85),
- RenderingInformation.from(BODY_TEMPLATE, SUBJECT_TEMPLATE))
+ RenderingInformation.from(BODY_TEMPLATE, SUBJECT_TEMPLATE,
HTML_BODY_TEMPLATE))
.addThreshold(new QuotaThreshold(0.98),
- RenderingInformation.from(OTHER_BODY_TEMPLATE,
OTHER_SUBJECT_TEMPLATE))
+ RenderingInformation.from(OTHER_BODY_TEMPLATE,
OTHER_SUBJECT_TEMPLATE, OTHER_HTML_BODY_TEMPLATE))
.gracePeriod(Duration.ofDays(3))
.subjectTemplate(YET_ANOTHER_SUBJECT_TEMPLATE)
.bodyTemplate(YET_ANOTHER_BODY_TEMPLATE)
+ .htmlBodyTemplate(YET_ANOTHER_HTML_BODY_TEMPLATE)
.name("listener-name")
.build());
}
@@ -215,6 +222,59 @@ public class QuotaMailingListenerConfigurationTest {
.isInstanceOf(IllegalArgumentException.class);
}
+ @Test
+ public void fromShouldNotSetHtmlBodyTemplateWhenOmitted() throws Exception
{
+ XMLConfiguration xmlConfiguration =
FileConfigurationProvider.getConfig(toStream(
+ "<configuration>\n" +
+ " <thresholds>\n" +
+ " <threshold>" +
+ " <value>0.85</value>" +
+ " <bodyTemplate>" + BODY_TEMPLATE + "</bodyTemplate>\n" +
+ " </threshold>\n" +
+ " </thresholds>\n" +
+ "</configuration>"));
+
+ QuotaMailingListenerConfiguration result =
QuotaMailingListenerConfiguration.from(xmlConfiguration);
+
+ assertThat(result.getHtmlBodyTemplate(new QuotaThreshold(0.85)))
+ .isEmpty();
+ }
+
+ @Test
+ public void
getHtmlBodyTemplateShouldFallbackToGlobalValueWhenThresholdValueIsOmitted()
throws Exception {
+ XMLConfiguration xmlConfiguration =
FileConfigurationProvider.getConfig(toStream(
+ "<configuration>\n" +
+ " <thresholds>\n" +
+ " <threshold>" +
+ " <value>0.85</value>" +
+ " </threshold>\n" +
+ " </thresholds>\n" +
+ " <htmlBodyTemplate>" + HTML_BODY_TEMPLATE +
"</htmlBodyTemplate>\n" +
+ "</configuration>"));
+
+ QuotaMailingListenerConfiguration result =
QuotaMailingListenerConfiguration.from(xmlConfiguration);
+
+ assertThat(result.getHtmlBodyTemplate(new QuotaThreshold(0.85)))
+ .contains(HTML_BODY_TEMPLATE);
+ }
+
+ @Test
+ public void fromShouldThrowOnEmptyHtmlBodyTemplate() throws Exception {
+ XMLConfiguration xmlConfiguration =
FileConfigurationProvider.getConfig(toStream(
+ "<configuration>\n" +
+ " <thresholds>\n" +
+ " <threshold>" +
+ " <value>0.85</value>" +
+ " <htmlBodyTemplate></htmlBodyTemplate>\n" +
+ " </threshold>\n" +
+ " </thresholds>\n" +
+ " <name>listener-name</name>\n" +
+ "</configuration>"));
+
+ assertThatThrownBy(() ->
QuotaMailingListenerConfiguration.from(xmlConfiguration))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
@Test
public void fromShouldThrowOnEmptyBodyTemplate() throws Exception {
XMLConfiguration xmlConfiguration =
FileConfigurationProvider.getConfig(toStream(
diff --git
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
index 3a44044139..6f675759ec 100644
---
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
+++
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
@@ -37,6 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
+import jakarta.mail.internet.MimeMultipart;
+
import org.apache.james.events.Event;
import org.apache.james.eventsourcing.eventstore.EventStore;
import org.apache.james.mailbox.quota.QuotaFixture.Counts;
@@ -45,6 +47,7 @@ import
org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
import org.apache.james.mailbox.store.event.EventFactory;
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.apache.mailet.base.test.FakeMailContext;
+import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
public interface QuotaThresholdMailingIntegrationTest {
@@ -316,6 +319,33 @@ public interface QuotaThresholdMailingIntegrationTest {
.hasSize(2);
}
+ @Test
+ default void
shouldSendMultipartAlternativeMailWhenHtmlTemplateIsConfigured(EventStore
store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new
QuotaThresholdListenersTestSystem(mailetContext, store,
+ QuotaMailingListenerConfiguration.builder()
+ .addThresholds(_50)
+
.htmlBodyTemplate(QuotaMailingListenerConfiguration.SAMPLE_HTML_BODY_TEMPLATE)
+ .gracePeriod(GRACE_PERIOD)
+ .build());
+
+ testee.event(eventBase()
+ .quotaCount(Counts._52_PERCENT)
+ .quotaSize(Sizes._30_PERCENT)
+ .instant(NOW)
+ .build());
+
+ MimeMultipart content = (MimeMultipart)
mailetContext.getSentMails().get(0).getMsg().getContent();
+
+ SoftAssertions softly = new SoftAssertions();
+
softly.assertThat(content.getContentType()).startsWith("multipart/alternative");
+
softly.assertThat(content.getBodyPart(0).getContentType()).startsWith("text/plain");
+ softly.assertThat((String) content.getBodyPart(1).getContent())
+ .startsWith("<html>")
+ .contains("You currently occupy more than <strong>50 %</strong> of
the total message count allocated to you.");
+ softly.assertAll();
+ }
+
@Test
default void shouldSendOneMailUponConcurrentEvents(EventStore store)
throws Exception {
FakeMailContext mailetContext = mailetContext();
diff --git
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
index 4f96458132..8d131e876d 100644
---
a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
+++
b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
@@ -29,6 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+
import org.apache.james.core.quota.QuotaCountLimit;
import org.apache.james.core.quota.QuotaCountUsage;
import org.apache.james.core.quota.QuotaSizeLimit;
@@ -449,6 +452,95 @@ class QuotaThresholdNoticeTest {
softly.assertAll();
}
+ @Test
+ void generateHtmlReportShouldBeEmptyWhenHtmlTemplateIsNotConfigured()
throws Exception {
+ QuotaThresholdChange sizeThresholdChange = new
QuotaThresholdChange(_80, NOW);
+
+ assertThat(noticeFor(DEFAULT_CONFIGURATION, sizeThresholdChange)
+ .generateHtmlReport(fileSystem))
+ .isEmpty();
+ }
+
+ @Test
+ void generateHtmlReportShouldUsePerThresholdTemplate() throws Exception {
+ QuotaMailingListenerConfiguration configuration =
QuotaMailingListenerConfiguration.builder()
+ .addThreshold(_80, RenderingInformation.from(
+ "classpath://templates/body1.mustache",
+ "classpath://templates/subject1.mustache",
+ "classpath://templates/html1.mustache"))
+ .htmlBodyTemplate("classpath://templates/html2.mustache")
+ .build();
+
+ assertThat(noticeFor(configuration, new QuotaThresholdChange(_80, NOW))
+ .generateHtmlReport(fileSystem))
+ .contains("[HTML_1]");
+ }
+
+ @Test
+ void
generateHtmlReportShouldFallbackToGlobalTemplateWhenSpecificThresholdValueIsOmitted()
throws Exception {
+ QuotaMailingListenerConfiguration configuration =
QuotaMailingListenerConfiguration.builder()
+ .addThreshold(_80, RenderingInformation.from(
+ "classpath://templates/body1.mustache",
+ "classpath://templates/subject1.mustache"))
+ .htmlBodyTemplate("classpath://templates/html2.mustache")
+ .build();
+
+ assertThat(noticeFor(configuration, new QuotaThresholdChange(_80, NOW))
+ .generateHtmlReport(fileSystem))
+ .contains("[HTML_2]");
+ }
+
+ @Test
+ void generateMimeMessageShouldBePlainTextWhenHtmlTemplateIsNotConfigured()
throws Exception {
+ MimeMessage mimeMessage = noticeFor(DEFAULT_CONFIGURATION, new
QuotaThresholdChange(_80, NOW))
+ .generateMimeMessage(fileSystem)
+ .build();
+
+ assertThat(mimeMessage.getContentType()).startsWith("text/plain");
+ }
+
+ @Test
+ void
generateMimeMessageShouldCombineBothAlternativesWhenHtmlTemplateIsConfigured()
throws Exception {
+ QuotaMailingListenerConfiguration configuration =
QuotaMailingListenerConfiguration.builder()
+ .bodyTemplate("classpath://templates/body1.mustache")
+ .htmlBodyTemplate("classpath://templates/html1.mustache")
+ .build();
+
+ MimeMessage mimeMessage = noticeFor(configuration, new
QuotaThresholdChange(_80, NOW))
+ .generateMimeMessage(fileSystem)
+ .build();
+ MimeMultipart multipart = (MimeMultipart) mimeMessage.getContent();
+
+ SoftAssertions softly = new SoftAssertions();
+
softly.assertThat(mimeMessage.getContentType()).startsWith("multipart/alternative");
+ softly.assertThat(multipart.getCount()).isEqualTo(2);
+
softly.assertThat(multipart.getBodyPart(0).getContentType()).startsWith("text/plain");
+
softly.assertThat(multipart.getBodyPart(0).getContent()).isEqualTo("[BODY_1]");
+
softly.assertThat(multipart.getBodyPart(1).getContentType()).startsWith("text/html");
+
softly.assertThat(multipart.getBodyPart(1).getContent()).isEqualTo("[HTML_1]");
+ softly.assertAll();
+ }
+
+ @Test
+ void defaultHtmlTemplateShouldGenerateAHumanReadableMessage() throws
Exception {
+ QuotaMailingListenerConfiguration configuration =
QuotaMailingListenerConfiguration.builder()
+
.htmlBodyTemplate(QuotaMailingListenerConfiguration.SAMPLE_HTML_BODY_TEMPLATE)
+ .build();
+
+ assertThat(QuotaThresholdNotice.builder()
+ .withConfiguration(configuration)
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._92_PERCENT)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(new
QuotaThresholdChange(_80, NOW), NotAlreadyReachedDuringGracePeriod))
+ .countThreshold(HistoryEvolution.higherThresholdReached(new
QuotaThresholdChange(_80, NOW), NotAlreadyReachedDuringGracePeriod))
+ .build()
+ .get()
+ .generateHtmlReport(fileSystem)
+ .get())
+ .contains("You currently occupy <strong>82 bytes</strong> on a
total of <strong>100 bytes</strong> allocated to you.")
+ .contains("You currently have <strong>92</strong> messages on a
total of <strong>100</strong> allowed for you.");
+ }
+
@Test
void
renderingShouldDefaultToDefaultValueWhenSpecificThresholdAndGlobalValueIsOmited()
throws Exception {
QuotaMailingListenerConfiguration configuration =
QuotaMailingListenerConfiguration.builder()
@@ -472,4 +564,14 @@ class QuotaThresholdNoticeTest {
.isEqualTo("[BODY_2]");
softly.assertAll();
}
+
+ private QuotaThresholdNotice noticeFor(QuotaMailingListenerConfiguration
configuration, QuotaThresholdChange sizeThresholdChange) {
+ return QuotaThresholdNotice.builder()
+ .withConfiguration(configuration)
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._92_PERCENT)
+
.sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange,
NotAlreadyReachedDuringGracePeriod))
+ .build()
+ .get();
+ }
}
\ No newline at end of file
diff --git
a/mailbox/plugin/quota-mailing/src/test/resources/templates/html1.mustache
b/mailbox/plugin/quota-mailing/src/test/resources/templates/html1.mustache
new file mode 100644
index 0000000000..eefc529170
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/resources/templates/html1.mustache
@@ -0,0 +1 @@
+[HTML_1]
\ No newline at end of file
diff --git
a/mailbox/plugin/quota-mailing/src/test/resources/templates/html2.mustache
b/mailbox/plugin/quota-mailing/src/test/resources/templates/html2.mustache
new file mode 100644
index 0000000000..45c98f33cf
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/resources/templates/html2.mustache
@@ -0,0 +1 @@
+[HTML_2]
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]