email: 使用例

ここでは email パッケージを使って電子メールメッセージを読む・書く・送信するいくつかの例を紹介します。より複雑な MIME メッセージについても扱います。

まず、単純なテキストメッセージを作成して送信する方法を見てみましょう (テキストの内容にもアドレスにも Unicode 文字を含められます)。

# 実際の送信に使う smtplib をインポートします
import smtplib

# 必要な email モジュールをインポートします
from email.message import EmailMessage

# textfile に名前が入っているプレーンテキストファイルを読み込み用に開きます。
with open(textfile) as fp:
    # text/plain メッセージを作成します
    msg = EmailMessage()
    msg.set_content(fp.read())

# me == 送信者のメールアドレス
# you == 受信者のメールアドレス
msg['Subject'] = f'The contents of {textfile}'
msg['From'] = me
msg['To'] = you

# 自分の SMTP サーバー経由でメッセージを送信します。
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()

RFC 822 ヘッダーの解析は、 parser モジュールにあるクラスを使用することにより、簡単に実現できます:

# 必要な email モジュールをインポートします
#from email.parser import BytesParser
from email.parser import Parser
from email.policy import default

# 電子メールのヘッダーがファイルにある場合、次の 2 行のコメントを外します:
# with open(messagefile, 'rb') as fp:
#     headers = BytesParser(policy=default).parse(fp)

#  文字列中のヘッダーを解析する場合 (一般的ではありません) は、次のようにします:
headers = Parser(policy=default).parsestr(
        'From: Foo Bar <user@example.com>\n'
        'To: <someone_else@example.com>\n'
        'Subject: Test message\n'
        '\n'
        'Body would go here\n')

#  これで、辞書としてヘッダーの各項目にアクセスできます:
print('To: {}'.format(headers['to']))
print('From: {}'.format(headers['from']))
print('Subject: {}'.format(headers['subject']))

# アドレスの各部分にもアクセスできます:
print('Recipient username: {}'.format(headers['to'].addresses[0].username))
print('Sender name: {}'.format(headers['from'].addresses[0].display_name))

つぎに、あるディレクトリ内にある何枚かの家族写真をひとつの MIME メッセージに収めて送信する例です:

# Import smtplib for the actual sending function.
import smtplib

# Here are the email package modules we'll need.
from email.message import EmailMessage

# Create the container email message.
msg = EmailMessage()
msg['Subject'] = 'Our family reunion'
# me == the sender's email address
# family = the list of all recipients' email addresses
msg['From'] = me
msg['To'] = ', '.join(family)
msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'

# Open the files in binary mode.  You can also omit the subtype
# if you want MIMEImage to guess it.
for file in pngfiles:
    with open(file, 'rb') as fp:
        img_data = fp.read()
    msg.add_attachment(img_data, maintype='image',
                                 subtype='png')

# Send the email via our own SMTP server.
with smtplib.SMTP('localhost') as s:
    s.send_message(msg)

つぎはあるディレクトリに含まれている内容全体をひとつの電子メールメッセージとして送信するやり方です: [1]

#!/usr/bin/env python3

"""ディレクトリの内容を MIME メッセージとして送信します。"""

import os
import smtplib
# ファイル名の拡張子から MIME 型を推測するために使います
import mimetypes

from argparse import ArgumentParser

from email.message import EmailMessage
from email.policy import SMTP


