Codeunit 6700 O365 Sync. Management
- App
- Base Application
- Versions
- 17-28
- Obsolete
- Pending 28.0, seen from 28. Contact sync has been moved to the assisted setup experience with the new Graph-based implementation.
Procedures, 18Events, 1Obsolete, 4
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/O365SyncManagement.Codeunit.al450 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.
// ------------------------------------------------------------------------------------------------
#pragma warning disable AA0247
#if not CLEAN28
codeunit 6700 "O365 Sync. Management"
{
ObsoleteReason = 'Contact sync has been moved to the assisted setup experience with the new Graph-based implementation.';
ObsoleteState = Pending;
ObsoleteTag = '28.0';
trigger OnRun()
var
IsHandled: Boolean;
begin
OnBeforeOnRun(IsHandled);
if IsHandled then
exit;
CODEUNIT.Run(CODEUNIT::"Exchange Contact Sync.");
CODEUNIT.Run(CODEUNIT::"Booking Customer Sync.");
CODEUNIT.Run(CODEUNIT::"Booking Service Sync.");
end;
var
IsolatedStorageManagement: Codeunit "Isolated Storage Management";
ProgressWindow: Dialog;
BookingsConnectionID: Text;
ConnectionErr: Label '%1 is unable to connect to Exchange. This may be due to a service outage or invalid credentials.', Comment = '%1 = User who cannot connect';
LoggingConstTxt: Label 'Contact synchronization.';
O365RecordMissingErr: Label 'The Office 365 synchronization setup record is not configured correctly.';
ExchangeConnectionID: Text;
RegisterConnectionTxt: Label 'Register connection.';
SetupO365Qst: Label 'Would you like to configure your connection to Office 365 now?';
BookingsConnectionString: SecretText;
ExchangeConnectionString: SecretText;
GettingContactsTxt: Label 'Getting Exchange contacts.';
GettingBookingCustomersTxt: Label 'Getting Booking customers.';
GettingBookingServicesTxt: Label 'Getting Booking services.';
NoUserAccessErr: Label 'Could not connect to %1. Verify that %2 is an administrator in the Bookings mailbox.', Comment = '%1 = The Bookings company; %2 = The user';
JobQueueEntryDescTxt: Label 'Auto-created for retrieval of new data from Outlook and Bookings. Can be deleted if not used. Will be recreated when the feature is activated.';
[TryFunction]
[Scope('OnPrem')]
procedure GetBookingMailboxes(BookingSync: Record "Booking Sync"; var TempBookingMailbox: Record "Booking Mailbox" temporary; MailboxName: Text)
var
BookingMailbox: Record "Booking Mailbox";
begin
BookingSync."Booking Mailbox Address" := CopyStr(MailboxName, 1, 80);
RegisterBookingsConnection(BookingSync);
TempBookingMailbox.Reset();
TempBookingMailbox.DeleteAll();
if BookingMailbox.FindSet() then
repeat
TempBookingMailbox.Init();
TempBookingMailbox.TransferFields(BookingMailbox);
TempBookingMailbox.Insert();
until BookingMailbox.Next() = 0;
end;
[Scope('OnPrem')]
procedure CreateExchangeConnection(var ExchangeSync: Record "Exchange Sync") Valid: Boolean
var
User: Record User;
AuthenticationEmail: Text[250];
begin
IsO365Setup(false);
if GetUser(User, ExchangeSync."User ID") then begin
AuthenticationEmail := User."Authentication Email";
Valid := ValidateExchangeConnection(AuthenticationEmail, ExchangeSync);
end;
end;
[Scope('OnPrem')]
procedure IsO365Setup(AddOnTheFly: Boolean): Boolean
var
User: Record User;
LocalExchangeSync: Record "Exchange Sync";
AuthenticationEmail: Text[250];
Password: SecretText;
Token: SecretText;
begin
if GetUser(User, UserId()) then
AuthenticationEmail := User."Authentication Email";
if not LocalExchangeSync.Get(UserId) or
(AuthenticationEmail = '') or (LocalExchangeSync."Folder ID" = '') or not GetPasswordOrToken(LocalExchangeSync, Password, Token)
then
if AddOnTheFly then begin
if not OpenSetupWindow() then
Error(O365RecordMissingErr)
end else
Error(O365RecordMissingErr);
exit(true);
end;
procedure OpenSetupWindow(): Boolean
var
ExchangeSyncSetup: Page "Exchange Sync. Setup";
begin
if Confirm(SetupO365Qst, true) then
exit(ExchangeSyncSetup.RunModal() = ACTION::OK);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Company-Initialize", 'OnCompanyInitialize', '', false, false)]
local procedure SetupContactSyncJobQueue()
var
JobQueueEntry: Record "Job Queue Entry";
TwentyFourHours: Integer;
begin
if TASKSCHEDULER.CanCreateTask() then begin
JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit);
JobQueueEntry.SetRange("Object ID to Run", CODEUNIT::"O365 Sync. Management");
if JobQueueEntry.IsEmpty() then begin
TwentyFourHours := 24 * 60;
JobQueueEntry.InitRecurringJob(TwentyFourHours);
JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit;
JobQueueEntry."Object ID to Run" := CODEUNIT::"O365 Sync. Management";
JobQueueEntry.Description := CopyStr(JobQueueEntryDescTxt, 1, MaxStrLen(JobQueueEntry.Description));
CODEUNIT.Run(CODEUNIT::"Job Queue - Enqueue", JobQueueEntry);
end;
end;
end;
[Scope('OnPrem')]
[obsolete('Deprecated not in use -Booking sync setup', '28.0')]
procedure SyncBookingCustomers(var BookingSync: Record "Booking Sync")
var
BookingCustomerSync: Codeunit "Booking Customer Sync.";
begin
Session.LogMessage('0000ACP', 'Syncing Bookings Customers', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TraceCategory());
CheckUserAccess(BookingSync);
ShowProgress(GettingBookingCustomersTxt);
RegisterBookingsConnection(BookingSync);
CloseProgress();
BookingCustomerSync.SyncRecords(BookingSync);
end;
[Scope('OnPrem')]
procedure SyncBookingServices(var BookingSync: Record "Booking Sync")
var
BookingServiceSync: Codeunit "Booking Service Sync.";
begin
Session.LogMessage('0000ACQ', 'Syncing Bookings Services', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TraceCategory());
CheckUserAccess(BookingSync);
ShowProgress(GettingBookingServicesTxt);
RegisterBookingsConnection(BookingSync);
CloseProgress();
BookingServiceSync.SyncRecords(BookingSync);
end;
[Scope('OnPrem')]
procedure SyncExchangeContacts(ExchangeSync: Record "Exchange Sync"; FullSync: Boolean)
var
ExchangeContactSync: Codeunit "Exchange Contact Sync.";
begin
Session.LogMessage('0000ACR', 'Syncing Exchange Contacts', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TraceCategory());
ShowProgress(GettingContactsTxt);
RegisterExchangeConnection(ExchangeSync);
CloseProgress();
ExchangeContactSync.SyncRecords(ExchangeSync, FullSync);
end;
procedure LogActivityFailed(RecordID: Variant; UserID: Code[50]; ActivityDescription: Text; ActivityMessage: Text)
var
ActivityLog: Record "Activity Log";
begin
ActivityMessage := GetLastErrorText + ' ' + ActivityMessage;
ClearLastError();
ActivityLog.LogActivityForUser(RecordID, ActivityLog.Status::Failed, CopyStr(LoggingConstTxt, 1, 30),
ActivityDescription, ActivityMessage, UserID);
end;
[Scope('OnPrem')]
procedure BuildBookingsConnectionStringAsSecretText(var BookingSync: Record "Booking Sync") ConnectionString: SecretText
var
User: Record User;
ExchangeSync: Record "Exchange Sync";
Password: SecretText;
Token: SecretText;
begin
// Example connection string
// {UserName}="user@user.onmicrosoft.com";{Password}="1234";{FolderID}="Dynamics NAV";{Uri}=https://outlook.office365.com/EWS/Exchange.asmx
ExchangeSync.Get(BookingSync."User ID");
if (not GetUser(User, BookingSync."User ID")) or
(User."Authentication Email" = '') or
(not GetPasswordOrToken(ExchangeSync, Password, Token))
then
Error(O365RecordMissingErr);
ConnectionString :=
SecretStrSubstNo(
'{UserName}=%1;{Password}=%2;{Token}=%3;{Mailbox}=%4;',
User."Authentication Email",
Password,
Token,
BookingSync."Booking Mailbox Address");
if not Token.IsEmpty() then
ConnectionString := SecretStrSubstNo('%1;{Uri}=%2', ConnectionString, ExchangeSync.GetExchangeEndpoint());
end;
[Scope('OnPrem')]
procedure BuildExchangeConnectionStringAsSecretText(var ExchangeSync: Record "Exchange Sync") ConnectionString: SecretText
var
User: Record User;
Token: SecretText;
Password: SecretText;
begin
// Example connection string
// {UserName}="user@user.onmicrosoft.com";{Password}="1234";{FolderID}="Dynamics NAV";{Uri}=https://outlook.office365.com/EWS/Exchange.asmx
if (not GetUser(User, ExchangeSync."User ID")) or
(User."Authentication Email" = '') or
(ExchangeSync."Folder ID" = '') or (not GetPasswordOrToken(ExchangeSync, Password, Token))
then
Error(O365RecordMissingErr);
ConnectionString :=
SecretStrSubstNo(
'{UserName}=%1;{Password}=%2;{Token}=%3;{FolderID}=%4;',
User."Authentication Email",
Password,
Token,
ExchangeSync."Folder ID");
if not Token.IsEmpty() then
ConnectionString := SecretStrSubstNo('%1;{Uri}=%2', ConnectionString, ExchangeSync.GetExchangeEndpoint());
end;
[Scope('OnPrem')]
procedure RegisterBookingsConnection(BookingSync: Record "Booking Sync")
var
ExchangeSync: Record "Exchange Sync";
begin
if ExchangeConnectionID <> '' then
UnregisterConnection(ExchangeConnectionID);
if BookingsConnectionReady(BookingSync) then
exit;
if BookingsConnectionID <> '' then
UnregisterConnection(BookingsConnectionID);
ExchangeSync.Get(BookingSync."User ID");
BookingsConnectionID := CreateGuid();
if RegisterConnection(ExchangeSync, BookingsConnectionID, BookingsConnectionString) then
SetConnection(ExchangeSync, BookingsConnectionID);
end;
[Scope('OnPrem')]
procedure RegisterExchangeConnection(ExchangeSync: Record "Exchange Sync")
begin
if BookingsConnectionID <> '' then
UnregisterConnection(BookingsConnectionID);
if ExchangeConnectionReady(ExchangeSync) then
exit;
if ExchangeConnectionID <> '' then
UnregisterConnection(ExchangeConnectionID);
ExchangeConnectionID := CreateGuid();
if RegisterConnection(ExchangeSync, ExchangeConnectionID, ExchangeConnectionString) then
SetConnection(ExchangeSync, ExchangeConnectionID);
end;
procedure TraceCategory(): Text
begin
exit('ExchangeSync');
end;
[TryFunction]
[NonDebuggable]
local procedure TryRegisterConnection(ConnectionID: Guid; ConnectionString: SecretText)
begin
// Using a try function, as these may throw an exception under certain circumstances (improper credentials, broken connection)
RegisterTableConnection(TABLECONNECTIONTYPE::Exchange, ConnectionID, ConnectionString.Unwrap());
end;
local procedure RegisterConnection(ExchangeSync: Record "Exchange Sync"; ConnectionID: Guid; ConnectionString: SecretText) Success: Boolean
begin
Success := TryRegisterConnection(ConnectionID, ConnectionString);
if not Success then
ConnectionFailure(ExchangeSync);
end;
[TryFunction]
local procedure TrySetConnection(ConnectionID: Guid)
begin
// Using a try function, as these may throw an exception under certain circumstances (improper credentials, broken connection)
SetDefaultTableConnection(TABLECONNECTIONTYPE::Exchange, ConnectionID);
end;
local procedure SetConnection(ExchangeSync: Record "Exchange Sync"; ConnectionID: Guid) Success: Boolean
begin
Success := TrySetConnection(ConnectionID);
if not Success then
ConnectionFailure(ExchangeSync);
end;
[TryFunction]
local procedure UnregisterConnection(var ConnectionID: Text)
begin
UnregisterTableConnection(TABLECONNECTIONTYPE::Exchange, ConnectionID);
ConnectionID := '';
end;
local procedure ConnectionFailure(ExchangeSync: Record "Exchange Sync")
begin
LogActivityFailed(
ExchangeSync.RecordId, RegisterConnectionTxt, StrSubstNo(ConnectionErr, ExchangeSync."User ID"), ExchangeSync."User ID");
if GuiAllowed() then begin
CloseProgress();
Error(ConnectionErr, ExchangeSync."User ID");
end;
end;
[Scope('OnPrem')]
procedure ValidateExchangeConnection(AuthenticationEmail: Text[250]; var ExchangeSync: Record "Exchange Sync") Valid: Boolean
var
ExchangeWebServicesServer: Codeunit "Exchange Web Services Server";
Credentials: DotNet ExchangeCredentials;
Endpoint: Text[250];
begin
CreateExchangeAccountCredentials(ExchangeSync, Credentials);
Endpoint := ExchangeSync."Exchange Service URI";
Valid := ExchangeWebServicesServer.InitializeAndValidate(AuthenticationEmail, Endpoint, Credentials);
if Valid and (Endpoint <> ExchangeSync."Exchange Service URI") then begin
ExchangeSync.Validate("Exchange Service URI", Endpoint);
ExchangeSync.Modify();
end;
end;
[NonDebuggable]
local procedure CreateExchangeAccountCredentials(var ExchangeSync: Record "Exchange Sync"; var Credentials: DotNet ExchangeCredentials)
var
User: Record User;
WebCredentials: DotNet WebCredentials;
OAuthCredentials: DotNet OAuthCredentials;
AuthenticationEmail: Text[250];
Token: SecretText;
Password: SecretText;
begin
if GetUser(User, ExchangeSync."User ID") then
AuthenticationEmail := User."Authentication Email";
if AuthenticationEmail = '' then
Error(O365RecordMissingErr);
if not GetPasswordOrToken(ExchangeSync, Password, Token) then
Error(O365RecordMissingErr);
if not Token.IsEmpty() then
Credentials := OAuthCredentials.OAuthCredentials(Token.Unwrap())
else
Credentials := WebCredentials.WebCredentials(AuthenticationEmail, Password.Unwrap());
end;
local procedure GetUser(var User: Record User; UserID: Text[50]): Boolean
begin
User.SetRange("User Name", UserID);
exit(User.FindFirst());
end;
procedure ShowProgress(Message: Text)
begin
if GuiAllowed then begin
CloseProgress();
ProgressWindow.Open(Message);
end;
end;
procedure CloseProgress()
begin
if GuiAllowed then
if TryCloseProgress() then;
end;
[TryFunction]
local procedure TryCloseProgress()
begin
ProgressWindow.Close();
end;
[NonDebuggable]
local procedure ConnectionStringChanged(Before: SecretText; After: SecretText): Boolean
begin
exit(Before.Unwrap() <> After.Unwrap());
end;
local procedure BookingsConnectionReady(BookingSync: Record "Booking Sync") Ready: Boolean
var
NewConnectionString: SecretText;
begin
NewConnectionString := BuildBookingsConnectionStringAsSecretText(BookingSync);
Ready := (BookingsConnectionID <> '') and (not ConnectionStringChanged(BookingsConnectionString, NewConnectionString));
BookingsConnectionString := NewConnectionString;
end;
local procedure ExchangeConnectionReady(ExchangeSync: Record "Exchange Sync") Ready: Boolean
var
NewConnectionString: SecretText;
begin
NewConnectionString := BuildExchangeConnectionStringAsSecretText(ExchangeSync);
Ready := (ExchangeConnectionID <> '') and (not ConnectionStringChanged(ExchangeConnectionString, NewConnectionString));
ExchangeConnectionString := NewConnectionString;
end;
[Scope('OnPrem')]
local procedure GetPasswordOrToken(ExchangeSync: Record "Exchange Sync"; var Password: SecretText; var Token: SecretText): Boolean
var
AzureADMgt: Codeunit "Azure AD Mgt.";
Value: SecretText;
begin
Token := AzureADMgt.GetAccessTokenAsSecretText(AzureADMgt.GetO365Resource(), AzureADMgt.GetO365ResourceName(), false);
if Token.IsEmpty() and not IsNullGuid(ExchangeSync."Exchange Account Password Key") then
if IsolatedStorageManagement.Get(ExchangeSync."Exchange Account Password Key", DATASCOPE::Company, Value) then
Password := Value;
exit((not Token.IsEmpty()) or (not Password.IsEmpty()));
end;
local procedure CheckUserAccess(BookingSync: Record "Booking Sync")
var
ExchangeSync: Record "Exchange Sync";
Credentials: DotNet ExchangeCredentials;
ExchangeServiceFactory: DotNet ServiceWrapperFactory;
ExchangeService: DotNet ExchangeServiceWrapper;
begin
ExchangeSync.Get(BookingSync."User ID");
ExchangeService := ExchangeServiceFactory.CreateServiceWrapper2013();
CreateExchangeAccountCredentials(ExchangeSync, Credentials);
ExchangeService.SetNetworkCredential(Credentials);
ExchangeService.ExchangeServiceUrl := ExchangeSync.GetExchangeEndpoint();
if not ExchangeService.CanAccessBookingMailbox(BookingSync."Booking Mailbox Address") then
Error(NoUserAccessErr, BookingSync."Booking Mailbox Name", BookingSync."User ID");
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeOnRun(var IsHandled: Boolean)
begin
end;
}
#endif