← 목록으로AWS

AWS SES Mail Manager를 활용한 메일 수신 자동화 파이프라인

SES Mail Manager로 특정 주소의 수신 메일을 S3에 저장하고, Lambda로 인증코드를 파싱하여 내부 API를 자동 호출하는 메일 수신 자동화 파이프라인 구축

AWSPythonAutomationDevOps
2025-03-15

목표

외부에서 특정 메일 주소로 전송되는 인증 메일을 자동으로 수신하고, 메일 본문에서 인증코드를 추출하여 내부 API를 자동 호출하는 파이프라인을 구축합니다.

외부 메일 발송
    ↓
[specific-address]@[domain] 으로 수신
    ↓
SES Mail Manager (메일 수신 전용 엔드포인트)
    ↓
S3에 .eml 파일 저장
    ↓
S3 이벤트 → Lambda 트리거
    ↓
메일 파싱 → 인증코드 추출 → 내부 API 호출

왜 Mail Manager인가

기존 도메인의 메일은 외부 네임서버에서 관리되고 있었습니다. 특정 주소로 오는 메일만 AWS에서 별도로 처리해야 하는 상황에서, 기존 메일 시스템을 건드리지 않고 SES Mail Manager를 통해 별도의 메일 전용 엔드포인트를 만들어 해결했습니다.

  • 기존 메일 시스템에 영향 없음
  • 특정 주소만 AWS에서 수신/처리
  • S3 저장, Lambda 연동 등 유연한 후처리

Mail Manager 설정

1. 주소 목록 등록

Amazon SES > Mail Manager에서 수신할 이메일 주소를 개별 등록합니다.

[specific-address-1]@[domain]
[specific-address-2]@[domain]

개별 이메일 주소 단위로 수신을 제어할 수 있습니다.

2. 트래픽 정책 생성

수신 메일에 대한 허용/차단 정책을 설정합니다.

3. 규칙 세트 생성

수신된 메일에 대한 처리 규칙을 정의합니다.

  • 조건: 제목에 특정 텍스트 포함 여부 확인
  • 작업: S3 쓰기, SNS 발송 등 다양한 방식으로 설정 가능

4. 수신 엔드포인트 생성

엔드포인트 생성 시 발급되는 A Record를 도메인 관리 서비스에서 MX 레코드로 등록해야 합니다.

# 엔드포인트 생성 후 발급되는 A Record 예시
[endpoint-id].mail-manager-smtp.amazonaws.com

# 도메인 MX 레코드에 등록
MX  [subdomain].[domain]  [endpoint-id].mail-manager-smtp.amazonaws.com

5. IAM 역할 확인

다음 항목이 올바르게 설정되어야 S3에 메일이 저장됩니다:

  • IAM 역할 정책: S3 PutObject 권한
  • IAM 역할 신뢰 정책: SES 서비스가 역할을 assume할 수 있도록 설정

IAM 역할 설정이 잘못되면 메일은 수신되지만 S3에 저장되지 않습니다.

6. 설정 확인

# MX 레코드 확인
nslookup -type=MX [subdomain].[domain]
# → [endpoint-id].mail-manager-smtp.amazonaws.com 확인

S3 → Lambda 이벤트 연동

아키텍처

S3 버킷 (메일 .eml 저장)
    ↓ 객체 생성 이벤트
Lambda (ses-mail-parser)
    ├── S3에서 .eml 파일 읽기
    ├── 메일 파싱 (MIME 헤더, multipart)
    ├── 인증코드 추출 (정규식)
    └── 내부 API 호출 (인증코드 전달)

Lambda 권한 구조

Lambda에는 두 가지 권한이 필요합니다:

1. Lambda 실행 역할 (Lambda가 할 수 있는 일)
   ├── S3 버킷 객체 읽기 (s3:GetObject)
   └── CloudWatch Logs 쓰기

2. 리소스 기반 정책 (누가 Lambda를 호출할 수 있는지)
   └── S3 서비스가 Lambda를 invoke할 수 있도록 허용

Lambda 실행 역할은 "코드가 실행될 때 무엇을 할 수 있는지", 리소스 기반 정책은 "누가 이 Lambda를 트리거할 수 있는지"를 정의합니다.

S3 이벤트 알림 설정

S3 버킷 > 속성 > 이벤트 알림 > 새 이벤트 알림 생성
  이벤트 유형: 객체 생성 (s3:ObjectCreated:*)
  대상: Lambda 함수 (ses-mail-parser)

Lambda 메일 파싱 로직

.eml 파일 구조

