Skip to content

Commit

Permalink
Bss.Platform.Notifications.Smtp - redirect notifications to test emai…
Browse files Browse the repository at this point in the history
…l if not production environment (#13)

* Bss.Platform.Notifications.Smtp - redirect notifications to test email if not production environment

* prettify readme

* refactoring

* actualize readme

* actualize readme

* actualize readme

* actualize readme

* actualize readme

* actualize readme

* actualize readme

---------

Co-authored-by: Khaperskaia, Anna <[email protected]>
Co-authored-by: Artem Leshchev <[email protected]>
  • Loading branch information
3 people authored May 28, 2024
1 parent dd6d30b commit a47c471
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 3 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This repository offers a wide collection of .NET packages for use in microservic
- [Kubernetes Insights](#Insights)
- [Kubernetes Health Checks](#Health-Checks)
- [NHibernate](#NHibernate)
- [Notifications](#Notifications)

## RabbitMQ

Expand Down Expand Up @@ -272,3 +273,50 @@ To mock IQueryable, in your code, call:
var entity = new Entity();
repository.GetQueryable().Returns(new TestQueryable<Entity>(entity));
```

## Notifications

To use platform senders for notifications, first install the [NuGet package](https://www.nuget.org/packages/Luxoft.Bss.Platform.Notifications):
```shell
dotnet add package Luxoft.Bss.Platform.Notifications
```

Then register notifications service in DI
```C#
services
.AddPlatformNotifications(builder.Environment, builder.Configuration)
```

Then fill configuration settings:
```json
{
"NotificationSender": {
"Server": "smtp.server.com", -- smtp server host
"RedirectTo": ["[email protected]"] -- this address will be used as single recipient for all messages on non-prod environments
}
}
```

Now you can send messages to smtp server:
```C#
public class YourNotificationRequestHandler(IEmailSender sender) : IRequestHandler<YourNotificationRequest>
{
public async Task Handle(YourNotificationRequest request, CancellationToken cancellationToken)
{
var attachment = new Attachment(new MemoryStream(), request.AttachmentName);
attachment.ContentDisposition!.Inline = true;

var message = new EmailModel(
request.Subject,
request.Body,
new MailAddress(request.From),
new[] { new MailAddress(request.To) },
Attachments: new[] { attachment });

await sender.SendAsync(message, token);
}
}
```

> [!NOTE]
> Note that attachment will be inlined only if its 'Inline' field is true and its name is referred as image source in message body.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Luxoft.Bss.Platform.Notifications</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions"/>
</ItemGroup>
</Project>
58 changes: 58 additions & 0 deletions src/Bss.Platform.Notifications/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Bss.Platform.Notifications.Interfaces;
using Bss.Platform.Notifications.Models;
using Bss.Platform.Notifications.Services;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Bss.Platform.Notifications;

public static class DependencyInjection
{
public static IServiceCollection AddPlatformNotifications(
this IServiceCollection services,
IHostEnvironment hostEnvironment,
IConfiguration configuration)
{
var settings = configuration.GetSection(NotificationSenderOptions.SectionName).Get<NotificationSenderOptions>()!;

return AddTestEnvironmentRedirection(services, hostEnvironment, settings)
.AddMailMessageSenders(settings)
.Configure<NotificationSenderOptions>(configuration.GetSection(NotificationSenderOptions.SectionName))
.AddScoped<IEmailSender, EmailSender>();
}

private static IServiceCollection AddTestEnvironmentRedirection(
this IServiceCollection services,
IHostEnvironment hostEnvironment,
NotificationSenderOptions settings)
{
if (hostEnvironment.IsProduction())
{
return services;
}

if (settings.RedirectTo?.Length == 0)
{
throw new ArgumentException("Test email address is not provided");
}

return services.AddScoped<IRedirectService, RedirectService>();
}

private static IServiceCollection AddMailMessageSenders(this IServiceCollection services, NotificationSenderOptions settings)
{
if (settings.IsSmtpEnabled)
{
services.AddScoped<IMailMessageSender, SmtpSender>();
}

if (!string.IsNullOrWhiteSpace(settings.OutputFolder))
{
services.AddScoped<IMailMessageSender, FileSender>();
}

return services;
}
}
10 changes: 10 additions & 0 deletions src/Bss.Platform.Notifications/Interfaces/IEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Net.Mail;

using Bss.Platform.Notifications.Models;

namespace Bss.Platform.Notifications.Interfaces;

public interface IEmailSender
{
Task<MailMessage> SendAsync(EmailModel model, CancellationToken token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Net.Mail;

namespace Bss.Platform.Notifications.Interfaces;

public interface IMailMessageSender
{
Task SendAsync(MailMessage message, CancellationToken token);
}
8 changes: 8 additions & 0 deletions src/Bss.Platform.Notifications/Interfaces/IRedirectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Net.Mail;

namespace Bss.Platform.Notifications.Interfaces;

public interface IRedirectService
{
void Redirect(MailMessage message);
}
12 changes: 12 additions & 0 deletions src/Bss.Platform.Notifications/Models/EmailModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Net.Mail;

namespace Bss.Platform.Notifications.Models;

public record EmailModel(
string Subject,
string Body,
MailAddress From,
MailAddress[] To,
MailAddress[]? Cc = null,
MailAddress[]? ReplyTo = null,
Attachment[]? Attachments = null);
16 changes: 16 additions & 0 deletions src/Bss.Platform.Notifications/Models/NotificationSenderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Bss.Platform.Notifications.Models;

public class NotificationSenderOptions
{
public const string SectionName = "NotificationSender";

public bool IsSmtpEnabled { get; set; } = true;

public string? OutputFolder { get; set; }

public string Server { get; set; } = default!;

public int Port { get; set; } = 25;

public string[]? RedirectTo { get; set; }
}
76 changes: 76 additions & 0 deletions src/Bss.Platform.Notifications/Services/EmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Net.Mail;
using System.Text.RegularExpressions;

using Bss.Platform.Notifications.Interfaces;
using Bss.Platform.Notifications.Models;

namespace Bss.Platform.Notifications.Services;

internal class EmailSender(IEnumerable<IMailMessageSender> senders, IRedirectService? redirectService = null) : IEmailSender
{
public async Task<MailMessage> SendAsync(EmailModel model, CancellationToken token)
{
var message = Convert(model);

redirectService?.Redirect(message);

foreach (var sender in senders)
{
await sender.SendAsync(message, token);
}

return message;
}

private static MailMessage Convert(EmailModel model)
{
var mailMessage = new MailMessage { Subject = model.Subject, Body = model.Body, From = model.From, IsBodyHtml = true };

AddRange(mailMessage.To, model.To);

if (model.Cc?.Length > 0)
{
AddRange(mailMessage.CC, model.Cc);
}

if (model.ReplyTo?.Length > 0)
{
AddRange(mailMessage.ReplyToList, model.ReplyTo);
}

if (model.Attachments?.Length > 0)
{
SetAttachments(model.Attachments, mailMessage);
}

return mailMessage;
}

private static void SetAttachments(Attachment[] attachments, MailMessage mailMessage)
{
foreach (var attachment in attachments)
{
mailMessage.Attachments.Add(attachment);
if (!attachment.ContentDisposition!.Inline)
{
continue;
}

var srcRegex = $"src\\s*=\\s*\"{attachment.Name}\"";
if (!Regex.IsMatch(mailMessage.Body, srcRegex, RegexOptions.IgnoreCase))
{
continue;
}

mailMessage.Body = Regex.Replace(mailMessage.Body, srcRegex, $"src=\"cid:{attachment.ContentId}\"", RegexOptions.IgnoreCase);
}
}

private static void AddRange(MailAddressCollection collection, IEnumerable<MailAddress> addresses)
{
foreach (var address in addresses)
{
collection.Add(address);
}
}
}
25 changes: 25 additions & 0 deletions src/Bss.Platform.Notifications/Services/FileSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Net.Mail;

using Bss.Platform.Notifications.Interfaces;
using Bss.Platform.Notifications.Models;

using Microsoft.Extensions.Options;

namespace Bss.Platform.Notifications.Services;

internal class FileSender(IOptions<NotificationSenderOptions> settings) : IMailMessageSender
{
public async Task SendAsync(MailMessage message, CancellationToken token)
{
using var client = this.GetSmtpClient();
await client.SendMailAsync(message, token);
}

private SmtpClient GetSmtpClient() =>
new()
{
UseDefaultCredentials = true,
DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
PickupDirectoryLocation = settings.Value.OutputFolder
};
}
42 changes: 42 additions & 0 deletions src/Bss.Platform.Notifications/Services/RedirectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Net.Mail;

using Bss.Platform.Notifications.Interfaces;
using Bss.Platform.Notifications.Models;

using Microsoft.Extensions.Options;

namespace Bss.Platform.Notifications.Services;

internal class RedirectService(IOptions<NotificationSenderOptions> settings) : IRedirectService
{
public void Redirect(MailMessage message)
{
AddRecipientsToBody(message);

ClearRecipients(message);

foreach (var address in settings.Value.RedirectTo!.Select(z => z.Trim()).Distinct().Select(z => new MailAddress(z)))
{
message.To.Add(address);
}
}

private static void ClearRecipients(MailMessage message)
{
message.To.Clear();
message.CC.Clear();
message.Bcc.Clear();
message.ReplyToList.Clear();
}

private static void AddRecipientsToBody(MailMessage message)
{
var originalRecipients =
$"From: {message.From!.Address}<br>"
+ $"To: {string.Join("; ", message.To.Select(x => x.Address))}<br>"
+ $"CC: {string.Join("; ", message.CC.Select(x => x.Address))}<br>"
+ $"Reply To: {string.Join("; ", message.ReplyToList.Select(x => x.Address))}<br><br>";

message.Body = $"{originalRecipients}{message.Body}";
}
}
19 changes: 19 additions & 0 deletions src/Bss.Platform.Notifications/Services/SmtpSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Net.Mail;

using Bss.Platform.Notifications.Interfaces;
using Bss.Platform.Notifications.Models;

using Microsoft.Extensions.Options;

namespace Bss.Platform.Notifications.Services;

internal class SmtpSender(IOptions<NotificationSenderOptions> settings) : IMailMessageSender
{
public async Task SendAsync(MailMessage message, CancellationToken token)
{
using var client = this.GetSmtpClient();
await client.SendMailAsync(message, token);
}

private SmtpClient GetSmtpClient() => new(settings.Value.Server, settings.Value.Port) { UseDefaultCredentials = true };
}
9 changes: 9 additions & 0 deletions src/Bss.Platform.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Events", "Bss.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Events.Abstractions", "Bss.Platform.Events.Abstractions\Bss.Platform.Events.Abstractions.csproj", "{BE2E219F-3F3B-48E8-B13B-A71321BB6372}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Notifications", "Bss.Platform.Notifications\Bss.Platform.Notifications.csproj", "{A97421A9-58B7-4948-8231-2028B5D11F36}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notifications", "Notifications", "{94640F4D-8BC6-407F-835E-73B454972CAD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -50,6 +54,7 @@ Global
{285EBB7B-6B2A-453E-98F6-30AC38317A76} = {8F22FE4E-8FD4-4309-8B47-C33AA6E52052}
{51668BE6-C4A4-4E62-9BEC-529C33A434DB} = {80AA8C01-842D-4A5F-BCBE-48E0361783A0}
{BE2E219F-3F3B-48E8-B13B-A71321BB6372} = {80AA8C01-842D-4A5F-BCBE-48E0361783A0}
{A97421A9-58B7-4948-8231-2028B5D11F36} = {94640F4D-8BC6-407F-835E-73B454972CAD}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{860BFBD8-26EA-44F9-980E-21B828FC8F72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -92,5 +97,9 @@ Global
{BE2E219F-3F3B-48E8-B13B-A71321BB6372}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE2E219F-3F3B-48E8-B13B-A71321BB6372}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE2E219F-3F3B-48E8-B13B-A71321BB6372}.Release|Any CPU.Build.0 = Release|Any CPU
{A97421A9-58B7-4948-8231-2028B5D11F36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A97421A9-58B7-4948-8231-2028B5D11F36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A97421A9-58B7-4948-8231-2028B5D11F36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A97421A9-58B7-4948-8231-2028B5D11F36}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
6 changes: 3 additions & 3 deletions src/__SolutionItems/CommonAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
[assembly: AssemblyCompany("Luxoft")]
[assembly: AssemblyCopyright("Copyright © Luxoft 2024")]

[assembly: AssemblyVersion("1.4.0.0")]
[assembly: AssemblyFileVersion("1.4.0.0")]
[assembly: AssemblyInformationalVersion("1.4.0.0")]
[assembly: AssemblyVersion("1.5.0.0")]
[assembly: AssemblyFileVersion("1.5.0.0")]
[assembly: AssemblyInformationalVersion("1.5.0.0")]

#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
Expand Down

0 comments on commit a47c471

Please sign in to comment.