diff --git a/connectors/email/element-templates/email-outbound-connector.json b/connectors/email/element-templates/email-outbound-connector.json index c3fbe020bc..b128388ed5 100644 --- a/connectors/email/element-templates/email-outbound-connector.json +++ b/connectors/email/element-templates/email-outbound-connector.json @@ -4,7 +4,7 @@ "id" : "io.camunda.connectors.email.v1", "description" : "Execute email requests", "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/email/", - "version" : 1, + "version" : 2, "category" : { "id" : "connectors", "name" : "Connectors" @@ -538,6 +538,29 @@ }, "tooltip" : "Comma-separated list of email, e.g., 'email1@domain.com,email2@domain.com' or '=[ \"email1@domain.com\", \"email2@domain.com\"]'", "type" : "String" + }, { + "id" : "smtpHeaders", + "label" : "Headers", + "optional" : true, + "feel" : "required", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.headers", + "type" : "zeebe:input" + }, + "condition" : { + "allMatch" : [ { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Additional email headers", + "type" : "String" }, { "id" : "smtpSubject", "label" : "Subject", @@ -565,12 +588,42 @@ "tooltip" : "Email's subject", "type" : "String" }, { - "id" : "smtpBody", - "label" : "Email Content", + "id" : "contentType", + "label" : "ContentType", "optional" : false, - "constraints" : { - "notEmpty" : true + "value" : "PLAIN", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.contentType", + "type" : "zeebe:input" }, + "condition" : { + "allMatch" : [ { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Email's contentType", + "type" : "Dropdown", + "choices" : [ { + "name" : "PLAIN", + "value" : "PLAIN" + }, { + "name" : "HTML", + "value" : "HTML" + }, { + "name" : "HTML & Plaintext", + "value" : "MULTIPART" + } ] + }, { + "id" : "smtpBody", + "label" : "Email Text Content", + "optional" : false, "feel" : "optional", "group" : "sendEmailSmtp", "binding" : { @@ -579,6 +632,10 @@ }, "condition" : { "allMatch" : [ { + "property" : "contentType", + "oneOf" : [ "PLAIN", "MULTIPART" ], + "type" : "simple" + }, { "property" : "data.smtpActionDiscriminator", "equals" : "sendEmailSmtp", "type" : "simple" @@ -590,6 +647,33 @@ }, "tooltip" : "Email's content", "type" : "Text" + }, { + "id" : "smtpHtmlBody", + "label" : "Email Html Content", + "optional" : false, + "feel" : "optional", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.htmlBody", + "type" : "zeebe:input" + }, + "condition" : { + "allMatch" : [ { + "property" : "contentType", + "oneOf" : [ "HTML", "MULTIPART" ], + "type" : "simple" + }, { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Email's Html content", + "type" : "Text" }, { "id" : "pop3maxToBeRead", "label" : "Maximum number of emails to be read", diff --git a/connectors/email/element-templates/hybrid/email-outbound-connector-hybrid.json b/connectors/email/element-templates/hybrid/email-outbound-connector-hybrid.json index 345da43598..13f8b9e28d 100644 --- a/connectors/email/element-templates/hybrid/email-outbound-connector-hybrid.json +++ b/connectors/email/element-templates/hybrid/email-outbound-connector-hybrid.json @@ -4,7 +4,7 @@ "id" : "io.camunda.connectors.email.v1-hybrid", "description" : "Execute email requests", "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/email/", - "version" : 1, + "version" : 2, "category" : { "id" : "connectors", "name" : "Connectors" @@ -543,6 +543,29 @@ }, "tooltip" : "Comma-separated list of email, e.g., 'email1@domain.com,email2@domain.com' or '=[ \"email1@domain.com\", \"email2@domain.com\"]'", "type" : "String" + }, { + "id" : "smtpHeaders", + "label" : "Headers", + "optional" : true, + "feel" : "required", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.headers", + "type" : "zeebe:input" + }, + "condition" : { + "allMatch" : [ { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Additional email headers", + "type" : "String" }, { "id" : "smtpSubject", "label" : "Subject", @@ -570,12 +593,42 @@ "tooltip" : "Email's subject", "type" : "String" }, { - "id" : "smtpBody", - "label" : "Email Content", + "id" : "contentType", + "label" : "ContentType", "optional" : false, - "constraints" : { - "notEmpty" : true + "value" : "PLAIN", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.contentType", + "type" : "zeebe:input" }, + "condition" : { + "allMatch" : [ { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Email's contentType", + "type" : "Dropdown", + "choices" : [ { + "name" : "PLAIN", + "value" : "PLAIN" + }, { + "name" : "HTML", + "value" : "HTML" + }, { + "name" : "HTML & Plaintext", + "value" : "MULTIPART" + } ] + }, { + "id" : "smtpBody", + "label" : "Email Text Content", + "optional" : false, "feel" : "optional", "group" : "sendEmailSmtp", "binding" : { @@ -584,6 +637,10 @@ }, "condition" : { "allMatch" : [ { + "property" : "contentType", + "oneOf" : [ "PLAIN", "MULTIPART" ], + "type" : "simple" + }, { "property" : "data.smtpActionDiscriminator", "equals" : "sendEmailSmtp", "type" : "simple" @@ -595,6 +652,33 @@ }, "tooltip" : "Email's content", "type" : "Text" + }, { + "id" : "smtpHtmlBody", + "label" : "Email Html Content", + "optional" : false, + "feel" : "optional", + "group" : "sendEmailSmtp", + "binding" : { + "name" : "data.smtpAction.htmlBody", + "type" : "zeebe:input" + }, + "condition" : { + "allMatch" : [ { + "property" : "contentType", + "oneOf" : [ "HTML", "MULTIPART" ], + "type" : "simple" + }, { + "property" : "data.smtpActionDiscriminator", + "equals" : "sendEmailSmtp", + "type" : "simple" + }, { + "property" : "protocol", + "equals" : "smtp", + "type" : "simple" + } ] + }, + "tooltip" : "Email's Html content", + "type" : "Text" }, { "id" : "pop3maxToBeRead", "label" : "Maximum number of emails to be read", diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java index 69e2fa4340..3afcbcdd40 100644 --- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java +++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/outbound/JakartaEmailActionExecutor.java @@ -6,6 +6,8 @@ */ package io.camunda.connector.email.client.jakarta.outbound; +import static io.camunda.connector.email.outbound.protocols.actions.ContentType.PLAIN; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.camunda.connector.email.authentication.Authentication; @@ -14,12 +16,12 @@ import io.camunda.connector.email.outbound.model.EmailRequest; import io.camunda.connector.email.outbound.protocols.Protocol; import io.camunda.connector.email.outbound.protocols.actions.*; +import io.camunda.connector.email.outbound.protocols.actions.ContentType; import io.camunda.connector.email.response.*; import jakarta.mail.*; -import jakarta.mail.internet.AddressException; -import jakarta.mail.internet.InternetAddress; -import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.*; import jakarta.mail.search.*; +import java.nio.charset.StandardCharsets; import java.util.*; public class JakartaEmailActionExecutor implements EmailActionExecutor { @@ -249,13 +251,17 @@ private SendEmailResponse smtpSendEmail( Optional to = createParsedInternetAddresses(smtpSendEmail.to()); Optional cc = createParsedInternetAddresses(smtpSendEmail.cc()); Optional bcc = createParsedInternetAddresses(smtpSendEmail.bcc()); + Optional> headers = Optional.ofNullable(smtpSendEmail.headers()); Message message = new MimeMessage(session); message.setFrom(new InternetAddress(smtpSendEmail.from())); if (to.isPresent()) message.setRecipients(Message.RecipientType.TO, to.get()); if (cc.isPresent()) message.setRecipients(Message.RecipientType.CC, cc.get()); if (bcc.isPresent()) message.setRecipients(Message.RecipientType.BCC, bcc.get()); + headers.ifPresent(stringObjectMap -> setMessageHeaders(stringObjectMap, message)); message.setSubject(smtpSendEmail.subject()); - message.setText(smtpSendEmail.body()); + Multipart multipart = getMultipart(smtpSendEmail); + + message.setContent(multipart); try (Transport transport = session.getTransport()) { this.jakartaUtils.connectTransport(transport, authentication); transport.sendMessage(message, message.getAllRecipients()); @@ -266,6 +272,44 @@ private SendEmailResponse smtpSendEmail( } } + private void setMessageHeaders(Map stringObjectMap, Message message) { + stringObjectMap.forEach( + (key, value) -> { + try { + message.setHeader(key, value); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + }); + } + + private Multipart getMultipart(SmtpSendEmail smtpSendEmail) throws MessagingException { + Multipart multipart = new MimeMultipart(); + ContentType contentType = + smtpSendEmail.contentType() == null ? PLAIN : smtpSendEmail.contentType(); + switch (contentType) { + case PLAIN -> { + MimeBodyPart textPart = new MimeBodyPart(); + textPart.setText(smtpSendEmail.body(), StandardCharsets.UTF_8.name()); + multipart.addBodyPart(textPart); + } + case HTML -> { + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(smtpSendEmail.htmlBody(), JakartaUtils.HTML_CHARSET); + multipart.addBodyPart(htmlPart); + } + case MULTIPART -> { + MimeBodyPart textPart = new MimeBodyPart(); + textPart.setText(smtpSendEmail.body(), StandardCharsets.UTF_8.name()); + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(smtpSendEmail.htmlBody(), JakartaUtils.HTML_CHARSET); + multipart.addBodyPart(textPart); + multipart.addBodyPart(htmlPart); + } + } + return multipart; + } + private SearchTerm createSearchTerms(JsonNode jsonNode) throws AddressException { List searchTerms = new ArrayList<>(); if (jsonNode.has("operator")) { diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java index a4a227d815..aa1e8ccb55 100644 --- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java +++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java @@ -34,6 +34,7 @@ public class JakartaUtils { private static final Logger LOGGER = LoggerFactory.getLogger(JakartaUtils.class); private static final String REGEX_PATH_SPLITTER = "[./]"; + public static final String HTML_CHARSET = "text/html; charset=utf-8"; public Session createSession(Configuration configuration) { return Session.getInstance( diff --git a/connectors/email/src/main/java/io/camunda/connector/email/outbound/EmailConnectorFunction.java b/connectors/email/src/main/java/io/camunda/connector/email/outbound/EmailConnectorFunction.java index 1aaaf30efb..f1ef2a7727 100644 --- a/connectors/email/src/main/java/io/camunda/connector/email/outbound/EmailConnectorFunction.java +++ b/connectors/email/src/main/java/io/camunda/connector/email/outbound/EmailConnectorFunction.java @@ -25,7 +25,7 @@ name = "Email Connector", description = "Execute email requests", inputDataClass = EmailRequest.class, - version = 1, + version = 2, propertyGroups = { @ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"), @ElementTemplate.PropertyGroup(id = "protocol", label = "Protocol"), diff --git a/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/ContentType.java b/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/ContentType.java new file mode 100644 index 0000000000..5eecf55094 --- /dev/null +++ b/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/ContentType.java @@ -0,0 +1,23 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.email.outbound.protocols.actions; + +public enum ContentType { + PLAIN("text/plain; charset=utf-8"), + HTML("text/html; charset=utf-8"), + MULTIPART("multipart/mixed; charset=utf-8"); + + private final String value; + + ContentType(String value) { + this.value = value; + } + + public String type() { + return value; + } +} diff --git a/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/SmtpSendEmail.java b/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/SmtpSendEmail.java index 8b80d2a197..21c6532804 100644 --- a/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/SmtpSendEmail.java +++ b/connectors/email/src/main/java/io/camunda/connector/email/outbound/protocols/actions/SmtpSendEmail.java @@ -11,6 +11,7 @@ import io.camunda.connector.generator.java.annotation.TemplateSubType; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import java.util.Map; @TemplateSubType(id = "sendEmailSmtp", label = "Send Email") public record SmtpSendEmail( @@ -57,6 +58,16 @@ public record SmtpSendEmail( optional = true) @Valid Object bcc, + @TemplateProperty( + label = "Headers", + group = "sendEmailSmtp", + id = "smtpHeaders", + tooltip = "Additional email headers", + feel = Property.FeelMode.required, + binding = @TemplateProperty.PropertyBinding(name = "data.smtpAction.headers"), + optional = true) + @Valid + Map headers, @TemplateProperty( label = "Subject", group = "sendEmailSmtp", @@ -69,14 +80,48 @@ public record SmtpSendEmail( @NotNull String subject, @TemplateProperty( - label = "Email Content", + label = "ContentType", + group = "sendEmailSmtp", + id = "contentType", + defaultValue = "PLAIN", + type = TemplateProperty.PropertyType.Dropdown, + choices = { + @TemplateProperty.DropdownPropertyChoice(label = "PLAIN", value = "PLAIN"), + @TemplateProperty.DropdownPropertyChoice(label = "HTML", value = "HTML"), + @TemplateProperty.DropdownPropertyChoice( + label = "HTML & Plaintext", + value = "MULTIPART") + }, + tooltip = "Email's contentType", + binding = @TemplateProperty.PropertyBinding(name = "data.smtpAction.contentType")) + @Valid + ContentType contentType, + @TemplateProperty( + label = "Email Text Content", group = "sendEmailSmtp", id = "smtpBody", type = TemplateProperty.PropertyType.Text, tooltip = "Email's content", binding = @TemplateProperty.PropertyBinding(name = "data.smtpAction.body"), - feel = Property.FeelMode.optional) + feel = Property.FeelMode.optional, + condition = + @TemplateProperty.PropertyCondition( + property = "contentType", + oneOf = {"PLAIN", "MULTIPART"})) @Valid - @NotNull - String body) + String body, + @TemplateProperty( + label = "Email Html Content", + group = "sendEmailSmtp", + id = "smtpHtmlBody", + type = TemplateProperty.PropertyType.Text, + tooltip = "Email's Html content", + binding = @TemplateProperty.PropertyBinding(name = "data.smtpAction.htmlBody"), + feel = Property.FeelMode.optional, + condition = + @TemplateProperty.PropertyCondition( + property = "contentType", + oneOf = {"HTML", "MULTIPART"})) + @Valid + String htmlBody) implements SmtpAction {} diff --git a/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java b/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java index cd6d95fc79..6bcd6aedb0 100644 --- a/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java +++ b/connectors/email/src/test/java/io/camunda/connector/email/client/jakarta/JakartaExecutorTest.java @@ -23,6 +23,7 @@ import io.camunda.connector.email.outbound.protocols.actions.*; import io.camunda.connector.email.response.*; import jakarta.mail.*; +import jakarta.mail.internet.MimeMultipart; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -64,6 +65,7 @@ void executeSmtpSendEmail() throws MessagingException { when(smtpSendEmail.bcc()).thenReturn(List.of("bcc")); when(smtpSendEmail.from()).thenReturn("myself"); when(smtpSendEmail.body()).thenReturn("body"); + when(smtpSendEmail.contentType()).thenReturn(ContentType.PLAIN); when(session.getTransport()).thenReturn(transport); actionExecutor.execute(emailRequest); @@ -75,7 +77,7 @@ void executeSmtpSendEmail() throws MessagingException { try { return Arrays.stream(argument.getFrom()) .allMatch(address -> address.toString().contains("myself")) - && argument.getContent().toString().contains("body"); + && isMultipartBodyWithSinglePlaintextFormat(argument.getContent(), "body"); } catch (MessagingException | IOException e) { throw new RuntimeException(e); } @@ -87,6 +89,17 @@ void executeSmtpSendEmail() throws MessagingException { && Arrays.toString(argument).contains("bcc"))); } + private boolean isMultipartBodyWithSinglePlaintextFormat(Object content, String expectedContent) { + if (content instanceof MimeMultipart mimeMultipart) { + try { + return mimeMultipart.getBodyPart(0).getContent().equals(expectedContent); + } catch (IOException | MessagingException e) { + throw new RuntimeException("Error while reading body part from content", e); + } + } + return false; + } + @Test void executePop3ListEmails() throws MessagingException { JakartaUtils sessionFactory = mock(JakartaUtils.class);