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を回避できる
- ドメイン調査ツールで実態を把握してからホワイトリストを作ると精度が上がる
企業を追加するたびにホワイトリストを育てていく運用で、受信ボックスをクリーンに保てます。
参考
- imapflow ドキュメント
- Termux公式
- [Yahoo! JAPAN IMAPの設定](https://support.yahoo-net.jp/PccMail/s/article/H000007"""