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 5269c8d34f1dc..182f2c6674b0c 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 @@ protected int poll() throws Exception { 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 @@ protected void processCommit(Exchange exchange, String unreadLabelId) { * 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 add = new ArrayList<>(); add.add(unreadLabelId); @@ -176,7 +181,8 @@ protected void processRollback(Exchange exchange, String unreadLabelId) { 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 Exchange createExchange(ExchangePattern pattern, Message mail) { if (getConfiguration().isRaw()) { message.setBody(mail.getRaw()); } else { - List 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 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 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 0000000000000..7b960e0752f3e --- /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("

html

"))))); + + 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("someone@example.org")))); + + 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("someone@example.org"); + } + + @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 323652455c692..caed74bf91894 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`.