diff --git a/app.py b/app.py index 67ab777..97996f2 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ import os import json from pathlib import Path from datetime import datetime -from flask import Flask, render_template, request, jsonify, send_file, session, redirect, url_for +from flask import Flask, render_template, request, jsonify, send_file, session, redirect, url_for, make_response from werkzeug.security import generate_password_hash, check_password_hash import markdown import logging @@ -238,6 +238,45 @@ def rewrite_image_paths(md_content, filepath): md_content = re.sub(r'!\[([^\]]*)\]\(([^\)]+)\)', replace_image_path, md_content) return md_content +def rewrite_eml_file_links(md_content, filepath): + """Rewrite relative .eml file links to be served by Flask's /file/ route.""" + # Get the directory of the current file + file_dir = os.path.dirname(filepath) + + # Pattern to match markdown link syntax: [text](path) + def replace_link_path(match): + link_text = match.group(1) + link_path = match.group(2) + + # Only process .eml files + if not link_path.lower().endswith('.eml'): + return match.group(0) + + # Skip absolute URLs + if link_path.startswith(('http://', 'https://', '/file/')): + return match.group(0) + + # Resolve relative paths + if link_path.startswith('/'): + # Absolute path from content root + resolved_path = link_path.lstrip('/') + else: + # Relative path - resolve it relative to the file's directory + if file_dir: + resolved_path = os.path.normpath(os.path.join(file_dir, link_path)) + else: + resolved_path = link_path + + # Ensure forward slashes for URL + resolved_path = resolved_path.replace(os.sep, '/') + file_url = f"/file/{resolved_path}" + + return f"[{link_text}]({file_url})" + + # Replace all markdown link references to .eml files + md_content = re.sub(r'\[([^\]]+)\]\(([^\)]+\.eml)\)', replace_link_path, md_content) + return md_content + def build_directory_tree(): """Build a hierarchical tree of markdown files organized by directory.""" tree = {} @@ -370,6 +409,60 @@ def serve_image(filepath): logger.error(f"Error serving image {file_path}: {e}") return f"Error serving image: {e}", 500 +@app.route('/file/') +def serve_file(filepath): + """Serve files from content directory, with base64 decoding for .eml files if needed.""" + # Security: prevent directory traversal + if '..' in filepath or filepath.startswith('/'): + return "Invalid path", 400 + + file_path = os.path.join(CONTENT_DIR, filepath) + + # Ensure the file is within the content directory + try: + file_path = os.path.realpath(file_path) + content_path = os.path.realpath(CONTENT_DIR) + if not file_path.startswith(content_path): + return "Access denied", 403 + except Exception: + return "Invalid path", 400 + + if not os.path.exists(file_path): + logger.warning(f"File not found: {file_path}") + return "File not found", 404 + + try: + # Check if this is an .eml file + if file_path.lower().endswith('.eml'): + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + # Check if content is base64 encoded + # Base64 content is ASCII and contains only base64 characters + is_base64 = False + try: + if content.strip() and all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r' for c in content): + decoded = base64.b64decode(content, validate=True).decode('utf-8', errors='ignore') + # If it looks like an email (has headers), use decoded version + if 'From:' in decoded or 'To:' in decoded or 'Subject:' in decoded: + content = decoded + is_base64 = True + except Exception: + # Not valid base64, use original content + pass + + # Return as text/plain for viewing in browser + response = make_response(content) + response.headers['Content-Type'] = 'text/plain; charset=utf-8' + response.headers['Content-Disposition'] = f'inline; filename="{os.path.basename(file_path)}"' + return response + else: + # For other file types, just serve as-is + return send_file(file_path) + except Exception as e: + logger.error(f"Error serving file {file_path}: {e}") + return f"Error serving file: {e}", 500 + @app.route('/api/search', methods=['GET']) def search(): """Search markdown files.""" @@ -462,6 +555,9 @@ def view_file(filepath): # Rewrite image paths to be served by Flask md_content = rewrite_image_paths(md_content, filepath) + # Rewrite .eml file links to be served by Flask with base64 decoding + md_content = rewrite_eml_file_links(md_content, filepath) + # Normalize list indentation for proper markdown parsing md_content = normalize_list_indentation(md_content)