diff --git a/lnbits/extensions/smtp/crud.py b/lnbits/extensions/smtp/crud.py index 2eee4c3d9..c7b96df59 100644 --- a/lnbits/extensions/smtp/crud.py +++ b/lnbits/extensions/smtp/crud.py @@ -1,10 +1,9 @@ -from http import HTTPStatus from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import CreateEmail, CreateEmailaddress, Emailaddresses, Emails +from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress from .smtp import send_mail @@ -17,7 +16,7 @@ def get_test_mail(email, testemail): ) -async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses: +async def create_emailaddress(data: CreateEmailaddress) -> Emailaddress: emailaddress_id = urlsafe_short_hash() @@ -50,7 +49,7 @@ async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses: return new_emailaddress -async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses: +async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddress: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE smtp.emailaddress SET {q} WHERE id = ?", @@ -65,30 +64,22 @@ async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses: await send_mail(row, email) assert row, "Newly updated emailaddress couldn't be retrieved" - return Emailaddresses(**row) + return Emailaddress(**row) -async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddresses]: +async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddress]: row = await db.fetchone( "SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,) ) - return Emailaddresses(**row) if row else None + return Emailaddress(**row) if row else None -async def get_emailaddress_by_email(email: str) -> Optional[Emailaddresses]: +async def get_emailaddress_by_email(email: str) -> Optional[Emailaddress]: row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,)) - return Emailaddresses(**row) if row else None + return Emailaddress(**row) if row else None -# async def get_emailAddressByEmail(email: str) -> Optional[Emails]: -# row = await db.fetchone( -# "SELECT s.*, d.emailaddress as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.emailaddress = ?", -# (email,), -# ) -# return Subdomains(**row) if row else None - - -async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddresses]: +async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddress]: if isinstance(wallet_ids, str): wallet_ids = [wallet_ids] @@ -97,21 +88,22 @@ async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailadd f"SELECT * FROM smtp.emailaddress WHERE wallet IN ({q})", (*wallet_ids,) ) - return [Emailaddresses(**row) for row in rows] + return [Emailaddress(**row) for row in rows] async def delete_emailaddress(emailaddress_id: str) -> None: await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)) -## create emails -async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails: +async def create_email(wallet: str, data: CreateEmail, payment_hash: str = "") -> Email: + id = urlsafe_short_hash() await db.execute( """ - INSERT INTO smtp.email (id, wallet, emailaddress_id, subject, receiver, message, paid) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO smtp.email (id, payment_hash, wallet, emailaddress_id, subject, receiver, message, paid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( + id, payment_hash, wallet, data.emailaddress_id, @@ -122,36 +114,34 @@ async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails: ), ) - new_email = await get_email(payment_hash) + new_email = await get_email(id) assert new_email, "Newly created email couldn't be retrieved" return new_email -async def set_email_paid(payment_hash: str) -> Emails: - email = await get_email(payment_hash) +async def set_email_paid(payment_hash: str) -> bool: + email = await get_email_by_payment_hash(payment_hash) if email and email.paid == False: await db.execute( - """ - UPDATE smtp.email - SET paid = true - WHERE id = ? - """, - (payment_hash,), + f"UPDATE smtp.email SET paid = true WHERE payment_hash = ?", (payment_hash,) ) - new_email = await get_email(payment_hash) - assert new_email, "Newly paid email couldn't be retrieved" - return new_email + return True + return False -async def get_email(email_id: str) -> Optional[Emails]: +async def get_email_by_payment_hash(payment_hash: str) -> Optional[Email]: row = await db.fetchone( - "SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.id = ?", - (email_id,), + f"SELECT * FROM smtp.email WHERE payment_hash = ?", (payment_hash,) ) - return Emails(**row) if row else None + return Email(**row) if row else None -async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]: +async def get_email(id: str) -> Optional[Email]: + row = await db.fetchone(f"SELECT * FROM smtp.email WHERE id = ?", (id,)) + return Email(**row) if row else None + + +async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Email]: if isinstance(wallet_ids, str): wallet_ids = [wallet_ids] @@ -161,7 +151,7 @@ async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]: (*wallet_ids,), ) - return [Emails(**row) for row in rows] + return [Email(**row) for row in rows] async def delete_email(email_id: str) -> None: diff --git a/lnbits/extensions/smtp/migrations.py b/lnbits/extensions/smtp/migrations.py index 16d501665..f8f39635f 100644 --- a/lnbits/extensions/smtp/migrations.py +++ b/lnbits/extensions/smtp/migrations.py @@ -33,3 +33,7 @@ async def m001_initial(db): ); """ ) + + +async def m002_add_payment_hash(db): + await db.execute(f"ALTER TABLE smtp.email ADD COLUMN payment_hash TEXT NOT NULL;") diff --git a/lnbits/extensions/smtp/models.py b/lnbits/extensions/smtp/models.py index e2f3fc13f..bb0e1f2cc 100644 --- a/lnbits/extensions/smtp/models.py +++ b/lnbits/extensions/smtp/models.py @@ -15,7 +15,7 @@ class CreateEmailaddress(BaseModel): cost: int = Query(..., ge=0) -class Emailaddresses(BaseModel): +class Emailaddress(BaseModel): id: str wallet: str email: str @@ -36,7 +36,7 @@ class CreateEmail(BaseModel): message: str = Query(...) -class Emails(BaseModel): +class Email(BaseModel): id: str wallet: str emailaddress_id: str diff --git a/lnbits/extensions/smtp/smtp.py b/lnbits/extensions/smtp/smtp.py index e77bc0fa5..43253b54f 100644 --- a/lnbits/extensions/smtp/smtp.py +++ b/lnbits/extensions/smtp/smtp.py @@ -4,83 +4,107 @@ import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formatdate -from http import HTTPStatus from smtplib import SMTP_SSL as SMTP +from typing import Union from loguru import logger -from starlette.exceptions import HTTPException + +from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress + + +async def send_mail( + emailaddress: Union[Emailaddress, CreateEmailaddress], + email: Union[Email, CreateEmail], +): + smtp_client = SmtpService(emailaddress) + message = smtp_client.create_message(email) + await smtp_client.send_mail(email.receiver, message) def valid_email(s): # https://regexr.com/2rhq7 - pat = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" + pat = r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" if re.match(pat, s): return True - msg = f"SMTP - invalid email: {s}." - logger.error(msg) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) + log = f"SMTP - invalid email: {s}." + logger.error(log) + raise Exception(log) -async def send_mail(emailaddress, email): - valid_email(emailaddress.email) - valid_email(email.receiver) +class SmtpService: + def __init__(self, emailaddress: Union[Emailaddress, CreateEmailaddress]) -> None: + self.sender = emailaddress.email + self.smtp_server = emailaddress.smtp_server + self.smtp_port = emailaddress.smtp_port + self.smtp_user = emailaddress.smtp_user + self.smtp_password = emailaddress.smtp_password - ts = time.time() - date = formatdate(ts, True) - - msg = MIMEMultipart("alternative") - msg = MIMEMultipart("alternative") - msg["Date"] = date - msg["Subject"] = email.subject - msg["From"] = emailaddress.email - msg["To"] = email.receiver - - signature = "Email sent anonymiously by LNbits Sendmail extension." - text = f""" -{email.message} - -{signature} -""" - - html = f""" - -
- -{email.message}
-
-
{signature}
- - -""" - - part1 = MIMEText(text, "plain") - part2 = MIMEText(html, "html") - msg.attach(part1) - msg.attach(part2) - - try: - conn = SMTP( - host=emailaddress.smtp_server, port=emailaddress.smtp_port, timeout=10 + def render_email(self, email: Union[Email, CreateEmail]): + signature: str = "Email sent by LNbits SMTP extension." + text = f"{email.message}\n\n{signature}" + html = ( + """ + + + +""" + + email.message + + """
+""" + + signature + + """
+ + + """ ) - logger.debug("SMTP - connected to smtp server.") - # conn.set_debuglevel(True) - except: - msg = f"SMTP - error connecting to smtp server: {emailaddress.smtp_server}:{emailaddress.smtp_port}." - logger.error(msg) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) - try: - conn.login(emailaddress.smtp_user, emailaddress.smtp_password) - logger.debug("SMTP - successful login to smtp server.") - except: - msg = f"SMTP - error login into smtp {emailaddress.smtp_user}." - logger.error(msg) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) - try: - conn.sendmail(emailaddress.email, email.receiver, msg.as_string()) - logger.debug("SMTP - successfully send email.") - except socket.error as e: - msg = f"SMTP - error sending email: {str(e)}." - logger.error(msg) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) - finally: - conn.quit() + return text, html + + def create_message(self, email: Union[Email, CreateEmail]): + ts = time.time() + date = formatdate(ts, True) + + msg = MIMEMultipart("alternative") + msg["Date"] = date + msg["Subject"] = email.subject + msg["From"] = self.sender + msg["To"] = email.receiver + + text, html = self.render_email(email) + + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + msg.attach(part1) + msg.attach(part2) + return msg + + async def send_mail(self, receiver, msg: MIMEMultipart): + + valid_email(self.sender) + valid_email(receiver) + + try: + conn = SMTP(host=self.smtp_server, port=int(self.smtp_port), timeout=10) + logger.debug("SMTP - connected to smtp server.") + # conn.set_debuglevel(True) + except: + log = f"SMTP - error connecting to smtp server: {self.smtp_server}:{self.smtp_port}." + logger.debug(log) + raise Exception(log) + + try: + conn.login(self.smtp_user, self.smtp_password) + logger.debug("SMTP - successful login to smtp server.") + except: + log = f"SMTP - error login into smtp {self.smtp_user}." + logger.error(log) + raise Exception(log) + + try: + conn.sendmail(self.sender, receiver, msg.as_string()) + logger.debug("SMTP - successfully send email.") + except socket.error as e: + log = f"SMTP - error sending email: {str(e)}." + logger.error(log) + raise Exception(log) + finally: + conn.quit() diff --git a/lnbits/extensions/smtp/tasks.py b/lnbits/extensions/smtp/tasks.py index 9c544473f..93ed33bad 100644 --- a/lnbits/extensions/smtp/tasks.py +++ b/lnbits/extensions/smtp/tasks.py @@ -5,7 +5,7 @@ from loguru import logger from lnbits.core.models import Payment from lnbits.tasks import register_invoice_listener -from .crud import get_email, get_emailaddress, set_email_paid +from .crud import get_email_by_payment_hash, get_emailaddress, set_email_paid from .smtp import send_mail @@ -21,7 +21,7 @@ async def on_invoice_paid(payment: Payment) -> None: if payment.extra.get("tag") != "smtp": return - email = await get_email(payment.checking_id) + email = await get_email_by_payment_hash(payment.checking_id) if not email: logger.error("SMTP: email can not by fetched") return diff --git a/lnbits/extensions/smtp/templates/smtp/index.html b/lnbits/extensions/smtp/templates/smtp/index.html index bf43ad7fe..c64cdcfa2 100644 --- a/lnbits/extensions/smtp/templates/smtp/index.html +++ b/lnbits/extensions/smtp/templates/smtp/index.html @@ -57,6 +57,14 @@ :href="props.row.displayUrl" target="_blank" > +