# `email`: 예제

다음은 `email` 패키지를 사용하여 간단한 전자 우편 메시지뿐만 아니라 더 복잡한 MIME 메시지를 읽고 쓰고 보내는 방법에 대한 몇 가지 예입니다.

먼저 간단한 텍스트 메시지를 만들고 보내는 방법을 살펴보겠습니다 (텍스트 내용과 주소에 유니코드 문자가 포함될 수 있습니다):

    # 실제 전송 기능을 위해 smtplib를 임포트 합니다
    import smtplib

    # 필요한 email 모듈을 임포트 합니다
    from email.message import EmailMessage

    # textfile에 있는 이름의 텍스트 파일을 읽기 위해 엽니다.
    with open(textfile) as fp:
        # Create a text/plain message
        msg = EmailMessage()
        msg.set_content(fp.read())

    # me == 발신자의 전자 우편 주소
    # you == 수신자의 전자 우편 주소
    msg['Subject'] = f'The contents of {textfile}'
    msg['From'] = me
    msg['To'] = you

    # 자체 SMTP 서버를 통해 메시지를 보냅니다.
    s = smtplib.SMTP('localhost')
    s.send_message(msg)
    s.quit()

`parser` 모듈의 클래스를 사용하여 RFC 822 헤더를 쉽게 구문 분석할 수 있습니다:

    # 필요한 email 모듈을 임포트 합니다
    #from email.parser import BytesParser
    from email.parser import Parser
    from email.policy import default

    # 전자 우편 헤더가 파일에 있으면, 이 두 줄의 주석 처리를 제거하십시오:
    # with open(messagefile, 'rb') as fp:
    #     headers = BytesParser(policy=default).parse(fp)

    #  또는 문자열에서 헤더를 구문 분석하려면 (이는 드문 작업입니다), 다음을 사용하십시오:
    headers = Parser(policy=default).parsestr(
            'From: Foo Bar <user@example.com>\n'
            'To: <someone_else@example.com>\n'
            'Subject: Test message\n'
            '\n'
            'Body would go here\n')

    #  이제 헤더 항목을 딕셔너리로 액세스 할 수 있습니다:
    print('To: {}'.format(headers['to']))
    print('From: {}'.format(headers['from']))
    print('Subject: {}'.format(headers['subject']))

    # 또한 주소의 일부에 액세스 할 수 있습니다:
    print('Recipient username: {}'.format(headers['to'].addresses[0].username))
    print('Sender name: {}'.format(headers['from'].addresses[0].display_name))

다음은 디렉터리에 있을 수 있는 가족사진을 포함하는 MIME 메시지를 보내는 방법의 예입니다:

    # 실제 전송 기능을 위해 smtplib를 임포트 합니다.
    import smtplib

    # 다음은 필요한 email 패키지입니다.
    from email.message import EmailMessage

    # 컨테이너 전자 우편 메시지를 만듭니다.
    msg = EmailMessage()
    msg['Subject'] = 'Our family reunion'
    # me == 발신자의 전자 우편 주소
    # family = 모든 수신자의 전자 우편 주소 목록
    msg['From'] = me
    msg['To'] = ', '.join(family)
    msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'

    # 바이너리 모드로 파일을 엽니다. MIMEImage 가 추측하길 원하면
    # subtype 을 생략할 수도 있습니다.
    for file in pngfiles:
        with open(file, 'rb') as fp:
            img_data = fp.read()
        msg.add_attachment(img_data, maintype='image',
                                     subtype='png')

    # 자체 SMTP 서버를 통해 전자 우편을 보냅니다.
    with smtplib.SMTP('localhost') as s:
        s.send_message(msg)

