Merge pull request #1333 from lnbits/smtp

Extension: SMTP, send mails for sats
This commit is contained in:
Arc 2023-01-09 12:18:51 +00:00 committed by GitHub
commit e362b04f0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1372 additions and 0 deletions

View File

@ -0,0 +1,14 @@
<h1>SMTP Extension</h1>
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.

View File

@ -0,0 +1,34 @@
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
from .views_api import * # noqa
def smtp_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -0,0 +1,6 @@
{
"name": "SMTP",
"short_description": "Charge sats for sending emails",
"tile": "/smtp/static/smtp-bitcoin-email.png",
"contributors": ["dni"]
}

View File

@ -0,0 +1,168 @@
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 .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) -> Emailaddresses:
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) -> Emailaddresses:
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 Emailaddresses(**row)
async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddresses]:
row = await db.fetchone(
"SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)
)
return Emailaddresses(**row) if row else None
async def get_emailaddress_by_email(email: str) -> Optional[Emailaddresses]:
row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,))
return Emailaddresses(**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]:
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 [Emailaddresses(**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:
await db.execute(
"""
INSERT INTO smtp.email (id, wallet, emailaddress_id, subject, receiver, message, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
payment_hash,
wallet,
data.emailaddress_id,
data.subject,
data.receiver,
data.message,
False,
),
)
new_email = await get_email(payment_hash)
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)
if email and email.paid == False:
await db.execute(
"""
UPDATE smtp.email
SET paid = true
WHERE id = ?
""",
(payment_hash,),
)
new_email = await get_email(payment_hash)
assert new_email, "Newly paid email couldn't be retrieved"
return new_email
async def get_email(email_id: str) -> Optional[Emails]:
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,),
)
return Emails(**row) if row else None
async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
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 [Emails(**row) for row in rows]
async def delete_email(email_id: str) -> None:
await db.execute("DELETE FROM smtp.email WHERE id = ?", (email_id,))

View File

@ -0,0 +1,35 @@
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}
);
"""
)

View File

@ -0,0 +1,47 @@
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 Emailaddresses(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 Emails(BaseModel):
id: str
wallet: str
emailaddress_id: str
subject: str
receiver: str
message: str
paid: bool
time: int

View File

@ -0,0 +1,86 @@
import re
import socket
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 loguru import logger
from starlette.exceptions import HTTPException
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])?"
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)
async def send_mail(emailaddress, email):
valid_email(emailaddress.email)
valid_email(email.receiver)
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"""
<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.")
# 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,36 @@
import asyncio
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 .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(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)

View File

@ -0,0 +1,23 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About LNBits SMTP"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
LNBits SMTP: Get paid sats to send emails
</h5>
<p>
Charge people for using sending an email via your smtp server<br />
<a
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/smtp"
>More details</a
>
<br />
<small>Created by, <a href="https://github.com/dni">dni</a></small>
</p>
</q-card-section>
</q-card>
</q-expansion-item>

View File

@ -0,0 +1,185 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ email }}</h3>
<br />
<h5 class="q-my-none">{{ desc }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.receiver"
type="text"
label="Receiver"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.subject"
type="text"
label="Subject"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.message"
type="textarea"
label="Message "
></q-input>
<p>Total cost: {{ cost }} sats</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.receiver == '' || formDialog.data.subject == '' || formDialog.data.message == ''"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
console.log('{{ cost }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
subject: '',
receiver: '',
message: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.subject = ''
this.formDialog.data.receiver = ''
this.formDialog.data.message = ''
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
Invoice: function () {
var self = this
axios
.post('/smtp/api/v1/email/{{ emailaddress_id }}', {
emailaddress_id: '{{ emailaddress_id }}',
subject: self.formDialog.data.subject,
receiver: self.formDialog.data.receiver,
message: self.formDialog.data.message
})
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.get('/smtp/api/v1/email/' + self.paymentCheck)
.then(function (res) {
console.log(res.data)
if (res.data.paid) {
clearInterval(paymentChecker)
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
console.log(self.formDialog)
self.formDialog.data.subject = ''
self.formDialog.data.receiver = ''
self.formDialog.data.message = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
console.log('END')
}
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,528 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="primary"
@click="emailaddressDialog.show = true"
>New Emailaddress</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Emailaddresses</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportEmailaddressesCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="emailaddresses"
row-key="id"
:columns="emailaddressTable.columns"
:pagination.sync="emailaddressTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateEmailaddressDialog(props.row.id)"
icon="edit"
color="light-blue"
>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEmailaddress(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Emails</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportEmailsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="emails"
row-key="id"
:columns="emailsTable.columns"
:pagination.sync="emailsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEmail(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Sendmail extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "smtp/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="emailaddressDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="emailaddressDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
emit-value
v-model.trim="emailaddressDialog.data.email"
type="text"
label="Emailaddress "
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="emailaddressDialog.data.testemail"
type="text"
label="Emailaddress to test the server"
></q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_server"
type="text"
label="SMTP Host"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_user"
type="text"
label="SMTP User"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_password"
type="password"
label="SMTP Password"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_port"
type="text"
label="SMTP Port"
>
</q-input>
<div id="lolcheck">
<q-checkbox
name="anonymize"
v-model="emailaddressDialog.data.anonymize"
label="ANONYMIZE, don't save mails, no addresses in tx"
/>
</div>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.description"
type="textarea"
label="Description "
>
</q-input>
<q-input
filled
dense
v-model.number="emailaddressDialog.data.cost"
type="number"
label="Amount per email in satoshis"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
v-if="emailaddressDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="enableButton()"
type="submit"
>Create Emailaddress</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var LNSendmail = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.displayUrl = ['/smtp/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
emailaddresses: [],
emails: [],
emailaddressTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{
name: 'anonymize',
align: 'left',
label: 'Anonymize',
field: 'anonymize'
},
{
name: 'email',
align: 'left',
label: 'Emailaddress',
field: 'email'
},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'cost',
align: 'left',
label: 'Cost',
field: 'cost'
}
],
pagination: {
rowsPerPage: 10
}
},
emailsTable: {
columns: [
{
name: 'emailaddress',
align: 'left',
label: 'From',
field: 'emailaddress'
},
{
name: 'receiver',
align: 'left',
label: 'Receiver',
field: 'receiver'
},
{
name: 'subject',
align: 'left',
label: 'Subject',
field: 'subject'
},
{
name: 'message',
align: 'left',
label: 'Message',
field: 'message'
},
{
name: 'paid',
align: 'left',
label: 'Is paid',
field: 'paid'
}
],
pagination: {
rowsPerPage: 10
}
},
emailaddressDialog: {
show: false,
data: {}
}
}
},
methods: {
enableButton: function () {
return (
this.emailaddressDialog.data.cost == null ||
this.emailaddressDialog.data.cost < 0 ||
this.emailaddressDialog.data.testemail == null ||
this.emailaddressDialog.data.smtp_user == null ||
this.emailaddressDialog.data.smtp_password == null ||
this.emailaddressDialog.data.smtp_server == null ||
this.emailaddressDialog.data.smtp_port == null ||
this.emailaddressDialog.data.email == null ||
this.emailaddressDialog.data.description == null
)
},
getEmails: function () {
var self = this
LNbits.api
.request(
'GET',
'/smtp/api/v1/email?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.emails = response.data.map(function (obj) {
return LNSendmail(obj)
})
})
},
deleteEmail: function (emailId) {
var self = this
var email = _.findWhere(this.emails, {id: emailId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this email')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/smtp/api/v1/email/' + emailId,
_.findWhere(self.g.user.wallets, {id: email.wallet}).inkey
)
.then(function (response) {
self.emails = _.reject(self.emails, function (obj) {
return obj.id == emailId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportEmailsCSV: function () {
LNbits.utils.exportCSV(this.emailsTable.columns, this.emails)
},
getEmailAddresses: function () {
var self = this
LNbits.api
.request(
'GET',
'/smtp/api/v1/emailaddress?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.emailaddresses = response.data.map(function (obj) {
return LNSendmail(obj)
})
})
},
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.emailaddressDialog.data.wallet
})
var data = this.emailaddressDialog.data
if (data.id) {
this.updateEmailaddress(wallet, data)
} else {
this.createEmailaddress(wallet, data)
}
},
createEmailaddress: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/smtp/api/v1/emailaddress', wallet.inkey, data)
.then(function (response) {
self.emailaddresses.push(LNSendmail(response.data))
self.emailaddressDialog.show = false
self.emailaddressDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateEmailaddressDialog: function (formId) {
var link = _.findWhere(this.emailaddresses, {id: formId})
this.emailaddressDialog.data = _.clone(link)
this.emailaddressDialog.show = true
},
updateEmailaddress: function (wallet, data) {
var self = this
LNbits.api
.request(
'PUT',
'/smtp/api/v1/emailaddress/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.emailaddresses = _.reject(self.emailaddresses, function (obj) {
return obj.id == data.id
})
self.emailaddresses.push(LNSendmail(response.data))
self.emailaddressDialog.show = false
self.emailaddressDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteEmailaddress: function (emailaddressId) {
var self = this
var emailaddresses = _.findWhere(this.emailaddresses, {
id: emailaddressId
})
LNbits.utils
.confirmDialog(
'Are you sure you want to delete this emailaddress link?'
)
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/smtp/api/v1/emailaddress/' + emailaddressId,
_.findWhere(self.g.user.wallets, {id: emailaddresses.wallet})
.inkey
)
.then(function (response) {
self.emailaddresses = _.reject(self.emailaddresses, function (
obj
) {
return obj.id == emailaddressId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportEmailaddressesCSV: function () {
LNbits.utils.exportCSV(
this.emailaddressTable.columns,
this.emailaddresses
)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getEmailAddresses()
this.getEmails()
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,40 @@
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,
},
)

View File

@ -0,0 +1,170 @@
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
from lnbits.extensions.smtp.models import CreateEmail, CreateEmailaddress
from . import smtp_ext
from .crud import (
create_email,
create_emailaddress,
delete_email,
delete_emailaddress,
get_email,
get_emailaddress,
get_emailaddresses,
get_emails,
update_emailaddress,
)
from .smtp import 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(payment_hash)
if not email:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="paymenthash is wrong"
)
emailaddress = await get_emailaddress(email.emailaddress_id)
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 the request is coming for the non-existant emailaddress
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.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)