SMTP로 이메일 자동 발송 스크립트 만들기

매주 월요일 아침, 엑셀에서 숫자를 뽑아 정리한 뒤 팀원 열두 명에게 같은 형식의 메일을 하나씩 보내는 일. 한 통에 3분씩만 잡아도 36분이고, 한 달이면 두 시간이 넘습니다. 게다가 손으로 보내다 보면 수신자를 빠뜨리거나 지난주 첨부파일을 그대로 보내는 사고가 꼭 한 번은 납니다. 파이썬 이메일 자동 발송 스크립트를 한 번 만들어두면 이 작업이 명령어 한 줄로 끝납니다. 이 글에서는 앱 비밀번호 발급부터 첨부파일, 명단 기반 개인화 발송, 그리고 실제로 자주 막히는 오류까지 순서대로 다룹니다.

준비물: 앱 비밀번호와 SMTP 주소

파이썬 표준 라이브러리인 smtplib만 있으면 되므로 따로 설치할 패키지는 없습니다. 대신 메일 계정의 로그인 비밀번호를 그대로 쓸 수 없다는 점을 먼저 알아야 합니다. 구글은 2022년 5월부터 “보안 수준이 낮은 앱”의 접속을 차단했고, 그 이후로는 앱 전용 비밀번호를 따로 발급받아야 합니다.

지메일 기준 발급 순서입니다.

  1. 구글 계정 관리 → 보안 메뉴로 들어갑니다.
  2. 2단계 인증을 먼저 켭니다. 이게 켜져 있지 않으면 앱 비밀번호 메뉴 자체가 보이지 않습니다.
  3. 검색창에 “앱 비밀번호”를 입력해 해당 페이지로 이동합니다.
  4. 앱 이름을 적당히 입력하면 공백으로 구분된 16자리 문자열이 나옵니다.
  5. 공백을 뺀 16글자를 복사해 둡니다. 이 화면을 닫으면 다시 볼 수 없으니 바로 저장하세요.

네이버 메일을 쓴다면 메일 화면의 환경설정 → POP3/IMAP 설정에서 사용함으로 바꿔야 SMTP 접속이 열립니다. 2단계 인증을 켜둔 계정이라면 네이버도 애플리케이션 비밀번호를 따로 발급받아 써야 합니다.

서버 주소와 포트는 아래를 참고하세요. 포트에 따라 코드가 조금 달라지므로 이 표를 기준으로 삼으면 됩니다.

메일 서비스SMTP 주소포트접속 방식
지메일smtp.gmail.com587STARTTLS (SMTP + starttls())
지메일smtp.gmail.com465SSL (SMTP_SSL)
네이버smtp.naver.com465SSL (SMTP_SSL)
다음smtp.daum.net465SSL (SMTP_SSL)
회사 / 호스팅관리자에게 문의587 또는 465대개 STARTTLS

최소 코드 20줄로 첫 메일 보내기

아래 스크립트를 send_mail.py로 저장하면 바로 돌아갑니다. 계정 정보는 코드가 아니라 환경변수에서 읽어오는데, 이유는 다음 섹션에서 설명합니다.

import os
import smtplib
from email.message import EmailMessage

SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587


def send_mail(to_addr, subject, body):
    msg = EmailMessage()
    msg["From"] = os.environ["MAIL_USER"]
    msg["To"] = to_addr
    msg["Subject"] = subject
    msg.set_content(body)

    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
        smtp.starttls()
        smtp.login(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])
        smtp.send_message(msg)


if __name__ == "__main__":
    send_mail("받는사람@example.com", "테스트 메일", "파이썬에서 보낸 첫 메일입니다.")
로컬 SMTP 디버그 서버로 첨부파일 메일을 실제 발송한 결과
실제 실행입니다. 로컬 디버그 서버를 띄우면 계정 없이도 첨부·제목 인코딩까지 전부 확인할 수 있습니다.

핵심은 세 줄입니다. starttls()로 암호화 연결을 열고, login()으로 인증하고, send_message()로 보냅니다. with 블록을 쓰면 끝날 때 연결이 자동으로 닫히므로 quit()을 따로 부를 필요가 없습니다.

465 포트를 쓰는 네이버나 다음이라면 접속 부분만 이렇게 바꿉니다.

# 네이버 메일은 465 포트에 SSL로 바로 접속합니다.
# starttls()를 부르면 안 되고, SMTP 대신 SMTP_SSL을 씁니다.
with smtplib.SMTP_SSL("smtp.naver.com", 465) as smtp:
    smtp.login(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])
    smtp.send_message(msg)

오래된 예제에서 자주 보이는 MIMEText, MIMEMultipart 조합은 지금도 동작하지만 한글 제목을 넣을 때 인코딩을 직접 처리해야 합니다. 파이썬 3.6 이상에서는 EmailMessage를 쓰세요. 한글 헤더와 첨부파일 인코딩을 알아서 처리해 줍니다.

비밀번호를 코드에서 분리하기

