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

2025/04/24

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 = "cn.tuyucheng.taketoday.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类的必需属性:

cn:
    tuyucheng:
        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类并引用它们来发送一封简单的电子邮件:

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语法创建动态电子邮件模板。

为了进行此演示,我们将举一个例子,假设想向我们的用户发送个性化的水合警报电子邮件

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 , this is your friendly reminder to stay hydrated. Your body will thank you!</p>
    <div class="status">
        <p><strong>Last drink:</strong> </p>
        <p><strong>Hydration status:</strong> </p>
    </div>
</div>
</body>
</html>

在我们的模板中,我们使用Handlebars语法来定义、和的占位符。发送电子邮件时,我们会将这些占位符替换为实际值。

我们还使用内部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文件中:

cn:
    tuyucheng:
        sendgrid:
            hydration-alert-notification:
                template-id: ${HYDRATION_ALERT_TEMPLATE_ID}

发送水合警报电子邮件时,我们将在EmailDispatcher类中使用此配置的模板ID。

6.3 发送模板电子邮件

现在我们已经配置了模板ID,让我们创建一个自定义个性化类来保存占位符键名称及其对应的值:

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()方法填充该Map

现在,让我们创建一个新的服务方法来发送水合警报:

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模板中定义的占位符添加自定义值。

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

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

7. 测试SendGrid集成

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

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

7.1 配置测试环境

在编写测试之前,我们将在src/test/resources目录中创建一个application-integration-test.yaml文件,其中包含以下内容:

cn:
    tuyucheng:
        sendgrid:
            api-key: SG0101010101010101010101010101010101010101010101010101010101010101010
            from-email: no-reply@tuyucheng.com
            from-name: Tuyucheng
            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上下文以信任所有证书,因为MockServer默认使用自签名证书

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

@ContextConfiguration(classes = TestSendGridConfiguration.class)

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

7.3 验证SendGrid请求

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

// Set up test data
String toEmail = RandomString.make() + "@tuyucheng.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主体。然后,我们将MockServer配置为接收一个包含Authorization标头和JSON主体的POST请求,该请求指向SendGrid API路径,我们还指示MockServer在发出此请求时以202状态码进行响应。

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

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

8. 总结

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

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

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

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章