From 83dc4405bf4c4efddc18815c8cd700f1cb4bad58 Mon Sep 17 00:00:00 2001 From: Cyberes Date: Thu, 18 Jul 2024 19:21:05 -0600 Subject: [PATCH] support also saving inline attachments --- README.md | 1 - archiver.py | 6 +++++- config.yml.sample | 4 ++++ iarchiver/mail_conn.py | 7 ++++--- server.py | 25 ++++++++++++++++--------- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2f59066..6ac57ec 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ Does not support different accounts. ``` 4. Edit `/etc/secrets/imapviewer` with this content: ```shell - SQLITE3_DB= HTTP_AUTH_PASS= EMAIL_ARCHIVE_ROOT= ``` diff --git a/archiver.py b/archiver.py index 5763f17..784d699 100755 --- a/archiver.py +++ b/archiver.py @@ -26,11 +26,15 @@ def main(args): logger.critical('Bad config file.') sys.exit(1) + save_inline_attachments = config.get('save_inline_attachments', False) + if save_inline_attachments: + logger.info('Saving inline attachments as well') + attachments_dir = Path(config['attachments_path']) attachments_dir.mkdir(parents=True, exist_ok=True) database = EmailDatabase(Path(config['database_path'])) - mail = MailConnection(config['server'], config['username'], config['password'], attachments_dir) + mail = MailConnection(config['server'], config['username'], config['password'], attachments_dir, save_inline_attachments=save_inline_attachments) mail.load_folders() if config['server'] == 'imap.gmail.com': diff --git a/config.yml.sample b/config.yml.sample index be67f61..ea1a6e4 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -5,6 +5,10 @@ password: password123 database_path: emails.db attachments_path: attachments +# Should inline content be saved as well? +This will result in images and other inline content being saved, not just attachments. +save_inline_attachments: false + exclude_folders: - Trash - Drafts diff --git a/iarchiver/mail_conn.py b/iarchiver/mail_conn.py index 3a1113a..3acba1d 100644 --- a/iarchiver/mail_conn.py +++ b/iarchiver/mail_conn.py @@ -33,10 +33,11 @@ class FileAttachmentEncoder(JSONEncoder): class MailConnection: - def __init__(self, host: str, username: str, password: str, attachments_dir: Path): + def __init__(self, host: str, username: str, password: str, attachments_dir: Path, save_inline_attachments: bool = False): self.mail = imaplib.IMAP4_SSL(host) self.mail.login(username, password) self.attachments_dir = attachments_dir.expanduser().absolute().resolve() + self.save_inline_attachments = save_inline_attachments self.folder_structure = {} self.logger = logging.getLogger('iarchiver.mail_conn') self.logger.setLevel(logging.INFO) @@ -102,7 +103,7 @@ class MailConnection: if email_message.is_multipart(): for part in email_message.walk(): content_disposition = str(part.get("Content-Disposition")) - if "attachment" in content_disposition: + if 'attachment' in content_disposition or (self.save_inline_attachments and 'inline' in content_disposition): filename = part.get_filename() if filename: # The filename of the file is the hash of its content, which should de-duplicate files. @@ -119,7 +120,7 @@ class MailConnection: attachments.append(file_obj) raw_email_clean = email_message.as_string() return unix_timestamp, to_header, from_header, subject, raw_email_clean, attachments - except Exception as e: + except: self.logger.critical(traceback.format_exc()) return diff --git a/server.py b/server.py index 0f5950e..d135fdd 100644 --- a/server.py +++ b/server.py @@ -6,13 +6,11 @@ from datetime import datetime from pathlib import Path import magic -from flask import Flask, render_template, send_from_directory +from flask import Flask, render_template, send_from_directory, abort from flask_httpauth import HTTPBasicAuth app = Flask(__name__) -if not os.environ.get('SQLITE3_DB'): - raise Exception('SQLITE3_DB not set.') if not os.environ.get('HTTP_AUTH_PASS'): raise Exception('HTTP_AUTH_PASS not set.') if not os.environ.get('EMAIL_ARCHIVE_ROOT'): @@ -50,6 +48,8 @@ def index(): @auth.login_required def account(email_account): conn = get_db_connection(email_account) + if not conn: + abort(404) folders = conn.execute('SELECT name, table_name FROM folders_mapping').fetchall() syncs = conn.execute('SELECT * FROM syncs ORDER BY timestamp DESC').fetchall() conn.close() @@ -63,11 +63,13 @@ def account(email_account): @auth.login_required def folder(email_account, table_name): conn = get_db_connection(email_account) + if not conn: + abort(404) emails = conn.execute(f'SELECT * FROM {table_name} ORDER BY timestamp DESC').fetchall() conn.close() emails = [dict_from_row(email) for email in emails] - for email in emails: - email['timestamp'] = datetime.fromtimestamp(email['timestamp']).strftime('%Y-%m-%d %H:%M:%S') + for item in emails: + item['timestamp'] = datetime.fromtimestamp(item['timestamp']).strftime('%Y-%m-%d %H:%M:%S') return render_template('folder.html', emails=emails, table_name=table_name, email_account=email_account) @@ -75,6 +77,8 @@ def folder(email_account, table_name): @auth.login_required def email(email_account, table_name, id): conn = get_db_connection(email_account) + if not conn: + abort(404) email = dict_from_row(conn.execute(f'SELECT * FROM {table_name} WHERE id = ?', (id,)).fetchone()) conn.close() email['timestamp'] = datetime.fromtimestamp(email['timestamp']).strftime('%Y-%m-%d %H:%M:%S') @@ -87,15 +91,18 @@ def email(email_account, table_name, id): def download_file(email_account, filename): p = Path(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'attachments', filename) if not p.exists(): - return 404 + abort(404) mimetype = magic.from_file(str(p), mime=True) return send_from_directory(Path(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'attachments'), filename, mimetype=mimetype) def get_db_connection(email_account): - conn = sqlite3.connect(os.path.join(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'emails.db')) - conn.row_factory = sqlite3.Row - return conn + try: + conn = sqlite3.connect(os.path.join(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'emails.db')) + conn.row_factory = sqlite3.Row + return conn + except: + return None if __name__ == '__main__':