diff --git a/src/server.py b/src/server.py index 2559ea3..a2cd73c 100644 --- a/src/server.py +++ b/src/server.py @@ -405,15 +405,53 @@ def detect_drafts_folder(client: IMAPClient) -> str: return "Drafts" +def detect_sent_folder(client: IMAPClient) -> str: + folders = client.list_folders() + for flags, _delim, name in folders: + if b"\\Sent" in flags: + return name + for name in ("Sent", "Sent Messages", "Sent Items", "[Gmail]/Sent Mail", "INBOX.Sent"): + if client.folder_exists(name): + return name + return "Sent" + + # --------------------------------------------------------------------------- # Poke webhook # --------------------------------------------------------------------------- +_BODY_PREVIEW_LIMIT = 2000 + + +def _build_poke_message(email_data: dict, account: dict) -> str: + """Build the natural-language 'message' Poke's api-message endpoint expects. + + Every documented usage example for /api/v1/inbound/api-message wraps its + payload in a top-level 'message' string — the endpoint accepts "any JSON + object", but without 'message' there's nothing for Poke's agent to treat + as an actionable instruction, so it can ingest the payload as inert + context without ever surfacing a heads-up. + """ + preview = (email_data.get("body_text") or "").strip() + if not preview and email_data.get("body_html"): + preview = "(HTML-only email — see body_html for content)" + if len(preview) > _BODY_PREVIEW_LIMIT: + preview = preview[:_BODY_PREVIEW_LIMIT] + "... (truncated, see body_text for full content)" + + return ( + f"New email received on {account['from_address']}\n" + f"From: {email_data['from']}\n" + f"Subject: {email_data['subject'] or '(no subject)'}\n\n" + f"{preview}" + ) + + async def forward_to_poke( email_data: dict, account: dict, webhook_url: str, api_key: str ) -> bool: payload = { + "message": _build_poke_message(email_data, account), "account_id": account["id"], "from_address": account["from_address"], "from": email_data["from"], @@ -1012,6 +1050,26 @@ async def send_email( smtp.login(acc["smtp_username"], acc["smtp_password"]) smtp.sendmail(acc["smtp_username"], recipients, msg.as_string()) + # SMTP relay doesn't copy the message to Sent the way a provider's + # own webmail/Mail app does — that's a client-side step over IMAP, + # so we have to do it ourselves or the message is never visible + # anywhere in the account after sending. + try: + imap = get_imap_client(acc) + try: + sent_folder = detect_sent_folder(imap) + if not imap.folder_exists(sent_folder): + imap.create_folder(sent_folder) + imap.append(sent_folder, msg.as_bytes(), flags=[b"\\Seen"]) + finally: + imap.logout() + except Exception as e: + logger.warning( + "Sent via SMTP but failed to save copy to Sent folder for '%s': %s", + acc["id"], + e, + ) + return {"success": True, "message_id": msg.get("Message-ID", "")} return await asyncio.to_thread(_send)