Python3.0 の smtplib.py がそのままだと使えない

Python2.x だとうまくいくんだが、Python3.0 だとうまくいかないことがいくつか。

最終的な目標は

#!/usr/bin/env python3.0
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formatdate

e = 'iso-2022-jp'
m = MIMEText('日本語', 'plain', e)
m['Subject'] = Header('あいう', e)
m['To'] = 'to@example.com'
m['From'] = 'from@gmail.com'
m['Date'] = formatdate()

s = smtplib.SMTP('smtp.gmail.com', 587)
s.ehlo()
s.starttls()
s.ehlo()
s.login('from@gmail.com', 'google-password')
s.sendmail('from@gmail.com', 'to@example.com', m.as_string())
s.quit()

がちゃんと動くようになること。


まず、smtplib.SMTP.login が失敗して 502 Unrecognized command が返ってくる。
これは login 内で定義されている encode_plain で base64 エンコードしたときに、改行がついてしまうのが原因だ。
eol='' としてやることで、これを直すことができる。
さらに、encode_base64 *1 の第1引数は bytes 型なので encode() してやる必要がある。
SEE ALSO:


次に、日本語を含めたメールを smtplib.SMTP.sendmail で送ろうとすると UnicodeEncodeError で失敗する。
sendmail の処理はだいたい以下のような流れになっている。

  1. mail で MAIL コマンドを送る
  2. rcpt で RCPT コマンドを送る
  3. data で DATA コマンドを送る
    1. quotedata で . のクオートと改行コードの CRLF 化をする
    2. send でそれを送る

ここで問題なのは、 send は引数が str のインスタンスの場合、ascii 決め打ちで encode している点。
send の引数が str のインスタンスでない場合はそのまま送られるため、sendmail の引数をあらかじめ encode('iso-2022-jp') しておけばいいように思えるが、そうすると今度は quotedata で re.sub を使っており、re.sub の第3引数は str でなければならないため、bytes のインスタンスだと TypeError になってしまう。
なんというか設計が間違っているような気がしないでもない。
とりあえず日本語環境で動かすなら、iso-2022-jp 決め打ちでいける気がするので、そのように変更する。
SEE ALSO:


最終的に、smtplib.py はこのようになった。

--- smtplib.py.bak	2009-03-30 20:32:10.000000000 +0900
+++ smtplib.py	2009-03-30 21:18:20.000000000 +0900
@@ -302,7 +302,7 @@
         if self.debuglevel > 0: print('send:', repr(s), file=stderr)
         if hasattr(self, 'sock') and self.sock:
             if isinstance(s, str):
-                s = s.encode("ascii")
+                s = s.encode("iso-2022-jp")
             try:
                 self.sock.sendall(s)
             except socket.error:
@@ -542,7 +542,7 @@
             return encode_base64(response)
 
         def encode_plain(user, password):
-            return encode_base64("\0%s\0%s" % (user, password))
+            return encode_base64("\0{0}\0{1}".format(user, password).encode(), eol='')
 
 
         AUTH_PLAIN = "PLAIN"

encode_base64 のところでは、ついでに format を使うように変更してある。

*1:実体は email.base64mime.body_encode。内部で binascii.b2a_base64 を呼んでおり、それの第1引数が bytes 型