def main():
    parser = ArgumentParser(description="""\
Send the contents of a directory as a MIME message.
Unless the -o option is given, the email is sent by forwarding to your local
SMTP server, which then does the normal delivery process.  Your local machine
must be running an SMTP server.
""")
    parser.add_argument('-d', '--directory',
                        help="""Mail the contents of the specified directory,
                        otherwise use the current directory.  Only the regular
                        files in the directory are sent, and we don't recurse to
                        subdirectories.""")
    parser.add_argument('-o', '--output',
                        metavar='FILE',
                        help="""Print the composed message to FILE instead of
                        sending the message to the SMTP server.""")
    parser.add_argument('-s', '--sender', required=True,
                        help='The value of the From: header (required)')
    parser.add_argument('-r', '--recipient', required=True,
                        action='append', metavar='RECIPIENT',
                        default=[], dest='recipients',
                        help='A To: header value (at least one required)')
    args = parser.parse_args()
    directory = args.directory
    if not directory:
        directory = '.'
    # メッセージを作成します
    msg = EmailMessage()
    msg['Subject'] = f'Contents of directory {os.path.abspath(directory)}'
    msg['To'] = ', '.join(args.recipients)
    msg['From'] = args.sender
    msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'

    for filename in os.listdir(directory):
        path = os.path.join(directory, filename)
        if not os.path.isfile(path):
            continue
        # ファイルの拡張子からコンテントタイプを推測します。
        # エンコーディングは無視しますが、本来は gzip などで圧縮された
        # ファイルのような単純なケースを確認する必要があります。
        ctype, encoding = mimetypes.guess_file_type(path)
        if ctype is None or encoding is not None:
            # 推測できなかったか、ファイルがエンコード (圧縮) されているので、
            # 汎用のバイナリデータ型を使います。
            ctype = 'application/octet-stream'
        maintype, subtype = ctype.split('/', 1)
        with open(path, 'rb') as fp:
            msg.add_attachment(fp.read(),
                               maintype=maintype,
                               subtype=subtype,
                               filename=filename)
    # メッセージを送信または保存します
    if args.output:
        with open(args.output, 'wb') as fp:
            fp.write(msg.as_bytes(policy=SMTP))
    else:
        with smtplib.SMTP('localhost') as s:
            s.send_message(msg)


if __name__ == '__main__':
    main()

つぎに、上のような MIME メッセージをどうやって展開してひとつのディレクトリ上の複数ファイルにするかを示します:

#!/usr/bin/env python3

"""MIME メッセージをディレクトリ内のファイルに展開します。"""

import os
import email
import mimetypes

from email.policy import default

from argparse import ArgumentParser


def main():
    parser = ArgumentParser(description="""\
Unpack a MIME message into a directory of files.
""")
    parser.add_argument('-d', '--directory', required=True,
                        help="""Unpack the MIME message into the named
                        directory, which will be created if it doesn't already
                        exist.""")
    parser.add_argument('msgfile')
    args = parser.parse_args()

    with open(args.msgfile, 'rb') as fp:
        msg = email.message_from_binary_file(fp, policy=default)

    try:
        os.mkdir(args.directory)
    except FileExistsError:
        pass

    counter = 1
    for part in msg.walk():
        # multipart/* は単なるコンテナです
        if part.get_content_maintype() == 'multipart':
            continue
        # メールメッセージによって重要なファイルが上書きされないように、
        # 実際のアプリケーションではファイル名を安全なものにする必要があります
        filename = part.get_filename()
        if not filename:
            ext = mimetypes.guess_extension(part.get_content_type())
            if not ext:
                # 汎用のバイナリデータ用拡張子を使います
                ext = '.bin'
            filename = f'part-{counter:03d}{ext}'
        counter += 1
        with open(os.path.join(args.directory, filename), 'wb') as fp:
            fp.write(part.get_payload(decode=True))


if __name__ == '__main__':
    main()

代替のプレーンテキストバージョン付きの HTML メッセージを作成する方法の例です。もう少し面白くするために、 HTML 部分に関連する画像を追加し、さらに送信だけでなく、送信しようとしているもののコピーをディスクにも保存してみます。

#!/usr/bin/env python3

import smtplib

from email.message import EmailMessage
from email.headerregistry import Address
from email.utils import make_msgid

# 基本のテキストメッセージを作成します。
msg = EmailMessage()
msg['Subject'] = "Pourquoi pas des asperges pour ce midi ?"
msg['From'] = Address("Pepé Le Pew", "pepe", "example.com")
msg['To'] = (Address("Penelope Pussycat", "penelope", "example.com"),
             Address("Fabrette Pussycat", "fabrette", "example.com"))
msg.set_content("""\
Salut!

Cette recette [1] sera sûrement un très bon repas.

[1] http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718

--Pepé
""")

