読者です 読者をやめる 読者になる 読者になる

manage.pyのアクションを自作する

Django

symfonyのプロジェクトにはバッチスクリプトの作成を支援する機能があるみたい?なので、前々からこれだけはいいなぁと思っていたら、Djangoにもいつの間にか同様の機能ができていた。

この機能を使って、Postfixで、よくある「携帯の空メール受信処理」を実装してみる。

モジュール構成

アプリケーションディレクトリにmanagement/commandsというディレクトリを作って、そこに「アクション名.py」という名前のファイルを作る。

ここでは、myvideo.videoというアプリケーションにregisteruserというアクションを作るというケースを考える。まず、モジュール構成を整える。

$ pwd
/home/perezvon/myvide
$ mkdir -p management/commands
$ touch management/__init__.py
$ touch management/commands/__init__.py

結果、次のようなディレクトリ、ファイル構成になる。

+ myvideo
++ video
+++ management
++++ __init__.py
++++ commands
+++++ __init__.py
+++++ registeruser.py (これから作成するファイル)

Commandクラスを定義する

registeruser.pyで、django.core.management.base.BaseCommandを継承するCommandクラスを定義する。クラス名は必ずCommandでなくてはならない。

from django.core.mangement.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **opts):
        # ここに処理を書く
        pass

handleメソッドの*argsは、スクリプト実行時にコマンドラインから渡される引数。もし引数をとらないスクリプトならば、django.core.mangement.base.NoArgsCommandを継承してもよい。その場合は、handle_noargsメソッドをオーバーライドする。

from django.core.mangement.base import NoArgsCommand

class Command(NoArgsCommand):
    def handle(self, *args, **opts):
        # ここに処理を書く
        pass

今回はこちらのNoArgsCommandを使う。

実例

次のコードは、「空メールをPostfixで受け取り、そのメールアドレスが未登録であれば、仮登録を行ってユーザにメールを送り返す」という処理の一部。

# -*- coding: utf-8 -*-
import sys
from email import message_from_file

from django.core.mail import EmailMessage
from django.core.management.base import NoArgsCommand
from django.contrib.auth.models import User

class Command(NoArgsCommand):
    def handle_noargs(self, **opts):
        message = message_from_file(sys.stdin)
        # Envelope Fromからメールアドレスを取得
        addr = os.environ['SENDER']
        try:
            user = User.objects.get(email=addr)
        except User.DoesNotExist:
            # ここでメールアドレスの登録処理をする

            # 処理が完了したならば、送信元にメールを送り返す
            EmailMessage("subject", "body", to=[addr]).send()

もしEnvelope Fromではなく、Header Fromからアドレスを取得したいならば、

addr = os.environ['SENER']

ではなく、

addr = message['From']

とする。os.environ['SENDER']はpostfixでlocal配信エージェント経由でコマンドを実行した場合の例。

登録処理の部分はいろいろなパターンが考えられるので省略。ただ、たいていは次の二つのパターンのどちらかだろう。

  • 先に空メールを送信して、本登録用の一時URLを送信する。
  • 先にユーザ情報を登録して、最後に空メールを送信させて、ユーザを有効にする。

上の例では、前者のパターンで考えている。ただ、Djangoの標準のユーザモデルを利用するならば、後者のパターンの方が楽かもしれない。

Postfixの設定を行う

/etc/myvideoのようなディレクトリを作り、ここにエイリアスファイルを置く。
/etc/myvideo/aliasesの内容は次のようにした。

register: "|/home/perezvon/myvideo/manage.py registeruser --pythonpath=/home/perezvon"

register@example.com等にメールが届いた時に、manage.py registeruserが実行されるようにする。

postaliasを実行。

$ sudo /usr/sbin/postalias /etc/myvideo/aliases

main.cfを設定して、このaliasesを有効にする。

alias_maps = hash:/etc/aliases
  hash:/etc/myvideo/aliases

alias_database = hash:/etc/aliases
  hash:/etc/myvideo/aliases

PYTHON_EGG_CACHEの設定

他の言語だと、上記の手順を終えれば、コマンド配信はできるはずだが、Pythonの場合はもう一つ設定が必要な場合がある。それが、PYTHON_EGG_CACHE。

スクリプトの実行ユーザが、PYTHON_EGG_CACHEで指定したディレクトリに対する書き込み権限を持たない場合、次のようなエラーが出てegg形式のモジュールのインポートができない。

[Errno 13] Permission denied:
'/.python-eggs' The Python egg cache directory is currently set to:
/.python-eggs Perhaps your account does not have write access to this
directory? You can change the cache directory by setting the
PYTHON_EGG_CACHE environment variable to point to an accessible directory.

Postfixの場合は、main.cfのexport_environmentで環境変数で設定できるので、コマンドの実行ユーザ(通常はnobody:nobody)が書き込める場所を指定する。

export_environment = PYTHON_EGG_CACHE=/tmp/postfix/.python-egg

以上の設定をしたら、Postfixの設定をリロード。

$ sudo /sbin/service postfix reload

コマンド配信が上手く動いていれば、

$ echo test | mail register@example.com

のようにメールを送信した時に、ログ(/var/log/maillog)に

Apr  2 16:04:54 u3 postfix/local[12856]: 73206486F65: to=<register@example.com>, relay=local, delay=1.3, delays=0.77/0.01/0/0.54,
dsn=2.0.0, status=sent (delivered to command: /home/perezvon/myvideo/manage.py registeruser --pythonpath=/home/perezvon)

のような出力が出るはずである。

余談

Djangoのドキュメントの説明だと、上記のaliasesは、

register: "|/usr/bin/django-admin.py registeruser --settings=myvideo.settings"

のように書けそうなものだが、django-admin.py経由だと自作アクションを読み込んでくれないようだ。この形式の方がデプロイが楽なので、この形式で使えることを期待していたのだが・・・