Chienomi

Postfixのlocal(8)/virtual(8)の置き換え AltPostLocal

開発::application

AltPostLocalはPostfixのlocal(8)とvirtual(8)の代わりに利用できるLDA(MDA)である。

これはlocal(8)を置き換えることで、virtual(8)の機能まで内包したLDAとして使うことができる。

動機

local(8)は高速・軽量だが、柔軟性に欠ける。 複雑なフィルタを組み合わせたい場合などは、かなり面倒だ。

また、virtual(8)は一見local(8)と同じようなものに見えるがパイプは使えないため、 パイプでフィルタしようとした場合は、一旦virtualでローカルユーザーに展開した上でaliasesで処理する必要があったりする。

そしてなにより、近年多いバウンスを使った攻撃への対応だ。 これは、わざとメールをバウンスさせ、Return-Pathを使って迷惑メールを配信させるという攻撃があるのだが、Postfixにおいて「決してバウンスさせない」という設定は結構難しく、頭を悩ますことになる。

AltPostLocalはこのような、「メールを決してバウンスせず、複数のドメインを柔軟にホストする」ことを容易にするためのものである。 これは、「自分に関するメールを受け取るだけのPostfix」にも適している。

local(8)とは

local(8)デーモンはmaster(8)によって起動され、qmgr(8)からリクエストを受け取り、メールの配送要求を処理する。 aliases(5)データベースと~/.forwardファイルを理解し、ファイル、またはコマンドに配送する。

配送状態はbounce(8), defer(8), trace(8)のデーモンに通知される。

もっと易しく言うと、Postfixが「自分宛てのメールだ」と判断したメールがlocal(8)に渡され、local(8)はその配送業務を担当する配達員である。 その中には、「住所不明」で送り返すことや、他のサーバーへの転送を依頼すること、他の配送業者に受け渡すことも含まれる。

local(8)はPostfixの一部として動作するように作られているため、セキュリティ上の理由で動作が制限されている部分もある。

local(8)はさっと起動し、素早く配送を行って終了することを前提としている、軽量で高速なものだ。 同時にPostfixのローカル配送パートでもあり、Postfixがメールを受け取ったあとサーバー内でどうなったかということをコントロールするための要素でもある。

しかし、そのためlocal(8)で発生するbounceやdeferが、現代では厄介な要素になっている面もある。 同時に、local(8)はちょっとの設定ミスで大きな問題を引き起こす、かなり「繊細」なプログラムだ。

AltPostLocalはPostfixのpipe(8)から呼び出される外部コマンドである。 こちらはPostfixとのつながりが一方向のパイプと終了ステータスしかないため、もっと構図はシンプルだ。

動作概要

AltPostLocalはPostfixのpipe機能を利用してPostfixのコンポーネントとなり、local_transportの設定によってlocal(8)を置き換える。

非常に重要な設定となるのが、master.cf

altpostlocal    unix  -       n       n       -       -       pipe
  flags=F user=email argv=/usr/local/bin/altpostlocal -f ${sender} -- ${recipient}

である。 (実行ユーザーに注意)

これで、main.cf上でaltpostlocalがコマンドとして認識されるので、main.cf

local_transport = altpostlocal

が効くようになる。

その上で

local_recipient_maps =

とすればすべてのメールが一旦はAltPostLocalに渡されるようになる。

pipeで起動されるプログラムは、標準入力からメールが渡される。 ${sender}${recipient}がそれぞれ送り主、宛先に置き換わる。

pipeで起動されたプログラムとPostfixの関係として重要になってくるのは終了ステータス。 この情報はsysexits.h(3head)のmanpageにある。 特に重要な意味を持ってくるのが67(EX_NOUSER)。

なお、こういう仕組みなので、AltPostLocalは状況によってexitで処理を打ち切ることができる。

違いにフォーカスする

Postfixが(も)使うaliases(5)のフォーマットは次のようなものになる。

foo: bar, baz

これでfoo宛のメールがbarbazに転送される。 宛先としてリモートのアドレスを指定することもでき、この場合は転送される。

また、|"/bin/foo foo_arg"のようにコマンドの標準入力に流すことも可能。 この場合はコマンドの終了ステータスがそのまま配送ステータスになる。

virtual(5)も似たような感じだが、パイプは使えない。

これと比べればAltPostLocalは複雑だ。YAMLファイルであり、aliases(5)で

foo: |"/bin/foo foo_arg"

と表現されるものが、

aliases:
  foo:
    type: pipe
    cmd: /bin/foo
    args: [foo_arg]

となる。 また、

foo: /var/maildir/foo/Maildir/

というものが、

foo:
  type: maildir
  value: /var/maildir/foo/Maildir

となる。

これだけ見るとちょっと面倒そうだが、複雑なものはaliasesの場合ちょっと書きづらい面がある。 それがわかりやすく書けるということに加えて、複合する場合はもっと簡単になっている。

例えば、

foo: |"/usr/local/bin/mailfilter foo 3"
bar: |"/usr/local/bin/mailfilter baz 2"

みたいなのがあると、同じコマンドを大量に書くという事態が発生したりする。 ところが、YAMLの場合は

anchors:
  - &filter
    type: cmd
    cmd: /usr/local/bin/mailfilter
aliases:
  foo: {<<: *filter, args: [foo, 3]}
  baz: {<<: *filter, args: [baz, 2]}

のように書ける。

もっと大きく違ってくるのがマルチドメインの扱い。 virtualはコマンドが扱えないため、virtualで配送されるメールをフィルタしようとすると、まずaliasesに

_virtual_filter_foo: |"/usr/local/bin/mailfilter foo 3 foo@example.net"

のように定義しておき、virtualで

foo@example.net: _virtual_filter_foo

としてたらい回しにする必要がある。 一方、AltPostLocalはそれ自体がマルチドメインに対応しているため、

foo@exmaple.net: {<<: *filter, args: [foo, 3, foo@example.net]}

のようにするだけでいい。

aliases(5)とvirtual(5)を使う方法は、local(8)がaliases(5)の名前に配送することは避けられないため、mydestinationにある値、例えば_virtual_filter_foo@example.comにメールを直接送られてしまう可能性が残り、そのためフィルタ用の名前はわかりにくいものにならざるを得なかったりする。

類似の問題として、例えばabuseという名前は全ドメインで持たせたいとする。 これを、個別のメールボックスにせずにrootに配送するとしても、virtual(5)では

abuse@example.net: root
abuse@example.org: root
abuse@example.info: root

のように個別に書く必要がある。 対して、AltPostLocalはドメイン部がマッチしなければ名前部だけでマッチさせようとするため、

abuse: *root

のように書いておくだけでいい。

失敗時の扱い

配送失敗のパターンとしては

  • 配送先メールボックスが存在しないか、アクセスできない
  • ユーザーが存在しない
  • コマンドが失敗する

などがある。 いずれにせよ、これらの失敗はバウンスを発生しうる。

この「バウンスを発生しうる」というのが結構厄介で、特に「存在しないユーザーにメールを投げてバウンスさせることでスパムメールを送らせる」という手口がある。

このため、現代ではバウンスは発生させたくないのだ。

Postfixの場合、luser_relayオプションによってlocal(8)が配送するものに関しては、定義されていないメールアドレス宛のメールを拾うことができる。 だが、これはvirtual(8)には対応しておらず、他のドメインに対しては攻撃が成立してしまったりする。

また、コマンド失敗が配送失敗になってしまうのは、コマンドを自分で作ったときにリスクが大きく、いじりにくい。

また、バウンスを避けるとロストさせるしかないということにもなったりする。

AltPostLocalにおいてはこの解決が重要なポイントになっている。 まず、どこにもマッチしなかったメールをdefaultという設定によって配送できるようになっている。

default:
  type: maildir
  value: /var/maildir/default/Maildir

さらに、例外を発生させた場合は、rescueという項目に書かれたディレクトリに配送させることができる。

rescue: /var/maildir/rescue/Maildir/new

rescueはメールを保存するだけなので、Maildir扱いしたいのであればnewまでつけておく必要がある。

デフォルトではメールとして配送するが、rescue_jsonが真であればJSON形式で保存され、再配送を試みることが容易になる。 (altpostlocal -j jsonfile.jsonの形式で再配送できる)

そもそも特定のコマンドが失敗を誘発させる可能性があるのであれば、個別にonerrorという設定を書ける。

foo:
  type: pipe
  cmd: dangerfilter
  onerror:
    type: pipe
    cmd: saferfilter
    onerror:
      type: maildir
      value: /var/maildir/safezone

その他の機能

エイリアスでの複数処理

alises(5)はひとつの名前に対して複数の配送先を書ける。これはAltPostLocalでも同じ、というか、AltPostLocalは配送はシーケンスで書くのが前提で、直接マッピングを書くのはREADMEにない裏技。

aliases:
  foo: 
    - type: maildir
      value: /var/maildir/foo
    - type: pipe
      cmd: foofilter

local(8)は複数の配送を並列で走らせるが、AltPostLocalは順番に試みる。 つまり、エラーを発生させた場合はその先に書かれているものは実行されないため、リスクがあるものは後ろに書いたほうが良い。

また、type: nouserは直ちにAltPostLocalを終了させるため、それ以降に書かれたアクションは実行されない。

REMatch

REMatchはマッチを開始する前にrecipientsに対して正規表現マッチングを行い、マッチした場合はrecipientsそのものを置き換える。

基本的にlocal(8)は軽量・高速な作りになっており、節度ある動作をするが、AltPostLocalはより重く、高機能である。

そしてその最たるものがREMatchであり、AltPostLocalとしても決して利用を推奨はしない機能だ。 当然ながら全パターンに対して正規表現コンパイルが必要であり、しかも全てのrecipientに対して全パターンのテストが行われる。 AltPostLocalの処理全体から見ても、非常に重い処理だ。