다음은 디렉터리의 전체 내용을 전자 우편 메시지로 보내는 방법의 예입니다: [1]

    #!/usr/bin/env python3

    """디렉터리의 내용을 MIME 메시지로 보냅니다."""

    import os
    import smtplib
    # 파일 이름 확장자를 기반으로 MIME 유형을 추측하기 위해
    import mimetypes

    from argparse import ArgumentParser

    from email.message import EmailMessage
    from email.policy import SMTP

    def main():
        parser = ArgumentParser(description="""\
    Send the contents of a directory as a MIME message.
    Unless the -o option is given, the email is sent by forwarding to your local
    SMTP server, which then does the normal delivery process.  Your local machine
    must be running an SMTP server.
    """)
        parser.add_argument('-d', '--directory',
                            help="""Mail the contents of the specified directory,
                            otherwise use the current directory.  Only the regular
                            files in the directory are sent, and we don't recurse to
                            subdirectories.""")
        parser.add_argument('-o', '--output',
                            metavar='FILE',
                            help="""Print the composed message to FILE instead of
                            sending the message to the SMTP server.""")
        parser.add_argument('-s', '--sender', required=True,
                            help='The value of the From: header (required)')
        parser.add_argument('-r', '--recipient', required=True,
                            action='append', metavar='RECIPIENT',
                            default=[], dest='recipients',
                            help='A To: header value (at least one required)')
        args = parser.parse_args()
        directory = args.directory
        if not directory:
            directory = '.'
        # 메시지를 만듭니다
        msg = EmailMessage()
        msg['Subject'] = f'Contents of directory {os.path.abspath(directory)}'
        msg['To'] = ', '.join(args.recipients)
        msg['From'] = args.sender
        msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'

        for filename in os.listdir(directory):
            path = os.path.join(directory, filename)
            if not os.path.isfile(path):
                continue
            # 파일 확장자를 기준으로 콘텐츠 유형을 추측합니다.
            # gzip 되었거나 압축된 파일과 같은 간단한 사항을
            # 확인해야 하지만, 인코딩은 무시됩니다.
            ctype, encoding = mimetypes.guess_file_type(path)
            if ctype is None or encoding is not None:
                # 추측할 수 없거나 파일이 인코딩(압축)되었기 때문에,
                # 일반적인 비트 덩어리 유형을 사용합니다.
                ctype = 'application/octet-stream'
            maintype, subtype = ctype.split('/', 1)
            with open(path, 'rb') as fp:
                msg.add_attachment(fp.read(),
                                   maintype=maintype,
                                   subtype=subtype,
                                   filename=filename)
        # 이제 메시지를 보내거나 저장합니다
        if args.output:
            with open(args.output, 'wb') as fp:
                fp.write(msg.as_bytes(policy=SMTP))
        else:
            with smtplib.SMTP('localhost') as s:
                s.send_message(msg)

    if __name__ == '__main__':
        main()

다음은 위와 같은 MIME 메시지를 디렉터리로 푸는 방법의 예입니다:

    #!/usr/bin/env python3

    """MIME 메시지를 디렉터리에 풉니다."""

    import os
    import email
    import mimetypes

    from email.policy import default

    from argparse import ArgumentParser

    def main():
        parser = ArgumentParser(description="""\
    Unpack a MIME message into a directory of files.
    """)
        parser.add_argument('-d', '--directory', required=True,
                            help="""Unpack the MIME message into the named
                            directory, which will be created if it doesn't already
                            exist.""")
        parser.add_argument('msgfile')
        args = parser.parse_args()

        with open(args.msgfile, 'rb') as fp:
            msg = email.message_from_binary_file(fp, policy=default)

        try:
            os.mkdir(args.directory)
        except FileExistsError:
            pass

        counter = 1
        for part in msg.walk():
            # multipart/* 는 단지 컨테이너입니다
            if part.get_content_maintype() == 'multipart':
                continue
            # 전자 우편 메시지를 사용하여 중요한 파일을 덮어쓸 수 없도록 응용 프로그램은
            # 주어진 파일명을 청소해야 합니다.
            filename = part.get_filename()
            if not filename:
                ext = mimetypes.guess_extension(part.get_content_type())
                if not ext:
                    # 일반적인 비트 덩어리 확장자를 사용합니다
                    ext = '.bin'
                filename = f'part-{counter:03d}{ext}'
            counter += 1
            with open(os.path.join(args.directory, filename), 'wb') as fp:
                fp.write(part.get_payload(decode=True))

    if __name__ == '__main__':
        main()

다음은 대체 일반 텍스트 버전으로 HTML 메시지를 만드는 방법의 예입니다. 좀 더 흥미롭게 하기 위해, html 부분에 관련 이미지를 포함하고, 보낼 뿐만 아니라, 보낼 것의 사본을 디스크에 저장합니다.

    #!/usr/bin/env python3

    import smtplib

    from email.message import EmailMessage
    from email.headerregistry import Address
    from email.utils import make_msgid

    # 기본 텍스트 메시지를 만듭니다.
    msg = EmailMessage()
    msg['Subject'] = "Pourquoi pas des asperges pour ce midi ?"
    msg['From'] = Address("Pepé Le Pew", "pepe", "example.com")
    msg['To'] = (Address("Penelope Pussycat", "penelope", "example.com"),
                 Address("Fabrette Pussycat", "fabrette", "example.com"))
    msg.set_content("""\
    Salut!

    Cette recette [1] sera sûrement un très bon repas.

    [1] http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718

    --Pepé
    """)

    # html 버전을 추가합니다. 그러면 메시지가 원본 텍스트 메시지가
    # 첫 번째 부분이고 새 html 메시지가 두 번째 부분인
    # multipart/alternative 컨테이너로 변환됩니다.
    asparagus_cid = make_msgid()
    msg.add_alternative("""\
    <html>
      <head></head>
      <body>
        <p>Salut!</p>
        <p>Cette
            <a href="http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718">
                recette
            </a> sera sûrement un très bon repas.
        </p>
        <img src="cid:{asparagus_cid}">
      </body>
    </html>
    """.format(asparagus_cid=asparagus_cid[1:-1]), subtype='html')
    # html에서 사용하기 위해 msgid에서 <>를 벗겨야 했습니다.

    # 이제 관련 이미지를 html 부분에 추가합니다.
    with open("roasted-asparagus.jpg", 'rb') as img:
        msg.get_payload()[1].add_related(img.read(), 'image', 'jpeg',
                                         cid=asparagus_cid)

    # 보낼 것의 지역 사본을 만듭니다.
    with open('outgoing.msg', 'wb') as f:
        f.write(bytes(msg))

    # 지역 SMTP 서버를 통해 메시지를 보냅니다.
    with smtplib.SMTP('localhost') as s:
        s.send_message(msg)

