From f1410c14289658fc5288669c7ea7439f1d8c212a Mon Sep 17 00:00:00 2001 From: huijiro Date: Mon, 6 Oct 2025 18:46:19 -0300 Subject: [PATCH 01/14] Added function to send email to specific emails addresses --- src/io/email.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/io/email.ts b/src/io/email.ts index ad68ddeb..62316fc2 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -459,6 +459,76 @@ export class Email { return ''; } + async send( + req: AgentRequest, + context: AgentContext, + to: string[], + reply: 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' + ); + } + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: needed for complex async email operations + return new Promise(async (resolve, reject) => { + try { + let attachments: Attachment[] = []; + if (reply.attachments) { + attachments = await Promise.all( + reply.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 mail = new MailComposer({ + date: new Date(), + from: { + name: from?.name ?? context.agent.name, + address: from?.email ?? this.toEmail() ?? '', + }, + to: to.join(', '), + subject: reply.subject ?? '', + text: reply.text, + html: reply.html, + attachments, + }); + const newemail = mail.compile(); + newemail.build(async (err, message) => { + if (err) { + reject(err); + } else { + try { + await context.email.sendReply( + context.agent.id, + message.toString(), + authToken, + newemail.messageId() + ); + resolve(newemail.messageId()); + } catch (ex) { + reject(ex); + } + } + }); + } catch (ex) { + reject(ex); + } + }); + } + /** * send a reply to the email */ From 0ee3c3c133357d5ce19d8fe8553a5d8daca06aa5 Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 7 Oct 2025 12:05:47 -0300 Subject: [PATCH 02/14] Implemented feedback --- src/io/email.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/io/email.ts b/src/io/email.ts index 62316fc2..d7ff7eb2 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -493,13 +493,19 @@ export class Email { }) ); } + + const normalizedTo = to.map((addr) => addr.trim()).filter(Boolean); + if (normalizedTo.length === 0) { + throw new Error('at least one recipient email is required'); + } + const mail = new MailComposer({ date: new Date(), from: { name: from?.name ?? context.agent.name, address: from?.email ?? this.toEmail() ?? '', }, - to: to.join(', '), + to: normalizedTo.join(', '), subject: reply.subject ?? '', text: reply.text, html: reply.html, From 42db72e8f24a861ad5e2c11132d71847ef10c58a Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 7 Oct 2025 12:19:59 -0300 Subject: [PATCH 03/14] Implemented feedback --- src/io/email.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/io/email.ts b/src/io/email.ts index d7ff7eb2..f3bb0886 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -499,11 +499,16 @@ export class Email { 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: from?.email ?? this.toEmail() ?? '', + address: fromAddress, }, to: normalizedTo.join(', '), subject: reply.subject ?? '', From 86018ed7032ed6ea5db9fc9d5a13f4acd19b7163 Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 7 Oct 2025 12:37:20 -0300 Subject: [PATCH 04/14] Implemented feedback --- src/io/email.ts | 85 ++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/io/email.ts b/src/io/email.ts index f3bb0886..c2f11c68 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -475,48 +475,49 @@ export class Email { 'email authorization token is required but not found in metadata' ); } - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: needed for complex async email operations - return new Promise(async (resolve, reject) => { - try { - let attachments: Attachment[] = []; - if (reply.attachments) { - attachments = await Promise.all( - reply.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'); - } + return (async () => { + let attachments: Attachment[] = []; + if (reply.attachments) { + attachments = await Promise.all( + reply.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 fromAddress = from?.email ?? this.toEmail(); - if (!fromAddress) { - throw new Error('a valid from email address is required'); - } + const normalizedTo = to.map((addr) => addr.trim()).filter(Boolean); + if (normalizedTo.length === 0) { + throw new Error('at least one recipient email is required'); + } - const mail = new MailComposer({ - date: new Date(), - from: { - name: from?.name ?? context.agent.name, - address: fromAddress, - }, - to: normalizedTo.join(', '), - subject: reply.subject ?? '', - text: reply.text, - html: reply.html, - attachments, - }); - const newemail = mail.compile(); + 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: reply.subject ?? '', + text: reply.text, + html: reply.html, + attachments, + }); + const newemail = mail.compile(); + + return new Promise((resolve, reject) => { newemail.build(async (err, message) => { if (err) { reject(err); @@ -534,10 +535,8 @@ export class Email { } } }); - } catch (ex) { - reject(ex); - } - }); + }); + })(); } /** From 08b81b70550f20f411aad69f7245c49a761be462 Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 7 Oct 2025 12:45:45 -0300 Subject: [PATCH 05/14] Added comments to the email send function --- src/io/email.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/io/email.ts b/src/io/email.ts index c2f11c68..da36f092 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -459,6 +459,17 @@ 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 reply - 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, From 6fea78ad3a4bf8d80693b4e8672d4c9a3fabdd05 Mon Sep 17 00:00:00 2001 From: huijiro Date: Wed, 8 Oct 2025 14:53:48 -0300 Subject: [PATCH 06/14] Added email send with dedicated api Requires inbound implementation on catalyst. --- src/apis/email.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++ src/io/email.ts | 2 +- src/types.ts | 17 ++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/apis/email.ts b/src/apis/email.ts index 208329be..cada3d19 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 da36f092..54e7a4b8 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -534,7 +534,7 @@ export class Email { reject(err); } else { try { - await context.email.sendReply( + await context.email.send( context.agent.id, message.toString(), authToken, diff --git a/src/types.ts b/src/types.ts index 4ad7b16f..18e99a89 100644 --- a/src/types.ts +++ b/src/types.ts @@ -573,6 +573,23 @@ 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. + * + * @param req - The agent request containing metadata with email-auth-token + * @param context - The agent context + * @param to - Array of recipient email addresses + * @param reply - 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 + */ + send( + agentId: string, + email: string, + authToken: string, + messageId: string + ): Promise; /** * send an email reply to an incoming email * From b863ce1267b78d6ac1dd348172dbe582f8007842 Mon Sep 17 00:00:00 2001 From: huijiro Date: Thu, 9 Oct 2025 18:56:17 -0300 Subject: [PATCH 07/14] Making IO more in line with other IOs --- src/io/email.ts | 102 ++++++++++++++++++++++++++---------------------- src/types.ts | 9 +---- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/io/email.ts b/src/io/email.ts index 54e7a4b8..d53ea096 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -251,6 +251,7 @@ export class Email { private readonly _message: ParsedMail; constructor(data: ParsedMail) { + console.trace('Email data: ', data); this._message = data; } @@ -259,38 +260,46 @@ export class Email { } toString() { - return `[Email id=${this.messageId()},from=${this.fromEmail()},subject=${this.subject()}]`; + return `[Email id=${this.messageId},from=${this.fromEmail},subject=${this.subject}]`; + } + + get _raw(): unknown { + return this._message; + } + + get payload(): ParsedMail { + return this._message; } /** * The date of the email. */ - date(): Date | null { - return this._message.date ?? null; + get date(): Date | undefined { + return this._message.date; } /** * The message ID of the email. */ - messageId(): string | null { - return this._message.messageId ?? null; + get messageId(): string | undefined { + return this._message.messageId; } /** * The headers of the email. */ - headers(): Headers { + get headers(): Headers { return this._message.headers; } /** - * The email address of the recipient or null if there is no recipient. + * The email address of the recipient or undefined if there is no recipient. * * If the email has multiple recipients, the email addresses are comma separated. */ - to(): string | null { + get to(): string | undefined { if (!this._message.to) { - return null; + return undefined; } if (Array.isArray(this._message.to)) { return this._message.to.map((addr) => addr.text.trim()).join(', '); @@ -298,80 +307,80 @@ export class Email { if (typeof this._message.to === 'object' && 'text' in this._message.to) { return this._message.to.text; } - return null; + return undefined; } /** - * The email address of the sender or null if there is no sender. + * The email address of the sender or undefined if there is no sender. */ - fromEmail(): string | null { - return this._message.from?.value[0]?.address ?? null; + get fromEmail(): string | undefined { + return this._message.from?.value[0]?.address; } /** - * The name of the sender or null if there is no name. + * The name of the sender or undefined if there is no name. */ - fromName(): string | null { - return this._message.from?.value[0]?.name ?? null; + get fromName(): string | undefined { + return this._message.from?.value[0]?.name; } /** - * The email address of the first recipient or null if there is no recipient. + * The email address of the first recipient or undefined if there is no recipient. */ - toEmail(): string | null { + get toEmail(): string | undefined { if (!this._message.to) { - return null; + return undefined; } if (Array.isArray(this._message.to)) { - return this._message.to[0]?.value[0]?.address ?? null; + return this._message.to[0]?.value[0]?.address; } if (typeof this._message.to === 'object' && 'value' in this._message.to) { - return this._message.to.value[0]?.address ?? null; + return this._message.to.value[0]?.address; } - return null; + return undefined; } /** - * The name of the first recipient or null if there is no name. + * The name of the first recipient or undefined if there is no name. */ - toName(): string | null { + get toName(): string | undefined { if (!this._message.to) { - return null; + return undefined; } if (Array.isArray(this._message.to)) { - return this._message.to[0]?.value[0]?.name ?? null; + return this._message.to[0]?.value[0]?.name; } if (typeof this._message.to === 'object' && 'value' in this._message.to) { - return this._message.to.value[0]?.name ?? null; + return this._message.to.value[0]?.name; } - return null; + return undefined; } /** - * The subject of the email or null if there is no subject. + * The subject of the email or undefined if there is no subject. */ - subject(): string | null { - return this._message.subject ?? null; + get subject(): string | undefined { + return this._message.subject; } /** - * The plain text body of the email or null if there is no plain text body. + * The plain text body of the email or undefined if there is no plain text body. */ - text(): string | null { - return this._message.text ?? null; + get text(): string | undefined { + return this._message.text; } /** - * The HTML body of the email or null if there is no HTML body. + * The HTML body of the email or undefined if there is no HTML body. */ - html(): string | null { - return this._message.html ? this._message.html : null; + get html(): string | undefined { + return this._message.html ? this._message.html : undefined; } /** * The attachments of the email or an empty array if there are no attachments. */ - attachments(): IncomingEmailAttachment[] { + get attachments(): IncomingEmailAttachment[] { if (!this._message.attachments || this._message.attachments.length === 0) { return []; } @@ -449,7 +458,7 @@ export class Email { if (subject) { return subject; } - const _subject = this.subject(); + const _subject = this.subject; if (_subject) { if (_subject.toUpperCase().startsWith('RE:')) { return _subject; @@ -509,7 +518,7 @@ export class Email { throw new Error('at least one recipient email is required'); } - const fromAddress = from?.email ?? this.toEmail(); + const fromAddress = from?.email ?? this.toEmail; if (!fromAddress) { throw new Error('a valid from email address is required'); } @@ -587,16 +596,16 @@ export class Email { ); } const mail = new MailComposer({ - inReplyTo: this.messageId() ?? undefined, - references: this.messageId() ?? undefined, + inReplyTo: this.messageId ?? undefined, + references: this.messageId ?? undefined, date: new Date(), from: { name: from?.name ?? context.agent.name, - address: from?.email ?? this.toEmail() ?? '', + address: from?.email ?? this.toEmail ?? '', }, to: { - name: this.fromName() ?? undefined, - address: this.fromEmail() ?? undefined, + name: this.fromName ?? undefined, + address: this.fromEmail ?? undefined, } as Address, subject: this.makeReplySubject(reply.subject), text: reply.text, @@ -633,6 +642,7 @@ export class Email { */ export async function parseEmail(data: Buffer): Promise { try { + console.trace('raw email data: ', data.toString()); const message = await simpleParser(data); return new Email(message); } catch (error) { @@ -640,4 +650,4 @@ export async function parseEmail(data: Buffer): Promise { `Failed to parse email: ${error instanceof Error ? error.message : 'Unknown error'}` ); } -} +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 18e99a89..f434e721 100644 --- a/src/types.ts +++ b/src/types.ts @@ -575,14 +575,6 @@ export interface VectorStorage { export interface EmailService { /** * 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 reply - 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 */ send( agentId: string, @@ -590,6 +582,7 @@ export interface EmailService { authToken: string, messageId: string ): Promise; + /** * send an email reply to an incoming email * From 2693428438f9cde0819881f593545d9d36cb61b8 Mon Sep 17 00:00:00 2001 From: huijiro Date: Fri, 10 Oct 2025 16:19:57 -0300 Subject: [PATCH 08/14] Removed email logging --- src/io/email.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/io/email.ts b/src/io/email.ts index d53ea096..ec625274 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -251,7 +251,6 @@ export class Email { private readonly _message: ParsedMail; constructor(data: ParsedMail) { - console.trace('Email data: ', data); this._message = data; } @@ -642,7 +641,6 @@ export class Email { */ export async function parseEmail(data: Buffer): Promise { try { - console.trace('raw email data: ', data.toString()); const message = await simpleParser(data); return new Email(message); } catch (error) { @@ -650,4 +648,5 @@ export async function parseEmail(data: Buffer): Promise { `Failed to parse email: ${error instanceof Error ? error.message : 'Unknown error'}` ); } -} \ No newline at end of file +} + From 9efcbf487dc045e1d15f8416d700a71e90515a7b Mon Sep 17 00:00:00 2001 From: huijiro Date: Fri, 10 Oct 2025 16:23:02 -0300 Subject: [PATCH 09/14] Updated tests --- test/io/email.test.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test/io/email.test.ts b/test/io/email.test.ts index 7fbb2191..db98a488 100644 --- a/test/io/email.test.ts +++ b/test/io/email.test.ts @@ -27,7 +27,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); @@ -56,8 +56,8 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments()).not.toThrow(); - const attachments = email.attachments(); + expect(email.attachments).toBeDefined(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -82,8 +82,8 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments()).not.toThrow(); - const attachments = email.attachments(); + expect(email.attachments).toBeDefined(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -112,7 +112,7 @@ Invalid attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); @@ -164,7 +164,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test-fallback.txt'); @@ -190,7 +190,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -215,7 +215,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -240,7 +240,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -265,7 +265,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -290,7 +290,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); }); @@ -315,7 +315,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].contentDisposition).toBe('inline'); @@ -356,7 +356,7 @@ Malformed URL attachment `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); @@ -393,7 +393,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); } @@ -429,7 +429,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(0); } @@ -458,7 +458,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments(); + const attachments = email.attachments; expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); From d60fab2400eef09a63bba152928f635c2afdc2c4 Mon Sep 17 00:00:00 2001 From: huijiro Date: Fri, 10 Oct 2025 16:29:08 -0300 Subject: [PATCH 10/14] Implemented feedback --- test/io/email.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/io/email.test.ts b/test/io/email.test.ts index db98a488..567a800d 100644 --- a/test/io/email.test.ts +++ b/test/io/email.test.ts @@ -139,7 +139,7 @@ Attachment without filename const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments()).toThrow( + expect(() => email.attachments).toThrow( 'Invalid attachment headers: missing filename' ); }); From 7f7fca936dadda25e4bca779923f13b62f446ba3 Mon Sep 17 00:00:00 2001 From: huijiro Date: Mon, 13 Oct 2025 13:12:55 -0300 Subject: [PATCH 11/14] Revert structure change --- src/io/email.ts | 99 ++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/src/io/email.ts b/src/io/email.ts index ec625274..54e7a4b8 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -259,46 +259,38 @@ export class Email { } toString() { - return `[Email id=${this.messageId},from=${this.fromEmail},subject=${this.subject}]`; - } - - get _raw(): unknown { - return this._message; - } - - get payload(): ParsedMail { - return this._message; + return `[Email id=${this.messageId()},from=${this.fromEmail()},subject=${this.subject()}]`; } /** * The date of the email. */ - get date(): Date | undefined { - return this._message.date; + date(): Date | null { + return this._message.date ?? null; } /** * The message ID of the email. */ - get messageId(): string | undefined { - return this._message.messageId; + messageId(): string | null { + return this._message.messageId ?? null; } /** * The headers of the email. */ - get headers(): Headers { + headers(): Headers { return this._message.headers; } /** - * The email address of the recipient or undefined if there is no recipient. + * The email address of the recipient or null if there is no recipient. * * If the email has multiple recipients, the email addresses are comma separated. */ - get to(): string | undefined { + to(): string | null { if (!this._message.to) { - return undefined; + return null; } if (Array.isArray(this._message.to)) { return this._message.to.map((addr) => addr.text.trim()).join(', '); @@ -306,80 +298,80 @@ export class Email { if (typeof this._message.to === 'object' && 'text' in this._message.to) { return this._message.to.text; } - return undefined; + return null; } /** - * The email address of the sender or undefined if there is no sender. + * The email address of the sender or null if there is no sender. */ - get fromEmail(): string | undefined { - return this._message.from?.value[0]?.address; + fromEmail(): string | null { + return this._message.from?.value[0]?.address ?? null; } /** - * The name of the sender or undefined if there is no name. + * The name of the sender or null if there is no name. */ - get fromName(): string | undefined { - return this._message.from?.value[0]?.name; + fromName(): string | null { + return this._message.from?.value[0]?.name ?? null; } /** - * The email address of the first recipient or undefined if there is no recipient. + * The email address of the first recipient or null if there is no recipient. */ - get toEmail(): string | undefined { + toEmail(): string | null { if (!this._message.to) { - return undefined; + return null; } if (Array.isArray(this._message.to)) { - return this._message.to[0]?.value[0]?.address; + return this._message.to[0]?.value[0]?.address ?? null; } if (typeof this._message.to === 'object' && 'value' in this._message.to) { - return this._message.to.value[0]?.address; + return this._message.to.value[0]?.address ?? null; } - return undefined; + return null; } /** - * The name of the first recipient or undefined if there is no name. + * The name of the first recipient or null if there is no name. */ - get toName(): string | undefined { + toName(): string | null { if (!this._message.to) { - return undefined; + return null; } if (Array.isArray(this._message.to)) { - return this._message.to[0]?.value[0]?.name; + return this._message.to[0]?.value[0]?.name ?? null; } if (typeof this._message.to === 'object' && 'value' in this._message.to) { - return this._message.to.value[0]?.name; + return this._message.to.value[0]?.name ?? null; } - return undefined; + return null; } /** - * The subject of the email or undefined if there is no subject. + * The subject of the email or null if there is no subject. */ - get subject(): string | undefined { - return this._message.subject; + subject(): string | null { + return this._message.subject ?? null; } /** - * The plain text body of the email or undefined if there is no plain text body. + * The plain text body of the email or null if there is no plain text body. */ - get text(): string | undefined { - return this._message.text; + text(): string | null { + return this._message.text ?? null; } /** - * The HTML body of the email or undefined if there is no HTML body. + * The HTML body of the email or null if there is no HTML body. */ - get html(): string | undefined { - return this._message.html ? this._message.html : undefined; + html(): string | null { + return this._message.html ? this._message.html : null; } /** * The attachments of the email or an empty array if there are no attachments. */ - get attachments(): IncomingEmailAttachment[] { + attachments(): IncomingEmailAttachment[] { if (!this._message.attachments || this._message.attachments.length === 0) { return []; } @@ -457,7 +449,7 @@ export class Email { if (subject) { return subject; } - const _subject = this.subject; + const _subject = this.subject(); if (_subject) { if (_subject.toUpperCase().startsWith('RE:')) { return _subject; @@ -517,7 +509,7 @@ export class Email { throw new Error('at least one recipient email is required'); } - const fromAddress = from?.email ?? this.toEmail; + const fromAddress = from?.email ?? this.toEmail(); if (!fromAddress) { throw new Error('a valid from email address is required'); } @@ -595,16 +587,16 @@ export class Email { ); } const mail = new MailComposer({ - inReplyTo: this.messageId ?? undefined, - references: this.messageId ?? undefined, + inReplyTo: this.messageId() ?? undefined, + references: this.messageId() ?? undefined, date: new Date(), from: { name: from?.name ?? context.agent.name, - address: from?.email ?? this.toEmail ?? '', + address: from?.email ?? this.toEmail() ?? '', }, to: { - name: this.fromName ?? undefined, - address: this.fromEmail ?? undefined, + name: this.fromName() ?? undefined, + address: this.fromEmail() ?? undefined, } as Address, subject: this.makeReplySubject(reply.subject), text: reply.text, @@ -649,4 +641,3 @@ export async function parseEmail(data: Buffer): Promise { ); } } - From 79f71193373e855042aa78af5ac0ce6b771adc56 Mon Sep 17 00:00:00 2001 From: huijiro Date: Mon, 13 Oct 2025 14:48:21 -0300 Subject: [PATCH 12/14] Fixed tests --- test/io/email.test.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/io/email.test.ts b/test/io/email.test.ts index 567a800d..d74f28d0 100644 --- a/test/io/email.test.ts +++ b/test/io/email.test.ts @@ -27,7 +27,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); @@ -57,7 +57,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); expect(email.attachments).toBeDefined(); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -83,7 +83,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); expect(email.attachments).toBeDefined(); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -112,7 +112,7 @@ Invalid attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); @@ -139,7 +139,7 @@ Attachment without filename const email = await parseEmail(Buffer.from(emailContent)); - expect(() => email.attachments).toThrow( + expect(() => email.attachments()).toThrow( 'Invalid attachment headers: missing filename' ); }); @@ -164,7 +164,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test-fallback.txt'); @@ -190,7 +190,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -215,7 +215,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -240,7 +240,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -265,7 +265,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -290,7 +290,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); }); @@ -315,7 +315,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].contentDisposition).toBe('inline'); @@ -356,7 +356,7 @@ Malformed URL attachment `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); @@ -393,7 +393,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); } @@ -429,7 +429,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(0); } @@ -458,7 +458,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - const attachments = email.attachments; + const attachments = email.attachments(); expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); From 8122e73ac4132d04e2be2b3fb238cacb182e600c Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 14 Oct 2025 13:05:27 -0300 Subject: [PATCH 13/14] Implemented feedback --- src/apis/email.ts | 2 +- src/io/email.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/apis/email.ts b/src/apis/email.ts index cada3d19..f43c8408 100644 --- a/src/apis/email.ts +++ b/src/apis/email.ts @@ -45,7 +45,7 @@ export default class EmailApi implements EmailService { 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` + `error sending email: ${resp.response.statusText} (${resp.response.status})${body}` ); }); } catch (ex) { diff --git a/src/io/email.ts b/src/io/email.ts index 54e7a4b8..52c0015a 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -465,7 +465,7 @@ export class Email { * @param req - The agent request containing metadata with email-auth-token * @param context - The agent context * @param to - Array of recipient email addresses - * @param reply - The email content (subject, text, html, attachments) + * @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 @@ -474,7 +474,7 @@ export class Email { req: AgentRequest, context: AgentContext, to: string[], - reply: EmailReply, + email: EmailReply, from?: { name?: string; email?: string; @@ -489,9 +489,9 @@ export class Email { return (async () => { let attachments: Attachment[] = []; - if (reply.attachments) { + if (email.attachments) { attachments = await Promise.all( - reply.attachments.map(async (attachment) => { + email.attachments.map(async (attachment) => { const resp = await fromDataType(attachment.data); return { filename: attachment.filename, @@ -521,9 +521,9 @@ export class Email { address: fromAddress, }, to: normalizedTo.join(', '), - subject: reply.subject ?? '', - text: reply.text, - html: reply.html, + subject: email.subject ?? '', + text: email.text, + html: email.html, attachments, }); const newemail = mail.compile(); From 3e358d0147fd55524c5e9bd2c6c04273c3a02ee9 Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 14 Oct 2025 17:47:58 -0300 Subject: [PATCH 14/14] Changelog and Versioning --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) 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,