diff --git a/app/main.py b/app/main.py index bf82947..4ebd404 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,6 @@ import datetime as dt import logging +import os import re import secrets import threading @@ -104,9 +105,22 @@ import ssl as _ssl import json as _json2 from email.mime.multipart import MIMEMultipart as _MIMEMultipart from email.mime.text import MIMEText as _MIMEText +from email.mime.image import MIMEImage as _MIMEImage from email.header import Header as _Header from email.utils import formataddr as _formataddr +# MONT logo for HTML emails, attached inline (Content-ID) rather than +# embedded as a base64 data: URI in . Outlook's desktop/Word +# renderer does not support data: URIs for images at all (silently +# drops them), so a cid: reference to an attached image is the only +# way to get the logo to show up there. +_LOGO_EMAIL_PATH = os.path.join(os.path.dirname(__file__), "static", "logo-email.png") +try: + with open(_LOGO_EMAIL_PATH, "rb") as _f: + _LOGO_EMAIL_BYTES = _f.read() +except OSError: + _LOGO_EMAIL_BYTES = b"" + def _generate_password(length: int = 10) -> str: alphabet = _string.ascii_letters + _string.digits while True: @@ -128,11 +142,24 @@ def _send_email(to: str, subject: str, html_body: str) -> None: # connection just to have the SMTP server reject the recipient a few # seconds later with a cryptic 501. raise ValueError(f"'{to}' не похож на email-адрес - уведомление не отправлено") - msg = _MIMEMultipart("alternative") + # "related" wrapper so the inline logo (Content-ID) sits alongside the + # alternative/html part rather than being just another attachment - + # needed for cid: references in html_body to resolve in mail clients. + has_logo = _LOGO_EMAIL_BYTES and "cid:mont_logo" in html_body + msg = _MIMEMultipart("related") if has_logo else _MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = _formataddr((str(_Header(SMTP_FROM_NAME, "utf-8")), SMTP_FROM_EMAIL)) msg["To"] = to - msg.attach(_MIMEText(html_body, "html", "utf-8")) + if has_logo: + alt = _MIMEMultipart("alternative") + alt.attach(_MIMEText(html_body, "html", "utf-8")) + msg.attach(alt) + logo = _MIMEImage(_LOGO_EMAIL_BYTES, "png") + logo.add_header("Content-ID", "") + logo.add_header("Content-Disposition", "inline", filename="logo.png") + msg.attach(logo) + else: + msg.attach(_MIMEText(html_body, "html", "utf-8")) ctx = _ssl.create_default_context() with _smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx) as srv: srv.login(SMTP_USERNAME, SMTP_PASSWORD) @@ -264,8 +291,8 @@ def _apply_access_decision(db, pending, days): if products: items = "".join(f"
  • {escape(p)}
  • " for p in products) products_html = ( - '

    Предоставлен доступ к продуктам:

    ' - f'' + '

    Предоставлен доступ к продуктам:

    ' + f'' ) day_word = "день" if days == 1 else ("дня" if days < 5 else "дней") @@ -275,40 +302,40 @@ def _apply_access_decision(db, pending, days): "Доступ к Инфраструктурному полигону MONT") email_subhead = "Ваш доступ продлён" if is_renewal else "Ваш запрос одобрен" cred_row = ( - f'Пароль' - f'{password}' + f'Пароль' + f'{password}' ) if not is_renewal else "" access_text = f"Вам {email_action} доступ к полигону на {days} {day_word}." html_email = f""" - +
    - +
    -
    - MONT
    -

    {email_subject}

    -

    {email_subhead}

    -
    + MONT +

    {email_subject}

    +

    {email_subhead}

    +
    -

    Здравствуйте, {escape(pending.name)}!
    +

    Здравствуйте, {escape(pending.name)}!
    {access_text}

    - - - - - +
    Адрес портала{portal_url}
    Логин{username}
    + + + + {cred_row} - - + +
    Адрес портала{portal_url}
    Логин{username}
    Доступ до{expires.strftime("%d.%m.%Y")}
    Доступ до{expires.strftime("%d.%m.%Y")}
    {products_html}
    - Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на mont@mont.ru +
    + Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на RGalyaviev@mont.ru
    """ @@ -329,22 +356,22 @@ def _apply_access_decision(db, pending, days): manager_contact = pending.manager if pending.manager else "менеджера MONT" html_email = f""" - +
    - +
    -
    - MONT
    -

    Запрос на доступ к полигону MONT

    -
    + MONT +

    Запрос на доступ к полигону MONT

    +
    -

    Здравствуйте, {escape(pending.name)}!

    +

    Здравствуйте, {escape(pending.name)}!

    К сожалению, на данный момент мы не можем предоставить доступ к полигону.

    -

    +

    Для уточнения деталей свяжитесь с {escape(manager_contact)}.
    - Если не знаете кто ваш менеджер — напишите на mont@mont.ru.

    + Если не знаете кто ваш менеджер — напишите на RGalyaviev@mont.ru.

    +
    С уважением, команда MONT
    @@ -619,8 +646,8 @@ async def _process_callback_query(cq: dict): if products: items = "".join(f"
  • {escape(p)}
  • " for p in products) products_html = ( - '

    Предоставлен доступ к продуктам:

    ' - f'' + '

    Предоставлен доступ к продуктам:

    ' + f'' ) day_word = "день" if days == 1 else ("дня" if days < 5 else "дней") @@ -630,40 +657,40 @@ async def _process_callback_query(cq: dict): "Доступ к Инфраструктурному полигону MONT") email_subhead = "Ваш доступ продлён" if is_renewal else "Ваш запрос одобрен" cred_row = ( - f'Пароль' - f'{password}' + f'Пароль' + f'{password}' ) if not is_renewal else "" access_text = f"Вам {email_action} доступ к полигону на {days} {day_word}." html_email = f""" - +
    - +
    -
    - MONT
    -

    {email_subject}

    -

    {email_subhead}

    -
    + MONT +

    {email_subject}

    +

    {email_subhead}

    +
    -

    Здравствуйте, {escape(pending.name)}!
    +

    Здравствуйте, {escape(pending.name)}!
    {access_text}

    - - - - - +
    Адрес портала{portal_url}
    Логин{username}
    + + + + {cred_row} - - + +
    Адрес портала{portal_url}
    Логин{username}
    Доступ до{expires.strftime("%d.%m.%Y")}
    Доступ до{expires.strftime("%d.%m.%Y")}
    {products_html}
    - Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на mont@mont.ru +
    + Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на RGalyaviev@mont.ru
    """ @@ -693,22 +720,22 @@ async def _process_callback_query(cq: dict): manager_contact = pending.manager if pending.manager else "менеджера MONT" html_email = f""" - +
    - +
    -
    - MONT
    -

    Запрос на доступ к полигону MONT

    -
    + MONT +

    Запрос на доступ к полигону MONT

    +
    -

    Здравствуйте, {escape(pending.name)}!

    +

    Здравствуйте, {escape(pending.name)}!

    К сожалению, на данный момент мы не можем предоставить доступ к полигону.

    -

    +

    Для уточнения деталей свяжитесь с {escape(manager_contact)}.
    - Если не знаете кто ваш менеджер — напишите на mont@mont.ru.

    + Если не знаете кто ваш менеджер — напишите на RGalyaviev@mont.ru.

    +
    С уважением, команда MONT
    @@ -1364,39 +1391,39 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)): products_html = "" if products: items = "".join(f"
  • {escape(p)}
  • " for p in products) - products_html = f"

    Предоставлен доступ к продуктам:

    " + products_html = f"

    Предоставлен доступ к продуктам:

    " html_email = f""" - +
    - +
    -
    - MONT
    -

    Доступ к Инфраструктурному полигону MONT

    -

    Ваш запрос одобрен

    -
    + MONT +

    Доступ к Инфраструктурному полигону MONT

    +

    Ваш запрос одобрен

    +
    -

    Здравствуйте, {escape(pending.name)}!
    +

    Здравствуйте, {escape(pending.name)}!
    Вам предоставлен доступ к полигону на {days} {'день' if days==1 else 'дня' if days<5 else 'дней'}.

    - - - - - - - - - +
    Адрес портала{portal_url}
    Логин{username}
    Пароль{password}
    Доступ до{expires.strftime('%d.%m.%Y')}
    + + + + + + + +
    Адрес портала{portal_url}
    Логин{username}
    Пароль{password}
    Доступ до{expires.strftime('%d.%m.%Y')}
    {products_html}
    - Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на mont@mont.ru +
    + Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на RGalyaviev@mont.ru
    """ @@ -1429,22 +1456,22 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)): manager_contact = pending.manager if pending.manager else "менеджера MONT" html_email = f""" - +
    - +
    -
    - MONT
    -

    Запрос на доступ к полигону MONT

    -
    + MONT +

    Запрос на доступ к полигону MONT

    +
    -

    Здравствуйте, {escape(pending.name)}!

    +

    Здравствуйте, {escape(pending.name)}!

    К сожалению, на данный момент мы не можем предоставить доступ к полигону.

    -

    +

    Для уточнения деталей, пожалуйста, свяжитесь с {escape(manager_contact)}.
    - Если вы не знаете, кто ваш менеджер, напишите нам на mont@mont.ru — мы поможем.

    + Если вы не знаете, кто ваш менеджер, напишите нам на RGalyaviev@mont.ru — мы поможем.

    +
    С уважением, команда MONT
    diff --git a/app/static/logo-email.png b/app/static/logo-email.png new file mode 100644 index 0000000..49805c4 Binary files /dev/null and b/app/static/logo-email.png differ