Przewiń do głównej treści
  1. Blog/

Python wysyłanie wiadomości email z osadzonymi obrazkami

·656 słów·4 min
Programowanie Python Email
Bartosz Płóciennik
Autor
Bartosz Płóciennik
Spis treści

Maile to już dość dojrzała technologia, gdzie pierwsza wiadomość została wysłana w 1969 roku, jednak mimo to wciąż potrafi nastręczyć problemów jeśli chodzi o dostarczalność, wygląd czy załączniki.

Obecnie mamy kilka technik na umieszczenie zdjeć w mailach i nie zawsze jest to prosty wybór. Poniżej znajdziesz przykład wykorzystujący Content ID czyli wysyłanie zdjęć jako załączników i ich użycie w kodzie HTML.

Python
#

Utwórzy plik send_examle_email.py

1
2
3
4
5
6
7
import os
import smtplib
from dataclasses import dataclass
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from jinja2 import Environment, FileSystemLoader

W importach nic szczególnego:

  • skorzystamy z postawowej paczki smtplib
  • dodatkowo użyjemy Jinja2 aby renderować naszą templatkę (bez problemu można użyć zamiast tego Django templates)
1
2
3
4
@dataclass
class Attachment:
    path: str
    content_name: str

dla przejrzystości dorzucamy prostą dataclass.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if __name__ == "__main__":
    jpg_attachment = Attachment(
        path="images/fire.jpg",
        content_name="fire",
    )
    webp_attachment = Attachment(
        path="images/mountains.webp",
        content_name="mountains",
    )

    send_message(
        email_from="[email protected]",
        email_subject="Welcome to the jungle",
        email_to="[email protected]",
        html_template="welcome.html",
        txt_template= "welcome.txt",
        attachments=[jpg_attachment, webp_attachment],
    )

Tworzymy dwa obiekty definiując ich ścieżkę a także content_name, któr będzie kluczowy aby nasz Content ID w pliku HTML potem odpowiednio podstawił obrazek.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def send_message(
    email_from: str,
    email_subject: str,
    email_to: str,
    html_template: str,
    txt_template: str,
    attachments: list[Attachment],
) -> None:

    msg = MIMEMultipart("alternative")
    msg["From"] = email_from
    msg["Subject"] = email_subject
    msg["To"] = email_to

Tworzymy obiekt MIMEMultipart do którego zaraz będziemy dorzucac nasze templatki a także załączniki.

1
2
3
    env = Environment(loader=FileSystemLoader("templates"))
    html_template = env.get_template(html_template)
    txt_template = env.get_template(txt_template)

Aby skorzystać z renderowania tworzymy obiekt Environment i wskazujemy folder w którym ma szukać plików. Następnie pobieramy template na podstawie ścieżki.

1
2
3
4
5
6
7
8
    rendered_html_template = html_template.render({
        "fire_content_id": "fire",
        "mountains_content_id": "mountains",
    })
    rendered_txt_template = txt_template.render()

    msg.attach(MIMEText(rendered_html_template, "html"))
    msg.attach(MIMEText(rendered_txt_template, "plain"))

W przypadku html_template dorzucamy context, który wykorzystamy w naszej templatce, aby wskazać nazwę Content ID obrazka do “podstawienia”.

1
2
3
4
5
6
7
8
9
    for attachment in attachments:
        path = os.path.abspath(attachment.path)
        with open(path, "rb") as file:
            content_file = MIMEImage(file.read())
            content_file.add_header(
                "Content-ID",
                f"<{attachment.content_name}>",
            )
            msg.attach(content_file)

Otwieramy nasz plik a następnie wrzucamy go do obiektu MIMEImage a nastepnie dodajemy nagłówek Content-ID, dzięki któremu będziemy mogli użyć pliku w naszej wyrenderowanej templatce.

Zauważ, że wartość tego nagłówka musi być zawarta pomiędzy znakami <name> inaczej nie zadziała 😒

1
2
    server = smtplib.SMTP(host="mailpit", port=1025)
    server.send_message(msg=msg)

no i na koniec wysyłka naszej wiadomości.

Templatki
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<html>
<body>

<table>
    <tbody>
    <tr>
        <td>JPG</td>
        <!-- cid:fire -->
        <td><img src="cid:{{fire_content_id}}" /></td>
    </tr>
    <tr>
        <td>WEBP</td>
        <!-- cid:mountains -->
        <td><img src="cid:{{mountains_content_id}}" /></td>
    </tr>
    </tbody>
</table>

</body>
</html>

Nasz template tworzymy pod templates/welcome.html a w środku przy image src stosujemy składnie cid:nasza_nazwa. Pamiętaj aby nazwa była taka jak w naszym kodzie.

Welcome, just simple text template...

Te templatka txt nie jest w ogóle potrzebna, ale dorzuciłem ją dla zasady. Powinna zostać utworzona pod templates/welcome.txt.

Rezultat
#

Inbox reuslt

Nasze obrazki w kodzie maila koniec końców i tak będą wrzucone jako base64 a jedyna róźnica od zwykłego wrzucenia ich jako base64 jest taka, że dodatkowo będa widoczne jako załączniki (niesamowite, prawda?).

Content-Type: image/jpeg
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-ID: <fire>

/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU
FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo
KCgoKCgoKCgoKCgoKCgoKCgoKC...

Obsługa SVG
#

Jeśli chodzi o SVG to temat nie jest taki prosty i w powyższym przykładzie odpuściłem sobie próby jego zaimplementowania poprzez Content ID.

W teorii powinno się to sprowadzać do tego aby MIMEImage otrzymywał typ obrazka MIMEImage(file.read(), _subtype="image/svg+xml"), ale w praktyce nie działa to tak jak należy.

Jeśli koniecznie potrzebujesz SVG to śmiało polecam artykuł CSS Tricks.

Przykładowy kod
#

bplociennik/bplociennik-examples

Python
0
0

W powyższym repo w folderze python-email-embed-image znajdziesz gotowy przykład z tego wpisu, który po odpaleniu wyśle takiego testowego maila na kontener mailpit pod http://localhost:8025/.

Źródła
#

Related

Prymitywna wtyczka do Firefoxa napisana przez GitHub Copilot AI
·443 słów·3 min
Programowanie AI Firefox YouTube
Rzuciłem sobie wyzwanie a raczej AI, aby w 30 minut stworzyć działającą wtyczkę do Firefoxa. Całkiem sporo korzystam z YouTube. Najczęściej chyba ze strony gdzie mam filmy od wszystkch, których subskrybuje. Jak coś obejrzę to często nie wracam już do takiego filmu i lubię mieć porządek. YouTube daje nam taką opcję poprzez funkcję ukryj.
Notion jak dostać bana na adres IP z Cloudflare za zapisanie notatki
·312 słów·2 min
Cyberbezpieczeństwo Notion Cloudflare
Zobacz jak drobna notatka zapisana w Notion może spowodować otrzymanie bana na adres IP od Cloudflare a także jak może to potencjalnie utrudnić prace innym.