diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b9bc5d..225516ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @agentuity/sdk Changelog +## 0.0.155 + +### Patch Changes + +- Added functionality to send email. + ## 0.0.154 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index a8acbaba..952f05c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentuity/sdk", - "version": "0.0.154", + "version": "0.0.155", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentuity/sdk", - "version": "0.0.154", + "version": "0.0.155", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/package.json b/package.json index 9fc04aca..3cb7d91d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.154", + "version": "0.0.155", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, diff --git a/src/apis/email.ts b/src/apis/email.ts index 208329be..f43c8408 100644 --- a/src/apis/email.ts +++ b/src/apis/email.ts @@ -4,6 +4,58 @@ import type { EmailService } from '../types'; import { POST } from './api'; export default class EmailApi implements EmailService { + /** + * send an email + */ + async send( + agentId: string, + email: string, + authToken: string, + messageId: string + ): Promise { + const tracer = getTracer(); + const currentContext = context.active(); + + // Create a child span using the current context + const span = tracer.startSpan('agentuity.email.send', {}, currentContext); + + try { + // Create a new context with the child span + const spanContext = trace.setSpan(currentContext, span); + + // Execute the operation within the new context + return await context.with(spanContext, async () => { + span.setAttribute('@agentuity/agentId', agentId); + span.setAttribute('@agentuity/emailMessageId', messageId); + + const resp = await POST( + '/email/send', + email, + { + 'Content-Type': 'message/rfc822', + 'X-Agentuity-Message-Id': messageId, + }, + undefined, + authToken + ); + if (resp.status === 200) { + span.setStatus({ code: SpanStatusCode.OK }); + return; + } + const body = await resp.response.text(); + span.setStatus({ code: SpanStatusCode.ERROR, message: body }); + throw new Error( + `error sending email: ${resp.response.statusText} (${resp.response.status})${body}` + ); + }); + } catch (ex) { + recordException(span, ex); + throw ex; + } finally { + span.end(); + } + } + /** * send an email reply to an incoming email */ diff --git a/src/io/email.ts b/src/io/email.ts index ad68ddeb..52c0015a 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -459,6 +459,97 @@ export class Email { return ''; } + /** + * Send a new email to the specified recipients. + * + * @param req - The agent request containing metadata with email-auth-token + * @param context - The agent context + * @param to - Array of recipient email addresses + * @param email - The email content (subject, text, html, attachments) + * @param from - Optional sender information (defaults to the recipient of the incoming email) + * @returns Promise resolving to the message ID of the sent email + * @throws Error if email-auth-token is missing, recipients list is empty, or from address is invalid + */ + async send( + req: AgentRequest, + context: AgentContext, + to: string[], + email: EmailReply, + from?: { + name?: string; + email?: string; + } + ): Promise { + const authToken = req.metadata?.['email-auth-token'] as string; + if (!authToken) { + throw new Error( + 'email authorization token is required but not found in metadata' + ); + } + + return (async () => { + let attachments: Attachment[] = []; + if (email.attachments) { + attachments = await Promise.all( + email.attachments.map(async (attachment) => { + const resp = await fromDataType(attachment.data); + return { + filename: attachment.filename, + content: await resp.data.buffer(), + contentType: resp.data.contentType, + contentDisposition: + attachment.contentDisposition ?? ('attachment' as const), + }; + }) + ); + } + + const normalizedTo = to.map((addr) => addr.trim()).filter(Boolean); + if (normalizedTo.length === 0) { + throw new Error('at least one recipient email is required'); + } + + const fromAddress = from?.email ?? this.toEmail(); + if (!fromAddress) { + throw new Error('a valid from email address is required'); + } + + const mail = new MailComposer({ + date: new Date(), + from: { + name: from?.name ?? context.agent.name, + address: fromAddress, + }, + to: normalizedTo.join(', '), + subject: email.subject ?? '', + text: email.text, + html: email.html, + attachments, + }); + const newemail = mail.compile(); + + return new Promise((resolve, reject) => { + newemail.build(async (err, message) => { + if (err) { + reject(err); + } else { + try { + await context.email.send( + context.agent.id, + message.toString(), + authToken, + newemail.messageId() + ); + resolve(newemail.messageId()); + } catch (ex) { + reject(ex); + } + } + }); + }); + })(); + } + /** * send a reply to the email */ diff --git a/src/types.ts b/src/types.ts index cdd067eb..09c0487c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -573,6 +573,16 @@ export interface VectorStorage { * EmailService provides a way to send email replies to incoming emails */ export interface EmailService { + /** + * Send a new email to the specified recipients. + */ + send( + agentId: string, + email: string, + authToken: string, + messageId: string + ): Promise; + /** * send an email reply to an incoming email * diff --git a/test/io/email.test.ts b/test/io/email.test.ts index 7fbb2191..d74f28d0 100644 --- a/test/io/email.test.ts +++ b/test/io/email.test.ts @@ -56,7 +56,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments()).not.toThrow(); + expect(email.attachments).toBeDefined(); const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -82,7 +82,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments()).not.toThrow(); + expect(email.attachments).toBeDefined(); const attachments = email.attachments(); expect(attachments).toHaveLength(0); });