質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.49%

pythonを使ってSMTPでメール送信時に、添付ファイル本文の日本語部分が読めません

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 3
  • VIEW 5,841

score 7

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ページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • otn

    2019/04/01 13:51

    > はい、送信時のものと受信側のもので、ファイルは同一です。
    はどうやって確認したのでしょう?
    出来るだけ主観じゃなくて客観的事実を書いてください。

    キャンセル

  • striker

    2019/04/01 17:22

    otnさん、返信遅くなってすいません。
    正確には、下記になります。
    ファイルの比較方法は、WinMergeで比較しました。
    (1)読めない時:送信時の添付ファイルと、受信メールの添付ファイルは不一致(日本語部分のみ)。
    (1)読める時:送信時の添付ファイルと、受信メールの添付ファイルは完全一致。

    キャンセル

  • otn

    2019/04/01 18:55 編集

    ということは、前のコメントでの、
    > はい、送信時のものと受信側のもので、ファイルは同一です。
    は間違いと言う事ですね。これには振り回されました。大きな間違いです。

    キャンセル

回答 1

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フラグを指定してバイト列のままを読み出す必要がありますね。

回答終わり。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/03 21:46

    ikedasさん、返信遅くなってすいません。丁寧でわかりやすい解説、ありがとうございます!
    僕もMMETextにしたらうまく送信できました。また、教えて頂いたようにset_payload()の引数をbyte型にしたら、MIMEBaseでもうまく送信できました。丸3日悩まされた原因がわかりました。
    本当にありがとうございました。ソースは良ければ明日、投稿します。
    iso-2022-jpに変えてUnicodeEncodeErrorになるのは、元ファイルのエンコードがiso-2022jpではないから、ではないですか...?

    キャンセル

  • 2019/04/03 22:04

    うーん、まあそういうことです。半角片仮名はISO-2022-JPでは認められていないので、UTF-8からISO-2022-JPに変換しようとしても失敗するのが正しいです。
    でもISO-2022-JPなのに半角片仮名が入ってるメッセージってありますよね。その辺の対応も含めて書きたいと思っています。しばしお待ちください。

    キャンセル

  • 2019/04/04 00:03

    書きました。お待たせしてすみません。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る