迷惑メールを一掃するため，ドメインホワイトリストを作成した

みなさん，迷惑メールで苦労していませんか？

私は苦労しています．年末の大掃除ということで，迷惑メールを一掃してしまいましょう．

## 状況

キャリアメールに毎日，5〜10 通程度の迷惑メールが着弾します．有料の迷惑メールブロック機能を使用しているのですが，ブロックできるのは 5 割程度で，何通かは私のスマホの着信音を鳴らしてしまいます．

現在個人で利用しているメールアドレスは Gmail のもので，キャリアメールはほぼ使っていません．LINE や Twitter などの SNS 全盛の今（インスタ？TikTok？何それ，おいしいの？），使用するのは家族とのやり取りのみで，他には携帯メールのアドレスでなければ登録できないサービスや，かなり昔に登録したサービスのものがたまに届く程度です．

もはや，受信するメールのほとんどが迷惑メールという状況です．1 年前くらいまではほどんど迷惑メールなんてこなかったのに，なぜほとんど使わないアドレスに急に届くようになったのでしょう．どこかが個人情報流出させたのでしょうか？

## 対応方針

使っていないメールアドレスとは言っても，全く使っていないわけではないですし，古い知り合いはこのメールアドレスしか知らないということもあります．本来ならメールアドレスを変更するのが筋でしょうが，今では疎遠になっている人たちにアドレス変更を連絡するのも億劫です．何より，アドレス変更したところでどうせまた迷惑メールは届くようになるわけですから，「たとえ迷惑メール業者にメールアドレスを知られても，迷惑メールは届かない」ようにしたいです．

迷惑メールの傾向を調べてみると，適当な文字列で取得したと思われるドメインから送信されています．送信元を偽装しているメールは受信しないように設定しているので，届いているメールに関しては送信元の偽装は行われていません．また，タイトルや本文は，芸能人を思わせるような文脈であったり，何かのサイトの料金を請求するもの，ただ何かの URL が書いてあるだけのものなど，さまざまです．一貫している特徴としては，日本語がおかしいことくらいでしょうか？

ちなみに，「鈴木さんですよね？〇〇（姓）さんから聞きました」という，疎遠になっている知人を装うメールもありました．馬鹿なのかな？私が〇〇だ．

迷惑メールであることを的確に判断するポイントはなさそうですが，正規のドメインを乗っ取るようなことまではしていないので，ホワイトリストで受信するドメインを指定するようにします．やり取りする相手が限られていることと，今後も増えることはほぼないと考えられることから，この対応で少なくとも今は問題ないでしょう．

## ホワイトリスト作成

幸運なことに，私はこれまで受信したメールは削除せず，用が済んだメールは「アーカイブ」という名前のフォルダに移動するという使い方をしています．また，迷惑メールは即迷惑メールとして報告する（報告後，ゴミ箱に移動される）か「迷惑メール」というフォルダに放り込むようにしていたため，本来のメールと迷惑メールは完全に分離されています．

このため，「アーカイブ」フォルダから送信元メールアドレスを抽出し，ドメイン部分を取り出すだけでホワイトリストを作れそうです．

いつの間にかキャリアメールも IMAP でアクセスできるのが当たり前になっていますね．NPM の [`imap`](https://www.npmjs.com/package/imap) というパッケージを使い，指定したフォルダから送信元ドメインを抽出する簡単なスクリプトを書いてみます．

最初に `imap` と，TypeScript で書きたいので `ts-node`, `typescript`, `@types/imap` をインストールします．

```console
$ yarn add imap
$ yarn add -D ts-node typescript @types/imap
```

`tsconfig.json` を書きます．

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  },
  "exclude": ["node_modules"]
}
```

スクリプト本体である `main.ts` を書きます．書き捨てのコードなのでちゃんとしたものではないですが，対象のフォルダは決めうちにせず，コマンドライン引数で対象のフォルダを指定するようにします．

```ts:main.ts
import util from 'util';
import Imap from 'imap';

const imap = new Imap({
  host: process.env.IMAP_HOST || '',
  port: parseInt(process.env.IMAP_PORT || '993', 10),
  user: process.env.IMAP_USERNAME || '',
  password: process.env.IMAP_PASSWORD || '',
  tls: (process.env.IMAP_TLS || '1') === '1'
});

// util.promisify を使うと，コールバックをとる非同期関数を
// Promise を返す関数に変換することができます．
const openBox = util.promisify(imap.openBox.bind(imap));

