@@ -89,6 +89,10 @@ class BusinessUserHost(BaseModel): is_log_history_enabled = models.BooleanField(default=False, verbose_name=_('Log history enabled')) + @property + def display_name(self) -> str: + return self.company_name or self.user.email + @property def accounts(self) -> QuerySet[BusinessAccount]: return self.accounts @@ -1,11 +1,13 @@ import logging +from typing import Any, Sequence import dns.resolver from django.conf import settings from django.core.mail import EmailMessage, send_mail from django.template import TemplateDoesNotExist from django.template.loader import get_template -from django.utils.html import format_html, strip_tags +from django.utils.html import strip_tags +from django.utils.safestring import SafeString from django.utils.translation import gettext_lazy as _ from authentication.exceptions.email_exceptions import LetterNotFound, LetterUnknownException @@ -28,16 +30,18 @@ class EmailService: self.user.save() return self.user.is_subscribed_to_emails - def send_email(self, subject: str, message: str, user_email: str): + @staticmethod + def send_email(subject: str, message: str, user_emails: Sequence[str]) -> None: try: - email_domain = user_email.split('@')[-1] - dns.resolver.resolve(email_domain, 'MX') + for user_email in user_emails: + email_domain = user_email.split('@')[-1] + dns.resolver.resolve(email_domain, 'MX') send_mail( subject=subject, message=strip_tags(message), html_message=message, from_email=settings.EMAIL_HOST_USER, - recipient_list=(user_email,), + recipient_list=user_emails, auth_password=settings.EMAIL_HOST_PASSWORD, ) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout): @@ -46,134 +50,98 @@ class EmailService: logger.exception(exc) raise Exception(_('Error occured when proceed email sending')) + @staticmethod + def _render_letter_template(template_name: str, context: dict[str, Any]) -> SafeString: + try: + template = get_template(f'{template_name}.html') + html_message = template.render(context=context) + except TemplateDoesNotExist: + raise LetterNotFound + except Exception as exc: + raise LetterUnknownException from exc + return html_message + def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() - self.send_email( - subject='Подтверждение аккаунта', - message=format_html( - """ - Добро пожаловать на платформу AIR! - - Ваша почта на платформе - {} - Для того, чтобы подтвердить регистрацию - перейдите по ссылке ниже: - [Нажмите для активации] - Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку: - {}?token={} - """, - self.user.email, - settings.USER_CONFIRMATION_URL, - token.key, - settings.USER_CONFIRMATION_URL, - token.key, - ), - user_email=self.user.email, + html_message = self._render_letter_template( + template_name='authentication/account_confirmation', + context={'email': self.user.email, 'confirmation_url': settings.USER_CONFIRMATION_URL, 'confirmation_token': token.key} ) + self.send_email(subject='Подтверждение аккаунта', message=html_message, user_emails=(self.user.email,)) def send_password_reset_email(self): token = EmailTokenService(self.user).generate_user_token() self.send_email( subject='Сброс пароля', message=f'{settings.USER_PASSWORD_RESET_URL}?token={token.key}', - user_email=self.user.email, + user_emails=(self.user.email,) ) def send_error_email(self, error: ErrorReport): - message = f""" - Пользователь {error.author.email} сообщил об ошибке: - {error.report_text} - Скриншоты ошибки (если пользователь их приложил) - во вложении - """ - + html_message = self._render_letter_template( + template_name='authentication/error_email', + context={'email': error.author.email, 'report_text': error.report_text} + ) mail = EmailMessage( subject=f'Ошибка у пользователя {error.author.email}!', - body=message, + body=html_message, from_email=settings.EMAIL_HOST_USER, to=settings.ERROR_EMAIL_RECIPIENTS, reply_to=(error.author.email,), ) + mail.content_subtype = 'html' if error.additional_images is not None: for _, img in error.additional_images.items(): image = MinIOService().get_object('air-errors', img) mail.attach(img, image) - mail.send(fail_silently=True) def send_copr_purchase_email(self, host: BusinessUserHost): - message = f""" - Пользователь {host.user.email} зарегистрировал корпоративный аккаунт: - - Компания: {host.company_name} - Сектор: {host.company_sector} - ИНН: {host.ITN} - ОГРН: {host.PSRN} - Контактное лицо: - {host.preferred_name}, {host.job_title} - {host.corporate_phone} - {host.corporate_email} - """ - - mail = EmailMessage( - subject='Новый корпоративный аккаунт', - body=message, - from_email=settings.EMAIL_HOST_USER, - to=('a.ippolitov@air.fail', 't.bikbov@air.fail'), + html_message = self._render_letter_template(template_name='authentication/corporate_purchase_email', context={'host': host}) + self.send_email( + subject='Новый корпоративный аккаунт', message=html_message, user_emails=('a.ippolitov@air.fail', 't.bikbov@air.fail') ) - mail.send(fail_silently=True) def send_corporate_greeting_email(self, account: BusinessAccount, password: str | None = None): if self.user.host_account is None: raise Exception(_('Regular users cannot send introductory letters')) token = EmailTokenService(account.user).generate_user_token() - try: - template = get_template('authentication/corporate_greeting_email.html') - html_message = template.render( - context={ - 'company_name': self.user.host_account.company_name, - 'invitation_url': settings.INVITATION_RESPONSE_URL, - 'token': token.key, - 'email': account.user.email, - 'password': password, - } - ) - except TemplateDoesNotExist: - raise LetterNotFound - except Exception as exc: - raise LetterUnknownException from exc + html_message = self._render_letter_template( + template_name='authentication/corporate_greeting_email', + context={ + 'business_account': account, + 'invitation_url': settings.INVITATION_RESPONSE_URL, + 'token': token.key, + 'password': password, + } + ) self.send_email( - subject='Ваш аккаунт на платформе AIR', message=html_message, user_email=account.user.email + subject='Ваш аккаунт на платформе AIR', message=html_message, user_emails=(account.user.email,) ) def send_reinvited_email(self, account: BusinessAccount, password: str | None = None) -> None: - try: - template = get_template('authentication/reinvited_employee_letter.html') - html_message = template.render(context={'email': account.user.email, 'password': password}) - except TemplateDoesNotExist: - raise LetterNotFound - except Exception as exc: - raise LetterUnknownException from exc - self.send_email( - subject='Ваш аккаунт на платформе AIR', message=html_message, user_email=account.user.email + html_message = self._render_letter_template( + template_name='authentication/reinvited_employee_letter', + context={'email': account.user.email, 'password': password} ) + self.send_email(subject='Ваш аккаунт на платформе AIR', message=html_message, user_emails=(account.user.email,)) - def send_corporate_invitation_email(self, account: BusinessAccount): + def send_corporate_invitation_email(self, account: BusinessAccount) -> None: if self.user.host_account is None: raise Exception(_('Regular users cannot send invitation letters')) token = EmailTokenService(account.user).generate_user_token() - message = format_html( - """ - Добрый день, {}! - Компания {} приглашает вас получить доступ - к корпоративному аккаунту на нашей платформе. - Чтобы ответить на приглашение - перейдите по ссылке ниже: - [Нажмите для подтверждения] - Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку: - {}?token={} - """, - account.user.email, - self.user.host_account.company_name, - settings.INVITATION_RESPONSE_URL, - token.key, - settings.INVITATION_RESPONSE_URL, - token.key, + html_message = self._render_letter_template( + template_name='authentication/corporate_invitation_email', + context={ + "email": account.user.email, + "host": self.user.host_account, + "invite_url": settings.INVITATION_RESPONSE_URL, + "token": token.key, + } ) - self.send_email('', message, account.user.email) + self.send_email('Приглашение на платформу AIR', html_message, (account.user.email,)) + + @classmethod + def send_low_balance_email(cls, host: BusinessUserHost) -> None: + html_message = cls._render_letter_template(template_name='authentication/low_balance_email', context={'host': host}) + cls.send_email(f'AIR: баланс корпоративного аккаунта ниже {host.token_cap}', html_message, host.token_cap_emails) @@ -0,0 +1,16 @@ + + + + + Подтверждение аккаунта + + +

Добро пожаловать на платформу AIR!

+
+

Ваша почта на платформе - {{ email }}

+

Для того, чтобы подтвердить регистрацию - перейдите по ссылке ниже:

+

[Нажмите для активации]

+

Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку:

+

{{ confirmation_url }}?token={{ confirmation_token }}

+ + \ No newline at end of file @@ -7,7 +7,7 @@

Добро пожаловать на платформу AIR!

- Этот аккаунт был создан для вас {{ company_name }} в рабочих целях.
+ Этот аккаунт был создан для вас {{ business_account.parent_company.display_name }} в рабочих целях.
Вы можете использовать все ресурсы корпоративного аккаунта — оплачивать подписку нет необходимости.

@@ -18,7 +18,7 @@ {{ invitation_url }}?token={{ token }}

Ваши данные для входа:
- E-mail: {{ email }}
+ E-mail: {{ business_account.user.email }}
Пароль: {{ password }}

Всегда с вами, команда AIR

@@ -0,0 +1,19 @@ + + + + + Приглашение на платформу AIR + + +

Добрый день, {{ email }}!

+ +

{% if host.company_name %}Компания{% else %}Пользователь{% endif %} {{ host.display_name }} приглашает вас получить доступ к корпоративному + аккаунту на нашей платформе.

+

Чтобы ответить на приглашение – перейдите по ссылке ниже:

+

Нажмите для подтверждения

+ +

Если ссылка не открывается по кнопке, вставьте её в адресную строку + браузера:
{{ invite_url }}?token={{ token }}

+

Всегда с вами, команда AIR

+ + @@ -0,0 +1,24 @@ + + + + + Новый корпоративный аккаунт + + +

Пользователь {{ host.user.email }} зарегистрировал корпоративный аккаунт:

+ +

Компания: {{ host.company_name|default:"Отсутствует" }}

+

Сектор: {{ host.company_sector }}

+

ИНН: {{ host.ITN|default:"Отсутствует" }}

+

ОГРН: {{ host.PSRN|default:"Отсутствует" }}

+ +

Контактное лицо:

+

+ {{ host.preferred_name }}, {{ host.job_title }}
+ {{ host.corporate_phone }}
+ {{ host.corporate_email }} +

+ +

Всегда с вами, команда AIR

+ + @@ -0,0 +1,13 @@ + + + + + Новая ошибка на платформе + + +

Пользователь {{ email }} сообщил об ошибке:

+

{{ report_text }}

+

Скриншоты ошибки (если пользователь их приложил) - во вложении

+

Всегда с вами, команда AIR

+ + \ No newline at end of file @@ -0,0 +1,10 @@ + + + + + AIR: баланс корпоративного аккаунта ниже {{ host.token_cap }} + + +

Для пополнения обратитесь по контактам, указанным в договоре

+ + \ No newline at end of file @@ -20,12 +20,7 @@ def send_low_balance_message(): token_cap__gt=F('user__payment_plan__current_token_balance'), ) for host in hosts: - for email in host.token_cap_emails: - EmailService(None).send_email( - f'AIR: баланс корпоративного аккаунта ниже {host.token_cap}', - 'Для пополнения обратитесь по контактам, указанным в договоре', - email, - ) + EmailService.send_low_balance_email(host) @shared_task