Python: emailの文字コードの扱い
#Python3 では文字を扱うとき内部でUnicode(bytesではなくstr)に変換する。 メールサーバーから取り出すとき、bytesとして取り出して、strに変換する
bytesの段階ではUTF8, CP932, Shift-JIS, ISO-2022-JP (JIS) 等色々な文字エンコーディング
通常のencode/decodeなら、errorsパラメータに'replace'等を指定すれば回避できる
code:python
>> suzaki = b'''\x1b$B='yu?@<R\x1b(B'''
>> suzaki.decode('iso2022jp', errors='replace')
'洲�神社'
emailモジュール の場合、errorsを指定できないところでdecodeしているコードがある
code:python
def make_header(decoded_seq, maxlinelen=None, header_name=None, continuation_ws=' '):
h = Header(maxlinelen=maxlinelen, header_name=header_name,
continuation_ws=continuation_ws)
for s, charset in decoded_seq:
# None means us-ascii but we can simply pass it on to h.append()
if charset is not None and not isinstance(charset, Charset):
charset = Charset(charset)
h.append(s, charset) # <- 第3引数がerrorsだけど、make_header関数の呼び出し側で指定できない
return h
append(s, charset=None, errors='strict')
この MIME ヘッダに文字列 s を追加します。
いずれの場合でも、 RFC 2822 準拠のヘッダを RFC 2047 の規則を用いて生成する際、文字列は指定された文字セットの出力コーデックを用いてエンコードされます。出力コーデックを用いて文字列がエンコードできないときは UnicodeError が発生します。
Optional errors is passed as the errors argument to the decode call if s is a byte string.
code:python
>> from email.header import make_header
>> suzaki = b'''\x1b$B='yu?@<R\x1b(B'''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.6/email/header.py", line 174, in make_header
h.append(s, charset)
File ".../lib/python3.6/email/header.py", line 295, in append
s = s.decode(input_charset, errors)
UnicodeDecodeError: 'iso2022_jp' codec can't decode bytes in position 5-6: illegal multibyte sequence
しょうがないので、make_headerを改造したコードを用意して回避
code:email_make_header.py
def make_header(decoded_seq, maxlinelen=None, header_name=None, continuation_ws=' '):
h = email.header.Header(maxlinelen=maxlinelen, header_name=header_name,
continuation_ws=continuation_ws)
for s, charset in decoded_seq:
if charset is not None and not isinstance(charset, email.charset.Charset):
charset = email.charset.Charset(charset)
h.append(s, charset, errors='replace')
return h
利用例
code:python
'洲�神社'
問題を再現させるメールの作り方
code:send_jis_ng_mail.py
import base64
import email
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def make_message():
suzaki = b'''\x1b$B='!|!!??;R\x1b(B''' # 洲﨑神社のJISバイト列
b64name = base64.b64encode(suzaki).decode().strip('=')
mstr = f'=?ISO-2022-JP?B?{b64name}?=' # base64 MIME encoded
message = MIMEMultipart()
message'From' = 'shimizukawa@example.com' message'To' = 'shimizukawa@example.com' message'Subject' = f'{mstr} some string {mstr}' message.attach(MIMEText(suzaki, 'plain', 'iso2022_jp'))
attach = email.message.Message()
attach.add_header('Content-Type', 'application/octet-stream')
attach.add_header('Content-Transfer-Encoding', 'base64')
attach.set_payload(suzaki)
message.attach(attach)
return message
def send(message):
s = smtplib.SMTP('smtp.example.com')
s.send_message(message)
s.quit()
if __name__ == '__main__':
msg = make_message()
print(msg.as_string())
send(msg)
これで送信されるメールのテキストデータ
code:bash
$ python3 mailtest.py
Content-Type: multipart/mixed; boundary="===============2648188771522103711=="
MIME-Version: 1.0
From: shimizukawa@example.com
To: shimizukawa@example.com
Subject: =?ISO-2022-JP?B?GyRCPSchfCEhPz87UhsoQg?= some string =?ISO-2022-JP?B?GyRCPSchfCEhPz87UhsoQg?=
--===============2648188771522103711==
Content-Type: text/plain; charset="iso2022_jp"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
GyRCPSchfCEhPz87UhsoQg==
--===============2648188771522103711==
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="=?ISO-2022-JP?B?GyRCPSchfCEhPz87UhsoQg?=.txt"
B='!|!!??;R
--===============2648188771522103711==--