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 e8dc6b87bf8c CAMEL-24344: camel-google-mail - return a body for raw
and non-multipart messages
e8dc6b87bf8c is described below
commit e8dc6b87bf8c53c2c3b28f538fdb183f572d8254
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 6 13:38:41 2026 +0200
CAMEL-24344: camel-google-mail - return a body for raw and non-multipart
messages
The stream consumer always asked the Gmail API for the FULL format, but the
raw field is only populated for the RAW format, so raw=true always produced
a
null body. The consumer now asks for the format matching the option.
The non-raw path only looked at the first element of getParts(), so a
message
that is not multipart carried no body, and nested multiparts yielded
nothing.
The body is now taken from the first part carrying data, walking into nested
parts. Payload and header list are no longer dereferenced unguarded, and a
failed exchange no longer marks the mail as unread when markAsRead was not
enabled.
Closes #25357
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../mail/stream/GoogleMailStreamConsumer.java | 57 +++++++--
.../stream/GoogleMailStreamConsumerBodyTest.java | 128 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 12 ++
3 files changed, 189 insertions(+), 8 deletions(-)
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
index 5269c8d34f1d..182f2c6674b0 100644
---
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumer.java
@@ -92,7 +92,8 @@ public class GoogleMailStreamConsumer extends
ScheduledBatchPollingConsumer {
if (c.getMessages() != null) {
for (Message message : c.getMessages()) {
- Message mess = getClient().users().messages().get("me",
message.getId()).setFormat("FULL").execute();
+ Message mess
+ = getClient().users().messages().get("me",
message.getId()).setFormat(messageFormat()).execute();
Exchange exchange =
createExchange(getEndpoint().getExchangePattern(), mess);
answer.add(exchange);
}
@@ -167,8 +168,12 @@ public class GoogleMailStreamConsumer extends
ScheduledBatchPollingConsumer {
* Strategy when processing the exchange failed.
*/
protected void processRollback(Exchange exchange, String unreadLabelId) {
+ if (!getConfiguration().isMarkAsRead()) {
+ // the mail was never marked as read, so there is nothing to roll
back
+ return;
+ }
try {
- LOG.warn("Exchange failed, so rolling back mail {} to un {}",
exchange, unreadLabelId);
+ LOG.warn("Exchange failed, so marking mail {} as unread again",
exchange);
List<String> add = new ArrayList<>();
add.add(unreadLabelId);
@@ -176,7 +181,8 @@ public class GoogleMailStreamConsumer extends
ScheduledBatchPollingConsumer {
getClient().users().messages()
.modify("me",
exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_ID, String.class),
mods).execute();
} catch (Exception e) {
- getExceptionHandler().handleException("Error occurred mark as read
mail. This exception is ignored.", exchange, e);
+ getExceptionHandler().handleException("Error occurred marking the
mail as unread. This exception is ignored.",
+ exchange, e);
}
}
@@ -190,17 +196,52 @@ public class GoogleMailStreamConsumer extends
ScheduledBatchPollingConsumer {
if (getConfiguration().isRaw()) {
message.setBody(mail.getRaw());
} else {
- List<MessagePart> parts = mail.getPayload().getParts();
- if (parts != null && parts.get(0).getBody().getData() != null) {
- byte[] bodyBytes =
Base64.decodeBase64(parts.get(0).getBody().getData().trim());
- String body = new String(bodyBytes, StandardCharsets.UTF_8);
+ String body = extractBody(mail.getPayload());
+ if (body != null) {
message.setBody(body);
}
}
- configureHeaders(message, mail.getPayload().getHeaders());
+ if (mail.getPayload() != null && mail.getPayload().getHeaders() !=
null) {
+ configureHeaders(message, mail.getPayload().getHeaders());
+ }
return exchange;
}
+ /**
+ * The message format the consumer has to ask for. The raw field of a
message is only populated when the RAW format
+ * is requested, the payload only when the FULL format is.
+ */
+ String messageFormat() {
+ return getConfiguration().isRaw() ? "RAW" : "FULL";
+ }
+
+ /**
+ * Returns the decoded content of the first part carrying data, walking
into nested multiparts. A message that is
+ * not multipart at all keeps its content directly on the payload.
+ */
+ private String extractBody(MessagePart part) {
+ if (part == null) {
+ return null;
+ }
+
+ if (part.getBody() != null && part.getBody().getData() != null) {
+ byte[] bodyBytes =
Base64.decodeBase64(part.getBody().getData().trim());
+ return new String(bodyBytes, StandardCharsets.UTF_8);
+ }
+
+ List<MessagePart> parts = part.getParts();
+ if (parts != null) {
+ for (MessagePart child : parts) {
+ String body = extractBody(child);
+ if (body != null) {
+ return body;
+ }
+ }
+ }
+
+ return null;
+ }
+
private void configureHeaders(org.apache.camel.Message message,
List<MessagePartHeader> headers) {
for (MessagePartHeader header : headers) {
String headerName = header.getName();
diff --git
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
new file mode 100644
index 000000000000..7b960e0752f3
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamConsumerBodyTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.google.mail.stream;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import com.google.api.client.util.Base64;
+import com.google.api.services.gmail.model.Message;
+import com.google.api.services.gmail.model.MessagePart;
+import com.google.api.services.gmail.model.MessagePartBody;
+import com.google.api.services.gmail.model.MessagePartHeader;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Verifies how the stream consumer turns a Gmail message into an exchange:
which format it asks the API for, and where
+ * it picks the body up from.
+ */
+class GoogleMailStreamConsumerBodyTest {
+
+ private DefaultCamelContext context;
+
+ @AfterEach
+ void tearDown() {
+ if (context != null) {
+ context.stop();
+ }
+ }
+
+ private GoogleMailStreamConsumer consumer(boolean raw) throws Exception {
+ if (context != null) {
+ context.stop();
+ }
+ context = new DefaultCamelContext();
+ context.start();
+ GoogleMailStreamEndpoint endpoint = context.getEndpoint(
+
"google-mail-stream://index?clientId=id&clientSecret=secret&raw=" + raw,
+ GoogleMailStreamEndpoint.class);
+ return new GoogleMailStreamConsumer(endpoint, exchange -> {
+ }, "UNREAD", List.of());
+ }
+
+ private static MessagePartBody body(String content) {
+ return new
MessagePartBody().setData(Base64.encodeBase64URLSafeString(content.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void theRawOptionAsksForTheRawFormat() throws Exception {
+ // the raw field of a message is only returned for the RAW format,
asking for FULL always left it null
+ assertThat(consumer(true).messageFormat()).isEqualTo("RAW");
+ assertThat(consumer(false).messageFormat()).isEqualTo("FULL");
+ }
+
+ @Test
+ void aNonMultipartMessageKeepsItsBody() throws Exception {
+ Message mail = new Message().setId("1").setThreadId("t1")
+ .setPayload(new
MessagePart().setMimeType("text/plain").setBody(body("plain content")));
+
+ Exchange exchange =
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+ assertThat(exchange.getIn().getBody()).isEqualTo("plain content");
+ }
+
+ @Test
+ void aMultipartMessageUsesTheFirstPartCarryingData() throws Exception {
+ Message mail = new Message().setId("2").setPayload(new
MessagePart().setMimeType("multipart/alternative")
+ .setParts(List.of(
+ new MessagePart().setMimeType("multipart/mixed")
+ .setParts(List.of(new
MessagePart().setMimeType("text/plain").setBody(body("nested")))),
+ new
MessagePart().setMimeType("text/html").setBody(body("<p>html</p>")))));
+
+ Exchange exchange =
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+ assertThat(exchange.getIn().getBody()).isEqualTo("nested");
+ }
+
+ @Test
+ void aMessageWithoutPayloadIsNotAFailure() throws Exception {
+ Message mail = new Message().setId("3");
+
+ Exchange exchange =
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+ assertThat(exchange.getIn().getBody()).isNull();
+
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_ID)).isEqualTo("3");
+ }
+
+ @Test
+ void headersAreMappedWhenPresent() throws Exception {
+ Message mail = new Message().setId("4").setPayload(new MessagePart()
+ .setBody(body("content"))
+ .setHeaders(List.of(
+ new MessagePartHeader().setName("Subject").setValue("a
subject"),
+ new
MessagePartHeader().setName("From").setValue("[email protected]"))));
+
+ Exchange exchange =
consumer(false).createExchange(ExchangePattern.InOnly, mail);
+
+
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_SUBJECT)).isEqualTo("a
subject");
+
assertThat(exchange.getIn().getHeader(GoogleMailStreamConstants.MAIL_FROM)).isEqualTo("[email protected]");
+ }
+
+ @Test
+ void aPayloadWithoutHeadersIsNotAFailure() throws Exception {
+ Message mail = new Message().setId("5").setPayload(new
MessagePart().setBody(body("content")));
+
+ assertThatCode(() ->
consumer(false).createExchange(ExchangePattern.InOnly,
mail)).doesNotThrowAnyException();
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 323652455c69..caed74bf9189 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -1710,3 +1710,15 @@ transport `from` and the `atmosphere-websocket:` `to`.
Allowing untrusted sender
drive `WebsocketConstants.CONNECTION_KEY_LIST`, which selects the target peers
and
takes precedence over `CONNECTION_KEY`, without such a mapping step is not the
intended
use of the component.
+
+=== camel-google-mail - the raw option now returns the message
+
+The `google-mail-stream` consumer always asked the Gmail API for the `FULL`
message format, but the
+`raw` field is only populated for the `RAW` format, so `raw=true` produced a
`null` body. The consumer
+now requests the format that matches the option.
+
+Because the Gmail API does not return the parsed `payload` for the `RAW`
format, a route running with
+`raw=true` no longer receives the `CamelGoogleMailStreamSubject`, `...From`,
`...To`, `...Cc`, `...Bcc`
+and `...MessageId` headers — those values are part of the RFC 2822 content
that is now in the body.
+`CamelGoogleMailStreamId`, `...ThreadId` and `...LabelIds` are still set.
Routes that need the parsed
+headers should keep the default `raw=false`.