# HTML 版を追加します。
# メッセージは multipart/alternative コンテナになり、
# 最初のパートが元のテキストメッセージ、
# 2 番目のパートが新しい HTML メッセージになります。
asparagus_cid = make_msgid()
msg.add_alternative("""\
<html>
  <head></head>
  <body>
    <p>Salut!</p>
    <p>Cette
        <a href="http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718">
            recette
        </a> sera sûrement un très bon repas.
    </p>
    <img src="cid:{asparagus_cid}">
  </body>
</html>
""".format(asparagus_cid=asparagus_cid[1:-1]), subtype='html')
# HTML で使うために msgid から <> を取り除く必要がある点に注意してください。

# HTML パートに関連する画像を追加します。
with open("roasted-asparagus.jpg", 'rb') as img:
    msg.get_payload()[1].add_related(img.read(), 'image', 'jpeg',
                                     cid=asparagus_cid)

# 送信する内容のローカルコピーを作成します。
with open('outgoing.msg', 'wb') as f:
    f.write(bytes(msg))

# ローカル SMTP サーバー経由でメッセージを送信します。
with smtplib.SMTP('localhost') as s:
    s.send_message(msg)

前の例のメッセージが送信されてきたら、これを処理する方法の一つは次のようなものです:

import os
import sys
import tempfile
import mimetypes
import webbrowser

# 必要な email モジュールをインポートします
from email import policy
from email.parser import BytesParser


def magic_html_parser(html_text, partfiles):
    """安全性に配慮して処理し、partfiles にリンクする HTML を返します。

    href="cid:...." 属性を書き換え、partfiles 内のファイル名を指すようにします。
    簡単ではありませんが、html.parser を使えば実現できるはずです。
    """
    raise NotImplementedError("Add the magic needed")


# 実際のプログラムでは、引数からファイル名を取得します。
with open('outgoing.msg', 'rb') as fp:
    msg = BytesParser(policy=policy.default).parse(fp)

# これでヘッダーの各項目に辞書としてアクセスでき、
# 非 ASCII 文字は Unicode に変換されます:
print('To:', msg['to'])
print('From:', msg['from'])
print('Subject:', msg['subject'])

# メッセージ内容のプレビューを表示するには、最も書式の少ないペイロードを
# 取り出して最初の 3 行を表示できます。
# もちろん、プレーンテキストのパートが
# なければ HTML の最初の 3 行を表示してもあまり役に立ちませんが、
# ここでは考え方を示すための例としています。
simplest = msg.get_body(preferencelist=('plain', 'html'))
print()
print(''.join(simplest.get_content().splitlines(keepends=True)[:3]))

ans = input("View full message?")
if ans.lower()[0] == 'n':
    sys.exit()

# 表示のために最も情報の豊かな代替パートを取り出せます:
richest = msg.get_body()
partfiles = {}
if richest['content-type'].maintype == 'text':
    if richest['content-type'].subtype == 'plain':
        for line in richest.get_content().splitlines():
            print(line)
        sys.exit()
    elif richest['content-type'].subtype == 'html':
        body = richest
    else:
        print("Don't know how to display {}".format(richest.get_content_type()))
        sys.exit()
elif richest['content-type'].content_type == 'multipart/related':
    body = richest.get_body(preferencelist=('html'))
    for part in richest.iter_attachments():
        fn = part.get_filename()
        if fn:
            extension = os.path.splitext(part.get_filename())[1]
        else:
            extension = mimetypes.guess_extension(part.get_content_type())
        with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as f:
            f.write(part.get_content())
            # ここでも <> を除いて、メール形式の cid を HTML 形式にします。
            partfiles[part['content-id'][1:-1]] = f.name
else:
    print("Don't know how to display {}".format(richest.get_content_type()))
    sys.exit()
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
    f.write(magic_html_parser(body.get_content(), partfiles))
webbrowser.open(f.name)
os.remove(f.name)
for fn in partfiles.values():
    os.remove(fn)

# もちろん、この単純なプログラムでは扱えないメッセージも数多くありますが、
# よくあるメッセージには対応できます。

プロンプトまでの、上記のプログラムの出力はこうなります:

To: Penelope Pussycat <penelope@example.com>, Fabrette Pussycat <fabrette@example.com>
From: Pepé Le Pew <pepe@example.com>
Subject: Pourquoi pas des asperges pour ce midi ?

Salut!

Cette recette [1] sera sûrement un très bon repas.

脚注