From dc9d3d48cef71edbf3ac926c0efd4ecc01e107299921e1b0114e6cbf6c8dec6b Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 30 Jun 2026 19:13:50 -0400 Subject: [PATCH] =?UTF-8?q?Bot=20commit:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20email=5Ftask.ip?= =?UTF-8?q?ynb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- email_task.ipynb | 266 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 email_task.ipynb diff --git a/email_task.ipynb b/email_task.ipynb new file mode 100644 index 0000000..fab8029 --- /dev/null +++ b/email_task.ipynb @@ -0,0 +1,266 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "da5a2e63", + "metadata": {}, + "outputs": [], + "source": [ + "# ignore\n", + "import os\n", + "\n", + "def load_env(file_path):\n", + " \"\"\"Загружает переменные окружения из файла .env с поддержкой булевых значений.\"\"\"\n", + " with open(file_path) as f:\n", + " for line in f:\n", + " line = line.strip()\n", + " if not line or line.startswith('#'):\n", + " continue\n", + " \n", + " try:\n", + " key, value = line.split('=', 1)\n", + " key = key.strip()\n", + " value = value.strip('\"\\' ')\n", + " # Преобразование строковых значений в булевые\n", + " if value.lower() in ['true', '1', 'yes']:\n", + " value = True\n", + " elif value.lower() in ['false', '0', 'no']:\n", + " value = False\n", + " \n", + " # Проверка, что ключ не пустой\n", + " if key:\n", + " os.environ[key] = str(value) # Храним как строку\n", + " except ValueError:\n", + " print(f\"Ошибка: неверный формат в строке '{line}'\")\n", + "\n", + "try:\n", + " load_env('env')\n", + "except:\n", + " load_env('env_example')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afd6bc25", + "metadata": {}, + "outputs": [], + "source": [ + "import smtplib\n", + "from email.mime.multipart import MIMEMultipart\n", + "from email.mime.text import MIMEText\n", + "from celery import Celery\n", + "from config import *\n", + "from templates import templates\n", + "import logging\n", + "from typing import Optional\n", + "import asyncio \n", + "from email.utils import formatdate, make_msgid\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05fadc2d", + "metadata": {}, + "outputs": [], + "source": [ + "def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:\n", + " return f\"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}\"\n", + "\n", + "\n", + "async def create_email_message(\n", + " to_email: str,\n", + " subject: str,\n", + " html_content: str,\n", + " user_id: int = None,\n", + "):\n", + " msg = MIMEMultipart(\"alternative\")\n", + " \n", + " msg['From'] = f\"{FROM_NAME} <{FROM_EMAIL}>\"\n", + " msg['To'] = to_email\n", + " msg['Subject'] = subject\n", + " msg['Message-ID'] = make_msgid(domain=FROM_EMAIL.split('@')[1])\n", + " msg['Date'] = formatdate(localtime=True)\n", + " msg['X-Priority'] = \"3\"\n", + " msg['Importance'] = \"normal\"\n", + " msg['X-Auto-Response-Suppress'] = \"All\"\n", + " \n", + " # token = await create_unsubscribe_token(to_email, user_id)\n", + " unsubscribe_url = build_unsubscribe_url(to_email, None, FULL_DOMAIN)\n", + " if to_email != FROM_EMAIL:\n", + " msg['List-Unsubscribe'] = f\"<{unsubscribe_url}>\"\n", + " msg['List-Unsubscribe-Post'] = \"List-Unsubscribe=One-Click\"\n", + " \n", + " msg.attach(MIMEText(html_content, 'html'))\n", + " \n", + " return msg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6687f2ab", + "metadata": {}, + "outputs": [], + "source": [ + "def connect(email_server, email_port):\n", + " server = smtplib.SMTP(email_server, email_port)\n", + " server.starttls(context=ctx)\n", + " return server\n", + "\n", + "\n", + "\n", + "def send_email(\n", + " to_email: str,\n", + " html_content: str,\n", + " subject: str,\n", + " user_id: Optional[str] = None,\n", + ") -> bool:\n", + " msg =asyncio.run(create_email_message(\n", + " to_email=to_email,\n", + " subject=subject,\n", + " html_content=html_content,\n", + " user_id=user_id,\n", + " ))\n", + "\n", + "\n", + " try:\n", + " with connect(EMAIL_SMTP_SERVER, EMAIL_SMTP_PORT) as server:\n", + " server.login(FROM_EMAIL, EMAIL_PASS)\n", + " server.send_message(msg)\n", + " # server.close()\n", + "\n", + " logging.info(f\"Email sent successfully to: {to_email} | Subject: {subject}\")\n", + " return True\n", + "\n", + " except Exception as e:\n", + " logging.error(\n", + " f\"Error sending email to: {to_email}, \"\n", + " f\"Subject: {subject}, Error: {str(e)}\"\n", + " )\n", + " return False\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81e35a88", + "metadata": {}, + "outputs": [], + "source": [ + "celery_app_email = Celery('emails', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}/email', backend=f'redis://{REDIS_HOST}:{6379 if PROD else 30003}/0', task_track_started=True )\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30ca10ab", + "metadata": {}, + "outputs": [], + "source": [ + "@celery_app_email.task(\n", + " bind=True,\n", + " rate_limit='30/m',\n", + " name='email_task.send_email',\n", + " retry_backoff=True,\n", + " retry_backoff_max=600,\n", + " retry_jitter=True,\n", + " max_retries=3,\n", + " soft_time_limit=30,\n", + " time_limit=45,\n", + ")\n", + "def notify(self, to_email: str, data: dict, subject: str = EMAIL_SUBJECT, template: str = 'main.html'):\n", + " logging.info(f\"Отправка email на {to_email} (попытка {self.request.retries + 1})\")\n", + " try:\n", + " html_content = templates.get_template(f\"email/{template}\").render({\n", + " \"subject\": subject, \n", + " \"data\": data\n", + " })\n", + " \n", + " ok = send_email(to_email, html_content, subject=subject)\n", + " if ok:\n", + " return ok\n", + " else:\n", + " raise self.retry(exc=exc)\n", + " \n", + " except Exception as exc:\n", + " logging.warning(\n", + " f\"Ошибка при отправке email на {to_email} (попытка {self.request.retries + 1}): {exc}\"\n", + " )\n", + " raise self.retry(exc=exc)" + ] + }, + { + "cell_type": "markdown", + "id": "1837d7c2", + "metadata": {}, + "source": [ + "## Run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daf4fe38", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "\n", + "IGNORE = ['build.ipynb', 'data.ipynb']\n", + "\n", + "for _filename in [i for i in os.listdir() if 'ipynb' in i and i not in IGNORE]:\n", + " filename_without_extension = os.path.splitext(os.path.basename(_filename))[0]\n", + "\n", + " with open(f'{filename_without_extension}.ipynb', 'r') as f:\n", + " data = f.read()\n", + "\n", + " main = json.loads(data)\n", + " app = []\n", + " for cell in main['cells']:\n", + " if '## Run' in cell['source']:\n", + " break\n", + " if '# ignore\\n' in cell['source']:\n", + " continue\n", + " app.append(''.join(cell['source']))\n", + "\n", + " with open(f'{filename_without_extension}.py', 'w') as f:\n", + " f.write('\\n\\n'.join(app))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5194611", + "metadata": {}, + "outputs": [], + "source": [ + "!celery -A email_task.celery_app_email worker --loglevel=info" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}