This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git


The following commit(s) were added to refs/heads/master by this push:
     new 3671c78d log4net.Ext.Mail (#302)
3671c78d is described below

commit 3671c78d2d92a58ad5610dfbffe784739f9169f0
Author: Jan Friedrich <[email protected]>
AuthorDate: Tue Aug 4 10:05:37 2026 +0200

    log4net.Ext.Mail (#302)
    
    * #300 create new assemblies for SmtpAppender
    
    * add MailKit based SmtpAppender to log4net.Ext.Mail
    
    log4net.Ext.Mail.Appender.SmtpAppender exposes the same options as
    log4net.Appender.SmtpAppender, but sends via MailKit instead of the obsolete
    System.Net.Mail.SmtpClient. Sending goes through the new ISmtpTransport
    abstraction (default: MailKitSmtpTransport) so the appender can be tested
    without an SMTP server.
    
    * add tests for the MailKit based SmtpAppender
    
    * - TelnetAppender: register client before sending welcome banner
      (log messages could be dropped via HasConnections race)
    - test: free TCP port, locked receive buffer, assert on stream
      content instead of read count, 30s timeout, log client errors
    
    * Move ISmtpTransport and MailKitSmtpTransport to Appender/Internal
    
    * Make the MailKit transport internal
    
    * document the MailKit based SmtpAppender
    
    Recommend the MailKit appender from log4net.Ext.Mail over the built-in one,
    which relies on the deprecated System.Net.Mail.SmtpClient, and port all
    configuration samples to the new type. Adds the changelog entry for #300.
    
    * simplify type in return statement
    
    * fix PackageVersion for MauiTestApplication
    
    * Deprecate the built-in SmtpAppender and release log4net.Ext.Mail #300
---
 examples/Maui/MauiTestApplication.csproj           |   6 +
 scripts/build-preview.ps1                          |   3 +-
 scripts/build-release.ps1                          |   4 +-
 scripts/update-version.ps1                         |   2 +-
 src/Directory.Build.props                          |   2 +
 .../3.3.3/300-add-mailkit-based-smtpappender.xml   |  12 +
 src/log4net.Ext.Mail.Tests/.editorconfig           |   2 +
 .../Appender/FakeSmtpTransport.cs                  | 173 +++++++
 .../Appender/SmtpAppenderTest.cs                   | 547 +++++++++++++++++++++
 .../log4net.Ext.Mail.Tests.csproj                  |  31 ++
 .../Appender/Internal/ISmtpTransport.cs            |  87 ++++
 .../Appender/Internal/MailKitSmtpTransport.cs      |  59 +++
 .../Appender/SmtpAppender.cs                       | 302 ++++++++----
 .../log4net.Ext.Mail.csproj}                       |  78 ++-
 .../Appender/Internal/SimpleTelnetClient.cs        |  54 +-
 src/log4net.Tests/Appender/TelnetAppenderTest.cs   |  98 +++-
 src/log4net.sln                                    |  16 +-
 src/log4net/Appender/SmtpAppender.cs               |  10 +
 src/log4net/Appender/TelnetAppender.cs             |   6 +-
 src/log4net/Util/Log4NetAssert.cs                  |  29 +-
 src/log4net/log4net.csproj                         |   1 -
 src/site/antora/modules/ROOT/pages/features.adoc   |   1 +
 .../ROOT/pages/manual/configuration/appenders.adoc |   1 +
 .../configuration/appenders/smtpappender.adoc      | 190 ++++++-
 .../appenders/smtppickupdirappender.adoc           |   3 +
 .../ROOT/pages/manual/supported-frameworks.adoc    |   4 +
 26 files changed, 1515 insertions(+), 206 deletions(-)

diff --git a/examples/Maui/MauiTestApplication.csproj 
b/examples/Maui/MauiTestApplication.csproj
index aa29a009..c006ad17 100644
--- a/examples/Maui/MauiTestApplication.csproj
+++ b/examples/Maui/MauiTestApplication.csproj
@@ -30,6 +30,12 @@
     <!-- Versions -->
     <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
     <ApplicationVersion>1</ApplicationVersion>
+    <!--
+      The Android SDK derives PackageVersion from ApplicationDisplayVersion, 
while the
+      TargetFramework-less evaluation defaults it to $(Version) from 
Directory.Build.props.
+      NuGet requires one value across all target frameworks (NU1105), so pin 
it here.
+    -->
+    <PackageVersion>$(ApplicationDisplayVersion)</PackageVersion>
 
     <!-- To develop, package, and publish an app to the Microsoft Store, see: 
https://aka.ms/MauiTemplateUnpackaged -->
     <WindowsPackageType>None</WindowsPackageType>
diff --git a/scripts/build-preview.ps1 b/scripts/build-preview.ps1
index d0e9f1ca..169cefe8 100644
--- a/scripts/build-preview.ps1
+++ b/scripts/build-preview.ps1
@@ -3,9 +3,10 @@ param(
   $Preview = '1'
 )
 'building ...'
-dotnet build -c Release 
"-p:GeneratePackages=true;PackageVersion=$Version-preview.$Preview" 
$PSScriptRoot/../src/log4net/log4net.csproj
+dotnet build -c Release 
"-p:GeneratePackages=true;PackageVersion=$Version-preview.$Preview" 
$PSScriptRoot/../src/log4net.sln
 'signing ...'
 gpg --armor --output 
$PSScriptRoot\..\build\artifacts\log4net.$Version-preview.$Preview.nupkg.asc 
--detach-sig 
$PSScriptRoot\..\build\artifacts\log4net.$Version-preview.$Preview.nupkg
+gpg --armor --output 
$PSScriptRoot\..\build\artifacts\log4net.Ext.Mail.$Version-preview.$Preview.nupkg.asc
 --detach-sig 
$PSScriptRoot\..\build\artifacts\log4net.Ext.Mail.$Version-preview.$Preview.nupkg
 'create tag?'
 pause
 'creating tag ...'
diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1
index 2bffde8e..b28e5b87 100644
--- a/scripts/build-release.ps1
+++ b/scripts/build-release.ps1
@@ -22,7 +22,7 @@ function Write-HashAndSignature
 "cleaning $PSScriptRoot/../build/ ..." 
 Remove-Item $PSScriptRoot/../build/ -Force -Recurse -ErrorAction 
SilentlyContinue
 'building ...'
-dotnet test -c Release "-p:GeneratePackages=true;PackageVersion=$Version" 
$PSScriptRoot/../src/log4net/log4net.csproj
+dotnet test -c Release "-p:GeneratePackages=true;PackageVersion=$Version" 
$PSScriptRoot/../src/log4net.sln
 'compressing source ...'
 pushd $PSScriptRoot/..
 git archive --format=zip --output 
$PSScriptRoot/../build/artifacts/apache-log4net-source-$Version.zip master
@@ -37,6 +37,8 @@ popd
 'signing ...'
 Move-Item $PSScriptRoot/../build/artifacts/log4net.$Version.nupkg 
$PSScriptRoot/../build/artifacts/apache-log4net.$Version.nupkg
 Write-HashAndSignature 
$PSScriptRoot/../build/artifacts/apache-log4net.$Version.nupkg
+Move-Item $PSScriptRoot/../build/artifacts/log4net.Ext.Mail.$Version.nupkg 
$PSScriptRoot/../build/artifacts/apache-log4net.Ext.Mail.$Version.nupkg
+Write-HashAndSignature 
$PSScriptRoot/../build/artifacts/apache-log4net.Ext.Mail.$Version.nupkg
 Write-HashAndSignature 
$PSScriptRoot/../build/artifacts/apache-log4net-source-$Version.zip
 Write-HashAndSignature 
$PSScriptRoot/../build/artifacts/apache-log4net-binaries-$Version.zip
 Write-HashAndSignature $PSScriptRoot/../build/artifacts/verify-release.ps1
diff --git a/scripts/update-version.ps1 b/scripts/update-version.ps1
index 5e7f35d5..fd9329e2 100644
--- a/scripts/update-version.ps1
+++ b/scripts/update-version.ps1
@@ -52,7 +52,7 @@ Update-TextVersion 
$PSScriptRoot/../doc/MailTemplate.Result.txt $OldVersion $New
 Update-TextVersion $PSScriptRoot/../doc/MailTemplate.Announce.txt $OldVersion 
$NewVersion
 Update-TextVersion $PSScriptRoot/build-preview.ps1 $OldVersion $NewVersion
 Update-TextVersion $PSScriptRoot/build-release.ps1 $OldVersion $NewVersion
-Update-XmlVersion $PSScriptRoot/../src/log4net/log4net.csproj $NewVersion 
'/Project/PropertyGroup/Version'
+Update-XmlVersion $PSScriptRoot/../src/Directory.Build.props $NewVersion 
'/Project/PropertyGroup/VersionPrefix'
 Update-XmlVersion $PSScriptRoot/../src/Directory.Build.props $OldVersion 
'/Project/PropertyGroup/Log4NetPackageVersion'
 Update-XmlVersion $PSScriptRoot/../examples/Directory.Build.props $OldVersion 
'/Project/PropertyGroup/Version'
 Update-TextVersion 
$PSScriptRoot/../src/site/antora/modules/ROOT/partials/supported-versions.adoc 
$OldVersion $NewVersion
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 4cdce339..36f673f7 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -14,7 +14,9 @@
     <SatelliteResourceLanguages>en;en-US</SatelliteResourceLanguages>
   </PropertyGroup>
   <PropertyGroup Label="Package Versions">
+    <VersionPrefix>3.3.3</VersionPrefix>
     <Log4NetPackageVersion>3.3.2</Log4NetPackageVersion>
+    <MailKitPackageVersion>4.17.0</MailKitPackageVersion>
     
<SystemConfigurationConfigurationManagerPackageVersion>4.5.0</SystemConfigurationConfigurationManagerPackageVersion>
     
<MicrosoftSourceLinkGitHubPackageVersion>8.0.0</MicrosoftSourceLinkGitHubPackageVersion>
     <!-- Analyzer packages -->
diff --git a/src/changelog/3.3.3/300-add-mailkit-based-smtpappender.xml 
b/src/changelog/3.3.3/300-add-mailkit-based-smtpappender.xml
new file mode 100644
index 00000000..5e15492e
--- /dev/null
+++ b/src/changelog/3.3.3/300-add-mailkit-based-smtpappender.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="https://logging.apache.org/xml/ns";
+       xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="added">
+  <issue id="300" link="https://github.com/apache/logging-log4net/issues/300"/>
+  <issue id="302" link="https://github.com/apache/logging-log4net/pull/302"/>
+  <description format="asciidoc">Add a MailKit based `SmtpAppender` in the new 
`log4net.Ext.Mail` assembly
+  and mark `log4net.Appender.SmtpAppender` as obsolete, because Microsoft no 
longer recommends
+  `System.Net.Mail.SmtpClient` for new development
+  (requested by @DietzeC, implemented by @FreeAndNil in 
https://github.com/apache/logging-log4net/pull/302[#302])</description>
+</entry>
\ No newline at end of file
diff --git a/src/log4net.Ext.Mail.Tests/.editorconfig 
b/src/log4net.Ext.Mail.Tests/.editorconfig
new file mode 100644
index 00000000..fb1bc15f
--- /dev/null
+++ b/src/log4net.Ext.Mail.Tests/.editorconfig
@@ -0,0 +1,2 @@
+# CA1861: Avoid constant arrays as arguments
+dotnet_diagnostic.CA1861.severity = none
\ No newline at end of file
diff --git a/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs 
b/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs
new file mode 100644
index 00000000..7b268952
--- /dev/null
+++ b/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs
@@ -0,0 +1,173 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+using log4net.Ext.Mail.Appender;
+using log4net.Ext.Mail.Appender.Internal;
+
+using MailKit.Security;
+
+using MimeKit;
+
+namespace log4net.Ext.Mail.Tests.Appender;
+
+/// <summary>
+/// An <see cref="ISmtpTransport"/> that records what the appender asked it to 
do
+/// instead of talking to an SMTP server.
+/// </summary>
+internal sealed class FakeSmtpTransport : ISmtpTransport
+{
+  /// <summary>
+  /// The names of the transport methods that were called, in order.
+  /// </summary>
+  internal List<string> Calls { get; } = [];
+
+  internal string? ConnectedHost { get; private set; }
+
+  internal int ConnectedPort { get; private set; }
+
+  internal SecureSocketOptions SecureSocketOptions { get; private set; }
+
+  internal ICredentials? Credentials { get; private set; }
+
+  internal SaslMechanism? SaslMechanism { get; private set; }
+
+  /// <summary>
+  /// A snapshot of every message passed to <see cref="Send"/>, taken before 
the
+  /// appender disposes the <see cref="MimeMessage"/>.
+  /// </summary>
+  internal List<SentMail> SentMails { get; } = [];
+
+  internal bool DisconnectedWithQuit { get; private set; }
+
+  internal bool IsDisposed { get; private set; }
+
+  /// <summary>
+  /// When set, <see cref="Send"/> throws this instead of recording the 
message.
+  /// </summary>
+  internal Exception? SendException { get; set; }
+
+  public bool IsConnected { get; private set; }
+
+  public bool IsAuthenticated { get; private set; }
+
+  public void Connect(string host, int port, SecureSocketOptions 
secureSocketOptions)
+  {
+    Calls.Add(nameof(Connect));
+    ConnectedHost = host;
+    ConnectedPort = port;
+    SecureSocketOptions = secureSocketOptions;
+    IsConnected = true;
+  }
+
+  public void Authenticate(ICredentials credentials)
+  {
+    Calls.Add(nameof(Authenticate));
+    Credentials = credentials;
+    IsAuthenticated = true;
+  }
+
+  public void Authenticate(SaslMechanism mechanism)
+  {
+    Calls.Add(nameof(Authenticate));
+    SaslMechanism = mechanism;
+    IsAuthenticated = true;
+  }
+
+  public void Send(MimeMessage message)
+  {
+    Calls.Add(nameof(Send));
+    if (SendException is Exception exception)
+    {
+      throw exception;
+    }
+    SentMails.Add(new SentMail(message));
+  }
+
+  public void Disconnect(bool quit)
+  {
+    Calls.Add(nameof(Disconnect));
+    DisconnectedWithQuit = quit;
+    IsConnected = false;
+  }
+
+  public void Dispose()
+  {
+    Calls.Add(nameof(Dispose));
+    IsDisposed = true;
+  }
+}
+
+/// <summary>
+/// The parts of a <see cref="MimeMessage"/> the tests assert on, captured 
eagerly
+/// because the appender disposes the message once it has been sent.
+/// </summary>
+internal sealed class SentMail
+{
+  internal SentMail(MimeMessage message)
+  {
+    From = Addresses(message.From);
+    To = Addresses(message.To);
+    Cc = Addresses(message.Cc);
+    Bcc = Addresses(message.Bcc);
+    ReplyTo = Addresses(message.ReplyTo);
+    Subject = message.Subject;
+    Priority = message.Priority;
+
+    Header? subjectHeader = message.Headers.FirstOrDefault(h => h.Id == 
HeaderId.Subject);
+    RawSubjectHeader = subjectHeader is null
+      ? string.Empty
+      : Encoding.ASCII.GetString(subjectHeader.RawValue);
+
+    TextPart? textPart = message.Body as TextPart;
+    Body = textPart?.Text ?? string.Empty;
+    BodyCharset = textPart?.ContentType.Charset;
+  }
+
+  internal string[] From { get; }
+
+  internal string[] To { get; }
+
+  internal string[] Cc { get; }
+
+  internal string[] Bcc { get; }
+
+  internal string[] ReplyTo { get; }
+
+  internal string? Subject { get; }
+
+  /// <summary>
+  /// The on-the-wire subject header, so that tests can check the applied 
charset.
+  /// </summary>
+  internal string RawSubjectHeader { get; }
+
+  internal MessagePriority Priority { get; }
+
+  internal string Body { get; }
+
+  internal string? BodyCharset { get; }
+
+  private static string[] Addresses(InternetAddressList list)
+    => list.Mailboxes.Select(m => m.Address).ToArray();
+}
diff --git a/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs 
b/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs
new file mode 100644
index 00000000..5a7117c0
--- /dev/null
+++ b/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs
@@ -0,0 +1,547 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Mail;
+using System.Text;
+using log4net.Core;
+using log4net.Ext.Mail.Appender;
+using log4net.Ext.Mail.Appender.Internal;
+using log4net.Layout;
+using MailKit.Security;
+using MimeKit;
+using NUnit.Framework;
+
+namespace log4net.Ext.Mail.Tests.Appender;
+
+/// <summary>
+/// Unit tests for the MailKit based <see cref="SmtpAppender"/>. No mail 
leaves the
+/// process: every test drives a <see cref="FakeSmtpTransport"/>.
+/// </summary>
+[TestFixture]
+public class SmtpAppenderTest
+{
+  /// <summary>
+  /// An <see cref="IErrorHandler"/> that collects what the appender reported.
+  /// </summary>
+  private sealed class SilentErrorHandler : IErrorHandler
+  {
+    private readonly StringBuilder _buffer = new();
+
+    public string Message => _buffer.ToString();
+
+    public void Error(string message) => _buffer.Append(message + '\n');
+
+    public void Error(string message, Exception e) => _buffer.Append(message + 
'\n' + e.Message + '\n');
+
+    public void Error(string message, Exception? e, ErrorCode errorCode)
+      => _buffer.Append(message + '\n' + e?.Message + '\n');
+  }
+
+  private FakeSmtpTransport _transport = null!;
+  private SilentErrorHandler _errorHandler = null!;
+
+  [SetUp]
+  public void SetUp()
+  {
+    _transport = new FakeSmtpTransport();
+    _errorHandler = new SilentErrorHandler();
+  }
+
+  [TearDown]
+  public void TearDown() => _transport.Dispose();
+
+  /// <summary>
+  /// Creates an appender wired to <see cref="_transport"/> with the minimum 
required options,
+  /// configured so that every appended event is sent immediately.
+  /// </summary>
+  private SmtpAppender CreateAppender(string? header = null, string? footer = 
null)
+  {
+    PatternLayout layout = new() { ConversionPattern = "%m%n", Header = 
header, Footer = footer };
+    layout.ActivateOptions();
+
+    return new(() => _transport)
+    {
+      Layout = layout,
+      ErrorHandler = _errorHandler,
+      SmtpHost = "mail.example.com",
+      From = "[email protected]",
+      To = "[email protected]",
+      Subject = "subject",
+      // BufferSize of 1 makes BufferingAppenderSkeleton send each event 
straight away.
+      BufferSize = 1,
+    };
+  }
+
+  private static LoggingEvent CreateEvent(string message)
+    => new(new LoggingEventData
+    {
+      LoggerName = "TestLogger",
+      Level = Level.Error,
+      Message = message,
+      TimeStampUtc = DateTime.UtcNow,
+    });
+
+  /// <summary>
+  /// Activates the appender and appends a single event, which triggers one 
send.
+  /// </summary>
+  private static void Append(SmtpAppender appender, string message = "log 
message")
+  {
+    appender.ActivateOptions();
+    appender.DoAppend(CreateEvent(message));
+  }
+
+  [Test]
+  public void SendsOneMailPerEventWhenNotBuffering()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    Assert.That(_errorHandler.Message, Is.Empty);
+    Assert.That(_transport.SentMails, Has.Count.EqualTo(1));
+  }
+
+  [Test]
+  public void ConnectsToConfiguredHostAndPort()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.SmtpHost = "smtp.internal";
+    appender.Port = 2525;
+
+    Append(appender);
+
+    Assert.That(_transport.ConnectedHost, Is.EqualTo("smtp.internal"));
+    Assert.That(_transport.ConnectedPort, Is.EqualTo(2525));
+  }
+
+  [Test]
+  public void DefaultPortIs25()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    Assert.That(_transport.ConnectedPort, Is.EqualTo(25));
+  }
+
+  [Test]
+  public void ConnectAuthenticateSendDisconnectHappenInOrder()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Authentication = SmtpAppender.SmtpAuthentication.Basic;
+    appender.Username = "user";
+    appender.Password = "secret";
+
+    Append(appender);
+
+    Assert.That(_transport.Calls, Is.EqualTo(new[] { "Connect", 
"Authenticate", "Send", "Disconnect", "Dispose" }));
+  }
+
+  [Test]
+  public void DoesNotAuthenticateWhenAuthenticationIsNone()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    Assert.That(_transport.IsAuthenticated, Is.False);
+    Assert.That(_transport.Calls, Does.Not.Contain("Authenticate"));
+  }
+
+  [Test]
+  public void BasicAuthenticationPassesUsernameAndPassword()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Authentication = SmtpAppender.SmtpAuthentication.Basic;
+    appender.Username = "user";
+    appender.Password = "secret";
+
+    Append(appender);
+
+    Assert.That(_transport.SaslMechanism, Is.Null);
+    NetworkCredential credential = (NetworkCredential)_transport.Credentials!;
+    Assert.That(credential.UserName, Is.EqualTo("user"));
+    Assert.That(credential.Password, Is.EqualTo("secret"));
+  }
+
+  [Test]
+  public void NtlmAuthenticationUsesTheNtlmSaslMechanism()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Authentication = SmtpAppender.SmtpAuthentication.Ntlm;
+    appender.Username = "user";
+    appender.Password = "secret";
+
+    Append(appender);
+
+    Assert.That(_transport.Credentials, Is.Null);
+    Assert.That(_transport.SaslMechanism, Is.InstanceOf<SaslMechanismNtlm>());
+    Assert.That(_transport.SaslMechanism!.Credentials.GetCredential(null, 
null).UserName, Is.EqualTo("user"));
+  }
+
+  [Test]
+  public void EnableSslOffConnectsWithoutTransportSecurity()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    Assert.That(_transport.SecureSocketOptions, 
Is.EqualTo(SecureSocketOptions.None));
+  }
+
+  [Test]
+  public void EnableSslOnNegotiatesTransportSecurity()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.EnableSsl = true;
+
+    Append(appender);
+
+    Assert.That(_transport.SecureSocketOptions, 
Is.EqualTo(SecureSocketOptions.Auto));
+  }
+
+  [Test]
+  public void BodyContainsTheRenderedEvent()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender, "something broke");
+
+    Assert.That(_transport.SentMails[0].Body, Does.Contain("something broke"));
+  }
+
+  [Test]
+  public void BodyIsWrappedInLayoutHeaderAndFooter()
+  {
+    SmtpAppender appender = CreateAppender(header: "<<HEADER>>", footer: 
"<<FOOTER>>");
+
+    Append(appender, "the event");
+
+    string body = _transport.SentMails[0].Body;
+    Assert.That(body, Does.StartWith("<<HEADER>>"));
+    Assert.That(body, Does.Contain("the event"));
+    Assert.That(body, Does.EndWith("<<FOOTER>>"));
+  }
+
+  [Test]
+  public void BodyUsesTheConfiguredEncoding()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.BodyEncoding = Encoding.UTF8;
+
+    Append(appender, "weißes Schönwetter");
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.BodyCharset, Is.EqualTo("utf-8"));
+    Assert.That(mail.Body, Does.Contain("weißes Schönwetter"));
+  }
+
+  [Test]
+  public void SubjectIsSet()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Subject = "error in production";
+
+    Append(appender);
+
+    Assert.That(_transport.SentMails[0].Subject, Is.EqualTo("error in 
production"));
+  }
+
+  [Test]
+  public void SubjectUsesTheConfiguredEncoding()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Subject = "Störung im Betrieb";
+    appender.SubjectEncoding = Encoding.UTF8;
+
+    Append(appender);
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.Subject, Is.EqualTo("Störung im Betrieb"));
+    // A non-ASCII subject must be transfer-encoded, naming the configured 
charset.
+    Assert.That(mail.RawSubjectHeader.ToLowerInvariant(), 
Does.Contain("utf-8"));
+  }
+
+  [Test]
+  public void FromAndToAreSet()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.From = "[email protected]";
+    appender.To = "[email protected]";
+
+    Append(appender);
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.From, Is.EqualTo(new[] { "[email protected]" }));
+    Assert.That(mail.To, Is.EqualTo(new[] { "[email protected]" }));
+  }
+
+  [Test]
+  public void CcAndBccAreOmittedWhenNotConfigured()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.Cc, Is.Empty);
+    Assert.That(mail.Bcc, Is.Empty);
+    Assert.That(mail.ReplyTo, Is.Empty);
+  }
+
+  [Test]
+  public void ReplyToIsSet()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.ReplyTo = "[email protected]";
+
+    Append(appender);
+
+    Assert.That(_transport.SentMails[0].ReplyTo, Is.EqualTo(new[] { 
"[email protected]" }));
+  }
+
+  [Test]
+  public void CommaDelimitedRecipientsAreAllAdded()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.To = "[email protected],[email protected]";
+    appender.Cc = "[email protected], [email protected]";
+    appender.Bcc = "[email protected],[email protected]";
+
+    Append(appender);
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.To, Is.EqualTo(new[] { "[email protected]", 
"[email protected]" }));
+    Assert.That(mail.Cc, Is.EqualTo(new[] { "[email protected]", 
"[email protected]" }));
+    Assert.That(mail.Bcc, Is.EqualTo(new[] { "[email protected]", 
"[email protected]" }));
+  }
+
+  [Test]
+  public void SemicolonDelimitedRecipientsAreAllAdded()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.To = "[email protected];[email protected]";
+    appender.Bcc = "[email protected]; [email protected]";
+
+    Append(appender);
+
+    SentMail mail = _transport.SentMails[0];
+    Assert.That(mail.To, Is.EqualTo(new[] { "[email protected]", 
"[email protected]" }));
+    Assert.That(mail.Bcc, Is.EqualTo(new[] { "[email protected]", 
"[email protected]" }));
+  }
+
+  [Test]
+  public void DisplayNamesContainingCommasSurviveParsing()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.To = "\"Doe, John\" <[email protected]>, [email protected]";
+
+    Append(appender);
+
+    Assert.That(_transport.SentMails[0].To, Is.EqualTo(new[] { 
"[email protected]", "[email protected]" }));
+  }
+
+  [Test]
+  public void LeadingAndTrailingSeparatorsAreTrimmedFromRecipients()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.To = ",[email protected];";
+    appender.Cc = ";[email protected],";
+    appender.Bcc = ",[email protected],";
+
+    Assert.That(appender.To, Is.EqualTo("[email protected]"));
+    Assert.That(appender.Cc, Is.EqualTo("[email protected]"));
+    Assert.That(appender.Bcc, Is.EqualTo("[email protected]"));
+  }
+
+  [TestCase(MailPriority.Low, MessagePriority.NonUrgent)]
+  [TestCase(MailPriority.Normal, MessagePriority.Normal)]
+  [TestCase(MailPriority.High, MessagePriority.Urgent)]
+  public void PriorityIsMappedOntoTheMimePriorityHeader(MailPriority 
configured, MessagePriority expected)
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.Priority = configured;
+
+    Append(appender);
+
+    Assert.That(_transport.SentMails[0].Priority, Is.EqualTo(expected));
+  }
+
+  [Test]
+  public void DefaultPriorityIsNormal()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Assert.That(appender.Priority, Is.EqualTo(MailPriority.Normal));
+  }
+
+  [Test]
+  public void TransportIsDisconnectedAndDisposedAfterSending()
+  {
+    SmtpAppender appender = CreateAppender();
+
+    Append(appender);
+
+    Assert.That(_transport.DisconnectedWithQuit, Is.True);
+    Assert.That(_transport.IsConnected, Is.False);
+    Assert.That(_transport.IsDisposed, Is.True);
+  }
+
+  [Test]
+  public void TransportIsDisconnectedAndDisposedWhenSendingFails()
+  {
+    SmtpAppender appender = CreateAppender();
+    _transport.SendException = new InvalidOperationException("relay refused");
+
+    Append(appender);
+
+    Assert.That(_transport.Calls, Does.Contain("Disconnect"));
+    Assert.That(_transport.IsDisposed, Is.True);
+  }
+
+  [Test]
+  public void SendFailureIsReportedToTheErrorHandlerAndNotThrown()
+  {
+    SmtpAppender appender = CreateAppender();
+    _transport.SendException = new InvalidOperationException("relay refused");
+
+    Assert.DoesNotThrow(() => Append(appender));
+
+    Assert.That(_errorHandler.Message, Does.Contain("Error occurred while 
sending e-mail notification."));
+    Assert.That(_errorHandler.Message, Does.Contain("relay refused"));
+  }
+
+  [TestCase(nameof(SmtpAppender.SmtpHost))]
+  [TestCase(nameof(SmtpAppender.From))]
+  [TestCase(nameof(SmtpAppender.To))]
+  public void MissingRequiredOptionIsReportedToTheErrorHandler(string option)
+  {
+    SmtpAppender appender = CreateAppender();
+    switch (option)
+    {
+      case nameof(SmtpAppender.SmtpHost):
+        appender.SmtpHost = null;
+        break;
+      case nameof(SmtpAppender.From):
+        appender.From = null;
+        break;
+      default:
+        appender.To = null;
+        break;
+    }
+
+    Assert.DoesNotThrow(() => Append(appender));
+
+    Assert.That(_transport.SentMails, Is.Empty);
+    Assert.That(_errorHandler.Message, Does.Contain(option));
+  }
+
+  [Test]
+  public void EachSendUsesAFreshTransport()
+  {
+    List<FakeSmtpTransport> transports = [];
+    PatternLayout layout = new() { ConversionPattern = "%m%n" };
+    layout.ActivateOptions();
+    SmtpAppender appender = new(() =>
+    {
+      FakeSmtpTransport transport = new();
+      transports.Add(transport);
+      return transport;
+    })
+    {
+      Layout = layout,
+      ErrorHandler = _errorHandler,
+      SmtpHost = "mail.example.com",
+      From = "[email protected]",
+      To = "[email protected]",
+      BufferSize = 1,
+    };
+    appender.ActivateOptions();
+
+    appender.DoAppend(CreateEvent("first"));
+    appender.DoAppend(CreateEvent("second"));
+
+    Assert.That(transports, Has.Count.EqualTo(2));
+    Assert.That(transports[0].SentMails[0].Body, Does.Contain("first"));
+    Assert.That(transports[1].SentMails[0].Body, Does.Contain("second"));
+  }
+
+  [Test]
+  public void BufferedEventsAreSentInASingleMail()
+  {
+    SmtpAppender appender = CreateAppender();
+    appender.BufferSize = 10;
+    appender.ActivateOptions();
+
+    appender.DoAppend(CreateEvent("one"));
+    appender.DoAppend(CreateEvent("two"));
+    appender.DoAppend(CreateEvent("three"));
+    Assert.That(_transport.SentMails, Is.Empty, "the buffer is not full yet");
+
+    appender.Flush(true);
+
+    Assert.That(_transport.SentMails, Has.Count.EqualTo(1));
+    string body = _transport.SentMails[0].Body;
+    Assert.That(body, Does.Contain("one"));
+    Assert.That(body, Does.Contain("two"));
+    Assert.That(body, Does.Contain("three"));
+  }
+
+  [Test]
+  public void RequiresALayout()
+  {
+    PatternLayout layout = new() { ConversionPattern = "%m%n" };
+    layout.ActivateOptions();
+    SmtpAppender appender = new(() => _transport)
+    {
+      ErrorHandler = _errorHandler,
+      SmtpHost = "mail.example.com",
+      From = "[email protected]",
+      To = "[email protected]",
+      BufferSize = 1,
+    };
+    appender.ActivateOptions();
+
+    appender.DoAppend(CreateEvent("no layout"));
+
+    Assert.That(_transport.SentMails, Is.Empty);
+    Assert.That(_errorHandler.Message, Is.Not.Empty);
+  }
+
+  [Test]
+  public void NullTransportFactoryIsRejected()
+    => Assert.Throws<ArgumentNullException>(() => new SmtpAppender(null!));
+
+  [Test]
+  public void DefaultConstructorUsesTheMailKitTransport()
+  {
+    // Nothing is sent here; this only pins down that the log4net-configurable
+    // parameterless constructor exists and produces a usable appender.
+    SmtpAppender appender = new();
+
+    Assert.That(appender.Port, Is.EqualTo(25));
+    Assert.That(appender.Authentication, 
Is.EqualTo(SmtpAppender.SmtpAuthentication.None));
+    Assert.That(appender.SubjectEncoding, Is.EqualTo(Encoding.UTF8));
+    Assert.That(appender.BodyEncoding, Is.EqualTo(Encoding.UTF8));
+    Assert.That(appender.EnableSsl, Is.False);
+  }
+}
diff --git a/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj 
b/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
new file mode 100644
index 00000000..0add8141
--- /dev/null
+++ b/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
@@ -0,0 +1,31 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <IsTestProject>true</IsTestProject>
+    <TargetFrameworks>net8.0</TargetFrameworks>
+    <NoWarn>NETSDK1138;CS1701</NoWarn>
+    <OutputType>Library</OutputType>
+    <OutputPath>bin\$(Configuration)</OutputPath>
+    <Configurations>Debug;Release</Configurations>
+    <Platforms>AnyCPU</Platforms>
+    <Deterministic>true</Deterministic>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <DefineConstants>TRACE;DEBUG;$(DefineConstants)</DefineConstants>
+    <!-- suppress analyzer mismatch warning -->
+    <NoWarn>CS8032</NoWarn>
+    <VSTestLogger>quackers</VSTestLogger>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile 
Include="..\log4net\Diagnostics\CodeAnalysis\CallerArgumentExpressionAttribute.cs"
 Link="Diagnostics\CodeAnalysis\CallerArgumentExpressionAttribute.cs" />
+    <Compile Include="..\log4net\Diagnostics\CodeAnalysis\IsExternalInit.cs" 
Link="Diagnostics\CodeAnalysis\IsExternalInit.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\log4net.Ext.Mail\log4net.Ext.Mail.csproj" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="NUnit" Version="$(NUnitPackageVersion)" />
+    <PackageReference Include="NUnit.Analyzers" 
Version="$(NUnitAnalyzersPackageVersion)" />
+    <PackageReference Include="NUnit3TestAdapter" 
Version="$(NUnit3TestAdapterPackageVersion)" />
+    <PackageReference Include="Quackers.TestLogger" 
Version="$(QuackersTestLoggerPackageVersion)" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" 
Version="$(MicrosoftNetTestSdkPackageVersion)" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/log4net.Ext.Mail/Appender/Internal/ISmtpTransport.cs 
b/src/log4net.Ext.Mail/Appender/Internal/ISmtpTransport.cs
new file mode 100644
index 00000000..65ef28c6
--- /dev/null
+++ b/src/log4net.Ext.Mail/Appender/Internal/ISmtpTransport.cs
@@ -0,0 +1,87 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Net;
+using MailKit.Security;
+using MimeKit;
+
+namespace log4net.Ext.Mail.Appender.Internal;
+
+/// <summary>
+/// The slice of the MailKit SMTP client API used by <see 
cref="SmtpAppender"/>.
+/// </summary>
+/// <remarks>
+/// <para>
+/// This interface exists so that <see cref="SmtpAppender"/> can be unit 
tested without
+/// talking to a real SMTP server. <see cref="MailKitSmtpTransport"/> is the 
production
+/// implementation and simply forwards to <see 
cref="MailKit.Net.Smtp.SmtpClient"/>.
+/// </para>
+/// <para>
+/// The members mirror their MailKit counterparts, so an implementation that 
wraps
+/// <see cref="MailKit.Net.Smtp.SmtpClient"/> needs no translation logic.
+/// </para>
+/// </remarks>
+internal interface ISmtpTransport : IDisposable
+{
+  /// <summary>
+  /// Gets a value indicating whether the transport is connected to a server.
+  /// </summary>
+  bool IsConnected { get; }
+
+  /// <summary>
+  /// Gets a value indicating whether the transport has been authenticated.
+  /// </summary>
+  bool IsAuthenticated { get; }
+
+  /// <summary>
+  /// Connects to the SMTP server at <paramref name="host"/> and <paramref 
name="port"/>.
+  /// </summary>
+  /// <param name="host">The name or address of the SMTP server.</param>
+  /// <param name="port">The port the SMTP server is listening on.</param>
+  /// <param name="secureSocketOptions">The transport security to use.</param>
+  void Connect(string host, int port, SecureSocketOptions secureSocketOptions);
+
+  /// <summary>
+  /// Authenticates using the supplied <paramref name="credentials"/> and 
whichever
+  /// SASL mechanism the server and client agree on.
+  /// </summary>
+  /// <param name="credentials">The credentials to authenticate with.</param>
+  void Authenticate(ICredentials credentials);
+
+  /// <summary>
+  /// Authenticates using an explicit SASL <paramref name="mechanism"/>.
+  /// </summary>
+  /// <param name="mechanism">The SASL mechanism to authenticate with.</param>
+  void Authenticate(SaslMechanism mechanism);
+
+  /// <summary>
+  /// Sends the specified <paramref name="message"/>.
+  /// </summary>
+  /// <param name="message">The message to send.</param>
+  void Send(MimeMessage message);
+
+  /// <summary>
+  /// Disconnects from the SMTP server.
+  /// </summary>
+  /// <param name="quit">
+  /// <see langword="true"/> to send the <c>QUIT</c> command before 
disconnecting.
+  /// </param>
+  void Disconnect(bool quit);
+}
diff --git a/src/log4net.Ext.Mail/Appender/Internal/MailKitSmtpTransport.cs 
b/src/log4net.Ext.Mail/Appender/Internal/MailKitSmtpTransport.cs
new file mode 100644
index 00000000..69768242
--- /dev/null
+++ b/src/log4net.Ext.Mail/Appender/Internal/MailKitSmtpTransport.cs
@@ -0,0 +1,59 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System.Net;
+using MailKit.Net.Smtp;
+using MailKit.Security;
+using MimeKit;
+
+namespace log4net.Ext.Mail.Appender.Internal;
+
+/// <summary>
+/// The default <see cref="ISmtpTransport"/> implementation, backed by
+/// <see cref="MailKit.Net.Smtp.SmtpClient"/>.
+/// </summary>
+internal sealed class MailKitSmtpTransport : ISmtpTransport
+{
+  private readonly SmtpClient _client = new();
+
+  /// <inheritdoc/>
+  public bool IsConnected => _client.IsConnected;
+
+  /// <inheritdoc/>
+  public bool IsAuthenticated => _client.IsAuthenticated;
+
+  /// <inheritdoc/>
+  public void Connect(string host, int port, SecureSocketOptions 
secureSocketOptions)
+    => _client.Connect(host, port, secureSocketOptions);
+
+  /// <inheritdoc/>
+  public void Authenticate(ICredentials credentials) => 
_client.Authenticate(credentials);
+
+  /// <inheritdoc/>
+  public void Authenticate(SaslMechanism mechanism) => 
_client.Authenticate(mechanism);
+
+  /// <inheritdoc/>
+  public void Send(MimeMessage message) => _client.Send(message);
+
+  /// <inheritdoc/>
+  public void Disconnect(bool quit) => _client.Disconnect(quit);
+
+  /// <inheritdoc/>
+  public void Dispose() => _client.Dispose();
+}
diff --git a/src/log4net/Appender/SmtpAppender.cs 
b/src/log4net.Ext.Mail/Appender/SmtpAppender.cs
similarity index 52%
copy from src/log4net/Appender/SmtpAppender.cs
copy to src/log4net.Ext.Mail/Appender/SmtpAppender.cs
index 77683ef9..53f07aa4 100644
--- a/src/log4net/Appender/SmtpAppender.cs
+++ b/src/log4net.Ext.Mail/Appender/SmtpAppender.cs
@@ -1,10 +1,10 @@
 #region Apache License
 //
-// Licensed to the Apache Software Foundation (ASF) under one or more 
+// Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements. See the NOTICE file distributed with
-// this work for additional information regarding copyright ownership. 
+// this work for additional information regarding copyright ownership.
 // The ASF licenses this file to you under the Apache License, Version 2.0
-// (the "License"); you may not use this file except in compliance with 
+// (the "License"); you may not use this file except in compliance with
 // the License. You may obtain a copy of the License at
 //
 // http://www.apache.org/licenses/LICENSE-2.0
@@ -19,25 +19,38 @@
 
 using System;
 using System.IO;
+using System.Net;
+using System.Net.Mail;
 using System.Text;
 
-using System.Net.Mail;
+using log4net.Appender;
 using log4net.Core;
+using log4net.Ext.Mail.Appender.Internal;
 using log4net.Util;
+using MailKit.Security;
+
+using MimeKit;
+using MimeKit.Text;
 
-namespace log4net.Appender;
+namespace log4net.Ext.Mail.Appender;
 
 /// <summary>
-/// Send an e-mail when a specific logging event occurs, typically on errors 
-/// or fatal errors.
+/// Send an e-mail when a specific logging event occurs, typically on errors
+/// or fatal errors, using MailKit as the SMTP client.
 /// </summary>
 /// <remarks>
 /// <para>
+/// This appender exposes the same options as <see 
cref="log4net.Appender.SmtpAppender"/>,
+/// so an existing configuration can be pointed at this type without change. 
The difference
+/// is the transport: it sends through <see 
cref="MailKit.Net.Smtp.SmtpClient"/> instead of
+/// the obsolete <see cref="System.Net.Mail.SmtpClient"/>.
+/// </para>
+/// <para>
 /// The number of logging events delivered in this e-mail depend on
 /// the value of <see cref="BufferingAppenderSkeleton.BufferSize"/> option. The
 /// <see cref="SmtpAppender"/> keeps only the last
-/// <see cref="BufferingAppenderSkeleton.BufferSize"/> logging events in its 
-/// cyclic buffer. This keeps memory requirements at a reasonable level while 
+/// <see cref="BufferingAppenderSkeleton.BufferSize"/> logging events in its
+/// cyclic buffer. This keeps memory requirements at a reasonable level while
 /// still delivering useful application context.
 /// </para>
 /// <para>
@@ -49,58 +62,66 @@ namespace log4net.Appender;
 /// <para>
 /// To set the SMTP server port use the <see cref="Port"/> property. The 
default port is 25.
 /// </para>
+/// <para>
+/// Unlike <see cref="System.Net.Mail.SmtpClient"/>, MailKit has no notion of 
a machine-wide
+/// default SMTP server, so <see cref="SmtpHost"/> is required.
+/// </para>
 /// </remarks>
-/// <author>Nicko Cadell</author>
-/// <author>Gert Driesen</author>
 public class SmtpAppender : BufferingAppenderSkeleton
 {
+  private readonly Func<ISmtpTransport> _transportFactory;
+
   /// <summary>
-  /// Default constructor
+  /// Default constructor. Sends through <see cref="MailKitSmtpTransport"/>.
   /// </summary>
-  /// <remarks>
-  /// <para>
-  /// Default constructor
-  /// </para>
-  /// </remarks>
   public SmtpAppender()
-  {
-  }
+    : this(static () => new MailKitSmtpTransport())
+  { }
+
+  /// <summary>
+  /// Creates an appender that obtains its transport from <paramref 
name="transportFactory"/>.
+  /// </summary>
+  /// <param name="transportFactory">
+  /// Called once per e-mail to create the transport used to send it.
+  /// </param>
+  internal SmtpAppender(Func<ISmtpTransport> transportFactory)
+    => _transportFactory = transportFactory.EnsureNotNull();
 
   /// <summary>
   /// Gets or sets a comma-delimited list of recipient e-mail addresses.
   /// </summary>
   public string? To
   {
-    get => _to;
-    set => _to = MaybeTrimSeparators(value);
+    get;
+    set => field = MaybeTrimSeparators(value);
   }
 
   /// <summary>
-  /// Gets or sets a comma-delimited list of recipient e-mail addresses 
+  /// Gets or sets a comma-delimited list of recipient e-mail addresses
   /// that will be carbon copied.
   /// </summary>
   public string? Cc
   {
-    get => _cc;
-    set => _cc = MaybeTrimSeparators(value);
+    get;
+    set => field = MaybeTrimSeparators(value);
   }
 
   /// <summary>
-  /// Gets or sets a semicolon-delimited list of recipient e-mail addresses
+  /// Gets or sets a comma-delimited list of recipient e-mail addresses
   /// that will be blind carbon copied.
   /// </summary>
   /// <value>
-  /// A semicolon-delimited list of e-mail addresses.
+  /// A comma-delimited list of e-mail addresses.
   /// </value>
   /// <remarks>
   /// <para>
-  /// A semicolon-delimited list of recipient e-mail addresses.
+  /// Semicolons are also accepted as separators, for backward compatibility.
   /// </para>
   /// </remarks>
   public string? Bcc
   {
-    get => _bcc;
-    set => _bcc = MaybeTrimSeparators(value);
+    get;
+    set => field = MaybeTrimSeparators(value);
   }
 
   /// <summary>
@@ -109,11 +130,6 @@ public SmtpAppender()
   /// <value>
   /// The e-mail address of the sender.
   /// </value>
-  /// <remarks>
-  /// <para>
-  /// The e-mail address of the sender.
-  /// </para>
-  /// </remarks>
   public string? From { get; set; }
 
   /// <summary>
@@ -122,25 +138,19 @@ public SmtpAppender()
   /// <value>
   /// The subject line of the e-mail message.
   /// </value>
-  /// <remarks>
-  /// <para>
-  /// The subject line of the e-mail message.
-  /// </para>
-  /// </remarks>
   public string? Subject { get; set; }
 
   /// <summary>
-  /// Gets or sets the name of the SMTP relay mail server to use to send 
+  /// Gets or sets the name of the SMTP relay mail server to use to send
   /// the e-mail messages.
   /// </summary>
   /// <value>
-  /// The name of the e-mail relay server. If SmtpServer is not set, the 
-  /// name of the local SMTP server is used.
+  /// The name of the e-mail relay server.
   /// </value>
   /// <remarks>
   /// <para>
-  /// The name of the e-mail relay server. If SmtpServer is not set, the 
-  /// name of the local SMTP server is used.
+  /// This option is required. MailKit, unlike <see 
cref="System.Net.Mail.SmtpClient"/>,
+  /// has no machine-wide default SMTP server to fall back on.
   /// </para>
   /// </remarks>
   public string? SmtpHost { get; set; }
@@ -150,13 +160,17 @@ public SmtpAppender()
   /// </summary>
   /// <remarks>
   /// <para>
-  /// Valid Authentication mode values are: <see 
cref="SmtpAuthentication.None"/>, 
-  /// <see cref="SmtpAuthentication.Basic"/>, and <see 
cref="SmtpAuthentication.Ntlm"/>. 
-  /// The default value is <see cref="SmtpAuthentication.None"/>. When using 
-  /// <see cref="SmtpAuthentication.Basic"/> you must specify the <see 
cref="Username"/> 
+  /// Valid Authentication mode values are: <see 
cref="SmtpAuthentication.None"/>,
+  /// <see cref="SmtpAuthentication.Basic"/>, and <see 
cref="SmtpAuthentication.Ntlm"/>.
+  /// The default value is <see cref="SmtpAuthentication.None"/>. When using
+  /// <see cref="SmtpAuthentication.Basic"/> you must specify the <see 
cref="Username"/>
   /// and <see cref="Password"/> to use to authenticate.
-  /// When using <see cref="SmtpAuthentication.Ntlm"/> the Windows credentials 
for the current
-  /// thread, if impersonating, or the process will be used to authenticate. 
+  /// </para>
+  /// <para>
+  /// <see cref="SmtpAuthentication.Ntlm"/> authenticates with the NTLM SASL 
mechanism.
+  /// Note that MailKit cannot reuse the Windows logon session of the current 
thread or
+  /// process the way <see cref="System.Net.Mail.SmtpClient"/> could, so
+  /// <see cref="Username"/> and <see cref="Password"/> must be supplied for 
NTLM as well.
   /// </para>
   /// </remarks>
   public SmtpAuthentication Authentication { get; set; } = 
SmtpAuthentication.None;
@@ -166,9 +180,9 @@ public SmtpAppender()
   /// </summary>
   /// <remarks>
   /// <para>
-  /// A <see cref="Username"/> and <see cref="Password"/> must be specified 
when 
-  /// <see cref="Authentication"/> is set to <see 
cref="SmtpAuthentication.Basic"/>, 
-  /// otherwise the username will be ignored. 
+  /// A <see cref="Username"/> and <see cref="Password"/> must be specified 
when
+  /// <see cref="Authentication"/> is set to <see 
cref="SmtpAuthentication.Basic"/>
+  /// or <see cref="SmtpAuthentication.Ntlm"/>, otherwise the username will be 
ignored.
   /// </para>
   /// </remarks>
   public string? Username { get; set; }
@@ -178,9 +192,9 @@ public SmtpAppender()
   /// </summary>
   /// <remarks>
   /// <para>
-  /// A <see cref="Username"/> and <see cref="Password"/> must be specified 
when 
-  /// <see cref="Authentication"/> is set to <see 
cref="SmtpAuthentication.Basic"/>, 
-  /// otherwise the password will be ignored. 
+  /// A <see cref="Username"/> and <see cref="Password"/> must be specified 
when
+  /// <see cref="Authentication"/> is set to <see 
cref="SmtpAuthentication.Basic"/>
+  /// or <see cref="SmtpAuthentication.Ntlm"/>, otherwise the password will be 
ignored.
   /// </para>
   /// </remarks>
   public string? Password { get; set; }
@@ -211,14 +225,30 @@ public SmtpAppender()
   /// If you are using this appender to report errors then
   /// you may want to set the priority to <see cref="MailPriority.High"/>.
   /// </para>
+  /// <para>
+  /// The value is mapped onto the MIME <c>Priority</c> header:
+  /// <see cref="MailPriority.Low"/> becomes <see 
cref="MessagePriority.NonUrgent"/>,
+  /// <see cref="MailPriority.Normal"/> becomes <see 
cref="MessagePriority.Normal"/> and
+  /// <see cref="MailPriority.High"/> becomes <see 
cref="MessagePriority.Urgent"/>.
+  /// </para>
   /// </remarks>
   public MailPriority Priority { get; set; } = MailPriority.Normal;
 
   /// <summary>
-  /// Enable or disable use of SSL when sending e-mail message
+  /// Enable or disable use of SSL/TLS when sending e-mail message
   /// </summary>
   /// <remarks>
-  /// This is available on MS .NET 2.0 runtime and higher
+  /// <para>
+  /// When <see langword="true"/>, <see cref="SecureSocketOptions.Auto"/> is 
used, which
+  /// negotiates implicit TLS or <c>STARTTLS</c> depending on the <see 
cref="Port"/> and
+  /// what the server advertises. When <see langword="false"/> the connection 
is not
+  /// encrypted at all (<see cref="SecureSocketOptions.None"/>), matching the 
behaviour of
+  /// <see cref="System.Net.Mail.SmtpClient.EnableSsl"/>.
+  /// </para>
+  /// <para>
+  /// Use <see cref="SecureSocketOptions"/> directly via a custom
+  /// <see cref="ISmtpTransport"/> if you need finer control.
+  /// </para>
   /// </remarks>
   public bool EnableSsl { get; set; }
 
@@ -231,7 +261,7 @@ public SmtpAppender()
   /// Gets or sets the subject encoding to be used.
   /// </summary>
   /// <remarks>
-  /// The default encoding is the operating system's current ANSI codepage.
+  /// The default encoding is <see cref="Encoding.UTF8"/>.
   /// </remarks>
   public Encoding SubjectEncoding { get; set; } = Encoding.UTF8;
 
@@ -239,7 +269,7 @@ public SmtpAppender()
   /// Gets or sets the body encoding to be used.
   /// </summary>
   /// <remarks>
-  /// The default encoding is the operating system's current ANSI codepage.
+  /// The default encoding is <see cref="Encoding.UTF8"/>.
   /// </remarks>
   public Encoding BodyEncoding { get; set; } = Encoding.UTF8;
 
@@ -281,7 +311,7 @@ protected override void SendBuffer(LoggingEvent[] events)
   }
 
   /// <summary>
-  /// This appender requires a <see cref="Layout"/> to be set.
+  /// This appender requires a <see cref="AppenderSkeleton.Layout"/> to be set.
   /// </summary>
   protected override bool RequiresLayout => true;
 
@@ -291,61 +321,78 @@ protected override void SendBuffer(LoggingEvent[] events)
   /// <param name="messageBody">the body text to include in the mail</param>
   protected virtual void SendEmail(string messageBody)
   {
-    // Create and configure the smtp client
-#pragma warning disable CS0618 // Type or member is obsolete
-    using SmtpClient smtpClient = new();
-#pragma warning restore CS0618 // Type or member is obsolete
-    if (!string.IsNullOrEmpty(SmtpHost))
-    {
-      smtpClient.Host = SmtpHost;
-    }
-    smtpClient.Port = Port;
-    smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
-    smtpClient.EnableSsl = EnableSsl;
+    using MimeMessage message = CreateMessage(messageBody);
+    using ISmtpTransport transport = _transportFactory().EnsureNotNull();
 
-    if (Authentication == SmtpAuthentication.Basic)
+    transport.Connect(
+        SmtpHost.EnsureNotNullOrEmpty(),
+        Port,
+        EnableSsl ? SecureSocketOptions.Auto : SecureSocketOptions.None);
+    try
     {
-      // Perform basic authentication
-      smtpClient.Credentials = new System.Net.NetworkCredential(Username, 
Password);
+      switch (Authentication)
+      {
+        case SmtpAuthentication.Basic:
+          transport.Authenticate(new NetworkCredential(Username, Password));
+          break;
+        case SmtpAuthentication.Ntlm:
+          transport.Authenticate(new SaslMechanismNtlm(new 
NetworkCredential(Username, Password)));
+          break;
+        case SmtpAuthentication.None:
+        default:
+          break;
+      }
+
+      transport.Send(message);
     }
-    else if (Authentication == SmtpAuthentication.Ntlm)
+    finally
     {
-      // Perform integrated authentication (NTLM)
-      smtpClient.Credentials = 
System.Net.CredentialCache.DefaultNetworkCredentials;
+      transport.Disconnect(true);
     }
+  }
 
-    using MailMessage mailMessage = new();
-    mailMessage.Body = messageBody;
-    mailMessage.BodyEncoding = BodyEncoding;
-    mailMessage.From = new MailAddress(From.EnsureNotNull());
-    mailMessage.To.Add(_to.EnsureNotNull());
-    if (!string.IsNullOrEmpty(_cc))
+  /// <summary>
+  /// Builds the <see cref="MimeMessage"/> for the given body text from the 
configured options.
+  /// </summary>
+  /// <param name="messageBody">the body text to include in the mail</param>
+  /// <returns>the message to send</returns>
+  protected virtual MimeMessage CreateMessage(string messageBody)
+  {
+    MimeMessage message = new();
+    message.From.AddRange(ParseAddresses(From.EnsureNotNullOrEmpty()));
+    message.To.AddRange(ParseAddresses(To.EnsureNotNullOrEmpty()));
+    if (!string.IsNullOrEmpty(Cc))
     {
-      mailMessage.CC.Add(_cc);
+      message.Cc.AddRange(ParseAddresses(Cc!));
     }
-    if (!string.IsNullOrEmpty(_bcc))
+    if (!string.IsNullOrEmpty(Bcc))
     {
-      mailMessage.Bcc.Add(_bcc);
+      message.Bcc.AddRange(ParseAddresses(Bcc!));
     }
     if (!string.IsNullOrEmpty(ReplyTo))
     {
-      mailMessage.ReplyToList.Add(new MailAddress(ReplyTo));
+      message.ReplyTo.AddRange(ParseAddresses(ReplyTo!));
     }
-    mailMessage.Subject = Subject;
-    mailMessage.SubjectEncoding = SubjectEncoding;
-    mailMessage.Priority = Priority;
 
-    // TODO: Consider using SendAsync to send the message without blocking. We 
would need a SendCompletedCallback to log errors.
-    smtpClient.Send(mailMessage);
-  }
+    if (Subject is not null)
+    {
+      // Set through the header collection so that the configured encoding is 
honoured.
+      message.Headers.Replace(HeaderId.Subject, SubjectEncoding, Subject);
+    }
 
-  private string? _to;
-  private string? _cc;
-  private string? _bcc;
+    message.Priority = Priority switch
+    {
+      MailPriority.Low => MessagePriority.NonUrgent,
+      MailPriority.High => MessagePriority.Urgent,
+      _ => MessagePriority.Normal,
+    };
 
-  // authentication fields
+    TextPart body = new(TextFormat.Plain);
+    body.SetText(BodyEncoding, messageBody);
+    message.Body = body;
 
-  // server port, default port 25
+    return message;
+  }
 
   /// <summary>
   /// Values for the <see cref="Authentication"/> property.
@@ -371,10 +418,11 @@ public enum SmtpAuthentication
     Basic,
 
     /// <summary>
-    /// Integrated authentication
+    /// NTLM authentication.
     /// </summary>
     /// <remarks>
-    /// Uses the Windows credentials from the current thread or process to 
authenticate.
+    /// Requires a username and password to be supplied; MailKit cannot reuse 
the
+    /// Windows logon session of the current thread or process.
     /// </remarks>
     Ntlm
   }
@@ -386,4 +434,54 @@ public enum SmtpAuthentication
   /// Trims leading and trailing commas or semicolons
   /// </summary>
   private static string? MaybeTrimSeparators(string? s) => 
s?.Trim(_addressDelimiters);
-}
\ No newline at end of file
+
+  /// <summary>
+  /// Parses a comma- or semicolon-delimited list of addresses.
+  /// </summary>
+  /// <remarks>
+  /// RFC 5322 only allows commas, which is what <see 
cref="InternetAddressList.Parse(string)"/>
+  /// accepts, so semicolon-delimited lists are retried after normalization.
+  /// </remarks>
+  private static InternetAddressList ParseAddresses(string addresses)
+    => InternetAddressList.TryParse(addresses, out InternetAddressList? list)
+      ? list
+      : InternetAddressList.Parse(ReplaceUnquotedSemicolons(addresses));
+
+  /// <summary>
+  /// Replaces every semicolon that is not inside a quoted string with a comma.
+  /// </summary>
+  private static string ReplaceUnquotedSemicolons(string addresses)
+  {
+    StringBuilder result = new(addresses.Length);
+    bool inQuotes = false;
+    bool escaped = false;
+    foreach (char character in addresses)
+    {
+      if (escaped)
+      {
+        escaped = false;
+        result.Append(character);
+        continue;
+      }
+
+      switch (character)
+      {
+        case '\\' when inQuotes:
+          escaped = true;
+          result.Append(character);
+          break;
+        case '"':
+          inQuotes = !inQuotes;
+          result.Append(character);
+          break;
+        case ';' when !inQuotes:
+          result.Append(',');
+          break;
+        default:
+          result.Append(character);
+          break;
+      }
+    }
+    return result.ToString();
+  }
+}
diff --git a/src/log4net/log4net.csproj 
b/src/log4net.Ext.Mail/log4net.Ext.Mail.csproj
similarity index 56%
copy from src/log4net/log4net.csproj
copy to src/log4net.Ext.Mail/log4net.Ext.Mail.csproj
index c1568607..cc92e95d 100644
--- a/src/log4net/log4net.csproj
+++ b/src/log4net.Ext.Mail/log4net.Ext.Mail.csproj
@@ -1,34 +1,15 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <Version>3.3.3</Version>
-    <PackageId>log4net</PackageId>
-    <Product>Apache log4net</Product>
+    <PackageId>log4net.Ext.Mail</PackageId>
+    <Product>Apache log4net.Ext.Mail</Product>
     <Title>$(Product)</Title>
