Windowsで動くウェブ開発専用のSMTP・POPサーバ

Apache Jamesを使って、開発に都合のいいSMTP・POPサーバを構築します。
screenshot


0. この記事を書くに至るまでの経緯、とか。

開発用メールサーバに欲しい機能として、以下の4つがあげられます。

  1. メールが外に漏れない。
  2. スタンドアローンWindowsで動く。
  3. あらゆるアドレスに送ったメールが1つのPOPアカウントから見える。
  4. 届いたメールがウェブから見える。

1。メールを外に送出しない設定ができること。ウェブアプリケーションの開発に、リアルワールドに直結したメールサーバを使っていると、ちょっとしたミスで、外に出してはいけないメールを送ってしまう可能性があります。いままでメールの操作で何度かひどいミスをしているので(@の前に自分の苗字を書くつもりで名前を書いたりとか……)、なんとかしたいと思っていました。

2。ローカルのWindows上で動いてほしい。今年に入って開発環境をノートPCに移したため、ネットワークにつながらない環境で開発を行う機会が増えました。「会員登録」のような、外のメールサーバにつなぎにいく処理でネットにつながらないと、メールに書かれた内容を確認するためにログの設定を変えたり、DBの中身を見たりと、非常に面倒です。

3。全メールを1つのinboxにフォワードしてほしい。複数のテスト用アドレスを準備しても受信するのが面倒だとだんだん1つの(しかも日常使っている)アドレスしか使わなくなったりします。きちんとテストするために、アドレスは何でも使えて、受信も簡単な環境にしたいと思っていました。

4。Seleniumでテストしたい。たぶんブラウザで動くPOPメーラかなんかが使えるだろうと思います。Seleniumで動かすには同じドメインに入っている必要があるので、Gmailでというわけにはいきません。自動テストにはシンプルなインタフェースの方がいいので、自作するといいかもしれません。


とりあえず最後のは置いといて(まだ探してません)、上の3つはApache Jamesを使うことで解決できました。以下、インストール手順の紹介です。Windowsでしか試していませんが、Javaが動けばどのような環境でも使えると思います。


1. ローカルホストにホスト名を設定する

user@localhost のようなメールアドレスは、アプリケーションのバリデーションを通らない可能性があります。例えばCommons Validatorに含まれるEmailValidatorの場合、「user@[127.0.0.1]」は通しますが「user@localhost」は通しません*1。これでは動作テストには使用できません。

この問題を手っ取り早く回避するため、hostsファイルを書き換えてローカルホストにホスト名を設定します。自分は、以下の投稿などを参考に、ローカルホスト用のTLDとして'.local'を使うことにしました。

今のところ、プライベート IP アドレスと類似の、LAN 上で安心して使えるドメイン名はありません。私が使っている .localは draft-ietf-dnsind-local-names で提案されていたものですが、このドラフトは現在は期限切れで削除されています。

期限切れのドラフトは以下にあります。

というわけで、進行中のプロジェクトには「(プロジェクト名).local」を使用し、メールアドレスや、プロジェクト共通で使うウェブアプリケーション(phpMyAdminとか)には「default.local」を使うことにしました。

  • C:\WINDOWS\system32\drivers\etc\hosts
127.0.0.1       default.local


2. Apache Jamesをダウンロードして起動する

でとりあえずは動きます。

Phoenix 4.0.1

James 2.2.0
Remote Manager Service started plain:4555
POP3 Service started plain:110
SMTP Service started plain:25
NNTP Service started plain:119
Fetch POP Disabled
FetchMail Disabled


3. Jamesの管理サービスにtelnetで接続してユーザを追加する

telnetlocalhostポート4555に接続します。id:root / PASS:root。

