VagrantでSMTPサーバーを作ってPythonで送信APIを作成する

# はじめに

どうも、新卒でラクスに入社した@hirobexです。
今は[MailDealer](https://www.maildealer.jp/)というサービスの開発をしています。
名前の通り、メールを扱う商材なのですが、私にはメール周りの知識が乏しいです。

なので、今回Advent Calenderという機会に少し自分でメール関連の機能を触ってみました。


# SMTPサーバーの設定

## vagrantfileの設定例

```ruby:Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.network "private_network", ip: "192.168.33.10"
end
```

## Postfixのインストール

```shell
sudo apt-get install -y postfix
```

メールサーバーの設定形式を聞かれるため、「設定無し」を選択

インストール完了後、設定ファイルをコピーしてくるように言われるのでコピーしておく。

```shell
cp /usr/share/postfix/main.cf.debian /etc/postfix/main.cf
```

## Postfixの設定

```shell
sudo vim /etc/postfix/main.cf
```

今回は、下記のように設定

```conf:main.cf
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain

# ローカルホスト以外からも受け付ける
inet_interfaces = all

# 通信をipv4のみに制限
inet_protocols = ipv4

# ローカルアドレスの他に、ip aコマンドで出力されるenp0sのinetアドレスを設定
mynetworks = 127.0.0.0/8, 10.0.2.0/24

# 受信したメールの保存形式・保存場所を指定
home_mailbox = Maildir/

# 受信するドメインの設定
mydestination = $myhostname, $mydomain

smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no
```

### メールエイリアスの更新
```shell
newaliases
```

### メールユーザの追加

今回はメール受信用のユーザを新たに追加した。

```shell
sudo adduser user
sudo chmod 777 /var/spool/mail

su - user
cd
mkdir Maildir
```

### postfixの起動
```shell
systemctl start postfix
```

## 送信テスト

### hostsを設定

ipアドレスは、postfixの設定時と同じように`ip a`コマンドで出力される enp0s のinetアドレスを設定

```hosts
10.0.2.15       mail.example.com
```

### telnetで接続して送信テスト
ログイン

```shell
telnet mail.example.com 25
```
成功時 `220 mail.example.com ESMTP Postfix (Ubuntu)`

FROMアドレスの入力

```shell
mail from: user@mail.example.com
```

 `250 2.1.0 Ok`

Toアドレスの入力

```shell
rcpt to: user@mail.example.com
```

成功時 `250 2.1.5 Ok`

本文の入力開始

```shell
data
```

本文入力

```shell
Hello World!
.

```

成功時 `250 2.0.0 Ok: queued as ○○○`

ログアウト

```shell
quit
```

成功時 

```
221 2.0.0 Bye
Connection closed by foreign host.
```

テスト実行後、下記コマンドでメールデータが新規作成されているか確認

```shell
su - user
cd /MailDir/new
ls
```

下記のようなデータが作成されていれば送受信が成功している

`1639895199.V801I3f68dM923995.mail`

### 送信がうまく行かない場合

`/var/log/maillog`でエラーログが見れる

# 送信APIの作成

今回はfastAPIで作成する。

コードはこう

```python
import smtplib
from email.mime.text import MIMEText
from smtplib import *

from fastapi import FastAPI

app = FastAPI()


@app.post("/send")
def send_email(smtp_server: str, from_address: str, to_address: str, subject: str, message: str):
    with smtplib.SMTP(smtp_server, 25) as smtp:
        msg = MIMEText(message)
        msg["Subject"] = subject
        try:
            smtp.sendmail(msg=msg.as_string(), from_addr=from_address, to_addrs=to_address)
        except (SMTPRecipientsRefused, SMTPHeloError, SMTPSenderRefused, SMTPDataError, SMTPNotSupportedError) as e:
            return {"ERROR_LOG": e}
    return {"to_address": to_address, "subject": subject, "message": message}
```

FastAPIは、自動でAPIのドキュメントを作成してくれる便利機能があるので、利用する
特に設定していなければ、以下のURLにドキュメントが生成される

http://127.0.0.1:8000/docs

Try it Outをクリックすると、Parameterが入力できるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2301870/4c3ce141-51f3-9bf3-59b4-821ae63b1e05.png)


smtp_serverには、Vagrantで設定したprivate ipアドレスを入力する。
from_addresと、to_addressには、テスト送信と同じものを用いる
subjectとmessageは適当に入力する。

excecuteを押して、200が返ってきたら、またMaildirを確認する。

ファイルが増えていたら成功！


# あとがき

ほぼほぼPostfixの設定で埋まってしまいました。
逆に言うと、これだけのコードでAPIが作れてしまうFastAPIがすごいのかもしれませんね。

次はSMTPログインを試したり、DovecotでPOP/IMAP受信ができるようにしたりもしたいですね。

# 参考文献

- [smtplib --- SMTP プロトコルクライアント](https://docs.python.org/ja/3/library/smtplib.html)
- [Python：smtplib【SMTP プロトコルクライアント】 - リファレンス メモ](https://cercopes-z.com/Python/stdlib-smtplib-py.html)
- [Pythonでメールを送信](https://qiita.com/kawa-Kotaro/items/460977f050bf0e2828f2)
- [Vagrant + VirtualBox + CentOS 7 + Postfix + Dovecotでメールサーバを構築してメール送信のテストを行う](https://qiita.com/tomiokario/items/162e5265cead529de570)
- [Ubuntuでメールサーバー構築](https://qiita.com/mizuki_takahashi/items/1b33e1f679359827c17d)
- [CentOS 7 の Postfix で独自ドメインの設定](https://qiita.com/bezeklik/items/438eadbdb06672f3c3b6)