diff --git a/lnbits/extensions/smtp/README.md b/lnbits/extensions/smtp/README.md deleted file mode 100644 index 5b7757e2..00000000 --- a/lnbits/extensions/smtp/README.md +++ /dev/null @@ -1,14 +0,0 @@ -

SMTP Extension

- -This extension allows you to setup a smtp, to offer sending emails with it for a small fee. - -## Requirements - -- SMTP Server - -## Usage - -1. Create new emailaddress -2. Verify if email goes to your testemail. Testmail is sent on create and update -3. Share the link with the email form. - diff --git a/lnbits/extensions/smtp/__init__.py b/lnbits/extensions/smtp/__init__.py deleted file mode 100644 index 9b89a0c4..00000000 --- a/lnbits/extensions/smtp/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -import asyncio - -from fastapi import APIRouter -from fastapi.staticfiles import StaticFiles - -from lnbits.db import Database -from lnbits.helpers import template_renderer -from lnbits.tasks import catch_everything_and_restart - -db = Database("ext_smtp") - -smtp_static_files = [ - { - "path": "/smtp/static", - "app": StaticFiles(directory="lnbits/extensions/smtp/static"), - "name": "smtp_static", - } -] - -smtp_ext: APIRouter = APIRouter(prefix="/smtp", tags=["smtp"]) - - -def smtp_renderer(): - return template_renderer(["lnbits/extensions/smtp/templates"]) - - -from .tasks import wait_for_paid_invoices -from .views import * # noqa: F401,F403 -from .views_api import * # noqa: F401,F403 - - -def smtp_start(): - loop = asyncio.get_event_loop() - loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/smtp/config.json b/lnbits/extensions/smtp/config.json deleted file mode 100644 index 325ebfa7..00000000 --- a/lnbits/extensions/smtp/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "SMTP", - "short_description": "Charge sats for sending emails", - "tile": "/smtp/static/smtp-bitcoin-email.png", - "contributors": ["dni"] -} diff --git a/lnbits/extensions/smtp/crud.py b/lnbits/extensions/smtp/crud.py deleted file mode 100644 index bc8d9e1a..00000000 --- a/lnbits/extensions/smtp/crud.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress -from .smtp import send_mail - - -def get_test_mail(email, testemail): - return CreateEmail( - emailaddress_id=email, - subject="LNBits SMTP - Test Email", - message="This is a test email from the LNBits SMTP extension! email is working!", - receiver=testemail, - ) - - -async def create_emailaddress(data: CreateEmailaddress) -> Emailaddress: - - emailaddress_id = urlsafe_short_hash() - - # send test mail for checking connection - email = get_test_mail(data.email, data.testemail) - await send_mail(data, email) - - await db.execute( - """ - INSERT INTO smtp.emailaddress (id, wallet, email, testemail, smtp_server, smtp_user, smtp_password, smtp_port, anonymize, description, cost) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - emailaddress_id, - data.wallet, - data.email, - data.testemail, - data.smtp_server, - data.smtp_user, - data.smtp_password, - data.smtp_port, - data.anonymize, - data.description, - data.cost, - ), - ) - - new_emailaddress = await get_emailaddress(emailaddress_id) - assert new_emailaddress, "Newly created emailaddress couldn't be retrieved" - return new_emailaddress - - -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 = ?", - (*kwargs.values(), emailaddress_id), - ) - row = await db.fetchone( - "SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,) - ) - - # send test mail for checking connection - email = get_test_mail(row.email, row.testemail) - await send_mail(row, email) - - assert row, "Newly updated emailaddress couldn't be retrieved" - return Emailaddress(**row) - - -async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddress]: - row = await db.fetchone( - "SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,) - ) - return Emailaddress(**row) if row else None - - -async def get_emailaddress_by_email(email: str) -> Optional[Emailaddress]: - row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,)) - return Emailaddress(**row) if row else None - - -async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddress]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM smtp.emailaddress WHERE wallet IN ({q})", (*wallet_ids,) - ) - - 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,)) - - -async def create_email(wallet: str, data: CreateEmail, payment_hash: str = "") -> Email: - id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO smtp.email (id, payment_hash, wallet, emailaddress_id, subject, receiver, message, paid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - id, - payment_hash, - wallet, - data.emailaddress_id, - data.subject, - data.receiver, - data.message, - False, - ), - ) - - 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) -> bool: - email = await get_email_by_payment_hash(payment_hash) - if email and email.paid is False: - await db.execute( - "UPDATE smtp.email SET paid = true WHERE payment_hash = ?", (payment_hash,) - ) - return True - return False - - -async def get_email_by_payment_hash(payment_hash: str) -> Optional[Email]: - row = await db.fetchone( - "SELECT * FROM smtp.email WHERE payment_hash = ?", (payment_hash,) - ) - return Email(**row) if row else None - - -async def get_email(id: str) -> Optional[Email]: - row = await db.fetchone("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] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.wallet IN ({q})", - (*wallet_ids,), - ) - - return [Email(**row) for row in rows] - - -async def delete_email(email_id: str) -> None: - await db.execute("DELETE FROM smtp.email WHERE id = ?", (email_id,)) diff --git a/lnbits/extensions/smtp/migrations.py b/lnbits/extensions/smtp/migrations.py deleted file mode 100644 index 22500e10..00000000 --- a/lnbits/extensions/smtp/migrations.py +++ /dev/null @@ -1,39 +0,0 @@ -async def m001_initial(db): - - await db.execute( - f""" - CREATE TABLE smtp.emailaddress ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - email TEXT NOT NULL, - testemail TEXT NOT NULL, - smtp_server TEXT NOT NULL, - smtp_user TEXT NOT NULL, - smtp_password TEXT NOT NULL, - smtp_port TEXT NOT NULL, - anonymize BOOLEAN NOT NULL, - description TEXT NOT NULL, - cost INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} - ); - """ - ) - - await db.execute( - f""" - CREATE TABLE smtp.email ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - emailaddress_id TEXT NOT NULL, - subject TEXT NOT NULL, - receiver TEXT NOT NULL, - message TEXT NOT NULL, - paid BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} - ); - """ - ) - - -async def m002_add_payment_hash(db): - await db.execute("ALTER TABLE smtp.email ADD COLUMN payment_hash TEXT;") diff --git a/lnbits/extensions/smtp/models.py b/lnbits/extensions/smtp/models.py deleted file mode 100644 index bb0e1f2c..00000000 --- a/lnbits/extensions/smtp/models.py +++ /dev/null @@ -1,47 +0,0 @@ -from fastapi import Query -from pydantic import BaseModel - - -class CreateEmailaddress(BaseModel): - wallet: str = Query(...) - email: str = Query(...) - testemail: str = Query(...) - smtp_server: str = Query(...) - smtp_user: str = Query(...) - smtp_password: str = Query(...) - smtp_port: str = Query(...) - description: str = Query(...) - anonymize: bool - cost: int = Query(..., ge=0) - - -class Emailaddress(BaseModel): - id: str - wallet: str - email: str - testemail: str - smtp_server: str - smtp_user: str - smtp_password: str - smtp_port: str - anonymize: bool - description: str - cost: int - - -class CreateEmail(BaseModel): - emailaddress_id: str = Query(...) - subject: str = Query(...) - receiver: str = Query(...) - message: str = Query(...) - - -class Email(BaseModel): - id: str - wallet: str - emailaddress_id: str - subject: str - receiver: str - message: str - paid: bool - time: int diff --git a/lnbits/extensions/smtp/smtp.py b/lnbits/extensions/smtp/smtp.py deleted file mode 100644 index 43253b54..00000000 --- a/lnbits/extensions/smtp/smtp.py +++ /dev/null @@ -1,110 +0,0 @@ -import re -import socket -import time -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.utils import formatdate -from smtplib import SMTP_SSL as SMTP -from typing import Union - -from loguru import logger - -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 = 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 - log = f"SMTP - invalid email: {s}." - logger.error(log) - raise Exception(log) - - -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 - - 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 - + """

