Embedded images replaced with cid linked resources. (#313)

* Added logic to replace embedded images with linked resources

* Added alt text for images and replaced NewtonSoft with Text.Json

* Fix draft mime preparation

* Fix crashes for signatures without images.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
This commit is contained in:
Tiktack
2024-08-11 23:58:54 +02:00
committed by GitHub
parent 983bc21448
commit 5912adff93
23 changed files with 148 additions and 253 deletions

View File

@@ -1,8 +1,8 @@
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
@@ -58,14 +58,14 @@ namespace Wino.Core.Authenticators
if (!response.IsSuccessStatusCode)
throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed);
var parsed = JObject.Parse(responseString);
var parsed = JsonNode.Parse(responseString).AsObject();
if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error"]["message"].Value<string>());
throw new GoogleAuthenticationException(parsed["error"]["message"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>();
var refreshToken = parsed["refresh_token"].Value<string>();
var expiresIn = parsed["expires_in"].Value<long>();
var accessToken = parsed["access_token"].GetValue<string>();
var refreshToken = parsed["refresh_token"].GetValue<string>();
var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
@@ -76,12 +76,12 @@ namespace Wino.Core.Authenticators
var userinfoResponse = await client.GetAsync(UserInfoEndpoint);
string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
var parsedUserInfo = JObject.Parse(userinfoResponseContent);
var parsedUserInfo = JsonNode.Parse(userinfoResponseContent).AsObject();
if (parsedUserInfo.ContainsKey("error"))
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].Value<string>());
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].GetValue<string>());
var username = parsedUserInfo["emailAddress"].Value<string>();
var username = parsedUserInfo["emailAddress"].GetValue<string>();
return new TokenInformation()
{
@@ -166,13 +166,13 @@ namespace Wino.Core.Authenticators
string responseString = await response.Content.ReadAsStringAsync();
var parsed = JObject.Parse(responseString);
var parsed = JsonNode.Parse(responseString).AsObject();
// TODO: Error parsing is incorrect.
if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error_description"].Value<string>());
throw new GoogleAuthenticationException(parsed["error_description"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>();
var accessToken = parsed["access_token"].GetValue<string>();
string activeRefreshToken = refresh_token;
@@ -182,10 +182,10 @@ namespace Wino.Core.Authenticators
if (parsed.ContainsKey("refresh_token"))
{
activeRefreshToken = parsed["refresh_token"].Value<string>();
activeRefreshToken = parsed["refresh_token"].GetValue<string>();
}
var expiresIn = parsed["expires_in"].Value<long>();
var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
return new TokenInformationBase()

View File

@@ -1,9 +1,12 @@
using System.IO;
using System;
using System.IO;
using System.Text;
using Google.Apis.Gmail.v1.Data;
using HtmlAgilityPack;
using MimeKit;
using MimeKit.IO;
using MimeKit.IO.Filters;
using MimeKit.Utils;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
@@ -48,5 +51,71 @@ namespace Wino.Core.Extensions
return new AddressInformation() { Name = address.Name, Address = address.Address };
}
/// <summary>
/// Sets html body replacing base64 images with cid linked resources.
/// Updates text body based on html.
/// </summary>
/// <param name="bodyBuilder">Body builder.</param>
/// <param name="htmlContent">Html content that can have embedded images.</param>
/// <returns>Body builder with set HtmlBody.</returns>
public static BodyBuilder SetHtmlBody(this BodyBuilder bodyBuilder, string htmlContent)
{
if (string.IsNullOrEmpty(htmlContent)) return bodyBuilder;
var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
var imgNodes = doc.DocumentNode.SelectNodes("//img");
if (imgNodes != null)
{
foreach (var node in imgNodes)
{
var src = node.GetAttributeValue("src", string.Empty);
if (string.IsNullOrEmpty(src)) continue;
if (!src.StartsWith("data:image"))
{
continue;
}
var parts = src.Substring(11).Split([";base64,"], StringSplitOptions.None);
string mimeType = parts[0];
string base64Content = parts[1];
var alt = node.GetAttributeValue("alt", $"Embedded_Image.{mimeType}");
// Convert the base64 content to binary data
byte[] imageData = Convert.FromBase64String(base64Content);
// Create a new linked resource as MimePart
var image = new MimePart("image", mimeType)
{
ContentId = MimeUtils.GenerateMessageId(),
Content = new MimeContent(new MemoryStream(imageData)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentDescription = alt.Replace(" ", "_"),
FileName = alt,
ContentTransferEncoding = ContentEncoding.Base64
};
bodyBuilder.LinkedResources.Add(image);
node.SetAttributeValue("src", $"cid:{image.ContentId}");
}
}
bodyBuilder.HtmlBody = doc.DocumentNode.InnerHtml;
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
{
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
}
return bodyBuilder;
}
}
}

View File

@@ -1,36 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Wino.Core.Http
{
/// <summary>
/// We need to generate HttpRequestMessage for batch requests, and sometimes we need to
/// serialize content as json. However, some of the fields like 'ODataType' must be ignored
/// in order PATCH requests to succeed. Therefore Microsoft account synchronizer uses
/// special JsonSerializerSettings for ignoring some of the properties.
/// </summary>
public class MicrosoftJsonContractResolver : DefaultContractResolver
{
private readonly HashSet<string> ignoreProps = new HashSet<string>()
{
"ODataType"
};
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (ignoreProps.Contains(property.PropertyName))
{
property.ShouldSerialize = _ => false;
}
return property;
}
}
}

View File

@@ -1,15 +1,13 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests
{
/// <summary>
/// Bundle that encapsulates batch request and native request without a response.
/// </summary>
@@ -43,7 +41,7 @@ namespace Wino.Core.Domain.Models.Requests
{
var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
return JsonSerializer.Deserialize<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
}
public override string ToString()

View File

@@ -1,7 +1,7 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.AutoDiscovery;
@@ -43,7 +43,7 @@ namespace Wino.Core.Services
{
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AutoDiscoverySettings>(content);
return JsonSerializer.Deserialize<AutoDiscoverySettings>(content);
}
catch (Exception ex)
{

View File

@@ -636,10 +636,7 @@ namespace Wino.Core.Services
_ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature),
};
if (!string.IsNullOrEmpty(builder.HtmlBody))
{
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
}
builder.SetHtmlBody(builder.HtmlBody);
message.Body = builder.ToMessageBody();

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Newtonsoft.Json;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
@@ -44,7 +44,7 @@ namespace Wino.Core.Services
var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
var translationLookups = JsonConvert.DeserializeObject<Dictionary<string, string>>(stremValue);
var translationLookups = JsonSerializer.Deserialize<Dictionary<string, string>>(stremValue);
// Insert new translation key-value pairs.
// Overwrite existing values for the same keys.

View File

@@ -31,7 +31,6 @@
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
<PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="morelinq" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />