diff --git a/samples/BCAgents/AgentEmailIntegration/README.md b/samples/BCAgents/AgentEmailIntegration/README.md new file mode 100644 index 00000000..0aa4b2fa --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/README.md @@ -0,0 +1,151 @@ + +# Sample Agent + Email Integration (Business Central) + +This AL extension is a small **sample** showing how to connect **Agents** with **Email** in Microsoft Dynamics 365 Business Central. + +It uses a recurring **scheduled task** to: + +1. Retrieve inbound emails from a configured email account. +2. Create (or append to) **Agent Tasks** based on the email conversation. +3. Send email replies for agent outputs that have been **Reviewed**. + +## How it works + +```mermaid +sequenceDiagram + participant TS as TaskScheduler + participant D as Sample Dispatcher + participant A as Agent + participant RE as Sample Retrieve Emails + participant E as Email + participant ATS as Agent Task + participant SR as Sample Send Replies + participant EM as Email Message + + TS->>D: Run + D->>A: IsActive(Agent) + alt ShouldRun = false (Inactive) + D-->>TS: Exit + else ShouldRun = true + D->>RE: Retrieve emails + RE->>E: Retrieve emails + loop For each retrieved Email Inbox record + RE->>ATS: TaskExists(Conversation Id)? + alt Existing task + RE->>ATS: Append Agent Task Message + Note over RE,ATS: External ID = External Message Id + else New task + RE->>ATS: Create Agent Task + initial message + Note over RE,ATS: External ID = Conversation Id + end + RE->>E: Mark email as read + end + + D->>SR: SendEmailReplies(Setup) + loop For each reviewed output message + SR->>ATS: Get external message id + SR->>EM: CreateReply + SR->>E: Reply + SR->>ATS: Mark output message as Sent + end + + D->>TS: Create task + D->>ATS: Update Last Sync At if retrevial success + end +``` + +```mermaid +sequenceDiagram + participant TS as TaskScheduler + participant EH as Sample Error Handler + participant A as Agent + + TS->>EH: Run + EH->>A: IsActive(Agent) + alt ShouldRun = false + EH-->>TS: Exit + else ShouldRun = true + EH->>TS: Create task + end +``` + +An “agent cycle” is executed by the scheduled task running codeunit `50100 "Sample Dispatcher"`: + +1. **ShouldRun check** + - `Sample Dispatcher` calls `Agent.IsActive(Setup."Agent User Security ID")` to decide whether to run. +2. **Retrieve emails → Agent tasks** + - `50102 "Sample Retrieve Emails"` calls `Email.RetrieveEmails(...)`. + - For each email in `Email Inbox`: + - If an agent task already exists for the email **conversation**, the message is appended to the existing task. + - Otherwise, a new agent task is created. + - The email is marked as read via `Email.MarkAsRead(...)`. +3. **Send replies** + - `50103 "Sample Send Replies"` finds agent task output messages with: + - `Type = Output` + - `Status = Reviewed` + - `Agent User Security ID = Setup."Agent User Security ID"` + - It creates a reply-all email (`Email Message.CreateReplyAll`) and sends it with `Email.ReplyAll(...)`. + - On success, the agent output message is marked as sent. +4. **Reschedule** + - `Sample Dispatcher` schedules itself again using `TaskScheduler.CreateTask(...)`. + - The sample currently reschedules with a fixed delay of **1 minute**. + +If the dispatcher fails, `50101 "Sample Error Handler"` is used as the task error handler and will reschedule the next run. + +## Included objects + +- `table 50100 "Sample Setup"` + - Stores which agent and email account to use, sync timestamps, and scheduled task IDs. +- `codeunit 50100 "Sample Dispatcher"` + - Orchestrates one cycle (retrieve emails, send replies, reschedule). +- `codeunit 50101 "Sample Error Handler"` + - Reschedules the dispatcher if the scheduled task run fails. +- `codeunit 50102 "Sample Retrieve Emails"` + - Retrieves inbound emails and creates/appends agent task messages. +- `codeunit 50103 "Sample Send Replies"` + - Sends reply-all emails for reviewed agent outputs. + +## Setup (what you need to configure) + +This repo focuses on the integration logic and **does not include a setup page**. +To run the sample you must create a `Sample Setup` record (`table 50100`) in your own way (for example: add a small page in a companion extension, insert the record from a one-off helper codeunit, or create it during development/debugging). + +Populate: + +- `Agent User Security ID` + - The user security ID of the agent user. +- `Email Account ID` + - The email account to retrieve from and reply with. +- `Email Connector` + - The connector type used by the email account. + +These are used behind the scenes for tracking sychronizations: + +- `Earliest Sync At` + - Used as the lower bound when retrieving emails. +- `Last Sync At` + - Updated after a successful dispatcher run. + +## Important note: scheduled task user + email access + +Business Central scheduled tasks run under a **user context** (the user who scheduled/created the task). + +That means: + +- The user who schedules the dispatcher must have permission to run this extension’s objects. +- The user must have access to the selected email account/connector. + If the user does not have access, email retrieval/reply operations can fail. + +## Notes / limitations (by design for a sample) + +- The dispatcher’s “should run” logic is currently minimal (it only checks `Agent.IsActive(...)`). +- Email retrieval uses a simple filter: unread only, attachments loaded, last message only, earliest email = `Earliest Sync At`, max 50. +- Attachment MIME filtering is marked as TODO in `Sample Retrieve Emails`. + +## Quick start (developer flow) + +1. Publish the extension. +2. Configure Email in Business Central (email account + connector). +3. Schedule with the Schedule Sync action. +4. Send an email to the email account selected. + diff --git a/samples/BCAgents/AgentEmailIntegration/app.json b/samples/BCAgents/AgentEmailIntegration/app.json new file mode 100644 index 00000000..7c02bd8c --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/app.json @@ -0,0 +1,40 @@ +{ + "id": "db9ab3df-d57d-47fb-8870-f7617f9341aa", + "name": "Sample Agent Email Integration", + "publisher": "Microsoft", + "version": "1.0.0.0", + "brief": "", + "description": "", + "privacyStatement": "", + "EULA": "", + "help": "", + "url": "", + "logo": "", + "dependencies": [ + { + "id": "00155c68-8cdd-4d60-a451-2034ad094223", + "name": "Business Central Agent Playground", + "publisher": "Microsoft", + "version": "27.2.0.0" + } + ], + "screenshots": [], + "platform": "1.0.0.0", + "application": "27.0.0.0", + "idRanges": [ + { + "from": 50100, + "to": 50149 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "runtime": "16.0", + "features": [ + "NoImplicitWith" + ], + "target":"Cloud" +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al new file mode 100644 index 00000000..7be8eafa --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +using System.Agents; + +codeunit 50104 "Sample" +{ + + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Determines whether the scheduled agent task should run for the given setup. + /// + /// Setup record. + /// True if the agent is active; otherwise false. + procedure ShouldRun(Setup: Record "Sample Setup"): Boolean + var + Agent: Codeunit Agent; + begin + // TODO: Implement validation logic to determine if the agent should run + if not Agent.IsActive(Setup."Agent User Security ID") then + exit(false); + + exit(true); + end; + + /// + /// Schedules the next dispatcher execution. + /// + /// Setup record. + procedure ScheduleNextRun(Setup: Record "Sample Setup") + begin + // Ensure no other process can change the setup while we are scheduling the task + Setup.LockTable(); + // Ensure we have the latest record before modifying + Setup.GetBySystemId(Setup.SystemId); + + // Remove existing scheduled task if any before rescheduling + RemoveScheduledTask(Setup); + + Setup."Scheduled Task ID" := TaskScheduler.CreateTask(Codeunit::"Sample Dispatcher", Codeunit::"Sample Error Handler", true, CompanyName(), CurrentDateTime() + ScheduleDelay(), Setup.RecordId); + + Setup.Modify(); + Commit(); + end; + + /// + /// Cancels any previously scheduled tasks and clears task IDs on the setup record. + /// + /// Setup record containing scheduled task IDs. + procedure RemoveScheduledTask(Setup: Record "Sample Setup") + var + NullGuid: Guid; + begin + if TaskScheduler.TaskExists(Setup."Scheduled Task ID") then + TaskScheduler.CancelTask(Setup."Scheduled Task ID"); + + Setup."Scheduled Task ID" := NullGuid; + Setup.Modify(); + end; + + /// + /// Returns the delay (in milliseconds) used when scheduling the next run. + /// + /// Delay in milliseconds. + local procedure ScheduleDelay(): Integer + begin + exit(2 * 60 * 1000); // 2 minutes + end; +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al new file mode 100644 index 00000000..11ece333 --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +using System.Agents; + +codeunit 50100 "Sample Dispatcher" +{ + + Access = Internal; + TableNo = "Sample Setup"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + begin + RunSampleAgent(Rec); + end; + + /// + /// Executes one agent cycle: retrieves emails, sends replies, and schedules the next run. + /// + /// Agent setup record. + procedure RunSampleAgent(Setup: Record "Sample Setup") + var + Sample: Codeunit "Sample"; + LastSync: DateTime; + RetrievalSuccess: Boolean; + begin + // Validate task should still run + if not Sample.ShouldRun(Setup) then + exit; + + LastSync := CurrentDateTime(); + + // Sync emails + RetrievalSuccess := Codeunit.Run(Codeunit::"Sample Retrieve Emails", Setup); + Codeunit.Run(Codeunit::"Sample Send Replies", Setup); + + // Reschedule next run + Sample.ScheduleNextRun(Setup); + + if RetrievalSuccess then + UpdateLastSync(Setup, LastSync); + end; + + /// + /// Persists the last successful synchronization datetime on the setup record. + /// + /// Setup record to update. + /// Datetime to store as the last sync time. + local procedure UpdateLastSync(var Setup: Record "Sample Setup"; LastSync: DateTime) + begin + Setup.GetBySystemId(Setup.SystemId); + Setup."Last Sync At" := LastSync; + Setup.Modify(); + Commit(); + end; + +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al new file mode 100644 index 00000000..4481404b --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +codeunit 50101 "Sample Error Handler" +{ + + Access = Internal; + TableNo = "Sample Setup"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + begin + RunSampleAgent(Rec); + end; + + /// + /// Handles dispatcher failures by rescheduling the next run. + /// + /// Agent setup record. + procedure RunSampleAgent(Setup: Record "Sample Setup") + var + Sample: Codeunit "Sample"; + begin + // Validate task should still run + if not Sample.ShouldRun(Setup) then + exit; + + // Reschedule run + Sample.ScheduleNextRun(Setup); + end; +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al new file mode 100644 index 00000000..82553715 --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al @@ -0,0 +1,215 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +using System.Agents; +using System.Email; + +codeunit 50102 "Sample Retrieve Emails" +{ + + Access = Internal; + TableNo = "Sample Setup"; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Email Inbox" = rd; + + var + AgentTaskTitleLbl: Label 'Email from %1', Comment = '%1 = Sender Name'; + MessageTemplateLbl: Label 'Subject: %1
Body: %2', Comment = '%1 = Subject, %2 = Body'; + + trigger OnRun() + begin + RunRetrieveEmails(Rec); + end; + + /// + /// Retrieves emails and creates/updates agent tasks. + /// + /// Agent setup record containing email account configuration. + procedure RunRetrieveEmails(Setup: Record "Sample Setup") + var + EmailInbox: Record "Email Inbox"; + TempFilters: Record "Email Retrieval Filters" temporary; + Email: Codeunit "Email"; + LastSync: DateTime; + begin + LastSync := CurrentDateTime(); + + SetEmailFilters(Setup, TempFilters); + Email.RetrieveEmails(Setup."Email Account ID", Setup."Email Connector", EmailInbox, TempFilters); + + if EmailInbox.FindSet() then; + + repeat + // Process each email as needed + AddEmailToAgentTask(Setup, EmailInbox); + until EmailInbox.Next() = 0; + + UpdateEarliestSyncTime(Setup, EmailInbox.Count(), LastSync); + Commit(); + end; + + /// + /// Updates the earliest sync datetime used for the next retrieval window. + /// + /// Setup record. + /// Number of emails retrieved/processed in this run. + /// Datetime of the last synchronization. + local procedure UpdateEarliestSyncTime(var Setup: Record "Sample Setup"; EmailCount: Integer; LastSync: DateTime) + begin + Setup.GetBySystemId(Setup.SystemId); + + // Only update the earliest sync time if we processed the fewer emails than the limit + // This ensures we don't miss any emails in case there are more emails to process + if EmailCount < MaxNoOfEmailsToProcess() then + Setup."Earliest Sync At" := LastSync; + Setup.Modify(); + end; + + /// + /// Returns the maximum number of emails to retrieve/process per run. + /// + /// Maximum number of emails to process. + local procedure MaxNoOfEmailsToProcess(): Integer + begin + exit(50); + end; + + /// + /// Builds the email retrieval filters. + /// + /// Setup record. + /// Temporary retrieval filters record to populate. + local procedure SetEmailFilters(Setup: Record "Sample Setup"; var TempFilters: Record "Email Retrieval Filters" temporary) + begin + TempFilters."Unread Emails" := true; + TempFilters."Load Attachments" := true; + TempFilters."Last Message Only" := true; + TempFilters."Earliest Email" := Setup."Earliest Sync At"; + TempFilters."Max No. of Emails" := MaxNoOfEmailsToProcess(); + TempFilters.Insert(); + end; + + /// + /// Adds a retrieved email to an agent task (existing or new task). + /// + /// Setup record. + /// Current email inbox record to process. + local procedure AddEmailToAgentTask(Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox") + var + AgentTaskBuilder: Codeunit "Agent Task Builder"; + begin + if AgentTaskBuilder.TaskExists(Setup."Agent User Security ID", EmailInbox."Conversation Id") then + AddEmailToExistingTask(EmailInbox) + else + AddEmailToNewAgentTask(Setup, EmailInbox); + + MarkEmailAsProcessed(Setup, EmailInbox); + end; + + /// + /// Appends an email as a new message on an existing agent task. + /// + /// Email to be appended. + local procedure AddEmailToExistingTask(var EmailInbox: Record "Email Inbox") + var + AgentTaskRecord: Record "Agent Task"; + AgentTaskMessage: Record "Agent Task Message"; + AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder"; + EmailMessage: Codeunit "Email Message"; + MessageText: Text; + begin + AgentTaskRecord.ReadIsolation(IsolationLevel::ReadCommitted); + AgentTaskRecord.SetRange("External ID", EmailInbox."Conversation Id"); + if not AgentTaskRecord.FindFirst() then + exit; + + AgentTaskMessage.ReadIsolation(IsolationLevel::ReadCommitted); + AgentTaskMessage.SetRange("Task ID", AgentTaskRecord.ID); + AgentTaskMessage.SetRange("External ID", EmailInbox."External Message Id"); + if AgentTaskMessage.Count() >= 1 then + exit; + + EmailMessage.Get(EmailInbox."Message Id"); + MessageText := StrSubstNo(MessageTemplateLbl, EmailMessage.GetSubject(), EmailMessage.GetBody()); + + AgentTaskMessageBuilder.Initialize(EmailInbox."Sender Address", MessageText) + .SetMessageExternalID(EmailInbox."External Message Id") + .SetIgnoreAttachment(false) + .SetAgentTask(AgentTaskRecord); + + AddEmailAttachmentsToTaskMessage(EmailMessage, AgentTaskMessageBuilder); + AgentTaskMessageBuilder.Create(); + end; + + /// + /// Creates a new agent task and adds the email as the initial task message. + /// + /// Setup record. + /// Email used to create the task. + local procedure AddEmailToNewAgentTask(Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox") + var + AgentTaskRecord: Record "Agent Task"; + AgentTaskBuilder: Codeunit "Agent Task Builder"; + AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder"; + EmailMessage: Codeunit "Email Message"; + MessageText: Text; + AgentTaskTitle: Text[150]; + begin + EmailMessage.Get(EmailInbox."Message ID"); + MessageText := StrSubstNo(MessageTemplateLbl, EmailMessage.GetSubject(), EmailMessage.GetBody()); + AgentTaskTitle := CopyStr(StrSubstNo(AgentTaskTitleLbl, EmailInbox."Sender Name"), 1, MaxStrLen(AgentTaskRecord.Title)); + + AgentTaskMessageBuilder.Initialize(EmailInbox."Sender Address", MessageText) + .SetMessageExternalID(EmailInbox."External Message ID") + .SetIgnoreAttachment(false); + + AgentTaskBuilder.Initialize(Setup."Agent User Security ID", AgentTaskTitle) + .SetExternalId(EmailInbox."Conversation Id") + .AddTaskMessage(AgentTaskMessageBuilder); + + AddEmailAttachmentsToTaskMessage(EmailMessage, AgentTaskMessageBuilder); + AgentTaskBuilder.Create(); + end; + + /// + /// Marks the processed email as read in the external mailbox. + /// + /// Setup record. + /// Email containing the external message identifier. + local procedure MarkEmailAsProcessed(var Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox") + var + Email: Codeunit "Email"; + begin + Email.MarkAsRead(Setup."Email Account ID", Setup."Email Connector", EmailInbox."External Message Id"); + end; + + /// + /// Adds all email attachments to the agent task message being built. + /// + /// Email message containing attachments to add. + /// Builder instance to add the attachments to. + local procedure AddEmailAttachmentsToTaskMessage(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder") + var + InStream: InStream; + FileMIMEType: Text[100]; + IsFileMimeTypeSupported: Boolean; + Ignore: Boolean; + begin + if not EmailMessage.Attachments_First() then + exit; + + repeat + EmailMessage.Attachments_GetContent(InStream); + FileMIMEType := CopyStr(EmailMessage.Attachments_GetContentType(), 1, 100); + // TODO: Add logic to check for supported MIME types + IsFileMimeTypeSupported := true; // Placeholder for actual MIME type check + Ignore := IsFileMimeTypeSupported; // Placeholder for actual logic to determine if attachment should be ignored even if file type is supported + AgentTaskMessageBuilder.AddAttachment(EmailMessage.Attachments_GetName(), FileMIMEType, InStream, Ignore); + until EmailMessage.Attachments_Next() = 0; + end; +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al new file mode 100644 index 00000000..6faed3aa --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; +using System.Agents; +using System.Email; + + +codeunit 50103 "Sample Send Replies" +{ + + Access = Internal; + TableNo = "Sample Setup"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + begin + SendEmailReplies(Rec); + end; + + var + AllSentSuccessfully: Boolean; + EmailSubjectTxt: Label 'Agent reply to task %1', Comment = '%1 = Agent Task id'; + + /// + /// Sends email replies for reviewed output messages and marks them as sent. + /// + /// Setup record. + procedure SendEmailReplies(Setup: Record "Sample Setup") + var + OutputAgentTaskMessage: Record "Agent Task Message"; + InputAgentTaskMessage: Record "Agent Task Message"; + AgentMessage: Codeunit "Agent Message"; + begin + AllSentSuccessfully := true; + + OutputAgentTaskMessage.ReadIsolation(IsolationLevel::ReadCommitted); + OutputAgentTaskMessage.SetRange(Status, OutputAgentTaskMessage.Status::Reviewed); + OutputAgentTaskMessage.SetRange(Type, OutputAgentTaskMessage.Type::Output); + OutputAgentTaskMessage.SetRange("Agent User Security ID", Setup."Agent User Security ID"); + + if not OutputAgentTaskMessage.FindSet() then + exit; + + repeat + if not InputAgentTaskMessage.Get(OutputAgentTaskMessage."Task ID", OutputAgentTaskMessage."Input Message ID") then + continue; + + if InputAgentTaskMessage."External ID" = '' then + continue; + + if TryReply(Setup, InputAgentTaskMessage, OutputAgentTaskMessage) then + AgentMessage.SetStatusToSent(OutputAgentTaskMessage) + else + AllSentSuccessfully := false; + until OutputAgentTaskMessage.Next() = 0; + Commit(); + end; + + /// + /// Returns whether all replies were sent successfully in the last run. + /// + /// True if all replies were sent successfully; otherwise false. + procedure GetAllSentSuccessfully(): Boolean + begin + exit(AllSentSuccessfully); + end; + + /// + /// Attempts to create and send a reply email for an agent task output message. + /// + /// Setup record. + /// The original inbound message being replied to. + /// The outbound agent message to send. + /// True if the email was sent successfully; otherwise false. + local procedure TryReply(Setup: Record "Sample Setup"; var InputAgentTaskMessage: Record "Agent Task Message"; var OutputAgentTaskMessage: Record "Agent Task Message"): Boolean + var + AgentMessage: Codeunit "Agent Message"; + Email: Codeunit Email; + EmailMessage: Codeunit "Email Message"; + Body: Text; + Subject: Text; + begin + Subject := StrSubstNo(EmailSubjectTxt, InputAgentTaskMessage."Task ID"); + Body := AgentMessage.GetText(OutputAgentTaskMessage); + EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID"); + AddMessageAttachments(EmailMessage, OutputAgentTaskMessage); + + exit(Email.ReplyAll(EmailMessage, Setup."Email Account ID", Setup."Email Connector")); + end; + + /// + /// Adds agent task message attachments to the email message being sent. + /// + /// Email message to add attachments to. + /// Task message with the attachments to be included. + local procedure AddMessageAttachments(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessage: Record "Agent Task Message") + var + AgentTaskFile: Record "Agent Task File"; + AgentTaskMessageAttachment: Record "Agent Task Message Attachment"; + AgentTaskFileInStream: InStream; + begin + AgentTaskMessageAttachment.SetRange("Task ID", AgentTaskMessage."Task ID"); + AgentTaskMessageAttachment.SetRange("Message ID", AgentTaskMessage.ID); + if not AgentTaskMessageAttachment.FindSet() then + exit; + + repeat + if not AgentTaskFile.Get(AgentTaskMessageAttachment."Task ID", AgentTaskMessageAttachment."File ID") then + exit; + + AgentTaskFile.CalcFields(Content); + AgentTaskFile.Content.CreateInStream(AgentTaskFileInStream, TextEncoding::UTF8); + EmailMessage.AddAttachment(AgentTaskFile."File Name", AgentTaskFile."File MIME Type", AgentTaskFileInStream); + until AgentTaskMessageAttachment.Next() = 0; + end; +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al new file mode 100644 index 00000000..46c0180f --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +using System.Agents; +using System.Email; +using System.Agents.Playground.CustomAgent; +using System.Threading; + +page 50100 "Sample Setup" +{ + PageType = Card; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = "Sample Setup"; + + layout + { + area(Content) + { + group(GroupName) + { + field(Name; AgentName) + { + Caption = 'Agent'; + ApplicationArea = All; + Editable = false; + + trigger OnAssistEdit() + var + CustomAgentInfo: Record "Custom Agent Info"; + CustomAgent: Codeunit "Custom Agent"; + begin + Rec."Agent User Security ID" := SelectAgents(); // TODO: BCApps change required + + CustomAgent.GetCustomAgentById(Rec."Agent User Security ID", CustomAgentInfo); + AgentName := CustomAgentInfo."User Name"; + end; + } + + field(EmailAccount; EmailAccountName) + { + Caption = 'Email Account'; + ApplicationArea = All; + Editable = false; + + trigger OnAssistEdit() + var + TempEmailAccount: Record "Email Account" temporary; + EmailAccounts: Page "Email Accounts"; + begin + if not CheckMailboxExists() then + Page.RunModal(Page::"Email Account Wizard"); + + if not CheckMailboxExists() then + exit; + + EmailAccounts.EnableLookupMode(); + EmailAccounts.FilterConnectorV4Accounts(true); + if EmailAccounts.RunModal() = Action::LookupOK then begin + EmailAccounts.GetAccount(TempEmailAccount); + Rec."Email Account ID" := TempEmailAccount."Account Id"; + Rec."Email Connector" := TempEmailAccount.Connector; + Rec."Email Address" := TempEmailAccount."Email Address"; + EmailAccountName := TempEmailAccount."Email Address"; + end; + end; + } + } + } + } + + actions + { + area(Processing) + { + action(ScheduleSync) + { + Caption = 'Schedule Sync'; + ApplicationArea = All; + + trigger OnAction() + var + Sample: Codeunit "Sample"; + begin + Sample.ScheduleNextRun(Rec); + end; + } + action(RemoveScheduledSync) + { + Caption = 'Remove Scheduled Sync'; + ApplicationArea = All; + + trigger OnAction() + var + Sample: Codeunit "Sample"; + begin + Sample.RemoveScheduledTask(Rec); + end; + } + action(ScheduledTasks) + { + Caption = 'Scheduled Tasks'; + ApplicationArea = All; + + trigger OnAction() + var + ScheduledTask: Page "Scheduled Tasks"; + begin + ScheduledTask.Run(); + end; + } + } + } + + trigger OnOpenPage() + var + CustomAgentInfo: Record "Custom Agent Info"; + CustomAgent: Codeunit "Custom Agent"; + begin + if not Rec.FindFirst() then begin + Rec.Init(); + Rec.Insert(); + end else begin + CustomAgent.GetCustomAgentById(Rec."Agent User Security ID", CustomAgentInfo); + AgentName := CustomAgentInfo."User Name"; + EmailAccountName := Rec."Email Address"; + end; + end; + + trigger OnQueryClosePage(CloseAction: Action): Boolean + begin + Rec.Modify(); + end; + + var + AgentName: Text; + EmailAccountName: Text; + + + local procedure CheckMailboxExists(): Boolean + var + EmailAccounts: Record "Email Account"; + EmailAccount: Codeunit "Email Account"; + IConnector: Interface "Email Connector"; + begin + EmailAccount.GetAllAccounts(false, EmailAccounts); + if EmailAccounts.IsEmpty() then + exit(false); + + repeat + IConnector := EmailAccounts.Connector; + if IConnector is "Email Connector v4" then + exit(true); + until EmailAccounts.Next() = 0; + end; +} \ No newline at end of file diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al new file mode 100644 index 00000000..5227cd7b --- /dev/null +++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Agent.Sample; + +using System.Email; + +table 50100 "Sample Setup" +{ + Access = Internal; + Extensible = false; + InherentEntitlements = RIMDX; + InherentPermissions = RIMDX; + DataClassification = SystemMetadata; + + fields + { + field(1; Id; Integer) + { + AutoIncrement = true; + } + field(2; "Agent User Security ID"; Guid) + { + } + field(3; "Email Account ID"; Guid) + { + } + field(4; "Email Connector"; Enum "Email Connector") + { + } + field(5; "Email Address"; Text[2048]) + { + } + + field(6; "Last Sync At"; DateTime) + { + } + field(7; "Earliest Sync At"; DateTime) + { + } + field(8; "Scheduled Task ID"; Guid) + { + } + } + + keys + { + key(Key1; Id) + { + Clustered = true; + } + } +} \ No newline at end of file