hyperagents 1.2.3rc5.post3__py3-none-any.whl
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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""EmailConnector — Email integration via SMTP or API (Gmail/Outlook)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import smtplib
|
|
7
|
+
from email.mime.multipart import MIMEMultipart
|
|
8
|
+
from email.mime.text import MIMEText
|
|
9
|
+
from email.mime.base import MIMEBase
|
|
10
|
+
from email import encoders
|
|
11
|
+
from typing import AsyncIterator
|
|
12
|
+
import asyncio
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from integrations.base import (
|
|
17
|
+
BaseConnector,
|
|
18
|
+
ConnectorCapability,
|
|
19
|
+
ConnectorInfo,
|
|
20
|
+
InboundMessage,
|
|
21
|
+
OutboundMessage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("hyperclaw.integrations.email")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EmailConnector(BaseConnector):
|
|
28
|
+
"""
|
|
29
|
+
Email connector supporting SMTP, Gmail API, and Outlook API.
|
|
30
|
+
|
|
31
|
+
Required config:
|
|
32
|
+
- provider: "smtp", "gmail", or "outlook"
|
|
33
|
+
|
|
34
|
+
For SMTP:
|
|
35
|
+
- smtp_host, smtp_port, username, password
|
|
36
|
+
|
|
37
|
+
For Gmail/Outlook:
|
|
38
|
+
- access_token (OAuth2)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: dict) -> None:
|
|
42
|
+
self.config = config
|
|
43
|
+
self.provider = config.get("provider", "smtp")
|
|
44
|
+
self.smtp_host = config.get("smtp_host", "")
|
|
45
|
+
self.smtp_port = config.get("smtp_port", 587)
|
|
46
|
+
self.username = config.get("username", "")
|
|
47
|
+
self.password = config.get("password", "")
|
|
48
|
+
self.access_token = config.get("access_token", "")
|
|
49
|
+
|
|
50
|
+
if config.get("enabled", False):
|
|
51
|
+
if self.provider == "smtp":
|
|
52
|
+
if not self.smtp_host or not self.username or not self.password:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"EmailConnector (SMTP) requires smtp_host, username, password"
|
|
55
|
+
)
|
|
56
|
+
elif self.provider in ("gmail", "outlook"):
|
|
57
|
+
if not self.access_token:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"EmailConnector ({self.provider}) requires access_token"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self._client: httpx.AsyncClient | None = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def info(self) -> ConnectorInfo:
|
|
66
|
+
return ConnectorInfo(
|
|
67
|
+
connector_id="email",
|
|
68
|
+
platform="email",
|
|
69
|
+
category="messaging",
|
|
70
|
+
capabilities=frozenset(
|
|
71
|
+
[
|
|
72
|
+
ConnectorCapability.SEND_MESSAGE,
|
|
73
|
+
ConnectorCapability.RECEIVE_MESSAGE,
|
|
74
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
75
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
76
|
+
ConnectorCapability.SEARCH,
|
|
77
|
+
ConnectorCapability.LIST_DATA,
|
|
78
|
+
]
|
|
79
|
+
),
|
|
80
|
+
auth_type="oauth2" if self.provider != "smtp" else "api_key",
|
|
81
|
+
rate_limit_per_minute=100,
|
|
82
|
+
supports_threads=True,
|
|
83
|
+
supports_attachments=True,
|
|
84
|
+
supports_reactions=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
88
|
+
if self._client is None:
|
|
89
|
+
headers = {}
|
|
90
|
+
if self.access_token:
|
|
91
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
92
|
+
self._client = httpx.AsyncClient(timeout=30.0, headers=headers)
|
|
93
|
+
return self._client
|
|
94
|
+
|
|
95
|
+
async def health(self) -> bool:
|
|
96
|
+
"""Check if email service is accessible."""
|
|
97
|
+
try:
|
|
98
|
+
if self.provider == "smtp":
|
|
99
|
+
# Test SMTP connection
|
|
100
|
+
loop = asyncio.get_event_loop()
|
|
101
|
+
return await loop.run_in_executor(None, self._test_smtp)
|
|
102
|
+
elif self.provider == "gmail":
|
|
103
|
+
client = await self._get_client()
|
|
104
|
+
response = await client.get(
|
|
105
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/profile"
|
|
106
|
+
)
|
|
107
|
+
return response.status_code == 200
|
|
108
|
+
elif self.provider == "outlook":
|
|
109
|
+
client = await self._get_client()
|
|
110
|
+
response = await client.get(
|
|
111
|
+
"https://graph.microsoft.com/v1.0/me"
|
|
112
|
+
)
|
|
113
|
+
return response.status_code == 200
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Email health check failed: {e}")
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def _test_smtp(self) -> bool:
|
|
121
|
+
"""Test SMTP connection."""
|
|
122
|
+
try:
|
|
123
|
+
server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
|
124
|
+
server.starttls()
|
|
125
|
+
server.login(self.username, self.password)
|
|
126
|
+
server.quit()
|
|
127
|
+
return True
|
|
128
|
+
except Exception:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
async def _send_impl(self, message: OutboundMessage) -> str:
|
|
132
|
+
"""Send an email."""
|
|
133
|
+
if self.provider == "smtp":
|
|
134
|
+
return await self._send_smtp(message)
|
|
135
|
+
elif self.provider == "gmail":
|
|
136
|
+
return await self._send_gmail(message)
|
|
137
|
+
elif self.provider == "outlook":
|
|
138
|
+
return await self._send_outlook(message)
|
|
139
|
+
|
|
140
|
+
raise ValueError(f"Unknown provider: {self.provider}")
|
|
141
|
+
|
|
142
|
+
async def _send_smtp(self, message: OutboundMessage) -> str:
|
|
143
|
+
"""Send via SMTP."""
|
|
144
|
+
loop = asyncio.get_event_loop()
|
|
145
|
+
|
|
146
|
+
def _send():
|
|
147
|
+
msg = MIMEMultipart()
|
|
148
|
+
msg["From"] = self.username
|
|
149
|
+
msg["To"] = message.recipient_id
|
|
150
|
+
msg["Subject"] = message.attachments[0].get("subject", "No Subject") if message.attachments else "No Subject"
|
|
151
|
+
|
|
152
|
+
msg.attach(MIMEText(message.content, "plain"))
|
|
153
|
+
|
|
154
|
+
server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
|
155
|
+
server.starttls()
|
|
156
|
+
server.login(self.username, self.password)
|
|
157
|
+
server.sendmail(self.username, message.recipient_id, msg.as_string())
|
|
158
|
+
server.quit()
|
|
159
|
+
|
|
160
|
+
return "smtp_sent"
|
|
161
|
+
|
|
162
|
+
return await loop.run_in_executor(None, _send)
|
|
163
|
+
|
|
164
|
+
async def _send_gmail(self, message: OutboundMessage) -> str:
|
|
165
|
+
"""Send via Gmail API."""
|
|
166
|
+
import base64
|
|
167
|
+
|
|
168
|
+
client = await self._get_client()
|
|
169
|
+
|
|
170
|
+
# Build raw email
|
|
171
|
+
email_content = f"To: {message.recipient_id}\r\nSubject: Message\r\n\r\n{message.content}"
|
|
172
|
+
raw = base64.urlsafe_b64encode(email_content.encode()).decode()
|
|
173
|
+
|
|
174
|
+
response = await client.post(
|
|
175
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
|
|
176
|
+
json={"raw": raw},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if response.status_code not in (200, 201):
|
|
180
|
+
raise Exception(f"Gmail send failed: {response.text}")
|
|
181
|
+
|
|
182
|
+
data = response.json()
|
|
183
|
+
return data.get("id", "")
|
|
184
|
+
|
|
185
|
+
async def _send_outlook(self, message: OutboundMessage) -> str:
|
|
186
|
+
"""Send via Outlook/Graph API."""
|
|
187
|
+
client = await self._get_client()
|
|
188
|
+
|
|
189
|
+
payload = {
|
|
190
|
+
"message": {
|
|
191
|
+
"subject": "Message",
|
|
192
|
+
"body": {
|
|
193
|
+
"contentType": "Text",
|
|
194
|
+
"content": message.content,
|
|
195
|
+
},
|
|
196
|
+
"toRecipients": [
|
|
197
|
+
{"emailAddress": {"address": message.recipient_id}}
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
response = await client.post(
|
|
203
|
+
"https://graph.microsoft.com/v1.0/me/sendMail",
|
|
204
|
+
json=payload,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if response.status_code not in (200, 202):
|
|
208
|
+
raise Exception(f"Outlook send failed: {response.text}")
|
|
209
|
+
|
|
210
|
+
return "outlook_sent"
|
|
211
|
+
|
|
212
|
+
async def _receive_impl(self) -> AsyncIterator[InboundMessage]:
|
|
213
|
+
"""Receive emails (via API providers)."""
|
|
214
|
+
if self.provider == "gmail":
|
|
215
|
+
async for msg in self._receive_gmail():
|
|
216
|
+
yield msg
|
|
217
|
+
elif self.provider == "outlook":
|
|
218
|
+
async for msg in self._receive_outlook():
|
|
219
|
+
yield msg
|
|
220
|
+
else:
|
|
221
|
+
# SMTP receive would require IMAP
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
async def _receive_gmail(self) -> AsyncIterator[InboundMessage]:
|
|
225
|
+
"""Receive via Gmail API."""
|
|
226
|
+
client = await self._get_client()
|
|
227
|
+
|
|
228
|
+
response = await client.get(
|
|
229
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages",
|
|
230
|
+
params={"maxResults": 20},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if response.status_code != 200:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
data = response.json()
|
|
237
|
+
|
|
238
|
+
for msg_ref in data.get("messages", []):
|
|
239
|
+
msg_response = await client.get(
|
|
240
|
+
f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{msg_ref['id']}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if msg_response.status_code != 200:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
msg = msg_response.json()
|
|
247
|
+
headers = {h["name"]: h["value"] for h in msg.get("payload", {}).get("headers", [])}
|
|
248
|
+
|
|
249
|
+
yield InboundMessage(
|
|
250
|
+
message_id=msg.get("id", ""),
|
|
251
|
+
platform="email",
|
|
252
|
+
sender_id=headers.get("From", ""),
|
|
253
|
+
sender_name=headers.get("From", ""),
|
|
254
|
+
content=msg.get("snippet", ""),
|
|
255
|
+
thread_id=msg.get("threadId"),
|
|
256
|
+
raw=msg,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
async def _receive_outlook(self) -> AsyncIterator[InboundMessage]:
|
|
260
|
+
"""Receive via Outlook/Graph API."""
|
|
261
|
+
client = await self._get_client()
|
|
262
|
+
|
|
263
|
+
response = await client.get(
|
|
264
|
+
"https://graph.microsoft.com/v1.0/me/messages",
|
|
265
|
+
params={"$top": 20},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if response.status_code != 200:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
data = response.json()
|
|
272
|
+
|
|
273
|
+
for msg in data.get("value", []):
|
|
274
|
+
sender = msg.get("from", {}).get("emailAddress", {})
|
|
275
|
+
|
|
276
|
+
yield InboundMessage(
|
|
277
|
+
message_id=msg.get("id", ""),
|
|
278
|
+
platform="email",
|
|
279
|
+
sender_id=sender.get("address", ""),
|
|
280
|
+
sender_name=sender.get("name", sender.get("address", "")),
|
|
281
|
+
content=msg.get("bodyPreview", ""),
|
|
282
|
+
thread_id=msg.get("conversationId"),
|
|
283
|
+
raw=msg,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def list_inbox(
|
|
287
|
+
self,
|
|
288
|
+
max_results: int = 20,
|
|
289
|
+
query: str = "",
|
|
290
|
+
) -> list[dict]:
|
|
291
|
+
"""List inbox messages."""
|
|
292
|
+
if self.provider == "gmail":
|
|
293
|
+
client = await self._get_client()
|
|
294
|
+
params = {"maxResults": max_results}
|
|
295
|
+
if query:
|
|
296
|
+
params["q"] = query
|
|
297
|
+
|
|
298
|
+
response = await client.get(
|
|
299
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages",
|
|
300
|
+
params=params,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if response.status_code != 200:
|
|
304
|
+
raise Exception(f"Gmail list failed: {response.text}")
|
|
305
|
+
|
|
306
|
+
return response.json().get("messages", [])
|
|
307
|
+
|
|
308
|
+
elif self.provider == "outlook":
|
|
309
|
+
client = await self._get_client()
|
|
310
|
+
params = {"$top": max_results}
|
|
311
|
+
if query:
|
|
312
|
+
params["$search"] = f'"{query}"'
|
|
313
|
+
|
|
314
|
+
response = await client.get(
|
|
315
|
+
"https://graph.microsoft.com/v1.0/me/messages",
|
|
316
|
+
params=params,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if response.status_code != 200:
|
|
320
|
+
raise Exception(f"Outlook list failed: {response.text}")
|
|
321
|
+
|
|
322
|
+
return response.json().get("value", [])
|
|
323
|
+
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
async def search_emails(self, query: str) -> list[dict]:
|
|
327
|
+
"""Search emails."""
|
|
328
|
+
return await self.list_inbox(max_results=50, query=query)
|
|
329
|
+
|
|
330
|
+
async def get_thread(self, thread_id: str) -> dict:
|
|
331
|
+
"""Get an email thread."""
|
|
332
|
+
client = await self._get_client()
|
|
333
|
+
|
|
334
|
+
if self.provider == "gmail":
|
|
335
|
+
response = await client.get(
|
|
336
|
+
f"https://gmail.googleapis.com/gmail/v1/users/me/threads/{thread_id}"
|
|
337
|
+
)
|
|
338
|
+
elif self.provider == "outlook":
|
|
339
|
+
response = await client.get(
|
|
340
|
+
f"https://graph.microsoft.com/v1.0/me/messages",
|
|
341
|
+
params={"$filter": f"conversationId eq '{thread_id}'"},
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
return {}
|
|
345
|
+
|
|
346
|
+
if response.status_code != 200:
|
|
347
|
+
raise Exception(f"Get thread failed: {response.text}")
|
|
348
|
+
|
|
349
|
+
return response.json()
|
|
350
|
+
|
|
351
|
+
async def create_draft(
|
|
352
|
+
self,
|
|
353
|
+
to: str,
|
|
354
|
+
subject: str,
|
|
355
|
+
body: str,
|
|
356
|
+
) -> str:
|
|
357
|
+
"""Create an email draft."""
|
|
358
|
+
client = await self._get_client()
|
|
359
|
+
|
|
360
|
+
if self.provider == "gmail":
|
|
361
|
+
import base64
|
|
362
|
+
|
|
363
|
+
email_content = f"To: {to}\r\nSubject: {subject}\r\n\r\n{body}"
|
|
364
|
+
raw = base64.urlsafe_b64encode(email_content.encode()).decode()
|
|
365
|
+
|
|
366
|
+
response = await client.post(
|
|
367
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/drafts",
|
|
368
|
+
json={"message": {"raw": raw}},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
elif self.provider == "outlook":
|
|
372
|
+
response = await client.post(
|
|
373
|
+
"https://graph.microsoft.com/v1.0/me/messages",
|
|
374
|
+
json={
|
|
375
|
+
"subject": subject,
|
|
376
|
+
"body": {"contentType": "Text", "content": body},
|
|
377
|
+
"toRecipients": [{"emailAddress": {"address": to}}],
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
raise ValueError("Draft creation requires Gmail or Outlook provider")
|
|
382
|
+
|
|
383
|
+
if response.status_code not in (200, 201):
|
|
384
|
+
raise Exception(f"Create draft failed: {response.text}")
|
|
385
|
+
|
|
386
|
+
return response.json().get("id", "")
|
|
387
|
+
|
|
388
|
+
async def _search_impl(self, query: str, **kwargs) -> list[dict]:
|
|
389
|
+
"""Search implementation."""
|
|
390
|
+
return await self.search_emails(query)
|
|
391
|
+
|
|
392
|
+
async def _list_impl(
|
|
393
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
394
|
+
) -> list[dict]:
|
|
395
|
+
"""List implementation."""
|
|
396
|
+
if resource_type == "inbox":
|
|
397
|
+
return await self.list_inbox(
|
|
398
|
+
max_results=filters.get("max_results", 20),
|
|
399
|
+
query=filters.get("query", ""),
|
|
400
|
+
)
|
|
401
|
+
return []
|