From 882e33b7310e785c9c4115314b9dd31be77e823c Mon Sep 17 00:00:00 2001 From: Sanjana Date: Wed, 1 Jul 2026 23:18:50 +0530 Subject: [PATCH 1/5] Add Grails 8 email guide (v8 prose, snippets, guides.yml) --- conf/guides.yml | 44 ++++++++- .../grails-email/v8/guide/awsSesService.adoc | 9 ++ guides/grails-email/v8/guide/controller.adoc | 24 +++++ .../grails-email/v8/guide/emailContract.adoc | 25 +++++ .../grails-email/v8/guide/gettingStarted.adoc | 17 ++++ .../grails-email/v8/guide/helpWithGrails.adoc | 1 + .../grails-email/v8/guide/introduction.adoc | 5 + .../v8/guide/mailConfiguration.adoc | 9 ++ .../grails-email/v8/guide/requirements.adoc | 4 + .../v8/guide/sendGridService.adoc | 9 ++ guides/grails-email/v8/guide/smtpMail.adoc | 17 ++++ guides/grails-email/v8/guide/summary.adoc | 8 ++ guides/grails-email/v8/guide/testing.adoc | 17 ++++ guides/grails-email/v8/snippets/build.gradle | 3 + .../grails-app/conf/application-email.yml | 27 ++++++ .../grails-app/conf/spring/resources.groovy | 23 +++++ .../controllers/example/EmailCmd.groovy | 27 ++++++ .../controllers/example/MailController.groovy | 23 +++++ .../services/example/SmtpEmailService.groovy | 36 +++++++ .../groovy/example/MailControllerSpec.groovy | 52 ++++++++++ .../groovy/example/AwsSesMailService.groovy | 84 +++++++++++++++++ .../src/main/groovy/example/Email.groovy | 14 +++ .../main/groovy/example/EmailService.groovy | 8 ++ .../example/SendGridEmailService.groovy | 94 +++++++++++++++++++ 24 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 guides/grails-email/v8/guide/awsSesService.adoc create mode 100644 guides/grails-email/v8/guide/controller.adoc create mode 100644 guides/grails-email/v8/guide/emailContract.adoc create mode 100644 guides/grails-email/v8/guide/gettingStarted.adoc create mode 100644 guides/grails-email/v8/guide/helpWithGrails.adoc create mode 100644 guides/grails-email/v8/guide/introduction.adoc create mode 100644 guides/grails-email/v8/guide/mailConfiguration.adoc create mode 100644 guides/grails-email/v8/guide/requirements.adoc create mode 100644 guides/grails-email/v8/guide/sendGridService.adoc create mode 100644 guides/grails-email/v8/guide/smtpMail.adoc create mode 100644 guides/grails-email/v8/guide/summary.adoc create mode 100644 guides/grails-email/v8/guide/testing.adoc create mode 100644 guides/grails-email/v8/snippets/build.gradle create mode 100644 guides/grails-email/v8/snippets/grails-app/conf/application-email.yml create mode 100644 guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy create mode 100644 guides/grails-email/v8/snippets/grails-app/controllers/example/EmailCmd.groovy create mode 100644 guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy create mode 100644 guides/grails-email/v8/snippets/grails-app/services/example/SmtpEmailService.groovy create mode 100644 guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy create mode 100644 guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy create mode 100644 guides/grails-email/v8/snippets/src/main/groovy/example/Email.groovy create mode 100644 guides/grails-email/v8/snippets/src/main/groovy/example/EmailService.groovy create mode 100644 guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy diff --git a/conf/guides.yml b/conf/guides.yml index 902a3eeffff..f65928523d4 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,45 @@ 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: 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..ff2eaeb4981 --- /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/AwsSesMailService.groovy +---- +include::../snippets/src/main/groovy/example/AwsSesMailService.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..4284b8a5106 --- /dev/null +++ b/guides/grails-email/v8/guide/controller.adoc @@ -0,0 +1,24 @@ +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[] +---- + +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..b00ae26944b --- /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..3ed683e3b2e --- /dev/null +++ b/guides/grails-email/v8/guide/gettingStarted.adoc @@ -0,0 +1,17 @@ +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. + +[[cloneAndRun]] +== Verify CI + +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..2e645780007 --- /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..dec2bf470c3 --- /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-email.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..3a3d30e5e88 --- /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 — 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..26863c2315d --- /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-email.yml b/guides/grails-email/v8/snippets/grails-app/conf/application-email.yml new file mode 100644 index 00000000000..10029f647f0 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/conf/application-email.yml @@ -0,0 +1,27 @@ + #tag::mailConfig[] + 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..781a706a847 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy @@ -0,0 +1,23 @@ +import example.AwsSesMailService +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(AwsSesMailService) + break + default: + emailService(SmtpEmailService) + } + //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..da381480777 --- /dev/null +++ b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy @@ -0,0 +1,23 @@ +package example + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +@Slf4j +@CompileStatic +class MailController { + + EmailService emailService + + static allowedMethods = [send: 'POST'] + + def send(EmailCmd cmd) { + if (cmd.hasErrors()) { + render status: 422 + return + } + log.info '{}', cmd.toString() + 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..a445f41d2cd --- /dev/null +++ b/guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy @@ -0,0 +1,52 @@ +package example + +import grails.testing.mixin.integration.Integration +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.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> + } + + @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/AwsSesMailService.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy new file mode 100644 index 00000000000..d3aca87f235 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy @@ -0,0 +1,84 @@ +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 AwsSesMailService 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) { + log.warn('aws.ses.source not set') + } + } + + private Body bodyOfEmail(Email email) { + if (email.htmlBody) { + Content htmlBody = Content.builder().data(email.htmlBody).build() + return Body.builder().html(htmlBody).build() + } + if (email.textBody) { + Content textBody = Content.builder().data(email.textBody).build() + return Body.builder().text(textBody).build() + } + Body.builder().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.') + log.warn('Error message: {}', ex.message) + } + } +} 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..af423901408 --- /dev/null +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy @@ -0,0 +1,94 @@ +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) + } catch (IOException ex) { + log.error(ex.message) + } + } + + private Content contentOfEmail(Email email) { + if (email.textBody) { + return new Content('text/plain', email.textBody) + } + if (email.htmlBody) { + return new Content('text/html', email.htmlBody) + } + return null + } + + 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) + Content content = contentOfEmail(email) + mail.addContent(content) + mail + } +} From 52ffc535a3c4cce0211632dff055940530cd63f0 Mon Sep 17 00:00:00 2001 From: Sanjana Date: Thu, 23 Jul 2026 14:22:08 +0530 Subject: [PATCH 2/5] Address review feedback on Grails 8 email guide. --- conf/guides.yml | 5 +++-- guides/grails-email/v8/guide/emailContract.adoc | 2 +- guides/grails-email/v8/guide/gettingStarted.adoc | 5 +---- guides/grails-email/v8/guide/introduction.adoc | 2 +- guides/grails-email/v8/guide/smtpMail.adoc | 2 +- guides/grails-email/v8/guide/summary.adoc | 2 +- .../conf/{application-email.yml => application.yml} | 0 7 files changed, 8 insertions(+), 10 deletions(-) rename guides/grails-email/v8/snippets/grails-app/conf/{application-email.yml => application.yml} (100%) diff --git a/conf/guides.yml b/conf/guides.yml index f65928523d4..8bf0e74c58e 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -2191,7 +2191,7 @@ guides: - name: 'grails-email' 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.' + 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' @@ -2276,7 +2276,8 @@ guides: toc: introduction: title: Introduction - requirements: What you will need + requirements: + title: What you will need gettingStarted: title: Getting Started emailContract: diff --git a/guides/grails-email/v8/guide/emailContract.adoc b/guides/grails-email/v8/guide/emailContract.adoc index b00ae26944b..f210773438e 100644 --- a/guides/grails-email/v8/guide/emailContract.adoc +++ b/guides/grails-email/v8/guide/emailContract.adoc @@ -22,4 +22,4 @@ Add the mail dependencies in `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. +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 index 3ed683e3b2e..f380b1f3a66 100644 --- a/guides/grails-email/v8/guide/gettingStarted.adoc +++ b/guides/grails-email/v8/guide/gettingStarted.adoc @@ -9,9 +9,6 @@ cd grails-email/initial 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. - -[[cloneAndRun]] -== Verify CI +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/introduction.adoc b/guides/grails-email/v8/guide/introduction.adoc index 2e645780007..144e59678b4 100644 --- a/guides/grails-email/v8/guide/introduction.adoc +++ b/guides/grails-email/v8/guide/introduction.adoc @@ -1,4 +1,4 @@ -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. +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. diff --git a/guides/grails-email/v8/guide/smtpMail.adoc b/guides/grails-email/v8/guide/smtpMail.adoc index dec2bf470c3..0b5dcfeda7f 100644 --- a/guides/grails-email/v8/guide/smtpMail.adoc +++ b/guides/grails-email/v8/guide/smtpMail.adoc @@ -11,7 +11,7 @@ Configure SMTP settings in `application.yml`: [source,yaml] .grails-app/conf/application.yml ---- -include::../snippets/grails-app/conf/application-email.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 index 3a3d30e5e88..f6f2cd1c6e4 100644 --- a/guides/grails-email/v8/guide/summary.adoc +++ b/guides/grails-email/v8/guide/summary.adoc @@ -3,6 +3,6 @@ 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 — no live email in CI +* 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/snippets/grails-app/conf/application-email.yml b/guides/grails-email/v8/snippets/grails-app/conf/application.yml similarity index 100% rename from guides/grails-email/v8/snippets/grails-app/conf/application-email.yml rename to guides/grails-email/v8/snippets/grails-app/conf/application.yml From efc870f4c52f5cca052d9d4ed4eb7a36e634c0cf Mon Sep 17 00:00:00 2001 From: Sanjana Date: Thu, 23 Jul 2026 14:27:04 +0530 Subject: [PATCH 3/5] Refresh PR after review fixes From 611b7d50523e6d1c10dfbb5120a2bedbfcfe2dc7 Mon Sep 17 00:00:00 2001 From: Sanjana Date: Thu, 23 Jul 2026 22:23:38 +0530 Subject: [PATCH 4/5] Sync email guide snippets with companion review fixes. --- .../grails-email/v8/guide/awsSesService.adoc | 4 +- guides/grails-email/v8/guide/controller.adoc | 5 ++ .../grails-email/v8/guide/gettingStarted.adoc | 2 +- .../snippets/grails-app/conf/application.yml | 5 +- .../grails-app/conf/spring/resources.groovy | 9 ++- .../controllers/example/MailController.groovy | 4 +- .../groovy/example/MailControllerSpec.groovy | 56 +++++++++++++++++++ ...rvice.groovy => AwsSesEmailService.groovy} | 6 +- .../example/SendGridEmailService.groovy | 25 ++++----- 9 files changed, 91 insertions(+), 25 deletions(-) rename guides/grails-email/v8/snippets/src/main/groovy/example/{AwsSesMailService.groovy => AwsSesEmailService.groovy} (93%) diff --git a/guides/grails-email/v8/guide/awsSesService.adoc b/guides/grails-email/v8/guide/awsSesService.adoc index ff2eaeb4981..557923595c8 100644 --- a/guides/grails-email/v8/guide/awsSesService.adoc +++ b/guides/grails-email/v8/guide/awsSesService.adoc @@ -1,9 +1,9 @@ AWS SES is the third provider option: [source,groovy] -.src/main/groovy/example/AwsSesMailService.groovy +.src/main/groovy/example/AwsSesEmailService.groovy ---- -include::../snippets/src/main/groovy/example/AwsSesMailService.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 index 4284b8a5106..8bde24d20e1 100644 --- a/guides/grails-email/v8/guide/controller.adoc +++ b/guides/grails-email/v8/guide/controller.adoc @@ -14,6 +14,11 @@ The controller delegates to `EmailService`: 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] diff --git a/guides/grails-email/v8/guide/gettingStarted.adoc b/guides/grails-email/v8/guide/gettingStarted.adoc index f380b1f3a66..4598fc8d915 100644 --- a/guides/grails-email/v8/guide/gettingStarted.adoc +++ b/guides/grails-email/v8/guide/gettingStarted.adoc @@ -9,6 +9,6 @@ cd grails-email/initial 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. +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/snippets/grails-app/conf/application.yml b/guides/grails-email/v8/snippets/grails-app/conf/application.yml index 10029f647f0..dfcc8599869 100644 --- a/guides/grails-email/v8/snippets/grails-app/conf/application.yml +++ b/guides/grails-email/v8/snippets/grails-app/conf/application.yml @@ -1,4 +1,5 @@ - #tag::mailConfig[] +#tag::mailConfig[] +grails: mail: host: ${SMTP_HOST:localhost} port: ${SMTP_PORT:1025} @@ -9,7 +10,7 @@ props: mail.smtp.auth: ${SMTP_AUTH:false} mail.smtp.starttls.enable: ${SMTP_STARTTLS:false} - #end::mailConfig[] +#end::mailConfig[] #tag::emailProvider[] email: provider: ${EMAIL_PROVIDER:smtp} 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 index 781a706a847..975fdf8e599 100644 --- a/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy +++ b/guides/grails-email/v8/snippets/grails-app/conf/spring/resources.groovy @@ -1,4 +1,4 @@ -import example.AwsSesMailService +import example.AwsSesEmailService import example.SendGridEmailService import example.SmtpEmailService import grails.util.Environment @@ -14,10 +14,13 @@ beans = { emailService(SendGridEmailService) break case 'ses': - emailService(AwsSesMailService) + emailService(AwsSesEmailService) break - default: + 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/MailController.groovy b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy index da381480777..f21a7587243 100644 --- a/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy +++ b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy @@ -3,6 +3,8 @@ package example import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY + @Slf4j @CompileStatic class MailController { @@ -13,7 +15,7 @@ class MailController { def send(EmailCmd cmd) { if (cmd.hasErrors()) { - render status: 422 + respond cmd.errors, view: '/application/errors', status: UNPROCESSABLE_ENTITY return } log.info '{}', cmd.toString() 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 index a445f41d2cd..ea9bec05793 100644 --- 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 @@ -1,12 +1,14 @@ 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 @@ -39,6 +41,60 @@ class MailControllerSpec extends Specification { }) // <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_ENTITY + def body = new JsonSlurper().parseText(ex.responseBodyAsString) + body.message + body.path + body._links + 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_ENTITY + def body = new JsonSlurper().parseText(ex.responseBodyAsString) + body.message + body.path + body._links + 0 * emailService.send(_) + } + @TestConfiguration static class EmailServiceConfiguration { private DetachedMockFactory factory = new DetachedMockFactory() diff --git a/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy similarity index 93% rename from guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy rename to guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy index d3aca87f235..7fd7c56b926 100644 --- a/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesMailService.groovy +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy @@ -15,7 +15,7 @@ import software.amazon.awssdk.services.ses.model.SendEmailResponse @Slf4j @CompileStatic -class AwsSesMailService implements EmailService, GrailsConfigurationAware { // <1> +class AwsSesEmailService implements EmailService, GrailsConfigurationAware { // <1> String sourceEmail SesClient sesClient @@ -77,8 +77,8 @@ class AwsSesMailService implements EmailService, GrailsConfigurationAware { // log.info('Email sent! {}', response.messageId()) } catch (Exception ex) { - log.warn('The email was not sent.') - log.warn('Error message: {}', ex.message) + log.warn('The email was not sent.', ex) + throw ex } } } 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 index af423901408..58aa80aee26 100644 --- a/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/SendGridEmailService.groovy @@ -46,21 +46,16 @@ class SendGridEmailService implements EmailService, GrailsConfigurationAware { 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) + log.error(ex.message, ex) + throw ex } } - private Content contentOfEmail(Email email) { - if (email.textBody) { - return new Content('text/plain', email.textBody) - } - if (email.htmlBody) { - return new Content('text/html', email.htmlBody) - } - return null - } - private Personalization buildPersonalization(Email email) { Personalization personalization = new Personalization() personalization.subject = email.subject @@ -87,8 +82,12 @@ class SendGridEmailService implements EmailService, GrailsConfigurationAware { SendGridEmail fromEmail = new SendGridEmail(this.from) mail.from = fromEmail mail.addPersonalization(personalization) - Content content = contentOfEmail(email) - mail.addContent(content) + if (email.textBody) { + mail.addContent(new Content('text/plain', email.textBody)) + } + if (email.htmlBody) { + mail.addContent(new Content('text/html', email.htmlBody)) + } mail } } From 7c086fd6b87f8fd6a620da52e9c62fc5727700c2 Mon Sep 17 00:00:00 2001 From: Sanjana Date: Sat, 25 Jul 2026 00:38:55 +0530 Subject: [PATCH 5/5] Re-vendor email snippets from companion b07505d. --- guides/grails-email/v8/guide/testing.adoc | 2 +- .../controllers/example/MailController.groovy | 2 +- .../groovy/example/MailControllerSpec.groovy | 12 ++++++------ .../main/groovy/example/AwsSesEmailService.groovy | 13 ++++++------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/guides/grails-email/v8/guide/testing.adoc b/guides/grails-email/v8/guide/testing.adoc index 26863c2315d..19d0be8a5a3 100644 --- a/guides/grails-email/v8/guide/testing.adoc +++ b/guides/grails-email/v8/guide/testing.adoc @@ -12,6 +12,6 @@ Run tests: [source,bash] ---- -cd complete +cd ../complete ./gradlew test integrationTest ---- 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 index f21a7587243..6f90ba13f8b 100644 --- a/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy +++ b/guides/grails-email/v8/snippets/grails-app/controllers/example/MailController.groovy @@ -18,7 +18,7 @@ class MailController { respond cmd.errors, view: '/application/errors', status: UNPROCESSABLE_ENTITY return } - log.info '{}', cmd.toString() + 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/src/integration-test/groovy/example/MailControllerSpec.groovy b/guides/grails-email/v8/snippets/src/integration-test/groovy/example/MailControllerSpec.groovy index ea9bec05793..9c4efb6ff62 100644 --- 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 @@ -60,11 +60,11 @@ class MailControllerSpec extends Specification { then: ex != null - ex.statusCode == HttpStatus.UNPROCESSABLE_ENTITY + ex.statusCode == HttpStatus.UNPROCESSABLE_CONTENT def body = new JsonSlurper().parseText(ex.responseBodyAsString) body.message - body.path - body._links + body.path == '/mail/send' + body._links.self.href == "http://localhost:$serverPort/mail/send" 0 * emailService.send(_) } @@ -87,11 +87,11 @@ class MailControllerSpec extends Specification { then: ex != null - ex.statusCode == HttpStatus.UNPROCESSABLE_ENTITY + ex.statusCode == HttpStatus.UNPROCESSABLE_CONTENT def body = new JsonSlurper().parseText(ex.responseBodyAsString) body.message - body.path - body._links + body.path == '/mail/send' + body._links.self.href == "http://localhost:$serverPort/mail/send" 0 * emailService.send(_) } 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 index 7fd7c56b926..752144d6d8a 100644 --- a/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy +++ b/guides/grails-email/v8/snippets/src/main/groovy/example/AwsSesEmailService.groovy @@ -28,22 +28,21 @@ class AwsSesEmailService implements EmailService, GrailsConfigurationAware { // } this.sesClient = SesClient.builder().region(Region.of(awsRegion)).build() - this.sourceEmail = co.getProperty('aws.ses.source', String, '') + this.sourceEmail = co.getProperty('aws.ses.source', String) if (!this.sourceEmail) { - log.warn('aws.ses.source not set') + throw new IllegalStateException('aws.ses.source not set') } } private Body bodyOfEmail(Email email) { + Body.Builder bodyBuilder = Body.builder() if (email.htmlBody) { - Content htmlBody = Content.builder().data(email.htmlBody).build() - return Body.builder().html(htmlBody).build() + bodyBuilder.html(Content.builder().data(email.htmlBody).build()) } if (email.textBody) { - Content textBody = Content.builder().data(email.textBody).build() - return Body.builder().text(textBody).build() + bodyBuilder.text(Content.builder().data(email.textBody).build()) } - Body.builder().build() + bodyBuilder.build() } private Destination destination(Email email) {