앱 비밀번호를 스크립트 안에 그대로 적어두면, 그 파일을 깃허브에 올리거나 동료에게 보내는 순간 계정이 통째로 넘어갑니다. 공개 저장소에 올라간 자격 증명은 자동 수집 봇이 몇 분 안에 찾아냅니다. 환경변수로 빼두는 습관을 들이세요.

# 윈도우 PowerShell — 지금 열려 있는 창에서만 유효
$env:MAIL_USER = "내계정@gmail.com"
$env:MAIL_PASS = "abcdefghijklmnop"

# 윈도우 — 계정에 영구 등록 (등록 후 새 터미널을 열어야 반영됨)
setx MAIL_USER "내계정@gmail.com"
setx MAIL_PASS "abcdefghijklmnop"

# 맥 / 리눅스 — ~/.zshrc 또는 ~/.bashrc 에 추가
export MAIL_USER="내계정@gmail.com"
export MAIL_PASS="abcdefghijklmnop"

여러 프로젝트를 오간다면 python-dotenv 패키지로 프로젝트마다 .env 파일을 두는 방법도 있습니다. 이때는 .gitignore.env를 반드시 추가해야 합니다.

HTML 본문과 파일 첨부하기

실무에서 보내는 메일은 대개 표나 강조가 들어간 HTML 본문에 엑셀이나 PDF가 붙습니다. add_alternative()로 HTML 버전을, add_attachment()로 파일을 붙입니다.

import mimetypes
import os
import smtplib
from email.message import EmailMessage
from pathlib import Path


def send_report(to_addr, subject, html_body, attach_path=None):
    msg = EmailMessage()
    msg["From"] = os.environ["MAIL_USER"]
    msg["To"] = to_addr
    msg["Subject"] = subject

    # 텍스트 버전을 먼저 넣고 HTML을 대체 본문으로 추가합니다.
    msg.set_content("HTML을 지원하지 않는 환경에서는 첨부파일을 확인해 주세요.")
    msg.add_alternative(html_body, subtype="html")

    if attach_path:
        path = Path(attach_path)
        ctype, encoding = mimetypes.guess_type(path.name)
        if ctype is None or encoding is not None:
            ctype = "application/octet-stream"
        maintype, subtype = ctype.split("/", 1)
        msg.add_attachment(
            path.read_bytes(),
            maintype=maintype,
            subtype=subtype,
            filename=path.name,
        )

    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])
        smtp.send_message(msg)


if __name__ == "__main__":
    html = """
    <h2>8월 매출 리포트</h2>
    <p>지난달 대비 <strong>12% 증가</strong>했습니다.</p>
    <p>상세 내역은 첨부된 엑셀 파일을 확인해 주세요.</p>
    """
    send_report("팀장님@example.com", "8월 매출 리포트", html, "report_08.xlsx")

mimetypes.guess_type()은 확장자를 보고 파일 종류를 추측합니다. 판단하지 못하면 application/octet-stream으로 처리되는데, 이렇게 보내도 수신자가 다운로드하는 데는 문제가 없습니다.

첨부 용량은 지메일 기준 25MB가 한도입니다. 넘어가면 발송 자체가 거부되므로, 큰 파일은 클라우드에 올리고 본문에 링크를 넣는 편이 안전합니다.

CSV 명단으로 개인화 메일 보내기

자동화의 효과가 가장 큰 지점입니다. 명단을 CSV로 준비해 두면 수신자마다 이름과 숫자가 다른 메일을 한 번에 돌릴 수 있습니다. 먼저 recipients.csv를 이렇게 만듭니다.

name,email,amount
김철수,chulsoo@example.com,120000
이영희,younghee@example.com,85000
박민수,minsoo@example.com,240000

그다음 스크립트입니다. 실패한 건을 따로 모아 두는 부분이 중요합니다. 스무 명에게 보내다 세 번째에서 멈춰버리면 누구까지 보냈는지 알 수 없기 때문입니다.

import csv
import os
import smtplib
import time
from email.message import EmailMessage

SUBJECT_TPL = "{name}님, 8월 정산 내역 안내드립니다"
BODY_TPL = """{name}님 안녕하세요.

8월 정산 금액은 {amount}원입니다.
확인 후 회신 부탁드립니다.

감사합니다."""


def send_bulk(csv_path):
    user = os.environ["MAIL_USER"]
    password = os.environ["MAIL_PASS"]
    sent, failed = 0, []

    # 연결을 한 번만 맺고 재사용합니다. 매번 접속하면 훨씬 느리고 차단당하기 쉽습니다.
    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login(user, password)

        # 엑셀로 저장한 CSV는 앞에 BOM이 붙으므로 utf-8-sig로 읽습니다.
        with open(csv_path, encoding="utf-8-sig", newline="") as f:
            for row in csv.DictReader(f):
                try:
                    row["amount"] = format(int(row["amount"]), ",")
                    msg = EmailMessage()
                    msg["From"] = user
                    msg["To"] = row["email"]
                    msg["Subject"] = SUBJECT_TPL.format(**row)
                    msg.set_content(BODY_TPL.format(**row))
                    smtp.send_message(msg)
                    sent += 1
                    print("보냄:", row["email"])
                except Exception as e:
                    failed.append((row.get("email"), str(e)))
                    print("실패:", row.get("email"), e)
                time.sleep(1)  # 연속 발송 간격. 스팸 차단을 피하는 최소한의 배려

    print("총 {}건 발송, {}건 실패".format(sent, len(failed)))
    return failed


