{ "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 db 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\n" ] }, { "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", "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", " 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'{redis_uri}2', backend=f'{redis_uri}2', 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)\n" ] }, { "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.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }