#!/usr/bin/env python3 """ GAVEL — Step 2: Convert Emails to PDF ====================================== Converts .eml files (from Step 1) into clean, readable PDFs. Attachments are placed immediately after their email in the output folder. SETUP (run once): Mac: /usr/local/bin/pip3 install reportlab Windows: pip install reportlab USAGE: 1. Run step1_extract_emails.py first 2. Edit the JOBS section below to match your Step 1 output folders 3. Run: python3 step2_convert_to_pdf.py WHAT YOU GET: - One PDF per email, named: 0001_Subject_00_email.pdf - Attachments saved right after: 0001_Subject_att1_filename.pdf - Emails with attachments show "Attachments: filename" in the header - Emails without attachments show nothing extra - All text in black, clean and readable """ import os import email import re import html as htmlmod import shutil import quopri import platform from email import policy # --------------------------------------------------------------- # CONFIGURATION — edit this section before running # Must match the out_dir values from step1_extract_emails.py # --------------------------------------------------------------- JOBS = [ { "label": "Person Name", "eml_dir": "./Person_Name_Emails/", # Folder created by Step 1 "out_dir": "./Person_Name_PDFs/", # Where to save the PDFs }, # Add more people by copying the block above: # { # "label": "Another Person", # "eml_dir": "./Another_Person_Emails/", # "out_dir": "./Another_Person_PDFs/", # }, ] # --------------------------------------------------------------- def check_reportlab(): """Check if reportlab is installed and give clear instructions if not.""" try: import reportlab return True except ImportError: print("\nERROR: The 'reportlab' library is not installed.") print("To install it, open Terminal (Mac) or Command Prompt (Windows) and run:") if platform.system() == 'Windows': print(" pip install reportlab") else: print(" /usr/local/bin/pip3 install reportlab") print("\nThen run this script again.") return False def safe(t): """Escape HTML special characters for PDF rendering.""" return str(t).replace('&', '&').replace('<', '<').replace('>', '>') def decode_payload(part): """Decode email part, handling quoted-printable and different character sets.""" cte = str(part.get('Content-Transfer-Encoding', '')).lower().strip() raw = part.get_payload(decode=True) if raw is None: return '' # Handle quoted-printable encoding (removes = artifacts) if cte == 'quoted-printable' or b'=\n' in raw or b'=3D' in raw: try: raw = quopri.decodestring(raw) except: pass charset = part.get_content_charset() or 'utf-8' try: return raw.decode(charset, 'ignore') except: return raw.decode('utf-8', 'ignore') def clean_html(raw): """Strip HTML tags and return clean readable plain text.""" # Remove style and script blocks entirely raw = re.sub(r'<(style|script)[^>]*>.*?', '', raw, flags=re.DOTALL | re.IGNORECASE) # Replace line-break tags with newlines raw = re.sub(r'<(br|BR)\s*/?>', '\n', raw) raw = re.sub(r'', '\n', raw, flags=re.IGNORECASE) # Strip all remaining HTML tags raw = re.sub(r'<[^>]+>', '', raw) # Decode HTML entities (e.g. & → &) raw = htmlmod.unescape(raw) # Clean up whitespace while preserving paragraph breaks lines = [l.strip() for l in raw.splitlines()] result = [] prev_blank = False for l in lines: if not l: if not prev_blank: result.append('') prev_blank = True else: result.append(l) prev_blank = False return '\n'.join(result).strip() def clean_plain(text): """Remove quoted-printable soft line breaks and encoding artifacts.""" text = re.sub(r'=\r?\n', '', text) # Remove soft line breaks text = re.sub(r'=[0-9A-Fa-f]{2}', '', text) # Remove hex artifacts return text.strip() def eml_to_pdf(eml_path, pdf_path): """Convert a single .eml file to a PDF.""" from reportlab.lib.pagesizes import letter from reportlab.lib.styles import ParagraphStyle from reportlab.lib.colors import black from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer from reportlab.lib.units import inch with open(eml_path, 'rb') as f: msg = email.message_from_binary_file(f, policy=policy.default) # Extract email headers subject = str(msg.get('Subject', 'No Subject')) sender = str(msg.get('From', '')) to = str(msg.get('To', '')) date = str(msg.get('Date', '')) body = '' attachments = [] # Extract body and attachment names if msg.is_multipart(): for part in msg.walk(): ct = part.get_content_type() cd = str(part.get('Content-Disposition', '')) fn = part.get_filename() if fn and 'attachment' in cd: attachments.append(fn) elif ct == 'text/plain' and 'attachment' not in cd and not body: try: body = clean_plain(decode_payload(part)) except: pass # Fall back to HTML if no plain text found if not body: for part in msg.walk(): ct = part.get_content_type() cd = str(part.get('Content-Disposition', '')) if ct == 'text/html' and 'attachment' not in cd: try: body = clean_html(decode_payload(part)) break except: pass else: try: raw = decode_payload(msg) if 'From: {safe(sender)}", header_style), Paragraph(f"To: {safe(to)}", header_style), Paragraph(f"Date: {safe(date)}", header_style), Paragraph(f"Subject: {safe(subject)}", header_style), ] # Only show attachments line if there are actual attachments if attachments: att_list = ', '.join(safe(a) for a in attachments) story.append(Paragraph(f"Attachments: {att_list}", header_style)) story.append(Spacer(1, 0.2 * inch)) # Add email body line by line for line in body.splitlines(): line = line.strip() if line: story.append(Paragraph(safe(line), body_style)) else: story.append(Spacer(1, 0.1 * inch)) doc.build(story) def run_job(label, eml_dir, out_dir): """Convert all EML files in a folder to PDFs.""" eml_dir = os.path.normpath(eml_dir) out_dir = os.path.normpath(out_dir) if not os.path.exists(eml_dir): print(f"\n[{label}] ERROR: EML folder not found at:") print(f" {eml_dir}") print(f" Make sure you ran Step 1 first.") return # Wipe output folder completely to avoid duplicates from previous runs shutil.rmtree(out_dir, ignore_errors=True) os.makedirs(out_dir, exist_ok=True) eml_files = sorted([f for f in os.listdir(eml_dir) if f.endswith('.eml')]) total = len(eml_files) if total == 0: print(f"\n[{label}] No .eml files found in {eml_dir}") print(f" Make sure Step 1 completed successfully.") return print(f"\n[{label}] Converting {total} emails to PDF...") ok = bad = 0 for fname in eml_files: try: base = fname.replace('.eml', '') eml_path = os.path.join(eml_dir, fname) pdf_path = os.path.join(out_dir, base + '_00_email.pdf') # Convert email to PDF eml_to_pdf(eml_path, pdf_path) # Copy attachments immediately after their email, in sorted order for att in sorted(os.listdir(eml_dir)): if att.startswith(base + '_att') and not att.endswith('.eml'): shutil.copy2( os.path.join(eml_dir, att), os.path.join(out_dir, att) ) ok += 1 if ok % 25 == 0: print(f" {ok}/{total} done...") except Exception as e: bad += 1 print(f" Warning: Could not convert {fname} — {e}") print(f"[{label}] Done! {ok} PDFs created, {bad} errors") print(f"[{label}] Output folder: {os.path.abspath(out_dir)}") # --------------------------------------------------------------- # MAIN # --------------------------------------------------------------- if __name__ == '__main__': print("=" * 50) print("GAVEL — Step 2: Convert Emails to PDF") print(f"Running on: {platform.system()} {platform.release()}") print("=" * 50) if not check_reportlab(): exit(1) for job in JOBS: run_job(job['label'], job['eml_dir'], job['out_dir']) print("\nAll jobs complete.") print("Check your output folders for the PDFs.")