それでも、この機能をどうしても使いたくて、そのためにlocal(8)での配送を諦めるような状況にあったとしたら、この機能があることで救われる可能性がある。 そういった理由から搭載されている機能であり、Aliasesではカバーできないマッチングを必要とする場合の最終手段として利用できる。

Extension

Postfixの場合、main.cf(5)のrecipient_delimiterによってメールアドレスを拡張できる。 デフォルトでは+が指定されており、ほとんどのメールサービスでは+によってメールアドレスを拡張することが可能だ。

逆に言えば、+によって本来とは異なるメールアドレスを利用可能だということを多くの人が認識しているため、サービスによってはローカルパートに+を含むメールアドレスを登録できなかったりするほか、+を外したアドレスに対して送り付けてくるスパムも多い。

これは自分でメールサーバーはホストするそれなりの動機である。recipient_delimiterは複数の記号を指定することができ、

recipient_delimiter = +-

とすれば、+に加えて-もデリミタとして使えるようになる。

aliases(5)の解釈としてはこのデリミタによる分解を行わない状態でマッチしてからデリミタによって分離したローカルパートをマッチするため、デリミタを含んでいる別のルールを持つアカウントを作ること自体は可能。

ただ、これはいくつか制限がある。

  • 指定できるのは「記号の1文字」だけ
  • デリミタを複数含む場合の検索が厄介

AltPostLocalのExtensionでは区切りとなる「正規表現パターン」を指定し、なるべく長くマッチさせる。

例えば、

extension: "\\+"

であり、recipientがfoo+bar+baz@example.comである場合、検索順序は

  1. foo+bar+baz@example.com
  2. foo+bar@example.com
  3. foo@example.com
  4. foo+bar+baz
  5. foo+bar
  6. foo

である。

もちろん、recipient_delimiter = +-相当の指定として

extension: "[+-]"

と書いても良い。

さらに、正規表現であるために

extension: "(?:uk|\\+)"

と書くこともできる。 この場合、haruka@example.comというrecipientの実際の宛先はhar@example.comになるし、yuka@example.comの実際の宛先はy@example.comになる。

これだけだとだいぶクセが強いが、

extension: "(?:_aka_|-)"

あたりならその良さが分かってもらえるかもしれない。

nouserとnothing

aliases(5)で意図的にNOUSERを発生させたい場合、

foo: |"exit 67"

と書くという方法がある。

また、aliases(5)でメールをブラックホールに捨てたい場合は

foo: /dev/null

と書ける。

これはこれで悪くないと思うが、AltPostLocalではより明瞭なアクションとしてtype: nousertype: nothingが用意されている。

nothingに関してはアクションとして

foo: []

と書いても良いのだけども。

die_quietly

例えrescueを書き、nouserアクションを使わなかったとしても、依然としてAltPostLocalが何らかのエラーを発生させる可能性はある。 これは、主にエラーハンドリングの処理、つまりrescueの中でエラーを発生させた場合だ。 典型的には、rescueで指定されたパスが、書き込めるディレクトリでないとか。

AltPostLocalが異常終了すれば、メールは何らかの形で再配送される可能性がある。 だが同時にそれはバウンスを発生させる可能性もある。

die_quietlyは例えメールが闇に葬られたとしても、バウンスを発生させないためにステータス0で終了する。

なお、die_quietlyを有効にしていても、設定のYAMLファイルがパースできない場合などは異常終了する。

Maildir

Maildirのファイル名は非常に複雑でとても扱えないように見える……が、実際のところ割と好きにつけられる。 AltPostLocalでは

name = sprintf("%d.M%dP%d.%s:2", now.to_i, now.nsec / 1000, Process.pid, Socket.gethostname)

という付け方をしている。

Dovecotから見えない場合はファイルの所有者が原因である場合が多い。

あとは、Postfixの場合は自然とMaildir/下に置くようになっているので、そこが噛み合っていない(なにかしらにMaildir/があったりなかったりする)とか、newcurの下に置いてないといったあたり。

メールの転送

Postfixのキュー戻しはpostdrop(1)を使うのだけど、postdrop(1)を直接叩く方法がよく分からなかったため、sendmail(1)を使った。

Postfixのsendmail(1)での転送は

sendmail recipient < mail

で良いため、非常に簡単。

だが、この場合SPFやDKIMのような送信元検証がどうなるのかよく分からない。 そもそも構造的にSPFに関しては転送時は通らないはずだ。

local(8)が何かこれらの問題を軽減するための対策を行っているのだとすれば、AltPostLocalにも取り込む必要があるだろう。

転送は比較的頻繁に行うものであるため、AltPostLocalではtype: pipeで表現可能だが、type: forwardという専用のタイプが用意されている。

type: forward
value: foo@exmaple.org

.forward

local(8)は~/.forwardを解釈するが、これを必要とするような古典的なUnixシステムとAltPostLocalは相性がそれほど良くないため、そのような場合は素直にlocal(8)を使うだろうと判断し、サポートしていない。

同様に、aliases(5)のパーサーも持っていない。

