From 569990a760a5209c7e3882a1e2ef77023c124411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Wed, 11 Jan 2023 19:21:13 +0100 Subject: [PATCH] feature request from ben new pr --- lnbits/extensions/smtp/crud.py | 70 ++++---- lnbits/extensions/smtp/migrations.py | 4 + lnbits/extensions/smtp/models.py | 4 +- lnbits/extensions/smtp/smtp.py | 158 ++++++++++-------- lnbits/extensions/smtp/tasks.py | 4 +- .../extensions/smtp/templates/smtp/index.html | 79 ++++++++- lnbits/extensions/smtp/views_api.py | 27 ++- 7 files changed, 229 insertions(+), 117 deletions(-) diff --git a/lnbits/extensions/smtp/crud.py b/lnbits/extensions/smtp/crud.py index 2eee4c3d..62159703 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) + 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 16d50166..f8f39635 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 e2f3fc13..bb0e1f2c 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 e77bc0fa..43253b54 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 9c544473..93ed33ba 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 bf43ad7f..c64cdcfa 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" > + {{ col.value }} @@ -154,6 +162,42 @@ + + + + + + +
+ Submit +
+
+
+
@@ -316,10 +360,10 @@ emailsTable: { columns: [ { - name: 'emailaddress', + name: 'emailaddress_id', align: 'left', label: 'From', - field: 'emailaddress' + field: 'emailaddress_id' }, { name: 'receiver', @@ -350,6 +394,10 @@ rowsPerPage: 10 } }, + emailDialog: { + show: false, + data: {} + }, emailaddressDialog: { show: false, data: {} @@ -453,6 +501,33 @@ LNbits.utils.notifyApiError(error) }) }, + sendEmail: function () { + var self = this + var emailaddress = _.findWhere(this.emailaddresses, { + id: self.emailDialog.data.emailaddress_id + }) + var wallet = _.findWhere(this.g.user.wallets, { + id: emailaddress.wallet + }) + LNbits.api + .request( + 'POST', + '/smtp/api/v1/email/' + emailaddress.id + '/send', + wallet.adminkey, + self.emailDialog.data + ) + .then(function (response) { + self.emailDialog.show = false + self.emailDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + showEmailDialog: function (emailaddress_id) { + this.emailDialog.data.emailaddress_id = emailaddress_id + this.emailDialog.show = true + }, updateEmailaddressDialog: function (formId) { var link = _.findWhere(this.emailaddresses, {id: formId}) this.emailaddressDialog.data = _.clone(link) diff --git a/lnbits/extensions/smtp/views_api.py b/lnbits/extensions/smtp/views_api.py index 4ae1f966..92b2d0bd 100644 --- a/lnbits/extensions/smtp/views_api.py +++ b/lnbits/extensions/smtp/views_api.py @@ -4,7 +4,7 @@ from fastapi import Depends, HTTPException, Query from lnbits.core.crud import get_user from lnbits.core.services import check_transaction_status, create_invoice -from lnbits.decorators import WalletTypeInfo, get_key_type +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from . import smtp_ext from .crud import ( @@ -19,7 +19,7 @@ from .crud import ( update_emailaddress, ) from .models import CreateEmail, CreateEmailaddress -from .smtp import valid_email +from .smtp import send_mail, valid_email ## EMAILS @@ -44,6 +44,7 @@ async def api_smtp_send_email(payment_hash): ) emailaddress = await get_emailaddress(email.emailaddress_id) + assert emailaddress try: status = await check_transaction_status(email.wallet, payment_hash) @@ -59,11 +60,9 @@ async def api_smtp_send_email(payment_hash): @smtp_ext.post("/api/v1/email/{emailaddress_id}") async def api_smtp_make_email(emailaddress_id, data: CreateEmail): - valid_email(data.receiver) emailaddress = await get_emailaddress(emailaddress_id) - # If the request is coming for the non-existant emailaddress if not emailaddress: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, @@ -94,6 +93,26 @@ async def api_smtp_make_email(emailaddress_id, data: CreateEmail): return {"payment_hash": payment_hash, "payment_request": payment_request} +@smtp_ext.post( + "/api/v1/email/{emailaddress_id}/send", dependencies=[Depends(require_admin_key)] +) +async def api_smtp_make_email_send(emailaddress_id, data: CreateEmail): + valid_email(data.receiver) + emailaddress = await get_emailaddress(emailaddress_id) + if not emailaddress: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Emailaddress address does not exist.", + ) + email = await create_email(wallet=emailaddress.wallet, data=data) + if not email: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Email could not be fetched." + ) + await send_mail(emailaddress, email) + return {"sent": True} + + @smtp_ext.delete("/api/v1/email/{email_id}") async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)): email = await get_email(email_id)