-    <Description>log4net is a tool to help the programmer output log 
statements to a variety of output targets.
-In case of problems with an application, it is helpful to enable logging so 
that the problem
-can be located. With log4net it is possible to enable logging at runtime 
without modifying the
-application binary. The log4net package is designed so that log statements can 
remain in
-shipped code without incurring a high performance cost. It follows that the 
speed of logging
-(or rather not logging) is crucial.
-
-At the same time, log output can be so voluminous that it quickly becomes 
overwhelming.
-One of the distinctive features of log4net is the notion of hierarchical 
loggers.
-Using these loggers it is possible to selectively control which log statements 
are output
-at arbitrary granularity.
-
-log4net is designed with two distinct goals in mind: speed and flexibility
-    </Description>
+    <Description>log4net.Ext.Mail provides Appenders for sending 
mails</Description>
     <Platforms>AnyCPU</Platforms>
-    <TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>netstandard2.0</TargetFrameworks>
     <Configurations>Debug;Release</Configurations>
-    <RootNamespace>log4net</RootNamespace>
-    <AssemblyName>log4net</AssemblyName>
-    <ProjectType>Local</ProjectType>
     <OutputType>Library</OutputType>
     <MapFileExtensions>true</MapFileExtensions>
     <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
-    <SignAssembly>true</SignAssembly>
-    <AssemblyOriginatorKeyFile>..\..\log4net.snk</AssemblyOriginatorKeyFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <OutputPath>..\..\build\$(Configuration)</OutputPath>
     <PackageOutputPath>..\..\build\artifacts</PackageOutputPath>
