Skip to content

Commit

Permalink
Added Pop3Client and ImapClient metrics/tracing for Connect and Authe…
Browse files Browse the repository at this point in the history
…nticate

Partial fix for issue #1499
  • Loading branch information
jstedfast committed Feb 24, 2024
1 parent cce6309 commit d00ad94
Show file tree
Hide file tree
Showing 12 changed files with 966 additions and 674 deletions.
18 changes: 9 additions & 9 deletions MailKit/ClientMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ namespace MailKit {
sealed class ClientMetrics
{
public readonly Histogram<double> ConnectionDuration;
public readonly Counter<long> TaskCounter;
public readonly Histogram<double> TaskDuration;
public readonly Counter<long> OperationCounter;
public readonly Histogram<double> OperationDuration;
public readonly string MeterName;

public ClientMetrics (Meter meter, string meterName, string an, string protocol)
Expand All @@ -47,15 +47,15 @@ public ClientMetrics (Meter meter, string meterName, string an, string protocol)
unit: "s",
description: $"The duration of successfully established connections to {an} {protocol} server.");

TaskCounter = meter.CreateCounter<long> (
name: $"{meterName}.client.task.count",
unit: "{command}",
description: $"The number of times a client performed a task on {an} {protocol} server.");
OperationCounter = meter.CreateCounter<long> (
name: $"{meterName}.client.operation.count",
unit: "{operation}",
description: $"The number of times a client performed an operation on {an} {protocol} server.");

TaskDuration = meter.CreateHistogram<double> (
name: $"{meterName}.client.task.duration",
OperationDuration = meter.CreateHistogram<double> (
name: $"{meterName}.client.operation.duration",
unit: "ms",
description: $"The amount of time it takes for the {protocol} server to perform a task.");
description: $"The amount of time it takes for the {protocol} server to perform an operation.");
}

public TagList GetTags (Uri uri, Exception ex)
Expand Down
266 changes: 147 additions & 119 deletions MailKit/Net/Imap/AsyncImapClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,22 +312,29 @@ public override async Task AuthenticateAsync (SaslMechanism mechanism, Cancellat
await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false);
};

detector.IsAuthenticating = true;
using var operation = engine.StartNetworkOperation (NetworkOperation.Authenticate);

try {
await engine.RunAsync (ic).ConfigureAwait (false);
} finally {
detector.IsAuthenticating = false;
}
detector.IsAuthenticating = true;

try {
await engine.RunAsync (ic).ConfigureAwait (false);
} finally {
detector.IsAuthenticating = false;
}

ProcessAuthenticateResponse (ic, mechanism);
ProcessAuthenticateResponse (ic, mechanism);

// Query the CAPABILITIES again if the server did not include an
// untagged CAPABILITIES response to the AUTHENTICATE command.
if (engine.CapabilitiesVersion == capabilitiesVersion)
await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false);
// Query the CAPABILITIES again if the server did not include an
// untagged CAPABILITIES response to the AUTHENTICATE command.
if (engine.CapabilitiesVersion == capabilitiesVersion)
await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false);

await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false);
await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
operation.SetError (ex);
throw;
}
}

/// <summary>
Expand Down Expand Up @@ -386,41 +393,84 @@ public override async Task AuthenticateAsync (Encoding encoding, ICredentials cr
{
CheckCanAuthenticate (encoding, credentials);

int capabilitiesVersion = engine.CapabilitiesVersion;
var uri = new Uri ("imap://" + engine.Uri.Host);
NetworkCredential cred;
ImapCommand ic = null;
SaslMechanism sasl;
string id;
using var operation = engine.StartNetworkOperation (NetworkOperation.Authenticate);

try {
int capabilitiesVersion = engine.CapabilitiesVersion;
var uri = new Uri ("imap://" + engine.Uri.Host);
NetworkCredential cred;
ImapCommand ic = null;
SaslMechanism sasl;
string id;

foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) {
cred = credentials.GetCredential (uri, authmech);
foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) {
cred = credentials.GetCredential (uri, authmech);

if ((sasl = SaslMechanism.Create (authmech, encoding, cred)) == null)
continue;
if ((sasl = SaslMechanism.Create (authmech, encoding, cred)) == null)
continue;

ConfigureSaslMechanism (sasl, uri);
ConfigureSaslMechanism (sasl, uri);

cancellationToken.ThrowIfCancellationRequested ();
cancellationToken.ThrowIfCancellationRequested ();

var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName);
var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName);

if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) {
string ir = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false);
if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) {
string ir = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false);

command += " " + ir + "\r\n";
} else {
command += "\r\n";
command += " " + ir + "\r\n";
} else {
command += "\r\n";
}

ic = engine.QueueCommand (cancellationToken, null, command);
ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => {
string challenge = await sasl.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false);
var buf = Encoding.ASCII.GetBytes (challenge + "\r\n");
await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false);
await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false);
};

detector.IsAuthenticating = true;

try {
await engine.RunAsync (ic).ConfigureAwait (false);
} finally {
detector.IsAuthenticating = false;
}

if (ic.Response != ImapCommandResponse.Ok) {
EmitAndThrowOnAlert (ic);
if (ic.Bye)
throw new ImapProtocolException (ic.ResponseText);
continue;
}

engine.State = ImapEngineState.Authenticated;

cred = credentials.GetCredential (uri, sasl.MechanismName);
id = GetSessionIdentifier (cred.UserName);
if (id != identifier) {
engine.FolderCache.Clear ();
identifier = id;
}

// Query the CAPABILITIES again if the server did not include an
// untagged CAPABILITIES response to the AUTHENTICATE command.
if (engine.CapabilitiesVersion == capabilitiesVersion)
await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false);

await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false);
return;
}

ic = engine.QueueCommand (cancellationToken, null, command);
ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => {
string challenge = await sasl.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false);
var buf = Encoding.ASCII.GetBytes (challenge + "\r\n");
CheckCanLogin (ic);

await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false);
await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false);
};
// fall back to the classic LOGIN command...
cred = credentials.GetCredential (uri, "DEFAULT");

ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password);

detector.IsAuthenticating = true;

Expand All @@ -430,63 +480,27 @@ public override async Task AuthenticateAsync (Encoding encoding, ICredentials cr
detector.IsAuthenticating = false;
}

if (ic.Response != ImapCommandResponse.Ok) {
EmitAndThrowOnAlert (ic);
if (ic.Bye)
throw new ImapProtocolException (ic.ResponseText);
continue;
}
if (ic.Response != ImapCommandResponse.Ok)
throw CreateAuthenticationException (ic);

engine.State = ImapEngineState.Authenticated;

cred = credentials.GetCredential (uri, sasl.MechanismName);
id = GetSessionIdentifier (cred.UserName);
if (id != identifier) {
engine.FolderCache.Clear ();
identifier = id;
}

// Query the CAPABILITIES again if the server did not include an
// untagged CAPABILITIES response to the AUTHENTICATE command.
// untagged CAPABILITIES response to the LOGIN command.
if (engine.CapabilitiesVersion == capabilitiesVersion)
await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false);

await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false);
return;
}

CheckCanLogin (ic);

// fall back to the classic LOGIN command...
cred = credentials.GetCredential (uri, "DEFAULT");

ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password);

detector.IsAuthenticating = true;

try {
await engine.RunAsync (ic).ConfigureAwait (false);
} finally {
detector.IsAuthenticating = false;
}

if (ic.Response != ImapCommandResponse.Ok)
throw CreateAuthenticationException (ic);

engine.State = ImapEngineState.Authenticated;

id = GetSessionIdentifier (cred.UserName);
if (id != identifier) {
engine.FolderCache.Clear ();
identifier = id;
} catch (Exception ex) {
operation.SetError (ex);
throw;
}

// Query the CAPABILITIES again if the server did not include an
// untagged CAPABILITIES response to the LOGIN command.
if (engine.CapabilitiesVersion == capabilitiesVersion)
await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false);

await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false);
}

async Task SslHandshakeAsync (SslStream ssl, string host, CancellationToken cancellationToken)
Expand Down Expand Up @@ -551,9 +565,9 @@ async Task PostConnectAsync (Stream stream, string host, int port, SecureSocketO
throw ImapCommandException.Create ("STARTTLS", ic);
}
}
} catch {
} catch (Exception ex) {
secure = false;
engine.Disconnect ();
engine.Disconnect (ex);
throw;
} finally {
connecting = false;
Expand Down Expand Up @@ -639,30 +653,37 @@ public override async Task ConnectAsync (string host, int port = 0, SecureSocket

ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls);

var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false);
stream.WriteTimeout = timeout;
stream.ReadTimeout = timeout;
using var operation = engine.StartNetworkOperation (NetworkOperation.Connect);

engine.Uri = uri;
try {
var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false);
stream.WriteTimeout = timeout;
stream.ReadTimeout = timeout;

if (options == SecureSocketOptions.SslOnConnect) {
var ssl = new SslStream (stream, false, ValidateRemoteCertificate);
engine.Uri = uri;

try {
await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
ssl.Dispose ();
if (options == SecureSocketOptions.SslOnConnect) {
var ssl = new SslStream (stream, false, ValidateRemoteCertificate);

try {
await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
ssl.Dispose ();

throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143);
}

throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143);
secure = true;
stream = ssl;
} else {
secure = false;
}

secure = true;
stream = ssl;
} else {
secure = false;
await PostConnectAsync (stream, host, port, options, starttls, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
operation.SetError (ex);
throw;
}

await PostConnectAsync (stream, host, port, options, starttls, cancellationToken).ConfigureAwait (false);
}

/// <summary>
Expand Down Expand Up @@ -804,36 +825,43 @@ public override async Task ConnectAsync (Stream stream, string host, int port =
{
CheckCanConnect (stream, host, port);

Stream network;

ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls);

engine.Uri = uri;
using var operation = engine.StartNetworkOperation (NetworkOperation.Connect);

if (options == SecureSocketOptions.SslOnConnect) {
var ssl = new SslStream (stream, false, ValidateRemoteCertificate);
try {
Stream network;

try {
await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
ssl.Dispose ();
engine.Uri = uri;

if (options == SecureSocketOptions.SslOnConnect) {
var ssl = new SslStream (stream, false, ValidateRemoteCertificate);

throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143);
try {
await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
ssl.Dispose ();

throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143);
}

network = ssl;
secure = true;
} else {
network = stream;
secure = false;
}

network = ssl;
secure = true;
} else {
network = stream;
secure = false;
}
if (network.CanTimeout) {
network.WriteTimeout = timeout;
network.ReadTimeout = timeout;
}

if (network.CanTimeout) {
network.WriteTimeout = timeout;
network.ReadTimeout = timeout;
await PostConnectAsync (network, host, port, options, starttls, cancellationToken).ConfigureAwait (false);
} catch (Exception ex) {
operation.SetError (ex);
throw;
}

await PostConnectAsync (network, host, port, options, starttls, cancellationToken).ConfigureAwait (false);
}

/// <summary>
Expand Down Expand Up @@ -871,7 +899,7 @@ public override async Task DisconnectAsync (bool quit, CancellationToken cancell

disconnecting = true;

engine.Disconnect ();
engine.Disconnect (null);
}

/// <summary>
Expand Down
Loading

0 comments on commit d00ad94

Please sign in to comment.