diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1117d16
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,93 @@
+root = true
+
+[*]
+charset = utf-8-bom
+insert_final_newline = false
+trim_trailing_whitespace = true
+
+# Markdown specific settings
+[*.md]
+indent_style = space
+indent_size = 2
+
+# YAML specific settings
+[*.yaml]
+indent_style = space
+indent_size = 2
+
+# x.proj specific settings
+[*.{csproj,props}]
+indent_style = space
+indent_size = 2
+
+[*.{cs,vb}]
+
+#### Naming styles ####
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+
+csharp_style_prefer_primary_constructors = false:suggestion
+csharp_using_directive_placement = inside_namespace:warning
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_namespace_match_folder = false:suggestion
+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_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+
+### Visual Studio ###
+
+# IDE0005: Using directive is unnecessary.
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0130: Namespace does not match folder structure
+dotnet_diagnostic.IDE0130.severity = none
diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml
new file mode 100644
index 0000000..b7681a6
--- /dev/null
+++ b/.github/workflows/github-actions-ci.yaml
@@ -0,0 +1,59 @@
+name: Continuous Integration
+
+on:
+ pull_request:
+ branches: [ "main" ]
+ push:
+ branches: [ "releases/**" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ permissions:
+ checks: write
+ pull-requests: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore PosInformatique.Foundations.slnx
+
+ - name: Build solution
+ run: |
+ dotnet build PosInformatique.Foundations.slnx \
+ --configuration Release \
+ --no-restore
+
+ - name: Run tests (.NET 8.0)
+ run: |
+ dotnet test PosInformatique.Foundations.slnx \
+ --configuration Release \
+ --no-build \
+ --logger "trx" \
+ --results-directory ./TestResults \
+ --collect "XPlat Code Coverage" \
+ --framework net8.0
+
+ - name: Run tests (.NET 9.0)
+ run: |
+ dotnet test PosInformatique.Foundations.slnx \
+ --configuration Release \
+ --no-build \
+ --logger "trx" \
+ --results-directory ./TestResults \
+ --collect "XPlat Code Coverage" \
+ --framework net9.0
+
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ if: (!cancelled())
+ with:
+ files: |
+ TestResults/**/*.trx
diff --git a/.github/workflows/github-actions-release.yaml b/.github/workflows/github-actions-release.yaml
new file mode 100644
index 0000000..c196d88
--- /dev/null
+++ b/.github/workflows/github-actions-release.yaml
@@ -0,0 +1,41 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ VersionPrefix:
+ type: string
+ description: The version of the library
+ required: true
+ default: 1.0.0
+ VersionSuffix:
+ type: string
+ description: The version suffix of the library (for example rc.1)
+
+run-name: ${{ inputs.VersionSuffix && format('{0}-{1}', inputs.VersionPrefix, inputs.VersionSuffix) || inputs.VersionPrefix }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.x'
+
+ - name: Build all the NuGet packages
+ run: |
+ dotnet pack PosInformatique.Foundations.slnx \
+ --configuration Release \
+ --property:VersionPrefix=${{ github.event.inputs.VersionPrefix }} \
+ --property:VersionSuffix=${{ github.event.inputs.VersionSuffix }} \
+ --output ./artifacts
+
+ - name: Publish the package to nuget.org
+ run: |
+ dotnet nuget push "./artifacts/*.nupkg" \
+ --api-key "${{ secrets.NUGET_APIKEY }}" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
diff --git a/CodeCoverage.runsettings b/CodeCoverage.runsettings
new file mode 100644
index 0000000..2ce4a98
--- /dev/null
+++ b/CodeCoverage.runsettings
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ .*\.dll$
+
+
+ .*xunit.*
+ .*webjobs.*
+ bunit.*
+ fluentvalidation.*
+ moq.*
+ .*durabletask.*
+ microsoft.*
+ .*tests\.dll$
+
+
+
+
+
+
+ ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$
+ ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$
+
+
+
+
+
+ ^MimeKit\..*
+
+
+
+
+
+ .*\.razor
+ .*\.cshtml
+
+
+
+
+ True
+ True
+ True
+ False
+
+
+
+
+
+
+
+
+
+
+ opencover,cobertura
+ [MimeKit.*]*
+ GeneratedCodeAttribute,CompilerGeneratedAttribute
+ **/*.razor,**/*.razor.cs,**/*.cshtml
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..4c60f5f
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,54 @@
+
+
+
+
+ Gilles TOURREAU
+ P.O.S Informatique
+ P.O.S Informatique
+ Copyright (c) P.O.S Informatique. All rights reserved.
+ https://github.com/PosInformatique/PosInformatique.Foundations
+ git
+
+
+ latest
+
+
+ enable
+
+
+ $(NoWarn);SA0001;NU1903
+
+
+ true
+
+
+ PosInformatique.Foundations.$(MSBuildProjectName)
+ PosInformatique.Foundations.$(MSBuildProjectName.Replace(" ", "_"))
+
+
+
+
+ stylecop.json
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..6415f3a
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,34 @@
+
+
+ true
+
+ 8.0.0
+ 9.0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Icon.png b/Icon.png
new file mode 100644
index 0000000..1b2cc54
Binary files /dev/null and b/Icon.png differ
diff --git a/PosInformatique.Foundations.slnx b/PosInformatique.Foundations.slnx
new file mode 100644
index 0000000..cd18ea0
--- /dev/null
+++ b/PosInformatique.Foundations.slnx
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 908ec8f..a269e78 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,97 @@
-# PosInformatique.Foundations
-A lightweight collection of foundational .NET libraries for standardizing technical and functional development with reusable components.
+# PosInformatique.Foundations
+
+
+
+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.
+
+## ✨ Philosophy
+
+- **Granular**: each library is independent, lightweight, and minimal.
+- **Composable**: you bring exactly the pieces you need, nothing more.
+- **Practical**: packages can be foundational (value objects, abstractions, contracts) or technical utilities (helpers, validation rules, extensions).
+- **Consistent**: all packages follow the same naming convention and version alignment.
+- **Standards-based**: whenever possible, implementations follow well-known standards (e.g. RFC 5322 for email addresses, E.164 for phone numbers,...).
+
+➡️ Each package has **no strong dependency** on another. You are free to pick only what you need.
+➡️ These libraries are **not structuring frameworks**; they are small utilities meant to fill missing gaps in your applications.
+
+## 📦 Packages Overview
+
+You can install any package using the .NET CLI or NuGet Package Manager.
+
+| |Package | Description | NuGet |
+|--|---------|-------------|-------|
+||[**PosInformatique.Foundations.EmailAddresses**](./src/EmailAddresses/README.md) | Strongly-typed value object representing an email address with validation and normalization as RFC 5322 compliant. | [](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing) |
+||[**PosInformatique.Foundations.Emailing.Azure**](./src/Emailing.Azure/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Azure Communication Service**. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure) |
+||[**PosInformatique.Foundations.Emailing.Graph**](./src/Emailing.Graph/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Microsoft Graph API**. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.EntityFramework) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.Json) |
+||[**PosInformatique.Foundations.People**](./src/People/README.md) | Strongly-typed value objects for first and last names with validation and normalization. | [](https://www.nuget.org/packages/PosInformatique.Foundations.People) |
+||[**PosInformatique.Foundations.People.DataAnnotations**](./src/People.DataAnnotations/README.md) | DataAnnotations attributes for `FirstName` and `LastName` value objects. | [](https://www.nuget.org/packages/PosInformatique.Foundations.People.DataAnnotations) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.People.EntityFramework) |
+||[**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). | [](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentAssertions) |
+||[**PosInformatique.Foundations.People.FluentValidation**](./src/People.FluentValidation/README.md) | [FluentValidation](https://fluentvalidation.net/) extensions for `FirstName` and `LastName` value objects. | [](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentValidation) |
+||[**PosInformatique.Foundations.People.Json**](./src/People.Json/README.md) | `System.Text.Json` converters for `FirstName` and `LastName`, with validation and easy registration via `AddPeopleConverters()`. | [](https://www.nuget.org/packages/PosInformatique.Foundations.People.Json) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.EntityFramework) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.FluentValidation) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.Json) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor) |
+||[**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. | [](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Scriban) |
+
+> Note: Most of the packages are completely independent. You install only what you need.
+
+## 🚀 Why use PosInformatique.Foundations?
+
+- Avoid reinventing common value objects and utilities.
+- Apply standards-based implementations (RFC, E.164, ...).
+- Improve consistency across your projects.
+- Get lightweight, modular libraries tailored to single responsibilities.
+- Add missing building blocks to your projects without introducing a heavyweight framework.
+
+## 📌 .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.
+
+> 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.
+> See [NuGet dependency resolution](https://learn.microsoft.com/en-us/nuget/concepts/dependency-resolution)
+and [transitive dependencies in Visual Studio](https://devblogs.microsoft.com/dotnet/introducing-transitive-dependencies-in-visual-studio/)
+for more details.
+
+### Example with Microsoft.Graph
+
+The [PosInformatique.Foundations.Emailing.Graph](https://www.nuget.org/packages/[PosInformatique.Foundations.Emailing.Graph/)
+package depends on [Microsoft.Graph](https://www.nuget.org/packages/Microsoft.Graph/) **5.89.0**
+for backward compatibility with a wide range of existing projects.
+
+If your application requires a newer version, you can simply add an explicit reference in your project, for example:
+
+```xml
+
+
+
+
+```
+
+In this case, your project will use [Microsoft.Graph](https://www.nuget.org/packages/Microsoft.Graph/) **5.96.0**
+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.
+
+## 📄 License
+
+Licensed under the [MIT License](./LICENSE).
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..9d34b15
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "9.0.305",
+ "rollForward": "latestFeature"
+ }
+}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..a6e1a64
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ net8.0;net9.0
+ enable
+
+ true
+
+ Icon.png
+ https://github.com/PosInformatique/PosInformatique.Foundations
+ README.md
+ MIT
+
+
+
+
+
+ <_Parameter1>$(AssemblyName).Tests
+
+
+ <_Parameter1>DynamicProxyGenAssembly2
+
+
+
\ No newline at end of file
diff --git a/src/EmailAddresses.EntityFramework/CHANGELOG.md b/src/EmailAddresses.EntityFramework/CHANGELOG.md
new file mode 100644
index 0000000..771d9aa
--- /dev/null
+++ b/src/EmailAddresses.EntityFramework/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with the support Entity Framework persitance for EmailAddress value object.
diff --git a/src/EmailAddresses.EntityFramework/EmailAddressPropertyExtensions.cs b/src/EmailAddresses.EntityFramework/EmailAddressPropertyExtensions.cs
new file mode 100644
index 0000000..a609889
--- /dev/null
+++ b/src/EmailAddresses.EntityFramework/EmailAddressPropertyExtensions.cs
@@ -0,0 +1,53 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace Microsoft.EntityFrameworkCore
+{
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
+ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Contains extension methods to map a to a string column.
+ ///
+ public static class EmailAddressPropertyExtensions
+ {
+ ///
+ /// Configures the specified to be mapped on a column with a SQL EmailAddress type.
+ /// The EmailAddress type must be mapped to a VARCHAR(320).
+ ///
+ /// Type of the property which must be .
+ /// Entity property to map in the .
+ /// The instance to configure the configuration of the property.
+ /// If the specified argument is .
+ /// If the specified generic type is not a .
+ public static PropertyBuilder IsEmailAddress(this PropertyBuilder property)
+ {
+ ArgumentNullException.ThrowIfNull(property);
+
+ if (typeof(T) != typeof(EmailAddress))
+ {
+ throw new ArgumentException($"The '{nameof(IsEmailAddress)}()' method must be called on '{nameof(EmailAddress)} class.", nameof(property));
+ }
+
+ return property
+ .IsUnicode(false)
+ .HasMaxLength(320)
+ .HasColumnType("EmailAddress")
+ .HasConversion(EmailAddressConverter.Instance);
+ }
+
+ private sealed class EmailAddressConverter : ValueConverter
+ {
+ private EmailAddressConverter()
+ : base(v => v.ToString(), v => EmailAddress.Parse(v))
+ {
+ }
+
+ public static EmailAddressConverter Instance { get; } = new EmailAddressConverter();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses.EntityFramework/EmailAddresses.EntityFramework.csproj b/src/EmailAddresses.EntityFramework/EmailAddresses.EntityFramework.csproj
new file mode 100644
index 0000000..2bb7599
--- /dev/null
+++ b/src/EmailAddresses.EntityFramework/EmailAddresses.EntityFramework.csproj
@@ -0,0 +1,31 @@
+
+
+
+ true
+
+
+ Provides Entity Framework Core integration for the EmailAddress value object.
+ Enables seamless mapping of RFC 5322 compliant email addresses as strongly-typed properties in Entity Framework Core entities, with safe conversion to string.
+
+ email;emailaddress;entityframework;efcore;valueobject;validation;rfc5322;mapping;conversion;dotnet;posinformatique
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EmailAddresses.EntityFramework/README.md b/src/EmailAddresses.EntityFramework/README.md
new file mode 100644
index 0000000..a6f2ebc
--- /dev/null
+++ b/src/EmailAddresses.EntityFramework/README.md
@@ -0,0 +1,73 @@
+# PosInformatique.Foundations.EmailAddresses.EntityFramework
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework/)
+
+## Introduction
+Provides **Entity Framework Core** integration for the `EmailAddress` value object from
+[PosInformatique.Foundations.EmailAddresses](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/).
+This package enables seamless mapping of RFC 5322 compliant email addresses as strongly-typed properties in Entity Framework Core entities.
+
+It ensures proper SQL type mapping, validation, and conversion to `VARCHAR` when persisted to the database.
+
+## Install
+You can install the package from NuGet:
+
+```powershell
+dotnet add package PosInformatique.Foundations.EmailAddresses.EntityFramework
+```
+
+This package depends on the base package [PosInformatique.Foundations.EmailAddresses](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/).
+
+## Features
+- Provides an extension method `IsEmailAddress()` to configure EF Core properties for `EmailAddress`.
+- Maps to `VARCHAR(320)` database columns using the SQL type `EmailAddress` (you must define the SQL type `EmailAddress` mapped to `VARCHAR(320)` in your database).
+- Ensures validation, normalization, and safe conversion to/from database fields.
+- Built on top of the core `EmailAddress` value object.
+
+## Use cases
+- **Entity mapping**: enforce strong typing for email addresses at the persistence layer.
+- **Consistency**: ensure the same validation rules are applied in your entities and database.
+- **Safety**: prevent invalid strings being stored in your database
+
+## Examples
+
+> ⚠️ To use `IsEmailAddress()`, you must first define the SQL type `EmailAddress` mapped to `VARCHAR(320)` in your database.
+For SQL Server, you can create it with:
+
+```sql
+CREATE TYPE EmailAddress FROM VARCHAR(320) NOT NULL;
+```
+
+### Example: Configure an entity
+```csharp
+using Microsoft.EntityFrameworkCore;
+using PosInformatique.Foundations;
+
+public class User
+{
+ public int Id { get; set; }
+ public EmailAddress Email { get; set; }
+}
+
+public class ApplicationDbContext : DbContext
+{
+ public DbSet Users => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .Property(u => u.Email)
+ .IsEmailAddress();
+ }
+}
+```
+
+This will configure the `Email` property of the `User` entity with:
+- `VARCHAR(320)` (Non-unicode) column length
+- SQL column type `EmailAddress`
+
+## Links
+- [NuGet package: EmailAddresses.EntityFramework](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework/)
+- [NuGet package: EmailAddresses (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
\ No newline at end of file
diff --git a/src/EmailAddresses.FluentValidation/CHANGELOG.md b/src/EmailAddresses.FluentValidation/CHANGELOG.md
new file mode 100644
index 0000000..5a5116c
--- /dev/null
+++ b/src/EmailAddresses.FluentValidation/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with the support FluentValidation for the validation of EmailAddress value object.
diff --git a/src/EmailAddresses.FluentValidation/EmailAddressValidator.cs b/src/EmailAddresses.FluentValidation/EmailAddressValidator.cs
new file mode 100644
index 0000000..067438d
--- /dev/null
+++ b/src/EmailAddresses.FluentValidation/EmailAddressValidator.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace FluentValidation
+{
+ using FluentValidation.Validators;
+ using PosInformatique.Foundations.EmailAddresses;
+
+ internal sealed class EmailAddressValidator : PropertyValidator
+ {
+ public override string Name
+ {
+ get => "EmailAddressValidator";
+ }
+
+ public override bool IsValid(ValidationContext context, string value)
+ {
+ if (value is not null)
+ {
+ return EmailAddress.IsValid(value);
+ }
+
+ return true;
+ }
+
+ protected override string GetDefaultMessageTemplate(string errorCode)
+ {
+ return $"'{{PropertyName}}' must be a valid email address.";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses.FluentValidation/EmailAddresses.FluentValidation.csproj b/src/EmailAddresses.FluentValidation/EmailAddresses.FluentValidation.csproj
new file mode 100644
index 0000000..035ca70
--- /dev/null
+++ b/src/EmailAddresses.FluentValidation/EmailAddresses.FluentValidation.csproj
@@ -0,0 +1,30 @@
+
+
+
+ true
+
+
+ Provides a FluentValidation extension to validate email addresses
+ using the strongly-typed EmailAddress value object (RFC 5322 compliant).
+
+ email;emailaddress;fluentvalidation;validation;dotnet;rfc5322;ddd;valueobject;parsing;posinformatique
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EmailAddresses.FluentValidation/EmailAddressesValidatorExtensions.cs b/src/EmailAddresses.FluentValidation/EmailAddressesValidatorExtensions.cs
new file mode 100644
index 0000000..55f2467
--- /dev/null
+++ b/src/EmailAddresses.FluentValidation/EmailAddressesValidatorExtensions.cs
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace FluentValidation
+{
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Contains extension methods for FluentValidation to validate e-mail addresses.
+ ///
+ public static class EmailAddressesValidatorExtensions
+ {
+ ///
+ /// Defines a validator that checks if a property is a valid e-mail address
+ /// (parsable by the class).
+ /// Validation fails if the value is not a valid e-mail address.
+ /// If the value is , validation succeeds.
+ /// Use the validator
+ /// to disallow values.
+ ///
+ /// The type of the object being validated.
+ /// The rule builder on which the validator is defined.
+ /// The instance to continue configuring the property validator.
+ /// If the specified argument is .
+ public static IRuleBuilderOptions MustBeEmailAddress(this IRuleBuilder ruleBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(ruleBuilder);
+
+ return ruleBuilder.SetValidator(new EmailAddressValidator());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses.FluentValidation/README.md b/src/EmailAddresses.FluentValidation/README.md
new file mode 100644
index 0000000..74ce2e0
--- /dev/null
+++ b/src/EmailAddresses.FluentValidation/README.md
@@ -0,0 +1,81 @@
+# PosInformatique.Foundations.EmailAddresses.FluentValidation
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation/)
+
+## Introduction
+This package provides a [FluentValidation](https://fluentvalidation.net/) extension for validating email addresses using the
+[EmailAddress](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/) value object.
+
+It ensures that only **valid RFC 5322 compliant email addresses** are accepted when validating string properties.
+
+## Install
+You can install the package from [NuGet](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation/):
+
+```powershell
+dotnet add package PosInformatique.Foundations.EmailAddresses.FluentValidation
+```
+
+This package depends on the base package [PosInformatique.Foundations.EmailAddresses](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/).
+
+## Features
+- [FluentValidation](https://fluentvalidation.net/) extension for email address validation
+- Uses the same parsing and validation rules as the [EmailAddress](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/) value object
+- Clear and consistent error messages
+> `null` values are accepted (combine with `NotNull()` validator to forbid nulls)
+
+## Usage
+
+### Basic validation
+```csharp
+using FluentValidation;
+
+public class User
+{
+ public string Email { get; set; }
+}
+
+public class UserValidator : AbstractValidator
+{
+ public UserValidator()
+ {
+ RuleFor(u => u.Email).MustBeEmailAddress();
+ }
+}
+```
+
+### Null values are ignored
+```csharp
+var validator = new UserValidator();
+
+// Valid, because null is ignored
+var result1 = validator.Validate(new User { Email = null });
+Console.WriteLine(result1.IsValid); // True
+
+// Valid, because it's a valid email
+var result2 = validator.Validate(new User { Email = "alice@company.com" });
+Console.WriteLine(result2.IsValid); // True
+
+// Invalid, because it's not a valid email
+var result3 = validator.Validate(new User { Email = "not-an-email" });
+Console.WriteLine(result3.IsValid); // False
+```
+
+### Require non-null values
+```csharp
+public class UserValidator : AbstractValidator
+{
+ public UserValidator()
+ {
+ RuleFor(u => u.Email)
+ .NotEmpty() // Disallow null and empty
+ .MustBeEmailAddress();
+ }
+}
+```
+
+## Links
+- [NuGet package: EmailAddresses.FluentValidation](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation/)
+- [NuGet package: EmailAddresses (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
+- [FluentValidation](https://fluentvalidation.net/)
diff --git a/src/EmailAddresses.Json/CHANGELOG.md b/src/EmailAddresses.Json/CHANGELOG.md
new file mode 100644
index 0000000..69013f7
--- /dev/null
+++ b/src/EmailAddresses.Json/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with the support JSON serialization (with System.Text.Json) for EmailAddress value object.
diff --git a/src/EmailAddresses.Json/EmailAddressJsonConverter.cs b/src/EmailAddresses.Json/EmailAddressJsonConverter.cs
new file mode 100644
index 0000000..99a4745
--- /dev/null
+++ b/src/EmailAddresses.Json/EmailAddressJsonConverter.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.EmailAddresses.Json
+{
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// which allows to serialize and deserialize an
+ /// as a JSON string.
+ ///
+ public sealed class EmailAddressJsonConverter : JsonConverter
+ {
+ ///
+ public override bool HandleNull => true;
+
+ ///
+ public override EmailAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var input = reader.GetString();
+
+ if (input is null)
+ {
+ return null;
+ }
+
+ if (!EmailAddress.TryParse(input, out var emailAddress))
+ {
+ throw new JsonException($"'{input}' is not a valid email address.");
+ }
+
+ return emailAddress;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, EmailAddress value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses.Json/EmailAddresses.Json.csproj b/src/EmailAddresses.Json/EmailAddresses.Json.csproj
new file mode 100644
index 0000000..d7aa7d2
--- /dev/null
+++ b/src/EmailAddresses.Json/EmailAddresses.Json.csproj
@@ -0,0 +1,31 @@
+
+
+
+ true
+
+
+ Provides a System.Text.Json converter for the MimeType value object.
+ Enables seamless serialization and deserialization of MIME types within JSON documents.
+
+ mimetype;mediatype;contenttype;valueobject;ddd;json;parsing;validation;dotnet;posinformatique
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EmailAddresses.Json/EmailAddressesJsonSerializerOptionsExtensions.cs b/src/EmailAddresses.Json/EmailAddressesJsonSerializerOptionsExtensions.cs
new file mode 100644
index 0000000..48c4596
--- /dev/null
+++ b/src/EmailAddresses.Json/EmailAddressesJsonSerializerOptionsExtensions.cs
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace System.Text.Json
+{
+ using PosInformatique.Foundations.EmailAddresses;
+ using PosInformatique.Foundations.EmailAddresses.Json;
+
+ ///
+ /// Contains extension methods to configure for
+ /// JSON serialization.
+ ///
+ public static class EmailAddressesJsonSerializerOptionsExtensions
+ {
+ ///
+ /// Registers the to the .
+ ///
+ /// which the
+ /// converter will be added in the collection.
+ /// The instance to continue the configuration.
+ /// If the specified argument is .
+ public static JsonSerializerOptions AddEmailAddressesConverters(this JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ if (!options.Converters.Any(c => c is EmailAddressJsonConverter))
+ {
+ options.Converters.Add(new EmailAddressJsonConverter());
+ }
+
+ return options;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses.Json/README.md b/src/EmailAddresses.Json/README.md
new file mode 100644
index 0000000..1d11951
--- /dev/null
+++ b/src/EmailAddresses.Json/README.md
@@ -0,0 +1,83 @@
+# PosInformatique.Foundations.EmailAddresses.Json
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json/)
+
+## Introduction
+Provides a **System.Text.Json** converter for the `EmailAddress` value object from
+[PosInformatique.Foundations.EmailAddresses](../EmailAddresses/README.md). Enables seamless serialization and deserialization
+of **RFC 5322 compliant** email addresses within JSON documents.
+
+## Install
+You can install the package from NuGet:
+
+```powershell
+dotnet add package PosInformatique.Foundations.EmailAddresses.Json
+```
+
+This package depends on the base package [PosInformatique.Foundations.EmailAddresses](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/).
+
+## Features
+- Provides a `JsonConverter` for serialization and deserialization.
+- Ensures validation of **RFC 5322 compliant** email addresses when deserializing.
+- Can be used via attributes (`[JsonConverter]`) or through `JsonSerializerOptions` extension method.
+- Ensures consistency with the base `EmailAddress` value object.
+
+## Use cases
+- **Serialization**: Convert value objects into JSON strings without losing semantics
+- **Validation**: Guarantee that only valid RFC 5322 email addresses are accepted in JSON payloads
+- **Integration**: Plug directly into `System.Text.Json` configuration
+
+## Examples
+
+### Example 1: DTO with `[JsonConverter]` attribute
+```csharp
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using PosInformatique.Foundations.EmailAddresses;
+using PosInformatique.Foundations.EmailAddresses.Json;
+
+public class UserDto
+{
+ [JsonConverter(typeof(EmailAddressJsonConverter))]
+ public EmailAddress Email { get; set; } = default!;
+}
+
+// Serialization
+var dto = new UserDto { Email = "john.doe@example.com" };
+var json = JsonSerializer.Serialize(dto);
+// Result: {"Email":"john.doe@example.com"}
+
+// Deserialization
+var input = "{ "Email": "alice@company.org" }";
+var deserialized = JsonSerializer.Deserialize(input);
+Console.WriteLine(deserialized!.Email); // "alice@company.org"
+```
+
+### Example 2: Use extension method without attributes
+```csharp
+using System.Text.Json;
+using PosInformatique.Foundations.EmailAddresses;
+
+public class CustomerDto
+{
+ public EmailAddress Email { get; set; } = default!;
+}
+
+var options = new JsonSerializerOptions().AddEmailAddressesConverters();
+
+// Serialization
+var dto = new CustomerDto { Email = "bob@myapp.com" };
+var json = JsonSerializer.Serialize(dto, options);
+// Result: {"Email":"bob@myapp.com"}
+
+// Deserialization
+var input = "{ "Email": "carol@myapp.com" }";
+var deserialized = JsonSerializer.Deserialize(input, options);
+Console.WriteLine(deserialized!.Email); // "carol@myapp.com"
+```
+
+## Links
+- [NuGet package: EmailAddresses.Json](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json/)
+- [NuGet package: EmailAddresses (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
diff --git a/src/EmailAddresses/CHANGELOG.md b/src/EmailAddresses/CHANGELOG.md
new file mode 100644
index 0000000..206c6d3
--- /dev/null
+++ b/src/EmailAddresses/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with strongly-typed EmailAddress value object.
diff --git a/src/EmailAddresses/EmailAddress.cs b/src/EmailAddresses/EmailAddress.cs
new file mode 100644
index 0000000..e1e9d9d
--- /dev/null
+++ b/src/EmailAddresses/EmailAddress.cs
@@ -0,0 +1,326 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.EmailAddresses
+{
+ using System.Diagnostics.CodeAnalysis;
+ using MimeKit;
+
+ ///
+ /// Represents a valid e-mail address. Any attempt to create an invalid e-mail is rejected.
+ ///
+ ///
+ /// This class provides several features:
+ ///
+ ///
+ /// Implements and so that instances
+ /// can be compared and used seamlessly in generic scenarios such as collections, sorting,
+ /// or equality checks.
+ ///
+ ///
+ /// Implements and to enable generic
+ /// conversion to and from string representations, making it easy to integrate with a wide
+ /// range of components that rely on string formatting and parsing.
+ ///
+ ///
+ ///
+ public sealed class EmailAddress : IEquatable, IComparable, IFormattable, IParsable
+ {
+ private static readonly ParserOptions Options = new()
+ {
+ AllowAddressesWithoutDomain = false,
+ AddressParserComplianceMode = RfcComplianceMode.Strict,
+ Rfc2047ComplianceMode = RfcComplianceMode.Strict,
+ AllowUnquotedCommasInAddresses = false,
+ };
+
+ private readonly string value;
+
+ private EmailAddress(MailboxAddress address)
+ {
+ this.value = address.Address;
+
+ var parts = this.value.Split('@');
+ this.UserName = parts[0];
+ this.Domain = parts[1];
+ }
+
+ ///
+ /// Gets the user name part of the email address (The part before the @ separator).
+ ///
+ public string UserName { get; }
+
+ ///
+ /// Gets the domain part of the email address (The part after the @ separator).
+ ///
+ public string Domain { get; }
+
+ ///
+ /// Implicitly converts an to a .
+ ///
+ /// The email address to convert.
+ /// The string representation of the email address.
+ /// Thrown when the argument is .
+ public static implicit operator string(EmailAddress emailAddress)
+ {
+ ArgumentNullException.ThrowIfNull(emailAddress);
+
+ return emailAddress.value;
+ }
+
+ ///
+ /// Implicitly converts a to an .
+ ///
+ /// The string to convert to an email address.
+ /// An instance.
+ /// Thrown when the argument is .
+ /// Thrown when the string is not a valid email address.
+ public static implicit operator EmailAddress(string emailAddress)
+ {
+ ArgumentNullException.ThrowIfNull(emailAddress);
+
+ return Parse(emailAddress);
+ }
+
+ ///
+ /// Determines whether two instances are equal.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if the email addresses are equal; otherwise, .
+ public static bool operator ==(EmailAddress? left, EmailAddress? right)
+ {
+ return Equals(left, right);
+ }
+
+ ///
+ /// Determines whether two instances are not equal.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if the email addresses are not equal; otherwise, .
+ public static bool operator !=(EmailAddress? left, EmailAddress? right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Determines whether one is less than another.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if is less than ; otherwise, .
+ public static bool operator <(EmailAddress? left, EmailAddress? right)
+ {
+ return Comparer.Default.Compare(left, right) < 0;
+ }
+
+ ///
+ /// Determines whether one is less than or equal to another.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if is less than or equal to ; otherwise, .
+ public static bool operator <=(EmailAddress? left, EmailAddress? right)
+ {
+ return Comparer.Default.Compare(left, right) <= 0;
+ }
+
+ ///
+ /// Determines whether one is greater than another.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if is greater than ; otherwise, .
+ public static bool operator >(EmailAddress? left, EmailAddress? right)
+ {
+ return Comparer.Default.Compare(left, right) > 0;
+ }
+
+ ///
+ /// Determines whether one is greater than or equal to another.
+ ///
+ /// The first email address to compare.
+ /// The second email address to compare.
+ /// if is greater than or equal to ; otherwise, .
+ public static bool operator >=(EmailAddress? left, EmailAddress? right)
+ {
+ return Comparer.Default.Compare(left, right) >= 0;
+ }
+
+ ///
+ /// Parses a string representation of an email address.
+ ///
+ /// The string to parse.
+ /// An instance.
+ /// Thrown when the argument is .
+ /// Thrown when the string is not a valid email address.
+ public static EmailAddress Parse(string s)
+ {
+ ArgumentNullException.ThrowIfNull(s);
+
+ if (!TryParse(s, out var result))
+ {
+ throw new FormatException($"'{s}' is not a valid email address.");
+ }
+
+ return result;
+ }
+
+ ///
+ /// Parses a string representation of an email address using the specified format provider.
+ ///
+ /// The string to parse.
+ /// The format provider (not used in this implementation).
+ /// An instance.
+ /// Thrown when the argument is .
+ /// Thrown when the string is not a valid email address.
+ static EmailAddress IParsable.Parse(string s, IFormatProvider? provider)
+ {
+ ArgumentNullException.ThrowIfNull(s);
+
+ return Parse(s);
+ }
+
+ ///
+ /// Tries to parse a string representation of an email address.
+ ///
+ /// The string to parse.
+ /// When this method returns, contains the parsed if the parsing succeeded, or if it failed.
+ /// if the parsing succeeded; otherwise, .
+ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)][NotNullWhen(true)] out EmailAddress? result)
+ {
+ var emailAddress = TryParse(s);
+
+ if (emailAddress is null)
+ {
+ result = null;
+ return false;
+ }
+
+ result = new EmailAddress(emailAddress);
+ return true;
+ }
+
+ ///
+ /// Tries to parse a string representation of an email address using the specified format provider.
+ ///
+ /// The string to parse.
+ /// The format provider (not used in this implementation).
+ /// When this method returns, contains the parsed if the parsing succeeded, or if it failed.
+ /// if the parsing succeeded; otherwise, .
+ static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)][NotNullWhen(true)] out EmailAddress? result)
+ {
+ return TryParse(s, out result);
+ }
+
+ ///
+ /// Determines if the specified is a valid e-mail address.
+ ///
+ /// E-mail address value to test.
+ /// if the is valid e-mail address, otherwise.
+ public static bool IsValid(string value)
+ {
+ return TryParse(value, out var _);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is not EmailAddress emailAddress)
+ {
+ return false;
+ }
+
+ return this.Equals(emailAddress);
+ }
+
+ ///
+ /// Determines whether the current is equal to another .
+ ///
+ /// The other email address to compare with.
+ /// if the email addresses are equal; otherwise, .
+ public bool Equals(EmailAddress? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (!this.value.Equals(other.value, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return this.value.GetHashCode(StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Returns the string representation of the .
+ ///
+ /// The string representation of the .
+ public override string ToString()
+ {
+ return this.value;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider)
+ {
+ return this.ToString();
+ }
+
+ ///
+ /// Compare the current instance with the one.
+ ///
+ /// Other e-mail address to compare.
+ ///
+ /// A value that indicates the relative order of the objects being compared:
+ ///
+ /// 0 if the current is equal to the .
+ /// A value less than 0 if the current is before the in alphabetic order.
+ /// A value greater than 0 if the current is after the in alphabetic order.
+ ///
+ ///
+ public int CompareTo(EmailAddress? other)
+ {
+ if (other is null)
+ {
+ return string.Compare(this.value, null, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return string.Compare(this.value, other.value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static MailboxAddress? TryParse(string? input)
+ {
+ if (input is null)
+ {
+ return null;
+ }
+
+ input = input.ToLowerInvariant();
+
+ if (!MailboxAddress.TryParse(Options, input, out var address))
+ {
+ return null;
+ }
+
+ if (address.IsInternational)
+ {
+ return null;
+ }
+
+ return address;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EmailAddresses/EmailAddresses.csproj b/src/EmailAddresses/EmailAddresses.csproj
new file mode 100644
index 0000000..22b7061
--- /dev/null
+++ b/src/EmailAddresses/EmailAddresses.csproj
@@ -0,0 +1,27 @@
+
+
+
+ true
+
+
+ Provides a strongly-typed EmailAddress value object that ensures only valid RFC 5322 compliant email addresses can be instantiated.
+ Includes parsing, validation, comparison, and formatting features with case-insensitive handling for consistent equality checks.
+
+ email;emailaddress;valueobject;ddd;rfc5322;parsing;validation;dotnet;posinformatique
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EmailAddresses/Icon.png b/src/EmailAddresses/Icon.png
new file mode 100644
index 0000000..ed1fab2
Binary files /dev/null and b/src/EmailAddresses/Icon.png differ
diff --git a/src/EmailAddresses/MailKit/.editorconfig b/src/EmailAddresses/MailKit/.editorconfig
new file mode 100644
index 0000000..c9b6d64
--- /dev/null
+++ b/src/EmailAddresses/MailKit/.editorconfig
@@ -0,0 +1,11 @@
+[*.cs]
+
+# For MailKit source code, disable warnings.
+dotnet_analyzer_diagnostic.severity = none
+dotnet_diagnostic.CS1574.severity = none
+dotnet_diagnostic.CS1734.severity = none
+dotnet_diagnostic.CS8600.severity = none
+dotnet_diagnostic.CS8604.severity = none
+dotnet_diagnostic.CS8618.severity = none
+dotnet_diagnostic.CS8625.severity = none
+dotnet_diagnostic.IDE0005.severity = none
diff --git a/src/EmailAddresses/MailKit/AddressParserFlags.cs b/src/EmailAddresses/MailKit/AddressParserFlags.cs
new file mode 100644
index 0000000..c851a57
--- /dev/null
+++ b/src/EmailAddresses/MailKit/AddressParserFlags.cs
@@ -0,0 +1,42 @@
+//
+// AddressParserFlags.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+
+namespace MimeKit {
+ [Flags]
+ internal enum AddressParserFlags
+ {
+ AllowMailboxAddress = 1 << 0,
+ AllowGroupAddress = 1 << 1,
+ ThrowOnError = 1 << 2,
+ Internal = 1 << 3,
+
+ TryParse = AllowMailboxAddress | AllowGroupAddress,
+ InternalTryParse = TryParse | Internal,
+ Parse = TryParse | ThrowOnError
+ }
+}
diff --git a/src/EmailAddresses/MailKit/DomainList.cs b/src/EmailAddresses/MailKit/DomainList.cs
new file mode 100644
index 0000000..e42420f
--- /dev/null
+++ b/src/EmailAddresses/MailKit/DomainList.cs
@@ -0,0 +1,167 @@
+//
+// DomainList.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Collections;
+using System.Globalization;
+using System.Collections.Generic;
+
+using MimeKit.Utils;
+
+namespace MimeKit {
+ ///
+ /// A domain list.
+ ///
+ ///
+ /// Represents a list of domains, such as those that an email was routed through.
+ ///
+ internal class DomainList : IEnumerable
+ {
+ readonly static byte[] DomainSentinels = new [] { (byte) ',', (byte) ':' };
+ IList domains;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new based on the domains provided.
+ ///
+ /// A domain list.
+ ///
+ /// is .
+ ///
+ public DomainList (IEnumerable domains)
+ {
+ if (domains is null)
+ throw new ArgumentNullException (nameof (domains));
+
+ this.domains = new List (domains);
+ }
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new .
+ ///
+ public DomainList ()
+ {
+ domains = Array.Empty ();
+ }
+
+ #region IEnumerable implementation
+
+ ///
+ /// Get an enumerator for the list of domains.
+ ///
+ ///
+ /// Gets an enumerator for the list of domains.
+ ///
+ /// The enumerator.
+ public IEnumerator GetEnumerator ()
+ {
+ return domains.GetEnumerator ();
+ }
+
+ #endregion
+
+ #region IEnumerable implementation
+
+ ///
+ /// Get an enumerator for the list of domains.
+ ///
+ ///
+ /// Gets an enumerator for the list of domains.
+ ///
+ /// The enumerator.
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return domains.GetEnumerator ();
+ }
+
+ #endregion
+
+ ///
+ /// Try to parse a list of domains.
+ ///
+ ///
+ /// Attempts to parse a from the text buffer starting at the
+ /// specified index. The index will only be updated if a was
+ /// successfully parsed.
+ ///
+ /// if a was successfully parsed;
+ /// otherwise, .
+ /// The buffer to parse.
+ /// The index to start parsing.
+ /// An index of the end of the input.
+ /// A flag indicating whether an
+ /// exception should be thrown on error.
+ /// The parsed DomainList.
+ internal static bool TryParse (byte[] buffer, ref int index, int endIndex, bool throwOnError, out DomainList route)
+ {
+ var domains = new List ();
+ int startIndex = index;
+
+ route = null;
+
+ do {
+ // skip over the '@'
+ index++;
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete domain-list at offset: {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (!ParseUtils.TryParseDomain (buffer, ref index, endIndex, DomainSentinels, throwOnError, out var domain))
+ return false;
+
+ domains.Add (domain);
+
+ // Note: obs-domain-list allows for null domains between commas
+ do {
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex || buffer[index] != (byte) ',')
+ break;
+
+ index++;
+ } while (true);
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, throwOnError))
+ return false;
+ } while (index < buffer.Length && buffer[index] == (byte) '@');
+
+ route = new DomainList (domains);
+
+ return true;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Encodings/Base64Decoder.cs b/src/EmailAddresses/MailKit/Encodings/Base64Decoder.cs
new file mode 100644
index 0000000..c97c487
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Encodings/Base64Decoder.cs
@@ -0,0 +1,184 @@
+//
+// Base64Decoder.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+
+#if NET6_0_OR_GREATER
+using System.Buffers.Text;
+using System.Runtime.Intrinsics.X86;
+using System.Runtime.Intrinsics.Arm;
+#endif
+
+namespace MimeKit.Encodings {
+ ///
+ /// Incrementally decodes content encoded with the base64 encoding.
+ ///
+ ///
+ /// Base64 is an encoding often used in MIME to encode binary content such
+ /// as images and other types of multimedia to ensure that the data remains
+ /// intact when sent via 7bit transports such as SMTP.
+ ///
+ internal class Base64Decoder : IMimeDecoder
+ {
+ internal static ReadOnlySpan base64_rank => new byte[256] {
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255, 0,255,255,
+ 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255,
+ 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+ };
+
+ int previous;
+ uint saved;
+ byte bytes;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new base64 decoder.
+ ///
+ public Base64Decoder ()
+ {
+#if NET9_0_OR_GREATER
+ EnableHardwareAcceleration = Ssse3.IsSupported || AdvSimd.Arm64.IsSupported;
+#elif NET6_0_OR_GREATER
+ EnableHardwareAcceleration = Ssse3.IsSupported || (AdvSimd.Arm64.IsSupported && BitConverter.IsLittleEndian);
+#endif
+ }
+
+#if NET6_0_OR_GREATER
+ ///
+ /// Get or set whether the should use hardware acceleration when available.
+ ///
+ ///
+ /// Gets or sets whether the should use hardware acceleration when available.
+ ///
+ /// if hardware acceleration should be enabled; otherwise, .
+ public bool EnableHardwareAcceleration {
+ get; set;
+ }
+#endif
+
+ ///
+ /// Estimate the length of the output.
+ ///
+ ///
+ /// Estimates the number of bytes needed to decode the specified number of input bytes.
+ ///
+ /// The estimated output length.
+ /// The input length.
+ public int EstimateOutputLength (int inputLength)
+ {
+ // decoding base64 converts 4 bytes of input into 3 bytes of output
+ return ((inputLength / 4) * 3) + 3;
+ }
+
+#if NET6_0_OR_GREATER
+ [SkipLocalsInit]
+#endif
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ unsafe int Decode (ref byte table, byte* input, byte* inend, byte* output)
+ {
+ byte* outptr = output;
+ byte* inptr = input;
+
+ // decode every quartet into a triplet
+ while (inptr < inend) {
+ byte c = *inptr++;
+ byte rank = Unsafe.Add (ref table, c);
+
+ if (rank != 0xFF) {
+ previous = (previous << 8) | c;
+ saved = (saved << 6) | rank;
+ bytes++;
+
+ if (bytes == 4) {
+ if ((previous & 0xFF0000) != ((byte) '=') << 16) {
+ *outptr++ = (byte) ((saved >> 16) & 0xFF);
+ if ((previous & 0xFF00) != ((byte) '=') << 8) {
+ *outptr++ = (byte) ((saved >> 8) & 0xFF);
+ if ((previous & 0xFF) != (byte) '=')
+ *outptr++ = (byte) (saved & 0xFF);
+ }
+ }
+ saved = 0;
+ bytes = 0;
+ }
+ }
+ }
+
+ return (int) (outptr - output);
+ }
+
+ ///
+ /// Decode the specified input into the output buffer.
+ ///
+ ///
+ /// Decodes the specified input into the output buffer.
+ /// The output buffer should be large enough to hold all the
+ /// decoded input. For estimating the size needed for the output buffer,
+ /// see .
+ ///
+ /// The number of bytes written to the output buffer.
+ /// A pointer to the beginning of the input buffer.
+ /// The length of the input buffer.
+ /// A pointer to the beginning of the output buffer.
+ public unsafe int Decode (byte* input, int length, byte* output)
+ {
+ ref byte table = ref MemoryMarshal.GetReference (base64_rank);
+
+ return Decode (ref table, input, input + length, output);
+ }
+
+ ///
+ /// Reset the decoder.
+ ///
+ ///
+ /// Resets the state of the decoder.
+ ///
+ public void Reset ()
+ {
+ previous = 0;
+ saved = 0;
+ bytes = 0;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Encodings/IMimeDecoder.cs b/src/EmailAddresses/MailKit/Encodings/IMimeDecoder.cs
new file mode 100644
index 0000000..5f7574f
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Encodings/IMimeDecoder.cs
@@ -0,0 +1,69 @@
+//
+// IMimeDecoder.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+namespace MimeKit.Encodings {
+ ///
+ /// An interface for incrementally decoding content.
+ ///
+ ///
+ /// An interface for incrementally decoding content.
+ ///
+ internal interface IMimeDecoder
+ {
+ ///
+ /// Estimate the length of the output.
+ ///
+ ///
+ /// Estimates the number of bytes needed to decode the specified number of input bytes.
+ ///
+ /// The estimated output length.
+ /// The input length.
+ int EstimateOutputLength (int inputLength);
+
+ ///
+ /// Decode the specified input into the output buffer.
+ ///
+ ///
+ /// Decodes the specified input into the output buffer.
+ /// The output buffer should be large enough to hold all the
+ /// decoded input. For estimating the size needed for the output buffer,
+ /// see .
+ ///
+ /// The number of bytes written to the output buffer.
+ /// A pointer to the beginning of the input buffer.
+ /// The length of the input buffer.
+ /// A pointer to the beginning of the output buffer.
+ unsafe int Decode (byte* input, int length, byte* output);
+
+ ///
+ /// Reset the decoder.
+ ///
+ ///
+ /// Resets the state of the decoder.
+ ///
+ void Reset ();
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Encodings/IPunycode.cs b/src/EmailAddresses/MailKit/Encodings/IPunycode.cs
new file mode 100644
index 0000000..6fb88d1
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Encodings/IPunycode.cs
@@ -0,0 +1,46 @@
+//
+// IPunycode.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+namespace MimeKit.Encodings {
+ ///
+ /// An interface for encoding and decoding international domain names.
+ ///
+ ///
+ /// An interface for encoding and decoding international domain names.
+ ///
+ internal interface IPunycode
+ {
+ ///
+ /// Decode a domain name, converting it to a string of Unicode characters.
+ ///
+ ///
+ /// Decodes a domain name, converting it to Unicode, according to the rules defined by the IDNA standard.
+ ///
+ /// The ASCII-encoded domain name.
+ /// The Unicode domain name.
+ string Decode (string domain);
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Encodings/Punycode.cs b/src/EmailAddresses/MailKit/Encodings/Punycode.cs
new file mode 100644
index 0000000..97bc11d
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Encodings/Punycode.cs
@@ -0,0 +1,69 @@
+//
+// Punycode.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Globalization;
+
+namespace MimeKit.Encodings {
+ ///
+ /// A class for encoding and decoding international domain names.
+ ///
+ ///
+ /// A class for encoding and decoding international domain names.
+ ///
+ internal class Punycode : IPunycode
+ {
+ readonly IdnMapping idn;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new instance of .
+ ///
+ public Punycode ()
+ {
+ idn = new IdnMapping ();
+ }
+
+ ///
+ /// Decode a domain name, converting it to a string of Unicode characters.
+ ///
+ ///
+ /// Decodes a domain name, converting it to Unicode, according to the rules defined by the IDNA standard.
+ ///
+ /// The ASCII-encoded domain name.
+ /// The Unicode domain name.
+ public string Decode (string ascii)
+ {
+ try {
+ return idn.GetUnicode (ascii);
+ } catch {
+ return ascii;
+ }
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Encodings/QuotedPrintableDecoder.cs b/src/EmailAddresses/MailKit/Encodings/QuotedPrintableDecoder.cs
new file mode 100644
index 0000000..384b56d
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Encodings/QuotedPrintableDecoder.cs
@@ -0,0 +1,183 @@
+//
+// QuotedPrintableDecoder.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+
+using MimeKit.Utils;
+
+namespace MimeKit.Encodings {
+ ///
+ /// Incrementally decodes content encoded with the quoted-printable encoding.
+ ///
+ ///
+ /// Quoted-Printable is an encoding often used in MIME to textual content outside
+ /// the ASCII range in order to ensure that the text remains intact when sent
+ /// via 7bit transports such as SMTP.
+ ///
+ internal class QuotedPrintableDecoder : IMimeDecoder
+ {
+ enum QpDecoderState : byte {
+ PassThrough,
+ EqualSign,
+ SoftBreak,
+ DecodeByte
+ }
+
+ readonly bool rfc2047;
+ QpDecoderState state;
+ byte saved;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new quoted-printable decoder.
+ ///
+ /// if this decoder will be used to decode rfc2047 encoded-word tokens; otherwise, .
+ public QuotedPrintableDecoder (bool rfc2047)
+ {
+ this.rfc2047 = rfc2047;
+ }
+
+ ///
+ /// Estimate the length of the output.
+ ///
+ ///
+ /// Estimates the number of bytes needed to decode the specified number of input bytes.
+ ///
+ /// The estimated output length.
+ /// The input length.
+ public int EstimateOutputLength (int inputLength)
+ {
+ switch (state) {
+ case QpDecoderState.PassThrough: return inputLength;
+ case QpDecoderState.EqualSign: return inputLength + 1; // add an extra byte in case the '=' character is not the start of a valid hex sequence
+ default: return inputLength + 2; // add an extra 2 bytes in case the =X sequence is not the start of a valid hex sequence
+ }
+ }
+
+ ///
+ /// Decode the specified input into the output buffer.
+ ///
+ ///
+ /// Decodes the specified input into the output buffer.
+ /// The output buffer should be large enough to hold all the
+ /// decoded input. For estimating the size needed for the output buffer,
+ /// see .
+ ///
+ /// The number of bytes written to the output buffer.
+ /// A pointer to the beginning of the input buffer.
+ /// The length of the input buffer.
+ /// A pointer to the beginning of the output buffer.
+ public unsafe int Decode (byte* input, int length, byte* output)
+ {
+ byte* inend = input + length;
+ byte* outptr = output;
+ byte* inptr = input;
+ byte c;
+
+ while (inptr < inend) {
+ switch (state) {
+ case QpDecoderState.PassThrough:
+ while (inptr < inend) {
+ c = *inptr++;
+
+ if (c == '=') {
+ state = QpDecoderState.EqualSign;
+ break;
+ } else if (rfc2047 && c == '_') {
+ *outptr++ = (byte) ' ';
+ } else {
+ *outptr++ = c;
+ }
+ }
+ break;
+ case QpDecoderState.EqualSign:
+ c = *inptr++;
+
+ if (c.IsXDigit ()) {
+ state = QpDecoderState.DecodeByte;
+ saved = c;
+ } else if (c == '=') {
+ // invalid encoded sequence - pass it through undecoded
+ *outptr++ = (byte) '=';
+ } else if (c == '\r') {
+ state = QpDecoderState.SoftBreak;
+ } else if (c == '\n') {
+ state = QpDecoderState.PassThrough;
+ } else {
+ // invalid encoded sequence - pass it through undecoded
+ state = QpDecoderState.PassThrough;
+ *outptr++ = (byte) '=';
+ *outptr++ = c;
+ }
+ break;
+ case QpDecoderState.SoftBreak:
+ state = QpDecoderState.PassThrough;
+ c = *inptr++;
+
+ if (c != '\n') {
+ // invalid encoded sequence - pass it through undecoded
+ *outptr++ = (byte) '=';
+ *outptr++ = (byte) '\r';
+ *outptr++ = c;
+ }
+ break;
+ case QpDecoderState.DecodeByte:
+ c = *inptr++;
+ if (c.IsXDigit ()) {
+ saved = saved.ToXDigit ();
+ c = c.ToXDigit ();
+
+ *outptr++ = (byte) ((saved << 4) | c);
+ } else {
+ // invalid encoded sequence - pass it through undecoded
+ *outptr++ = (byte) '=';
+ *outptr++ = saved;
+ *outptr++ = c;
+ }
+
+ state = QpDecoderState.PassThrough;
+ break;
+ }
+ }
+
+ return (int) (outptr - output);
+ }
+
+ ///
+ /// Reset the decoder.
+ ///
+ ///
+ /// Resets the state of the decoder.
+ ///
+ public void Reset ()
+ {
+ state = QpDecoderState.PassThrough;
+ saved = 0;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/GroupAddress.cs b/src/EmailAddresses/MailKit/GroupAddress.cs
new file mode 100644
index 0000000..b90d74d
--- /dev/null
+++ b/src/EmailAddresses/MailKit/GroupAddress.cs
@@ -0,0 +1,97 @@
+//
+// GroupAddress.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Linq;
+using System.Text;
+using System.Globalization;
+using System.Collections.Generic;
+
+using MimeKit.Utils;
+
+namespace MimeKit {
+ ///
+ /// An address group, as specified by rfc0822.
+ ///
+ ///
+ /// Group addresses are rarely used anymore. Typically, if you see a group address,
+ /// it will be of the form: "undisclosed-recipients: ;".
+ ///
+ internal class GroupAddress : InternetAddress
+ {
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new with the specified name and list of addresses. The
+ /// specified text encoding is used when encoding the name according to the rules of rfc2047.
+ ///
+ /// The character encoding to be used for encoding the name.
+ /// The name of the group.
+ /// A list of addresses.
+ ///
+ /// is .
+ ///
+ public GroupAddress (Encoding encoding, string name, IEnumerable addresses) : base (encoding, name)
+ {
+ Members = new InternetAddressList (addresses);
+ }
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new with the specified name. The specified
+ /// text encoding is used when encoding the name according to the rules of rfc2047.
+ ///
+ /// The character encoding to be used for encoding the name.
+ /// The name of the group.
+ ///
+ /// is .
+ ///
+ public GroupAddress (Encoding encoding, string name) : base (encoding, name)
+ {
+ Members = new InternetAddressList ();
+ }
+
+ ///
+ /// Get the members of the group.
+ ///
+ ///
+ /// Represents the member addresses of the group. If the group address properly conforms
+ /// to the internet standards, every group member should be of the
+ /// variety. When handling group addresses constructed by third-party software, it is possible
+ /// for groups to contain members of the variety.
+ /// When constructing new messages, it is recommended that address groups not contain
+ /// anything other than members in order to comply with internet
+ /// standards.
+ ///
+ /// The list of members.
+ public InternetAddressList Members {
+ get; private set;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/InternetAddress.cs b/src/EmailAddresses/MailKit/InternetAddress.cs
new file mode 100644
index 0000000..432194c
--- /dev/null
+++ b/src/EmailAddresses/MailKit/InternetAddress.cs
@@ -0,0 +1,703 @@
+//
+// InternetAddress.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Globalization;
+using System.ComponentModel;
+
+using MimeKit.Utils;
+
+namespace MimeKit {
+ ///
+ /// An abstract internet address, as specified by rfc0822.
+ ///
+ ///
+ /// An can be any type of address defined by the
+ /// original Internet Message specification.
+ /// There are effectively two (2) types of addresses: mailboxes and groups.
+ /// Mailbox addresses are what are most commonly known as email addresses and are
+ /// represented by the class.
+ /// Group addresses are themselves lists of addresses and are represented by the
+ /// class. While rare, it is still important to handle these
+ /// types of addresses. They typically only contain mailbox addresses, but may also
+ /// contain other group addresses.
+ ///
+ internal abstract class InternetAddress
+ {
+ const string AtomSpecials = "()<>@,;:\\\".[]";
+ Encoding encoding;
+ string name;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Initializes the and properties of the internet address.
+ ///
+ /// The character encoding to be used for encoding the name.
+ /// The name of the mailbox or group.
+ ///
+ /// is .
+ ///
+ protected InternetAddress (Encoding encoding, string name)
+ {
+ if (encoding is null)
+ throw new ArgumentNullException (nameof (encoding));
+
+ Encoding = encoding;
+ Name = name;
+ }
+
+ ///
+ /// Get or set the character encoding to use when encoding the name of the address.
+ ///
+ ///
+ /// The character encoding is used to convert the property, if it is set,
+ /// to a stream of bytes when encoding the internet address for transport.
+ ///
+ /// The character encoding.
+ ///
+ /// is .
+ ///
+ public Encoding Encoding {
+ get { return encoding; }
+ set {
+ if (value is null)
+ throw new ArgumentNullException (nameof (value));
+
+ if (value == encoding)
+ return;
+
+ encoding = value;
+ }
+ }
+
+ ///
+ /// Get or set the display name of the address.
+ ///
+ ///
+ /// A name is optional and is typically set to the name of the person
+ /// or group that own the internet address.
+ /// For example, the property of the following would be "John Smith".
+ /// John Smith <j.smith@example.com>
+ /// Likewise, the property of the following would be "undisclosed-recipients".
+ /// undisclosed-recipients: Alice <alice@wonderland.com>, Bob <bob@the-builder.com>;
+ ///
+ /// The name of the address.
+ public string Name {
+ get { return name; }
+ set {
+ if (value == name)
+ return;
+
+ name = value;
+ }
+ }
+
+ internal static bool TryParseLocalPart (byte[] text, ref int index, int endIndex, RfcComplianceMode compliance, bool skipTrailingCfws, bool throwOnError, out string localpart)
+ {
+ using var token = new ValueStringBuilder (128);
+ int startIndex = index;
+
+ localpart = null;
+
+ do {
+ bool escapedAt = false;
+ int start = index;
+
+ if (text[index] == (byte) '"') {
+ if (!ParseUtils.SkipQuoted (text, ref index, endIndex, throwOnError))
+ return false;
+ } else if (text[index].IsAtom ()) {
+ if (!ParseUtils.SkipAtom (text, ref index, endIndex))
+ return false;
+
+ if (compliance == RfcComplianceMode.Looser) {
+ // Allow local-parts that include escaped '@' symbols.
+ // See https://github.com/jstedfast/MimeKit/issues/1043 for details.
+ while (index + 1 < endIndex && text[index] == (byte) '\\' && text[index + 1] == (byte) '@') {
+ // track that we've encountered an escaped @ symbol
+ escapedAt = true;
+
+ // skip over the '\\' and '@' characters
+ index += 2;
+
+ if (!ParseUtils.SkipAtom (text, ref index, endIndex))
+ break;
+ }
+ }
+ } else {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid local-part at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ string word;
+
+ try {
+ word = CharsetUtils.UTF8.GetString (text, start, index - start);
+ } catch (DecoderFallbackException ex) {
+ if (compliance == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException ("Internationalized local-part tokens may only contain UTF-8 characters.", start, start, ex);
+
+ return false;
+ }
+
+ word = CharsetUtils.Latin1.GetString (text, start, index - start);
+ }
+
+ if (escapedAt)
+ word = word.Replace ("\\@", "%40");
+
+ token.Append (word);
+
+ int cfws = index;
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex || text[index] != (byte) '.') {
+ if (!skipTrailingCfws)
+ index = cfws;
+ break;
+ }
+
+ do {
+ token.Append ('.');
+ index++;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete local-part at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+ } while (compliance == RfcComplianceMode.Looser && text[index] == (byte) '.');
+
+ if (compliance == RfcComplianceMode.Looser && (index >= endIndex || text[index] == (byte) '@'))
+ break;
+ } while (true);
+
+ localpart = token.ToString ();
+
+ return true;
+ }
+
+ static ReadOnlySpan CommaGreaterThanOrSemiColon => ",>;"u8;
+
+ internal static bool TryParseAddrspec (byte[] text, ref int index, int endIndex, ReadOnlySpan sentinels, RfcComplianceMode compliance, bool throwOnError, out string addrspec, out int at)
+ {
+ int startIndex = index;
+
+ addrspec = null;
+ at = -1;
+
+ if (!TryParseLocalPart (text, ref index, endIndex, compliance, true, throwOnError, out var localpart))
+ return false;
+
+ if (index >= endIndex || ParseUtils.IsSentinel (text[index], sentinels)) {
+ addrspec = localpart;
+ return true;
+ }
+
+ if (text[index] != (byte) '@') {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid addr-spec token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ index++;
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete addr-spec token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete addr-spec token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (!ParseUtils.TryParseDomain (text, ref index, endIndex, sentinels, throwOnError, out var domain))
+ return false;
+
+ if (ParseUtils.IsIdnEncoded (domain))
+ domain = MailboxAddress.IdnMapping.Decode (domain);
+
+ addrspec = localpart + "@" + domain;
+ at = localpart.Length;
+
+ return true;
+ }
+
+ internal static bool TryParseMailbox (ParserOptions options, byte[] text, int startIndex, ref int index, int endIndex, string name, int codepage, bool throwOnError, out InternetAddress address)
+ {
+ var encoding = CharsetUtils.GetEncodingOrDefault (codepage, Encoding.UTF8);
+ DomainList route = null;
+
+ address = null;
+
+ // skip over the '<'
+ index++;
+
+ // Note: check for excessive angle brackets like the example described in section 7.1.2 of rfc7103...
+ if (index < endIndex && text[index] == (byte) '<') {
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Excessive angle brackets at offset {0}", index), startIndex, index);
+
+ return false;
+ }
+
+ do {
+ index++;
+ } while (index < endIndex && text[index] == '<');
+ }
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete mailbox at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (text[index] == (byte) '@') {
+ // Note: we always pass 'false' as the throwOnError argument here so that we can throw a more informative exception on error
+ if (!DomainList.TryParse (text, ref index, endIndex, false, out route)) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid route in mailbox at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (index >= endIndex || text[index] != (byte) ':') {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete route in mailbox at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ // skip over ':'
+ index++;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete mailbox at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+ }
+
+ // Note: The only syntactically correct sentinel token here is the '>', but alas... to deal with the first example
+ // in section 7.1.5 of rfc7103, we need to at least handle ',' as a sentinel and might as well handle ';' as well
+ // in case the mailbox is within a group address.
+ //
+ // Example:
+ if (!TryParseAddrspec (text, ref index, endIndex, CommaGreaterThanOrSemiColon, options.AddressParserComplianceMode, throwOnError, out string addrspec, out int at))
+ return false;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex || text[index] != (byte) '>') {
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Unexpected end of mailbox at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+ } else {
+ // skip over the '>'
+ index++;
+
+ // Note: check for excessive angle brackets like the example described in section 7.1.2 of rfc7103...
+ if (index < endIndex && text[index] == (byte) '>') {
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Excessive angle brackets at offset {0}", index), startIndex, index);
+
+ return false;
+ }
+
+ do {
+ index++;
+ } while (index < endIndex && text[index] == '>');
+ }
+ }
+
+ if (route != null)
+ address = new MailboxAddress (encoding, name, route, addrspec, at);
+ else
+ address = new MailboxAddress (encoding, name, addrspec, at);
+
+ return true;
+ }
+
+ static bool TryParseGroup (AddressParserFlags flags, ParserOptions options, byte[] text, int startIndex, ref int index, int endIndex, int groupDepth, string name, int codepage, out InternetAddress address)
+ {
+ var encoding = CharsetUtils.GetEncodingOrDefault (codepage, Encoding.UTF8);
+ bool throwOnError = (flags & AddressParserFlags.ThrowOnError) != 0;
+
+ // skip over the ':'
+ index++;
+
+ while (index < endIndex && (text[index] == ':' || text[index].IsBlank ()))
+ index++;
+
+ if (InternetAddressList.TryParse (flags | AddressParserFlags.AllowMailboxAddress, options, text, ref index, endIndex, true, groupDepth, out var members))
+ address = new GroupAddress (encoding, name, members);
+ else
+ address = new GroupAddress (encoding, name);
+
+ if (index >= endIndex || text[index] != (byte) ';') {
+ if (throwOnError && options.AddressParserComplianceMode == RfcComplianceMode.Strict)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Expected to find ';' at offset {0}", index), startIndex, index);
+
+ while (index < endIndex && text[index] != (byte) ';')
+ index++;
+ } else {
+ index++;
+ }
+
+ return true;
+ }
+
+ internal static bool TryParse (AddressParserFlags flags, ParserOptions options, byte[] text, ref int index, int endIndex, int groupDepth, out InternetAddress address)
+ {
+ bool throwOnError = (flags & AddressParserFlags.ThrowOnError) != 0;
+
+ address = null;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index == endIndex) {
+ if (throwOnError)
+ throw new ParseException ("No address found.", index, index);
+
+ return false;
+ }
+
+ // keep track of the start & length of the phrase
+ bool trimLeadingQuote = false;
+ int startIndex = index;
+ int length = 0;
+ int words = 0;
+
+ while (index < endIndex) {
+ bool quoted = text[index] == (byte) '"';
+
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (!ParseUtils.SkipWord (text, ref index, endIndex, throwOnError))
+ break;
+ } else if (text[index] == (byte) '"') {
+ int qstringIndex = index;
+
+ if (!ParseUtils.SkipQuoted (text, ref index, endIndex, false)) {
+ index = qstringIndex + 1;
+
+ ParseUtils.SkipWhiteSpace (text, ref index, endIndex);
+
+ if (!ParseUtils.SkipPhraseAtom (text, ref index, endIndex)) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete quoted-string token at offset {0}", qstringIndex), qstringIndex, endIndex);
+
+ break;
+ }
+
+ if (startIndex == qstringIndex)
+ trimLeadingQuote = true;
+ }
+ } else {
+ if (!ParseUtils.SkipPhraseAtom (text, ref index, endIndex))
+ break;
+ }
+
+ length = index - startIndex;
+
+ do {
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ // Note: some clients don't quote dots in the name
+ if (index >= endIndex || text[index] != (byte) '.')
+ break;
+
+ index++;
+
+ length = index - startIndex;
+ } while (true);
+
+ words++;
+
+ // Note: some clients don't quote commas in the name
+ if (options.AllowUnquotedCommasInAddresses && index < endIndex && text[index] == ',' && !quoted) {
+ index++;
+
+ length = index - startIndex;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+ }
+ }
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ // specials = "(" / ")" / "<" / ">" / "@" ; Must be in quoted-
+ // / "," / ";" / ":" / "\" / <"> ; string, to use
+ // / "." / "[" / "]" ; within a word.
+
+ if (index >= endIndex || text[index] == (byte) ',' || text[index] == (byte) '>' || text[index] == ';') {
+ // we've completely gobbled up an addr-spec w/o a domain
+ byte sentinel = index < endIndex ? text[index] : (byte) ',';
+ string name;
+
+ if ((flags & AddressParserFlags.AllowMailboxAddress) == 0) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Addr-spec token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (!options.AllowAddressesWithoutDomain) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete addr-spec token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ // rewind back to the beginning of the local-part
+ index = startIndex;
+
+ if (!TryParseLocalPart (text, ref index, endIndex, options.AddressParserComplianceMode, false, throwOnError, out var addrspec))
+ return false;
+
+ ParseUtils.SkipWhiteSpace (text, ref index, endIndex);
+
+ if (index < endIndex && text[index] == '(') {
+ int comment = index + 1;
+
+ // Note: this can't fail because it has already been skipped in TryParseLocalPart() above.
+ ParseUtils.SkipComment (text, ref index, endIndex);
+
+ name = Rfc2047.DecodePhrase (options, text, comment, (index - 1) - comment).Trim ();
+
+ ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError);
+ } else {
+ name = string.Empty;
+ }
+
+ if (index < endIndex && text[index] == (byte) '>') {
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Unexpected '>' token at offset {0}", index), startIndex, index);
+
+ return false;
+ }
+
+ index++;
+ }
+
+ if (index < endIndex && text[index] != sentinel) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Unexpected '{0}' token at offset {1}", (char) text[index], index), startIndex, index);
+
+ return false;
+ }
+
+ address = new MailboxAddress (Encoding.UTF8, name, addrspec, -1);
+
+ return true;
+ }
+
+ if (text[index] == (byte) ':') {
+ // rfc2822 group address
+ int nameIndex = startIndex;
+ int codepage = -1;
+ string name;
+
+ if ((flags & AddressParserFlags.AllowGroupAddress) == 0) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Group address token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (groupDepth >= options.MaxAddressGroupDepth) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Exceeded maximum rfc822 group depth at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (trimLeadingQuote) {
+ nameIndex++;
+ length--;
+ }
+
+ if (length > 0) {
+ name = Rfc2047.DecodePhrase (options, text, nameIndex, length, out codepage);
+ } else {
+ name = string.Empty;
+ }
+
+ if (codepage == -1)
+ codepage = 65001;
+
+ return TryParseGroup (flags, options, text, startIndex, ref index, endIndex, groupDepth + 1, MimeUtils.Unquote (name, true), codepage, out address);
+ }
+
+ if ((flags & AddressParserFlags.AllowMailboxAddress) == 0) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Mailbox address token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (text[index] == (byte) '@') {
+ // we're either in the middle of an addr-spec token or we completely gobbled up an addr-spec w/o a domain
+ string name;
+
+ // rewind back to the beginning of the local-part
+ index = startIndex;
+
+ if (!TryParseAddrspec (text, ref index, endIndex, CommaGreaterThanOrSemiColon, options.AddressParserComplianceMode, throwOnError, out var addrspec, out int at))
+ return false;
+
+ ParseUtils.SkipWhiteSpace (text, ref index, endIndex);
+
+ if (index < endIndex && text[index] == '(') {
+ int comment = index;
+
+ if (!ParseUtils.SkipComment (text, ref index, endIndex)) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete comment token at offset {0}", comment), comment, index);
+
+ return false;
+ }
+
+ comment++;
+
+ name = Rfc2047.DecodePhrase (options, text, comment, (index - 1) - comment).Trim ();
+ } else {
+ name = string.Empty;
+ }
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex) {
+ address = new MailboxAddress (Encoding.UTF8, name, addrspec, at);
+ return true;
+ }
+
+ if (text[index] == (byte) '<') {
+ // We have an address like "user@example.com "; i.e. the name is an unquoted string with an '@'.
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Unexpected '<' token at offset {0}", index), startIndex, index);
+
+ return false;
+ }
+
+ int nameEndIndex = index;
+ while (nameEndIndex > startIndex && text[nameEndIndex - 1].IsWhitespace ())
+ nameEndIndex--;
+
+ length = nameEndIndex - startIndex;
+
+ // fall through to the rfc822 angle-addr token case...
+ } else {
+ // Note: since there was no '<', there should not be a '>'... but we handle it anyway in order to
+ // deal with the second Unbalanced Angle Brackets example in section 7.1.3: second@example.org>
+ if (text[index] == (byte) '>') {
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Unexpected '>' token at offset {0}", index), startIndex, index);
+
+ return false;
+ }
+
+ index++;
+ }
+
+ address = new MailboxAddress (Encoding.UTF8, name, addrspec, at);
+
+ return true;
+ }
+ }
+
+ if (text[index] == (byte) '<') {
+ // rfc2822 angle-addr token
+ int nameIndex = startIndex;
+ int codepage = -1;
+ string name;
+
+ if (trimLeadingQuote) {
+ nameIndex++;
+ length--;
+ }
+
+ if (length > 0) {
+ var unquoted = MimeUtils.Unquote (text, nameIndex, length, true);
+
+ name = Rfc2047.DecodePhrase (options, unquoted, 0, unquoted.Length, out codepage);
+ } else {
+ name = string.Empty;
+ }
+
+ if (codepage == -1)
+ codepage = 65001;
+
+ return TryParseMailbox (options, text, startIndex, ref index, endIndex, name, codepage, throwOnError, out address);
+ }
+
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid address token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/InternetAddressList.cs b/src/EmailAddresses/MailKit/InternetAddressList.cs
new file mode 100644
index 0000000..17c3f00
--- /dev/null
+++ b/src/EmailAddresses/MailKit/InternetAddressList.cs
@@ -0,0 +1,170 @@
+//
+// InternetAddressList.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+#if ENABLE_SNM
+using System.Net.Mail;
+#endif
+
+using MimeKit.Utils;
+
+namespace MimeKit {
+ ///
+ /// A list of email addresses.
+ ///
+ ///
+ /// An may contain any number of addresses of any type
+ /// defined by the original Internet Message specification.
+ /// There are effectively two (2) types of addresses: mailboxes and groups.
+ /// Mailbox addresses are what are most commonly known as email addresses and are
+ /// represented by the class.
+ /// Group addresses are themselves lists of addresses and are represented by the
+ /// class. While rare, it is still important to handle these
+ /// types of addresses. They typically only contain mailbox addresses, but may also
+ /// contain other group addresses.
+ ///
+ internal class InternetAddressList
+ {
+ readonly List list;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new containing the supplied addresses.
+ ///
+ /// An initial list of addresses.
+ ///
+ /// is .
+ ///
+ public InternetAddressList (IEnumerable addresses)
+ {
+ if (addresses is null)
+ throw new ArgumentNullException (nameof (addresses));
+
+ if (addresses is IList lst)
+ list = new List (lst.Count);
+ else
+ list = new List ();
+
+ foreach (var address in addresses) {
+ list.Add (address);
+ }
+ }
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new, empty, .
+ ///
+ public InternetAddressList ()
+ {
+ list = new List ();
+ }
+
+ internal static bool TryParse (AddressParserFlags flags, ParserOptions options, byte[] text, ref int index, int endIndex, bool isGroup, int groupDepth, out List addresses)
+ {
+ bool throwOnError = (flags & AddressParserFlags.ThrowOnError) != 0;
+ var list = new List ();
+
+ addresses = null;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index == endIndex) {
+ if (throwOnError)
+ throw new ParseException ("No addresses found.", index, index);
+
+ return false;
+ }
+
+ while (index < endIndex) {
+ if (isGroup && text[index] == (byte) ';')
+ break;
+
+ if (!InternetAddress.TryParse (flags, options, text, ref index, endIndex, groupDepth, out var address)) {
+ if ((flags & AddressParserFlags.Internal) == 0) {
+ // Note: If flags contains the ThrowOnError flag, then InternetAddress.TryParse() would have thrown.
+ return false;
+ }
+
+ // skip this address...
+ while (index < endIndex && text[index] != (byte) ',' && (!isGroup || text[index] != (byte) ';'))
+ index++;
+ } else {
+ list.Add (address);
+ }
+
+ // Note: we loop here in case there are any extraneous commas
+ bool skippedComma = false;
+
+ do {
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex)
+ break;
+
+ if (isGroup && text[index] == (byte) ';')
+ break;
+
+ if (text[index] != (byte) ',') {
+ if (skippedComma)
+ break;
+
+ if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) {
+ if (throwOnError) {
+ if (isGroup)
+ throw new ParseException ("Expected ',' between addresses or ';' to denote the end of a group of addresses.", index, index);
+ else
+ throw new ParseException ("Expected ',' between addresses.", index, index);
+ }
+
+ return false;
+ } else {
+ // start of a new address?
+ break;
+ }
+ }
+
+ skippedComma = true;
+ index++;
+ } while (true);
+ }
+
+ addresses = list;
+
+ return true;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/MailboxAddress.cs b/src/EmailAddresses/MailKit/MailboxAddress.cs
new file mode 100644
index 0000000..3477900
--- /dev/null
+++ b/src/EmailAddresses/MailKit/MailboxAddress.cs
@@ -0,0 +1,214 @@
+//
+// MailboxAddress.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Globalization;
+using System.Collections.Generic;
+
+#if ENABLE_SNM
+using System.Net.Mail;
+#endif
+
+using MimeKit.Utils;
+using MimeKit.Encodings;
+
+namespace MimeKit {
+ ///
+ /// A mailbox address, as specified by rfc822.
+ ///
+ ///
+ /// Represents a mailbox address (commonly referred to as an email address)
+ /// for a single recipient.
+ ///
+ internal class MailboxAddress : InternetAddress
+ {
+ ///
+ /// Get or set the punycode implementation that should be used for encoding and decoding mailbox addresses.
+ ///
+ ///
+ /// Gets or sets the punycode implementation that should be used for encoding and decoding mailbox addresses.
+ ///
+ /// The punycode implementation.
+ public static IPunycode IdnMapping { get; set; }
+
+ string address;
+ int at;
+
+ static MailboxAddress ()
+ {
+ IdnMapping = new Punycode ();
+ }
+
+ internal MailboxAddress (Encoding encoding, string name, IEnumerable route, string address, int at) : base (encoding, name)
+ {
+ Route = new DomainList (route);
+
+ this.address = address;
+ this.at = at;
+ }
+
+ internal MailboxAddress (Encoding encoding, string name, string address, int at) : base (encoding, name)
+ {
+ Route = new DomainList ();
+
+ this.address = address;
+ this.at = at;
+ }
+
+ ///
+ /// Get the mailbox route.
+ ///
+ ///
+ /// A route is convention that is rarely seen in modern email systems, but is supported
+ /// for compatibility with email archives.
+ ///
+ /// The mailbox route.
+ public DomainList Route {
+ get; private set;
+ }
+
+ ///
+ /// Get or set the mailbox address.
+ ///
+ ///
+ /// Represents the actual email address and is in the form of user@domain.com.
+ ///
+ /// The mailbox address.
+ ///
+ /// is .
+ ///
+ ///
+ /// is malformed.
+ ///
+ public string Address {
+ get { return address; }
+ }
+
+ ///
+ /// Get the local-part of the email address.
+ ///
+ ///
+ /// Gets the local-part of the email address, sometimes referred to as the "user" portion of an email address.
+ ///
+ /// The local-part portion of the mailbox address.
+ public string LocalPart {
+ get {
+ return at != -1 ? address.Substring (0, at) : address;
+ }
+ }
+
+ ///
+ /// Get the domain of the email address.
+ ///
+ ///
+ /// Gets the domain of the email address.
+ ///
+ /// The domain portion of the mailbox address.
+ public string Domain {
+ get {
+ return at != -1 ? address.Substring (at + 1) : string.Empty;
+ }
+ }
+
+ ///
+ /// Get whether the address is an international address.
+ ///
+ ///
+ /// International addresses are addresses that contain international
+ /// characters in either their local-parts or their domains.
+ /// For more information, see section 3.2 of
+ /// rfc6532.
+ ///
+ /// if the address is an international address; otherwise, .
+ public bool IsInternational {
+ get {
+ if (string.IsNullOrEmpty (address))
+ return false;
+
+ if (ParseUtils.IsInternational (address))
+ return true;
+
+ foreach (var domain in Route) {
+ if (ParseUtils.IsInternational (domain))
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out MailboxAddress mailbox)
+ {
+ var flags = AddressParserFlags.AllowMailboxAddress;
+
+ if (throwOnError)
+ flags |= AddressParserFlags.ThrowOnError;
+
+ if (!InternetAddress.TryParse (flags, options, text, ref index, endIndex, 0, out var address)) {
+ mailbox = null;
+ return false;
+ }
+
+ mailbox = (MailboxAddress) address;
+
+ return true;
+ }
+
+ ///
+ /// Try to parse the given text into a new instance.
+ ///
+ ///
+ /// Parses a single . If the address is not a mailbox address or
+ /// there is more than a single mailbox address, then parsing will fail.
+ ///
+ /// if the address was successfully parsed; otherwise, .
+ /// The parser options to use.
+ /// The text.
+ /// The parsed mailbox address.
+ public static bool TryParse (ParserOptions options, string text, out MailboxAddress mailbox)
+ {
+ if (!ArgumentValidator.TryValidate (options, text)) {
+ mailbox = null;
+ return false;
+ }
+
+ var buffer = Encoding.UTF8.GetBytes (text);
+ int endIndex = buffer.Length;
+ int index = 0;
+
+ if (!TryParse (options, buffer, ref index, endIndex, false, out mailbox))
+ return false;
+
+ if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) {
+ mailbox = null;
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/ParseException.cs b/src/EmailAddresses/MailKit/ParseException.cs
new file mode 100644
index 0000000..9e5e856
--- /dev/null
+++ b/src/EmailAddresses/MailKit/ParseException.cs
@@ -0,0 +1,150 @@
+//
+// ParseException.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+
+#if SERIALIZABLE
+using System.Security;
+using System.Runtime.Serialization;
+#endif
+
+namespace MimeKit {
+ ///
+ /// A Parse exception as thrown by the various Parse methods in MimeKit.
+ ///
+ ///
+ /// A can be thrown by any of the Parse() methods
+ /// in MimeKit. Each exception instance will have a
+ /// which marks the byte offset of the token that failed to parse as well
+ /// as a which marks the byte offset where the error
+ /// occurred.
+ ///
+#if SERIALIZABLE
+ [Serializable]
+#endif
+ internal class ParseException : FormatException
+ {
+#if SERIALIZABLE
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new .
+ ///
+ /// The serialization info.
+ /// The stream context.
+ ///
+ /// is .
+ ///
+ [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")]
+ protected ParseException (SerializationInfo info, StreamingContext context) : base (info, context)
+ {
+ TokenIndex = info.GetInt32 ("TokenIndex");
+ ErrorIndex = info.GetInt32 ("ErrorIndex");
+ }
+#endif
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new .
+ ///
+ /// The error message.
+ /// The byte offset of the token.
+ /// The byte offset of the error.
+ /// The inner exception.
+ public ParseException (string message, int tokenIndex, int errorIndex, Exception innerException) : base (message, innerException)
+ {
+ TokenIndex = tokenIndex;
+ ErrorIndex = errorIndex;
+ }
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// Creates a new .
+ ///
+ /// The error message.
+ /// The byte offset of the token.
+ /// The byte offset of the error.
+ public ParseException (string message, int tokenIndex, int errorIndex) : base (message)
+ {
+ TokenIndex = tokenIndex;
+ ErrorIndex = errorIndex;
+ }
+
+#if SERIALIZABLE
+ ///
+ /// When overridden in a derived class, sets the
+ /// with information about the exception.
+ ///
+ ///
+ /// Sets the
+ /// with information about the exception.
+ ///
+ /// The serialization info.
+ /// The streaming context.
+ ///
+ /// is .
+ ///
+ [SecurityCritical]
+#if NET8_0_OR_GREATER
+ [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")]
+#endif
+ public override void GetObjectData (SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData (info, context);
+
+ info.AddValue ("TokenIndex", TokenIndex);
+ info.AddValue ("ErrorIndex", ErrorIndex);
+ }
+#endif
+
+ ///
+ /// Get the byte index of the token that was malformed.
+ ///
+ ///
+ /// The token index is the byte offset at which the token started.
+ ///
+ /// The byte index of the token.
+ public int TokenIndex {
+ get; private set;
+ }
+
+ ///
+ /// Get the index of the byte that caused the error.
+ ///
+ ///
+ /// The error index is the byte offset at which the parser encountered an error.
+ ///
+ /// The index of the byte that caused error.
+ public int ErrorIndex {
+ get; private set;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/ParserOptions.cs b/src/EmailAddresses/MailKit/ParserOptions.cs
new file mode 100644
index 0000000..7bb68e7
--- /dev/null
+++ b/src/EmailAddresses/MailKit/ParserOptions.cs
@@ -0,0 +1,193 @@
+//
+// ParserOptions.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Reflection;
+using System.Collections.Generic;
+
+#if ENABLE_CRYPTO
+using MimeKit.Cryptography;
+#endif
+
+using MimeKit.Utils;
+using System.Diagnostics.CodeAnalysis;
+
+namespace MimeKit {
+ ///
+ /// Parser options as used by as well as various Parse and TryParse methods in MimeKit.
+ ///
+ ///
+ /// allows you to change and/or override default parsing options used by methods such
+ /// as and others.
+ ///
+ internal class ParserOptions
+ {
+ ///
+ /// The default parser options.
+ ///
+ ///
+ /// If a is not supplied to or other Parse and TryParse
+ /// methods throughout MimeKit, will be used.
+ ///
+ public static readonly ParserOptions Default = new ParserOptions ();
+
+ ///
+ /// Get or set the compliance mode that should be used when parsing rfc822 addresses.
+ ///
+ ///
+ /// In general, you'll probably want this value to be
+ /// (the default) as it allows maximum interoperability with existing (broken) mail clients
+ /// and other mail software such as sloppily written perl scripts (aka spambots).
+ /// Even in mode, the address parser
+ /// is fairly liberal in what it accepts. Setting it to
+ /// just makes it try harder to deal with garbage input.
+ ///
+ /// The RFC compliance mode.
+ public RfcComplianceMode AddressParserComplianceMode { get; set; }
+
+ ///
+ /// Get or set whether the rfc822 address parser should ignore unquoted commas in address names.
+ ///
+ ///
+ /// In general, you'll probably want this value to be (the default) as it allows
+ /// maximum interoperability with existing (broken) mail clients and other mail software such as
+ /// sloppily written perl scripts (aka spambots) that do not properly quote the name when it
+ /// contains a comma.
+ ///
+ /// if the address parser should ignore unquoted commas in address names; otherwise, .
+ public bool AllowUnquotedCommasInAddresses { get; set; }
+
+ ///
+ /// Get or set whether the rfc822 address parser should allow addresses without a domain.
+ ///
+ ///
+ /// In general, you'll probably want this value to be (the default) as it allows
+ /// maximum interoperability with older email messages that may contain local UNIX addresses.
+ /// This option exists in order to allow parsing of mailbox addresses that do not have an
+ /// @domain component. These types of addresses are rare and were typically only used when sending
+ /// mail to other users on the same UNIX system.
+ ///
+ /// if the address parser should allow mailbox addresses without a domain; otherwise, .
+ public bool AllowAddressesWithoutDomain { get; set; }
+
+ ///
+ /// Get or set the maximum address group depth the parser should accept.
+ ///
+ ///
+ /// This option exists in order to define the maximum recursive depth of an rfc822 group address
+ /// that the parser should accept before bailing out with the assumption that the address is maliciously
+ /// formed. If the value is set too large, then it is possible that a maliciously formed set of
+ /// recursive group addresses could cause a stack overflow.
+ ///
+ /// The maximum address group depth.
+ public int MaxAddressGroupDepth { get; set; }
+
+ ///
+ /// Get or set the maximum MIME nesting depth the parser should accept.
+ ///
+ ///
+ /// This option exists in order to define the maximum recursive depth of MIME parts that the parser
+ /// should accept before treating further nesting as a leaf-node MIME part and not recursing any further.
+ /// If the value is set too large, then it is possible that a maliciously formed set of deeply nested
+ /// multipart MIME parts could cause a stack overflow.
+ ///
+ /// The maximum MIME nesting depth.
+ public int MaxMimeDepth { get; set; }
+
+ ///
+ /// Get or set the compliance mode that should be used when parsing Content-Type and Content-Disposition parameters.
+ ///
+ ///
+ /// In general, you'll probably want this value to be
+ /// (the default) as it allows maximum interoperability with existing (broken) mail clients
+ /// and other mail software such as sloppily written perl scripts (aka spambots).
+ /// Even in mode, the parameter parser
+ /// is fairly liberal in what it accepts. Setting it to
+ /// just makes it try harder to deal with garbage input.
+ ///
+ /// The RFC compliance mode.
+ public RfcComplianceMode ParameterComplianceMode { get; set; }
+
+ ///
+ /// Get or set the compliance mode that should be used when decoding rfc2047 encoded words.
+ ///
+ ///
+ /// In general, you'll probably want this value to be
+ /// (the default) as it allows maximum interoperability with existing (broken) mail clients
+ /// and other mail software such as sloppily written perl scripts (aka spambots).
+ ///
+ /// The RFC compliance mode.
+ public RfcComplianceMode Rfc2047ComplianceMode { get; set; }
+
+ ///
+ /// Get or set a value indicating whether the Content-Length value should be
+ /// respected when parsing mbox streams.
+ ///
+ ///
+ /// For more details about why this may be useful, you can find more information
+ /// at
+ /// http://www.jwz.org/doc/content-length.html.
+ ///
+ /// if the Content-Length value should be respected;
+ /// otherwise, .
+ public bool RespectContentLength { get; set; }
+
+ ///
+ /// Get or set the charset encoding to use as a fallback for 8bit headers.
+ ///
+ ///
+ /// and
+ ///
+ /// use this charset encoding as a fallback when decoding 8bit text into unicode. The first
+ /// charset encoding attempted is UTF-8, followed by this charset encoding, before finally
+ /// falling back to iso-8859-1.
+ ///
+ /// The charset encoding.
+ public Encoding CharsetEncoding { get; set; }
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ ///
+ /// By default, new instances of enable rfc2047 work-arounds
+ /// (which are needed for maximum interoperability with mail software used in the wild)
+ /// and do not respect the Content-Length header value.
+ ///
+ public ParserOptions ()
+ {
+ AddressParserComplianceMode = RfcComplianceMode.Loose;
+ ParameterComplianceMode = RfcComplianceMode.Loose;
+ Rfc2047ComplianceMode = RfcComplianceMode.Loose;
+ CharsetEncoding = CharsetUtils.UTF8;
+ AllowUnquotedCommasInAddresses = true;
+ AllowAddressesWithoutDomain = true;
+ RespectContentLength = false;
+ MaxAddressGroupDepth = 3;
+ MaxMimeDepth = 1024;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/RfcComplianceMode.cs b/src/EmailAddresses/MailKit/RfcComplianceMode.cs
new file mode 100644
index 0000000..e1e1be0
--- /dev/null
+++ b/src/EmailAddresses/MailKit/RfcComplianceMode.cs
@@ -0,0 +1,50 @@
+//
+// RfcComplianceMode.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+namespace MimeKit {
+ ///
+ /// An RFC compliance mode.
+ ///
+ ///
+ /// An RFC compliance mode.
+ ///
+ internal enum RfcComplianceMode {
+ ///
+ /// Attempt to be even more liberal in accepting broken and/or invalid formatting.
+ ///
+ Looser = -1,
+
+ ///
+ /// Attempt to be more liberal accepting broken and/or invalid formatting.
+ ///
+ Loose,
+
+ ///
+ /// Do not attempt to be overly liberal in accepting broken and/or invalid formatting.
+ ///
+ Strict
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/ArgumentValidator.cs b/src/EmailAddresses/MailKit/Utils/ArgumentValidator.cs
new file mode 100644
index 0000000..7105c08
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/ArgumentValidator.cs
@@ -0,0 +1,43 @@
+//
+// ArgumentValidator.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+
+namespace MimeKit.Utils {
+ static class ArgumentValidator
+ {
+ public static bool TryValidate (ParserOptions options, string text)
+ {
+ if (options is null)
+ return false;
+
+ if (text is null)
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/ByteArrayBuilder.cs b/src/EmailAddresses/MailKit/Utils/ByteArrayBuilder.cs
new file mode 100644
index 0000000..946aa18
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/ByteArrayBuilder.cs
@@ -0,0 +1,76 @@
+//
+// ByteArrayBuilder.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Buffers;
+
+namespace MimeKit.Utils {
+ internal ref struct ByteArrayBuilder
+ {
+ byte[] buffer;
+ int length;
+
+ public ByteArrayBuilder (int initialCapacity)
+ {
+ buffer = ArrayPool.Shared.Rent (initialCapacity);
+ length = 0;
+ }
+
+ void EnsureCapacity (int capacity)
+ {
+ if (capacity > buffer.Length) {
+ var resized = ArrayPool.Shared.Rent (capacity);
+ Buffer.BlockCopy (buffer, 0, resized, 0, length);
+ ArrayPool.Shared.Return (buffer);
+ buffer = resized;
+ }
+ }
+
+ public void Append (byte c)
+ {
+ EnsureCapacity (length + 1);
+
+ buffer[length++] = c;
+ }
+
+ public byte[] ToArray ()
+ {
+ var array = new byte[length];
+
+ Buffer.BlockCopy (buffer, 0, array, 0, length);
+
+ return array;
+ }
+
+ public void Dispose ()
+ {
+ if (buffer != null) {
+ ArrayPool.Shared.Return (buffer);
+ buffer = null;
+ }
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/ByteExtensions.cs b/src/EmailAddresses/MailKit/Utils/ByteExtensions.cs
new file mode 100644
index 0000000..c954b59
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/ByteExtensions.cs
@@ -0,0 +1,189 @@
+//
+// ByteExtensions.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+
+namespace MimeKit.Utils {
+ [Flags]
+ enum CharType : ushort {
+ None = 0,
+ IsAscii = (1 << 0),
+ IsAtom = (1 << 1),
+ IsAttrChar = (1 << 2),
+ IsBlank = (1 << 3),
+ IsControl = (1 << 4),
+ IsDomainSafe = (1 << 5),
+ IsEncodedPhraseSafe = (1 << 6),
+ IsEncodedWordSafe = (1 << 7),
+ IsQuotedPrintableSafe = (1 << 8),
+ IsSpace = (1 << 9),
+ IsSpecial = (1 << 10),
+ IsTokenSpecial = (1 << 11),
+ IsWhitespace = (1 << 12),
+ IsXDigit = (1 << 13),
+ IsPhraseAtom = (1 << 14),
+ IsFieldText = (1 << 15),
+
+ IsAsciiAtom = IsAscii | IsAtom,
+ }
+
+ static class ByteExtensions
+ {
+ const string AtomSafe = "!#$%&'*+-/=?^_`{|}~";
+ const string AttributeSpecials = "*'%"; // attribute specials from rfc2184/rfc2231
+ const string EncodedWordSpecials = "()<>@,;:\"/[]?.=_"; // rfc2047 5.1
+ const string EncodedPhraseSafe = "!*+-/"; // rfc2047 5.3 (but w/o '=' and '_' since they need to be encoded, obviously)
+ const string Specials = "()<>[]:;@\\,.\""; // rfc5322 3.2.3
+ internal const string TokenSpecials = "()<>@,;:\\\"/[]?="; // rfc2045 5.1
+ const string Whitespace = " \t\r\n";
+
+ static readonly CharType[] table = new CharType[256];
+
+ static void RemoveFlags (string values, CharType bit)
+ {
+ for (int i = 0; i < values.Length; i++)
+ table[(byte) values[i]] &= ~bit;
+ }
+
+ static void SetFlags (string values, CharType bit)
+ {
+ for (int i = 0; i < values.Length; i++)
+ table[values[i]] |= bit;
+ }
+
+ static ByteExtensions ()
+ {
+ for (int i = 0; i < 256; i++) {
+ if (i < 127) {
+ if (i < 32)
+ table[i] |= CharType.IsControl | CharType.IsTokenSpecial;
+ if (i > 32)
+ table[i] |= CharType.IsAttrChar;
+ if (i >= 32 && i != 61) // space + all printable characters *except* '=' (61)
+ table[i] |= CharType.IsQuotedPrintableSafe | CharType.IsEncodedWordSafe;
+ if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z'))
+ table[i] |= CharType.IsEncodedPhraseSafe | CharType.IsAtom | CharType.IsPhraseAtom;
+ if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F'))
+ table[i] |= CharType.IsXDigit;
+ if (i >= 33 && i != 58) // all printable characters *except* ':'
+ table[i] |= CharType.IsFieldText;
+ if ((i >= 33 && i <= 90) || i >= 94) // all printable characters *except* '[', '\\', and ']'
+ table[i] |= CharType.IsDomainSafe;
+
+ table[i] |= CharType.IsAscii;
+ } else {
+ if (i == 127)
+ table[i] |= CharType.IsAscii;
+ else
+ table[i] |= CharType.IsAtom | CharType.IsPhraseAtom;
+
+ table[i] |= CharType.IsControl | CharType.IsTokenSpecial;
+ }
+ }
+
+ table['\t'] |= CharType.IsQuotedPrintableSafe | CharType.IsBlank;
+ table[' '] |= CharType.IsSpace | CharType.IsBlank;
+
+ SetFlags (Whitespace, CharType.IsWhitespace);
+ SetFlags (AtomSafe, CharType.IsAtom | CharType.IsPhraseAtom);
+ SetFlags (TokenSpecials, CharType.IsTokenSpecial);
+ SetFlags (Specials, CharType.IsSpecial);
+ RemoveFlags (Specials, CharType.IsAtom | CharType.IsPhraseAtom);
+ RemoveFlags (EncodedWordSpecials, CharType.IsEncodedWordSafe);
+ RemoveFlags (AttributeSpecials + TokenSpecials, CharType.IsAttrChar);
+ SetFlags (EncodedPhraseSafe, CharType.IsEncodedPhraseSafe);
+
+ // Note: Allow '[' and ']' in the display-name of a mailbox address
+ table['['] |= CharType.IsPhraseAtom;
+ table[']'] |= CharType.IsPhraseAtom;
+
+ // Note: Allow ')' in the display-name of a mailbox address for rfc7103 unbalanced parenthesis
+ table[')'] |= CharType.IsPhraseAtom;
+ }
+
+ //public static bool IsAscii (this byte c)
+ //{
+ // return (table[c] & CharType.IsAscii) != 0;
+ //}
+
+ public static bool IsAsciiAtom (this byte c)
+ {
+ return (table[c] & CharType.IsAsciiAtom) == CharType.IsAsciiAtom;
+ }
+
+ public static bool IsPhraseAtom (this byte c)
+ {
+ return (table[c] & CharType.IsPhraseAtom) != 0;
+ }
+
+ public static bool IsAtom (this byte c)
+ {
+ return (table[c] & CharType.IsAtom) != 0;
+ }
+
+ public static bool IsBlank (this byte c)
+ {
+ return (table[c] & CharType.IsBlank) != 0;
+ }
+
+ public static bool IsCtrl (this byte c)
+ {
+ return (table[c] & CharType.IsControl) != 0;
+ }
+
+ public static bool IsDomain (this byte c)
+ {
+ return (table[c] & CharType.IsDomainSafe) != 0;
+ }
+
+ public static bool IsType (this byte c, CharType type)
+ {
+ return (table[c] & type) != 0;
+ }
+
+ public static bool IsWhitespace (this byte c)
+ {
+ return (table[c] & CharType.IsWhitespace) != 0;
+ }
+
+ public static bool IsXDigit (this byte c)
+ {
+ return (table[c] & CharType.IsXDigit) != 0;
+ }
+
+ public static byte ToXDigit (this byte c)
+ {
+ if (c >= 0x41) {
+ if (c >= 0x61)
+ return (byte) (c - (0x61 - 0x0a));
+
+ return (byte) (c - (0x41 - 0x0A));
+ }
+
+ return (byte) (c - 0x30);
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/CharsetUtils.cs b/src/EmailAddresses/MailKit/Utils/CharsetUtils.cs
new file mode 100644
index 0000000..f85c2db
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/CharsetUtils.cs
@@ -0,0 +1,495 @@
+//
+// CharsetUtils.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Globalization;
+using System.Collections.Generic;
+
+namespace MimeKit.Utils {
+ internal static class CharsetUtils
+ {
+ static readonly char[] DashUnderscore = new[] { '-', '_' };
+ static readonly Dictionary aliases;
+ public static readonly Encoding Latin1;
+ public static readonly Encoding UTF8;
+
+ static CharsetUtils ()
+ {
+ try {
+ Latin1 = Encoding.GetEncoding (28591, new EncoderExceptionFallback (), new DecoderExceptionFallback ());
+ } catch (NotSupportedException) {
+ // Note: Some ASP.NET environments running on .NET Framework >= v4.6.1 do not include the full spectrum of
+ // text encodings, so iso-8859-1 will not always be available unless the program includes a reference to
+ // the System.Text.Encoding.CodePages nuget package *and* it has been registered via the
+ // System.Text.Encoding.RegisterProvider method. In cases where this hasn't been done, we need some fallback
+ // logic that tries to handle this at least somewhat.
+
+ // Use ASCII as a fallback.
+ Latin1 = Encoding.ASCII;
+ }
+
+ // Note: Encoding.UTF8.GetString() replaces invalid bytes with a unicode '?' character,
+ // so we use our own UTF8 instance when using GetString() if we do not want it to do that.
+ UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ());
+
+ aliases = new Dictionary (MimeUtils.OrdinalIgnoreCase);
+
+ AddAliases (aliases, 65001, -1, "utf-8", "utf8", "unicode");
+
+ // ANSI_X3.4-1968 is used on some systems and should be
+ // treated the same as US-ASCII.
+ AddAliases (aliases, 20127, -1, "ansi_x3.4-1968");
+
+ // ANSI_X3.110-1983 is another odd-ball charset that appears
+ // every once in a while and seems closest to ISO-8859-1.
+ AddAliases (aliases, 28591, -1, "ansi_x3.110-1983", "latin1");
+
+ // Macintosh aliases
+ AddAliases (aliases, 10000, -1, "macintosh");
+ AddAliases (aliases, 10079, -1, "x-mac-icelandic");
+
+ // Korean charsets (aliases for euc-kr)
+ // 'upgrade' ks_c_5601-1987 to euc-kr since it is a superset
+ AddAliases (aliases, 51949, -1,
+ "ks_c_5601-1987",
+ "ksc-5601-1987",
+ "ksc-5601_1987",
+ "ksc-5601",
+ "5601",
+ "ks_c_5861-1992",
+ "ksc-5861-1992",
+ "ksc-5861_1992",
+ "euckr-0",
+ "euc-kr");
+
+ // Chinese charsets (aliases for big5)
+ AddAliases (aliases, 950, -1, "big5", "big5-0", "big5-hkscs", "big5.eten-0", "big5hkscs-0");
+
+ // Chinese charsets (aliases for gb2312)
+ int gb2312 = AddAliases (aliases, 936, -1, "gb2312", "gb-2312", "gb2312-0", "gb2312-80", "gb2312.1980-0");
+
+ // Chinese charsets (euc-cn and gbk not supported on Mono)
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=844082 seems to suggest falling back to gb2312.
+ AddAliases (aliases, 51936, gb2312, "euc-cn", "gbk-0", "x-gbk", "gbk");
+
+ // Chinese charsets (hz-gb-2312 not suported on Mono)
+ AddAliases (aliases, 52936, gb2312, "hz-gb-2312", "hz-gb2312");
+
+ // Chinese charsets (aliases for gb18030)
+ AddAliases (aliases, 54936, -1, "gb18030-0", "gb18030");
+
+ // Japanese charsets (aliases for euc-jp)
+ AddAliases (aliases, 51932, -1, "eucjp-0", "euc-jp", "ujis-0", "ujis");
+
+ // Japanese charsets (aliases for Shift_JIS)
+ AddAliases (aliases, 932, -1, "shift_jis", "jisx0208.1983-0", "jisx0212.1990-0", "pck");
+
+ // Note from http://msdn.microsoft.com/en-us/library/system.text.encoding.getencodings.aspx
+ // Encodings 50220 and 50222 are both associated with the name "iso-2022-jp", but they
+ // are not identical. Encoding 50220 converts half-width Katakana characters to
+ // full-width Katakana characters, whereas encoding 50222 uses a shift-in/shift-out
+ // sequence to encode half-width Katakana characters. The display name for encoding
+ // 50222 is "Japanese (JIS-Allow 1 byte Kana - SO/SI)" to distinguish it from encoding
+ // 50220, which has the display name "Japanese (JIS)".
+ //
+ // If your application requests the encoding name "iso-2022-jp", the .NET Framework
+ // returns encoding 50220. However, the encoding that is appropriate for your application
+ // will depend on the preferred treatment of the half-width Katakana characters.
+ AddAliases (aliases, 50220, -1, "iso-2022-jp");
+ }
+
+ static bool ProbeCharset (int codepage)
+ {
+ try {
+ Encoding.GetEncoding (codepage);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ static int AddAliases (Dictionary dict, int codepage, int fallback, params string[] names)
+ {
+ int value = ProbeCharset (codepage) ? codepage : fallback;
+
+ for (int i = 0; i < names.Length; i++)
+ dict.Add (names[i], value);
+
+ return value;
+ }
+
+ static bool TryParseInt32 (string text, int startIndex, int count, out int value)
+ {
+#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
+ return int.TryParse (text.AsSpan (startIndex, count), NumberStyles.None, CultureInfo.InvariantCulture, out value);
+#else
+ return int.TryParse (text.Substring (startIndex, count), NumberStyles.None, CultureInfo.InvariantCulture, out value);
+#endif
+ }
+
+ static int ParseIsoCodePage (string charset, int startIndex)
+ {
+ if ((charset.Length - startIndex) < 5)
+ return -1;
+
+ int dash = charset.IndexOfAny (DashUnderscore, startIndex);
+ if (dash == -1)
+ dash = charset.Length;
+
+ if (!TryParseInt32 (charset, startIndex, dash - startIndex, out int iso))
+ return -1;
+
+ if (iso == 10646)
+ return 1201;
+
+ if (dash + 2 > charset.Length)
+ return -1;
+
+ int codepageIndex = dash + 1;
+ int codepage;
+
+ switch (iso) {
+ case 8859:
+ if (!TryParseInt32 (charset, codepageIndex, charset.Length - codepageIndex, out codepage))
+ return -1;
+
+ if (codepage <= 0 || (codepage > 9 && codepage < 13) || codepage > 15)
+ return -1;
+
+ codepage += 28590;
+ break;
+ case 2022:
+ int n = charset.Length - codepageIndex;
+
+ if (n == 2 && string.Compare (charset, codepageIndex, "jp", 0, 2, StringComparison.OrdinalIgnoreCase) == 0)
+ codepage = 50220;
+ else if (n == 2 && string.Compare (charset, codepageIndex, "kr", 0, 2, StringComparison.OrdinalIgnoreCase) == 0)
+ codepage = 50225;
+ else
+ codepage = -1;
+ break;
+ default:
+ return -1;
+ }
+
+ return codepage;
+ }
+
+ internal static int ParseCodePage (string charset)
+ {
+ int codepage;
+ int i;
+
+ if (charset.StartsWith ("windows", StringComparison.OrdinalIgnoreCase)) {
+ i = 7;
+
+ if (i == charset.Length)
+ return -1;
+
+ if (charset[i] == '-' || charset[i] == '_') {
+ if (i + 1 == charset.Length)
+ return -1;
+
+ i++;
+ }
+
+ if (i + 2 < charset.Length && charset[i] == 'c' && charset[i + 1] == 'p')
+ i += 2;
+
+ if (TryParseInt32 (charset, i, charset.Length - i, out codepage))
+ return codepage;
+ } else if (charset.StartsWith ("ibm", StringComparison.OrdinalIgnoreCase)) {
+ i = 3;
+
+ if (i == charset.Length)
+ return -1;
+
+ if (charset[i] == '-' || charset[i] == '_')
+ i++;
+
+ if (TryParseInt32 (charset, i, charset.Length - i, out codepage))
+ return codepage;
+ } else if (charset.StartsWith ("iso", StringComparison.OrdinalIgnoreCase)) {
+ i = 3;
+
+ if (i == charset.Length)
+ return -1;
+
+ if (charset[i] == '-' || charset[i] == '_')
+ i++;
+
+ if ((codepage = ParseIsoCodePage (charset, i)) != -1)
+ return codepage;
+ } else if (charset.StartsWith ("cp", StringComparison.OrdinalIgnoreCase)) {
+ i = 2;
+
+ if (i == charset.Length)
+ return -1;
+
+ if (charset[i] == '-' || charset[i] == '_')
+ i++;
+
+ if (TryParseInt32 (charset, i, charset.Length - i, out codepage))
+ return codepage;
+ } else if (charset.Equals ("latin1", StringComparison.OrdinalIgnoreCase)) {
+ return 28591;
+ }
+
+ return -1;
+ }
+
+ public static int GetCodePage (string charset)
+ {
+ if (charset is null)
+ throw new ArgumentNullException (nameof (charset));
+
+ int codepage;
+
+ lock (aliases) {
+ if (!aliases.TryGetValue (charset, out codepage)) {
+ Encoding encoding;
+
+ codepage = ParseCodePage (charset);
+
+ if (codepage == -1) {
+ try {
+ encoding = Encoding.GetEncoding (charset);
+ codepage = encoding.CodePage;
+
+ if (!aliases.ContainsKey (encoding.WebName))
+ aliases[encoding.WebName] = codepage;
+ } catch {
+ codepage = -1;
+ }
+ } else {
+ try {
+ encoding = Encoding.GetEncoding (codepage);
+ if (!aliases.ContainsKey (encoding.WebName))
+ aliases[encoding.WebName] = codepage;
+ } catch {
+ codepage = -1;
+ }
+ }
+
+ if (!aliases.ContainsKey (charset))
+ aliases[charset] = codepage;
+ }
+ }
+
+ return codepage;
+ }
+
+ public static Encoding GetEncoding (int codepage, string fallback)
+ {
+ if (fallback is null)
+ throw new ArgumentNullException (nameof (fallback));
+
+ var encoderFallback = new EncoderReplacementFallback (fallback);
+ var decoderFallback = new DecoderReplacementFallback (fallback);
+
+ return Encoding.GetEncoding (codepage, encoderFallback, decoderFallback);
+ }
+
+ public static Encoding GetEncoding (int codepage)
+ {
+ return Encoding.GetEncoding (codepage);
+ }
+
+ public static Encoding GetEncodingOrDefault (int codepage, Encoding defaultEncoding)
+ {
+ try {
+ return Encoding.GetEncoding (codepage);
+ } catch {
+ return defaultEncoding;
+ }
+ }
+
+ class InvalidByteCountFallback : DecoderFallback
+ {
+ class InvalidByteCountFallbackBuffer : DecoderFallbackBuffer
+ {
+ readonly InvalidByteCountFallback fallback;
+ const string replacement = "?";
+ bool invalid;
+ int current;
+
+ public InvalidByteCountFallbackBuffer (InvalidByteCountFallback fallback)
+ {
+ this.fallback = fallback;
+ }
+
+ public override bool Fallback (byte[] bytesUnknown, int index)
+ {
+ fallback.InvalidByteCount++;
+ invalid = true;
+ current = 0;
+ return true;
+ }
+
+ public override char GetNextChar ()
+ {
+ if (!invalid)
+ return '\0';
+
+ if (current == replacement.Length)
+ return '\0';
+
+ return replacement[current++];
+ }
+
+ public override bool MovePrevious ()
+ {
+ if (current == 0)
+ return false;
+
+ current--;
+
+ return true;
+ }
+
+ public override int Remaining {
+ get { return invalid ? replacement.Length - current : 0; }
+ }
+
+ public override void Reset ()
+ {
+ invalid = false;
+ current = 0;
+
+ base.Reset ();
+ }
+ }
+
+ public InvalidByteCountFallback ()
+ {
+ Reset ();
+ }
+
+ public int InvalidByteCount {
+ get; private set;
+ }
+
+ public void Reset ()
+ {
+ InvalidByteCount = 0;
+ }
+
+ public override DecoderFallbackBuffer CreateFallbackBuffer ()
+ {
+ return new InvalidByteCountFallbackBuffer (this);
+ }
+
+ public override int MaxCharCount {
+ get { return 1; }
+ }
+ }
+
+ internal static char[] ConvertToUnicode (ParserOptions options, byte[] input, int startIndex, int length, out int charCount)
+ {
+ var invalid = new InvalidByteCountFallback ();
+ var userCharset = options.CharsetEncoding;
+ int min = int.MaxValue;
+ int bestCharCount = 0;
+ Encoding encoding;
+ Decoder decoder;
+ int[] codepages;
+ int best = -1;
+ int count;
+
+ // Note: 65001 is UTF-8 and 28591 is iso-8859-1
+ if (userCharset != null && userCharset.CodePage != 65001 && userCharset.CodePage != 28591) {
+ codepages = new [] { 65001, userCharset.CodePage, 28591 };
+ } else {
+ codepages = new [] { 65001, 28591 };
+ }
+
+ for (int i = 0; i < codepages.Length; i++) {
+ encoding = Encoding.GetEncoding (codepages[i], new EncoderReplacementFallback ("?"), invalid);
+ decoder = encoding.GetDecoder ();
+
+ count = decoder.GetCharCount (input, startIndex, length, true);
+ if (invalid.InvalidByteCount < min) {
+ min = invalid.InvalidByteCount;
+ bestCharCount = count;
+ best = codepages[i];
+
+ if (min == 0)
+ break;
+ }
+
+ invalid.Reset ();
+ }
+
+ encoding = GetEncoding (best, "?");
+ decoder = encoding.GetDecoder ();
+
+ var output = new char[bestCharCount];
+
+ try {
+ charCount = decoder.GetChars (input, startIndex, length, output, 0, true);
+ } catch {
+ charCount = 0;
+ }
+
+ return output;
+ }
+
+ internal static char[] ConvertToUnicode (Encoding encoding, byte[] input, int startIndex, int length, out int charCount)
+ {
+ var decoder = encoding.GetDecoder ();
+ int count = decoder.GetCharCount (input, startIndex, length, true);
+ var output = new char[count];
+
+ try {
+ charCount = decoder.GetChars (input, startIndex, length, output, 0, true);
+ } catch {
+ charCount = 0;
+ }
+
+ return output;
+ }
+
+ internal static char[] ConvertToUnicode (ParserOptions options, int codepage, byte[] input, int startIndex, int length, out int charCount)
+ {
+ Encoding encoding = null;
+
+ if (codepage != -1) {
+ try {
+ encoding = GetEncoding (codepage);
+ } catch (NotSupportedException) {
+ } catch (ArgumentException) {
+ }
+ }
+
+ if (encoding is null)
+ return ConvertToUnicode (options, input, startIndex, length, out charCount);
+
+ return ConvertToUnicode (encoding, input, startIndex, length, out charCount);
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/MimeUtils.cs b/src/EmailAddresses/MailKit/Utils/MimeUtils.cs
new file mode 100644
index 0000000..9957261
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/MimeUtils.cs
@@ -0,0 +1,171 @@
+//
+// MimeUtils.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.Net.NetworkInformation;
+using System.Security.Cryptography;
+
+namespace MimeKit.Utils {
+ ///
+ /// MIME utility methods.
+ ///
+ ///
+ /// Various utility methods that don't belong anywhere else.
+ ///
+ internal static class MimeUtils
+ {
+ static readonly char[] UnquoteChars = new[] { '\r', '\n', '\t', '\\', '"' };
+
+ ///
+ /// A string comparer that performs a case-insensitive ordinal string comparison.
+ ///
+ ///
+ /// A string comparer that performs a case-insensitive ordinal string comparison.
+ ///
+ public static readonly IEqualityComparer OrdinalIgnoreCase;
+
+ static MimeUtils ()
+ {
+#if NETFRAMEWORK || NETSTANDARD2_0
+ OrdinalIgnoreCase = new OptimizedOrdinalIgnoreCaseComparer ();
+#else
+ OrdinalIgnoreCase = StringComparer.OrdinalIgnoreCase;
+#endif
+ }
+
+#if !NET6_0_OR_GREATER
+ internal static void GetRandomBytes (byte[] buffer)
+ {
+#if NETSTANDARD2_1 || NET5_0_OR_GREATER
+ RandomNumberGenerator.Fill (buffer);
+#else
+ using (var random = RandomNumberGenerator.Create ())
+ random.GetBytes (buffer);
+#endif
+ }
+#endif
+
+ ///
+ /// Unquote the specified text.
+ ///
+ ///
+ /// Unquotes the specified text, removing any escaped backslashes within.
+ ///
+ /// The unquoted text.
+ /// The text to unquote.
+ /// if tab characters should be converted to a space; otherwise, .
+ ///
+ /// is .
+ ///
+ public static string Unquote (string text, bool convertTabsToSpaces = false)
+ {
+ if (text is null)
+ throw new ArgumentNullException (nameof (text));
+
+ int index = text.IndexOfAny (UnquoteChars);
+
+ if (index == -1)
+ return text;
+
+ var builder = new ValueStringBuilder (text.Length);
+ bool escaped = false;
+ bool quoted = false;
+
+ for (int i = 0; i < text.Length; i++) {
+ switch (text[i]) {
+ case '\r':
+ case '\n':
+ escaped = false;
+ break;
+ case '\t':
+ builder.Append (convertTabsToSpaces ? ' ' : '\t');
+ escaped = false;
+ break;
+ case '\\':
+ if (escaped)
+ builder.Append ('\\');
+ escaped = !escaped;
+ break;
+ case '"':
+ if (escaped) {
+ builder.Append ('"');
+ escaped = false;
+ } else {
+ quoted = !quoted;
+ }
+ break;
+ default:
+ builder.Append (text[i]);
+ escaped = false;
+ break;
+ }
+ }
+
+ return builder.ToString ();
+ }
+
+ internal static byte[] Unquote (byte[] text, int startIndex, int length, bool convertTabsToSpaces = false)
+ {
+ using var builder = new ByteArrayBuilder (length);
+ bool escaped = false;
+ bool quoted = false;
+
+ for (int i = startIndex; i < startIndex + length; i++) {
+ switch ((char) text[i]) {
+ case '\r':
+ case '\n':
+ escaped = false;
+ break;
+ case '\t':
+ builder.Append ((byte) (convertTabsToSpaces ? ' ' : '\t'));
+ escaped = false;
+ break;
+ case '\\':
+ if (escaped)
+ builder.Append ((byte) '\\');
+ escaped = !escaped;
+ break;
+ case '"':
+ if (escaped) {
+ builder.Append ((byte) '"');
+ escaped = false;
+ } else {
+ quoted = !quoted;
+ }
+ break;
+ default:
+ builder.Append (text[i]);
+ escaped = false;
+ break;
+ }
+ }
+
+ return builder.ToArray ();
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/ParseUtils.cs b/src/EmailAddresses/MailKit/Utils/ParseUtils.cs
new file mode 100644
index 0000000..04a05df
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/ParseUtils.cs
@@ -0,0 +1,304 @@
+//
+// ParseUtils.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Globalization;
+
+namespace MimeKit.Utils {
+ static class ParseUtils
+ {
+ public static bool SkipWhiteSpace (byte[] text, ref int index, int endIndex)
+ {
+ int startIndex = index;
+
+ while (index < endIndex && text[index].IsWhitespace ())
+ index++;
+
+ return index > startIndex;
+ }
+
+ public static bool SkipComment (byte[] text, ref int index, int endIndex)
+ {
+ bool escaped = false;
+ int depth = 1;
+
+ index++;
+
+ while (index < endIndex && depth > 0) {
+ if (text[index] == (byte) '\\') {
+ escaped = !escaped;
+ } else if (!escaped) {
+ if (text[index] == (byte) '(')
+ depth++;
+ else if (text[index] == (byte) ')')
+ depth--;
+ } else {
+ escaped = false;
+ }
+
+ index++;
+ }
+
+ return depth == 0;
+ }
+
+ public static bool SkipCommentsAndWhiteSpace (byte[] text, ref int index, int endIndex, bool throwOnError)
+ {
+ SkipWhiteSpace (text, ref index, endIndex);
+
+ while (index < endIndex && text[index] == (byte) '(') {
+ int startIndex = index;
+
+ if (!SkipComment (text, ref index, endIndex)) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete comment token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ SkipWhiteSpace (text, ref index, endIndex);
+ }
+
+ return true;
+ }
+
+ public static bool SkipQuoted (byte[] text, ref int index, int endIndex, bool throwOnError)
+ {
+ int startIndex = index;
+ bool escaped = false;
+
+ // skip over leading '"'
+ index++;
+
+ while (index < endIndex) {
+ if (text[index] == (byte) '\\') {
+ escaped = !escaped;
+ } else if (!escaped) {
+ if (text[index] == (byte) '"')
+ break;
+ } else {
+ escaped = false;
+ }
+
+ index++;
+ }
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete quoted-string token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ // skip over the closing '"'
+ index++;
+
+ return true;
+ }
+
+ public static bool SkipAtom (byte[] text, ref int index, int endIndex)
+ {
+ int start = index;
+
+ while (index < endIndex && text[index].IsAtom ())
+ index++;
+
+ return index > start;
+ }
+
+ // Note: a "phrase atom" is a more lenient atom (e.g. mailbox display-name phrase atom)
+ public static bool SkipPhraseAtom (byte[] text, ref int index, int endIndex)
+ {
+ int start = index;
+
+ while (index < endIndex && text[index].IsPhraseAtom ())
+ index++;
+
+ return index > start;
+ }
+
+ public static bool SkipWord (byte[] text, ref int index, int endIndex, bool throwOnError)
+ {
+ if (text[index] == (byte) '"')
+ return SkipQuoted (text, ref index, endIndex, throwOnError);
+
+ if (text[index].IsAtom ())
+ return SkipAtom (text, ref index, endIndex);
+
+ return false;
+ }
+
+ public static bool IsSentinel (byte c, ReadOnlySpan sentinels)
+ {
+ for (int i = 0; i < sentinels.Length; i++) {
+ if (c == sentinels[i])
+ return true;
+ }
+
+ return false;
+ }
+
+ static bool TryParseDotAtom (byte[] text, ref int index, int endIndex, ReadOnlySpan sentinels, bool throwOnError, string tokenType, out string dotatom)
+ {
+ using var token = new ValueStringBuilder (128);
+ int startIndex = index;
+ int comment;
+
+ dotatom = null;
+
+ do {
+ if (!text[index].IsAtom ()) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid {0} token at offset {1}", tokenType, startIndex), startIndex, index);
+
+ return false;
+ }
+
+ int start = index;
+ while (index < endIndex && text[index].IsAtom ())
+ index++;
+
+ try {
+ token.Append (CharsetUtils.UTF8.GetString (text, start, index - start));
+ } catch (DecoderFallbackException ex) {
+ if (throwOnError)
+ throw new ParseException ("Internationalized domains may only contain UTF-8 characters.", start, start, ex);
+
+ return false;
+ }
+
+ comment = index;
+ if (!SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ if (index >= endIndex || text[index] != (byte) '.') {
+ index = comment;
+ break;
+ }
+
+ // skip over the '.'
+ index++;
+
+ if (!SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError))
+ return false;
+
+ // allow domains to end with a '.', but strip it off
+ if (index >= endIndex || IsSentinel (text[index], sentinels))
+ break;
+
+ token.Append ('.');
+ } while (true);
+
+ dotatom = token.ToString ();
+
+ return true;
+ }
+
+ static bool TryParseDomainLiteral (byte[] text, ref int index, int endIndex, bool throwOnError, out string domain)
+ {
+ using var token = new ValueStringBuilder (128);
+ int startIndex = index++;
+
+ domain = null;
+
+ token.Append ('[');
+
+ SkipWhiteSpace (text, ref index, endIndex);
+
+ do {
+ while (index < endIndex && text[index].IsDomain ()) {
+ token.Append ((char) text[index]);
+ index++;
+ }
+
+ SkipWhiteSpace (text, ref index, endIndex);
+
+ if (index >= endIndex) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Incomplete domain literal token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+
+ if (text[index] == (byte) ']')
+ break;
+
+ if (!text[index].IsDomain ()) {
+ if (throwOnError)
+ throw new ParseException (string.Format (CultureInfo.InvariantCulture, "Invalid domain literal token at offset {0}", startIndex), startIndex, index);
+
+ return false;
+ }
+ } while (true);
+
+ token.Append (']');
+ index++;
+
+ domain = token.ToString ();
+
+ return true;
+ }
+
+ public static bool TryParseDomain (byte[] text, ref int index, int endIndex, ReadOnlySpan sentinels, bool throwOnError, out string domain)
+ {
+ if (text[index] == (byte) '[')
+ return TryParseDomainLiteral (text, ref index, endIndex, throwOnError, out domain);
+
+ return TryParseDotAtom (text, ref index, endIndex, sentinels, throwOnError, "domain", out domain);
+ }
+
+ public static bool IsInternational (string value, int startIndex, int count)
+ {
+ int endIndex = startIndex + count;
+
+ for (int i = startIndex; i < endIndex; i++) {
+ if (value[i] > 127)
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool IsInternational (string value, int startIndex)
+ {
+ return IsInternational (value, startIndex, value.Length - startIndex);
+ }
+
+ public static bool IsInternational (string value)
+ {
+ return IsInternational (value, 0, value.Length);
+ }
+
+ public static bool IsIdnEncoded (string value)
+ {
+ if (value.StartsWith ("xn--", StringComparison.Ordinal))
+ return true;
+
+ return value.IndexOf (".xn--", StringComparison.Ordinal) != -1;
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/Rfc2047.cs b/src/EmailAddresses/MailKit/Utils/Rfc2047.cs
new file mode 100644
index 0000000..cdbc2fc
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/Rfc2047.cs
@@ -0,0 +1,536 @@
+//
+// Rfc2047.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2025 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System;
+using System.Text;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+using MimeKit.Encodings;
+
+namespace MimeKit.Utils {
+ ///
+ /// Utility methods for encoding and decoding rfc2047 encoded-word tokens.
+ ///
+ ///
+ /// Utility methods for encoding and decoding rfc2047 encoded-word tokens.
+ ///
+ internal static class Rfc2047
+ {
+ readonly struct Token
+ {
+ const char SevenBit = '7';
+ const char EightBit = '8';
+
+ public readonly int StartIndex;
+ public readonly int Length;
+ public readonly char Encoding;
+
+#if REDUCE_TOKEN_SIZE
+ // Note: .NET codepages range from ~34-65001 which all fit within a ushort, but we also need '-1'
+ // to denote "unknown" and 0 to denote an unencoded ascii word.
+ readonly ushort codePage;
+
+ public int CodePage { get { return codePage == ushort.MaxValue ? -1 : codePage; } }
+#else
+ public readonly int CodePage;
+#endif
+
+ public bool Is8bit { get { return CodePage == 0 && Encoding == EightBit; } }
+
+ public bool IsEncoded { get { return CodePage != 0; } }
+
+ public Token (string charset, string culture, char encoding, int startIndex, int length)
+ {
+#if REDUCE_TOKEN_SIZE
+ int cp = CharsetUtils.GetCodePage (charset);
+ codePage = cp < 0 ? ushort.MaxValue : (ushort) cp;
+#else
+ CodePage = CharsetUtils.GetCodePage (charset);
+#endif
+ StartIndex = startIndex;
+ Length = length;
+ Encoding = encoding;
+ }
+
+ public Token (int startIndex, int length, bool is8bit = false)
+ {
+ Encoding = is8bit ? EightBit : SevenBit;
+ StartIndex = startIndex;
+ Length = length;
+#if REDUCE_TOKEN_SIZE
+ codePage = 0;
+#else
+ CodePage = 0;
+#endif
+ }
+ }
+
+ struct CodePageCount
+ {
+ public readonly int CodePage;
+ public int Count;
+
+ public CodePageCount (int codepage)
+ {
+ CodePage = codepage;
+ Count = 1;
+ }
+ }
+
+ interface ITokenWriter : IDisposable
+ {
+ bool IgnoreWhitespaceBetweenEncodedWords { get; }
+ void Write (ref ValueStringBuilder output, ref Token token);
+ void Flush (ref ValueStringBuilder output);
+ }
+
+ class TokenDecoder : ITokenWriter
+ {
+ readonly ParserOptions options;
+ readonly byte[] input, scratch;
+ CodePageCount[] codepages;
+ QuotedPrintableDecoder qp;
+ Base64Decoder base64;
+ IMimeDecoder decoder;
+ int codepageIndex;
+ int scratchLength;
+ char encoding;
+ int codepage;
+
+ public TokenDecoder (ParserOptions options, byte[] input, byte[] scratch)
+ {
+ this.options = options;
+ this.scratch = scratch;
+ this.input = input;
+
+ codepages = ArrayPool.Shared.Rent (16);
+ base64 = null;
+ qp = null;
+
+ decoder = null;
+ codepageIndex = 0;
+ scratchLength = 0;
+ encoding = '\0';
+ codepage = 0;
+ }
+
+ public bool IgnoreWhitespaceBetweenEncodedWords {
+ get { return true; }
+ }
+
+ void AddCodePage (int codepage)
+ {
+ for (int i = 0; i < codepageIndex; i++) {
+ if (codepages[i].CodePage == codepage) {
+ codepages[i].Count++;
+ return;
+ }
+ }
+
+ if (codepageIndex == codepages.Length) {
+ var resized = ArrayPool.Shared.Rent (codepages.Length * 2);
+ codepages.AsSpan ().CopyTo (resized);
+ ArrayPool.Shared.Return (codepages);
+ codepages = resized;
+ }
+
+ codepages[codepageIndex++] = new CodePageCount (codepage);
+ }
+
+ public unsafe void Write (ref ValueStringBuilder output, ref Token token)
+ {
+ if (decoder != null && (token.CodePage != codepage || char.ToUpperInvariant (token.Encoding) != encoding)) {
+ // We've reached the end of a series of encoded-word tokens using identical charsets & encodings.
+ //
+ // In order to work around broken mailers, we need to combine the raw decoded content of runs of
+ // identically encoded-word tokens before converting to unicode strings.
+ Flush (ref output);
+ }
+
+ if (token.IsEncoded) {
+ // Save encoded-word state so that we can treat consecutive encoded-word payloads with identical
+ // charsets & encodings as one continuous block, thus allowing us to handle cases where a
+ // hex-encoded triplet of a quoted-printable encoded payload is split between 2 or more
+ // encoded-word tokens.
+ encoding = char.ToUpperInvariant (token.Encoding);
+ codepage = token.CodePage;
+ if (encoding == 'Q')
+ decoder = qp ??= new QuotedPrintableDecoder (true);
+ else
+ decoder = base64 ??= new Base64Decoder ();
+
+ AddCodePage (codepage);
+
+ fixed (byte* inbuf = input, outbuf = scratch) {
+ int n = decoder.Decode (inbuf + token.StartIndex, token.Length, outbuf + scratchLength);
+ scratchLength += n;
+ }
+ } else if (token.Is8bit) {
+ // *sigh* I hate broken mailers...
+ var unicode = CharsetUtils.ConvertToUnicode (options, input, token.StartIndex, token.Length, out int length);
+ output.Append (unicode.AsSpan (0, length));
+ } else {
+ // pure 7bit ascii, a breath of fresh air...
+ int endIndex = token.StartIndex + token.Length;
+ for (int i = token.StartIndex; i < endIndex; i++)
+ output.Append ((char) input[i]);
+ }
+ }
+
+ public void Flush (ref ValueStringBuilder output)
+ {
+ // Reset any base64/quoted-printable decoder state
+ decoder?.Reset ();
+ decoder = null;
+
+ if (scratchLength > 0) {
+ // Convert any decoded encoded-word payloads into unicode and append to our 'decoded' buffer.
+ var unicode = CharsetUtils.ConvertToUnicode (options, codepage, scratch, 0, scratchLength, out int length);
+ output.Append (unicode.AsSpan (0, length));
+ scratchLength = 0;
+ }
+ }
+
+ public int GetMostCommonCodePage ()
+ {
+ int codepage = Encoding.UTF8.CodePage;
+ int max = 0;
+
+ for (int i = 0; i < codepageIndex; i++) {
+ if (codepages[i].Count > max) {
+ codepage = codepages[i].CodePage;
+ max = codepages[i].Count;
+ }
+ }
+
+ return codepage;
+ }
+
+ public void Dispose ()
+ {
+ ArrayPool.Shared.Return (codepages);
+ }
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ static bool IsAscii (byte c)
+ {
+ return c < 128;
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ static bool IsAsciiAtom (byte c)
+ {
+ return c.IsAsciiAtom ();
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ static bool IsAtom (byte c)
+ {
+ return c.IsAtom ();
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ static bool IsBbQq (byte c)
+ {
+ return c == 'B' || c == 'b' || c == 'Q' || c == 'q';
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ static bool IsLwsp (byte c)
+ {
+ return c.IsWhitespace ();
+ }
+
+ static unsafe bool TryGetEncodedWordToken (byte* input, byte* word, int length, out Token token)
+ {
+ token = default;
+
+ if (length < 7)
+ return false;
+
+ byte* inend = word + length - 2;
+ byte* inptr = word;
+
+ // check if this could even be an encoded-word token
+ if (*inptr++ != '=' || *inptr++ != '?' || *inend++ != '?' || *inend++ != '=')
+ return false;
+
+ inend -= 2;
+
+ if (*inptr == '?' || *inptr == '*') {
+ // this would result in an empty charset
+ return false;
+ }
+
+ string charset, culture;
+
+ using (var buffer = new ValueStringBuilder (32)) {
+ // find the end of the charset name
+ while (*inptr != '?' && *inptr != '*') {
+ if (!IsAsciiAtom (*inptr))
+ return false;
+
+ buffer.Append ((char) *inptr);
+ inptr++;
+ }
+
+ charset = buffer.ToString ();
+ }
+
+ if (*inptr == '*') {
+ // we found a language code...
+ inptr++;
+
+ using (var buffer = new ValueStringBuilder (32)) {
+ // find the end of the language code
+ while (*inptr != '?') {
+ if (!IsAsciiAtom (*inptr))
+ return false;
+
+ buffer.Append ((char) *inptr);
+ inptr++;
+ }
+
+ culture = buffer.ToString ();
+ }
+ } else {
+ culture = null;
+ }
+
+ // skip over the '?' to get to the encoding
+ inptr++;
+
+ char encoding;
+ if (*inptr == 'B' || *inptr == 'b' || *inptr == 'Q' || *inptr == 'q') {
+ encoding = (char) *inptr++;
+ } else {
+ return false;
+ }
+
+ if (*inptr != '?' || inptr == inend)
+ return false;
+
+ // skip over the '?' to get to the payload
+ inptr++;
+
+ int start = (int) (inptr - input);
+ int len = (int) (inend - inptr);
+
+ token = new Token (charset, culture, encoding, start, len);
+
+ return true;
+ }
+
+ static unsafe void TokenizePhrase (ParserOptions options, ITokenWriter writer, ref ValueStringBuilder decoded, byte* inbuf, int startIndex, int length)
+ {
+ byte* text, word, inptr = inbuf + startIndex;
+ byte* inend = inptr + length;
+ var lwsp = new Token (0, 0);
+ bool encoded = false;
+ Token token;
+ bool ascii;
+ int n;
+
+ while (inptr < inend) {
+ text = inptr;
+ while (inptr < inend && IsLwsp (*inptr))
+ inptr++;
+
+ lwsp = new Token ((int) (text - inbuf), (int) (inptr - text));
+
+ word = inptr;
+ ascii = true;
+ if (inptr < inend && IsAsciiAtom (*inptr)) {
+ if (options.Rfc2047ComplianceMode == RfcComplianceMode.Loose) {
+ // Make an extra effort to detect and separate encoded-word
+ // tokens that have been merged with other words.
+ bool is_rfc2047 = false;
+
+ if (inptr + 2 < inend && *inptr == '=' && *(inptr + 1) == '?') {
+ inptr += 2;
+
+ // skip past the charset (if one is even declared, sigh)
+ while (inptr < inend && *inptr != '?') {
+ ascii = ascii && IsAscii (*inptr);
+ inptr++;
+ }
+
+ // sanity check encoding type
+ if (inptr + 3 >= inend || *inptr != '?' || !IsBbQq (*(inptr + 1)) || *(inptr + 2) != '?') {
+ ascii = true;
+ goto non_rfc2047;
+ }
+
+ inptr += 3;
+
+ // find the end of the rfc2047 encoded word token
+ while (inptr + 2 < inend && !(*inptr == '?' && *(inptr + 1) == '=')) {
+ ascii = ascii && IsAscii (*inptr);
+ inptr++;
+ }
+
+ if (inptr + 2 > inend || *inptr != '?' || *(inptr + 1) != '=') {
+ // didn't find an end marker...
+ inptr = word + 2;
+ ascii = true;
+
+ goto non_rfc2047;
+ }
+
+ is_rfc2047 = true;
+ inptr += 2;
+ }
+
+ non_rfc2047:
+ if (!is_rfc2047) {
+ // stop if we encounter a possible rfc2047 encoded
+ // token even if it's inside another word, sigh.
+ while (inptr < inend && IsAtom (*inptr)) {
+ if (inptr + 2 < inend && *inptr == '=' && *(inptr + 1) == '?')
+ break;
+ ascii = ascii && IsAscii (*inptr);
+ inptr++;
+ }
+ }
+ } else {
+ // encoded-word tokens are atoms
+ while (inptr < inend && IsAsciiAtom (*inptr)) {
+ //ascii = ascii && IsAscii (*inptr);
+ inptr++;
+ }
+ }
+
+ n = (int) (inptr - word);
+ if (TryGetEncodedWordToken (inbuf, word, n, out token)) {
+ // rfc2047 states that you must ignore all whitespace between
+ // encoded-word tokens
+ if ((!encoded || !writer.IgnoreWhitespaceBetweenEncodedWords) && lwsp.Length > 0) {
+ // previous token was not encoded, so preserve whitespace
+ writer.Write (ref decoded, ref lwsp);
+ }
+
+ writer.Write (ref decoded, ref token);
+ encoded = true;
+ } else {
+ // append the lwsp and atom tokens
+ if (lwsp.Length > 0)
+ writer.Write (ref decoded, ref lwsp);
+
+ token = new Token ((int) (word - inbuf), n, !ascii);
+ writer.Write (ref decoded, ref token);
+
+ encoded = false;
+ }
+ } else {
+ // append the lwsp token
+ if (lwsp.Length > 0)
+ writer.Write (ref decoded, ref lwsp);
+
+ // append the non-ascii atom token
+ ascii = true;
+ while (inptr < inend && !IsLwsp (*inptr) && !IsAsciiAtom (*inptr)) {
+ ascii = ascii && IsAscii (*inptr);
+ inptr++;
+ }
+
+ token = new Token ((int) (word - inbuf), (int) (inptr - word), !ascii);
+ writer.Write (ref decoded, ref token);
+
+ encoded = false;
+ }
+ }
+
+ writer.Flush (ref decoded);
+ }
+
+ internal static string DecodePhrase (ParserOptions options, byte[] phrase, int startIndex, int count, out int codepage)
+ {
+ codepage = Encoding.UTF8.CodePage;
+
+ if (count == 0)
+ return string.Empty;
+
+ unsafe {
+ fixed (byte* inbuf = phrase) {
+ var scratch = count < 2048 ? ArrayPool.Shared.Rent (count) : new byte[count];
+ var decoder = new TokenDecoder (options, phrase, scratch);
+ var decoded = new ValueStringBuilder (count);
+
+ try {
+ TokenizePhrase (options, decoder, ref decoded, inbuf, startIndex, count);
+ codepage = decoder.GetMostCommonCodePage ();
+ return decoded.ToString ();
+ } finally {
+ if (count < 2048)
+ ArrayPool.Shared.Return (scratch);
+ decoder.Dispose ();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Decode a phrase.
+ ///
+ ///
+ /// Decodes the phrase(s) starting at the given index and spanning across
+ /// the specified number of bytes using the supplied parser options.
+ ///
+ /// The decoded phrase.
+ /// The parser options to use.
+ /// The phrase to decode.
+ /// The starting index.
+ /// The number of bytes to decode.
+ ///
+ /// is .
+ /// -or-
+ /// is .
+ ///
+ ///
+ /// and do not specify
+ /// a valid range in the byte array.
+ ///
+ public static string DecodePhrase (ParserOptions options, byte[] phrase, int startIndex, int count)
+ {
+ if (options is null)
+ throw new ArgumentNullException (nameof (options));
+
+ if (phrase is null)
+ throw new ArgumentNullException (nameof (phrase));
+
+ if (startIndex < 0 || startIndex > phrase.Length)
+ throw new ArgumentOutOfRangeException (nameof (startIndex));
+
+ if (count < 0 || startIndex + count > phrase.Length)
+ throw new ArgumentOutOfRangeException (nameof (count));
+
+ return DecodePhrase (options, phrase, startIndex, count, out _);
+ }
+ }
+}
diff --git a/src/EmailAddresses/MailKit/Utils/ValueStringBuilder.cs b/src/EmailAddresses/MailKit/Utils/ValueStringBuilder.cs
new file mode 100644
index 0000000..4516c25
--- /dev/null
+++ b/src/EmailAddresses/MailKit/Utils/ValueStringBuilder.cs
@@ -0,0 +1,318 @@
+#nullable enable
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Globalization;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace MimeKit.Utils {
+ internal ref partial struct ValueStringBuilder
+ {
+ private char[]? _arrayToReturnToPool;
+ private Span _chars;
+ private int _pos;
+
+ public ValueStringBuilder (int initialCapacity)
+ {
+ _arrayToReturnToPool = ArrayPool.Shared.Rent (initialCapacity);
+ _chars = _arrayToReturnToPool;
+ _pos = 0;
+ }
+
+ public int Length {
+ get => _pos;
+ }
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ ///
+ /// Get a pinnable reference to the builder.
+ /// Does not ensure there is a null char after
+ /// This overload is pattern matched in the C# 7.3+ compiler so you can omit
+ /// the explicit method call, and write eg "fixed (char* c = builder)"
+ ///
+ public ref char GetPinnableReference ()
+ {
+ return ref MemoryMarshal.GetReference (_chars);
+ }
+
+ ///
+ /// Get a pinnable reference to the builder.
+ ///
+ /// Ensures that the builder has a null char after
+ public ref char GetPinnableReference (bool terminate)
+ {
+ if (terminate) {
+ EnsureCapacity (Length + 1);
+ _chars[Length] = '\0';
+ }
+ return ref MemoryMarshal.GetReference (_chars);
+ }
+#endif
+
+ public ref char this[int index] {
+ get {
+ Debug.Assert (index < _pos);
+ return ref _chars[index];
+ }
+ }
+
+ public override string ToString ()
+ {
+ string s = _chars.Slice (0, _pos).ToString ();
+ Dispose ();
+ return s;
+ }
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ /// Returns the underlying storage of the builder.
+ public Span RawChars => _chars;
+#endif
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ ///
+ /// Returns a span around the contents of the builder.
+ ///
+ /// Ensures that the builder has a null char after
+ public ReadOnlySpan AsSpan (bool terminate)
+ {
+ if (terminate) {
+ EnsureCapacity (Length + 1);
+ _chars[Length] = '\0';
+ }
+ return _chars.Slice (0, _pos);
+ }
+#endif
+
+ public ReadOnlySpan AsSpan () => _chars.Slice (0, _pos);
+#if UNUSED_VALUESTRINGBUILDER_API
+ public ReadOnlySpan AsSpan (int start) => _chars.Slice (start, _pos - start);
+ public ReadOnlySpan AsSpan (int start, int length) => _chars.Slice (start, length);
+#endif
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ public bool TryCopyTo (Span destination, out int charsWritten)
+ {
+ if (_chars.Slice (0, _pos).TryCopyTo (destination)) {
+ charsWritten = _pos;
+ Dispose ();
+ return true;
+ } else {
+ charsWritten = 0;
+ Dispose ();
+ return false;
+ }
+ }
+#endif
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ public void Insert (int index, char value, int count)
+ {
+ if (_pos > _chars.Length - count) {
+ Grow (count);
+ }
+
+ int remaining = _pos - index;
+ _chars.Slice (index, remaining).CopyTo (_chars.Slice (index + count));
+ _chars.Slice (index, count).Fill (value);
+ _pos += count;
+ }
+#endif
+
+ public void Insert (int index, string? s)
+ {
+ if (s is null) {
+ return;
+ }
+
+ int count = s.Length;
+
+ if (_pos > (_chars.Length - count)) {
+ Grow (count);
+ }
+
+ int remaining = _pos - index;
+ _chars.Slice (index, remaining).CopyTo (_chars.Slice (index + count));
+ s
+#if !NET6_0_OR_GREATER
+ .AsSpan ()
+#endif
+ .CopyTo (_chars.Slice (index));
+ _pos += count;
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ public void Append (char c)
+ {
+ int pos = _pos;
+ if ((uint) pos < (uint) _chars.Length) {
+ _chars[pos] = c;
+ _pos = pos + 1;
+ } else {
+ GrowAndAppend (c);
+ }
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ public void Append (string? s)
+ {
+ if (s is null) {
+ return;
+ }
+
+ int pos = _pos;
+ if (s.Length == 1 && (uint) pos < (uint) _chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
+ {
+ _chars[pos] = s[0];
+ _pos = pos + 1;
+ } else {
+ AppendSlow (s);
+ }
+ }
+
+ private void AppendSlow (string s)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - s.Length) {
+ Grow (s.Length);
+ }
+
+ s
+#if !NET6_0_OR_GREATER
+ .AsSpan ()
+#endif
+ .CopyTo (_chars.Slice (pos));
+ _pos += s.Length;
+ }
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ public void Append (char c, int count)
+ {
+ if (_pos > _chars.Length - count) {
+ Grow (count);
+ }
+
+ Span dst = _chars.Slice (_pos, count);
+ for (int i = 0; i < dst.Length; i++) {
+ dst[i] = c;
+ }
+ _pos += count;
+ }
+#endif
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ public unsafe void Append (char* value, int length)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - length) {
+ Grow (length);
+ }
+
+ Span dst = _chars.Slice (_pos, length);
+ for (int i = 0; i < dst.Length; i++) {
+ dst[i] = *value++;
+ }
+ _pos += length;
+ }
+#endif
+
+ public void Append (ReadOnlySpan value)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - value.Length) {
+ Grow (value.Length);
+ }
+
+ value.CopyTo (_chars.Slice (_pos));
+ _pos += value.Length;
+ }
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ public Span AppendSpan (int length)
+ {
+ int origPos = _pos;
+ if (origPos > _chars.Length - length) {
+ Grow (length);
+ }
+
+ _pos = origPos + length;
+ return _chars.Slice (origPos, length);
+ }
+#endif
+
+#if NET6_0_OR_GREATER
+#if UNUSED_VALUESTRINGBUILDER_API
+ internal void AppendSpanFormattable (T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable
+ {
+ if (value.TryFormat (_chars.Slice (_pos), out int charsWritten, format, provider)) {
+ _pos += charsWritten;
+ } else {
+ Append (value.ToString (format, provider));
+ }
+ }
+#endif
+#else
+ internal void AppendInvariant (T value, string? format = null) where T: IFormattable
+ {
+ Append (value.ToString (format, CultureInfo.InvariantCulture));
+ }
+
+#if UNUSED_VALUESTRINGBUILDER_API
+ internal void AppendSpanFormattable (T value, string? format = null, IFormatProvider? provider = null) where T: IFormattable
+ {
+ Append (value.ToString (format, provider));
+ }
+#endif
+#endif
+
+
+ [MethodImpl (MethodImplOptions.NoInlining)]
+ private void GrowAndAppend (char c)
+ {
+ Grow (1);
+ Append (c);
+ }
+
+ ///
+ /// Resize the internal buffer either by doubling current buffer size or
+ /// by adding to
+ /// whichever is greater.
+ ///
+ ///
+ /// Number of chars requested beyond current position.
+ ///
+ [MethodImpl (MethodImplOptions.NoInlining)]
+ private void Grow (int additionalCapacityBeyondPos)
+ {
+ Debug.Assert (additionalCapacityBeyondPos > 0);
+ Debug.Assert (_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
+
+ // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative
+ char[] poolArray = ArrayPool.Shared.Rent ((int) Math.Max ((uint) (_pos + additionalCapacityBeyondPos), (uint) _chars.Length * 2));
+
+ _chars.Slice (0, _pos).CopyTo (poolArray);
+
+ char[]? toReturn = _arrayToReturnToPool;
+ _chars = _arrayToReturnToPool = poolArray;
+ if (toReturn != null) {
+ ArrayPool.Shared.Return (toReturn);
+ }
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ public void Dispose ()
+ {
+ char[]? toReturn = _arrayToReturnToPool;
+ this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
+ if (toReturn != null) {
+ ArrayPool.Shared.Return (toReturn);
+ }
+ }
+ }
+}
diff --git a/src/EmailAddresses/README.md b/src/EmailAddresses/README.md
new file mode 100644
index 0000000..848c717
--- /dev/null
+++ b/src/EmailAddresses/README.md
@@ -0,0 +1,77 @@
+# PosInformatique.Foundations.EmailAddresses
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+
+## Introduction
+This package provides a strongly-typed **EmailAddress** value object that ensures only valid email addresses (RFC 5322 compliant) can be instantiated.
+
+It simplifies validation, parsing, comparison, and string formatting of email addresses.
+
+## Install
+You can install the package from [NuGet](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/):
+
+```powershell
+dotnet add package PosInformatique.Foundations.EmailAddresses
+```
+
+## Features
+- Strongly-typed email address validation and parsing
+- Ensures email addresses are formatted according to **RFC 5322**
+- Email values are always stored and compared in **lowercase** to avoid case-sensitive inconsistencies
+- Implements `IEquatable`, `IComparable`, and operator overloads for equality and ordering
+- Provides `IFormattable` and `IParsable` for seamless integration with .NET APIs
+- Implicit conversion between `string` and `EmailAddress`
+
+## Use cases
+- **Validation**: prevent invalid emails from being stored in your domain entities.
+- **Type safety**: avoid dealing with raw strings when working with email addresses.
+- **Conversion**: use implicit/explicit parsing and formatting seamlessly when working with APIs.
+- **Consistency**: ensures a single, robust email parsing logic across all projects.
+
+## Examples
+
+### Create and validate email addresses
+```csharp
+// Implicit conversion from string
+EmailAddress email = "john.doe@example.com";
+
+// Access parts
+Console.WriteLine(email.UserName); // "john.doe"
+Console.WriteLine(email.Domain); // "example.com"
+
+// Validation
+bool valid = EmailAddress.IsValid("test@domain.com"); // true
+bool invalid = EmailAddress.IsValid("not-an-email"); // false
+```
+
+### Parsing
+```csharp
+var email = EmailAddress.Parse("alice@company.org");
+Console.WriteLine(email); // "alice@company.org"
+
+if (EmailAddress.TryParse("bob@company.org", out var parsed))
+{
+ Console.WriteLine(parsed.Domain); // "company.org"
+}
+```
+
+### Comparisons
+```csharp
+var a = (EmailAddress)"alice@company.com";
+var b = (EmailAddress)"bob@company.com";
+
+Console.WriteLine(a == b); // false
+Console.WriteLine(a != b); // true
+Console.WriteLine(a < b); // true, alphabetic order
+
+var list = new List { b, a };
+list.Sort(); // Sorted alphabetically
+```
+
+## Links
+- [NuGet package: EmailAddresses (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses/)
+- [NuGet package: EmailAddresses.EntityFramework](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework/)
+- [NuGet package: EmailAddresses.FluentValidations](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation/)
+- [NuGet package: EmailAddresses.Json](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
diff --git a/src/Emailing.Azure/AzureEmailProvider.cs b/src/Emailing.Azure/AzureEmailProvider.cs
new file mode 100644
index 0000000..ca4fdf8
--- /dev/null
+++ b/src/Emailing.Azure/AzureEmailProvider.cs
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing.Azure
+{
+ using System.Globalization;
+
+ ///
+ /// Implementation of the to send the e-mail using
+ /// Azure Communication Service.
+ ///
+ public sealed class AzureEmailProvider : IEmailProvider
+ {
+ private readonly global::Azure.Communication.Email.EmailClient client;
+
+ ///
+ /// Initializes a new instance of the class
+ /// using the Microsoft .
+ ///
+ ///
+ /// used to call the Azure Communication Service API.
+ /// Thrown when the argument is .
+ public AzureEmailProvider(global::Azure.Communication.Email.EmailClient client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+
+ this.client = client;
+ }
+
+ ///
+ public async Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(message);
+
+ var receipients = new global::Azure.Communication.Email.EmailRecipients()
+ {
+ To =
+ {
+ new global::Azure.Communication.Email.EmailAddress(message.To.Email, message.To.DisplayName),
+ },
+ };
+
+ var content = new global::Azure.Communication.Email.EmailContent(message.Subject)
+ {
+ Html = message.HtmlContent,
+ };
+
+ var azureMessage = new global::Azure.Communication.Email.EmailMessage(message.From.Email, receipients, content)
+ {
+ Headers =
+ {
+ { "X-Priority", Convert.ToString(Convert.ToInt32(message.Importance, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture) },
+ { "Importance", Convert.ToString(message.Importance, CultureInfo.InvariantCulture) },
+ },
+ };
+
+ await this.client.SendAsync(global::Azure.WaitUntil.Started, azureMessage, cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Azure/AzureEmailingBuilderExtensions.cs b/src/Emailing.Azure/AzureEmailingBuilderExtensions.cs
new file mode 100644
index 0000000..b1185e7
--- /dev/null
+++ b/src/Emailing.Azure/AzureEmailingBuilderExtensions.cs
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ using global::Azure.Communication.Email;
+ using global::Azure.Core.Extensions;
+ using Microsoft.Extensions.Azure;
+ using Microsoft.Extensions.DependencyInjection.Extensions;
+ using PosInformatique.Foundations.Emailing;
+ using PosInformatique.Foundations.Emailing.Azure;
+
+ ///
+ /// Extension methods to configure the Azure Communication Service provider
+ /// for the .
+ ///
+ public static class AzureEmailingBuilderExtensions
+ {
+ ///
+ /// Configure the provider of to use Azure Communication Service.
+ ///
+ /// which to configure.
+ /// Uri to the the Azure Communication Service instance.
+ /// Allows to configure the used by the provider.
+ /// The instance to continue the configuration of the emailing feature.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public static EmailingBuilder UseAzureCommunicationService(this EmailingBuilder builder, Uri uri, Action>? clientBuilder = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(uri);
+
+ builder.Services.TryAddSingleton();
+
+ builder.Services.AddAzureClients(builder =>
+ {
+ var emailClientBuilder = builder.AddEmailClient(uri);
+
+ if (clientBuilder is not null)
+ {
+ clientBuilder(emailClientBuilder);
+ }
+ });
+
+ return builder;
+ }
+
+ ///
+ /// Configure the provider of to use Azure Communication Service.
+ ///
+ /// which to configure.
+ /// Connection string to the Azure Communication Service instance.
+ /// Allows to configure the used by the provider.
+ /// The instance to continue the configuration of the emailing feature.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public static EmailingBuilder UseAzureCommunicationService(this EmailingBuilder builder, string connectionString, Action>? clientBuilder = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(connectionString);
+
+ builder.Services.TryAddSingleton();
+
+ builder.Services.AddAzureClients(builder =>
+ {
+ var emailClientBuilder = builder.AddEmailClient(connectionString);
+
+ if (clientBuilder is not null)
+ {
+ clientBuilder(emailClientBuilder);
+ }
+ });
+
+ return builder;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Azure/CHANGELOG.md b/src/Emailing.Azure/CHANGELOG.md
new file mode 100644
index 0000000..bb7782e
--- /dev/null
+++ b/src/Emailing.Azure/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with Azure Communication Service Emailing provider.
diff --git a/src/Emailing.Azure/Emailing.Azure.csproj b/src/Emailing.Azure/Emailing.Azure.csproj
new file mode 100644
index 0000000..574fa69
--- /dev/null
+++ b/src/Emailing.Azure/Emailing.Azure.csproj
@@ -0,0 +1,32 @@
+
+
+
+ true
+
+
+ Provides an IEmailProvider implementation for PosInformatique.Foundations.Emailing using Azure Communication Service.
+ Uses Azure.Communication.Email.EmailClient to send templated emails and can be configured via AddEmailing().UseAzureCommunicationService(), including EmailClient and EmailClientOptions customization.
+
+ email;emailing;azure;azurecommunicationservice;communication;emailclient;provider;dotnet;dependencyinjection;posinformatique
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Emailing.Azure/README.md b/src/Emailing.Azure/README.md
new file mode 100644
index 0000000..e706330
--- /dev/null
+++ b/src/Emailing.Azure/README.md
@@ -0,0 +1,109 @@
+# PosInformatique.Foundations.Emailing.Azure
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure/)
+
+## Introduction
+
+[PosInformatique.Foundations.Emailing.Azure](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure/) provides an `IEmailProvider`
+implementation for [PosInformatique.Foundations.Emailing](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing/)
+based on the `EmailClient` from [Azure.Communication.Email](https://www.nuget.org/packages/Azure.Communication.Email).
+
+It allows you to send templated emails (created via `IEmailManager`) using **Azure Communication Service**.
+
+## Install
+
+You can install the package from [NuGet](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure/):
+
+```powershell
+dotnet add package PosInformatique.Foundations.Emailing.Azure
+```
+
+## Features
+
+- `IEmailProvider` implementation using [Azure.Communication.Email.EmailClient](https://learn.microsoft.com/en-us/dotnet/api/azure.communication.email.emailclient?view=azure-dotnet).
+- Simple configuration through `AddEmailing().UseAzureCommunicationService(...)`.
+- Supports configuration via:
+ - Azure Communication Service **connection string**, or
+ - Azure Communication Service **endpoint URI**.
+- Callback to configure `EmailClient` / `EmailClientOptions`:
+ - Authentication (managed identity, connection string, etc.).
+ - Retry policy, logging, diagnostics, and other Azure client options.
+
+## Basic configuration
+
+### Using connection string
+
+```csharp
+using Microsoft.Extensions.DependencyInjection;
+using PosInformatique.Foundations.EmailAddresses;
+
+var services = new ServiceCollection();
+
+// Your ACS connection string
+var acsConnectionString = configuration["AzureCommunicationService:ConnectionString"];
+
+services
+ .AddEmailing(options =>
+ {
+ options.SenderEmailAddress = EmailAddress.Parse("no-reply@myapp.com");
+
+ // Register your templates here...
+ // options.RegisterTemplate(EmailTemplateIdentifiers.Invitation, invitationTemplate);
+ })
+ .UseAzureCommunicationService(acsConnectionString);
+```
+
+### Using endpoint URI and Azure identity (managed identity)
+
+You can also configure the provider using the ACS endpoint URI, and configure authentication
+(for example with managed identity) and client options using the `clientBuilder` parameter.
+
+```csharp
+using Azure.Communication.Email;
+using Azure.Identity;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+
+var acsEndpoint = new Uri(configuration["AzureCommunicationService:Endpoint"]);
+
+services
+ .AddEmailing(options =>
+ {
+ options.SenderEmailAddress = EmailAddress.Parse("no-reply@myapp.com");
+
+ // Register your templates here...
+ })
+ .UseAzureCommunicationService(
+ acsEndpoint,
+ clientBuilder =>
+ {
+ // Configure EmailClient and EmailClientOptions here
+
+ clientBuilder.ConfigureOptions((EmailClientOptions options) =>
+ {
+ // Example: configure retry, diagnostics, etc.
+ options.Retry.MaxRetries = 5;
+ });
+
+ // Example: use managed identity for authentication
+ clientBuilder.WithCredential(new DefaultAzureCredential());
+ });
+```
+
+## Typical usage end-to-end
+
+1. Configure emailing and templates with `AddEmailing(...)`.
+2. Configure the Azure provider using `UseAzureCommunicationService(...)`.
+3. Inject `IEmailManager` and create an email from a template identifier.
+4. Add recipients and models.
+5. Call `SendAsync(...)` to send via Azure Communication Service.
+
+## Links
+
+- [NuGet package: Emailing (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing/)
+- [NuGet package: Emailing.Azure](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
+- [Azure Communication Services – Email documentation](https://learn.microsoft.com/azure/communication-services/concepts/email/)
\ No newline at end of file
diff --git a/src/Emailing.Graph/CHANGELOG.md b/src/Emailing.Graph/CHANGELOG.md
new file mode 100644
index 0000000..eb7a9ad
--- /dev/null
+++ b/src/Emailing.Graph/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with Microsoft Graph Emailing provider.
diff --git a/src/Emailing.Graph/Emailing.Graph.csproj b/src/Emailing.Graph/Emailing.Graph.csproj
new file mode 100644
index 0000000..4c17a30
--- /dev/null
+++ b/src/Emailing.Graph/Emailing.Graph.csproj
@@ -0,0 +1,33 @@
+
+
+
+ true
+
+
+ Provides an IEmailProvider implementation for PosInformatique.Foundations.Emailing using Microsoft Graph API.
+ Uses Microsoft.Graph.GraphServiceClient to send templated emails through Microsoft 365 mailboxes with Azure AD authentication via TokenCredential.
+
+
+ email;emailing;graph;microsoftgraph;microsoft365;azure;azuread;tokencredential;provider;dotnet;dependencyinjection;posinformatique
+
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Emailing.Graph/GraphEmailProvider.cs b/src/Emailing.Graph/GraphEmailProvider.cs
new file mode 100644
index 0000000..bcfbc0c
--- /dev/null
+++ b/src/Emailing.Graph/GraphEmailProvider.cs
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing.Graph
+{
+ using Microsoft.Graph;
+ using Microsoft.Graph.Models;
+ using Microsoft.Graph.Users.Item.SendMail;
+
+ ///
+ /// Implementation of the to send the e-mail using
+ /// the Graph API.
+ ///
+ public sealed class GraphEmailProvider : IEmailProvider
+ {
+ private readonly GraphServiceClient serviceClient;
+
+ ///
+ /// Initializes a new instance of the class
+ /// using the Microsoft .
+ ///
+ ///
+ /// used to call the Azure Communication Service API.
+ /// Thrown when the argument is .
+ public GraphEmailProvider(GraphServiceClient serviceClient)
+ {
+ ArgumentNullException.ThrowIfNull(serviceClient);
+
+ this.serviceClient = serviceClient;
+ }
+
+ ///
+ public async Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(message);
+
+ var importance = message.Importance switch
+ {
+ EmailImportance.Low => Importance.Low,
+ EmailImportance.High => Importance.High,
+ _ => Importance.Normal,
+ };
+
+ var graphMessage = new Message()
+ {
+ Body = new ItemBody
+ {
+ ContentType = BodyType.Html,
+ Content = message.HtmlContent,
+ },
+ Importance = importance,
+ Subject = message.Subject,
+ ToRecipients =
+ [
+ new()
+ {
+ EmailAddress = new EmailAddress
+ {
+ Address = message.To.Email.ToString(),
+ Name = message.To.DisplayName,
+ },
+ },
+ ],
+ };
+
+ var body = new SendMailPostRequestBody()
+ {
+ Message = graphMessage,
+ SaveToSentItems = false,
+ };
+
+ await this.serviceClient.Users[message.From.Email.ToString()].SendMail.PostAsync(body, cancellationToken: cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Graph/GraphEmailingBuilderExtensions.cs b/src/Emailing.Graph/GraphEmailingBuilderExtensions.cs
new file mode 100644
index 0000000..600b651
--- /dev/null
+++ b/src/Emailing.Graph/GraphEmailingBuilderExtensions.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ using Azure.Core;
+ using Microsoft.Extensions.DependencyInjection.Extensions;
+ using Microsoft.Graph;
+ using PosInformatique.Foundations.Emailing;
+ using PosInformatique.Foundations.Emailing.Graph;
+
+ ///
+ /// Extension methods to configure the Azure Communication Service provider
+ /// for the .
+ ///
+ public static class GraphEmailingBuilderExtensions
+ {
+ ///
+ /// Configure the provider of to use Azure Communication Service.
+ ///
+ /// which to configure.
+ /// The for authenticating to Microsoft Graph API.
+ /// The base service URL of the API Graph. If not specified the https://graph.microsoft.com/v1.0 will be use.
+ /// The instance to continue the configuration of the emailing feature.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public static EmailingBuilder UseGraph(this EmailingBuilder builder, TokenCredential tokenCredential, string? baseUrl = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(tokenCredential);
+
+ builder.Services.TryAddSingleton(sp =>
+ {
+ var serviceClient = new GraphServiceClient(tokenCredential, null, baseUrl);
+
+ return new GraphEmailProvider(serviceClient);
+ });
+
+ return builder;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Graph/README.md b/src/Emailing.Graph/README.md
new file mode 100644
index 0000000..0a62105
--- /dev/null
+++ b/src/Emailing.Graph/README.md
@@ -0,0 +1,123 @@
+### PosInformatique.Foundations.Emailing.Graph
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph/)
+
+## Introduction
+
+[PosInformatique.Foundations.Emailing.Graph](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph/)
+provides an `IEmailProvider`
+implementation for [PosInformatique.Foundations.Emailing](../Emailing/README.md) based on the **Microsoft Graph** API.
+
+It uses [Microsoft.Graph.GraphServiceClient](https://learn.microsoft.com/en-us/graph/sdks/create-client?tabs=csharp)
+to send templated emails (created via `IEmailManager`)
+through a Microsoft 365 mailbox, using Azure AD authentication.
+
+Authentication is fully driven by an
+[Azure.Core.TokenCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.core.tokencredential?view=azure-dotnet)
+instance, allowing you to use:
+
+- Managed identity
+- Client credentials (client id/secret or certificate)
+- Interactive login, device code, etc.
+
+## Install
+
+You can install the package from [NuGet](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph/):
+
+```powershell
+dotnet add package PosInformatique.Foundations.Emailing.Graph
+```
+
+## Features
+
+- `IEmailProvider` implementation using `Microsoft.Graph.GraphServiceClient`.
+- Simple configuration through `AddEmailing().UseGraph(...)`.
+- Authentication configured via `TokenCredential`:
+ - `DefaultAzureCredential` (managed identity, VS, CLI, etc.)
+ - `ClientSecretCredential`, `ClientCertificateCredential`, etc.
+- Optional `baseUrl` parameter to customize the Graph endpoint (defaults to `https://graph.microsoft.com/v1.0`).
+- Sends HTML emails using the `EmailMessage` produced by [PosInformatique.Foundations.Emailing](../Emailing/README.md).
+
+## Basic configuration
+
+### Using DefaultAzureCredential (managed identity or local dev)
+
+```csharp
+using Azure.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using PosInformatique.Foundations.EmailAddresses;
+using PosInformatique.Foundations.Emailing;
+using PosInformatique.Foundations.Emailing.Graph;
+
+var services = new ServiceCollection();
+
+// TokenCredential for Microsoft Graph (for example: managed identity or local dev)
+var credential = new DefaultAzureCredential();
+
+services
+ .AddEmailing(options =>
+ {
+ options.SenderEmailAddress = EmailAddress.Parse("sender@yourtenant.onmicrosoft.com");
+
+ // Register your templates here...
+ // options.RegisterTemplate(EmailTemplateIdentifiers.Invitation, invitationTemplate);
+ })
+ .UseGraph(credential);
+```
+
+### Using client credentials (app registration)
+
+If you want to authenticate with a client id / tenant id / client secret:
+
+```csharp
+using Azure.Identity;
+using PosInformatique.Foundations.Emailing.Graph;
+
+var tenantId = configuration["AzureAd:TenantId"];
+var clientId = configuration["AzureAd:ClientId"];
+var clientSecret = configuration["AzureAd:ClientSecret"];
+
+var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
+
+services
+ .AddEmailing(options =>
+ {
+ options.SenderEmailAddress = EmailAddress.Parse("sender@yourtenant.onmicrosoft.com");
+ })
+ .UseGraph(credential);
+```
+
+The `TokenCredential` you provide is responsible for acquiring tokens for the Microsoft Graph API. The provider does not manage scopes or credentials itself; this is entirely delegated to the credential implementation.
+
+### Custom Graph endpoint
+
+You can optionally customize the Graph base URL (for example, for national clouds):
+
+```csharp
+var baseUrl = "https://graph.microsoft.com/v1.0/beta";
+
+services
+ .AddEmailing(options =>
+ {
+ options.SenderEmailAddress = EmailAddress.Parse("sender@yourtenant.onmicrosoft.com");
+ })
+ .UseGraph(credential, baseUrl);
+```
+
+If `baseUrl` is `null`, `https://graph.microsoft.com/v1.0` is used by default.
+
+## Typical end-to-end usage
+
+1. Configure emailing (sender address, templates) with `AddEmailing(...)`.
+2. Configure the Graph provider with `UseGraph(TokenCredential, baseUrl?)`.
+3. Inject `IEmailManager` and create emails from template identifiers.
+4. Add recipients and models.
+5. Call `SendAsync(...)` to send emails via Microsoft Graph.
+
+## Links
+
+- [NuGet package: Emailing](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing/)
+s- [Microsoft Graph .NET SDK](https://learn.microsoft.com/graph/sdks/sdks-overview)
+- [Azure Identity (TokenCredential)](https://learn.microsoft.com/dotnet/azure/sdk/authentication/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
\ No newline at end of file
diff --git a/src/Emailing.Templates.Razor/CHANGELOG.md b/src/Emailing.Templates.Razor/CHANGELOG.md
new file mode 100644
index 0000000..b6d4aad
--- /dev/null
+++ b/src/Emailing.Templates.Razor/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with Emailing template based on Razor view.
diff --git a/src/Emailing.Templates.Razor/Emailing.Templates.Razor.csproj b/src/Emailing.Templates.Razor/Emailing.Templates.Razor.csproj
new file mode 100644
index 0000000..d54f45f
--- /dev/null
+++ b/src/Emailing.Templates.Razor/Emailing.Templates.Razor.csproj
@@ -0,0 +1,28 @@
+
+
+
+ true
+
+
+ Provides helpers to create EmailTemplate instances using Razor components for subject and HTML body.
+ Built on top of PosInformatique.Foundations.Text.Templating.Razor, it supports strongly-typed models and Razor layout features for reusable email designs.
+
+ email;emailing;razor;blazor;templating;component;layout;dotnet;posinformatique;emailtemplate
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Emailing.Templates.Razor/README.md b/src/Emailing.Templates.Razor/README.md
new file mode 100644
index 0000000..6264b3a
--- /dev/null
+++ b/src/Emailing.Templates.Razor/README.md
@@ -0,0 +1,221 @@
+# PosInformatique.Foundations.Emailing.Templates.Razor
+
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/)
+[](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/)
+
+## Introduction
+
+[PosInformatique.Foundations.Emailing.Templates.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/)
+provides helpers to create `EmailTemplate` instances using Razor components as text templates for the subject and HTML body.
+
+It is built on top of:
+
+- [PosInformatique.Foundations.Emailing](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing/)
+- [PosInformatique.Foundations.Text.Templates.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor/)
+
+This allows you to design your email content with Blazor-style Razor components, benefiting from layout reuse, strongly-typed models, and familiar Razor syntax.
+
+## Install
+
+You can install the package from [NuGet](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/):
+
+```powershell
+dotnet add package PosInformatique.Foundations.Emailing.Templates.Razor
+```
+
+You also need a Blazor-compatible environment for compiling/executing Razor components.
+
+## Features
+
+- `RazorEmailTemplate.Create()` to build `EmailTemplate` from Razor components.
+- Base classes for Razor components:
+ - `RazorEmailTemplateSubject` for email subject.
+ - `RazorEmailTemplateBody` for email HTML body.
+- Strongly-typed model support via the `Model` parameter.
+- Supports Razor layout features for the email body (reuse consistent layout across multiple templates).
+- `UseRazorEmailTemplates()` extension method to enable Razor-based email templates in the emailing configuration.
+
+## Configuring emailing with Razor email templates
+
+### 1. Configure emailing services
+
+Use `AddEmailing(...)` and then `UseRazorEmailTemplates()`.
+
+```csharp
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+
+var emailingBuilder = services.AddEmailing(options =>
+{
+ // Configure emailing options here and register the templates
+})
+UseRazorEmailTemplates();.
+```
+
+- `AddEmailing(...)` registers the core emailing services and the templates.
+- `UseRazorEmailTemplates()` enables Razor-based text templating for emailing.
+
+### 2. Define the email model
+
+Example model used by the templates:
+
+```csharp
+using PosInformatique.Foundations.Emailing;
+
+public sealed class InvitationEmailTemplateModel
+{
+ public string FirstName { get; set; } = string.Empty;
+ public string InvitationLink { get; set; } = string.Empty;
+}
+```
+
+### 3. Subject component
+
+Create a Razor component for the subject that inherits from `RazorEmailTemplateSubject` and uses the `Model` parameter.
+
+`InvitationEmailSubject.razor`:
+
+```razor
+@using PosInformatique.Foundations.Emailing
+@using PosInformatique.Foundations.Emailing.Templates.Razor
+@inherits RazorEmailTemplateSubject
+
+Invitation for @Model.FirstName
+```
+
+This component:
+
+- Renders a single line of text.
+- Uses the strongly-typed `Model` to build the subject.
+
+### 4. Body component with layout
+
+You can define a layout component that centralizes common HTML structure (header, footer, styles, etc.),
+then reuse it across different email bodies.
+
+`EmailLayout.razor` (layout component):
+
+```razor
+@inherits LayoutComponentBase
+
+
+
+
+
+ @Title
+
+
+
+
+
+ @Title
+
+
+
+ @Body
+
+
+
+
+
+
+```
+
+Now create the specific body component for your invitation email.
+
+`InvitationEmailBody.razor`:
+
+```razor
+@using PosInformatique.Foundations.Emailing
+@using PosInformatique.Foundations.Emailing.Templates.Razor
+@inherits RazorEmailTemplateBody
+@layout EmailLayout
+
+@{
+ Title = $"Invitation for {Model.FirstName}";
+}
+
+
Hello @Model.FirstName,
+
+
+ You have been invited to join our platform.
+ Please click the link below to accept your invitation:
+
+ If you did not expect this email, you can safely ignore it.
+
+```
+
+Key points:
+
+- The body component inherits from `RazorEmailTemplateBody`.
+- It uses `@layout EmailLayout` to reuse the common HTML structure.
+- It uses the `Model` property to inject data into the HTML.
+
+## Creating an EmailTemplate from Razor components
+
+Use the static helper `RazorEmailTemplate.Create()` to build an `EmailTemplate` instance.
+
+```csharp
+using PosInformatique.Foundations.Emailing;
+using PosInformatique.Foundations.Emailing.Templates.Razor;
+
+var invitationTemplate = RazorEmailTemplate.Create<
+ InvitationEmailSubject,
+ InvitationEmailBody>();
+```
+
+You can then register this template in `EmailingOptions`:
+
+```csharp
+options.RegisterTemplate(EmailTemplateIdentifiers.Invitation, invitationTemplate);
+```
+
+After that, the rest of the flow is the same as with any other `EmailTemplate`:
+
+- Use `IEmailManager.Create(EmailTemplateIdentifiers.Invitation)` to create the email.
+- Add recipients and models.
+- Call `SendAsync(...)` to send.
+
+## Links
+
+- [NuGet package: Emailing (core library)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing/)
+- [NuGet package: Emailing.Templates.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/)
+- [NuGet package: Text.Templating.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor/)
+- [Source code](https://github.com/PosInformatique/PosInformatique.Foundations)
\ No newline at end of file
diff --git a/src/Emailing.Templates.Razor/RazorEmailTemplate.cs b/src/Emailing.Templates.Razor/RazorEmailTemplate.cs
new file mode 100644
index 0000000..ebb5732
--- /dev/null
+++ b/src/Emailing.Templates.Razor/RazorEmailTemplate.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing.Templates.Razor
+{
+ using PosInformatique.Foundations.Text.Templating.Razor;
+
+ ///
+ /// Used to create an using Razor text templating for the subject and body.
+ ///
+ /// Type of the model used to generate the subject and body of the e-mail.
+ public static class RazorEmailTemplate
+ {
+ ///
+ /// Creates an using the specified Razor components as text templating for the subject and body.
+ ///
+ /// Type of the Razor component used to generate the content of the e-mail subject.
+ /// Type of the Razor component used to generate the content of the e-mail body.
+ /// An instance using the specified Razor components as text templating for the subject and body.
+ public static EmailTemplate Create()
+ where TSubjectComponent : RazorEmailTemplateSubject
+ where TBodyComponent : RazorEmailTemplateBody
+ {
+ return new EmailTemplate(
+ new RazorTextTemplate(typeof(TSubjectComponent)),
+ new RazorTextTemplate(typeof(TBodyComponent)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Templates.Razor/RazorEmailTemplateBody.cs b/src/Emailing.Templates.Razor/RazorEmailTemplateBody.cs
new file mode 100644
index 0000000..96fb275
--- /dev/null
+++ b/src/Emailing.Templates.Razor/RazorEmailTemplateBody.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing.Templates.Razor
+{
+ using Microsoft.AspNetCore.Components;
+
+ ///
+ /// Base class of a Razor component which is used to generate the body of an email.
+ ///
+ /// Type of the used to generate the body of the e-mail.
+ public abstract class RazorEmailTemplateBody : ComponentBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected RazorEmailTemplateBody()
+ {
+ }
+
+ ///
+ /// Gets or sets the model used to generate the body of the e-mail.
+ ///
+ [Parameter]
+ public TModel Model { get; set; } = default!;
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Templates.Razor/RazorEmailTemplateServiceCollectionExtensions.cs b/src/Emailing.Templates.Razor/RazorEmailTemplateServiceCollectionExtensions.cs
new file mode 100644
index 0000000..c2da371
--- /dev/null
+++ b/src/Emailing.Templates.Razor/RazorEmailTemplateServiceCollectionExtensions.cs
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension methods to configure the Email templates based on Razor components.
+ ///
+ public static class RazorEmailTemplateServiceCollectionExtensions
+ {
+ ///
+ /// Adds Razor text templating support to the emailing services.
+ ///
+ /// to configure.
+ /// The instance to continue the configuration of the emailing service.
+ /// Thrown when the argument is .
+ public static EmailingBuilder UseRazorEmailTemplates(this EmailingBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ builder.Services.AddRazorTextTemplating();
+
+ return builder;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing.Templates.Razor/RazorEmailTemplateSubject.cs b/src/Emailing.Templates.Razor/RazorEmailTemplateSubject.cs
new file mode 100644
index 0000000..ef56752
--- /dev/null
+++ b/src/Emailing.Templates.Razor/RazorEmailTemplateSubject.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing.Templates.Razor
+{
+ using Microsoft.AspNetCore.Components;
+
+ ///
+ /// Base class of a Razor component which is used to generate the subject of an email.
+ ///
+ /// Type of the used to generate the subject of the e-mail.
+ public abstract class RazorEmailTemplateSubject : ComponentBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected RazorEmailTemplateSubject()
+ {
+ }
+
+ ///
+ /// Gets or sets the model used to generate the body of the e-mail.
+ ///
+ [Parameter]
+ public TModel Model { get; set; } = default!;
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/CHANGELOG.md b/src/Emailing/CHANGELOG.md
new file mode 100644
index 0000000..c76f51e
--- /dev/null
+++ b/src/Emailing/CHANGELOG.md
@@ -0,0 +1,2 @@
+1.0.0
+ - Initial release with Emailing infrastructure.
diff --git a/src/Emailing/Email.cs b/src/Emailing/Email.cs
new file mode 100644
index 0000000..4fe4b81
--- /dev/null
+++ b/src/Emailing/Email.cs
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ ///
+ /// Represents a templated e-mail to send.
+ ///
+ /// Type of data model injected to the to generate the e-mail.
+ public sealed class Email
+ {
+ ///
+ /// Initializes a new instance of the class
+ /// with the specified .
+ ///
+ /// The e-mail template used to generate the e-mail to send.
+ /// Thrown when the argument is .
+ public Email(EmailTemplate template)
+ {
+ ArgumentNullException.ThrowIfNull(template);
+
+ this.Template = template;
+
+ this.Importance = EmailImportance.Normal;
+ this.Recipients = [];
+ }
+
+ ///
+ /// Gets or sets the importance of the e-mail.
+ ///
+ public EmailImportance Importance { get; set; }
+
+ ///
+ /// Gets the collection of the recipients which the e-mail have to be send.
+ ///
+ public EmailRecipientCollection Recipients { get; }
+
+ ///
+ /// Gets the e-mail template used to generate the e-mail to send.
+ ///
+ public EmailTemplate Template { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailContact.cs b/src/Emailing/EmailContact.cs
new file mode 100644
index 0000000..4fb6655
--- /dev/null
+++ b/src/Emailing/EmailContact.cs
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Represents an e-mail contact (e-mail address and a display name).
+ ///
+ public class EmailContact
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The e-mail of the contact.
+ /// The display name of the contact (can be empty).
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public EmailContact(EmailAddress email, string displayName)
+ {
+ ArgumentNullException.ThrowIfNull(email);
+ ArgumentNullException.ThrowIfNull(displayName);
+
+ this.Email = email;
+ this.DisplayName = displayName;
+ }
+
+ ///
+ /// Gets the e-mail of the contact.
+ ///
+ public EmailAddress Email { get; }
+
+ ///
+ /// Gets the display name of the contact.
+ ///
+ public string DisplayName { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailImportance.cs b/src/Emailing/EmailImportance.cs
new file mode 100644
index 0000000..76e5f77
--- /dev/null
+++ b/src/Emailing/EmailImportance.cs
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ ///
+ /// Importance of the .
+ ///
+ public enum EmailImportance
+ {
+ ///
+ /// Low importance.
+ ///
+ Low = 5,
+
+ ///
+ /// Normal importance.
+ ///
+ Normal = 3,
+
+ ///
+ /// High importance.
+ ///
+ High = 1,
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailManager.cs b/src/Emailing/EmailManager.cs
new file mode 100644
index 0000000..2975047
--- /dev/null
+++ b/src/Emailing/EmailManager.cs
@@ -0,0 +1,95 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using Microsoft.Extensions.Options;
+ using PosInformatique.Foundations.Text.Templating;
+
+ internal sealed class EmailManager : IEmailManager
+ {
+ private readonly IOptions options;
+
+ private readonly IEmailProvider provider;
+
+ private readonly IServiceProvider serviceProvider;
+
+ public EmailManager(IOptions options, IEmailProvider provider, IServiceProvider serviceProvider)
+ {
+ if (options.Value.SenderEmailAddress is null)
+ {
+ throw new ArgumentException("Sender email address is required.", nameof(options));
+ }
+
+ this.options = options;
+ this.provider = provider;
+ this.serviceProvider = serviceProvider;
+ }
+
+ public Email Create(EmailTemplateIdentifier identifier)
+ {
+ ArgumentNullException.ThrowIfNull(identifier);
+
+ var template = this.options.Value.GetTemplate(identifier);
+
+ if (template is null)
+ {
+ throw new ArgumentException("Unable to find a template for the specified identifier.", nameof(identifier));
+ }
+
+ return new Email(template);
+ }
+
+ public async Task SendAsync(Email email, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(email);
+
+ var senderEmailAddress = this.options.Value.SenderEmailAddress!;
+
+ foreach (var recipient in email.Recipients)
+ {
+ // Render the subject
+ using var subjectOutputWriter = new StringWriter();
+
+ var textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider);
+
+ await email.Template.Subject.RenderAsync(recipient.Model, subjectOutputWriter, textTemplateRenderContext, cancellationToken);
+
+ var subject = subjectOutputWriter.ToString();
+
+ // Render the HTML content
+ using var htmlContentOutputWriter = new StringWriter();
+
+ textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider);
+
+ await email.Template.HtmlBody.RenderAsync(recipient.Model, htmlContentOutputWriter, textTemplateRenderContext, cancellationToken);
+
+ var htmlContent = htmlContentOutputWriter.ToString();
+
+ var message = new EmailMessage(
+ new EmailContact(senderEmailAddress, string.Empty),
+ new EmailContact(recipient.Address, recipient.DisplayName),
+ subject,
+ htmlContent)
+ {
+ Importance = email.Importance,
+ };
+
+ await this.provider.SendAsync(message, cancellationToken);
+ }
+ }
+
+ private sealed class TextTemplateRenderContext : ITextTemplateRenderContext
+ {
+ public TextTemplateRenderContext(IServiceProvider serviceProvider)
+ {
+ this.ServiceProvider = serviceProvider;
+ }
+
+ public IServiceProvider ServiceProvider { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailMessage.cs b/src/Emailing/EmailMessage.cs
new file mode 100644
index 0000000..1a5b583
--- /dev/null
+++ b/src/Emailing/EmailMessage.cs
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ ///
+ /// Represents an e-mail generated an can be send to the .
+ ///
+ public sealed class EmailMessage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The sender of the e-mail message.
+ /// The recipient of the e-mail message.
+ /// The subject of the e-mail message.
+ /// The HTML content of the e-mail message.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public EmailMessage(EmailContact from, EmailContact to, string subject, string htmlContent)
+ {
+ ArgumentNullException.ThrowIfNull(from);
+ ArgumentNullException.ThrowIfNull(to);
+ ArgumentNullException.ThrowIfNull(subject);
+ ArgumentNullException.ThrowIfNull(htmlContent);
+
+ this.From = from;
+ this.To = to;
+ this.Subject = subject;
+ this.HtmlContent = htmlContent;
+
+ this.Importance = EmailImportance.Normal;
+ }
+
+ ///
+ /// Gets the sender of the e-mail message.
+ ///
+ public EmailContact From { get; }
+
+ ///
+ /// Gets the recipient of the e-mail message.
+ ///
+ public EmailContact To { get; }
+
+ ///
+ /// Gets the subject of the e-mail message.
+ ///
+ public string Subject { get; }
+
+ ///
+ /// Gets the HTML content of the e-mail message.
+ ///
+ public string HtmlContent { get; }
+
+ ///
+ /// Gets or sets the importance of the e-mail message.
+ ///
+ public EmailImportance Importance { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailRecipient.cs b/src/Emailing/EmailRecipient.cs
new file mode 100644
index 0000000..a4de0fa
--- /dev/null
+++ b/src/Emailing/EmailRecipient.cs
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Represents a recipient of a to send.
+ ///
+ /// Data model injected to the to generate the e-mail.
+ public sealed class EmailRecipient
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// E-mail address of the recipient.
+ /// Display name of the recipient (can be empty).
+ /// Data model to apply on the to generate the e-mail for the recipient.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public EmailRecipient(EmailAddress address, string displayName, TModel model)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+ ArgumentNullException.ThrowIfNull(displayName);
+ ArgumentNullException.ThrowIfNull(model);
+
+ this.Address = address;
+ this.DisplayName = displayName;
+ this.Model = model;
+ }
+
+ ///
+ /// Gets the e-mail address of the recipient.
+ ///
+ public EmailAddress Address { get; }
+
+ ///
+ /// Gets the display name of the recipient.
+ ///
+ public string DisplayName { get; }
+
+ ///
+ /// Gets the data model to apply on the to generate the e-mail for the recipient.
+ ///
+ public TModel Model { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailRecipientCollection.cs b/src/Emailing/EmailRecipientCollection.cs
new file mode 100644
index 0000000..c2a1f30
--- /dev/null
+++ b/src/Emailing/EmailRecipientCollection.cs
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Represents a collection of to send
+ /// the .
+ ///
+ /// 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.
+ /// Display name of the recipient (can be empty).
+ /// 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 .
+ /// Thrown when the argument is .
+ public EmailRecipient Add(EmailAddress address, string displayName, TModel model)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+ ArgumentNullException.ThrowIfNull(displayName);
+ ArgumentNullException.ThrowIfNull(model);
+
+ var recipient = new EmailRecipient(address, displayName, model);
+
+ this.Add(recipient);
+
+ return recipient;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailTemplate.cs b/src/Emailing/EmailTemplate.cs
new file mode 100644
index 0000000..769293d
--- /dev/null
+++ b/src/Emailing/EmailTemplate.cs
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using PosInformatique.Foundations.Text.Templating;
+
+ ///
+ /// Represents an e-mail template used to generate e-mail to send to the .
+ ///
+ /// Type of data model injected to the and to generate the e-mail.
+ public class EmailTemplate
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The text template used to generate the subject of the e-mail to send.
+ /// The text template used to generate the HTML content of the e-mail to send.
+ /// Thrown when the argument is .
+ /// Thrown when the argument is .
+ public EmailTemplate(TextTemplate subject, TextTemplate htmlBody)
+ {
+ ArgumentNullException.ThrowIfNull(subject);
+ ArgumentNullException.ThrowIfNull(htmlBody);
+
+ this.Subject = subject;
+ this.HtmlBody = htmlBody;
+ }
+
+ ///
+ /// Gets the text template used to generate the subject of the e-mail to send.
+ ///
+ public TextTemplate Subject { get; }
+
+ ///
+ /// Gets the text template used to generate the HTML content of the e-mail to send.
+ ///
+ public TextTemplate HtmlBody { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailTemplateIdentifier.cs b/src/Emailing/EmailTemplateIdentifier.cs
new file mode 100644
index 0000000..f1eae00
--- /dev/null
+++ b/src/Emailing/EmailTemplateIdentifier.cs
@@ -0,0 +1,28 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ ///
+ /// Represents an unique identifier.
+ ///
+ /// Data model injected to the to generate the e-mail.
+ public sealed class EmailTemplateIdentifier
+ {
+ private EmailTemplateIdentifier()
+ {
+ }
+
+ ///
+ /// Creates a new identifier.
+ ///
+ /// A new identifier.
+ public static EmailTemplateIdentifier Create()
+ {
+ return new EmailTemplateIdentifier();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/Emailing.csproj b/src/Emailing/Emailing.csproj
new file mode 100644
index 0000000..b341ee0
--- /dev/null
+++ b/src/Emailing/Emailing.csproj
@@ -0,0 +1,38 @@
+
+
+
+ true
+
+
+ Provides a lightweight, template-based emailing infrastructure for .NET.
+ Allows registering strongly-typed email templates, instantiating emails from models, and sending them through pluggable providers (e.g. Azure Communication Service).
+
+
+ email;emailing;templating;razor;scriban;dotnet;posinformatique;azure;communication;service;provider;dependencyinjection
+
+
+ $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/CHANGELOG.md"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Emailing/EmailingBuilder.cs b/src/Emailing/EmailingBuilder.cs
new file mode 100644
index 0000000..f8dfc5c
--- /dev/null
+++ b/src/Emailing/EmailingBuilder.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Used to configure e-mailing feature.
+ ///
+ public sealed class EmailingBuilder
+ {
+ ///
+ /// Initializes a new instance of the class
+ /// to configure the e-mailing feature.
+ ///
+ /// The services being configured.
+ /// Thrown when the argument is .
+ public EmailingBuilder(IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ this.Services = services;
+ }
+
+ ///
+ /// Gets the services being configured.
+ ///
+ public IServiceCollection Services { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Emailing/EmailingOptions.cs b/src/Emailing/EmailingOptions.cs
new file mode 100644
index 0000000..4a0a517
--- /dev/null
+++ b/src/Emailing/EmailingOptions.cs
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) P.O.S Informatique. All rights reserved.
+//
+//-----------------------------------------------------------------------
+
+namespace PosInformatique.Foundations.Emailing
+{
+ using PosInformatique.Foundations.EmailAddresses;
+
+ ///
+ /// Represents the e-mailing feature options.
+ ///
+ public class EmailingOptions
+ {
+ private readonly Dictionary