Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Email Backup for a PyQt Password Manager

Tech 1

Python 3 alongside PyQt5 provides robust tools for building desktop utilities. For a credentail vault application, integrating an automated email backup mechanism ensures data redundancy. This implementation utilizes smtplib and email for mail dispatch, alongside sqlite3 for data extraction.

SMTP Configuration and Authentication

To transmit data via email services like QQ or NetEase (163), an authorization token is required instead of a standard password. This token is typically generated within the email provider's settings.

Constructing the email handler involves establishing an SSL connection, authenticating with the token, and assembling the MIME payload:

python import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart

class MailDispatcher: def init(self, host, port, sender, token, recipients, topic): self.smtp_host = host self.smtp_port = port self.sender_addr = sender self.auth_token = token self.recipient_list = recipients self.subject = topic self.payload = MIMEMultipart('mixed')

def authenticate(self):
    self.connection = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port)
    status, _ = self.connection.login(self.sender_addr, self.auth_token)
    if status not in (235, 503):
        raise ConnectionError("SMTP authentication failed")

def build_headers(self, alias="VaultBackup"):
    sender_format = f"{alias} <{self.sender_addr}>"
    self.payload['Subject'] = self.subject
    self.payload['From'] = sender_format
    self.payload['Too'] = ";".join(self.recipient_list)

def attach_text(self, body):
    text_part = MIMEText(body, 'plain', 'utf-8')
    self.payload.attach(text_part)

def attach_file(self, filepath):
    with open(filepath, 'rb') as f:
        file_data = f.read()
    attachment = MIMEText(file_data, 'base64', 'utf-8')
    attachment["Content-Type"] = 'application/octet-stream'
    attachment.add_header('Content-Disposition', 'attachment', filename=filepath.split('/')[-1])
    self.payload.attach(attachment)

def dispatch(self):
    self.connection.sendmail(self.sender_addr, self.recipient_list, self.payload.as_string())
    self.connection.close()

Database Extraction and Backup Execution

The backup procedure queries the SQLite database to fetch all stored credentials, formats them into a readable string, and then invokes the MailDispatcher to send both the text summary and the raw database file as an attachment.

python def execute_backup(self): cursor = self.db_conn.curser() cursor.execute('SELECT * FROM CREDENTIALS') records = cursor.fetchall()

    body_content = ' '.join(['Site', 'User', 'Pass', 'Link']) + '\n'
    for row in records:
        row_data = ' '.join([str(col) for col in row[1:]]) + '\n'
        body_content += row_data

    dispatcher = MailDispatcher(
        host="smtp.qq.com",
        port=465,
        sender="your_email@qq.com",
        token="your_auth_token",
        recipients=["recipient@example.com"],
        topic="Credential Vault Backup"
    )
    dispatcher.authenticate()
    dispatcher.build_headers()
    dispatcher.attach_text(body_content)
    dispatcher.attach_file(self.db_path)
    dispatcher.dispatch()

Complete Application Code

Integrating the mail dispatcher into the main PyQt5 window finalizes the vault application:

python import sys import sqlite3 import os from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QAction, QAbstractItemView, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox)

class CredentialVault(QMainWindow): def init(self): super(CredentialVault, self).init() self.setup_toolbar() self.setup_database() self.setup_table_ui() self.setGeometry(300, 300, 700, 350) self.setWindowTitle('Credential Vault')

def setup_toolbar(self):
    new_action = QAction('New', self)
    edit_action = QAction('Edit', self)
    del_action = QAction('Delete', self)
    backup_action = QAction('Backup', self)

    new_action.setShortcut('Ctrl+N')
    edit_action.setShortcut('Ctrl+E')
    del_action.setShortcut('Delete')
    backup_action.setShortcut('Ctrl+B')

    new_action.triggered.connect(self.add_record)
    edit_action.triggered.connect(self.modify_record)
    del_action.triggered.connect(self.remove_record)
    backup_action.triggered.connect(self.execute_backup)

    toolbar = self.addToolBar('Actions')
    toolbar.addAction(new_action)
    toolbar.addAction(edit_action)
    toolbar.addAction(del_action)
    toolbar.addAction(backup_action)