RubyのYAMLライブラリはとても微妙

AltPostLocalにはaliases(5)で言う

foo: bar

のようなエイリアス設定の機能がない。 というか、元々あったのだけど、必要ないので削除した。

なぜならば、YAMLにはアンカー/エイリアスの機能があり、

foo: &foo
  type: maildir
  value: /var/mail/foo/Maildir
bar: *foo

のように書けるからだ。 このアンカーによりaliases(5)と比べると複雑な配送を必要とする場合にかなり楽に書ける。

そしてAltPostLocalはRubyで書かれているので、当然ながらRubyのYAMLライブラリを使っている。 しかし、最近のYAMLライブラリの基盤であるPsychライブラリはだいぶ微妙だ。

最大の微妙ポイントはPsych 4で入った非互換変更によるところにあり、Psych.loadPsych.safe_load相当に変わった。 これにYAML.loadが引きずられて、非常に大きな非互換を生じた。

これ自体が拙速に行われたことで、十分な説明も行われなかったことが問題だと思っているが、もっと問題なのは “safe” の概念だ。

Psych 4では “safe” のために扱えるクラスを限定している。 これは、「任意のクラスをロードできるとリモートーごと実行の脆弱性に発展する」というもっともらしい説明をしているが、実際のところ限定されているクラスは「JSONにあるクラス」である。

例えば、TimeクラスはRubyの組み込みクラスであり、なおかつYAMLの公式の仕様としてtimestampというタグがある。 にもかかわらず、Timeはロードできない。

別の見方をすれば、YAML.dumpYAML.loadは非対称なメソッドになった。 YAML.dumpした文字列はYAML.loadできるとは限らない。

「JSONでなくYAMLを使う動機」を全く理解しておらず、著しく間違っている。 YAMLライブラリはYAMLのライブラリが必要とされる理由を無視している。というか、YAMLライブラリの作者はYAMLではなくJSONが好きなのだろうな、と感じる。

YAMLはそもそも冒頭Goalsで

  1. YAML should be easily readable by humans.
  2. YAML data should be portable between programming languages.
  3. YAML should match the native data structures of dynamic languages.
  4. YAML should have a consistent model to support generic tools.
  5. YAML should support one-pass processing.
  6. YAML should be expressive and extensible.
  7. YAML should be easy to implement and use.

と言っているのであり、JSONとは本質的に全く異なる。にも関わらず、JSONと同等にしようとするのは根本的に間違っているのだ。

デフォルトでエイリアスが使えなくなったのもそうだ。 循環参照をしている場合に再帰処理すると落ちるから、というのが理由らしいが、そんなのはスクリプトのバグであり、プログラマの責任だ。 そもそもYAMLで読んだものをイテレーターで再帰するということ自体が普通ではないのだし、デフォルトで禁止するようなものではない。

Psychが4になった当初はYAML.unsafe_loadが存在せず、YAMLではなくPsychを使うように改修する必要があり、非常に難儀した。 今はYAML.unsafe_loadが使えるので、新規に書くプログラムに関しては今この瞬間はそこまで困らない。

だが、Psych側はいつunsafe_loadは必要ないと言い出してもおかしくない調子であり、信用できない……と私は感じている。

だが、Rubyには代替となるYAMLライブラリがない。 いい感じの(YAML 1.2に対応した)YAMLライブラリがあるのは、C, C++, JavaScript, Go, Nim, Rustあたり。

かなり頭の痛い問題だが、現状はYAML.unsafe_loadを使うことでなんとかなる。 なお、AltPostLocalでは必要となるのがエイリアスだけなので、YAML.loadにオプションを渡している。

実際に動かした感想

私のメールサーバーは(一部外部ユーザーのものを含めて)デリミタを含まないで112のメールアドレスと、49のメールボックスを持つ。

メールボックスを持つもののうち約半数が追加のメールフィルタ(例えばMailDeliver)を使うものだ。

local(8)からの移行作業はなかなか大変だったが、思った以上によく動いた。 観察している限りdeferredもbounceも発生しておらず、うまくカバーできており、外部フィルタとの連携も良好。 local(8)の代替とは無謀なことかとも思っていたが、存外うまく動作し、当初の目的を達成できているようだ。

現代においてメールサーバーを運用する難しさのまぁまぁの部分を解決してくれる、良いソリューションだったと感じている。

ちなみに、現代のメールサーバーの難しさを低減するため、今回実験的にSASLによるリレーを行わないようにした。 つまり、外部への送信サーバーとしては機能しない。 メールの送信は外部のESPに頼ることにした。

送信サーバーの機能が最終的に必要かどうかは後々判断していくことになると思うが、「メールを送信せず、メールをはじかないメールサーバー」であれば、現代でも現実的に運用可能だと思う。

ただし、メール転送を可能にするためには、OP25B制限があるサーバーでは運用できない。 逆に、メール転送もしないならOP25Bのあるサーバーでも運用できる。

AltPostLocal運用になり、(ほぼ)個人用メールサーバーとしてはかなり使いやすくなった面もある。

