diff --git a/conf/guides.yml b/conf/guides.yml index 902a3eeffff..8bf0e74c58e 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -2190,10 +2190,11 @@ guides: title: Help with Grails - name: 'grails-email' - title: 'Send Email and Spock Spring' - subtitle: 'Learn how to send emails with AWS SES and SendGrid from a Grails app and leverage Spock Spring integration to verify interaction.' + title: 'Sending Email from a Grails 8 Application (and Testing It)' + subtitle: 'Send mail over SMTP or through SendGrid / AWS SES, externalize configuration, and verify send interactions with Spock Spring integration tests, without live email.' authors: - 'Sergio del Amo' + - 'Sanjana' category: 'Grails Testing' publicationDate: '2018-06-04' versions: @@ -2257,6 +2258,46 @@ guides: test: Test helpWithGrails: title: Do you need help with Grails? + '8': + sourcePath: guides/grails-email/v8 + publicationDate: '2026-07-01' + tags: + - 'email' + - 'smtp' + - 'sendgrid' + - 'ses' + - 'aws' + - 'spock-spring' + - 'integration-testing' + - 'mail' + sampleRef: + repo: 'grails-guides/grails-email' + branch: 'grails8' + toc: + introduction: + title: Introduction + requirements: + title: What you will need + gettingStarted: + title: Getting Started + emailContract: + title: Email contract + smtpMail: + title: SMTP with the Mail plugin + sendGridService: + title: SendGrid + awsSesService: + title: AWS SES + mailConfiguration: + title: Wire the provider + controller: + title: Controller + testing: + title: Testing without live email + summary: + title: Summary + helpWithGrails: + title: Do you need help with Grails? - name: 'grails-events' title: 'Grails Events' diff --git a/guides/grails-email/v8/guide/awsSesService.adoc b/guides/grails-email/v8/guide/awsSesService.adoc new file mode 100644 index 00000000000..557923595c8 --- /dev/null +++ b/guides/grails-email/v8/guide/awsSesService.adoc @@ -0,0 +1,9 @@ +AWS SES is the third provider option: + +[source,groovy] +.src/main/groovy/example/AwsSesEmailService.groovy +---- +include::../snippets/src/main/groovy/example/AwsSesEmailService.groovy[] +---- + +Set `EMAIL_PROVIDER=ses` and configure `AWS_REGION` and `AWS_SOURCE` plus standard AWS credentials for the SDK. diff --git a/guides/grails-email/v8/guide/controller.adoc b/guides/grails-email/v8/guide/controller.adoc new file mode 100644 index 00000000000..8bde24d20e1 --- /dev/null +++ b/guides/grails-email/v8/guide/controller.adoc @@ -0,0 +1,29 @@ +Accept JSON payloads with a command object that implements `Email` and validates required fields: + +[source,groovy] +.grails-app/controllers/example/EmailCmd.groovy +---- +include::../snippets/grails-app/controllers/example/EmailCmd.groovy[] +---- + +The controller delegates to `EmailService`: + +[source,groovy] +.grails-app/controllers/example/MailController.groovy +---- +include::../snippets/grails-app/controllers/example/MailController.groovy[] +---- + +[IMPORTANT] +==== +This endpoint is demonstration-only. It accepts arbitrary recipients and message content with no authentication, so a deployed copy can become an outbound mail relay through the configured provider. Before any public or production exposure, add authentication, authorization, recipient controls, and abuse/rate-limit protection. +==== + +Example request: + +[source,bash] +---- +curl -X POST http://localhost:8080/mail/send \ + -H 'Content-Type: application/json' \ + -d '{"recipient":"user@example.com","subject":"Hello","textBody":"Hi there"}' +---- diff --git a/guides/grails-email/v8/guide/emailContract.adoc b/guides/grails-email/v8/guide/emailContract.adoc new file mode 100644 index 00000000000..f210773438e --- /dev/null +++ b/guides/grails-email/v8/guide/emailContract.adoc @@ -0,0 +1,25 @@ +Define a small `Email` contract and a single `EmailService` boundary that every provider implements: + +[source,groovy] +.src/main/groovy/example/Email.groovy +---- +include::../snippets/src/main/groovy/example/Email.groovy[] +---- + +[source,groovy] +.src/main/groovy/example/EmailService.groovy +---- +include::../snippets/src/main/groovy/example/EmailService.groovy[] +---- + +Controllers and tests depend on `EmailService`, not on SMTP, SendGrid, or SES directly. Swap providers by changing Spring wiring and configuration. + +Add the mail dependencies in `build.gradle`: + +[source,groovy] +.build.gradle +---- +include::../snippets/build.gradle[] +---- + +Grails 8 uses `org.grails.plugins:grails-mail` 5.x from the Grails plugin repository, compatible with Apache Grails 8. diff --git a/guides/grails-email/v8/guide/gettingStarted.adoc b/guides/grails-email/v8/guide/gettingStarted.adoc new file mode 100644 index 00000000000..4598fc8d915 --- /dev/null +++ b/guides/grails-email/v8/guide/gettingStarted.adoc @@ -0,0 +1,14 @@ +Clone the repository and verify tests pass: + +[source,bash] +---- +git clone -b grails8 https://github.com/grails-guides/grails-email.git +cd grails-email/initial +./gradlew test +---- + +The `initial/` project is a Grails 8 REST API starter with no mail wiring yet. + +To skip ahead, `cd ../complete` and run the same commands; that tree contains the finished mail services, controller, and integration spec. + +Both `initial` and `complete` must pass in CI (see `.github/workflows/grails8.yml`). diff --git a/guides/grails-email/v8/guide/helpWithGrails.adoc b/guides/grails-email/v8/guide/helpWithGrails.adoc new file mode 100644 index 00000000000..e062f614b1a --- /dev/null +++ b/guides/grails-email/v8/guide/helpWithGrails.adoc @@ -0,0 +1 @@ +include::{commondir}/common-helpWithGrails.adoc[] diff --git a/guides/grails-email/v8/guide/introduction.adoc b/guides/grails-email/v8/guide/introduction.adoc new file mode 100644 index 00000000000..144e59678b4 --- /dev/null +++ b/guides/grails-email/v8/guide/introduction.adoc @@ -0,0 +1,5 @@ +Learn how to send mail from a Grails 8 application over SMTP (Grails Mail plugin) and through a provider (SendGrid / AWS SES), externalize configuration in `application.yml`, and verify send interactions with Spock Spring integration tests, without dispatching real email in CI. + +This guide parallels the HTTP client guide in shape: an external integration behind a mockable service boundary, with tests that assert the contract. + +This guide follows the standard Grails guides layout: work in the `initial/` project and compare your progress with `complete/`. diff --git a/guides/grails-email/v8/guide/mailConfiguration.adoc b/guides/grails-email/v8/guide/mailConfiguration.adoc new file mode 100644 index 00000000000..913ee4a1dff --- /dev/null +++ b/guides/grails-email/v8/guide/mailConfiguration.adoc @@ -0,0 +1,9 @@ +Wire the active provider in `grails-app/conf/spring/resources.groovy`: + +[source,groovy] +.grails-app/conf/spring/resources.groovy +---- +include::../snippets/grails-app/conf/spring/resources.groovy[] +---- + +The switch reads `email.provider` from configuration. Provider wiring is skipped in the `test` environment so integration tests can register a mock `EmailService` bean. diff --git a/guides/grails-email/v8/guide/requirements.adoc b/guides/grails-email/v8/guide/requirements.adoc new file mode 100644 index 00000000000..ca2ab782ff4 --- /dev/null +++ b/guides/grails-email/v8/guide/requirements.adoc @@ -0,0 +1,4 @@ +* Approximately 45 minutes +* JDK 21 (Apache Grails 8 requires Java 21) +* A https://www.grails.org/[Grails] installation or the bundled Gradle wrapper in `initial/` and `complete/` +* Optional: local SMTP catcher on port **1025** for manual `bootRun` verification (for example https://github.com/mailhog/MailHog[MailHog]) diff --git a/guides/grails-email/v8/guide/sendGridService.adoc b/guides/grails-email/v8/guide/sendGridService.adoc new file mode 100644 index 00000000000..58c9b149868 --- /dev/null +++ b/guides/grails-email/v8/guide/sendGridService.adoc @@ -0,0 +1,9 @@ +SendGrid implements the same `EmailService` contract: + +[source,groovy] +.src/main/groovy/example/SendGridEmailService.groovy +---- +include::../snippets/src/main/groovy/example/SendGridEmailService.groovy[] +---- + +Set `EMAIL_PROVIDER=sendgrid` and provide `SENDGRID_APIKEY` and `SENDGRID_FROM_EMAIL` (see the `sendgrid` block in `application.yml`). diff --git a/guides/grails-email/v8/guide/smtpMail.adoc b/guides/grails-email/v8/guide/smtpMail.adoc new file mode 100644 index 00000000000..0b5dcfeda7f --- /dev/null +++ b/guides/grails-email/v8/guide/smtpMail.adoc @@ -0,0 +1,17 @@ +The default provider delegates to the Grails Mail plugin: + +[source,groovy] +.grails-app/services/example/SmtpEmailService.groovy +---- +include::../snippets/grails-app/services/example/SmtpEmailService.groovy[] +---- + +Configure SMTP settings in `application.yml`: + +[source,yaml] +.grails-app/conf/application.yml +---- +include::../snippets/grails-app/conf/application.yml[] +---- + +For local development, point `SMTP_HOST` and `SMTP_PORT` at a mail catcher on `localhost:1025`. diff --git a/guides/grails-email/v8/guide/summary.adoc b/guides/grails-email/v8/guide/summary.adoc new file mode 100644 index 00000000000..f6f2cd1c6e4 --- /dev/null +++ b/guides/grails-email/v8/guide/summary.adoc @@ -0,0 +1,8 @@ +You added outbound email to a Grails 8 application: + +* An `EmailService` boundary with SMTP, SendGrid, and AWS SES implementations +* Externalized provider and mail settings in `application.yml` +* A REST controller with command-object validation +* Spock Spring integration tests that mock the sender, with no live email in CI + +Next steps: add HTML templates, queue outbound mail asynchronously, or wire email into domain events. diff --git a/guides/grails-email/v8/guide/testing.adoc b/guides/grails-email/v8/guide/testing.adoc new file mode 100644 index 00000000000..19d0be8a5a3 --- /dev/null +++ b/guides/grails-email/v8/guide/testing.adoc @@ -0,0 +1,17 @@ +Verify the HTTP endpoint builds the message and invokes `EmailService` without sending live email. Register a Spock mock as the primary `EmailService` bean: + +[source,groovy] +.src/integration-test/groovy/example/MailControllerSpec.groovy +---- +include::../snippets/src/integration-test/groovy/example/MailControllerSpec.groovy[] +---- + +The spec posts to `/mail/send` and asserts `emailService.send` is called with the expected recipient, subject, and body. + +Run tests: + +[source,bash] +---- +cd ../complete +./gradlew test integrationTest +---- diff --git a/guides/grails-email/v8/snippets/build.gradle b/guides/grails-email/v8/snippets/build.gradle new file mode 100644 index 00000000000..fe7513c545d --- /dev/null +++ b/guides/grails-email/v8/snippets/build.gradle @@ -0,0 +1,3 @@ + implementation "org.grails.plugins:grails-mail:5.0.3" + implementation "com.sendgrid:sendgrid-java:4.9.3" + implementation "software.amazon.awssdk:ses:2.29.45" diff --git a/guides/grails-email/v8/snippets/grails-app/conf/application.yml b/guides/grails-email/v8/snippets/grails-app/conf/application.yml new file mode 100644 index 00000000000..dfcc8599869 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/conf/application.yml @@ -0,0 +1,28 @@ +#tag::mailConfig[] +grails: + mail: + host: ${SMTP_HOST:localhost} + port: ${SMTP_PORT:1025} + username: ${SMTP_USERNAME:} + password: ${SMTP_PASSWORD:} + default: + from: ${SMTP_FROM:no-reply@example.com} + props: + mail.smtp.auth: ${SMTP_AUTH:false} + mail.smtp.starttls.enable: ${SMTP_STARTTLS:false} +#end::mailConfig[] +#tag::emailProvider[] +email: + provider: ${EMAIL_PROVIDER:smtp} +#end::emailProvider[] +#tag::sendgrid[] +sendgrid: + api: ${SENDGRID_APIKEY:} + from: ${SENDGRID_FROM_EMAIL:} +#end::sendgrid[] +#tag::awsses[] +aws: + ses: + source: ${AWS_SOURCE:} + region: ${AWS_REGION:} +#end::awsses[] diff --git a/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy b/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy new file mode 100644 index 00000000000..975fdf8e599 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy @@ -0,0 +1,26 @@ +import example.AwsSesEmailService +import example.SendGridEmailService +import example.SmtpEmailService +import grails.util.Environment + +beans = { + if (Environment.current == Environment.TEST) { + return + } + //tag::emailServiceBeans[] + def provider = application.config.getProperty('email.provider', String, 'smtp') + switch (provider) { + case 'sendgrid': + emailService(SendGridEmailService) + break + case 'ses': + emailService(AwsSesEmailService) + break + case 'smtp': + emailService(SmtpEmailService) + break + default: + throw new IllegalArgumentException("Unsupported email.provider: ${provider}") + } + //end::emailServiceBeans[] +} diff --git a/guides/grails-email/v8/snippets/grails-app/controllers/example/EmailCmd.groovy b/guides/grails-email/v8/snippets/grails-app/controllers/example/EmailCmd.groovy new file mode 100644 index 00000000000..0675da445fa --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/controllers/example/EmailCmd.groovy @@ -0,0 +1,27 @@ +package example + +import grails.compiler.GrailsCompileStatic +import grails.validation.Validateable +import groovy.transform.ToString + +@ToString +@GrailsCompileStatic +class EmailCmd implements Validateable, Email { + String recipient + List cc = [] + List bcc = [] + String subject + String htmlBody + String textBody + String replyTo + + static constraints = { + recipient nullable: false // <1> + subject nullable: false // <2> + htmlBody nullable: true + textBody nullable: true, validator: { String val, EmailCmd obj -> // <3> + !(!obj.htmlBody && !val) + } + replyTo nullable: true + } +} diff --git a/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy new file mode 100644 index 00000000000..6f90ba13f8b --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy @@ -0,0 +1,25 @@ +package example + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY + +@Slf4j +@CompileStatic +class MailController { + + EmailService emailService + + static allowedMethods = [send: 'POST'] + + def send(EmailCmd cmd) { + if (cmd.hasErrors()) { + respond cmd.errors, view: '/application/errors', status: UNPROCESSABLE_ENTITY + return + } + log.info 'Sending mail to {} with subject {}', cmd.recipient, cmd.subject + emailService.send(cmd) + render status: 200 + } +} diff --git a/guides/grails-email/v8/snippets/grails-app/services/example/SmtpEmailService.groovy b/guides/grails-email/v8/snippets/grails-app/services/example/SmtpEmailService.groovy new file mode 100644 index 00000000000..2556127a1b7 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/services/example/SmtpEmailService.groovy @@ -0,0 +1,36 @@ +package example + +import grails.plugins.mail.MailService +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +@Slf4j +@CompileStatic +class SmtpEmailService implements EmailService { // <1> + + MailService mailService + + @Override + void send(Email email) { + mailService.sendMail { + to email.recipient + subject email.subject + if (email.textBody) { + text email.textBody + } + if (email.htmlBody) { + html email.htmlBody + } + if (email.replyTo) { + replyTo email.replyTo + } + if (email.cc) { + cc email.cc as String[] + } + if (email.bcc) { + bcc email.bcc as String[] + } + } + log.debug('SMTP message handed off to MailService for {}', email.recipient) + } +} diff --git a/guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy b/guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy new file mode 100644 index 00000000000..9c4efb6ff62 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy @@ -0,0 +1,108 @@ +package example + +import grails.testing.mixin.integration.Integration +import groovy.json.JsonSlurper +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestTemplate +import spock.lang.Specification +import spock.mock.DetachedMockFactory + +@Integration +class MailControllerSpec extends Specification { + + @Autowired + EmailService emailService + + void '/mail/send invokes email service with expected message'() { + when: + RestTemplate client = new RestTemplate() + ResponseEntity resp = client.postForEntity( + "http://localhost:$serverPort/mail/send", + [ + subject : 'Test', + recipient: 'delamos@grails.example', + textBody : 'Hola hola' + ], + Map + ) + + then: + resp.statusCode == HttpStatus.OK + 1 * emailService.send({ Email email -> + email.recipient == 'delamos@grails.example' && + email.subject == 'Test' && + email.textBody == 'Hola hola' + }) // <1> + } + + void 'invalid request without recipient returns 422 with validation errors'() { + when: + RestTemplate client = new RestTemplate() + HttpClientErrorException ex = null + try { + client.postForEntity( + "http://localhost:$serverPort/mail/send", + [ + subject : 'Test', + textBody: 'Hola hola' + ], + Map + ) + } catch (HttpClientErrorException e) { + ex = e + } + + then: + ex != null + ex.statusCode == HttpStatus.UNPROCESSABLE_CONTENT + def body = new JsonSlurper().parseText(ex.responseBodyAsString) + body.message + body.path == '/mail/send' + body._links.self.href == "http://localhost:$serverPort/mail/send" + 0 * emailService.send(_) + } + + void 'invalid request without subject returns 422 with validation errors'() { + when: + RestTemplate client = new RestTemplate() + HttpClientErrorException ex = null + try { + client.postForEntity( + "http://localhost:$serverPort/mail/send", + [ + recipient: 'delamos@grails.example', + textBody : 'Hola hola' + ], + Map + ) + } catch (HttpClientErrorException e) { + ex = e + } + + then: + ex != null + ex.statusCode == HttpStatus.UNPROCESSABLE_CONTENT + def body = new JsonSlurper().parseText(ex.responseBodyAsString) + body.message + body.path == '/mail/send' + body._links.self.href == "http://localhost:$serverPort/mail/send" + 0 * emailService.send(_) + } + + @TestConfiguration + static class EmailServiceConfiguration { + private DetachedMockFactory factory = new DetachedMockFactory() + + @Bean + @Primary + EmailService emailService() { + factory.Mock(EmailService) + } + } +} diff --git a/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy new file mode 100644 index 00000000000..752144d6d8a --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy @@ -0,0 +1,83 @@ +package example + +import grails.config.Config +import grails.core.support.GrailsConfigurationAware +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.ses.SesClient +import software.amazon.awssdk.services.ses.model.Body +import software.amazon.awssdk.services.ses.model.Content +import software.amazon.awssdk.services.ses.model.Destination +import software.amazon.awssdk.services.ses.model.Message +import software.amazon.awssdk.services.ses.model.SendEmailRequest +import software.amazon.awssdk.services.ses.model.SendEmailResponse + +@Slf4j +@CompileStatic +class AwsSesEmailService implements EmailService, GrailsConfigurationAware { // <1> + + String sourceEmail + SesClient sesClient + + @Override + void setConfiguration(Config co) { + String awsRegion = co.getProperty('aws.ses.region', String) + if (!awsRegion) { + throw new IllegalStateException('aws.ses.region not set') + } + this.sesClient = SesClient.builder().region(Region.of(awsRegion)).build() + + this.sourceEmail = co.getProperty('aws.ses.source', String) + if (!this.sourceEmail) { + throw new IllegalStateException('aws.ses.source not set') + } + } + + private Body bodyOfEmail(Email email) { + Body.Builder bodyBuilder = Body.builder() + if (email.htmlBody) { + bodyBuilder.html(Content.builder().data(email.htmlBody).build()) + } + if (email.textBody) { + bodyBuilder.text(Content.builder().data(email.textBody).build()) + } + bodyBuilder.build() + } + + private Destination destination(Email email) { + Destination.Builder destinationBuilder = Destination.builder().toAddresses(email.recipient) + if (email.cc) { + destinationBuilder = destinationBuilder.ccAddresses(email.cc) + } + if (email.bcc) { + destinationBuilder = destinationBuilder.bccAddresses(email.bcc) + } + destinationBuilder.build() + } + + private Message composeMessage(Email email) { + Content subject = Content.builder().data(email.subject).build() + Body body = bodyOfEmail(email) + Message.builder().subject(subject).body(body).build() + } + + @Override + void send(Email email) { + try { + Destination destination = destination(email) + Message message = composeMessage(email) + SendEmailRequest sendEmailRequest = SendEmailRequest.builder() + .source(sourceEmail) + .destination(destination) + .message(message) + .build() + SendEmailResponse response = sesClient.sendEmail(sendEmailRequest) + log.info('Email sent! {}', response.messageId()) + + } catch (Exception ex) { + log.warn('The email was not sent.', ex) + throw ex + } + } +} diff --git a/guides/grails-email/v8/snippets/src/main/groovy/example/Email.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/Email.groovy new file mode 100644 index 00000000000..c1e38aa5d97 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/Email.groovy @@ -0,0 +1,14 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +interface Email { + String getRecipient() + List getCc() + List getBcc() + String getSubject() + String getHtmlBody() + String getTextBody() + String getReplyTo() +} diff --git a/guides/grails-email/v8/snippets/src/main/groovy/example/EmailService.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/EmailService.groovy new file mode 100644 index 00000000000..f219336c2a7 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/EmailService.groovy @@ -0,0 +1,8 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +interface EmailService { + void send(Email email) +} diff --git a/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy new file mode 100644 index 00000000000..58aa80aee26 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy @@ -0,0 +1,93 @@ +package example + +import com.sendgrid.Method +import com.sendgrid.Request +import com.sendgrid.Response +import com.sendgrid.SendGrid +import com.sendgrid.helpers.mail.Mail +import com.sendgrid.helpers.mail.objects.Content +import com.sendgrid.helpers.mail.objects.Email as SendGridEmail +import com.sendgrid.helpers.mail.objects.Personalization +import grails.config.Config +import grails.core.support.GrailsConfigurationAware +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +@Slf4j +@CompileStatic +class SendGridEmailService implements EmailService, GrailsConfigurationAware { // <1> + + String api + String from + + @Override + void setConfiguration(Config co) { + this.api = co.getProperty('sendgrid.api', String) + if (!this.api) { + throw new IllegalStateException('sendgrid.api not set') + } + this.from = co.getProperty('sendgrid.from', String) + if (!this.from) { + throw new IllegalStateException('sendgrid.from not set') + } + } + + @Override + void send(Email email) { + Mail mail = buildEmail(email) + SendGrid sg = new SendGrid(api) + Request request = new Request() + try { + request.with { + method = Method.POST + endpoint = 'mail/send' + body = mail.build() + } + Response response = sg.api(request) + log.info('Status Code: {}', String.valueOf(response.statusCode)) + log.debug('Body: {}', response.body) + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new IllegalStateException( + "SendGrid returned status ${response.statusCode}: ${response.body}") + } + } catch (IOException ex) { + log.error(ex.message, ex) + throw ex + } + } + + private Personalization buildPersonalization(Email email) { + Personalization personalization = new Personalization() + personalization.subject = email.subject + + SendGridEmail to = new SendGridEmail(email.recipient) + personalization.addTo(to) + + if (email.cc) { + for (String cc : email.cc) { + personalization.addCc(new SendGridEmail(cc)) + } + } + if (email.bcc) { + for (String bcc : email.bcc) { + personalization.addBcc(new SendGridEmail(bcc)) + } + } + personalization + } + + private Mail buildEmail(Email email) { + Personalization personalization = buildPersonalization(email) + Mail mail = new Mail() + SendGridEmail fromEmail = new SendGridEmail(this.from) + mail.from = fromEmail + mail.addPersonalization(personalization) + if (email.textBody) { + mail.addContent(new Content('text/plain', email.textBody)) + } + if (email.htmlBody) { + mail.addContent(new Content('text/html', email.htmlBody)) + } + mail + } +}