/** * rspamd 스팸 필터 서비스 * 스팸 검사, 학습 API 연동 */ const RSPAMD_HOST = process.env.RSPAMD_HOST || "rspamd"; const RSPAMD_PORT = process.env.RSPAMD_PORT || 11333; const RSPAMD_BASE_URL = `http://${RSPAMD_HOST}:${RSPAMD_PORT}`; // 스팸 점수 임계값 (이 값 이상이면 스팸으로 분류) const SPAM_THRESHOLD = 6.0; /** * 이메일 스팸 검사 * @param {string} rawEmail - 원본 이메일 데이터 (RFC822 형식) * @returns {Promise<{isSpam: boolean, score: number, action: string}>} */ const checkSpam = async (rawEmail) => { try { const response = await fetch(`${RSPAMD_BASE_URL}/checkv2`, { method: "POST", headers: { "Content-Type": "text/plain", }, body: rawEmail, }); if (!response.ok) { console.error("[rspamd] 검사 실패:", response.status); return { isSpam: false, score: 0, action: "no action" }; } const result = await response.json(); // action: no action, greylist, add header, rewrite subject, soft reject, reject const isSpam = result.score >= SPAM_THRESHOLD || result.action === "reject" || result.action === "add header" || result.action === "rewrite subject"; console.log( `[rspamd] 검사 결과: score=${result.score}, action=${result.action}, isSpam=${isSpam}` ); return { isSpam, score: result.score || 0, action: result.action || "no action", symbols: result.symbols || {}, }; } catch (error) { console.error("[rspamd] 검사 오류:", error.message); // rspamd 연결 실패 시 스팸이 아닌 것으로 처리 return { isSpam: false, score: 0, action: "no action" }; } }; /** * 이메일을 스팸으로 학습 * @param {string} rawEmail - 원본 이메일 데이터 * @returns {Promise} */ const learnSpam = async (rawEmail) => { try { const response = await fetch(`${RSPAMD_BASE_URL}/learnspam`, { method: "POST", headers: { "Content-Type": "text/plain", }, body: rawEmail, }); if (!response.ok) { console.error("[rspamd] 스팸 학습 실패:", response.status); return false; } const result = await response.json(); console.log("[rspamd] 스팸 학습 완료:", result); return result.success !== false; } catch (error) { console.error("[rspamd] 스팸 학습 오류:", error.message); return false; } }; /** * 이메일을 정상 메일(ham)로 학습 * @param {string} rawEmail - 원본 이메일 데이터 * @returns {Promise} */ const learnHam = async (rawEmail) => { try { const response = await fetch(`${RSPAMD_BASE_URL}/learnham`, { method: "POST", headers: { "Content-Type": "text/plain", }, body: rawEmail, }); if (!response.ok) { console.error("[rspamd] 햄 학습 실패:", response.status); return false; } const result = await response.json(); console.log("[rspamd] 햄 학습 완료:", result); return result.success !== false; } catch (error) { console.error("[rspamd] 햄 학습 오류:", error.message); return false; } }; /** * rspamd 상태 확인 * @returns {Promise} */ const checkHealth = async () => { try { const response = await fetch(`${RSPAMD_BASE_URL}/ping`, { method: "GET", }); return response.ok; } catch { return false; } }; /** * 이메일 객체를 RFC822 형식으로 변환 * @param {Object} email - 이메일 객체 (from, to, subject, text, html) * @returns {string} */ const buildRawEmail = (email) => { const boundary = "----=_Part_" + Date.now(); let raw = ""; raw += `From: ${email.from || "unknown@unknown.com"}\r\n`; raw += `To: ${email.to || "unknown@unknown.com"}\r\n`; raw += `Subject: ${email.subject || "(no subject)"}\r\n`; raw += `Date: ${ email.date ? new Date(email.date).toUTCString() : new Date().toUTCString() }\r\n`; raw += `Message-ID: ${email.messageId || `<${Date.now()}@local>`}\r\n`; raw += `MIME-Version: 1.0\r\n`; if (email.html) { raw += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`; raw += `--${boundary}\r\n`; raw += `Content-Type: text/plain; charset=utf-8\r\n\r\n`; raw += `${email.text || ""}\r\n`; raw += `--${boundary}\r\n`; raw += `Content-Type: text/html; charset=utf-8\r\n\r\n`; raw += `${email.html}\r\n`; raw += `--${boundary}--\r\n`; } else { raw += `Content-Type: text/plain; charset=utf-8\r\n\r\n`; raw += `${email.text || ""}\r\n`; } return raw; }; module.exports = { checkSpam, learnSpam, learnHam, checkHealth, buildRawEmail, SPAM_THRESHOLD, };