Copy sent mail to Sent folder; add message field to Poke webhook
- 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:
@@ -405,15 +405,53 @@ def detect_drafts_folder(client: IMAPClient) -> str:
|
|||||||
return "Drafts"
|
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
|
# 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(
|
async def forward_to_poke(
|
||||||
email_data: dict, account: dict, webhook_url: str, api_key: str
|
email_data: dict, account: dict, webhook_url: str, api_key: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
payload = {
|
payload = {
|
||||||
|
"message": _build_poke_message(email_data, account),
|
||||||
"account_id": account["id"],
|
"account_id": account["id"],
|
||||||
"from_address": account["from_address"],
|
"from_address": account["from_address"],
|
||||||
"from": email_data["from"],
|
"from": email_data["from"],
|
||||||
@@ -1012,6 +1050,26 @@ async def send_email(
|
|||||||
smtp.login(acc["smtp_username"], acc["smtp_password"])
|
smtp.login(acc["smtp_username"], acc["smtp_password"])
|
||||||
smtp.sendmail(acc["smtp_username"], recipients, msg.as_string())
|
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 {"success": True, "message_id": msg.get("Message-ID", "")}
|
||||||
|
|
||||||
return await asyncio.to_thread(_send)
|
return await asyncio.to_thread(_send)
|
||||||
|
|||||||
Reference in New Issue
Block a user