1. Overview

Sending emails is an important feature for modern web applications, whether it’s for user registration, password resets, or promotional campaigns.

In this tutorial, we’ll explore how to send emails using SendGrid within a Spring Boot application. We’ll walk through the necessary configurations and implement email-sending functionality for different use cases.

2. Setting up SendGrid

To follow this tutorial, we’ll first need a SendGrid account. SendGrid offers a free tier that allows us to send up to 100 emails per day, which is sufficient for our demonstration.

Once we’ve signed up, we’ll need to create an API key to authenticate our requests to the SendGrid service.

Finally, we’ll need to verify our sender identity to send emails successfully.

3. Setting up the Project

Before we can start sending emails with SendGrid, we’ll need to include an SDK dependency and configure our application correctly.

3.1. Dependencies

Let’s start by adding the SendGrid SDK dependency to our project’s pom.xml file:

<dependency>
    <groupId>com.sendgrid</groupId>
    <artifactId>sendgrid-java</artifactId>
    <version>4.10.2</version>
</dependency>

This dependency provides us with the necessary classes to interact with the SendGrid service and send emails from our application.

3.2. Defining SendGrid Configuration Properties

Now, to interact with the SendGrid service and send emails to our users, we need to configure our API key to authenticate API requests. We’ll also need to configure the sender’s name and email address, which should match the sender identity we’ve set up in our SendGrid account.

We’ll store these properties in our project’s application.yaml file and use @ConfigurationProperties to map the values to a POJO, which our service layer references when interacting with SendGrid:

@Validated
@ConfigurationProperties(prefix = "com.baeldung.sendgrid")
class SendGridConfigurationProperties {
    @NotBlank
    @Pattern(regexp = "^SG[0-9a-zA-Z._]{67}$")
    private String apiKey;

    @Email
    @NotBlank
    private String fromEmail;

    @NotBlank
    private String fromName;

    // standard setters and getters
}

We’ve also added validation annotations to ensure all the required properties are configured correctly. If any of the defined validations fail, the Spring ApplicationContext will fail to start up. This allows us to conform to the fail-fast principle.

Below is a snippet of our application.yaml file, which defines the required properties that’ll be mapped to our SendGridConfigurationProperties class automatically:

com:
  baeldung:
    sendgrid:
      api-key: ${SENDGRID_API_KEY}
      from-email: ${SENDGRID_FROM_EMAIL}
      from-name: ${SENDGRID_FROM_NAME}

We use the ${} property placeholder to load the values of our properties from environment variables. Accordingly, this setup allows us to externalize the SendGrid properties and easily access them in our application.

3.3. Configuring SendGrid Beans

Now that we’ve configured our properties, let’s reference them to define the necessary beans:

@Configuration
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class SendGridConfiguration {
    private final SendGridConfigurationProperties sendGridConfigurationProperties;

    // standard constructor

    @Bean
    public SendGrid sendGrid() {
        String apiKey = sendGridConfigurationProperties.getApiKey();
        return new SendGrid(apiKey);
    }
}

Using constructor injection, we inject an instance of our SendGridConfigurationProperties class we created earlier. Then we use the configured API key to create a SendGrid bean.

Next, we’ll create a bean to represent the sender for all our outgoing emails:

@Bean
public Email fromEmail() {
    String fromEmail = sendGridConfigurationProperties.getFromEmail();
    String fromName = sendGridConfigurationProperties.getFromName();
    return new Email(fromEmail, fromName);
}

With these beans in place, we can autowire them in our service layer to interact with the SendGrid service.

4. Sending Simple Emails

Now that we’ve defined our beans, let’s create an EmailDispatcher class and reference them to send a simple email:

private static final String EMAIL_ENDPOINT = "mail/send";

public void dispatchEmail(String emailId, String subject, String body) {
    Email toEmail = new Email(emailId);
    Content content = new Content("text/plain", body);
    Mail mail = new Mail(fromEmail, subject, toEmail, content);

    Request request = new Request();
    request.setMethod(Method.POST);
    request.setEndpoint(EMAIL_ENDPOINT);
    request.setBody(mail.build());

    sendGrid.api(request);
}

In our dispatchEmail() method, we create a new Mail object that represents the email we want to send, then set it as the request body of our Request object.

Finally, we use the SendGrid bean to send the request to the SendGrid service.

5. Sending Emails with Attachments

In addition to sending simple plain text emails, SendGrid also allows us to send emails with attachments.

First, we’ll create a helper method to convert a MultipartFile to an Attachments object from the SendGrid SDK:

private Attachments createAttachment(MultipartFile file) {
    byte[] encodedFileContent = Base64.getEncoder().encode(file.getBytes());
    Attachments attachment = new Attachments();
    attachment.setDisposition("attachment");
    attachment.setType(file.getContentType());
    attachment.setFilename(file.getOriginalFilename());
    attachment.setContent(new String(encodedFileContent, StandardCharsets.UTF_8));
    return attachment;
}

In our createAttachment() method, we’re creating a new Attachments object and setting its properties based on the MultipartFile parameter.

It’s important to note that we Base64 encode the file’s content before setting it in the Attachments object.

Next, let’s update our dispatchEmail() method to accept an optional list of MultipartFile objects:

public void dispatchEmail(String emailId, String subject, String body, List<MultipartFile> files) {
    // ... same as above

    if (files != null && !files.isEmpty()) {
        for (MultipartFile file : files) {
            Attachments attachment = createAttachment(file);
            mail.addAttachments(attachment);
        }
    }

    // ... same as above
}

We iterate over each file in our files parameter, create its corresponding Attachments object using our createAttachment() method, and add it to our Mail object. The rest of the method remains the same.

6. Sending Emails with Dynamic Templates

SendGrid also allows us to create dynamic email templates using HTML and Handlebars syntax.

For this demonstration, we’ll take an example where we want to send a personalized hydration alert email to our users.

6.1. Creating the HTML Template

First, we’ll create an HTML template for our hydration alert email:

<html>
    <head>
        <style>
            body { font-family: Arial; line-height: 2; text-align: Center; }
            h2 { color: DeepSkyBlue; }
            .alert { background: Red; color: White; padding: 1rem; font-size: 1.5rem; font-weight: bold; }
            .message { border: .3rem solid DeepSkyBlue; padding: 1rem; margin-top: 1rem; }
            .status { background: LightCyan; padding: 1rem; margin-top: 1rem; }
        </style>
    </head>
    <body>
        <div class="alert">⚠️ URGENT HYDRATION ALERT ⚠️</div>
        <div class="message">
            <h2>It's time to drink water!</h2>
            <p>Hey {{name}}, this is your friendly reminder to stay hydrated. Your body will thank you!</p>
            <div class="status">
                <p><strong>Last drink:</strong> {{lastDrinkTime}}</p>
                <p><strong>Hydration status:</strong> {{hydrationStatus}}</p>
            </div>
        </div>
    </body>
</html>

*In our template, we’re using Handlebars syntax to define placeholders of {{name}}, {{lastDrinkTime}}, and {{hydrationStatus}}*. We’ll replace these placeholders with actual values when we send the email.

We also use internal CSS to beautify our email template.

6.2. Configuring the Template ID

Once we create our template in SendGrid, it’s assigned a unique template ID.

To hold this template ID, we’ll define a nested class inside our SendGridConfigurationProperties class:

@Valid
private HydrationAlertNotification hydrationAlertNotification = new HydrationAlertNotification();

class HydrationAlertNotification {
    @NotBlank
    @Pattern(regexp = "^d-[a-f0-9]{32}$")
    private String templateId;

    // standard setter and getter
}

We again add validation annotations to ensure that we configure the template ID correctly and it matches the expected format.

