Python でメールの送受信をするちょっとしたクライアントが欲しかったのですが、これぞ!というサンプルが無かったり、Python 2.6.2以前の書き方だったりしたので、自作してみました。
なお、「Python 2.6.2以前の書き方」というのは、こういうことです。
メールの送信方法としてはTLSを用いる。ここで、通信にSSLを使用する場合(後述)は、当然だがstarttlsは用いなくてよい。また、Pythonのバージョンによってstarttlsメソッドの前後でehloメソッドを使う必要がある。
まず、SMTPのSSLによる送信について、SMTP_SSLというクラスがPython 2.6で加えられたのだが、それは初期にはバグを含んでいたらしく、実質的にSMTP_SSLクラスが使えるようになったのはPython 2.6.3以降となる。よって、それ以前のバージョンの場合はTLSによる送信をしなければならない。また、SMTPオブジェクトのstarttlsメソッドについてだが、バージョン2.6より古い場合、starttlsメソッドの前後でehloメソッドを使用する必要があり、さらにそれが2.5系なのかさらにそれよりも古いのかによって場合分けする必要がある。
「プログラミングと慶應通信 : Pythonでgmailの送受信」より
やりたいこと
実装
email_client.py
# -*- coding: utf-8 -*- import re import time from getpass import getpass import email from email.header import decode_header from email.Header import Header from email.MIMEText import MIMEText from email import Utils from imaplib import IMAP4_SSL from smtplib import SMTP_SSL LOGIN_USERNAME = None LOGIN_PASSWORD = None class EmailClient(object): def __init__(self, user, password): self.user = user self.password = password self.smtp_host = 'smtp.gmail.com' self.smtp_port = 465 self.imap_host = 'imap.gmail.com' self.imap_port = 993 self.email_default_encoding = 'iso-2022-jp' self.timeout = 1 * 60 # sec def send_email(self, from_address, to_addresses, cc_addresses, bcc_addresses, subject, body): """ Send an email Args: to_addresses: must be a list cc_addresses: must be a list bcc_addresses: must be a list """ try: # Note: need Python 2.6.3 or more conn = SMTP_SSL(self.smtp_host, self.smtp_port) conn.login(self.user, self.password) msg = MIMEText(body, 'plain', self.email_default_encoding) msg['Subject'] = Header(subject, self.email_default_encoding) msg['From'] = from_address msg['To'] = ', '.join(to_addresses) if cc_addresses: msg['CC'] = ', '.join(cc_addresses) if bcc_addresses: msg['BCC'] = ', '.join(bcc_addresses) msg['Date'] = Utils.formatdate(localtime=True) # TODO: Attached file conn.sendmail(from_address, to_addresses, msg.as_string()) except: raise finally: conn.close() def get_email(self, from_address, subject_pattern=None): """ Get the latest unread email """ timeout = time.time() + self.timeout try: conn = IMAP4_SSL(self.imap_host, self.imap_port) while True: # Note: If you want to search unread emails, you should login after new emails are arrived conn.login(self.user, self.password) conn.list() conn.select('inbox') #typ, data = conn.search(None, 'ALL') #typ, data = conn.search(None, '(UNSEEN HEADER Subject "%s")' % subject) #typ, data = conn.search(None, '(ALL HEADER FROM "%s")' % from_address) # Search unread ones typ, data = conn.search(None, '(UNSEEN HEADER FROM "%s")' % from_address) ids = data[0].split() print "ids=%s" % ids # Search from backwards for id in ids[::-1]: typ, data = conn.fetch(id, '(RFC822)') raw_email = data[0][1] msg = email.message_from_string(raw_email) msg_subject = decode_header(msg.get('Subject'))[0][0] msg_encoding = decode_header(msg.get('Subject'))[0][1] or self.email_default_encoding if subject_pattern and re.match(subject_pattern, msg_subject.decode(msg_encoding)) is None: continue # TODO: Cannot use when maintype is 'multipart' return { 'from_address': msg.get('From'), 'to_addresses': msg.get('To'), 'cc_addresses': msg.get('CC'), 'bcc_addresses': msg.get('BCC'), 'date': msg.get('Date'), 'subject': msg_subject.decode(msg_encoding), 'body': msg.get_payload().decode(msg_encoding), # TODO: Attached file } if time.time() > timeout: raise Exception("Timeout!") time.sleep(5) except: raise finally: conn.close() conn.logout() if __name__ == "__main__": email_address = raw_input("Enter email address: ") if not LOGIN_USERNAME else LOGIN_USERNAME email_password = getpass("Enter email password: ") if not LOGIN_PASSWORD else LOGIN_PASSWORD email_client = EmailClient(email_address, email_password) # Send an email to myself email_client.send_email(email_address, [email_address], None, None, u"テスト", u"テストメールです") # Check the email email = email_client.get_email(email_address, u"テスト") print "from_address=%s" % email['from_address'] print "to_addresses=%s" % email['to_addresses'] print "cc_addresses=%s" % email['cc_addresses'] print "bcc_addresses=%s" % email['bcc_addresses'] print "date=%s" % email['date'] print "subject=%s" % email['subject'] print "body=%s" % email['body']
GitHub にもコードをアップしています。
https://github.com/akiyoko/python-email
メール送信の方はシンプルで分かりやすかったのですが、メール検索の方は結構ややこしかったです。
imaplib.IMAP4_SSL.search() の第二引数にいろいろな条件を指定してメールを検索できるのですが、search() の戻り値(の2番目)の data はスペース区切りの id の羅列(例えば「1 2 3」のような)になっていて、id は受信トレイにあるメールの連番になっているようです。その id を使って、imaplib.IMAP4_SSL.fetch() でメールの内容を取得することができます。
fetch() の戻り値(の2番目)の data は扱いにさらにクセがあります。上のコードを見てもらえば分かるように、変換やデコードを何回も繰り返してやっと目的のものが取得できています。
実行結果
$ python email_client.py Enter email address: xxxxx@gmail.com Enter email password: (password) ids=['2824'] from_address=xxxxx@gmail.com to_addresses=xxxxx@gmail.com cc_addresses=None bcc_addresses=None date=Sat, 28 Jun 2014 12:24:19 +0900 subject=テスト body=テストメールです
何度も言いますが、Gmail でしかテストしていませんのでご注意を。
事前準備(Gmail の設定)
なお、事前準備として、Gmail で IMAP を有効にするために、Gmail の設定をする必要があります。Gmail の設定は、右上の歯車のアイコンをクリックして変更することができます。
参考
メール送信
メール受信
- http://yuji.wordpress.com/2011/06/22/python-imaplib-imap-example-with-gmail/
- http://symfoware.blog68.fc2.com/blog-entry-891.html
- http://mediawiki.tuntunkun.com/index.php/Imaplib_Gmail%E3%81%8B%E3%82%89%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B
- http://www.zumwalt.info/blog/2013/11/python-imap/