改訂版 Google App Engine(Python)でKay-Frameworkを使い、メールを受信する

以前に掲載した記事「Google App Engine(Python)でKay-Frameworkを使い、メールを受信する」をKayのメール受信用のハンドラークラスを使う方法に書き直しました。

プロジェクトの作成

サンプルのプロジェクトを作成します。
ここではアプリケーションIDを「myproject」としています。

python kay/manage.py startproject myproject
cd myproject

アプリケーションを作成します。

python manage.py startapp myapp

settings.pyを編集します。

settings.py

INSTALLED_APPS = (
  'myapp',  # 追加
)

APP_MOUNT_POINTS = {
  'myapp': '/',  # 追加
}

アプリケーションを動かし、設定が正しく行われているか確認します。
開発サーバーを起動します。

python manage.py runserver

http://localhost:8080/」にアクセスし、「Hello」と表示されることを確認します。

GAE にアップロードします。

python manage.py appcfg update

「http://アプリケーションID.appspot.com/」にアクセスして、「Hello」と表示されることを確認します。

※Kayの導入について詳しくはKay チュートリアルをご覧ください。

メールの受信設定

Kay-Frameworkでメールを受信できるように設定します。

app.yamlを編集します。

次の値を追加し、メール受信機能を有効にします。

app.yaml

inbound_services:
- mail

メール受信用ハンドラーを定義します。

myapp/views.py

import logging
from kay.handlers.mail import MailBaseHandler
class  MyMailHandler(MailBaseHandler):
    def receive(self, mail_message, address):
        '''メール受信処理
           @param mail_message: メールオブジェクト(google.appengine.api.mail.InboundEmailMessage)
           @param address: メールを受け取ったメールアドレス
        '''
        logging.debug('receiving_email')

urls.pyを編集し、「/_ah/mail/」へのリクエストに対して行う処理を設定します。

urls.py

view_groups = [
  ViewGroup(
    Rule('/_ah/queue/deferred', endpoint='deferred',
         view='kay.handlers.task.task_handler'),
    Rule('/maintenance_page', endpoint='_internal/maintenance_page',
         view='kay._internal.views.maintenance_page'),
    # 追加
    Rule('/_ah/mail/<address>', endpoint='receive_mail',
         view=('myapp.views.MyMailHandler', (), {})),
  )
]

以上で、メールを受信すると、myapp.views.MyMailHandler.receive()が呼び出されるようになりました。

認証機能

開発環境でメールの受信テストを行うためには、管理者権限が必要です。

そこで、Kay-Frameworkの認証機能を有効にします。

settings.py

INSTALLED_APPS = (
    'kay.auth', #追加
    'myapp',
)

MIDDLEWARE_CLASSES = (
    'kay.sessions.middleware.SessionMiddleware', #追加
    'kay.auth.middleware.AuthenticationMiddleware', #追加
)
AUTH_USER_BACKEND = 'kay.auth.backends.googleaccount.GoogleBackend' #追加
AUTH_USER_MODEL = 'kay.auth.models.GoogleUser' #追加

http://localhost:8080/」にアクセスしたときに、ログインフォームを表示するようにします。

admin_requiredデコレータでindexを修飾します。

myapp/views.py

from kay.auth.decorators import admin_required

@admin_required
def index(request):
    return render_to_response('myapp/index.html', {'message': 'Hello'})

http://localhost:8080/」にアクセスして、ログインフォームが表示されることを確認します。

メールの受信処理

開発環境でメールの受信処理を呼び出してみましょう。

開発サーバーを起動します。

python manage.py runserver

http://localhost:8080/_ah/admin/inboundmail」にアクセスして、メールを送ってみます。

receiving_email関数が呼ばれ、ログに「receiving_email」と出力されます。

次のようなエラーメッセージが表示された場合、管理者権限でログインしていないことが原因です。
http://localhost:8080/」にアクセスして、「Sign in as Administrator」をチェックしてログインしてください。

Message send failure
Current logged in user is not authorized to view this page

メールの解析

受信したメールを解析します。

メールアドレスの解析

メールアドレスを解析する関数を作成します。

myapp/views.py

from email.Utils import parseaddr, getaddresses
from email.Header import decode_header

