You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/12/05 07:10:15 UTC
[james-project] 08/17: JAMES-3431 DsnParameters POJO
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 1a0e727278eb5ccd2eb4b55bf94385524adb1df8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Dec 1 08:57:01 2020 +0700
JAMES-3431 DsnParameters POJO
---
.../main/java/org/apache/mailet/DsnParameters.java | 285 +++++++++++++++++++++
.../java/org/apache/mailet/DsnParametersTest.java | 285 +++++++++++++++++++++
2 files changed, 570 insertions(+)
diff --git a/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java b/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java
new file mode 100644
index 0000000..03f7479
--- /dev/null
+++ b/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java
@@ -0,0 +1,285 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you 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 *
+ * *
+ * http://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 org.apache.mailet;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Represents DSN parameters attached to the envelope of an Email transiting over SMTP
+ *
+ * See https://tools.ietf.org/html/rfc3461
+ */
+public class DsnParameters {
+ public static final String NOTIFY_PARAMETER = "NOTIFY";
+ public static final String RFC_822_PREFIX = "rfc822;";
+ public static final String ORCPT_PARAMETER = "ORCPT";
+ public static final String ENVID_PARAMETER = "ENVID";
+ public static final String RET_PARAMETER = "RET";
+
+ /**
+ * RET parameter allow the sender to control which part of the bounced message should be returned to the sender.
+ *
+ * Either headers or full.
+ *
+ * https://tools.ietf.org/html/rfc3461#section-4.3
+ */
+ public enum Ret {
+ FULL,
+ HDRS;
+
+ public static Optional<Ret> fromSMTPArgLine(Map<String, String> mailFromArgLine) {
+ return Optional.ofNullable(mailFromArgLine.get(RET_PARAMETER))
+ .map(input -> parse(input)
+ .orElseThrow(() -> new IllegalArgumentException(input + " is not a supported value for RET DSN parameter")));
+ }
+
+ public static Optional<Ret> parse(String string) {
+ Preconditions.checkNotNull(string);
+
+ return Arrays.stream(Ret.values())
+ .filter(value -> value.toString().equalsIgnoreCase(string))
+ .findAny();
+ }
+ }
+
+ /**
+ * ENVID allow the sender to correlate a bounce with a submission.
+ *
+ * https://tools.ietf.org/html/rfc3461#section-4.4
+ */
+ public static class EnvId {
+ public static Optional<EnvId> fromSMTPArgLine(Map<String, String> mailFromArgLine) {
+ return Optional.ofNullable(mailFromArgLine.get(ENVID_PARAMETER))
+ .map(EnvId::of);
+ }
+
+ public static EnvId of(String value) {
+ Preconditions.checkNotNull(value);
+
+ return new EnvId(value);
+ }
+
+ private final String value;
+
+ private EnvId(String value) {
+ this.value = value;
+ }
+
+ public String asString() {
+ return value;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof EnvId) {
+ EnvId that = (EnvId) o;
+
+ return Objects.equals(this.value, that.value);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(value);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("value", value)
+ .toString();
+ }
+ }
+
+ /**
+ * NOTIFY controls in which situations a bounce should be emited for a given recipient.
+ *
+ * https://tools.ietf.org/html/rfc3461#section-4.1
+ */
+ public enum Notify {
+ NEVER,
+ SUCCESS,
+ FAILURE,
+ DELAY;
+
+ public static EnumSet<Notify> parse(String input) {
+ Preconditions.checkNotNull(input);
+
+ return validate(EnumSet.copyOf(Splitter.on(",")
+ .omitEmptyStrings()
+ .trimResults()
+ .splitToList(input)
+ .stream()
+ .map(string -> parseValue(string)
+ .orElseThrow(() -> new IllegalArgumentException(string + " could not be associated with any RCPT NOTIFY value")))
+ .collect(Guavate.toImmutableList())));
+ }
+
+ public static Optional<Notify> parseValue(String input) {
+ return Arrays.stream(Notify.values())
+ .filter(value -> value.toString().equalsIgnoreCase(input))
+ .findAny();
+ }
+
+ private static EnumSet<Notify> validate(EnumSet<Notify> input) {
+ Preconditions.checkArgument(!input.contains(NEVER) || input.size() == 1,
+ "RCPT Notify should not contain over values when containing never");
+
+ return input;
+ }
+ }
+
+ /**
+ * Holds NOTIFY and ORCPT parameters for a specific recipient.
+ */
+ public static class RecipientDsnParameters {
+ public static Optional<RecipientDsnParameters> fromSMTPArgLine(Map<String, String> rcptToArgLine) {
+ Optional<EnumSet<Notify>> notifyParameter = Optional.ofNullable(rcptToArgLine.get(NOTIFY_PARAMETER))
+ .map(Notify::parse);
+ Optional<MailAddress> orcptParameter = Optional.ofNullable(rcptToArgLine.get(ORCPT_PARAMETER))
+ .map(RecipientDsnParameters::parseOrcpt);
+
+ if (notifyParameter.isEmpty() && orcptParameter.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(new RecipientDsnParameters(notifyParameter, orcptParameter));
+ }
+
+ private static MailAddress parseOrcpt(String input) {
+ Preconditions.checkArgument(input.startsWith(RFC_822_PREFIX), "ORCPT must start with the rfc822 prefix");
+ String addressPart = input.substring(RFC_822_PREFIX.length());
+ try {
+ return new MailAddress(addressPart);
+ } catch (AddressException e) {
+ throw new IllegalArgumentException(addressPart + " could not be parsed", e);
+ }
+ }
+
+ private final Optional<EnumSet<Notify>> notifyParameter;
+ private final Optional<MailAddress> orcptParameter;
+
+ RecipientDsnParameters(Optional<EnumSet<Notify>> notifyParameter, Optional<MailAddress> orcptParameter) {
+ this.notifyParameter = notifyParameter;
+ this.orcptParameter = orcptParameter;
+ }
+
+ public Optional<EnumSet<Notify>> getNotifyParameter() {
+ return notifyParameter;
+ }
+
+ public Optional<MailAddress> getOrcptParameter() {
+ return orcptParameter;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof RecipientDsnParameters) {
+ RecipientDsnParameters that = (RecipientDsnParameters) o;
+
+ return Objects.equals(this.notifyParameter, that.notifyParameter)
+ && Objects.equals(this.orcptParameter, that.orcptParameter);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(notifyParameter, orcptParameter);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("notifyParameter", notifyParameter)
+ .add("orcptParameter", orcptParameter)
+ .toString();
+ }
+ }
+
+ public static Optional<DsnParameters> of(Optional<EnvId> envIdParameter, Optional<Ret> retParameter, ImmutableMap<MailAddress, RecipientDsnParameters> rcptParameters) {
+ if (envIdParameter.isEmpty() && retParameter.isEmpty() && rcptParameters.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(new DsnParameters(envIdParameter, retParameter, rcptParameters));
+ }
+
+ private final Optional<EnvId> envIdParameter;
+ private final Optional<Ret> retParameter;
+ private final ImmutableMap<MailAddress, RecipientDsnParameters> rcptParameters;
+
+ DsnParameters(Optional<EnvId> envIdParameter, Optional<Ret> retParameter, ImmutableMap<MailAddress, RecipientDsnParameters> rcptParameters) {
+ this.envIdParameter = envIdParameter;
+ this.retParameter = retParameter;
+ this.rcptParameters = rcptParameters;
+ }
+
+ public Optional<EnvId> getEnvIdParameter() {
+ return envIdParameter;
+ }
+
+ public Optional<Ret> getRetParameter() {
+ return retParameter;
+ }
+
+ public ImmutableMap<MailAddress, RecipientDsnParameters> getRcptParameters() {
+ return rcptParameters;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof DsnParameters) {
+ DsnParameters that = (DsnParameters) o;
+
+ return Objects.equals(this.envIdParameter, that.envIdParameter)
+ && Objects.equals(this.retParameter, that.retParameter)
+ && Objects.equals(this.rcptParameters, that.rcptParameters);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(envIdParameter, retParameter, rcptParameters);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("envIdParameter", envIdParameter)
+ .add("retParameter", retParameter)
+ .add("rcptParameters", rcptParameters)
+ .toString();
+ }
+}
diff --git a/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java b/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java
new file mode 100644
index 0000000..ff0e6c3
--- /dev/null
+++ b/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java
@@ -0,0 +1,285 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you 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 *
+ * *
+ * http://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 org.apache.mailet;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.EnumSet;
+import java.util.Optional;
+
+import org.apache.james.core.MailAddress;
+import org.apache.mailet.DsnParameters.EnvId;
+import org.apache.mailet.DsnParameters.Notify;
+import org.apache.mailet.DsnParameters.RecipientDsnParameters;
+import org.apache.mailet.DsnParameters.Ret;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class DsnParametersTest {
+ @Test
+ void dsnParametersShouldRespectBeanContract() {
+ EqualsVerifier.forClass(DsnParameters.class).verify();
+ }
+
+ @Test
+ void recipientDsnParametersShouldRespectBeanContract() {
+ EqualsVerifier.forClass(RecipientDsnParameters.class).verify();
+ }
+
+ @Test
+ void envIdShouldRespectBeanContract() {
+ EqualsVerifier.forClass(EnvId.class).verify();
+ }
+
+ @Nested
+ class RetTest {
+ @Test
+ void parseShouldReturnEmptyOnUnknownValue() {
+ assertThat(Ret.parse("unknown")).isEmpty();
+ }
+
+ @Test
+ void parseShouldReturnEmptyOnEmptyValue() {
+ assertThat(Ret.parse("")).isEmpty();
+ }
+
+ @Test
+ void parseShouldThrowOnNullValue() {
+ assertThatThrownBy(() -> Ret.parse(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void parseShouldRecogniseHDRS() {
+ assertThat(Ret.parse("HDRS")).contains(Ret.HDRS);
+ }
+
+ @Test
+ void parseShouldRecogniseFull() {
+ assertThat(Ret.parse("FULL")).contains(Ret.FULL);
+ }
+
+ @Test
+ void parseShouldIgnoreCase() {
+ assertThat(Ret.parse("HdrS")).contains(Ret.HDRS);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenNoParameters() {
+ assertThat(Ret.fromSMTPArgLine(ImmutableMap.of()))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenOtherParameters() {
+ assertThat(Ret.fromSMTPArgLine(ImmutableMap.of("OTHER", "value")))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldRecogniseValidValues() {
+ assertThat(Ret.fromSMTPArgLine(ImmutableMap.of("RET", "HDRS")))
+ .contains(Ret.HDRS);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldThrowOnInvalidValue() {
+ assertThatThrownBy(() -> Ret.fromSMTPArgLine(ImmutableMap.of("RET", "invalid")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+ @Nested
+ class EnvIdTest {
+ @Test
+ void parseShouldReturnEmptyOnUnknownValue() {
+ assertThat(Ret.parse("unknown")).isEmpty();
+ }
+
+ @Test
+ void parseShouldReturnEmptyOnEmptyValue() {
+ assertThat(EnvId.of("").asString()).isEqualTo("");
+ }
+
+ @Test
+ void ofShouldThrowOnNullValue() {
+ assertThatThrownBy(() -> EnvId.of(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenNoParameters() {
+ assertThat(EnvId.fromSMTPArgLine(ImmutableMap.of()))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenOtherParameters() {
+ assertThat(EnvId.fromSMTPArgLine(ImmutableMap.of("OTHER", "value")))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldRecogniseValidValues() {
+ assertThat(EnvId.fromSMTPArgLine(ImmutableMap.of("ENVID", "valueee")))
+ .contains(EnvId.of("valueee"));
+ }
+ }
+
+ @Nested
+ class NotifyTest {
+ @Test
+ void parseShouldThrowOnUnknownValue() {
+ assertThatThrownBy(() -> Notify.parse("unknown"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void parseShouldThrowOnNullValue() {
+ assertThatThrownBy(() -> Notify.parse(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void parseShouldThrowOnEmptyValue() {
+ assertThatThrownBy(() -> Notify.parse(""))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void parseShouldThrowOnBlankValue() {
+ assertThatThrownBy(() -> Notify.parse(" "))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void parseShouldThrowComasValue() {
+ assertThatThrownBy(() -> Notify.parse(" , , "))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void parseShouldRecogniseNever() {
+ assertThat(Notify.parse("NEVER")).contains(Notify.NEVER);
+ }
+
+ @Test
+ void parseShouldRecogniseDelay() {
+ assertThat(Notify.parse("DELAY")).contains(Notify.DELAY);
+ }
+
+ @Test
+ void parseShouldRecogniseSuccess() {
+ assertThat(Notify.parse("SUCCESS")).contains(Notify.SUCCESS);
+ }
+
+ @Test
+ void parseShouldRecogniseFailure() {
+ assertThat(Notify.parse("FAILURE")).contains(Notify.FAILURE);
+ }
+
+ @Test
+ void parseShouldIgnoreCase() {
+ assertThat(Notify.parse("FaiLurE")).contains(Notify.FAILURE);
+ }
+
+ @Test
+ void parseShouldAcceptAllValues() {
+ assertThat(Notify.parse("SUCCESS,FAILURE,DELAY")).contains(Notify.FAILURE, Notify.SUCCESS, Notify.DELAY);
+ }
+
+ @Test
+ void parseShouldTrimValues() {
+ assertThat(Notify.parse("SUCCESS, FAILURE, DELAY")).contains(Notify.FAILURE, Notify.SUCCESS, Notify.DELAY);
+ }
+
+ @Test
+ void parseShouldIgnoreExtraComas() {
+ assertThat(Notify.parse("SUCCESS, ,FAILURE, DELAY")).contains(Notify.FAILURE, Notify.SUCCESS, Notify.DELAY);
+ }
+
+ @Test
+ void parseShouldThrowWhenNeverIsCombinedWithAnotherValue() {
+ assertThatThrownBy(() -> Notify.parse("NEVER,SUCCESS"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+ @Nested
+ class RecipientDsnParametersTest {
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenNoParameters() {
+ assertThat(RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of()))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldReturnEmptyWhenOtherParameters() {
+ assertThat(RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("OTHER", "value")))
+ .isEmpty();
+ }
+
+ @Test
+ void fromSMTPArgLineShouldAcceptOrcpt() throws Exception {
+ assertThat(RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("ORCPT", "rfc822;bob@apache.org")))
+ .contains(new RecipientDsnParameters(Optional.empty(), Optional.of(new MailAddress("bob@apache.org"))));
+ }
+
+ @Test
+ void fromSMTPArgShouldLineRejectUnknownOrcptAddressScheme() {
+ assertThatThrownBy(() -> RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("ORCPT", "other;bob@apache.org")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldRejectNoOrcptAddressScheme() {
+ assertThatThrownBy(() -> RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("ORCPT", "bob@apache.org")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldRejectInvalidOrcptAddress() {
+ assertThatThrownBy(() -> RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("ORCPT", "bob@apache@oups.org")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void fromSMTPArgLineShouldRejectInvalidNotifyArgument() {
+ assertThatThrownBy(() -> RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("NOTIFY", "NEVER,DELAY")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void fromSMTPArgLineAcceptNotifyArgument() {
+ assertThat(RecipientDsnParameters.fromSMTPArgLine(ImmutableMap.of("NOTIFY", "SUCCESS")))
+ .contains(new RecipientDsnParameters(Optional.of(EnumSet.of(Notify.SUCCESS)), Optional.empty()));
+ }
+ }
+
+ @Test
+ void ofShouldReturnEmptyWhenAllParamsAreEmpty() {
+ assertThat(DsnParameters.of(Optional.empty(), Optional.empty(), ImmutableMap.of()))
+ .isEmpty();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org