@@ -37,31 +18,22 @@ log4net is designed with two distinct goals in mind: speed 
and flexibility
   </PropertyGroup>
   <PropertyGroup Label="NuGet generation">
     <Authors>The Apache Software Foundation</Authors>
-    <Copyright>Copyright 2004-2025 The Apache Software Foundation</Copyright>
     <DevelopmentDependency>false</DevelopmentDependency>
     <IsPackable>true</IsPackable>
     <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
     <PackageProjectUrl>https://logging.apache.org/log4net/</PackageProjectUrl>
     <PackageIcon>package-icon.png</PackageIcon>
-    <PackageTags>logging log tracing logfiles</PackageTags>
+    <PackageTags>logging log tracing mail smtp</PackageTags>
     <Owners>Apache Logging Project</Owners>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
-    <AssemblyTitle>Apache log4net for .NET</AssemblyTitle>
+    <AssemblyTitle>Apache log4net mail extensions</AssemblyTitle>
     <AssemblyProduct>$(AssemblyName)</AssemblyProduct>
     <AssemblyCompany>The Apache Software Foundation</AssemblyCompany>
-    <Copyright>Copyright %A9 2004 - $([System.DateTime]::Now.Year) The Apache 
Software Foundation</Copyright>
+    <Copyright>Copyright %A9 2026 - $([System.DateTime]::Now.Year) The Apache 
Software Foundation</Copyright>
     <AssemblyCopyright>$(Copyright)</AssemblyCopyright>
     <AssemblyTrademark>Apache and Apache log4net are trademarks of The Apache 
Software Foundation</AssemblyTrademark>
     <AssemblyDefaultAlias>$(AssemblyName)</AssemblyDefaultAlias>
-    <AssemblyCulture>
-    </AssemblyCulture>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(TargetFramework)'=='net462'">
-    <AssemblyTitle>$(AssemblyTitle) Framework 4.6.2</AssemblyTitle>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
-    <AssemblyTitle>$(AssemblyTitle) Standard 2.0</AssemblyTitle>
   </PropertyGroup>
   <PropertyGroup>
     <PackageReadmeFile>README.md</PackageReadmeFile>
@@ -75,22 +47,29 @@ log4net is designed with two distinct goals in mind: speed 
and flexibility
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)'=='Release' ">
     <AssemblyConfiguration>Retail</AssemblyConfiguration>
-    <DefineConstants>TRACE;STRONG;$(DefineConstants)</DefineConstants>
+    <!--
+      Unlike log4net, this assembly is deliberately not strong named: it is a 
satellite package
+      with no COM or GAC scenario, and signing it later would be a breaking 
change for consumers.
+      Hence no SignAssembly/AssemblyOriginatorKeyFile here, and no STRONG 
constant either - it
+      is not referenced by any source file in this project.
+    -->
+    <DefineConstants>TRACE;$(DefineConstants)</DefineConstants>
     <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
     