def _parseaddress(message, field):
    '''sender,to,ccから名前とメールアドレスを取得するジェネレータ

       @param message: メールのメッセージオブジェクト(google.appengine.api.mail.InboundEmailMessage)
       @param field: フィールとを表す文字列(sender,to,cc)
       @return: (名前,メールアドレス)のtuple
    '''

    if hasattr(message, field):
        for (name, addr) in getaddresses([getattr(message, field)]):
            if name: #名前があればデコードする
                (name, charset) = decode_header(name)[0]
                if charset: #charsetがあればデコードする
                    name = name.decode(charset)
            yield (name, addr)

メールのFrom,To,Ccには複数のメールアドレスが登録されている場合があります。
この関数では、(名前,メールアドレス)のtupleをメールアドレスの数だけ返します。

このままでは使いにくいので、解析結果を文字列に変換する関数を作成します。

myapp/views.py

def _join_address(gen):
    '''_parseaddress関数で解析したメールアドレスを整形します。
       @param gen: _parseaddress
       @return: 名前<メールアドレス> の形に整形した文字列
    '''
    return u' '.join([u'%s<%s>' % addr for addr in gen])

これでメールアドレスを取得できるようになりました。

次のようにして取得することができます。

#送信者
sender = _join_address(_parseaddress(mail_message, 'sender'))
#宛先
to = _join_address(_parseaddress(mail_message, 'to'))
#CC
cc = _join_address(_parseaddress(mail_message, 'cc'))

_parseaddress関数と_join_address関数は、一つにまとめてもいいでしょう。

メールの件名の解析

メールの件名は、そのままではエンコードされていて読めません。
件名を解析する関数を作成します。

myapp/views.py

from email.Header import decode_header

def _parsesubject(message):
    '''デコードしたメールの件名を取得する
       @param message: メールのメッセージオブジェクト(google.appengine.api.mail.InboundEmailMessage)
       @return: デコードしたメールの件名
    '''

    if not hasattr(message, 'subject'): return u''
    (subject, charset) = decode_header(message.subject)[0]
    if not charset: charset = 'utf-8'
    return subject.decode(charset)

この関数を使用することで、メールの件名を取得できます。

#件名
subject = _parsesubject(mail_message)

メールの日付の解析

メールの日付はRFC 2822形式の文字列になっています。
このままでは扱いにくいですので、datetimeオブジェクトに変換する関数を作成します。

myapp/views.py

import datetime
from email.Utils import parsedate

def _parsedate(date):
    '''日付を解析してdatetimeオブジェクトを返す
       @param date: RFC 2822形式の日付を表す文字列
       @return: datetimeオブジェクト
    '''
    return datetime.datetime(*parsedate(date)[0:6])

次のようにして、メールの日付を表すdatetimeオブジェクトを取得できます。

#日付
date = _parsedate(mail_message.date)

メールの本文を取得する

メールの最初のテキストパートを取得する関数を作成します。

myapp/views.py

def _parsebody(message):
    '''メールの最初のテキストパートを取得する
    '''
    content_type, payload = message.bodies(content_type='text/plain').next()
    return payload.decode()

次のようにしてメールの本文を取得することができます。

#本文
body = _parsebody(mail_message)

HTMLメールが送られてきたときの対応は、今回は省略します。

メールの内容をデータストアに保存する

メールの内容をデータストアに保存するためのモデルを作成します。

myapp/models.py

from google.appengine.ext import db

class Email(db.Model):
    sender = db.StringProperty()
    to = db.StringProperty()
    cc = db.StringProperty()
    subject = db.StringProperty()
    date = db.DateTimeProperty()
    body = db.TextProperty()

受信したメールを解析して、データストアに保存する処理は次のようになります。

myapp/views.py

from google.appengine.api import mail
from myapp import models
from kay.handlers.mail import MailBaseHandler
class  MyMailHandler(MailBaseHandler):
    def receive(self, mail_message, address):
        '''メール受信処理
           @param mail_message: メールオブジェクト(google.appengine.api.mail.InboundEmailMessage)
           @param address: メールを受け取ったメールアドレス
        '''
        #送信者
        sender = _join_address(_parseaddress(mail_message, 'sender'))
        #宛先
        to = _join_address(_parseaddress(mail_message, 'to'))
        #CC
        cc = _join_address(_parseaddress(mail_message, 'cc'))
        #件名
        subject = _parsesubject(mail_message)
        #日付
        date = _parsedate(mail_message.date)
        #本文
        body = _parsebody(mail_message)

        models.Email(sender=sender,
                     to=to,
                     cc=cc,
                     subject=subject,
                     date=date,
                     body=body,
                     ).put()