C:\> telnet localhost 4555
JAMES Remote Administration Tool 2.2.0
Please enter your login and password
Login id:
root
Password:
root
Welcome root. HELP for a list of commands
help
Currently implemented commands:
help                                    display this help
listusers                               display existing accounts
countusers                              display the number of existing accounts
adduser [username] [password]           add a new user
verify [username]                       verify if specified user exist
deluser [username]                      delete existing user
setpassword [username] [password]       sets a user's password
setalias [user] [alias]                 locally forwards all email for 'user' to
 'alias'
showalias [username]                    shows a user's current email alias
unsetalias [user]                       unsets an alias for 'user'
setforwarding [username] [emailaddress] forwards a user's email to another email
 address
showforwarding [username]               shows a user's current email forwarding
unsetforwarding [username]              removes a forward
user [repositoryname]                   change to another user repository
shutdown                                kills the current JVM (convenient when J
ames is run as a daemon)
quit                                    close connection

# ユーザを作成します。
add user test1 test
User test1 added
add user test2 test
User test2 added
add user test3 test
User test3 added
quit
Bye


4. 設定ファイルを修正

初回の起動で、設定ファイルが './apps/james/SAR-INF' 以下に展開されています。一度起動しないと設定ファイルは生成されないことに注意してください。設定を変える前にconfig.xmlのバックアップをしておきます。

  • ./apps/james/SAR-INF/config.xml → config.xml.dist等にバックアップ

config.xmlに、以下の修正を加えます。

上でhostsファイルに設定したホスト名をservernameに設定。

<servername>localhost</servername>
↓
<servername>default.local</servername>

存在しないユーザ名でもエラーにしないよう、以下を削除。

<mailet match="HostIsLocal" class="ToProcessor">
   <processor> local-address-error </processor>
   <notice>550 - Requested action not taken: no such user here</notice>
</mailet>

リモートに送信しないよう、以下を削除。

<mailet match="All" class="RemoteDelivery">
   <outgoing> file://var/mail/outgoing/ </outgoing>
   <!-- alternative database repository example below -->
   :
   (略)
   :
</mailet>

同じ位置に、以下を追加。

<mailet match="All" class="Forward">
   <forwardTo>test1@default.local</forwardTo>
</mailet>

これで存在しないローカルアドレスと、全リモートアドレスが test1@default.local にフォワードされます。

Control-CでJamesを止めて、run.batで再度起動して下さい。

これで、

  • test1@default.local
  • test2@default.local
  • test3@default.local

のアドレスが使えるようになっています。

SMTPをdefault.localに設定してtest1@default.localにメールを送信、POPサーバにtest1/testで接続してメールが受信できることを確認してください。同様にtest2、test3の送受信ができます。

また、ユーザを作成したtest1〜3以外のあらゆるアドレスにメールを送信した場合は、test1にメールがフォワードされます。確認してみてください。


5. 設定ファイルをダイエット

配布されている巨大なconfig.xmlから、コメントや開発に不要そうなスパムブラックリストの設定などを取り除いた設定を以下に示します。このまま使う場合は、assembly.xmlから、NNTP Server、NNTP Repository、FetchPOP Service、FetchMail Serviceをコメントアウトする必要があります。

<?xml version="1.0"?>
<!DOCTYPE config [
<!ENTITY listserverConfig SYSTEM "../conf/james-listmanager.xml">
<!ENTITY listserverStores SYSTEM "../conf/james-liststores.xml">
<!ENTITY fetchmailConfig SYSTEM "../conf/james-fetchmail.xml">
]>

