feat(email): send attachments as WhatsApp documents, fix forward sender

- Add /send-document endpoint to WhatsApp bridge (base64 document send)
- save_email_as_note() now saves attachment files to disk alongside note
- email_digest: extract original sender for Fwd: emails so header shows
  the real author, not the forwarder; send attachment files after summary
- email_forward: send attachment files as documents after text parts
- Add extract_original_sender() and save_email_attachment_files() helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 07:50:40 +00:00
parent 417de65069
commit 51af0918a4
4 changed files with 145 additions and 11 deletions

View File

@@ -93,6 +93,46 @@ def get_email_attachments(msg) -> list:
attachments.append(f"[{part.get_content_type()}]")
return attachments
def save_email_attachment_files(msg, dest_dir: Path) -> list:
"""Save attachment files from email to dest_dir. Returns list of saved file paths."""
saved = []
if not msg.is_multipart():
return saved
dest_dir.mkdir(parents=True, exist_ok=True)
for part in msg.walk():
filename = part.get_filename()
if not filename:
continue
filename = decode_mime_header(filename)
payload = part.get_payload(decode=True)
if payload is None:
continue
dest = dest_dir / filename
# Avoid overwriting — append counter if needed
counter = 1
while dest.exists():
stem, suffix = Path(filename).stem, Path(filename).suffix
dest = dest_dir / f"{stem}_{counter}{suffix}"
counter += 1
dest.write_bytes(payload)
saved.append(dest)
return saved
def extract_original_sender(subject: str, body_content: str, from_full: str) -> str:
"""If email is a forward, extract original sender from body."""
if not re.match(r'^(fwd?|fw)\s*[:\s]', subject, re.IGNORECASE):
return from_full
match = re.search(
r'(?:De la|From):\s*(.+?)(?:\n|$)',
body_content, re.IGNORECASE | re.MULTILINE
)
if match:
candidate = match.group(1).strip()
# Skip blank or markdown artifacts
if candidate and not candidate.startswith('**') and '@' in candidate or len(candidate) > 3:
return candidate
return from_full
def extract_sender_email(from_header: str) -> str:
"""Extract just the email address from From header"""
match = re.search(r'<([^>]+)>', from_header)
@@ -204,11 +244,15 @@ def save_email_as_note(eid: str) -> dict:
KB_PATH.mkdir(parents=True, exist_ok=True)
filepath.write_text(content, encoding='utf-8')
# Save attachment files next to the note
att_dir = KB_PATH / f"{date_prefix}_{slug}_attachments"
attachment_paths = save_email_attachment_files(msg, att_dir)
# Mark as seen
mail.store(eid.encode(), '+FLAGS', '\\Seen')
mail.logout()
return {
'ok': True,
'file': str(filepath),
@@ -216,6 +260,7 @@ def save_email_as_note(eid: str) -> dict:
'from': sender_email,
'from_full': from_addr,
'date': date_str,
'attachment_paths': [str(p) for p in attachment_paths],
}
def save_unread_emails():