dhisana 0.0.1.dev239__tar.gz → 0.0.1.dev241__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.
Files changed (120) hide show
  1. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/setup.py +1 -1
  3. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/schemas/common.py +1 -0
  4. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/google_oauth_tools.py +6 -0
  5. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/google_workspace_tools.py +7 -0
  6. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/mailgun_tools.py +6 -0
  7. dhisana-0.0.1.dev241/src/dhisana/utils/mailreach_tools.py +123 -0
  8. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/microsoft365_tools.py +8 -0
  9. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/sendgrid_tools.py +10 -0
  10. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/smtp_email_tools.py +6 -0
  11. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/test_connect.py +26 -0
  12. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/PKG-INFO +1 -1
  13. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/SOURCES.txt +2 -0
  14. dhisana-0.0.1.dev241/tests/test_mailreach.py +179 -0
  15. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/README.md +0 -0
  16. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/pyproject.toml +0 -0
  17. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/setup.cfg +0 -0
  18. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/__init__.py +0 -0
  19. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/cli/__init__.py +0 -0
  20. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/cli/cli.py +0 -0
  21. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/cli/datasets.py +0 -0
  22. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/cli/models.py +0 -0
  23. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/cli/predictions.py +0 -0
  24. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/schemas/__init__.py +0 -0
  25. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/schemas/sales.py +0 -0
  26. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/ui/__init__.py +0 -0
  27. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/ui/components.py +0 -0
  28. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/__init__.py +0 -0
  29. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/add_mapping.py +0 -0
  30. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/agent_tools.py +0 -0
  31. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/apollo_tools.py +0 -0
  32. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  33. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/built_with_api_tools.py +0 -0
  34. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/cache_output_tools.py +0 -0
  35. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  36. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  37. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  38. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  39. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/clay_tools.py +0 -0
  40. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/clean_properties.py +0 -0
  41. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/company_utils.py +0 -0
  42. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  43. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/compose_search_query.py +0 -0
  44. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  45. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/composite_tools.py +0 -0
  46. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/dataframe_tools.py +0 -0
  47. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/domain_parser.py +0 -0
  48. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/email_body_utils.py +0 -0
  49. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/email_parse_helpers.py +0 -0
  50. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/email_provider.py +0 -0
  51. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/enrich_lead_information.py +0 -0
  52. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  53. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/fetch_openai_config.py +0 -0
  54. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/field_validators.py +0 -0
  55. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/g2_tools.py +0 -0
  56. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_content.py +0 -0
  57. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_email.py +0 -0
  58. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_email_response.py +0 -0
  59. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_flow.py +0 -0
  60. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  61. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  62. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  63. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  64. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/google_custom_search.py +0 -0
  65. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  66. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  67. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/instantly_tools.py +0 -0
  68. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/linkedin_crawler.py +0 -0
  69. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/lusha_tools.py +0 -0
  70. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  71. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openai_helpers.py +0 -0
  72. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  73. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  74. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  75. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  76. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  77. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  78. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/profile.py +0 -0
  79. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  80. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  81. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/python_function_to_tools.py +0 -0
  82. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/research_lead.py +0 -0
  83. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  84. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  85. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/search_router.py +0 -0
  86. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/search_router_jobs.py +0 -0
  87. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  88. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  89. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  90. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serpapi_google_search.py +0 -0
  91. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  92. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  93. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  94. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serperdev_local_business.py +0 -0
  95. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/serperdev_search.py +0 -0
  96. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/trasform_json.py +0 -0
  97. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  98. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/workflow_code_model.py +0 -0
  99. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/utils/zoominfo_tools.py +0 -0
  100. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/workflow/__init__.py +0 -0
  101. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/workflow/agent.py +0 -0
  102. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/workflow/flow.py +0 -0
  103. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/workflow/task.py +0 -0
  104. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana/workflow/test.py +0 -0
  105. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/dependency_links.txt +0 -0
  106. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/entry_points.txt +0 -0
  107. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/requires.txt +0 -0
  108. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/src/dhisana.egg-info/top_level.txt +0 -0
  109. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_agent_tools.py +0 -0
  110. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_apollo_company_search.py +0 -0
  111. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_apollo_lead_search.py +0 -0
  112. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_connectivity.py +0 -0
  113. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_email_body_utils.py +0 -0
  114. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_google_document.py +0 -0
  115. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_hubspot_call_logs.py +0 -0
  116. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_linkedin_serper.py +0 -0
  117. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_mcp_connectivity.py +0 -0
  118. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_proxycurl_get_company_search_id.py +0 -0
  119. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_proxycurl_job_count.py +0 -0
  120. {dhisana-0.0.1.dev239 → dhisana-0.0.1.dev241}/tests/test_structured_output_with_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev239
3
+ Version: 0.0.1.dev241
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev239',
5
+ version='0.0.1-dev241',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -378,6 +378,7 @@ class SendEmailContext(BaseModel):
378
378
  sender_email: str
379
379
  labels: Optional[List[str]]
380
380
  body_format: BodyFormat = BodyFormat.AUTO
381
+ headers: Optional[Dict[str, str]] = None
381
382
 
382
383
  class QueryEmailContext(BaseModel):
383
384
  start_time: str
@@ -145,6 +145,12 @@ async def send_email_using_google_oauth_async(
145
145
  message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
146
146
  message["subject"] = send_email_context.subject
147
147
 
148
+ extra_headers = getattr(send_email_context, "headers", None) or {}
149
+ for header, value in extra_headers.items():
150
+ if not header or value is None:
151
+ continue
152
+ message[header] = str(value)
153
+
148
154
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
149
155
 
150
156
  payload: Dict[str, Any] = {"raw": raw_message}
@@ -179,6 +179,12 @@ async def send_email_using_service_account_async(
179
179
  message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
180
180
  message['subject'] = send_email_context.subject
181
181
 
182
+ extra_headers = getattr(send_email_context, "headers", None) or {}
183
+ for header, value in extra_headers.items():
184
+ if not header or value is None:
185
+ continue
186
+ message[header] = str(value)
187
+
182
188
  # Base64-encode the message
183
189
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
184
190
 
@@ -531,6 +537,7 @@ class SendEmailContext(BaseModel):
531
537
  sender_email: str
532
538
  labels: Optional[List[str]]
533
539
  body_format: BodyFormat = BodyFormat.AUTO
540
+ headers: Optional[Dict[str, str]] = None
534
541
 
535
542
  @assistant_tool
536
543
  async def send_email_using_service_account_async(
@@ -126,6 +126,12 @@ async def send_email_using_mailgun_async(
126
126
  "html": html_body,
127
127
  }
128
128
 
129
+ extra_headers = getattr(send_email_context, "headers", None) or {}
130
+ for header, value in extra_headers.items():
131
+ if not header or value is None:
132
+ continue
133
+ data[f"h:{header}"] = str(value)
134
+
129
135
  async with aiohttp.ClientSession() as session:
130
136
  async with session.post(
131
137
  f"https://api.mailgun.net/v3/{domain}/messages",
@@ -0,0 +1,123 @@
1
+ import os
2
+ import aiohttp
3
+ import logging
4
+ from typing import List, Dict, Any, Optional
5
+ from dhisana.utils.assistant_tool_tag import assistant_tool
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ base_url = 'https://api.mailreach.co/api/v1'
11
+
12
+
13
+ def get_mailreach_api_key(tool_config: Optional[List[Dict]] = None) -> str:
14
+ """
15
+ Retrieves the MailReach API key from the provided tool configuration or environment variables.
16
+
17
+ Args:
18
+ tool_config (list): A list of dictionaries containing the tool configuration.
19
+ Each dictionary should have a "name" key and a "configuration" key,
20
+ where "configuration" is a list of dictionaries containing "name" and "value" keys.
21
+
22
+ Returns:
23
+ str: The MailReach API key.
24
+
25
+ Raises:
26
+ ValueError: If the MailReach integration has not been configured.
27
+ """
28
+ api_key = None
29
+
30
+ if tool_config:
31
+ mailreach_config = next(
32
+ (item for item in tool_config if item.get("name") == "mailreach"), None
33
+ )
34
+ if mailreach_config:
35
+ config_map = {
36
+ item["name"]: item["value"]
37
+ for item in mailreach_config.get("configuration", [])
38
+ if item
39
+ }
40
+ api_key = config_map.get("apiKey")
41
+
42
+ api_key = api_key or os.getenv("MAILREACH_API_KEY")
43
+
44
+ if not api_key:
45
+ raise ValueError(
46
+ "MailReach integration is not configured. Please configure the connection to MailReach in Integrations."
47
+ )
48
+
49
+ return api_key
50
+
51
+
52
+ def get_mailreach_headers(tool_config: Optional[List[Dict]] = None) -> Dict[str, str]:
53
+ """
54
+ Get the headers required for MailReach API requests.
55
+
56
+ Args:
57
+ tool_config (list): Optional tool configuration containing API credentials.
58
+
59
+ Returns:
60
+ Dict[str, str]: Headers dictionary with x-api-key and Content-Type.
61
+ """
62
+ api_key = get_mailreach_api_key(tool_config)
63
+ headers = {
64
+ "x-api-key": api_key,
65
+ "Content-Type": "application/json"
66
+ }
67
+ return headers
68
+
69
+
70
+ async def _handle_mailreach_response(response: aiohttp.ClientResponse) -> Any:
71
+ """
72
+ Handle MailReach API responses consistently.
73
+
74
+ Args:
75
+ response: The aiohttp ClientResponse object.
76
+
77
+ Returns:
78
+ The JSON response data.
79
+
80
+ Raises:
81
+ aiohttp.ClientResponseError: For rate limits or other errors.
82
+ """
83
+ if response.status == 200:
84
+ return await response.json()
85
+ elif response.status == 429:
86
+ raise aiohttp.ClientResponseError(
87
+ request_info=response.request_info,
88
+ history=response.history,
89
+ status=response.status,
90
+ message="Rate limit exceeded",
91
+ headers=response.headers
92
+ )
93
+ else:
94
+ error_message = await response.text()
95
+ logger.error(f"MailReach API Error {response.status}: {error_message}")
96
+ response.raise_for_status()
97
+
98
+
99
+ @assistant_tool
100
+ async def ping_mailreach(
101
+ tool_config: Optional[List[Dict]] = None
102
+ ) -> Dict[str, Any]:
103
+ """
104
+ Ping the MailReach API to verify connectivity and authentication.
105
+
106
+ This is a simple endpoint to test if your API key is valid and the service is accessible.
107
+
108
+ Args:
109
+ tool_config (list): Optional tool configuration containing API credentials.
110
+
111
+ Returns:
112
+ Dict[str, Any]: Response from the ping endpoint, typically containing a success message.
113
+ """
114
+ url = f"{base_url}/ping"
115
+ headers = get_mailreach_headers(tool_config)
116
+
117
+ logger.info("Pinging MailReach API...")
118
+
119
+ async with aiohttp.ClientSession() as session:
120
+ async with session.get(url, headers=headers) as response:
121
+ result = await _handle_mailreach_response(response)
122
+ logger.info("MailReach ping successful")
123
+ return result
@@ -168,6 +168,14 @@ async def send_email_using_microsoft_graph_async(
168
168
  ],
169
169
  }
170
170
 
171
+ extra_headers = getattr(send_email_context, "headers", None) or {}
172
+ if extra_headers:
173
+ message_payload["internetMessageHeaders"] = [
174
+ {"name": header, "value": str(value)}
175
+ for header, value in extra_headers.items()
176
+ if header and value is not None
177
+ ]
178
+
171
179
  headers = {
172
180
  "Authorization": f"Bearer {token}",
173
181
  "Content-Type": "application/json",
@@ -59,6 +59,7 @@ async def send_email_with_sendgrid(
59
59
  message: str,
60
60
  tool_config: Optional[List[Dict]] = None,
61
61
  body_format: Optional[str] = None,
62
+ custom_headers: Optional[Dict[str, str]] = None,
62
63
  ):
63
64
  """
64
65
  Send an email using SendGrid's v3 Mail Send API.
@@ -69,6 +70,7 @@ async def send_email_with_sendgrid(
69
70
  - subject: Subject string.
70
71
  - message: HTML body content.
71
72
  - tool_config: Optional integration configuration list.
73
+ - custom_headers: Optional mapping of header names to values.
72
74
  """
73
75
  api_key = get_sendgrid_api_key(tool_config)
74
76
 
@@ -98,6 +100,13 @@ async def send_email_with_sendgrid(
98
100
  "content": content,
99
101
  }
100
102
 
103
+ if custom_headers:
104
+ payload["headers"] = {
105
+ header: str(value)
106
+ for header, value in custom_headers.items()
107
+ if header and value is not None
108
+ }
109
+
101
110
  headers = {
102
111
  "Authorization": f"Bearer {api_key}",
103
112
  "Content-Type": "application/json",
@@ -143,6 +152,7 @@ async def send_email_using_sendgrid_async(
143
152
  message=ctx.body or "",
144
153
  body_format=getattr(ctx, "body_format", None),
145
154
  tool_config=tool_config,
155
+ custom_headers=getattr(ctx, "headers", None),
146
156
  )
147
157
  # Normalise output to a string id-like value
148
158
  if isinstance(result, dict) and result.get("status") == 202:
@@ -174,6 +174,12 @@ async def send_email_via_smtp_async(
174
174
  generated_id = f"<{uuid.uuid4()}@{domain_part}>"
175
175
  msg["Message-ID"] = generated_id
176
176
 
177
+ extra_headers = getattr(ctx, "headers", None) or {}
178
+ for header, value in extra_headers.items():
179
+ if not header or value is None:
180
+ continue
181
+ msg[header] = str(value)
182
+
177
183
  smtp_kwargs = dict(
178
184
  hostname=smtp_server,
179
185
  port=smtp_port,
@@ -483,6 +483,31 @@ async def test_sendgrid(api_key: str) -> Dict[str, Any]:
483
483
  return {"success": False, "status_code": 0, "error_message": str(e)}
484
484
 
485
485
 
486
+ async def test_mailreach(api_key: str) -> Dict[str, Any]:
487
+ """
488
+ Basic MailReach connectivity check using the Ping endpoint.
489
+
490
+ Uses the /v1/ping endpoint to verify API key validity and service availability.
491
+ Reference: https://docs.mailreach.co/reference/getv1ping
492
+ """
493
+ url = "https://api.mailreach.co/api/v1/ping"
494
+ headers = {"x-api-key": api_key}
495
+ try:
496
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
497
+ async with session.get(url, headers=headers) as response:
498
+ status = response.status
499
+ data = await safe_json(response)
500
+ if status != 200:
501
+ msg = None
502
+ if data and isinstance(data, dict):
503
+ msg = data.get("message") or data.get("error")
504
+ return {"success": False, "status_code": status, "error_message": msg or f"MailReach non-200: {status}"}
505
+ return {"success": True, "status_code": status, "error_message": None}
506
+ except Exception as e:
507
+ logger.error(f"MailReach test failed: {e}")
508
+ return {"success": False, "status_code": 0, "error_message": str(e)}
509
+
510
+
486
511
  async def test_samgov(api_key: str) -> Dict[str, Any]:
487
512
  """Test SAM.gov connectivity by fetching a single opportunity."""
488
513
 
@@ -1789,6 +1814,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
1789
1814
  "mcpServer": test_mcp_server,
1790
1815
  "slack": test_slack,
1791
1816
  "mailgun": test_mailgun,
1817
+ "mailreach": test_mailreach,
1792
1818
  "sendgrid": test_sendgrid,
1793
1819
  "samgov": test_samgov,
1794
1820
  "scraperapi": test_scraperapi,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev239
3
+ Version: 0.0.1.dev241
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -63,6 +63,7 @@ src/dhisana/utils/instantly_tools.py
63
63
  src/dhisana/utils/linkedin_crawler.py
64
64
  src/dhisana/utils/lusha_tools.py
65
65
  src/dhisana/utils/mailgun_tools.py
66
+ src/dhisana/utils/mailreach_tools.py
66
67
  src/dhisana/utils/microsoft365_tools.py
67
68
  src/dhisana/utils/openai_assistant_and_file_utils.py
68
69
  src/dhisana/utils/openai_helpers.py
@@ -110,6 +111,7 @@ tests/test_email_body_utils.py
110
111
  tests/test_google_document.py
111
112
  tests/test_hubspot_call_logs.py
112
113
  tests/test_linkedin_serper.py
114
+ tests/test_mailreach.py
113
115
  tests/test_mcp_connectivity.py
114
116
  tests/test_proxycurl_get_company_search_id.py
115
117
  tests/test_proxycurl_job_count.py
@@ -0,0 +1,179 @@
1
+ import asyncio
2
+ import unittest
3
+ import os
4
+ from unittest.mock import AsyncMock, MagicMock, patch
5
+
6
+ # Test imports
7
+ from src.dhisana.utils.mailreach_tools import (
8
+ get_mailreach_api_key,
9
+ get_mailreach_headers,
10
+ ping_mailreach
11
+ )
12
+ from src.dhisana.utils.test_connect import test_mailreach
13
+
14
+
15
+ class TestMailReachTools(unittest.TestCase):
16
+ """Test suite for MailReach integration tools."""
17
+
18
+ def test_get_mailreach_api_key_from_config(self):
19
+ """Test retrieving API key from tool configuration."""
20
+ tool_config = [
21
+ {
22
+ 'name': 'mailreach',
23
+ 'configuration': [
24
+ {'name': 'apiKey', 'value': 'test_api_key_123'}
25
+ ]
26
+ }
27
+ ]
28
+ api_key = get_mailreach_api_key(tool_config)
29
+ self.assertEqual(api_key, 'test_api_key_123')
30
+
31
+ def test_get_mailreach_api_key_from_env(self):
32
+ """Test retrieving API key from environment variable."""
33
+ with patch.dict(os.environ, {'MAILREACH_API_KEY': 'env_api_key_456'}):
34
+ api_key = get_mailreach_api_key()
35
+ self.assertEqual(api_key, 'env_api_key_456')
36
+
37
+ def test_get_mailreach_api_key_missing(self):
38
+ """Test that missing API key raises ValueError."""
39
+ with patch.dict(os.environ, {}, clear=True):
40
+ with self.assertRaises(ValueError) as context:
41
+ get_mailreach_api_key()
42
+ self.assertIn('not configured', str(context.exception))
43
+
44
+ def test_get_mailreach_headers(self):
45
+ """Test that headers are properly formatted."""
46
+ tool_config = [
47
+ {
48
+ 'name': 'mailreach',
49
+ 'configuration': [
50
+ {'name': 'apiKey', 'value': 'test_key'}
51
+ ]
52
+ }
53
+ ]
54
+ headers = get_mailreach_headers(tool_config)
55
+ self.assertEqual(headers['x-api-key'], 'test_key')
56
+ self.assertEqual(headers['Content-Type'], 'application/json')
57
+
58
+ def test_ping_mailreach_success(self):
59
+ """Test successful ping to MailReach API."""
60
+ async def runner():
61
+ tool_config = [
62
+ {
63
+ 'name': 'mailreach',
64
+ 'configuration': [
65
+ {'name': 'apiKey', 'value': 'test_api_key'}
66
+ ]
67
+ }
68
+ ]
69
+
70
+ mock_response = AsyncMock()
71
+ mock_response.status = 200
72
+ mock_response.json = AsyncMock(return_value={'message': 'pong', 'status': 'ok'})
73
+
74
+ mock_get = AsyncMock()
75
+ mock_get.__aenter__.return_value = mock_response
76
+
77
+ with patch('aiohttp.ClientSession') as mock_session:
78
+ mock_session.return_value.__aenter__.return_value.get = MagicMock(return_value=mock_get)
79
+
80
+ result = await ping_mailreach(tool_config)
81
+
82
+ self.assertIsInstance(result, dict)
83
+ self.assertEqual(result.get('message'), 'pong')
84
+
85
+ asyncio.run(runner())
86
+
87
+ def test_ping_mailreach_rate_limit(self):
88
+ """Test rate limit handling in ping."""
89
+ async def runner():
90
+ tool_config = [
91
+ {
92
+ 'name': 'mailreach',
93
+ 'configuration': [
94
+ {'name': 'apiKey', 'value': 'test_api_key'}
95
+ ]
96
+ }
97
+ ]
98
+
99
+ mock_response = AsyncMock()
100
+ mock_response.status = 429
101
+ mock_response.text = AsyncMock(return_value='Rate limit exceeded')
102
+ mock_response.request_info = MagicMock()
103
+ mock_response.history = []
104
+ mock_response.headers = {}
105
+
106
+ mock_get = AsyncMock()
107
+ mock_get.__aenter__.return_value = mock_response
108
+
109
+ with patch('aiohttp.ClientSession') as mock_session:
110
+ mock_session.return_value.__aenter__.return_value.get = MagicMock(return_value=mock_get)
111
+
112
+ with self.assertRaises(Exception):
113
+ await ping_mailreach(tool_config)
114
+
115
+ asyncio.run(runner())
116
+
117
+
118
+ class TestMailReachConnectivity(unittest.TestCase):
119
+ """Test suite for MailReach connectivity check."""
120
+
121
+ def test_mailreach_connection_success(self):
122
+ """Test successful MailReach connection."""
123
+ async def runner():
124
+ mock_response = AsyncMock()
125
+ mock_response.status = 200
126
+ mock_response.json = AsyncMock(return_value={'message': 'pong'})
127
+
128
+ mock_get = AsyncMock()
129
+ mock_get.__aenter__.return_value = mock_response
130
+
131
+ with patch('aiohttp.ClientSession') as mock_session:
132
+ mock_session.return_value.__aenter__.return_value.get = MagicMock(return_value=mock_get)
133
+
134
+ result = await test_mailreach('test_api_key')
135
+
136
+ self.assertTrue(result['success'])
137
+ self.assertEqual(result['status_code'], 200)
138
+ self.assertIsNone(result['error_message'])
139
+
140
+ asyncio.run(runner())
141
+
142
+ def test_mailreach_connection_invalid_key(self):
143
+ """Test MailReach connection with invalid API key."""
144
+ async def runner():
145
+ mock_response = AsyncMock()
146
+ mock_response.status = 401
147
+ mock_response.json = AsyncMock(return_value={'error': 'Invalid API key'})
148
+
149
+ mock_get = AsyncMock()
150
+ mock_get.__aenter__.return_value = mock_response
151
+
152
+ with patch('aiohttp.ClientSession') as mock_session:
153
+ mock_session.return_value.__aenter__.return_value.get = MagicMock(return_value=mock_get)
154
+
155
+ result = await test_mailreach('invalid_key')
156
+
157
+ self.assertFalse(result['success'])
158
+ self.assertEqual(result['status_code'], 401)
159
+ self.assertIsNotNone(result['error_message'])
160
+
161
+ asyncio.run(runner())
162
+
163
+ def test_mailreach_connection_timeout(self):
164
+ """Test MailReach connection timeout."""
165
+ async def runner():
166
+ with patch('aiohttp.ClientSession') as mock_session:
167
+ mock_session.return_value.__aenter__.return_value.get.side_effect = asyncio.TimeoutError()
168
+
169
+ result = await test_mailreach('test_api_key')
170
+
171
+ self.assertFalse(result['success'])
172
+ self.assertEqual(result['status_code'], 0)
173
+ self.assertIsNotNone(result['error_message'])
174
+
175
+ asyncio.run(runner())
176
+
177
+
178
+ if __name__ == '__main__':
179
+ unittest.main()
File without changes
File without changes