aws-ses-v2の動作確認をローカルで行う

## はじめに

[aws-sdk-rust](https://github.com/awslabs/aws-sdk-rust) の [sesv2](https://github.com/awslabs/aws-sdk-rust/tree/main/sdk/sesv2) を使いたくて、ローカルでメールの動作確認をしたかったのですが、[localstack](https://github.com/localstack/localstack)を使おうとしたら`no provider exists for service sesv2`とのエラーが出てしまいました。

しばらくググっていると[aws-ses-v2-local](https://www.npmjs.com/package/aws-ses-v2-local)を見つけたので、こちらを使って動作確認してみます。

## aws-ses-v2-local　を dockerコンテナ内で動かす

```docker-compose.yaml
version: "3.7"

services:
  local_ses:
    image: node:18.1.0
    working_dir: /app
    ports:
      - 8005:8005
    volumes:
      - ./local_ses:/local_ses
    tty: true
```


package.json を作成し, node_modules を生成します。

```bash
docker-compose run local_ses bash
npm init # 対話形式でいろいろ聞かれるので答える
npm install aws-ses-v2-local
```

のちのち実行すると`SyntaxError: Cannot use import statement outside a module`エラーがでるので、指示通り`"type": "module"`を package.json に追記します。ついでに、`npm start`で実行できるように scripts に追記します。

```diff:local_ses/package.json
(略)
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
+ "type": "module",
  "scripts": {
+   "start": "node index.js"
  },
(略)
```

実行するjsファイルを作成します。[aws-ses-v2-local](https://www.npmjs.com/package/aws-ses-v2-local)の Usage の通りですが、`TypeError: server is not a function`とのエラーが出てしまったのでそこだけ変えています。

```diff:local_ses/index.js
import server from 'aws-ses-v2-local'
- server({ port: 8005 })
+ let s = await server.default({ port: 8005 })
console.log('aws-ses-v2-local: server up and running')
```

`docker-compose up` で立ち上がるようにします。

```diff:docker-compose.yaml
version: "3.7"

services:
  local_ses:
    image: node:18.1.0
    working_dir: /app
    ports:
      - 8005:8005
    volumes:
      - ./local_ses:/local_ses
    tty: true
+   command: "npm start"
```

立ち上げると以下のようにログが出力されます。

```
$ docker-compose up
local_ses_1  | 
local_ses_1  | > app@1.0.0 start
local_ses_1  | > node index.js
local_ses_1  | 
local_ses_1  | aws-ses-v2-local: server up and running
```

## aws-sdk-rust sesv2 でメールを送信する
Rustのコンテナを追加します。

```diff:docker-compose.yaml
version: "3.7"

services:
+ app:
+   image: rust:1.60
+   working_dir: /app
+   volumes:
+     - ./app:/app
+   environment:
+     - AWS_ACCESS_KEY_ID=xxx
+     - AWS_SECRET_ACCESS_KEY=xxx
+     - AWS_REGION=ap-northeast-1
+   tty: true
  local-ses:
    build: ./local_ses
    ports:
      - 8005:8005
    command: "npm start"
    volumes:
      - ./local_ses:/app
    tty: true
```

```toml:app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
aws-config = "0.13.0"
aws-sdk-sesv2 = "0.13.0"
tokio = { version = "1", features = ["full"] }
http = "0.2.8"
```

`main.rs` は[aws-sdk-rustのgithubのReadme](https://github.com/awslabs/aws-sdk-rust#readme) と [ses v2のコード例](https://docs.aws.amazon.com/sdk-for-rust/latest/dg/rust_sesv2_code_examples.html)を参考にして書きました。

```rust:app/src/main.rs
use aws_sdk_sesv2::{Client, Error, Endpoint};
use aws_sdk_sesv2::model::{Body, Content, Destination, EmailContent, Message};
use http::Uri;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let shared_config = aws_config::load_from_env().await;
    // let client = Client::new(&shared_config);
    // ローカルで動作確認するときは、以下のようにエンドポイントを指定して client をつくる
    let ses_local_config = aws_sdk_sesv2::config::Builder::from(&shared_config)
        .endpoint_resolver(
            Endpoint::immutable(Uri::from_static("http://local_ses:8005")),
        ).build();

    let client = Client::from_conf(ses_local_config);

    let dest = Destination::builder().to_addresses("user@example.com").build();
    let subject_content = Content::builder().data("テストです").charset("UTF-8").build();
    let body_content = Content::builder().data("テストの内容です").charset("UTF-8").build();
    let body = Body::builder().text(body_content).build();

    let msg = Message::builder()
        .subject(subject_content)
        .body(body)
        .build();

let email_content = EmailContent::builder().simple(msg).build();

    let req = client
        .send_email()
        .from_email_address("sender@example.com")
        .destination(dest)
        .content(email_content)
    ;
    let resp = req.send().await;
    println!("response: {:?}", resp);
    Ok(())
}
```

app コンテナ内で `cargo run`を実行すると、成功のレスポンスが返ってきます。

```
$ cargo run
response: Ok(SendEmailOutput { message_id: Some("ses-351447512") })
```

http://localhost:8005 にアクセスすると、メールの内容を確認することができます。

![メール内容確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656409/a8208b3b-9e1b-8f9c-ba73-7872ae532d04.png)