<PathMap>$(MSBuildProjectDirectory)\=$(MSBuildProjectDirectory.Replace($(MSBuildThisFileDirectory),"D:\Git\apache\logging-log4net"))\</PathMap>
   </PropertyGroup>
   <PropertyGroup />
-  <ItemGroup Condition="'$(TargetFramework)'=='net462'">
-    <Reference Include="System.Configuration" />
-    <Reference Include="System.Web" />
+  <ItemGroup>
+    <InternalsVisibleTo Include="log4net.Ext.Mail.Tests" />
   </ItemGroup>
-  <ItemGroup Condition="'$(TargetFramework)'!='net462'">
-    <PackageReference Include="System.Configuration.ConfigurationManager" 
Version="$(SystemConfigurationConfigurationManagerPackageVersion)" />
+  <ItemGroup Label="Shared sources from log4net">
+    <!-- Log4NetAssert and the attributes it uses are internal to log4net, so 
they are
+         compiled into this assembly rather than referenced. -->
+    <Compile 
Include="..\log4net\Diagnostics\CodeAnalysis\CallerArgumentExpressionAttribute.cs"
 Link="Diagnostics\CodeAnalysis\CallerArgumentExpressionAttribute.cs" />
+    <Compile Include="..\log4net\Diagnostics\CodeAnalysis\NotNullAttribute.cs" 
Link="Diagnostics\CodeAnalysis\NotNullAttribute.cs" />
+    <Compile 
Include="..\log4net\Diagnostics\CodeAnalysis\ValidatedNotNullAttribute.cs" 
Link="Diagnostics\CodeAnalysis\ValidatedNotNullAttribute.cs" />
+    <Compile Include="..\log4net\Util\Log4NetAssert.cs" 
Link="Util\Log4NetAssert.cs" />
   </ItemGroup>
   <ItemGroup>
-    <None Include="..\..\log4net.snk">
-      <Link>log4net.snk</Link>
-    </None>
     <None Include="..\..\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
   <ItemGroup Label="Packaging">
@@ -99,16 +78,19 @@ log4net is designed with two distinct goals in mind: speed 
and flexibility
     </Content>
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\log4net\log4net.csproj" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="MailKit" Version="$(MailKitPackageVersion)" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" 
Version="$(MicrosoftSourceLinkGitHubPackageVersion)" PrivateAssets="All" />
     <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" 
Version="$(MicrosoftNetAnalyzersPackageVersion)" PrivateAssets="All" 
IncludeAssets="All" />
   </ItemGroup>
-  <Import Project="../MonoForFramework.targets" />
   <Target Name="_ResolveCopyLocalNuGetPackagePdbsAndXml" 
Condition="$(CopyLocalLockFileAssemblies) == true" 
AfterTargets="ResolveReferences">
     <!-- "Workaround" for missing '.pdb'-Files from NuGet Packages -->
     <!-- https://github.com/dotnet/sdk/issues/1458#issuecomment-420456386 -->
     <ItemGroup>
-      <ReferenceCopyLocalPaths 
Include="@(ReferenceCopyLocalPaths-&gt;'%(RootDir)%(Directory)%(Filename).pdb')"
 Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' and 
Exists('%(RootDir)%(Directory)%(Filename).pdb')" />
-      <ReferenceCopyLocalPaths 
Include="@(ReferenceCopyLocalPaths-&gt;'%(RootDir)%(Directory)%(Filename).xml')"
 Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' and 
Exists('%(RootDir)%(Directory)%(Filename).xml')" />
+      <ReferenceCopyLocalPaths 
Include="@(ReferenceCopyLocalPaths->'%(RootDir)%(Directory)%(Filename).pdb')" 
Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' and 
Exists('%(RootDir)%(Directory)%(Filename).pdb')" />
+      <ReferenceCopyLocalPaths 
Include="@(ReferenceCopyLocalPaths->'%(RootDir)%(Directory)%(Filename).xml')" 
Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' and 
Exists('%(RootDir)%(Directory)%(Filename).xml')" />
     </ItemGroup>
   </Target>
 </Project>
\ No newline at end of file
diff --git a/src/log4net.Tests/Appender/Internal/SimpleTelnetClient.cs 
b/src/log4net.Tests/Appender/Internal/SimpleTelnetClient.cs
index 72767bf7..38f104ff 100644
--- a/src/log4net.Tests/Appender/Internal/SimpleTelnetClient.cs
+++ b/src/log4net.Tests/Appender/Internal/SimpleTelnetClient.cs
@@ -20,6 +20,7 @@
 */
 
 using System;
+using System.IO;
 using System.Net;
 using System.Net.Sockets;
 using System.Threading;