開発サーバーで動作確認

開発環境でメールの受信処理を呼び出してみましょう。

開発サーバーを起動します。

python manage.py runserver

http://localhost:8080/_ah/admin/inboundmail」にアクセスして、メールを送ってみます。

メールを送信できたら、「http://localhost:8080/_ah/admin/datastore」にアクセスして、送信したメールの内容がデータストアに登録されているか確認します。

GAEで動作確認

GAE にアップロードします。

python manage.py appcfg update

それでは、メールを送信してみましょう。

メールアドレスは

適当な文字列@アプリケーションID.appspotmail.com

となります。

メールアドレスが見つからない旨のエラーメールがかえってきた場合は、メール受信機能が有効になっていません。
app.yamlの設定を確認してください。

送信したメールの内容がデータストアに登録されているか確認します。

最後に

メールを解析する関数は、google.appengine.api.mail.InboundEmailMessageのサブクラスを作り、そこに実装してもいいかもしれません。

今回は単純なテキストメールのみを扱い、HTMLメールや添付ファイル、携帯電話のメールの対応などは、扱いませんでした。
リクエストがあれば、続編を書くかもしれません。

以上で、App Engineでメールを受信する方法の説明を終わります。

不具合がありましたら、お知らせいただけると喜びます。

編集履歴

  • 2010年8月18日
    コメントで教えていただいたurls.pyの修正を行いました。

  • 2010年8月19日
    認証機能の設定で、settings.AUTH_USER_BACKENDとsettings.AUTH_USER_MODELの値を設定していなかった不具合を修正しました。

  • 2010年8月20日
    _parseaddress関数のメールアドレスの名前がエンコードされていないときにエラーになる不具合を修正しました。

関連ページ

コメント

  1. Pingback: Google App Engine(Python)でKay-Frameworkを使い、メールを受信する | 山本隆の開発日誌

  2. Pingback: Google App Engineに待望のメール受信機能 « 山本隆の開発日誌

  3. いつも参考にさせていただいています。どうもありがとうございます。

    >myapp/urls.pyを編集し、「/_ah/mail/」へのリクエストに対して行う処理を設定します。
    >myapp/urls.py

    とありますが、myappではなくmyproject/urls.pyではないでしょうか?
    初心者なもので勘違いかもしれません。

  4. コメントありがとうございます。
    ご指摘いただいた箇所を修正しました。
    今後ともよろしくお願いします。

  5. 度々申し訳ございません。
    本記事のソースを動かしてみると、開発環境では動作しましたが、本番環境の場合、
    TypeError: decode() argument 1 must be string, not None
    とエラーがでました。
    私のミスの可能性も高いのですが、ご報告まで。

    どうぞよろしくお願いします。

  6. ご指摘いただいたエラーを確認することが出来ませんでした。
    よろしければエラーが発生するメールを「yamamoto@gesource.jp」送っていただけると助かります。

    PayloadEncodingErrorなら確認できるのですが…。
    たとえばメールの本文中に機種依存文字があると、このエラーが発生します。

    記事中の_parsebody関数で、「payload.decode()」を読んでいるところがあります。

    def _parsebody(message):
      ”’メールの最初のテキストパートを取得する
      ”’
      content_type, payload = message.bodies(content_type=’text/plain’).next()
      return payload.decode() #<=ここ この「payload.decode()」はgoogle.appengine.api.mail.EncodedPayload.decode()です。 このメソッドの中でstring.decode()を呼んでいますが、半角カタカナ文字などがあるとデコードに失敗し、PayloadEncodingError例外が投げられます。 対策としては、charsetが'iso-2022-jp'のときは'iso-2022-jp-ext'を使うことで半角カタカナに対応します。 機種依存文字はあきらめて、string.decode()の引数にerrors='ignore'を使用します。 もっといい方法があれば教えてください。

  7. _parseaddress関数の不具合を修正しました。
    バグ報告ありがとうございました。

  8. こんにちは、

    「対策としては、charsetが’iso-2022-jp’のときは’iso-2022-jp-ext’を使うことで半角カタカナに対応します。
    機種依存文字はあきらめて、string.decode()の引数にerrors=’ignore’を使用します。」と教えましたので

    return payload.decode(‘iso-2022-jp-ext’)

    にすればエラーがなくなりますか。

    よろしくお願い申し上げます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください