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密钥进行认证。同时需要配置发件人名称和邮箱地址,这些必须与SendGrid账户中设置的发件人身份匹配。

我们将这些属性存储在项目的application.yaml文件中,并使用*@ConfigurationProperties*将值映射到POJO,服务层在调用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;

    // 标准getter/setter方法
}

我们还添加了验证注解确保所有必需属性正确配置。如果任何验证失败,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;

    // 标准构造函数

    @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类并引用它们发送简单邮件:

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将request发送到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) {
    // ... 同上

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

    // ... 同上
}

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

6. 发送动态模板邮件

SendGrid还支持使用HTML和Handlebars语法创建动态邮件模板。

本示例中,我们将实现一个向用户发送个性化补水提醒邮件的功能

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">⚠️ 紧急补水提醒 ⚠️</div>
        <div class="message">
            <h2>该喝水了!</h2>
            <p>嘿 {{name}},这是你的友好补水提醒。你的身体会感谢你的!</p>
            <div class="status">
                <p><strong>上次喝水时间:</strong> {{lastDrinkTime}}</p>
                <p><strong>补水状态:</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方法
}

我们再次添加验证注解,确保正确配置模板ID且格式符合预期。

类似地,在application.yaml文件中添加对应的模板ID属性:

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

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()*方法填充**。

现在创建新的服务方法发送补水提醒:

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", "很久以前");
    personalization.add("hydrationStatus", "渴得像骆驼");
    personalization.addTo(toEmail);

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

    // ... 发送请求过程同前
}

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

然后,在将请求发送到SendGrid之前,将此personalization对象和templateId设置到Mail对象上。

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: test@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类中,我们创建自定义Client,将所有请求通过server.url属性指定的代理服务器路由。我们还配置SSL上下文信任所有证书,因为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);

// 配置MockServer预期行为
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头和JSON请求体。我们还指示MockServer在收到此请求时响应202状态码。

接着,使用测试数据调用*dispatchEmail()*方法,并验证是否向MockServer发送了预期请求(仅一次)。

通过使用MockServer模拟SendGrid API,我们确保集成按预期工作,而无需实际发送任何邮件或产生任何费用

8. 总结

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

我们逐步完成了必要配置,实现了发送简单邮件、带附件邮件以及使用动态模板的HTML邮件功能。

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

一如既往,本文使用的所有代码示例可在GitHub上获取。


原始标题:Sending Emails in Spring Boot Using SendGrid | Baeldung