From ea8058a2e87e27868eb20f9ca638c140d2404f14 Mon Sep 17 00:00:00 2001 From: Stefano Dell'Osa Date: Tue, 19 Nov 2024 23:32:03 +0100 Subject: [PATCH] Added ftp user to camera class, improved keyring check at deserialization time. (#82) * Added ftp user to camera class * Added keyring check at deserialization time * Added email keyring check at deserialization time * load_configuration function now provides feeback if keyrings configuration is inconsistent or missing. * Added logic to fix keyring config at deserialization time * Run button is disabled if faulty keyring configuration is loaded until it get fixed * Added logic to prevent run if keyring is misconfigured * Fixed FTP camera user modifications in FTP server * Improved email keyring check * removed unused import, fixed email config check at run * fixed keyring credential modifications * Due to a limitation of keyring lib, if a credential set is modified it is shown as user@service in system keyring manager causing ftp authentication problems * Fixed email keyring insertion * Improved email test log --------- Co-authored-by: Alessandro Palla --- wadas/domain/configuration.py | 50 +++++++++++++-- wadas/domain/ftp_camera.py | 10 ++- wadas/domain/ftps_server.py | 4 ++ wadas/ui/configure_email_dialog.py | 46 +++++++++---- wadas/ui/configure_ftp_cameras_dialog.py | 80 ++++++++++++++++------- wadas/ui/mainwindow.py | 58 +++++++++++++++-- wadas/ui/qt/configure_email.ui | 82 +++++++++++------------- wadas/ui/qt/ui_configure_email.py | 66 ++++++++++--------- 8 files changed, 269 insertions(+), 127 deletions(-) diff --git a/wadas/domain/configuration.py b/wadas/domain/configuration.py index 3d74627..bbe395d 100644 --- a/wadas/domain/configuration.py +++ b/wadas/domain/configuration.py @@ -26,6 +26,8 @@ def load_configuration_from_file(file_path): """Method to load configuration from YAML file.""" + valid_ftp_keyring = True + valid_email_keyring = True with open(str(file_path), "r") as file: logging.info("Loading configuration from file...") @@ -38,7 +40,27 @@ def load_configuration_from_file(file_path): for key in notification: if key in Notifier.notifiers: if key == Notifier.NotifierTypes.EMAIL.value: - Notifier.notifiers[key] = EmailNotifier(**notification[key]) + email_notifier = EmailNotifier(**notification[key]) + Notifier.notifiers[key] = email_notifier + credentials = keyring.get_credential("WADAS_email", email_notifier.sender_email) + if not credentials: + logger.error( + "Unable to find email credentials for %s stored on the system." + "Please insert them through email configuration dialog.", + email_notifier.sender_email, + ) + valid_email_keyring = False + elif credentials and credentials.username != email_notifier.sender_email: + logger.error( + "Email username on the system (%s) does not match with username " + "provided in configuration file (%s). Please make sure valid email " + "credentials are in use by editing them from email configuration " + "dialog.", + credentials.username, + email_notifier.sender_email, + ) + valid_email_keyring = False + # FTP Server if FTPsServer.ftps_server and FTPsServer.ftps_server.server: FTPsServer.ftps_server.server.close_all() @@ -70,11 +92,28 @@ def load_configuration_from_file(file_path): os.makedirs(ftp_camera.ftp_folder, exist_ok=True) credentials = keyring.get_credential(f"WADAS_FTP_camera_{ftp_camera.id}", "") if credentials: - FTPsServer.ftps_server.add_user( - credentials.username, - credentials.password, - ftp_camera.ftp_folder, + if credentials.username != ftp_camera.user: + logger.error( + "Keyring stored user (%s) differs from configuration file one (%s)." + " Please make sure to align system stored credential with" + " configuration file. System credentials will be used.", + ftp_camera.user, + credentials.username, + ) + valid_ftp_keyring = False + else: + FTPsServer.ftps_server.add_user( + credentials.username, + credentials.password, + ftp_camera.ftp_folder, + ) + else: + logger.error( + "Unable to find credentials for %s on this system. " + "Please add credentials manually from FTP Camera configuration dialog.", + ftp_camera.id, ) + valid_ftp_keyring = False Camera.detection_params = wadas_config["camera_detection_params"] # FastAPI Actuator Server FastAPIActuatorServer.actuator_server = ( @@ -107,6 +146,7 @@ def load_configuration_from_file(file_path): OperationMode.cur_operation_mode = None logger.info("Configuration loaded from file %s.", file_path) + return valid_ftp_keyring, valid_email_keyring def save_configuration_to_file(file): diff --git a/wadas/domain/ftp_camera.py b/wadas/domain/ftp_camera.py index 7595ca1..8c5e463 100644 --- a/wadas/domain/ftp_camera.py +++ b/wadas/domain/ftp_camera.py @@ -11,12 +11,13 @@ class FTPCamera(Camera): """FTP Camera class, specialization of Camera class.""" - def __init__(self, id, ftp_folder, enabled=True, actuators=None): + def __init__(self, id, ftp_folder, ftp_user, enabled=True, actuators=None): if actuators is None: actuators = [] super().__init__(id, enabled) self.type = Camera.CameraTypes.FTP_CAMERA self.ftp_folder = ftp_folder + self.user = ftp_user self.actuators = actuators def serialize(self): @@ -25,8 +26,9 @@ def serialize(self): return { "type": self.type.value, "id": self.id, - "enabled": self.enabled, "ftp_folder": self.ftp_folder, + "ftp_user": self.user, + "enabled": self.enabled, "actuators": actuators, } @@ -37,4 +39,6 @@ def deserialize(data): actuators = ( [Actuator.actuators[key] for key in data["actuators"]] if "actuators" in data else [] ) - return FTPCamera(data["id"], data["ftp_folder"], data["enabled"], actuators) + return FTPCamera( + data["id"], data["ftp_folder"], data["ftp_user"], data["enabled"], actuators + ) diff --git a/wadas/domain/ftps_server.py b/wadas/domain/ftps_server.py index 9aed654..b1db0fe 100644 --- a/wadas/domain/ftps_server.py +++ b/wadas/domain/ftps_server.py @@ -97,6 +97,10 @@ def has_user(self, username): """Wrapper method of authorizer to check if a user already exists""" return self.authorizer.has_user(username) + def remove_user(self, username): + """Wrapper method of authorizer to remove a user.""" + return self.authorizer.remove_user(username) + def run(self): """Method to create new thread and run a FTPS server.""" self.server = ThreadedFTPServer((self.ip, self.port), self.handler) diff --git a/wadas/ui/configure_email_dialog.py b/wadas/ui/configure_email_dialog.py index a47e2cb..8a50ea4 100644 --- a/wadas/ui/configure_email_dialog.py +++ b/wadas/ui/configure_email_dialog.py @@ -57,7 +57,7 @@ def initialize_form(self): self.ui.lineEdit_smtpServer.setText(self.email_notifier.smtp_hostname) self.ui.lineEdit_port.setText(self.email_notifier.smtp_port) credentials = keyring.get_credential("WADAS_email", self.email_notifier.sender_email) - if credentials.username == self.email_notifier.sender_email: + if credentials and credentials.username == self.email_notifier.sender_email: self.ui.lineEdit_senderEmail.setText(credentials.username) self.ui.lineEdit_password.setText(credentials.password) if recipients_email := self.email_notifier.recipients_email: @@ -82,6 +82,7 @@ def accept_and_close(self): recipients, self.ui.checkBox_email_en.isChecked(), ) + self.add_email_credentials(self.ui.lineEdit_senderEmail.text(), self.ui.lineEdit_password.text()) else: self.email_notifier.enabled = self.ui.checkBox_email_en.isChecked() self.email_notifier.sender_email = self.ui.lineEdit_senderEmail.text() @@ -89,14 +90,32 @@ def accept_and_close(self): self.email_notifier.smtp_port = self.ui.lineEdit_port.text() self.email_notifier.recipients_email = [] self.email_notifier.recipients_email = recipients - keyring.set_password( - "WADAS_email", - self.ui.lineEdit_senderEmail.text(), - self.ui.lineEdit_password.text(), - ) + self.add_email_credentials(self.ui.lineEdit_senderEmail.text(), self.ui.lineEdit_password.text()) + Notifier.notifiers[Notifier.NotifierTypes.EMAIL.value] = self.email_notifier self.accept() + def add_email_credentials(self, username, password): + """Method to add email credentials to system keyring.""" + + # If credentials exist remove them (workaround keyring bug) + credentials = keyring.get_credential( + f"WADAS_email", "" + ) + if credentials: + try: + keyring.delete_password(f"WADAS_email", credentials.username) + except keyring.errors.PasswordDeleteError: + # Credentials not in the system + pass + + # Set new/modified credentials for camera + keyring.set_password( + "WADAS_email", + username, + password, + ) + def validate_email_configurations(self): """Check if inserted email config is valid.""" @@ -192,9 +211,12 @@ def send_email(self): self.ui.lineEdit_port.text(), context=context, ) as smtp_server: - - smtp_server.login(sender, credentials.password) - - for recipient in recipients: - smtp_server.sendmail(sender, recipient, message.as_string()) - self.ui.label_status.setText("Test email(s) sent!") + try: + smtp_server.login(sender, credentials.password) + + for recipient in recipients: + smtp_server.sendmail(sender, recipient, message.as_string()) + self.ui.plainTextEdit_test_log.setPlainText("Test email(s) sent!") + except smtplib.SMTPResponseException as e: + self.ui.label_status.setText(f"Test email(s) Failed!") + self.ui.plainTextEdit_test_log.setPlainText(f"{e.smtp_code}: {e.smtp_error}") \ No newline at end of file diff --git a/wadas/ui/configure_ftp_cameras_dialog.py b/wadas/ui/configure_ftp_cameras_dialog.py index e0331ac..81888dd 100644 --- a/wadas/ui/configure_ftp_cameras_dialog.py +++ b/wadas/ui/configure_ftp_cameras_dialog.py @@ -164,28 +164,37 @@ def accept_and_close(self): f"WADAS_FTP_camera_{camera.id}", "" ) if credentials and ( - credentials.username != cur_user - or credentials.password != cur_pass + credentials.username == cur_user + and credentials.password == cur_pass ): - keyring.set_password( - f"WADAS_FTP_camera_{cur_ui_id}", + break + else: + self.add_camera_credentials(cur_ui_id, cur_user, cur_pass) + # If user existed in ftp server, remove it. + if FTPsServer.ftps_server.has_user(cur_user): + FTPsServer.ftps_server.remove_user(cur_user) + # add modified/missing user. + FTPsServer.ftps_server.add_user( cur_user, cur_pass, + camera.ftp_folder, ) break if not found: # If camera id is not in cameras list, then is new or modified + camera_user = self.get_camera_user(i) + camera_pass = self.get_camera_pass(i) + camera_ftp_path = os.path.join(FTPsServer.ftps_server.ftp_dir, cur_ui_id) camera = FTPCamera( cur_ui_id, - os.path.join(FTPsServer.ftps_server.ftp_dir, cur_ui_id), + camera_ftp_path, + camera_user ) cameras.append(camera) # Store credentials in keyring - keyring.set_password( - f"WADAS_FTP_camera_{cur_ui_id}", - self.get_camera_user(i), - self.get_camera_pass(i), - ) + self.add_camera_credentials(cur_ui_id, camera_user, camera_pass) + # Add FTP Camera to FTP server users list + self.add_camera_to_ftp_server(cur_ui_id, camera_ftp_path, camera_user, camera_pass) # Check for cameras old id (prior to modification) and remove them orphan_cameras = ( @@ -205,25 +214,48 @@ def accept_and_close(self): cur_camera_id = self.get_camera_id(i) if cur_camera_id: cur_cam_ftp_dir = os.path.join(FTPsServer.ftps_server.ftp_dir, cur_camera_id) - camera = FTPCamera(cur_camera_id, cur_cam_ftp_dir) + camera_user = self.get_camera_user(i) + camera_pass = self.get_camera_pass(i) + camera = FTPCamera(cur_camera_id, cur_cam_ftp_dir, camera_user) cameras.append(camera) # Store credentials in keyring - keyring.set_password( - f"WADAS_FTP_camera_{cur_camera_id}", - self.get_camera_user(i), - self.get_camera_pass(i), - ) + self.add_camera_credentials(cur_camera_id, camera_user, camera_pass) # Add camera user to FTPS server - if not FTPsServer.ftps_server.has_user(cur_camera_id): - if not os.path.isdir(cur_cam_ftp_dir): - os.makedirs(cur_cam_ftp_dir, exist_ok=True) - FTPsServer.ftps_server.add_user( - self.get_camera_user(i), - self.get_camera_pass(i), - cur_cam_ftp_dir, - ) + self.add_camera_to_ftp_server(cur_camera_id, cur_cam_ftp_dir, camera_user, camera_pass) self.accept() + def add_camera_credentials(self, camera_id, user, password): + """Method to add camera credentials to keyring.""" + + # If credentials exist remove them (workaround keyring bug) + credentials = keyring.get_credential( + f"WADAS_FTP_camera_{camera_id}", "" + ) + if credentials: + try: + keyring.delete_password(f"WADAS_FTP_camera_{camera_id}", credentials.username) + except keyring.errors.PasswordDeleteError: + # Credentials not in the system + pass + + # Set new/modified credentials for camera + keyring.set_password( + f"WADAS_FTP_camera_{camera_id}", + user, + password, + ) + + def add_camera_to_ftp_server(self, cur_camera_id, cam_ftp_dir, camera_user, camera_pass): + """Method to add FTP camera to FTP server""" + if not FTPsServer.ftps_server.has_user(cur_camera_id): + if not os.path.isdir(cam_ftp_dir): + os.makedirs(cam_ftp_dir, exist_ok=True) + FTPsServer.ftps_server.add_user( + camera_user, + camera_pass, + cam_ftp_dir, + ) + def reject_and_close(self): self._stop_ftp_server() diff --git a/wadas/ui/mainwindow.py b/wadas/ui/mainwindow.py index 107389b..6673019 100644 --- a/wadas/ui/mainwindow.py +++ b/wadas/ui/mainwindow.py @@ -21,7 +21,7 @@ from wadas.domain.actuator import Actuator from wadas.domain.ai_model import AiModel from wadas.domain.animal_detection_mode import AnimalDetectionAndClassificationMode -from wadas.domain.camera import cameras +from wadas.domain.camera import cameras, Camera from wadas.domain.configuration import load_configuration_from_file, save_configuration_to_file from wadas.domain.fastapi_actuator_server import FastAPIActuatorServer from wadas.domain.ftps_server import initialize_fpts_logger @@ -68,6 +68,8 @@ def __init__(self): self.configuration_file_name = "" self.key_ring = None self.ftp_server = None + self.valid_email_keyring = False + self.valid_ftp_keyring = False # Connect Actions self._connect_actions() @@ -268,6 +270,9 @@ def update_toolbar_status(self): if not OperationMode.cur_operation_mode_type: self.ui.actionConfigure_Ai_model.setEnabled(False) self.ui.actionRun.setEnabled(False) + elif OperationMode.cur_operation_mode_type == OperationMode.OperationModeTypes.TestModelMode: + self.ui.actionConfigure_Ai_model.setEnabled(True) + self.ui.actionRun.setEnabled(True) elif ( OperationMode.cur_operation_mode_type == OperationMode.OperationModeTypes.AnimalDetectionMode and not cameras @@ -276,7 +281,11 @@ def update_toolbar_status(self): self.ui.actionRun.setEnabled(False) else: self.ui.actionConfigure_Ai_model.setEnabled(True) - self.ui.actionRun.setEnabled(True) + valid_configuration = True + if ((self.enabled_email_notifier_exists() and not self.valid_email_keyring) or + (self.ftp_camera_exists() and not self.valid_ftp_keyring)): + valid_configuration = False + self.ui.actionRun.setEnabled(valid_configuration) self.ui.actionStop.setEnabled(False) self.ui.actionSave_configuration_as.setEnabled(self.isWindowModified()) self.ui.actionSave_configuration_as_menu.setEnabled(self.isWindowModified()) @@ -287,6 +296,17 @@ def update_toolbar_status(self): self.isWindowModified() and bool(self.configuration_file_name) ) + def ftp_camera_exists(self): + """Method that checks if at least one FTP camera exists in camera list.""" + + return any(camera.type == Camera.CameraTypes.FTP_CAMERA for camera in cameras) + + def enabled_email_notifier_exists(self): + """Method that checks if email notifier is configured.""" + + return any(((Notifier.notifiers[notifier] and Notifier.notifiers[notifier].type == Notifier.NotifierTypes.EMAIL + and Notifier.notifiers[notifier].enabled) for notifier in Notifier.notifiers)) + def update_toolbar_status_on_run(self, running): """Update toolbar status while running model.""" @@ -339,11 +359,11 @@ def configure_email(self): credentials = keyring.get_credential("WADAS_email", "") logger.info("Saved credentials for %s", credentials.username) + self.valid_email_keyring = True self.setWindowModified(True) self.update_toolbar_status() else: logger.debug("Email configuration aborted.") - return def check_notification_enablement(self): """Method to check whether a notification protocol has been set in WADAS. @@ -356,15 +376,16 @@ def check_notification_enablement(self): Notifier.notifiers[notifier] and Notifier.notifiers[notifier].type == Notifier.NotifierTypes.EMAIL ): - credentials = keyring.get_credential("WADAS_email", "") - if notifier and credentials.username: + credentials = keyring.get_credential("WADAS_email", + Notifier.notifiers[notifier].sender_email) + if notifier and credentials and credentials.username and credentials.password: notification_cfg = True if Notifier.notifiers[notifier].enabled: notification_enabled = True message = "" if not notification_cfg: logger.warning("No notification protocol set.") - message = "No notification protocol set. Do you wish to continue anyway?" + message = "No notification protocol properly set. Do you wish to continue anyway?" elif not notification_enabled: logger.warning("No notification protocol enabled.") message = "No enabled notification protocol. Do you wish to continue anyway?" @@ -472,7 +493,7 @@ def load_config_from_file(self): ) if file_name[0]: - load_configuration_from_file(file_name[0]) + self.valid_ftp_keyring, self.valid_email_keyring = load_configuration_from_file(file_name[0]) self.configuration_file_name = file_name[0] self.setWindowModified(False) self.update_toolbar_status() @@ -480,12 +501,35 @@ def load_config_from_file(self): self.update_en_camera_list() self.update_en_actuator_list() + if not self.valid_email_keyring: + reply = QMessageBox.question( + self, + "Invalid email credentials.", + "Would you like to edit email configuration to fix credentials issue?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.Yes: + self.configure_email() + + if not self.valid_ftp_keyring: + reply = QMessageBox.question( + self, + "Invalid FTP camera credentials", + "Would you like to edit FTP camera configuration to fix credentials issue?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.Yes: + self.configure_ftp_cameras() + def configure_ftp_cameras(self): """Method to trigger ftp cameras configuration dialog""" configure_ftp_cameras_dlg = DialogFTPCameras() if configure_ftp_cameras_dlg.exec(): logger.info("FTP Server and Cameras configured.") + self.valid_ftp_keyring = True self.setWindowModified(True) self.update_toolbar_status() self.update_en_camera_list() diff --git a/wadas/ui/qt/configure_email.ui b/wadas/ui/qt/configure_email.ui index 7da24ce..4147536 100644 --- a/wadas/ui/qt/configure_email.ui +++ b/wadas/ui/qt/configure_email.ui @@ -7,7 +7,7 @@ 0 0 522 - 302 + 337 @@ -35,10 +35,23 @@ 10 10 461 - 186 + 196 + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + @@ -46,19 +59,20 @@ - - + + + + QLineEdit::EchoMode::Password + + - - + + - SMTP server + password - - - @@ -66,19 +80,6 @@ - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - @@ -86,35 +87,28 @@ + + + - - - - QLineEdit::EchoMode::Password - - + + - - + + - password + SMTP server - - - - Qt::Orientation::Vertical - - - - 20 - 40 - + + + + true - + @@ -129,7 +123,7 @@ 10 50 481 - 121 + 161 diff --git a/wadas/ui/qt/ui_configure_email.py b/wadas/ui/qt/ui_configure_email.py index 1474ea4..4194738 100644 --- a/wadas/ui/qt/ui_configure_email.py +++ b/wadas/ui/qt/ui_configure_email.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'configure_email.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.8.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -17,14 +17,14 @@ QPalette, QPixmap, QRadialGradient, QTransform) from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, - QPushButton, QSizePolicy, QSpacerItem, QTabWidget, - QTextEdit, QWidget) + QPlainTextEdit, QPushButton, QSizePolicy, QSpacerItem, + QTabWidget, QTextEdit, QWidget) class Ui_DialogConfigureEmail(object): def setupUi(self, DialogConfigureEmail): if not DialogConfigureEmail.objectName(): DialogConfigureEmail.setObjectName(u"DialogConfigureEmail") - DialogConfigureEmail.resize(522, 302) + DialogConfigureEmail.resize(522, 337) sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -38,70 +38,72 @@ def setupUi(self, DialogConfigureEmail): self.tab_3.setObjectName(u"tab_3") self.gridLayoutWidget = QWidget(self.tab_3) self.gridLayoutWidget.setObjectName(u"gridLayoutWidget") - self.gridLayoutWidget.setGeometry(QRect(10, 10, 461, 186)) + self.gridLayoutWidget.setGeometry(QRect(10, 10, 461, 196)) self.gridLayout = QGridLayout(self.gridLayoutWidget) self.gridLayout.setObjectName(u"gridLayout") self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.gridLayout.addItem(self.horizontalSpacer, 3, 4, 1, 1) + self.label_4 = QLabel(self.gridLayoutWidget) self.label_4.setObjectName(u"label_4") self.gridLayout.addWidget(self.label_4, 3, 1, 1, 1) - self.lineEdit_port = QLineEdit(self.gridLayoutWidget) - self.lineEdit_port.setObjectName(u"lineEdit_port") - - self.gridLayout.addWidget(self.lineEdit_port, 3, 3, 1, 1) - - self.label_3 = QLabel(self.gridLayoutWidget) - self.label_3.setObjectName(u"label_3") + self.lineEdit_password = QLineEdit(self.gridLayoutWidget) + self.lineEdit_password.setObjectName(u"lineEdit_password") + self.lineEdit_password.setEchoMode(QLineEdit.EchoMode.Password) - self.gridLayout.addWidget(self.label_3, 2, 1, 1, 1) + self.gridLayout.addWidget(self.lineEdit_password, 1, 3, 1, 2) - self.lineEdit_smtpServer = QLineEdit(self.gridLayoutWidget) - self.lineEdit_smtpServer.setObjectName(u"lineEdit_smtpServer") + self.label_2 = QLabel(self.gridLayoutWidget) + self.label_2.setObjectName(u"label_2") - self.gridLayout.addWidget(self.lineEdit_smtpServer, 2, 3, 1, 2) + self.gridLayout.addWidget(self.label_2, 1, 1, 1, 1) self.label = QLabel(self.gridLayoutWidget) self.label.setObjectName(u"label") self.gridLayout.addWidget(self.label, 0, 1, 1, 1) - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.gridLayout.addItem(self.horizontalSpacer, 3, 4, 1, 1) - self.pushButton_testEmail = QPushButton(self.gridLayoutWidget) self.pushButton_testEmail.setObjectName(u"pushButton_testEmail") self.gridLayout.addWidget(self.pushButton_testEmail, 4, 3, 1, 2) + self.lineEdit_smtpServer = QLineEdit(self.gridLayoutWidget) + self.lineEdit_smtpServer.setObjectName(u"lineEdit_smtpServer") + + self.gridLayout.addWidget(self.lineEdit_smtpServer, 2, 3, 1, 2) + self.lineEdit_senderEmail = QLineEdit(self.gridLayoutWidget) self.lineEdit_senderEmail.setObjectName(u"lineEdit_senderEmail") self.gridLayout.addWidget(self.lineEdit_senderEmail, 0, 3, 1, 2) - self.lineEdit_password = QLineEdit(self.gridLayoutWidget) - self.lineEdit_password.setObjectName(u"lineEdit_password") - self.lineEdit_password.setEchoMode(QLineEdit.EchoMode.Password) + self.lineEdit_port = QLineEdit(self.gridLayoutWidget) + self.lineEdit_port.setObjectName(u"lineEdit_port") - self.gridLayout.addWidget(self.lineEdit_password, 1, 3, 1, 2) + self.gridLayout.addWidget(self.lineEdit_port, 3, 3, 1, 1) - self.label_2 = QLabel(self.gridLayoutWidget) - self.label_2.setObjectName(u"label_2") + self.label_3 = QLabel(self.gridLayoutWidget) + self.label_3.setObjectName(u"label_3") - self.gridLayout.addWidget(self.label_2, 1, 1, 1, 1) + self.gridLayout.addWidget(self.label_3, 2, 1, 1, 1) - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + self.plainTextEdit_test_log = QPlainTextEdit(self.gridLayoutWidget) + self.plainTextEdit_test_log.setObjectName(u"plainTextEdit_test_log") + self.plainTextEdit_test_log.setReadOnly(True) - self.gridLayout.addItem(self.verticalSpacer, 4, 1, 1, 1) + self.gridLayout.addWidget(self.plainTextEdit_test_log, 5, 3, 1, 2) self.tabWidget.addTab(self.tab_3, "") self.tab_4 = QWidget() self.tab_4.setObjectName(u"tab_4") self.textEdit_recipient_email = QTextEdit(self.tab_4) self.textEdit_recipient_email.setObjectName(u"textEdit_recipient_email") - self.textEdit_recipient_email.setGeometry(QRect(10, 50, 481, 121)) + self.textEdit_recipient_email.setGeometry(QRect(10, 50, 481, 161)) self.label_5 = QLabel(self.tab_4) self.label_5.setObjectName(u"label_5") self.label_5.setGeometry(QRect(10, 10, 471, 16)) @@ -147,10 +149,10 @@ def setupUi(self, DialogConfigureEmail): def retranslateUi(self, DialogConfigureEmail): DialogConfigureEmail.setWindowTitle(QCoreApplication.translate("DialogConfigureEmail", u"Email configuration", None)) self.label_4.setText(QCoreApplication.translate("DialogConfigureEmail", u"Port", None)) - self.label_3.setText(QCoreApplication.translate("DialogConfigureEmail", u"SMTP server", None)) + self.label_2.setText(QCoreApplication.translate("DialogConfigureEmail", u"password", None)) self.label.setText(QCoreApplication.translate("DialogConfigureEmail", u"Sender email", None)) self.pushButton_testEmail.setText(QCoreApplication.translate("DialogConfigureEmail", u"Test email", None)) - self.label_2.setText(QCoreApplication.translate("DialogConfigureEmail", u"password", None)) + self.label_3.setText(QCoreApplication.translate("DialogConfigureEmail", u"SMTP server", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QCoreApplication.translate("DialogConfigureEmail", u"Sender", None)) self.label_5.setText(QCoreApplication.translate("DialogConfigureEmail", u"Insert recipients email address(es) separated by comma and space. ", None)) self.label_6.setText(QCoreApplication.translate("DialogConfigureEmail", u"Example: email1@domail.com, email2@domail.com, email3@domail.com", None))