- - - """ - ) - 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/static/smtp-bitcoin-email.png b/lnbits/extensions/smtp/static/smtp-bitcoin-email.png deleted file mode 100644 index e80b6c9a..00000000 Binary files a/lnbits/extensions/smtp/static/smtp-bitcoin-email.png and /dev/null differ diff --git a/lnbits/extensions/smtp/tasks.py b/lnbits/extensions/smtp/tasks.py deleted file mode 100644 index 93ed33ba..00000000 --- a/lnbits/extensions/smtp/tasks.py +++ /dev/null @@ -1,36 +0,0 @@ -import asyncio - -from loguru import logger - -from lnbits.core.models import Payment -from lnbits.tasks import register_invoice_listener - -from .crud import get_email_by_payment_hash, get_emailaddress, set_email_paid -from .smtp import send_mail - - -async def wait_for_paid_invoices(): - invoice_queue = asyncio.Queue() - register_invoice_listener(invoice_queue) - while True: - payment = await invoice_queue.get() - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "smtp": - return - - email = await get_email_by_payment_hash(payment.checking_id) - if not email: - logger.error("SMTP: email can not by fetched") - return - - emailaddress = await get_emailaddress(email.emailaddress_id) - if not emailaddress: - logger.error("SMTP: emailaddress can not by fetched") - return - - await payment.set_pending(False) - await send_mail(emailaddress, email) - await set_email_paid(payment_hash=payment.payment_hash) diff --git a/lnbits/extensions/smtp/templates/smtp/_api_docs.html b/lnbits/extensions/smtp/templates/smtp/_api_docs.html deleted file mode 100644 index cfb811d1..00000000 --- a/lnbits/extensions/smtp/templates/smtp/_api_docs.html +++ /dev/null @@ -1,23 +0,0 @@ - - - -
- LNBits SMTP: Get paid sats to send emails -
-

- Charge people for using sending an email via your smtp server
- More details -
- Created by, dni -

-
-
-
diff --git a/lnbits/extensions/smtp/templates/smtp/display.html b/lnbits/extensions/smtp/templates/smtp/display.html deleted file mode 100644 index f60d22c1..00000000 --- a/lnbits/extensions/smtp/templates/smtp/display.html +++ /dev/null @@ -1,175 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -

{{ email }}

-
-
{{ desc }}
-
- - - - -

Total cost: {{ cost }} sats

-
- Submit -
-
-
-
-
- - - - - - -
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/smtp/templates/smtp/index.html b/lnbits/extensions/smtp/templates/smtp/index.html deleted file mode 100644 index 4b00cd08..00000000 --- a/lnbits/extensions/smtp/templates/smtp/index.html +++ /dev/null @@ -1,604 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - -
-
- - - New Emailaddress - - - - - -
-
-
Emailaddresses
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Emails
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} Sendmail extension -
-
- - - {% include "smtp/_api_docs.html" %} - -
-
- - - - - - - -
- Submit -
-
-
-
- - - - - - - - - - - - - - - -
- -
- - - - -
- Update Form - Create Emailaddress - Cancel -
-
-
-
-
- -{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/smtp/views.py b/lnbits/extensions/smtp/views.py deleted file mode 100644 index df208a77..00000000 --- a/lnbits/extensions/smtp/views.py +++ /dev/null @@ -1,40 +0,0 @@ -from http import HTTPStatus - -from fastapi import Depends, HTTPException, Request -from fastapi.templating import Jinja2Templates -from starlette.responses import HTMLResponse - -from lnbits.core.models import User -from lnbits.decorators import check_user_exists - -from . import smtp_ext, smtp_renderer -from .crud import get_emailaddress - -templates = Jinja2Templates(directory="templates") - - -@smtp_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return smtp_renderer().TemplateResponse( - "smtp/index.html", {"request": request, "user": user.dict()} - ) - - -@smtp_ext.get("/{emailaddress_id}") -async def display(request: Request, emailaddress_id): - emailaddress = await get_emailaddress(emailaddress_id) - if not emailaddress: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Emailaddress does not exist." - ) - - return smtp_renderer().TemplateResponse( - "smtp/display.html", - { - "request": request, - "emailaddress_id": emailaddress.id, - "email": emailaddress.email, - "desc": emailaddress.description, - "cost": emailaddress.cost, - }, - ) diff --git a/lnbits/extensions/smtp/views_api.py b/lnbits/extensions/smtp/views_api.py deleted file mode 100644 index 66bc4983..00000000 --- a/lnbits/extensions/smtp/views_api.py +++ /dev/null @@ -1,190 +0,0 @@ -from http import HTTPStatus - -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, require_admin_key - -from . import smtp_ext -from .crud import ( - create_email, - create_emailaddress, - delete_email, - delete_emailaddress, - get_email, - get_email_by_payment_hash, - get_emailaddress, - get_emailaddresses, - get_emails, - update_emailaddress, -) -from .models import CreateEmail, CreateEmailaddress -from .smtp import send_mail, valid_email - - -## EMAILS -@smtp_ext.get("/api/v1/email") -async def api_email( - g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) -): - wallet_ids = [g.wallet.id] - if all_wallets: - user = await get_user(g.wallet.user) - if user: - wallet_ids = user.wallet_ids - return [email.dict() for email in await get_emails(wallet_ids)] - - -@smtp_ext.get("/api/v1/email/{payment_hash}") -async def api_smtp_send_email(payment_hash): - email = await get_email_by_payment_hash(payment_hash) - if not email: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="paymenthash is wrong" - ) - - emailaddress = await get_emailaddress(email.emailaddress_id) - assert emailaddress - - try: - status = await check_transaction_status(email.wallet, payment_hash) - is_paid = not status.pending - except Exception: - return {"paid": False} - if is_paid: - if emailaddress.anonymize: - await delete_email(email.id) - return {"paid": True} - return {"paid": False} - - -@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 not emailaddress: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Emailaddress address does not exist.", - ) - try: - memo = f"sent email from {emailaddress.email} to {data.receiver}" - if emailaddress.anonymize: - memo = "sent email" - - payment_hash, payment_request = await create_invoice( - wallet_id=emailaddress.wallet, - amount=emailaddress.cost, - memo=memo, - extra={"tag": "smtp"}, - ) - except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) - - email = await create_email( - payment_hash=payment_hash, wallet=emailaddress.wallet, data=data - ) - - if not email: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Email could not be fetched." - ) - 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) - - if not email: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNsubdomain does not exist." - ) - - if email.wallet != g.wallet.id: - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your email.") - - await delete_email(email_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) - - -## EMAILADDRESSES -@smtp_ext.get("/api/v1/emailaddress") -async def api_emailaddresses( - g: WalletTypeInfo = Depends(get_key_type), - all_wallets: bool = Query(False), -): - wallet_ids = [g.wallet.id] - if all_wallets: - user = await get_user(g.wallet.user) - if user: - wallet_ids = user.wallet_ids - return [ - emailaddress.dict() for emailaddress in await get_emailaddresses(wallet_ids) - ] - - -@smtp_ext.post("/api/v1/emailaddress") -@smtp_ext.put("/api/v1/emailaddress/{emailaddress_id}") -async def api_emailaddress_create( - data: CreateEmailaddress, - emailaddress_id=None, - g: WalletTypeInfo = Depends(get_key_type), -): - if emailaddress_id: - emailaddress = await get_emailaddress(emailaddress_id) - - if not emailaddress: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Emailadress does not exist." - ) - if emailaddress.wallet != g.wallet.id: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your emailaddress." - ) - - emailaddress = await update_emailaddress(emailaddress_id, **data.dict()) - else: - emailaddress = await create_emailaddress(data=data) - return emailaddress.dict() - - -@smtp_ext.delete("/api/v1/emailaddress/{emailaddress_id}") -async def api_emailaddress_delete( - emailaddress_id, g: WalletTypeInfo = Depends(get_key_type) -): - emailaddress = await get_emailaddress(emailaddress_id) - - if not emailaddress: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Emailaddress does not exist." - ) - if emailaddress.wallet != g.wallet.id: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your Emailaddress." - ) - - await delete_emailaddress(emailaddress_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT)