Skip to content

Commit

Permalink
Added SmtpClient.SendCommand[Async]() as new protected methods
Browse files Browse the repository at this point in the history
This will allow subclasses of SmtpClient to extend the functionality
by adding support for custom SMTP command extensions.

See issue #891 for feature request.
  • Loading branch information
jstedfast committed Aug 7, 2019
1 parent ac663d3 commit b395841
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 0 deletions.
44 changes: 44 additions & 0 deletions MailKit/Net/Smtp/AsyncSmtpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,50 @@ namespace MailKit.Net.Smtp
{
public partial class SmtpClient
{
/// <summary>
/// Asynchronously send a custom command to the SMTP server.
/// </summary>
/// <remarks>
/// <para>Asynchronously sends a custom command to the SMTP server.</para>
/// <note type="note">The command string should not include the terminating <c>\r\n</c> sequence.</note>
/// </remarks>
/// <returns>The command response.</returns>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="command"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.ObjectDisposedException">
/// The <see cref="SmtpClient"/> has been disposed.
/// </exception>
/// <exception cref="ServiceNotConnectedException">
/// The <see cref="SmtpClient"/> is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation has been canceled.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="SmtpCommandException">
/// The SMTP command failed.
/// </exception>
/// <exception cref="SmtpProtocolException">
/// An SMTP protocol exception occurred.
/// </exception>
protected Task<SmtpResponse> SendCommandAsync (string command, CancellationToken cancellationToken = default (CancellationToken))
{
if (command == null)
throw new ArgumentNullException (nameof (command));

CheckDisposed ();

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

return SendCommandAsync (command, true, cancellationToken);
}

/// <summary>
/// Asynchronously authenticate using the specified SASL mechanism.
/// </summary>
Expand Down
44 changes: 44 additions & 0 deletions MailKit/Net/Smtp/SmtpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,50 @@ async Task<SmtpResponse> SendCommandAsync (string command, bool doAsync, Cancell
return Stream.ReadResponse (cancellationToken);
}

/// <summary>
/// Send a custom command to the SMTP server.
/// </summary>
/// <remarks>
/// <para>Sends a custom command to the SMTP server.</para>
/// <note type="note">The command string should not include the terminating <c>\r\n</c> sequence.</note>
/// </remarks>
/// <returns>The command response.</returns>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="command"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.ObjectDisposedException">
/// The <see cref="SmtpClient"/> has been disposed.
/// </exception>
/// <exception cref="ServiceNotConnectedException">
/// The <see cref="SmtpClient"/> is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation has been canceled.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="SmtpCommandException">
/// The SMTP command failed.
/// </exception>
/// <exception cref="SmtpProtocolException">
/// An SMTP protocol exception occurred.
/// </exception>
protected SmtpResponse SendCommand (string command, CancellationToken cancellationToken = default (CancellationToken))
{
if (command == null)
throw new ArgumentNullException (nameof (command));

CheckDisposed ();

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

return SendCommandAsync (command, false, cancellationToken).GetAwaiter ().GetResult ();
}

async Task<SmtpResponse> SendEhloAsync (bool ehlo, bool doAsync, CancellationToken cancellationToken)
{
string command = ehlo ? "EHLO " : "HELO ";
Expand Down
147 changes: 147 additions & 0 deletions UnitTests/Net/Smtp/SmtpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2532,5 +2532,152 @@ public async Task TestDeliveryStatusNotificationAsync ()
Assert.IsFalse (client.IsConnected, "Failed to disconnect");
}
}

class CustomSmtpClient : SmtpClient
{
public SmtpResponse SendCommand (string command)
{
return SendCommand (command, CancellationToken.None);
}

public Task<SmtpResponse> SendCommandAsync (string command)
{
return SendCommandAsync (command, CancellationToken.None);
}
}

[Test]
public void TestCustomCommand ()
{
var commands = new List<SmtpReplayCommand> ();
commands.Add (new SmtpReplayCommand ("", "comcast-greeting.txt"));
commands.Add (new SmtpReplayCommand ("EHLO unit-tests.mimekit.org\r\n", "comcast-ehlo.txt"));
commands.Add (new SmtpReplayCommand ("VRFY Smith\r\n", "rfc0821-vrfy.txt"));
commands.Add (new SmtpReplayCommand ("EXPN Example-People\r\n", "rfc0821-expn.txt"));

using (var client = new CustomSmtpClient ()) {
client.LocalDomain = "unit-tests.mimekit.org";

Assert.Throws<ServiceNotConnectedException> (() => client.SendCommand ("COMMAND"));

try {
client.ReplayConnect ("localhost", new SmtpReplayStream (commands, false));
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Connect: {0}", ex);
}

Assert.IsTrue (client.IsConnected, "Client failed to connect.");
Assert.IsFalse (client.IsSecure, "IsSecure should be false.");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.Authentication), "Failed to detect AUTH extension");
Assert.IsTrue (client.AuthenticationMechanisms.Contains ("LOGIN"), "Failed to detect the LOGIN auth mechanism");
Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Failed to detect the PLAIN auth mechanism");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.EightBitMime), "Failed to detect 8BITMIME extension");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.EnhancedStatusCodes), "Failed to detect ENHANCEDSTATUSCODES extension");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.Size), "Failed to detect SIZE extension");
Assert.AreEqual (36700160, client.MaxSize, "Failed to parse SIZE correctly");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.StartTLS), "Failed to detect STARTTLS extension");

Assert.Throws<ArgumentException> (() => client.Capabilities |= SmtpCapabilities.UTF8);

Assert.AreEqual (120000, client.Timeout, "Timeout");
client.Timeout *= 2;

Assert.Throws<ArgumentNullException> (() => client.SendCommandAsync (null));

SmtpResponse response = null;

try {
response = client.SendCommand ("VRFY Smith");
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Verify: {0}", ex);
}

Assert.NotNull (response, "VRFY result");
Assert.AreEqual (SmtpStatusCode.Ok, response.StatusCode, "VRFY response code");
Assert.AreEqual ("Fred Smith <Smith@USC-ISIF.ARPA>", response.Response, "VRFY response");

try {
response = client.SendCommand ("EXPN Example-People");
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Expand: {0}", ex);
}

Assert.NotNull (response, "EXPN result");
Assert.AreEqual (SmtpStatusCode.Ok, response.StatusCode, "EXPN response code");
Assert.AreEqual ("Jon Postel <Postel@USC-ISIF.ARPA>\nFred Fonebone <Fonebone@USC-ISIQ.ARPA>\nSam Q. Smith <SQSmith@USC-ISIQ.ARPA>\nQuincy Smith <@USC-ISIF.ARPA:Q-Smith@ISI-VAXA.ARPA>\n<joe@foo-unix.ARPA>\n<xyz@bar-unix.ARPA>", response.Response, "EXPN response");
}
}

[Test]
public async Task TestCustomCommandAsync ()
{
var commands = new List<SmtpReplayCommand> ();
commands.Add (new SmtpReplayCommand ("", "comcast-greeting.txt"));
commands.Add (new SmtpReplayCommand ("EHLO unit-tests.mimekit.org\r\n", "comcast-ehlo.txt"));
commands.Add (new SmtpReplayCommand ("VRFY Smith\r\n", "rfc0821-vrfy.txt"));
commands.Add (new SmtpReplayCommand ("EXPN Example-People\r\n", "rfc0821-expn.txt"));

using (var client = new CustomSmtpClient ()) {
client.LocalDomain = "unit-tests.mimekit.org";

Assert.Throws<ServiceNotConnectedException> (async () => await client.SendCommandAsync ("COMMAND"));

try {
await client.ReplayConnectAsync ("localhost", new SmtpReplayStream (commands, true));
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Connect: {0}", ex);
}

Assert.IsTrue (client.IsConnected, "Client failed to connect.");
Assert.IsFalse (client.IsSecure, "IsSecure should be false.");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.Authentication), "Failed to detect AUTH extension");
Assert.IsTrue (client.AuthenticationMechanisms.Contains ("LOGIN"), "Failed to detect the LOGIN auth mechanism");
Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Failed to detect the PLAIN auth mechanism");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.EightBitMime), "Failed to detect 8BITMIME extension");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.EnhancedStatusCodes), "Failed to detect ENHANCEDSTATUSCODES extension");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.Size), "Failed to detect SIZE extension");
Assert.AreEqual (36700160, client.MaxSize, "Failed to parse SIZE correctly");

Assert.IsTrue (client.Capabilities.HasFlag (SmtpCapabilities.StartTLS), "Failed to detect STARTTLS extension");

Assert.Throws<ArgumentException> (() => client.Capabilities |= SmtpCapabilities.UTF8);

Assert.AreEqual (120000, client.Timeout, "Timeout");
client.Timeout *= 2;

Assert.Throws<ArgumentNullException> (async () => await client.SendCommandAsync (null));

SmtpResponse response = null;

try {
response = await client.SendCommandAsync ("VRFY Smith");
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Verify: {0}", ex);
}

Assert.NotNull (response, "VRFY result");
Assert.AreEqual (SmtpStatusCode.Ok, response.StatusCode, "VRFY response code");
Assert.AreEqual ("Fred Smith <Smith@USC-ISIF.ARPA>", response.Response, "VRFY response");

try {
response = await client.SendCommandAsync ("EXPN Example-People");
} catch (Exception ex) {
Assert.Fail ("Did not expect an exception in Expand: {0}", ex);
}

Assert.NotNull (response, "EXPN result");
Assert.AreEqual (SmtpStatusCode.Ok, response.StatusCode, "EXPN response code");
Assert.AreEqual ("Jon Postel <Postel@USC-ISIF.ARPA>\nFred Fonebone <Fonebone@USC-ISIQ.ARPA>\nSam Q. Smith <SQSmith@USC-ISIQ.ARPA>\nQuincy Smith <@USC-ISIF.ARPA:Q-Smith@ISI-VAXA.ARPA>\n<joe@foo-unix.ARPA>\n<xyz@bar-unix.ARPA>", response.Response, "EXPN response");
}
}
}
}

0 comments on commit b395841

Please sign in to comment.