ドメイン数が多くなってくると、そのドメイン宛のメールをある程度拾う必要があるが、各ドメインのメールの受信をセットアップするのは結構大変だ。 メールボックスを用意し、MUAを設定し……までになるとかなりしんどい。

また、メールアドレスのドメインを拡張したい場合でも、すぐ使うとは限らないため、必要になるまで遅延させたい部分もある。

AltPostLocalの場合

  • ドメインを超えてローカルパートでエイリアスを設定できる
  • デフォルトの受け取り先を設定できる

というのが非常に効く。

まず、一般的な(RFCにあるような)メールアドレスの受け取り先を用意し、次に「自分の」メールボックスを用意する。

例えば

info: *general
www: *general
webmaster: *general
abuse: *general
john: *me
johnan: *me
jh: *me

のように。

こうすると、「ドメインとして受け取る必要のあるアドレス」は全体的にまとめて受け取れるし、「自分宛てのアドレス」は個別にメールボックスを設定しなくて良い。 例えば、新たにexmaple.appを取ったとして、john@example.appを登録するまでもないのだ。

さらに、予期せぬアドレスに対してもdefaultが効くため、人にメールアドレスを教えるときに準備していないアドレスを教えたりしても大丈夫だし、また間違ったアドレスを教えてしまった場合も受け取れはするので問題にならない。

ただし、新規ドメインを追加したときにPostfixのmain.cf(5)はmydestinationのために設定する必要はある。

defaultが拾うことでスパムメールを拾ってしまう問題が懸念されるかもしれないが、実際のところドメインだけを見て送り付けてくるスパムメールのローカルパートはだいたい決まったものになっているため、そこらへんを拾うエイリアスを設定してブラックホール行きにすればいい。

ただし、ドメインのないエイリアスのマッチが行われるのは、ドメイン付きのエイリアスのマッチが終わってからであることには注意が必要。

maildirタイプにはjunkフラグが追加されたが、これも潜在的には地味に便利。 メールボックスが明らかに迷惑メールしか来ないメールボックスは閉鎖すればいいので、nothingを指定すれば良いのだけど、これまで使っていたメールアドレスがお漏らしによって迷惑メールまみれになったとかだと、ただちに閉鎖できなかったりする。 こういうケースで、「このメールアドレス宛に送られてくるものはほぼほぼ迷惑メール」というアドレスをjunkフラグつきで設定しておくと、アカウント操作に関わるメールなど捨てると困るやつを一応受け取っておくということが可能。 別にそういうメールを専用の隔離メールボックスにまわしてもいいのだけど、メールクライアントの設定が面倒な上に新着メール通知が鬱陶しいだろうから、junkにつっこめるのはメリットがある。

メール送信についてのあれこれ

昨年末にGoogleとYahooがDMARCを必須にしたことでより送信が難しくなった。 もっとも、従来でも小さいサーバーでSPF/DKIM/DMARCに対応した上でメールを送ってもGoogleは高い確率でメールを拒否する。 このような大手プロバイダーは、オープンなインフラであるeメールを称しながらも独自のルールでオープンな疎通を妨げ、独立プロバイダーを排斥している面があり、ここ10年くらいその問題への対応を続けてきたが、気まぐれに不明なルールで受け取らなくなったりすることが多いため、「やってられない」という気持ちになってきている。

個人的なeメールを継続的にやりとりするような状況は、今やeメールを続けることは現実的にないと見ていいだろう。そう考えると大量のメールをやりとりする状況はあまり考えられない。 このため、送信ボリュームだけ見ればそれほど高額になることはないだろう。

なお、PostmarkはDMARCを別契約にしていて、そこそこ高い。 私の場合は3名義の送信元があるため、扱いに少し困っているが、おそらくはSendgridを使っていくのが今は無難だろう。 PostmarkもGoogleとYahooが受け取らないとなれば、標準対応する可能性もなくはないと思うが。

だが、DMARCに対応すれば良いという考え方は妄信的にGoogleやYahooに追従することがインターネットの正しいマナーだという考えのもとであれば大筋で間違いではないが、実のところ問題もある。 DMARCは日本では法的に問題がある(通信の秘密を侵害しうる)ものでもある。当事者間の同意というのは、個人間でメールをやりとりする場合に形成するのはまるで現実的なシナリオが存在しないだろう。

ちなみに、この問題は受信側にあるため、どらちかといえばユーザーが選択するメールボックスの問題だと見ることができる。 例えば、私がBobにメールを送るとする。 私は私のメールサーバー、BobはGMailを使っている。 このとき、私のメールサーバーがDMARCに対応していても、していなくても、私のメールサーバーが通信の秘密に関わる問題になることはない。なぜならばこのとき、私のメールサーバーがDMARCのためにメールを見ることはしていないからだ。

逆に、Bobが私にメールを送ったとしても、私のメールサーバーが問題になることはない。 なぜならば、私のメールサーバーはDMARC認証を行っていないからだ。 むしろ、AltPostLocalはとりあえず全てのメールを受け取るので、受け取る過程でメールを見る必要がない。 実際はMailDeliverを介してメールはフィルタされるし、ここは争点になりうる。