마지막 예에서 메시지를 보냈다면, 다음은 그것을 처리하는 한 가지 방법입니다:

    import os
    import sys
    import tempfile
    import mimetypes
    import webbrowser

    # 필요한 email 모듈을 임포트 합니다
    from email import policy
    from email.parser import BytesParser

    def magic_html_parser(html_text, partfiles):
        """partfiles 에 링크된 안전 위생 처리된 html 을 반환합니다.

        partfiles의 파일명을 가리키도록 href="cid:...." 어트리뷰트를 다시 씁니다.
        사소하지는 않지만, html.parser 를 사용하면 가능합니다.
        """
        raise NotImplementedError("Add the magic needed")

    # 실제 프로그램에서는 인자에서 파일명을 얻습니다.
    with open('outgoing.msg', 'rb') as fp:
        msg = BytesParser(policy=policy.default).parse(fp)

    # 이제 헤더 항목을 딕셔너리로 액세스할 수 있으며, 비 ASCII는
    # 유니코드로 변환됩니다:
    print('To:', msg['to'])
    print('From:', msg['from'])
    print('Subject:', msg['subject'])

    # 메시지 내용의 미리 보기를 인쇄하려면, 가장 덜 포맷된 형식의
    # 페이 로드를 추출하고 처음 세 줄을 인쇄합니다. 물론, 메시지에
    # 일반 텍스트 부분이 없을 때, html의 처음 세 줄을 인쇄하는 것은
    # 쓸모가 없지만, 이것은 단지 개념적인 예일 뿐입니다.
    simplest = msg.get_body(preferencelist=('plain', 'html'))
    print()
    print(''.join(simplest.get_content().splitlines(keepends=True)[:3]))

    ans = input("View full message?")
    if ans.lower()[0] == 'n':
        sys.exit()

    # 표시하기 위해 가장 풍부한 대안을 추출할 수 있습니다:
    richest = msg.get_body()
    partfiles = {}
    if richest['content-type'].maintype == 'text':
        if richest['content-type'].subtype == 'plain':
            for line in richest.get_content().splitlines():
                print(line)
            sys.exit()
        elif richest['content-type'].subtype == 'html':
            body = richest
        else:
            print("Don't know how to display {}".format(richest.get_content_type()))
            sys.exit()
    elif richest['content-type'].content_type == 'multipart/related':
        body = richest.get_body(preferencelist=('html'))
        for part in richest.iter_attachments():
            fn = part.get_filename()
            if fn:
                extension = os.path.splitext(part.get_filename())[1]
            else:
                extension = mimetypes.guess_extension(part.get_content_type())
            with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as f:
                f.write(part.get_content())
                # cid의 전자 우편 형식에서 html 형식으로 가기 위해 <>을 다시 제거합니다.
                partfiles[part['content-id'][1:-1]] = f.name
    else:
        print("Don't know how to display {}".format(richest.get_content_type()))
        sys.exit()
    with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
        f.write(magic_html_parser(body.get_content(), partfiles))
    webbrowser.open(f.name)
    os.remove(f.name)
    for fn in partfiles.values():
        os.remove(fn)

    # 물론, 이 간단한 프로그램을 망칠 수 있는 많은 전자 우편
    # 메시지가 있지만, 가장 흔한 것을 처리합니다.

프롬프트까지, 위의 출력은 다음과 같습니다:

    To: Penelope Pussycat <penelope@example.com>, Fabrette Pussycat <fabrette@example.com>
    From: Pepé Le Pew <pepe@example.com>
    Subject: Pourquoi pas des asperges pour ce midi ?

    Salut!

    Cette recette [1] sera sûrement un très bon repas.

각주

[1]

영감과 예를 주신 Matthew Dixon Cowles에게 감사드립니다.