Files
microdao-daarion/tools/agent_email/agent_email.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

963 lines
32 KiB
Python

"""
AgentEmailTool - Email tool for AI agents
Uses AgentMail.to as primary SDK, with fallback to smtplib/imap-tools
This tool provides email capabilities for DAARION agents:
- Create/manage email inboxes
- Send/receive emails
- Email authentication (OTP, magic links)
- Analyze email content
"""
import os
import re
import json
import asyncio
import logging
import tempfile
import hashlib
import time
from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
from dataclasses import dataclass, field
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import aiofiles
# Security
from cryptography.fernet import Fernet
import base64
# Rate limiting
from collections import defaultdict
from threading import Lock
# AgentMail SDK
try:
from agentmail import AgentMail, AsyncAgentMail
AGENTMAIL_AVAILABLE = True
except ImportError:
AGENTMAIL_AVAILABLE = False
# Fallback
try:
import smtplib
import imaplib
from email.parser import Parser
FALLBACK_AVAILABLE = True
except ImportError:
FALLBACK_AVAILABLE = False
logger = logging.getLogger(__name__)
# ============================================================================
# CONFIGURATION & RATE LIMITING
# ============================================================================
@dataclass
class EmailConfig:
"""Email tool configuration"""
agentmail_api_key: Optional[str] = None
# Fallback SMTP/IMAP settings
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
imap_host: str = "imap.gmail.com"
imap_port: int = 993
# Security
encryption_key: Optional[bytes] = None
credentials_dir: str = "/tmp/agent_email_creds"
temp_dir: str = "/tmp/agent_email_attachments"
# Rate limiting
rate_limit_per_minute: int = 8
# PII protection
redact_pii_in_logs: bool = True
class RateLimiter:
"""Simple rate limiter for email operations"""
def __init__(self, max_per_minute: int = 8):
self.max_per_minute = max_per_minute
self.requests: Dict[str, List[float]] = defaultdict(list)
self.lock = Lock()
def is_allowed(self, agent_id: str) -> bool:
"""Check if request is allowed for agent"""
with self.lock:
now = time.time()
# Clean old requests
self.requests[agent_id] = [
t for t in self.requests[agent_id]
if now - t < 60
]
# Check limit
if len(self.requests[agent_id]) >= self.max_per_minute:
return False
# Add request
self.requests[agent_id].append(now)
return True
def wait_time(self, agent_id: str) -> float:
"""Get seconds to wait before next request"""
with self.lock:
if agent_id not in self.requests or not self.requests[agent_id]:
return 0
oldest = min(self.requests[agent_id])
return max(0, 60 - (time.time() - oldest))
# Global rate limiter
rate_limiter = RateLimiter()
# ============================================================================
# CREDENTIALS STORAGE (ENCRYPTED)
# ============================================================================
class SecureCredentialsStore:
"""Encrypted credentials storage using Fernet"""
def __init__(self, config: EmailConfig):
self.config = config
self.credentials_dir = Path(config.credentials_dir)
self.credentials_dir.mkdir(parents=True, exist_ok=True)
# Initialize encryption
if config.encryption_key:
self.fernet = Fernet(config.encryption_key)
else:
# Generate key if not provided
key = Fernet.generate_key()
self.fernet = Fernet(key)
# In production, store this key securely (e.g., in Second Me)
logger.warning("No encryption key provided, generated temporary key")
def _get_file_path(self, agent_id: str) -> Path:
"""Get credentials file path for agent"""
# Hash agent_id for filename
agent_hash = hashlib.sha256(agent_id.encode()).hexdigest()[:16]
return self.credentials_dir / f"creds_{agent_hash}.enc"
def save(self, agent_id: str, credentials: Dict[str, Any]) -> None:
"""Save encrypted credentials"""
file_path = self._get_file_path(agent_id)
data = json.dumps(credentials).encode()
encrypted = self.fernet.encrypt(data)
file_path.write_bytes(encrypted)
logger.info(f"Credentials saved for agent: {self._redact_agent(agent_id)}")
def load(self, agent_id: str) -> Optional[Dict[str, Any]]:
"""Load decrypted credentials"""
file_path = self._get_file_path(agent_id)
if not file_path.exists():
return None
try:
encrypted = file_path.read_bytes()
decrypted = self.fernet.decrypt(encrypted)
return json.loads(decrypted)
except Exception as e:
logger.error(f"Failed to load credentials: {e}")
return None
def delete(self, agent_id: str) -> None:
"""Delete credentials"""
file_path = self._get_file_path(agent_id)
if file_path.exists():
file_path.unlink()
logger.info(f"Credentials deleted for agent: {self._redact_agent(agent_id)}")
def _redact_agent(self, agent_id: str) -> str:
"""Redact agent_id for logging"""
if len(agent_id) > 8:
return f"{agent_id[:4]}...{agent_id[-4:]}"
return "****"
# ============================================================================
# PII & SECURITY HELPERS
# ============================================================================
class PIIGuard:
"""PII protection for logging and auditing"""
# Patterns to redact
PATTERNS = [
(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]'),
(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', '[CARD]'),
(r'\b\d{6,10}\b', '[OTP]'),
(r'(?i)(password|passwd|pwd)[=:\s]+\S+', '[PASSWORD]'),
(r'(?i)(token|api[_-]?key|secret)[=:\s]+\S+', '[TOKEN]'),
]
@classmethod
def redact(cls, text: str) -> str:
"""Redact PII from text"""
if not text:
return text
result = text
for pattern, replacement in cls.PATTERNS:
result = re.sub(pattern, replacement, result)
return result
@classmethod
def redact_dict(cls, data: Dict) -> Dict:
"""Redact PII from dictionary"""
return {k: cls.redact(str(v)) for k, v in data.items()}
class AuditLogger:
"""Audit log for email operations (no PII)"""
def __init__(self, log_dir: str = "/tmp/agent_email_logs"):
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
def log(self, agent_id: str, operation: str, details: Dict[str, Any]) -> None:
"""Log operation without PII"""
safe_details = PIIGuard.redact_dict(details)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"agent_id": hashlib.sha256(agent_id.encode()).hexdigest()[:16],
"operation": operation,
"details": safe_details
}
log_file = self.log_dir / f"audit_{datetime.utcnow().strftime('%Y%m%d')}.jsonl"
with open(log_file, "a") as f:
f.write(json.dumps(log_entry) + "\n")
# ============================================================================
# MAIN EMAIL TOOL CLASS
# ============================================================================
class AgentEmailTool:
"""
Email tool for AI agents.
Provides email inbox management, send/receive, and authentication automation.
Usage:
tool = AgentEmailTool(agent_id="sofiia")
inbox = tool.create_inbox()
tool.send(to=["user@example.com"], subject="Hello", body="Test")
emails = tool.receive(unread_only=True)
"""
def __init__(
self,
agent_id: str,
config: Optional[EmailConfig] = None,
api_key: Optional[str] = None
):
"""
Initialize email tool for agent.
Args:
agent_id: Unique identifier for the agent
config: Email configuration (uses env vars if not provided)
api_key: AgentMail API key (overrides config)
"""
self.agent_id = agent_id
self.config = config or self._load_config()
self.api_key = api_key or self.config.agentmail_api_key or os.getenv("AGENTMAIL_API_KEY")
# Initialize storage and logging
self.creds_store = SecureCredentialsStore(self.config)
self.audit = AuditLogger()
# Client (initialized lazily)
self._client = None
self._async_client = None
# Temp directory for attachments
self.temp_dir = Path(self.config.temp_dir)
self.temp_dir.mkdir(parents=True, exist_ok=True)
def _load_config(self) -> EmailConfig:
"""Load config from environment"""
return EmailConfig(
agentmail_api_key=os.getenv("AGENTMAIL_API_KEY"),
encryption_key=os.getenv("EMAIL_ENCRYPTION_KEY", "").encode() or None,
credentials_dir=os.getenv("EMAIL_CREDS_DIR", "/tmp/agent_email_creds"),
rate_limit_per_minute=int(os.getenv("EMAIL_RATE_LIMIT", "8")),
)
@property
def client(self):
"""Lazy-load AgentMail client"""
if not self._client and AGENTMAIL_AVAILABLE and self.api_key:
self._client = AgentMail(api_key=self.api_key)
return self._client
@property
def async_client(self):
"""Lazy-load async AgentMail client"""
if not self._async_client and AGENTMAIL_AVAILABLE and self.api_key:
self._async_client = AsyncAgentMail(api_key=self.api_key)
return self._async_client
# ========================================================================
# RATE LIMITING
# ========================================================================
def _check_rate_limit(self) -> None:
"""Check rate limit, raise if exceeded"""
if not rate_limiter.is_allowed(self.agent_id):
wait = rate_limiter.wait_time(self.agent_id)
raise RateLimitError(f"Rate limit exceeded. Wait {wait:.1f}s")
# ========================================================================
# INBOX MANAGEMENT
# ========================================================================
def create_inbox(
self,
username: Optional[str] = None,
domain: Optional[str] = None,
display_name: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new email inbox for the agent.
Args:
username: Optional username (defaults to agent_id) - IGNORED in AgentMail API
domain: Optional domain - IGNORED in AgentMail API
display_name: Optional display name
Returns:
Dict with inbox details: inbox_id, email_address, api_key, etc.
Raises:
RateLimitError: If rate limit exceeded
EmailError: If creation fails
"""
self._check_rate_limit()
try:
if self.client:
inbox = self.client.inboxes.create()
result = {
"inbox_id": inbox.inbox_id,
"email_address": inbox.inbox_id,
"display_name": display_name or inbox.display_name,
"created_at": datetime.utcnow().isoformat(),
"provider": "agentmail"
}
# Store credentials
self.creds_store.save(self.agent_id, {
"inbox_id": result["inbox_id"],
"email": result["email_address"],
"provider": "agentmail",
"created_at": result["created_at"]
})
self.audit.log(self.agent_id, "create_inbox", result)
logger.info(f"Inbox created: {result['email_address']}")
return result
else:
raise EmailError("AgentMail not available and fallback not implemented")
except Exception as e:
self.audit.log(self.agent_id, "create_inbox_error", {"error": str(e)})
raise EmailError(f"Failed to create inbox: {e}")
def list_inboxes(self) -> List[Dict[str, Any]]:
"""List all inboxes for this agent"""
self._check_rate_limit()
creds = self.creds_store.load(self.agent_id)
if not creds:
return []
try:
if self.client:
response = self.client.inboxes.list()
return [
{
"inbox_id": ib.inbox_id,
"email": ib.inbox_id,
}
for ib in response.inboxes
]
except Exception as e:
logger.error(f"Failed to list inboxes: {e}")
return []
def delete_inbox(self, inbox_id: Optional[str] = None) -> Dict[str, Any]:
"""
Delete an inbox.
Args:
inbox_id: Specific inbox to delete (uses stored if not provided)
Returns:
Deletion confirmation
"""
self._check_rate_limit()
inbox_id = inbox_id or (self.creds_store.load(self.agent_id) or {}).get("inbox_id")
if not inbox_id:
raise EmailError("No inbox_id provided and no stored inbox found")
try:
if self.client:
self.client.inboxes.delete(inbox_id=inbox_id)
self.creds_store.delete(self.agent_id)
self.audit.log(self.agent_id, "delete_inbox", {"inbox_id": inbox_id})
return {"status": "deleted", "inbox_id": inbox_id}
except Exception as e:
raise EmailError(f"Failed to delete inbox: {e}")
# ========================================================================
# SENDING EMAILS
# ========================================================================
def send(
self,
to: List[str],
subject: str,
body: str,
html: Optional[str] = None,
attachments: Optional[List[str]] = None,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
inbox_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send an email.
Args:
to: Recipient email addresses
subject: Email subject
body: Plain text body
html: HTML body (optional)
attachments: List of file paths to attach
cc: CC recipients
bcc: BCC recipients
inbox_id: Specific inbox to send from (uses stored if not provided)
Returns:
Dict with send confirmation: message_id, sent_at, etc.
Raises:
RateLimitError: If rate limit exceeded
EmailError: If send fails
"""
self._check_rate_limit()
# Get inbox_id
inbox_id = inbox_id or (self.creds_store.load(self.agent_id) or {}).get("inbox_id")
if not inbox_id:
raise EmailError("No inbox_id provided and no stored inbox found")
# Process attachments securely
attachment_paths = []
if attachments:
for path in attachments:
p = Path(path)
if p.exists() and p.is_file():
# Copy to temp dir (security)
temp_path = self.temp_dir / f"{time.time()}_{p.name}"
import shutil
shutil.copy2(p, temp_path)
attachment_paths.append(str(temp_path))
try:
if self.client:
result = self.client.inboxes.messages.send(
inbox_id=inbox_id,
to=to,
subject=subject,
text=body,
html=html or f"<p>{body.replace(chr(10), '<br>')}</p>",
attachments=attachment_paths or None,
cc=cc,
bcc=bcc
)
response = {
"status": "sent",
"message_id": result.message_id if hasattr(result, 'message_id') else str(result),
"sent_at": datetime.utcnow().isoformat(),
"to": to,
"subject": PIIGuard.redact(subject)
}
self.audit.log(self.agent_id, "send_email", response)
logger.info(f"Email sent: {subject}")
# Cleanup temp attachments
for path in attachment_paths:
try:
Path(path).unlink()
except:
pass
return response
else:
# Fallback to smtplib
return self._send_fallback(
to, subject, body, html, attachments, inbox_id
)
except Exception as e:
self.audit.log(self.agent_id, "send_error", {"error": str(e)})
raise EmailError(f"Failed to send email: {e}")
def _send_fallback(
self,
to: List[str],
subject: str,
body: str,
html: Optional[str],
attachments: Optional[List[str]],
inbox_id: str
) -> Dict[str, Any]:
"""Fallback send using smtplib"""
if not FALLBACK_AVAILABLE:
raise EmailError("No email provider available")
creds = self.creds_store.load(self.agent_id)
if not creds or not creds.get("smtp"):
raise EmailError("No fallback credentials configured")
smtp_creds = creds["smtp"]
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = creds.get("email", inbox_id)
msg['To'] = ", ".join(to)
msg.attach(MIMEText(body, 'plain'))
if html:
msg.attach(MIMEText(html, 'html'))
# Attachments
for filepath in attachments or []:
with open(filepath, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename={Path(filepath).name}')
msg.attach(part)
with smtplib.SMTP(smtp_creds["host"], smtp_creds["port"]) as server:
server.starttls()
server.login(smtp_creds["user"], smtp_creds["pass"])
server.send_message(msg)
return {"status": "sent", "provider": "fallback"}
# ========================================================================
# RECEIVING EMAILS
# ========================================================================
def receive(
self,
unread_only: bool = True,
limit: int = 20,
query: Optional[str] = None,
inbox_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
Receive emails.
Args:
unread_only: Only get unread messages
limit: Maximum number of messages
query: Search query
inbox_id: Specific inbox (uses stored if not provided)
Returns:
List of email dicts with keys: id, from, to, subject, body, date, attachments, raw_html
"""
self._check_rate_limit()
inbox_id = inbox_id or (self.creds_store.load(self.agent_id) or {}).get("inbox_id")
if not inbox_id:
raise EmailError("No inbox_id provided")
try:
if self.client:
labels = ["unread"] if unread_only else None
response = self.client.inboxes.messages.list(
inbox_id=inbox_id,
limit=limit,
labels=labels
)
results = []
for msg in response.messages:
results.append({
"id": msg.message_id,
"from": msg.from_,
"to": msg.to,
"subject": msg.subject,
"body": msg.preview,
"html": getattr(msg, "html_content", ""),
"date": str(msg.timestamp),
"attachments": msg.attachments or [],
"labels": msg.labels or [],
"raw_html": ""
})
self.audit.log(self.agent_id, "receive_emails", {
"count": len(results),
"query": query
})
return results
else:
return self._receive_fallback(inbox_id, unread_only, limit)
except Exception as e:
self.audit.log(self.agent_id, "receive_error", {"error": str(e)})
raise EmailError(f"Failed to receive emails: {e}")
def _receive_fallback(
self,
inbox_id: str,
unread_only: bool,
limit: int
) -> List[Dict[str, Any]]:
"""Fallback receive using imaplib"""
creds = self.creds_store.load(self.agent_id)
if not creds or not creds.get("imap"):
raise EmailError("No fallback credentials")
imap_creds = creds["imap"]
with imaplib.IMAP4_SSL(imap_creds["host"], imap_creds["port"]) as mail:
mail.login(imap_creds["user"], imap_creds["pass"])
mail.select('INBOX')
status, data = mail.search(None, 'UNSEEN' if unread_only else 'ALL')
email_ids = data[0].split()[-limit:]
results = []
for eid in email_ids:
status, msg_data = mail.fetch(eid, '(RFC822)')
email_message = Parser().parsestr(msg_data[0][1])
results.append({
"id": email_message['Message-ID'],
"from": email_message['From'],
"to": email_message['To'],
"subject": email_message['Subject'],
"body": email_message.get_payload(),
"date": email_message['Date'],
"attachments": [],
"raw_html": ""
})
return results
# ========================================================================
# FORWARDING
# ========================================================================
def forward(
self,
email_id: str,
to: List[str],
inbox_id: Optional[str] = None
) -> Dict[str, Any]:
"""Forward an email to new recipients."""
self._check_rate_limit()
# Get original email
emails = self.receive(limit=100, inbox_id=inbox_id)
original = next((e for e in emails if e["id"] == email_id), None)
if not original:
raise EmailError(f"Email {email_id} not found")
# Forward with original content
forwarded_body = f"""
---------- Forwarded message ----------
From: {original['from']}
Subject: {original['subject']}
Date: {original['date']}
{original['body']}
"""
return self.send(
to=to,
subject=f"Fwd: {original['subject']}",
body=forwarded_body,
inbox_id=inbox_id
)
# ========================================================================
# EMAIL ANALYSIS
# ========================================================================
def analyze_and_extract(self, email_dict: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyze email and extract structured information.
Extracts:
- summary: Brief summary of email
- action_items: Any action items mentioned
- credentials: OTP, tokens, passwords found
- auth_urls: URLs for authentication
- files: Any file references
Args:
email_dict: Email dict from receive()
Returns:
Dict with extracted information
"""
body = email_dict.get("body", "")
html = email_dict.get("html", "")
subject = email_dict.get("subject", "")
result = {
"summary": self._extract_summary(subject, body),
"action_items": self._extract_action_items(body),
"credentials": self._extract_credentials(body),
"auth_urls": self._extract_auth_urls(body + html),
"files": self._extract_files(body + html),
"sentiment": self._analyze_sentiment(body)
}
self.audit.log(self.agent_id, "analyze_email", {
"email_id": email_dict.get("id", "unknown")
})
return result
def _extract_summary(self, subject: str, body: str) -> str:
"""Extract brief summary"""
lines = body.split('\n')
preview = ' '.join(lines[:3])[:200]
return f"Re: {subject} - {preview}..."
def _extract_action_items(self, body: str) -> List[str]:
"""Extract action items"""
action_patterns = [
r'(?i)(?:please|kindly|could you|would you)\s+(.+?)(?:\.|$)',
r'(?i)(?:action required|todo|to-do|deadline)\s*:?\s*(.+?)(?:\.|$)',
]
items = []
for pattern in action_patterns:
items.extend(re.findall(pattern, body))
return items[:5]
def _extract_credentials(self, body: str) -> List[Dict[str, str]]:
"""Extract credentials/OTP/tokens from email"""
credentials = []
# OTP patterns
otp_patterns = [
r'(?i)(?:code|otp|verification code|one-time)\s*:?\s*(\d{4,8})',
r'(?i)(\d{4,6})\s*(?:is your|is the|code)',
]
for pattern in otp_patterns:
matches = re.findall(pattern, body)
for match in matches:
credentials.append({
"type": "otp",
"value": match,
"redacted": f"{match[:2]}***{match[-2:]}"
})
return credentials
def _extract_auth_urls(self, text: str) -> List[str]:
"""Extract authentication URLs"""
url_pattern = r'https?://[^\s<>"\']+(?:reset|verify|login|auth|confirm|link|click)[^\s<>"\']*'
urls = re.findall(url_pattern, text, re.IGNORECASE)
return list(set(urls))[:5]
def _extract_files(self, text: str) -> List[str]:
"""Extract file references"""
patterns = [
r'(?i)(?:attached|attachment|file):\s*([^\s]+)',
r'(?i)\.pdf[^\s]*\.pdf',
r'(?i)\.doc[^\s]*\.doc',
r'(?i)\.xlsx?[^\s]*\.xls',
]
files = []
for pattern in patterns:
files.extend(re.findall(pattern, text))
return list(set(files))[:5]
def _analyze_sentiment(self, body: str) -> str:
"""Simple sentiment analysis"""
positive = ['thank', 'great', 'excellent', 'appreciate', 'welcome']
negative = ['sorry', 'issue', 'problem', 'error', 'failed', 'urgent']
body_lower = body.lower()
pos_count = sum(1 for w in positive if w in body_lower)
neg_count = sum(1 for w in negative if w in body_lower)
if pos_count > neg_count:
return "positive"
elif neg_count > pos_count:
return "negative"
return "neutral"
# ========================================================================
# AUTHENTICATION AUTOMATION
# ========================================================================
async def use_for_auth(
self,
service_url: str,
email_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Use email for authentication (OTP, magic links).
Attempts to:
1. Extract OTP/token from email
2. Navigate to service and input OTP
3. Or click magic link
Args:
service_url: URL of service to authenticate with
email_data: Email dict containing OTP/link
Returns:
Dict with auth result: success, method, details
"""
analysis = self.analyze_and_extract(email_data)
result = {
"service_url": service_url,
"method": None,
"success": False,
"details": {}
}
# Try magic links first
if analysis["auth_urls"]:
# In production, would use browser automation
result["method"] = "magic_link"
result["details"]["urls_found"] = analysis["auth_urls"]
result["success"] = True # Would click link
# Try OTP
elif analysis["credentials"]:
otp = analysis["credentials"][0]["value"]
result["method"] = "otp"
result["details"]["otp"] = analysis["credentials"][0]["redacted"]
result["success"] = True # Would input OTP
self.audit.log(self.agent_id, "auth_attempt", result)
return result
# ========================================================================
# ASYNC OPERATIONS
# ========================================================================
async def create_inbox_async(
self,
username: Optional[str] = None,
domain: Optional[str] = None
) -> Dict[str, Any]:
"""Async version of create_inbox"""
if not self.async_client:
return self.create_inbox(username, domain)
self._check_rate_limit()
username = username or self.agent_id.lower().replace("_", "-")
inbox = await self.async_client.inboxes.create(
username=username,
domain=domain
)
return {
"inbox_id": str(inbox),
"email_address": f"{username}@{domain or 'agentmail.to'}",
"provider": "agentmail"
}
async def receive_async(
self,
unread_only: bool = True,
limit: int = 20,
inbox_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Async version of receive"""
if not self.async_client:
return self.receive(unread_only, limit, inbox_id=inbox_id)
self._check_rate_limit()
inbox_id = inbox_id or (self.creds_store.load(self.agent_id) or {}).get("inbox_id")
messages = await self.async_client.inboxes.messages.list(
inbox_id=inbox_id,
limit=limit,
unread_only=unread_only
)
return [
{
"id": str(m),
"body": getattr(m, "text_content", "")
}
for m in messages
]
# ============================================================================
# ERROR CLASSES
# ============================================================================
class EmailError(Exception):
"""Base email error"""
pass
class RateLimitError(EmailError):
"""Rate limit exceeded"""
pass
# ============================================================================
# REGISTRATION FOR OCTOTOOLS
# ============================================================================
def register_tools() -> Dict[str, Any]:
"""
Register AgentEmailTool with OctoTools.
Returns:
Dict of tool definitions
"""
return {
"agent_email": {
"class": AgentEmailTool,
"description": "Email tool for AI agents - send/receive emails, manage inboxes, automation",
"methods": [
"create_inbox",
"list_inboxes",
"delete_inbox",
"send",
"receive",
"forward",
"analyze_and_extract",
"use_for_auth"
]
}
}