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,
GenerateWireguardPublicKey, GenerateWireguardPrivateKey
)
from Email import EmailSender
DASHBOARD_VERSION = 'v4.2.0'
@ -92,6 +92,7 @@ Dashboard Logger Class
class DashboardLogger:
def __init__(self):
self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'),
isolation_level=None,
check_same_thread=False)
self.loggerdb.row_factory = sqlite3.Row
self.__createLogDatabase()
@ -107,20 +108,20 @@ class DashboardLogger:
if self.loggerdb.in_transaction:
self.loggerdb.commit()
def log(self, URL: str = "", IP: str = "", Status: str = "true", Message: str = "") -> bool:
pass
def log(self, URL: str = "", IP: str = "", Status: str = "true", Message: str = "") -> bool:
try:
with self.loggerdb:
loggerdbCursor = self.loggerdb.cursor()
loggerdbCursor.execute(
"INSERT INTO DashboardLog (LogID, URL, IP, Status, Message) VALUES (?, ?, ?, ?, ?)", (str(uuid.uuid4()), URL, IP, Status, Message,))
if self.loggerdb.in_transaction:
self.loggerdb.commit()
return True
loggerdbCursor = self.loggerdb.cursor()
loggerdbCursor.execute(
"INSERT INTO DashboardLog (LogID, URL, IP, Status, Message) VALUES (?, ?, ?, ?, ?);", (str(uuid.uuid4()), URL, IP, Status, Message,))
loggerdbCursor.close()
self.loggerdb.commit()
return True
except Exception as e:
print(f"[WGDashboard] Access Log Error: {str(e)}")
return False
"""
Peer Job Logger
"""
@ -1878,7 +1879,8 @@ class DashboardConfig:
"port": "",
"encryption": "",
"username": "",
"email_password": ""
"email_password": "",
"send_from": ""
},
"WireGuardConfiguration": {
"autostart": ""
@ -1919,22 +1921,22 @@ class DashboardConfig:
sqlUpdate("UPDATE DashboardAPIKeys SET ExpiredAt = datetime('now', 'localtime') WHERE Key = ?", (key, ))
self.DashboardAPIKeys = self.__getAPIKeys()
def __configValidation(self, key, value: Any) -> [bool, str]:
if type(value) is str and len(value) == 0:
def __configValidation(self, section : str, key: str, value: Any) -> [bool, str]:
if type(value) is str and len(value) == 0 and section not in ['Email', 'WireGuardConfiguration']:
return False, "Field cannot be empty!"
if key == "peer_global_dns":
if section == "Peers" and key == "peer_global_dns":
return ValidateDNSAddress(value)
if key == "peer_endpoint_allowed_ip":
if section == "Peers" and key == "peer_endpoint_allowed_ip":
value = value.split(",")
for i in value:
try:
ipaddress.ip_network(i, strict=False)
except Exception as e:
return False, str(e)
if key == "wg_conf_path":
if section == "Server" and key == "wg_conf_path":
if not os.path.exists(value):
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 not self.__checkPassword(
value["currentPassword"], self.GetConfig("Account", "password")[1].encode("utf-8")):
@ -1954,7 +1956,7 @@ class DashboardConfig:
return False, None
if not init:
valid, msg = self.__configValidation(key, value)
valid, msg = self.__configValidation(section, key, value)
if not valid:
return False, msg
@ -2021,32 +2023,7 @@ class DashboardConfig:
if key not in self.hiddenAttribute:
the_dict[section][key] = self.GetConfig(section, key)[1]
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
@ -2054,16 +2031,9 @@ Database Connection Functions
sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
sqldb.row_factory = sqlite3.Row
# cursor = sqldb.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 = []
# with sqldb:
try:
cursor = sqldb.cursor()
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)
sqldb.close()
DashboardConfig = DashboardConfig()
# EmailSender = EmailSender()
EmailSender = EmailSender(DashboardConfig)
_, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix")
cors = CORS(app, resources={rf"{APP_PREFIX}/api/*": {
"origins": "*",
@ -3008,7 +2977,6 @@ def API_Welcome_VerifyTotpLink():
DashboardConfig.SetConfig("Account", "enable_totp", "true")
return ResponseObject(totp == data['totp'])
@app.post(f'{APP_PREFIX}/api/Welcome_Finish')
def API_Welcome_Finish():
data = request.get_json()
@ -3039,7 +3007,6 @@ class Locale:
with open(os.path.join(f"{self.localePath}active_languages.json"), "r") as f:
self.activeLanguages = json.loads(''.join(f.readlines()))
def getLanguage(self) -> dict | None:
currentLanguage = DashboardConfig.GetConfig("Server", "dashboard_language")[1]
if currentLanguage == "en":
@ -3056,7 +3023,6 @@ class Locale:
else:
DashboardConfig.SetConfig("Server", "dashboard_language", lang_id)
Locale = Locale()
@app.get(f'{APP_PREFIX}/api/locale')
@ -3075,6 +3041,19 @@ def API_Locale_Update():
Locale.updateLanguage(data['lang_id'])
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')
def API_SystemStatus():
cpu_percpu = psutil.cpu_percent(interval=0.5, percpu=True)

View File

@ -1,14 +1,14 @@
<script setup>
import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {preventDefault} from "ol/events/Event.js";
import {onMounted} from "vue";
import {fetchPost} from "@/utilities/fetch.js";
import {onMounted, ref} from "vue";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
const store = DashboardConfigurationStore()
onMounted(() => {
document.querySelectorAll("#emailAccount input").forEach(x => {
x.addEventListener("blur", async () => {
checkEmailReady()
document.querySelectorAll("#emailAccount input, #emailAccount select").forEach(x => {
x.addEventListener("change", async () => {
let id = x.attributes.getNamedItem('id').value;
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Email",
@ -22,22 +22,53 @@ onMounted(() => {
x.classList.remove('is-valid')
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>
<template>
<div class="card" id="emailAccount">
<div class="card">
<div class="card-header">
<h6 class="my-2">
<h6 class="my-2 d-flex">
<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>
</div>
<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="col-12 col-lg-4">
<div class="form-group">
@ -70,12 +101,19 @@ onMounted(() => {
<LocaleText t="Encryption"></LocaleText>
</small></strong>
</label>
<input id="encryption"
v-model="store.Configuration.Email.encryption"
type="text" class="form-control">
<select class="form-select"
v-model="store.Configuration.Email.encryption"
id="encryption">
<option value="STARTTLS">
STARTTLS
</option>
<option value="NOTLS">
<LocaleText t="No Encryption"></LocaleText>
</option>
</select>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="col-12 col-lg-4">
<div class="form-group">
<label for="username" class="text-muted mb-1">
<strong><small>
@ -87,7 +125,7 @@ onMounted(() => {
type="text" class="form-control">
</div>
</div>
<div class="col-12 col-lg-6">
<div class="col-12 col-lg-4">
<div class="form-group">
<label for="email_password" class="text-muted mb-1">
<strong><small>
@ -99,8 +137,39 @@ onMounted(() => {
type="password" class="form-control">
</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>
</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>
</div>
</div>
</template>

View File

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