jstedfast/MailKit · error · ServiceNotConnectedException

The SmtpClient must be connected before you can send…

Error message

The SmtpClient must be connected before you can send commands.

What it means

AsyncSmtpClient.SendCommandAsync validates state before sending a raw protocol command. If the client has not completed a connection (IsConnected is false) it throws ServiceNotConnectedException with this message, since there is no protocol stream to write to.

Solutions

  1. Await ConnectAsync (with host, port and options) before calling SendCommandAsync.
  2. Check the IsConnected property before sending raw commands.
  3. If the connection dropped, reconnect (ConnectAsync) before issuing commands.
  4. Catch ServiceNotConnectedException and trigger reconnection logic.

Example fix

// before
await client.SendCommandAsync("NOOP\r\n");
// after
if (!client.IsConnected)
    await client.ConnectAsync("smtp.example.com", 465, SecureSocketOptions.SslOnConnect);
await client.SendCommandAsync("NOOP\r\n");
Defensive patterns

Strategy: validation

Validate before calling

if (client == null || client.IsDisposed) throw new ObjectDisposedException(nameof(client));
if (!client.IsConnected)
    await client.ConnectAsync(smtpHost, smtpPort, SecureSocketOptions.StartTls);

Type guard

bool CanSendCommands(AsyncSmtpClient c) => c != null && !c.IsDisposed && c.IsConnected;

Try / catch

try {
    var resp = await client.SendCommandAsync("NOOP\r\n");
} catch (ServiceNotConnectedException) {
    await client.ConnectAsync(smtpHost, smtpPort, SecureSocketOptions.Auto);
    // retry the command
}

Prevention

When it happens

Trigger: Calling SendCommandAsync before ConnectAsync, or after the connection dropped, was disconnected via DisconnectAsync, or failed during the SMTP handshake.

Common situations: Sending custom commands at app startup before the connection task completes; reusing a client instance after a previous send failed and closed the session.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of jstedfast/MailKit@9d3859a785 (2026-09-15). Data as JSON: /api/errors/c609d33d4ab9fe4a. Report an issue: GitHub.

Appendix: source

Thrown at MailKit/Net/Smtp/AsyncSmtpClient.cs:132

		/// </exception>
		/// <exception cref="System.OperationCanceledException">
		/// The operation has been canceled.
		/// </exception>
		/// <exception cref="System.IO.IOException">
		/// An I/O error occurred.
		/// </exception>
		/// <exception cref="SmtpProtocolException">
		/// An SMTP protocol exception occurred.
		/// </exception>
		protected Task<SmtpResponse> SendCommandAsync (string command, CancellationToken cancellationToken = default)
		{
			if (command == null)
				throw new ArgumentNullException (nameof (command));

			CheckDisposed ();

			if (!IsConnected)
				throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can send commands.");

			if (!command.EndsWith ("\r\n", StringComparison.Ordinal))
				command += "\r\n";

			return SendCommandInternalAsync (command, cancellationToken);
		}

		Task<SmtpResponse> SendEhloAsync (bool connecting, string helo, CancellationToken cancellationToken)
		{
			var command = CreateEhloCommand (helo);

			if (connecting)
				return Stream!.SendCommandAsync (command, cancellationToken);

			return SendCommandInternalAsync (command, cancellationToken);
		}

		async Task EhloAsync (bool connecting, CancellationToken cancellationToken)

View on GitHub (pinned to 9d3859a785)