diff --git a/MailKit/MailKit.csproj b/MailKit/MailKit.csproj
index b95480bfb0..097b0b5bc0 100644
--- a/MailKit/MailKit.csproj
+++ b/MailKit/MailKit.csproj
@@ -114,6 +114,7 @@
+
diff --git a/MailKit/MailKitLite.csproj b/MailKit/MailKitLite.csproj
index ea6192d73a..37dc31252c 100644
--- a/MailKit/MailKitLite.csproj
+++ b/MailKit/MailKitLite.csproj
@@ -119,6 +119,7 @@
+
diff --git a/MailKit/Net/Proxy/ProxyClient.cs b/MailKit/Net/Proxy/ProxyClient.cs
index c2d8f26c75..d9b99202c8 100644
--- a/MailKit/Net/Proxy/ProxyClient.cs
+++ b/MailKit/Net/Proxy/ProxyClient.cs
@@ -31,6 +31,10 @@
using System.Net.Sockets;
using System.Threading.Tasks;
+#if NET6_0_OR_GREATER
+using System.Net.Http;
+#endif
+
namespace MailKit.Net.Proxy
{
///
@@ -45,6 +49,25 @@ namespace MailKit.Net.Proxy
///
public abstract class ProxyClient : IProxyClient
{
+#if NET6_0_OR_GREATER
+ static IProxyClient systemProxy;
+
+ ///
+ /// Get a client for the default system proxy.
+ ///
+ ///
+ /// Gets a client for the default system proxy.
+ ///
+ /// A client for the default system proxy.
+ public static IProxyClient SystemProxy {
+ get {
+ systemProxy ??= new WebProxyClient (HttpClient.DefaultProxy);
+
+ return systemProxy;
+ }
+ }
+#endif
+
///
/// Initializes a new instance of the class.
///
diff --git a/MailKit/Net/Proxy/WebProxyClient.cs b/MailKit/Net/Proxy/WebProxyClient.cs
new file mode 100644
index 0000000000..7c94a511d5
--- /dev/null
+++ b/MailKit/Net/Proxy/WebProxyClient.cs
@@ -0,0 +1,194 @@
+//
+// WebProxyClient.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2024 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#if NET6_0_OR_GREATER
+
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MailKit.Net.Proxy
+{
+ ///
+ /// A proxy client that makes use of a .
+ ///
+ ///
+ /// A proxy client that makes use of a .
+ ///
+ internal class WebProxyClient : ProxyClient
+ {
+ readonly IWebProxy proxy;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The web proxy.
+ ///
+ /// is null.
+ ///
+ public WebProxyClient (IWebProxy proxy) : base ("System", 0)
+ {
+ if (proxy is null)
+ throw new ArgumentNullException (nameof (proxy));
+
+ this.proxy = proxy;
+ }
+
+ static Uri GetTargetUri (string host, int port)
+ {
+ string scheme;
+
+ switch (port) {
+ case 25: case 465: case 587: scheme = "smtp"; break;
+ case 110: case 995: scheme = "pop"; break;
+ case 143: case 993: scheme = "imap"; break;
+ default: scheme = "http"; break;
+ }
+
+ return new Uri ($"{scheme}://{host}:{port}");
+ }
+
+ static NetworkCredential GetNetworkCredential (ICredentials credentials, Uri uri)
+ {
+ if (credentials is NetworkCredential network)
+ return network;
+
+ return credentials.GetCredential (uri, "Basic");
+ }
+
+ static ProxyClient GetProxyClient (Uri proxyUri, ICredentials credentials)
+ {
+ var credential = GetNetworkCredential (credentials, proxyUri);
+
+ if (proxyUri.Scheme.Equals ("https", StringComparison.OrdinalIgnoreCase))
+ return new HttpsProxyClient (proxyUri.Host, proxyUri.Port, credential);
+
+ if (proxyUri.Scheme.Equals ("http", StringComparison.OrdinalIgnoreCase))
+ return new HttpProxyClient (proxyUri.Host, proxyUri.Port, credential);
+
+ throw new NotImplementedException ($"The default system proxy does not support {proxyUri.Scheme}.");
+ }
+
+ ///
+ /// Connect to the target host.
+ ///
+ ///
+ /// Connects to the target host and port through the proxy server.
+ ///
+ /// The connected network stream.
+ /// The host name of the target server.
+ /// The target server port.
+ /// The cancellation token.
+ ///
+ /// is null.
+ ///
+ ///
+ /// is not between 0 and 65535.
+ ///
+ ///
+ /// The is a zero-length string.
+ ///
+ ///
+ /// The operation was canceled via the cancellation token.
+ ///
+ ///
+ /// A socket error occurred trying to connect to the remote host.
+ ///
+ ///
+ /// An I/O error occurred.
+ ///
+ public override Stream Connect (string host, int port, CancellationToken cancellationToken = default)
+ {
+ ValidateArguments (host, port);
+
+ var targetUri = GetTargetUri (host, port);
+ var proxyUri = proxy.GetProxy (targetUri);
+
+ if (proxyUri is null) {
+ // Note: if the proxy URI is null, then it means that the proxy should be bypassed.
+ var socket = SocketUtils.Connect (host, port, LocalEndPoint, cancellationToken);
+ return new NetworkStream (socket, true);
+ }
+
+ var proxyClient = GetProxyClient (proxyUri, proxy.Credentials);
+
+ return proxyClient.Connect (host, port, cancellationToken);
+ }
+
+ ///
+ /// Asynchronously connect to the target host.
+ ///
+ ///
+ /// Asynchronously connects to the target host and port through the proxy server.
+ ///
+ /// The connected network stream.
+ /// The host name of the target server.
+ /// The target server port.
+ /// The cancellation token.
+ ///
+ /// is null.
+ ///
+ ///
+ /// is not between 0 and 65535.
+ ///
+ ///
+ /// The is a zero-length string.
+ ///
+ ///
+ /// The operation was canceled via the cancellation token.
+ ///
+ ///
+ /// A socket error occurred trying to connect to the remote host.
+ ///
+ ///
+ /// An I/O error occurred.
+ ///
+ public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default)
+ {
+ ValidateArguments (host, port);
+
+ var targetUri = GetTargetUri (host, port);
+ var proxyUri = proxy.GetProxy (targetUri);
+
+ if (proxyUri is null) {
+ // Note: if the proxy URI is null, then it means that the proxy should be bypassed.
+ var socket = await SocketUtils.ConnectAsync (host, port, LocalEndPoint, cancellationToken).ConfigureAwait (false);
+ return new NetworkStream (socket, true);
+ }
+
+ var proxyClient = GetProxyClient (proxyUri, proxy.Credentials);
+
+ return await proxyClient.ConnectAsync (host, port, cancellationToken);
+ }
+ }
+}
+
+#endif
diff --git a/UnitTests/Net/Proxy/WebProxyClientTests.cs b/UnitTests/Net/Proxy/WebProxyClientTests.cs
new file mode 100644
index 0000000000..936b9365d9
--- /dev/null
+++ b/UnitTests/Net/Proxy/WebProxyClientTests.cs
@@ -0,0 +1,151 @@
+//
+// WebProxyClientTests.cs
+//
+// Author: Jeffrey Stedfast
+//
+// Copyright (c) 2013-2024 .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System.Net;
+
+using MailKit.Net.Proxy;
+
+namespace UnitTests.Net.Proxy
+{
+ [TestFixture]
+ public class WebProxyClientTests
+ {
+ const int ConnectTimeout = 5 * 1000; // 5 seconds
+
+ [Test]
+ public void TestArgumentExceptions ()
+ {
+ var credentials = new NetworkCredential ("user", "password");
+ var proxy = ProxyClient.SystemProxy;
+
+ Assert.Throws (() => new WebProxyClient (null));
+
+ Assert.That (proxy.ProxyPort, Is.EqualTo (1080));
+ Assert.That (proxy.ProxyHost, Is.EqualTo ("System"));
+ Assert.That (proxy.ProxyCredentials, Is.Null);
+
+ Assert.Throws (() => proxy.Connect (null, 80));
+ Assert.Throws (() => proxy.Connect (null, 80, ConnectTimeout));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync (null, 80));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync (null, 80, ConnectTimeout));
+
+ Assert.Throws (() => proxy.Connect (string.Empty, 80));
+ Assert.Throws (() => proxy.Connect (string.Empty, 80, ConnectTimeout));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync (string.Empty, 80));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync (string.Empty, 80, ConnectTimeout));
+
+ Assert.Throws (() => proxy.Connect ("www.google.com", 0));
+ Assert.Throws (() => proxy.Connect ("www.google.com", 0, ConnectTimeout));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync ("www.google.com", 0));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync ("www.google.com", 0, ConnectTimeout));
+
+ Assert.Throws (() => proxy.Connect ("www.google.com", 80, -ConnectTimeout));
+ Assert.ThrowsAsync (async () => await proxy.ConnectAsync ("www.google.com", 80, -ConnectTimeout));
+ }
+
+ [Test]
+ public void TestConnect ()
+ {
+ var proxy = ProxyClient.SystemProxy;
+ Stream stream = null;
+
+ try {
+ stream = proxy.Connect ("www.google.com", 80);
+ } catch (TimeoutException) {
+ Assert.Inconclusive ("Timed out.");
+ } catch (Exception ex) {
+ Assert.Fail (ex.Message);
+ } finally {
+ stream?.Dispose ();
+ }
+ }
+
+ [Test]
+ public async Task TestConnectAsync ()
+ {
+ var proxy = ProxyClient.SystemProxy;
+ Stream stream = null;
+
+ try {
+ stream = await proxy.ConnectAsync ("www.google.com", 80);
+ } catch (TimeoutException) {
+ Assert.Inconclusive ("Timed out.");
+ } catch (Exception ex) {
+ Assert.Fail (ex.Message);
+ } finally {
+ stream?.Dispose ();
+ }
+ }
+
+ [Test]
+ public void TestConnectViaWebProxy ()
+ {
+ using (var server = new HttpProxyListener ()) {
+ server.Start (IPAddress.Loopback, 0);
+
+ var credentials = new NetworkCredential ("username", "password");
+ var webProxy = new WebProxy (new Uri ($"http://{server.IPAddress}:{server.Port}"), true, null, credentials);
+
+ var proxy = new WebProxyClient (webProxy);
+ Stream stream = null;
+
+ try {
+ stream = proxy.Connect ("www.google.com", 80, ConnectTimeout);
+ } catch (TimeoutException) {
+ Assert.Inconclusive ("Timed out.");
+ } catch (Exception ex) {
+ Assert.Fail (ex.Message);
+ } finally {
+ stream?.Dispose ();
+ }
+ }
+ }
+
+ [Test]
+ public async Task TestConnectViaWebProxyAsync ()
+ {
+ using (var server = new HttpProxyListener ()) {
+ server.Start (IPAddress.Loopback, 0);
+
+ var credentials = new NetworkCredential ("username", "password");
+ var webProxy = new WebProxy (new Uri ($"http://{server.IPAddress}:{server.Port}"), true, null, credentials);
+
+ var proxy = new WebProxyClient (webProxy);
+ Stream stream = null;
+
+ try {
+ stream = await proxy.ConnectAsync ("www.google.com", 80, ConnectTimeout);
+ } catch (TimeoutException) {
+ Assert.Inconclusive ("Timed out.");
+ } catch (Exception ex) {
+ Assert.Fail (ex.Message);
+ } finally {
+ stream?.Dispose ();
+ }
+ }
+ }
+ }
+}