ここで重要になってくるのが総務省による資料で、

宛先不明の電子メールに関して、受信側サーバにおいて電子メールの送信ドメインを機械的に確認し、認証できない場合については送信元サーバに対してエラーメールを返さないようにする行為も、通信の秘密を「当事者の意思に反して利用する」ことに当たり、通信の秘密の「侵害」(窃用)に当たり得ると考えられる。

とあったりするが、これは電子メールの「内容を見ている」ということに対するものである。 AltPostLocalは「送りつけられたメールボックス」を元に動作しており、メールのデータには一切関与しない(エンベロープ1だけで動作する)ため、AltPostLocalが通信の秘密を侵害する可能性は本当にない。

では、MailDeliverの場合はどうなのか。 MailDeliverはメールフィルタであり、メールデータをもとに動作する。 こちらはAltPostLocalとは異なり、データを受け取った時点でエンベロープ情報を失っているため、メールデータを扱わざるを得ない。 実際にMailDeliverがどのようなフィルタリングをを行うかは、プログラマブルフィルタである以上は設定次第だが、メールデータを見ないでフィルタリングを行うことは原理的に不可能である。

IAjapanによると

迷惑メール対策では、受信メールサーバでベイジアンやヒューリスティック、あるいは海外のレピュテーションDBやオープンリレーDBのブラックリストに基づくものなど、各種のフィルタリングを行うことがあります。通信の秘密の保護の対象はメールの全ての部分についても及ぶため、これらのフィルタリングは受信者の個別の同意がなければ行うことができないとされています。

また、@ニフティ提供の資料では

2006年1月23日 電気通信事業分野におけるプライバシー情報に関する懇談会(第18回会合)で、電気通信事業者が行う電子メールのフィルタリングと電気通信事業法第4条(通信の秘密の保護)の関係について基本的考え方が公表された。

初期設定をフィルタリングオンの状態で提供するための条件

  • 利用者が、いったんフィルタリングサービスの提供に同意した後も、随時、任意に同意内容を変更できる状態(設定変更できる状態)であること *フィルタリングサービス提供に対する同意の有無にかかわらず、その他の提供条件が同一であること
  • フィルタリングサービスの内容等が明確に限定されていること
  • 通常の利用者であれば当該サービスの提供に同意することがアンケート調査結果等の資料によって合理的に推定されること
  • 利用者に対し、フィルタリングサービスの内容等について、事前の十分な説明を実施すること(事業法第26条に規定する重要事項説明に準じた手続により説明すること)

とあり、大筋「受信者の同意があれば良い」(送信者の同意を得る必要はない)ということが確認できる。

となると運用の問題で、私のメールサーバーの場合、外部ユーザーはすべて私が直接連絡を取れる人であり、それ以外は私自身である。 その上で、外部ユーザーに対してはメールフィルタを通していないため、同意を取るべき相手は私しかいない。 なので、私のサーバーにおいては問題ない。

本筋(AltPostLocal)から外れた話のように感じるかもしれないが、実はここでAltPostLocalそもそものコンセプトの問題に戻ってくる。 AltPostLocalは基本的に、「何があってもバウンスさせない」というものであり、そのように設定しない限りは存在しないメールアドレスに送りつけようが、配送でエラーが起きようが、メールフィルタがエラーになろうがバウンスさせない。

この「届かなかったということを相手に伝えない」ということが同意がない限り違法だということになってしまうとAltPostLocalのコンセプトが成立しなくなる。

しかし、DMARCやメールフィルタの問題は「メールの中身を見ている」ことにあり、エンベロープでは問題になりようがない。 エンベロープは配送の宛先を記しているものであり、エンベロープを見てはいけないとなればそもそもメールの配送自体が不可能だ。 そして、エンベロープを見ることに問題はなく、エンベロープを見た結果メールを捨てても問題はないわけである。2 ここでは「公平さを欠く差別性」が問題になったりするのだが、AltPostLocalは宛先によって一律に動作するためこの点も問題ない。

ここで話をDMARCのことに戻す。 DMARCで問題になるのは、私がBobにメールを送ったときのGMailの挙動だ。 GMailがDMARCをもとにフィルタリングするには、当事者、つまり私とBobの同意が必要ということである。 とはいえ、私がGMailがフィルタリングすることを許せないといったところで、Googleが耳を傾けることはありえない。現実的にはGoogleとBobの間の話になる。

となると、「BobはGMailを使っているんだから、そこは納得してるでしょ?」という私の側の結論になるのである。

これが業務上のメールとなるとちょっと難しくて、「あー、私はDMARCに同意してないからね、DMARCに対応しないで送るね」と言ったところで、相手に届かないと困るのは私、ということが普通にあるわけだ。

そうなると現実的な対応として、業務に関わるメールはDMARCに対応せざるを得ない。 そして、カスタマーサポートへの問い合わせで相手がメールを受け取らないというのも普通に困る3ため、結局個人的なアドレスでもDMARCに対応せざるを得ない。

そして、DMARCに対応しさえすれば受け取ってくれるなら自前で構築してもいいのだが、それでも(特にGMailは)受け取ってくれないので、大手ESPに頼る以外の選択肢がないのである。

このような状況は誰が引き起こしたのだろうか。 もちろん、最も悪いのはスパムメールを送りつけるような者である。 スパムメールが、送信者認証すらなかった牧歌的なeメールの世界を破壊したのだ。

だが、ここに至るまでのひどい状況を作り出した重大な存在はGoogleとYahooだ。 GoogleとYahooが圧倒的なシェアを武器に「eメールとして正当であっても、意に染まないものは流通させない」という構図を作り出した。 これは、相手に対応を迫るだけならマシで、実際は問答無用に排除したのである。

同時に、eメールのプライバシーは今や完全に破壊されている。 それはメールのフィルタリングが中身を見るという一律機械的なものに留まらず、受信メールをマーケティングに利用することすらある。

だから個人間eメールは、もはやなくなったほうが良いレベルのものだろう。 どうせ疎通が信用できないのだから、ユーザーが信用できるプラットフォームを選ぶほうがまだマシだ。

カスタマーサービスもメールのような到達が信用できないものを使うのはやめてほしい。 サイト上にメッセージシステムを設置するか、カスタマーからの連絡はフォームから行うようにしたほうが良い。

AltPostLocalを使ってeメールの受信専用サーバーを構成する具体的手順

はじめに

この章の内容は、プロトコル, eメール, サーバーセキュリティに対して十分な知識がある人を対象としている。また、eメールサーバーの構成モデルに対する知識もあるものとしている。 安全ではないeメールサーバーは他者に重大な被害を与える可能性があるため、くれぐれも慎重に行うこと。

サーバー慣れしていない人には難しいように見えるかもしれないが、普通にメールサーバーを立てるための説明をすると、とても書けたものではないレベルの説明をした上で、大量の注意事項を並べる必要があるため、ここに書ける程度になっている時点で「AltPostLocalを使えばとても簡単に実現できるようになる」といって偽りはない。

サーバーホストを立てる

安定して運用可能なサーバーを用意し、PostfixおよびDovecotをセットアップする。

eメールのサーバーは、運用をやめるためには手順が必要で、停止後最長1ヶ月程度の運用が必要となる。そのことを考慮に入れておくこと。 なお、サーバーマイグレーションはそれほど難しくなく、同様に最長1ヶ月程度の移行期間が必要になるが、ほとんどの場合12時間以内に完了できる。

もちろん、サーバーのOSはArchlinuxを使うのが良いだろう😉

PostfixのセットアップはAltPostLocalに絡むため、とりあえずインストールするだけで良い。

また、Certbotを導入しておく。

Certbot

できればメールサーバーでwebサーバーは動かさないほうが良い。 Webメールを動作させたいのであれば必要になってくるけれど、webサーバーがないほうが楽になる。

webサーバーが動いていなければCertbotはスタンドアローン動作が可能なので、名前解決できるようにすればいきなりcerbotが叩ける。

certbot certonly --email foo@example.com -d mail.example.com

eメールは有効なものを入力するべきだが、特に検証されるわけではない。

なお、MXの名前がexmaple.comだとして、exmaple.comをサーバーに与えてしまうと取り扱いにくいので、素直にmail.example.comにしたほうが良い。

Dovecot

IMAPSだけで運用するのが良い。 ここではexample-configをコピーした状態から開始するという前提で話を進める。

DovecotはデフォルトでIMAP, POP3, LMTPを有効にしている。 が、AltPostLocalはローカル配送をDovecotに頼らないので4IMAPSだけで良い。 この設定はdovecot.confにある。

protocols = imap

listenに関わる設定はconf.d/10-master.confにあり、ここでポートに0を設定するとlistenしなくなる。 平文IMAPを無効にするには、

  inet_listener imap {
    port = 0
  }
  inet_listener imaps {
    port = 993
    ssl = yes
  }

のようにする。

好みの問題だが、PLAINLOGINでの認証を望まない場合は、conf.d/10-auth.conf

auth_mechanisms = cram-md5

のようにする。 なお、現実には

disable_plaintext_auth = yes

とすれば、平文IMAPでPLAINやLOGINで認証することはできなくなり、これで十分だと考えられる。

続いて、SSL関連。 conf.d/10-ssl.conf

ssl = required

として平文IMAPを禁止し、ssl_certfullchain.pemのパス、ssl_keyprivkey.pemのパスを入れる。

最後にユーザーの設定を行う。 デフォルトでは/etc/dovecot/usersがパスワードファイルになっており、これはpasswd(5)に従ったもの。ざっくり

$user:$pw:$uid:$gid::$home::

という形式で、このあとextra fieldsが続く。 extra fieldsを使ってMaildirの固有メールディレクトリを用意するには、userdb_mail=maildir:$pathとする。 一例としては、

