여러 명에게 정해진 틀에 맞춰서 메일을 보내기 위해 뉴스레터 서비스를 많이 사용합니다. 저는 n명의 전기장판 출판사 저자에게 이메일로 월별 매출현황을 보냅니다. 월별 매출현황은 매월 업데이트됩니다. 저자마다 다른 파일을 보내야하기 때문에, 뉴스레터 서비스를 이용해도 여러번 반복해서 메일을 보내야 합니다.

📝 (1) 매월 저자별 정산파일을 생성한다 → (2) 저자별 정산파일을 매달 전송한다

그래서 매월 저자별 정산파일을 생성하고, 생성된 파일을 메일로 보낼 수 있도록 구성했습니다. 메일에는 첨부파일이 포함될 수도 있고, 아닐 수도 있습니다. 또한, 본문에 이미지가 포함될 수도 있고 아닐 수도 있습니다. 그리고 메일에 받는 사람 이름이 들어가면 좀 더 좋을 것 같습니다. 코드를 짜면서 중점적으로 고려한 사항은 다음과 같습니다.

  1. 첨부파일이 여러개(0~n개) 포함될 수 있다.
  2. 본문에 이미지가 여러개(0~n개) 포함될 수 있다.
  3. 받는 사람마다 본문의 내용이 일부(이름, 직책 등) 수정될 수 있다.

테스트용 첨부파일[test01.txt, test02.txt]과 이미지[test01.jpg, test02.png, test03.png]입니다. 필요하신 분은 다운받아서 사용하세요. 이미지는 경기대학교소성박물관에서 소장하고 있는 십장생도입니다. 국립중앙박물관 e뮤지엄에서 다운로드 받았습니다.

Transclude of 43-1.zip

0. 코드 개요 한눈에 보기

mailing service v1.1.0
├─ mailing_execute.py
├─ mailing_module.py
├─ test01.jpg
├─ test01.txt
├─ test02.png
├─ test02.txt
└─ test03.png

시작하기에 앞서 오늘 공유할 코드 구조를 확인하겠습니다. [mailing service v1.1.0]라는 폴더 안에 [mailing_module.py]와 [mailing_execute.py], 그리고 첨부파일이 있습니다. [mailing_module.py]은 메일을 보내기 위해 필요한 class(이메일 구성, smtp 접속, 메일 보내기)가 담겨있습니다. [mailing_execute.py]는 [mailing_module.py]의 class를 실행하는 코드입니다. 메일 본문과, 첨부파일, 그리고 보내는 사람 및 받는 사람 등의 정보 또한 [mailing_execute.py]에 있습니다.

1. 메일 구성 및 전송 클래스 생성

파이썬 smtplib과 email 모듈을 사용해서 메일을 보내봅시다. 메일을 보내는데 필요한 기본적인 함수는 전부[mailing_module.py]에 작성합니다. 우선 필요한 모듈을 불러옵니다.

1-1. 모듈 불러오기

import smtplib
from email import encoders
from email.utils import formataddr
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

여러 명에게 정해진 틀에 맞춰서 메일을 보낼 수 있도록 class로 구성하겠습니다. 클래스 EmailSenderinit, EmailContent, EmailSend 3개의 def로 구성되어 있습니다.

1-2. class EmailSender

class EmailSender:
    # 세션 생성
    def __init__(self):
        str_host = "smtp.gmail.com"
        num_port = 587
        self.session = smtplib.SMTP(host=str_host, port=num_port)
        self.session.starttls()
        self.session.login("gmail 계정", "앱 비밀번호")
 
    # 이메일 구성
    def EmailContent(self, Subject, Content, ContentArgs, ImagesDict, AttachmentsDict):
        self.msg = MIMEMultipart()
 
        # 이메일 제목
        self.msg["Subject"] = Subject
 
        # 이메일 본문
        body = Content.safe_substitute(ContentArgs)
        mime_body = MIMEText(body, "html")
        self.msg.attach(mime_body)
 
        # 본문에 이미지 추가
        if ImagesDict:
            for image_cid in ImagesDict:
                with open(ImagesDict[image_cid], "rb") as img_file:
                    mime_img = MIMEImage(img_file.read())
                    mime_img.add_header("Content-Disposition", "attachment", filename=ImagesDict[image_cid])
                    mime_img.add_header("Content-ID", "<" + image_cid + ">")
                self.msg.attach(mime_img)
 
        # 첨부파일 추가
        if AttachmentsDict:
            for attachment_name in AttachmentsDict:
                mime_att = MIMEBase("application", "octect-stream")
                with open(AttachmentsDict[attachment_name], "rb") as att_file:
                    mime_att.set_payload(att_file.read())
                    encoders.encode_base64(mime_att)
                    mime_att.add_header("Content-Disposition", "attachment", filename=attachment_name)
                self.msg.attach(mime_att)
 
    # 메일 보내기
    def EmailSend(self, from_addr, to_addrs, cc_addrs):
        self.msg["From"] = formataddr(from_addr)
        self.msg["To"] = ",".join(to_addrs)  # 수신자리스트
        self.msg["Cc"] = ",".join(cc_addrs)  # 참조리스트
 
        self.session.send_message(self.msg)
        del self.msg

