14
9

More than 1 year has passed since last update.

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

Posted at

はじめに

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

実際、公式ドキュメントでも

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_alternativeadd_attachmentを用いることでMIME Typeがmultipart/alternativeのメッセージの作成やファイルの添付が可能です。

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)

公式ドキュメントのソースコード

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

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()

巷で紹介されているソースコード

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

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

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)

ソースコードを比較すると、最終的なソースコードで示したソースコードが簡潔であることが分かります。また、後述しますが、巷で紹介されているソースコードで生成されている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/plaintext/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

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では、

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/alternativemultipart/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の仕様の解説が長くなりましたが、ソースコードの解説をします。

メッセージの作成

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

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

multipart/alternativeの部分の作成

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

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

multipart/mixedの部分の作成

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

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

参考

さいごに

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

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9