diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/BaseEmailTest.java b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/BaseEmailTest.java
index c7c79e5df2..22b13a35c2 100644
--- a/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/BaseEmailTest.java
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/BaseEmailTest.java
@@ -38,7 +38,6 @@ public class BaseEmailTest {
private static final GreenMail greenMail = new GreenMail();
@TempDir File tempDir;
private GreenMailUser greenMailUser = greenMail.setUser("test@camunda.com", "password");
- ;
@BeforeAll
static void setup() {
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/InboundEmailTest.java b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/InboundEmailTest.java
new file mode 100644
index 0000000000..6431a69cd1
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/InboundEmailTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information regarding copyright
+ * ownership. Camunda licenses this file to you under the Apache License,
+ * Version 2.0; 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 io.camunda.connector.e2e;
+
+import static io.camunda.connector.e2e.BpmnFile.replace;
+import static org.mockito.Mockito.when;
+
+import io.camunda.connector.e2e.app.TestConnectorRuntimeApplication;
+import io.camunda.connector.runtime.inbound.state.ProcessImportResult;
+import io.camunda.connector.runtime.inbound.state.ProcessStateStore;
+import io.camunda.operate.CamundaOperateClient;
+import io.camunda.operate.exception.OperateException;
+import io.camunda.operate.model.ProcessDefinition;
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
+import io.camunda.zeebe.model.bpmn.instance.Process;
+import io.camunda.zeebe.process.test.assertions.BpmnAssert;
+import io.camunda.zeebe.spring.test.ZeebeSpringTest;
+import jakarta.mail.Flags;
+import jakarta.mail.MessagingException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.awaitility.core.ConditionTimeoutException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(
+ classes = {TestConnectorRuntimeApplication.class},
+ properties = {
+ "spring.main.allow-bean-definition-overriding=true",
+ },
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ZeebeSpringTest
+@ExtendWith(MockitoExtension.class)
+public class InboundEmailTest extends BaseEmailTest {
+
+ private static final ScheduledExecutorService scheduler =
+ Executors.newSingleThreadScheduledExecutor();
+ private static AtomicLong counter = new AtomicLong(1);
+ @Autowired ProcessStateStore processStateStore;
+ @Autowired CamundaOperateClient camundaOperateClient;
+ @Mock private ProcessDefinition processDef;
+ @Autowired private ZeebeClient zeebeClient;
+
+ @BeforeEach
+ public void beforeEach() {
+ super.reset();
+ }
+
+ @Test
+ public void shouldReceiveEmailAndSetAsSeen() throws OperateException, MessagingException {
+ var model =
+ replace(
+ "email-inbound-connector-intermediate_unseen_read.bpmn",
+ BpmnFile.Replace.replace("55555", super.getUnsecureImapPort()));
+
+ mockProcessDefinition(model);
+
+ scheduler.schedule(
+ () -> super.sendEmail("test@camunda.com", "test", "hey"), 2, TimeUnit.SECONDS);
+
+ processStateStore.update(
+ new ProcessImportResult(
+ Map.of(
+ new ProcessImportResult.ProcessDefinitionIdentifier(
+ processDef.getBpmnProcessId(), processDef.getTenantId()),
+ new ProcessImportResult.ProcessDefinitionVersion(
+ processDef.getKey(), processDef.getVersion().intValue()))));
+ var bpmnTest = ZeebeTest.with(zeebeClient).deploy(model).createInstance();
+
+ bpmnTest = bpmnTest.waitForProcessCompletion();
+
+ Assertions.assertTrue(
+ Arrays.stream(super.getLastReceivedEmails())
+ .findFirst()
+ .get()
+ .getFlags()
+ .contains(Flags.Flag.SEEN));
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("subject", "test");
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("plainTextBody", "hey");
+ }
+
+ @Test
+ public void shouldThrowWhenAllMessageAreSeen() throws OperateException, MessagingException {
+ var model =
+ replace(
+ "email-inbound-connector-intermediate_unseen_read.bpmn",
+ BpmnFile.Replace.replace("55555", super.getUnsecureImapPort()));
+
+ mockProcessDefinition(model);
+
+ super.sendEmail("test@camunda.com", "test", "hey");
+
+ Arrays.stream(getLastReceivedEmails()).findFirst().get().setFlag(Flags.Flag.SEEN, true);
+
+ processStateStore.update(
+ new ProcessImportResult(
+ Map.of(
+ new ProcessImportResult.ProcessDefinitionIdentifier(
+ processDef.getBpmnProcessId(), processDef.getTenantId()),
+ new ProcessImportResult.ProcessDefinitionVersion(
+ processDef.getKey(), processDef.getVersion().intValue()))));
+ var bpmnTest = ZeebeTest.with(zeebeClient).deploy(model).createInstance();
+
+ Assertions.assertThrows(ConditionTimeoutException.class, bpmnTest::waitForProcessCompletion);
+ }
+
+ @Test
+ public void shouldReceiveEmailAndDelete() throws OperateException, MessagingException {
+ var model =
+ replace(
+ "email-inbound-connector-intermediate_unseen_delete.bpmn",
+ BpmnFile.Replace.replace("55555", super.getUnsecureImapPort()));
+
+ mockProcessDefinition(model);
+
+ scheduler.schedule(
+ () -> super.sendEmail("test@camunda.com", "test", "hey"), 2, TimeUnit.SECONDS);
+
+ processStateStore.update(
+ new ProcessImportResult(
+ Map.of(
+ new ProcessImportResult.ProcessDefinitionIdentifier(
+ processDef.getBpmnProcessId(), processDef.getTenantId()),
+ new ProcessImportResult.ProcessDefinitionVersion(
+ processDef.getKey(), processDef.getVersion().intValue()))));
+
+ var bpmnTest = ZeebeTest.with(zeebeClient).deploy(model).createInstance();
+
+ bpmnTest = bpmnTest.waitForProcessCompletion();
+
+ Assertions.assertTrue(
+ Arrays.stream(super.getLastReceivedEmails())
+ .findFirst()
+ .get()
+ .getFlags()
+ .contains(Flags.Flag.DELETED));
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("subject", "test");
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("plainTextBody", "hey");
+ }
+
+ @Test
+ public void shouldReceiveEmailAndMove() throws OperateException, MessagingException {
+ var model =
+ replace(
+ "email-inbound-connector-intermediate_unseen_move.bpmn",
+ BpmnFile.Replace.replace("55555", super.getUnsecureImapPort()));
+
+ mockProcessDefinition(model);
+
+ scheduler.schedule(
+ () -> super.sendEmail("test@camunda.com", "test", "hey"), 2, TimeUnit.SECONDS);
+
+ processStateStore.update(
+ new ProcessImportResult(
+ Map.of(
+ new ProcessImportResult.ProcessDefinitionIdentifier(
+ processDef.getBpmnProcessId(), processDef.getTenantId()),
+ new ProcessImportResult.ProcessDefinitionVersion(
+ processDef.getKey(), processDef.getVersion().intValue()))));
+
+ var bpmnTest = ZeebeTest.with(zeebeClient).deploy(model).createInstance();
+
+ bpmnTest = bpmnTest.waitForProcessCompletion();
+
+ Assertions.assertEquals(2, getLastReceivedEmails().length);
+ Assertions.assertTrue(
+ Arrays.stream(getLastReceivedEmails())
+ .findFirst()
+ .get()
+ .getFlags()
+ .contains(Flags.Flag.DELETED));
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("subject", "test");
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("plainTextBody", "hey");
+ }
+
+ private void mockProcessDefinition(BpmnModelInstance model) throws OperateException {
+ when(camundaOperateClient.getProcessDefinitionModel(1L)).thenReturn(model);
+ when(processDef.getKey()).thenReturn(1L);
+ when(processDef.getTenantId()).thenReturn(zeebeClient.getConfiguration().getDefaultTenantId());
+ when(processDef.getBpmnProcessId())
+ .thenReturn(model.getModelElementsByType(Process.class).stream().findFirst().get().getId());
+ when(processDef.getVersion()).thenReturn(counter.getAndIncrement());
+ }
+
+ @Test
+ public void shouldPollEmailAndMove() throws OperateException, MessagingException {
+ var model =
+ replace(
+ "email-inbound-connector-intermediate_all_delete.bpmn",
+ BpmnFile.Replace.replace("55555", super.getUnsecureImapPort()));
+
+ mockProcessDefinition(model);
+
+ super.sendEmail("test@camunda.com", "test", "hey");
+
+ Arrays.stream(super.getLastReceivedEmails()).findFirst().get().setFlag(Flags.Flag.SEEN, true);
+
+ processStateStore.update(
+ new ProcessImportResult(
+ Map.of(
+ new ProcessImportResult.ProcessDefinitionIdentifier(
+ processDef.getBpmnProcessId(), processDef.getTenantId()),
+ new ProcessImportResult.ProcessDefinitionVersion(
+ processDef.getKey(), processDef.getVersion().intValue()))));
+
+ var bpmnTest = ZeebeTest.with(zeebeClient).deploy(model).createInstance();
+
+ bpmnTest = bpmnTest.waitForProcessCompletion();
+
+ Assertions.assertTrue(
+ Arrays.stream(super.getLastReceivedEmails())
+ .findFirst()
+ .get()
+ .getFlags()
+ .contains(Flags.Flag.DELETED));
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("subject", "test");
+ BpmnAssert.assertThat(bpmnTest.getProcessInstanceEvent())
+ .hasVariableWithValue("plainTextBody", "hey");
+ }
+}
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/EmailTests.java b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/OutboundEmailTests.java
similarity index 99%
rename from connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/EmailTests.java
rename to connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/OutboundEmailTests.java
index d8326142fa..36cad8a345 100644
--- a/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/EmailTests.java
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/java/io/camunda/connector/e2e/OutboundEmailTests.java
@@ -47,7 +47,7 @@
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ZeebeSpringTest
@ExtendWith(MockitoExtension.class)
-public class EmailTests extends BaseEmailTest {
+public class OutboundEmailTests extends BaseEmailTest {
private static final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_delete.bpmn b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_delete.bpmn
new file mode 100644
index 0000000000..76a9b9c9f7
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_delete.bpmn
@@ -0,0 +1,64 @@
+
+
+
+
+ Flow_1vp6srd
+
+
+ Flow_0lofu08
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0lofu08
+ Flow_1vp6srd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_move.bpmn b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_move.bpmn
new file mode 100644
index 0000000000..27d4d11d4e
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_all_move.bpmn
@@ -0,0 +1,65 @@
+
+
+
+
+ Flow_1vp6srd
+
+
+ Flow_0lofu08
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0lofu08
+ Flow_1vp6srd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_delete.bpmn b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_delete.bpmn
new file mode 100644
index 0000000000..6ae220a00f
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_delete.bpmn
@@ -0,0 +1,64 @@
+
+
+
+
+ Flow_1vp6srd
+
+
+ Flow_0lofu08
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0lofu08
+ Flow_1vp6srd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_move.bpmn b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_move.bpmn
new file mode 100644
index 0000000000..1b88dff42c
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_move.bpmn
@@ -0,0 +1,65 @@
+
+
+
+
+ Flow_1vp6srd
+
+
+ Flow_0lofu08
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0lofu08
+ Flow_1vp6srd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_read.bpmn b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_read.bpmn
new file mode 100644
index 0000000000..ce2472e664
--- /dev/null
+++ b/connectors-e2e-test/connectors-e2e-test-mail/src/test/resources/email-inbound-connector-intermediate_unseen_read.bpmn
@@ -0,0 +1,64 @@
+
+
+
+
+ Flow_1vp6srd
+
+
+ Flow_0lofu08
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0lofu08
+ Flow_1vp6srd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/JakartaEmailListener.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/JakartaEmailListener.java
index 78b4958ce0..452ca52799 100644
--- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/JakartaEmailListener.java
+++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/JakartaEmailListener.java
@@ -11,12 +11,9 @@
import io.camunda.connector.email.client.jakarta.utils.JakartaUtils;
import java.util.Objects;
import java.util.concurrent.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class JakartaEmailListener implements EmailListener {
- private static final Logger log = LoggerFactory.getLogger(JakartaEmailListener.class);
private ScheduledExecutorService scheduledExecutorService;
private PollingManager pollingManager;
diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/PollingManager.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/PollingManager.java
index ea7cf8cc16..b604379f31 100644
--- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/PollingManager.java
+++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/inbound/PollingManager.java
@@ -17,23 +17,29 @@
import java.util.Arrays;
import java.util.Objects;
import org.eclipse.angus.mail.imap.IMAPMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class PollingManager {
+ private static final Logger log = LoggerFactory.getLogger(PollingManager.class);
private final InboundConnectorContext connectorContext;
private final EmailListenerConfig emailListenerConfig;
private final JakartaUtils jakartaUtils;
private final Folder folder;
private final Store store;
+ private final Authentication authentication;
public PollingManager(
InboundConnectorContext connectorContext,
EmailListenerConfig emailListenerConfig,
+ Authentication authentication,
JakartaUtils jakartaUtils,
Folder folder,
Store store) {
this.connectorContext = connectorContext;
this.emailListenerConfig = emailListenerConfig;
+ this.authentication = authentication;
this.jakartaUtils = jakartaUtils;
this.folder = folder;
this.store = store;
@@ -52,16 +58,15 @@ public static PollingManager create(
jakartaUtils.createSession(emailInboundConnectorProperties.data().imapConfig());
store = session.getStore();
jakartaUtils.connectStore(store, authentication);
- folder =
- jakartaUtils.findImapFolder(
- store.getDefaultFolder(), emailListenerConfig.folderToListen());
+ folder = jakartaUtils.findImapFolder(store, emailListenerConfig.folderToListen());
folder.open(Folder.READ_WRITE);
if (emailListenerConfig.pollingConfig().handlingStrategy().equals(HandlingStrategy.MOVE)
&& (Objects.isNull(emailListenerConfig.pollingConfig().targetFolder())
|| emailListenerConfig.pollingConfig().targetFolder().isBlank()))
throw new RuntimeException(
"If the post process action is `MOVE`, a target folder must be specified");
- return new PollingManager(connectorContext, emailListenerConfig, jakartaUtils, folder, store);
+ return new PollingManager(
+ connectorContext, emailListenerConfig, authentication, jakartaUtils, folder, store);
} catch (MessagingException e) {
try {
if (folder != null && folder.isOpen()) {
@@ -78,12 +83,32 @@ public static PollingManager create(
}
public void poll() {
+ this.prepareForPolling();
switch (this.emailListenerConfig.pollingConfig()) {
case PollAll pollAll -> pollAllAndProcess(pollAll);
case PollUnseen pollUnseen -> pollUnseenAndProcess(pollUnseen);
}
}
+ private void prepareForPolling() {
+ if (!this.store.isConnected()) {
+ try {
+ this.jakartaUtils.connectStore(store, authentication);
+ } catch (MessagingException e) {
+ log.error("Could not reconnect to store", e);
+ throw new RuntimeException("Could not reconnect to store");
+ }
+ }
+ if (!this.folder.isOpen()) {
+ try {
+ this.folder.open(Folder.READ_WRITE);
+ } catch (MessagingException e) {
+ log.error("Could not reopen folder", e);
+ throw new RuntimeException("Could not reopen folder");
+ }
+ }
+ }
+
private void pollAllAndProcess(PollAll pollAll) {
try {
Message[] messages = this.folder.getMessages();
diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java
index c0040c4d8a..69e2fa4340 100644
--- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java
+++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java
@@ -64,9 +64,8 @@ private List imapSearchEmails(
ImapSearchEmails imapSearchEmails, Authentication authentication, Session session) {
try (Store store = session.getStore()) {
this.jakartaUtils.connectStore(store, authentication);
- Folder defaultFolder = store.getDefaultFolder();
String targetFolder = imapSearchEmails.searchEmailFolder();
- try (Folder imapFolder = this.jakartaUtils.findImapFolder(defaultFolder, targetFolder)) {
+ try (Folder imapFolder = this.jakartaUtils.findImapFolder(store, targetFolder)) {
return searchEmails(imapFolder, imapSearchEmails.criteria());
}
} catch (MessagingException e) {
@@ -78,9 +77,8 @@ private ReadEmailResponse imapReadEmail(
ImapReadEmail imapReadEmail, Authentication authentication, Session session) {
try (Store store = session.getStore()) {
this.jakartaUtils.connectStore(store, authentication);
- Folder defaultFolder = store.getDefaultFolder();
String targetFolder = imapReadEmail.readEmailFolder();
- try (Folder imapFolder = this.jakartaUtils.findImapFolder(defaultFolder, targetFolder)) {
+ try (Folder imapFolder = this.jakartaUtils.findImapFolder(store, targetFolder)) {
imapFolder.open(Folder.READ_ONLY);
Message[] messages = imapFolder.search(new MessageIDTerm(imapReadEmail.messageId()));
return Arrays.stream(messages)
@@ -108,9 +106,8 @@ private DeleteEmailResponse imapDeleteEmail(
ImapDeleteEmail imapDeleteEmail, Authentication authentication, Session session) {
try (Store store = session.getStore()) {
this.jakartaUtils.connectStore(store, authentication);
- Folder defaultFolder = store.getDefaultFolder();
String targetFolder = imapDeleteEmail.deleteEmailFolder();
- try (Folder folder = this.jakartaUtils.findImapFolder(defaultFolder, targetFolder)) {
+ try (Folder folder = this.jakartaUtils.findImapFolder(store, targetFolder)) {
return deleteEmail(folder, imapDeleteEmail.messageId());
}
} catch (MessagingException e) {
@@ -122,18 +119,9 @@ private MoveEmailResponse imapMoveEmails(
ImapMoveEmail imapMoveEmail, Authentication authentication, Session session) {
try (Store store = session.getStore()) {
this.jakartaUtils.connectStore(store, authentication);
- Folder rootFolder = store.getDefaultFolder();
String fromFolder = imapMoveEmail.fromFolder();
- String toFolder = imapMoveEmail.toFolder();
- Folder sourceImapFolder = this.jakartaUtils.findImapFolder(rootFolder, fromFolder);
- if (!sourceImapFolder.exists()) throw new MessagingException("Source folder does not exist");
+ Folder sourceImapFolder = this.jakartaUtils.findImapFolder(store, fromFolder);
sourceImapFolder.open(Folder.READ_WRITE);
- Folder targetImapFolder =
- store.getFolder(
- String.join(String.valueOf(rootFolder.getSeparator()), toFolder.split("\\.")));
- if (!targetImapFolder.exists()) targetImapFolder.create(Folder.HOLDS_MESSAGES);
- targetImapFolder.open(Folder.READ_WRITE);
-
Message[] messages = sourceImapFolder.search(new MessageIDTerm(imapMoveEmail.messageId()));
Message message =
Arrays.stream(messages)
@@ -143,10 +131,8 @@ private MoveEmailResponse imapMoveEmails(
new MessagingException(
"Email with messageId %s does not exist"
.formatted(imapMoveEmail.messageId())));
- sourceImapFolder.copyMessages(new Message[] {message}, targetImapFolder);
- this.jakartaUtils.markAsDeleted(message);
+ this.jakartaUtils.moveMessage(store, message, imapMoveEmail.toFolder());
sourceImapFolder.close();
- targetImapFolder.close();
return new MoveEmailResponse(
imapMoveEmail.messageId(), imapMoveEmail.fromFolder(), imapMoveEmail.toFolder());
} catch (MessagingException e) {
@@ -158,9 +144,8 @@ private List imapListEmails(
ImapListEmails imapListEmails, Authentication authentication, Session session) {
try (Store store = session.getStore()) {
this.jakartaUtils.connectStore(store, authentication);
- Folder rootFolder = store.getDefaultFolder();
String targetFolder = imapListEmails.listEmailsFolder();
- try (Folder imapFolder = this.jakartaUtils.findImapFolder(rootFolder, targetFolder)) {
+ try (Folder imapFolder = this.jakartaUtils.findImapFolder(store, targetFolder)) {
imapFolder.open(Folder.READ_ONLY);
return Arrays.stream(imapFolder.getMessages())
.map(this.jakartaUtils::createBodylessEmail)
diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java
index 0157bc5196..a4a227d815 100644
--- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java
+++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java
@@ -33,6 +33,7 @@
public class JakartaUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JakartaUtils.class);
+ private static final String REGEX_PATH_SPLITTER = "[./]";
public Session createSession(Configuration configuration) {
return Session.getInstance(
@@ -165,31 +166,19 @@ public Comparator retrieveEmailComparator(
};
}
- private Folder findFolderRecursively(Folder rootFolder, String targetFolder)
- throws MessagingException {
- if (targetFolder == null || targetFolder.isEmpty() || "INBOX".equals(targetFolder)) {
- return rootFolder.getFolder("INBOX");
- }
- Folder[] folders = rootFolder.list();
- for (Folder folder : folders) {
- if (folder.getName().equals(targetFolder)) {
- return folder;
- } else {
- Folder folderReturned = findFolderRecursively(folder, targetFolder);
- if (folderReturned != null) {
- return folderReturned;
- }
- }
- }
- return null;
- }
-
- public Folder findImapFolder(Folder rootFolder, String targetFolder) throws MessagingException {
- Folder folder = findFolderRecursively(rootFolder, targetFolder);
- if (folder != null) {
- return folder;
+ public Folder findImapFolder(Store store, String folderPath) throws MessagingException {
+ if (folderPath == null || folderPath.isEmpty() || "INBOX".equalsIgnoreCase(folderPath)) {
+ return store.getFolder("INBOX");
}
- throw new MessagingException("Unable to find IMAP folder");
+ char separator = store.getDefaultFolder().getSeparator();
+ String formattedPath =
+ Optional.of(folderPath)
+ .map(string -> string.split(REGEX_PATH_SPLITTER))
+ .map(strings -> String.join(String.valueOf(separator), strings))
+ .orElseThrow(() -> new RuntimeException("No folder has been set"));
+ Folder folder = store.getFolder(formattedPath);
+ if (!folder.exists()) throw new RuntimeException("Folder " + formattedPath + " does not exist");
+ return folder;
}
public Email createBodylessEmail(Message message) {
@@ -321,9 +310,9 @@ public void moveMessage(Store store, Message message, String targetFolder) {
char separator = imapFolder.getSeparator();
String targetFolderFormatted =
Optional.ofNullable(targetFolder)
- .map(string -> string.split("\\."))
+ .map(string -> string.split(REGEX_PATH_SPLITTER))
.map(strings -> String.join(String.valueOf(separator), strings))
- .orElse("temp");
+ .orElseThrow(() -> new RuntimeException("No folder has been set"));
Folder targetImapFolder = store.getFolder(targetFolderFormatted);
if (!targetImapFolder.exists()) targetImapFolder.create(Folder.HOLDS_MESSAGES);
targetImapFolder.open(Folder.READ_WRITE);
diff --git a/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java b/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java
index 5250fda8f8..cd6d95fc79 100644
--- a/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java
+++ b/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java
@@ -21,10 +21,7 @@
import io.camunda.connector.email.outbound.protocols.Protocol;
import io.camunda.connector.email.outbound.protocols.Smtp;
import io.camunda.connector.email.outbound.protocols.actions.*;
-import io.camunda.connector.email.response.DeleteEmailResponse;
-import io.camunda.connector.email.response.ListEmailsResponse;
-import io.camunda.connector.email.response.ReadEmailResponse;
-import io.camunda.connector.email.response.SearchEmailsResponse;
+import io.camunda.connector.email.response.*;
import jakarta.mail.*;
import java.io.IOException;
import java.nio.file.Files;
@@ -513,6 +510,68 @@ void executeImapSearchEmails() throws MessagingException, IOException {
Assertions.assertInstanceOf(List.class, object);
}
+ @Test
+ void executeImapMoveEmail() throws MessagingException, IOException {
+ JakartaUtils sessionFactory = mock(JakartaUtils.class);
+ ObjectMapper objectMapper = new ObjectMapper();
+
+ JakartaEmailActionExecutor actionExecutor =
+ JakartaEmailActionExecutor.create(sessionFactory, objectMapper);
+
+ EmailRequest emailRequest = mock(EmailRequest.class);
+ ImapMoveEmail imapMoveEmail = mock(ImapMoveEmail.class);
+ SimpleAuthentication simpleAuthentication = mock(SimpleAuthentication.class);
+ Protocol protocol = mock(Imap.class);
+ Session session = mock(Session.class);
+ Store store = mock(Store.class);
+ Folder folder = mock(Folder.class);
+ Folder defaultFolder = mock(Folder.class);
+ Folder targetFolder = mock(Folder.class);
+ Message message = mock(Message.class);
+
+ when(sessionFactory.createSession(any())).thenReturn(session);
+
+ // Authentication
+ when(simpleAuthentication.username()).thenReturn("user");
+ when(simpleAuthentication.password()).thenReturn("secret");
+ doNothing().when(store).connect(any(), any());
+
+ when(sessionFactory.findImapFolder(any(), any())).thenReturn(folder);
+ when(folder.search(any())).thenReturn(new Message[] {message});
+ when(store.getDefaultFolder()).thenReturn(defaultFolder);
+ when(defaultFolder.getSeparator()).thenReturn('|');
+ when(folder.exists()).thenReturn(Boolean.TRUE);
+ when(message.getContent()).thenReturn("string");
+ when(message.isMimeType("text/plain")).thenReturn(true);
+ when(message.getHeader(any())).thenReturn(new String[] {"1"});
+ when(emailRequest.authentication()).thenReturn(simpleAuthentication);
+ when(session.getProperties()).thenReturn(new Properties());
+ when(session.getStore()).thenReturn(store);
+ when(emailRequest.data()).thenReturn(protocol);
+ when(protocol.getProtocolAction()).thenReturn(imapMoveEmail);
+ when(imapMoveEmail.fromFolder()).thenReturn("");
+ when(imapMoveEmail.toFolder()).thenReturn("test.to/folder");
+ when(store.getFolder("test|to|folder")).thenReturn(targetFolder);
+ when(sessionFactory.createBodylessEmail(any()))
+ .thenReturn(
+ new Email(
+ null,
+ "1",
+ "",
+ List.of(),
+ "",
+ List.of(""),
+ List.of(""),
+ OffsetDateTime.now(),
+ OffsetDateTime.now(),
+ 1));
+ doNothing().when(store).connect(any(), any());
+
+ Object object = actionExecutor.execute(emailRequest);
+
+ Assertions.assertInstanceOf(MoveEmailResponse.class, object);
+ }
+
@Test
void executeImapSearchEmailsCriteriaSpecification() throws MessagingException {
JakartaUtils sessionFactory = mock(JakartaUtils.class);
diff --git a/connectors/idp-extraction/element-templates/hybrid/hybrid-idp-extraction-outbound-connector-hybrid.json b/connectors/idp-extraction/element-templates/hybrid/hybrid-idp-extraction-outbound-connector-hybrid.json
index b8536f62ab..3db1d2c91a 100644
--- a/connectors/idp-extraction/element-templates/hybrid/hybrid-idp-extraction-outbound-connector-hybrid.json
+++ b/connectors/idp-extraction/element-templates/hybrid/hybrid-idp-extraction-outbound-connector-hybrid.json
@@ -170,7 +170,7 @@
},
"type" : "String"
} ],
- "icon": {
- "contents": "data:image/png;base64,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"
+ "icon" : {
+ "contents" : "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE5LjE3ODkiIGhlaWdodD0iMTkuMTc4OSIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4wMSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOm11bHRpcGx5Ii8+CjxwYXRoIGQ9Ik0xNi43ODA2IDcuMTkyMDhIMTEuOTg1OVYyLjM5NzM3SDE2Ljc4MDZWNy4xOTIwOFpNMTMuMTg0NiA1Ljk5MzRIMTUuNTgxOVYzLjU5NjA1SDEzLjE4NDZWNS45OTM0WiIgZmlsbD0iI0ZDNUQwRCIvPgo8cGF0aCBkPSJNMTAuMTg3OSA4Ljk5MDFWNS4zOTQwN0g1LjM5MzE4VjEzLjc4NDhIMTMuNzgzOVY4Ljk5MDFIMTAuMTg3OVpNNi41OTE4NiA2LjU5Mjc0SDguOTg5MjFWOC45OTAxSDYuNTkxODZWNi41OTI3NFpNOC45ODkyMSAxMi41ODYxSDYuNTkxODZWMTAuMTg4OEg4Ljk4OTIxVjEyLjU4NjFaTTEyLjU4NTIgMTIuNTg2MUgxMC4xODc5VjEwLjE4ODhIMTIuNTg1MlYxMi41ODYxWiIgZmlsbD0iI0ZDNUQwRCIvPgo8cGF0aCBkPSJNMTUuNTgxOSAxNi43ODE1SDMuNTk1MTZDMy4yNzczNyAxNi43ODExIDIuOTcyNjkgMTYuNjU0NyAyLjc0Nzk3IDE2LjQzQzIuNTIzMjYgMTYuMjA1MyAyLjM5Njg1IDE1LjkwMDYgMi4zOTY0OCAxNS41ODI4VjMuNTk2MDVDMi4zOTY4NSAzLjI3ODI1IDIuNTIzMjYgMi45NzM1NyAyLjc0Nzk3IDIuNzQ4ODZDMi45NzI2OSAyLjUyNDE0IDMuMjc3MzcgMi4zOTc3MyAzLjU5NTE2IDIuMzk3MzdIOS41ODg1NVYzLjU5NjA1SDMuNTk1MTZWMTUuNTgyOEgxNS41ODE5VjkuNTg5NDRIMTYuNzgwNlYxNS41ODI4QzE2Ljc4MDMgMTUuOTAwNiAxNi42NTM5IDE2LjIwNTMgMTYuNDI5MSAxNi40M0MxNi4yMDQ0IDE2LjY1NDcgMTUuODk5NyAxNi43ODExIDE1LjU4MTkgMTYuNzgxNVoiIGZpbGw9IiMxNjE2MTYiLz4KPC9zdmc+Cg=="
}
}
\ No newline at end of file
diff --git a/connectors/idp-extraction/element-templates/idp-extraction-outbound-connector.json b/connectors/idp-extraction/element-templates/idp-extraction-outbound-connector.json
index 6edcaa712f..43b67c216b 100644
--- a/connectors/idp-extraction/element-templates/idp-extraction-outbound-connector.json
+++ b/connectors/idp-extraction/element-templates/idp-extraction-outbound-connector.json
@@ -165,7 +165,7 @@
},
"type" : "String"
} ],
- "icon": {
- "contents": "data:image/png;base64,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"
+ "icon" : {
+ "contents" : "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE5LjE3ODkiIGhlaWdodD0iMTkuMTc4OSIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4wMSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOm11bHRpcGx5Ii8+CjxwYXRoIGQ9Ik0xNi43ODA2IDcuMTkyMDhIMTEuOTg1OVYyLjM5NzM3SDE2Ljc4MDZWNy4xOTIwOFpNMTMuMTg0NiA1Ljk5MzRIMTUuNTgxOVYzLjU5NjA1SDEzLjE4NDZWNS45OTM0WiIgZmlsbD0iI0ZDNUQwRCIvPgo8cGF0aCBkPSJNMTAuMTg3OSA4Ljk5MDFWNS4zOTQwN0g1LjM5MzE4VjEzLjc4NDhIMTMuNzgzOVY4Ljk5MDFIMTAuMTg3OVpNNi41OTE4NiA2LjU5Mjc0SDguOTg5MjFWOC45OTAxSDYuNTkxODZWNi41OTI3NFpNOC45ODkyMSAxMi41ODYxSDYuNTkxODZWMTAuMTg4OEg4Ljk4OTIxVjEyLjU4NjFaTTEyLjU4NTIgMTIuNTg2MUgxMC4xODc5VjEwLjE4ODhIMTIuNTg1MlYxMi41ODYxWiIgZmlsbD0iI0ZDNUQwRCIvPgo8cGF0aCBkPSJNMTUuNTgxOSAxNi43ODE1SDMuNTk1MTZDMy4yNzczNyAxNi43ODExIDIuOTcyNjkgMTYuNjU0NyAyLjc0Nzk3IDE2LjQzQzIuNTIzMjYgMTYuMjA1MyAyLjM5Njg1IDE1LjkwMDYgMi4zOTY0OCAxNS41ODI4VjMuNTk2MDVDMi4zOTY4NSAzLjI3ODI1IDIuNTIzMjYgMi45NzM1NyAyLjc0Nzk3IDIuNzQ4ODZDMi45NzI2OSAyLjUyNDE0IDMuMjc3MzcgMi4zOTc3MyAzLjU5NTE2IDIuMzk3MzdIOS41ODg1NVYzLjU5NjA1SDMuNTk1MTZWMTUuNTgyOEgxNS41ODE5VjkuNTg5NDRIMTYuNzgwNlYxNS41ODI4QzE2Ljc4MDMgMTUuOTAwNiAxNi42NTM5IDE2LjIwNTMgMTYuNDI5MSAxNi40M0MxNi4yMDQ0IDE2LjY1NDcgMTUuODk5NyAxNi43ODExIDE1LjU4MTkgMTYuNzgxNVoiIGZpbGw9IiMxNjE2MTYiLz4KPC9zdmc+Cg=="
}
}
\ No newline at end of file