"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.

-[ 脚注 ]-

[1] 最初の思いつきと用例は Matthew Dixon Cowles のおかげです。
