メールクライアントは作らなかったRoundcube を選んだ4つの条件
Webメール連携で、IMAPクライアントは自作しなかった。PHP製・無料・自社製品での実績・AutoLoginがある、の4条件でRoundcubeを選んだ。開けたらAutoLoginはサンプルだったが、選定は正しかった。
作らない、と決めた
ベータに上げた話で、最後に入った機能がメール連携だと書いた。この記事は、その手前の話だ。何を使うかを決めた日のこと。
接続先は LGWAN の中にある自治体のメールサーバだ。こちらにメールを配送する仕組みは要らない。要るのは、そこへ繋ぎに行くクライアントだけ。
IMAP クライアントを自作する案は、検討して捨てた。IMAP は仕様が広い。フォルダ、フラグ、検索、MIME、添付、文字コード、スレッド。「メールが読める」までは短い。「毎日使える」までが長い。ここに夏を使う価値はない。
だから、載せる側の条件を先に書き出した。あとから理由を付けたのではなく、探す前に4つ決めた。
| 条件 | なぜ |
|---|---|
| PHP で書かれている | 本体と同じ土俵に載る。運用で覚える道具が増えない |
| 無料である | 製品として機体ごと配る。費用が団体数に比例すると成り立たない |
| 自社製品で使っていた | 未知の OSS ではなく、落とし穴の位置を知っているものを選ぶ |
| AutoLogin / AutoLogout がある | NGW のログインから、パスワードを打たせずに入る |
選んだのは Roundcube 1.7.3 だった。4つがそれぞれ何を意味したかを、順に書く。
PHP で書かれている — 上限の、ひとつ内側だった
NGW の Linux 版は PHP 8.5 で動いている。Roundcube 1.7 系の要求はこうだ。
php >=8.1 <8.6
上限のひとつ内側で、ぎりぎり公式サポートに入る。1.6 系を選んでいたら外れていた。版の噛み合わせは、運が良かっただけだと思っている。
同居のために足したものは、これだけだった。
php8.5-intl… 1つphp-imap拡張 … 要らない(Roundcube は自分でソケットを喋る)
閉域網では、拡張ひとつ足すのも手作業になる。「拡張が不要」は仕様表の隅の一行だが、実際には採用の可否を分ける行だった。
無料である — 代償は GPL-3.0 だった
Roundcube は GPL-3.0 だ。NGW は製品として機体ごと配る。「無料で使える」の次に決めるべきは、どう混ぜないかだった。
原則を3つ立てた。
- 別のディレクトリに置く。公開領域の外に展開し、Apache の別名で必要な部分だけを見せる
- 別のデータベースにする。NGW のデータベースには表を1つも足さない
- HTTP 越しの疎結合にする。つなぎ役は Roundcube のプラグイン側に置き、NGW 本体の PHP は1行も読み込まない
3つ目が効いた。ライセンスの都合で始めた分離が、そのまま版上げの安全になった。Roundcube を上げても、NGW は無傷だ。
ライセンスは見た目にも効いた。自社製品で使っていたスキンは商用のもので、個人・非商用に限るという条件が付いていた。これは持ち込めない。同梱のスキンは1種類しかないので、選ばせたいなら別ライセンスのものを足す、という形に落ち着いた。
無料の OSS を製品に載せるとき、最初に読むのは機能一覧ではなく LICENSE だった。
自社製品で使っていた — 落とし穴の位置を知っている
弊社の別の製品が、すでに Roundcube を使っている。これは「動く」証明であると同時に、踏む石がどこにあるかを知っているという意味だった。
実際、3つを引き継いだ。
- 外部のサイトへ POST で戻るという考え方(後で書く)
- サーバ側の振り分けを使わないフィルター。あちらではサーバ機能に頼らず、受信箱を開いたときに IMAP でメールを移していた。接続先のメールサーバがその機能を開けている保証はないので、同じものを持ってきた。PHP 8.5 と Roundcube 1.7 で動くよう、7か所だけ直した
- スキンのライセンス(上に書いたもの)
初めて触る OSS なら、この3つは全部「現地で初めて分かる」ものだった。知っている、というのは条件として弱そうに見えて、いちばん時間を返してくれた。
AutoLogin がある — 開けたら、サンプルだった
ここが決め手で、いちばん誤解しやすいところだ。
Roundcube には autologon と autologout が最初から同梱されている。条件を満たしている、と思った。中を開いた。
/**
* Sample plugin to try out some hooks.
* This performs an automatic login if accessed from localhost
*/
public function authenticate($args)
{
if (!empty($_GET['_autologin']) && $this->is_localhost()) {
$args['user'] = 'me';
$args['pass'] = '******';
ユーザー名は me。パスワードは伏せ字。127.0.0.1 からしか動かない。これは実装ではない。サンプルだ。
もう片方の autologout は、少し実用に近かった。
if (!empty($_SESSION['user_id']) && !empty($_POST['_autologout']) && $this->known_client()) {
$rcmail->logout_actions();
$rcmail->kill_session();
}
冒頭のコメントに「外部のサイトから POST を送ってログアウトさせる」使い方が書いてある。
がっかりしてから、考え直した。同梱の2つが証明していたのは、機能ではない。外部システムと行き来する前提で、フックが開いていることだ。そちらのほうが、完成品が付いてくるよりずっと価値がある。完成品は要件に合わないが、フックは合わせられる。
自前のプラグインは、5つのフックだけで書けた。
| フック | 使いみち |
|---|---|
startup | 券を持って来たらログイン動作に切り替える/ログアウト前に NGW のログイン ID を控える |
authenticate | 復号したアドレスとパスワードを渡す |
login_after | Roundcube のセッション ID を控える(NGW 側から個別に消すため) |
message_before_send | 共有アドレスのとき、差出人の表示名を「課名 職員名」に差し替える |
logout_after | NGW のホームへ POST で戻す |
サンプルとの違いは、パスワードの出どころだ。
- パスワードは暗号化して保管する。鍵はソース管理の外に置く
- NGW が発行するのは使い捨ての券(64桁の16進)だけ。
/mail/?_ngwtoken=…に載るのはこれ - 券は1回しか成功しない。更新した行数で判定している
- パスワードは URL にも POST にも載らない
「AutoLogin が付いている」で選んだが、正しくは「AutoLogin を書ける」で選ぶべきだった。結果は同じで、理由が違う。
行きと帰りで、手段が違う
選定が終わってから、本題が残っていた。NGW の画面は POST でしか開かない。 URL を直接叩いても開けない造りにしてある。
つまり、メールから NGW に帰るのに、リダイレクトが使えない。
やったのは、戻るときに自動で送信されるフォームを書き出すことだ。
<form name="frmReturn" method="POST" action="/ngw/ngwMainInfo.php">
<input type="hidden" name="fdtNgwtLoginId" value="…">
<input type="hidden" name="fdtNgwtLoginDate" value="…">
<noscript><button type="submit">戻る</button></noscript>
</form>
<script>document.getElementById('frmReturn').submit();</script>
ログイン ID を GET のクエリに載せなかったのは、意図してのことだ。この ID だけで NGW のセッションになる。 ブラウザの履歴にも、Apache のアクセスログにも残してはいけない。だからメールを開くときに券の表へ預けておいて、帰るときに POST へ積み替える。
行きと帰りで、非対称になった。
| 向き | 手段 | 載るもの |
|---|---|---|
| NGW → メール | GET のリダイレクト | 使い捨ての券だけ |
| メール → NGW | 自動送信のフォーム(POST) | ログイン ID と日時 |
途中まで、「Cookie を POST に積み替える橋渡しのページ」を1枚作るつもりでいた。結局作っていない。戻りたい側がフォームを書き出せば済む。 画面が1枚減った。
……のだが、この記事を書くために読み直したら、プラグインのコメントに「橋渡しのページを経由する」と作らなかった設計のまま書いてあった。直した。動いているコードは正しくて、説明だけが古い。いちばん見つけにくい種類のずれだ。
戻るボタンが3つに増えた
行き来ができるようになったら、押せる場所が増えた。
- 左上のロゴ。押すと NGW へ戻る。ログアウトはしない
- ツールバーの「NGW」ボタン。ログアウトして NGW へ戻る
- 画面が狭いときに畳まれる「続く」メニューにも、同じ項目
3 は要らなかった。狭い画面で畳まれる先は、スキンが自分で作る別のメニューで、ツールバーのボタンはそこへそのまま移る。二重に登録していただけだ。消した。
1 と 2 も、スキンによっては重なる。既定のスキンは左上にロゴを出すうえ、左の柱に標準のログアウトも並ぶ。同じ絵が二つ出る。 だから既定のスキンではツールバーのボタンを出さないことにした。
if ($rcmail->task == 'mail' && … && !in_array('elastic', $skinChain, true)) {
$this->add_button([…], 'toolbar');
}
判定にスキンの名前を使ったのにも理由がある。置き場所を探す関数のほうは「CSS が実在するか」で答えを返すので、CSS を置かない今回の用途には使えない。名前の連なりを見れば、派生スキンもまとめて外れる。
結果として、職員がメールを閉じるときに押すものはひとつになった。足すのは簡単で、減らすほうが設計だった。
閉域網でも使えるはず、を閉域網の外で確かめる
現地に持ち込む前に確かめておきたいことがあった。
| 確かめたかったこと | 結果 |
|---|---|
| 依存の取得ができない環境で入るか | 依存を同梱した 35MB の配布物を、署名を検証して展開。既存の更新配布の仕組みに載せた |
| PHP 8.5 で警告が出ないか | すべての警告を出す設定で、ゼロ |
| 外部の IMAP まで経路が通るか | 到達した。認証失敗が返る=経路は OK |
| 古い TLS のサーバでも繋がるか | 繋がらなかった。証明書の名前が一致していない |
| 本物のメールサーバで動くか | 未検証。現地でしか確かめられない |
4行目が本題だった。繋がらないとき、Roundcube のログに出るのはこれだけだ。
Could not connect to ... Unknown reason
経路が塞がっているのか、TLS が古いのか、証明書の名前が違うのか、切り分けられない。
現地でこのログを見ながら電話することになる、と想像した時点で手を止めて、切り分け用のコマンドを1本書いた。
$ php ngwMailConnCheck.php mail.example.lg.jp 993
DNS : 10.x.x.x
接続 : OK (0.12秒)
TLS : TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
証明書 : CN=… 発行=… 期限=…
挨拶 : * OK …
DNS → 到達 → TLS → 証明書の名前 → 機能一覧の順に、止まったところまで出す。検証をゆるめて試す引数も付けた。これがあれば「証明書が原因です」と1回で言い切れる。
OSS を閉域網へ持ち込むとき、本体より先に要るのは診断の道具だった。 本体は動くか動かないかだが、道具は「なぜ動かないか」を答える。現地に持って行くのは、この2つで1組だ。
AIとの協働作業での学び
条件を4つ先に書き出したのは私で、それを「満たしている」と言ったのも私だった。同梱の autologon を開いて Sample plugin の一行を見つけたのは、AI に「このプラグインは実運用で使えるのか」と聞いたときだ。私は同梱されている事実で満足していて、中を読んでいなかった。
ここで学んだのは、条件の書き方のほうだ。「AutoLogin がある」は製品の機能表を写した条件で、こちらの都合が入っていない。正しくは「NGW のセッションから、パスワードを渡さずに IMAP へログインさせられる」だった。前者は Yes/No で答えが出るぶん、確かめた気になれてしまう。
もう1つ。プラグインのコメントに、作らなかった橋渡しのページの名前が残っていた。これも、記事を書くために読み直して見つけたものだ。コードは直したがコメントは直していない、という差分は、動作の確認では絶対に出てこない。 説明を書き直す作業を、たまに挟むしかない。
今回、AI が速かったのはフックを5つに絞るところだった。逆に、AI が最後まで言わなかったのは「戻るボタンは1つでいい」だ。押す人の身になる判断は、まだこちらの仕事だと思っている。