feature request from ben new pr

This commit is contained in:
dni ⚡ 2023-01-11 19:21:13 +01:00
parent a813a37e68
commit 569990a760
7 changed files with 229 additions and 117 deletions

View File

@ -1,10 +1,9 @@
from http import HTTPStatus
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import CreateEmail, CreateEmailaddress, Emailaddresses, Emails from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress
from .smtp import send_mail 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() emailaddress_id = urlsafe_short_hash()
@ -50,7 +49,7 @@ async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
return new_emailaddress 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()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute( await db.execute(
f"UPDATE smtp.emailaddress SET {q} WHERE id = ?", 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) await send_mail(row, email)
assert row, "Newly updated emailaddress couldn't be retrieved" 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( row = await db.fetchone(
"SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,) "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,)) 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]: async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddress]:
# 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]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] 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,) 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: async def delete_emailaddress(emailaddress_id: str) -> None:
await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)) await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,))
## create emails async def create_email(wallet: str, data: CreateEmail, payment_hash: str = "") -> Email:
async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails: id = urlsafe_short_hash()
await db.execute( 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 (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (
id,
payment_hash, payment_hash,
wallet, wallet,
data.emailaddress_id, 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" assert new_email, "Newly created email couldn't be retrieved"
return new_email return new_email
async def set_email_paid(payment_hash: str) -> Emails: async def set_email_paid(payment_hash: str) -> bool:
email = await get_email(payment_hash) email = await get_email_by_payment_hash(payment_hash)
if email and email.paid == False: if email and email.paid == False:
await db.execute( await db.execute(
""" f"UPDATE smtp.email SET paid = true WHERE payment_hash = {payment_hash}"
UPDATE smtp.email
SET paid = true
WHERE id = ?
""",
(payment_hash,),
) )
new_email = await get_email(payment_hash) return True
assert new_email, "Newly paid email couldn't be retrieved" return False
return new_email
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( 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 = ?", f"SELECT * FROM smtp.email WHERE payment_hash = {payment_hash}"
(email_id,),
) )
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): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
@ -161,7 +151,7 @@ async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
(*wallet_ids,), (*wallet_ids,),
) )
return [Emails(**row) for row in rows] return [Email(**row) for row in rows]
async def delete_email(email_id: str) -> None: async def delete_email(email_id: str) -> None:

View File

@ -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;")

View File

@ -15,7 +15,7 @@ class CreateEmailaddress(BaseModel):
cost: int = Query(..., ge=0) cost: int = Query(..., ge=0)
class Emailaddresses(BaseModel): class Emailaddress(BaseModel):
id: str id: str
wallet: str wallet: str
email: str email: str
@ -36,7 +36,7 @@ class CreateEmail(BaseModel):
message: str = Query(...) message: str = Query(...)
class Emails(BaseModel): class Email(BaseModel):
id: str id: str
wallet: str wallet: str
emailaddress_id: str emailaddress_id: str

View File

@ -4,83 +4,107 @@ import time
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate from email.utils import formatdate
from http import HTTPStatus
from smtplib import SMTP_SSL as SMTP from smtplib import SMTP_SSL as SMTP
from typing import Union
from loguru import logger 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): def valid_email(s):
# https://regexr.com/2rhq7 # 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): if re.match(pat, s):
return True return True
msg = f"SMTP - invalid email: {s}." log = f"SMTP - invalid email: {s}."
logger.error(msg) logger.error(log)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) raise Exception(log)
async def send_mail(emailaddress, email): class SmtpService:
valid_email(emailaddress.email) def __init__(self, emailaddress: Union[Emailaddress, CreateEmailaddress]) -> None:
valid_email(email.receiver) 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() def render_email(self, email: Union[Email, CreateEmail]):
date = formatdate(ts, True) signature: str = "Email sent by LNbits SMTP extension."
text = f"{email.message}\n\n{signature}"
msg = MIMEMultipart("alternative") html = (
msg = MIMEMultipart("alternative") """
msg["Date"] = date <html>
msg["Subject"] = email.subject <head></head>
msg["From"] = emailaddress.email <body>
msg["To"] = email.receiver <p>"""
+ email.message
signature = "Email sent anonymiously by LNbits Sendmail extension." + """</p>
text = f""" <p>"""
{email.message} + signature
+ """</p>
{signature} </body>
""" </html>
"""
html = f"""
<html>
<head></head>
<body>
<p>{email.message}<p>
<br>
<p>{signature}</p>
</body>
</html>
"""
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
) )
logger.debug("SMTP - connected to smtp server.") return text, html
# conn.set_debuglevel(True)
except: def create_message(self, email: Union[Email, CreateEmail]):
msg = f"SMTP - error connecting to smtp server: {emailaddress.smtp_server}:{emailaddress.smtp_port}." ts = time.time()
logger.error(msg) date = formatdate(ts, True)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
try: msg = MIMEMultipart("alternative")
conn.login(emailaddress.smtp_user, emailaddress.smtp_password) msg["Date"] = date
logger.debug("SMTP - successful login to smtp server.") msg["Subject"] = email.subject
except: msg["From"] = self.sender
msg = f"SMTP - error login into smtp {emailaddress.smtp_user}." msg["To"] = email.receiver
logger.error(msg)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) text, html = self.render_email(email)
try:
conn.sendmail(emailaddress.email, email.receiver, msg.as_string()) part1 = MIMEText(text, "plain")
logger.debug("SMTP - successfully send email.") part2 = MIMEText(html, "html")
except socket.error as e: msg.attach(part1)
msg = f"SMTP - error sending email: {str(e)}." msg.attach(part2)
logger.error(msg) return msg
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
finally: async def send_mail(self, receiver, msg: MIMEMultipart):
conn.quit()
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()

