가이드 · python3 smtp
제품 팀은 Python 3 SMTP를 어떻게 안전하게 구현해야 하나요?
Python 3 SMTP는 브라우저나 사용자가 제어하는 코드가 아니라 승인된 서버 워커 뒤에서 구현하세요. EmailMessage로 메시지를 만들고, 엔벨로프 수신자를 화면에 표시되는 헤더와 분리하고, 검증된 SSL 컨텍스트를 만들고, 유한한 연결 타임아웃을 설정하고, 연결 시작부터 TLS를 쓰려면 SMTP_SSL을, 명시적으로 업그레이드하려면 SMTP.starttls() 다음에 EHLO를 사용하세요. 자격 증명은 시크릿 관리자에서 불러오고, send_message()를 호출하고, 거부된 수신자 결과를 확인하고, 정확한 시도 결과를 저장하세요. 일시적인 실패에 한해서만 상한이 있는 백오프로 재시도하고, SMTP 접수를 받은편지함 도달의 증거로 취급하지 마세요.
승인된 이메일 작업 하나를 정의하세요
계정 인증, 영수증, 요청된 알림, 보안 알림처럼 승인된 제품 이벤트에서 시작하세요. SMTP 연결을 열기 전에 내구성 있는 발신 작업을 저장하세요. 이 작업에는 안정적인 업무 이벤트 키, 테넌트, 메시지 유형, 템플릿 리비전, 승인된 엔벨로프 발신자와 수신자, 화면에 표시되는 From ID, 수신 동의 또는 필요성의 근거, 현재의 발송 제외 결과가 들어 있어야 합니다. 브라우저, 모바일, 템플릿, 사용자 입력이 SMTP 호스트, 자격 증명, 엔벨로프 발신자, 임의의 헤더, 제한 없는 수신자를 선택해서는 안 됩니다. 호출자와 테넌트를 승인하고, 주소를 검증하고, 수신자와 첨부 파일 수에 상한을 두고, 줄바꿈 인젝션을 방지하세요. 작업은 한 번만 확보하고 추가 전용(append-only) 시도 기록을 유지하세요. Python의 smtplib는 프로토콜 클라이언트이며 업무 멱등성, 테넌트 격리, 수신 동의, 발송 제외, 내구성 있는 큐를 제공하지 않습니다. 이러한 통제는 그 주변의 애플리케이션이 담당해야 합니다.
EmailMessage로 구조화된 메시지를 만드세요
헤더와 MIME 문자열을 이어 붙이지 말고 email.message.EmailMessage를 사용하세요. 검증된 값으로 From, To, Subject, 안정적인 애플리케이션 상관 헤더를 설정한 다음, 일반 텍스트에는 set_content를, 필요할 때 HTML 파트에는 add_alternative를 호출하고, 명시적으로 지원하는 파일 유형과 크기에 한해서만 add_attachment를 사용하세요. 텍스트와 HTML은 승인된 동일한 템플릿 리비전에서 생성하세요. 신뢰할 수 없는 값은 출력 컨텍스트에 맞게 이스케이프하고, 원본 사용자 HTML을 렌더링하지 마세요. 헤더, 제목, 추적 필드, 첨부 파일 이름에 시크릿, 액세스 토큰, 불필요한 개인 데이터, 내부 데이터베이스 키를 넣지 마세요. email 패키지는 자체 정책에 따라 직렬화하며 평탄화 과정에서 MIME 경계를 생성할 수 있으므로, 이후의 무결성 통제가 정확한 바이트에 의존한다면 최종 직렬화 결과에 서명하거나 해시하세요. SMTP 엔벨로프는 별도로 유지하세요. 화면에 표시되는 To와 Cc 헤더는 독자에게 알리는 용도이고, 전송 수신자 목록이 RCPT TO 명령을 제어합니다.
암시적 TLS와 STARTTLS를 의도적으로 선택하세요
서버가 연결 시작부터 TLS를 요구하면 SMTP_SSL을 사용하세요. 문서화된 서버 워크플로가 즉시 STARTTLS 업그레이드를 요구할 때에만 평문 연결에 SMTP를 사용하세요. Python의 smtplib 문서에 따르면 starttls는 이후의 SMTP 명령을 TLS 안에 두며, 클라이언트는 그 후에 ehlo를 다시 호출해야 합니다. 필수 TLS 업그레이드 전에는 절대 인증하지 마세요. 인증서 검증과 호스트 이름 확인이 안전한 클라이언트 기본값을 사용하도록 ssl.create_default_context로 컨텍스트를 만들고, 일반적인 라이브러리 연결 과정을 통해 예상되는 서버 호스트 이름을 전달하세요. 암호화가 필수일 때 STARTTLS 지원 부족, 인증서 실패, 호스트 이름 불일치, TLS 협상 실패는 완전한 중단 사유로 취급하세요. 프로덕션을 동작시키려고 검증을 비활성화하거나 검증되지 않은 컨텍스트로 대체하지 마세요. 홉 단위 TLS는 SMTP 연결을 보호할 뿐, 저장된 메시지 내용, 제공업체의 처리, 수신 측 저장소, 최종 메일함은 보호하지 않습니다.
자격 증명은 서버 측에 두고 범위를 제한하세요
SMTP 사용자 이름, 비밀번호 또는 토큰은 런타임에 관리형 시크릿 서비스에서 불러오세요. 소스 관리, Docker 레이어, Git에 커밋된 구성, URL, 명령줄 인수, 디버그 출력, 분석 도구, 예외 보고서, 테스트 스냅샷, 노트북, 티켓, 프롬프트에 절대 넣지 마세요. 계정 전체의 관리자 시크릿보다 하나의 환경, 발신 도메인 또는 허용된 워크로드로 범위가 제한된 자격 증명을 선호하세요. 프로덕션을 개발 및 CI와 분리하세요. 교체를 일상적인 절차로 만드세요. 대체 자격 증명을 발급하고, 워커를 업데이트하고, 통제된 전송 테스트를 실행하고, 인증과 결과 증거를 확인한 뒤, 이전 자격 증명을 폐기합니다. 시크릿 접근은 발송 프로세스로 제한하고 관리자의 읽기를 감사하세요. Python의 login 메서드는 서버가 알린 인증 메커니즘을 시도합니다. 서버, 연결 보안, 계정, 메커니즘이 수용 가능한지는 여전히 애플리케이션이 판단해야 합니다. 반복되는 인증 실패는 비밀번호를 빠르게 재시도하는 대신 해당 코호트를 일시 중지하고 조사를 시작하게 해야 합니다.
명시적인 타임아웃과 상한이 있는 연결 수명을 사용하세요
연결과 블로킹 작업이 워커를 무기한 점유하지 않도록 SMTP 또는 SMTP_SSL에 유한한 타임아웃을 전달하세요. 소켓 타임아웃 하나로는 큐 보관 시간을 완전히 통제할 수 없으므로 바깥쪽에 작업 데드라인과 취소 정책을 적용하세요. 접근이 직렬화되고 상태가 안전하다고 입증되지 않았다면 동시 작업 사이에서 공유 SMTP 객체를 유지하지 마세요. 단순한 설계는 상한이 있는 배치에 대해 연결 하나를 열고, 서버에 인사하고, 필요하면 TLS를 설정하고, 인증하고, 소수의 메시지를 제출하고, quit을 호출하고, 오류나 수명 제한 후에는 연결을 폐기합니다. 재사용은 오버헤드를 줄일 수 있지만, 서버 연결 끊김, 타임아웃, 부분 상태 이후의 모호성을 키웁니다. 연결당 메시지 수에 상한을 두고 의도적으로 재연결하세요. 자격 증명이나 메시지 내용을 로그에 남기지 않으면서 연결 지연, TLS 협상, 인증, 명령 지연, 서버 연결 끊김, 작업 경과 시간을 모니터링하세요. SMTP 서버는 Python과 무관하게 바뀌는 한도를 적용할 수 있습니다.
메시지 하나를 보내고 일부 수신자 결과를 보관하세요
SMTP.sendmail은 전송 엔벨로프에 from_addr와 to_addrs를 사용하며 메시지 헤더를 다시 쓰지 않습니다. SMTP.send_message는 EmailMessage를 직렬화하며, 명시적인 엔벨로프 값이 제공되지 않으면 기본값을 도출합니다. 프로덕션 코드에서는 Bcc 처리와 테넌트 권한 부여가 모호하지 않도록 승인된 엔벨로프 발신자와 수신자 목록을 명시적으로 전달하세요. Python 문서에 따르면 sendmail은 수신자가 한 명 이상 수락되면 정상적으로 반환하고, 거부된 수신자마다 딕셔너리를 반환합니다. 따라서 예외가 없다는 것이 모든 수신자의 성공과 같지 않습니다. 접수된 수신자 범위와 거부된 수신자 범위를 상태 코드와 정제된 진단 정보와 함께 별도로 저장하세요. 일부 수신자만 거부되었다면 접수된 수신자는 재시도하지 마세요. 공유되는 메시지 시도를 유지하면서 각 수신자를 독립적으로 승인된 결과로 취급하세요. 이후 DATA 단계에서 발생하는 예외는 RCPT 거부와 다르며 별도의 분류가 필요합니다.
단계와 영구성에 따라 예외를 분류하세요
smtplib 예외를 명시적으로 처리하고 SMTP 코드와 정제된 서버 메시지를 보존하세요. SMTPConnectError와 타임아웃은 일시적일 수 있지만, 잘못된 호스트, 포트, 방화벽, 장애를 드러내는 것일 수도 있습니다. STARTTLS 또는 SMTPUTF8 이후의 SMTPNotSupportedError는 해당 기능을 요구하는 구성을 중단시켜야 합니다. SMTPAuthenticationError는 무작정 재시도할 것이 아니라 자격 증명, 계정, 메커니즘, TLS를 조사해야 합니다. SMTPSenderRefused와 SMTPRecipientsRefused는 ID 또는 수신자 단위의 결정이 필요합니다. SMTPDataError는 예상치 못한 DATA 응답을 설명하며, 확장 상태에 따라 콘텐츠, 정책, 할당량, 일시적인 수신 측 동작을 나타낼 수 있습니다. 제공업체별 문서를 따르면서, 4xx 응답은 상한이 있는 재시도 후보로, 5xx 응답은 해당 시도에 대한 영구적 실패로 분류하세요. 지수 백오프, 지터, 시도 횟수 및 큐 보관 시간 상한, 데드 레터 상태를 사용하세요. 발송 제외, 스팸 신고, 수신 거부, 폐기된 권한, 잘못된 수신자 증거가 있으면 절대 재시도하지 마세요.
모호한 제출 결과를 대조하세요
클라이언트가 메시지 데이터를 전송한 후 최종 서버 응답을 확인하기 전에 발생한 네트워크 타임아웃이나 연결 끊김은 모호합니다. Python이 예외를 발생시켰더라도 서버가 책임을 수락했을 수 있습니다. 곧바로 새 논리 발송을 만들지 마세요. 해당 시도를 알 수 없음으로 표시하고, 안정적인 이벤트 및 추적 식별자를 유지하고, 가능하면 제공업체 로그나 이후 전송 이벤트를 조회하세요. SMTP 서비스가 멱등성이나 검색 가능한 상관 정보를 제공하지 않는다면, 메시지 유형, 경과 시간, 중복으로 인한 피해, 사용자 경험에 근거한 제품 결정을 정의하세요. 보안 알림과 비밀번호 재설정 메시지는 영수증이나 금융 공지와 중복 위험이 다릅니다. 원래의 시도와 재시도 연결을 원장에 보존하세요. SMTP는 종단 간 정확히 한 번 전달을 제공하지 않으므로 그것을 보장한다고 주장하지 마세요. DATA 수락 전후를 포함해 프로토콜 단계마다 연결을 끊는 통제된 서버 픽스처로 이 분기를 테스트하세요.
SMTP 접수와 전달, 참여도를 구분하세요
send_message 호출이 성공했다는 것은 Python의 문서화된 의미에 따라 관찰된 SMTP 단계에서 수신자가 한 명 이상 수락되었다는 뜻입니다. 모든 수신자가 수락되었다거나, 대상 서버가 이후에도 메시지를 보관했다거나, 메시지가 받은편지함 폴더에 도달했다거나, 사람이 읽었다는 것을 증명하지는 않습니다. 제공업체 제출, 수신 서버 접수, 일시적 또는 영구적 실패, 이후의 반송, 스팸 신고, 수신 거부, 메일함 위치, 참여도를 별개의 증거로 모델링하세요. 가능하면 인증된 제공업체 이벤트를 수집하고, 중복을 제거하고, 발생 시각과 처리 시각을 따로 보존하세요. 이후 발송 직전에 영구 반송, 스팸 신고, 수신 거부를 적용하세요. 열람과 클릭은 전송의 증거가 아니며 개인정보 보호 기술의 영향을 받을 수 있습니다. 테넌트 안전 코호트, 템플릿 리비전, 발신 도메인, 상태 유형, 시간별로 개인정보를 최소화한 집계 지표를 유지하세요. 거부 급증, 알 수 없는 결과, 큐 경과 시간, TLS 실패, 인증 실패, 비정상적인 수신자 팬아웃에 대해 알림을 설정하세요.
실제 고객 메일을 보내지 않고 로컬에서 테스트하세요
메시지 구성, 헤더 삽입 거부, 수신자 권한 부여, Bcc 제거, 일반 텍스트 및 HTML 대체본, Unicode 처리, 첨부 파일 경계, 발송 제외 검사를 단위 테스트하세요. 제어된 로컬 SMTP 테스트 서버 또는 프로토콜 픽스처로 인사 실패, STARTTLS 누락, 인증서 실패, 인증 오류, 부분 RCPT 수락, DATA 4xx 및 5xx 응답, 연결 해제, 지연 응답을 시뮬레이션하세요. 프로덕션 형태의 시크릿이나 고객 콘텐츠에 사용 중단된 무인증 디버깅 서비스를 사용하지 마세요. 연동 테스트에는 명시적 할당량과 정리가 있는 전용 계정 및 제어된 수신자를 사용하세요. 원시 수신 메시지, 인증 결과, 표시 헤더, 회신 동작, 이벤트 상관관계를 검증하고 픽스처와 로그 전반에 시크릿 스캔을 실행하세요.
SendHQ의 활용 방식
SendHQ는 검증된 도메인 발송, 전달 이벤트, 발송 제외를 위한 워크스페이스 단위 이메일 API를 문서화합니다. 이 가이드는 Python의 표준 라이브러리 SMTP 클라이언트를 다루므로 현재 연동 방법과 API 계약은 SendHQ 문서를 사용하세요.
자주 묻는 질문
Python SMTP 자격 증명을 클라이언트 코드에 넣어도 되나요?
아니요. 환경과 워크로드 범위가 좁고, 접근이 감사되고, 정기적으로 교체되고, 로그에 남지 않는 서버 측 시크릿 관리자에 보관하세요.
Python에서는 언제 SMTP_SSL을 사용해야 하나요?
연결 시작부터 TLS가 필요할 때 SMTP_SSL을 사용하세요. SMTP와 starttls의 조합은 실패 시 차단되는(fail closed) 문서화된 명시적 업그레이드 워크플로에만 사용하세요.
starttls 이후에 EHLO를 다시 호출해야 하나요?
네. Python의 smtplib 문서는 보호된 연결 안에서 기능을 다시 확인할 수 있도록 starttls 이후에 ehlo를 다시 호출하라고 안내합니다.
send_message가 성공하면 모든 수신자가 수락되었다는 뜻인가요?
아니요. Python은 수신자가 한 명 이상 수락되면 정상적으로 반환하고, 거부된 수신자는 별도로 반환합니다. 수신자별 결과를 각각 저장하고 독립적으로 처리하세요.
SMTPAuthenticationError 이후에는 어떻게 해야 하나요?
영향을 받는 구성을 일시 중지하고 TLS, 서버, 계정, 시크릿, 서버가 알린 메커니즘을 점검하세요. 무작정 자격 증명을 재시도하면 계정 잠금이나 침해 신호가 증폭될 수 있습니다.
모든 SMTPDataError를 재시도해야 하나요?
아니요. 정확한 상태와 진단 정보를 보존한 다음, 일시적인 4xx 조건과 영구적인 5xx 정책, 콘텐츠, 할당량, 구성 실패를 구분하세요.
SMTP 접수가 받은편지함 도달을 결정하나요?
아니요. 이는 범위가 한정된 전송 증거입니다. 이후의 릴레이, 수신 측 필터링, 반송, 메일함 규칙, 폴더 위치, 사람의 참여는 별개의 결과입니다.
이 가이드에서 SendHQ 전용 연동을 다루나요?
아니요. Python의 표준 라이브러리 SMTP 클라이언트를 다룹니다. 현재 연동 방법과 API 계약은 SendHQ 문서를 참고하세요.
출처
- Python 3 smtplib 문서 — Python Software Foundation
- Python 3 EmailMessage 문서 — Python Software Foundation
- Python 3 ssl 문서 — Python Software Foundation
- RFC 5321: 단순 메일 전송 프로토콜(SMTP) — RFC Editor
- RFC 3207: TLS 기반 보안 SMTP를 위한 SMTP 서비스 확장 — RFC Editor
- RFC 4954: 인증을 위한 SMTP 서비스 확장 — RFC Editor