Send an email via SMTP. Args: config: Resolved email config (from resolve_config) to: Recipient email address(es), comma-separated subject: Email subject body: Email body text cc: CC recipients, comma-separated attachments: List of workspace-relat
(
config: dict,
to: str,
subject: str,
body: str,
cc: Optional[str] = None,
attachments: Optional[list[str]] = None,
workspace_path: Optional[Path] = None,
agent_id: Optional[uuid.UUID] = None,
)
| 154 | |
| 155 | |
| 156 | async def send_email( |
| 157 | config: dict, |
| 158 | to: str, |
| 159 | subject: str, |
| 160 | body: str, |
| 161 | cc: Optional[str] = None, |
| 162 | attachments: Optional[list[str]] = None, |
| 163 | workspace_path: Optional[Path] = None, |
| 164 | agent_id: Optional[uuid.UUID] = None, |
| 165 | ) -> str: |
| 166 | """Send an email via SMTP. |
| 167 | |
| 168 | Args: |
| 169 | config: Resolved email config (from resolve_config) |
| 170 | to: Recipient email address(es), comma-separated |
| 171 | subject: Email subject |
| 172 | body: Email body text |
| 173 | cc: CC recipients, comma-separated |
| 174 | attachments: List of workspace-relative file paths to attach |
| 175 | workspace_path: Agent workspace root for resolving attachment paths |
| 176 | agent_id: Optional UUID of the agent for retrieving files from storage |
| 177 | """ |
| 178 | cfg = resolve_config(config) |
| 179 | addr = cfg["email_address"] |
| 180 | password = cfg["auth_code"] |
| 181 | |
| 182 | if not addr or not password: |
| 183 | return "❌ Email not configured. Please set email address and authorization code in tool config." |
| 184 | |
| 185 | msg = MIMEMultipart() |
| 186 | msg["From"] = addr |
| 187 | msg["To"] = to |
| 188 | msg["Subject"] = subject |
| 189 | if cc: |
| 190 | msg["Cc"] = cc |
| 191 | msg["Message-ID"] = make_msgid() |
| 192 | msg["Date"] = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z") |
| 193 | |
| 194 | msg.attach(MIMEText(body, "plain", "utf-8")) |
| 195 | |
| 196 | # Attach files |
| 197 | if attachments and workspace_path: |
| 198 | from app.services.storage import get_storage_backend, normalize_storage_key |
| 199 | storage = get_storage_backend() |
| 200 | |
| 201 | for rel_path in attachments: |
| 202 | clean_rel = rel_path.replace("\\", "/").strip().lstrip("/") |
| 203 | prefix = str(agent_id) if agent_id else workspace_path.name |
| 204 | storage_key = normalize_storage_key(f"{prefix}/{clean_rel}") |
| 205 | file_bytes = None |
| 206 | filename = Path(clean_rel).name |
| 207 | |
| 208 | # 1. Try to read from the storage backend (e.g. S3 or local storage) |
| 209 | try: |
| 210 | if await storage.exists(storage_key) and await storage.is_file(storage_key): |
| 211 | file_bytes = await storage.read_bytes(storage_key) |
| 212 | except Exception: |
| 213 | pass |
no test coverage detected