foo@example.com:{plain}pass:1000:1000::/home/mail/example.com/foo@example.com::userdb_mail=maildir:/home/mail/example.com/foo@example.com/Maildir

という感じ。 なお、ドキュメントには見つけられなかったが、さらに:を1個増やして

foo@example.com:{plain}pass:1000:1000::/home/mail/example.com/foo@example.com:::Maildir:/home/mail/example.com/foo@example.com/Maildir

としても通る。

なお、ユーザー名は認証に使うユーザー名なので、メールアドレスである必要はない。 メールアドレスを使うのが一般的なのは、メールプロバイダーでのアカウント発行の都合だろう。

しかしここでバーチャルメールボックスを扱うためのメールユーザーが必要になる。 適当でいいのだけど、ある程度わかりやすいほうが良いかもしれない。

そこで、ここではRedHatのマニュアルと同じvmailユーザーを、vmailグループに所属させ、5000:5000になるようにしたい。 この場合、まずグループを用意しておく必要がある。

groupadd -g 5000 vmail

それからユーザーを追加。

useradd -u 5000 -g vmail -s /sbin/nologin -d /home/vmail vmail

conf.d/10-master.confservice authを設定

  unix_listener auth-userdb {
    mode = 0600
    user = vmail
    group = vmail
  }

箱になるホームディレクトリを用意

mkdir /home/vmail
chown vmail:vmail /home/vmail

ここまでやったらユーザーの薛悌に移っていける。

passwdファイルのパスワードフィールドにCRAM-MD5を使いたい場合は次のコマンドで生成できる。

doveadm pw -s CRAM-MD5

なお、認証メカニズムがPLAINやLOGINの場合でも、パスワードフィールドにCRAM-MD5を使うことはできる。

ユーザーの設定が終わったら満を持してDovecotを起動させる。 Dovecotの起動(再起動)が行われると、ユーザーのメールボックス等は自動的に作られる。

AltPostLocalの導入と設定

まずはAltPostLocalを入手する。 私はroot/usr/local/opt以下に置いている。

git clone "https://github.com/reasonset/altpostlocal.git"

リンクを貼っておく。

ln -s /usr/local/opt/altpostlocal/bin/altpostlocal /usr/local/bin/

設定ファイルを作る。

mkdir /etc/altpostlocal
: > /etc/altpostlocal/altpostlocal.yaml

設定ファイルを編集する。 例としては最低限こんな感じ。

---
extension: "\\+"
aliases:
  foo:
    - type: maildir
      value: /home/vmail/foo/Maildir
default:
  - type: maildir
  - value: /home/vmail/default/Maildir
rescue: /home/vmail/rescue
rescue_json: yes

注意点として、運用を開始するとAltPostLocalはこのファイルを保存した瞬間に反映されるので、中途半端な状態で保存しないように注意。コピーで作業してmvするのを勧める。

Postfixの設定

AltPostLocalの動作部分は動作概要にあるように設定する。 ユーザーは先の例ではvmailになる。

それ以外のPostfixの設定に進もう。 myhostnameはSSLに関わってくるため、certbotで認証したドメインにする。

myhostname = mail.example.com

mydomainもそれに合わせる。

mydomain = example.com

mydestinationには、受け取るすべてのドメインを列挙する

mydestination = example.com, example.net, example.org, example.org

このサーバーから外にメールを届けることは許さないので、relay_domainsは空にしておく

relay_domains =

SSLの設定。例だけ

smtpd_tls_security_level = may
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_session_cache_database = btree:/var/cache/postfix/smtpd_scache
smtp_tls_security_level = may
smtp_tls_CAfile = /etc/letsencrypt/live/mail.example.com/cert.pem
smtp_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtp_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtp_tls_session_cache_database = btree:/var/cache/postfix/smtp_scache

転送のことを考えてsmtp_も設定しているが、転送もしないならなくて良い。 このサーバーを経由した転送は拒否される可能性がそれなりに高いため、転送はしない方針のほうが良いかもしれない。 あるいは、このメールサーバーで転送するのではなく、転送先で取り込む設定ができるならそれを考えても良い。

SSLキャッシュのディレクトリを用意する。

mkdir /var/cache/postfix
chown postfix:postfix /var/cache/postfix

これでPostfixを起動すれば良い。 メールを送信しないサーバーなので、SASLやsubmissionの設定も必要なく、かなり楽。

MX設定

メールを受け取れる状態になったので、経路を開く。 DNSでMXレコードの設定を行う。 ドメインがメールアドレスのドメインパートの部分(この例ではexample.comのルート)、値がメールサーバーのAまたはAAAAレコードのホスト名(この例ではmail.example.com)となる。

注意点として、値はCNAMEレコードのホスト名を指すことは禁止されている。

諸注意

  • サーバーのenableし忘れに注意
  • AltPostLocal設定ファイルは反映する前にYAMLとしての正当性を確認すること (不正なYAMLだとpipeがコケる)