initial commit

This commit is contained in:
Mathias Beaulieu-Duncan 2025-07-14 21:58:09 -04:00
commit 261042f920
Signed by: mathias
GPG Key ID: 1C16CF05BAF9162D
40 changed files with 801 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
**/appsettings.json

13
.idea/.idea.Cakemail/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/.idea.Cakemail.iml
/projectSettingsUpdater.xml
/modules.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,12 @@
using System.Net.Http.Headers;
namespace CM.Authentication.Abstractions;
public static class HttpClientExtensions
{
public static HttpClient SetJwtAccessToken(this HttpClient httpClient, JwtTokenResult token)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token.TokenType.ToString(), token.AccessToken);
return httpClient;
}
}

View File

@ -0,0 +1,6 @@
namespace CM.Authentication.Abstractions;
public interface IJwtTokenManagerService
{
Task<JwtTokenResult> GetTokenAsync(CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,13 @@
namespace CM.Authentication.Abstractions;
public class JwtTokenResult
{
public required string AccessToken { get; set; }
public required TokenType TokenType { get; set; }
public required int ExpiresIn { get; set; }
public required string RefreshToken { get; set; }
public required IEnumerable<long> Accounts { get; set; }
// helpers
public required DateTime ExpiresAt { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace CM.Authentication.Abstractions;
public enum TokenType
{
Bearer
}

View File

@ -0,0 +1,10 @@
namespace CM.Authentication;
public class AuthenticationOptions
{
public required string TokenEndpoint { get; set; }
public required string Username { get; set; }
public required string Password { get; set; }
public JwtTokenCacheOptions CacheOptions { get; set; } = new JwtTokenCacheOptions();
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CM.Authentication.Abstractions\CM.Authentication.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
namespace CM.Authentication;
public class JwtTokenCacheOptions
{
public uint ExpirationOffset { get; set; } = 15;
public string CacheKey { get; set; } = "CakemailJwtTokenManager.JwtTokenResult";
}

View File

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace CM.Authentication;
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
)]
[JsonSerializable(typeof(JwtTokenResponse))]
public partial class JwtTokenManagerJsonContext : JsonSerializerContext
{
}

View File

@ -0,0 +1,83 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using CM.Authentication.Abstractions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CM.Authentication;
public class JwtTokenManagerService(IOptions<AuthenticationOptions> options, IHttpClientFactory httpClientFactory, IMemoryCache? memoryCache, ILogger<JwtTokenManagerService>? logger)
: IJwtTokenManagerService
{
private readonly AuthenticationOptions _options = options.Value;
private readonly JwtTokenCacheOptions _cacheOptions = options.Value.CacheOptions;
private readonly HttpClient _httpClient = httpClientFactory.CreateClient();
private readonly TimeSpan _cacheExpirationOffset = TimeSpan.FromSeconds(options.Value.CacheOptions.ExpirationOffset);
public async Task<JwtTokenResult> GetTokenAsync(CancellationToken cancellationToken = default)
{
if (memoryCache != null)
{
var memoryGetValueResult = memoryCache.TryGetValue(_cacheOptions.CacheKey, out JwtTokenResult? cachedToken);
if (memoryGetValueResult && null != cachedToken)
{
return cachedToken;
}
}
var formContentKeyValues = new List<KeyValuePair<string, string>>()
{
new ("grant_type", "password"),
new ("username", _options.Username),
new ("password", _options.Password)
};
var formContent = new FormUrlEncodedContent(formContentKeyValues);
var request = new HttpRequestMessage(HttpMethod.Post, _options.TokenEndpoint)
{
Content = formContent
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadFromJsonAsync(JwtTokenManagerJsonContext.Default.JwtTokenResponse, cancellationToken);
if (tokenResponse is null)
throw new InvalidOperationException("Failed to deserialize the token response content.");
var parsedResult = Enum.TryParse<TokenType>(tokenResponse.TokenType, true, out var tokenType);
if (parsedResult == false)
throw new InvalidOperationException($"Unsupported token type: {tokenResponse.TokenType}");
var now = DateTime.UtcNow;
var expiration = TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
var expiresAt = now.Add(expiration);
var result = new JwtTokenResult
{
AccessToken = tokenResponse.AccessToken,
TokenType = tokenType,
Accounts = tokenResponse.Accounts,
RefreshToken = tokenResponse.RefreshToken,
ExpiresIn = tokenResponse.ExpiresIn,
ExpiresAt = expiresAt,
};
if (null != memoryCache && expiration < _cacheExpirationOffset)
{
logger?.LogWarning("Caching is enable but the token expiration time [{expiration}] is less than the expiration offset [{cacheExpirationOffset}]. Caching is ignored, please validate your authorization server configuration and the {className} cache expiration offset configuration",
expiration, _cacheExpirationOffset, nameof(JwtTokenManagerService));
}
else
memoryCache?.Set(_cacheOptions.CacheKey, result, expiration.Subtract(_cacheExpirationOffset));
return result;
}
}

View File

@ -0,0 +1,12 @@
using CM.Authentication.Abstractions;
namespace CM.Authentication;
public class JwtTokenResponse
{
public required string AccessToken { get; set; }
public required string TokenType { get; set; }
public required int ExpiresIn { get; set; }
public required string RefreshToken { get; set; }
public required IEnumerable<long> Accounts { get; set; }
}

View File

@ -0,0 +1,28 @@
using CM.Authentication.Abstractions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CM.Authentication;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCakemailJwtTokenManager(this IServiceCollection services, string configurationSection = "Cakemail:Authentication")
{
services.AddOptions<AuthenticationOptions>()
.BindConfiguration(configurationSection);
services.AddSingleton<IJwtTokenManagerService>(serviceProvider =>
{
var options = serviceProvider.GetRequiredService<IOptions<AuthenticationOptions>>();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
var logger = serviceProvider.GetService<ILogger<JwtTokenManagerService>>();
return new JwtTokenManagerService(options, httpClientFactory, memoryCache, logger);
});
return services;
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace CM.Authentication;
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(AuthenticationOptions))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

19
CM.Sdk/CM.Sdk.csproj Normal file
View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CM.Authentication\CM.Authentication.csproj" />
<ProjectReference Include="..\CM.TransactionalEmail\CM.TransactionalEmail.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using CM.TransactionalEmail;
using CM.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CM.Sdk;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCakemailSdk(this IServiceCollection services)
{
services.AddCakemailJwtTokenManager();
services.AddTransient<TransactionalEmailService>();
return services;
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,7 @@
namespace CM.Shared;
public class CustomAttribute
{
public required string Name { get; set; }
public required string Value { get; set; }
}

1
CM.Tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
appsettings.json

44
CM.Tests/CM.Tests.csproj Normal file
View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<PublishAot>true</PublishAot>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CM.Authentication\CM.Authentication.csproj" />
<ProjectReference Include="..\CM.Sdk\CM.Sdk.csproj" />
<ProjectReference Include="..\CM.TransactionalEmail\CM.TransactionalEmail.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

37
CM.Tests/TestFixture.cs Normal file
View File

@ -0,0 +1,37 @@
using CM.Authentication;
using CM.Sdk;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace CM.Tests;
public class TestFixture
{
public IServiceProvider ServiceProvider { get; private set; }
public IConfiguration Configuration { get; private set; }
public TestFixture()
{
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = configurationBuilder.Build();
var services = new ServiceCollection();
services.AddSingleton(Configuration);
ConfigureServices(services);
ServiceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddCakemailSdk();
}
}

View File

@ -0,0 +1,54 @@
using CM.Authentication.Abstractions;
using CM.TransactionalEmail;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace CM.Tests;
public class TransactionalEmailTests(TestFixture fixture, ITestOutputHelper output) : IClassFixture<TestFixture>
{
[Fact]
public async Task AuthenticationAsync()
{
var jwtTokenManagerService = fixture.ServiceProvider.GetRequiredService<IJwtTokenManagerService>();
var response = await jwtTokenManagerService.GetTokenAsync();
output.WriteLine("Successfully receive token type {0}", response.TokenType.ToString());
}
[Fact]
public async Task SendEmailAsync()
{
var configuration = fixture.ServiceProvider.GetRequiredService<IConfiguration>();
var transactionalEmailService = fixture.ServiceProvider.GetRequiredService<TransactionalEmailService>();
var testRecipient = configuration["TestRecipient"];
var senderId = configuration["SenderId"];
Assert.NotNull(testRecipient);
Assert.NotNull(senderId);
await transactionalEmailService.SendEmailAsync(new SendEmailOptions()
{
Email = testRecipient,
Sender = new Sender
{
Id = senderId
},
Content = new EmailContent
{
Subject = "Send Email Test",
Text = "Test Email",
Encoding = EmailEncoding.Utf8,
},
Tracking = new TrackingOptions
{
ClicksHtml = false,
ClicksText = false,
Opens = true
},
Queue = 0
});
}
}

View File

@ -0,0 +1,15 @@
{
"TestRecipient": "",
"SenderId": "",
"Cakemail": {
"Authentication": {
"TokenEndpoint": "",
"Username": "",
"Password": "",
"Cache": {
"ExpirationOffset": 15,
"CacheKey": "CakemailJwtTokenManager.JwtTokenResult"
}
}
}
}

View File

@ -0,0 +1,35 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace CM.TransactionalEmail;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AttachmentType
{
[EnumMember(Value = "csv")]
Csv,
[EnumMember(Value = "doc")]
Doc,
[EnumMember(Value = "docx")]
Docx,
[EnumMember(Value = "calendar")]
Calendar,
[EnumMember(Value = "jpeg")]
Jpeg,
[EnumMember(Value = "pdf")]
Pdf,
[EnumMember(Value = "png")]
Png,
[EnumMember(Value = "xls")]
Xls,
[EnumMember(Value = "xlsx")]
Xlsx
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CM.Authentication.Abstractions\CM.Authentication.Abstractions.csproj" />
<ProjectReference Include="..\CM.Shared\CM.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace CM.TransactionalEmail;
public class EmailAttachment
{
public required string Filename { get; set; }
public AttachmentType Type { get; set; }
public required string Content { get; set; }
}

View File

@ -0,0 +1,13 @@
using CM.Shared;
namespace CM.TransactionalEmail;
public class EmailContent
{
public required string Subject { get; set; }
public string? Html { get; set; }
public string? Text { get; set; }
public EmailTemplate? Template { get; set; }
public EmailEncoding Encoding { get; set; } = EmailEncoding.Utf8;
public IEnumerable<CustomAttribute>? CustomAttributes { get; set; }
}

View File

@ -0,0 +1,118 @@
using System.Text.Json.Serialization;
namespace CM.TransactionalEmail;
[JsonConverter(typeof(JsonStringEnumConverter<EmailEncoding>))]
public enum EmailEncoding
{
[JsonStringEnumMemberName("utf-8")]
Utf8,
[JsonStringEnumMemberName("armscii-8")]
Armscii8,
[JsonStringEnumMemberName("ascii")]
Ascii,
[JsonStringEnumMemberName("big-5")]
Big5,
[JsonStringEnumMemberName("cp51932")]
Cp51932,
[JsonStringEnumMemberName("cp866")]
Cp866,
[JsonStringEnumMemberName("cp936")]
Cp936,
[JsonStringEnumMemberName("euc-cn")]
EucCn,
[JsonStringEnumMemberName("euc-jp")]
EucJp,
[JsonStringEnumMemberName("eucjp-win")]
EucJpWin,
[JsonStringEnumMemberName("euc-kr")]
EucKr,
[JsonStringEnumMemberName("euc-tw")]
EucTw,
[JsonStringEnumMemberName("hz")]
Hz,
[JsonStringEnumMemberName("iso-2022-jp")]
Iso2022Jp,
[JsonStringEnumMemberName("iso-2022-jp-ms")]
Iso2022JpMs,
[JsonStringEnumMemberName("iso-2022-kr")]
Iso2022Kr,
[JsonStringEnumMemberName("iso-8859-1")]
Iso88591,
[JsonStringEnumMemberName("iso-8859-10")]
Iso885910,
[JsonStringEnumMemberName("iso-8859-13")]
Iso885913,
[JsonStringEnumMemberName("iso-8859-14")]
Iso885914,
[JsonStringEnumMemberName("iso-8859-15")]
Iso885915,
[JsonStringEnumMemberName("iso-8859-16")]
Iso885916,
[JsonStringEnumMemberName("iso-8859-2")]
Iso88592,
[JsonStringEnumMemberName("iso-8859-3")]
Iso88593,
[JsonStringEnumMemberName("iso-8859-4")]
Iso88594,
[JsonStringEnumMemberName("iso-8859-5")]
Iso88595,
[JsonStringEnumMemberName("iso-8859-6")]
Iso88596,
[JsonStringEnumMemberName("iso-8859-7")]
Iso88597,
[JsonStringEnumMemberName("iso-8859-8")]
Iso88598,
[JsonStringEnumMemberName("iso-8859-9")]
Iso88599,
[JsonStringEnumMemberName("jis")]
Jis,
[JsonStringEnumMemberName("koi8-r")]
Koi8R,
[JsonStringEnumMemberName("sjis")]
Sjis,
[JsonStringEnumMemberName("sjis-win")]
SjisWin,
[JsonStringEnumMemberName("uhc")]
Uhc,
[JsonStringEnumMemberName("windows-1251")]
Windows1251,
[JsonStringEnumMemberName("windows-1252")]
Windows1252
}

View File

@ -0,0 +1,9 @@
using CM.Shared;
namespace CM.TransactionalEmail;
public class EmailTemplate
{
public required int Id { get; set; }
public required IEnumerable<CustomAttribute> CustomAttributes { get; set; }
}

View File

@ -0,0 +1,14 @@
using CM.Shared;
namespace CM.TransactionalEmail;
public class SendEmailOptions
{
public required string Email { get; set; }
public required Sender Sender { get; set; }
public required EmailContent Content { get; set; }
public long? GroupId { get; set; }
public TrackingOptions? Tracking { get; set; }
public IEnumerable<CustomAttribute>? AdditionalHeaders { get; set; }
public long? Queue { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace CM.TransactionalEmail;
public class Sender
{
public required string Id { get; set; }
public string? Name { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace CM.TransactionalEmail;
public class TrackingOptions
{
public bool? Opens { get; set; }
public bool? ClicksHtml { get; set; }
public bool? ClicksText { get; set; }
}

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace CM.TransactionalEmail;
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
[JsonSerializable(typeof(SendEmailOptions))]
[JsonSerializable(typeof(EmailEncoding))]
internal partial class TransactionEmailJsonSerializerContext: JsonSerializerContext
{
}

View File

@ -0,0 +1,28 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization.Metadata;
using CM.Authentication.Abstractions;
namespace CM.TransactionalEmail;
public class TransactionalEmailService(IHttpClientFactory httpClientFactory, IJwtTokenManagerService jwtTokenManagerService)
{
private readonly string _endpoint = "https://api.cakemail.dev";
private readonly HttpClient _httpClient = httpClientFactory.CreateClient();
public async Task SendEmailAsync(SendEmailOptions options, CancellationToken cancellationToken = default)
{
var endpoint = $"{_endpoint}/emails";
var token = await jwtTokenManagerService.GetTokenAsync(cancellationToken);
_httpClient.SetJwtAccessToken(token);
var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
{
Content = JsonContent.Create(options, TransactionEmailJsonSerializerContext.Default.SendEmailOptions)
};
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
}
}

46
Cakemail.sln Normal file
View File

@ -0,0 +1,46 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.Sdk", "CM.Sdk\CM.Sdk.csproj", "{59009AC6-0D2E-4E0E-B548-28BB8E773EA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.Authentication", "CM.Authentication\CM.Authentication.csproj", "{98B1EF3D-F322-4E86-9A78-D0E27E6F56B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.TransactionalEmail", "CM.TransactionalEmail\CM.TransactionalEmail.csproj", "{27CBC083-5AE5-46BC-8B65-EAA39A16E8C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.Authentication.Abstractions", "CM.Authentication.Abstractions\CM.Authentication.Abstractions.csproj", "{F55E3C54-533F-4630-8B3A-ECD46AD530F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.Tests", "CM.Tests\CM.Tests.csproj", "{AA54F60C-491D-4142-A70D-C27E2803521D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CM.Shared", "CM.Shared\CM.Shared.csproj", "{30DA9908-310F-4E04-87B3-4D8FE40F818B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{59009AC6-0D2E-4E0E-B548-28BB8E773EA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59009AC6-0D2E-4E0E-B548-28BB8E773EA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59009AC6-0D2E-4E0E-B548-28BB8E773EA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59009AC6-0D2E-4E0E-B548-28BB8E773EA0}.Release|Any CPU.Build.0 = Release|Any CPU
{98B1EF3D-F322-4E86-9A78-D0E27E6F56B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98B1EF3D-F322-4E86-9A78-D0E27E6F56B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98B1EF3D-F322-4E86-9A78-D0E27E6F56B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98B1EF3D-F322-4E86-9A78-D0E27E6F56B2}.Release|Any CPU.Build.0 = Release|Any CPU
{27CBC083-5AE5-46BC-8B65-EAA39A16E8C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27CBC083-5AE5-46BC-8B65-EAA39A16E8C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27CBC083-5AE5-46BC-8B65-EAA39A16E8C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27CBC083-5AE5-46BC-8B65-EAA39A16E8C8}.Release|Any CPU.Build.0 = Release|Any CPU
{F55E3C54-533F-4630-8B3A-ECD46AD530F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F55E3C54-533F-4630-8B3A-ECD46AD530F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F55E3C54-533F-4630-8B3A-ECD46AD530F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F55E3C54-533F-4630-8B3A-ECD46AD530F7}.Release|Any CPU.Build.0 = Release|Any CPU
{AA54F60C-491D-4142-A70D-C27E2803521D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA54F60C-491D-4142-A70D-C27E2803521D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA54F60C-491D-4142-A70D-C27E2803521D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA54F60C-491D-4142-A70D-C27E2803521D}.Release|Any CPU.Build.0 = Release|Any CPU
{30DA9908-310F-4E04-87B3-4D8FE40F818B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30DA9908-310F-4E04-87B3-4D8FE40F818B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30DA9908-310F-4E04-87B3-4D8FE40F818B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30DA9908-310F-4E04-87B3-4D8FE40F818B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,15 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003Ftheni_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F9d4b58d4f19922362e61be6b764873c3d446ca20cf99191c90bc38eeda9a36_003FExceptionAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFormUrlEncodedContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003Ftheni_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F93a897b9177494b2a3eff514cc4e7d4d4edf0ccb6f797e5ca0247855a49f7_003FFormUrlEncodedContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003Ftheni_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc7182f66cbfc2cc1a8ba6826297b525dffec464bdfc5d2f38bba5785696796c_003FJsonContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProviderServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003Ftheni_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F621526284d3e4868b98948aff942206525763e57e194a27623b66c384466_003FServiceProviderServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=396c6bbb_002Da51a_002D4bb9_002Dbb2f_002De25616b330cc/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="AuthenticationAsync" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::AA54F60C-491D-4142-A70D-C27E2803521D::net8.0::CM.Tests.TransactionalEmailTests.AuthenticationAsync&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b6e5eb46_002Dd425_002D4092_002D8a1b_002Dc7b0038d7f20/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="SendEmailAsync" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::AA54F60C-491D-4142-A70D-C27E2803521D::net9.0::CM.Tests.TransactionalEmailTests.SendEmailAsync&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>