From a2e3b8cdef73dcfb1ddd6d5a61eb4852dcd26d18 Mon Sep 17 00:00:00 2001 From: Wim Gielis Date: Sat, 4 Jan 2025 02:55:48 +0100 Subject: [PATCH] Export to html and email.py Hello, If useful, you can use it fully or partially. I use this approach to quickly look at an entry from my mailbox or from the HTML file (that is generated) on the tablet or phone or hard disk. I know that KeepassXC exists but still this code can be useful. This Python script will export the entries to an HTML file on the hard drive. Optionally, send it to your Gmail email address. Warning: passwords in plain text. Use wisely. --- tests/Export to html and email.py | 357 ++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 tests/Export to html and email.py diff --git a/tests/Export to html and email.py b/tests/Export to html and email.py new file mode 100644 index 00000000..3f82170e --- /dev/null +++ b/tests/Export to html and email.py @@ -0,0 +1,357 @@ +# region: Docstring +''' +Author: Wim Gielis (wim.gielis@gmail.com) +Timing: January 2025 +''' +# endregion + +# region: Imports +import os +import re +import uuid +from datetime import datetime +from pykeepass import PyKeePass +from jinja2 import Template + +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders +# endregion + +# region: Constant values +KEEPASS_DB_PATH = "D:/Paswoorden.kdbx" +KEEPASS_DB_PASSWORD = "test" +OUTPUT_HTML_FILE = "D:/pw.html" +HTML_TEMPLATE_FILE = "D:/template_for_export.html" + +DATE_FORMAT = '%d-%m-%Y' + +# For sending the HTML file by email +SEND_EMAIL = True +RECIPIENT_EMAIL = "your_email_address@gmail.com" +SENDER_EMAIL = "your_email_address@gmail.com" +SENDER_PASSWORD = "your_Google_app_password" +EMAIL_SUBJECT = f"pw ({datetime.now().strftime(DATE_FORMAT)})" +EMAIL_BODY = "" # "Please find attached the KeePass export file." +# endregion + +# region: Main code +def write_to_file(full_file_name, file_contents): + with open(full_file_name, "w", encoding="utf-8") as f: + f.write(file_contents) + +def format_date(date_obj): + """ + Format a datetime object as 'dd-mm-yyyy'. + If the date_obj is None, return 'N/A'. + """ + return date_obj.strftime(DATE_FORMAT) if date_obj else 'N/A' + +def send_email(file_path, recipient_email, sender_email, sender_password, email_subject, email_body): + """ + Send an email with the generated HTML file as an attachment. + + :param file_path: Path to the HTML file to attach. + :param recipient_email: Recipient's email address. + :param sender_email: Sender's Gmail address. + :param sender_password: Sender's Gmail app password. + """ + + # Create a new email + message = MIMEMultipart() + message['From'] = sender_email + message['To'] = recipient_email + message['Subject'] = email_subject + message.attach(MIMEText(email_body, 'plain')) + + # Attach the HTML file + with open(file_path, 'rb') as attachment: + part = MIMEBase('application', 'octet-stream') + part.set_payload(attachment.read()) + encoders.encode_base64(part) + part.add_header('Content-Disposition', f'attachment; filename={os.path.basename(file_path)}') + message.attach(part) + + # Send the email + with smtplib.SMTP('smtp.gmail.com', 587) as server: + server.starttls() + server.login(sender_email, sender_password) + server.send_message(message) + + print(f"Email sent successfully to {recipient_email}") + +def export_keepass_to_html(db_path, password, output_file, template_file): + + try: + # Load the KeePass database + kp = PyKeePass(db_path, password=password) + + def resolve_username(entry): + ''' + Function to derive the username for an entry + ''' + if isinstance(entry.username, str): + if re.match(r'^\{REF:U.*:[0-9A-Z]{32}\}', entry.username): + return entry.deref('username') + else: + return entry.username + elif entry.username is not None: + referenced_entry = kp.find_entries(uuid=entry.username, first=True) + return referenced_entry.username if referenced_entry else "Unresolved Reference" + return "No Username" + + def resolve_password(entry): + ''' + Function to derive the password for an entry + ''' + if isinstance(entry.password, str): + if re.match(r'^\{REF:P.*:[0-9A-Z]{32}\}', entry.password): + return entry.deref('password') + else: + return entry.password + elif entry.password is not None: + referenced_entry = kp.find_entries(uuid=entry.password, first=True) + return referenced_entry.password if referenced_entry else "Unresolved Reference" + return "No Password" + + def extract_group_iterative(root_groups): + ''' + Iterative function to extract groups and their subgroups + ''' + stack = [{"group": group, "parent_id": None} for group in root_groups] + group_data = [] + + while stack: + current = stack.pop() + group = current["group"] + parent_id = current["parent_id"] + + group_info = { + "id": f"group-{uuid.uuid4().hex}", + "name": group.name, + "parent_id": parent_id, + "entries": sorted([ + { + "title": entry.title or "No Title", + "username": resolve_username(entry) or "No Username", + "password": resolve_password(entry) or "No Password", + "url": entry.url or "No URL", + "notes": (entry.notes or "No Notes").replace("\n", "
"), + "custom_properties": entry.custom_properties or {}, + "ctime": format_date(entry.ctime), + "mtime": format_date(entry.mtime), + "expiry_time": format_date(entry.expiry_time), + "expired": entry.expired + } + for entry in group.entries], key=lambda e: e["title"].lower()) + } + + # Add the current group's subgroups to the stack for processing + stack.extend( sorted( + [{"group": subgroup, "parent_id": group_info["id"]} for subgroup in group.subgroups], + key=lambda sg: sg["group"].name.lower() + )) + group_data.append(group_info) + + # Sort all groups by their name (case insensitive) + return sorted(group_data, key=lambda g: g["name"].lower()) + + # Extract all groups iteratively + root_groups = [group for group in kp.groups if group.group is None] + grouped_entries = extract_group_iterative(root_groups) + + # Load the HTML template + with open(template_file, "r", encoding="utf-8") as template_file_obj: + template_content = template_file_obj.read() + template = Template(template_content) + + # Render the HTML and write to a file + html_output = template.render(grouped_entries=grouped_entries) + write_to_file(output_file, html_output) + + print(f"Export completed. HTML saved to: {output_file}") + + except Exception as e: + print(f"Error occurred: {e}") +# endregion + +# region: Call the script +if __name__ == "__main__": + + example_template = """ + + + + + + KeePass export + + + +
+

