Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -167,16 +168,21 @@ 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<String> add = new ArrayList<>();
add.add(unreadLabelId);
ModifyMessageRequest mods = new ModifyMessageRequest().setAddLabelIds(add);
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);
}
}

Expand All @@ -190,17 +196,52 @@ public Exchange createExchange(ExchangePattern pattern, Message mail) {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
davsclaus marked this conversation as resolved.
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("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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`.