Pythonでemailを送信する 【htmlもファイル添付もEmailMessageのみで十分ですよ！！】

# はじめに

Pythonでemailを送信する方法を説明する記事はたくさんありますが、ほとんど(というか全て)の記事で`MIMEText`および`MIMEMultipart`を使用した例が紹介されています。しかし、python3.6で追加された`email.message`モジュールにある`EmailMessage`を使えばより簡潔にメッセージの作成が行えるので紹介します。

実際、[公式ドキュメント](https://docs.python.org/ja/3.10/library/email.mime.html?highlight=mimemultipart#email.mime.multipart.MIMEMultipart)でも

> This module is part of the legacy (Compat32) email API. Its functionality is partially replaced by the contentmanager in the new API, but in certain applications these classes may still be useful, even in non-legacy code.

と書かれており、特別な理由がない限りは`EmailMessage`を使えば良いと思います。(引用した文中では`contentmanager`によってリプレイスされたと書かれていますが、`EmailMessage`がいい感じに扱ってくれるので、一般的な用途においてはユーザーが意識する必要はありません。)

以下では、最終的なソースコード、公式ドキュメントのソースコード、巷で紹介されているソースコードを示した後に、簡単なemailの仕様の確認とコードの解説を行います。

# 最終的なソースコード

以下に示すように、`EmailMessage`が持つ`add_alternative`や`add_attachment`を用いることでMIME Typeが`multipart/alternative`のメッセージの作成やファイルの添付が可能です。

```python
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg['From'] = 'from@example.com'
msg['To'] = 'to@example.com'
msg['Subject'] = 'Subject'

msg.add_alternative('Hello, world.', subtype='text')
msg.add_alternative('<h1>Helo, world.</h1>', subtype='html')
with open('example.pdf', 'rb') as f:
    msg.add_attachment(
        f.read(),
        maintype='application',
        subtype='pdf',
        filename='example.pdf'
    )

with smtplib.SMTP('SMTP_HOST', 'SMTP_PORT') as smtp:
    smtp.starttls()
    smtp.login('USER', 'PASSWORD')
    smtp.send_message(msg)
```

<details><summary>[公式ドキュメント](https://docs.python.org/ja/3.10/library/email.examples.html)のソースコード</summary><div>

コメントは省略しています。また、このソースコードはテキストメッセージの送信のみに対応しています。

```python
import smtplib
from email.message import EmailMessage

with open(textfile) as fp:
    msg = EmailMessage()
    msg.set_content(fp.read())

msg['Subject'] = f'The contents of {textfile}'
msg['From'] = me
msg['To'] = you

s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
```
</div></details>

<details><summary>巷で紹介されているソースコード</summary><div>

importの目的を簡単に説明します。
- `encoders`: pdfファイルをbase64エンコード
- `MIMEBase`: 添付ファイルの作成
- `MIMEText`: テキストメッセージの作成
- `MIMEMultipart`: MIMEが`multipart/alternative`のメッセージを作成

ソースコードを見ると、特にファイルを添付するために少し努力が必要なことが分かります。

```python
import smtplib
from email import encoders
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

msg = MIMEMultipart('alternative')
msg['From'] = 'from@example.com'
msg['To'] = 'to@example.com'
msg['Subject'] = 'Subject'

msg.attach(MIMEText('Hello, world.', 'plain'))
msg.attach(MIMEText('<h1>Hello, world.</h1>', 'html'))

with open('example.pdf', 'rb') as f:
    attachment = MIMEBase('application', 'pdf')
    attachment.set_payload(f.read())
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment', filename='exmaple.pdf')
msg.attach(attachment)

with smtplib.SMTP('SMTP_HOST', 'SMTP_PORT') as smtp:
    smtp.starttls()
    smtp.login('USER', 'PASSWORD')
    smtp.send_message(msg)
```
</div></details>

ソースコードを比較すると、[最終的なソースコード](#最終的なソースコード)で示したソースコードが簡潔であることが分かります。また、後述しますが、巷で紹介されているソースコードで生成されているemailは**RFC1341に反しています**。そのため、メールが正常に表示されなくても文句は言えません。

## Emailの仕様

ソースコードの解説の前に、簡単にemailの仕様を説明します。以下の説明は、間違ってはいないと思いますが、必要十分ではありません。例えば、指定可能なMIME Type全てに言及はしていません。また、一部のメールクライアントでのみ確認を行っているため、使用しているアプリによっては挙動が異なるかもしれません。さらに、平易な解説にすることは目的にしておらず、例えば「MIME Typeとは？」や「base64とは?」のような基本的な事項について解説は行いません。

### MIME Type

プレーンテキストとhtmlの同時送信およびファイル添付の仕組みを理解するためには、メール送信のプロトコルであるSMTPで指定可能なMIME Typeのうち、
- `text/plain`
- `text/html`
- `multipart/alternative`
- `multipart/mixed`
を知っておく必要があります。

`text/plain`はその名前の通りテキストメールであることを表し、`text/html`はhtmlメールであることを表します。受信したメールを確認してみると、以下のように`Content-Tytpe`にいずれかが指定されていることが分かります。

```
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
```

```
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: base64
```

しかし、セキュリティの観点からhtmlメールを拒否することが可能であったり、そもそもhtmlメールを表示できないメールクライアントもあります。そのため、`text/plain`と`text/html`のいずれかではなく、両方を含むメールが送られてくることもあります。その時に指定されているのがMIME Typeが`multipart/alternative`で、以下のように指定されます。

```
Content-Type: multipart/alternative; boundary="===============foo=="

(省略)

--===============foo==
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit

Hello, world.
--===============foo==
Content-Type: text/html; charset="us-ascii"
Content-Transfer-Encoding: 7bit

<meta http-equiv="Content-Type" content="text/html; charset=us-ascii"><h1>Hello, world.</h1>

--===============foo==--
```

この`text/alternative`はその名の通り、代替可能な複数のコンテンツを指定しおり、メールクライアントではどちらかの内容を表示します。優先順位は[RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html)で

> In general, user agents that compose multipart/alternative entities should place the body parts in increasing order of preference, that is, with the preferred format last.

と指定されており、上の例では`text/html`が優先されます。実際、Gmailではこの仕様に従い、最後のコンテンツが表示されます。

上で、"巷で紹介されているソースコードで生成されているemailはRFC1341に反しています"と述べましたが、ここで、その理由を説明します。巷で紹介されているソースコードで生成されるメールは以下のようになります。添付しているファイルが代替可能なコンテンツとして扱われており、一般的にはそれは意図していないでしょう。

```
Content-Type: multipart/alternative; boundary="===============foo=="

(省略)

--===============foo==
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit

Hello, world.
--===============foo==
Content-Type: text/html; charset="us-ascii"
Content-Transfer-Encoding: 7bit

<meta http-equiv="Content-Type" content="text/html; charset=us-ascii"><h1>Hello, world.</h1>
--===============foo==
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="exmaple.pdf"


--===============foo==--
```

そこで、ファイルを添付する場合には別のMIME Typeが指定されます。それが、`multipart/mixed`で、文法は`multipart/alternative`と同様であり、以下のようにコンテンツを指定します。

```
Content-Type: multipart/mixed; boundary="===============foo=="

(省略)

--===============foo==
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

Hello, world.

--===============foo==
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="example.pdf"


--===============foo==--
```

意味は`multipart/alternative`とは異なり、[RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html)では、

> The primary subtype for multipart, "mixed", is intended for use when the body parts are independent and intended to be displayed serially.

と述べられており、つまり、上の例では、"Hello, world."というメッセージと"example.pdf"は独立しており、両方が表示されることを期待しています。

以上、`multipart/alternative`と`multipart/mixed`の仕様と文法を説明してきましたが、それらを組み合わせることも可能であり、その場合は以下のようになります。これが作成したい目的のemailです。

```
Content-Type: multipart/mixed; boundary="===============foo=="

(省略)

--===============foo==
Content-Type: multipart/alternative; boundary="===============bar=="

--===============bar==
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

Hello, world.

--===============bar==
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 7bit

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><h1>Helo, world.</h1>

--===============bar==--
--===============foo==
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="example.pdf"


--===============foo==--
```

## ソースコードの解説

Emailの仕様の解説が長くなりましたが、ソースコードの解説をします。

### メッセージの作成

```python
msg = EmailMessage()
msg['From'] = 'from@example.com'
msg['To'] = 'to@example.com'
msg['Subject'] = 'Subject'
```

送信元のメールアドレス、送信先のメールアドレス、件名を設定します。

### `multipart/alternative`の部分の作成

```python
msg.add_alternative('Hello, world.', subtype='text')
msg.add_alternative('<h1>Helo, world.</h1>', subtype='html')
```

テキストメールとhtmlメールのコンテンツを作成します。RFC1341に従うメールクライアントでは後ろのコンテンツが優先されて表示されるため、この場合は`text/html`が優先されます。

### `multipart/mixed`の部分の作成

```python
with open('example.pdf', 'rb') as f:
    msg.add_attachment(f.read(), maintype='application', subtype='pdf', filename='example.pdf')
```

ファイルを読み込み添付します。

# 参考

- Pythonドキュメント ([email: 使用例](https://docs.python.org/ja/3/library/email.examples.html))
- [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html)

# さいごに

公式ドキュメントを参考にテキストメールを作成した後にhtmlメールを作成しようと、"python email html"をキーワードに検索してみると、`MIMEText`と`MIMEMultipart`を用いた例しかなく、また、パスワードのマスクなどをちょっと変更しただけの記事が量産されており、コピペで記事を書いているんだろうと感じました。htmlメールを送るために使うクラスを変更するはずはないと思いcpythonの実装を確認してみると、この記事で紹介した実装例を考えつきました。自分はこれらかも1次情報を追うことを怠らないでおこうと思った良い経験でした。