Last active
April 16, 2025 10:19
-
-
Save karavan/f6191646b0ee3c59df5f7ef03cfeba5e to your computer and use it in GitHub Desktop.
smtp debug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| #-*- coding: utf-8 -*- | |
| import asyncio | |
| import os.path | |
| import aiofiles | |
| import aiocsv | |
| import json | |
| import sys | |
| import aiosqlite | |
| import logging | |
| from aiosmtplib import SMTP | |
| from email.mime.application import MIMEApplication | |
| from email.mime.multipart import MIMEMultipart | |
| from email.mime.text import MIMEText | |
| from datetime import datetime | |
| from jinja2 import Template | |
| import asyncclick as click | |
| REQUESTS_CA_BUNDLE = "/Storage/Virtualenv/smtp-client/team.crt" | |
| # if sys.platform == 'win32': | |
| # loop = asyncio.get_event_loop() | |
| # if not loop.is_running() and not isinstance(loop, asyncio.ProactorEventLoop): | |
| # loop = asyncio.ProactorEventLoop() | |
| # asyncio.set_event_loop(loop) | |
| class Config: | |
| """Структура конфига для всех доступных команд.""" | |
| def __init__(self, | |
| smtp_host, | |
| smtp_port, | |
| smtp_user, | |
| smtp_password, | |
| from_addr, | |
| mail_subject): | |
| self.smtp_host = smtp_host | |
| self.smtp_port = smtp_port | |
| self.smtp_user = smtp_user | |
| self.smtp_password = smtp_password | |
| self.from_addr = from_addr | |
| self.mail_subject = mail_subject | |
| @staticmethod | |
| async def load(file_path: str): | |
| """Загрузка конфигурации из файла. | |
| Возвращает заполненную конфигурацию. | |
| Параметры: | |
| file_path - полный путь к файлу конфигурации | |
| """ | |
| async with aiofiles.open(file_path, mode="r", encoding="utf-8") as config_file: | |
| conf_str = await config_file.read() | |
| conf = json.loads(conf_str) | |
| return Config(**conf) | |
| class Context: | |
| def __init__(self): | |
| self.logger = None | |
| self.config = None | |
| async def with_smtp(self, send_email_func): | |
| smtp = SMTP( | |
| hostname=self.config.smtp_host, | |
| port=self.config.smtp_port | |
| ) | |
| await smtp.connect(validate_certs=False) | |
| await smtp.starttls() | |
| await smtp.login( | |
| username=self.config.smtp_user, | |
| password=self.config.smtp_password | |
| ) | |
| await send_email_func(self, smtp) | |
| await smtp.quit() | |
| class TicketsDB: | |
| def __init__(self, conn: aiosqlite.Connection): | |
| self.conn = conn | |
| async def init(self): | |
| create_schema = """create table if not exists tickets | |
| ( | |
| number text primary key, | |
| email text, | |
| order_id text, | |
| result text | |
| )""" | |
| await self.conn.execute(create_schema) | |
| async def upsert_ticket(self, email, order_id, result): | |
| query = "insert or replace into tickets(email, order_id, result) values (?, ?, ?)" | |
| await self.conn.execute(query, (email, order_id, result)) | |
| async def is_ticket_success(self, number): | |
| query = "select result from tickets where number = :number" | |
| cur: aiosqlite.Cursor = await self.conn.cursor() | |
| await cur.execute(query, {"number":number}) | |
| result = await cur.fetchone() | |
| await cur.close() | |
| return result is not None and result[0] == "success" | |
| @click.group() | |
| @click.pass_context | |
| @click.option("--conf", "-c", "conf", type=str, default="config.json", help="Path JSON config file.") | |
| async def cli(ctx: click.core.Context, conf: str): | |
| out_file_name = datetime.now().strftime("logs/%Y-%m-%d__%H-%M-%S") + "_out.txt" | |
| logger = logging.getLogger("asyncio") | |
| fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s") | |
| stream_handler = logging.StreamHandler(stream=sys.stdout) | |
| stream_handler.setFormatter(fmt) | |
| logger.addHandler(stream_handler) | |
| file_handler = logging.FileHandler(filename=out_file_name) | |
| file_handler.setFormatter(fmt) | |
| logger.addHandler(file_handler) | |
| logger.setLevel(logging.INFO) | |
| ctx.obj.logger = logger | |
| ctx.obj.config = await Config.load(conf) | |
| @cli.command() | |
| @click.pass_context | |
| @click.option("--recipients", "-r", "recipients_path", type=str, default="emails.csv", help="Path to recipients file.") | |
| @click.option("--msg", "-m", "msg_path", type=str, default="template.html", help="Path to message template body.") | |
| async def send_tickets(ctx: click.core.Context, recipients_path: str, msg_path: str): | |
| async def send_email_func(ctx: Context, smtp: SMTP): | |
| async with aiofiles.open(msg_path, mode="r", encoding="utf-8") as template_file: | |
| msg_body = await template_file.read() | |
| tmpl = Template(msg_body) | |
| async with aiofiles.open(recipients_path, mode="r", encoding="utf-8") as csv_file: | |
| recipients = aiocsv.AsyncDictReader(csv_file) | |
| async for r in recipients: | |
| email = r["email"].strip() | |
| bcc_email = r["bcc_email"].strip() | |
| link = r['link'].strip() | |
| text_html = r['text_html'] | |
| msg = MIMEMultipart("html") | |
| msg["Subject"] = ctx.config.mail_subject | |
| msg["From"] = ctx.config.from_addr | |
| msg["To"] = email | |
| msg["Bcc"] = bcc_email | |
| body = tmpl.render({"delivery_link": link, "orders_comp": text_html}) | |
| msg.attach(MIMEText(body, "html")) | |
| log_msg = f"{email}: " | |
| try: | |
| await smtp.send_message(msg) | |
| ctx.logger.info(log_msg + "success") | |
| except Exception as e: | |
| ctx.logger.error(f"{log_msg}{e}") | |
| await ctx.obj.with_smtp(send_email_func) | |
| if __name__ == '__main__': | |
| cli(obj=Context(), _anyio_backend="asyncio") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment