ken_nogi/pleasanter/develop/留守電変換/autoMailCheckerAndUpload.js
Kenichiro NOGI 88a402ce0f up
2026-07-10 18:13:30 +09:00

495 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});