Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

Twistedでテスト用SMTPサーバーを書く

メールのテストとかってめんどい。
Mockとかそーいうのすらもめんどい。
もうサーバーを書いてしまった方がいいんじゃない?ってひと向け。
Twisted本持ってるひとには無用のエントリだね、そのまんまだから。

mailserver.py
from twisted.mail import smtp, maildir
from zope.interface import implements
from twisted.internet import protocol, reactor, defer

import os
from email.Header import Header

class MaildirMessageWriter(object):
    implements(smtp.IMessage)

    def __init__(self, user_dir):
        print "mailbox %s " % user_dir 
        if not os.path.exists(user_dir):
            os.mkdir(user_dir)
        inbox_dir = os.path.join(user_dir, 'Inbox')
        self.mailbox = maildir.MaildirMailbox(inbox_dir)
        self.lines = []

    def lineReceived(self, line):
        self.lines.append(line)

    def eomReceived(self):
        self.lines.append('')
        messge_data = '\n'.join(self.lines)
        return self.mailbox.appendMessage(messge_data)

    def connectionLost(self):
        del(self.lines)
       
class LocalDelivary(object):
    implements(smtp.IMessageDelivery)

    def __init__(self, base_dir, valid_domain):
        if not os.path.isdir(base_dir):
            raise ValueError, "%s is not dirctory"
        self.base_dir = base_dir
        self.valid_domain = valid_domain
    
    def receivedHeader(self, helo, origin, recipients):
        host_name, client_ip = helo
        header_value = "by %s from %s with ESMTP ; %s" % (host_name, 
                client_ip,
                smtp.rfc822date())
        return "Recived: %s " % Header(header_value) 

    def validateTo(self, user):
        if not user.dest.domain in self.valid_domain:
            raise smtp.SMTPBadRcpt()
        print "Accepting mail for %s " % user.dest
        return lambda:MaildirMessageWriter(
                self._get_addr_writer(str(user.dest)))

    def _get_addr_writer(self, address):
        return os.path.join(self.base_dir, "%s" % address)
    
    def validateFrom(self, helo, origin_address):
        return origin_address

class SMTPFactory(protocol.ServerFactory):
    
    def __init__(self, base_dir, valid_domain):
        self.base_dir = base_dir
        self.valid_domain = valid_domain

    def buildProtocol(self, addr):
        delivery = LocalDelivary(self.base_dir, self.valid_domain)
        smtp_protocol = smtp.SMTP(delivery)
        smtp_protocol.factory = self
        return smtp_protocol

if __name__ == '__main__':
    reactor.listenTCP(2555, SMTPFactory('/tmp', ['localhost']))
    from twisted.internet import ssl
    reactor.run()

このコードだけでもなんとなく流れがわかる。
ToをvalidateしてMessageを処理するってイメージ。

最初Growlに通知しようかと思ってたんだけどそんなに良くもなかったのでそのままファイルに書き出し。
newってフォルダ中になんとかIDみたいな(なんだっけ)のが付与されてるファイルが吐かれる。
こんな感じ

Recived: by [127.0.0.1] from 127.0.0.1 with ESMTP ; Thu, 16 Aug 2007 08:28:04 +0900 
Mime-Version: 1.0 (Apple Message framework v752.3)
Content-Transfer-Encoding: 7bit
Message-Id: <34D621AF-F079-49D3-A0E1-DBAEE64B61CA@gmail.com>
Content-Type: text/plain; charset=US-ASCII; format=flowed
To: test@localhost
From: =?ISO-2022-JP?B?GyRCPj44NhsoQiAbJEJLLRsoQg==?= <xxxxxxxxxx@gmail.com>
Subject: TEST
Date: Thu, 16 Aug 2007 08:28:04 +0900
X-Mailer: Apple Mail (2.752.3)

test mail

まあ見たらわかるけどeomReceivedのところをいじればいろいろできる。
今回はMaildirMailBoxに任せてるけど。
(こいつ自体も内部でFactoryから返ってくるクラスに委譲してるけど)
smtp、popとかその辺は全然見てなかったんだけどなかなか簡単にいろんなことできそう。
もっとカスタマイズしたいひとはsmtp以下のソース見てみるといいよ。


うくく。