SendGridのAPIを呼び出すためのJava SDKはあるし、Spring BootでAutoConfigurationも用意されていますが、SpringのRestClientを使って直接APIを呼び出す方法をメモします。
SendGridのJava SDKは
- コードが自分の好みでない
- 開発・テスト用途の送信先の変更が面倒 (プロパティ一つでエンドポイントを切り替えられない)
- 古いApache HttpClientを使っている
などの理由で、個人的には使いません。
メールを送信するだけであれば https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send のAPIを実行するだけであり、
基本的な使い方であればシンプルなREST APIアクセスで済むため、わざわざSDKを使わなくてもSpringのRestClientを使って直接APIを呼び出すので十分です。
また、RestClientを使うと、Interceptorが使えるので
- クライアントサイドのアクセスログ
- Observability (Tracing, Metrics)
- Retry
など既存の機能を使え、他のAPI呼び出しと同じ運用ができるメリットがあります。
目次
プロジェクト作成
Spring InitializrでSpring Bootのプロジェクトを作成します。
curl -s https://start.spring.io/starter.tgz \
-d artifactId=demo-sendgrid \
-d name=demo-sendgrid \
-d baseDir=demo-sendgrid \
-d packageName=com.example \
-d dependencies=web,actuator,configuration-processor,prometheus,testcontainers \
-d type=maven-project \
-d applicationName=DemoSendgridApplication | tar -xzvf -
cd demo-sendgrid
まずはSendGridのアクセス情報をプロパティ化します。
cat <<'EOF' > src/main/java/com/example/SendGridProps.java
package com.example;
import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConfigurationProperties(prefix = "sendgrid")
public record SendGridProps(String apiKey, @DefaultValue("https://api.sendgrid.com") URI baseUrl,
@DefaultValue("noreply@example.com") String from) {
}
EOF
メインクラスに@ConfigurationPropertiesScanを追加して、SendGridPropsをスキャンできるようにします。
cat <<'EOF' > src/main/java/com/example/DemoSendgridApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoSendgridApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSendgridApplication.class, args);
}
}
EOF
ここから本題です。/v3/mail/send APIのドキュメントに従って、リクエストボディを作成します。
送信先、件名、本文、返信先を設定するだけであれば、Mapで十分です。今回はHelloControllerに直接APIアクセスを書きますが、SendGridSenderクラスなどを作成した方が良いでしょう。
cat <<'EOF' > src/main/java/com/example/HelloController.java
package com.example;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
@RestController
public class HelloController {
private final RestClient restClient;
private final SendGridProps props;
public HelloController(RestClient.Builder restClientBuilder, SendGridProps props) {
this.restClient = restClientBuilder.baseUrl(props.baseUrl())
.defaultHeaders(headers -> headers.setBearerAuth(props.apiKey()))
.defaultStatusHandler(__ -> true, (req, res) -> {
})
.build();
this.props = props;
}
@PostMapping(path = "/send")
public Map<String, Object> send(@RequestBody Message message) {
ResponseEntity<String> response = this.restClient.post()
.uri("/v3/mail/send")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("personalizations",
List.of(Map.of("to", List.of(Map.of("email", message.to())), "subject", message.subject())), "from",
Map.of("email", this.props.from()), "content",
List.of(Map.of("type", "text/plain", "value", message.content()))))
.retrieve()
.toEntity(String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"Failed to send a mail: " + response.getBody());
}
return Map.of("message", "Sent");
}
public record Message(String to, String subject, String content) {
}
}
EOF
TestcontainersでSendGridのモックを起動
動作確認やテスト用に実際のSendGrid APIを呼び出すのではなく、モック化サービスを使いたいです。
SendGrid MailDevはその用途にぴったりでした。
モックSMTPサーバーであるMailDevのフロントエンドにSendGrid APIをラップしているようです。
amd/arm両方のDockerイメージが用意されているのも使いやすいです。
SendGrid MailDevをTestcontainersで起動するための設定を追加します。
cat <<'EOF' > src/test/java/com/example/TestcontainersConfiguration.java
package com.example;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.testcontainers.containers.FixedHostPortGenericContainer;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
FixedHostPortGenericContainer<?> sendgrid(@Value("${maildev.port:31080}") int maildevPort) {
var container = new FixedHostPortGenericContainer<>("ykanazawa/sendgrid-maildev")
.withEnv("SENDGRID_DEV_API_SERVER", ":3030")
.withEnv("SENDGRID_DEV_API_KEY", "SG.test")
.withEnv("SENDGRID_DEV_SMTP_SERVER", "127.0.0.1:1025")
.withExposedPorts(3030, 1080)
.waitingFor(new LogMessageWaitStrategy().withRegEx(".*sendgrid-dev entered RUNNING state.*")
.withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)))
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("sendgrid-maildev")));
return maildevPort > 0 ? container.withFixedExposedPort(maildevPort, 1080) : container;
}
@Bean
DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer<?> sendgrid) {
return registry -> {
registry.add("sendgrid.base-url", () -> "http://127.0.0.1:" + sendgrid.getMappedPort(3030));
registry.add("sendgrid.api-key", () -> "SG.test");
registry.add("maildev.port", () -> sendgrid.getMappedPort(1080));
};
}
}
EOF
メール受信画面をブラウザで確認したいため、MailDevのWeb UIのポートを31080に固定しました。
Testcontainersをローカル開発用途で使うために、test-runコマンドでアプリケーションを起動します。
./mvnw spring-boot:test-run
またはIDEでsrc/test/java/com/example/TestDemoSendgridApplication.javaを実行します。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.5)
2025-05-19T10:46:18.092+09:00 INFO 57122 --- [demo-sendgrid] [ main] com.example.DemoSendgridApplication : Starting DemoSendgridApplication using Java 21.0.6 with PID 57122 (/private/tmp/demo-sendgrid/target/classes started by toshiaki in /private/tmp/demo-sendgrid)
2025-05-19T10:46:18.093+09:00 INFO 57122 --- [demo-sendgrid] [ main] com.example.DemoSendgridApplication : No active profile set, falling back to 1 default profile: "default"
2025-05-19T10:46:18.402+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2025-05-19T10:46:18.407+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-05-19T10:46:18.407+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.40]
2025-05-19T10:46:18.423+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2025-05-19T10:46:18.423+09:00 INFO 57122 --- [demo-sendgrid] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 317 ms
2025-05-19T10:46:18.472+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.images.PullPolicy : Image pull policy will be performed by: DefaultPullPolicy()
2025-05-19T10:46:18.473+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.t.utility.ImageNameSubstitutor : Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2025-05-19T10:46:18.479+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.DockerClientFactory : Testcontainers version: 1.20.6
2025-05-19T10:46:18.542+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.t.d.DockerClientProviderStrategy : Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2025-05-19T10:46:18.627+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.t.d.DockerClientProviderStrategy : Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2025-05-19T10:46:18.627+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.DockerClientFactory : Docker host IP address is localhost
2025-05-19T10:46:18.639+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.DockerClientFactory : Connected to docker:
Server Version: 27.5.1
API Version: 1.47
Operating System: OrbStack
Total Memory: 96439 MB
2025-05-19T10:46:18.679+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.testcontainers/ryuk:0.11.0 : Creating container for image: testcontainers/ryuk:0.11.0
2025-05-19T10:46:18.699+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.t.utility.RegistryAuthLocator : Credential helper/store (docker-credential-osxkeychain) does not have credentials for https://index.docker.io/v1/
2025-05-19T10:46:18.781+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.testcontainers/ryuk:0.11.0 : Container testcontainers/ryuk:0.11.0 is starting: 785a37255ea791b78ae1e2171d33a284d4a76ba532fe6c7ddd1991ab63ec540a
2025-05-19T10:46:18.938+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.testcontainers/ryuk:0.11.0 : Container testcontainers/ryuk:0.11.0 started in PT0.258628S
2025-05-19T10:46:18.940+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.t.utility.RyukResourceReaper : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2025-05-19T10:46:18.940+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.DockerClientFactory : Checking the system...
2025-05-19T10:46:18.940+09:00 INFO 57122 --- [demo-sendgrid] [ main] org.testcontainers.DockerClientFactory : ✔︎ Docker server version should be at least 1.6.0
2025-05-19T10:46:18.942+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.ykanazawa/sendgrid-maildev:latest : Creating container for image: ykanazawa/sendgrid-maildev:latest
2025-05-19T10:46:18.977+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.ykanazawa/sendgrid-maildev:latest : Container ykanazawa/sendgrid-maildev:latest is starting: b084cbf0c96ec57eaeca647dd574415ccb89e1fd852dc031e83c7b7d9563bcc8
2025-05-19T10:46:19.147+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDERR: /usr/lib/python3.11/site-packages/supervisor/options.py:474: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.
2025-05-19T10:46:19.147+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDERR: self.warnings.warn(
2025-05-19T10:46:19.150+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:19,150 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
2025-05-19T10:46:19.151+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:19,150 INFO Included extra file "/etc/supervisor/conf.d/app.conf" during parsing
2025-05-19T10:46:19.153+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:19,153 INFO RPC interface 'supervisor' initialized
2025-05-19T10:46:19.154+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:19,153 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2025-05-19T10:46:19.154+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:19,153 INFO supervisord started with pid 1
2025-05-19T10:46:20.162+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:20,158 INFO spawned: 'maildev' with pid 7
2025-05-19T10:46:20.166+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:20,163 INFO spawned: 'sendgrid-dev' with pid 8
2025-05-19T10:46:20.174+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: SENDGRID_DEV_API_SERVER :3030
2025-05-19T10:46:20.175+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: SENDGRID_DEV_API_KEY SG.test
2025-05-19T10:46:20.176+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: SENDGRID_DEV_SMTP_SERVER 127.0.0.1:1025
2025-05-19T10:46:20.176+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: SENDGRID_DEV_SMTP_USERNAME
2025-05-19T10:46:20.176+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: SENDGRID_DEV_SMTP_PASSWORD
2025-05-19T10:46:20.176+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT:
2025-05-19T10:46:20.176+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: ____ __
2025-05-19T10:46:20.177+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: / __/___/ / ___
2025-05-19T10:46:20.177+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: / _// __/ _ \/ _ \
2025-05-19T10:46:20.177+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: /___/\__/_//_/\___/ v3.3.10-dev
2025-05-19T10:46:20.177+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: High performance, minimalist Go web framework
2025-05-19T10:46:20.178+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: https://echo.labstack.com
2025-05-19T10:46:20.178+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: ____________________________________O/_______
2025-05-19T10:46:20.178+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: O\
2025-05-19T10:46:20.179+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: ⇨ http server started on [::]:3030
2025-05-19T10:46:20.377+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: MailDev using directory /tmp/maildev-7
2025-05-19T10:46:20.385+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: MailDev webapp running at http://0.0.0.0:1080/
2025-05-19T10:46:20.385+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: MailDev SMTP Server running at 0.0.0.0:1025
2025-05-19T10:46:21.394+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:21,391 INFO success: maildev entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2025-05-19T10:46:21.394+09:00 INFO 57122 --- [demo-sendgrid] [ream--195085445] sendgrid-maildev : STDOUT: 2025-05-19 01:46:21,392 INFO success: sendgrid-dev entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2025-05-19T10:46:21.394+09:00 INFO 57122 --- [demo-sendgrid] [ main] tc.ykanazawa/sendgrid-maildev:latest : Container ykanazawa/sendgrid-maildev:latest started in PT2.452433S
2025-05-19T10:46:21.562+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
2025-05-19T10:46:21.577+09:00 INFO 57122 --- [demo-sendgrid] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-05-19T10:46:21.582+09:00 INFO 57122 --- [demo-sendgrid] [ main] com.example.DemoSendgridApplication : Started DemoSendgridApplication in 3.599 seconds (process running for 3.698)
2025-05-19T10:46:31.351+09:00 INFO 57122 --- [demo-sendgrid] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-05-19T10:46:31.351+09:00 INFO 57122 --- [demo-sendgrid] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2025-05-19T10:46:31.352+09:00 INFO 57122 --- [demo-sendgrid] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
http://localhost:31080/ でMailDevのWeb UIを開きます。

APIを呼び出してメールを送信します。
$ curl -s http://localhost:8080/send --json '{"to":"makingx@example.com","subject":"Hello World!", "content": "This is a test mail!"}'
{"message":"Sent"}
MailDevのWeb UIでメールを受信したことを確認します。

Integration Testの作成
Testcontainersの設定をそのまま使ってIntegration Testを作成します。
MainDevは受信したメールを返すREST APIがあるので、/email APIを呼び出して受信したメールを確認します。
ただし、このAPIはHTTP/2に対応していません。RestClientのデフォルトのClientHttpRequestFactoryはSpring Boot 3.4ではHTTP/2を使うJdkClientHttpRequestFactoryを使う設定になっているため、
HTTP/2未対応のSimpleClientHttpRequestFactoryを使うようにspring.http.client.factory=simpleを指定します。
cat <<'EOF' > src/test/java/com/example/HelloControllerIntegrationTests.java
package com.example;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@Import(TestcontainersConfiguration.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "maildev.port=0", "spring.http.client.factory=simple" })
public class HelloControllerIntegrationTests {
RestClient restClient;
@LocalServerPort
int serverPort;
int maildevPort;
@BeforeEach
void setUp(@Autowired RestClient.Builder restClientBuilder, @Value("${maildev.port}") int maildevPort) {
this.restClient = restClientBuilder.defaultStatusHandler(__ -> true, (req, res) -> {
}).build();
this.maildevPort = maildevPort;
}
@Test
void testSend() {
ResponseEntity<String> response = this.restClient.post()
.uri("http://localhost:{port}/send", this.serverPort)
.contentType(MediaType.APPLICATION_JSON)
.body("""
{"to":"makingx@example.com","subject":"Hello World!", "content": "This is a test mail!"}
""")
.retrieve()
.toEntity(String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualToIgnoringWhitespace("""
{"message":"Sent"}
""");
List<Map<String, Object>> received = this.restClient.get()
.uri("http://localhost:{port}/email", this.maildevPort)
.retrieve()
.body(new ParameterizedTypeReference<>() {
});
assertThat(received).hasSize(1);
assertThat(received.get(0)).containsEntry("subject", "Hello World!");
assertThat(received.get(0)).containsEntry("text", "This is a test mail!\n");
assertThat(received.get(0)).containsEntry("to", List.of(Map.of("address", "makingx@example.com", "name", "")));
assertThat(received.get(0)).containsEntry("from",
List.of(Map.of("address", "noreply@example.com", "name", "")));
}
}
EOF
テストが成功することを確認してください。
./mvnw test
SendGridに向き先変更
ローカルで動作確認できたので、送信先をSendGridに変更します。
次のコマンドでSendGridのAPIキーと送信元メールアドレスを引数に設定して、アプリケーションを起動します。
./mvnw spring-boot:run -Dspring-boot.run.arguments="--sendgrid.api-key=YOUR_SENDGRID_API_KEY --sendgrid.from=YOUR_VERIFIED_SENDER_EMAIL"
APIを呼び出してメールを送信します。
$ curl -s http://localhost:8080/send --json '{"to":"YOUR_REAL_EMAIL","subject":"Hello World!", "content": "This is a test mail!"}'
{"message":"Sent"}
メールが届くことを確認してください。

SendGridのAPIをRestClientで直接呼び出す方法をメモしました。
Testcontainersを使って、SendGridのモックを起動してテストする方法も紹介しました。