class EmaalSender는 세션을 생성하고, 본문을 생성하고, 전송하는 순서로 되어있습니다. def __init__()는 세션을 생성합니다. def EmailContent로 메일 제목, 본문, 첨부파일을 self.msg에 추가합니다. def EmailSend로 앞서 생성한 메일 콘텐츠(self.msg)를 전송합니다.

세션 생성

    # 세션 생성
    def __init__(self):
        str_host = "smtp.gmail.com"
        num_port = 587
        self.session = smtplib.SMTP(host=str_host, port=num_port)
        self.session.starttls()
        self.session.login("gmail 계정", "앱 비밀번호")

host 주소와 port 번호를 받아서 세션을 생성합니다. 보내는 메일 서버에 따라 일부 수정될 수 있습니다. 참고로 gmail을 사용할 때는 ‘앱 비밀번호’를 사용해야 합니다.

이메일 구성

    # 이메일 구성
    def EmailContent(self, Subject, Content, ContentArgs, ImagesDict, AttachmentsDict):

def EmailContent에 필요한 변수를 확인해 봅시다. 아래 변수들은 [mailing_execute.py]에 입력됩니다. 변수를 지정하고 EmailContent의 인스턴스를 생성하는 과정은 뒤에서 좀 더 자세히 살펴보겠습니다.

  • Subject는 제목입니다.
    Subject = "파이썬을 이용한 메일 보내기"
  • Content는 메일 본문(html)입니다.
    Content = Template(
        """<html>
            <head></head>
            <body>
                ${Name}님 안녕하세요.<br>
                ${Company}입니다.<br>
                <br>
                십장생도를 보냅니다.<br>
                <img src="cid:image01"><br>
                <br>
                변환한 파일입니다.<br>
                <img src="cid:image02"><br>
                <img src="cid:image03"><br>
                감사합니다.<br>
                <br>
                이권희 드림.<br>
                <br>
                *본 메일은 테스트 메일입니다.<br>
            </body>
        </html>"""
    )
  • ContentArgs는 본문에 들어갈 맞춤형 변수(딕셔너리 타입)입니다.
    ContentArgs = {"Name": "전기장판 출판사", "Company": "사담미디어"}  # 대치어:딕셔너리
  • ImagesDict는 본문에 추가되는 이미지(딕셔너리 타입)입니다.
    ImagesDict = {"image01": "test01.jpg", "image02": "test02.png", "image03": "test03.png"}  # 이미지:딕셔너리
  • AttachmentsDict는 본문에 추가되는 첨부파일(리스트 타입)입니다.
    AttachmentsDict = {"test01.txt": "test01.txt", "test02.txt": "test02.txt"}  # 첨부파일:딕셔너리(파일명:경로)

MIMEMultipart 생성

        self.msg = MIMEMultipart()

MIMEMultipart()를 생성합니다. MIMEMultipart()는 메일 제목과 본문, 첨부파일이나 이미지 등 메일 컨텐츠를 담는 그릇입니다. 우리는 앞으로 메일의 제목, 본문, 첨부파일 등을 self.msg = MIMEMultipart()에 담을 것입니다. 그리고 MIMEMultipart() 구성이 끝나면, self.msg를 원하는 곳으로 전송하면 끝입니다.

이메일 제목 및 본문 추가

				# 이메일 제목
        self.msg["Subject"] = Subject
 
        # 이메일 본문
        body = Content.safe_substitute(ContentArgs)
        mime_body = MIMEText(body, "html")
        self.msg.attach(mime_body)

MIMEMultipart(), 즉 msg에 이메일 제목과 본문을 추가합니다. 받아온 Content에서 대치어(ContentArgs)를 substitute로 변환해 줍니다. 변환해준 body를 MIMEText(body, 'html') 형태로 self.msg(MIMEMultipart())에 추가합니다.

