Symfoware

Symfowareについての考察blog

Python デバッグ用のSMTPサーバーを作成(SocketServer)

Pythonのsmtpdモジュールを使用して、SMTPデバッグサーバーを起動してみました。
Python デバッグ用のSMTPサーバーを起動する


困ったことがあって、CodeIgniter 3のメールライブラリから送信できないんです。
Email Class


こんなサンプルコードで試しましたが、反応してくれません。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index(){
  4.         
  5.         // メール送信用のライブラリをロード
  6.         $this->load->library('email');
  7.         
  8.         // メール送信用の設定
  9.         $config['protocol'] = 'smtp';
  10.         $config['smtp_host'] = 'localhost';
  11.         $config['smtp_port'] = 1025;
  12.         // 設定を反映
  13.         $this->email->initialize($config);
  14.         // 送信元
  15.         $this->email->from('your@example.com', 'Your Name');
  16.         // 送信先
  17.         $this->email->to('someone@example.com');
  18.         // メールのタイトルと本文
  19.         $this->email->subject('Email Test');
  20.         $this->email->message('Testing the email class.');
  21.         
  22.         // 送信実行
  23.         echo $this->email->send();
  24.         
  25.         echo 'fin'.PHP_EOL;
  26.     }
  27. }






SocketServer



添付ファイルのない、プレーンなテキストメールのテストが行えれば良いので、
自分でSMTPサーバーを書いてみます。

参考にしたのはこちら。
SocketServer — ネットワークサーバ構築のためのフレームワーク
SocketServerモジュールを使ってTCPサーバを書く

最初のコードはこうなりました。


  1. # -*- coding:utf-8 -*-
  2. import SocketServer
  3. class MyTCPHandler(SocketServer.StreamRequestHandler):
  4.     
  5.     def handle(self):
  6.         
  7.         while True:
  8.             self.data = self.rfile.readline().strip().upper()
  9.             print self.data
  10.             
  11.             if self.data == 'QUIT':
  12.                 self.request.send('221 Bye\r\n')
  13.                 break
  14.             
  15.             if self.data == 'DATA':
  16.                 self.request.send('354 End data with <CR><LF>.<CR><LF>\r\n')
  17.                 continue
  18.             
  19.             if self.data == '.':
  20.                 pass
  21.             
  22.             self.request.send('250 OK\r\n')
  23.         
  24.         self.request.close()
  25.         
  26.         
  27. if __name__ == "__main__":
  28.     
  29.     server = SocketServer.TCPServer(('0.0.0.0', 1025), MyTCPHandler)
  30.     server.serve_forever()




CodeIgniterのプログラムを実行してみます。


$ php index.php test




狙い通りの応答がありました。


$ python fake_smtp.py
EHLO [127.0.0.1]
MAIL FROM:<YOUR@EXAMPLE.COM>
RCPT TO:<SOMEONE@EXAMPLE.COM>
DATA
USER-AGENT: CODEIGNITER
DATE: WED, 30 DEC 2015 22:07:07 +0900
FROM: "YOUR NAME" <YOUR@EXAMPLE.COM>
RETURN-PATH: <YOUR@EXAMPLE.COM>
TO: SOMEONE@EXAMPLE.COM
SUBJECT: =?UTF-8?Q?EMAIL=20TEST?=
REPLY-TO: "YOUR@EXAMPLE.COM" <YOUR@EXAMPLE.COM>
X-SENDER: YOUR@EXAMPLE.COM
X-MAILER: CODEIGNITER
X-PRIORITY: 3 (NORMAL)
MESSAGE-ID: <5683D6FBD1E36@EXAMPLE.COM>
MIME-VERSION: 1.0
CONTENT-TYPE: TEXT/PLAIN; CHARSET=UTF-8
CONTENT-TRANSFER-ENCODING: 8BIT

TESTING THE EMAIL CLASS.

.
QUIT










もう少し改良