<config>
   <James>
      <postmaster>Postmaster@default.local</postmaster>
      <servernames autodetect="true" autodetectIP="true">
         <servername>default.local</servername>
      </servernames>

      <usernames ignoreCase="true" enableAliases="true" enableForwarding="true"/>

      <inboxRepository>
         <repository destinationURL="file://var/mail/inboxes/" type="MAIL"/>
      </inboxRepository>

   </James>

   <spoolmanager>
      <threads> 10 </threads>

      <mailetpackages>
         <mailetpackage>org.apache.james.transport.mailets</mailetpackage>
      </mailetpackages>
      <matcherpackages>
         <matcherpackage>org.apache.james.transport.matchers</matcherpackage>
      </matcherpackages>

      <processor name="root">
         <mailet match="RelayLimit=30" class="Null"/>
         <mailet match="All" class="ToProcessor">
            <processor> transport </processor>
         </mailet>
      </processor>

      <processor name="transport">
         <mailet match="RecipientIsLocal" class="LocalDelivery"/>

         <mailet match="RemoteAddrNotInNetwork=127.0.0.1" class="ToProcessor">
            <processor> relay-denied </processor>
            <notice>550 - Requested action not taken: relaying denied</notice>
         </mailet>

         <!-- 外に送信する場合 -->
         <!--
         <mailet match="All" class="RemoteDelivery">
            <outgoing> file://var/mail/outgoing/ </outgoing>
            <delayTime datetime="2006-07-30T05:23:56+09:00">  5 minutes </delayTime>
            <delayTime datetime="2006-07-30T05:23:56+09:00"> 10 minutes </delayTime>
            <delayTime datetime="2006-07-30T05:23:56+09:00"> 45 minutes </delayTime>
            <delayTime datetime="2006-07-30T05:23:56+09:00">  2 hours </delayTime>
            <delayTime datetime="2006-07-30T05:23:56+09:00">  3 hours </delayTime>
            <delayTime datetime="2006-07-30T05:23:56+09:00">  6 hours </delayTime>
            <maxRetries> 25 </maxRetries>
            <deliveryThreads datetime="2006-07-30T05:23:56+09:00"> 1 </deliveryThreads>
            <sendpartial>false</sendpartial>
         </mailet>
         -->

         <!-- バウンスメールの実験用。bounce_で始まるアドレスがバウンスする。-->
         <!--
	 <mailet match="RecipientIsRegex=bounce_.+" class="Bounce">
            <message>bounce mail test</message>
	    <passThrough>false</passThrough>
	 </mailet>
         -->

         <mailet match="All" class="Forward">
            <forwardTo>test1@default.local</forwardTo>
         </mailet>
      </processor>

      <processor name="error">
         <mailet match="All" class="ToRepository">
            <repositoryPath>file://var/mail/error/</repositoryPath>
         </mailet>
      </processor>

      <processor name="relay-denied">
         <mailet match="All" class="ToRepository">
            <repositoryPath>file://var/mail/relay-denied/</repositoryPath>
         </mailet>
      </processor>

   </spoolmanager>

   <dnsserver>
      <servers></servers>
      <autodiscover>false</autodiscover>
      <authoritative>false</authoritative>
   </dnsserver>

   <remotemanager>
      <port>4555</port>
      <handler>
         <helloName autodetect="true">myMailServer</helloName>
         <administrator_accounts>
            <account login="root" password="root"/>
         </administrator_accounts>
         <connectiontimeout> 60000 </connectiontimeout>
      </handler>
   </remotemanager>

   <pop3server enabled="true">
      <port>110</port>
      <handler>
         <helloName autodetect="true">myMailServer</helloName>
         <connectiontimeout>120000</connectiontimeout>
      </handler>
   </pop3server>

   <smtpserver enabled="true">
      <port>25</port>
      <handler>
         <helloName autodetect="true">myMailServer</helloName>
         <connectiontimeout>360000</connectiontimeout>
         <authorizedAddresses>127.0.0.0/8</authorizedAddresses>
         <maxmessagesize>0</maxmessagesize>
      </handler>
   </smtpserver>

   <mailstore>
      <repositories>
         <repository class="org.apache.james.mailrepository.AvalonMailRepository">
            <protocols>
               <protocol>file</protocol>
            </protocols>
            <types>
               <type>MAIL</type>
            </types>
         </repository>

         <repository class="org.apache.james.mailrepository.AvalonSpoolRepository">
            <protocols>
               <protocol>file</protocol>
            </protocols>
            <types>
               <type>SPOOL</type>
            </types>
         </repository>

         <repository class="org.apache.james.mailrepository.JDBCMailRepository">
            <protocols>
               <protocol>db</protocol>
            </protocols>
            <types>
               <type>MAIL</type>
            </types>
            <config>
               <sqlFile>file://conf/sqlResources.xml</sqlFile>
            </config>
         </repository>

         <repository class="org.apache.james.mailrepository.JDBCSpoolRepository">
            <protocols>
               <protocol>db</protocol>
            </protocols>
            <types>
               <type>SPOOL</type>
            </types>
            <config>
               <sqlFile>file://conf/sqlResources.xml</sqlFile>
               <maxcache>1000</maxcache>
            </config>
         </repository>

         <repository class="org.apache.james.mailrepository.JDBCMailRepository">
            <protocols>
               <protocol>dbfile</protocol>
            </protocols>
            <types>
               <type>MAIL</type>
            </types>
            <config>
               <sqlFile>file://conf/sqlResources.xml</sqlFile>
               <filestore>file://var/dbmail</filestore>
            </config>
         </repository>

         <repository class="org.apache.james.mailrepository.JDBCSpoolRepository">
            <protocols>
               <protocol>dbfile</protocol>
            </protocols>
            <types>
               <type>SPOOL</type>
            </types>
            <config>
               <sqlFile>file://conf/sqlResources.xml</sqlFile>
               <filestore>file://var/dbmail</filestore>
               <maxcache>1000</maxcache>
            </config>
         </repository>

         <repository class="org.apache.james.mailrepository.MBoxMailRepository">
            <protocols>
               <protocol>mbox</protocol>
            </protocols>
            <types>
               <type>MAIL</type>
            </types>
         </repository>
      </repositories>

      <spoolRepository>
         <repository destinationURL="file://var/mail/spool/" type="SPOOL"/>
      </spoolRepository>

   </mailstore>


   <users-store>
      <repository name="LocalUsers" class="org.apache.james.userrepository.UsersFileRepository">
         <destination URL="file://var/users/"/>
      </repository>

   </users-store>

   <database-connections>
      <data-sources></data-sources>
   </database-connections>

   <objectstorage>
      <repositories>
         <repository class="org.apache.james.mailrepository.filepair.File_Persistent_Object_Repository">
            <protocols>
               <protocol>file</protocol>
            </protocols>
            <types>
               <type>OBJECT</type>
            </types>
            <models>
               <model>SYNCHRONOUS</model>
               <model>ASYNCHRONOUS</model>
               <model>CACHE</model>
            </models>
         </repository>

         <repository class="org.apache.james.mailrepository.filepair.File_Persistent_Stream_Repository">
            <protocols>
               <protocol>file</protocol>
            </protocols>
            <types>
               <type>STREAM</type>
            </types>
            <models>
               <model>SYNCHRONOUS</model>
               <model>ASYNCHRONOUS</model>
               <model>CACHE</model>
            </models>
         </repository>
      </repositories>
   </objectstorage>

   <connections>
      <idle-timeout>300000</idle-timeout>
      <max-connections>30</max-connections>
   </connections>

   <sockets>
      <server-sockets>
         <factory name="plain" class="org.apache.avalon.cornerstone.blocks.sockets.DefaultServerSocketFactory"/>
      </server-sockets>
      <client-sockets>
         <factory name="plain" class="org.apache.avalon.cornerstone.blocks.sockets.DefaultSocketFactory"/>
      </client-sockets>
   </sockets>

   <thread-manager>
      <thread-group>
         <name>default</name>
         <priority>5</priority>
         <is-daemon>false</is-daemon>
         <max-threads>100</max-threads>
         <min-threads>20</min-threads>
         <min-spare-threads>20</min-spare-threads>
      </thread-group>
   </thread-manager>
</config>

*1:ドコモのRFC違反アドレスを通すCommons EmailValidator拡張 のテストケースを参照してください。