const getBody = (message: Imap.ImapMessage) =>
  new Promise<string>((resolve, reject) => {
    let buffer = '';
    message
      .on('body', stream => {
        stream
          .on('data', (chunk: Buffer) => (buffer += chunk.toString('utf8')))
          .on('error', reject);
      })
      .on('end', () => resolve(buffer));
  });

imap
  .once('ready', async () => {
    const domains = new Set();
    const name = process.argv[2];
    // openBox でフォルダを開きます．
    // 第 2 引数が true なら Read Only モードになります．
    // 今回は読み込みしか行わないため，安全のために true にしておきます．
    const box = await openBox(name, true);
    imap.seq
      .fetch(`1:${box.messages.total}`, {
        // bodies には取得する要素を指定するようです．
        // 欲しいのは送信元だけなので，ヘッダーの FROM だけを指定します．
        bodies: 'HEADER.FIELDS (FROM)'
      })
      .on('message', async message => {
        const body = await getBody(message);
        const headers = Imap.parseHeader(body);
        // ドメイン部分だけを取り出し，標準出力に表示します．
        headers.from
          .map(x => x.replace(/.*<(.*)>$/, '$1'))
          .map(x => x.split('@').pop())
          .forEach(domain => {
            // 一度表示したドメインは domains に格納し，
            // 2 回以上表示しないようにします．
            if (domains.has(domain)) return;
            domains.add(domain);
            console.info(domain);
          });
      })
      .on('error', err => console.error(err));
    imap.end();
  })
  .connect();
```

各種環境変数を設定します．私は [direnv](https://direnv.net/) を使っているので，パスワード以外は `.envrc` に環境変数を設定します（使用していない場合は，コマンド実行の前に設定すればいいです）．パスワードはどこにも残したくないし，画面にも一切表示されないようにしたいので，いつも　`read -s` で入力するようにしています．

```bash:.envrc
layout node

export IMAP_USERNAME=<ユーザー名>
export IMAP_HOST=<IMAP サーバー>
```



```console
$ echo -n 'Password: ' >&2; read -s p; echo >&2;
Password:
$ IMAP_PASSWORD=$p ts-node main.ts アーカイブ | tee domains.txt
mail.yahoo.co.jp
ezweb.ne.jp
accounts.google.com
k.vodafone.ne.jp
...
```

これでドメインのリストが得られました．

ここで念のため，生成されたリストが正しいことを確認しておきましょう．私の場合，1 件だけ迷惑メールが「アーカイブ」に紛れ込んでいたため，見覚えのないドメインがありました．

## ホワイトリスト設定

今回生成されたホワイトリストには 29 件のドメインがありました．もっと多かったらスクリプト書こうと思ってましたが，この程度なら手動入力でいいでしょう．

ここで一つ注意点があります．私の使用しているキャリアは docomo なのですが，ホワイトリストの設定は，実はドメインではなく，送信元メールアドレスとの後方一致で判定されると書かれています．このまま生成されたドメインを設定すると，「spammixi.com」のようなドメインからのメールを受信してしまいます．これを防ぐためには「@spammixi.com」というように「@」から書いておくとよいです．

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18183/60c2b63c-783c-700a-cb83-5686ca6394bc.png)
> https://www.mydocomo.com/dcm/dfw/web/pub2/useful/sp_mode/domain/spam.html

docomo には上のように「@mixi.jp」を許可し，「mixi.jp」を拒否すると書かれていますが，今回は PC メール全てを拒否にしているので，許可だけを指定します．

入力時に足すこともできますが，面倒なので `awk` で加工します．

```console
$ cat domains.txt | awk '$0 = "@" $0'
@mail.yahoo.co.jp
@ezweb.ne.jp
@accounts.google.com
@k.vodafone.ne.jp
...
```

これをひとつひとつコピペして設定していけば，対応完了です．

さらば，迷惑メール．

## 後始末

もう「迷惑メール」に放り込んでいたメールは不要なので，削除しました．また，有料の迷惑メールブロック機能も今後は無用の長物となってしまうので，少し様子を見て解約しようと思います．

## 昔話

そういえば大学入学して間もない頃，引っ越して 1, 2 週間くらいで，ある国連機関と同名の謎の組織からダイレクトメールが届いたことがありました．その時点で住所知ってるのって大学を除くと不動産業者，電力会社，ガス会社，水道局だけです．契約直後の個人情報売ったら足がつくのなんて誰でもわかりますし，一体どこが売ったんですかね？