Copy sent mail to Sent folder; add message field to Poke webhook
CI / lint (push) Failing after 5s
CI / build (push) Successful in 5s
CI / docker (push) Skipped
Build & Push Container Image / build-and-push (push) Successful in 30s

- send_email only relayed via SMTP, which doesn't copy the message
  to Sent the way a provider's own webmail/Mail app does — that's a
  client-side IMAP APPEND step that was simply missing, so sent mail
  never showed up anywhere in the account. Added detect_sent_folder
  (mirrors detect_drafts_folder) and append the sent copy after a
  successful SMTP send.
- forward_to_poke's payload never included a top-level "message"
  field, which every documented /api/v1/inbound/api-message example
  uses to give the agent something actionable — without it the
  webhook could get accepted (200/success) but ingested as inert
  context with nothing to surface. Added _build_poke_message() to
  generate one from the email's from/subject/body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 01:56:58 -04:00
co-authored by Claude Sonnet 5
parent 3e816beb6e
commit 64d795173c
+58
View File
@@ -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)