163 lines
6.0 KiB
JavaScript
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,
|
|
};
|