본문에 이미지 추가

# 본문에 이미지 추가
        if ImagesDict:
            for image_cid in ImagesDict:
                with open(ImagesDict[image_cid], "rb") as img_file:
                    mime_img = MIMEImage(img_file.read())
                    mime_img.add_header("Content-Disposition", "attachment", filename=ImagesDict[image_cid])
                    mime_img.add_header("Content-ID", "<" + image_cid + ">")
                self.msg.attach(mime_img)

본문에 이미지를 추가합니다. ImagesDict가 비었다면 bool(ImagesDict)False가 됩니다. 만약 비어있지 않다면, 이미지를 추가(self.msg.attach(mime_img))합니다.

이때, image_cidimages(딕셔너리)의 key가 됩니다. mime_img.add_header('Content-ID', '<' + image_cid + '>') 명령어를 통해서 본문 html의 원하는 위치(다음과 같이 지정: <img src="cid:image01">)에 key에 대응하는 이미지('test01.jpg’)가 포함되게 됩니다.

첨부파일 추가

# 첨부파일 추가
        if AttachmentsDict:
            for attachment_name in AttachmentsDict:
                mime_att = MIMEBase("application", "octect-stream")
                with open(AttachmentsDict[attachment_name], "rb") as att_file:
                    mime_att.set_payload(att_file.read())
                    encoders.encode_base64(mime_att)
                    mime_att.add_header("Content-Disposition", "attachment", filename=attachment_name)
                self.msg.attach(mime_att)

첨부파일 추가도 이미지 추가와 동일합니다. 다만, MIMEImage()가 아니라 MIMEBase()를 사용합니다. 그리고 첨부파일을 읽어서 변환하는 작업(mime_att.set_payload(att_file.read()), encoders.encode_base64(mime_att))이 추가됩니다.

메일 보내기

# 메일 보내기
    def EmailSend(self, from_addr, to_addrs, cc_addrs):
        self.msg["From"] = formataddr(from_addr)
        self.msg["To"] = ",".join(to_addrs)  # 수신자리스트
        self.msg["Cc"] = ",".join(cc_addrs)  # 참조리스트
 
        self.session.send_message(self.msg)
        del self.msg

다음은 메일을 보내는 과정입니다. from_addrto_addrs는 다음과 같습니다.

앞서 생성한 self.msg를 전송합니다. 보내는 사람은 이름을 지정할 수 있도록 formataddr명령어를 이용합니다. 받는 사람이나 참조도 여러명 포함될 수 있도록 ",".join(to_addrs)를 이용합니다. 수신자도 보낸 사람처럼 이름과 메일을 조합해서 사용하려고 했는데, 여러명일 때 네이버 메일에서 전송이 안되는 오류가 있어서 제외했습니다.

2. 인스턴스 생성 및 메일 전송

[mailing_module.py]에서 클래스를 불러와서 인스턴스를 생성하고, 필요한 변수를 입력하여 메일을 전송해 보겠습니다. 역시 필요한 모듈을 불러옵니다.

2-1. 모듈 불러오기

from string import Template
from mailing_module import *

특별한 부분은 없습니다. Template을 사용할 예정이기 때문에, Template을 불러오고, 나머진[mailing_module.py]에서 전부 불러옵니다.

2-2. 세션 연결 및 본문 생성

emailsender = EmailSender()
 
Subject = "파이썬을 이용한 메일 보내기"
Content = Template(
    """<html>
        <head></head>
        <body>
            ${Name}님 안녕하세요.<br>
            ${Company}입니다.<br>
            <br>
            십장생도를 보냅니다.<br>
            <img src="cid:image01"><br>
            <br>
            변환한 파일입니다.<br>
            <img src="cid:image02"><br>
            <img src="cid:image03"><br>
            감사합니다.<br>
            <br>
            이권희 드림.<br>
            <br>
            *본 메일은 테스트 메일입니다.<br>
        </body>
    </html>"""
)
ContentArgs = {"Name": "전기장판 출판사", "Company": "사담미디어"}  # 대치어:딕셔너리
ImagesDict = {"image01": "test01.jpg", "image02": "test02.png", "image03": "test03.png"}  # 이미지:딕셔너리
AttachmentsDict = {"test01.txt": "test01.txt", "test02.txt": "test02.txt"}  # 첨부파일:딕셔너리(파일명:경로)
 
emailsender.EmailContent(Subject, Content, ContentArgs, ImagesDict, AttachmentsDict)

class EmailSender()를 바탕으로 이메일 제목, 본문, 이미지, 첨부파일이 담긴 self.msg를 생성하겠습니다. 먼저 emailsender = EmailSender()로 세션에 연결합니다.

제목과 본문, 대치어, 이미지, 첨부파일 변수를 만들어줍니다. 만약, 대치어와 이미지, 첨부파일이 없으면 비워두시면 됩니다. 빈 딕셔너리나 리스트는 if 문에서 False처리 되기 때문에, 자연스럽게 건너뛰고 코드가 실행됩니다. emailsender.EmailContent(Subject, Content, ContentArgs, ImagesDict, AttachmentsDict) 명령어로 우리가 보낼 메일 내용이 담긴 self.msg가 생성되었습니다.

2-3. EmailSend로 메일 보내기

from_addr = ("사담미디어", "[email protected]")  # 보내는 사람(이름과 메일주소):튜플
to_addrs = ["[email protected]", "[email protected]"]  # 수신자:리스트
cc_addrs = ["[email protected]"]  # 참조:리스트
 
emailsender.(from_addr, to_addrs, cc_addrs)

앞서 생성한 self.msg를 전송하겠습니다. 보내는 사람과 받는 사람만 지정해주시면 됩니다. 수신자는 리스트 형태로 여러명을 지정해도 됩니다. emailsender.(from_addr, to_addrs, cc_addrs) 명령어를 통해 메일을 보내면 끝!

저랑 같은 첨부파일을 사용하셨다면, 아래와 같은 형태로 메일이 전송됩니다. 참고로 gmail에서 보내는 메일주소를 추가하시면, smtp를 이용할 때도 @gmail이 아닌 다른 주소로 메일을 전송할 수 있습니다.

메일 보내기 결과 / 근데 왜 맥 메일앱은 첨부파일 이미지도 미리보기로 줄까요…


3. 코드 전문

아래는 코드 전문입니다. 위에서 공유한 첨부파일 test01.txt와 test02.txt에 아래 코드가 들어있습니다. 그런데, 초기 버전이라 조금 다른 부분이 있습니다.

3-1. mailing_module.py

# 
class EmailSender:
    # 세션 생성
    def __init__(self):
        str_host = "smtp.gmail.com"
        num_port = 587
        self.session = smtplib.SMTP(host=str_host, port=num_port)
        self.session.starttls()
        self.session.login("gmail 계정", "앱 비밀번호")
 
    # 이메일 구성
    def EmailContent(self, Subject, Content, ContentArgs, ImagesDict, AttachmentsDict):
        self.msg = MIMEMultipart()
 
        # 이메일 제목
        self.msg["Subject"] = Subject
 
        # 이메일 본문
        body = Content.safe_substitute(ContentArgs)
        mime_body = MIMEText(body, "html")
        self.msg.attach(mime_body)
 
        # 본문에 이미지 추가
        if ImagesDict:
            for image_cid in ImagesDict:
                with open(ImagesDict[image_cid], "rb") as img_file:
                    mime_img = MIMEImage(img_file.read())
                    mime_img.add_header("Content-Disposition", "attachment", filename=ImagesDict[image_cid])
                    mime_img.add_header("Content-ID", "<" + image_cid + ">")
                self.msg.attach(mime_img)
 
        # 첨부파일 추가
        if AttachmentsDict:
            for attachment_name in AttachmentsDict:
                mime_att = MIMEBase("application", "octect-stream")
                with open(AttachmentsDict[attachment_name], "rb") as att_file:
                    mime_att.set_payload(att_file.read())
                    encoders.encode_base64(mime_att)
                    mime_att.add_header("Content-Disposition", "attachment", filename=attachment_name)
                self.msg.attach(mime_att)
 
    # 메일 보내기
    def EmailSend(self, from_addr, to_addrs, cc_addrs):
        self.msg["From"] = formataddr(from_addr)
        self.msg["To"] = ",".join(to_addrs)  # 수신자리스트
        self.msg["Cc"] = ",".join(cc_addrs)  # 참조리스트
 
        self.session.send_message(self.msg)
        del self.msg

3-2. mailing_execute.py

# 
emailsender = EmailSender()
# 
from_addr = ("사담미디어", "[email protected]")  # 보내는 사람(이름과 메일주소):튜플
to_addrs = ["[email protected]", "[email protected]"]  # 수신자:리스트
cc_addrs = ["[email protected]"]  # 참조:리스트
 
emailsender.EmailSend(from_addr, to_addrs, cc_addrs)