Feat: email notifications (#3007)

* Feat: email notifications / email db backup

Because not using a simple smtp connection is silly

* Added form

* broken, and annoying bug

Why wont the input display?!?!?

* make

* Encourage protonmail use, because its great.

* feat: small UI polishing

* chore: make bundle

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
Arc 2025-03-03 12:59:08 +00:00 committed by GitHub
parent fe9b62e8a8
commit 991e0db50b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 242 additions and 3 deletions

View file

@ -1,5 +1,8 @@
import asyncio import asyncio
import json import json
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from http import HTTPStatus from http import HTTPStatus
from typing import Optional, Tuple from typing import Optional, Tuple
@ -62,6 +65,12 @@ async def send_notification(
logger.debug(f"Sent nostr notification: {message_type}") logger.debug(f"Sent nostr notification: {message_type}")
except Exception as e: except Exception as e:
logger.error(f"Error sending nostr notification {message_type}: {e}") logger.error(f"Error sending nostr notification {message_type}: {e}")
try:
if settings.lnbits_email_notifications_enabled:
await send_email_notification(message)
logger.debug(f"Sent email notification: {message_type}")
except Exception as e:
logger.error(f"Error sending email notification {message_type}: {e}")
async def send_nostr_notification(message: str) -> dict: async def send_nostr_notification(message: str) -> dict:
@ -102,6 +111,43 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
return response.json() return response.json()
async def send_email_notification(message: str) -> dict:
await send_email(
settings.lnbits_email_notifications_server,
settings.lnbits_email_notifications_port,
settings.lnbits_email_notifications_password,
settings.lnbits_email_notifications_email,
settings.lnbits_email_notifications_to_emails,
"LNbits Notification",
message,
)
return {"status": "ok"}
async def send_email(
server: str,
port: int,
password: str,
from_email: str,
to_emails: list,
subject: str,
message: str,
):
msg = MIMEMultipart()
msg["From"] = from_email
msg["To"] = ", ".join(to_emails)
msg["Subject"] = subject
msg.attach(MIMEText(message, "plain"))
try:
with smtplib.SMTP(server, port) as smtp_server:
smtp_server.starttls()
smtp_server.login(from_email, password)
smtp_server.sendmail(from_email, to_emails, msg.as_string())
logger.debug(f"Emails sent successfully to: {', '.join(to_emails)}")
except Exception as e:
logger.debug(f"Failed to send email: {e}")
def is_message_type_enabled(message_type: NotificationType) -> bool: def is_message_type_enabled(message_type: NotificationType) -> bool:
if message_type == NotificationType.balance_update: if message_type == NotificationType.balance_update:
return settings.lnbits_notification_credit_debit return settings.lnbits_notification_credit_debit

View file

@ -2,7 +2,14 @@
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('notifications_configure')"></span> <span v-text="$t('notifications_configure')"></span>
<q-btn
round
flat
@click="hideInputsToggle()"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn>
</h6> </h6>
<q-separator class="q-mt-md q-mb-sm"></q-separator> <q-separator class="q-mt-md q-mb-sm"></q-separator>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-sm-12 col-md-6"> <div class="col-sm-12 col-md-6">
@ -39,12 +46,13 @@
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-input <q-input
type="password" :type="hideInputToggle ? 'password' : 'text'"
filled filled
v-model="formData.lnbits_nostr_notifications_private_key" v-model="formData.lnbits_nostr_notifications_private_key"
/> />
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item tag="label" v-ripple> <q-item tag="label" v-ripple>
<q-item-section> <q-item-section>
<q-item-label <q-item-label
@ -117,7 +125,7 @@
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-input <q-input
type="password" :type="hideInputToggle ? 'password' : 'text'"
filled filled
v-model="formData.lnbits_telegram_notifications_access_token" v-model="formData.lnbits_telegram_notifications_access_token"
/> />
@ -139,6 +147,150 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
</div> </div>
<div class="col-sm-12">
<q-separator></q-separator>
</div>
<div class="col-12">
<strong v-text="$t('notifications_email_config')"></strong>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_enable_email')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_enable_email_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_email_notifications_enabled"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<div v-if="formData.lnbits_email_notifications_enabled" class="row">
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_email"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_password')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_password_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_password"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_to_emails')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_to_emails_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="emailNotificationAddress"
@keydown.enter="addEmailNotificationAddress"
>
<q-btn
@click="addEmailNotificationAddress()"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="to_email in formData.lnbits_email_notifications_to_emails"
:key="to_email"
removable
@remove="removeEmailNotificationAddress(to_email)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="to_email"></span
></q-chip>
</div>
</q-item-section>
</q-item>
</div>
<div class="col">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_server_port')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_server_port_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_port"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notifications_send_email_server')"
></q-item-label>
<q-item-label
caption
v-text="$t('notifications_send_email_server_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
:type="hideInputToggle ? 'password' : 'text'"
filled
v-model="formData.lnbits_email_notifications_server"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
</div> </div>
<q-separator> </q-separator> <q-separator> </q-separator>
<h6 class="q-mb-sm"> <h6 class="q-mb-sm">

View file

@ -394,6 +394,12 @@ class NotificationsSettings(LNbitsSettings):
lnbits_telegram_notifications_enabled: bool = Field(default=False) lnbits_telegram_notifications_enabled: bool = Field(default=False)
lnbits_telegram_notifications_access_token: str = Field(default="") lnbits_telegram_notifications_access_token: str = Field(default="")
lnbits_telegram_notifications_chat_id: str = Field(default="") lnbits_telegram_notifications_chat_id: str = Field(default="")
lnbits_email_notifications_enabled: bool = Field(default=False)
lnbits_email_notifications_email: str = Field(default="")
lnbits_email_notifications_password: str = Field(default="")
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
lnbits_email_notifications_port: int = Field(default=587)
lnbits_email_notifications_to_emails: list[str] = Field(default=[])
lnbits_notification_settings_update: bool = Field(default=True) lnbits_notification_settings_update: bool = Field(default=True)
lnbits_notification_credit_debit: bool = Field(default=True) lnbits_notification_credit_debit: bool = Field(default=True)

File diff suppressed because one or more lines are too long

View file

@ -196,6 +196,22 @@ window.localisation.en = {
notifications_chat_id: 'Chat ID', notifications_chat_id: 'Chat ID',
notifications_chat_id_desc: 'Chat ID to send the notifications to', notifications_chat_id_desc: 'Chat ID to send the notifications to',
notifications_email_config: 'Email Configuration',
notifications_enable_email: 'Enable Email',
notifications_enable_email_desc: 'Send notfications over Email',
notifications_send_email: 'Send email',
notifications_send_email_desc: 'Email you will send from',
notifications_send_email_password: 'Send email password',
notifications_send_email_password_desc:
'Password for the email you will send from',
notifications_send_email_server_port: 'Send email SMTP port',
notifications_send_email_server_port_desc: 'Port for the SMTP server',
notifications_send_email_server: 'Send email SMTP server',
notifications_send_email_server_desc:
'SMTP server for the email you will send from',
notifications_send_to_emails: 'Emails to send to',
notifications_send_to_emails_desc: 'Emails notifications will be sent to',
notification_settings_update: 'Settings updated', notification_settings_update: 'Settings updated',
notification_settings_update_desc: notification_settings_update_desc:
'Notify when server settings have been updated', 'Notify when server settings have been updated',

View file

@ -53,8 +53,10 @@ window.AdminPageLogic = {
chartReady: false, chartReady: false,
formAddAdmin: '', formAddAdmin: '',
formAddUser: '', formAddUser: '',
hideInputToggle: true,
formAddExtensionsManifest: '', formAddExtensionsManifest: '',
nostrNotificationIdentifier: '', nostrNotificationIdentifier: '',
emailNotificationAddress: '',
formAllowedIPs: '', formAllowedIPs: '',
formCallbackUrlRule: '', formCallbackUrlRule: '',
formBlockedIPs: '', formBlockedIPs: '',
@ -270,6 +272,23 @@ window.AdminPageLogic = {
m => m !== identifer m => m !== identifer
) )
}, },
addEmailNotificationAddress() {
const email = this.emailNotificationAddress.trim()
const emails = this.formData.lnbits_email_notifications_to_emails
if (email && email.length && !emails.includes(email)) {
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
this.emailNotificationAddress = ''
}
},
removeEmailNotificationAddress(email) {
const emails = this.formData.lnbits_email_notifications_to_emails
this.formData.lnbits_email_notifications_to_emails = emails.filter(
m => m !== email
)
},
hideInputsToggle() {
this.hideInputToggle = !this.hideInputToggle
},
async toggleServerLog() { async toggleServerLog() {
this.serverlogEnabled = !this.serverlogEnabled this.serverlogEnabled = !this.serverlogEnabled
if (this.serverlogEnabled) { if (this.serverlogEnabled) {