はじめに
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_alternative
やadd_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)
[公式ドキュメント](https://docs.python.org/ja/3.10/library/email.examples.html)のソースコード
コメントは省略しています。また、このソースコードはテキストメッセージの送信のみに対応しています。
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/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で
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/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の仕様の解説が長くなりましたが、ソースコードの解説をします。
メッセージの作成
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')
ファイルを読み込み添付します。
参考
- Pythonドキュメント (email: 使用例)
- RFC1341
さいごに
公式ドキュメントを参考にテキストメールを作成した後にhtmlメールを作成しようと、"python email html"をキーワードに検索してみると、MIMEText
とMIMEMultipart
を用いた例しかなく、また、パスワードのマスクなどをちょっと変更しただけの記事が量産されており、コピペで記事を書いているんだろうと感じました。htmlメールを送るために使うクラスを変更するはずはないと思いcpythonの実装を確認してみると、この記事で紹介した実装例を考えつきました。自分はこれらかも1次情報を追うことを怠らないでおこうと思った良い経験でした。