CodeLogic.Mail
5.0.121
dotnet add package CodeLogic.Mail --version 5.0.121
NuGet\Install-Package CodeLogic.Mail -Version 5.0.121
<PackageReference Include="CodeLogic.Mail" Version="5.0.121" />
<PackageVersion Include="CodeLogic.Mail" Version="5.0.121" />
<PackageReference Include="CodeLogic.Mail" />
paket add CodeLogic.Mail --version 5.0.121
#r "nuget: CodeLogic.Mail, 5.0.121"
#:package CodeLogic.Mail@5.0.121
#addin nuget:?package=CodeLogic.Mail&version=5.0.121
#tool nuget:?package=CodeLogic.Mail&version=5.0.121
CodeLogic.Mail
SMTP sending and IMAP reading for CodeLogic 5 — with a built-in template engine, attachments, and RFC 2177 IDLE push notifications.
Compose messages with a fluent MailBuilder, send them over SMTP, render variable-substitution templates (conditionals, loops, and layouts), and read mailboxes over IMAP — including live IDLE push. Built on MailKit and MimeKit. Every fallible operation returns a CodeLogic Result / Result<T> whose Error has a stable mail.* code (see MailErrorCodes) instead of throwing on protocol, auth, or timeout errors.
Install
dotnet add package CodeLogic.Mail
Quick start
using CL.Mail;
await Libraries.LoadAsync<MailLibrary>();
await CodeLogic.ConfigureAsync();
await CodeLogic.StartAsync();
var mail = Libraries.Get<MailLibrary>();
// Compose with the fluent builder and send via SMTP.
var message = mail.CreateMessage()
.From("noreply@example.com", "My App")
.To("user@example.com")
.Subject("Welcome!")
.HtmlBody("<h1>Hello</h1><p>Welcome aboard.</p>")
.TextBody("Hello — welcome aboard.")
.Build();
Result<MailSendReceipt> result = await mail.Smtp.SendAsync(message);
if (result.IsFailure)
Console.WriteLine($"Send failed ({result.Error!.Code}): {result.Error.Message}");
else
Console.WriteLine($"Sent, Message-ID {result.Value!.MessageId}");
Features
- SMTP sending — plain-text and/or HTML bodies, CC/BCC, attachments, custom headers, and message priority. A fresh connection is opened per send (no pooling).
- Fluent
MailBuilder—mail.CreateMessage()composes an immutableMailMessage;Build()throws if sender, recipients, subject, or a body are missing. - Template engine — JSON-backed templates with
{{var}}/${var}/{var}substitution (HTML-encoded in HTML bodies;{{{var}}}for trusted HTML),{{#if}}…{{#else}}…{{/if}}conditionals,{{#each}}…{{/each}}loops, and{{#section}}/ layout composition. Values are inserted once: placeholders inside a value are never expanded. - Templated send —
SendTemplatedAsyncrenders a template by id and sends it in one call. - IMAP reading — list folders, fetch and page messages, fetch by UID with attachments (decoded up to
Imap.MaxAttachmentBytes, 25 MiB by default; a bigger one is returned asTooLargewith no content), fetch one attachment on demand (GetAttachmentAsync), search, move/copy/delete, and add/remove flags. - IMAP IDLE — RFC 2177 push notifications via
StartIdleAsyncand theNewMailReceivedevent. - Result-based errors — operations return CodeLogic
Result/Result<T>; sends returnResult<MailSendReceipt>(Message-ID + send time). Failures areErrorkinds (ValidationError,NotFoundError,UnauthorizedError,TimeoutError,UnavailableError,InternalError) with aMailErrorCodescode. An unexpected exception gives a generic message (Details= exception type); the exception goes to the log. Upgrading from 4.x: see the changelog.
Configuration
Auto-generated on first run as config.mail.json. The Imap section is optional — omit it to disable IMAP entirely.
{
"enabled": true,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"username": "noreply@example.com",
"password": "your-password-or-app-password",
"securityMode": "StartTls",
"timeoutSeconds": 30
},
"imap": {
"host": "imap.example.com",
"port": 993,
"username": "inbox@example.com",
"password": "your-password-or-app-password",
"securityMode": "SslTls",
"timeoutSeconds": 30,
"enableIdle": false,
"idleRefreshMinutes": 25,
"maxAttachmentBytes": 26214400
},
"defaultFromEmail": "noreply@example.com",
"defaultFromName": "My App",
"templateDirectory": "templates"
}
| Setting | Default | Description |
|---|---|---|
Enabled |
true |
Master switch; when false the services aren't created and health reports disabled. |
Smtp.Host / Smtp.Port |
smtp.example.com / 587 |
SMTP server and port. |
Smtp.Username / Smtp.Password |
"" |
SMTP credentials (use an app password where required). |
Smtp.SecurityMode |
StartTls |
None, StartTls (STARTTLS, port 587), or SslTls (implicit TLS, port 465); any other value is a config error. |
Smtp.TimeoutSeconds |
30 |
SMTP connect/operation timeout. |
Smtp.AttachmentBaseDirectory |
null |
When set, attachments must be files inside this directory. |
Imap |
null |
Optional; omit the whole section to disable IMAP. |
Imap.SecurityMode |
SslTls |
None, StartTls, or SslTls (implicit TLS, port 993). |
Imap.EnableIdle |
false |
Allow RFC 2177 IDLE push monitoring (StartIdleAsync is refused while false). |
Imap.IdleRefreshMinutes |
25 |
IDLE connection refresh interval (1–28; keep under 29). |
Imap.MaxAttachmentBytes |
26214400 (25 MiB) |
Largest attachment GetMessageAsync loads into memory; a bigger one is returned with TooLarge and no content. |
Imap.MaxMessageBytes |
33554432 (32 MiB) |
Combined decoded body and attachment bytes for one message fetch (1–134217728). |
Imap.MaxBodyBytes |
1048576 (1 MiB) |
Decoded bytes of one text or HTML body (1–16777216). |
Imap.MaxAttachments |
100 |
Attachment descriptors returned per message (1–1000). |
DefaultFromEmail / DefaultFromName |
null |
Fallback sender when a message from CreateMessage() / SendTemplatedAsync omits From. |
TemplateDirectory |
templates |
Template folder; relative to the library data dir unless absolute. |
Changing config while running
config.mail.json applies live when changed through Libraries.UpdateConfigAsync, Libraries.ReloadConfigAsync, or the config file watcher. Only what changed is rebuilt:
- SMTP settings:
mail.Smtpis replaced. Every send opens its own connection, so sends already running finish on the old settings. - Template directory:
mail.TemplateProviderandmail.TemplateEngineare replaced. - IMAP settings (added, changed or removed):
mail.Imapis replaced and the old connection is closed. Re-readmail.Imap— don't keep the old instance — and callStartIdleAsyncagain if you were idling. enabled: can be switched either way.
Other settings (default sender, ...) are read when used and need nothing rebuilt.
Documentation
Full guide: CL.Mail documentation. IMAP, IDLE, receive limits and templates: Receiving & Templates.
Requirements
- CodeLogic 5.0+ · .NET 11
- MailKit 4.x · MimeKit 4.x
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CodeLogic.Mail:
| Package | Downloads |
|---|---|
|
CodeLogic.WebLogic
Web application toolkit for CodeLogic 5 — routing, templates, forms, an ordered security pipeline, auth flows and realtime. No third-party dependencies beyond CodeLogic.*. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.121 | 83 | 11/10/2026 |
| 5.0.118-preview | 75 | 11/8/2026 |
| 5.0.117-preview | 77 | 11/7/2026 |
| 5.0.116-preview | 75 | 11/1/2026 |
| 5.0.115-preview | 130 | 11/1/2026 |
| 5.0.114-preview | 41 | 10/31/2026 |
| 5.0.113-preview | 46 | 10/31/2026 |
| 4.9.102 | 68 | 10/26/2026 |
| 4.9.98 | 590 | 10/25/2026 |
| 4.8.93 | 74 | 9/23/2026 |
| 4.8.87 | 77 | 9/13/2026 |
| 4.8.86 | 62 | 9/13/2026 |
| 4.8.85 | 63 | 9/12/2026 |
| 4.6.72 | 247 | 6/20/2026 |
| 4.6.69-preview | 68 | 6/20/2026 |
| 4.5.2 | 141 | 5/24/2026 |
| 4.5.2-preview.68 | 94 | 6/20/2026 |
| 4.5.1 | 187 | 5/24/2026 |
| 4.5.1-preview.56 | 119 | 5/24/2026 |
| 4.4.1 | 136 | 5/24/2026 |
# CL.Mail — Changelog
All notable changes to **CodeLogic.Mail** are documented here. Versions follow
[Semantic Versioning](https://semver.org/).
## 5.0 (2026-11-10)
### Breaking
- Targets **.NET 11** (`net11.0`) and requires **CodeLogic 5.0** (`[5.0.0, 6.0.0)`). On .NET 10, stay on
4.9.x. CodeLogic 5 makes `Result<T>` / `Result` C# 15 unions and `Error` a closed record with
kind subtypes (`NotFoundError`, `ValidationError`, ...).
- **`MailResult`, `MailResult<T>` and the `MailError` enum are removed.** Every operation now returns
CodeLogic's `Result` / `Result<T>` and fails with an `Error` whose `Code` is a `MailErrorCodes`
constant:
| Old | New |
|---|---|
| `Task<MailResult> SmtpService.SendAsync(...)` | `Task<Result<MailSendReceipt>>` |
| `Task<MailResult> MailLibrary.SendTemplatedAsync(...)` | `Task<Result<MailSendReceipt>>` |
| `MailResult` (IMAP connect/disconnect/folder/message ops, `SaveTemplateAsync`) | `Result` |
| `MailResult<T>` (fetch, search, list, load, `RenderAsync`) | `Result<T>` |
| `MailResult.MessageId` | `MailSendReceipt.MessageId` (via `result.Value`) |
| `MailResult.Timestamp` | `MailSendReceipt.SentAtUtc` on sends; dropped elsewhere |
| `result.Error` (`MailError` enum) | `result.Error?.Code` (`MailErrorCodes.*` string) or the `Error` kind |
| `result.ErrorMessage` | `result.Error?.Message` |
| `MailError.None` | `result.IsSuccess` / `result.Error is null` |
`IMailTemplateProvider` and `IMailTemplateEngine` implementations must change their return types
the same way.
Codes and kinds (each code always has the same kind):
| `MailErrorCodes` constant | Code | `Error` kind |
|---|---|---|
| `InvalidRecipient` | `mail.invalid_recipient` | `ValidationError` |
| `InvalidSender` | `mail.invalid_sender` | `ValidationError` |
| `InvalidSubject` | `mail.invalid_subject` | `ValidationError` |
| `InvalidBody` (new) | `mail.invalid_body` | `ValidationError` |
| `InvalidHeader` (new) | `mail.invalid_header` | `ValidationError` |
| `TemplateNotFound` | `mail.template_not_found` | `NotFoundError` |
| `InvalidTemplateId` (new) | `mail.invalid_template_id` | `ValidationError` |
| `TemplateRenderingFailed` | `mail.template_rendering_failed` | `InternalError` |
| `SmtpConfigInvalid` | `mail.smtp_config_invalid` | `ValidationError` |
| `SmtpAuthenticationFailed` | `mail.smtp_authentication_failed` | `UnauthorizedError` |
| `SmtpTimeout` | `mail.smtp_timeout` | `TimeoutError` |
| `SmtpConnectionFailed` (new) | `mail.smtp_connection_failed` | `UnavailableError` |
| `SmtpRejected` | `mail.smtp_rejected` | `ValidationError` (`Details` has the SMTP status) |
| `SmtpError` | `mail.smtp_error` | `InternalError` |
| `AttachmentNotFound` | `mail.attachment_not_found` | `NotFoundError` |
| `InvalidAttachment` (new) | `mail.invalid_attachment` | `ValidationError` |
| `ImapConfigInvalid` | `mail.imap_config_invalid` | `ValidationError` |
| `ImapAuthenticationFailed` | `mail.imap_authentication_failed` | `UnauthorizedError` |
| `ImapTimeout` | `mail.imap_timeout` | `TimeoutError` |
| `ImapConnectionFailed` (new) | `mail.imap_connection_failed` | `UnavailableError` |
| `ImapError` | `mail.imap_error` | `InternalError` |
| `ImapRejected` (new) | `mail.imap_rejected` | `ValidationError` (`Details` has the IMAP response) |
| `FolderNotFound` | `mail.folder_not_found` | `NotFoundError` |
| `MessageNotFound` | `mail.message_not_found` | `NotFoundError` |
| `Cancelled` (new) | `mail.cancelled` | `UnavailableError` |
| `Unknown` | `mail.unknown` | `InternalError` |
`SmtpRejected` is a `ValidationError` because the server refused something about this message
(sender, recipient, size, content), not because the service is broken; `Details` carries the
SMTP status code. Cancellation previously reported `MailError.Unknown` (or
`TemplateRenderingFailed` while rendering) and is now `mail.cancelled`.
Checking a send result:
```csharp
// 4.x
MailResult r = await mail.Smtp.SendAsync(message);
if (!r.IsSuccess) log.Warn($"Mail failed ({r.Error}): {r.ErrorMessage}");
else log.Info($"Mail sent {r.MessageId}");
// 5.0
Result<MailSendReceipt> r = await mail.Smtp.SendAsync(message);
if (r.IsFailure) log.Warn($"Mail failed ({r.Error!.Code}): {r.Error.Message}");
else log.Info($"Mail sent {r.Value!.MessageId}");
// or by kind
if (r.Error is TimeoutError or UnavailableError) { /* retry later */ }
```
- **`SendAsync` validates more before connecting.** Malformed sender or recipient (To/Cc/Bcc)
addresses fail with `mail.invalid_sender` / `mail.invalid_recipient` instead of an unexpected
exception, an empty body is `mail.invalid_body` (was `MailError.InvalidRecipient`), and an
attachment path that does not exist fails with `mail.attachment_not_found` (was silently dropped).
- **Custom headers, the subject and the sender name are validated.** A header name that is empty, not
printable US-ASCII, or contains a space or `:` used to make MimeKit throw, which surfaced as
`mail.unknown` (`InternalError`); it is now `mail.invalid_header` (`ValidationError`). The same code
rejects a header value with CR, LF or NUL, and a custom header the service writes itself (From, To, Cc,
Bcc, Subject, MIME-Version, Importance, Content-*). A `Subject` or `FromName` containing CR or LF fails
with `mail.invalid_subject` / `mail.invalid_sender` instead of being sent.
- An unreachable SMTP/IMAP server (`SocketException`) is now `mail.smtp_connection_failed` /
`mail.imap_connection_failed` (`UnavailableError`) instead of `Unknown` / `ImapError`.
- A template file that deserializes to nothing is `mail.template_rendering_failed` (was
`TemplateNotFound`).
- **Attachment paths must be fully qualified.** A relative path fails with the new code
`mail.invalid_attachment` (`ValidationError`) instead of being resolved against the working directory.
- **More headers are reserved.** Custom headers can no longer set `Sender`, `Reply-To`, `Date`, `Message-Id`,
`Return-Path`, `Received` or `Resent-*` (in addition to From/To/Cc/Bcc/Subject/MIME-Version/Importance/
Content-*); use the new `MailMessage.ReplyTo` / `MailBuilder.ReplyTo(...)` for a reply address.
- **`Imap.EnableIdle` is enforced.** `StartIdleAsync` fails with `mail.imap_config_invalid` while it is `false`
(the default); it was documented as the switch but never read.
- **IMAP failures keep their kind.** Every IMAP operation reported `mail.imap_error` (`InternalError`) for any
exception. Now an unreachable server, dropped connection or TLS failure is `mail.imap_connection_failed`,
bad credentials `mail.imap_authentication_failed`, a timeout `mail.imap_timeout`, a missing folder or message
`mail.folder_not_found` / `mail.message_not_found`, cancellation `mail.cancelled`, and a `NO`/`BAD` answer the
new `mail.imap_rejected` (`ValidationError`). A reconnect inside an operation reports the real cause instead of
an `InvalidOperationException` wrapped in `mail.imap_error`.
### Security
- **Template IDs can no longer escape the template directory.** `FileMailTemplateProvider` combined the
directory with the raw ID, so `../x`, `sub/x` or a rooted path could load, or overwrite on save, a JSON
file outside `TemplateDirectory`. IDs must now be file-name-safe (ASCII letters, digits, `-`, `_`, `.`;
starting with a letter or digit; at most 128 characters), and the resolved path is checked to sit
directly in the template directory. Anything else fails with the new code
`MailErrorCodes.InvalidTemplateId` (`mail.invalid_template_id`, `ValidationError`) for load, save and
layout lookup. `FileMailTemplateProvider.IsValidTemplateId` lets callers check an ID up front.
- **Unknown `SecurityMode` values fail closed.** A value outside `None`/`StartTls`/`SslTls` (e.g. a number in
the JSON) fell back to MailKit's `Auto` (opportunistic TLS, plaintext if the server offers no STARTTLS). It is
now a configuration error: `MailConfig.Validate` refuses it, `SendAsync` fails with `mail.smtp_config_invalid`
and IMAP with `mail.imap_config_invalid`.
- **`Smtp.AttachmentBaseDirectory`** (new, optional): when set, attachments outside it (after resolving `..`
and link targets) fail with `mail.invalid_attachment`.
- **The IMAP connection is serialized.** IDLE and public calls shared one `ImapClient` without a lock, which
MailKit does not support. Calls now take the connection one at a time; a call made while IDLE runs ends the
IDLE command gracefully, runs, and IDLE resumes.
### Fixed
- **Unexpected errors no longer echo exception text to callers.** `mail.unknown` (SMTP catch-all, template
listing and saving), `mail.smtp_error`, `mail.imap_error` (unexpected IMAP exceptions) and
`mail.template_rendering_failed` (load and render failures) returned `ex.Message` as the error message and the
exception type as `Details`. They now carry a fixed message; `Details` is the exception's type name and the
exception itself is logged. IMAP server answers (`mail.imap_rejected`, missing folder or message names) are
unchanged.
- **`MailMessage` tolerates `null` collections.** `Cc`, `Bcc`, `ReplyTo`, `Attachments` and `Headers` set to
`null` (through `init` or `with`) made `SendAsync` throw `NullReferenceException` during validation; they now
coalesce to empty, and a `null` `To` is the validation error `mail.invalid_recipient`.
- **IMAP attachments are decoded with a memory bound.** `GetMessageAsync` decoded every attachment into memory
whatever its size. The new `Imap.MaxAttachmentBytes` (default 25 MiB) caps what is buffered: an attachment above
it comes back with `ReceivedAttachment.TooLarge = true`, its full decoded `Size`, and `Content = null`; the
message itself is still returned and the decoder never holds more than the limit.
- **`DefaultFromEmail` / `DefaultFromName` are used.** They were documented as the fallback sender but never
read, so `Build()` threw without `From()`. `MailLibrary.CreateMessage()` and `SendTemplatedAsync` now return
builders that fall back to them; the new `MailBuilder(defaultFromEmail, defaultFromName)` constructor does the
same for builders you create. An invalid `DefaultFromEmail` (or a `DefaultFromName` with a line break) fails
configuration validation.
- **IDLE survives a dropped connection.** After a reconnect the loop kept using the folder of the disposed
client, so push notifications died silently after the first server drop. The folder is now resolved again on
the new connection, and messages that arrived in between are reported.
- **`ConnectAsync` no longer leaks the previous connection**: an existing client is disconnected and disposed
before the new one is created, and a client whose connect or login fails is disposed.
- **Live config changes are validated by the library too.** An invalid configuration that reaches
`OnConfigChangedAsync` is logged and ignored; later changes are compared with the configuration running.
### Added
- `MailMessage.ReplyTo` and `MailBuilder.ReplyTo(email)`.
- `MailBuilder(string? defaultFromEmail, string? defaultFromName = null)`.
- `SmtpConfig.AttachmentBaseDirectory`.
- `MailErrorCodes.InvalidAttachment` (`mail.invalid_attachment`) and `MailErrorCodes.ImapRejected` (`mail.imap_rejected`).
- `ImapConfig.MaxAttachmentBytes` (default `ImapConfig.DefaultMaxAttachmentBytes`, 25 MiB; must be at least 1) and
`ReceivedAttachment.TooLarge`.
### Documentation
- Configuration examples in the README and the docs use the camelCase keys the CodeLogic loader reads.
The PascalCase keys shown before are matched case-sensitively and were ignored, leaving defaults active.
- The live config section no longer says "CodeLogic 4.9+". The README lists `Imap.MaxMessageBytes`,
`MaxBodyBytes` and `MaxAttachments`, and `GetAttachmentAsync`. The docs list `GetAttachmentAsync` and
`ReceivedMessage.ContentTruncated`, show `CreatedAt` as `DateTime`, say `MailPriority` sets only the
`Importance` header, and switch on the result union in the error example. The README's trailing relative
link, which broke on nuget.org, is replaced by a link to the Receiving & Templates page.
- The `Enabled` setting's description no longer mentions inbox polling (there is none) or no-op operations:
when it is off, the `Smtp` / `Imap` accessors throw. The `InvalidHeader` code lists every reserved header.
### Changed
- Fetch bounded MIME sections instead of whole messages, cap aggregate decoded message bytes/body bytes/attachment counts, and add GetAttachmentAsync for on-demand content. Return ContentTruncated and part identifiers on descriptors.
- Package versions are pinned (MailKit and MimeKit 4.18.2) instead of floating (`4.*`), so a build restores the same dependencies every time.
- `SimpleTemplateEngine` uses source-generated regexes (`[GeneratedRegex]`) for its block, section and
placeholder patterns instead of parsing them through the static `Regex` cache on each render. Output is
unchanged.
- `ImapService` is split into partial files by concern (connection and error mapping, folders, messages, search,
IDLE). No behaviour change.
## 2026-10-27
### Security
- **HTML bodies encode substituted values.** `SimpleTemplateEngine` inserted every value into
`HtmlBody` as-is, so a user-supplied value (a user name, a browser description) could inject
markup or links into an e-mail. In `HtmlBody` and HTML layouts, `{{var}}`, `${var}` and `{var}`
are now HTML-encoded. Subjects and text bodies are unchanged.
- **Values are inserted once.** Substitution ran the three placeholder syntaxes one after another,
and loop output was substituted again by the outer pass, so a value containing `${other}` or
`{other}` was expanded: user text could pull other variables (links, tokens) into the message.
All syntaxes are now replaced in a single pass and nothing inside a value is expanded.
### Added
- **`{{{var}}}`** inserts a value without encoding, for values that are trusted HTML.
### Fixed
- **Text layouts get the text sections.** With a layout, the text layout was rendered with the
child's *HTML* sections (including `body`). Text and HTML layouts now each receive their own
sections, and a child's section wins over a variable of the same name.
### Upgrade notes
- Templates that pass ready-made HTML in a variable must switch that placeholder to `{{{var}}}`.
## 2026-10-24
### Added
- **Live config changes** (requires CodeLogic 4.9+). `MailLibrary` implements `IConfigReloadable`: a
change to `config.mail.json` made through `Libraries.UpdateConfigAsync`, `Libraries.ReloadConfigAsync`
or the config file watcher is applied without a restart, rebuilding only what changed. Changed SMTP
settings replace `Smtp` (sends in flight finish on the old settings); a changed template directory
replaces `TemplateProvider`/`TemplateEngine`; changed IMAP settings replace `Imap` and close the old
connection (re-read `Imap` and restart IDLE). `Enabled` can be switched either way.
## 2026-09-12
### Changed
- Unified the version line with the CodeLogic framework on **4.8.x**. Every official
library and the framework now share one `major.minor`, so a given `4.8.<patch>`
means the same generation across all packages.
- `version.txt` moved from `4.6` to `4.8`. The patch component remains the CI run
number, composed at pack time; `AssemblyVersion` stays pinned at `Major.Minor.0.0`
(now `4.8.0.0`) so every patch in the line loads interchangeably.
## 2026-06-20
### Documentation
- Full rewrite of the README and the multi-page docs set to the CodeLogic.Libs
house style. The README is now concise, Markdown-only, and NuGet-safe; the
guide is split into **Overview & Sending** (`docs/libs/mail/index.md`) and
**Receiving & Templates** (`docs/libs/mail/receiving-templates.md`), with the
old `docs/libs/mail.md` reduced to a redirect stub and the docs navigation
updated accordingly. No API changes.
## [4.5.1] — 2026-06-20
### Documentation
- Rewrote the README and the Mail & Templates guide to match the current public
API. The previous docs described members that no longer exist (`EmailMessage`,
`EmailAttachment`, `ReplyTo`, `RenderTemplateAsync`, `ReadInboxAsync`,
`ImapFilter`, `MarkAsReadAsync`, the `SendAsync(from:, to:, …)` /
`SendTemplateAsync` overloads, and the `UseSsl` / `FromAddress` config keys).
- Documented the real surface: the fluent `MailBuilder` (`CreateMessage()`),
`SmtpService.SendAsync(MailMessage)`, `MailLibrary.SendTemplatedAsync`, the
`MailResult` / `MailError` result model, and the JSON-backed template system
(`MailTemplate`, `IMailTemplateProvider`, `IMailTemplateEngine`).
- Documented previously undocumented features: template conditionals
(`{{#if}}`/`{{#else}}`), loops (`{{#each}}`), sections and layouts; the
`${var}` / `{var}` placeholder syntaxes; the full IMAP API (fetch/page, fetch
by UID, search via `ImapSearchCriteria`, move/copy/delete, flag operations,
folder management); and RFC 2177 IMAP IDLE push (`StartIdleAsync` /
`NewMailReceived`) with its `EnableIdle` / `IdleRefreshMinutes` config keys.
- Corrected the documented config schema and `SecurityMode` values
(`None` / `StartTls` / `SslTls`).
## [4.5.0] — 2026-05-24
### Changed
- **Unified versioning.** All CodeLogic.Libs now share a single version line
controlled by `version.txt` in the repo root. This is a version alignment
release — no functional changes to this library.
## [4.0.4] — 2026-04-16
### Changed
- README + manifest refresh for the v4 baseline. No functional changes vs 4.0.3.
- `LibraryManifest.Version` now reads from assembly metadata.
## [4.0.2] — 2026-04-09
### Changed
- Annotated mail configuration with `[ConfigField]` for the admin UI surface.
- Aligned with the v4 baseline across all libraries.
### Fixed
- Resolved null-reference warnings in `ImapService`.
## [4.0.0] — 2026-04-09
Major rewrite. Republished as v4.0.0 to reset the version line under the
unified v4 baseline. SMTP / IMAP services + a CodeLogic-native template
provider.
### Notes
- Earlier history is retained in the
[git log](https://github.com/zyntal-com/CodeLogic.Libs/commits/main/CL.Mail).