initial commit
This commit is contained in:
commit
261042f920
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
|
||||
**/appsettings.json
|
13
.idea/.idea.Cakemail/.idea/.gitignore
vendored
Normal file
13
.idea/.idea.Cakemail/.idea/.gitignore
vendored
Normal 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
|
4
.idea/.idea.Cakemail/.idea/encodings.xml
Normal file
4
.idea/.idea.Cakemail/.idea/encodings.xml
Normal 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>
|
8
.idea/.idea.Cakemail/.idea/indexLayout.xml
Normal file
8
.idea/.idea.Cakemail/.idea/indexLayout.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
6
.idea/.idea.Cakemail/.idea/vcs.xml
Normal file
6
.idea/.idea.Cakemail/.idea/vcs.xml
Normal 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>
|
@ -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>
|
12
CM.Authentication.Abstractions/HttpClientExtensions.cs
Normal file
12
CM.Authentication.Abstractions/HttpClientExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace CM.Authentication.Abstractions;
|
||||
|
||||
public interface IJwtTokenManagerService
|
||||
{
|
||||
Task<JwtTokenResult> GetTokenAsync(CancellationToken cancellationToken = default);
|
||||
}
|
13
CM.Authentication.Abstractions/JwtTokenResult.cs
Normal file
13
CM.Authentication.Abstractions/JwtTokenResult.cs
Normal 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; }
|
||||
}
|
6
CM.Authentication.Abstractions/TokenType.cs
Normal file
6
CM.Authentication.Abstractions/TokenType.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace CM.Authentication.Abstractions;
|
||||
|
||||
public enum TokenType
|
||||
{
|
||||
Bearer
|
||||
}
|
10
CM.Authentication/AuthenticationOptions.cs
Normal file
10
CM.Authentication/AuthenticationOptions.cs
Normal 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();
|
||||
}
|
20
CM.Authentication/CM.Authentication.csproj
Normal file
20
CM.Authentication/CM.Authentication.csproj
Normal 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>
|
7
CM.Authentication/JwtTokenCacheOptions.cs
Normal file
7
CM.Authentication/JwtTokenCacheOptions.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace CM.Authentication;
|
||||
|
||||
public class JwtTokenCacheOptions
|
||||
{
|
||||
public uint ExpirationOffset { get; set; } = 15;
|
||||
public string CacheKey { get; set; } = "CakemailJwtTokenManager.JwtTokenResult";
|
||||
}
|
12
CM.Authentication/JwtTokenManagerJsonContext.cs
Normal file
12
CM.Authentication/JwtTokenManagerJsonContext.cs
Normal 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
|
||||
{
|
||||
}
|
83
CM.Authentication/JwtTokenManagerService.cs
Normal file
83
CM.Authentication/JwtTokenManagerService.cs
Normal 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;
|
||||
}
|
||||
}
|
12
CM.Authentication/JwtTokenResponse.cs
Normal file
12
CM.Authentication/JwtTokenResponse.cs
Normal 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; }
|
||||
}
|
28
CM.Authentication/ServiceCollectionExtensions.cs
Normal file
28
CM.Authentication/ServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
9
CM.Authentication/SettingsJsonSerializerContext.cs
Normal file
9
CM.Authentication/SettingsJsonSerializerContext.cs
Normal 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
19
CM.Sdk/CM.Sdk.csproj
Normal 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>
|
17
CM.Sdk/ServiceCollectionExtensions.cs
Normal file
17
CM.Sdk/ServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
10
CM.Shared/CM.Shared.csproj
Normal file
10
CM.Shared/CM.Shared.csproj
Normal 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>
|
7
CM.Shared/CustomAttribute.cs
Normal file
7
CM.Shared/CustomAttribute.cs
Normal 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
1
CM.Tests/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
appsettings.json
|
44
CM.Tests/CM.Tests.csproj
Normal file
44
CM.Tests/CM.Tests.csproj
Normal 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
37
CM.Tests/TestFixture.cs
Normal 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();
|
||||
}
|
||||
}
|
54
CM.Tests/TransactionalEmailTests.cs
Normal file
54
CM.Tests/TransactionalEmailTests.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
15
CM.Tests/appsettings.example.json
Normal file
15
CM.Tests/appsettings.example.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"TestRecipient": "",
|
||||
"SenderId": "",
|
||||
"Cakemail": {
|
||||
"Authentication": {
|
||||
"TokenEndpoint": "",
|
||||
"Username": "",
|
||||
"Password": "",
|
||||
"Cache": {
|
||||
"ExpirationOffset": 15,
|
||||
"CacheKey": "CakemailJwtTokenManager.JwtTokenResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
CM.TransactionalEmail/AttachmentType.cs
Normal file
35
CM.TransactionalEmail/AttachmentType.cs
Normal 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
|
||||
}
|
19
CM.TransactionalEmail/CM.TransactionalEmail.csproj
Normal file
19
CM.TransactionalEmail/CM.TransactionalEmail.csproj
Normal 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>
|
8
CM.TransactionalEmail/EmailAttachment.cs
Normal file
8
CM.TransactionalEmail/EmailAttachment.cs
Normal 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; }
|
||||
}
|
13
CM.TransactionalEmail/EmailContent.cs
Normal file
13
CM.TransactionalEmail/EmailContent.cs
Normal 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; }
|
||||
}
|
118
CM.TransactionalEmail/EmailEncoding.cs
Normal file
118
CM.TransactionalEmail/EmailEncoding.cs
Normal 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
|
||||
}
|
9
CM.TransactionalEmail/EmailTemplate.cs
Normal file
9
CM.TransactionalEmail/EmailTemplate.cs
Normal 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; }
|
||||
}
|
14
CM.TransactionalEmail/SendEmailOptions.cs
Normal file
14
CM.TransactionalEmail/SendEmailOptions.cs
Normal 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; }
|
||||
}
|
7
CM.TransactionalEmail/Sender.cs
Normal file
7
CM.TransactionalEmail/Sender.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace CM.TransactionalEmail;
|
||||
|
||||
public class Sender
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
8
CM.TransactionalEmail/TrackingOptions.cs
Normal file
8
CM.TransactionalEmail/TrackingOptions.cs
Normal 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; }
|
||||
}
|
@ -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
|
||||
{
|
||||
|
||||
}
|
28
CM.TransactionalEmail/TransactionalEmailService.cs
Normal file
28
CM.TransactionalEmail/TransactionalEmailService.cs
Normal 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
46
Cakemail.sln
Normal 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
|
15
Cakemail.sln.DotSettings.user
Normal file
15
Cakemail.sln.DotSettings.user
Normal 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"><SessionState ContinuousTestingMode="0" IsActive="True" Name="AuthenticationAsync" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>xUnit::AA54F60C-491D-4142-A70D-C27E2803521D::net8.0::CM.Tests.TransactionalEmailTests.AuthenticationAsync</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b6e5eb46_002Dd425_002D4092_002D8a1b_002Dc7b0038d7f20/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="SendEmailAsync" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>xUnit::AA54F60C-491D-4142-A70D-C27E2803521D::net9.0::CM.Tests.TransactionalEmailTests.SendEmailAsync</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
Loading…
Reference in New Issue
Block a user