This guide explains how to update a project that uses MailKit 4.x so that it builds and behaves correctly with MailKit 5.0. It is written so that it can be followed by a developer or by an AI coding assistant.
For the complete list of changes (including new features), see ReleaseNotes.md. MailKit 5.0 depends on MimeKit 5.0, which has its own release notes and porting notes. Port MimeKit usage at the same time.
When asked to port a project to MailKit 5.0:
- Update the package references first (see Packages), then build the project to get the list of compile errors.
- Use the Detect hints in each section below (compiler errors, type and member names) to find the affected code. Search the whole solution, including test projects, since mocks and fakes of MailKit interfaces are frequently affected.
- Apply the Fix for each match. Prefer the smallest change that preserves the original behavior. Do not refactor unrelated code.
- Review the Behavior changes section. These do not cause compile errors, so search for the listed patterns explicitly and flag anything you can't safely change for the user.
- Rebuild and run the project's tests. Repeat until the build is clean.
- Summarize the changes for the user. List anything you couldn't port automatically, such as dashboards that match on telemetry values or logic that depended on removed hooks.
Unless stated otherwise, all types mentioned here are in the MailKit namespace. Many fixes rely on
extension methods in that namespace, so make sure that files calling Send (), CopyTo (),
MoveTo (), Expunge () or Store () have a using MailKit; directive.
| Area | Typical symptom | Section |
|---|---|---|
MailKitLite package |
Package not found | 1 |
| S/MIME, PGP, DKIM, ARC | Missing types or runtime errors | 1 |
SmtpClient.Send () return value |
Cannot implicitly convert type 'MailKit.SendResult' to 'string' |
2 |
Subclassing SmtpClient (DSN, recipient hooks) |
no suitable method found to override |
2 |
SmtpClient.DeliveryStatusNotificationType |
does not contain a definition for 'DeliveryStatusNotificationType' |
2 |
MessageSentEventArgs.Response, SmtpResponse.Response |
does not contain a definition for 'Response' |
2 |
ImapCapabilities.Xxx, HasFlag, &, |, ~ |
Operator or conversion errors on capabilities | 3 |
EnableQuickResync (), EnableUTF8 () |
Missing method | 4 |
| Sizes, counts, offsets | int/uint vs long conversion errors |
5 |
IMessageSummary.Folder |
Missing property | 6 |
Custom IMailFolder/MailFolder/IMailTransport implementations, or mocks |
Interface members not implemented, or mock setups fail | 7 |
SaslMechanism.Create () null checks |
Exception instead of null |
8 |
Compress () on .NET Framework/.NET Standard |
Missing method | 8 |
AccessControlList List<T> methods (Find, Sort, ForEach, ...) |
Missing method or conversion to List<AccessControl> fails |
8 |
| Removed obsolete APIs | Missing constructors or properties | 9 |
Detect: PackageReference (or packages.config) entries for MailKit, MailKitLite,
MimeKit or MimeKitLite. Also look for code that uses S/MIME, OpenPGP, DKIM or ARC (namespace
MimeKit.Cryptography).
Fix:
-
Replace
MailKitLitewithMailKitand update it to 5.0. MailKit no longer depends on BouncyCastle, so the two packages were identical. -
MailKit 5.0 depends on
MimeKit.Corerather than the full MimeKit package. If the project uses S/MIME, PGP/MIME, DKIM or ARC, add a reference toMimeKit.Cryptography(or theMimeKitmeta-package). Then callCryptographyModule.Initialize ()once during application startup:using MimeKit.Cryptography; CryptographyModule.Initialize ();
MailKit 5.0 uses request objects to send messages: ISendRequest/SendRequest in general, and
ISmtpSendRequest/SmtpSendRequest for SMTP. The methods on IMailTransport take a request and
return a SendResult (SmtpClient returns the subclass SmtpSendResult).
client.Send (message), client.Send (message, sender, recipients) and the overloads that take a
FormatOptions (plus their SendAsync variants) still exist. They are now extension methods in
MailKit.IMailTransportExtensions.
Detect: Calls like client.Send (message) fail with "no overload for method 'Send' takes N
arguments".
Fix: Add using MailKit; to the file.
Detect: string x = client.Send (...), string x = await client.SendAsync (...), or a
var result that's used as a string.
Fix:
// 4.x
string response = client.Send (message);
// 5.0
string response = client.Send (message).ResponseText;
// or, to keep the full result:
var result = client.Send (message);
Console.WriteLine (result.ResponseText);SendResult also has AcceptedRecipients and RejectedRecipients. To get the SMTP status code,
cast the result to SmtpSendResult:
var result = (SmtpSendResult) smtpClient.Send (message);
Console.WriteLine ($"{(int) result.StatusCode} {result.ResponseText}");These protected virtual methods were removed from SmtpClient:
4.x SmtpClient override |
5.0 SmtpSendRequest equivalent |
|---|---|
string? GetEnvelopeId (MimeMessage message) |
EnvelopeId property |
DeliveryStatusNotification? GetDeliveryStatusNotifications (MimeMessage message, MailboxAddress mailbox) |
DeliveryStatusNotifications property, or override GetDeliveryStatusNotifications (MailboxAddress recipient) |
void OnSenderAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) |
override OnSenderAccepted (MailboxAddress sender, SmtpResponse response) |
void OnSenderNotAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) |
override OnSenderNotAccepted (MailboxAddress sender, SmtpResponse response) |
void OnRecipientAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) |
override OnRecipientAccepted (MailboxAddress recipient, SmtpResponse response) |
void OnRecipientNotAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) |
override OnRecipientNotAccepted (MailboxAddress recipient, SmtpResponse response) |
void OnNoRecipientsAccepted (MimeMessage message) |
override OnNoRecipientsAccepted () |
DeliveryStatusNotificationType property on SmtpClient/ISmtpClient |
DeliveryStatusNotificationType property on SmtpSendRequest |
Inside a SmtpSendRequest subclass, the message is available from the Message property. The
default behavior is unchanged: OnSenderNotAccepted () and OnRecipientNotAccepted () throw
SmtpCommandException, and OnNoRecipientsAccepted () does nothing.
Detect: Classes that derive from SmtpClient and override any of the methods above. Uses of
client.DeliveryStatusNotificationType.
Fix: Move the logic into an SmtpSendRequest (or a subclass of it) and pass the request to
Send (). If the SmtpClient subclass only existed for these hooks, delete it and use SmtpClient
directly.
// 4.x
class DsnSmtpClient : SmtpClient
{
protected override string GetEnvelopeId (MimeMessage message) => message.MessageId;
protected override DeliveryStatusNotification? GetDeliveryStatusNotifications (MimeMessage message, MailboxAddress mailbox)
=> DeliveryStatusNotification.Failure;
}
using var client = new DsnSmtpClient ();
client.DeliveryStatusNotificationType = DeliveryStatusNotificationType.HeadersOnly;
client.Send (message);
// 5.0
using var client = new SmtpClient ();
var request = new SmtpSendRequest (message) {
EnvelopeId = message.MessageId,
DeliveryStatusNotificationType = DeliveryStatusNotificationType.HeadersOnly,
DeliveryStatusNotifications = DeliveryStatusNotification.Failure
};
client.Send (request);The same pattern applies to the hooks that report whether each recipient was accepted:
// 4.x
class TolerantSmtpClient : SmtpClient
{
protected override void OnRecipientNotAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response)
{
Log ($"{mailbox} rejected: {response.Response}");
// not calling base: deliver to the remaining recipients
}
}
// 5.0
class TolerantSendRequest : SmtpSendRequest
{
public TolerantSendRequest (MimeMessage message) : base (message) { }
public override void OnRecipientNotAccepted (MailboxAddress recipient, SmtpResponse response)
{
Log ($"{recipient} rejected: {response.ResponseText}");
}
}
var result = client.Send (new TolerantSendRequest (message));
// result.RejectedRecipients now lists the rejected mailboxes.To send with an explicit envelope sender and recipients, use
new SmtpSendRequest (message, sender, recipients). To report progress, set the request's
TransferProgress property (or keep using the extension overloads that take an
ITransferProgress).
SmtpClient's Prepare (), GetSize () and PreferSendAsBinaryData members are still
overridable.
Detect: .Response on an SmtpResponse or MessageSentEventArgs.
Fix: Rename to .ResponseText.
SmtpResponse.Response→SmtpResponse.ResponseText. The constructor parameter is nowresponseText, which matters for named arguments.MessageSentEventArgs.Response→MessageSentEventArgs.ResponseText.
The constructor changed from (MimeMessage message, string response) to
(ISendRequest request, SendResult result). The event args now expose Request, Message,
Result and ResponseText.
Detect: new MessageSentEventArgs ( (usually in custom transports or tests).
Fix: new MessageSentEventArgs (new SendRequest (message), new SendResult (response, accepted, rejected)).
The [Flags] enums ImapCapabilities, SmtpCapabilities and Pop3Capabilities were renamed to
ImapCapability, SmtpCapability and Pop3Capability. They are no longer flags and have no
None value. The old names now refer to read-only collection classes (capability sets).
Detect: ImapCapabilities., SmtpCapabilities. or Pop3Capabilities. followed by a member
name. Also Capabilities.HasFlag (, Capabilities & , Capabilities |= , Capabilities &= ~ and
comparisons with .None.
Fix:
| 4.x | 5.0 |
|---|---|
client.Capabilities.HasFlag (ImapCapabilities.Idle) |
client.Capabilities.Contains (ImapCapability.Idle) |
(client.Capabilities & ImapCapabilities.Idle) != 0 |
client.Capabilities.Contains (ImapCapability.Idle) |
(client.Capabilities & ImapCapabilities.Idle) == 0 |
!client.Capabilities.Contains (ImapCapability.Idle) |
client.Capabilities &= ~ImapCapabilities.Compress; |
client.Capabilities.Disable (ImapCapability.Compress); |
client.Capabilities == ImapCapabilities.None |
client.Capabilities.Count == 0 |
client.Capabilities.ToString () (for parsing) |
client.Capabilities.Names (the raw capability names) |
The same mappings apply to SmtpCapabilities → SmtpCapability and Pop3Capabilities →
Pop3Capability.
You can no longer assign to the Capabilities property. Use Disable () instead. If code combined
several flags with |, replace it with several Contains () calls joined with && (or ||). Mocks
that set up Capabilities should construct a set with new ImapCapabilities (new[] { ImapCapability.Idle, ... }).
Contains (string) and GetValues (string) expose capabilities that MailKit doesn't know about,
e.g. client.Capabilities.GetValues ("AUTH"). Don't use the capability set's ToString () output
for anything other than debugging.
Detect: EnableQuickResync (, EnableQuickResyncAsync (, EnableUTF8 ( or
EnableUTF8Async (.
Fix:
// 4.x
client.EnableQuickResync ();
client.EnableUTF8 ();
// 5.0 (one ENABLE command)
client.Enable (ImapFeatures.QuickResync | ImapFeatures.UTF8Accept);ImapFeatures is in MailKit.Net.Imap. EnableQuickResync () has also been removed from
IMailStore/MailStore, and EnableUTF8 () from IImapClient. Code that only has an IMailStore
must now use IImapClient (or ImapClient).
Message sizes, line counts, quota values, byte offsets and byte counts are now long:
| API | 4.x type | 5.0 type |
|---|---|---|
IMessageSummary.Size |
uint? |
long? |
IMailFolder.Size |
ulong? |
long? |
BodyPartText.Lines, BodyPartMessage.Lines |
uint |
long |
IMailFolder.GetStream (..., int offset, int count, ...) |
int |
long |
ImapFolder.CreateStream (), CommitStream () offset/length |
int |
long |
SearchQuery.LargerThan (), SmallerThan () |
int |
long |
IMailSpool.GetMessageSize () |
int |
long |
IMailSpool.GetMessageSizes () |
IList<int> |
IList<long> |
ISmtpClient.MaxSize, SmtpClient.MaxSize |
uint |
long |
FolderQuota.MessageLimit, StorageLimit, CurrentMessageCount, CurrentStorageSize |
uint? |
long? |
IMailFolder.SetQuota (), SetQuotaAsync () messageLimit/storageLimit |
uint? |
long? |
Detect: Conversion errors involving long, int, uint or ulong around these members.
Fix: Change the receiving variables, fields or properties to long (or long?). If storage
must stay 32-bit, use an explicit checked conversion and handle values larger than int.MaxValue.
None of these values is ever negative.
The IMessageSummary.Folder property and the MessageSummary (IMailFolder, int) constructor were
removed.
Detect: .Folder on an IMessageSummary/MessageSummary, or new MessageSummary (folder, index).
Fix: Keep track of the folder alongside the summaries. Usually this is just the folder on which
Fetch () was called. If summaries from several folders are mixed together, store
(IMailFolder folder, IMessageSummary summary) pairs. Use new MessageSummary (index) for tests
and mocks.
// 4.x
foreach (var summary in summaries)
var message = summary.Folder.GetMessage (summary.UniqueId);
// 5.0
foreach (var summary in summaries)
var message = folder.GetMessage (summary.UniqueId);Several interfaces now have a smaller set of request-based methods, and the convenience overloads
became extension methods. This doesn't affect callers, as long as they have using MailKit;. It
does affect classes that implement these interfaces or derive from the abstract base classes. It
also affects mocking frameworks, which can't mock extension methods.
| Interface / base class | 4.x members (now extension methods) | 5.0 members to implement |
|---|---|---|
IMailTransport/MailTransport |
Send (FormatOptions, MimeMessage, ...), Send (FormatOptions, MimeMessage, MailboxAddress, IEnumerable<MailboxAddress>, ...) and SendAsync variants |
SendResult Send (FormatOptions, ISendRequest, CancellationToken) and Task<SendResult> SendAsync (FormatOptions, ISendRequest, CancellationToken) |
IMailFolder/MailFolder |
CopyTo (..., IMailFolder destination, ...), MoveTo (..., IMailFolder destination, ...) |
CopyTo (IList<UniqueId>/IList<int>, ICopyRequest, ...), MoveTo (IList<UniqueId>/IList<int>, IMoveRequest, ...) |
IMailFolder/MailFolder |
Expunge (IList<UniqueId>, ...) |
Expunge (IList<UniqueId>, IExpungeRequest, ...) |
IMailFolder/MailFolder |
Store (..., IList<Annotation>, ...) and Store (..., ulong modseq, IList<Annotation>, ...) |
Store (..., IStoreAnnotationsRequest, ...) |
IMailFolder |
none | DeletedCount and DeletedStorageSize properties and DeletedCountChanged and DeletedStorageSizeChanged events (already provided by MailFolder) |
IStoreRequest, IAppendRequest (and derived interfaces) |
none | OnStarted () and OnCompleted () methods (no-ops in the built-in classes) |
IStoreRequest (and derived interfaces) |
none | OnProgress () method (no-op in the built-in classes) |
IFetchRequest |
none | OnStarted (), OnCompleted () and OnProgress () methods (no-ops in FetchRequest) |
IMailService (and IMailStore, IMailTransport, IMailSpool) |
none | ValueTask DisposeAsync () (already provided by MailService) |
IImapClient |
none | ImapIdleSession CreateIdleSession (ImapIdleOptions? options = null) (already provided by ImapClient) |
The request-based Store () overloads for annotations return the messages that weren't modified
(IList<UniqueId>/IList<int>), or bool for a single message.
Detect: "does not implement interface member", "no suitable method found to override", or
mock setups (Moq Setup, NSubstitute Returns, FakeItEasy A.CallTo) for the 4.x overloads
listed above.
Fix:
-
Implementations: Remove the old overrides and implement the request-based methods. Read the former arguments from the request, e.g.
request.Destination(copy/move),request.Annotations/request.UnchangedSince(annotations),request.Message/request.Sender/request.Recipients/request.TransferProgress(send). Callrequest.OnStarted (...)andrequest.OnCompleted (...)where appropriate so that callers relying on the callbacks keep working. -
Mocks: Set up the request-based interface method instead, matching any request of the relevant type. For example, with Moq:
// 4.x transport.Setup (t => t.Send (It.IsAny<FormatOptions> (), It.IsAny<MimeMessage> (), It.IsAny<CancellationToken> (), It.IsAny<ITransferProgress> ())) .Returns ("OK"); // 5.0 (client.Send (message) is an extension method that calls Send (ISendRequest, ...)) transport.Setup (t => t.Send (It.IsAny<ISendRequest> (), It.IsAny<CancellationToken> ())) .Returns (new SendResult ("OK", Array.Empty<MailboxAddress> (), Array.Empty<MailboxAddress> ()));
The extension methods without
FormatOptionscallSend (ISendRequest, ...). The ones withFormatOptionscallSend (FormatOptions, ISendRequest, ...). Set up whichever overload the code under test uses. The same applies toCopyTo/MoveTo/Expunge/StoreonIMailFolder. -
MessageSummary: Mocks and fakes ofIMessageSummaryshould drop theFolderproperty (see section 6).
SaslMechanism.Create ()returns a non-nullSaslMechanism. It throwsNotSupportedExceptionfor an unsupported mechanism instead of returningnull.- Detect:
SaslMechanism.Create (followed by anullcheck. - Fix: Use
SaslMechanism.TryCreate (name, credentials, out var sasl).
- Detect:
- IMAP
COMPRESSis only supported on .NET 8.0 and later.IImapClient.Compress ()/CompressAsync ()(and theImapClientmethods) don't exist in the .NET Framework and .NET Standard builds.- Detect:
Compress (/CompressAsync (calls in projects targetingnet4xornetstandard2.x. - Fix: Remove the call, or put it behind
#if NET8_0_OR_GREATER.
- Detect:
DeliveryStatusNotificationTypemoved fromMailKit.Net.SmtptoMailKit.- Fix: Add
using MailKit;.
- Fix: Add
AnnotationAttribute (string)now validates the specifier (RFC 5257 §3.2). It throwsArgumentExceptionfor empty components (a leading or trailing., or..), NUL or non-ASCII characters, or misplacedpriv/sharedcomponents.- Fix: Correct the specifier string.
AccessControlListno longer derives fromList<AccessControl>. It implementsIList<AccessControl>andIReadOnlyList<AccessControl>, and keepsAddRange (). Its methods throwArgumentNullExceptionfornullaccess controls.- Detect:
List<T>-only methods on anAccessControlList, such asFind,FindAll,Exists,Sort,ForEach,ConvertAll,ToArrayorRemoveAll, or passing one where aList<AccessControl>is expected. - Fix: Use LINQ (
FirstOrDefault,Where,Any,OrderBy,ToArray, ...), aforeachloop, ornew List<AccessControl> (acl).
- Detect:
| Removed | Replacement |
|---|---|
Parameterless BodyPart, BodyPartBasic, BodyPartMessage, BodyPartMultipart, BodyPartText constructors |
(ContentType, string) constructors |
Parameterless MailFolder constructor |
MailFolder (string fullName, char directorySeparator, FolderAttributes attributes) |
ImapFolderConstructorArgs.Name |
Derive the name from FullName and DirectorySeparator |
SslCipherAlgorithm, SslCipherStrength, SslHashAlgorithm, SslHashStrength, SslKeyExchangeAlgorithm, SslKeyExchangeStrength (.NET 10.0 build only) |
SslCipherSuite |
[Serializable], the protected (SerializationInfo, StreamingContext) constructors, and the GetObjectData () overrides on all MailKit exceptions (.NET Framework build only) |
None. Legacy exception serialization (SYSLIB0051) is obsolete and BinaryFormatter is insecure. Remove the serialization constructor from exception subclasses and log or transport the relevant properties instead |
These changes don't cause build errors. Search for the patterns below and review each match.
- Multi-line SMTP responses are joined with
\r\n(previously\n). This affectsSmtpResponse.ResponseText,SmtpCommandException.ResponseTextandSmtpProtocolException.LastResponse.- Detect:
Split ('\n'),Split ("\n")or.Split (new[] { '\n' }...)on these values (e.g. when splitting anSmtpCommandException.ResponseTextinto lines). - Fix: Split on
"\r\n", or trim a trailing'\r'from each line.
- Detect:
SmtpServiceNotAuthenticatedException(which derives fromServiceNotAuthenticatedException) is thrown for530 Authentication required.catch (ServiceNotAuthenticatedException)still works.- Detect: Exact type checks like
ex.GetType () == typeof (ServiceNotAuthenticatedException). - Fix: Use
is.
- Detect: Exact type checks like
SmtpProtocolExceptionmessages for unexpected disconnects no longer include the previous command's response text.- Detect: Code that parses
ex.Message. - Fix: Use the new
SmtpProtocolException.LastResponseandCommandproperties instead.
- Detect: Code that parses
- Telemetry
error.typevalues reported bymailkit.net.*.client.*metrics are more specific. For example,protocol_errormay now beresponse_ended,invalid_response,server_disconnectedorresponse_too_large.command_errormay now berejected,not_found,permission_denied, etc.- Detect: Dashboards, alerts or code that match on
protocol_errororcommand_error. These are usually outside the repository, so point them out to the user. - Fix: See the "Telemetry changes" table in ReleaseNotes.md.
- Detect: Dashboards, alerts or code that match on
- IMAP
COMPRESSnow flushes withZ_SYNC_FLUSHinstead ofZ_FULL_FLUSH. No code changes are required. Envelopeserialization no longer treats"localhost"as a placeholder domain. Older versions ofEnvelope.ToString ()wrote"localhost"as the domain of a domain-less mailbox (such asfejj). In 5.0,Envelope.TryParse ()reads"localhost"literally. As a result, envelope strings that were cached by 4.x deserialize such mailboxes asfejj@localhost. Strings serialized by 5.0 use an empty domain and round-trip correctly.- Detect: Code that persists
Envelope.ToString ()output (e.g. an offline cache) and parses it later withEnvelope.TryParse (). - Fix: Usually nothing; domain-less addresses are rare. If they matter, re-fetch the envelopes or rebuild the cache.
- Detect: Code that persists
SmtpClientvalidates addresses when sending withSMTPUTF8. If the server supportsSMTPUTF8and the message requires it (an internationalized sender or recipient address, orFormatOptions.International), every internationalized sender and recipient address is checked withSmtpUtf8AddressValidator. ASCII addresses, including IDN-encoded (xn--) domains, are not validated and are sent as-is. An invalid address (e.g. one that mixes scripts or contains disallowed code points) causesSend/SendAsyncto throwSmtpUtf8AddressExceptionbefore any SMTP commands are sent. The client stays connected.- Detect: Code that sends to internationalized addresses that the server previously accepted.
- Fix: Catch
SmtpUtf8AddressException(orArgumentException) and check itsMailboxandErrorproperties.
These are not required to port, but they often simplify code that worked around 4.x limitations:
SendResult.AcceptedRecipients/RejectedRecipientsinstead of tracking recipients throughSmtpClienthooks.CommandException.ErrorType/IsTransientandProtocolException.ErrorTypeinstead of parsing exception messages or status codes to decide whether to retry.SmtpCommandException.Command/ResponseTextandSmtpProtocolException.LastResponsefor diagnostics.- Request callbacks (
OnStarted ()/OnCompleted ()) onCopyRequest,MoveRequest,ExpungeRequest,StoreFlagsRequest,StoreLabelsRequest,StoreAnnotationsRequest,AppendRequest,ReplaceRequestandSendRequestto observe exactly what the server applied. ImapFeatures.IMAP4rev2andStatusItems.Deleted/IMailFolder.DeletedCount.StatusItems.DeletedStorage/IMailFolder.DeletedStorageSizeandFolderQuota.Resources(plus theMAILBOXandANNOTATION-STORAGElimits) for rfc9208 quota servers.await using var client = new ImapClient ();now works becauseMailServiceimplementsIAsyncDisposable. It does not log out, so keep callingDisconnectAsync (true)first.ImapIdleSession(client.CreateIdleSession ()) instead of a hand-writtenIdle ()loop withCountChanged/MessageExpunged/MessageFlagsChangedevent handlers and timers that restartIDLEevery few minutes.WaitForChangesAsync ()returns a batch of changes, re-issuesIDLEautomatically, falls back toNOOPpolling and stops gracefully when cancelled.