if __name__ == "__main__":
    send_bulk("recipients.csv")

실행하면 어느 주소로 보냈는지 한 줄씩 출력되고, 마지막에 성공·실패 건수가 정리됩니다. 실패 목록을 받아서 주소를 고친 뒤 그 부분만 다시 돌리면 됩니다.

자주 만나는 오류와 해결법

한글 컴퓨터 이름 때문에 발생한 UnicodeEncodeError
작업 중 실제로 만난 오류입니다. 컴퓨터 이름이 한글이면 EHLO 단계에서 터집니다. local_hostname을 지정하면 해결됩니다.

처음 돌릴 때 열에 아홉은 아래 다섯 가지 중 하나에 걸립니다.

오류 메시지원인해결
SMTPAuthenticationError (535)계정 비밀번호를 그대로 썼거나 2단계 인증이 꺼져 있음2단계 인증을 켜고 앱 비밀번호를 재발급받아 공백 없이 16자리 입력
SMTPServerDisconnected465 포트에 starttls()를 호출했거나 반대로 587에 SMTP_SSL 사용위 표의 포트·접속 방식 조합을 그대로 맞추기
TimeoutError / 응답 없음회사 방화벽이나 백신이 SMTP 포트를 막고 있음휴대폰 핫스팟으로 바꿔 테스트해 보고, 막힌 게 맞으면 사내 SMTP 서버 주소를 문의
SMTPRecipientsRefused수신자 주소 오타 또는 존재하지 않는 도메인CSV 명단에서 공백·오타 확인. strip()으로 앞뒤 공백 제거
보냈는데 안 옴수신자의 스팸함으로 분류됨제목의 과한 특수문자와 대문자를 줄이고, 처음에는 본인 계정으로 테스트

원인이 잡히지 않으면 디버그 모드를 켜세요. 서버와 주고받는 응답 코드가 그대로 보여서 어느 단계에서 끊겼는지 바로 드러납니다.

# 서버와 주고받는 대화를 그대로 출력합니다. 원인 파악할 때 가장 확실한 방법입니다.
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
    smtp.set_debuglevel(1)
    smtp.starttls()
    smtp.login(user, password)
    smtp.send_message(msg)

매일 자동 실행하고, 사고를 피하는 법

스크립트가 완성됐다면 마지막은 스케줄 등록입니다. 윈도우라면 작업 스케줄러에 등록해 매일 정해진 시각에 돌리면 되고, 맥이나 리눅스라면 cron을 쓰면 됩니다. 등록 방법은 이 블로그의 윈도우 작업 스케줄러로 스크립트 자동 실행 설정하기 글에 단계별로 정리해 두었습니다.

다만 자동 발송은 되돌릴 수 없는 작업입니다. 아래 항목은 실제로 사고가 나는 지점이니 한 번씩 확인하세요.

  • 테스트는 반드시 본인 주소로. 명단 파일의 이메일 열을 전부 본인 주소로 바꿔 한 번 돌려보고, 형식이 맞으면 진짜 명단으로 교체하세요.
  • 발송량 한도를 넘기지 마세요. 무료 지메일 계정은 하루 수신자 500명이 상한이고, 넘기면 계정 발송이 일시 정지됩니다. 그 이상이 필요하면 전용 발송 서비스를 쓰는 편이 맞습니다.
  • 여러 명에게 보낼 때 To에 몰아넣지 마세요. 수신자 목록이 서로에게 그대로 노출됩니다. 개인정보 유출로 이어질 수 있으니 위 예제처럼 한 명씩 따로 보내거나 Bcc를 쓰세요.
  • 광고성 메일은 법적 요건이 있습니다. 국내에서 광고 목적으로 메일을 보내려면 수신자의 사전 동의가 필요하고, 제목 맨 앞에 (광고) 표기와 본문에 수신거부 방법을 넣어야 합니다. 사내 공지나 거래 관계 안내는 여기 해당하지 않지만, 마케팅 발송이라면 반드시 확인하세요.
  • 실패 목록을 남기세요. 로그 없이 돌리면 몇 명에게 갔는지 아무도 모릅니다. 위 스크립트처럼 실패 건을 모아 출력하거나 파일로 저장해 두세요.

마무리

가장 작게 시작하는 방법은 이렇습니다. 앱 비밀번호를 발급받고, 두 번째 섹션의 20줄짜리 스크립트를 그대로 복사해 수신자를 본인 주소로 넣고 한 번 실행해 보세요. 메일함에 도착하는 것만 확인하면 나머지는 그 위에 붙여나가면 됩니다. 첨부파일이든 명단 발송이든 결국 같은 send_message() 한 줄을 감싸는 일입니다.

다음 글에서는 협업할 때 가장 자주 지적받는 부분인 깃 커밋 메시지 쓰는 법을, 실제로 쓸 수 있는 예제와 함께 정리하겠습니다.

댓글 남기기