もう少し真面目にコマンドの内容を分類します。


  1. # -*- coding:utf-8 -*-
  2. import SocketServer
  3. class MyTCPHandler(SocketServer.StreamRequestHandler):
  4.     
  5.     def handle(self):
  6.         
  7.         self._from = ''
  8.         self._to = ''
  9.         self._data = ''
  10.         is_data = False
  11.         
  12.         while True:
  13.             cmd_line = self.rfile.readline().strip()
  14.             
  15.             if is_data:
  16.                 if cmd_line == '.': # データの終了
  17.                     is_data = False
  18.                 else: # データの内容退避
  19.                     self._data += cmd_line + '\r\n'
  20.                     continue
  21.             
  22.             cmds = cmd_line.split(':')
  23.             cmd = cmds[0].upper()
  24.             
  25.             if cmd == 'MAIL FROM':
  26.                 self._from = cmds[1]
  27.                 
  28.             elif cmd == 'RCPT TO':
  29.                 self._to = cmds[1]
  30.                 
  31.             elif cmd == 'QUIT':
  32.                 self.request.send('221 Bye\r\n')
  33.                 break
  34.             
  35.             elif cmd == 'DATA':
  36.                 self.request.send('354 End data with <CR><LF>.<CR><LF>\r\n')
  37.                 is_data = True
  38.                 continue
  39.             
  40.             self.request.send('250 OK\r\n')
  41.         
  42.         self.request.close()
  43.         
  44.         print self._from
  45.         print self._to
  46.         print self._data
  47.         
  48.         
  49. if __name__ == "__main__":
  50.     
  51.     server = SocketServer.TCPServer(('0.0.0.0', 1025), MyTCPHandler)
  52.     server.serve_forever()




だいぶいい感じになってきました。


  1. $ python fake_smtp.py
  2. <your@example.com>
  3. <someone@example.com>
  4. User-Agent: CodeIgniter
  5. Date: Wed, 30 Dec 2015 22:20:22 +0900
  6. From: "Your Name" <your@example.com>
  7. Return-Path: <your@example.com>
  8. To: someone@example.com
  9. Subject: =?UTF-8?Q?Email=20Test?=
  10. Reply-To: "your@example.com" <your@example.com>
  11. X-Sender: your@example.com
  12. X-Mailer: CodeIgniter
  13. X-Priority: 3 (Normal)
  14. Message-ID: <5683da16173f1@example.com>
  15. Mime-Version: 1.0
  16. Content-Type: text/plain; charset=UTF-8
  17. Content-Transfer-Encoding: 8bit
  18. Testing the email class.







本文の解析を行う