def setup_database(self):
    home_dir = os.path.expanduser('~')
    vault_dir = os.path.join(home_dir, '.credential_vault')
    if not os.path.exists(vault_dir):
        os.makedirs(vault_dir)

    self.db_path = os.path.join(vault_dir, 'vault.db')
    self.db_conn = sqlite3.connect(self.db_path, isolation_level=None)
    cursor = self.db_conn.cursor()
    cursor.execute('''CREATE TABLE IF NOT EXISTS CREDENTIALS
                      (ID INTEGER PRIMARY KEY AUTOINCREMENT,
                       SITE TEXT, USER TEXT, PASS TEXT, LINK TEXT)''')
    cursor.execute('SELECT * FROM CREDENTIALS')
    self.cached_data = cursor.fetchall()

def setup_table_ui(self):
    self.data_table = QTableWidget()
    self.setCentralWidget(self.data_table)
    self.data_table.setColumnCount(4)
    self.data_table.setHorizontalHeaderLabels(['Site', 'User', 'Pass', 'Link'])
    self.data_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
    self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows)

    for row_idx, row_data in enumerate(self.cached_data):
        self.data_table.insertRow(row_idx)
        for col_idx in range(4):
            cell = QTableWidgetItem(row_data[col_idx + 1])
            self.data_table.setItem(row_idx, col_idx, cell)

def execute_backup(self):
    cursor = self.db_conn.cursor()
    cursor.execute('SELECT * FROM CREDENTIALS')
    records = cursor.fetchall()

    body_content = ' '.join(['Site', 'User', 'Pass', 'Link']) + '\n'
    for row in records:
        row_text = ' '.join([str(col) for col in row[1:]]) + '\n'
        body_content += row_text

    # MailDispatcher instantiation and dispatch logic as defined above
    # dispatcher = MailDispatcher(...)
    # dispatcher.authenticate() ...

def add_record(self):
    result = self.open_entry_dialog()
    if result and result[0]:
        _, site, user, pwd, link = result
        cursor = self.db_conn.cursor()
        cursor.execute("INSERT INTO CREDENTIALS (SITE, USER, PASS, LINK) VALUES (?, ?, ?, ?)",
                       (site, user, pwd, link))
        row_idx = self.data_table.rowCount()
        self.data_table.insertRow(row_idx)
        for col, val in enumerate([site, user, pwd, link]):
            self.data_table.setItem(row_idx, col, QTableWidgetItem(val))

def modify_record(self):
    selected = self.data_table.selectedItems()
    if not selected:
        QMessageBox.warning(self, 'Error', 'No row selected!')
        return

    row_idx = self.data_table.row(selected[0])
    current = [self.data_table.item(row_idx, c).text() for c in range(4)]
    result = self.open_entry_dialog(*current)

    if result and result[0]:
        _, site, user, pwd, link = result
        record_id = self.cached_data[row_idx][0]
        self.db_conn.execute("UPDATE CREDENTIALS SET SITE=?, USER=?, PASS=?, LINK=? WHERE ID=?",
                             (site, user, pwd, link, record_id))
        for col, val in enumerate([site, user, pwd, link]):
            self.data_table.setItem(row_idx, col, QTableWidgetItem(val))

def remove_record(self):
    selected = self.data_table.selectedItems()
    if not selected:
        QMessageBox.warning(self, 'Error', 'No row selected!')
        return

    row_idx = self.data_table.row(selected[0])
    record_id = self.cached_data[row_idx][0]
    self.db_conn.execute("DELETE FROM CREDENTIALS WHERE ID=?", (record_id,))
    self.data_table.removeRow(row_idx)
    del self.cached_data[row_idx]

def open_entry_dialog(self, s='', u='', p='', l=''):
    dialog = QDialog(self)
    layout = QVBoxLayout(dialog)

    inputs = {}
    for label, val in [('Site:', s), ('User:', u), ('Pass:', p), ('Link:', l)]:
        box = QHBoxLayout()
        lbl = QLabel(label)
        inp = QLineEdit(val)
        box.addWidget(lbl)
        box.addWidget(inp)
        layout.addLayout(box)
        inputs[label] = inp

    btn_box = QHBoxLayout()
    ok_btn = QPushButton('OK')
    cn_btn = QPushButton('Cancel')
    btn_box.addWidget(ok_btn)
    btn_box.addWidget(cn_btn)
    layout.addLayout(btn_box)

    ok_btn.clicked.connect(dialog.accept)
    cn_btn.clicked.connect(dialog.reject)

    if dialog.exec_():
        return (True, inputs['Site:'].text(), inputs['User:'].text(),
                inputs['Pass:'].text(), inputs['Link:'].text())
    return (False, None, None, None, None)

if name == 'main': app = QApplication(sys.argv) vault = CredentialVault() vault.show() app.exec_() vault.db_conn.close() sys.exit(0)

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.