diff --git a/Epi2O365/O365Connector.cs b/Epi2O365/O365Connector.cs index b0c7088..7d1058f 100644 --- a/Epi2O365/O365Connector.cs +++ b/Epi2O365/O365Connector.cs @@ -21,6 +21,12 @@ namespace Epi2O365 private string tenantID; private string BaseUrl; + private const int CreateVerifyDelayMs = 2500; + private const int CreateVerifyMaxRetries = 4; + private const int CreateHttpRetryMax = 1; // 1 zusätzlicher Versuch bei 429/5xx + private const int CreateRetryCount = 1; // Anzahl zusätzlicher Versuche + + public O365Connector(string clientID, string clientSecret, string tenantID, string BaseUrl) { this.clientID = clientID; @@ -29,14 +35,18 @@ namespace Epi2O365 this.BaseUrl = BaseUrl; } - public async Task SyncContact(Contact contact, string destinationAccountPrimaryAddress) + // OData-Escape für Filterwerte + private static string O(string s) => (s ?? string.Empty).Replace("'", "''"); + + public async Task SyncContact(Contact contact, string destinationAccountPrimaryAddress, int depth = 0) { try { var graphClient = await GetGraphClient(); + if (await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient)) { - Logger.Info("Contact " + contact.DisplayName + " found in O365"); + Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " found in O365"); string O365ContactID = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName); if (!string.IsNullOrEmpty(O365ContactID)) @@ -44,23 +54,98 @@ namespace Epi2O365 await graphClient.Users[destinationAccountPrimaryAddress] .Contacts[O365ContactID] .PatchAsync(contact); - Logger.Info($"Contact with ID '{O365ContactID}' successfully updated."); + Logger.Info(destinationAccountPrimaryAddress + $" | Contact with ID '{O365ContactID}' successfully updated"); } else { - Logger.Warn("No valid contact ID found for update."); + Logger.Warn(destinationAccountPrimaryAddress + " | No valid contact ID found for update."); } } else { - Logger.Info("Contact " + contact.DisplayName + " NOT found in O365"); - Logger.Info("Creating new Contact"); - await graphClient.Users[destinationAccountPrimaryAddress].Contacts.PostAsync(contact); + // --- Create-Flow mit NickName-basierter Verifikation --- + Logger.Info(destinationAccountPrimaryAddress + " | Contact " + contact.DisplayName + " NOT found in O365"); + Logger.Info(destinationAccountPrimaryAddress + " | Creating new Contact (Nick='" + contact.NickName + "')"); + + // kleiner HTTP-Retry nur für den POST (429/5xx) + int httpAttempts = 0; + while (true) + { + try + { + // Prefer: return=representation – liefert dir direkt eine ID (nur fürs Log) + var created = await graphClient.Users[destinationAccountPrimaryAddress] + .Contacts + .PostAsync(contact, rc => rc.Headers.Add("Prefer", "return=representation")); + Logger.Info(destinationAccountPrimaryAddress + " | Created contact: Id=" + (created?.Id ?? "n/a") + " Nick='" + (created?.NickName ?? contact.NickName) + "'"); + // NickName sicherstellen (einmaliges PATCH – idempotent) + if (!string.IsNullOrEmpty(created?.Id) && !string.IsNullOrWhiteSpace(contact.NickName)) + { + try + { + await graphClient.Users[destinationAccountPrimaryAddress] + .Contacts[created.Id] + .PatchAsync(new Contact { NickName = contact.NickName }); + + Logger.Debug(destinationAccountPrimaryAddress + $" | Ensured nickName='{contact.NickName}' on Id={created.Id}"); + } + catch (Exception ex) + { + Logger.Warn(destinationAccountPrimaryAddress + $" | Unable to ensure nickName on Id={created.Id}: {ex.Message}"); + } + } + + + break; // POST erfolgreich + } + catch (Microsoft.Kiota.Abstractions.ApiException apiEx) when ( + (apiEx.ResponseStatusCode >= 500 || apiEx.ResponseStatusCode == 429) + && httpAttempts < CreateHttpRetryMax) + { + httpAttempts++; + Logger.Warn(destinationAccountPrimaryAddress + $" | Create POST failed with {apiEx.ResponseStatusCode}. Retry {httpAttempts}/{CreateHttpRetryMax} in 1s …"); + await Task.Delay(1000); + } + } + + // Verifizieren mit bis zu N Wiederholungen – immer dieselbe NickName-Checkmethode + for (int attempt = 0; attempt <= CreateVerifyMaxRetries; attempt++) + { + await Task.Delay(CreateVerifyDelayMs); + Logger.Info(destinationAccountPrimaryAddress + $" | Starting to Verify: contact '{contact.NickName}'. (attempt {attempt})."); + bool existsNow = await DoesContactExist(contact, destinationAccountPrimaryAddress, graphClient); + if (existsNow) + { + Logger.Info(destinationAccountPrimaryAddress + $" | Verified: contact '{contact.NickName}' is present after create (attempt {attempt})."); + // Optional direkt noch die ID loggen: + var id = await GetContactIdByNickName(graphClient, destinationAccountPrimaryAddress, contact.NickName); + if (!string.IsNullOrEmpty(id)) + Logger.Debug(destinationAccountPrimaryAddress + $" | Verified ContactId: {id}"); + return; + } + + if (attempt == CreateVerifyMaxRetries) + { + Logger.Warn(destinationAccountPrimaryAddress + $" | Contact '{contact.NickName}' still not visible after create (attempts: {CreateVerifyMaxRetries + 1})."); + } + else + { + Logger.Info(destinationAccountPrimaryAddress + $" | Not visible yet, re-checking (attempt {attempt + 1}/{CreateVerifyMaxRetries}) …"); + } + } + + // optional: eine vorsichtige Wiederholung mit gleicher Logik (NickName-only) + if (depth < CreateRetryCount) + { + Logger.Warn(destinationAccountPrimaryAddress + $" | Retrying SyncContact once for Nick='{contact.NickName}' (depth {depth + 1}) …"); + await Task.Delay(2000); + await SyncContact(contact, destinationAccountPrimaryAddress, depth + 1); + } } } catch (Exception ex) { - Logger.Error("Error while synchronizing contact: " + ex.Message); + Logger.Error(destinationAccountPrimaryAddress + " | Error while synchronizing contact: " + ex.Message); } } @@ -68,11 +153,15 @@ namespace Epi2O365 { try { + var safe = O(nickName); + var contacts = await graphClient.Users[userPrimaryMail] .Contacts - .GetAsync(requestConfiguration => + .GetAsync(rc => { - requestConfiguration.QueryParameters.Filter = $"NickName eq '{nickName}'"; + rc.QueryParameters.Filter = $"nickName eq '{safe}'"; + rc.QueryParameters.Select = new[] { "id", "nickName", "displayName" }; + rc.QueryParameters.Top = 1; }); var contact = contacts?.Value?.FirstOrDefault(); @@ -80,11 +169,44 @@ namespace Epi2O365 } catch (Exception ex) { - Logger.Error($"Error retrieving contact with NickName '{nickName}': {ex.Message}"); + Logger.Error(userPrimaryMail + $" | Error retrieving contact with NickName '{nickName}': {ex.Message}"); return null; } } + private async Task DoesContactExist(Contact contact, string userPrimaryMail, GraphServiceClient graphClient) + { + string NickName = contact.NickName; + if (string.IsNullOrWhiteSpace(NickName)) + { + Logger.Warn(userPrimaryMail + " | No valid NickName found for the given contact."); + return false; + } + + try + { + var safe = O(NickName); + + var contacts = await graphClient.Users[userPrimaryMail] + .Contacts + .GetAsync(rc => + { + rc.QueryParameters.Filter = $"nickName eq '{safe}'"; + rc.QueryParameters.Select = new[] { "id" }; + rc.QueryParameters.Top = 1; + }); + + bool exists = contacts?.Value != null && contacts.Value.Any(); + Logger.Info(userPrimaryMail + $" | Contact with NickName {NickName} exists: {exists}"); + return exists; + } + catch (Exception ex) + { + Logger.Error(userPrimaryMail + $" | Error checking contacts: {ex.Message}"); + return false; + } + } + private async Task GetGraphClient() { try @@ -105,35 +227,6 @@ namespace Epi2O365 throw; } } - - private async Task DoesContactExist(Contact contact, string userPrimaryMail, GraphServiceClient graphClient) - { - string NickName = contact.NickName; - if (string.IsNullOrWhiteSpace(NickName)) - { - Logger.Warn("No valid NickName found for the given contact."); - return false; - } - - try - { - var contacts = await graphClient.Users[userPrimaryMail] - .Contacts - .GetAsync(requestConfiguration => - { - requestConfiguration.QueryParameters.Filter = $"NickName eq '{NickName}'"; - }); - - bool exists = contacts?.Value != null && contacts.Value.Any(); - Logger.Info($"Contact with NickName '{NickName}' exists: {exists}"); - return exists; - } - catch (Exception ex) - { - Logger.Error($"Error checking contacts: {ex.Message}"); - return false; - } - } } public class TokenAcquisitionProvider : IAccessTokenProvider diff --git a/Epi2O365/Program.cs b/Epi2O365/Program.cs index c50a8a3..fc04f80 100644 --- a/Epi2O365/Program.cs +++ b/Epi2O365/Program.cs @@ -13,7 +13,7 @@ namespace Epi2O365 { class Program { - private static readonly ILog Logger = LogManager.GetLogger(typeof(Program)); + //private static readonly ILog Logger = LogManager.GetLogger(typeof(Program)); private static ConfigLoader Configuration; private static Model.ContactList ContactList; private static O365Connector O365; @@ -22,7 +22,7 @@ namespace Epi2O365 { string pathLastRuntime = "LastRun.txt"; DateTime LastRun = DateTime.Parse("01.01.1970 10:00:00"); - + Logger.Info("Started2"); try { if (File.Exists(pathLastRuntime)) @@ -31,7 +31,7 @@ namespace Epi2O365 string datetimeFromText = File.ReadAllText(pathLastRuntime); Logger.Debug("Found datetime string: " + datetimeFromText); - LastRun = DateTime.Parse(datetimeFromText); + LastRun = DateTime.Parse(datetimeFromText).AddHours(-4); Logger.Info("Last run timestamp: " + LastRun.ToString()); } } @@ -105,6 +105,8 @@ namespace Epi2O365 Contact o365CompanyContact = new Contact { DisplayName = contactDetail.Payload[0].Name, + GivenName = contactDetail.Payload[0].Name, + Surname= contactDetail.Payload[0].Name1, CompanyName = contactDetail.Payload[0].Name, EmailAddresses = CompanyMailAddresses, BusinessPhones = CompanyPhoneNumbers, @@ -167,7 +169,7 @@ namespace Epi2O365 { try { - O365.SyncContact(o365contact, addressBookHolder); + await O365.SyncContact(o365contact, addressBookHolder); Logger.Info("Contact person synchronized for user: " + addressBookHolder); } catch (Exception ex)