From 40463d983174a74436749160de1634560d653b0b Mon Sep 17 00:00:00 2001 From: Donald Zou Date: Wed, 8 Jan 2025 18:09:05 +0800 Subject: [PATCH] Email functionality is done --- src/Email.py | 68 +++++++++++++ src/dashboard.py | 91 +++++++----------- .../dashboardEmailSettings.vue | 95 ++++++++++++++++--- src/static/app/src/utilities/fetch.js | 1 - 4 files changed, 185 insertions(+), 70 deletions(-) create mode 100644 src/Email.py diff --git a/src/Email.py b/src/Email.py new file mode 100644 index 0000000..d232f36 --- /dev/null +++ b/src/Email.py @@ -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}" \ No newline at end of file diff --git a/src/dashboard.py b/src/dashboard.py index 5a62bba..7336e6a 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -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) diff --git a/src/static/app/src/components/settingsComponent/dashboardEmailSettings.vue b/src/static/app/src/components/settingsComponent/dashboardEmailSettings.vue index 4edded1..98bab24 100644 --- a/src/static/app/src/components/settingsComponent/dashboardEmailSettings.vue +++ b/src/static/app/src/components/settingsComponent/dashboardEmailSettings.vue @@ -1,14 +1,14 @@ diff --git a/src/static/app/src/utilities/fetch.js b/src/static/app/src/utilities/fetch.js index e58fc28..616843c 100644 --- a/src/static/app/src/utilities/fetch.js +++ b/src/static/app/src/utilities/fetch.js @@ -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)