Email functionality is done

This commit is contained in:
Donald Zou 2025-01-08 18:09:05 +08:00
parent eb7dee013d
commit 40463d9831
4 changed files with 185 additions and 70 deletions

68
src/Email.py Normal file
View File

@ -0,0 +1,68 @@
import os.path
import smtplib
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
class EmailSender:
def __init__(self, DashboardConfig):
self.smtp = None
self.DashboardConfig = DashboardConfig
if not os.path.exists('./attachments'):
os.mkdir('./attachments')
def Server(self):
return self.DashboardConfig.GetConfig("Email", "server")[1]
def Port(self):
return self.DashboardConfig.GetConfig("Email", "port")[1]
def Encryption(self):
return self.DashboardConfig.GetConfig("Email", "encryption")[1]
def Username(self):
return self.DashboardConfig.GetConfig("Email", "username")[1]
def Password(self):
return self.DashboardConfig.GetConfig("Email", "email_password")[1]
def SendFrom(self):
return self.DashboardConfig.GetConfig("Email", "send_from")[1]
def ready(self):
return len(self.Server()) > 0 and len(self.Port()) > 0 and len(self.Encryption()) > 0 and len(self.Username()) > 0 and len(self.Password()) > 0
def send(self, receiver, subject, body, includeAttachment = False, attachmentName = ""):
if self.ready():
try:
self.smtp = smtplib.SMTP(self.Server(), port=int(self.Port()))
self.smtp.ehlo()
if self.Encryption() == "STARTTLS":
self.smtp.starttls()
self.smtp.login(self.Username(), self.Password())
message = MIMEMultipart()
message['Subject'] = subject
message['From'] = formataddr((Header(self.SendFrom()).encode(), self.Username()))
message["To"] = receiver
message.attach(MIMEText(body, "html"))
if includeAttachment and len(attachmentName) > 0:
attachmentPath = os.path.join('./attachments', attachmentName)
if os.path.exists(attachmentPath):
attachment = MIMEBase("application", "octet-stream")
with open(os.path.join('attachments', attachmentName), 'rb') as f:
attachment.set_payload(f.read())
encoders.encode_base64(attachment)
attachment.add_header("Content-Disposition", f"attachment; filename= {attachmentName}",)
message.attach(attachment)
else:
self.smtp.close()
return False, "Attachment does not exist"
self.smtp.sendmail(self.Username(), receiver, message.as_string())
self.smtp.close()
return True, None
except Exception as e:
return False, f"Send failed | Reason: {e}"

View File

@ -14,7 +14,7 @@ from Utilities import (
ValidateIPAddressesWithRange, ValidateIPAddresses, ValidateDNSAddress, ValidateIPAddressesWithRange, ValidateIPAddresses, ValidateDNSAddress,
GenerateWireguardPublicKey, GenerateWireguardPrivateKey GenerateWireguardPublicKey, GenerateWireguardPrivateKey
) )
from Email import EmailSender
DASHBOARD_VERSION = 'v4.2.0' DASHBOARD_VERSION = 'v4.2.0'
@ -92,6 +92,7 @@ Dashboard Logger Class
class DashboardLogger: class DashboardLogger:
def __init__(self): def __init__(self):
self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'), self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'),
isolation_level=None,
check_same_thread=False) check_same_thread=False)
self.loggerdb.row_factory = sqlite3.Row self.loggerdb.row_factory = sqlite3.Row
self.__createLogDatabase() self.__createLogDatabase()
@ -108,19 +109,19 @@ class DashboardLogger:
self.loggerdb.commit() self.loggerdb.commit()
def log(self, URL: str = "", IP: str = "", Status: str = "true", Message: str = "") -> bool: def log(self, URL: str = "", IP: str = "", Status: str = "true", Message: str = "") -> bool:
pass
try: try:
with self.loggerdb:
loggerdbCursor = self.loggerdb.cursor() loggerdbCursor = self.loggerdb.cursor()
loggerdbCursor.execute( loggerdbCursor.execute(
"INSERT INTO DashboardLog (LogID, URL, IP, Status, Message) VALUES (?, ?, ?, ?, ?)", (str(uuid.uuid4()), URL, IP, Status, Message,)) "INSERT INTO DashboardLog (LogID, URL, IP, Status, Message) VALUES (?, ?, ?, ?, ?);", (str(uuid.uuid4()), URL, IP, Status, Message,))
if self.loggerdb.in_transaction: loggerdbCursor.close()
self.loggerdb.commit() self.loggerdb.commit()
return True return True
except Exception as e: except Exception as e:
print(f"[WGDashboard] Access Log Error: {str(e)}") print(f"[WGDashboard] Access Log Error: {str(e)}")
return False return False
""" """
Peer Job Logger Peer Job Logger
""" """
@ -1878,7 +1879,8 @@ class DashboardConfig:
"port": "", "port": "",
"encryption": "", "encryption": "",
"username": "", "username": "",
"email_password": "" "email_password": "",
"send_from": ""
}, },
"WireGuardConfiguration": { "WireGuardConfiguration": {
"autostart": "" "autostart": ""
@ -1919,22 +1921,22 @@ class DashboardConfig:
sqlUpdate("UPDATE DashboardAPIKeys SET ExpiredAt = datetime('now', 'localtime') WHERE Key = ?", (key, )) sqlUpdate("UPDATE DashboardAPIKeys SET ExpiredAt = datetime('now', 'localtime') WHERE Key = ?", (key, ))
self.DashboardAPIKeys = self.__getAPIKeys() self.DashboardAPIKeys = self.__getAPIKeys()
def __configValidation(self, key, value: Any) -> [bool, str]: def __configValidation(self, section : str, key: str, value: Any) -> [bool, str]:
if type(value) is str and len(value) == 0: if type(value) is str and len(value) == 0 and section not in ['Email', 'WireGuardConfiguration']:
return False, "Field cannot be empty!" return False, "Field cannot be empty!"
if key == "peer_global_dns": if section == "Peers" and key == "peer_global_dns":
return ValidateDNSAddress(value) return ValidateDNSAddress(value)
if key == "peer_endpoint_allowed_ip": if section == "Peers" and key == "peer_endpoint_allowed_ip":
value = value.split(",") value = value.split(",")
for i in value: for i in value:
try: try:
ipaddress.ip_network(i, strict=False) ipaddress.ip_network(i, strict=False)
except Exception as e: except Exception as e:
return False, str(e) return False, str(e)
if key == "wg_conf_path": if section == "Server" and key == "wg_conf_path":
if not os.path.exists(value): if not os.path.exists(value):
return False, f"{value} is not a valid path" return False, f"{value} is not a valid path"
if key == "password": if section == "Account" and key == "password":
if self.GetConfig("Account", "password")[0]: if self.GetConfig("Account", "password")[0]:
if not self.__checkPassword( if not self.__checkPassword(
value["currentPassword"], self.GetConfig("Account", "password")[1].encode("utf-8")): value["currentPassword"], self.GetConfig("Account", "password")[1].encode("utf-8")):
@ -1954,7 +1956,7 @@ class DashboardConfig:
return False, None return False, None
if not init: if not init:
valid, msg = self.__configValidation(key, value) valid, msg = self.__configValidation(section, key, value)
if not valid: if not valid:
return False, msg return False, msg
@ -2022,31 +2024,6 @@ class DashboardConfig:
the_dict[section][key] = self.GetConfig(section, key)[1] the_dict[section][key] = self.GetConfig(section, key)[1]
return the_dict return the_dict
"""
Email Sender
"""
class EmailSender:
import smtplib
def __init__(self):
self.Server = DashboardConfig.GetConfig("Email", "server")[1]
self.Port = DashboardConfig.GetConfig("Email", "port")[1]
self.Encryption = DashboardConfig.GetConfig("Email", "encryption")[1]
self.Username = DashboardConfig.GetConfig("Email", "username")[1]
self.Password = DashboardConfig.GetConfig("Email", "email_password")[1]
self.login()
def ready(self):
return self.Server and self.Port and self.Encryption and self.Username and self.Password
def login(self):
if self.ready():
self.smtp = smtplib.SMTP(self.Server, port=int(self.Port))
if self.Encryption == "STARTTLS":
self.smtp.starttls()
self.smtp.login(self.Username, self.Password)
""" """
Database Connection Functions Database Connection Functions
@ -2054,16 +2031,9 @@ Database Connection Functions
sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False) sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
sqldb.row_factory = sqlite3.Row sqldb.row_factory = sqlite3.Row
# cursor = sqldb.cursor()
def sqlSelect(statement: str, paramters: tuple = ()) -> sqlite3.Cursor: def sqlSelect(statement: str, paramters: tuple = ()) -> sqlite3.Cursor:
# sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
# sqldb.row_factory = sqlite3.Row
# cursor = sqldb.cursor()
result = [] result = []
# with sqldb:
try: try:
cursor = sqldb.cursor() cursor = sqldb.cursor()
result = cursor.execute(statement, paramters) result = cursor.execute(statement, paramters)
@ -2086,9 +2056,8 @@ def sqlUpdate(statement: str, paramters: tuple = ()) -> sqlite3.Cursor:
print("[WGDashboard] SQLite Error:" + str(error) + " | Statement: " + statement) print("[WGDashboard] SQLite Error:" + str(error) + " | Statement: " + statement)
sqldb.close() sqldb.close()
DashboardConfig = DashboardConfig() DashboardConfig = DashboardConfig()
# EmailSender = EmailSender() EmailSender = EmailSender(DashboardConfig)
_, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix") _, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix")
cors = CORS(app, resources={rf"{APP_PREFIX}/api/*": { cors = CORS(app, resources={rf"{APP_PREFIX}/api/*": {
"origins": "*", "origins": "*",
@ -3008,7 +2977,6 @@ def API_Welcome_VerifyTotpLink():
DashboardConfig.SetConfig("Account", "enable_totp", "true") DashboardConfig.SetConfig("Account", "enable_totp", "true")
return ResponseObject(totp == data['totp']) return ResponseObject(totp == data['totp'])
@app.post(f'{APP_PREFIX}/api/Welcome_Finish') @app.post(f'{APP_PREFIX}/api/Welcome_Finish')
def API_Welcome_Finish(): def API_Welcome_Finish():
data = request.get_json() data = request.get_json()
@ -3039,7 +3007,6 @@ class Locale:
with open(os.path.join(f"{self.localePath}active_languages.json"), "r") as f: with open(os.path.join(f"{self.localePath}active_languages.json"), "r") as f:
self.activeLanguages = json.loads(''.join(f.readlines())) self.activeLanguages = json.loads(''.join(f.readlines()))
def getLanguage(self) -> dict | None: def getLanguage(self) -> dict | None:
currentLanguage = DashboardConfig.GetConfig("Server", "dashboard_language")[1] currentLanguage = DashboardConfig.GetConfig("Server", "dashboard_language")[1]
if currentLanguage == "en": if currentLanguage == "en":
@ -3056,7 +3023,6 @@ class Locale:
else: else:
DashboardConfig.SetConfig("Server", "dashboard_language", lang_id) DashboardConfig.SetConfig("Server", "dashboard_language", lang_id)
Locale = Locale() Locale = Locale()
@app.get(f'{APP_PREFIX}/api/locale') @app.get(f'{APP_PREFIX}/api/locale')
@ -3075,6 +3041,19 @@ def API_Locale_Update():
Locale.updateLanguage(data['lang_id']) Locale.updateLanguage(data['lang_id'])
return ResponseObject(data=Locale.getLanguage()) return ResponseObject(data=Locale.getLanguage())
@app.get(f'{APP_PREFIX}/api/email/ready')
def API_Email_Ready():
return ResponseObject(EmailSender.ready())
@app.post(f'{APP_PREFIX}/api/email/send')
def API_Email_Send():
data = request.get_json()
if "receiver" not in data.keys():
return ResponseObject(False, "Please at least specify receiver")
s, m = EmailSender.send(data.get('receiver'), data.get('subject', ''), data.get('body', ''))
return ResponseObject(s, m)
@app.get(f'{APP_PREFIX}/api/systemStatus') @app.get(f'{APP_PREFIX}/api/systemStatus')
def API_SystemStatus(): def API_SystemStatus():
cpu_percpu = psutil.cpu_percent(interval=0.5, percpu=True) cpu_percpu = psutil.cpu_percent(interval=0.5, percpu=True)

View File

@ -1,14 +1,14 @@
<script setup> <script setup>
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {preventDefault} from "ol/events/Event.js"; import {onMounted, ref} from "vue";
import {onMounted} from "vue"; import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {fetchPost} from "@/utilities/fetch.js";
const store = DashboardConfigurationStore() const store = DashboardConfigurationStore()
onMounted(() => { onMounted(() => {
document.querySelectorAll("#emailAccount input").forEach(x => { checkEmailReady()
x.addEventListener("blur", async () => { document.querySelectorAll("#emailAccount input, #emailAccount select").forEach(x => {
x.addEventListener("change", async () => {
let id = x.attributes.getNamedItem('id').value; let id = x.attributes.getNamedItem('id').value;
await fetchPost("/api/updateDashboardConfigurationItem", { await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Email", section: "Email",
@ -22,22 +22,53 @@ onMounted(() => {
x.classList.remove('is-valid') x.classList.remove('is-valid')
x.classList.add('is-invalid') x.classList.add('is-invalid')
} }
checkEmailReady()
}) })
}) })
}) })
}) })
const emailIsReady = ref(false)
const testEmailReceiver = ref("")
const testing = ref(false)
const checkEmailReady = async () => {
await fetchGet("/api/email/ready", {}, (res) => {
emailIsReady.value = res.status
})
}
const sendTestEmail = async () => {
testing.value = true
await fetchPost("/api/email/send", {
receiver: testEmailReceiver.value,
subject: "WGDashboard Testing Email",
body: "Test 1, 2, 3! Hello World :)"
}, (res) => {
if (res.status){
store.newMessage("Server", "Test email sent successfully!", "success")
}else{
store.newMessage("Server", `Test email sent failed! Reason: ${res.message}`, "danger")
}
testing.value = false
})
}
</script> </script>
<template> <template>
<div class="card" id="emailAccount"> <div class="card">
<div class="card-header"> <div class="card-header">
<h6 class="my-2"> <h6 class="my-2 d-flex">
<LocaleText t="Email Account"></LocaleText> <LocaleText t="Email Account"></LocaleText>
<span class="text-success ms-auto" v-if="emailIsReady">
<i class="bi bi-check-circle-fill me-2"></i>
<LocaleText t="Ready"></LocaleText>
</span>
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<form @submit="(e) => preventDefault(e)"> <form @submit="(e) => e.preventDefault(e)" id="emailAccount">
<div class="row gx-2 gy-2"> <div class="row gx-2 gy-2">
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
<div class="form-group"> <div class="form-group">
@ -70,12 +101,19 @@ onMounted(() => {
<LocaleText t="Encryption"></LocaleText> <LocaleText t="Encryption"></LocaleText>
</small></strong> </small></strong>
</label> </label>
<input id="encryption" <select class="form-select"
v-model="store.Configuration.Email.encryption" v-model="store.Configuration.Email.encryption"
type="text" class="form-control"> id="encryption">
<option value="STARTTLS">
STARTTLS
</option>
<option value="NOTLS">
<LocaleText t="No Encryption"></LocaleText>
</option>
</select>
</div> </div>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-4">
<div class="form-group"> <div class="form-group">
<label for="username" class="text-muted mb-1"> <label for="username" class="text-muted mb-1">
<strong><small> <strong><small>
@ -87,7 +125,7 @@ onMounted(() => {
type="text" class="form-control"> type="text" class="form-control">
</div> </div>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-4">
<div class="form-group"> <div class="form-group">
<label for="email_password" class="text-muted mb-1"> <label for="email_password" class="text-muted mb-1">
<strong><small> <strong><small>
@ -99,7 +137,38 @@ onMounted(() => {
type="password" class="form-control"> type="password" class="form-control">
</div> </div>
</div> </div>
<div class="col-12 col-lg-4">
<div class="form-group">
<label for="send_from" class="text-muted mb-1">
<strong><small>
<LocaleText t="Send From"></LocaleText>
</small></strong>
</label>
<input id="send_from"
v-model="store.Configuration.Email.send_from"
type="text" class="form-control">
</div> </div>
</div>
</div>
</form>
<hr>
<form
@submit="(e) => {e.preventDefault(); sendTestEmail()}"
class="input-group mb-3">
<input type="email" class="form-control rounded-start-3"
v-model="testEmailReceiver"
:disabled="testing"
placeholder="Test Email Receiver">
<button class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-end-3"
type="submit" value="Submit"
:disabled="testEmailReceiver.length === 0 || testing"
id="button-addon2">
<i class="bi bi-send me-2" v-if="!testing"></i>
<span class="spinner-border spinner-border-sm me-2" v-else>
<span class="visually-hidden">Loading...</span>
</span>
<LocaleText :t="!testing ? 'Send Test Email':'Sending...'"></LocaleText>
</button>
</form> </form>
</div> </div>
</div> </div>

View File

@ -60,7 +60,6 @@ export const fetchPost = async (url, body, callback) => {
if (!x.ok){ if (!x.ok){
if (x.status !== 200){ if (x.status !== 200){
if (x.status === 401){ if (x.status === 401){
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning") store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
} }
throw new Error(x.statusText) throw new Error(x.statusText)