ken_nogi/PleasanterSystem/dbbackup/lib/mailNotify.js
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

163 lines
6.0 KiB
JavaScript

"use strict";
/*
* バックアップ処理結果をローカルPostfix経由(→SendGridリレー)でメール通知する。
* 追加npm依存を避けるため、MIMEメッセージを自前で組み立てて `sendmail -t` にパイプする。
*/
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
function encodeMimeHeader(text) {
if (/^[\x20-\x7e]*$/.test(text)) return text;
const b64 = Buffer.from(text, "utf8").toString("base64");
return `=?UTF-8?B?${b64}?=`;
}
function base64Wrap(buffer) {
const b64 = buffer.toString("base64");
const lines = [];
for (let i = 0; i < b64.length; i += 76) {
lines.push(b64.slice(i, i + 76));
}
return lines.join("\r\n");
}
// ログファイルを添付したmultipart/mixedのMIMEメッセージ(生テキスト)を組み立てる。
function buildMimeMessage({ from, to, subject, bodyText, attachmentPath }) {
const boundary = `----dbbackup-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const headerLines = [
`From: ${from}`,
`To: ${to}`,
`Subject: ${encodeMimeHeader(subject)}`,
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="${boundary}"`,
];
const bodyLines = [
`--${boundary}`,
'Content-Type: text/plain; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
base64Wrap(Buffer.from(bodyText, "utf8")),
"",
];
if (attachmentPath && fs.existsSync(attachmentPath)) {
const attachmentName = path.basename(attachmentPath);
const content = fs.readFileSync(attachmentPath);
bodyLines.push(
`--${boundary}`,
`Content-Type: text/plain; name="${attachmentName}"`,
`Content-Disposition: attachment; filename="${attachmentName}"`,
"Content-Transfer-Encoding: base64",
"",
base64Wrap(content),
""
);
}
bodyLines.push(`--${boundary}--`, "");
return headerLines.join("\r\n") + "\r\n\r\n" + bodyLines.join("\r\n");
}
function sendRawMessage({ raw, sendmailBin, from, spawnFn = spawn }) {
return new Promise((resolve, reject) => {
// -f でエンベロープFromをヘッダーのFromと一致させる(未指定だと実行ユーザー宛のroot@hostnameになり、
// 受信側でヘッダーFromとの不一致によりスパム判定・拒否される恐れがあるため)。
const child = spawnFn(sendmailBin, ["-t", "-i", "-f", from], { stdio: ["pipe", "pipe", "pipe"] });
let stderr = "";
child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
child.on("error", (err) => reject(new Error(`sendmail起動失敗: ${err.message}`)));
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(`sendmail異常終了 (code=${code}): ${stderr.trim()}`));
return;
}
resolve();
});
child.stdin.end(raw);
});
}
async function sendMail({ from, to, subject, bodyText, attachmentPath, sendmailBin, spawnFn }) {
const raw = buildMimeMessage({ from, to, subject, bodyText, attachmentPath });
await sendRawMessage({ raw, sendmailBin, from, spawnFn });
}
function requireMailConfig() {
const from = process.env.MAIL_FROM;
const to = process.env.MAIL_TO;
const sendmailBin = process.env.MAIL_SENDMAIL_BIN || "/usr/sbin/sendmail";
return { from, to, sendmailBin };
}
// バックアップ処理完了後(成功/失敗いずれも)に結果報告メールを送る。
// MAIL_FROM/MAIL_TOが未設定の場合はスキップする(メール通知は必須機能ではないため)。
async function sendBackupReportMail({ success, hostname, database, elapsedSec, errorMessage, logFilePath }, deps = {}) {
const logger = deps.logger || require("./logger");
const sendMailFn = deps.sendMail || sendMail;
const { from, to, sendmailBin } = requireMailConfig();
if (!from || !to) {
logger.warn("MAIL_FROM/MAIL_TO が未設定のため、メール通知はスキップします");
return;
}
const statusLabel = success ? "成功" : "失敗";
const subject = `[dbbackup][${hostname}] バックアップ${statusLabel} (${database})`;
const lines = [
`サーバー: ${hostname}`,
`対象DB: ${database}`,
`結果: ${statusLabel}`,
`所要時間: ${elapsedSec}`,
];
if (!success && errorMessage) {
lines.push(`エラー内容: ${errorMessage}`);
}
lines.push("", "詳細は添付のログファイルを参照してください。");
try {
logger.info("メール通知送信開始");
await sendMailFn({ from, to, subject, bodyText: lines.join("\n"), attachmentPath: logFilePath, sendmailBin });
logger.info("メール通知送信成功");
} catch (err) {
logger.error(`メール通知送信失敗: ${err.message}`);
}
}
// Postfix→SendGridリレーの疎通確認用。backup.jsの実行フローとは独立して呼び出せる。
async function sendTestMail(deps = {}) {
const logger = deps.logger || require("./logger");
const sendMailFn = deps.sendMail || sendMail;
const { from, to, sendmailBin } = requireMailConfig();
if (!from || !to) {
throw new Error("MAIL_FROM/MAIL_TO が未設定です。.envに設定してください。");
}
const now = new Date();
const subject = `[dbbackup] テスト送信 ${now.toISOString()}`;
const bodyText = [
"これはdbbackupのメール通知機能のテスト送信です。",
`送信元: ${from}`,
`送信先: ${to}`,
`送信日時: ${now.toString()}`,
].join("\n");
logger.info(`テストメール送信開始 (from=${from} to=${to})`);
await sendMailFn({ from, to, subject, bodyText, sendmailBin });
logger.info("テストメール送信成功");
}
module.exports = {
buildMimeMessage,
sendMail,
sendBackupReportMail,
sendTestMail,
encodeMimeHeader,
};