Here’s the latest version that supports Visual Studio 2015.
Extension is convention based. It matches ClassName file with ClassNameTest or ClassNameTests and vice-versa, so you can easily navigate to the test file and back.
This article provides a comprehensive guide on how to perform logging within the Mail.dll .NET email client. It outlines step-by-step instructions for implementing robust logging mechanisms, enhancing troubleshooting capabilities, and tracking email communication effectively.
By following the methods detailed in the article, VisualBasic and C# developers can seamlessly integrate logging features into their applications, gaining valuable insights into the Imap, Pop3 and Smtp clients behavior and facilitating streamlined debugging processes.
To enable logging for all Mail.dll .NET clients (Imap, Pop3, Smtp) you only need to add the following line before you connect:
If you are using log4net, Mail.dll is going to use log4net instead of standard .NET System.Net.TraceSource class. Please refer to log4net manual on how to capture log entries.
Mail.dll uses logger called “Mail.dll”
_logger = LogManager.GetLogger("Mail.dll")
and level Info_logger.Info(message) to log information.
Please remember that even when using log4net, you need to enable logging by setting “Limilabs.Mail.Log.Enabled = True” or by setting Mail.dll trace switch in the config file (App.config, Web.config) to Verbose as shown above.
Log.WriteLine
Limilabs.Mail.Log class exposes WriteLine event. You can use that event to subscribe your own logging library in both .NET and .NET framework.
You’ll need to define a TextWriterTraceListener that writes to a file and connect it with Mail.dll trace source. The easiest solution is to modify your application’s config file App.config or Web.config accordingly:
Before you can use OAuth 2.0, you must register your application using the Google Developers Console. After you’ve registered, go to the API Access tab and copy the “Client ID” and “Client secret” values and specify “Redirect URI“, which you’ll need later.
At least product name must be specified:
Now create credentials:
Specify redirect URI:
After you’ve registered, copy the “Client ID” and “Client secret” values, which you’ll need later:
Now we can define clientID, clientSecret, redirect url and scope variables, as well as Google OAuth 2.0 server addresses. Scope basically specifies what services we want to have access to. In our case it is user’s email address and IMAP/SMTP access:
const string clientID = "12345.apps.googleusercontent.com";
const string clientSecret = "XXXYYY111";
const string redirectUri = "http://www.yourdomain.com/oauth2callback";
AuthorizationServerDescription server = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
TokenEndpoint = new Uri("https://oauth2.googleapis.com/token"),
ProtocolVersion = ProtocolVersion.V20,
};
List<string> scope = new List<string>
{
GoogleScope.ImapAndSmtp.Name,
GoogleScope.UserInfoEmailScope.Name
};
Obtain an OAuth 2.0 access token
As we are using ASP.NET we’ll use WebServerClient class:
WebServerClient consumer = new WebServerClient(server, clientID, clientSecret);
// Here redirect to authorization site occurs
consumer.RequestUserAuthorization(scope, new Uri(redirectUri));
If you use ASP.NET MVC the last line is different:
// Here redirect to authorization site occurs
OutgoingWebResponse response = consumer.PrepareRequestUserAuthorization(
scope, new Uri(redirectUri));
return response.AsActionResult();
At this point user is redirected to Google to authorize the access:
After this step user is redirected back to your website (http://www.yourdomain.com/oauth2callback). Following is this callback code. Its purpose is to get a refresh-token and an access-token:
An access token is usually valid for a maximum of one hour, and allows you to access the user’s data. You also received a refresh token. A refresh token can be used to request a new access token once the previous expired.
Access IMAP/SMTP server
Finally we’ll ask Google for user’s email and use LoginOAUTH2 method to access Gmail’s IMAP server:
GoogleApi api = new GoogleApi(accessToken);
string user = api.GetEmail();
using (Imap imap = new Imap())
{
imap.ConnectSSL("imap.gmail.com");
imap.LoginOAUTH2(user, accessToken);
imap.SelectInbox();
List<long> uids = imap.Search(Flag.Unseen);
foreach (long uid in uids)
{
var eml = imap.GetMessageByUID(uid);
IMail email = new MailBuilder().CreateFromEml(eml);
Console.WriteLine(email.Subject);
}
imap.Close();
}
Refreshing access token
An access token is usually short lived and valid for a maximum of one hour. The main reason behind this is security and prevention of replay attacks. This means that for long-lived applications you need to refresh the access token.
In most cases web applications don’t need to refresh access token (they request new one every time), thus when using WebServerClient refresh token is not sent. To force sending refresh token you need to set access_type url parameter to offline:
AuthorizationServerDescription authServer = new AuthorizationServerDescription
{
AuthorizationEndpoint =
new Uri("https://accounts.google.com/o/oauth2/auth?access_type=offline"),
...
};
Your refresh token will be sent only once – don’t loose it!
We recommend storing entire IAuthorizationState object received from WebServerClient.ProcessUserAuthorization method call. This object contains both: refresh token and access token, along with its expiration time.
In the example above the access token will not be refreshed if its remaining lifetime exceeds 20 minutes.
Retrieving lost refresh token
When your application receives a refresh token, it is important to store that refresh token for future use. If your application loses the refresh token, it will have to re-prompt the user for consent before obtaining another refresh token.
You’ll need to add approval_prompt=force to your parameters:
AuthorizationServerDescription authServer = new AuthorizationServerDescription
{
AuthorizationEndpoint =
new Uri("https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force"),
...
};
In this article I’ll show how to access Gmail account of any domain user, using OAuth 2.0, .NET IMAP component and service accounts. The basic idea is that domain administrator can use this method to access user email without knowing user’s password.
This scenario is very similar to 2-legged OAuth, which uses OAuth 1.0a. Although it still works, it has been deprecated by Google and OAuth 2.0 service accounts were introduced.
The following describes how to use XOAUTH2 and OAuth 2.0 to achieve the equivalent of 2-legged OAuth.
Download and save this private key, you’ll need that later:
Go to “Service accounts” and click “View Client ID”:
Make a note of the Client ID and Email address (Service account).
Google Apps Dashboard
Next step is to authorize access for newly created service account.
Visit your domain administration panel: https://www.google.com/a/cpanel/yourdomain.com/ManageOauthClients
Then click “Advanced tools”, “Authentication” and “Manage third party OAuth Client access”.
On this screen you can authorize service account to access email scope:
Use previously remembered Client ID and “https://mail.google.com/”, which is IMAP/SMTP API scope:
Client Name: 1234567890 One or More API Scopes: https://mail.google.com/
DotNetOpenAuth
DotNetOpenAuth is free, open source library that implements OAuth 2.0.
However you can not use the latest version. This is because Google has not updated their code to work with the most recent release. You’ll need to use 4.0 version.
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Limilabs.Client;
using Limilabs.Client.Authentication.SSPI.Ntlm;
/// <summary>
/// Assertion flow header used to generate the assertion flow message header.
/// </summary>
public class AssertionFlowHeader
{
/// <summary>
/// Gets or sets the encryption algorithm used by the assertion flow message.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("alg")]
public String Algorithm { get; set; }
/// <summary>
/// Gets or sets the type of the claim.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("typ")]
public String Type { get; set; }
};
/// <summary>
/// Google assertion flow header holding Google supported values.
/// </summary>
public class GoogleAssertionFlowHeader : AssertionFlowHeader
{
/// <summary>
/// The google signing algorithm, currently RSA-SHA256
/// </summary>
public const string GoogleSigningAlgorithm = "RS256";
/// <summary>
/// The type of the google assertion, currently JSON Web Token
/// </summary>
public const string GoogleAssertionType = "JWT";
public GoogleAssertionFlowHeader()
{
Algorithm = GoogleSigningAlgorithm;
Type = GoogleAssertionType;
}
};
/// <summary>
/// Assertion flow claim used to generate the assertion flow message claim.
/// </summary>
public class AssertionFlowClaim
{
public AssertionFlowClaim()
{
DateTime begin = new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc);
IssuedAt = (long)(DateTime.UtcNow - begin).TotalSeconds;
ExpiresAt = IssuedAt + 3600;
}
public AssertionFlowClaim(AuthorizationServerDescription authorizationServer)
: this()
{
Audience = authorizationServer.TokenEndpoint.ToString();
}
/// <summary>
/// Gets or sets the assertion flow issuer (e.g client ID).
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("iss")]
public String Issuer { get; set; }
/// <summary>
/// Gets or sets the service account user (for domain-wide delegation).
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("prn")]
public String Principal { get; set; }
/// <summary>
/// Gets or sets the scope.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("scope")]
public String Scope { get; set; }
/// <summary>
/// Gets or sets the token endpoint.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("aud")]
public String Audience { get; set; }
/// <summary>
/// Gets or sets the expected expiration of the token to retrieve.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("exp")]
public long ExpiresAt { get; set; }
/// <summary>
/// Gets or sets the UTC timestamp at which this claim has been built.
/// </summary>
[Newtonsoft.Json.JsonPropertyAttribute("iat")]
public long IssuedAt { get; set; }
};
/// <summary>
/// Assertion flow message to be sent to the token endpoint.
/// </summary>
public class AssertionFlowMessage : MessageBase
{
/// <summary>
/// Google supported assertion type
/// </summary>
public const string GoogleAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer";
/// <summary>
/// Initializes a new instance of the <see cref="AssertionFlowMessage"/> class.
/// </summary>
/// <param name='authorizationServer'> Authorization server description. </param>
public AssertionFlowMessage(AuthorizationServerDescription authorizationServer) :
base(new Version(2, 0), MessageTransport.Direct, authorizationServer.TokenEndpoint)
{
GrantType = "assertion";
AssertionType = GoogleAssertionType;
this.HttpMethods = HttpDeliveryMethods.PostRequest;
}
/// <summary>
/// Gets or sets the type of the grant (defaults to "assertion").
/// </summary>
[MessagePart("grant_type", IsRequired = true)]
public String GrantType { get; set; }
/// <summary>
/// Gets or sets the type of the assertion
/// (defaults to "http://oauth.net/grant_type/jwt/1.0/bearer").
/// </summary>
[MessagePart("assertion_type", IsRequired = true)]
public String AssertionType { get; set; }
/// <summary>
/// Gets or sets the assertion message.
/// </summary>
[MessagePart("assertion", IsRequired = true)]
public String Assertion { get; set; }
};
public class AssertionFlowClient : ClientBase
{
/// <summary>
/// Gets or sets the service account identifier.
/// </summary>
/// <value>
/// The service account identifier.
/// </value>
public String ServiceAccountId { get; set; }
/// <summary>
/// Gets or sets the service account user (used for domain-wide delegation).
/// </summary>
public String ServiceAccountUser { get; set; }
/// <summary>
/// Gets or sets the scope to get access for.
/// </summary>
public String Scope { get; set; }
/// <summary>
/// Gets the certificate used to sign the assertion.
/// </summary>
public X509Certificate2 Certificate { get; private set; }
/// <summary>
/// Gets or sets the JWT claim's header (defaults to Google's supported values).
/// </summary>
public AssertionFlowHeader Header { get; set; }
public RSACryptoServiceProvider Key { get; private set; }
/// <summary>
/// Initializes a new instance of the
/// <see cref="AssertionFlowClient"/> class.
/// </summary>
/// <param name='authorizationServer'>
/// Authorization server description.
/// </param>
/// <param name='certificate'>
/// Certificate to use to sign the assertion flow messages.
/// </param>
public AssertionFlowClient(
AuthorizationServerDescription authorizationServer,
X509Certificate2 certificate)
: base(authorizationServer, null, null)
{
if (certificate == null)
throw new ArgumentNullException("certificate");
if (certificate.PrivateKey == null)
throw new ArgumentNullException("certificate.PrivateKey");
Header = new GoogleAssertionFlowHeader();
Certificate = certificate;
// Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
byte[] privateKeyBlob = rsa.ExportCspBlob(true);
Key = new RSACryptoServiceProvider();
Key.ImportCspBlob(privateKeyBlob);
}
/// <summary>
/// Helper method to retrieve the Authorization State.
/// </summary>
/// <returns>
/// The authorization state.
/// </returns>
/// <param name='provider'>
/// The provider to use to retrieve the authorization state.
/// </param>
public static IAuthorizationState GetState(AssertionFlowClient provider)
{
if (provider.Scope == null)
throw new ArgumentNullException("Scope");
IAuthorizationState state = new AuthorizationState(provider.Scope.Split(' '));
if (provider.RefreshToken(state, null))
{
return state;
}
return null;
}
/// <summary>
/// Request a new access token using the OAuth 2.0 assertion flow.
/// </summary>
/// <returns>
/// Whether or not a new access token has been successfully retrieved.
/// </returns>
/// <param name='authorization'>
/// Object containing the current authorization state.
/// </param>
/// <param name='skipIfUsefulLifeExceeds'>
/// If set to <c>true</c> skip if useful life exceeds.
/// </param>
public new bool RefreshToken(
IAuthorizationState authorization,
TimeSpan? skipIfUsefulLifeExceeds)
{
return RefreshToken(authorization, skipIfUsefulLifeExceeds, this.Channel.Request);
}
public bool RefreshToken(
IAuthorizationState authorization,
TimeSpan? skipIfUsefulLifeExceeds,
Func<IDirectedProtocolMessage, IProtocolMessage> requestProvider)
{
if (authorization == null)
throw new ArgumentNullException("authorization");
if (this.Certificate == null)
throw new ArgumentNullException("Certificate");
// Check if the token is still valid.
if (skipIfUsefulLifeExceeds.HasValue && authorization.AccessTokenExpirationUtc.HasValue)
{
TimeSpan timeSpan = authorization.AccessTokenExpirationUtc.Value - DateTime.UtcNow;
if (timeSpan > skipIfUsefulLifeExceeds.Value)
{
return false;
}
}
AssertionFlowMessage requestMessage = GenerateMessage();
var response = requestProvider(requestMessage);
// Response is not strongly-typed to an AccessTokenSuccessResponse because DotNetOpenAuth can't infer the
// type from the request message type. The only way to get access to the result data is through the
// resulting Dictionary.
if (response.ExtraData.ContainsKey("access_token") && response.ExtraData.ContainsKey("expires_in"))
{
authorization.AccessToken = response.ExtraData["access_token"];
long expiresIn = long.Parse(response.ExtraData["expires_in"]);
DateTime utcNow = DateTime.UtcNow;
authorization.AccessTokenExpirationUtc = utcNow.AddSeconds(expiresIn);
authorization.AccessTokenIssueDateUtc = utcNow;
authorization.SaveChanges();
return true;
}
return false;
}
/// <summary>
/// Generates the assertion flow message to be sent to the token endpoint.
/// </summary>
/// <returns>
/// The assertion flow message.
/// </returns>
private AssertionFlowMessage GenerateMessage()
{
string header = JsonConvert.SerializeObject(Header);
string claim = JsonConvert.SerializeObject(
new AssertionFlowClaim(AuthorizationServer)
{
Issuer = this.ServiceAccountId,
Principal = this.ServiceAccountUser,
Scope = this.Scope
});
StringBuilder assertion = new StringBuilder();
assertion.Append(UnpaddedUrlSafeBase64Encode(header));
assertion.Append(".");
assertion.Append(UnpaddedUrlSafeBase64Encode(claim));
// TODO: Check if this is working on FIPS enabled systems.
byte[] data = Encoding.ASCII.GetBytes(assertion.ToString());
String signature = UnpaddedUrlSafeBase64Encode(Key.SignData(data, "SHA256"));
assertion.Append(".");
assertion.Append(signature);
return new AssertionFlowMessage(this.AuthorizationServer)
{
Assertion = assertion.ToString()
};
}
/// <summary>
/// Encode the provided UTF8 string into an URL safe base64 string.
/// </summary>
/// <returns>
/// The URL safe base64 string.
/// </returns>
/// <param name='value'>
/// String to encode.
/// </param>
private String UnpaddedUrlSafeBase64Encode(String value)
{
return UnpaddedUrlSafeBase64Encode(Encoding.UTF8.GetBytes(value));
}
/// <summary>
/// Encode the byte array into an URL safe base64 string.
/// </summary>
/// <returns>
/// The URL safe base64 string.
/// </returns>
/// <param name='bytes'>
/// Bytes to encode.
/// </param>
private String UnpaddedUrlSafeBase64Encode(Byte[] bytes)
{
return Convert.ToBase64String(bytes)
.Replace("=", String.Empty)
.Replace('+', '-')
.Replace('/', '_');
}
};