copy initial copy from my fork

This commit is contained in:
dni ⚡ 2023-01-09 09:41:19 +01:00
parent d5009a7d0a
commit 3d8a8664f2
13 changed files with 1400 additions and 0 deletions

View file

@ -0,0 +1,26 @@
<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 send on create and update
3. enjoy
## API Endpoints
- **Emailaddresses**
- GET /api/v1/emailaddress
- POST /api/v1/emailaddress
- PUT /api/v1/emailaddress/<domain_id>
- DELETE /api/v1/emailaddress/<domain_id>
- **Emails**
- GET /api/v1/email
- POST /api/v1/email/<emailaddress_id>
- GET /api/v1/email/<payment_hash>
- DELETE /api/v1/email/<email_id>

View file

@ -0,0 +1,25 @@
import asyncio
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_smtp")
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": "Let users send emails via your SMTP and earn sats",
"icon": "email",
"contributors": ["dni"]
}

View file

@ -0,0 +1,170 @@
from http import HTTPStatus
from typing import List, Optional, Union
from starlette.exceptions import HTTPException
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.params import Query
from pydantic.main import BaseModel
class CreateEmailaddress(BaseModel):
wallet: str = Query(...) # type: ignore
email: str = Query(...) # type: ignore
testemail: str = Query(...) # type: ignore
smtp_server: str = Query(...) # type: ignore
smtp_user: str = Query(...) # type: ignore
smtp_password: str = Query(...) # type: ignore
smtp_port: str = Query(...) # type: ignore
description: str = Query(...) # type: ignore
anonymize: bool
cost: int = Query(..., ge=0) # type: ignore
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(...) # type: ignore
subject: str = Query(...) # type: ignore
receiver: str = Query(...) # type: ignore
message: str = Query(...) # type: ignore
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,90 @@
import os
import re
import socket
import sys
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
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)
msg = MIMEMultipart("alternative")
msg["Subject"] = email.subject
msg["From"] = emailaddress.email
msg["To"] = email.receiver
signature = "Email sent anonymiously by LNbits Sendmail extension."
text = (
"""\
"""
+ email.message
+ """
"""
+ signature
+ """
"""
)
html = (
"""\
<html>
<head></head>
<body>
<p>"""
+ email.message
+ """<p>
<p><br>"""
+ 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()

View file

@ -0,0 +1,46 @@
import asyncio
from http import HTTPStatus
import httpx
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import (
delete_email,
get_email,
get_emailaddress,
get_emailaddress_by_email,
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 not payment.extra or "smtp" != payment.extra.get("tag"):
# not an lnurlp invoice
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-legends/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,44 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
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) # type: ignore
):
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,175 @@
from http import HTTPStatus
from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
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_emailaddress_by_email,
get_emailaddresses,
get_emails,
update_emailaddress,
)
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) # type: ignore
):
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) # type: ignore
):
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), # type: ignore
all_wallets: bool = Query(False), # type: ignore
):
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), # type: ignore
):
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) # type: ignore
):
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)