S3에 저장되는 .eml 파일은 MIME 원문 전체입니다:

MIME 헤더 (From, To, Subject, Date...)
    ↓
multipart/alternative
    ├── text/plain (텍스트 본문)
    └── text/html (HTML 본문)

본문 추출

import email
from email import policy
 
def extract_best_body(raw_email: bytes):
    msg = email.message_from_bytes(raw_email, policy=policy.default)
    text_body = None
    html_body = None
 
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            if content_type == 'text/plain' and not text_body:
                text_body = part.get_content()
            elif content_type == 'text/html' and not html_body:
                html_body = part.get_content()
    else:
        content_type = msg.get_content_type()
        content = msg.get_content()
        if content_type == 'text/plain':
            text_body = content
        else:
            html_body = content
 
    return text_body, html_body
  • text/plain이 가장 신뢰도 높으므로 우선 사용
  • 없으면 text/html에서 추출

인증코드 추출

import re
 
def extract_code(text_body, html_body):
    # text/plain에서 먼저 시도
    for body in [text_body, html_body]:
        if not body:
            continue
        # 6자리 숫자 인증코드 추출
        match = re.search(r'\b(\d{6})\b', body)
        if match:
            return match.group(1)
    return None

메일 본문 예시:

text/plain: "Can't use the link? Enter a code instead: 223638"
text/html:  "<td>Enter a code instead: <b>223638</b></td>"
→ 223638 추출

내부 API 호출

import urllib.request
import json
import os
 
def call_auth_api(code):
    api_url = os.environ.get('AUTH_API_URL')
    payload = json.dumps({'code': code}).encode()
 
    req = urllib.request.Request(
        api_url,
        data=payload,
        headers={'Content-Type': 'application/json'},
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        return {
            'status': resp.status,
            'body': resp.read().decode(),
        }

Lambda 핸들러

import boto3
 
s3 = boto3.client('s3')
 
def lambda_handler(event, context):
    # S3 이벤트에서 버킷명, 오브젝트 키 추출
    record = event['Records'][0]['s3']
    bucket = record['bucket']['name']
    key = record['object']['key']
 
    # .eml 파일 읽기
    obj = s3.get_object(Bucket=bucket, Key=key)
    raw_email = obj['Body'].read()
 
    # 메일 파싱
    text_body, html_body = extract_best_body(raw_email)
 
    # 인증코드 추출
    code = extract_code(text_body, html_body)
 
    if not code:
        return {'status': 'no_code_found'}
 
    # API 호출
    result = call_auth_api(code)
 
    return {
        'verification_code': code,
        'api_result': result,
    }

CloudWatch Logs에서 추출된 코드와 API 응답을 확인할 수 있습니다.


보안 고려사항

  • 발신자 화이트리스트 검증: 기대하는 발신자(예: noreply@[auth-service])인지 확인하여 스푸핑 방지
  • 코드 미추출 시 API 호출 차단: verification_code가 비어있으면 API 호출하지 않음
  • 인증 토큰은 환경변수로 관리: AUTH_API_URL, 인증 헤더 등은 Lambda 환경변수에서 로드
  • S3 버킷 접근 제한: SES와 Lambda만 접근 가능하도록 버킷 정책 설정

S3 접근 경로 최적화

내부 EC2에서 S3에 접근할 때 NAT Gateway 경유 대신 S3 Gateway Endpoint를 사용하여 데이터 전송료를 절감합니다.

[NAT Gateway 경유]
EC2 → NAT Gateway → S3  (데이터 처리 비용 발생)

[Gateway Endpoint]
EC2 → S3 Gateway Endpoint → S3  (비용 없음)

S3 버킷 정책

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSESPutObject",
      "Effect": "Allow",
      "Principal": { "Service": "ses.amazonaws.com" },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::[bucket-name]/*"
    },
    {
      "Sid": "AllowVPCEndpointAccess",
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::[bucket-name]",
        "arn:aws:s3:::[bucket-name]/*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:sourceVpce": "[vpce-id]"
        }
      }
    }
  ]
}

정리

  • SES Mail Manager로 기존 메일 시스템에 영향 없이 특정 주소만 AWS에서 수신
  • 수신 메일을 S3에 .eml로 저장, Lambda로 자동 파싱
  • MIME multipart 처리 + 정규식으로 인증코드 추출
  • 추출된 코드로 내부 API 자동 호출
  • IAM 역할/리소스 기반 정책 분리로 권한 관리
  • S3 Gateway Endpoint로 내부 접근 비용 최적화