diff --git a/connectors/sendgrid/element-templates/hybrid/sendgrid-outbound-connector-hybrid.json b/connectors/sendgrid/element-templates/hybrid/sendgrid-outbound-connector-hybrid.json index 5c8f590aa3..8cdc47dc2e 100644 --- a/connectors/sendgrid/element-templates/hybrid/sendgrid-outbound-connector-hybrid.json +++ b/connectors/sendgrid/element-templates/hybrid/sendgrid-outbound-connector-hybrid.json @@ -7,7 +7,7 @@ "keywords" : [ ] }, "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/sendgrid/", - "version" : 3, + "version" : 4, "category" : { "id" : "connectors", "name" : "Connectors" @@ -233,6 +233,18 @@ "type" : "simple" }, "type" : "Text" + }, { + "id" : "attachments", + "label" : "attachments", + "description" : "List of Camunda Documents", + "optional" : true, + "feel" : "required", + "group" : "content", + "binding" : { + "name" : "attachments", + "type" : "zeebe:input" + }, + "type" : "String" }, { "id" : "resultVariable", "label" : "Result variable", diff --git a/connectors/sendgrid/element-templates/sendgrid-outbound-connector.json b/connectors/sendgrid/element-templates/sendgrid-outbound-connector.json index 7bf0a23d5d..05e5db7807 100644 --- a/connectors/sendgrid/element-templates/sendgrid-outbound-connector.json +++ b/connectors/sendgrid/element-templates/sendgrid-outbound-connector.json @@ -7,7 +7,7 @@ "keywords" : [ ] }, "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/sendgrid/", - "version" : 3, + "version" : 4, "category" : { "id" : "connectors", "name" : "Connectors" @@ -228,6 +228,18 @@ "type" : "simple" }, "type" : "Text" + }, { + "id" : "attachments", + "label" : "attachments", + "description" : "List of Camunda Documents", + "optional" : true, + "feel" : "required", + "group" : "content", + "binding" : { + "name" : "attachments", + "type" : "zeebe:input" + }, + "type" : "String" }, { "id" : "resultVariable", "label" : "Result variable", diff --git a/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/SendGridFunction.java b/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/SendGridFunction.java index da56e4df5f..bf1b76645d 100644 --- a/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/SendGridFunction.java +++ b/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/SendGridFunction.java @@ -12,26 +12,29 @@ import com.sendgrid.Response; import com.sendgrid.SendGrid; import com.sendgrid.helpers.mail.Mail; +import com.sendgrid.helpers.mail.objects.Attachments; import com.sendgrid.helpers.mail.objects.Personalization; import io.camunda.connector.api.annotation.OutboundConnector; import io.camunda.connector.api.outbound.OutboundConnectorContext; import io.camunda.connector.api.outbound.OutboundConnectorFunction; import io.camunda.connector.generator.java.annotation.ElementTemplate; import io.camunda.connector.sendgrid.model.SendGridRequest; +import io.camunda.document.Document; import java.io.IOException; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @OutboundConnector( name = "SendGrid", - inputVariables = {"apiKey", "from", "to", "template", "content"}, + inputVariables = {"apiKey", "from", "to", "template", "content", "attachments"}, type = "io.camunda:sendgrid:1") @ElementTemplate( id = "io.camunda.connectors.SendGrid.v2", name = "SendGrid Outbound Connector", description = "Send an email via SendGrid", inputDataClass = SendGridRequest.class, - version = 3, + version = 4, propertyGroups = { @ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"), @ElementTemplate.PropertyGroup(id = "sender", label = "Sender"), @@ -43,11 +46,9 @@ icon = "icon.svg") public class SendGridFunction implements OutboundConnectorFunction { - private static final Logger LOGGER = LoggerFactory.getLogger(SendGridFunction.class); - protected static final ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - + private static final Logger LOGGER = LoggerFactory.getLogger(SendGridFunction.class); private final SendGridClientSupplier sendGridSupplier; public SendGridFunction() { @@ -84,6 +85,7 @@ private Mail createEmail(final SendGridRequest request) { mail.setFrom(request.getInnerSenGridEmailFrom()); addContentIfPresent(mail, request); addTemplateIfPresent(mail, request); + addAttachmentIfPresent(mail, request.getAttachments()); return mail; } @@ -98,6 +100,18 @@ private void addTemplateIfPresent(final Mail mail, final SendGridRequest request } } + private void addAttachmentIfPresent(final Mail mail, List documents) { + if (documents != null && !documents.isEmpty()) { + documents.forEach( + document -> { + Attachments attachments = + new Attachments.Builder(document.metadata().getFileName(), document.asInputStream()) + .build(); + mail.addAttachments(attachments); + }); + } + } + private void addContentIfPresent(final Mail mail, final SendGridRequest request) { if (request.hasContent()) { final SendGridRequest.Content content = request.getContent(); diff --git a/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/model/SendGridRequest.java b/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/model/SendGridRequest.java index c49429fd97..42e5b8de9f 100644 --- a/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/model/SendGridRequest.java +++ b/connectors/sendgrid/src/main/java/io/camunda/connector/sendgrid/model/SendGridRequest.java @@ -16,12 +16,15 @@ import io.camunda.connector.generator.java.annotation.TemplateProperty.DropdownPropertyChoice; import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyBinding; import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyCondition; +import io.camunda.document.Document; import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.List; import java.util.Map; import java.util.Objects; +import org.apache.commons.lang3.StringUtils; public class SendGridRequest { @TemplateProperty(group = "authentication", label = "SendGrid API key") @@ -99,6 +102,16 @@ public record Content( @Valid private Content content; + @TemplateProperty( + id = "attachments", + group = "content", + label = "attachments", + optional = true, + feel = Property.FeelMode.required, + description = + "List of Camunda Documents") + private List attachments; + @AssertTrue(message = "must not be empty") private boolean isSenderName() { return from != null && isNotBlank(from.name()); @@ -128,6 +141,16 @@ private boolean isHasContentOrTemplate() { return content != null || template != null; } + @AssertTrue(message = "each attached document must contain a file name") + private boolean isAttachmentsShouldContainsFileName() { + if (this.attachments == null || this.attachments.isEmpty()) { + return true; + } + return this.attachments.stream() + .map(doc -> doc.metadata().getFileName()) + .noneMatch(StringUtils::isBlank); + } + public String getApiKey() { return apiKey; } @@ -192,6 +215,14 @@ public void setMailType(MailType mailType) { this.mailType = mailType; } + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -205,12 +236,13 @@ public boolean equals(final Object o) { && Objects.equals(from, that.from) && Objects.equals(to, that.to) && Objects.equals(template, that.template) - && Objects.equals(content, that.content); + && Objects.equals(content, that.content) + && Objects.equals(attachments, that.attachments); } @Override public int hashCode() { - return Objects.hash(apiKey, from, to, template, content); + return Objects.hash(apiKey, from, to, template, content, attachments); } @Override @@ -225,6 +257,8 @@ public String toString() { + template + ", content=" + content + + ", attachments=" + + attachments + '}'; } } diff --git a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/BaseTest.java b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/BaseTest.java index 6c76a23a27..71785835fc 100644 --- a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/BaseTest.java +++ b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/BaseTest.java @@ -30,6 +30,8 @@ public class BaseTest { "src/test/resources/requests/validation/validate-receiver-email-tests-cases.json"; private static final String FAIL_REQUEST_WITH_WRONG_RECEIVER_NAME = "src/test/resources/requests/validation/validate-receiver-name-tests-cases.json"; + private static final String FAIL_REQUEST_WITH_ATTACHMENTS_WITHOUT_FILE_NAME = + "src/test/resources/requests/validation/request-with-attachments-without-file-name.json"; private static final String SUCCESS_REPLACE_SECRETS_REQUEST_CASES_PATH = "src/test/resources/requests/replace-secrets-success-test-cases.json"; @@ -48,6 +50,7 @@ protected interface ActualValue { String RECEIVER_NAME = "Jane Doe"; String SENDER_EMAIL = "john.doe@example.com"; String SENDER_NAME = "John Doe"; + String ATTACHED_FILE_NAME = "google-my-business-logo-png-transparent.png"; interface Content { String SUBJECT = "subject_test"; @@ -130,6 +133,10 @@ protected static Stream failTestWithWrongReceiverEmail() throws IOExcept return loadTestCasesFromResourceFile(FAIL_REQUEST_WITH_WRONG_RECEIVER_EMAIL); } + protected static Stream failTestWithEmptyFileName() throws IOException { + return loadTestCasesFromResourceFile(FAIL_REQUEST_WITH_ATTACHMENTS_WITHOUT_FILE_NAME); + } + protected static Stream failTestWithWrongReceiverName() throws IOException { return loadTestCasesFromResourceFile(FAIL_REQUEST_WITH_WRONG_RECEIVER_NAME); } diff --git a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridFunctionTest.java b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridFunctionTest.java index 957afd6e50..4feea6f0da 100644 --- a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridFunctionTest.java +++ b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridFunctionTest.java @@ -8,19 +8,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.sendgrid.Method; import com.sendgrid.Request; import com.sendgrid.Response; import com.sendgrid.SendGrid; +import io.camunda.client.api.response.DocumentMetadata; import io.camunda.connector.api.outbound.OutboundConnectorContext; import io.camunda.connector.sendgrid.model.SendGridRequest; import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import io.camunda.document.Document; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -42,6 +43,8 @@ public class SendGridFunctionTest extends BaseTest { private static final String PERSONALIZATION_JSON_NAME = "personalizations"; private static final String NAME_JSON_NAME = "name"; private static final String EMAIL_JSON_NAME = "email"; + private static final String ATTACHMENTS_JSON_NAME = "attachments"; + private static final String ATTACHMENTS_FILE_NAME_JSON_NAME = "filename"; private OutboundConnectorContext context; private SendGridFunction function; @@ -107,9 +110,15 @@ public void execute_shouldReturnNullIfResponseStatusCodeIs202(int statusCode) th public void execute_shouldCreateRequestWithMailAndExpectedData(String input) throws Exception { // Given context = contextBuilder.variables(input).build(); - ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(Request.class); + + var contextWithMocketDocument = mock(OutboundConnectorContext.class); + + var requestWithMockedDocument = prepareRequestWithDocumentMock(context); + + when(contextWithMocketDocument.bindVariables(any())).thenReturn(requestWithMockedDocument); + // When - function.execute(context); + function.execute(contextWithMocketDocument); verify(sendGridMock).api(requestArgumentCaptor.capture()); // Then we have POST request with mail participants, Request requestValue = requestArgumentCaptor.getValue(); @@ -122,10 +131,14 @@ public void execute_shouldCreateRequestWithMailAndExpectedData(String input) thr requestJsonObject.withObject( JsonPointer.valueOf("/" + PERSONALIZATION_JSON_NAME + "/0/" + TO_JSON_NAME + "/0")); + var array = (ArrayNode) requestJsonObject.get(ATTACHMENTS_JSON_NAME); + String attachmentName = array.get(0).get(ATTACHMENTS_FILE_NAME_JSON_NAME).textValue(); + assertThat(from.get(NAME_JSON_NAME).textValue()).isEqualTo(ActualValue.SENDER_NAME); assertThat(from.get(EMAIL_JSON_NAME).textValue()).isEqualTo(ActualValue.SENDER_EMAIL); assertThat(to.get(NAME_JSON_NAME).textValue()).isEqualTo(ActualValue.RECEIVER_NAME); assertThat(to.get(EMAIL_JSON_NAME).textValue()).isEqualTo(ActualValue.RECEIVER_EMAIL); + assertThat(attachmentName).isEqualTo(ActualValue.ATTACHED_FILE_NAME); } @ParameterizedTest(name = "Should send mail with template. Test case # {index}") @@ -133,16 +146,27 @@ public void execute_shouldCreateRequestWithMailAndExpectedData(String input) thr public void execute_shouldSendMailByTemplateIfTemplateExist(String input) throws Exception { // Given context = contextBuilder.variables(input).build(); - ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(Request.class); + + var contextWithMocketDocument = mock(OutboundConnectorContext.class); + + var requestWithMockedDocument = prepareRequestWithDocumentMock(context); + + when(contextWithMocketDocument.bindVariables(any())).thenReturn(requestWithMockedDocument); + // When - function.execute(context); + function.execute(contextWithMocketDocument); verify(sendGridMock).api(requestArgumentCaptor.capture()); // Then we have 'template_id' in sendGridRequest with expected ID and 'content' is not exist Request requestValue = requestArgumentCaptor.getValue(); + var requestJsonObject = objectMapper.readTree(requestValue.getBody()); + + var array = (ArrayNode) requestJsonObject.get(ATTACHMENTS_JSON_NAME); + String attachmentName = array.get(0).get(ATTACHMENTS_FILE_NAME_JSON_NAME).textValue(); assertThat(requestJsonObject.get(TEMPLATE_ID_JSON_NAME).textValue()) .isEqualTo(ActualValue.Template.ID); assertThat(requestJsonObject.has(CONTENT_JSON_NAME)).isFalse(); + assertThat(attachmentName).isEqualTo(ActualValue.ATTACHED_FILE_NAME); } @ParameterizedTest(name = "Should send mail with content. Test case # {index}") @@ -150,8 +174,16 @@ public void execute_shouldSendMailByTemplateIfTemplateExist(String input) throws public void execute_shouldSendMailIfContentExist(String input) throws Exception { // Given context = contextBuilder.variables(input).build(); + + var contextWithMocketDocument = mock(OutboundConnectorContext.class); + + var requestWithMockedDocument = prepareRequestWithDocumentMock(context); + + when(contextWithMocketDocument.bindVariables(any())).thenReturn(requestWithMockedDocument); + // When - function.execute(context); + function.execute(contextWithMocketDocument); + verify(sendGridMock).api(requestArgumentCaptor.capture()); // Then we have 'content' in sendGridRequest with expected data and template ID is not exist var requestJsonObject = objectMapper.readTree(requestArgumentCaptor.getValue().getBody()); @@ -166,4 +198,19 @@ public void execute_shouldSendMailIfContentExist(String input) throws Exception assertThat(requestJsonObject.has(TEMPLATE_ID_JSON_NAME)).isFalse(); } + + private SendGridRequest prepareRequestWithDocumentMock(OutboundConnectorContext context) { + var request = context.bindVariables(SendGridRequest.class); + + var document = mock(Document.class); + var documentMetadata = mock(DocumentMetadata.class); + when(document.metadata()).thenReturn(documentMetadata); + + String fileName = request.getAttachments().getFirst().metadata().getFileName(); + when(documentMetadata.getFileName()).thenReturn(fileName); + when(document.asInputStream()).thenReturn(new ByteArrayInputStream(new byte[0])); + + request.setAttachments(List.of(document)); + return request; + } } diff --git a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridRequestTest.java b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridRequestTest.java index 0c77d3b5c1..f0dfa125f7 100644 --- a/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridRequestTest.java +++ b/connectors/sendgrid/src/test/java/io/camunda/connector/sendgrid/SendGridRequestTest.java @@ -91,6 +91,19 @@ public void validate_shouldThrowExceptionWhenReceiverNameIsBlankOrNull(String in assertThat(thrown.getMessage()).contains("receiverName"); } + @ParameterizedTest + @MethodSource("failTestWithEmptyFileName") + public void validate_shouldThrowExceptionWhenDocumentsNamesAreEmpty(String input) { + var context = getContextBuilderWithSecrets().variables(input).build(); + + Exception exception = + assertThrows( + ConnectorInputException.class, + () -> context.bindVariables(SendGridRequest.class)); + + assertThat(exception.getMessage()).contains("attachmentsShouldContainsFileName"); + } + @ParameterizedTest(name = "Should replace secrets in template") @MethodSource("successReplaceSecretsTemplateRequestCases") void replaceSecrets_shouldReplaceSecretsWhenExistTemplateRequest(String input) { diff --git a/connectors/sendgrid/src/test/resources/requests/send-mail-by-template-success-cases.json b/connectors/sendgrid/src/test/resources/requests/send-mail-by-template-success-cases.json index 2d1a8178a7..87536b66cf 100644 --- a/connectors/sendgrid/src/test/resources/requests/send-mail-by-template-success-cases.json +++ b/connectors/sendgrid/src/test/resources/requests/send-mail-by-template-success-cases.json @@ -16,7 +16,18 @@ "shipAddress": "{{secrets.TEMPLATE_DATA_SHIP_ADDRESS}}", "shipZip": "{{secrets.TEMPLATE_DATA_SHIP_ZIP}}" } - } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "google-my-business-logo-png-transparent.png", + "size" : 66497, + "customProperties" : { } + } + }] }, { "apiKey": "send_grid_key", @@ -35,7 +46,18 @@ "shipAddress": "Krossener Str. 24", "shipZip": "10245" } - } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "google-my-business-logo-png-transparent.png", + "size" : 66497, + "customProperties" : { } + } + }] }, { "to":{ @@ -78,6 +100,17 @@ ] }, "id":"d-0b51e8f77bf8450fae379e0639ca0d11" - } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "google-my-business-logo-png-transparent.png", + "size" : 66497, + "customProperties" : { } + } + }] } ] diff --git a/connectors/sendgrid/src/test/resources/requests/send-mail-with-content-success-cases.json b/connectors/sendgrid/src/test/resources/requests/send-mail-with-content-success-cases.json index fbb741022d..8fa12feb38 100644 --- a/connectors/sendgrid/src/test/resources/requests/send-mail-with-content-success-cases.json +++ b/connectors/sendgrid/src/test/resources/requests/send-mail-with-content-success-cases.json @@ -13,7 +13,18 @@ "subject": "{{secrets.CONTENT_SUBJECT}}", "type": "{{secrets.CONTENT_TYPE}}", "value": "{{secrets.CONTENT_VALUE}}" - } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "google-my-business-logo-png-transparent.png", + "size" : 66497, + "customProperties" : { } + } + }] }, { "apiKey": "send_grid_key", @@ -29,6 +40,17 @@ "subject": "subject_test", "type": "text/json", "value": "Hello world" - } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "google-my-business-logo-png-transparent.png", + "size" : 66497, + "customProperties" : { } + } + }] } ] diff --git a/connectors/sendgrid/src/test/resources/requests/validation/request-with-attachments-without-file-name.json b/connectors/sendgrid/src/test/resources/requests/validation/request-with-attachments-without-file-name.json new file mode 100644 index 0000000000..8fdf64db11 --- /dev/null +++ b/connectors/sendgrid/src/test/resources/requests/validation/request-with-attachments-without-file-name.json @@ -0,0 +1,74 @@ +[ + { + "apiKey": "send_grid_key", + "from": { + "name": "John Doe", + "email": "john.doe@example.com" + }, + "to": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + }, + "template": { + "id": "d-0b51e8f77bf8450fae379e0639ca0d11", + "data": { + "accountName": "Feuerwehrmann Sam", + "shipAddress": "Krossener Str. 24", + "shipZip": "10245" + } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "", + "size" : 66497, + "customProperties" : { } + } + }] + }, + { + "apiKey": "send_grid_key", + "from": { + "name": "John Doe", + "email": "john.doe@example.com" + }, + "to": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + }, + "template": { + "id": "d-0b51e8f77bf8450fae379e0639ca0d11", + "data": { + "accountName": "Feuerwehrmann Sam", + "shipAddress": "Krossener Str. 24", + "shipZip": "10245" + } + }, + "attachments" : [ { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da0", + "metadata" : { + "contentType" : "image/png", + "fileName" : "file.png", + "size" : 66497, + "customProperties" : { } + } + }, + { + "camunda.document.type" : "camunda", + "storeId" : "in-memory", + "documentId" : "2ea3bfcc-7683-4cb6-b0ce-8052ca1d6da1", + "metadata" : { + "contentType" : "image/png", + "fileName" : " ", + "size" : 66497, + "customProperties" : { } + } + } + ] + } +] \ No newline at end of file