Skip to content

Invalidate SmtpClient connection when Host or Port changes - #132770

Open
rzikm wants to merge 8 commits into
mainfrom
rzikm/smtpclient-invalidate-connection-on-host
Open

Invalidate SmtpClient connection when Host or Port changes#132770
rzikm wants to merge 8 commits into
mainfrom
rzikm/smtpclient-invalidate-connection-on-host

Conversation

@rzikm

@rzikm rzikm commented Aug 26, 2026

Copy link
Copy Markdown
Member

Why

SmtpClient caches a live SmtpConnection in its transport and reuses it across Send calls. However, the Host and Port setters only cleared the legacy _servicePoint field and never dropped the cached connection. As a result, changing Host or Port between sends kept delivering mail to the original server instead of the newly configured one.

What

When Host or Port actually changes, release the cached transport connection so the next send re-establishes a fresh connection to the new target. Normal connection reuse (when the value is unchanged) is preserved, and the setters already throw while a send is in progress, so this never runs mid-send. ReleaseConnection is null-safe when no connection was ever opened.

Tests

Added regression tests in SmtpClientConnectionTest, which run across all three send paths (Send, SendAsync, SendMailAsync):

  • ChangingPort_DoesNotReuseConnectionToPreviousServer - points the client at a second loopback server and asserts the next message reaches the new server and not the original.
  • ChangingHost_EstablishesNewConnection - asserts a new connection is opened after the host changes.

Verified both tests fail without the fix and pass with it; the full System.Net.Mail functional suite (457 tests) passes.

Note

This PR was authored by GitHub Copilot.

SmtpClient caches a live SmtpConnection in its transport and reuses it across Send calls, but the Host and Port setters only cleared the legacy _servicePoint and never dropped the cached connection. As a result, changing Host or Port between sends kept delivering mail to the original server.

Release the cached connection when Host or Port actually changes so the next send establishes a fresh connection to the new target. Add regression tests across the sync Send, SendAsync, and SendMailAsync paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Copilot AI lite review requested due to automatic review settings August 26, 2026 09:05
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.Mail.SmtpClient so that changing Host or Port invalidates any cached SMTP transport connection, ensuring subsequent sends connect to the newly configured endpoint rather than reusing a connection established for the previous host/port.

Changes:

  • Release the cached transport connection when SmtpClient.Host changes.
  • Release the cached transport connection when SmtpClient.Port changes.
  • Add functional regression tests to verify host/port changes result in new connections and correct delivery.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs Releases the cached SMTP transport connection when Host/Port changes.
src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs Adds regression tests ensuring host/port changes establish a new connection and deliver to the intended server.

Comment thread src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs
Comment thread src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs Outdated
rzikm and others added 2 commits August 26, 2026 11:57
Extend the SmtpClient connection-reuse fix beyond Host/Port to cover the
other properties that change how a connection is established: Credentials,
UseDefaultCredentials, EnableSsl, and TargetName. Changing any of these now
invalidates the cached connection so the next send establishes a fresh one.

Move the potentially blocking connection shutdown off the property setters:
setters only mark the transport stale (InvalidateCachedConnection), and the
graceful close of the old connection happens lazily on the send path inside
GetConnectionAsync. IsConnected reports false while stale so EnsureConnection
falls through to reconnect.

Generalize the host regression test into a theory covering Host, Credentials,
and TargetName, and add a TLS test verifying that enabling EnableSsl after an
initial plaintext send establishes a new encrypted connection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Addresses review feedback: the default TargetName is derived once from the
host ("SMTPSVC/<host>"). When Host changed, the cached connection was
invalidated but TargetName kept targeting the previous host, causing a
Negotiate/NTLM SPN mismatch on the new connection.

Now, when Host changes and TargetName still holds the host-derived default,
TargetName is updated to match the new host. A TargetName explicitly set by
the caller is left untouched. Add test coverage for both the default-follows-
host and explicit-preservation cases.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Copilot AI review requested due to automatic review settings August 26, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs Outdated
Comment thread src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs Outdated
When GetConnectionAsync releases a previously cached SmtpConnection, only
gracefully QUIT one that was actually established; abort (force-close) a
connection whose connect attempt failed, since it may have no initialized
stream and the graceful path would dereference it and mask the original
failure. Also switch the connection test credential literal to the "foo"/"bar"
convention used throughout the mail tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Copilot AI review requested due to automatic review settings August 26, 2026 10:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs:23

  • _stale is written from property setters (no lock) and read from IsConnected on the send path; without synchronization this can be lost/seen late across threads, causing an old connection to be reused even after an invalidating configuration change. Marking the field volatile makes the invalidation reliably observable without widening locking.
        private readonly SmtpClient _client;
        private ICredentialsByHost? _credentials;
        private bool _shouldAbort;
        private bool _stale;

Comment thread src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs
Now that TargetName is connection-affecting and invalidates the cached
connection, guard its setter with the same _inCall check used by Host, Port,
Credentials, and Timeout so it throws SmtpInvalidOperationDuringSend rather than
mutating connection settings mid-send.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Copilot AI review requested due to automatic review settings August 26, 2026 10:33
The stale flag is written from property setters without holding the transport
lock and read from IsConnected on the send path. Marking it volatile makes an
invalidating configuration change reliably observable across threads without
widening locking, so a stale connection is not reused after invalidation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs:105

  • GetConnectionAsync performs potentially blocking shutdown work (ReleaseConnection() / Abort()) while holding the SmtpTransport lock. This increases lock hold times across network I/O (QUIT) and can delay SmtpTransport.Abort() (used by the send timeout path). Consider clearing _connection under the lock, releasing/aborting the previous connection outside the lock, then reacquiring the lock to create the new connection (so Abort() during shutdown can still flow through _shouldAbort).
        {
            lock (this)
            {
                // Release any previously cached connection (for example one that became stale
                // after a configuration change, or one whose connect attempt failed) before

Copilot AI review requested due to automatic review settings August 26, 2026 10:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs
EnableSsl is connection-affecting and invalidates the cached connection, so
guard its setter with the same _inCall check used by Host, Port, Credentials,
Timeout, and TargetName to prevent toggling SSL mid-send.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Copilot AI review requested due to automatic review settings August 26, 2026 10:58
GetConnectionAsync gracefully releasing a previously cached connection performs
a blocking QUIT over the network. Holding the transport lock across that I/O
delayed a concurrent Abort() (for example from the send-timeout path). Detach
the previous connection under the lock, shut it down outside the lock, then
reacquire the lock to create the new connection so an Abort() during shutdown
still flows through _shouldAbort. Sends are serialized by SmtpClient._inCall, so
no other GetConnectionAsync runs concurrently, and ShutdownConnection is
idempotent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d288b6ab-b489-4bb8-afb1-5552f3bae55d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants