diff --git a/src/OutlookAddin.Core/Models/EmailAddressMatch.cs b/src/OutlookAddin.Core/Models/EmailAddressMatch.cs index e41b5fc..ce1f595 100644 --- a/src/OutlookAddin.Core/Models/EmailAddressMatch.cs +++ b/src/OutlookAddin.Core/Models/EmailAddressMatch.cs @@ -4,15 +4,26 @@ namespace MarcusLaw.OutlookAddin.Core.Models { public sealed class EmailAddressMatch { + // EspoCRM's /EmailAddress/search payload uses "entityId" + "entityName" + // (newer builds) or sometimes "id" + "entityType" (older). Accept both + // by exposing parallel JSON-bound shadow setters that write the same + // primary field. + [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + [JsonPropertyName("entityId")] + public string EntityIdAlias { set => Id = value; get => Id; } + [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("entityType")] public string EntityType { get; set; } = string.Empty; + [JsonPropertyName("entityName")] + public string EntityNameAlias { set => EntityType = value; get => EntityType; } + [JsonPropertyName("emailAddress")] public string? EmailAddress { get; set; } } diff --git a/src/OutlookAddin.Core/Services/EspoCrmClient.cs b/src/OutlookAddin.Core/Services/EspoCrmClient.cs index 4140132..501fa01 100644 --- a/src/OutlookAddin.Core/Services/EspoCrmClient.cs +++ b/src/OutlookAddin.Core/Services/EspoCrmClient.cs @@ -144,7 +144,7 @@ namespace MarcusLaw.OutlookAddin.Core.Services return GetJsonAsync(path, cancellationToken); } - public Task EmailAddressSearchAsync(string emailAddress, string? entityType = null, CancellationToken cancellationToken = default) + public async Task> EmailAddressSearchAsync(string emailAddress, string? entityType = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(emailAddress)) throw new ArgumentException("emailAddress required", nameof(emailAddress)); var path = "EmailAddress/search?q=" + Uri.EscapeDataString(emailAddress); @@ -152,7 +152,31 @@ namespace MarcusLaw.OutlookAddin.Core.Services { path += "&entityType=" + Uri.EscapeDataString(entityType); } - return GetJsonAsync(path, cancellationToken); + + // EspoCRM returns a bare JSON array here, not the {total, list} + // envelope used by GlobalSearch. + var raw = await GetRawJsonAsync(path, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(raw)) return new List(); + try + { + return JsonSerializer.Deserialize>(raw, JsonOptions) + ?? new List(); + } + catch (JsonException) + { + // Some hosted builds wrap the array in { "list": [...] } — tolerate that too. + try + { + var envelope = JsonSerializer.Deserialize>(raw, JsonOptions); + return envelope?.List ?? new List(); + } + catch (JsonException) + { + _logger.Warning("EmailAddress/search returned unexpected JSON shape: {Snippet}", + raw.Substring(0, Math.Min(200, raw.Length))); + return new List(); + } + } } public Task GetCaseAsync(string id, string? select = null, CancellationToken cancellationToken = default) @@ -238,6 +262,20 @@ namespace MarcusLaw.OutlookAddin.Core.Services } } + private async Task GetRawJsonAsync(string relativePath, CancellationToken cancellationToken) + { + var response = await ExecuteAsync(HttpMethod.Get, relativePath, null, cancellationToken).ConfigureAwait(false); + try + { + await EnsureSuccessAsync(response).ConfigureAwait(false); + return await ReadBodyAsync(response).ConfigureAwait(false); + } + finally + { + response.Dispose(); + } + } + private async Task ExecuteAsync(HttpMethod method, string relativePath, string? jsonBody, CancellationToken cancellationToken) { return await _pipeline.ExecuteAsync(async ct => diff --git a/src/OutlookAddin.Core/Services/IEspoCrmClient.cs b/src/OutlookAddin.Core/Services/IEspoCrmClient.cs index 7726955..6036296 100644 --- a/src/OutlookAddin.Core/Services/IEspoCrmClient.cs +++ b/src/OutlookAddin.Core/Services/IEspoCrmClient.cs @@ -8,7 +8,7 @@ namespace MarcusLaw.OutlookAddin.Core.Services { Task FileMailAsync(FileEmailRequest request, CancellationToken cancellationToken = default); Task GlobalSearchAsync(string query, CancellationToken cancellationToken = default); - Task EmailAddressSearchAsync(string emailAddress, string? entityType = null, CancellationToken cancellationToken = default); + Task> EmailAddressSearchAsync(string emailAddress, string? entityType = null, CancellationToken cancellationToken = default); Task GetCaseAsync(string id, string? select = null, CancellationToken cancellationToken = default); Task GetAccountAsync(string id, string? select = null, CancellationToken cancellationToken = default); Task GetContactAsync(string id, string? select = null, CancellationToken cancellationToken = default); diff --git a/src/OutlookAddin.Core/Services/MatchingService.cs b/src/OutlookAddin.Core/Services/MatchingService.cs index e0cfe1a..190f28c 100644 --- a/src/OutlookAddin.Core/Services/MatchingService.cs +++ b/src/OutlookAddin.Core/Services/MatchingService.cs @@ -36,27 +36,27 @@ namespace MarcusLaw.OutlookAddin.Core.Services try { - var search = await _client.EmailAddressSearchAsync(senderEmail, "Contact", cancellationToken).ConfigureAwait(false); + var matches = await _client.EmailAddressSearchAsync(senderEmail, "Contact", cancellationToken).ConfigureAwait(false); - if (search.List.Count == 0) + if (matches.Count == 0) { _cache.Set(key, (MatchResult?)null, CacheExpiry); return null; } - if (search.List.Count > 1) + if (matches.Count > 1) { var ambiguous = new MatchResult { SenderEmail = senderEmail, IsAmbiguous = true, - CandidateCount = search.List.Count + CandidateCount = matches.Count }; _cache.Set(key, ambiguous, CacheExpiry); return ambiguous; } - var contactRef = search.List[0]; + var contactRef = matches[0]; var contact = await _client.GetContactAsync(contactRef.Id, ContactSelect, cancellationToken).ConfigureAwait(false); AccountEntity? account = null;