0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Yahooメールのなりすましスパムを自動検出してゴミ箱へ移動するスクリプト

0
Posted at

AndroidのTermuxからYahooメールのなりすましスパムを自動検出してゴミ箱へ移動する

はじめに

Yahooメールの受信ボックスに「楽天」や「SBI証券」を名乗りながら、全く関係ないドメインから送られてくるなりすましメールが大量に届いていました。

手動で削除するのは限界があるため、AndroidスマホのTermux上でNode.jsを動かし、IMAPでYahooメールサーバーに直接接続してなりすましメールを自動検出・ゴミ箱移動するスクリプトを作成しました。


目標の構成

Androidスマホ
    ↓
Termux(Linux環境)
    ↓
Node.js(imapflowライブラリ)
    ↓
YahooメールサーバーへIMAP接続
    ↓
「楽天」「SBI証券」を名乗るが正規ドメイン以外からのメールを検出
    ↓
Trashフォルダへ移動

環境構築

Termuxのセットアップ

pkg update && pkg upgrade -y
pkg install nodejs -y
mkdir ~/yahooMailFilter
cd ~/yahooMailFilter
npm init -y
npm install imapflow dotenv

認証情報の管理(.envファイル)

パスワードをコードに直書きしないため、dotenvで管理します。

cat > .env << 'EOF'
YAHOO_USER=あなたのYahooアドレス@yahoo.co.jp
YAHOO_PASS=アプリパスワード
EOF
chmod 600 .env

重要: YahooメールはIMAPアクセスを有効化し、アプリパスワードを発行する必要があります。通常のログインパスワードでは認証が拒否されます。


詰まった点と解決策

1. IMAPサーバーへの接続情報

Yahoo! JAPANのIMAP設定は以下の通りです:

項目 値
ホスト名 imap.mail.yahoo.co.jp
ポート 993
暗号化 SSL/TLS

2. Socket timeoutエラー

ループ中に messageMove を実行するとサーバーが切断されタイムアウトが発生しました。

原因: fetchループ中に移動処理を同時実行すると接続が競合する。

解決策: 2段階に分けて処理する。

ステップ1:全件スキャンして怪しいメールのUIDをリストに収集
    ↓
ステップ2:ループ終了後に1件ずつ新規接続で移動

3. 移動のたびに新規接続する

Yahooサーバーは移動処理後に接続を切断することがあります。そのため毎回新規接続する方式を採用しました。

function createClient() {
    return new ImapFlow({
        host: 'imap.mail.yahoo.co.jp',
        port: 993,
        secure: true,
        auth: {
            user: process.env.YAHOO_USER,
            pass: process.env.YAHOO_PASS
        },
        logger: false
    });
}

4. フォルダ名の確認

Yahooメールのゴミ箱フォルダはIMAPでは Trash、迷惑メールフォルダは Bulk Mail です。以下のコードでフォルダ名を確認できます。

const folders = await client.list();
folders.forEach(f => console.log(f.path));

5. ホワイトリスト方式でなりすまし検出

楽天を名乗るメールのドメインを調査したところ、正規ドメインは複数ありました(@rakuten.co.jp、@mail.rakuten-card.co.jp など)。

endsWith は引数を1つしか取れないため、配列と some() を組み合わせた方式を採用しました。

// NG:2番目以降の引数は無視される
!address.endsWith('@rakuten.co.jp', '@mail.rakuten.com')

// OK:配列とsome()で全ドメインをチェック
const whitelist = ['@rakuten.co.jp', '@mail.rakuten.com'];
!whitelist.some(d => address.endsWith(d))

完成した本番コード

require('dotenv').config();
const { ImapFlow } = require('imapflow');

/* IMAPクライアントを作成する関数 */
function createClient() {
    return new ImapFlow({
        host: 'imap.mail.yahoo.co.jp',
        port: 993,
        secure: true,
        auth: {
            user: process.env.YAHOO_USER,
            pass: process.env.YAHOO_PASS
        },
        logger: false
    });
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

(async () => {
    const client1 = createClient();
    await client1.connect();
    await client1.mailboxOpen('INBOX');

    const targetUids = [];

    /* 楽天用ホワイトリスト */
    const whiteAddressList = [
        '@rakuten.co.jp',
        '@mail.rakuten.com',
        '@shop.rakuten.co.jp',
        '@mkrm.rakuten.co.jp',
        '@info.rakuten.co.jp',
        '@id.rakuten.co.jp',
        '@faq.rakuten.co.jp',
        '@emagazine.rakuten.co.jp',
        '@account.rakuten.com',
        '@mobile.rakuten.co.jp',
        '@rakuten-card.co.jp',
        '@mail.rakuten-card.co.jp',
        '@point.rakuten.co.jp',
        '@pointcard.rakuten.co.jp'
    ];

    /* SBI用ホワイトリスト */
    const sbiWhiteList = [
        '@sbisec.co.jp',
        '@mail.sbisec.co.jp'
    ];

    /* 全件スキャン */
    for await (let msg of client1.fetch('1:*', { uid: true, envelope: true })) {
        const from = msg.envelope.from?.[0];
        if (!from) continue;

        const address = from.address || '';
        const nameLower = (from.name || '').toLowerCase();

        /* 楽天のなりすまし判定 */
        if (from.name && from.name.includes('楽天') &&
            !whiteAddressList.some(wA => address.endsWith(wA))) {
            console.log('楽天なりすまし検出:', from.name, address, 'UID:', msg.uid);
            targetUids.push(msg.uid);
        }

        /* SBIのなりすまし判定 */
        if (nameLower.includes('sbi') &&
            !sbiWhiteList.some(wA => address.endsWith(wA))) {
            console.log('SBIなりすまし検出:', from.name, address, 'UID:', msg.uid);
            targetUids.push(msg.uid);
        }
    }

    try { await client1.logout(); } catch (e) {}
    console.log(`検出件数: ${targetUids.length}件`);

    /* 1件ごとに新規接続でゴミ箱へ移動 */
    for (const uid of targetUids) {
        const client2 = createClient();
        try {
            await client2.connect();
            await client2.mailboxOpen('INBOX');
            await client2.messageMove(uid, 'Trash', { uid: true });
            console.log('移動成功 UID:', uid);
        } catch (err) {
            console.error('移動失敗 UID:', uid, err.message);
        } finally {
            try { await client2.logout(); } catch (e) {}
        }
        await sleep(2000);
    }

    console.log('=== 完了 ===');
})();

ドメイン調査ツール

新しい企業を追加する前に、実際に届いているメールのドメインを調査するツールも作成しました。

require('dotenv').config();
const { ImapFlow } = require('imapflow');

function createClient() {
    return new ImapFlow({
        host: 'imap.mail.yahoo.co.jp',
        port: 993,
        secure: true,
        auth: {
            user: process.env.YAHOO_USER,
            pass: process.env.YAHOO_PASS
        },
        logger: false
    });
}

(async () => {
    const client = createClient();
    await client.connect();
    await client.mailboxOpen('INBOX');

    const domainMap = {};
    const keyword = 'SBI'; // 調査したいキーワード

    for await (let msg of client.fetch('1:*', { uid: true, envelope: true })) {
        const from = msg.envelope.from?.[0];
        if (!from) continue;

        const name = from.name || '';
        const address = from.address || '';

        if (name.toLowerCase().includes(keyword.toLowerCase()) ||
            address.toLowerCase().includes(keyword.toLowerCase())) {
            const domain = address.split('@')[1] || 'unknown';
            domainMap[domain] = (domainMap[domain] || 0) + 1;
            console.log('送信者名:', name, 'アドレス:', address);
        }
    }

    try { await client.logout(); } catch (e) {}

    console.log(`\n=== ${keyword}ドメイン一覧 ===`);
    Object.entries(domainMap).forEach(([domain, count]) => {
        console.log(`${domain}: ${count}件`);
    });
})();

新しい企業を追加する手順

①調査ツールでドメイン一覧を取得
    ↓
②公式サイトと照合して正規ドメインを特定
    ↓
③本番コードにホワイトリストと判定処理を追加
// 追加例(三井住友の場合)
const smccWhiteList = [
    '@smbc-card.com',
    '@mail.smbc-card.com'
];
if (nameLower.includes('三井住友') &&
    !smccWhiteList.some(wA => address.endsWith(wA))) {
    targetUids.push(msg.uid);
}

まとめ

  • TermuxとNode.jsでYahooメールのIMAPに接続できる
  • なりすましメールはホワイトリスト方式で効率よく検出できる
  • 移動処理は毎回新規接続することでSocket timeoutを回避できる
  • ドメイン調査ツールで実態を把握してからホワイトリストを作ると精度が上がる

企業を追加するたびにホワイトリストを育てていく運用で、受信ボックスをクリーンに保てます。


参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?