import sqlite3 from email.message import EmailMessage def test_attachment_filename_is_part_of_ui_index_search(tmp_path, monkeypatch): from routes import email_routes db_path = tmp_path / "mail.db" conn = sqlite3.connect(db_path) conn.execute( """CREATE TABLE email_message_index ( owner TEXT, account_key TEXT, folder TEXT, uid TEXT, message_id TEXT, subject TEXT, from_name TEXT, from_address TEXT, to_text TEXT, cc_text TEXT, date_iso TEXT, date_display TEXT, date_epoch REAL, size INTEGER, flags TEXT, has_attachments INTEGER, attachment_names TEXT, updated_at TEXT)""" ) conn.execute( "INSERT INTO email_message_index VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ("felix", "acct", "INBOX", "7", "<7@example>", "Quarterly files", "A", "a@example.com", "felix@example.com", "", "", "", 1, 10, "", 1, "PDPUK-tax-return.pdf", "now"), ) conn.commit() conn.close() monkeypatch.setattr(email_routes, "SCHEDULED_DB", db_path) rows, total, _ = email_routes._email_index_search( "felix", "acct", "INBOX", "PDPUK-tax", 20, global_search=False ) assert total == 1 assert rows[0]["uid"] == "7" def test_signature_only_message_has_no_visible_attachments(): from routes.email_helpers import _has_visible_attachments msg = EmailMessage() msg.set_content("Regards") msg.add_related( b"tiny-logo", maintype="image", subtype="png", cid="logo", filename="signature.png", disposition="inline", ) assert _has_visible_attachments(msg) is False def test_header_only_related_message_is_not_marked_as_attached(): from routes.email_routes import _parse_email_list_record raw = ( b"Content-Type: multipart/related; boundary=abc\r\n" b"Subject: Signature only\r\nFrom: a@example.com\r\n\r\n" ) parsed = _parse_email_list_record(b"1 (UID 9 FLAGS () RFC822.SIZE 123)", raw) assert parsed["has_attachments"] is False def test_real_attachment_remains_visible_alongside_signature(): from routes.email_helpers import _has_visible_attachments msg = EmailMessage() msg.set_content("See attached") msg.add_attachment(b"report", maintype="application", subtype="pdf", filename="report.pdf") msg.add_attachment(b"tiny-logo", maintype="image", subtype="png", filename="logo.png") assert _has_visible_attachments(msg) is True def test_remote_search_explicitly_checks_mime_filename_headers(): from routes.email_routes import _email_imap_search_criteria criteria = _email_imap_search_criteria("report.pdf") assert "HEADER Content-Disposition" in criteria assert "HEADER Content-Type" in criteria def test_forwarding_filters_signature_assets_and_mobile_export_stops_bubbling(): inbox = open("static/js/emailInbox.js", encoding="utf-8").read() document = open("static/js/document.js", encoding="utf-8").read() assert "const forwardedAttachments = mode === 'forward'" in inbox assert "forwardedAttachments.map" in inbox assert ": [];" in inbox[inbox.index("const forwardedAttachments"):inbox.index("content += '\\n---\\n'", inbox.index("const forwardedAttachments"))] assert "showExportMenu(e, e.currentTarget.getBoundingClientRect())" in document def test_attachment_open_spins_icon_only(): library = open("static/js/emailLibrary.js", encoding="utf-8").read() start = library.index("reader.querySelectorAll('.email-attachment-open')") end = library.index("reader.querySelectorAll('.email-attachment-download')", start) handler = library[start:end] assert "openBtn.appendChild(wp.element)" in handler assert "email-attachment-open-label" not in handler def test_move_document_creates_destination_before_adopting_it(): document = open("static/js/document.js", encoding="utf-8").read() start = document.index("async function moveActiveDocumentToNewChat()") end = document.index("\n function showDocTabMenu", start) handler = document[start:end] assert "_autoCreateSession({ adopt: false, forceNew: true })" in handler assert "await _moveDocToSession(activeDocId, sessionId, { switchChat: true })" in handler def test_deferred_attachment_check_shows_feedback_and_repairs_stale_card_icon(): library = open("static/js/emailLibrary.js", encoding="utf-8").read() start = library.index("function _loadDeferredAttachmentsIntoReader") end = library.index('\n// "Open in new tab"', start) loader = library[start:end] assert "Loading attachments…" in loader assert "matchingEmail.has_attachments = visibleCurrent.length > 0" in loader assert "querySelector('.email-card-attachment')?.remove()" in loader def test_attachment_cache_backfill_preserves_message_id(tmp_path, monkeypatch): from routes import email_routes db_path = tmp_path / "mail.db" conn = sqlite3.connect(db_path) conn.execute( """CREATE TABLE email_attachment_metadata_cache ( owner TEXT, account_key TEXT, folder TEXT, uid TEXT, message_id TEXT, attachments_json TEXT, updated_at TEXT, PRIMARY KEY (owner, account_key, folder, uid))""" ) conn.execute( """CREATE TABLE email_message_index ( owner TEXT, account_key TEXT, folder TEXT, uid TEXT, has_attachments INTEGER, attachment_names TEXT, updated_at TEXT, PRIMARY KEY (owner, account_key, folder, uid))""" ) conn.execute( "INSERT INTO email_message_index VALUES (?,?,?,?,?,?,?)", ("felix", "acct", "INBOX", "7", 1, "old.png", "before"), ) conn.commit() conn.close() monkeypatch.setattr(email_routes, "SCHEDULED_DB", db_path) signature = [{"filename": "signature.png", "size": 1000}] email_routes._email_attachment_meta_cache_put( "felix", "acct", "INBOX", "7", "", signature ) email_routes._email_attachment_meta_cache_put( "felix", "acct", "INBOX", "7", "", signature ) conn = sqlite3.connect(db_path) assert conn.execute("SELECT message_id FROM email_attachment_metadata_cache").fetchone()[0] == "" assert conn.execute("SELECT has_attachments FROM email_message_index").fetchone()[0] == 0 conn.close() def test_single_email_tag_has_no_more_control(): library = open("static/js/emailLibrary.js", encoding="utf-8").read() assert 'class="email-tags-more email-tags-more-single"' not in library def test_email_folder_and_filter_pickers_treat_their_buttons_as_inside_clicks(): library = open("static/js/emailLibrary.js", encoding="utf-8").read() assert library.count( "bindMenuDismiss(menu, finishClose, e => !picker.contains(e.target))" ) == 2 assert "bindMenuDismiss(menu, finishClose, e => picker.contains(e.target))" not in library def test_empty_reply_has_two_editable_rows_and_reply_survives_compact_toolbar(): inbox = open("static/js/emailInbox.js", encoding="utf-8").read() library = open("static/js/emailLibrary.js", encoding="utf-8").read() assert "



\\n" in inbox fit_start = library.index("function _fitReaderActions") fit_end = library.index("const _readerActionFitObserver", fit_start) fit = library[fit_start:fit_end] assert '[data-act="ai-reply"], [data-act="reply-all"]' in fit assert '[data-act="reply"]' not in fit def test_email_toolbar_places_attachment_before_link(): document = open("static/js/document.js", encoding="utf-8").read() toolbar_start = document.index('
', toolbar_start) toolbar = document[toolbar_start:toolbar_end] assert toolbar.index('id="md-toolbar-attach-btn"') < toolbar.index('data-md="link"')