This is an automated email from the ASF dual-hosted git repository.
zbendhiba 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 f91d1da56502 CAMEL-23258: Add google-mail:draft DataType transformer
(#22389)
f91d1da56502 is described below
commit f91d1da56502dc1d157db8b5c0091b4954151a93
Author: Zineb BENDHIBA <[email protected]>
AuthorDate: Thu Apr 2 17:08:51 2026 +0200
CAMEL-23258: Add google-mail:draft DataType transformer (#22389)
* CAMEL-23258: Add google-mail:draft DataType transformer
This code was created with the help of Claude code
Fixes #CAMEL-23258
---
.../apache/camel/catalog/transformers.properties | 1 +
.../catalog/transformers/google-mail-draft.json | 14 +
components/camel-google/camel-google-mail/pom.xml | 5 +
.../org/apache/camel/transformer.properties | 2 +-
.../org/apache/camel/transformer/google-mail-draft | 2 +
.../camel/transformer/google-mail-draft.json | 14 +
.../src/main/docs/google-mail-component.adoc | 159 ++++++++
.../GoogleMailDraftDataTypeTransformer.java | 111 ++++++
.../GoogleMailDraftDataTypeTransformerTest.java | 440 +++++++++++++++++++++
9 files changed, 747 insertions(+), 1 deletion(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers.properties
index c261164480cc..601ee6a5db41 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers.properties
@@ -21,6 +21,7 @@ azure-storage-blob-application-cloudevents
azure-storage-datalake-application-cloudevents
azure-storage-queue-application-cloudevents
google-calendar-stream-application-cloudevents
+google-mail-draft
google-mail-stream-application-cloudevents
google-mail-update-message-labels
google-pubsub-application-cloudevents
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/google-mail-draft.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/google-mail-draft.json
new file mode 100644
index 000000000000..84793b6c4cf4
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/google-mail-draft.json
@@ -0,0 +1,14 @@
+{
+ "transformer": {
+ "kind": "transformer",
+ "name": "google-mail:draft",
+ "title": "Google Mail (Draft)",
+ "description": "Creates a Gmail Draft from String body and threading
metadata from headers",
+ "deprecated": false,
+ "javaType":
"org.apache.camel.component.google.mail.transform.GoogleMailDraftDataTypeTransformer",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-google-mail",
+ "version": "4.19.0-SNAPSHOT"
+ }
+}
+
diff --git a/components/camel-google/camel-google-mail/pom.xml
b/components/camel-google/camel-google-mail/pom.xml
index 08a202cab843..794167679c6d 100644
--- a/components/camel-google/camel-google-mail/pom.xml
+++ b/components/camel-google/camel-google-mail/pom.xml
@@ -155,6 +155,11 @@
<artifactId>commons-codec</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
diff --git
a/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties
index 20bd343808d0..1e495cc4bf67 100644
---
a/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties
+++
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-transformers=google-mail-stream:application-cloudevents
google-mail:update-message-labels
+transformers=google-mail-stream:application-cloudevents google-mail:draft
google-mail:update-message-labels
groupId=org.apache.camel
artifactId=camel-google-mail
version=4.19.0-SNAPSHOT
diff --git
a/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft
new file mode 100644
index 000000000000..1aae0ca7df0b
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.google.mail.transform.GoogleMailDraftDataTypeTransformer
diff --git
a/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft.json
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft.json
new file mode 100644
index 000000000000..84793b6c4cf4
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/generated/resources/META-INF/services/org/apache/camel/transformer/google-mail-draft.json
@@ -0,0 +1,14 @@
+{
+ "transformer": {
+ "kind": "transformer",
+ "name": "google-mail:draft",
+ "title": "Google Mail (Draft)",
+ "description": "Creates a Gmail Draft from String body and threading
metadata from headers",
+ "deprecated": false,
+ "javaType":
"org.apache.camel.component.google.mail.transform.GoogleMailDraftDataTypeTransformer",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-google-mail",
+ "version": "4.19.0-SNAPSHOT"
+ }
+}
+
diff --git
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
index 69381e4843e7..ed782de1e1b6 100644
---
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
+++
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
@@ -140,6 +140,165 @@ XML::
</route>
----
====
+== Data Type Transformer: draft
+
+The `google-mail:draft` transformer converts a String body into a Gmail
`Draft` using mail headers from the exchange.
+
+It works with `google-mail-stream`, which sets headers like `threadId`,
`messageId`, `from`, `to`, `cc`, `bcc`, and `subject` automatically when
consuming messages.
+You can also set these headers manually to create drafts from scratch.
+
+=== Headers
+
+The transformer reads the following headers:
+
+[cols="1,1,3"]
+|===
+| Header | Type | Description
+
+| `CamelGoogleMailStreamThreadId`
+| `String`
+| Thread ID for the conversation (optional, used for replies)
+
+| `CamelGoogleMailStreamMessageId`
+| `String`
+| Message ID to reply to (optional, sets In-Reply-To and References headers)
+
+| `CamelGoogleMailStreamFrom`
+| `String`
+| From address (optional)
+
+| `CamelGoogleMailStreamTo`
+| `String`
+| To address (optional)
+
+| `CamelGoogleMailStreamCc`
+| `String`
+| CC address (optional)
+
+| `CamelGoogleMailStreamBcc`
+| `String`
+| BCC address (optional)
+
+| `CamelGoogleMailStreamSubject`
+| `String`
+| Email subject (optional)
+|===
+
+All headers are optional.
+
+=== Message Body
+
+The message body is a `String` with the email content (plain text or HTML).
+
+Content type defaults to `text/plain; charset=UTF-8`. For HTML, set
`Exchange.CONTENT_TYPE` to `text/html; charset=UTF-8`.
+
+The transformer:
+
+* Builds an RFC 2822 MIME message and encodes it as Base64 URL-safe (required
by Gmail API)
+* Sets `In-Reply-To` and `References` headers when `messageId` is present
+* Associates the draft with the correct thread when `threadId` is present
+
+=== Examples
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// Create a draft reply from a received message
+from("google-mail-stream:0")
+ .setBody(constant("Thank you for your message. I will respond soon."))
+ .transformDataType(new DataType("google-mail:draft"))
+ .to("google-mail:drafts/create?inBody=content");
+
+// Create a new draft (no threading)
+from("direct:newDraft")
+ .setBody(constant("This is a new draft message"))
+ .setHeader("CamelGoogleMailStreamFrom", constant("[email protected]"))
+ .setHeader("CamelGoogleMailStreamTo", constant("[email protected]"))
+ .setHeader("CamelGoogleMailStreamSubject", constant("New Draft"))
+ .transformDataType(new DataType("google-mail:draft"))
+ .to("google-mail:drafts/create?inBody=content");
+----
+
+YAML::
++
+[source,yaml]
+----
+# Create a draft reply from a received message
+- route:
+ from:
+ uri: "google-mail-stream:0"
+ steps:
+ - setBody:
+ constant: "Thank you for your message. I will respond soon."
+ - transformDataType:
+ toType: "google-mail:draft"
+ - to:
+ uri: "google-mail:drafts/create"
+ parameters:
+ inBody: content
+
+# Create a new draft (no threading)
+- route:
+ from:
+ uri: "direct:newDraft"
+ steps:
+ - setBody:
+ constant: "This is a new draft message"
+ - setHeader:
+ name: CamelGoogleMailStreamFrom
+ constant: "[email protected]"
+ - setHeader:
+ name: CamelGoogleMailStreamTo
+ constant: "[email protected]"
+ - setHeader:
+ name: CamelGoogleMailStreamSubject
+ constant: "New Draft"
+ - transformDataType:
+ toType: "google-mail:draft"
+ - to:
+ uri: "google-mail:drafts/create"
+ parameters:
+ inBody: content
+----
+
+XML::
++
+[source,xml]
+----
+<!-- Create a draft reply from a received message -->
+<route>
+ <from uri="google-mail-stream:0"/>
+ <setBody>
+ <constant>Thank you for your message. I will respond soon.</constant>
+ </setBody>
+ <transformDataType toType="google-mail:draft"/>
+ <to uri="google-mail:drafts/create?inBody=content"/>
+</route>
+
+<!-- Create a new draft (no threading) -->
+<route>
+ <from uri="direct:newDraft"/>
+ <setBody>
+ <constant>This is a new draft message</constant>
+ </setBody>
+ <setHeader name="CamelGoogleMailStreamFrom">
+ <constant>[email protected]</constant>
+ </setHeader>
+ <setHeader name="CamelGoogleMailStreamTo">
+ <constant>[email protected]</constant>
+ </setHeader>
+ <setHeader name="CamelGoogleMailStreamSubject">
+ <constant>New Draft</constant>
+ </setHeader>
+ <transformDataType toType="google-mail:draft"/>
+ <to uri="google-mail:drafts/create?inBody=content"/>
+</route>
+----
+====
+
== More Information
diff --git
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformer.java
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformer.java
new file mode 100644
index 000000000000..9a57de4807b2
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformer.java
@@ -0,0 +1,111 @@
+/*
+ * 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.transform;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Properties;
+
+import jakarta.mail.Session;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+
+import com.google.api.client.util.Base64;
+import com.google.api.services.gmail.model.Draft;
+import com.google.api.services.gmail.model.Message;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants;
+import org.apache.camel.spi.DataType;
+import org.apache.camel.spi.DataTypeTransformer;
+import org.apache.camel.spi.Transformer;
+import org.apache.camel.util.ObjectHelper;
+
+import static jakarta.mail.Message.RecipientType.BCC;
+import static jakarta.mail.Message.RecipientType.CC;
+import static jakarta.mail.Message.RecipientType.TO;
+
+/**
+ * Data type transformer that builds a {@link Draft} from a String body and
mail headers populated by
+ * google-mail-stream.
+ */
+@DataTypeTransformer(name = "google-mail:draft",
+ description = "Creates a Gmail Draft from String body and
threading metadata from headers")
+public class GoogleMailDraftDataTypeTransformer extends Transformer {
+
+ @Override
+ public void transform(org.apache.camel.Message message, DataType fromType,
DataType toType) throws Exception {
+ String body = message.getBody(String.class);
+ if (ObjectHelper.isEmpty(body)) {
+ throw new IllegalArgumentException("Draft body must not be null or
empty");
+ }
+
+ String threadId =
message.getHeader(GoogleMailStreamConstants.MAIL_THREAD_ID, String.class);
+ String messageId =
message.getHeader(GoogleMailStreamConstants.MAIL_MESSAGE_ID, String.class);
+ String from = message.getHeader(GoogleMailStreamConstants.MAIL_FROM,
String.class);
+ String subject =
message.getHeader(GoogleMailStreamConstants.MAIL_SUBJECT, String.class);
+ String to = message.getHeader(GoogleMailStreamConstants.MAIL_TO,
String.class);
+ String cc = message.getHeader(GoogleMailStreamConstants.MAIL_CC,
String.class);
+ String bcc = message.getHeader(GoogleMailStreamConstants.MAIL_BCC,
String.class);
+
+ Session session = Session.getDefaultInstance(new Properties());
+ MimeMessage mimeMessage = new MimeMessage(session);
+
+ if (ObjectHelper.isNotEmpty(from)) {
+ mimeMessage.setFrom(new InternetAddress(from, true));
+ }
+ if (ObjectHelper.isNotEmpty(to)) {
+ mimeMessage.addRecipients(TO, InternetAddress.parse(to, true));
+ }
+ if (ObjectHelper.isNotEmpty(cc)) {
+ mimeMessage.addRecipients(CC, InternetAddress.parse(cc, true));
+ }
+ if (ObjectHelper.isNotEmpty(bcc)) {
+ mimeMessage.addRecipients(BCC, InternetAddress.parse(bcc, true));
+ }
+ if (ObjectHelper.isNotEmpty(subject)) {
+ mimeMessage.setSubject(subject, "UTF-8");
+ }
+
+ if (ObjectHelper.isNotEmpty(messageId)) {
+ mimeMessage.setHeader("In-Reply-To", messageId);
+ mimeMessage.setHeader("References", messageId);
+ }
+
+ String contentType = message.getHeader(Exchange.CONTENT_TYPE,
String.class);
+ if (contentType == null) {
+ contentType = "text/plain; charset=UTF-8";
+ }
+ mimeMessage.setContent(body, contentType);
+
+ String encodedEmail;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ mimeMessage.writeTo(baos);
+ encodedEmail =
Base64.encodeBase64URLSafeString(baos.toByteArray());
+ }
+
+ Message gmailMessage = new Message();
+ gmailMessage.setRaw(encodedEmail);
+
+ if (ObjectHelper.isNotEmpty(threadId)) {
+ gmailMessage.setThreadId(threadId);
+ }
+
+ Draft draft = new Draft();
+ draft.setMessage(gmailMessage);
+
+ message.setBody(draft);
+ }
+}
diff --git
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformerTest.java
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformerTest.java
new file mode 100644
index 000000000000..60b3a94b0d0b
--- /dev/null
+++
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/transform/GoogleMailDraftDataTypeTransformerTest.java
@@ -0,0 +1,440 @@
+/*
+ * 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.transform;
+
+import java.nio.charset.StandardCharsets;
+
+import jakarta.mail.internet.MimeUtility;
+
+import com.google.api.client.util.Base64;
+import com.google.api.services.gmail.model.Draft;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.DataType;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_BCC;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_CC;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_FROM;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_MESSAGE_ID;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_SUBJECT;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_THREAD_ID;
+import static
org.apache.camel.component.google.mail.stream.GoogleMailStreamConstants.MAIL_TO;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class GoogleMailDraftDataTypeTransformerTest extends CamelTestSupport {
+
+ private GoogleMailDraftDataTypeTransformer transformer;
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ return new DefaultCamelContext();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:input")
+ .transformDataType("google-mail:draft")
+ .to("mock:result");
+ }
+ };
+ }
+
+ @Override
+ protected void doPostSetup() {
+ transformer = new GoogleMailDraftDataTypeTransformer();
+ transformer.setCamelContext(context);
+ }
+
+ @Test
+ void testBasicDraftCreation() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("This is a test draft message");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+ assertThat(draft.getMessage()).isNotNull();
+ assertThat(draft.getMessage().getRaw()).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("This is a test draft message")
+ .contains("Content-Type: text/plain; charset=UTF-8");
+ }
+
+ @Test
+ void testDraftWithThreadingMetadata() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Reply message");
+ exchange.getMessage().setHeader(MAIL_THREAD_ID, "thread-123");
+ exchange.getMessage().setHeader(MAIL_MESSAGE_ID,
"<[email protected]>");
+ exchange.getMessage().setHeader(MAIL_FROM, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Re: Test Subject");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+ assertThat(draft.getMessage()).isNotNull();
+ assertThat(draft.getMessage().getThreadId()).isEqualTo("thread-123");
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("From: [email protected]")
+ .contains("Subject: Re: Test Subject")
+ .contains("In-Reply-To: <[email protected]>")
+ .contains("References: <[email protected]>")
+ .contains("Reply message");
+ }
+
+ @Test
+ void testDraftWithAllHeaders() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Complete message");
+ exchange.getMessage().setHeader(MAIL_FROM, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_TO, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_CC, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_BCC, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Complete Test");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("From: [email protected]")
+ .contains("To: [email protected]")
+ .contains("Cc: [email protected]")
+ .contains("Bcc: [email protected]")
+ .contains("Subject: Complete Test")
+ .contains("Complete message");
+ }
+
+ @Test
+ void testDraftWithEmptyBody() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Empty Body Test");
+
+ assertThatThrownBy(() -> transformer.transform(exchange.getMessage(),
DataType.ANY, DataType.ANY))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void testDraftWithNullBody() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody(null);
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Null Body Test");
+
+ assertThatThrownBy(() -> transformer.transform(exchange.getMessage(),
DataType.ANY, DataType.ANY))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void testDraftWithoutThreadId() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("New conversation");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "New Thread");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+ assertThat(draft.getMessage().getThreadId()).isNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("Subject: New Thread")
+ .contains("New conversation");
+ }
+
+ @Test
+ void testDraftWithHtmlBodyRequiresExplicitContentType() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ String htmlBody = "<html><body><h1>Hello</h1><p>This is
HTML</p></body></html>";
+ exchange.getMessage().setBody(htmlBody);
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "HTML Test");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .as("Without explicit Content-Type header, should default to
text/plain even for HTML-like body")
+ .contains("text/plain");
+ }
+
+ @Test
+ void testDraftWithHtmlBodyAndExplicitHtmlContentType() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ String htmlBody = "<html><body><h1>Hello</h1><p>This is
HTML</p></body></html>";
+ exchange.getMessage().setBody(htmlBody);
+ exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "text/html;
charset=UTF-8");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "HTML Test");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("text/html")
+ .contains(htmlBody);
+ }
+
+ @Test
+ void testDraftWithExplicitContentTypeHeader() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Some plain text");
+ exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "text/html");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Explicit CT");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .as("MimeMessage should set text/html and determine charset
automatically")
+ .contains("Content-Type: text/html");
+ }
+
+ @Test
+ void testDraftWithPlainTextDefaultContentType() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Just plain text, no HTML");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Plain Test");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage).contains("Content-Type: text/plain;
charset=UTF-8");
+ }
+
+ @Test
+ void testDraftWithNonAsciiSubject() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Message with accented subject");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Resumé for café
meeting");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("Subject: =?UTF-8?")
+ .contains("?=")
+ .contains("Message with accented subject");
+ String decodedSubject = MimeUtility.decodeText(
+ decodedMessage.lines().filter(l ->
l.startsWith("Subject:")).findFirst().orElse(""));
+ assertThat(decodedSubject)
+ .as("Non-ASCII characters should be preserved after decoding")
+ .contains("Resumé for café meeting");
+ }
+
+ @Test
+ void testDraftWithNonAsciiFrom() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Test message");
+ exchange.getMessage().setHeader(MAIL_FROM, "Renée Müller
<[email protected]>");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .as("MimeMessage should handle non-ASCII in From display name
and preserve the address")
+ .contains("From:")
+ .contains("[email protected]");
+ }
+
+ @Test
+ void testDraftWithAsciiHeadersNotEncoded() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Test message");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Plain ASCII Subject");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage).contains("Subject: Plain ASCII Subject");
+ }
+
+ @Test
+ void testDraftWithExplicitContentTypeIncludingCharset() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Some text");
+ exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "text/html;
charset=ISO-8859-1");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("Content-Type: text/html; charset=ISO-8859-1")
+ .doesNotContain("charset=UTF-8");
+ }
+
+ // E2E tests via Camel route
+
+ @Test
+ void testDraftCreationViaRoute() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.reset();
+ mock.expectedMessageCount(1);
+
+ template.send("direct:input", exchange -> {
+ exchange.getMessage().setBody("Route test message");
+ exchange.getMessage().setHeader(MAIL_FROM, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Route Test");
+ });
+
+ mock.assertIsSatisfied();
+
+ Exchange received = mock.getExchanges().get(0);
+ Draft draft = received.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+ assertThat(draft.getMessage()).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("From: [email protected]")
+ .contains("Subject: Route Test")
+ .contains("Route test message");
+ }
+
+ @Test
+ void testReplyDraftViaRoute() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.reset();
+ mock.expectedMessageCount(1);
+
+ template.send("direct:input", exchange -> {
+ exchange.getMessage().setBody("This is a reply");
+ exchange.getMessage().setHeader(MAIL_THREAD_ID,
"thread-reply-123");
+ exchange.getMessage().setHeader(MAIL_MESSAGE_ID,
"<[email protected]>");
+ exchange.getMessage().setHeader(MAIL_FROM, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Re: Original
Subject");
+ });
+
+ mock.assertIsSatisfied();
+
+ Exchange received = mock.getExchanges().get(0);
+ Draft draft = received.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
assertThat(draft.getMessage().getThreadId()).isEqualTo("thread-reply-123");
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("In-Reply-To: <[email protected]>")
+ .contains("References: <[email protected]>")
+ .contains("This is a reply");
+ }
+
+ @Test
+ void testMultilineBodyViaRoute() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.reset();
+ mock.expectedMessageCount(1);
+
+ String multilineBody = "Line 1\nLine 2\nLine 3\n\nLine 5";
+ template.send("direct:input", exchange -> {
+ exchange.getMessage().setBody(multilineBody);
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Multiline Test");
+ });
+
+ mock.assertIsSatisfied();
+
+ Exchange received = mock.getExchanges().get(0);
+ Draft draft = received.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("Line 1")
+ .contains("Line 2")
+ .contains("Line 3")
+ .contains("Line 5");
+ }
+
+ @Test
+ void testDraftWithMalformedEmailAddress() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Test message");
+ exchange.getMessage().setHeader(MAIL_TO, "not a valid@ address");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Malformed Test");
+
+ assertThatThrownBy(() -> transformer.transform(exchange.getMessage(),
DataType.ANY, DataType.ANY))
+ .isInstanceOf(Exception.class);
+ }
+
+ @Test
+ void testDraftWithMultipleRecipients() throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("Message to multiple recipients");
+ exchange.getMessage().setHeader(MAIL_TO, "[email protected],
[email protected]");
+ exchange.getMessage().setHeader(MAIL_CC, "[email protected],
[email protected]");
+ exchange.getMessage().setHeader(MAIL_BCC, "[email protected]");
+ exchange.getMessage().setHeader(MAIL_SUBJECT, "Multiple Recipients
Test");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Draft draft = exchange.getMessage().getBody(Draft.class);
+ assertThat(draft).isNotNull();
+
+ String decodedMessage = decodeMessage(draft.getMessage().getRaw());
+ assertThat(decodedMessage)
+ .contains("To: [email protected]")
+ .contains("[email protected]")
+ .contains("Cc: [email protected]")
+ .contains("[email protected]")
+ .contains("Bcc: [email protected]")
+ .contains("Subject: Multiple Recipients Test")
+ .contains("Message to multiple recipients");
+ }
+
+ // Helper method to decode Base64 message
+ private String decodeMessage(String encodedMessage) {
+ byte[] decodedBytes = Base64.decodeBase64(encodedMessage);
+ return new String(decodedBytes, StandardCharsets.UTF_8);
+ }
+}