HOWTO · Python
Python을 사용하여 여러 주소로 이메일 보내기
이 기사는 Python을 사용하여 여러 주소로 이메일을 보내는 방법을 보여줍니다.
이 페이지의 내용
같은 메시지를 여러 주소로 보내는 작업은 Python 표준 라이브러리만으로 해결됩니다. smtplib가 SMTP 세션을 관리하고, email.message의 EmailMessage가 헤더와 본문을 담습니다. 수신자에게 보여야 하는 주소는 모두 To 또는 Cc 헤더에 넣고, 숨겨야 할 주소는 Bcc에 넣은 다음 send_message를 한 번 호출하면 됩니다. 각 사서함으로 사본을 배달하는 일은 SMTP 서버가 처리합니다.
여러 이메일 수신자의 동작 원리
이메일에는 서로 다른 두 개의 수신자 목록이 있으며, 이 둘을 구분하면 다중 수신자와 관련된 대부분의 의문이 풀립니다.
- 엔벨로프 수신자는 클라이언트가
RCPT TO명령으로 SMTP 서버에 전달하는 주소입니다. 배달은 전적으로 이 목록에 의해 결정됩니다. - 헤더 수신자는 메시지 안의
To,Cc,Bcc줄입니다. 메일 클라이언트를 위한 표시 정보일 뿐이며 서버는 이 헤더를 기준으로 라우팅하지 않습니다.
배달 관점에서 To와 Cc는 동일하게 동작하며 모든 수신자가 두 목록을 볼 수 있습니다. Bcc 수신자도 다른 사람과 똑같이 배달되지만, send_message는 전송 전에 Bcc 헤더를 제거하므로 누가 숨은 참조로 지정됐는지는 아무도 볼 수 없습니다.
server.send_message(message)를 호출하면 smtplib가 To, Cc, Bcc 헤더를 읽어 주소마다 RCPT TO를 하나씩 보냅니다. to_addrs에 명시적 목록을 전달하면 배달 기준으로 헤더를 덮어쓸 수도 있습니다. 보이는 To 줄이 실제 배달 목록과 달라야 할 때, 예를 들어 숨겨진 메일링 리스트를 사용할 때 유용합니다.
모든 주소는 바이트 단위로 동일한 메시지 사본을 받습니다. 사람마다 다른 내용이 필요하다면 수신자별로 별도의 EmailMessage를 만들어 보내야 하며, 마지막 절에서 그 패턴을 다룹니다.
SMTP 서버 선택과 안전한 인증
실제 메일 제공업체를 이용하려면 세 가지가 필요합니다. 제공업체의 SMTP 호스트명과 포트, 계정 이름, 그리고 자격 증명입니다. 제공업체 문서가 기준입니다. 요구 사항은 바뀔 수 있으므로 복사해 온 코드 조각을 맹신하지 말고 현재 값을 확인하세요.
암호화된 연결 방식은 두 가지가 일반적입니다.
- 첫 바이트부터 암시적 TLS를 적용하는 방식으로, 보통 포트
465를 사용합니다.smtplib.SMTP_SSL(host, port)를 사용하세요. - 평문 연결을 STARTTLS로 업그레이드하는 방식으로, 보통 포트
587을 사용합니다.smtplib.SMTP(host, port)를 사용한 다음server.login(...)전에server.starttls()를 호출하세요.
많은 제공업체가 더 이상 SMTP에서 일반 계정 비밀번호를 받지 않습니다. 앱 비밀번호를 발급하거나, 비밀번호로 사용하는 API 키를 발급하거나, OAuth2를 요구하므로 제공업체가 지원하는 방식을 생성하세요. 자격 증명은 소스 코드 밖에 보관하세요. 아래 두 번째 예제처럼 환경 변수를 사용하는 것이 가장 간단한 시작점입니다. 실제 자격 증명을 저장소에 커밋하지 마세요.
여러 수신자용 EmailMessage 만들기
EmailMessage는 순서가 있는 헤더 딕셔너리에 본문을 더한 것처럼 동작합니다. 먼저 헤더를 설정하세요.
message["From"]— 발신자입니다. 대부분의 제공업체는 인증된 계정이나 검증된 별칭과 일치할 것을 요구하며, 불일치는 거부의 흔한 원인입니다.message["To"]— 쉼표로 구분된 문자열입니다. 프로그램을 확장하기 쉽게 유지하려면 Python 리스트를", ".join(recipients)로 연결하세요.message["Cc"]와message["Bcc"]— 선택 사항이며 같은 쉼표 구분 형식을 사용합니다.message["Subject"]— 제목 줄입니다.
message.set_content("...")는 일반 텍스트 본문을 채우고 Content-Type: text/plain을 자동으로 설정합니다. HTML 버전이 필요하면 set_content 다음에 message.add_alternative(html, subtype="html")을 호출해 텍스트를 선호하는 클라이언트에도 대체 본문을 남겨 두세요. message.add_attachment(...)는 PDF나 이미지 같은 파일을 추가합니다.
smtplib로 메시지 보내기
send_message는 구성한 메시지를 한 번 전송하고, 서버가 이를 모든 엔벨로프 수신자에게 전달하도록 맡깁니다. SMTP 객체를 컨텍스트 매니저로 사용하면 with 블록이 끝날 때 QUIT로 세션을 깔끔하게 닫습니다.
다음 완성된 스크립트를 실행해 보세요. localhost에서 작은 SMTP 테스트 서버를 시작하고, smtplib를 통해 To 두 곳과 Cc, Bcc 주소로 EmailMessage 하나를 보낸 다음, 서버가 기록한 모든 엔벨로프 수신자를 출력합니다.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
import smtplib
import socket
import threading
from email.message import EmailMessage
recipients_seen = []
def run_server(sock):
conn, _ = sock.accept()
conn.sendall(b"220 localhost ESMTP test server\r\n")
data_mode = False
buffer = b""
while True:
chunk = conn.recv(1024)
if not chunk:
return
buffer += chunk
while b"\r\n" in buffer:
line, buffer = buffer.split(b"\r\n", 1)
if data_mode:
if line == b".":
data_mode = False
conn.sendall(b"250 Message stored\r\n")
continue
command = line.decode().split(" ", 1)[0].upper()
if command == "RCPT":
address = line.decode().rsplit(":", 1)[-1].strip().strip("<>")
recipients_seen.append(address)
conn.sendall(b"250 OK\r\n")
elif command == "DATA":
data_mode = True
conn.sendall(b"354 End data with <CR><LF>.<CR><LF>\r\n")
elif command == "QUIT":
conn.sendall(b"221 Bye\r\n")
return
else:
conn.sendall(b"250 OK\r\n")
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(1)
port = listener.getsockname()[1]
thread = threading.Thread(target=run_server, args=(listener,))
thread.start()
message = EmailMessage()
message["Subject"] = "Team update"
message["From"] = "sender@example.com"
message["To"] = "alice@example.com, bob@example.com"
message["Cc"] = "carol@example.com"
message["Bcc"] = "dave@example.com"
message.set_content("Here is this week's team update.")
with smtplib.SMTP("127.0.0.1", port) as server:
server.send_message(message)
thread.join()
listener.close()
print("Delivered to these envelope recipients:")
for recipient in recipients_seen:
print(recipient)
Delivered to these envelope recipients:
alice@example.com
bob@example.com
dave@example.com
carol@example.com
테스트 서버는 헤더 주소마다 하나씩, 모두 네 개의 RCPT TO 주소를 기록했습니다. 여기에는 dave@example.com도 포함됩니다. 이 주소의 Bcc 헤더는 전송된 사본에서 제거되지만 배달에는 여전히 반영됩니다. 이것이 바로 엔벨로프와 헤더의 분리가 실제로 동작하는 모습입니다. 로컬 테스트 서버는 평문 SMTP를 사용하므로 이 스크립트는 암호화 없이 smtplib.SMTP를 사용합니다.
실제 제공업체를 통해 보내려면 같은 메시지를 다음처럼 변경하면 됩니다. 이 스크립트는 연결 정보를 환경 변수에서 읽고, 설정되어 있지 않으면 설정하라는 알림을 출력합니다.
import os
import smtplib
from email.message import EmailMessage
SMTP_HOST = os.environ.get("SMTP_HOST", "")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "465"))
SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
recipients = [
"alice@example.com",
"bob@example.com",
"carol@example.com",
]
message = EmailMessage()
message["Subject"] = "Team update"
message["From"] = SMTP_USER
message["To"] = ", ".join(recipients)
message.set_content("Here is this week's team update.")
if SMTP_HOST and SMTP_USER and SMTP_PASSWORD:
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
server.login(SMTP_USER, SMTP_PASSWORD)
refused = server.send_message(message)
print("Accepted for all recipients" if not refused else f"Refused: {refused}")
else:
print("Set SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASSWORD to send this message.")
Set SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASSWORD to send this message.
변수가 설정되어 있고 제공업체에 연결할 수 있으면, 같은 스크립트가 server.login으로 로그인하고 SMTP_SSL를 통해 전송한 뒤 Accepted for all recipients를 출력합니다.
배달 오류 처리와 예제 적용
장애는 예측 가능한 몇몇 지점에 집중됩니다.
- 인증 — 자격 증명이 틀렸거나, 제공업체가 계정 비밀번호 대신 앱 비밀번호나 OAuth2를 요구하면
server.login이smtplib.SMTPAuthenticationError를 발생시킵니다. - 연결 — 잘못된 호스트명은
socket.gaierror를 발생시키고, 잘못된 포트나 차단된 경로는smtplib.SMTPConnectError또는 타임아웃으로 나타납니다. 포트가 암호화 방식과 일치하는지 확인하세요.465에는SMTP_SSL,587에는SMTP와starttls()의 조합을 사용해야 합니다. - 수신자 거부 — 서버가 모든 주소를 거부하면
send_message가smtplib.SMTPRecipientsRefused를 발생시킵니다. 일부만 거부되면 나머지로는 계속 보내고, 거부된 각 주소를 SMTP 상태 코드와 메시지에 매핑한 딕셔너리를 반환합니다. 아무 알림이 없다고 성공이라고 가정하지 말고 그 반환값을 확인하세요.
SMTP 250 응답은 서버가 메시지를 중계용으로 수락했다는 뜻이지 받은 편지함에 도착했다는 뜻이 아닙니다. 제공업체가 여전히 필터링하거나 격리하거나 반송할 수 있으므로, 실제 발송 중에는 발신자 사서함의 반송 알림을 확인하세요.
예제를 적용하려면 recipients 리스트를 파일, 데이터베이스 또는 환경 변수에서 읽어 온 주소로 바꾸세요. 개인화된 내용이 필요하면 하나의 with 블록 안에서 연결을 열어 둔 채 사람마다 EmailMessage를 하나씩 만들어 send_message를 반복 호출하세요. 세션을 재사용하는 것이 다시 연결하는 것보다 훨씬 빠릅니다. 대규모 공지에는 Bcc로 주소를 비공개로 유지할 수 있지만 제공업체의 전송 속도 제한을 확인하세요. 아주 큰 목록은 전용 발송 서비스를 이용하는 것이 적합합니다.