ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

python3でContent-Transfer-Encoding ヘッダを 変更するのに苦労した話

msg = MIMEText(
main_text.encode("utf-8", "ignore"),
'plain',
'utf-8'
)

上のようなコードでメッセージを送信したとき、Content-Transfer-Encodingヘッダは7bitでした。

Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

上の例ではメールの本文(main_text)をbase64に変換し忘れていたことに気づき、次のようなコードに変えました

msg = MIMEText(
base64.b64encode( main_text.encode("utf-8", "ignore") ),
'plain',
'utf-8'
)

しかし、これでもEncodingヘッダは7bitでした。

Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

Content-Transfer-Encoding ヘッダをbase64に変更するにはどうすればいいのか?どうやらemail.encoders.encode_base64()を使えばいいっぽいです

ペイロードbase64 形式でエンコードし、 Content-Transfer-Encoding ヘッダを base64 に変更します。これはペイロード中のデータのほとんどが印刷不可能な文字である場合に適しています。 quoted-printable 形式よりも結果としてはコンパクトなサイズになるからです。 base64 形式の欠点は、これが人間にはまったく読めないテキストになってしまうことです。

ということで、msgにemail.encoders.encode_base64()を使えば解決しそうです。

msg = MIMEText(
base64.b64encode( main_text.encode("utf-8", "ignore") ),
'plain',
'utf-8'
)
email.encoders.encode_base64(msg)

しかし、これをすると次のヘッダになりました

Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

" Content-Transfer-Encoding:base64 "が追加されています。変更するんじゃないのかよ。
小一時間悩みました。しかしどうということはなさそうです。文章はちゃんと読みましょう。

ペイロードbase64 形式でエンコードし、 Content-Transfer-Encoding ヘッダを base64 に変更します。

email.encoders.encode_base64()自体が、base64変換の機能を持っていました。どうやら二重でbase64変換していたのが原因のようでした。
というわけで、次のように変更。

msg = MIMEText(
main_text.encode("utf-8", "ignore"),
'plain',
'utf-8'
)
email.encoders.encode_base64(msg)

ヘッダを確認します

Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

は?
やっぱり、Content-Transfer-Encodingヘッダが追加されてしまいます。どうなっとんじゃ。
email.encoders.encode_base64()の元のソースコードを覗いてみます。

def encode_base64(msg):
"""Encode the message's payload in Base64.
Also, add an appropriate Content-Transfer-Encoding header.
"""
orig = msg.get_payload(decode=True)
encdata = str(_bencode(orig), 'ascii')
msg.set_payload(encdata)
msg['Content-Transfer-Encoding'] = 'base64'

ぱっと見た感じ、
(1). msgのペイロードを取り出し、ついでにデコードもする(orig)
(2). origをasciiでエンコードする(encdata)
(3). msgのペイロードにencdataをセットする
(4). msgのContent-Transfer-Encodingヘッダをbase64にする
という処理をしてそうです
どうやら、email.encoders.encode_base64()を使わなくてもmsg['Content-Transfer-Encoding'] = 'base64'とやるだけでヘッダを変更できそうです。そもそも私はヘッダ情報を変更したいだけだったんだ。これでいきましょう。

msg = MIMEText(
main_text.encode("utf-8", "ignore"),
'plain',
'utf-8'
)
msg['Content-Transfer-Encoding'] = 'base64'
msg['Content-Transfer-Encoding'] = 'base64'
msg['Content-Transfer-Encoding'] = 'base64'

ヘッダを確認します

Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

ここにきて気づいたのですが、どうやらmsg["hoge"] = "piyo"は、変更でなくて追加のようです。その証拠に、

msg = MIMEText(
main_text.encode("utf-8", "ignore"),
'plain',
'utf-8'
)
msg['Content-Transfer-Encoding'] = 'base64'
msg['Subject'] = 'hogehoge'
msg['Content-Transfer-Encoding'] = 'base64'
msg['Subject'] = 'hogehoge'
msg['Content-Transfer-Encoding'] = 'base64'
msg['Subject'] = 'hogehoge'
Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

となります。
で、どうすればいいのか。
また小一時間悩んだ結果、原因はMIMETextクラスにありました。

class email.mime.text.MIMEText(_text, _subtype='plain', _charset=None, *, policy=compat32)
(中略)
_charset 引数に明示的に None をセットしない限りは、作成される MIMEText オブジェクトは charset の付いた Content-Type ヘッダと Content-Transfer-Endcoding ヘッダの両方を持ちます。これは後続の set_payload 呼び出しが、 set_payload コマンドに charset が渡したとしてもエンコードされたペイロードにはならないことを意味します。 Content-Transfer-Encoding ヘッダを削除することでこの振る舞いを「リセット」出来ます。これにより set_payload 呼び出しが新たなペイロードを自動的にエンコード (そして新たな Content-Transfer-Encoding ヘッダを追加) します。

つまり、Content-Transfer-Encodingヘッダを変更するには、
・_charset=Noneでメッセージを作成する
または
・一度Content-Transfer-Encoding ヘッダを削除してset_payload()する
の2通りのやり方があるようです。
後者の方法にContent-Transfer-Encoding ヘッダを削除と書いてありますが、削除の方法は書いてありません。「そもそもset_payloadってなんだっけ」と検索をかけると、それっぽいのがMessageクラスに見つかりました。

class email.message.Message(policy=compat32)
(中略)
replace_header(_name, _value)

Replace a header. Replace the first header found in the message that matches _name, retaining header order and field name case. If no matching header was found, a KeyError is raised.

「Messageオブジェクトの中にある最初に見つけた_nameヘッダの値を_valueに置き換える」というメンバメソッドです。
これを使えばいいんじゃないでしょうか。

msg = MIMEText(
base64.b64encode( main_text.encode("utf-8", "ignore") ),
'plain',
'utf-8'
)
msg.replace_header('Content-Transfer-Encoding', "base64")
Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Subject: =?utf-8?b?44CQTWltYW1vcmnjgJHmuKnluqYv5rm/
4K544OI44Oh44O844Or44Gn44GZ?=
To: hogehoge@test.mail
Cc:
Bcc:
Date: Mon, 19 Jun 2017 16:12:46 +0900

[この辺に本文]

できました!はー長かった。

ちなみに、MIMETextだけがこんな仕様なのかと疑問に思い、MIMEBaseでも試してみましたが、同じ挙動でした。たぶん他のMIMEクラスでも同様だと思います。

csv_msg = MIMEBase("text", "csv")
s = "hoge,piyo,fuga"
csv_msg.set_payload(s.encode("utf-8", "ignore"))
print(csv_msg)
"""
Content-Type: text/csv
MIME-Version: 1.0

hoge,piyo,fuga
"""
csv_msg["Content-Transfer-Encoding"] = "7bit"
encoders.encode_base64(csv_msg)
print(csv_msg)
"""
Content-Type: text/csv
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64

aG9nZSxwaXlvLGZ1Z2E=
"""
csv_msg["Content-Transfer-Encoding"] = "base64"
csv_msg["Content-Transfer-Encoding"] = "base64"
csv_msg["Content-Transfer-Encoding"] = "base64"
csv_msg["X-Moe"] = "honoka, kotori, umi"
print(csv_msg)
"""
Content-Type: text/csv
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
X-Moe: honoka, kotori, umi

aG9nZSxwaXlvLGZ1Z2E=
"""

ところで、メールのcharsetはutf-8iso-2022-jpのどっちにしたほうがいいんですかね。