まず、電子メールの内容は文字列ではありません。このことを簡単に説明します(分かっている人は「本題。」のところまで読み飛ばしてください)。
メールを読み書きする人々にとってはたしかにそうで、電子メールはそのほとんどが文字列 (テキストデータ) でできています。しかしプログラマにとっては違います。彼らにとって電子メールというのは、通信回線で伝送されたりサーバのディスクに保存されたりするバイト列です。
文字列は人が考えた概念であって、プログラムの中にしか存在しえないので、伝送したり保存したりするときにバイト列に符号化してやる必要があります。文字列を符号化するために用いる変換表がキャラクタセット (いわゆる「文字コード」) です (電子メールではほかに「伝送符号化」という符号化のやりかたも使いますが、ここでは説明を略します)。
Pythonでは (3.x以降では必ず)、文字列をstr型として、バイト列をbytes型として扱います。str型は文字列を符号化するencode()
メソッドを、bytes型はバイト列を文字列に変換するdecode()
メソッドを持っています。
プログラムで電子メールを作成するには、文字列やバイト列を元に、最終的にバイト列を作成する必要があります。ここまでの説明を前提に、ご質問に回答していきます。
本題。
python
1parent = MIMEMultipart()
2body = MIMEText(text, 'plain', 'utf-8')
3parent.attach(body)
4
5attachment = MIMEBase(type, subtype, encoders='utf-8')
6file = open(path, 'rt', encoding='utf-8')
7attachment.set_payload(file.read())
8file.close()
9encoders.encode_base64(attachment)
10parent.attach(attachment)
11attachment.add_header("Content-Disposition","attachment", filename=filename)
body
の文字列を本文に持ち、path
の場所に保存したファイルを添付したマルチパートのメッセージを作ろうというのですね。
MIMEText
クラスは"text" MIME型を持つメッセージパートを表すクラスですから、本文の方はこれでいいです。いっぽうMIMEには個々のメッセージパートに「本文」とそれ以外の「添付」という区別はありません。だから添付のほうも同じくMIMEText
クラスを使えばいいでしょう。
上で説明したように、元になるテキストデータは文字列なので、本文でも添付でもstr型のデータを使わなければなりません。open()
でテキストモードを明示的に指定しているのは、バイト列であるファイルの内容を読み出して文字列に変換しているのですから、これで正しいですね。
しかし、encode_base64()
を使って添付のペイロードをBASE64で伝送符号化しようとしています。この意図はいいのですが、BASE64はバイト列をバイト列に符号化する変換です。文字列 (str) を渡したので、これはうまく動かないでしょう。
実は、MIMEText
クラスでcharset
パラメータを指定してインスタンスを作った場合、最終的なメッセージ全体の符号化の際に伝送符号化を適切に判断してやってくれます (と、emailパッケージのソースに書いてありました)。だからここで伝送符号化しなくていいでしょう。
以上のことを元に、この部分を書き直してみたのが以下です (ほかにもちょっと変えました)。
python
1from email.mime.multipart import MIMEMultipart
2from email.mime.multipart import MIMEText
3
4text = '''
5本文……
6'''
7path = '/path/to/attachment.txt'
8filename = '添付ファイル.txt'
9
10parent = MIMEMultipart()
11body = MIMEText(text, 'plain', 'utf-8')
12parent.attach(body)
13
14with open(path, 'rt', encoding='utf-8') as f:
15 content = f.read()
16attachment = MIMEText(content, 'plain', 'utf-8')
17attachment.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', filename))
18
19parent.attach(attachment)
これでうまくいくのではないでしょうか。わたしはちゃんと確認できていないので、確認してみてください。
[2019-04-03追記]
python
1smtp.sendmail(from_address, to_address + bcc_addrs, parent.as_string())
ここですが、「電子メールはバイト列である」という原則からするとas_string()
じゃなくてas_bytes()
じゃないの? と思うかもしれません。実際、as_bytes()
もあるのでそっちを使ってもかまいません。両者はデフォルトでは同じ結果を出します (結果が文字列かバイト列かの違いだけで、どちらもASCIIの範囲の文字/バイトを使う)。が、as_string()はutf8
ポリシが有効だと違う結果になります。
しかし、キャラクタセットをutf-8
からiso-2022-jp
に変えたところ、UnicodeEncodeError例外が発生してしまうということでした。[以下2019-04-03追記]
質問者さんはすでに解決したようですが、改めて書くと、現実には、実際のファイルの内容が想定したキャラクタセットで符号化できるものだとは限らないからです。今回の場合、半角片仮名が含まれています。
ISO-2022-JPの符号化をするコデックは本来、半角片仮名を符号化することができません。しかし、「半角片仮名も使えるようにしようぜ」と考えて独自の方法で符号化できるようにしたコデックの実装も存在してしまっているのが現実です。それらのコデックの符号化方式には互換性がないので、一旦文字列に変換してから符号化すると、元のファイルと違うバイト列になってしまうかもしれません。
MIMETextのインスタンスを作るときに文字列ではなくバイト列を与えると、コデックによる変換をせずにバイト列そのままを符号化されたメッセージに入れてくれます。この場合、open()でファイルを開くときはbフラグを指定してバイト列のままを読み出す必要がありますね。
回答終わり。