Table of Contents

+ +
+ +
+ {% for group in grouped_entries %} + {% if not group.parent_id %} +
+

+ {{ group.name }} +

+
+ {% for entry in group.entries %} +
+
+ {% if entry.expired %} + + {% endif %} + {{ entry.title }} +
+
Username: {{ entry.username }}
+
Password: {{ entry.password }}
+
URL: {{ entry.url }}
+
Notes:
{{ entry.notes }}
+ {% if entry.custom_properties %} +
+
Custom properties:
+ {% for key, value in entry.custom_properties.items() %} +
- {{ key }}: {{ value }}
+ {% endfor %} +
+ {% endif %} +
Created: {{ entry.ctime }}
+
Modified: {{ entry.mtime }}
+
Expires: {{ entry.expiry_time }}
+ ⬆ Up +
+ {% endfor %} + {% for subgroup in grouped_entries %} + {% if subgroup.parent_id == group.id %} +
+

+ {{ subgroup.name }} +

+
+ {% for entry in subgroup.entries %} +
+
+ {% if entry.expired %} + + {% endif %} + {{ entry.title }} +
+
Username: {{ entry.username }}
+
Password: {{ entry.password }}
+
URL: {{ entry.url }}
+
Notes:
{{ entry.notes }}
+ {% if entry.custom_properties %} +
+
Custom properties:
+ {% for key, value in entry.custom_properties.items() %} +
- {{ key }}: {{ value }}
+ {% endfor %} +
+ {% endif %} +
Created: {{ entry.ctime }}
+
Modified: {{ entry.mtime }}
+
Expires: {{ entry.expiry_time }}
+ ⬆ Up +
+ {% endfor %} + {% for subsubgroup in grouped_entries %} + {% if subsubgroup.parent_id == subgroup.id %} +
+

+ {{ subsubgroup.name }} +

+
+ {% for entry in subsubgroup.entries %} +
+
+ {% if entry.expired %} + + {% endif %} + {{ entry.title }} +
+
Username: {{ entry.username }}
+
Password: {{ entry.password }}
+
URL: {{ entry.url }}
+
Notes:
{{ entry.notes }}
+ {% if entry.custom_properties %} +
+
Custom properties:
+ {% for key, value in entry.custom_properties.items() %} +
- {{ key }}: {{ value }}
+ {% endfor %} +
+ {% endif %} +
Created: {{ entry.ctime }}
+
Modified: {{ entry.mtime }}
+
Expires: {{ entry.expiry_time }}
+ ⬆ Up +
+ {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+ + + """ + + if not os.path.exists(HTML_TEMPLATE_FILE): + write_to_file(HTML_TEMPLATE_FILE, example_template) + + export_keepass_to_html(KEEPASS_DB_PATH, KEEPASS_DB_PASSWORD, OUTPUT_HTML_FILE, HTML_TEMPLATE_FILE) + if SEND_EMAIL: + send_email(OUTPUT_HTML_FILE, RECIPIENT_EMAIL, SENDER_EMAIL, SENDER_PASSWORD, EMAIL_SUBJECT, EMAIL_BODY) +# endregion \ No newline at end of file