编程

在 Spring Boot 中使用 SendGrid 发送邮件

34 2025-05-18 22:35:00

1. 概述

发送电子邮件是现代 web 应用的重要功能之一,无论是用户注册、密码重置,还是活动促销都可能用到。

本文中,我们将探索如何在 Spring Boot 应用中使用 SendGrid 发送电子邮件。我们将将介绍必要的配置,并通过不同的用例实现电子邮件发送功能。

2. 安装配置 SendGrid

首先,我们需要有一个 SendGrid 账号。SendGrid 提供了一个免费套餐,允许我们每天发送 100 封邮件,这个数量对于本文的演示足够了。

注册完后,我们需要创建一个 API 密钥来验证我们发送给 SendGrid 服务的请求。

最后,我们需要验证发送人身份,才能成功发送电子邮件。

3. 安装配置项目

在使用 SendGrid 发送电子邮件之前,我们需要引入 SDK 依赖并正确配置应用。

3.1. 依赖

让我们将 SendGrid SDK 依赖添加到项目的 pom.xml 文件中:

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

这个依赖为我们提供了与 SendGrid 服务交互以及从我们的应用发送电子邮件所需的类。

3.2. 定义 SendGrid 配置属性

接下来,为了和 SendGrid 服务互动并发送邮件给用户,我们需要配置验证 API 请求的 API 密钥。我们也需要配置发送人名字和电子邮件地址,这应该与我们在 SendGrid 帐户中设置的发件人身份相匹配。

我们将在项目的 application.yaml 文件中存储这些属性并且使用 @ConfigurationProperties 类将值匹配到 POJO,我们的服务层在与 SendGrid 交互时引用 POJO:

@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
}

我们同时还添加了验证注解,以确保所有必需的属性都配置正确。如果任何定义的验证失败,Spring ApplicationContext 将无法启动。这使我们能够遵守快速失败原则

下面是 application.yaml 文件的一个片段,它定义了将自动映射到我们的 SendGridConfigurationProperties 类的所需属性:

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

我们使用 ${} 属性占位符来从环境变量中加载属性值。因此,这种设置允许我们将 SendGrid 属性外部化,并在我们的应用中轻松访问它们

3.3. 配置 SendGrid Bean

既然我们配置了属性,让我们引用这些属性来定义必要的 bean:

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

    // standard constructor

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

通过构造函数注入,我们注入了一个早前创建的 SendGridConfigurationProperties 类的实例。然后我们使用配置的 API 密钥来创建 SendGrid Bean。

接下来,我们将创建一个 bean 来表示我们所有传出电子邮件的发件人:

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

有了这些 bean,我们可以在服务层自动连接它们,以与 SendGrid 服务交互。

4. 发送简单邮件

既然这些 bean 已经定义了,让我们来创建一个 EmailDispatcher 类并引用这些 bean 来发送一个简单的电子邮件:

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);
}

dispatchEmail() 方法中,我们创建了一个新的 Mail 对象,来表示我们要发送的邮件,然后将其设置为我们的 Request 对象的请求体。

最后,我们使用 SendGrid bean 来发送 SendGrid 服务的请求。

5. 发送带有附件的邮件

除了发送简单的普通文本邮件,SendGrid 也允许发送带附件的邮件。

首先,我们创建一个辅助方法,将 MultipartFile 转换成 SendGrid SDK 的 Attachments 对象:

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;
}

createAttachment() 方法中,我们创建了一个新的 Attachments 对象并基于 MultipartFile 参数设置其属性。

值得注意的是,在将文件内容设置在 Attachments 对象中之前,我们对其进行 Base64 编码。

接下来,我们更新了 dispatchEmail() 方法来接受 MultipartFile 对象的可选列表:

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
}

我们迭代  files 参数中的每个文件,使用 createAttachment() 方法创建其相应的 Attachments 对象,并将其添加到 Mail 对象中。其余方法保持不变。

6. 发送使用动态模板的邮件

SendGrid 也允许我们使用 HTML 和 Handlebars 语法来动态创建电子邮件模板。

对于本次演示,我们将举一个例子,我们想向用户发送一封个性化的 hydration 警报电子邮件。

6.1. 创建 HTML 模板

首先,我们将为我们的 hydration 警报邮件创建一个 HTML 模板:

<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>

在我们的模板中,我们使用了 Handlebars 语法来定义 {{name}}{{lastDrinkTime}}{{hydrationStatus}} 的占位符。当发送邮件时,我们使用实际值来替换这些占位符。

我们使用内部 CSS 来美化我们的邮件模板。

6.2. 配置模板 ID

当我们在 SendGrid 中创建模板后,就分配给它一个唯一的模板 ID。

为了持有此模板 ID,我们将在 SendGridConfigurationProperties 类中定义一个嵌套类:

@Valid
private HydrationAlertNotification hydrationAlertNotification = new HydrationAlertNotification();

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

    // standard setter and getter
}

我们再次添加验证注解,以确保正确配置模板 ID,并且它与预期的格式匹配。

同样,让我们将相应的模板 ID 属性添加到 application.yaml 文件中:

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

当发送 hydration 警报邮件时,我们在 EmailDispatcher 类中使用了这个配置的模板 ID。

6.3. 发送模板邮件

既然我们配置了自己的模板 ID,让我们创建一个 Personalization 类来持有这些占位符键名及其对应的值:

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;
    }
}

我们重写了 getDynamicTemplateData() 方法来返回 dynamicTemplateData 映射,我们使用 add() 方法填充。

现在,让我们创建新服务方法来发送 hydration 警报:

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   
}

dispatchHydrationAlert() 方法,我们创建了一个 DynamicTemplatePersonalization 类并为 HTML 模板中定义的展位符添加自定义值。

然后,我们设置了 personalization 对象 在将请求发送到 SendGrid 之前,我们将此 personalization 对象与 Mail 对象上的 templateId 一起设置。

SendGrid 将用提供的动态数据替换 HTML 模板中的占位符。这有助于我们向用户发送个性化电子邮件,同时保持一致的设计和布局。

7. 测试 SendGrid 集成

现在我们已经实现了使用 SendGrid 发送电子邮件的功能,让我们看看如何测试这种集成。

测试外部服务可能具有挑战性,因为我们不想在测试期间对 SendGrid 进行实际的 API 调用。这里我们将使用 MockServer,它将允许我们模拟传出的 SendGrid 调用。

7.1. 测试环境

在编写测试之前,我们使用如下内容在 src/test/resources 目录中创建了 application-integration-test.yaml 文件:

com:
  baeldung:
    sendgrid:
      api-key: SG0101010101010101010101010101010101010101010101010101010101010101010
      from-email: no-reply@baeldung.com
      from-name: Baeldung
      hydration-alert-notification:
        template-id: d-01010101010101010101010101010101

这些虚拟值绕过了我们之前在 SendGridConfigurationProperties 类中配置的验证。

现在,让我们设置我们的测试类:

@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";
}

我们使用 @ActiveProfile 注解来加载集成测试特定的属性。

我们也使用 @MockServerTest 注解来开启 MockServer 实例并且使用 ${mockServerPort} 占位符来创建 server.url 测试属性。这被为 MockServer 选择的自由端口所取代,我们将在下一节中配置我们的自定义 SendGrid REST 客户端时引用该端口。

7.2. 配置自定义 SendGrid REST 客户端

为了将 SendGrid API 请求路由到 MockServer,我们需要为 SendGrid SDK 配置一个自定义 REST 客户端。

我们将创建一个 @TestConfiguration 类,它定义了一个带有自定义 HttpClient 的新 SendGrid bean:

@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);
    }
}

TestSendGridConfiguration 类中,我们创建了一个自定义客户端,该客户端通过 server.url 属性指定的代理服务器路由所有请求。我们还将 SSL 上下文配置为信任所有证书,因为 MockServer 默认使用自签名证书。

要在我们的集成测试中使用此测试配置,我们需要将 @ContextConfiguration 注解添加到我们的测试类中:

@ContextConfiguration(classes = TestSendGridConfiguration.class)

这确保了我们的应用在运行集成测试时使用我们在 TestSendGridConfiguration 类中定义的 bean,而不是我们在 SendGridConfiguration 类时定义的 bean。

7.3. 验证 SendGrid 请求

最后,让我们来编写一个测试用例,来验证 dispatchEmail() 方法发送了预期请求给 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());

在我们的测试方法中,我们首先设置测试数据,并为 SendGrid 请求创建预期的 JSON 正文(body)。然后,我们将 MockServer 配置为期望对带有 Authorization header 和 JSON body 的 SendGrid API 路径发出 POST 请求。我们还指示 MockServer 在发出此请求时用 202 状态码进行响应。

接下来,我们使用测试数据调用 dispatchEmail() 方法,并验证是否向 MockServer 发送了一次预期的请求。

通过使用 MockServer 模拟 SendGrid API,我们确保我们的集成按预期工作,而不会实际发送任何电子邮件或产生任何成本。

8. 小结

本文中,我们探讨了如何在 Spring Boot 应用使用 SendGrid 发送电子邮件。

我们完成了必要的配置,并实现了发送简单电子邮件、带附件的电子邮件和带动态模板的 HTML 电子邮件的功能。

最后,为了验证我们的应用是否向 SendGrid发送了正确的请求,我们使用 MockServer 编写了一个集成测试。

Github 源码:https://github.com/eugenp/tutorials/tree/master/saas-modules/sendgrid