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