Postfix에서 메일을 Slack으로 포워딩하는 방법
안녕하세요, 시스템컨설턴트그룹28기 김준호입니다.
시스템컨설턴트그룹에서는 Postfix 기반의 메일 서버를 운영하며 부원들에게 각자의 이메일 계정을 발급하고 있습니다. 외부와의 소통이나 여러 요청 사항은 주로 회장메일을 통해 주고받습니다.
기존에는 회장메일로 도착한 메일을 담당자가 직접 확인한 뒤 필요한 내용을 부원들에게 공유했지만, 직접 메일함에 접속해야 하는 번거로움과 부원들에 대한 접근성이 낮다는 문제가 있었습니다.
이를 개선하기 위해 회장메일로 들어온 이메일을 Slack 채널로 자동 전달하는 작업을 진행했는데, 그 과정에서 있었던 일들을 공유하려 합니다.
Postfix란?
Postfix는 리눅스 환경에서 널리 사용되는 메일 전송 에이전트입니다. 메일을 수신하거나 다른 메일 서버로 전달하는 역할을 하며, 설정에 따라 특정 주소로 들어온 메일을 다른 주소로 복사하거나 외부 프로그램으로 전달하는 등의 처리를 할 수 있습니다.
회장메일에서 Slack으로 포워딩하기
회장메일을 Slack 채널에 전달하기 전에 한 가지 문제가 있었습니다. 예전부터 회장메일 주소로 스팸 메일이 주기적으로 들어오고 있었다는 점입니다.
메일 서버에는 이미 rspamd 등의 기본적인 스팸 필터링이 적용되어 있지만, 일부 스팸 메일은 여전히 필터를 통과하고 있습니다. 기존에는 회장메일 계정에서 직접 확인하고 무시했지만, Slack으로 자동 전달되면 부원 전체가 해당 스팸을 보게 됩니다.
따라서 단순히 모든 메일을 Slack으로 전달하는 것이 아니라, 필요한 메일만 전달하도록 중간에 한 번 더 필터링하는 구조가 필요했습니다. 즉, 목표는 다음과 같았습니다.
기존 회장메일 수신은 그대로 유지하면서,
Slack 전송용 복사본을 따로 만들고,
해당 복사본만 Python 스크립트로 전달하여
필터링 후 Slack으로 전송
다행히 Postfix는 여기서 필요한 모든 기능을 제공합니다.
1. recipient_bcc 설정
먼저 회장메일로 들어온 메일을 복사하도록 설정합니다.
sudo vi /etc/postfix/recipient_bcc
파일에는 다음 내용을 추가합니다.
scg@scg.skku.ac.kr slack@localhost
이 설정은 scg@scg.skku.ac.kr로 들어오는 메일에 대해 slack@localhost를 BCC 수신자로 추가한다는 의미입니다. 즉, 기존 회장메일 수신은 그대로 유지하면서 Slack 전송을 위한 복사본이 하나 더 만들어집니다.
여기서 BCC는 숨은 참조를 의미합니다. 원래 수신자인 회장메일에는 정상적으로 메일이 전달되고, Postfix 내부에서만 slack@localhost로 복사본이 추가됩니다.
설정 파일을 작성한 뒤에는 postmap 명령어를 실행합니다.
sudo postmap /etc/postfix/recipient_bcc
postmap 명령을 실행하면 /etc/postfix/recipient_bcc.db 파일이 생성됩니다. 아래에서 recipient_bcc_maps를 hash 타입으로 등록할 것이므로, Postfix가 조회할 hash DB 파일을 생성해야 합니다.
2. transport 설정
다음으로 slack@localhost에 들어온 메일을 일반적인 로컬 메일함에 저장하지 않고, 별도의 transport로 넘기도록 설정합니다.
sudo vi /etc/postfix/transport
파일에는 다음 내용을 추가합니다.
slack@localhost slack-service:
이 설정은 slack@localhost로 들어온 메일을 slack-service라는 transport로 처리하겠다는 의미입니다.
즉, slack@localhost는 실제 사람이 확인하는 메일함이 아니라, Slack 전송을 위한 중간 주소로 사용됩니다. 이 주소로 들어온 메일은 뒤에서 설정할 slack-service를 통해 Python 스크립트로 전달됩니다.
마찬가지로 postmap 명령어를 실행해 hash DB 파일을 생성합니다.
sudo postmap /etc/postfix/transport
3. main.cf 설정
앞에서 만든 recipient_bcc와 transport 설정 파일을 Postfix가 실제로 사용하도록 main.cf에 등록합니다.
sudo vi /etc/postfix/main.cf
다음 내용을 추가합니다.
recipient_bcc_maps = hash:/etc/postfix/recipient_bcc
transport_maps = hash:/etc/postfix/transport
이제 회장메일로 들어온 메일은 기존 수신자에게 정상적으로 전달되면서, 동시에 slack@localhost로 복사됩니다. 그리고 slack@localhost로 복사된 메일은 transport_maps 설정에 의해 slack-service로 전달됩니다.
여기서 hash: 접두사는 해당 map을 hash DB 파일로 조회하겠다는 의미입니다. 앞에서 postmap 명령을 실행해 생성한 .db 파일들이 이때 사용됩니다.
4. master.cf 설정
이제 slack-service transport가 실제로 어떤 명령을 실행할지 정의해야 합니다.
sudo vi /etc/postfix/master.cf
다음 내용을 추가합니다.
slack-service unix - n n - - pipe
flags=R user=scg argv=/usr/bin/python3 /path/to/python/script
이 설정은 slack-service라는 transport가 호출되었을 때, scg 사용자 권한으로 Python 스크립트를 실행하라는 의미입니다. 메일 내용은 pipe를 통해 스크립트의 표준 입력으로 전달됩니다.
즉, 전체 흐름은 다음과 같이 진행됩니다.
scg@scg.skku.ac.kr로 메일 수신
→ recipient_bcc_maps에 의해 slack@localhost로 복사
→ transport_maps에 의해 slack-service로 전달
→ master.cf의 pipe 설정에 따라 Python 스크립트 실행
→ Python 스크립트에서 필터링 후 Slack으로 메일 전송
5. Python 스크립트 작성
앞에서 설정한 pipe transport에 의해 Python 스크립트가 실행되면, Postfix는 메일 원문을 스크립트의 표준 입력으로 전달합니다. 따라서 스크립트에서는 표준 입력을 읽어 메일 데이터를 가져올 수 있습니다.
import sys
import email
from email import policy
raw_data = sys.stdin.buffer.read()
msg = email.message_from_bytes(raw_data, policy=policy.default)
이제 해당 메일 데이터를 Slack으로 전송하기만 하면 됩니다. Slack에서는 채널용 이메일 주소를 생성할 수 있는데, 이렇게 생성된 이메일로 데이터를 전송시키는 코드를 작성하면 됩니다. 자세한 내용은 Slack 공식 문서에서 확인하실 수 있습니다.
6. 설정 적용
sudo postfix check
sudo postfix reload
스팸 필터링
회장메일을 Slack으로 전달하는 것은 Postfix에서 간단히 설정할 수 있지만, 필터링 로직은 직접 추가해야 했습니다.
먼저 추가한 것은 기존 메일 서버에 적용되어 있던 rspamd의 검사 결과였습니다. rspamd가 스팸으로 판정한 이메일은 스팸함에 저장되는데, 이는 메일의 헤더에서 확인할 수 있습니다.
def is_spam(msg):
spamd_result = str(msg.get("X-Spamd-Result", "")).lower()
return "true" in spamd_result
하지만 이것만으로는 충분하지 않았습니다. rspamd에서 걸러지지 않는 스팸 메일들이 있었기 때문입니다. 그래서 메일함에 저장되어 있던 스팸 메일의 원문을 확인해보았습니다.
cd /var/vmail/scg.skku.ac.kr/scg/Maildir/cur/
cat "$(ls -t | head -n 1)"
아래는 해당 메일 원문 중 일부를 발췌한 내용입니다. *는 임의로 마스킹한 값입니다.
Delivered-To: scg@scg.skku.ac.kr
Return-Path: <uh*****@w***-b*****.******>
From: "N********" <uh*****@w***-b*****.******>
To: <t**@s********.**.**>
X-Spamd-Result: default: False [1.70 / 15.00];
...
FORGED_RECIPIENTS(2.00)[m:t**@s********.**.**,s:scg@scg.skku.ac.kr]
Content-Type: text/plain;
charset="windows-1251"
Content-Type: text/html;
charset="windows-1251"
먼저 메일 헤더를 보면, 실제로는 scg@scg.skku.ac.kr로 전달되었지만 To 헤더에는 다른 주소가 적혀 있습니다. 이메일에서는 사용자에게 보이는 To 헤더와 실제 배달 대상이 항상 일치하지는 않습니다. BCC, 포워딩, 메일링 리스트 같은 정상적인 상황에서도 이런 불일치가 발생할 수 있기 때문에, 이 사실만으로 스팸이라고 단정할 수는 없습니다. 다만 이 메일에서는 다른 의심 요소들과 함께 나타났기 때문에 참고할 만한 신호로 볼 수 있습니다.
rspamd도 이를 FORGED_RECIPIENTS 항목으로 감지하고 있었습니다. FORGED_RECIPIENTS는 메일 헤더에 표시된 수신자와 실제로 메일이 전달된 수신자가 다를 때 추가되는 rspamd 심볼입니다. 즉, 이 메일은 겉으로 보이는 수신자와 실제 배달 대상이 일치하지 않았고, rspamd는 이를 의심 요소로 판단해 2점을 추가했습니다. 하지만 최종 점수는 1.7점으로 임계값 15점보다 낮아 결국 스팸으로 판정되지 않았습니다.
그런데 메일 원문을 더 확인해보니 중요한 특징이 있었습니다. plain text 본문과 HTML 본문 모두 Content-Type 헤더에 charset="windows-1251"이 설정되어 있었습니다.
windows-1251은 주로 키릴 문자권에서 사용되는 문자 인코딩입니다. 이 메일의 제목과 본문은 한국어로 된 광고성 내용이었지만, 본문 MIME 파트의 charset은 windows-1251로 지정되어 있었습니다.
이후 회장메일로 수신된 메일들을 추가로 확인해보니, 확인한 범위 내에서는 스팸 메일들이 모두 windows-1251 인코딩을 사용하고 있었습니다. 반면 정상 메일 중에서는 windows-1251 인코딩을 사용하는 경우가 없었습니다.
물론 windows-1251을 사용하는 모든 메일이 일반적으로 스팸이라는 뜻은 아닙니다. 하지만 저희 회장메일의 수신 사례에서는 스팸 메일과 정상 메일을 구분하는 데 매우 뚜렷한 기준으로 작동했습니다. 회장메일로 들어오는 정상적인 메일은 대부분 한국어나 영어 기반이었기 때문에, 현재 운영 환경에서는 windows-1251 인코딩을 사용하는 메일을 Slack으로 전달하지 않아도 정상 메일이 잘못 차단될 가능성이 낮다고 판단했습니다.
따라서 rspamd 검사 결과와 별개로, 메일의 문자 인코딩이 windows-1251인 경우 Slack으로 전달하지 않도록 추가 필터링을 적용했습니다.
def is_spam(msg):
spamd_result = str(msg.get("X-Spamd-Result", "")).lower()
if "true" in spamd_result:
return True
for part in msg.walk():
charset = part.get_content_charset()
if charset and charset.lower() == "windows-1251":
return True
return False
결과
이번 작업을 통해 회장메일로 들어오는 메일을 Slack 채널로 자동 전달하고, 불필요한 스팸 메일들을 걸러낼 수 있게 되었습니다.
처음에는 rspamd를 통과한 스팸 메일들을 어떤 기준으로 필터링해야 할지 막막했습니다. 스팸의 제목이나 본문을 하드코딩하는 방식도 사용해봤고, LLM을 이용해 메일 내용을 분류해야 하나 고민하기도 했습니다.
하지만 실제로 수신된 메일 원문을 하나씩 확인해보면서, 회장메일로 들어오던 스팸 메일들이 windows-1251 인코딩을 사용하고 있다는 공통점을 찾을 수 있었습니다. 덕분에 복잡한 분류 로직 없이도 현재 운영 환경에 맞는 간단하고 효과적인 필터링을 적용할 수 있었습니다.
마무리
SCG에서는 웹사이트 개발과 관리뿐만 아니라, 인프라를 운영하며 다양한 문제를 직접 마주하고 해결해나가고 있습니다. 저 역시 SCG 덕분에 메일 서버를 다뤄보고, 실제 운영 환경에서 문제를 해결해보는 경험을 할 수 있었습니다.
SCG에 많은 관심 가져주시고, 지원도 많이 해주시면 좋겠습니다.
읽어주셔서 감사합니다!