495 lines
18 KiB
JavaScript
495 lines
18 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const POP3Client = require('poplib');
|
||
const axios = require('axios');
|
||
const simpleParser = require('mailparser').simpleParser;
|
||
|
||
// ==============================
|
||
// ログファイル設定
|
||
// ==============================
|
||
const SCRIPT_NAME = path.basename(__filename, '.js');
|
||
const LOG_FILE = path.join(__dirname, `${SCRIPT_NAME}.log`);
|
||
|
||
// ログ出力関数
|
||
function appendLog(message) {
|
||
fs.appendFileSync(LOG_FILE, message + '\n', { encoding: 'utf8' });
|
||
}
|
||
|
||
// console.log/console.errorを上書き
|
||
const origLog = console.log;
|
||
const origError = console.error;
|
||
console.log = (...args) => {
|
||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||
appendLog(msg);
|
||
origLog(...args);
|
||
};
|
||
console.error = (...args) => {
|
||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||
appendLog('[ERROR] ' + msg);
|
||
origError(...args);
|
||
};
|
||
|
||
// ==============================
|
||
// 設定
|
||
// ==============================
|
||
|
||
// メールサーバー設定
|
||
const POP3_HOST = 'sv304.xbiz.ne.jp';
|
||
const POP3_PORT = 110; // POP3(STARTTLS)
|
||
const POP3_USER = 'rpa.mailcheck.001@next-hd.co.jp';
|
||
const POP3_PASS = 'KWtQhG$hf9XF#62$';
|
||
|
||
// メール受信後にサーバーから削除するかどうかのフラグ
|
||
// true : 受信後に POP3 サーバーから削除する
|
||
// false : サーバー上には残したままにする
|
||
const DELETE_FROM_SERVER = true;
|
||
|
||
// Pleasanter にデータをアップロードするかどうかのフラグ
|
||
// true : Pleasanter へアップロードする
|
||
// false : Pleasanter へのアップロードをスキップする
|
||
const UPLOAD_TO_PLEASANTER = true;
|
||
|
||
// 取り込み済みの EML ファイルを完了フォルダ(eml_done)へ移動するかどうかのフラグ
|
||
// true : 判定・処理後に eml_done フォルダへ移動する(従来どおり)
|
||
// false : eml_files フォルダに残したままにする
|
||
const MOVE_EML_TO_DONE = true;
|
||
|
||
// EML ファイルの保存先・完了後移動先
|
||
const EML_SOURCE_DIR = path.join(__dirname, 'eml_files');
|
||
const EML_DONE_DIR = path.join(__dirname, 'eml_done');
|
||
|
||
// 必要ならフォルダを作成
|
||
if (!fs.existsSync(EML_SOURCE_DIR)) {
|
||
fs.mkdirSync(EML_SOURCE_DIR, { recursive: true });
|
||
}
|
||
if (!fs.existsSync(EML_DONE_DIR)) {
|
||
fs.mkdirSync(EML_DONE_DIR, { recursive: true });
|
||
}
|
||
|
||
// Pleasanter 登録用設定
|
||
const PLEASANTER_API_KEY = '6504c8a807677a3a576e10327f3c19876c55736ee45d4a845796b9e7f5e087bfd4bd0d8184863da8cf1733ca635111432ab334aea59102b06a96ee6d2c05190d';
|
||
const PLEASANTER_SITE_ID = '451995'; // ←必要なら変更
|
||
|
||
// ==============================
|
||
// キーワード設定(正規表現で判定)
|
||
// ==============================
|
||
const KEYWORDS = [
|
||
'NEXTGROUP IP-PBX ボイスメールシステム'
|
||
// ここに追加可能
|
||
];
|
||
const KEYWORD_REGEX = new RegExp(KEYWORDS.join('|'), 'i');
|
||
|
||
// ==============================
|
||
// メッセージ解析(checkMessage.js 相当)
|
||
// ==============================
|
||
|
||
function extractMessageFields(bodyText = '') {
|
||
const body = bodyText || '';
|
||
|
||
// まずは通常の日本語本文パターン
|
||
let numberMatch = body.match(/番号:\s*"([0-9]+)"\s*<([0-9]+)>/);
|
||
let lengthMatch = body.match(/長さ:\s*([0-9:]+)\s*seconds/i);
|
||
let dateMatch = body.match(/日付:\s*([0-9\/\- :]+)/);
|
||
let mailboxMatch = body.match(/メールボックス番号:([0-9]+)/);
|
||
|
||
// ==============================
|
||
// フォールバックパターン(文字化け気味のボイスメール本文用)
|
||
// ==============================
|
||
|
||
// 例: "09049200643" <09049200643>
|
||
if (!numberMatch) {
|
||
const altNumber = body.match(/"(\d{7,})"\s*<\1>/);
|
||
if (altNumber) {
|
||
// index 1 を 2 番目と同じにして既存の扱いと合わせる
|
||
numberMatch = [, altNumber[1], altNumber[1]];
|
||
}
|
||
}
|
||
|
||
// 例: 0:27 seconds, 0:03 seconds
|
||
if (!lengthMatch) {
|
||
lengthMatch = body.match(/(\d+:\d{2})\s*seconds/i);
|
||
}
|
||
|
||
// 例: 2026/02/20 - 12:58:00
|
||
if (!dateMatch) {
|
||
dateMatch = body.match(/(\d{4}\/\d{2}\/\d{2}\s*-\s*\d{2}:\d{2}:\d{2})/);
|
||
}
|
||
|
||
// メールボックス番号のフォールバック
|
||
// 例: 0504: (前後は文字化けしていても 4 桁+コロンは残っている)
|
||
if (!mailboxMatch) {
|
||
const altBox = body.match(/\b(\d{4}):/);
|
||
if (altBox) {
|
||
mailboxMatch = [, altBox[1]];
|
||
}
|
||
}
|
||
|
||
return {
|
||
number: numberMatch ? numberMatch[2] : null,
|
||
length: lengthMatch ? lengthMatch[1] : null,
|
||
date: dateMatch ? dateMatch[1].trim() : null,
|
||
mailbox: mailboxMatch ? mailboxMatch[1] : null
|
||
};
|
||
}
|
||
|
||
// ==============================
|
||
// POP3 でメール受信(autoMailChecker 相当)
|
||
// ==============================
|
||
|
||
function receiveEmails() {
|
||
return new Promise((resolve, reject) => {
|
||
let totalMessages = 0;
|
||
let currentMessage = 1;
|
||
let finished = false;
|
||
|
||
const client = new POP3Client(POP3_PORT, POP3_HOST, {
|
||
tlserrs: false,
|
||
enabletls: false, // ←ここをfalseに
|
||
debug: false
|
||
});
|
||
|
||
const safeResolve = () => {
|
||
if (!finished) {
|
||
finished = true;
|
||
resolve();
|
||
}
|
||
};
|
||
|
||
const safeReject = (err) => {
|
||
if (!finished) {
|
||
finished = true;
|
||
reject(err);
|
||
}
|
||
};
|
||
|
||
client.on('error', (err) => {
|
||
console.error('POP3 エラー:', err);
|
||
try {
|
||
client.quit();
|
||
} catch (e) {
|
||
// 無視
|
||
}
|
||
safeReject(err);
|
||
});
|
||
|
||
client.on('connect', () => {
|
||
console.log('POP3 サーバーに接続しました');
|
||
client.login(POP3_USER, POP3_PASS);
|
||
});
|
||
|
||
client.on('invalid-state', (cmd) => {
|
||
console.error('POP3 無効な状態でコマンドが呼ばれました:', cmd);
|
||
});
|
||
|
||
client.on('locked', (cmd) => {
|
||
console.error('POP3 クライアントがロック中です。コマンド:', cmd);
|
||
});
|
||
|
||
client.on('login', (status, rawData) => {
|
||
if (!status) {
|
||
console.error('ログイン失敗:', rawData);
|
||
client.quit();
|
||
safeReject(new Error('POP3 ログイン失敗'));
|
||
return;
|
||
}
|
||
|
||
console.log('ログイン成功');
|
||
client.stat(); // メール件数を取得
|
||
});
|
||
|
||
client.on('stat', (status, data, rawData) => {
|
||
if (!status) {
|
||
console.error('STAT 取得失敗:', rawData);
|
||
client.quit();
|
||
safeReject(new Error('POP3 STAT 失敗'));
|
||
return;
|
||
}
|
||
|
||
// poplib の実装差異を吸収しつつ、確実に件数だけを取得する
|
||
let msgCount = 0;
|
||
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
// 期待どおり [件数, サイズ] で来るケース
|
||
msgCount = parseInt(data[0], 10) || 0;
|
||
} else if (typeof data === 'string') {
|
||
// "1 1234" のような文字列で来るケース
|
||
const m = data.match(/^(\d+)/);
|
||
if (m) {
|
||
msgCount = parseInt(m[1], 10) || 0;
|
||
}
|
||
}
|
||
|
||
if ((!msgCount || Number.isNaN(msgCount)) && rawData) {
|
||
// 念のため rawData からも " +OK 3 1234" のような形式をパース
|
||
const m2 = rawData.toString().match(/\+OK\s+(\d+)/);
|
||
if (m2) {
|
||
msgCount = parseInt(m2[1], 10) || 0;
|
||
}
|
||
}
|
||
|
||
totalMessages = msgCount;
|
||
console.log(`サーバー上のメッセージ数: ${totalMessages}`);
|
||
|
||
if (totalMessages === 0) {
|
||
console.log('新しいメールはありません');
|
||
client.quit();
|
||
return; // quit イベントで resolve
|
||
}
|
||
|
||
currentMessage = 1;
|
||
client.retr(currentMessage);
|
||
});
|
||
|
||
client.on('retr', (status, msgNumber, data, rawData) => {
|
||
if (!status) {
|
||
console.error(`メッセージ ${msgNumber} の取得に失敗:`, rawData);
|
||
// 失敗したメッセージは削除せず、次へ進む
|
||
if (msgNumber < totalMessages) {
|
||
currentMessage = msgNumber + 1;
|
||
client.retr(currentMessage);
|
||
} else {
|
||
client.quit();
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log(`メッセージ ${msgNumber} を取得しました。ファイルへ保存します。`);
|
||
|
||
// 一意なファイル名を生成(メッセージ番号 + タイムスタンプ)
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const filename = `message_${String(msgNumber).padStart(4, '0')}_${timestamp}.eml`;
|
||
const filePath = path.join(EML_SOURCE_DIR, filename);
|
||
|
||
const emlBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'binary'); // preserve raw bytes to avoid mojibake
|
||
|
||
fs.writeFile(filePath, emlBuffer, (err) => {
|
||
if (err) {
|
||
console.error(`ファイル保存エラー (${filePath}):`, err);
|
||
// 保存に失敗した場合は削除しない
|
||
if (msgNumber < totalMessages) {
|
||
currentMessage = msgNumber + 1;
|
||
client.retr(currentMessage);
|
||
} else {
|
||
client.quit();
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log(`メッセージ ${msgNumber} を ${filePath} に保存しました。`);
|
||
|
||
// フラグに応じてサーバーから削除するかどうかを制御
|
||
if (DELETE_FROM_SERVER) {
|
||
console.log('設定によりサーバーから削除します。');
|
||
client.dele(msgNumber);
|
||
} else {
|
||
console.log('設定によりサーバーから削除しません。');
|
||
|
||
// 削除しない場合も次のメッセージへ進む
|
||
if (msgNumber < totalMessages) {
|
||
currentMessage = msgNumber + 1;
|
||
client.retr(currentMessage);
|
||
} else {
|
||
console.log('すべてのメッセージの受信が完了しました(サーバーからは削除していません)。');
|
||
client.quit();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
client.on('dele', (status, msgNumber, rawData) => {
|
||
if (!status) {
|
||
console.error(`メッセージ ${msgNumber} の削除に失敗:`, rawData);
|
||
} else {
|
||
console.log(`メッセージ ${msgNumber} をサーバーから削除しました。`);
|
||
}
|
||
|
||
if (msgNumber < totalMessages) {
|
||
currentMessage = msgNumber + 1;
|
||
client.retr(currentMessage);
|
||
} else {
|
||
console.log('すべてのメッセージの受信・削除が完了しました');
|
||
client.quit();
|
||
}
|
||
});
|
||
|
||
client.on('quit', (status, rawData) => {
|
||
if (!status) {
|
||
console.error('QUIT 失敗:', rawData);
|
||
} else {
|
||
console.log('POP3 セッションを終了しました');
|
||
}
|
||
safeResolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==============================
|
||
// EML 解析 & Pleasanter 送信(uploadMessageDataFromEML 相当)
|
||
// ==============================
|
||
|
||
async function processEmlFiles() {
|
||
try {
|
||
const files = fs.readdirSync(EML_SOURCE_DIR).filter(file => file.endsWith('.eml'));
|
||
|
||
for (const file of files) {
|
||
const filePath = path.join(EML_SOURCE_DIR, file);
|
||
const parsed = await simpleParser(fs.createReadStream(filePath));
|
||
|
||
const messageData = {
|
||
from: parsed.from?.text,
|
||
to: parsed.to?.text,
|
||
subject: parsed.subject,
|
||
text: parsed.text,
|
||
html: parsed.html,
|
||
date: parsed.date
|
||
};
|
||
|
||
console.log(`Processing: ${file}`);
|
||
console.log('Message:', messageData);
|
||
const messageFields = extractMessageFields(messageData.text);
|
||
console.log('Parsed message fields:', messageFields);
|
||
|
||
// 件名にキーワードが含まれているか判定
|
||
if (!messageData.subject || !KEYWORD_REGEX.test(messageData.subject)) {
|
||
console.log('件名にキーワードが含まれていないためスキップ:', messageData.subject);
|
||
// 完了フォルダへ移動する設定の場合のみ移動
|
||
if (MOVE_EML_TO_DONE) {
|
||
// 取り込み完了したemlファイルを移動(ファイル名の先頭に「【除外】」を付与、重複時は連番)
|
||
const ext = path.extname(file);
|
||
const base = path.basename(file, ext);
|
||
let excludedName = `【除外】${base}${ext}`;
|
||
let destPath = path.join(EML_DONE_DIR, excludedName);
|
||
let count = 1;
|
||
while (fs.existsSync(destPath)) {
|
||
excludedName = `【除外】${base}(${count})${ext}`;
|
||
destPath = path.join(EML_DONE_DIR, excludedName);
|
||
count++;
|
||
}
|
||
fs.renameSync(filePath, destPath);
|
||
console.log(`Moved ${file} to ${destPath}`);
|
||
} else {
|
||
console.log('設定により完了フォルダへの移動を行いません(除外メール)。');
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 添付ファイル確認
|
||
if (parsed.attachments?.length > 0) {
|
||
for (const attachment of parsed.attachments) {
|
||
console.log(`Attachment: ${attachment.filename}`);
|
||
}
|
||
}
|
||
|
||
// Pleasanter へアップロード(最初の添付ファイルのみ)
|
||
if (parsed.attachments?.length > 0) {
|
||
const firstAttachment = parsed.attachments[0];
|
||
const base64Data = firstAttachment.content.toString('base64');
|
||
|
||
if (UPLOAD_TO_PLEASANTER) {
|
||
await uploadToPleasanter(messageData, firstAttachment.filename, base64Data, messageFields);
|
||
} else {
|
||
console.log('設定により Pleasanter へのアップロードをスキップします。');
|
||
}
|
||
} else {
|
||
console.log('添付ファイルがないため Pleasanter へのアップロードはスキップします。');
|
||
}
|
||
|
||
// 完了フォルダへ移動する設定の場合のみ移動
|
||
if (MOVE_EML_TO_DONE) {
|
||
// 取り込み完了したemlファイルを移動(ファイル名の先頭に【済】を付与、重複時は連番)
|
||
const ext = path.extname(file);
|
||
const base = path.basename(file, ext);
|
||
let doneName = `【済】${base}${ext}`;
|
||
let destPath = path.join(EML_DONE_DIR, doneName);
|
||
let count = 1;
|
||
while (fs.existsSync(destPath)) {
|
||
doneName = `【済】${base}(${count})${ext}`;
|
||
destPath = path.join(EML_DONE_DIR, doneName);
|
||
count++;
|
||
}
|
||
fs.renameSync(filePath, destPath);
|
||
console.log(`Moved ${file} to ${destPath}`);
|
||
} else {
|
||
console.log('設定により完了フォルダへの移動を行いません(正常メール)。');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error processing EML files:', error);
|
||
}
|
||
}
|
||
|
||
async function uploadToPleasanter(messageData, filename, base64Content, messageFields = {}) {
|
||
const url = `https://nextoffice.next-hd.co.jp/pleasanter/api/items/${PLEASANTER_SITE_ID}/create`;
|
||
|
||
//const lines = (messageData.text || '').split('\n');
|
||
//const extractedLines = lines.slice(6, 9).join('\n');
|
||
let bodyMessage = '';
|
||
|
||
const classHash = {};
|
||
if (messageFields.number) {
|
||
classHash.ClassA = messageFields.number;
|
||
bodyMessage += `番号: ${messageFields.number}\n`;
|
||
}
|
||
if (messageFields.length) {
|
||
classHash.ClassB = messageFields.length;
|
||
bodyMessage += `長さ: ${messageFields.length}\n`;
|
||
}
|
||
if (messageFields.date) {
|
||
classHash.ClassC = messageFields.date;
|
||
bodyMessage += `日付: ${messageFields.date}\n`;
|
||
}
|
||
if (messageFields.mailbox) {
|
||
classHash.ClassD = messageFields.mailbox;
|
||
bodyMessage += `メールボックス: ${messageFields.mailbox}\n`;
|
||
}
|
||
|
||
|
||
const uploadData = {
|
||
ApiVersion: 1.1,
|
||
ApiKey: PLEASANTER_API_KEY,
|
||
Body: `${bodyMessage}`,
|
||
ClassHash: classHash,
|
||
AttachmentsHash: {
|
||
AttachmentsA: [
|
||
{
|
||
Name: filename,
|
||
Base64: base64Content
|
||
}
|
||
]
|
||
}
|
||
};
|
||
|
||
try {
|
||
const response = await axios.post(url, uploadData);
|
||
console.log('Pleasanter登録結果:', response.data);
|
||
} catch (error) {
|
||
console.error('Pleasanter登録エラー:', error.response?.data || error.message);
|
||
}
|
||
}
|
||
|
||
// ==============================
|
||
// メイン処理
|
||
// ==============================
|
||
|
||
async function main() {
|
||
// ログファイルに開始時刻を記録
|
||
const startTime = new Date().toISOString();
|
||
appendLog(`\n=== 処理開始: ${startTime} ===`);
|
||
|
||
console.log('=== メール受信(POP3) 開始 ===');
|
||
try {
|
||
await receiveEmails();
|
||
} catch (err) {
|
||
console.error('メール受信中にエラーが発生しました:', err);
|
||
}
|
||
console.log('=== メール受信(POP3) 終了 ===');
|
||
|
||
console.log('=== EML ファイル解析 & Pleasanter 送信開始 ===');
|
||
await processEmlFiles();
|
||
console.log('=== すべての処理が完了しました ===');
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error('致命的なエラー:', err);
|
||
});
|