Codeunit 8906 Email Editor, source in 29
src/System Application/App/Email/src/Email/Outbox/EmailEditor.Codeunit.al563 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace System.Email;
using System.Environment;
using System.Integration.Word;
using System.IO;
using System.Telemetry;
using System.Utilities;
codeunit 8906 "Email Editor"
{
Access = Internal;
InherentPermissions = X;
InherentEntitlements = X;
Permissions = tabledata "Email Outbox" = rimd,
tabledata "Tenant Media" = r,
tabledata "Email Related Record" = rd,
tabledata "Email View Policy" = r,
tabledata "Email Rate Limit" = r,
tabledata "Sent Email" = r;
procedure Open(EmailOutbox: Record "Email Outbox"; IsModal: Boolean): Enum "Email Action"
var
EmailEditor: Page "Email Editor";
begin
CheckPermissions(EmailOutbox);
EmailEditor.SetRecord(EmailOutbox);
if IsNewOutbox then
EmailEditor.SetAsNew();
BeforeOpenEmailEditor(EmailOutbox.GetMessageId());
if IsModal then begin
Commit(); // Commit before opening modally
EmailEditor.RunModal();
exit(EmailEditor.GetAction());
end
else
EmailEditor.Run();
end;
local procedure BeforeOpenEmailEditor(MessageId: Guid)
var
Email: Codeunit Email;
EmailMessage: Codeunit "Email Message";
Telemetry: Codeunit Telemetry;
Dimensions: Dictionary of [Text, Text];
LastModifiedNo: Integer;
begin
EmailMessage.Get(MessageId);
LastModifiedNo := EmailMessage.GetNoOfModifies();
Email.OnBeforeOpenEmailEditor(EmailMessage, IsNewOutbox);
EmailMessage.Get(MessageId); // Get any latest changes
if LastModifiedNo < EmailMessage.GetNoOfModifies() then begin
Dimensions.Add('Category', EmailCategoryLbl);
Dimensions.Add('EmailMessageId', EmailMessage.GetId());
Telemetry.LogMessage('0000I2G', EmailModifiedByEventTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, Dimensions);
end;
end;
procedure OpenWithScenario(EmailOutbox: Record "Email Outbox"; IsModal: Boolean; Scenario: Enum "Email Scenario"): Enum "Email Action"
var
EmailEditor: Page "Email Editor";
begin
CheckPermissions(EmailOutbox);
EmailEditor.SetRecord(EmailOutbox);
EmailEditor.SetEmailScenario(Scenario);
if IsNewOutbox then
EmailEditor.SetAsNew();
if IsModal then begin
Commit(); // Commit before opening modally
EmailEditor.RunModal();
exit(EmailEditor.GetAction());
end
else
EmailEditor.Run();
end;
procedure CheckPermissions(EmailOutbox: Record "Email Outbox")
var
EmailImpl: Codeunit "Email Impl";
EmailViewPolicy: Interface "Email View Policy";
begin
EmailViewPolicy := EmailImpl.GetUserEmailViewPolicy();
if EmailViewPolicy.HasAccess(EmailOutbox) then
exit;
Error(EmailMessageOpenPermissionErr);
end;
procedure SetAsNew()
begin
IsNewOutbox := true;
end;
procedure GetEmailAccount(EmailOutbox: Record "Email Outbox"; var EmailAccount: Record "Email Account");
var
EmailAccounts: Codeunit "Email Account";
begin
EmailAccounts.GetAllAccounts(EmailAccount);
if not EmailAccount.Get(EmailOutbox."Account Id", EmailOutbox.Connector) then
Clear(EmailAccount);
end;
procedure GetEmailMessage(var EmailOutbox: Record "Email Outbox"; var EmailMessageImpl: Codeunit "Email Message Impl.");
begin
if EmailMessageImpl.Get(EmailOutbox."Message Id") then
exit;
EmailMessageImpl.Create('', '', '', true);
EmailOutbox."Message Id" := EmailMessageImpl.GetId();
EmailOutbox.Modify();
end;
procedure CreateOutbox(var EmailOutbox: Record "Email Outbox")
var
TempDefaultEmailAccount: Record "Email Account";
EmailScenario: Codeunit "Email Scenario";
begin
EmailOutbox."User Security Id" := UserSecurityId();
EmailOutbox.Status := Enum::"Email Status"::Draft;
if EmailScenario.GetDefaultEmailAccount(TempDefaultEmailAccount) then begin
EmailOutbox."Account Id" := TempDefaultEmailAccount."Account Id";
EmailOutbox.Connector := TempDefaultEmailAccount.Connector;
end;
EmailOutbox.Insert();
end;
procedure ChangeEmailAccount(var EmailOutbox: Record "Email Outbox"; var ChosenEmailAccount: Record "Email Account")
var
EmailAccounts: Page "Email Accounts";
begin
EmailAccounts.EnableLookupMode();
if not IsNullGuid(ChosenEmailAccount."Account Id") then
EmailAccounts.SetAccount(ChosenEmailAccount);
if EmailAccounts.RunModal() = Action::LookupOK then begin
EmailAccounts.GetAccount(ChosenEmailAccount);
EmailOutbox."Account Id" := ChosenEmailAccount."Account Id";
EmailOutbox.Connector := ChosenEmailAccount.Connector;
EmailOutbox.Modify();
end;
end;
procedure UploadAttachment(EmailMessageImpl: Codeunit "Email Message Impl.")
var
FileName: Text;
Instream: InStream;
begin
UploadIntoStream('', '', '', FileName, Instream);
if FileName = '' then
exit;
UploadAttachment(EmailMessageImpl, FileName, Instream);
end;
procedure UploadAttachment(EmailMessageImpl: Codeunit "Email Message Impl."; SingleFile: FileUpload)
begin
// Default to MS-DOS encoding to keep consistent with the old behavior
UploadAttachment(EmailMessageImpl, SingleFile, TextEncoding::MSDos);
end;
procedure UploadAttachment(EmailMessageImpl: Codeunit "Email Message Impl."; SingleFile: FileUpload; EncodeType: TextEncoding)
var
TempInStream: InStream;
begin
SingleFile.CreateInStream(TempInStream, EncodeType);
if SingleFile.FileName <> '' then
UploadAttachment(EmailMessageImpl, SingleFile.FileName, TempInStream);
end;
procedure UploadAttachment(EmailMessageImpl: Codeunit "Email Message Impl."; FileName: Text; Instream: InStream)
var
AttachmentName, ContentType : Text[250];
AttachmentSize: Integer;
begin
AttachmentName := CopyStr(FileName, 1, 250);
ContentType := EmailMessageImpl.GetContentTypeFromFilename(FileName);
AttachmentSize := EmailMessageImpl.AddAttachmentInternal(AttachmentName, ContentType, Instream);
Session.LogMessage('0000CTX', StrSubstNo(UploadingAttachmentMsg, AttachmentSize, ContentType), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EmailCategoryLbl);
end;
procedure DownloadAttachment(MediaID: Guid; FileName: Text)
var
TenantMedia: Record "Tenant Media";
EmailMessage: Codeunit "Email Message";
MediaInStream: InStream;
Handled: Boolean;
begin
TenantMedia.Get(MediaID);
TenantMedia.CalcFields(Content);
if TenantMedia.Content.HasValue() then
TenantMedia.Content.CreateInStream(MediaInStream)
else begin
EmailMessage.OnGetAttachmentContent(MediaID, MediaInStream, Handled);
if not Handled then
Error(NoAttachmentContentMsg);
end;
DownloadFromStream(MediaInStream, '', '', '', FileName);
end;
procedure DownloadAttachments(Attachments: Dictionary of [Guid, Text]; FileName: Text)
var
TenantMedia: Record "Tenant Media";
DataCompression: Codeunit "Data Compression";
ZipTempBlob: Codeunit "Temp Blob";
MediaID: Guid;
MediaInStream: InStream;
ZipOutStream: OutStream;
ZipInStream: InStream;
begin
DataCompression.CreateZipArchive();
foreach MediaID in Attachments.Keys do begin
TenantMedia.Get(MediaID);
TenantMedia.CalcFields(Content);
TenantMedia.Content.CreateInStream(MediaInStream);
DataCompression.AddEntry(MediaInStream, Attachments.Get(MediaID));
end;
ZipTempBlob.CreateOutStream(ZipOutStream);
DataCompression.SaveZipArchive(ZipOutStream);
DataCompression.CloseZipArchive();
ZipTempBlob.CreateInStream(ZipInStream);
DownloadFromStream(ZipInStream, '', '', '', FileName);
end;
procedure ValidateEmailData(FromEmailAddress: Text; var EmailMessageImpl: Codeunit "Email Message Impl."): Boolean
begin
// Validate email account
if FromEmailAddress = '' then
Error(NoFromAccountErr);
// Validate recipients
EmailMessageImpl.ValidateRecipients();
// Verify related records against current recipients
VerifyRelatedRecords(EmailMessageImpl.GetId());
if EmailMessageImpl.GetSubject() = '' then
exit(Dialog.Confirm(NoSubjectlineQst, false));
exit(true);
end;
procedure SendOutbox(var EmailOutbox: Record "Email Outbox")
var
EmailImpl: Codeunit "Email Impl";
EmailMessage: Codeunit "Email Message";
begin
EmailMessage.Get(EmailOutbox."Message Id");
if not EmailImpl.Send(EmailMessage, EmailOutbox."Account Id", EmailOutbox.Connector, EmailOutbox) then
Error(SendingFailedErr, GetLastErrorText());
end;
procedure DiscardEmail(var EmailOutbox: Record "Email Outbox"; Confirm: Boolean): Boolean
begin
if Confirm then
if not Confirm(ConfirmDiscardEmailQst, true) then
exit(false);
exit(EmailOutbox.Delete(true)); // This should delete the email message, recipients and attachments as well.
end;
procedure AttachFromRelatedRecords(EmailMessageID: Guid);
var
TempEmailRelatedAttachment: Record "Email Related Attachment";
Email: Codeunit Email;
EmailRelatedAttachmentsPage: Page "Email Related Attachments";
begin
EmailRelatedAttachmentsPage.LookupMode(true);
EmailRelatedAttachmentsPage.SetMessageID(EmailMessageID);
if EmailRelatedAttachmentsPage.RunModal() <> Action::LookupOK then
exit;
EmailRelatedAttachmentsPage.GetSelectedAttachments(TempEmailRelatedAttachment);
if TempEmailRelatedAttachment.FindSet() then
repeat
Email.OnGetAttachment(TempEmailRelatedAttachment."Attachment Table ID", TempEmailRelatedAttachment."Attachment System ID", EmailMessageID);
until TempEmailRelatedAttachment.Next() = 0;
end;
local procedure GetPrimarySourceEntity(var PrimarySource: Integer; EmailMessageID: Guid; RelatedIds: List of [Integer]): Boolean
var
EmailRelatedRecord: Record "Email Related Record";
RelatedId: Integer;
begin
// If there is only one key in the dict, then there is no need to use DB resources.
if RelatedIds.Count = 1 then begin
PrimarySource := RelatedIds.Get(1);
exit(true);
end;
EmailRelatedRecord.SetRange("Email Message Id", EmailMessageID);
foreach RelatedId in RelatedIds do begin
EmailRelatedRecord.SetFilter("Table Id", Format(RelatedId));
if EmailRelatedRecord.FindSet() then
repeat
if EmailRelatedRecord."Relation Type" = EmailRelatedRecord."Relation Type"::"Primary Source" then begin
PrimarySource := RelatedId;
exit(true);
end;
until EmailRelatedRecord.Next() = 0;
end;
exit(false);
end;
local procedure FindEmailSourceEntities(EmailMessageID: Guid; var Dict: Dictionary of [Integer, Text]): Boolean
var
EmailRelatedRecord: Record "Email Related Record";
SystemIdFilter: Text;
begin
EmailRelatedRecord.SetRange("Email Message Id", EmailMessageID);
if EmailRelatedRecord.FindSet() then
repeat
if Dict.Get(EmailRelatedRecord."Table Id", SystemIdFilter) then
Dict.Set(EmailRelatedRecord."Table Id", SystemIdFilter + '|' + Format(EmailRelatedRecord."System Id"))
else
Dict.Add(EmailRelatedRecord."Table Id", Format(EmailRelatedRecord."System Id"));
until EmailRelatedRecord.Next() <= 0;
exit(EmailRelatedRecord.Count() > 0);
end;
procedure LoadWordTemplate(EmailMessageImpl: Codeunit "Email Message Impl."; EmailMessageID: Guid)
var
WordTemplateRecord: Record "Word Template";
WordTemplateToTextWizard: Page "Word Template To Text Wizard";
TemplateSize: Integer;
Dict: Dictionary of [Integer, Text];
PrimarySource: Integer;
begin
if FindEmailSourceEntities(EmailMessageID, Dict) then begin
if not GetPrimarySourceEntity(PrimarySource, EmailMessageID, Dict.Keys) then
Error(NoPrimarySourceOnEmailErr);
WordTemplateToTextWizard.SetData(Dict, PrimarySource);
WordTemplateToTextWizard.RunModal();
if not WordTemplateToTextWizard.WasDialogCompleted() then begin
Session.LogMessage('0000FL1', LoadingTemplateExitedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EmailCategoryLbl);
exit;
end;
WordTemplateToTextWizard.GetRecord(WordTemplateRecord);
if EmailMessageImpl.GetSubject() = '' then
EmailMessageImpl.SetSubject(WordTemplateRecord.Name);
EmailMessageImpl.SetBody(WordTemplateToTextWizard.GetDocumentAsText());
EmailMessageImpl.Modify();
TemplateSize := WordTemplateToTextWizard.GetDocumentSize();
Session.LogMessage('0000FL2', StrSubstNo(LoadingTemplateMsg, TemplateSize), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EmailCategoryLbl);
end;
end;
procedure AttachFromWordTemplate(EmailMessageImpl: Codeunit "Email Message Impl."; EmailMessageID: Guid)
var
WordTemplateRecord: Record "Word Template";
WordTemplateSelectionWizard: Page "Word Template Selection Wizard";
InStream: InStream;
Filename: Text;
FileSize: Integer;
ContentType: Text[250];
Dict: Dictionary of [Integer, Text];
begin
if FindEmailSourceEntities(EmailMessageID, Dict) then begin
WordTemplateSelectionWizard.SetData(Dict);
WordTemplateSelectionWizard.SaveAsDocumentStream();
WordTemplateSelectionWizard.RunModal();
if not WordTemplateSelectionWizard.WasDialogCompleted() then begin
Session.LogMessage('0000FL3', LoadingTemplateExitedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EmailCategoryLbl);
exit;
end;
WordTemplateSelectionWizard.GetRecord(WordTemplateRecord);
WordTemplateSelectionWizard.GetDocumentStream(InStream);
Filename := WordTemplateRecord.Name + '.' + WordTemplateSelectionWizard.GetDocumentFormat();
ContentType := EmailMessageImpl.GetContentTypeFromFilename(Filename);
FileSize := EmailMessageImpl.AddAttachmentInternal(CopyStr(Filename, 1, 250), ContentType, InStream);
Session.LogMessage('0000FL4', StrSubstNo(UploadingTemplateAttachmentMsg, FileSize, ContentType), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EmailCategoryLbl);
end else
Error(NoEmailSourceEntitiesErr);
end;
procedure GetRelatedAttachments(EmailMessageId: Guid; var EmailRelatedAttachmentOut: Record "Email Related Attachment")
var
EmailMessageImpl: Codeunit "Email Message Impl.";
begin
if not EmailMessageImpl.GetRelatedAttachments(EmailMessageId, EmailRelatedAttachmentOut) then
Message(NoRelatedAttachmentsErr);
end;
procedure LookupRecipients(MessageID: Guid; var Text: Text): Boolean
var
TempSuggestedEmailAddressLookup: Record "Email Address Lookup";
EmailRelatedRecord: Record "Email Related Record";
EmailAddressLookup: Codeunit "Email Address Lookup";
EmailAddressLookupPage: Page "Email Address Lookup";
EntityType: Enum "Email Address Entity";
begin
// Get Suggested Email Addresses
EmailRelatedRecord.SetRange("Email Message Id", MessageID);
if EmailRelatedRecord.FindSet() then
repeat
EmailAddressLookup.OnGetSuggestedAddresses(EmailRelatedRecord."Table Id", EmailRelatedRecord."System Id", TempSuggestedEmailAddressLookup);
until EmailRelatedRecord.Next() = 0;
if TempSuggestedEmailAddressLookup.FindFirst() then
EntityType := TempSuggestedEmailAddressLookup."Entity type";
EmailAddressLookupPage.AddSuggestions(TempSuggestedEmailAddressLookup);
TempSuggestedEmailAddressLookup.DeleteAll();
EmailAddressLookupPage.LookupMode(true);
EmailAddressLookupPage.SetEntityType(EntityType);
if (EmailAddressLookupPage.RunModal() = Action::LookupOK) or (EmailAddressLookupPage.WasFullAddressLookup()) then begin
EmailAddressLookupPage.GetSelectedSuggestions(TempSuggestedEmailAddressLookup);
if TempSuggestedEmailAddressLookup.FindSet() then begin
if (Text <> '') and (not Text.EndsWith(';')) then
Text += ';';
Text += EmailAddressLookup.GetSelectedSuggestionsAsText(TempSuggestedEmailAddressLookup);
// Added recipients is added as related entities on the email
AddRelatedRecordsFromEmailAddress(MessageID, TempSuggestedEmailAddressLookup);
exit(true);
end;
end;
exit(false);
end;
procedure VerifyRelatedRecords(MessageID: Guid)
var
EmailRelatedRecord: Record "Email Related Record";
EmailMessage: Codeunit "Email Message";
TempCache: Dictionary of [Text, Guid];
TempGuid: Guid;
Count: Integer;
begin
// Copy values to temporary cache
for Count := 1 to RelatedRecordsCache.Keys().Count() do begin
RelatedRecordsCache.Get(RelatedRecordsCache.Keys.Get(Count), TempGuid);
TempCache.Add(RelatedRecordsCache.Keys.Get(Count), TempGuid);
end;
// Remove related records that is To, Cc, Bcc
EmailMessage.Get(MessageID);
RemoveFromCacheIfExists(TempCache, EmailMessage, Enum::"Email Recipient Type"::"To");
RemoveFromCacheIfExists(TempCache, EmailMessage, Enum::"Email Recipient Type"::Cc);
RemoveFromCacheIfExists(TempCache, EmailMessage, Enum::"Email Recipient Type"::Bcc);
// Those left in cache was not found in To, Cc or Bcc, and needs to be removed as related records
for Count := 1 to TempCache.Values.Count() do
if EmailRelatedRecord.GetBySystemId(TempCache.Values.Get(Count)) then
if EmailRelatedRecord."Relation Origin" = Enum::"Email Relation Origin"::"Email Address Lookup" then
EmailRelatedRecord.Delete();
// Update global cache
for Count := 1 to TempCache.Keys.Count() do
RelatedRecordsCache.Remove(TempCache.Keys.Get(Count));
end;
internal procedure RemoveFromCacheIfExists(var TempCache: Dictionary of [Text, Guid]; var EmailMessage: Codeunit "Email Message"; RecipientType: Enum "Email Recipient Type")
var
Recipients: List of [Text];
Count: Integer;
begin
EmailMessage.GetRecipients(RecipientType, Recipients);
for Count := 1 to Recipients.Count() do
if TempCache.ContainsKey(Recipients.Get(Count)) then
TempCache.Remove(Recipients.Get(Count));
end;
internal procedure PopulateRelatedRecordCache(MessageID: Guid)
var
EmailRelatedRecord: Record "Email Related Record";
TempEmailAddressLookupRecord: Record "Email Address Lookup";
EmailAddressLookup: Codeunit "Email Address Lookup";
begin
EmailRelatedRecord.SetFilter("Email Message Id", MessageID);
if not EmailRelatedRecord.FindSet() then
exit;
repeat
if EmailRelatedRecord."Relation Origin" = Enum::"Email Relation Origin"::"Email Address Lookup" then begin
EmailAddressLookup.OnGetSuggestedAddresses(EmailRelatedRecord."Table Id", EmailRelatedRecord."System Id", TempEmailAddressLookupRecord);
if RelatedRecordsCache.Add(TempEmailAddressLookupRecord."E-Mail Address", EmailRelatedRecord.SystemId) then;
end;
until EmailRelatedRecord.Next() = 0;
end;
internal procedure AddRelatedRecordsFromEmailAddress(MessageID: Guid; var EmailAddressLookupSuggestion: Record "Email Address Lookup")
var
EmailRelatedRecord: Record "Email Related Record";
EmailMessage: Codeunit "Email Message";
Email: Codeunit Email;
EmailRelationType: Enum "Email Relation Type";
HasRelations: Boolean;
begin
EmailMessage.Get(MessageID);
HasRelations := Email.HasRelations(EmailMessage);
if EmailAddressLookupSuggestion.FindSet() then
repeat
if not HasRelations then begin
EmailRelationType := Enum::"Email Relation Type"::"Primary Source";
HasRelations := true;
end else
EmailRelationType := Enum::"Email Relation Type"::"Related Entity";
Email.AddRelation(EmailMessage, EmailAddressLookupSuggestion."Source Table Number", EmailAddressLookupSuggestion."Source System Id", EmailRelationType, Enum::"Email Relation Origin"::"Email Address Lookup");
// Add related record to cache
if EmailRelatedRecord.Get(EmailAddressLookupSuggestion."Source Table Number", EmailAddressLookupSuggestion."Source System Id", EmailMessage.GetId()) then
if RelatedRecordsCache.Add(EmailAddressLookupSuggestion."E-Mail Address", EmailRelatedRecord.SystemId) then;
until EmailAddressLookupSuggestion.Next() = 0;
end;
var
RelatedRecordsCache: Dictionary of [Text, Guid];
IsNewOutbox: Boolean;
ConfirmDiscardEmailQst: Label 'Go ahead and discard?';
EmailMessageOpenPermissionErr: Label 'You do not have permission to open the email message.';
NoSubjectlineQst: Label 'Do you want to send this message without a subject?';
NoFromAccountErr: Label 'You must specify a valid email account to send the message to.';
LoadingTemplateMsg: Label 'Applied word template to email body with size: %1.', Comment = '%1 - File size', Locked = true;
UploadingAttachmentMsg: Label 'Attached file with size: %1, Content type: %2', Comment = '%1 - File size, %2 - Content type', Locked = true;
UploadingTemplateAttachmentMsg: Label 'Attached word template with size: %1, Content type: %2', Comment = '%1 - File size, %2 - Content type', Locked = true;
EmailCategoryLbl: Label 'Email', Locked = true;
SendingFailedErr: Label 'The email was not sent because of the following error: "%1" \\Depending on the error, you might need to contact your administrator.', Comment = '%1 - the error that occurred.';
NoRelatedAttachmentsErr: Label 'Did not find any attachments related to this email.';
LoadingTemplateExitedMsg: Label 'Did not apply word template to email as user exited dialog.';
NoPrimarySourceOnEmailErr: Label 'Failed to find the primary source entity';
NoAttachmentContentMsg: Label 'The attachment content is no longer available.';
EmailModifiedByEventTxt: Label 'Email has been modified by event', Locked = true;
NoEmailSourceEntitiesErr: Label 'There are no linked email source entities';
}