feat: update style of mail

This commit is contained in:
2026-04-19 10:53:18 +03:00
parent 323e309c7e
commit 10e8dc4e96
6 changed files with 82 additions and 22 deletions

View File

@@ -4,5 +4,13 @@ from abc import ABC, abstractmethod
class ISender(ABC): class ISender(ABC):
@abstractmethod @abstractmethod
async def send(self, to: str, subject: str, body: str, plain: str | None = None) -> None: async def send(
self,
to: str,
subject: str,
body: str,
plain: str | None = None,
*,
inline_png: tuple[str, bytes] | None = None,
) -> None:
raise NotImplementedError raise NotImplementedError

View File

@@ -1,11 +1,11 @@
import base64
from pathlib import Path from pathlib import Path
_LOGO_PATH = Path(__file__).resolve().parent / 'templates' / 'static' / 'exa-logo.png' _LOGO_PATH = Path(__file__).resolve().parent / 'templates' / 'static' / 'exa-logo.png'
EXA_LOGO_CID = 'exa-logo'
def get_exa_logo_data_uri() -> str | None:
def get_exa_logo_png() -> bytes | None:
if not _LOGO_PATH.is_file(): if not _LOGO_PATH.is_file():
return None return None
data = base64.b64encode(_LOGO_PATH.read_bytes()).decode('ascii') return _LOGO_PATH.read_bytes()
return f'data:image/png;base64,{data}'

View File

@@ -1,5 +1,10 @@
import aiosmtplib import aiosmtplib
from email.message import EmailMessage from email.message import EmailMessage
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.policy import SMTP
from src.application.contracts.i_sender import ISender from src.application.contracts.i_sender import ISender
@@ -23,17 +28,34 @@ class EmailSender(ISender):
self._use_tls = use_tls self._use_tls = use_tls
self._timeout = timeout self._timeout = timeout
async def send(self, to: str, subject: str, body: str, plain: str | None = None) -> None: async def send(
message = EmailMessage() self,
message["From"] = self._from_addr to: str,
message["To"] = to subject: str,
message["Subject"] = subject body: str,
plain: str | None = None,
if plain: *,
message.set_content(plain) inline_png: tuple[str, bytes] | None = None,
message.add_alternative(body, subtype="html") ) -> None:
if inline_png and plain:
message = self._multipart_with_inline_png(
to=to,
subject=subject,
plain=plain,
html=body,
content_id=inline_png[0],
png_bytes=inline_png[1],
)
else: else:
message.set_content(body, subtype="html") message = EmailMessage()
message['From'] = self._from_addr
message['To'] = to
message['Subject'] = subject
if plain:
message.set_content(plain)
message.add_alternative(body, subtype='html')
else:
message.set_content(body, subtype='html')
await aiosmtplib.send( await aiosmtplib.send(
message, message,
@@ -41,6 +63,34 @@ class EmailSender(ISender):
port=self._port, port=self._port,
username=self._username, username=self._username,
password=self._password, password=self._password,
use_tls=True, use_tls=self._use_tls,
timeout=self._timeout, timeout=self._timeout,
) )
def _multipart_with_inline_png(
self,
*,
to: str,
subject: str,
plain: str,
html: str,
content_id: str,
png_bytes: bytes,
) -> MIMEMultipart:
root = MIMEMultipart('alternative', policy=SMTP)
root['Subject'] = subject
root['From'] = self._from_addr
root['To'] = to
root.attach(MIMEText(plain, 'plain', 'utf-8'))
related = MIMEMultipart('related', policy=SMTP)
related.attach(MIMEText(html, 'html', 'utf-8'))
image = MIMEImage(png_bytes, _subtype='png', policy=SMTP)
image.add_header('Content-ID', f'<{content_id}>')
image.add_header('Content-Disposition', 'inline', filename='exa-logo.png')
related.attach(image)
root.attach(related)
return root

View File

@@ -28,8 +28,8 @@
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"> <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr> <tr>
<td align="left" valign="middle"> <td align="left" valign="middle">
{% if logo_data_uri %} {% if logo_src %}
<img src="{{ logo_data_uri }}" alt="{{ brand }}" width="200" height="auto" <img src="{{ logo_src }}" alt="{{ brand }}" width="200" height="auto"
style="display:block;max-width:200px;height:auto;border:0;outline:none;text-decoration:none;" /> style="display:block;max-width:200px;height:auto;border:0;outline:none;text-decoration:none;" />
{% else %} {% else %}
<div style="font-family:Inter,'Segoe UI',Arial,Helvetica,sans-serif; <div style="font-family:Inter,'Segoe UI',Arial,Helvetica,sans-serif;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -3,7 +3,7 @@ from faststream.rabbit.fastapi import RabbitRouter, RabbitMessage
from src.infrastructure.config import settings from src.infrastructure.config import settings
from src.infrastructure.logger import logger from src.infrastructure.logger import logger
from src.infrastructure.mail import TemplateRenderer, get_email_sender, get_renderer from src.infrastructure.mail import TemplateRenderer, get_email_sender, get_renderer
from src.infrastructure.mail.assets import get_exa_logo_data_uri from src.infrastructure.mail.assets import EXA_LOGO_CID, get_exa_logo_png
from src.infrastructure.mail.sender import EmailSender from src.infrastructure.mail.sender import EmailSender
from datetime import datetime from datetime import datetime
from typing import Literal, Optional from typing import Literal, Optional
@@ -53,7 +53,8 @@ async def consume_email_code(
f"trace_id={trace_id}" f"trace_id={trace_id}"
) )
logo_data_uri = get_exa_logo_data_uri() logo_png = get_exa_logo_png()
logo_src = f'cid:{EXA_LOGO_CID}' if logo_png else ''
html = renderer.render( html = renderer.render(
'email_code.html', 'email_code.html',
@@ -61,7 +62,7 @@ async def consume_email_code(
code=msg_body.payload.code, code=msg_body.payload.code,
ttl_minutes=msg_body.payload.ttl_seconds // 60, ttl_minutes=msg_body.payload.ttl_seconds // 60,
brand='Экса', brand='Экса',
logo_data_uri=logo_data_uri, logo_src=logo_src,
trace_id=trace_id, trace_id=trace_id,
year=datetime.now().year, year=datetime.now().year,
) )
@@ -80,4 +81,5 @@ async def consume_email_code(
subject='Экса — код подтверждения', subject='Экса — код подтверждения',
body=html, body=html,
plain=text, plain=text,
inline_png=(EXA_LOGO_CID, logo_png) if logo_png else None,
) )