python3.6、 SMTPでメール送信
添付ファイル付(日本語部分を含む)のメールを送信時に、添付ファイルの日本語部分が読めない(文字化けではなくエンコードの値のまま)。
具体的には、
(1)添付ファイルのエンコードがUTF-8の時 -> 受信メールの添付ファイルの日本語部分が読めない
(2)添付ファイルのエンコードがiso-2022jpの時 -> 受信メールの添付ファイルの日本語部分は読める。
のように、(2)の場合は、正常に受信側で読み取れます。
ただ、システム上、元の添付ファイルがUTF-8になってしまっています。
なので、codecsで元の添付ファイルをiso-2022jpに変換しようとすると、下記のエラーになります。
「UnicodeEncodeError: 'iso2022_jp' codec can't encode character '\uff8c' in positi on 98: illegal multibyte sequence」
そもそも、UTF-8の添付ファイルに日本語が含まれてる場合に、正常に(日本語部分が読み取れる状態で)送信できないのでしょうか。
該当のソースコード
parent = MIMEMultipart()
body = MIMEText(text, 'plain', 'utf-8')
parent.attach(body)
attachment = MIMEBase(type, subtype, encoders='utf-8')
file = open(path, 'rt', encoding='utf-8')
attachment.set_payload(file.read())
file.close()
encoders.encode_base64(attachment)
parent.attach(attachment)
attachment.add_header("Content-Disposition","attachment", filename=filename)
parent['Subject'] = Header(subject, charset)
parent['From'] = from_address
parent['To'] = ",".join(to_address)
parent['Date'] = formatdate(localtime=True)
smtp = smtplib.SMTP('xxx.xxx.xxx.xxx', 25)
smtp.sendmail(from_address, to_address + bcc_addrs, parent.as_string())
試したこと
ここに問題に対して試したことを記載してください。
補足情報(FW/ツールのバージョンなど)
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
checkベストアンサー
0
まず、電子メールの内容は文字列ではありません。このことを簡単に説明します(分かっている人は「本題。」のところまで読み飛ばしてください)。
メールを読み書きする人々にとってはたしかにそうで、電子メールはそのほとんどが文字列 (テキストデータ) でできています。しかしプログラマにとっては違います。彼らにとって電子メールというのは、通信回線で伝送されたりサーバのディスクに保存されたりするバイト列です。
文字列は人が考えた概念であって、プログラムの中にしか存在しえないので、伝送したり保存したりするときにバイト列に符号化してやる必要があります。文字列を符号化するために用いる変換表がキャラクタセット (いわゆる「文字コード」) です (電子メールではほかに「伝送符号化」という符号化のやりかたも使いますが、ここでは説明を略します)。
Pythonでは (3.x以降では必ず)、文字列をstr型として、バイト列をbytes型として扱います。str型は文字列を符号化するencode()
メソッドを、bytes型はバイト列を文字列に変換するdecode()
メソッドを持っています。
プログラムで電子メールを作成するには、文字列やバイト列を元に、最終的にバイト列を作成する必要があります。ここまでの説明を前提に、ご質問に回答していきます。
本題。
parent = MIMEMultipart()
body = MIMEText(text, 'plain', 'utf-8')
parent.attach(body)
attachment = MIMEBase(type, subtype, encoders='utf-8')
file = open(path, 'rt', encoding='utf-8')
attachment.set_payload(file.read())
file.close()
encoders.encode_base64(attachment)
parent.attach(attachment)
attachment.add_header("Content-Disposition","attachment", filename=filename)
body
の文字列を本文に持ち、path
の場所に保存したファイルを添付したマルチパートのメッセージを作ろうというのですね。
MIMEText
クラスは"text" MIME型を持つメッセージパートを表すクラスですから、本文の方はこれでいいです。いっぽうMIMEには個々のメッセージパートに「本文」とそれ以外の「添付」という区別はありません。だから添付のほうも同じくMIMEText
クラスを使えばいいでしょう。
上で説明したように、元になるテキストデータは文字列なので、本文でも添付でもstr型のデータを使わなければなりません。open()
でテキストモードを明示的に指定しているのは、バイト列であるファイルの内容を読み出して文字列に変換しているのですから、これで正しいですね。
しかし、encode_base64()
を使って添付のペイロードをBASE64で伝送符号化しようとしています。この意図はいいのですが、BASE64はバイト列をバイト列に符号化する変換です。文字列 (str) を渡したので、これはうまく動かないでしょう。
実は、MIMEText
クラスでcharset
パラメータを指定してインスタンスを作った場合、最終的なメッセージ全体の符号化の際に伝送符号化を適切に判断してやってくれます (と、emailパッケージのソースに書いてありました)。だからここで伝送符号化しなくていいでしょう。
以上のことを元に、この部分を書き直してみたのが以下です (ほかにもちょっと変えました)。
from email.mime.multipart import MIMEMultipart
from email.mime.multipart import MIMEText
text = '''
本文……
'''
path = '/path/to/attachment.txt'
filename = '添付ファイル.txt'
parent = MIMEMultipart()
body = MIMEText(text, 'plain', 'utf-8')
parent.attach(body)
with open(path, 'rt', encoding='utf-8') as f:
content = f.read()
attachment = MIMEText(content, 'plain', 'utf-8')
attachment.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', filename))
parent.attach(attachment)
これでうまくいくのではないでしょうか。わたしはちゃんと確認できていないので、確認してみてください。
[2019-04-03追記]
smtp.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フラグを指定してバイト列のままを読み出す必要がありますね。
回答終わり。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.10%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
質問への追記・修正、ベストアンサー選択の依頼
y_waiwai
2019/03/31 13:28
送信した添付ファイルと受信した添付ファイルを比較したときに同一なんでしょうか
添付ファイルってのは読める読めない以前に、ナカミが変わってしまうとまずいですが。
striker
2019/03/31 13:37
早速返信頂き、ありがとうございます。
はい、送信時のものと受信側のもので、ファイルは同一です。
y_waiwai
2019/03/31 13:38
なら、その「読む」ところのコードを提示しましょう
striker
2019/03/31 13:53
失礼致しました。
下記になります。
parent = MIMEMultipart()
body = MIMEText(text, 'plain', 'utf-8')
attachment = MIMEBase(type, subtype, encoders='utf-8')
file = open(path, 'rt', encoding='utf-8')
attachment.set_payload(file.read())
file.close()
encoders.encode_base64(attachment)
parent.attach(attachment)
attachment.add_header("Content-Disposition","attachment", filename=filename)
smtp = smtplib.SMTP('xxx.xxx.xxx.xxx', 25)
smtp.sendmail(from_address, to_address + bcc_addrs, parent.as_string())
ikedas
2019/03/31 16:42
質問文に追記してください (質問文は編集できます)。あと、エラーメッセージの全文を提示してください。
striker
2019/03/31 19:45
ソースを質問文に追記しました。
エラーは起きず、受信側で添付ファイルを開くと日本語部分が読めないのが問題です。(\u307e\u3059~のようにエンコードされたまま)
ikedas
2019/03/31 20:39
「読める」場合と「読めない」場合にどう表示されるのかを示してもらえますか。スクリーンショットをとって質問文に貼ってもらえればいいです。
ikedas
2019/03/31 21:01
原因わかったけど、これから夜間作業なので、明日の晩見てもまだ回答されてなければ回答する。キーワード: バイナリとテキスト、コデック、半角片仮名
striker
2019/03/31 21:09
返信ありがとうございます。
読める場合と読めない場合のスクリーンショットを追加しました。
お忙しいところ、ありがとうございます。
提示頂いたキーワードを元に、自分でも調べてみます。
otn
2019/03/31 22:02
y_waiwaiさん> なら、その「読む」ところのコードを提示しましょう
> 失礼致しました。
> 下記になります
のコードはどう見ても、受信したファイルを読むコードじゃなくて、ファイルをメールに添付して送信するコードですが?
送受信のファイルが一致していて、受信したファイルが読めないなら、
・送信ファイルを作る時点で間違っていて、読めないファイルを作った
・受信ファイルを読む方法が間違っている
のどちらかであることは自明だと思いますが。
striker
2019/03/31 23:23
説明が足らずにすいません。
スクリプトで行ってるのは送信処理だけで、受信は通常のメーラーで確認してます。
otn
2019/04/01 13:51
> はい、送信時のものと受信側のもので、ファイルは同一です。
はどうやって確認したのでしょう?
出来るだけ主観じゃなくて客観的事実を書いてください。
striker
2019/04/01 17:22
otnさん、返信遅くなってすいません。
正確には、下記になります。
ファイルの比較方法は、WinMergeで比較しました。
(1)読めない時:送信時の添付ファイルと、受信メールの添付ファイルは不一致(日本語部分のみ)。
(1)読める時:送信時の添付ファイルと、受信メールの添付ファイルは完全一致。
otn
2019/04/01 18:55 編集
ということは、前のコメントでの、
> はい、送信時のものと受信側のもので、ファイルは同一です。
は間違いと言う事ですね。これには振り回されました。大きな間違いです。