266 lines
8.1 KiB
Plaintext
266 lines
8.1 KiB
Plaintext
{
|
||
"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
|
||
}
|