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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1f5932d1b561 CAMEL-24403: camel-mail: handle MessageRemovedException 
in processCommit to avoid null cause in error log
1f5932d1b561 is described below

commit 1f5932d1b56169335ef54ff11a183ee84a626b09
Author: mayurbm <[email protected]>
AuthorDate: Tue Aug 25 22:43:02 2026 +0530

    CAMEL-24403: camel-mail: handle MessageRemovedException in processCommit to 
avoid null cause in error log
    
    When an IMAP message is expunged between fetch time and the on-completion
    commit callback, jakarta.mail's MessageRemovedException carries a null
    message, producing an unreadable "Caused by: 
[javax.mail.MessageRemovedException
    - null]" log entry.
    
    MailConsumer.processCommit() now catches MessageRemovedException before the
    generic MessagingException handler and wraps it with an explicit description
    ("message state could not be updated"), since the exception can originate 
from
    either the setFlag calls or copyOrMoveMessageIfRequired(). Adds unit tests
    covering both the wrapped case and pass-through of other 
MessagingExceptions.
    
    Closes #25553
    
    Co-authored-by: Claude <[email protected]>
---
 .../apache/camel/component/mail/MailConsumer.java  |   6 +
 .../MailConsumerCommitExpungedMessageTest.java     | 168 +++++++++++++++++++++
 2 files changed, 174 insertions(+)

diff --git 
a/components/camel-mail/src/main/java/org/apache/camel/component/mail/MailConsumer.java
 
b/components/camel-mail/src/main/java/org/apache/camel/component/mail/MailConsumer.java
index 62fb3812d646..ff34dc74d783 100644
--- 
a/components/camel-mail/src/main/java/org/apache/camel/component/mail/MailConsumer.java
+++ 
b/components/camel-mail/src/main/java/org/apache/camel/component/mail/MailConsumer.java
@@ -28,6 +28,7 @@ import jakarta.mail.Flags;
 import jakarta.mail.Folder;
 import jakarta.mail.FolderNotFoundException;
 import jakarta.mail.Message;
+import jakarta.mail.MessageRemovedException;
 import jakarta.mail.MessagingException;
 import jakarta.mail.PasswordAuthentication;
 import jakarta.mail.Store;
@@ -526,6 +527,11 @@ public class MailConsumer extends 
ScheduledBatchPollingConsumer {
                 }
             }
 
+        } catch (MessageRemovedException e) {
+            MessagingException wrapped = new MessagingException(
+                    "Message already removed/expunged on server (message state 
could not be updated)", e);
+            getExceptionHandler().handleException(
+                    "Error occurred during committing mail message: " + mail, 
exchange, wrapped);
         } catch (MessagingException e) {
             getExceptionHandler().handleException("Error occurred during 
committing mail message: " + mail, exchange, e);
         }
diff --git 
a/components/camel-mail/src/test/java/org/apache/camel/component/mail/MailConsumerCommitExpungedMessageTest.java
 
b/components/camel-mail/src/test/java/org/apache/camel/component/mail/MailConsumerCommitExpungedMessageTest.java
new file mode 100644
index 000000000000..7e47f91d66e8
--- /dev/null
+++ 
b/components/camel-mail/src/test/java/org/apache/camel/component/mail/MailConsumerCommitExpungedMessageTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.camel.component.mail;
+
+import java.lang.reflect.Field;
+
+import jakarta.mail.Flags;
+import jakarta.mail.Folder;
+import jakarta.mail.Message;
+import jakarta.mail.MessageRemovedException;
+import jakarta.mail.MessagingException;
+import jakarta.mail.Session;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.Processor;
+import org.apache.camel.component.mail.Mailbox.Protocol;
+import org.apache.camel.spi.ExceptionHandler;
+import org.apache.camel.spi.ExchangeFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that processCommit() wraps MessageRemovedException (message 
already expunged on the IMAP server) with a
+ * non-null cause message so that the error log never shows "Caused by: [... - 
null]".
+ */
+class MailConsumerCommitExpungedMessageTest {
+
+    @Test
+    void testCommitWithExpungedMessageProducesNonNullCause() throws Exception {
+        JavaMailSender sender = mock(JavaMailSender.class);
+        Processor processor = mock(Processor.class);
+        CamelContext camelContext = mock(CamelContext.class);
+        ExtendedCamelContext ecc = mock(ExtendedCamelContext.class);
+        ExchangeFactory ef = mock(ExchangeFactory.class);
+        Session session = 
Session.getInstance(Mailbox.getSessionProperties(Protocol.imap));
+
+        when(sender.getSession()).thenReturn(session);
+        when(camelContext.getCamelContextExtension()).thenReturn(ecc);
+        when(ecc.getExchangeFactory()).thenReturn(ef);
+        when(ef.newExchangeFactory(any())).thenReturn(ef);
+
+        MailEndpoint endpoint = new MailEndpoint();
+        endpoint.setCamelContext(camelContext);
+        MailConfiguration config = new MailConfiguration();
+        config.configureProtocol(Protocol.imap.name());
+        config.setPort(Mailbox.getPort(Protocol.imap));
+        config.setFolderName("INBOX");
+        config.setDelete(false);
+        endpoint.setConfiguration(config);
+
+        Message mail = mock(Message.class);
+        doThrow(new 
MessageRemovedException()).when(mail).setFlag(any(Flags.Flag.class), 
any(boolean.class));
+
+        Folder folder = mock(Folder.class);
+        when(folder.isOpen()).thenReturn(true);
+
+        Exchange exchange = mock(Exchange.class);
+        org.apache.camel.Message camelMsg = 
mock(org.apache.camel.Message.class);
+        when(exchange.getIn()).thenReturn(camelMsg);
+        when(camelMsg.getHeader(MailConstants.MAIL_COPY_TO, 
config.getCopyTo(), String.class)).thenReturn(null);
+        when(camelMsg.getHeader(MailConstants.MAIL_MOVE_TO, 
config.getMoveTo(), String.class)).thenReturn(null);
+        when(camelMsg.getHeader(MailConstants.MAIL_DELETE, config.isDelete(), 
boolean.class)).thenReturn(false);
+        
when(exchange.removeProperty(MailConsumer.MAIL_MESSAGE_UID)).thenReturn(null);
+
+        ExceptionHandler exceptionHandler = mock(ExceptionHandler.class);
+
+        MailConsumer consumer = new MailConsumer(endpoint, processor, sender);
+        consumer.setExceptionHandler(exceptionHandler);
+
+        Field folderField = MailConsumer.class.getDeclaredField("folder");
+        folderField.setAccessible(true);
+        folderField.set(consumer, folder);
+
+        consumer.processCommit(mail, exchange);
+
+        ArgumentCaptor<String> msgCaptor = 
ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<Throwable> causeCaptor = 
ArgumentCaptor.forClass(Throwable.class);
+        verify(exceptionHandler).handleException(msgCaptor.capture(), 
any(Exchange.class), causeCaptor.capture());
+
+        Throwable caught = causeCaptor.getValue();
+        assertThat(caught)
+                .isInstanceOf(MessagingException.class)
+                .hasMessage("Message already removed/expunged on server 
(message state could not be updated)")
+                .hasCauseInstanceOf(MessageRemovedException.class);
+    }
+
+    @Test
+    void testCommitWithOtherMessagingExceptionPassedThroughAsIs() throws 
Exception {
+        JavaMailSender sender = mock(JavaMailSender.class);
+        Processor processor = mock(Processor.class);
+        CamelContext camelContext = mock(CamelContext.class);
+        ExtendedCamelContext ecc = mock(ExtendedCamelContext.class);
+        ExchangeFactory ef = mock(ExchangeFactory.class);
+        Session session = 
Session.getInstance(Mailbox.getSessionProperties(Protocol.imap));
+
+        when(sender.getSession()).thenReturn(session);
+        when(camelContext.getCamelContextExtension()).thenReturn(ecc);
+        when(ecc.getExchangeFactory()).thenReturn(ef);
+        when(ef.newExchangeFactory(any())).thenReturn(ef);
+
+        MailEndpoint endpoint = new MailEndpoint();
+        endpoint.setCamelContext(camelContext);
+        MailConfiguration config = new MailConfiguration();
+        config.configureProtocol(Protocol.imap.name());
+        config.setPort(Mailbox.getPort(Protocol.imap));
+        config.setFolderName("INBOX");
+        config.setDelete(false);
+        endpoint.setConfiguration(config);
+
+        MessagingException originalException = new MessagingException("Some 
other server error");
+
+        Message mail = mock(Message.class);
+        doThrow(originalException).when(mail).setFlag(any(Flags.Flag.class), 
any(boolean.class));
+
+        Folder folder = mock(Folder.class);
+        when(folder.isOpen()).thenReturn(true);
+
+        Exchange exchange = mock(Exchange.class);
+        org.apache.camel.Message camelMsg = 
mock(org.apache.camel.Message.class);
+        when(exchange.getIn()).thenReturn(camelMsg);
+        when(camelMsg.getHeader(MailConstants.MAIL_COPY_TO, 
config.getCopyTo(), String.class)).thenReturn(null);
+        when(camelMsg.getHeader(MailConstants.MAIL_MOVE_TO, 
config.getMoveTo(), String.class)).thenReturn(null);
+        when(camelMsg.getHeader(MailConstants.MAIL_DELETE, config.isDelete(), 
boolean.class)).thenReturn(false);
+        
when(exchange.removeProperty(MailConsumer.MAIL_MESSAGE_UID)).thenReturn(null);
+
+        ExceptionHandler exceptionHandler = mock(ExceptionHandler.class);
+
+        MailConsumer consumer = new MailConsumer(endpoint, processor, sender);
+        consumer.setExceptionHandler(exceptionHandler);
+
+        Field folderField = MailConsumer.class.getDeclaredField("folder");
+        folderField.setAccessible(true);
+        folderField.set(consumer, folder);
+
+        consumer.processCommit(mail, exchange);
+
+        ArgumentCaptor<Throwable> causeCaptor = 
ArgumentCaptor.forClass(Throwable.class);
+        verify(exceptionHandler).handleException(anyString(), 
any(Exchange.class), causeCaptor.capture());
+
+        assertThat(causeCaptor.getValue())
+                .isInstanceOf(MessagingException.class)
+                .hasMessage("Some other server error");
+    }
+}

Reply via email to