メール本文の解析も行ってみます。
以前書いた処理を参考にしました。
http://symfoware.blog68.fc2.com/blog-entry-892.html


  1. # -*- coding:utf-8 -*-
  2. import SocketServer
  3. import email,email.Header
  4. class MyTCPHandler(SocketServer.StreamRequestHandler):
  5.     
  6.     def handle(self):
  7.         
  8.         self._data = ''
  9.         is_data = False
  10.         
  11.         while True:
  12.             cmd_line = self.rfile.readline().strip()
  13.             
  14.             if is_data:
  15.                 if cmd_line == '.': # データの終了
  16.                     is_data = False
  17.                 else: # データの内容退避
  18.                     self._data += cmd_line + '\r\n'
  19.                     continue
  20.             
  21.             cmds = cmd_line.split(':')
  22.             cmd = cmds[0].upper()
  23.             
  24.             """ ここは捨てても、メッセージボディーから拾える
  25.             if cmd == 'MAIL FROM':
  26.                 self._from = cmds[1]
  27.                 
  28.             elif cmd == 'RCPT TO':
  29.                 self._to = cmds[1]
  30.             """
  31.             if cmd == 'QUIT':
  32.                 self.request.send('221 Bye\r\n')
  33.                 break
  34.             
  35.             elif cmd == 'DATA':
  36.                 self.request.send('354 End data with <CR><LF>.<CR><LF>\r\n')
  37.                 is_data = True
  38.                 continue
  39.             
  40.             self.request.send('250 OK\r\n')
  41.         
  42.         self.request.close()
  43.         
  44.         #メッセージをパース
  45.         msg = email.message_from_string(self._data)
  46.         
  47.         #タイトル取得
  48.         print self.decode(msg.get('Subject'))
  49.         # 送信者取得
  50.         print self.decode(msg.get('From'))
  51.         # 受信者取得
  52.         print self.decode(msg.get('To'))
  53.         
  54.         for part in msg.walk():
  55.             if part.get_content_maintype() == 'multipart':
  56.                 continue
  57.                 
  58.             #ファイル名を取得
  59.             filename = part.get_filename()
  60.             
  61.             #ファイル名が取得できなければ本文
  62.             if not filename:
  63.                 print self.decode_body(part)
  64.         
  65.         
  66.         
  67.     def decode(self, dec_target):
  68.         """
  69.         メールタイトル、送信者のデコード
  70.         """
  71.         decodefrag = email.Header.decode_header(dec_target)
  72.         title = ''
  73.         
  74.         for frag, enc in decodefrag:
  75.             if enc:
  76.                 title += unicode(frag, enc)
  77.             else:
  78.                 title += unicode(frag)
  79.         
  80.         return title
  81.         
  82.     
  83.     def decode_body(self, part):
  84.         """
  85.         メール本文のデコード
  86.         """
  87.         body = ''
  88.         charset = str(part.get_content_charset())
  89.         
  90.         if charset:
  91.             body = unicode(part.get_payload(), charset)
  92.             
  93.         else:
  94.             body = part.get_payload()
  95.         
  96.         return body
  97.         
  98.         
  99. if __name__ == "__main__":
  100.     
  101.     server = SocketServer.TCPServer(('0.0.0.0', 1025), MyTCPHandler)
  102.     server.serve_forever()




送信元のPHP + CodeIgniter 3プログラムも若干手を加えてみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index(){
  4.         
  5.         // メール送信用のライブラリをロード
  6.         $this->load->library('email');
  7.         
  8.         // メール送信用の設定
  9.         $config['protocol'] = 'smtp';
  10.         $config['smtp_host'] = 'localhost';
  11.         $config['smtp_port'] = 1025;
  12.         // 設定を反映
  13.         $this->email->initialize($config);
  14.         // 送信元
  15.         $this->email->from('your@example.com', '日本で送信者');
  16.         // 送信先
  17.         $this->email->to('someone@example.com');
  18.         // メールのタイトルと本文
  19.         $this->email->subject('日本語のタイトル');
  20.         $this->email->message('日本語の本文');
  21.         
  22.         // 送信実行
  23.         echo $this->email->send();
  24.         
  25.         echo 'fin'.PHP_EOL;
  26.     }
  27. }




実行すると狙い通りの結果が表示されました。


$ python fake_smtp.py
日本語のタイトル
日本で送信者<your@example.com>
someone@example.com
日本語の本文








メールの送信がおそい



