email: примеры

Далее представлены примеры использования пакета email для чтения, записи и отправки простых сообщений электронной почты, а также более сложных MIME сообщений.

Во-первых, давайте посмотрим, как создать и отправить простое текстовое сообщение (и текстовое содержимое, и адреса могут содержать символы Юникода):

# Импортировать smtplib для фактической функции отправки
import smtplib

# Импортировать необходимые нам модули электронной почты
from email.message import EmailMessage

# Открыть простой текстовый файл, имя которого находится в текстовом файле для
# чтения.
with open(textfile) as fp:
    # Создать текстовое / обычное сообщение
    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()

Парсинг заголовков RFC 822 можно легко выполнить с помощью классов из модуля parser:

# Импортировать необходимые нам модули электронной почты
from email.parser import BytesParser, 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 <[email protected]>\n'
        'To: <[email protected]>\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

# И imghdr, чтобы найти типы наших изображений
import imghdr

# Вот модули пакета электронной почты, которые нам понадобятся
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'

# Открыть файлы в двоичном режиме. Используйте imghdr, чтобы выяснить подтип
# MIME для каждого изображения.
for file in pngfiles:
    with open(file, 'rb') as fp:
        img_data = fp.read()
    msg.add_attachment(img_data, maintype='image',
                                 subtype=imghdr.what(None, img_data))

# Отправить электронное письмо через наш собственный 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="""\
Отправить содержимое каталога в виде MIME сообщения. Если не указан параметр
-o, электронное письмо отправляется путём пересылки на ваш локальный SMTP-
сервер, который затем выполняет нормальный процесс доставки. На вашем локальном
компьютере должен быть запущен SMTP-сервер.
""")
    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 = '.'
    # Create the message
    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'd или
        # компрессированы файлы.
        ctype, encoding = mimetypes.guess_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="""\
Распаковать сообщение MIME в каталог файлов.
""")
    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'] = "Ayons asperges pour le déjeuner"
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!

Cela ressemble à un excellent recipie[1] déjeuner.

[1] http://localhost.int/

--Pepé
""")

# Добавитьверсию в формате html. Это преобразует сообщение в составной /
# альтернативный контейнер, с исходным текстовым сообщением в качестве первой
# части и новым сообщением html в качестве второй части.
asparagus_cid = make_msgid()
msg.add_alternative("""\
<html>
  <head></head>
  <body>
    <p>Salut!</p>
    <p>Cela ressemble à un excellent
        <a href="http://localhost.int/">
            recipie
        </a> déjeuner.
    </p>
    <img src="cid:{asparagus_cid}" />
  </body>
</html>
""".format(asparagus_cid=asparagus_cid[1:-1]), subtype='html')
# обратите внимание, что нам нужно отделить <> от msgid для использования в html.

# Теперь добавим связанное изображение в 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

# Импортировать необходимые нам модули электронной почты
from email import policy
from email.parser import BytesParser

# Воображаемый модуль, который заставит это работать и будет безопасным.
from imaginary import magic_html_parser

# В реальной программе вы получите имя файла из аргументов.
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:
    # У magic_html_parser необходимо переписать атрибуты href=«cid: ....», чтобы они
    # указывали на имена файлов в partfiles. Он также должен выполнить безопасную
    # дезинфекцию html. Его можно было написать с помощью html.parser.
    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 <[email protected]>, Fabrette Pussycat <[email protected]>
From: Pepé Le Pew <[email protected]>
Subject: Ayons asperges pour le déjeuner

Salut!

Cela ressemble à un excellent recipie[1] déjeuner.

Сноски

[1]Спасибо Мэтью Диксону Коулзу за оригинальное вдохновение и примеры.