diff --git a/cron/scheduler.py b/cron/scheduler.py
index a51ade8efe65..1d23c915efc7 100644
--- a/cron/scheduler.py
+++ b/cron/scheduler.py
@@ -647,24 +647,69 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option
except Exception:
pass
+ # Track whether content is HTML so we can pass a proper email subject.
+ is_html = False
+
if wrap_response:
- task_name = job.get("name", job["id"])
- job_id = job.get("id", "")
- delivery_content = (
- f"Cronjob Response: {task_name}\n"
- f"(job_id: {job_id})\n"
- f"-------------\n\n"
- f"{content}\n\n"
- f"To stop or manage this job, send me a new message (e.g. \"stop reminder {task_name}\")."
- )
+ # Detect HTML content for two purposes: (1) skip the cron wrapper
+ # (it would break HTML detection downstream), and (2) build a
+ # proper email subject from the job name instead of "Hermes Agent".
+ # Don't wrap HTML email content — the wrapper prefix would break
+ # HTML detection (body must start with 0:
+ # Preamble text before HTML — strip it
+ delivery_content = stripped[start_idx:]
+ is_html = True
+ logger.debug("Job '%s': stripped preamble before HTML (kept %d chars from position %d)",
+ job["id"], len(delivery_content), start_idx)
+ else:
+ # No HTML — wrap normally
+ task_name = job.get("name", job["id"])
+ job_id = job.get("id", "")
+ delivery_content = (
+ f"Cronjob Response: {task_name}\n"
+ f"(job_id: {job_id})\n"
+ f"-------------\n\n"
+ f"{content}\n\n"
+ f'To stop or manage this job, send me a new message (e.g. "stop reminder {task_name}").'
+ )
else:
delivery_content = content
+ # Detect HTML even when wrapping is off so we can set a proper subject
+ stripped = content.strip().lower()
+ if stripped.startswith(" Option
delivered = False
if runtime_adapter is not None and loop is not None and getattr(loop, "is_running", lambda: False)():
send_metadata = {"thread_id": thread_id} if thread_id else None
+ # Attach email subject for HTML deliveries
+ if email_subject and platform == Platform.EMAIL:
+ if send_metadata is None:
+ send_metadata = {}
+ send_metadata["subject"] = email_subject
try:
# Send cleaned text (MEDIA tags stripped) — not the raw content
text_to_send = cleaned_delivery_content.strip()
@@ -779,7 +829,7 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option
if not delivered:
# Standalone path: run the async send in a fresh event loop (safe from any thread)
- coro = _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files)
+ coro = _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files, email_subject=email_subject)
try:
result = asyncio.run(coro)
except RuntimeError:
@@ -789,7 +839,7 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option
# fresh thread that has no running loop.
coro.close()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
- future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files))
+ future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files, email_subject=email_subject))
result = future.result(timeout=30)
except Exception as e:
msg = f"delivery to {platform_name}:{chat_id} failed: {e}"
diff --git a/gateway/platforms/email.py b/gateway/platforms/email.py
index 0fffb82d0b94..3176c8a1ab63 100644
--- a/gateway/platforms/email.py
+++ b/gateway/platforms/email.py
@@ -165,6 +165,8 @@ def _strip_html(html: str) -> str:
text = re.sub(r"
", "\n", html, flags=re.IGNORECASE)
text = re.sub(r"
]*>", "\n", text, flags=re.IGNORECASE) text = re.sub(r"
", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "", text, flags=re.IGNORECASE | re.DOTALL) + text = re.sub(r"]*>.*?", "", text, flags=re.IGNORECASE | re.DOTALL) text = re.sub(r"<[^>]+>", "", text) text = re.sub(r" ", " ", text) text = re.sub(r"&", "&", text) @@ -174,6 +176,12 @@ def _strip_html(html: str) -> str: return text.strip() +def _is_html_body(body: str) -> bool: + """Return True if the body looks like an HTML document.""" + stripped = body.strip().lower() + return stripped.startswith(" str: """Extract bare email address from 'Name