This is an automated email from the ASF dual-hosted git repository.

rcordier 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 dc51178a3c JAMES-4047 DeconnectionRight mailet (#2325)
dc51178a3c is described below

commit dc51178a3c11053d9c5c733decdce2ae2a7c1396
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Jul 3 05:37:39 2024 +0200

    JAMES-4047 DeconnectionRight mailet (#2325)
---
 .../pages/distributed/configure/mailets.adoc       |   2 +
 .../servers/partials/DeconnectionRight.adoc        |  17 ++
 .../james/transport/mailets/DeconnectionRight.java | 149 ++++++++++++++++
 .../transport/mailets/DeconnectionRightTest.java   | 195 +++++++++++++++++++++
 4 files changed, 363 insertions(+)

diff --git a/docs/modules/servers/pages/distributed/configure/mailets.adoc 
b/docs/modules/servers/pages/distributed/configure/mailets.adoc
index 1d7af110ab..d8ac82fc0c 100644
--- a/docs/modules/servers/pages/distributed/configure/mailets.adoc
+++ b/docs/modules/servers/pages/distributed/configure/mailets.adoc
@@ -20,6 +20,8 @@ include::partial$ContactExtractor.adoc[]
 
 include::partial$ConvertTo7Bit.adoc[]
 
+include::partial$DeconnectionRight.adoc[]
+
 include::partial$DKIMSign.adoc[]
 
 include::partial$DKIMVerify.adoc[]
diff --git a/docs/modules/servers/partials/DeconnectionRight.adoc 
b/docs/modules/servers/partials/DeconnectionRight.adoc
new file mode 100644
index 0000000000..b10df5652f
--- /dev/null
+++ b/docs/modules/servers/partials/DeconnectionRight.adoc
@@ -0,0 +1,17 @@
+=== DeconnectionRight
+
+Mailet to avoid people to receive emails outside working hour.
+
+Working hours are defined by a starting time and an end time, on weekdays from 
monday to friday.
+
+ Emails received outside working hours are to be delayed to next working hours.
+
+Example:
+
+....xml
+<mailet match="all" class="DeconnectionRight">
+    <zoneId>Europe/Paris</zoneId>
+    <workDayStart>07:00:00</workDayStart>
+    <workDayEnd>20:00:00</metricName>
+</mailet>
+....
diff --git 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DeconnectionRight.java
 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DeconnectionRight.java
new file mode 100644
index 0000000000..13b8d8dd8c
--- /dev/null
+++ 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/DeconnectionRight.java
@@ -0,0 +1,149 @@
+/****************************************************************
+ * 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.james.transport.mailets;
+
+import java.time.Clock;
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+import jakarta.mail.MessagingException;
+
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.GenericMailet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+/**
+ * Mailet to avoid people to receive emails outside working hour.<br/>
+ *
+ * Working hours are defined by a starting time and an end time, on weekdays 
from monday to friday.<br/>
+ *
+ * Emails received outside working hours are to be delayed to next working 
hours.<br/>
+ *
+ * Example:<br/>
+ *
+ * <pre>
+ *     <code>
+ * &lt;mailet match="all" class="DeconnectionRight"&gt;
+ *     &lt;zoneId&gt;Europe/Paris&lt;/zoneId&gt;
+ *     &lt;workDayStart&gt;07:00:00&lt;/workDayStart&gt;
+ *     &lt;workDayEnd&gt;20:00:00&lt;/metricName&gt;
+ * &lt;/mailet&gt;
+ *     </code>
+ * </pre>
+ */
+public class DeconnectionRight extends GenericMailet {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(DeconnectionRight.class);
+    private Clock clock;
+    private ZoneId zoneId;
+    private LocalTime workDayStart;
+    private LocalTime workDayEnd;
+
+    @Inject
+    public DeconnectionRight(Clock clock) {
+        this.clock = clock;
+    }
+
+    @VisibleForTesting
+    Optional<Duration> timeToWorkingHour(ZonedDateTime pointInTime) {
+        LocalTime localTime = pointInTime.toLocalTime();
+        DayOfWeek dayOfWeek = pointInTime.getDayOfWeek();
+        LocalDate localDate = pointInTime.toLocalDate();
+
+        if (dayOfWeek == DayOfWeek.SATURDAY) {
+            ZonedDateTime deliveryTime = localDate.plusDays(2)
+                .atTime(workDayStart)
+                .atZone(zoneId);
+
+            return Optional.of(Duration.between(pointInTime, deliveryTime));
+        }
+
+        if (dayOfWeek == DayOfWeek.SUNDAY) {
+            ZonedDateTime deliveryTime = localDate.plusDays(1)
+                .atTime(workDayStart)
+                .atZone(zoneId);
+
+            return Optional.of(Duration.between(pointInTime, deliveryTime));
+        }
+        if (localTime.equals(workDayStart) || localTime.equals(workDayEnd)) {
+            return Optional.empty();
+        }
+        if (localTime.isAfter(workDayStart) && localTime.isBefore(workDayEnd)) 
{
+            return Optional.empty();
+        }
+        if (localTime.isBefore(workDayStart)) {
+            ZonedDateTime deliveryTime = localDate
+                .atTime(workDayStart)
+                .atZone(zoneId);
+            return Optional.of(Duration.between(pointInTime, deliveryTime));
+        }
+        if (localTime.isAfter(workDayEnd) && 
dayOfWeek.equals(DayOfWeek.FRIDAY)) {
+            ZonedDateTime deliveryTime = localDate.plusDays(3)
+                .atTime(workDayStart)
+                .atZone(zoneId);
+            return Optional.of(Duration.between(pointInTime, deliveryTime));
+        }
+        if (localTime.isAfter(workDayEnd)) {
+            ZonedDateTime deliveryTime = localDate.plusDays(1)
+                .atTime(workDayStart)
+                .atZone(zoneId);
+
+            return Optional.of(Duration.between(pointInTime, deliveryTime));
+        }
+        LOGGER.error("Time at which mail was processed ({}) was not handled by 
{}", pointInTime, DeconnectionRight.class);
+        return Optional.empty();
+    }
+
+    @Override
+    public void service(Mail mail) throws MessagingException {
+        ZonedDateTime now = ZonedDateTime.now(clock)
+            .withZoneSameInstant(zoneId);
+
+        timeToWorkingHour(now)
+            .ifPresent(duration -> {
+                try {
+                    getMailetContext().sendMail(mail, Mail.DEFAULT, 
duration.getSeconds(), TimeUnit.SECONDS);
+                } catch (MessagingException e) {
+                    throw new RuntimeException(e);
+                }
+                // discard this mail and keep the scheduled copy
+                mail.setState(Mail.GHOST);
+            });
+    }
+
+    @Override
+    public void init() throws MessagingException {
+        zoneId = ZoneId.of(getInitParameter("zoneId"));
+        workDayStart = LocalTime.parse(getInitParameter("workDayStart"));
+        workDayEnd = LocalTime.parse(getInitParameter("workDayEnd"));
+
+        Preconditions.checkArgument(workDayEnd.isAfter(workDayStart));
+    }
+}
diff --git 
a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DeconnectionRightTest.java
 
b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DeconnectionRightTest.java
new file mode 100644
index 0000000000..3aab478d44
--- /dev/null
+++ 
b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/DeconnectionRightTest.java
@@ -0,0 +1,195 @@
+/****************************************************************
+ * 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.james.transport.mailets;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.test.FakeMail;
+import org.apache.mailet.base.test.FakeMailContext;
+import org.apache.mailet.base.test.FakeMailetConfig;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class DeconnectionRightTest {
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "2024-07-01T07:00:00.00Z",
+        "2024-07-01T10:15:30.00Z",
+        "2024-07-01T18:00:00.00Z",
+        "2024-07-01T18:00:00.01Z",
+        "2024-07-01T20:00:00.00Z"})
+    // 2024-07-01 => monday
+    void noDelaywithUTC(String dateTime) throws Exception {
+        Clock clock = Clock.fixed(Instant.parse(dateTime), ZoneId.of("UTC"));
+        DeconnectionRight testee = new DeconnectionRight(clock);
+        FakeMailContext mailetContext = FakeMailContext.defaultContext();
+        testee.init(FakeMailetConfig.builder()
+                .mailetContext(mailetContext)
+                .setProperty("zoneId", "UTC")
+                .setProperty("workDayStart", "07:00:00")
+                .setProperty("workDayEnd", "20:00:00")
+            .build());
+
+        FakeMail mail = 
FakeMail.builder().name("aMail").state(Mail.DEFAULT).build();
+        testee.service(mail);
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mail.getState()).isEqualTo(Mail.DEFAULT);
+            softly.assertThat(mailetContext.getSentMails()).isEmpty();
+        });
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "2024-07-01T20:00:00.01Z",
+        "2024-07-01T21:00:00.01Z",
+        "2024-07-02T03:00:00.01Z",
+        "2024-07-02T06:59:59.99Z"
+    })
+    // 2024-07-01 => monday
+    // 2024-07-01 => tuesday
+    void tomorrowWithUTC(String dateTime) throws Exception {
+        Instant now = Instant.parse(dateTime);
+        ZoneId zone = ZoneId.of("UTC");
+        Clock clock = Clock.fixed(now, zone);
+
+        DeconnectionRight testee = new DeconnectionRight(clock);
+        FakeMailContext mailetContext = FakeMailContext.defaultContext();
+        testee.init(FakeMailetConfig.builder()
+                .mailetContext(mailetContext)
+                .setProperty("zoneId", "UTC")
+                .setProperty("workDayStart", "07:00:00")
+                .setProperty("workDayEnd", "20:00:00")
+            .build());
+
+        FakeMail mail = 
FakeMail.builder().name("aMail").state(Mail.DEFAULT).build();
+        testee.service(mail);
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mail.getState()).isEqualTo(Mail.GHOST);
+            softly.assertThat(mailetContext.getSentMails()).hasSize(1);
+            
softly.assertThat(mailetContext.getSentMails().get(0).getDelay().get()).isEqualTo(new
 FakeMailContext.Delay(
+                Duration.between(ZonedDateTime.ofInstant(now, zone),
+                    
ZonedDateTime.ofInstant(Instant.parse("2024-07-02T07:00:00.00Z"), zone))
+                        .toSeconds(), TimeUnit.SECONDS
+            ));
+        });
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "2024-07-05T20:00:00.01Z",
+        "2024-07-05T21:00:00.01Z",
+        "2024-07-06T03:00:00.01Z",
+        "2024-07-07T03:00:00.01Z",
+        "2024-07-08T06:59:59.99Z"
+    })
+    // 2024-07-01 => monday
+    void afterWeekendWithUTC(String dateTime) throws Exception {
+        Instant now = Instant.parse(dateTime);
+        ZoneId zone = ZoneId.of("UTC");
+        Clock clock = Clock.fixed(now, zone);
+
+        DeconnectionRight testee = new DeconnectionRight(clock);
+        FakeMailContext mailetContext = FakeMailContext.defaultContext();
+        testee.init(FakeMailetConfig.builder()
+                .mailetContext(mailetContext)
+                .setProperty("zoneId", "UTC")
+                .setProperty("workDayStart", "07:00:00")
+                .setProperty("workDayEnd", "20:00:00")
+            .build());
+
+        FakeMail mail = 
FakeMail.builder().name("aMail").state(Mail.DEFAULT).build();
+        testee.service(mail);
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mail.getState()).isEqualTo(Mail.GHOST);
+            softly.assertThat(mailetContext.getSentMails()).hasSize(1);
+            
softly.assertThat(mailetContext.getSentMails().get(0).getDelay().get()).isEqualTo(new
 FakeMailContext.Delay(
+                Duration.between(ZonedDateTime.ofInstant(now, zone),
+                    
ZonedDateTime.ofInstant(Instant.parse("2024-07-08T07:00:00.00Z"), zone))
+                        .toSeconds(), TimeUnit.SECONDS
+            ));
+        });
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "2024-07-01T05:00:00.00Z",
+        "2024-07-01T10:15:30.00Z",
+        "2024-07-01T18:00:00.00Z"})
+    // 2024-07-01 => monday
+    void noDelaywithUTCPlus2(String dateTime) throws Exception {
+        Clock clock = Clock.fixed(Instant.parse(dateTime), ZoneId.of("UTC"));
+        DeconnectionRight testee = new DeconnectionRight(clock);
+        FakeMailContext mailetContext = FakeMailContext.defaultContext();
+        testee.init(FakeMailetConfig.builder()
+            .mailetContext(mailetContext)
+            .setProperty("zoneId", "Europe/Paris")
+            .setProperty("workDayStart", "07:00:00")
+            .setProperty("workDayEnd", "20:00:00")
+            .build());
+
+        FakeMail mail = 
FakeMail.builder().name("aMail").state(Mail.DEFAULT).build();
+        testee.service(mail);
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mail.getState()).isEqualTo(Mail.DEFAULT);
+            softly.assertThat(mailetContext.getSentMails()).isEmpty();
+        });
+    }
+
+    @Test// 2024-03-30 => saturday before time change
+    void handleTimeChange() throws Exception {
+        Instant now = Instant.parse("2024-03-30T05:00:00.00Z");
+        ZoneId zone = ZoneId.of("UTC");
+        Clock clock = Clock.fixed(now, ZoneId.of("UTC"));
+        DeconnectionRight testee = new DeconnectionRight(clock);
+        FakeMailContext mailetContext = FakeMailContext.defaultContext();
+        testee.init(FakeMailetConfig.builder()
+            .mailetContext(mailetContext)
+            .setProperty("zoneId", "Europe/Paris")
+            .setProperty("workDayStart", "07:00:00")
+            .setProperty("workDayEnd", "20:00:00")
+            .build());
+
+        FakeMail mail = 
FakeMail.builder().name("aMail").state(Mail.DEFAULT).build();
+        testee.service(mail);
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mail.getState()).isEqualTo(Mail.GHOST);
+            softly.assertThat(mailetContext.getSentMails()).hasSize(1);
+            
softly.assertThat(mailetContext.getSentMails().get(0).getDelay().get()).isEqualTo(new
 FakeMailContext.Delay(
+                Duration.between(ZonedDateTime.ofInstant(now, zone),
+                        // 7am on next monday besides time change
+                        
ZonedDateTime.ofInstant(Instant.parse("2024-04-01T05:00:00.00Z"), 
ZoneId.of("UTC")))
+                    .toSeconds(), TimeUnit.SECONDS
+            ));
+        });
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to