"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, };