Similarly, let’s add the corresponding template ID property to our application.yaml file:

com:
  baeldung:
    sendgrid:
      hydration-alert-notification:
        template-id: ${HYDRATION_ALERT_TEMPLATE_ID}

We’ll use this configured template ID in our EmailDispatcher class when sending our hydration alert email.

6.3. Sending Templated Emails

Now that we’ve configured our template ID, let’s create a custom Personalization class to hold our placeholder key names and their corresponding values:

class DynamicTemplatePersonalization extends Personalization {
    private final Map<String, Object> dynamicTemplateData = new HashMap<>();

    public void add(String key, String value) {
        dynamicTemplateData.put(key, value);
    }

    @Override
    public Map<String, Object> getDynamicTemplateData() {
        return dynamicTemplateData;
    }
}

We override the getDynamicTemplateData() method to return our dynamicTemplateData map, which we populate using the add() method.

Now, let’s create a new service method to send out our hydration alerts:

public void dispatchHydrationAlert(String emailId, String username) {
    Email toEmail = new Email(emailId);
    String templateId = sendGridConfigurationProperties.getHydrationAlertNotification().getTemplateId();

    DynamicTemplatePersonalization personalization = new DynamicTemplatePersonalization();
    personalization.add("name", username);
    personalization.add("lastDrinkTime", "Way too long ago");
    personalization.add("hydrationStatus", "Thirsty as a camel");
    personalization.addTo(toEmail);

    Mail mail = new Mail();
    mail.setFrom(fromEmail);
    mail.setTemplateId(templateId);
    mail.addPersonalization(personalization);

    // ... sending request process same as previous   
}

In our dispatchHydrationAlert() method, we create an instance of our DynamicTemplatePersonalization class and add custom values for the placeholders we defined in our HTML template.

Then, we set this personalization object along with the templateId on the Mail object before sending the request to SendGrid.

SendGrid will replace the placeholders in our HTML template with the provided dynamic data. This helps us to send personalized emails to our users while maintaining a consistent design and layout.

7. Testing the SendGrid Integration

Now that we’ve implemented the functionality to send emails using SendGrid, let’s look at how we can test this integration.

Testing external services can be challenging, as we don’t want to make actual API calls to SendGrid during our tests. This is where we’ll use MockServer, which will allow us to simulate the outgoing SendGrid calls.

7.1. Configuring the Test Environment

Before we write our test, we’ll create an application-integration-test.yaml file in our src/test/resources directory with the following content:

com:
  baeldung:
    sendgrid:
      api-key: SG0101010101010101010101010101010101010101010101010101010101010101010
      from-email: [email protected]
      from-name: Baeldung
      hydration-alert-notification:
        template-id: d-01010101010101010101010101010101

These dummy values bypass the validation we’d configured earlier in our SendGridConfigurationProperties class.

Now, let’s set up our test class:

@SpringBootTest
@ActiveProfiles("integration-test")
@MockServerTest("server.url=http://localhost:${mockServerPort}")
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class EmailDispatcherIntegrationTest {
    private MockServerClient mockServerClient;

    @Autowired
    private EmailDispatcher emailDispatcher;
    
    @Autowired
    private SendGridConfigurationProperties sendGridConfigurationProperties;
    
    private static final String SENDGRID_EMAIL_API_PATH = "/v3/mail/send";
}

We use the @ActiveProfiles annotation to load our integration-test-specific properties.

We also use the @MockServerTest annotation to start an instance of MockServer and create a server.url test property with the ${mockServerPort} placeholder. This is replaced by the chosen free port for MockServer, which we’ll reference in the next section where we configure our custom SendGrid REST client.

7.2. Configuring Custom SendGrid REST Client

In order to route our SendGrid API requests to MockServer, we need to configure a custom REST client for the SendGrid SDK.

We’ll create a @TestConfiguration class that defines a new SendGrid bean with a custom HttpClient:

@TestConfiguration
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class TestSendGridConfiguration {
    @Value("${server.url}")
    private URI serverUrl;

    @Autowired
    private SendGridConfigurationProperties sendGridConfigurationProperties;

    @Bean
    @Primary
    public SendGrid testSendGrid() {
        SSLContext sslContext = SSLContextBuilder.create()
          .loadTrustMaterial((chain, authType) -> true)
          .build();

        HttpClientBuilder clientBuilder = HttpClientBuilder.create()
          .setSSLContext(sslContext)
          .setProxy(new HttpHost(serverUrl.getHost(), serverUrl.getPort()));

        Client client = new Client(clientBuilder.build(), true);
        client.buildUri(serverUrl.toString(), null, null);

        String apiKey = sendGridConfigurationProperties.getApiKey();
        return new SendGrid(apiKey, client);
    }
}

In our TestSendGridConfiguration class, we create a custom Client that routes all requests through a proxy server specified by the server.url property. We also configure the SSL context to trust all certificates, as MockServer uses a self-signed certificate by default.

To use this test configuration in our integration test, we need to add the @ContextConfiguration annotation to our test class:

@ContextConfiguration(classes = TestSendGridConfiguration.class)

This ensures that our application uses the bean we’ve defined in our TestSendGridConfiguration class instead of the one we’ve defined in our SendGridConfiguration class when running integration tests.

7.3. Validating the SendGrid Request

Finally, let’s write a test case to verify that our dispatchEmail() method sends the expected request to SendGrid:

// Set up test data
String toEmail = RandomString.make() + "@baeldung.it";
String emailSubject = RandomString.make();
String emailBody = RandomString.make();
String fromName = sendGridConfigurationProperties.getFromName();
String fromEmail = sendGridConfigurationProperties.getFromEmail();
String apiKey = sendGridConfigurationProperties.getApiKey();

// Create JSON body
String jsonBody = String.format("""
    {
        "from": {
            "name": "%s",
            "email": "%s"
        },
        "subject": "%s",
        "personalizations": [{
            "to": [{
                "email": "%s"
            }]
        }],
        "content": [{
            "value": "%s"
        }]
    }
    """, fromName, fromEmail, emailSubject, toEmail, emailBody);

// Configure mock server expectations
mockServerClient
  .when(request()
    .withMethod("POST")
    .withPath(SENDGRID_EMAIL_API_PATH)
    .withHeader("Authorization", "Bearer " + apiKey)
    .withBody(new JsonBody(jsonBody, MatchType.ONLY_MATCHING_FIELDS)
  ))
  .respond(response().withStatusCode(202));

// Invoke method under test
emailDispatcher.dispatchEmail(toEmail, emailSubject, emailBody);

// Verify the expected request was made
mockServerClient
  .verify(request()
    .withMethod("POST")
    .withPath(SENDGRID_EMAIL_API_PATH)
    .withHeader("Authorization", "Bearer " + apiKey)
    .withBody(new JsonBody(jsonBody, MatchType.ONLY_MATCHING_FIELDS)
  ), VerificationTimes.once());

In our test method, we first set up the test data and create the expected JSON body for the SendGrid request. We then configure MockServer to expect a POST request to the SendGrid API path with the Authorization header and JSON body. We also instruct MockServer to respond with a 202 status code when this request is made.

Next, we invoke our dispatchEmail() method with the test data and verify that the expected request was made to MockServer exactly once.

By using MockServer to simulate the SendGrid API, we ensure that our integration works as expected without actually sending any emails or incurring any costs.

8. Conclusion

In this article, we explored how to send emails using SendGrid from a Spring Boot application.

We walked through the necessary configurations and implemented the functionality to send simple emails, emails with attachments, and HTML emails with dynamic templates.

Finally, to validate that our application sends the correct request to SendGrid, we wrote an integration test using MockServer.

As always, all the code examples used in this article are available over on GitHub.