mailbox/backend/services/rspamdService.js

175 lines
4.6 KiB
JavaScript
Raw Normal View History

2025-12-16 08:18:15 +09:00
/**
* 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<boolean>}
*/
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<boolean>}
*/
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<boolean>}
*/
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,
};