View File

@ -5,7 +5,7 @@ from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener 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 from .smtp import send_mail
@ -21,7 +21,7 @@ async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "smtp": if payment.extra.get("tag") != "smtp":
return return
email = await get_email(payment.checking_id) email = await get_email_by_payment_hash(payment.checking_id)
if not email: if not email:
logger.error("SMTP: email can not by fetched") logger.error("SMTP: email can not by fetched")
return return

View File

@ -57,6 +57,14 @@
:href="props.row.displayUrl" :href="props.row.displayUrl"
target="_blank" target="_blank"
></q-btn> ></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="email"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="showEmailDialog(props.row.id)"
></q-btn>
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} {{ col.value }}
@ -154,6 +162,42 @@
</q-card> </q-card>
</div> </div>
<q-dialog v-model="emailDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendEmail()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="emailDialog.data.receiver"
type="text"
label="Receiver"
></q-input>
<q-input
filled
dense
v-model.trim="emailDialog.data.subject"
type="text"
label="Subject"
></q-input>
<q-input
filled
dense
v-model.trim="emailDialog.data.message"
type="textarea"
label="Message "
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="emailDialog.data.receiver == '' || emailDialog.data.subject == '' || emailDialog.data.message == ''"
type="submit"
>Submit</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="emailaddressDialog.show" position="top"> <q-dialog v-model="emailaddressDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md"> <q-form @submit="sendFormData" class="q-gutter-md">
@ -316,10 +360,10 @@
emailsTable: { emailsTable: {
columns: [ columns: [
{ {
name: 'emailaddress', name: 'emailaddress_id',
align: 'left', align: 'left',
label: 'From', label: 'From',
field: 'emailaddress' field: 'emailaddress_id'
}, },
{ {
name: 'receiver', name: 'receiver',
@ -350,6 +394,10 @@
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
emailDialog: {
show: false,
data: {}
},
emailaddressDialog: { emailaddressDialog: {
show: false, show: false,
data: {} data: {}
@ -453,6 +501,33 @@
LNbits.utils.notifyApiError(error) 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) { updateEmailaddressDialog: function (formId) {
var link = _.findWhere(this.emailaddresses, {id: formId}) var link = _.findWhere(this.emailaddresses, {id: formId})
this.emailaddressDialog.data = _.clone(link) this.emailaddressDialog.data = _.clone(link)

View File

@ -4,7 +4,7 @@ from fastapi import Depends, HTTPException, Query
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import check_transaction_status, create_invoice 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 . import smtp_ext
from .crud import ( from .crud import (
@ -19,7 +19,7 @@ from .crud import (
update_emailaddress, update_emailaddress,
) )
from .models import CreateEmail, CreateEmailaddress from .models import CreateEmail, CreateEmailaddress
from .smtp import valid_email from .smtp import send_mail, valid_email
## EMAILS ## EMAILS
@ -44,6 +44,7 @@ async def api_smtp_send_email(payment_hash):
) )
emailaddress = await get_emailaddress(email.emailaddress_id) emailaddress = await get_emailaddress(email.emailaddress_id)
assert emailaddress
try: try:
status = await check_transaction_status(email.wallet, payment_hash) 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}") @smtp_ext.post("/api/v1/email/{emailaddress_id}")
async def api_smtp_make_email(emailaddress_id, data: CreateEmail): async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
valid_email(data.receiver) valid_email(data.receiver)
emailaddress = await get_emailaddress(emailaddress_id) emailaddress = await get_emailaddress(emailaddress_id)
# If the request is coming for the non-existant emailaddress
if not emailaddress: if not emailaddress:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, 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} 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}") @smtp_ext.delete("/api/v1/email/{email_id}")
async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)): async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)):
email = await get_email(email_id) email = await get_email(email_id)