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