diff --git a/.editorconfig b/.editorconfig index 1117d16..b360236 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,10 @@ indent_size = 2 [*.{cs,vb}] +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + #### Naming styles #### tab_width = 4 indent_size = 4 @@ -81,6 +85,10 @@ dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_object_initializer = true:suggestion +dotnet_style_qualification_for_event = true +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_property = true dotnet_style_collection_initializer = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion diff --git a/.github/workflows/github-actions-release.yaml b/.github/workflows/github-actions-release.yaml index c196d88..0be6c57 100644 --- a/.github/workflows/github-actions-release.yaml +++ b/.github/workflows/github-actions-release.yaml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 1.0.0 + default: 1.1.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4a4a215 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# PosInformatique.Foundations.Emailing.Azure + +## Changelog + +### 1.1.0 + +- Add the support of .NET 10.0 for all the packages. + +#### PosInformatique.Foundations.Emailing +- Add the support to send emails with attachments. +- Add a new overload method `EmailRecipientCollection.Add(EmailAddress, TModel)`. + +#### PosInformatique.Foundations.Emailing.Azure +- Add the support to send emails with attachments. +- Upgrade the [Microsoft.Extensions.Azure](https://www.nuget.org/packages/Microsoft.Extensions.Azure) dependency to version 1.13.1 to fix security vulnerabilities. + +#### PosInformatique.Foundations.Emailing.Graph +- Add the support to send emails with attachments. + +#### PosInformatique.Foundations.Text.Templating.Scriban +- Upgrade the [Scriban](https://www.nuget.org/packages/Scriban) dependency to version 7.0.0 to fix security vulnerabilities. + +### 1.0.0 +- Initial version of the following packages: + - [PosInformatique.Foundations.EmailAddresses](./src/EmailAddresses/README.md) + - [PosInformatique.Foundations.EmailAddresses.EntityFramework](./src/EmailAddresses.EntityFramework/README.md) + - [PosInformatique.Foundations.EmailAddresses.FluentValidation](./src/EmailAddresses.FluentValidation/README.md) + - [PosInformatique.Foundations.EmailAddresses.Json](./src/EmailAddresses.Json/README.md) + - [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) + - [PosInformatique.Foundations.Emailing.Azure](./src/Emailing.Azure/README.md) + - [PosInformatique.Foundations.Emailing.Graph](./src/Emailing.Graph/README.md) + - [PosInformatique.Foundations.Emailing.Templates.Razor](./src/Emailing.Templates.Razor/README.md) + - [PosInformatique.Foundations.MediaTypes](./src/MediaTypes/README.md) + - [PosInformatique.Foundations.MediaTypes.EntityFramework](./src/MediaTypes.EntityFramework/README.md) + - [PosInformatique.Foundations.MediaTypes.Json](./src/MediaTypes.Json/README.md) + - [PosInformatique.Foundations.People](./src/People/README.md) + - [PosInformatique.Foundations.People.DataAnnotations](./src/People.DataAnnotations/README.md) + - [PosInformatique.Foundations.People.EntityFramework](./src/People.EntityFramework/README.md) + - [PosInformatique.Foundations.People.FluentAssertions](./src/People.FluentAssertions/README.md) + - [PosInformatique.Foundations.People.FluentValidation](./src/People.FluentValidation/README.md) + - [PosInformatique.Foundations.People.Json](./src/People.Json/README.md) + - [PosInformatique.Foundations.PhoneNumbers](./src/PhoneNumbers/README.md) + - [PosInformatique.Foundations.PhoneNumbers.EntityFramework](./src/PhoneNumbers.EntityFramework/README.md) + - [PosInformatique.Foundations.PhoneNumbers.FluentValidation](./src/PhoneNumbers.FluentValidation/README.md) + - [PosInformatique.Foundations.PhoneNumbers.Json](./src/PhoneNumbers.Json/README.md) + - [PosInformatique.Foundations.Text.Templating](./src/Text.Templating/README.md) + - [PosInformatique.Foundations.Text.Templating.Razor](./src/Text.Templating.Razor/README.md) + - [PosInformatique.Foundations.Text.Templating.Scriban](./src/Text.Templating.Scriban/README.md) \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 6415f3a..9003ee3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,31 +4,34 @@ 8.0.0 9.0.0 + 10.0.0 - + - - - + + + + + - + - + - - + + - + \ No newline at end of file diff --git a/PosInformatique.Foundations.slnx b/PosInformatique.Foundations.slnx index cd18ea0..07ef3c9 100644 --- a/PosInformatique.Foundations.slnx +++ b/PosInformatique.Foundations.slnx @@ -2,9 +2,11 @@ + + diff --git a/README.md b/README.md index a269e78..75bdd9e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ PosInformatique.Foundations icon -PosInformatique.Foundations is a collection of small, focused .NET libraries that provide **simple, reusable building blocks** for your applications. +[PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) is a collection +of small, focused .NET libraries that provide **simple, reusable building blocks** for your applications. -The goal is to avoid shipping a monolithic framework by creating **modular NuGet packages**, each addressing a single responsibility. +The goal is to avoid shipping a monolithic framework by creating **modular NuGet packages**, +each addressing a single responsibility. ## ✨ Philosophy @@ -21,32 +23,32 @@ The goal is to avoid shipping a monolithic framework by creating **modular NuGet You can install any package using the .NET CLI or NuGet Package Manager. -| |Package | Description | NuGet | +| |Package (prefixed by PosInformatique.Foundations) | Description | NuGet | |--|---------|-------------|-------| -|PosInformatique.Foundations.EmailAddresses icon|[**PosInformatique.Foundations.EmailAddresses**](./src/EmailAddresses/README.md) | Strongly-typed value object representing an email address with validation and normalization as RFC 5322 compliant. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses) | -|PosInformatique.Foundations.EmailAddresses.EntityFramework icon|[**PosInformatique.Foundations.EmailAddresses.EntityFramework**](./src/EmailAddresses.EntityFramework/README.md) | Entity Framework Core integration for the `EmailAddress` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework) | -|PosInformatique.Foundations.EmailAddresses.FluentValidation icon|[**PosInformatique.Foundations.EmailAddresses.FluentValidation**](./src/EmailAddresses.FluentValidation/README.md) | FluentValidation integration for the `EmailAddress` value object, providing dedicated validators and rules to ensure RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation) | -|PosInformatique.Foundations.EmailAddresses.Json icon|[**PosInformatique.Foundations.EmailAddresses.Json**](./src/EmailAddresses.Json/README.md) | `System.Text.Json` converter for the `EmailAddress` value object, enabling seamless serialization and deserialization of RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json) | -|PosInformatique.Foundations.Emailing icon|[**PosInformatique.Foundations.Emailing**](./src/Emailing/README.md) | Template-based emailing infrastructure for .NET that lets you register strongly-typed email templates, create emails from models, and send them through pluggable providers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing) | -|PosInformatique.Foundations.Emailing.Azure icon|[**PosInformatique.Foundations.Emailing.Azure**](./src/Emailing.Azure/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Azure Communication Service**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Azure)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure) | -|PosInformatique.Foundations.Emailing.Graph icon|[**PosInformatique.Foundations.Emailing.Graph**](./src/Emailing.Graph/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Microsoft Graph API**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Graph)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph) | -|PosInformatique.Foundations.Emailing.Templates.Razor icon|[**PosInformatique.Foundations.Emailing.Templates.Razor**](./src/Emailing.Templates.Razor/README.md) | Helpers to build EmailTemplate instances from Razor components for subject and HTML body, supporting strongly-typed models and reusable layouts. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Templates.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor) | -|PosInformatique.Foundations.MediaTypes icon|[**PosInformatique.Foundations.MediaTypes**](./src/MediaTypes/README.md) | Immutable `MimeType` value object with well-known media types and helpers to map between media types and file extensions. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes) | -|PosInformatique.Foundations.MediaTypes.EntityFramework icon|[**PosInformatique.Foundations.MediaTypes.EntityFramework**](./src/MediaTypes.EntityFramework/README.md) | Entity Framework Core integration for the `MimeType` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.EntityFramework) | -|PosInformatique.Foundations.MediaTypes.Json icon|[**PosInformatique.Foundations.MediaTypes.Json**](./src/MediaTypes.Json/README.md) | `System.Text.Json` converter for the `MimeType` value object, enabling seamless serialization and deserialization of MIME types within JSON documents. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.Json) | -|PosInformatique.Foundations.People icon|[**PosInformatique.Foundations.People**](./src/People/README.md) | Strongly-typed value objects for first and last names with validation and normalization. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People)](https://www.nuget.org/packages/PosInformatique.Foundations.People) | -|PosInformatique.Foundations.People.DataAnnotations icon|[**PosInformatique.Foundations.People.DataAnnotations**](./src/People.DataAnnotations/README.md) | DataAnnotations attributes for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.DataAnnotations)](https://www.nuget.org/packages/PosInformatique.Foundations.People.DataAnnotations) | -|PosInformatique.Foundations.People.EntityFramework icon|[**PosInformatique.Foundations.People.EntityFramework**](./src/People.EntityFramework/README.md) | Entity Framework Core integration for `FirstName` and `LastName` value objects, providing fluent property configuration and value converters. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.People.EntityFramework) | -|PosInformatique.Foundations.People.FluentAssertions icon|[**PosInformatique.Foundations.People.FluentAssertions**](./src/People.FluentAssertions/README.md) | [FluentAssertions](https://fluentassertions.com/) extensions for `FirstName` and `LastName` to avoid ambiguity and provide `Should().Be(string)` assertions (case-sensitive on normalized values). | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentAssertions)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentAssertions) | -|PosInformatique.Foundations.People.FluentValidation icon|[**PosInformatique.Foundations.People.FluentValidation**](./src/People.FluentValidation/README.md) | [FluentValidation](https://fluentvalidation.net/) extensions for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentValidation) | -|PosInformatique.Foundations.People.Json icon|[**PosInformatique.Foundations.People.Json**](./src/People.Json/README.md) | `System.Text.Json` converters for `FirstName` and `LastName`, with validation and easy registration via `AddPeopleConverters()`. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.People.Json) | -|PosInformatique.Foundations.PhoneNumbers icon|[**PosInformatique.Foundations.PhoneNumbers**](./src/PhoneNumbers/README.md) | Strongly-typed value object representing a phone number in E.164 format, with parsing (including region-aware local numbers), validation, comparison, and formatting helpers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers) | -|PosInformatique.Foundations.PhoneNumbers.EntityFramework icon|[**PosInformatique.Foundations.PhoneNumbers.EntityFramework**](./src/PhoneNumbers.EntityFramework/README.md) | Entity Framework Core integration for the `PhoneNumber` value object, mapping it to a SQL `PhoneNumber` column type backed by `VARCHAR(16)` using a dedicated value converter. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.EntityFramework) | -|PosInformatique.Foundations.PhoneNumbers.FluentValidation icon|[**PosInformatique.Foundations.PhoneNumbers.FluentValidation**](./src/PhoneNumbers.FluentValidation/README.md) | FluentValidation integration for the `PhoneNumber` value object, providing dedicated validators and rules to ensure E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.FluentValidation) | -|PosInformatique.Foundations.PhoneNumbers.Json icon|[**PosInformatique.Foundations.PhoneNumbers.Json**](./src/PhoneNumbers.Json/README.md) | `System.Text.Json` converter for the `PhoneNumber` value object, enabling seamless serialization and deserialization of E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.Json) | -|PosInformatique.Foundations.Text.Templating icon|[**PosInformatique.Foundations.Text.Templating**](./src/Text.Templating/README.md) | Abstractions for text templating, including the `TextTemplate` base class and `ITextTemplateRenderContext` interface, to be used by concrete templating engine implementations such as Razor-based text templates. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating) | -|PosInformatique.Foundations.Text.Templating.Razor icon|[**PosInformatique.Foundations.Text.Templating.Razor**](./src/Text.Templating.Razor/README.md) | Razor-based text templating using Blazor components, allowing generation of text from Razor views with a strongly-typed Model parameter and full dependency injection integration. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor) | -|PosInformatique.Foundations.Text.Templating.Scriban icon|[**PosInformatique.Foundations.Text.Templating.Scriban**](./src/Text.Templating.Scriban/README.md) | Scriban-based text templating with mustache-style syntax, allowing generation of text from templates using a strongly-typed model and automatic property exposure. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Scriban)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Scriban) | +|PosInformatique.Foundations.EmailAddresses icon|[**EmailAddresses**](./src/EmailAddresses/README.md) | Strongly-typed value object representing an email address with validation and normalization as RFC 5322 compliant. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses) | +|PosInformatique.Foundations.EmailAddresses.EntityFramework icon|[**EmailAddresses.EntityFramework**](./src/EmailAddresses.EntityFramework/README.md) | Entity Framework Core integration for the `EmailAddress` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework) | +|PosInformatique.Foundations.EmailAddresses.FluentValidation icon|[**EmailAddresses.FluentValidation**](./src/EmailAddresses.FluentValidation/README.md) | FluentValidation integration for the `EmailAddress` value object, providing dedicated validators and rules to ensure RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation) | +|PosInformatique.Foundations.EmailAddresses.Json icon|[**EmailAddresses.Json**](./src/EmailAddresses.Json/README.md) | `System.Text.Json` converter for the `EmailAddress` value object, enabling seamless serialization and deserialization of RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json) | +|PosInformatique.Foundations.Emailing icon|[**Emailing**](./src/Emailing/README.md) | Template-based emailing infrastructure for .NET that lets you register strongly-typed email templates, create emails from models, and send them through pluggable providers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing) | +|PosInformatique.Foundations.Emailing.Azure icon|[**Emailing.Azure**](./src/Emailing.Azure/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Azure Communication Service**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Azure)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure) | +|PosInformatique.Foundations.Emailing.Graph icon|[**Emailing.Graph**](./src/Emailing.Graph/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Microsoft Graph API**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Graph)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph) | +|PosInformatique.Foundations.Emailing.Templates.Razor icon|[**Emailing.Templates.Razor**](./src/Emailing.Templates.Razor/README.md) | Helpers to build EmailTemplate instances from Razor components for subject and HTML body, supporting strongly-typed models and reusable layouts. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Templates.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor) | +|PosInformatique.Foundations.MediaTypes icon|[**MediaTypes**](./src/MediaTypes/README.md) | Immutable `MimeType` value object with well-known media types and helpers to map between media types and file extensions. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes) | +|PosInformatique.Foundations.MediaTypes.EntityFramework icon|[**MediaTypes.EntityFramework**](./src/MediaTypes.EntityFramework/README.md) | Entity Framework Core integration for the `MimeType` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.EntityFramework) | +|PosInformatique.Foundations.MediaTypes.Json icon|[**MediaTypes.Json**](./src/MediaTypes.Json/README.md) | `System.Text.Json` converter for the `MimeType` value object, enabling seamless serialization and deserialization of MIME types within JSON documents. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.Json) | +|PosInformatique.Foundations.People icon|[**People**](./src/People/README.md) | Strongly-typed value objects for first and last names with validation and normalization. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People)](https://www.nuget.org/packages/PosInformatique.Foundations.People) | +|PosInformatique.Foundations.People.DataAnnotations icon|[**People.DataAnnotations**](./src/People.DataAnnotations/README.md) | DataAnnotations attributes for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.DataAnnotations)](https://www.nuget.org/packages/PosInformatique.Foundations.People.DataAnnotations) | +|PosInformatique.Foundations.People.EntityFramework icon|[**People.EntityFramework**](./src/People.EntityFramework/README.md) | Entity Framework Core integration for `FirstName` and `LastName` value objects, providing fluent property configuration and value converters. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.People.EntityFramework) | +|PosInformatique.Foundations.People.FluentAssertions icon|[**People.FluentAssertions**](./src/People.FluentAssertions/README.md) | [FluentAssertions](https://fluentassertions.com/) extensions for `FirstName` and `LastName` to avoid ambiguity and provide `Should().Be(string)` assertions (case-sensitive on normalized values). | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentAssertions)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentAssertions) | +|PosInformatique.Foundations.People.FluentValidation icon|[**People.FluentValidation**](./src/People.FluentValidation/README.md) | [FluentValidation](https://fluentvalidation.net/) extensions for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentValidation) | +|PosInformatique.Foundations.People.Json icon|[**People.Json**](./src/People.Json/README.md) | `System.Text.Json` converters for `FirstName` and `LastName`, with validation and easy registration via `AddPeopleConverters()`. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.People.Json) | +|PosInformatique.Foundations.PhoneNumbers icon|[**PhoneNumbers**](./src/PhoneNumbers/README.md) | Strongly-typed value object representing a phone number in E.164 format, with parsing (including region-aware local numbers), validation, comparison, and formatting helpers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers) | +|PosInformatique.Foundations.PhoneNumbers.EntityFramework icon|[**PhoneNumbers.EntityFramework**](./src/PhoneNumbers.EntityFramework/README.md) | Entity Framework Core integration for the `PhoneNumber` value object, mapping it to a SQL `PhoneNumber` column type backed by `VARCHAR(16)` using a dedicated value converter. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.EntityFramework) | +|PosInformatique.Foundations.PhoneNumbers.FluentValidation icon|[**PhoneNumbers.FluentValidation**](./src/PhoneNumbers.FluentValidation/README.md) | FluentValidation integration for the `PhoneNumber` value object, providing dedicated validators and rules to ensure E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.FluentValidation) | +|PosInformatique.Foundations.PhoneNumbers.Json icon|[**PhoneNumbers.Json**](./src/PhoneNumbers.Json/README.md) | `System.Text.Json` converter for the `PhoneNumber` value object, enabling seamless serialization and deserialization of E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.Json) | +|PosInformatique.Foundations.Text.Templating icon|[**Text.Templating**](./src/Text.Templating/README.md) | Abstractions for text templating, including the `TextTemplate` base class and `ITextTemplateRenderContext` interface, to be used by concrete templating engine implementations such as Razor-based text templates. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating) | +|PosInformatique.Foundations.Text.Templating.Razor icon|[**Text.Templating.Razor**](./src/Text.Templating.Razor/README.md) | Razor-based text templating using Blazor components, allowing generation of text from Razor views with a strongly-typed Model parameter and full dependency injection integration. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor) | +|PosInformatique.Foundations.Text.Templating.Scriban icon|[**Text.Templating.Scriban**](./src/Text.Templating.Scriban/README.md) | Scriban-based text templating with mustache-style syntax, allowing generation of text from templates using a strongly-typed model and automatic property exposure. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Scriban)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Scriban) | > Note: Most of the packages are completely independent. You install only what you need. @@ -58,13 +60,17 @@ You can install any package using the .NET CLI or NuGet Package Manager. - Get lightweight, modular libraries tailored to single responsibilities. - Add missing building blocks to your projects without introducing a heavyweight framework. +## 📜 Changelog +Each package maintains its own changelog in its respective `CHANGELOG.md` file located in the package's source directory, but +for your convenience, we also provide a consolidated [changelog here](./CHANGELOG.md). + ## 📌 .NET and dependency compatibility All [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) packages are designed to be compatible with **.NET 8.0**, **.NET 9.0** and **.NET 10.0**. To maximize backward compatibility with existing projects, dependencies on external libraries (such as `Microsoft.Graph`, etc.) intentionally target **relatively old versions**. This avoids forcing you to update your entire solution to the -latest versions used internally by PosInformatique.Foundations. +latest versions used internally by [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations). > Important: It is the responsibility of the application developer to explicitly reference and update any **transitive dependencies** in their own project if they want to use newer versions. @@ -92,6 +98,10 @@ while still consuming [PosInformatique.Foundations.Emailing.Graph](https://www.nuget.org/packages/[PosInformatique.Foundations.Emailing.Graph/). This is **recommended**, especially to benefit from the latest security updates and bug fixes of the underlying dependencies. +> The next versions of [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) packages +> will updated their dependencies to newer versions, if there is security vulnerabilities reported by NuGet or GitHub advisories. +> This to force also developers to avoid using vulnerable versions of the dependencies when upgrading to the new versions of the packages. + ## 📄 License Licensed under the [MIT License](./LICENSE). diff --git a/global.json b/global.json index 9d34b15..8912ad6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.305", + "version": "10.0.201", "rollForward": "latestFeature" } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a6e1a64..1f93a14 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 enable true diff --git a/src/Emailing.Azure/AzureEmailProvider.cs b/src/Emailing.Azure/AzureEmailProvider.cs index ca4fdf8..b1ad593 100644 --- a/src/Emailing.Azure/AzureEmailProvider.cs +++ b/src/Emailing.Azure/AzureEmailProvider.cs @@ -57,6 +57,16 @@ public async Task SendAsync(EmailMessage message, CancellationToken cancellation }, }; + foreach (var attachment in message.Attachments) + { + var attachmentContent = await BinaryData.FromStreamAsync(attachment.Content, cancellationToken); + + azureMessage.Attachments.Add(new global::Azure.Communication.Email.EmailAttachment( + attachment.FileName, + attachment.ContentType.ToString(), + attachmentContent)); + } + await this.client.SendAsync(global::Azure.WaitUntil.Started, azureMessage, cancellationToken); } } diff --git a/src/Emailing.Azure/CHANGELOG.md b/src/Emailing.Azure/CHANGELOG.md index bb7782e..8bdeff2 100644 --- a/src/Emailing.Azure/CHANGELOG.md +++ b/src/Emailing.Azure/CHANGELOG.md @@ -1,2 +1,8 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments using Azure Communication Service Emailing provider. + - Upgrade the [Microsoft.Extensions.Azure](https://www.nuget.org/packages/Microsoft.Extensions.Azure) dependency to version 1.13.1 to fix security vulnerabilities. + - [Azure Identity Libraries and Microsoft Authentication Library Elevation of Privilege Vulnerability](https://github.com/advisories/GHSA-m5vv-6r4h-3vj9) + - [Azure Identity Library for .NET Information Disclosure Vulnerability](https://github.com/advisories/GHSA-wvxc-855f-jvrv) + +1.0.0 - Initial release with Azure Communication Service Emailing provider. diff --git a/src/Emailing.Graph/CHANGELOG.md b/src/Emailing.Graph/CHANGELOG.md index eb7a9ad..4e4324a 100644 --- a/src/Emailing.Graph/CHANGELOG.md +++ b/src/Emailing.Graph/CHANGELOG.md @@ -1,2 +1,5 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments using Microsoft Graph Emailing provider. + +1.0.0 - Initial release with Microsoft Graph Emailing provider. diff --git a/src/Emailing.Graph/GraphEmailProvider.cs b/src/Emailing.Graph/GraphEmailProvider.cs index bcfbc0c..15edcbe 100644 --- a/src/Emailing.Graph/GraphEmailProvider.cs +++ b/src/Emailing.Graph/GraphEmailProvider.cs @@ -66,6 +66,27 @@ public async Task SendAsync(EmailMessage message, CancellationToken cancellation ], }; + if (message.Attachments.Count > 0) + { + graphMessage.Attachments = new List(); + + foreach (var attachment in message.Attachments) + { + using var attachmentContent = new MemoryStream(); + + await attachment.Content.CopyToAsync(attachmentContent, cancellationToken); + + var graphAttachment = new FileAttachment() + { + Name = attachment.FileName, + ContentBytes = attachmentContent.ToArray(), + ContentType = attachment.ContentType.ToString(), + }; + + graphMessage.Attachments.Add(graphAttachment); + } + } + var body = new SendMailPostRequestBody() { Message = graphMessage, diff --git a/src/Emailing/CHANGELOG.md b/src/Emailing/CHANGELOG.md index c76f51e..70485ef 100644 --- a/src/Emailing/CHANGELOG.md +++ b/src/Emailing/CHANGELOG.md @@ -1,2 +1,6 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments. + - Add a new overload method `EmailRecipientCollection.Add(EmailAddress, TModel)`. + +1.0.0 - Initial release with Emailing infrastructure. diff --git a/src/Emailing/Email.cs b/src/Emailing/Email.cs index 4fe4b81..a88fff5 100644 --- a/src/Emailing/Email.cs +++ b/src/Emailing/Email.cs @@ -24,10 +24,16 @@ public Email(EmailTemplate template) this.Template = template; + this.Attachments = []; this.Importance = EmailImportance.Normal; this.Recipients = []; } + /// + /// Gets the collection of attachments included with the email message. + /// + public Collection Attachments { get; } + /// /// Gets or sets the importance of the e-mail. /// diff --git a/src/Emailing/EmailAttachment.cs b/src/Emailing/EmailAttachment.cs new file mode 100644 index 0000000..655384f --- /dev/null +++ b/src/Emailing/EmailAttachment.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Foundations.Emailing +{ + using PosInformatique.Foundations.MediaTypes; + + /// + /// Represents an email attachment of the . + /// + public class EmailAttachment + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the file. + /// The MIME type of the . + /// The content of the attachment. The stream will be read when the + /// is called. The stream will not be disposed by the e-mailing system it is the responsibility + /// of the caller to dispose it once the e-mail have been sent. + /// Thrown when the argument is . + /// Thrown when argument is empty or contains white spaces. + /// Thrown when the argument is . + /// Thrown when the argument is . + /// Thrown when the stream argument is not readable. + public EmailAttachment(string fileName, MimeType contentType, Stream content) + { + ArgumentException.ThrowIfNullOrWhiteSpace(fileName); + ArgumentNullException.ThrowIfNull(contentType); + ArgumentNullException.ThrowIfNull(content); + + if (!content.CanRead) + { + throw new ArgumentException("The content stream must be readable.", nameof(content)); + } + + this.FileName = fileName; + this.ContentType = contentType; + this.Content = content; + } + + /// + /// Gets the content of the attachment. + /// + /// + /// The stream will be read when the + /// is called. The will not be disposed by the e-mailing system it is the responsibility + /// of the caller to dispose it once the e-mail have been sent. + /// + public Stream Content { get; } + + /// + /// Gets the file name of the attachment. + /// + public string FileName { get; } + + /// + /// Gets the content type of the attachment. + /// + public MimeType ContentType { get; } + } +} \ No newline at end of file diff --git a/src/Emailing/EmailManager.cs b/src/Emailing/EmailManager.cs index 2975047..4dc8870 100644 --- a/src/Emailing/EmailManager.cs +++ b/src/Emailing/EmailManager.cs @@ -49,36 +49,70 @@ public async Task SendAsync(Email email, CancellationToken cance var senderEmailAddress = this.options.Value.SenderEmailAddress!; - foreach (var recipient in email.Recipients) + // Copy the attachments in memory. + var attachmentsContent = new List(email.Attachments.Count); + + foreach (var attachment in email.Attachments) { - // Render the subject - using var subjectOutputWriter = new StringWriter(); + var attachementStream = new MemoryStream(); + + await attachment.Content.CopyToAsync(attachementStream, cancellationToken); + + attachementStream.Position = 0; + + attachmentsContent.Add(attachementStream); + } - var textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); + try + { + foreach (var recipient in email.Recipients) + { + // Render the subject + using var subjectOutputWriter = new StringWriter(); - await email.Template.Subject.RenderAsync(recipient.Model, subjectOutputWriter, textTemplateRenderContext, cancellationToken); + var textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); - var subject = subjectOutputWriter.ToString(); + await email.Template.Subject.RenderAsync(recipient.Model, subjectOutputWriter, textTemplateRenderContext, cancellationToken); - // Render the HTML content - using var htmlContentOutputWriter = new StringWriter(); + var subject = subjectOutputWriter.ToString(); - textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); + // Render the HTML content + using var htmlContentOutputWriter = new StringWriter(); - await email.Template.HtmlBody.RenderAsync(recipient.Model, htmlContentOutputWriter, textTemplateRenderContext, cancellationToken); + textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); - var htmlContent = htmlContentOutputWriter.ToString(); + await email.Template.HtmlBody.RenderAsync(recipient.Model, htmlContentOutputWriter, textTemplateRenderContext, cancellationToken); - var message = new EmailMessage( - new EmailContact(senderEmailAddress, string.Empty), - new EmailContact(recipient.Address, recipient.DisplayName), - subject, - htmlContent) - { - Importance = email.Importance, - }; + var htmlContent = htmlContentOutputWriter.ToString(); - await this.provider.SendAsync(message, cancellationToken); + var message = new EmailMessage( + new EmailContact(senderEmailAddress, string.Empty), + new EmailContact(recipient.Address, recipient.DisplayName), + subject, + htmlContent) + { + Importance = email.Importance, + }; + + for (int i = 0; i < email.Attachments.Count; i++) + { + attachmentsContent[i].Position = 0; + + var emailAttachement = email.Attachments[i]; + var newEmailAttachement = new EmailAttachment(emailAttachement.FileName, emailAttachement.ContentType, attachmentsContent[i]); + + message.Attachments.Add(newEmailAttachement); + } + + await this.provider.SendAsync(message, cancellationToken); + } + } + finally + { + foreach (var attachmentContent in attachmentsContent) + { + await attachmentContent.DisposeAsync(); + } } } diff --git a/src/Emailing/EmailMessage.cs b/src/Emailing/EmailMessage.cs index 1a5b583..afe7f96 100644 --- a/src/Emailing/EmailMessage.cs +++ b/src/Emailing/EmailMessage.cs @@ -34,9 +34,15 @@ public EmailMessage(EmailContact from, EmailContact to, string subject, string h this.Subject = subject; this.HtmlContent = htmlContent; + this.Attachments = []; this.Importance = EmailImportance.Normal; } + /// + /// Gets the collection of attachments included with the email message. + /// + public Collection Attachments { get; } + /// /// Gets the sender of the e-mail message. /// diff --git a/src/Emailing/EmailRecipientCollection.cs b/src/Emailing/EmailRecipientCollection.cs index c2a1f30..3aad337 100644 --- a/src/Emailing/EmailRecipientCollection.cs +++ b/src/Emailing/EmailRecipientCollection.cs @@ -15,6 +15,19 @@ namespace PosInformatique.Foundations.Emailing /// Data model injected to the to generate the e-mail for each recipient. public class EmailRecipientCollection : Collection> { + /// + /// Creates and add new in the . + /// + /// E-mail address of the recipient. + /// Data model to apply on the to generate the e-mail for the recipient. + /// The created and added. + /// Thrown when the argument is . + /// Thrown when the argument is . + public EmailRecipient Add(EmailAddress address, TModel model) + { + return this.Add(address, string.Empty, model); + } + /// /// Creates and add new in the . /// diff --git a/src/Emailing/Emailing.csproj b/src/Emailing/Emailing.csproj index b341ee0..2dc6fb9 100644 --- a/src/Emailing/Emailing.csproj +++ b/src/Emailing/Emailing.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Emailing/README.md b/src/Emailing/README.md index 53b1898..9b46bbd 100644 --- a/src/Emailing/README.md +++ b/src/Emailing/README.md @@ -44,6 +44,7 @@ and one of its concrete implementations (for example - Central `IEmailManager` to create and send emails. - Pluggable `IEmailProvider` to send the final `EmailMessage` (transport-agnostic design). - Support of the *Importance* of the e-mails (**Low, Normal and High). +- Support for sending emails with attachments. ## Basic concepts @@ -206,7 +207,22 @@ invitationEmail.Recipients.Add( }); ``` -### 3. Send the email +### 3. Add attachments (optional) + +You can add attachments to the email by using the `Attachments` collection +of the `EmailMessage` class: + +```csharp +using var fileContent = File.OpenRead("document.pdf"); + +invitationEmail.Attachments.Add(new EmailAttachment("MyFile.pdf", "MimeTypes.Pdf", fileContent)); +``` + +> **Note:** The `IEmailManager.SendAsync()` method will read the content of the attachment stream +> but will not dispose of it. Make sure to manage the `Stream` lifetime appropriately +> by calling the `Dispose()` method on the `Stream` instance after the email has been sent. + +### 4. Send the email Once the email and its recipients are configured, you ask the `IEmailManager` to send it: @@ -224,6 +240,8 @@ Under the hood: - The configured sender (`EmailingOptions.SenderEmailAddress`). - The recipient address and display name. - The generated subject and HTML content. + - The importance. + - The associated attachments. 4. It calls `IEmailProvider.SendAsync(...)` to actually send the message. The provider implementation is responsible for the technical details (SMTP, Azure Communication Service, etc.). diff --git a/src/Text.Templating.Razor/README.md b/src/Text.Templating.Razor/README.md index 533c3f5..5261fcd 100644 --- a/src/Text.Templating.Razor/README.md +++ b/src/Text.Templating.Razor/README.md @@ -155,6 +155,20 @@ Formatted data: @this.Formatter.Format(Model) As long as `IDateTimeProvider` and `IMyFormatter` are registered in the `IServiceCollection`, they are available during template rendering. +## HTML rendering and character encoding + +The output of Razor templates is standard HTML. This means that special characters (including accents) +are HTML-encoded by default when using expressions like `@Model.Name`. + +If you need to output already-encoded or raw HTML content from your model in a Razor Component (Blazor-style), +you must explicitly disable HTML encoding in your Razor template using the `MarkupString` class, for example: + +```razor +@((MarkupString)Model.Name) +``` + +Use this only when you are sure that the content is safe (to avoid XSS vulnerabilities). + ## Links - [NuGet package: Emailing.Templates.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/) diff --git a/src/Text.Templating.Scriban/CHANGELOG.md b/src/Text.Templating.Scriban/CHANGELOG.md index 11422a3..60151b9 100644 --- a/src/Text.Templating.Scriban/CHANGELOG.md +++ b/src/Text.Templating.Scriban/CHANGELOG.md @@ -1,2 +1,9 @@ -1.0.0 +1.1.0 + - Upgrade the [Scriban](https://www.nuget.org/packages/Scriban) dependency to version 7.0.0 to fix security vulnerabilities. + - [Scriban Affected by Memory Exhaustion (OOM) via Unbounded String Generation (Denial of Service)](https://github.com/advisories/GHSA-5rpf-x9jg-8j5p) + - [Scriban: Sandbox escape due to TypedObjectAccessorcache bypassing MemberFilter after TemplateContext reuse](https://github.com/advisories/GHSA-5wr9-m6jw-xx44) + - [Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString](https://github.com/advisories/GHSA-m2p3-hwv5-xpqw) + - [Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation](https://github.com/advisories/GHSA-xw6w-9jjh-p9cr) + +1.0.0 - Initial release with the Scriban Text Templating feature. diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 94f80d6..18bf08c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -24,7 +24,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 $(SolutionDir)\CodeCoverage.runsettings @@ -33,7 +33,7 @@ false - $(NoWarn);SA0001 + $(NoWarn);SA0001;EnableGenerateDocumentationFile diff --git a/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs b/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs index 885c8a2..72c61a2 100644 --- a/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs +++ b/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs @@ -6,7 +6,9 @@ namespace PosInformatique.Foundations.Emailing.Azure.Tests { + using System.Reflection; using PosInformatique.Foundations.EmailAddresses; + using PosInformatique.Foundations.MediaTypes; public class AzureEmailProviderTest { @@ -33,8 +35,16 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, var from = new EmailContact(EmailAddress.Parse("sender@domain.com"), "Ignored"); var to = new EmailContact(EmailAddress.Parse("recipient@domain.com"), "The recipient"); + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, new MemoryStream([1, 2])); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, new MemoryStream([3, 4])); + var message = new EmailMessage(from, to, "The subject", "The HTML content") { + Attachments = + { + attachment1, + attachment2, + }, Importance = importance, }; @@ -42,7 +52,13 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, azureClient.Setup(c => c.SendAsync(global::Azure.WaitUntil.Started, It.IsAny(), cancellationToken)) .Callback((global::Azure.WaitUntil _, global::Azure.Communication.Email.EmailMessage m, CancellationToken _) => { - m.Attachments.Should().BeEmpty(); + m.Attachments.Should().HaveCount(2); + m.Attachments[0].Content.ToArray().Should().BeEquivalentTo(new byte[] { 1, 2 }); + m.Attachments[0].ContentType.Should().Be("application/pdf"); + m.Attachments[0].Name.Should().Be("Attachment1"); + m.Attachments[1].Content.ToArray().Should().BeEquivalentTo(new byte[] { 3, 4 }); + m.Attachments[1].ContentType.Should().Be("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + m.Attachments[1].Name.Should().Be("Attachment2"); m.Headers.Should().HaveCount(2); m.Headers["X-Priority"].Should().Be(expectedXPriority); m.Headers["Importance"].Should().Be(expectedImportance); @@ -63,6 +79,9 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, await provider.SendAsync(message, cancellationToken); azureClient.VerifyAll(); + + IsOpen(attachment1.Content).Should().BeTrue(); + IsOpen(attachment2.Content).Should().BeTrue(); } [Fact] @@ -78,5 +97,12 @@ await provider.Invoking(p => p.SendAsync(null, default)) azureClient.VerifyAll(); } + + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } } } \ No newline at end of file diff --git a/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs b/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs index 706dd2c..9187821 100644 --- a/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs +++ b/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs @@ -6,12 +6,14 @@ namespace PosInformatique.Foundations.Emailing.Graph.Tests { + using System.Reflection; using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Graph.Users.Item.SendMail; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Serialization.Json; + using PosInformatique.Foundations.MediaTypes; public class GraphEmailProviderTest { @@ -31,7 +33,7 @@ public void Constructor_WithServiceClientArgumentNull() [InlineData(EmailImportance.Low, Importance.Low)] [InlineData(EmailImportance.Normal, Importance.Normal)] [InlineData(EmailImportance.High, Importance.High)] - public async Task SendAsync(EmailImportance importance, Importance expectedImportance) + public async Task SendAsync_WithNoAttachment(EmailImportance importance, Importance expectedImportance) { var cancellationToken = new CancellationTokenSource().Token; @@ -85,6 +87,81 @@ public async Task SendAsync(EmailImportance importance, Importance expectedImpor serializationWriterFactory.VerifyAll(); } + [Theory] + [InlineData(EmailImportance.Low, Importance.Low)] + [InlineData(EmailImportance.Normal, Importance.Normal)] + [InlineData(EmailImportance.High, Importance.High)] + public async Task SendAsync_WithAttachment(EmailImportance importance, Importance expectedImportance) + { + var cancellationToken = new CancellationTokenSource().Token; + + var serializationWriterFactory = new Mock(MockBehavior.Strict); + serializationWriterFactory.Setup(f => f.GetSerializationWriter("application/json")) + .Returns(new JsonSerializationWriter()); + + var requestAdapter = new Mock(MockBehavior.Strict); + requestAdapter.Setup(r => r.BaseUrl) + .Returns("http://base/url"); + requestAdapter.Setup(r => r.EnableBackingStore(null)); + requestAdapter.Setup(r => r.SerializationWriterFactory) + .Returns(serializationWriterFactory.Object); + requestAdapter.Setup(r => r.SendNoContentAsync(It.IsAny(), It.IsNotNull>>(), cancellationToken)) + .Callback((RequestInformation requestInfo, Dictionary> _, CancellationToken _) => + { + requestInfo.HttpMethod.Should().Be(Method.POST); + requestInfo.URI.Should().Be("http://base/url/users/sender%40domain.com/sendMail"); + + var jsonMessage = KiotaJsonSerializer.DeserializeAsync(requestInfo.Content).GetAwaiter().GetResult(); + + jsonMessage.Message.Attachments.Should().HaveCount(2); + jsonMessage.Message.Attachments[0].As().ContentBytes.Should().Equal([1, 2]); + jsonMessage.Message.Attachments[0].ContentType.Should().Be("application/pdf"); + jsonMessage.Message.Attachments[0].Name.Should().Be("Attachment1"); + jsonMessage.Message.Attachments[1].As().ContentBytes.Should().Equal([3, 4]); + jsonMessage.Message.Attachments[1].ContentType.Should().Be("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + jsonMessage.Message.Attachments[1].Name.Should().Be("Attachment2"); + jsonMessage.Message.Body.Content.Should().Be("The HTML content"); + jsonMessage.Message.Body.ContentType.Should().Be(BodyType.Html); + jsonMessage.Message.BccRecipients.Should().BeNull(); + jsonMessage.Message.CcRecipients.Should().BeNull(); + jsonMessage.Message.Importance.Should().Be(expectedImportance); + jsonMessage.Message.ToRecipients.Should().HaveCount(1); + jsonMessage.Message.ToRecipients[0].EmailAddress.Address.Should().Be("recipient@domain.com"); + jsonMessage.Message.ToRecipients[0].EmailAddress.Name.Should().Be("The recipient"); + jsonMessage.SaveToSentItems.Should().BeFalse(); + }) + .Returns(Task.CompletedTask); + + var graphServiceClient = new Mock(MockBehavior.Strict, requestAdapter.Object, null); + + var client = new GraphEmailProvider(graphServiceClient.Object); + + var from = new EmailContact(EmailAddresses.EmailAddress.Parse("sender@domain.com"), "The sender"); + var to = new EmailContact(EmailAddresses.EmailAddress.Parse("recipient@domain.com"), "The recipient"); + + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, new MemoryStream([1, 2])); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, new MemoryStream([3, 4])); + + var message = new EmailMessage(from, to, "The subject", "The HTML content") + { + Attachments = + { + attachment1, + attachment2, + }, + Importance = importance, + }; + + await client.SendAsync(message, cancellationToken); + + graphServiceClient.VerifyAll(); + requestAdapter.VerifyAll(); + serializationWriterFactory.VerifyAll(); + + IsOpen(attachment1.Content).Should().BeTrue(); + IsOpen(attachment2.Content).Should().BeTrue(); + } + [Fact] public async Task SendSync_WithMessageArgumentNull() { @@ -104,5 +181,12 @@ await provider.Invoking(p => p.SendAsync(null, default)) requestAdapter.VerifyAll(); serviceClient.VerifyAll(); } + + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } } } \ No newline at end of file diff --git a/tests/Emailing.Tests/EmailAttachmentTest.cs b/tests/Emailing.Tests/EmailAttachmentTest.cs new file mode 100644 index 0000000..64639e3 --- /dev/null +++ b/tests/Emailing.Tests/EmailAttachmentTest.cs @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Foundations.Emailing.Tests +{ + using PosInformatique.Foundations.MediaTypes; + + public class EmailAttachmentTest + { + [Fact] + public void Constructor() + { + var content = new Mock(MockBehavior.Strict); + content.Setup(s => s.CanRead) + .Returns(true); + + var attachment = new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + content.Object); + + attachment.Content.Should().BeSameAs(content.Object); + attachment.ContentType.Should().Be(MimeTypes.Application.Pdf); + attachment.FileName.Should().Be("File.pdf"); + + content.VerifyAll(); + } + + [Fact] + public void Constructor_WithContentNoReadable() + { + var content = new Mock(MockBehavior.Strict); + content.Setup(s => s.CanRead) + .Returns(false); + + var act = () => + { + new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + content.Object); + }; + + act.Should().ThrowExactly() + .WithMessage("The content stream must be readable. (Parameter 'content')") + .WithParameterName("content"); + + content.VerifyAll(); + } + + [Fact] + public void Constructor_WithContentArgumentNull() + { + var act = () => + { + new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + null); + }; + + act.Should().ThrowExactly() + .WithParameterName("content"); + } + + [Fact] + public void Constructor_WithContentTypeArgumentNull() + { + var act = () => + { + new EmailAttachment( + "File.pdf", + null, + default); + }; + + act.Should().ThrowExactly() + .WithParameterName("contentType"); + } + + [Fact] + public void Constructor_WithFileNameArgumentNull() + { + var act = () => + { + new EmailAttachment( + null, + default, + default); + }; + + act.Should().ThrowExactly() + .WithParameterName("fileName"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithFileNameWithWhitespaces(string fileName) + { + var act = () => + { + new EmailAttachment( + fileName, + default, + default); + }; + + act.Should().ThrowExactly() + .WithMessage("The value cannot be an empty string or composed entirely of whitespace. (Parameter 'fileName')") + .WithParameterName("fileName"); + } + } +} \ No newline at end of file diff --git a/tests/Emailing.Tests/EmailManagerTest.cs b/tests/Emailing.Tests/EmailManagerTest.cs index 20faabd..8de1ef7 100644 --- a/tests/Emailing.Tests/EmailManagerTest.cs +++ b/tests/Emailing.Tests/EmailManagerTest.cs @@ -6,8 +6,10 @@ namespace PosInformatique.Foundations.Emailing.Tests { + using System.Reflection; using Microsoft.Extensions.Options; using PosInformatique.Foundations.EmailAddresses; + using PosInformatique.Foundations.MediaTypes; using PosInformatique.Foundations.Text.Templating; public class EmailManagerTest @@ -129,8 +131,19 @@ public async Task SendAsync() var emailAddressRecipient1 = EmailAddress.Parse("email1@domain.com"); var emailAddressRecipient2 = EmailAddress.Parse("email2@domain.com"); + var content1 = new MemoryStream([1, 2]); + var content2 = new MemoryStream([3, 4]); + + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, content1); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, content2); + var email = new Email(template) { + Attachments = + { + attachment1, + attachment2, + }, Importance = EmailImportance.High, Recipients = { @@ -144,27 +157,51 @@ public async Task SendAsync() var options = new EmailingOptions(); options.SenderEmailAddress = sender; + var contentStreams = new List(); + var provider = new Mock(MockBehavior.Strict); provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient1), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { + m.Attachments[0].Content.As().Position.Should().Be(0); + m.Attachments[0].Content.As().ToArray().Should().Equal(1, 2); + m.Attachments[0].ContentType.Should().Be(MimeTypes.Application.Pdf); + m.Attachments[0].FileName.Should().Be("Attachment1"); + m.Attachments[1].Content.As().Position.Should().Be(0); + m.Attachments[1].Content.As().ToArray().Should().Equal(3, 4); + m.Attachments[1].ContentType.Should().Be(MimeTypes.Application.Docx); + m.Attachments[1].FileName.Should().Be("Attachment2"); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); m.Subject.Should().Be("Subject 1"); m.HtmlContent.Should().Be("HTML Content 1"); m.To.DisplayName.Should().Be("The display name 1"); + + contentStreams.Add(m.Attachments[0].Content); + contentStreams.Add(m.Attachments[1].Content); }) .Returns(Task.CompletedTask); provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient2), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { + m.Attachments[0].Content.As().Position.Should().Be(0); + m.Attachments[0].Content.As().ToArray().Should().Equal(1, 2); + m.Attachments[0].ContentType.Should().Be(MimeTypes.Application.Pdf); + m.Attachments[0].FileName.Should().Be("Attachment1"); + m.Attachments[1].Content.As().Position.Should().Be(0); + m.Attachments[1].Content.As().ToArray().Should().Equal(3, 4); + m.Attachments[1].ContentType.Should().Be(MimeTypes.Application.Docx); + m.Attachments[1].FileName.Should().Be("Attachment2"); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); m.Subject.Should().Be("Subject 2"); m.HtmlContent.Should().Be("HTML Content 2"); m.To.DisplayName.Should().Be("The display name 2"); + + contentStreams.Add(m.Attachments[0].Content); + contentStreams.Add(m.Attachments[1].Content); }) .Returns(Task.CompletedTask); @@ -172,6 +209,14 @@ public async Task SendAsync() await manager.SendAsync(email, cancellationToken); + foreach (var contentStream in contentStreams) + { + IsOpen(contentStream).Should().BeFalse(); + } + + IsOpen(email.Attachments[0].Content).Should().BeTrue(); + IsOpen(email.Attachments[1].Content).Should().BeTrue(); + htmlBody.VerifyAll(); provider.VerifyAll(); subject.VerifyAll(); @@ -190,6 +235,13 @@ await manager.Invoking(m => m.SendAsync(null, default)) .WithParameterName("email"); } + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } + internal sealed class Model { } diff --git a/tests/Emailing.Tests/EmailMessageTest.cs b/tests/Emailing.Tests/EmailMessageTest.cs index a43cfd6..7bba5cb 100644 --- a/tests/Emailing.Tests/EmailMessageTest.cs +++ b/tests/Emailing.Tests/EmailMessageTest.cs @@ -22,6 +22,7 @@ public void Constructor() "The subject", "HTML content"); + emailMessage.Attachments.Should().BeEmpty(); emailMessage.From.Should().Be(from); emailMessage.Importance.Should().Be(EmailImportance.Normal); emailMessage.HtmlContent.Should().Be("HTML content"); diff --git a/tests/Emailing.Tests/EmailRecipientCollectionTest.cs b/tests/Emailing.Tests/EmailRecipientCollectionTest.cs index 7a48f84..554038f 100644 --- a/tests/Emailing.Tests/EmailRecipientCollectionTest.cs +++ b/tests/Emailing.Tests/EmailRecipientCollectionTest.cs @@ -25,6 +25,44 @@ public void Add() var collection = new EmailRecipientCollection(); + var result = collection.Add(EmailAddress.Parse("name@domain.com"), model); + + collection.Should().Equal(result); + + result.Address.Should().Be(EmailAddress.Parse("name@domain.com")); + result.DisplayName.Should().BeEmpty(); + result.Model.Should().BeSameAs(model); + } + + [Fact] + public void Add_WithNullAddress() + { + var collection = new EmailRecipientCollection(); + + collection.Invoking(c => c.Add(null, default)) + .Should().ThrowExactly() + .WithParameterName("address"); + } + + [Fact] + public void Add_WithNullModel() + { + var address = EmailAddress.Parse("email@domain.com"); + + var collection = new EmailRecipientCollection(); + + collection.Invoking(c => c.Add(address, null)) + .Should().ThrowExactly() + .WithParameterName("model"); + } + + [Fact] + public void Add_WithDisplayName() + { + var model = new Model(); + + var collection = new EmailRecipientCollection(); + var result = collection.Add(EmailAddress.Parse("name@domain.com"), "The display name", model); collection.Should().Equal(result); @@ -35,7 +73,7 @@ public void Add() } [Fact] - public void Add_WithNullAddress() + public void Add_WithDisplayName_WithNullAddress() { var collection = new EmailRecipientCollection(); @@ -45,7 +83,7 @@ public void Add_WithNullAddress() } [Fact] - public void Add_WithNullDisplayName() + public void Add_WithDisplayName_WithNullDisplayName() { var address = EmailAddress.Parse("email@domain.com"); @@ -57,7 +95,7 @@ public void Add_WithNullDisplayName() } [Fact] - public void Add_WithNullModel() + public void Add_WithDisplayName_WithNullModel() { var address = EmailAddress.Parse("email@domain.com"); diff --git a/tests/Emailing.Tests/EmailTest.cs b/tests/Emailing.Tests/EmailTest.cs index b5c3841..ab47057 100644 --- a/tests/Emailing.Tests/EmailTest.cs +++ b/tests/Emailing.Tests/EmailTest.cs @@ -20,6 +20,7 @@ public void Constructor() var email = new Email(template); + email.Attachments.Should().BeEmpty(); email.Importance.Should().Be(EmailImportance.Normal); email.Recipients.Should().BeEmpty(); email.Template.Should().BeSameAs(template); diff --git a/tests/People.Tests/NameTestData.cs b/tests/People.Tests/NameTestData.cs index 1a8b99a..664a615 100644 --- a/tests/People.Tests/NameTestData.cs +++ b/tests/People.Tests/NameTestData.cs @@ -27,6 +27,7 @@ public static class NameTestData "$$Jean", "Jean@$+Patrick", "Jean-Patrick.", + "Jean-Patrick1234", string.Empty, " ", " ", @@ -49,6 +50,7 @@ public static class NameTestData "$$Dupont", "Du@$+Pont", "Du-pont.", + "Dupont1234", string.Empty, " ", };