@@ -43,27 +44,46 @@ internal sealed class SimpleTelnetClient(
   /// </summary>
   internal void Run(Action<string> log) => Task.Run(() =>
   {
-    log("client: starting ...");
-    _client.Connect(new IPEndPoint(IPAddress.Loopback, port));
-    log("client: connected");
-    // Get a stream object for reading and writing
-    using NetworkStream stream = _client.GetStream();
-    log("client: has stream");
+    try
+    {
+      log("client: starting ...");
+      _client.Connect(new IPEndPoint(IPAddress.Loopback, port));
+      log("client: connected");
+      // Get a stream object for reading and writing
+      using NetworkStream stream = _client.GetStream();
+      log("client: has stream");
 
-    int i;
-    byte[] bytes = new byte[256];
+      int i;
+      byte[] bytes = new byte[256];
 
-    // Loop to receive all the data sent by the server 
-    while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
-    {
-      string data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
-      log("client: read: " + data);
-      received(data);
-      if (_cancellationTokenSource.Token.IsCancellationRequested)
+      // Loop to receive all the data sent by the server
+      while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
       {
-        log("client: canceled");
-        return;
+        string data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
+        log("client: read: " + data);
+        received(data);
+        if (_cancellationTokenSource.Token.IsCancellationRequested)
+        {
+          log("client: canceled");
+          return;
+        }
       }
+      log("client: end of stream");
+    }
+    // The test asserts on the received data, so a failing client must not end 
up
+    // as an unobserved task exception - log it instead.
+    catch (SocketException e)
+    {
+      log("client: error: " + e);
+    }
+    catch (IOException e)
+    {
+      log("client: error: " + e);
+    }
+    catch (ObjectDisposedException e)
+    {
+      // expected when the client is disposed while reading
+      log("client: disposed: " + e.Message);
     }
   }, _cancellationTokenSource.Token);
 
diff --git a/src/log4net.Tests/Appender/TelnetAppenderTest.cs 
b/src/log4net.Tests/Appender/TelnetAppenderTest.cs
index 71b3b093..9596f065 100644
--- a/src/log4net.Tests/Appender/TelnetAppenderTest.cs
+++ b/src/log4net.Tests/Appender/TelnetAppenderTest.cs
@@ -18,7 +18,10 @@
 #endregion
 
 using System;
-using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
 using System.Threading;
 using System.Xml;
 using log4net.Appender;
@@ -43,13 +46,24 @@ public sealed class TelnetAppenderTest
   /// https://github.com/apache/logging-log4net/issues/194
   /// 
https://stackoverflow.com/questions/79053363/log4net-telnetappender-doesnt-work-after-migrate-to-log4net-3
   /// </remarks>
+  /// <summary>
+  /// Maximum time to wait for a message to arrive at the client.
+  /// </summary>
+  private static readonly TimeSpan _receiveTimeout = TimeSpan.FromSeconds(30);
+
+  private const string WelcomeMessage = "TelnetAppender";
+
   [Test]
   public void TelnetTest()
   {
-    List<string> received = [];
+    // The received data is a TCP byte stream - the server writes are not 
necessarily
+    // mapped 1:1 to the client reads, so the test asserts on the accumulated 
text
+    // instead of counting the individual reads.
+    StringBuilder received = new();
+    object receivedSyncRoot = new();
 
     XmlDocument log4NetConfig = new();
-    int port = 9090;
+    int port = FindFreeTcpPort();
     log4NetConfig.LoadXml(
       $"""
       <log4net>
@@ -68,38 +82,70 @@ public void TelnetTest()
     string logId = Guid.NewGuid().ToString();
     ILoggerRepository repository = LogManager.CreateRepository(logId);
     XmlConfigurator.Configure(repository, log4NetConfig["log4net"]!);
-    using (SimpleTelnetClient telnetClient = new(Received, port))
+    try
+    {
+      using (SimpleTelnetClient telnetClient = new(Received, port))
+      {
+        TestContext.Out.WriteLine("test: starting client ...");
+        telnetClient.Run(TestContext.Out.WriteLine);
+        WaitForReceived("welcome message", WelcomeMessage);
+        ILogger logger = repository.GetLogger("Telnet");
+        TestContext.Out.WriteLine("test: logging to client ...");
+        logger.Log(typeof(TelnetAppenderTest), Level.Info, logId, null);
+        TestContext.Out.WriteLine("test: waiting for message of client ...");
+        WaitForReceived("log message", logId);
+        TestContext.Out.WriteLine("test: canceling client ...");
+      }
+    }
+    finally
+    {
+      repository.Shutdown();
+    }
+    Assert.That(ReceivedText(), 
Does.StartWith(WelcomeMessage).And.Contain(logId));
+
+    void Received(string message)
     {
-      TestContext.Out.WriteLine("test: starting client ...");
-      telnetClient.Run(TestContext.Out.WriteLine);
-      WaitForReceived(1); // wait for welcome message
-      ILogger logger = repository.GetLogger("Telnet");
-      TestContext.Out.WriteLine("test: logging to client ...");
-      logger.Log(typeof(TelnetAppenderTest), Level.Info, logId, null);
-      TestContext.Out.WriteLine("test: waiting for message of client ...");
-      WaitForReceived(2); // wait for log message
-      TestContext.Out.WriteLine("test: canceling client ...");
+      lock (receivedSyncRoot)
+      {
+        received.Append(message);
+      }
     }
-    repository.Shutdown();
-    Assert.That(received, Has.Count.EqualTo(2));
-    Assert.That(received[1], Does.Contain(logId));
 
-    void Received(string message) => received.Add(message);
+    string ReceivedText()
+    {
+      lock (receivedSyncRoot)
+      {
+        return received.ToString();
+      }
+    }
 
-    void WaitForReceived(int count)
+    void WaitForReceived(string what, string expected)
     {
-      int retries = 1;
-      while (received.Count < count)
+      Stopwatch stopwatch = Stopwatch.StartNew();
+      while (ReceivedText().IndexOf(expected, StringComparison.Ordinal) < 0)
       {
-        retries++;
-        TestContext.Out.WriteLine($"receiver: waiting for message {count} of 
client - retry {retries} failed");
-        if (retries > 500)
+        if (stopwatch.Elapsed > _receiveTimeout)
         {
-          Assert.Fail("Timeout waiting for received messages");
+          Assert.Fail($"Timeout waiting for {what} - received so far: 
'{ReceivedText()}'");
         }
-        Thread.Sleep(10);
+        Thread.Sleep(20);
       }
-      TestContext.Out.WriteLine($"receiver: waiting for message {count} of 
client - retry {retries} succeeded");
+      TestContext.Out.WriteLine($"receiver: received {what} after 
{stopwatch.ElapsedMilliseconds} ms");
+    }
+  }
+
+  /// <summary>
+  /// Asks the OS for a currently unused TCP port - a fixed port would collide 
with
+  /// other tests or processes on the build machine.
+  /// </summary>
+  private static int FindFreeTcpPort()
+  {
+    using Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, 
ProtocolType.Tcp);
+    socket.Bind(new IPEndPoint(IPAddress.Any, 0));
+    if (socket.LocalEndPoint is IPEndPoint endPoint)
+    {
+      return endPoint.Port;
     }
+    throw new InvalidOperationException("Could not determine a free TCP port");
   }
 }
\ No newline at end of file
diff --git a/src/log4net.sln b/src/log4net.sln
index 3ecbc281..f0c53061 100644
--- a/src/log4net.sln
+++ b/src/log4net.sln
@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.9.34622.214
+# Visual Studio Version 18
+VisualStudioVersion = 18.7.11903.348 stable
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "log4net", 
"log4net\log4net.csproj", "{181FE707-E161-4722-9F38-6AAAB6FAA106}"
 EndProject
@@ -56,6 +56,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = 
".scripts", ".scripts", "{C0
                ..\scripts\update-version.ps1 = ..\scripts\update-version.ps1
        EndProjectSection
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "log4net.Ext.Mail", 
"log4net.Ext.Mail\log4net.Ext.Mail.csproj", 
"{73AA7739-6E5E-6256-E2A2-6A3B79864E0B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "log4net.Ext.Mail.Tests", 
"log4net.Ext.Mail.Tests\log4net.Ext.Mail.Tests.csproj", 
"{56A9114E-30BC-0ED0-843E-97613E9E6221}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
@@ -94,6 +98,14 @@ Global
                {605058A8-CE6D-4190-91DA-7B3E9F84489E}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
                {605058A8-CE6D-4190-91DA-7B3E9F84489E}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
                {605058A8-CE6D-4190-91DA-7B3E9F84489E}.Release|Any CPU.Build.0 
= Release|Any CPU
+               {73AA7739-6E5E-6256-E2A2-6A3B79864E0B}.Debug|Any CPU.ActiveCfg 
= Debug|Any CPU
+               {73AA7739-6E5E-6256-E2A2-6A3B79864E0B}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
+               {73AA7739-6E5E-6256-E2A2-6A3B79864E0B}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
+               {73AA7739-6E5E-6256-E2A2-6A3B79864E0B}.Release|Any CPU.Build.0 
= Release|Any CPU
+               {56A9114E-30BC-0ED0-843E-97613E9E6221}.Debug|Any CPU.ActiveCfg 
= Debug|Any CPU
+               {56A9114E-30BC-0ED0-843E-97613E9E6221}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
+               {56A9114E-30BC-0ED0-843E-97613E9E6221}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
+               {56A9114E-30BC-0ED0-843E-97613E9E6221}.Release|Any CPU.Build.0 
= Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
diff --git a/src/log4net/Appender/SmtpAppender.cs 
b/src/log4net/Appender/SmtpAppender.cs
index 77683ef9..65dc221a 100644
--- a/src/log4net/Appender/SmtpAppender.cs
+++ b/src/log4net/Appender/SmtpAppender.cs
@@ -49,9 +49,19 @@ namespace log4net.Appender;
 /// <para>
 /// To set the SMTP server port use the <see cref="Port"/> property. The 
default port is 25.
 /// </para>
+/// <para>
+/// This appender is deprecated, because <see 
cref="System.Net.Mail.SmtpClient"/> is no longer
+/// recommended by Microsoft. Use the MailKit based <c>SmtpAppender</c> from
+/// <c>log4net.Ext.Mail</c> instead.
+/// </para>
 /// </remarks>
 /// <author>Nicko Cadell</author>
 /// <author>Gert Driesen</author>
+[Obsolete("""
+  SmtpAppender sends through System.Net.Mail.SmtpClient, which Microsoft no 
longer recommends.
+  Use the MailKit based SmtpAppender from log4net.Ext.Mail instead, see
+  
https://logging.apache.org/log4net/manual/configuration/appenders/smtpappender.html
+  """)]
 public class SmtpAppender : BufferingAppenderSkeleton
 {
   /// <summary>
diff --git a/src/log4net/Appender/TelnetAppender.cs 
b/src/log4net/Appender/TelnetAppender.cs
index ca4cff4c..adb4cdae 100644
--- a/src/log4net/Appender/TelnetAppender.cs
+++ b/src/log4net/Appender/TelnetAppender.cs
@@ -338,13 +338,17 @@ private void OnConnect(IAsyncResult asyncResult)
         int currentActiveConnectionsCount = _clients.Count;
         if (currentActiveConnectionsCount < MaxConnections)
         {
+          // Register the client before sending the welcome message, otherwise 
a client
+          // that logs as soon as it has received the welcome message can race 
with
+          // AddClient and see HasConnections == false.
+          AddClient(client);
           try
           {
             client.Send($"TelnetAppender v1.0 ({currentActiveConnectionsCount 
+ 1} active connections)\r\n\r\n");
-            AddClient(client);
           }
           catch (Exception e) when (!e.IsFatal())
           {
+            RemoveClient(client);
             client.Dispose();
           }
         }
diff --git a/src/log4net/Util/Log4NetAssert.cs 
b/src/log4net/Util/Log4NetAssert.cs
index 28ea8894..88eef2cb 100644
--- a/src/log4net/Util/Log4NetAssert.cs
+++ b/src/log4net/Util/Log4NetAssert.cs
@@ -42,7 +42,7 @@ private static ArgumentNullException ArgumentNull(string 
name, string? errorMess
   /// <returns>Value (when not null)</returns>
   /// <exception cref="ArgumentNullException" />
   public static T EnsureNotNull<T>([NotNull][ValidatedNotNull] this T? value,
-    [CallerArgumentExpression("value")] string name = "",
+    [CallerArgumentExpression(nameof(value))] string name = "",
     string? errorMessage = null)
     where T : class
   {
@@ -54,6 +54,31 @@ private static ArgumentNullException ArgumentNull(string 
name, string? errorMess
     return value;
   }
 
+  /// <summary>
+  /// Ensures that <paramref name="value"/> is not <see langword="null"/> or 
empty and returns the validated value
+  /// </summary>
+  /// <param name="value">Value to validate</param>
+  /// <param name="name">Name of the value</param>
+  /// <param name="errorMessage">Error message (optional)</param>
+  /// <returns>Value (when not null)</returns>
+  /// <exception cref="ArgumentNullException" />
+  public static string EnsureNotNullOrEmpty(
+    [NotNull][ValidatedNotNull] this string? value,
+    [CallerArgumentExpression(nameof(value))] string name = "",
+    string? errorMessage = null)
+  {
+    // Not string.IsNullOrEmpty: its [NotNullWhen(false)] annotation is absent 
from the
+    // netstandard2.0 reference assembly, so it would not satisfy [NotNull].
+    if (value is string { Length: > 0})
+    {
+      return value;
+    }
+    throw ArgumentNull(name, NotNullOrEmptyMessage(errorMessage, name));
+  }
+
+  private static string NotNullOrEmptyMessage(string? existingMessage, string 
name)
+     => existingMessage ?? string.Format("'{0}' cannot be null or empty.", 
name);
+
   /// <summary>
   /// Ensures that <paramref name="value"/> is not null and an instance of 
<typeparamref name="T"/>
   /// and returns the validated value
@@ -67,7 +92,7 @@ private static ArgumentNullException ArgumentNull(string 
name, string? errorMess
   /// <exception cref="InvalidCastException" />
   public static T EnsureIs<T>(
     [NotNull][ValidatedNotNull] this object? value,
-    [CallerArgumentExpression("value")] string name = "",
+    [CallerArgumentExpression(nameof(value))] string name = "",
     string? errorMessage = null)
   {
     if (value is T result)
diff --git a/src/log4net/log4net.csproj b/src/log4net/log4net.csproj
index c1568607..3f78b5ff 100644
--- a/src/log4net/log4net.csproj
+++ b/src/log4net/log4net.csproj
@@ -1,6 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <Version>3.3.3</Version>
     <PackageId>log4net</PackageId>
     <Product>Apache log4net</Product>
     <Title>$(Product)</Title>
diff --git a/src/site/antora/modules/ROOT/pages/features.adoc 
b/src/site/antora/modules/ROOT/pages/features.adoc
index b268c3d8..e5eb0141 100644
--- a/src/site/antora/modules/ROOT/pages/features.adoc
+++ b/src/site/antora/modules/ROOT/pages/features.adoc
@@ -103,6 +103,7 @@ The RollingFileAppender can be configured to log to 
multiple files based upon da
 
 |xref:manual/configuration/appenders/smtpappender.adoc[]
 |Sends logging events to an email address.
+The MailKit based appender from the `log4net.Ext.Mail` package is recommended; 
the built-in variant relies on the deprecated `System.Net.Mail.SmtpClient`.
 
 |xref:manual/configuration/appenders/smtppickupdirappender.adoc[]
 |Sends logging events to an email address but writes the emails to a 
configurable directory rather than sending them directly via SMTP.
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
index 6bfa9983..e2e0c18a 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
@@ -92,6 +92,7 @@ The RollingFileAppender can be configured to log to multiple 
files based upon da
 
 |xref:manual/configuration/appenders/smtpappender.adoc[]
 |Sends logging events to an email address.
+The MailKit based appender from the `log4net.Ext.Mail` package is recommended; 
the built-in variant relies on the deprecated `System.Net.Mail.SmtpClient`.
 
 |xref:manual/configuration/appenders/smtppickupdirappender.adoc[]
 |Sends logging events to an email address but writes the emails to a 
configurable directory rather than sending them directly via SMTP.
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtpappender.adoc
 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtpappender.adoc
index aa1a35e9..2f5ca1ee 100644
--- 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtpappender.adoc
+++ 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtpappender.adoc
@@ -21,6 +21,50 @@
 The `SmtpAppender` sends log events via email using the Simple Mail Transfer 
Protocol (SMTP).
 This appender is useful for sending email notifications when certain events 
occur, such as errors or critical failures in your application.
 
+There are two implementations to choose from:
+
+[cols="Type,Assembly,SMTP client"]
+|===
+|Type |Assembly |SMTP client
+
+|`log4net.Ext.Mail.Appender.SmtpAppender` (recommended)
+|`log4net.Ext.Mail`
+|https://github.com/jstedfast/MailKit[MailKit]
+
+|`log4net.Appender.SmtpAppender` (deprecated)
+|`log4net`
+|`System.Net.Mail.SmtpClient`
+|===
+
+[IMPORTANT]
+====
+The built-in `log4net.Appender.SmtpAppender` is *no longer recommended*.
+
+It sends mail through 
https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient#remarks[System.Net.Mail.SmtpClient],
 which Microsoft no longer recommends for new development because it does not 
support many modern protocols.
+In practice this shows up as connection failures against modern mail servers, 
most often around SSL/TLS negotiation.
+
+Use the xref:#mailkit-smtpappender[MailKit based `SmtpAppender`] instead.
+It offers the same configuration options, so migrating usually means changing 
nothing but the `type` attribute.
+====
+
+[#mailkit-smtpappender]
+== MailKit based SmtpAppender (recommended)
+
+This appender lives in the separate `log4net.Ext.Mail` package, so that 
log4net itself does not take on a mail dependency.
+Add it to your project:
+
+[source,shell]
+----
+dotnet add package log4net.Ext.Mail
+----
+
+Because the appender is not part of the `log4net` assembly, the `type` 
attribute must be assembly-qualified:
+
+[source,xml]
+----
+type="log4net.Ext.Mail.Appender.SmtpAppender, log4net.Ext.Mail"
+----
+
 The following example shows how to configure the `SmtpAppender` to deliver log 
events via SMTP email.
 The `To`, `From`, `Subject` and `SmtpHost` are required parameters.
 This example shows how to deliver only significant events.
@@ -33,7 +77,7 @@ Messages not sent will be discarded.
 
 [source,xml]
 ----
-<appender name="SmtpAppender" type="log4net.Appender.SmtpAppender">
+<appender name="SmtpAppender" type="log4net.Ext.Mail.Appender.SmtpAppender, 
log4net.Ext.Mail">
   <to value="[email protected]" />
   <from value="[email protected]" />
   <subject value="test logging message" />
@@ -53,7 +97,7 @@ This example shows how to configure the `SmtpAppender` to 
deliver all messages i
 
 [source,xml]
 ----
-<appender name="SmtpAppender" type="log4net.Appender.SmtpAppender">
+<appender name="SmtpAppender" type="log4net.Ext.Mail.Appender.SmtpAppender, 
log4net.Ext.Mail">
   <to value="[email protected]" />
   <from value="[email protected]" />
   <subject value="test logging message" />
@@ -70,7 +114,7 @@ This example shows a more verbose formatting layout for the 
mail messages.
 
 [source,xml]
 ----
-<appender name="SmtpAppender" type="log4net.Appender.SmtpAppender,log4net">
+<appender name="SmtpAppender" type="log4net.Ext.Mail.Appender.SmtpAppender, 
log4net.Ext.Mail">
   <to value="[email protected]" />
   <from value="[email protected]" />
   <subject value="test logging message" />
@@ -84,4 +128,142 @@ This example shows a more verbose formatting layout for 
the mail messages.
     <conversionPattern value="%property{log4net:HostName} :: %level :: 
%message %newlineLogger: %logger%newlineThread: %thread%newlineDate: 
%date%newline%newline" />
   </layout>
 </appender>
-----
\ No newline at end of file
+----
+
+This example authenticates against a mail server that requires an encrypted 
connection on the submission port.
+
+[source,xml]
+----
+<appender name="SmtpAppender" type="log4net.Ext.Mail.Appender.SmtpAppender, 
log4net.Ext.Mail">
+  <to value="[email protected],[email protected]" />
+  <cc value="[email protected]" />
+  <from value="[email protected]" />
+  <replyTo value="[email protected]" />
+  <subject value="Errors from the payment service" />
+  <smtpHost value="smtp.example.com" />
+  <port value="587" />
+  <enableSsl value="true" />
+  <authentication value="Basic" />
+  <username value="[email protected]" />
+  <password value="secret" />
+  <priority value="High" />
+  <bufferSize value="512" />
+  <lossy value="true" />
+  <evaluator type="log4net.Core.LevelEvaluator">
+    <threshold value="ERROR"/>
+  </evaluator>
+  <layout type="log4net.Layout.PatternLayout">
+    <conversionPattern value="%newline%date [%thread] %-5level %logger - 
%message%newline" />
+  </layout>
+</appender>
+----
+
+[#mailkit-smtpappender-options]
+=== Options
+
+[cols="Option,Description"]
+|===
+|Option |Description
+
+|`to`
+|*Required.* Comma-delimited list of recipient email addresses.
+
+|`from`
+|*Required.* The sender's email address.
+
+|`subject`
+|*Required.* The subject line of the email.
+
+|`smtpHost`
+|*Required.* The name or address of the SMTP relay server.
+
+|`cc`
+|Comma-delimited list of addresses to carbon copy.
+
+|`bcc`
+|Comma-delimited list of addresses to blind carbon copy.
+
+|`replyTo`
+|The reply-to address.
+
+|`port`
+|The port the SMTP server listens on. Defaults to `25`.
+
+|`enableSsl`
+|Whether to secure the connection. Defaults to `false`. See 
xref:#mailkit-smtpappender-differences[].
+
+|`authentication`
+|One of `None` (default), `Basic` or `Ntlm`. `Basic` and `Ntlm` both require 
`username` and `password`.
+
+|`username`
+|The user name to authenticate with. Ignored when `authentication` is `None`.
+
+|`password`
+|The password to authenticate with. Ignored when `authentication` is `None`.
+
+|`priority`
+|One of `Low`, `Normal` (default) or `High`, mapped onto the MIME `Priority` 
header.
+
+|`subjectEncoding`
+|The encoding of the subject line, for example `utf-8`. Defaults to `utf-8`.
+
+|`bodyEncoding`
+|The encoding of the message body, for example `utf-8`. Defaults to `utf-8`.
+
+|`bufferSize`
+|How many log events to buffer into one email. See 
xref:manual/configuration/appenders.adoc[].
+
+|`lossy`
+|Whether buffered events may be discarded rather than sent.
+
+|`evaluator`
+|An `ITriggeringEventEvaluator` deciding which events trigger a send.
+|===
+
+`to`, `cc` and `bcc` also accept semicolons as separators, and quoted display 
names such as `"Doe, John" <[email protected]>`.
+
+[#mailkit-smtpappender-differences]
+=== Differences from the legacy appender
+
+The options above are named exactly as in the legacy appender, but a few 
behave differently:
+
+* `smtpHost` is *required*. MailKit has no notion of a machine-wide default 
SMTP server, so there is nothing to fall back on when the option is omitted.
+* `enableSsl` set to `true` negotiates transport security automatically: 
implicit TLS on port 465, otherwise `STARTTLS` when the server advertises it.
+Set to `false`, the connection is not encrypted at all.
+* `authentication` set to `Ntlm` requires `username` and `password`.
+MailKit cannot reuse the Windows logon session of the current thread or 
process, which the legacy appender did.
+* Semicolon-delimited recipient lists in `to`, `cc` and `bcc` are parsed 
correctly.
+
+[#legacy-smtpappender]
+== Built-in SmtpAppender (deprecated)
+
+[NOTE]
+====
+This appender is still shipped and still works, but it is marked `[Obsolete]`, 
so referencing
+`log4net.Appender.SmtpAppender` from code produces a compiler warning.
+Configuring it from XML keeps working without any warning.
+See the note at the xref:#smtpappender[top of this page].
+====
+
+The built-in appender is configured with the same options, except for the 
differences listed in xref:#mailkit-smtpappender-differences[].
+It needs no assembly-qualified type name, because it is part of the `log4net` 
assembly.
+
+[source,xml]
+----
+<appender name="SmtpAppender" type="log4net.Appender.SmtpAppender">
+  <to value="[email protected]" />
+  <from value="[email protected]" />
+  <subject value="test logging message" />
+  <smtpHost value="smtp.example.com" />
+  <bufferSize value="512" />
+  <lossy value="true" />
+  <evaluator type="log4net.Core.LevelEvaluator">
+    <threshold value="WARN"/>
+  </evaluator>
+  <layout type="log4net.Layout.PatternLayout">
+    <conversionPattern value="%newline%date [%thread] %-5level %logger - 
%message%newline%newline%newline" />
+  </layout>
+</appender>
+----
+
+If you cannot take on the MailKit dependency, 
xref:manual/configuration/appenders/smtppickupdirappender.adoc[] is an 
alternative that avoids an SMTP client altogether by writing the emails to a 
pickup directory.
\ No newline at end of file
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtppickupdirappender.adoc
 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtppickupdirappender.adoc
index bd4929ec..742e22d9 100644
--- 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtppickupdirappender.adoc
+++ 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/smtppickupdirappender.adoc
@@ -21,6 +21,9 @@
 The `SmtpPickupDirAppender` is configured similarly to the 
xref:./smtpappender.adoc[].
 The only difference is that rather than specify a SmtpHost parameter a 
`PickupDir` must be specified.
 
+Because this appender writes the emails to disk and leaves their delivery to 
another process, it uses no SMTP client at all.
+It is therefore unaffected by the deprecation of `System.Net.Mail.SmtpClient`, 
and remains a good option when you cannot take on the MailKit dependency that 
the recommended xref:./smtpappender.adoc#mailkit-smtpappender[MailKit based 
`SmtpAppender`] requires.
+
 The `PickupDir` parameter is a path that must exist and the code executing the 
appender must have permission to create new files and write to them in this 
directory.
 The path is relative to the application's base directory 
(AppDomain.BaseDirectory).
 
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/supported-frameworks.adoc 
b/src/site/antora/modules/ROOT/pages/manual/supported-frameworks.adoc
index b2571b3d..682b1c2c 100644
--- a/src/site/antora/modules/ROOT/pages/manual/supported-frameworks.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/supported-frameworks.adoc
@@ -63,3 +63,7 @@ The following appenders are supported on the specified 
frameworks:
 | TraceAppender                 | x | x
 | UdpAppender                   | x | x
 |===
+
+The recommended MailKit based `SmtpAppender` is not part of the `log4net` 
assembly.
+It ships in the separate `log4net.Ext.Mail` package, which targets .NET 
Standard 2.0 and is therefore usable from both .NET Framework 4.7.1 and modern 
.NET applications.
+See xref:manual/configuration/appenders/smtpappender.adoc[] for details.

Reply via email to