dhisana 0.0.1.dev227__tar.gz → 0.0.1.dev228__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/setup.py +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/schemas/common.py +9 -1
- dhisana-0.0.1.dev228/src/dhisana/utils/email_body_utils.py +72 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_oauth_tools.py +25 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_workspace_tools.py +33 -9
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/mailgun_tools.py +14 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/microsoft365_tools.py +10 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/sendgrid_tools.py +14 -3
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/smtp_email_tools.py +10 -6
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/SOURCES.txt +2 -0
- dhisana-0.0.1.dev228/tests/test_email_body_utils.py +23 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/README.md +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/setup.cfg +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/test_connect.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -364,6 +364,12 @@ class Integration(IntegrationBase):
|
|
|
364
364
|
Integration.model_rebuild()
|
|
365
365
|
IntegrationUpdate.model_rebuild()
|
|
366
366
|
|
|
367
|
+
class BodyFormat(str, Enum):
|
|
368
|
+
AUTO = "auto"
|
|
369
|
+
HTML = "html"
|
|
370
|
+
TEXT = "text"
|
|
371
|
+
|
|
372
|
+
|
|
367
373
|
class SendEmailContext(BaseModel):
|
|
368
374
|
recipient: str
|
|
369
375
|
subject: str
|
|
@@ -371,6 +377,7 @@ class SendEmailContext(BaseModel):
|
|
|
371
377
|
sender_name: str
|
|
372
378
|
sender_email: str
|
|
373
379
|
labels: Optional[List[str]]
|
|
380
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
374
381
|
|
|
375
382
|
class QueryEmailContext(BaseModel):
|
|
376
383
|
start_time: str
|
|
@@ -386,4 +393,5 @@ class ReplyEmailContext(BaseModel):
|
|
|
386
393
|
sender_email: str
|
|
387
394
|
sender_name: str
|
|
388
395
|
mark_as_read: str = "True"
|
|
389
|
-
add_labels: Optional[List[str]] = None
|
|
396
|
+
add_labels: Optional[List[str]] = None
|
|
397
|
+
reply_body_format: BodyFormat = BodyFormat.AUTO
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Small helpers for handling e-mail bodies across providers."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
import html as html_lib
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def looks_like_html(text: str) -> bool:
|
|
9
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
10
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_format_hint(format_hint: Optional[str]) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Normalize a user-supplied format hint into html/text/auto.
|
|
16
|
+
|
|
17
|
+
Accepts variations like "plain" or "plaintext" as text.
|
|
18
|
+
"""
|
|
19
|
+
if not format_hint:
|
|
20
|
+
return "auto"
|
|
21
|
+
fmt_raw = getattr(format_hint, "value", format_hint)
|
|
22
|
+
fmt = str(fmt_raw).strip().lower()
|
|
23
|
+
if fmt in ("html",):
|
|
24
|
+
return "html"
|
|
25
|
+
if fmt in ("text", "plain", "plain_text", "plaintext"):
|
|
26
|
+
return "text"
|
|
27
|
+
return "auto"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def html_to_plain_text(html: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
33
|
+
This keeps newlines on block boundaries and strips tags.
|
|
34
|
+
"""
|
|
35
|
+
if not html:
|
|
36
|
+
return ""
|
|
37
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
38
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
39
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
40
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
41
|
+
text = html_lib.unescape(text)
|
|
42
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
43
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
44
|
+
return text.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plain_text_to_html(text: str) -> str:
|
|
48
|
+
"""Wrap plain text in a minimal HTML container that preserves newlines."""
|
|
49
|
+
if text is None:
|
|
50
|
+
return ""
|
|
51
|
+
escaped = html_lib.escape(text)
|
|
52
|
+
return f'<div style="white-space: pre-wrap">{escaped}</div>'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def body_variants(body: Optional[str], format_hint: Optional[str]) -> Tuple[str, str, str]:
|
|
56
|
+
"""
|
|
57
|
+
Return (plain, html, resolved_format) honoring an optional format hint.
|
|
58
|
+
|
|
59
|
+
resolved_format is "html" or "text" after applying auto-detection.
|
|
60
|
+
"""
|
|
61
|
+
content = body or ""
|
|
62
|
+
fmt = _normalize_format_hint(format_hint)
|
|
63
|
+
|
|
64
|
+
if fmt == "html":
|
|
65
|
+
return html_to_plain_text(content), content, "html"
|
|
66
|
+
if fmt == "text":
|
|
67
|
+
return content, plain_text_to_html(content), "text"
|
|
68
|
+
|
|
69
|
+
if looks_like_html(content):
|
|
70
|
+
return html_to_plain_text(content), content, "html"
|
|
71
|
+
|
|
72
|
+
return content, plain_text_to_html(content), "text"
|
|
@@ -2,6 +2,7 @@ import base64
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
from email.mime.multipart import MIMEMultipart
|
|
5
6
|
from email.mime.text import MIMEText
|
|
6
7
|
from typing import Any, Dict, List, Optional
|
|
7
8
|
|
|
@@ -22,6 +23,7 @@ from dhisana.utils.email_parse_helpers import (
|
|
|
22
23
|
)
|
|
23
24
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
25
|
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
26
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
25
27
|
from typing import Optional as _Optional # avoid name clash in wrappers
|
|
26
28
|
|
|
27
29
|
def _status_phrase(code: int) -> str:
|
|
@@ -127,7 +129,18 @@ async def send_email_using_google_oauth_async(
|
|
|
127
129
|
"""
|
|
128
130
|
token = get_google_access_token(tool_config)
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
133
|
+
send_email_context.body,
|
|
134
|
+
getattr(send_email_context, "body_format", None),
|
|
135
|
+
)
|
|
136
|
+
# Use multipart/alternative when we have both; fall back to single part for pure text.
|
|
137
|
+
if resolved_fmt == "text":
|
|
138
|
+
message = MIMEText(plain_body, "plain", _charset="utf-8")
|
|
139
|
+
else:
|
|
140
|
+
message = MIMEMultipart("alternative")
|
|
141
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
142
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
143
|
+
|
|
131
144
|
message["to"] = send_email_context.recipient
|
|
132
145
|
message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
133
146
|
message["subject"] = send_email_context.subject
|
|
@@ -265,7 +278,17 @@ async def reply_to_email_google_oauth_async(
|
|
|
265
278
|
message_id_header = headers_map.get("Message-ID", "") or ""
|
|
266
279
|
|
|
267
280
|
# 2) Build reply MIME
|
|
268
|
-
|
|
281
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
282
|
+
reply_email_context.reply_body,
|
|
283
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
284
|
+
)
|
|
285
|
+
if resolved_reply_fmt == "text":
|
|
286
|
+
msg = MIMEText(plain_reply, "plain", _charset="utf-8")
|
|
287
|
+
else:
|
|
288
|
+
msg = MIMEMultipart("alternative")
|
|
289
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
290
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
291
|
+
|
|
269
292
|
msg["To"] = to_addresses
|
|
270
293
|
if cc_addresses:
|
|
271
294
|
msg["Cc"] = cc_addresses
|
|
@@ -24,8 +24,9 @@ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
|
24
24
|
from dhisana.schemas.sales import MessageItem
|
|
25
25
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
26
26
|
from dhisana.utils.email_parse_helpers import *
|
|
27
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
27
28
|
import asyncio
|
|
28
|
-
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext)
|
|
29
|
+
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext, BodyFormat)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
################################################################################
|
|
@@ -161,15 +162,18 @@ async def send_email_using_service_account_async(
|
|
|
161
162
|
|
|
162
163
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
166
|
+
send_email_context.body,
|
|
167
|
+
getattr(send_email_context, "body_format", None),
|
|
168
|
+
)
|
|
165
169
|
|
|
166
|
-
if
|
|
170
|
+
if resolved_fmt == "text":
|
|
171
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
172
|
+
else:
|
|
167
173
|
# Gmail prefers multipart/alternative when HTML is present.
|
|
168
174
|
message = MIMEMultipart("alternative")
|
|
169
|
-
message.attach(MIMEText(
|
|
170
|
-
message.attach(MIMEText(
|
|
171
|
-
else:
|
|
172
|
-
message = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
175
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
176
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
173
177
|
|
|
174
178
|
message['to'] = send_email_context.recipient
|
|
175
179
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
@@ -509,6 +513,7 @@ class SendEmailContext(BaseModel):
|
|
|
509
513
|
sender_name: str
|
|
510
514
|
sender_email: str
|
|
511
515
|
labels: Optional[List[str]]
|
|
516
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
512
517
|
|
|
513
518
|
@assistant_tool
|
|
514
519
|
async def send_email_using_service_account_async(
|
|
@@ -537,8 +542,18 @@ async def send_email_using_service_account_async(
|
|
|
537
542
|
|
|
538
543
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
539
544
|
|
|
545
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
546
|
+
send_email_context.body,
|
|
547
|
+
getattr(send_email_context, "body_format", None),
|
|
548
|
+
)
|
|
549
|
+
|
|
540
550
|
# Construct the MIME text message
|
|
541
|
-
|
|
551
|
+
if resolved_fmt == "text":
|
|
552
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
553
|
+
else:
|
|
554
|
+
message = MIMEMultipart("alternative")
|
|
555
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
556
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
542
557
|
message['to'] = send_email_context.recipient
|
|
543
558
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
544
559
|
message['subject'] = send_email_context.subject
|
|
@@ -893,7 +908,16 @@ async def reply_to_email_async(
|
|
|
893
908
|
message_id_header = headers_dict.get('Message-ID', '')
|
|
894
909
|
|
|
895
910
|
# 3. Create the reply email message
|
|
896
|
-
|
|
911
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
912
|
+
reply_email_context.reply_body,
|
|
913
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
914
|
+
)
|
|
915
|
+
if resolved_reply_fmt == "text":
|
|
916
|
+
msg = MIMEText(plain_reply, _subtype="plain", _charset="utf-8")
|
|
917
|
+
else:
|
|
918
|
+
msg = MIMEMultipart("alternative")
|
|
919
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
920
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
897
921
|
msg['To'] = to_addresses
|
|
898
922
|
if cc_addresses:
|
|
899
923
|
msg['Cc'] = cc_addresses
|
|
@@ -7,6 +7,7 @@ import aiohttp
|
|
|
7
7
|
|
|
8
8
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
9
9
|
from dhisana.schemas.common import SendEmailContext
|
|
10
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def get_mailgun_notify_key(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -59,6 +60,7 @@ async def send_email_with_mailgun(
|
|
|
59
60
|
subject: str,
|
|
60
61
|
message: str,
|
|
61
62
|
tool_config: Optional[List[Dict]] = None,
|
|
63
|
+
body_format: Optional[str] = None,
|
|
62
64
|
):
|
|
63
65
|
"""
|
|
64
66
|
Send an email using the Mailgun API.
|
|
@@ -74,13 +76,17 @@ async def send_email_with_mailgun(
|
|
|
74
76
|
api_key = get_mailgun_notify_key(tool_config)
|
|
75
77
|
domain = get_mailgun_notify_domain(tool_config)
|
|
76
78
|
|
|
79
|
+
body = message or ""
|
|
77
80
|
data = {
|
|
78
81
|
"from": sender,
|
|
79
82
|
"to": recipients,
|
|
80
83
|
"subject": subject,
|
|
81
|
-
"html": message,
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
plain_body, html_body, _ = body_variants(body, body_format)
|
|
87
|
+
data["text"] = plain_body
|
|
88
|
+
data["html"] = html_body
|
|
89
|
+
|
|
84
90
|
async with aiohttp.ClientSession() as session:
|
|
85
91
|
async with session.post(
|
|
86
92
|
f"https://api.mailgun.net/v3/{domain}/messages",
|
|
@@ -107,11 +113,17 @@ async def send_email_using_mailgun_async(
|
|
|
107
113
|
api_key = get_mailgun_notify_key(tool_config)
|
|
108
114
|
domain = get_mailgun_notify_domain(tool_config)
|
|
109
115
|
|
|
116
|
+
plain_body, html_body, _ = body_variants(
|
|
117
|
+
send_email_context.body,
|
|
118
|
+
getattr(send_email_context, "body_format", None),
|
|
119
|
+
)
|
|
120
|
+
|
|
110
121
|
data = {
|
|
111
122
|
"from": f"{send_email_context.sender_name} <{send_email_context.sender_email}>",
|
|
112
123
|
"to": [send_email_context.recipient],
|
|
113
124
|
"subject": send_email_context.subject,
|
|
114
|
-
"
|
|
125
|
+
"text": plain_body,
|
|
126
|
+
"html": html_body,
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
async with aiohttp.ClientSession() as session:
|
|
@@ -12,6 +12,7 @@ from dhisana.schemas.common import (
|
|
|
12
12
|
ReplyEmailContext,
|
|
13
13
|
)
|
|
14
14
|
from dhisana.schemas.sales import MessageItem
|
|
15
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_microsoft365_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -149,11 +150,18 @@ async def send_email_using_microsoft_graph_async(
|
|
|
149
150
|
base_url = "https://graph.microsoft.com/v1.0"
|
|
150
151
|
base_res = _base_resource(sender_email, tool_config, auth_mode)
|
|
151
152
|
|
|
153
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
154
|
+
send_email_context.body,
|
|
155
|
+
getattr(send_email_context, "body_format", None),
|
|
156
|
+
)
|
|
157
|
+
content_type = "Text" if resolved_fmt == "text" else "HTML"
|
|
158
|
+
content_body = plain_body if resolved_fmt == "text" else html_body
|
|
159
|
+
|
|
152
160
|
message_payload: Dict[str, Any] = {
|
|
153
161
|
"subject": send_email_context.subject,
|
|
154
162
|
"body": {
|
|
155
|
-
"contentType":
|
|
156
|
-
"content":
|
|
163
|
+
"contentType": content_type,
|
|
164
|
+
"content": content_body,
|
|
157
165
|
},
|
|
158
166
|
"toRecipients": [
|
|
159
167
|
{"emailAddress": {"address": send_email_context.recipient}}
|
|
@@ -14,6 +14,7 @@ import aiohttp
|
|
|
14
14
|
|
|
15
15
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
16
16
|
from dhisana.schemas.common import SendEmailContext
|
|
17
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
17
18
|
|
|
18
19
|
# --------------------------------------------------------------------------- #
|
|
19
20
|
# Mailgun (re-exported from dedicated module for backward compatibility)
|
|
@@ -57,6 +58,7 @@ async def send_email_with_sendgrid(
|
|
|
57
58
|
subject: str,
|
|
58
59
|
message: str,
|
|
59
60
|
tool_config: Optional[List[Dict]] = None,
|
|
61
|
+
body_format: Optional[str] = None,
|
|
60
62
|
):
|
|
61
63
|
"""
|
|
62
64
|
Send an email using SendGrid's v3 Mail Send API.
|
|
@@ -79,6 +81,12 @@ async def send_email_with_sendgrid(
|
|
|
79
81
|
if not to_list:
|
|
80
82
|
return {"error": "No recipients provided"}
|
|
81
83
|
|
|
84
|
+
plain_body, html_body, _ = body_variants(message, body_format)
|
|
85
|
+
content = [
|
|
86
|
+
{"type": "text/plain", "value": plain_body},
|
|
87
|
+
{"type": "text/html", "value": html_body},
|
|
88
|
+
]
|
|
89
|
+
|
|
82
90
|
payload = {
|
|
83
91
|
"personalizations": [
|
|
84
92
|
{
|
|
@@ -87,9 +95,7 @@ async def send_email_with_sendgrid(
|
|
|
87
95
|
}
|
|
88
96
|
],
|
|
89
97
|
"from": from_obj,
|
|
90
|
-
"content":
|
|
91
|
-
{"type": "text/html", "value": message or ""}
|
|
92
|
-
],
|
|
98
|
+
"content": content,
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
headers = {
|
|
@@ -126,11 +132,16 @@ async def send_email_using_sendgrid_async(
|
|
|
126
132
|
Provider-style wrapper for SendGrid using SendEmailContext.
|
|
127
133
|
Returns an opaque token since SendGrid does not return a message id.
|
|
128
134
|
"""
|
|
135
|
+
plain_body, html_body, _ = body_variants(
|
|
136
|
+
ctx.body,
|
|
137
|
+
getattr(ctx, "body_format", None),
|
|
138
|
+
)
|
|
129
139
|
result = await send_email_with_sendgrid(
|
|
130
140
|
sender=f"{ctx.sender_name} <{ctx.sender_email}>",
|
|
131
141
|
recipients=[ctx.recipient],
|
|
132
142
|
subject=ctx.subject,
|
|
133
143
|
message=ctx.body or "",
|
|
144
|
+
body_format=getattr(ctx, "body_format", None),
|
|
134
145
|
tool_config=tool_config,
|
|
135
146
|
)
|
|
136
147
|
# Normalise output to a string id-like value
|
|
@@ -33,6 +33,7 @@ from dhisana.utils.google_workspace_tools import (
|
|
|
33
33
|
QueryEmailContext,
|
|
34
34
|
SendEmailContext,
|
|
35
35
|
)
|
|
36
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
# --------------------------------------------------------------------------- #
|
|
@@ -151,15 +152,18 @@ async def send_email_via_smtp_async(
|
|
|
151
152
|
str
|
|
152
153
|
The Message-ID of the sent message (e.g., "<uuid@yourdomain.com>").
|
|
153
154
|
"""
|
|
154
|
-
|
|
155
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
156
|
+
ctx.body,
|
|
157
|
+
getattr(ctx, "body_format", None),
|
|
158
|
+
)
|
|
155
159
|
|
|
156
|
-
if
|
|
160
|
+
if resolved_fmt == "text":
|
|
161
|
+
msg = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
162
|
+
else:
|
|
157
163
|
# Build multipart/alternative so HTML-capable clients see rich content.
|
|
158
164
|
msg = MIMEMultipart("alternative")
|
|
159
|
-
msg.attach(MIMEText(
|
|
160
|
-
msg.attach(MIMEText(
|
|
161
|
-
else:
|
|
162
|
-
msg = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
165
|
+
msg.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
166
|
+
msg.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
163
167
|
|
|
164
168
|
msg["From"] = f"{ctx.sender_name} <{ctx.sender_email}>"
|
|
165
169
|
msg["To"] = ctx.recipient
|
|
@@ -38,6 +38,7 @@ src/dhisana/utils/compose_three_step_workflow.py
|
|
|
38
38
|
src/dhisana/utils/composite_tools.py
|
|
39
39
|
src/dhisana/utils/dataframe_tools.py
|
|
40
40
|
src/dhisana/utils/domain_parser.py
|
|
41
|
+
src/dhisana/utils/email_body_utils.py
|
|
41
42
|
src/dhisana/utils/email_parse_helpers.py
|
|
42
43
|
src/dhisana/utils/email_provider.py
|
|
43
44
|
src/dhisana/utils/enrich_lead_information.py
|
|
@@ -105,6 +106,7 @@ tests/test_agent_tools.py
|
|
|
105
106
|
tests/test_apollo_company_search.py
|
|
106
107
|
tests/test_apollo_lead_search.py
|
|
107
108
|
tests/test_connectivity.py
|
|
109
|
+
tests/test_email_body_utils.py
|
|
108
110
|
tests/test_google_document.py
|
|
109
111
|
tests/test_hubspot_call_logs.py
|
|
110
112
|
tests/test_linkedin_serper.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from dhisana.schemas.common import BodyFormat
|
|
4
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"body,format_hint,expected_resolved",
|
|
9
|
+
[
|
|
10
|
+
("<p>Hello</p>", BodyFormat.HTML, "html"),
|
|
11
|
+
("Hello", BodyFormat.TEXT, "text"),
|
|
12
|
+
],
|
|
13
|
+
)
|
|
14
|
+
def test_body_variants_honors_body_format_enum(body, format_hint, expected_resolved):
|
|
15
|
+
plain, html, resolved = body_variants(body, format_hint)
|
|
16
|
+
|
|
17
|
+
if expected_resolved == "html":
|
|
18
|
+
assert html == body
|
|
19
|
+
assert plain == "Hello"
|
|
20
|
+
else:
|
|
21
|
+
assert plain == body
|
|
22
|
+
assert html.startswith("<div")
|
|
23
|
+
assert resolved == expected_resolved
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev228}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|