在 Spring Boot 中使用 SendGrid 发送电子邮件

1、概览

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

本文将带你了解如何在 Spring Boot 应用中使用 SendGrid 发送电子邮件。

2、SendGrid 设置

在开始之前,我们首先需要一个 SendGrid 账户。SendGrid 提供了免费套餐,允许我们每天发送多达 100 封电子邮件,这对于演示来说已经足够了。

注册完成后,需要创建一个 API Key 来对我们发送到 SendGrid 服务的请求进行 身份认证

3、项目设置

在开始使用 SendGrid 发送电子邮件之前,需要添加 SDK 依赖并配置应用。

3.1、依赖

首先,在项目的 pom.xml 文件中添加 SendGrid SDK 依赖:

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

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

3.2、定义 SendGrid 配置属性

现在,为了与 SendGrid 服务交互并向用户发送电子邮件,我们需要配置 API Key 以验证 API 请求。我们还需要配置发件人姓名和电子邮件地址,它们应与我们在 SendGrid 账户中设置的发件人身份相匹配。

我们在项目的 application.yaml 文件中配置这些属性,并使用 @ConfigurationProperties 将这些值映射到 POJO,Service 层在与 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;

    // 标准的 Getter / Setter
}

如上。还添加了 @Validated 验证注解,以确保正确配置所有必要属性。如果定义的任何验证失败,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;

    // 构造函数注入
    public SendGridConfiguration (SendGridConfigurationProperties sendGridConfigurationProperties){
        this.sendGridConfigurationProperties = sendGridConfigurationProperties;
    }

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

通过构造函数注入,我们注入了之前创建的 SendGridConfigurationProperties 类的实例。然后,我们使用配置的 API Key 创建 SendGrid Bean。

接下来,创建一个 Bean 来代表所有我们发送的邮件的发件人:

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

有了这些 Bean,我们就可以在 Service 层中自动装配它们,以便与 SendGrid 服务交互。

4、发送简单的电子邮件

Bean 定义好后,让我们创建一个 EmailDispatcher 类并引用它们来发送一封简单的电子邮件:

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 对象的请求体(Request Body)。

最后,使用 SendGrid Bean 将请求发送到 SendGrid 服务。

5、发送带附件的电子邮件

除了发送简单的纯文本电子邮件,SendGrid 还允许我们发送带附件的电子邮件。

首先,创建一个 helper 方法,用于将 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) {
    // ... 同上

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

    // ...  同上
}

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

6、用动态模板发送电子邮件

SendGrid 还允许我们使用 HTMLHandlebars 语法 创建动态电子邮件模板。

以向用户发送个性化 “喝水提醒” 电子邮件为例。

6.1、创建 HTML 模板

首先,要为 “喝水提醒” 电子邮件创建 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;

    // Getter / Setter 方法省略
}

再次添加 @Valid 校验注解,以确保正确配置模板 ID 并使其符合预期格式。

同样,在 application.yaml 文件中添加相应的模板 ID 属性:

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

发送 “喝水提醒” 电子邮件时,我们将在 EmailDispatcher 类中使用此配置的模板 ID。

6.3、发送模板电子邮件

配置了模板 ID 后,让我们创建一个自定义 Personalization 类来保存我们的占位符 KEY 名及其相应的值:

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 Map,并使用 add() 方法对其进行填充。

现在,创建一个新的 Service 方法来发送 “喝水提醒”:

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

    // ... 发送请求流程与之前相同   
}

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

然后,在向 SendGrid 发送请求之前,我们会在 Mail 对象上设置该 personalization 对象和 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";
}

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

我们还使用 @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 Context 以信任所有证书,因为 MockServer 默认使用自签名证书。

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

@ContextConfiguration(classes = TestSendGridConfiguration.class)

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

7.3、验证 SendGrid 请求

最后,编写一个测试用例来验证我们的 dispatchEmail() 方法是否向 SendGrid 发送了预期的请求:

// 设置测试数据
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();

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

// 配置模拟服务器预期值
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));

// 调用被测方法
emailDispatcher.dispatchEmail(toEmail, emailSubject, emailBody);

// 验证请求是否符合预期
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 请求体。然后,对 MockServer 进行配置,使其能够接收到发送到 SendGrid API 路径的 POST 请求,并包含 Authorization Header 和 JSON 请求体。我们还指示 MockServer 在发出请求时响应 202 状态代码。

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

通过使用 MockServer 来模拟 SendGrid API,可以确保我们的集成能够按照预期运行,而无需实际发送任何电子邮件或产生任何费用。

8、总结

本文介绍了如何在 Spring Boot 中使用 SendGrid 发送电子邮件,首先介绍了如何整合、配置 SendGrid,然后介绍了发送简单电子邮件、带附件电子邮件和动态 HTML 模板电子邮件等功能,最后,使用 MockServer 编写集成测试来验证应用是否向 SendGrid 发送了正确的请求。


Ref:https://www.baeldung.com/java-email-sendgrid