support also saving inline attachments

This commit is contained in:
Cyberes 2024-07-18 19:21:05 -06:00
parent 953351e8a8
commit 83dc4405bf
5 changed files with 29 additions and 14 deletions

View File

@ -30,7 +30,6 @@ Does not support different accounts.
``` ```
4. Edit `/etc/secrets/imapviewer` with this content: 4. Edit `/etc/secrets/imapviewer` with this content:
```shell ```shell
SQLITE3_DB=<path to your sqlite3 database>
HTTP_AUTH_PASS=<your encoded password> HTTP_AUTH_PASS=<your encoded password>
EMAIL_ARCHIVE_ROOT=<archive root path> EMAIL_ARCHIVE_ROOT=<archive root path>
``` ```

View File

@ -26,11 +26,15 @@ def main(args):
logger.critical('Bad config file.') logger.critical('Bad config file.')
sys.exit(1) 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 = Path(config['attachments_path'])
attachments_dir.mkdir(parents=True, exist_ok=True) attachments_dir.mkdir(parents=True, exist_ok=True)
database = EmailDatabase(Path(config['database_path'])) 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() mail.load_folders()
if config['server'] == 'imap.gmail.com': if config['server'] == 'imap.gmail.com':

View File

@ -5,6 +5,10 @@ password: password123
database_path: emails.db database_path: emails.db
attachments_path: attachments 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: exclude_folders:
- Trash - Trash
- Drafts - Drafts

View File

@ -33,10 +33,11 @@ class FileAttachmentEncoder(JSONEncoder):
class MailConnection: 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 = imaplib.IMAP4_SSL(host)
self.mail.login(username, password) self.mail.login(username, password)
self.attachments_dir = attachments_dir.expanduser().absolute().resolve() self.attachments_dir = attachments_dir.expanduser().absolute().resolve()
self.save_inline_attachments = save_inline_attachments
self.folder_structure = {} self.folder_structure = {}
self.logger = logging.getLogger('iarchiver.mail_conn') self.logger = logging.getLogger('iarchiver.mail_conn')
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
@ -102,7 +103,7 @@ class MailConnection:
if email_message.is_multipart(): if email_message.is_multipart():
for part in email_message.walk(): for part in email_message.walk():
content_disposition = str(part.get("Content-Disposition")) 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() filename = part.get_filename()
if filename: if filename:
# The filename of the file is the hash of its content, which should de-duplicate files. # 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) attachments.append(file_obj)
raw_email_clean = email_message.as_string() raw_email_clean = email_message.as_string()
return unix_timestamp, to_header, from_header, subject, raw_email_clean, attachments return unix_timestamp, to_header, from_header, subject, raw_email_clean, attachments
except Exception as e: except:
self.logger.critical(traceback.format_exc()) self.logger.critical(traceback.format_exc())
return return

View File

@ -6,13 +6,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
import magic 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 from flask_httpauth import HTTPBasicAuth
app = Flask(__name__) app = Flask(__name__)
if not os.environ.get('SQLITE3_DB'):
raise Exception('SQLITE3_DB not set.')
if not os.environ.get('HTTP_AUTH_PASS'): if not os.environ.get('HTTP_AUTH_PASS'):
raise Exception('HTTP_AUTH_PASS not set.') raise Exception('HTTP_AUTH_PASS not set.')
if not os.environ.get('EMAIL_ARCHIVE_ROOT'): if not os.environ.get('EMAIL_ARCHIVE_ROOT'):
@ -50,6 +48,8 @@ def index():
@auth.login_required @auth.login_required
def account(email_account): def account(email_account):
conn = get_db_connection(email_account) conn = get_db_connection(email_account)
if not conn:
abort(404)
folders = conn.execute('SELECT name, table_name FROM folders_mapping').fetchall() folders = conn.execute('SELECT name, table_name FROM folders_mapping').fetchall()
syncs = conn.execute('SELECT * FROM syncs ORDER BY timestamp DESC').fetchall() syncs = conn.execute('SELECT * FROM syncs ORDER BY timestamp DESC').fetchall()
conn.close() conn.close()
@ -63,11 +63,13 @@ def account(email_account):
@auth.login_required @auth.login_required
def folder(email_account, table_name): def folder(email_account, table_name):
conn = get_db_connection(email_account) conn = get_db_connection(email_account)
if not conn:
abort(404)
emails = conn.execute(f'SELECT * FROM {table_name} ORDER BY timestamp DESC').fetchall() emails = conn.execute(f'SELECT * FROM {table_name} ORDER BY timestamp DESC').fetchall()
conn.close() conn.close()
emails = [dict_from_row(email) for email in emails] emails = [dict_from_row(email) for email in emails]
for email in emails: for item in emails:
email['timestamp'] = datetime.fromtimestamp(email['timestamp']).strftime('%Y-%m-%d %H:%M:%S') 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) 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 @auth.login_required
def email(email_account, table_name, id): def email(email_account, table_name, id):
conn = get_db_connection(email_account) 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()) email = dict_from_row(conn.execute(f'SELECT * FROM {table_name} WHERE id = ?', (id,)).fetchone())
conn.close() conn.close()
email['timestamp'] = datetime.fromtimestamp(email['timestamp']).strftime('%Y-%m-%d %H:%M:%S') 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): def download_file(email_account, filename):
p = Path(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'attachments', filename) p = Path(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'attachments', filename)
if not p.exists(): if not p.exists():
return 404 abort(404)
mimetype = magic.from_file(str(p), mime=True) 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) return send_from_directory(Path(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'attachments'), filename, mimetype=mimetype)
def get_db_connection(email_account): def get_db_connection(email_account):
conn = sqlite3.connect(os.path.join(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'emails.db')) try:
conn.row_factory = sqlite3.Row conn = sqlite3.connect(os.path.join(os.environ.get('EMAIL_ARCHIVE_ROOT'), email_account, 'emails.db'))
return conn conn.row_factory = sqlite3.Row
return conn
except:
return None
if __name__ == '__main__': if __name__ == '__main__':