うまく動いて喜んでいたのですが、どうもメールの送信が遅い。
調べてみると、Emailクラスの1972行目付近で時間がかかっています。


  1.     protected function _smtp_connect()
  2.     {
  3.         if (is_resource($this->_smtp_connect))
  4.         {
  5.             return TRUE;
  6.         }
  7.         $ssl = ($this->smtp_crypto === 'ssl') ? 'ssl://' : '';
  8.         $this->_smtp_connect = fsockopen($ssl.$this->smtp_host,
  9.                             $this->smtp_port,
  10.                             $errno,
  11.                             $errstr,
  12.                             $this->smtp_timeout);
  13.         if ( ! is_resource($this->_smtp_connect))
  14.         {
  15.             $this->_set_error_message('lang:email_smtp_error', $errno.' '.$errstr);
  16.             return FALSE;
  17.         }
  18.         stream_set_timeout($this->_smtp_connect, $this->smtp_timeout);
  19.         // ここの処理が遅い
  20.         $this->_set_error_message($this->_get_smtp_data());
  21.         if ($this->smtp_crypto === 'tls')
  22.         {
  23.             $this->_send_command('hello');
  24.             $this->_send_command('starttls');
  25.             $crypto = stream_socket_enable_crypto($this->_smtp_connect, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  26.             if ($crypto !== TRUE)
  27.             {
  28.                 $this->_set_error_message('lang:email_smtp_error', $this->_get_smtp_data());
  29.                 return FALSE;
  30.             }
  31.         }
  32.         return $this->_send_command('hello');
  33.     }




どうやら、最初に接続した時に表示される220応答を待っている模様。
http://ash.jp/net/telnet_smtp.htm

接続時、適当なメッセージを返すよう修正します。


  1. # -*- coding:utf-8 -*-
  2. import SocketServer
  3. import email,email.Header
  4. class MyTCPHandler(SocketServer.StreamRequestHandler):
  5.     
  6.     def handle(self):
  7.         
  8.         self._data = ''
  9.         is_data = False
  10.         
  11.         # 接続時の応答メッセージ
  12.         self.request.send('220 Python FakeSMTP version 0.1\r\n')
  13.         
  14.         while True:
  15.             cmd_line = self.rfile.readline().strip()
  16.             
  17.             if is_data:
  18.                 if cmd_line == '.': # データの終了
  19.                     is_data = False
  20.                     self.send_mail(self._data)
  21.                     self._data = ''
  22.                     
  23.                 else: # データの内容退避
  24.                     self._data += cmd_line + '\r\n'
  25.                     continue
  26.             
  27.             cmds = cmd_line.split(':')
  28.             cmd = cmds[0].upper()
  29.             
  30.             if cmd == 'QUIT':
  31.                 self.request.send('221 Bye\r\n')
  32.                 break
  33.             
  34.             elif cmd == 'DATA':
  35.                 self.request.send('354 End data with <CR><LF>.<CR><LF>\r\n')
  36.                 is_data = True
  37.                 continue
  38.             
  39.             self.request.send('250 OK\r\n')
  40.         
  41.         self.request.close()
  42.         
  43.     def send_mail(self, data):
  44.         
  45.         #メッセージをパース
  46.         msg = email.message_from_string(data)
  47.         
  48.         #タイトル取得
  49.         print self.decode(msg.get('Subject'))
  50.         # 送信者取得
  51.         print self.decode(msg.get('From'))
  52.         # 受信者取得
  53.         print self.decode(msg.get('To'))
  54.         
  55.         for part in msg.walk():
  56.             if part.get_content_maintype() == 'multipart':
  57.                 continue
  58.                 
  59.             #ファイル名を取得
  60.             filename = part.get_filename()
  61.             
  62.             #ファイル名が取得できなければ本文
  63.             if not filename:
  64.                 print self.decode_body(part)
  65.         
  66.         
  67.         
  68.     def decode(self, dec_target):
  69.         """
  70.         メールタイトル、送信者のデコード
  71.         """
  72.         decodefrag = email.Header.decode_header(dec_target)
  73.         title = ''
  74.         
  75.         for frag, enc in decodefrag:
  76.             if enc:
  77.                 title += unicode(frag, enc)
  78.             else:
  79.                 title += unicode(frag)
  80.         
  81.         return title
  82.         
  83.     
  84.     def decode_body(self, part):
  85.         """
  86.         メール本文のデコード
  87.         """
  88.         body = ''
  89.         charset = str(part.get_content_charset())
  90.         
  91.         if charset:
  92.             body = unicode(part.get_payload(), charset)
  93.             
  94.         else:
  95.             body = part.get_payload()
  96.         
  97.         return body
  98.         
  99.         
  100. if __name__ == "__main__":
  101.     
  102.     server = SocketServer.TCPServer(('0.0.0.0', 1025), MyTCPHandler)
  103.     server.serve_forever()




これでCodeIgniterでも使用できる高速なデバッグSMTPサーバーが出来ました。
関連記事

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2015/12/30(水) 23:11:23|
  2. Python
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集
<<Python mechanizeでブラウザ操作をエミュレート | ホーム | Python デバッグ用のSMTPサーバーを起動する>>

コメント

コメントの投稿


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://symfoware.blog68.fc2.com/tb.php/1824-c989a5f6
この記事にトラックバックする(FC2ブログユーザー)