MimeKit 5.0 is a major release with a number of breaking changes. This guide explains how to port code written against MimeKit 4.x (4.18.1 and earlier). The release notes list every change; this guide covers only the ones that require changes to your code or that change behavior you may rely on.
The guide is written for people and for automated (AI) refactoring tools. Most sections start with a mapping table that can be applied mechanically, followed by the semantic changes that need a person (or an agent) to review them. The checklist at the end summarizes everything.
- Packages and assemblies
- S/MIME and OpenPGP
- DKIM and ARC
- MimeParser and MimeReader
- Message-ids, addresses and other MIME APIs
- HTML
- Removed APIs
- TNEF
- Porting checklist
The cryptography support (S/MIME, PGP/MIME, DKIM, ARC and the new DMARC support) has been split out of the core library into a separate assembly.
| 4.x | 5.0 |
|---|---|
MimeKit package (assembly MimeKit.dll) |
MimeKit meta-package, which references MimeKit.Core (assembly MimeKit.Core.dll) and MimeKit.Cryptography |
MimeKitLite package |
MimeKit.Core package |
- Projects that reference the
MimeKitpackage continue to get everything and usually need no changes. - Projects that don't need cryptography can reference
MimeKit.Coreon its own. - Namespaces are unchanged: the core types are still in
MimeKit,MimeKit.IO,MimeKit.Textand so on, and the cryptography types are still inMimeKit.Cryptography. - Anything that refers to the assembly by name must be updated, because the core types now live in
MimeKit.Core. This includes assembly-qualified type names in configuration files,[InternalsVisibleTo]workarounds, linker/trimmer descriptors and binding redirects.
| 4.x | 5.0 |
|---|---|
message.Sign (...), Encrypt (...), SignAndEncrypt (...) (instance methods) |
The same calls, now extension methods. Add using MimeKit.Cryptography;. |
MimeVisitor.VisitMultipartSigned (), VisitMultipartEncrypted (), VisitApplicationPkcs7Mime (), VisitApplicationPkcs7Signature (), VisitApplicationPgpEncrypted (), VisitApplicationPgpSignature () |
Derive from MimeKit.Cryptography.CryptographicMimeVisitor and override the same methods there |
CryptographyContext.DigestAlgorithmRank, EnabledDigestAlgorithms, Enable (DigestAlgorithm), Disable (DigestAlgorithm), IsEnabled (DigestAlgorithm) (also on ICryptographyContext) |
The same members on OpenPgpContext |
X509CertificateDatabase.GetSelectCommand (DbConnection connection, ...) and the other protected Get*Command, GetTableColumns, CreateTable, AddTableColumn, CreateIndex and RemoveIndex methods |
The same methods without the DbConnection parameter. Use the protected CreateCommand () method to create commands. |
IX509CertificateDatabase.Update (X509CrlRecord), X509CertificateDatabase.GetUpdateCommand (DbConnection, X509CrlRecord) |
Removed (obsolete and unused) |
AsymmetricAlgorithmExtensions.AsAsymmetricAlgorithm () |
Removed. AsAsymmetricKeyParameter () and AsAsymmetricCipherKeyPair () are unchanged. |
MimeKit 4.x automatically used DefaultSecureMimeContext for S/MIME when a SQLite library was available.
To make MimeKit AOT-compatible, MimeKit 5.0 no longer registers any S/MIME context. If you use S/MIME
without registering a context, CryptographyContext.Create () (and therefore the MultipartSigned,
MultipartEncrypted and ApplicationPkcs7Mime methods that don't take a context) throws a
NotSupportedException.
Register the context you want during startup:
CryptographyContext.Register (typeof (MySecureMimeContext));
// or, to keep the 4.x behavior:
CryptographyContext.Register (() => new DefaultSecureMimeContext ());Alternatively, pass a context explicitly to the methods that accept one.
The parser in MimeKit.Core knows nothing about cryptography. It only creates MultipartSigned,
MultipartEncrypted, ApplicationPkcs7Mime, ApplicationPkcs7Signature, ApplicationPgpEncrypted and
ApplicationPgpSignature entities once MimeKit.Cryptography has registered its entity factory. This
happens automatically the first time CryptographyContext is used (for example, by
CryptographyContext.Register ()). Messages that are parsed before then contain a plain Multipart or
MimePart instead, so body is MultipartSigned checks silently fail.
If your application might parse messages before it touches CryptographyContext, call this during
startup:
MimeKit.Cryptography.CryptographyModule.Initialize ();- OpenPGP integrity is enforced. Decryption rejects modified encrypted data and, by default,
legacy packets without integrity protection. Set
OpenPgpContext.AllowUnauthenticatedDecryptiontotrueonly if legacy compatibility is necessary; this does not bypass integrity checks on protected messages.DecryptTo ()andDecryptToAsync ()may write unauthenticated output before throwing, so do not consume their output until they complete successfully. - Temporary S/MIME trust is explicit. Ordinary imports, including PKCS#12 and message-supplied
certificates, no longer create trust anchors. Use
temporaryContext.Import (rootCertificate, true)orImportAsync (rootCertificate, true)with a Bouncy CastleX509Certificateobtained from a trusted source. Passingfalseto these overloads removes trust; ordinary imports preserve any trust already configured. Do not mark certificates trusted just because they are self-signed. - CMS expansion is bounded.
SecureMimeContext.MaxDecompressedSizedefaults to 64 MiB and includes MIME headers. Set a larger positive byte limit if required. All four decompression APIs throwFormatExceptionwhen the limit is exceeded; stream outputs may contain partial data. Implementers ofISecureMimeContextmust also provide this property. - CRL retrieval requires a trusted path. With
CheckCertificateRevocationenabled, the Bouncy Castle contexts first validate a certificate path without revocation checking, fetch CRLs only for that path, and then validate with revocation checking. An untrusted chain now fails without fetching its distribution points. Configure roots and intermediate certificates locally as needed. This is not a network sandbox: use outbound network restrictions appropriate to your deployment. - SHA-256 is the default signing digest. The
Sign (),SignAsync (),SignAndEncrypt ()andSignAndEncryptAsync ()overloads that don't take aDigestAlgorithmnow use SHA-256 instead of SHA-1. PassDigestAlgorithm.Sha1explicitly if a recipient really can't handle SHA-256. - OpenPGP digest preferences. The default
OpenPgpContext.DigestAlgorithmRanknow matches GnuPG: SHA-512, SHA-384, SHA-256, SHA-224 and SHA-1 (SHA-1 used to be first, and RIPEMD-160 is no longer included). This only affects the preferences advertised by keys created withGnuPGContext.GenerateKeyPair (). - No more binary serialization.
ParseException,TnefException,CertificateNotFoundException,DigitalSignatureVerifyException,PrivateKeyNotFoundExceptionandPublicKeyNotFoundExceptionare no longer[Serializable]and no longer have(SerializationInfo, StreamingContext)constructors orGetObjectData ()overrides. Remove any subclass constructors or overrides that chain to them.
| 4.x | 5.0 |
|---|---|
IDkimPublicKeyLocator, DkimPublicKeyLocatorBase |
IDnsResolver (see below) |
new DkimVerifier (IDkimPublicKeyLocator), new ArcVerifier (IDkimPublicKeyLocator) |
new DkimVerifier (IDnsResolver), new ArcVerifier (IDnsResolver) |
DkimVerifierBase.PublicKeyLocator (protected) |
DkimVerifierBase.DnsResolver |
bool DkimVerifier.Verify (...), Task<bool> VerifyAsync (...) |
DkimSignatureValidationResult Verify (...), Task<DkimSignatureValidationResult> VerifyAsync (...). Test result.Status == DkimSignatureStatus.Pass. |
| — | DkimSignatureValidationResult[] Verify (MimeMessage) and VerifyAsync: verify every DKIM-Signature header, up to DkimVerifier.MaxSignatures (default 10) |
new DkimSigner (string domain, string selector, ...), ArcSigner (string domain, string selector, ...) and DkimSignerBase (string domain, string selector, ...) (obsolete) |
Removed. Use the constructors that take the private key. |
DkimSignerBase.PrivateKey setter (protected, obsolete) |
Removed. Pass the private key to the base class constructor. |
In 4.x, you implemented LocatePublicKey (methods, domain, selector): you built the
<selector>._domainkey.<domain> query name yourself, fetched the TXT record and (usually via
DkimPublicKeyLocatorBase.GetPublicKey ()) parsed the key. In 5.0, you only perform the DNS TXT query.
MimeKit builds the query name and parses (and validates) the key record itself.
class DnsResolver : IDnsResolver
{
public DnsTxtResponse QueryTxt (string domain, CancellationToken cancellationToken = default)
{
// Perform a TXT query for `domain` (it is already the full query name).
// Concatenate the character-strings within each TXT record, but return
// each record as a separate string.
if (nxdomain)
return new DnsTxtResponse (DnsQueryStatus.NonExistentDomain);
if (serverFailureOrTimeout)
return new DnsTxtResponse (DnsQueryStatus.TemporaryFailure);
return new DnsTxtResponse (records);
}
public Task<DnsTxtResponse> QueryTxtAsync (string domain, CancellationToken cancellationToken = default)
{
// ...
}
}Distinguishing NonExistentDomain from TemporaryFailure matters: a temporary failure is reported as
TempError (DKIM) or ArcValidationErrors.DnsTemporaryFailure (ARC) so that receivers can defer the
message rather than fail it. The same IDnsResolver is used by the new DmarcVerifier. See the
DkimVerifier sample for a complete implementation using
DnsClient.NET.
- Verification no longer throws for bad input. A malformed
DKIM-Signatureheader, a failed key lookup, an unparsable key record or an exception thrown by yourIDnsResolveris reported through the result (PermErrororTempError, withReasonandException), not as an exception. Only argument errors and cancellation throw. Removetry/catchblocks that existed to treatFormatExceptionorParseExceptionas a failed signature. - A
boolis not enough anymore.Pass,Fail,Policy,TempErrorandPermErrormatch the RFC 8601dkimresults. Useresult.ToAuthenticationMethodResult ()to build an Authentication-Results entry. - Stricter key records and signatures. Key records are parsed according to RFC 6376: records with
duplicate tags, a misplaced or wrong
v=, an unknownk=, or ans=that excludesemailare ignored;h=and key-type mismatches are rejected; an emptyp=is treated as revoked; andt=sis enforced. Signatures whosex=expiration time has passed are nowPermError. Messages that "verified" with 4.x may not verify with 5.0. - ARC. As required by RFC 8617, any error still makes the chain
Fail, butArcHeaderValidationResult.Reason/Exceptionand the newArcValidationErrors.DnsTemporaryFailureflag now tell you why.
| 4.x | 5.0 |
|---|---|
ExperimentalMimeParser |
MimeParser |
MimeParser |
LegacyMimeParser (retained for compatibility; only use it if you rely on its exact behavior) |
mimeParser.ReadHeaders (), ReadEntity (), ReadMessage () (and Async variants), inherited from MimeReader |
Removed. MimeParser no longer derives from MimeReader. Use ParseHeaders (), ParseEntity () or ParseMessage (), or subclass MimeReader directly. |
OnMboxMarkerRead (byte[] buffer, int startIndex, int count, long beginOffset, int lineNumber, CancellationToken) (and Async) |
OnMboxMarkerRead (byte[] buffer, int startIndex, int count, CancellationToken). The offset and line number are passed to OnMboxMarkerBegin (). |
OnMultipartBoundary (), OnMultipartEndBoundary () (and Async; obsolete) |
OnMultipartBoundaryEnd (), OnMultipartEndBoundaryEnd () |
persistent: false in parser constructors, SetStream, or stream Load/LoadAsync |
storage: MimeContentStorage.Memory (or omit it where optional) |
persistent: true in the same APIs |
storage: MimeContentStorage.Source (requires a seekable stream) |
For most code, the parser change is a source-compatible rename: new MimeParser (stream) and
MimeMessage.Load () keep working, but are now backed by the new parser.
The persistent boolean overloads are replaced, not retained. This applies to MimeParser,
IMimeParser.SetStream, MimeMessage.Load/LoadAsync, and MimeEntity.Load/LoadAsync.
LegacyMimeParser retains its public boolean constructors and SetStream overloads, including
their nonseekable-stream fallback. It implements the new IMimeParser.SetStream overloads
explicitly, so calls through IMimeParser use the enum and its stricter validation.
MimeContentStorage.Memory (0) copies MIME part content into memory and remains the default.
You may close or change the input stream after parsing. MimeContentStorage.Source (1) avoids
the copy by retaining bounded references to the input stream. The caller owns the stream in
both modes. For Source, keep it open and its contents unchanged until all parsed content is
no longer needed. Disposing a parsed message or entity does not close the source stream.
There is no ownership-transfer option.
Unlike persistent: true, Source throws ArgumentException for a nonseekable stream rather
than falling back to copying. To explicitly retain the old fallback, use
stream.CanSeek ? MimeContentStorage.Source : MimeContentStorage.Memory.
Invalid storage or format enum values throw ArgumentOutOfRangeException; rejected
SetStream arguments do not reset or otherwise change the parser.
MimeReader only scans input and has no content storage option.
using var stream = File.OpenRead ("message.eml");
using var message = MimeMessage.Load (stream, MimeContentStorage.Source);
// Use message content before disposing stream.MimeParsersubclasses.MimeParsermirrors all ofMimeReader'sOn*callbacks asprotected virtualmethods. If you override any of them, you must call the base implementation, because that is whereMimeParserbuilds the message.MimeParserstill exposesOptions,IsEndOfStream,Position,ComplianceLoggerandComplianceOptions.MimeReadercontent callbacks. The newline that precedes a multipart boundary marker belongs to the boundary (RFC 2046) and is no longer passed toOnMimePartContentRead[Async] ()orOnMultipartEpilogueRead[Async] (). The bytes passed to these callbacks now exactly match the range between theBeginandEndoffsets. Remove any code that trimmed the trailing newline itself.- Header length limit. The new
ParserOptions.MaxHeaderLength(16 KB by default) bounds how much a single header can buffer. Longer headers make the parser throw aFormatException. - Byte-for-byte round trips.
MimeParsernow re-serializes messages exactly as they were parsed, even when they're truncated or don't end with a newline. If you append messages to an mbox file, setFormatOptions.EnsureNewLinetotrue. - Mbox edge cases.
ParseMessage ()now throws aFormatExceptionfor a stream that contains nothing but mboxFromlines. AFromline that is followed by lines that aren't headers is now a parse error, where previously that content was swallowed by the mbox marker.
| 4.x | 5.0 |
|---|---|
MessagePartial.Join (ParserOptions, MimeMessage, IEnumerable<MessagePartial>) (and overloads) |
Takes an IReadOnlyList<MessagePartial>. Arrays and List<MessagePartial> work unchanged; call .ToList () on other sequences. |
MimeKit.IO.BoundStream |
Renamed to MimeKit.IO.BoundedStream. Update constructor calls, type references and subclasses; the constructor parameters and stream behavior are unchanged. |
ContentEncoding.Default |
Renamed to ContentEncoding.Unspecified; its numeric value remains zero. |
ContentEncoding.Unspecified retains the old behavior: assigning it to
MimePart.ContentTransferEncoding removes the header, while using it for MimeContent.Encoding
indicates that the stream has not been transfer-encoded. Encoder and decoder filters pass the
data through unchanged. Update any stored enum names from Default to Unspecified; numeric
values are unchanged.
- Message-id validation. The
MimeMessage.MessageId,ResentMessageIdandInReplyTosetters, theMimeEntity.ContentIdsetter andMimeMessage.References(Add,AddRange,Insertand the indexer) share the same validation. Values that 4.x silently truncated or altered now throw anArgumentException:<>, a second msg-id or other trailing garbage, unquoted whitespace in a msg-id without angle brackets, and line breaks or NUL characters. Angle brackets remain optional. - Empty msg-ids. When parsing, an empty msg-id (
<>) is ignored:MessageId,ResentMessageId,InReplyTo,ContentIdandMimeUtils.ParseMessageId ()returnnullinstead of"", andReferences/MimeUtils.EnumerateReferences ()skip it. Check fornullrather than an empty string. - Trimmed display names.
InternetAddress.Name(and soMailboxAddress.NameandGroupAddress.Name) is trimmed when set, including by constructors and the parser. A whitespace-only name becomes"". - IDN-encoded domains are no longer decoded. The address parser and the
MailboxAddress.Addresssetter preserve the domain as-is, soMailboxAddress.AddressandDomainmay now contain A-labels (e.g.user@xn--bcher-kva.example). UseGetAddress (false)or the newGetDomain (false)to get the Unicode form for display, andGetAddress (true)/GetDomain (true)for the ASCII form.GetAddress (false)now decodes rather than returning the address as-is.IsInternationalisfalsefor an IDN-encoded domain,Equals ()no longer treats the A-label and Unicode forms as equal, andFormatOptions.Internationalno longer causes an IDN-encoded domain to be written as Unicode. To validate the Unicode form of a parsed address withSmtpUtf8AddressValidator, passSmtpUtf8AddressValidationOptions.DecodeIdn. MessagePartial.Join ()validates theidandnumberof every partial up front, so a mismatchedidor a missingnumberthrowsInvalidOperationExceptioneven for a single partial.MimeEntity.WriteTo ()is no longer virtual.WriteTo (FormatOptions, Stream, bool, CancellationToken)andWriteToAsync ()can no longer be overridden; a non-recursive writer now writes the entity and all of its descendants. Customize a subclass's output through its headers or content instead.
MimeMessage and MimeEntity still implement IDisposable; existing using statements and calls to
Dispose () remain valid. MimeKit now walks structural ownership iteratively, with storage proportional
to nesting depth: a message owns its Body, a multipart owns its children in list order, and a
MessagePart owns its Message. Each object's Dispose (bool) override runs after cleanup has been
attempted for its structural descendants.
Update subclasses so that Dispose (bool) releases only resources owned directly by that object and
calls base.Dispose (disposing). Do not dispose Body, multipart children or MessagePart.Message
there; traversal no longer happens inside base.Dispose (disposing). In particular, code before the
base call can no longer assume that children are still usable. Custom-owned resources are still the
override's responsibility; stack safety does not extend to arbitrary recursive application code.
If cleanup throws, MimeKit still attempts the remaining siblings and parent cleanup hooks. A single
exception is rethrown; multiple exceptions are reported in an AggregateException in traversal order.
Calling Dispose () again on the root retries incomplete branches and failed hooks without repeating
successful hooks. A failed hook may have partially released its resources, so it must tolerate retry.
Successful local cleanup suppresses finalization even if a descendant failed; failed local cleanup
does not suppress finalization.
Treat the tree as unusable once disposal has begun, even if disposal throws. Retain ownership links for
retry: do not clear, replace or otherwise mutate them during or after disposal, and do not dispose the
same tree concurrently. Reentrant disposal of an object already being disposed is ignored. An ownership
cycle is reported as an InvalidOperationException, while other cleanup is still attempted.
Base64Encoder, QuotedPrintableEncoder, UUEncoder, YEncEncoder and the
EncoderFilter.Create factories now default to CRLF rather than LF line endings.
Their existing constructor and factory signatures remain available. To preserve the old output,
select NewLineFormat.Unix explicitly:
var base64 = new Base64Encoder (NewLineFormat.Unix);
var quotedPrintable = new QuotedPrintableEncoder (NewLineFormat.Unix, maxLineLength: 76);
var uuencode = new UUEncoder (NewLineFormat.Unix);
var yencode = new YEncEncoder (NewLineFormat.Unix);
var filter = EncoderFilter.Create (ContentEncoding.Base64, NewLineFormat.Unix);The new overloads accept only Dos and Unix; Mixed and invalid enum values throw
ArgumentOutOfRangeException. Continue sizing buffers with EstimateOutputLength rather than
assuming one byte per newline. Quoted-printable uses the selected format for both hard and soft
line breaks: hard breaks survive decoding, whereas soft breaks do not.
Ordinary MIME serialization still uses FormatOptions.NewLineFormat. Freshly encoded base64,
quoted-printable and uuencoded bodies now produce those line endings directly, rather than passing
through a second conversion filter. Already encoded bodies and unencoded text retain newline
normalization.
MimeKit.Encodings.YEncoder and YDecoder have been renamed to YEncEncoder and
YEncDecoder. Update references to the new names. Their Checksum properties still expose
the running CRC state; convert it to a wire checksum with
unchecked ((uint) codec.Checksum) ^ 0xffffffff.
YEncReader, YEncWriter, YEncHeader and YEncOptions also live in MimeKit.Encodings.
They process standalone yEnc blocks without changing MimeParser or adding a MIME
content-transfer-encoding. Pass an article body after transport decoding; NNTP dot-unstuffing
is the caller's responsibility.
using var reader = new YEncReader (articleBody, leaveOpen: true);
while (reader.ReadNext ()) {
var header = reader.Header;
using var decoded = new MemoryStream ();
reader.DecodeTo (decoded);
// Only use decoded content after DecodeTo succeeds.
}
using var writer = new YEncWriter (destination, leaveOpen: true);
uint checksum = writer.Write (new YEncHeader ("example.bin", decodedSize), source);ReadNextAsync, DecodeToAsync and WriteAsync provide equivalent asynchronous operations.
Input need not be seekable. Each writer source must contain exactly the declared number of
decoded bytes, starting at its current position. It is left open; the writer does not flush the
destination. Readers buffer ahead, so do not access the underlying stream between blocks.
Decode each pending block before calling ReadNext again; use Stream.Null to discard it
while still checking its integrity.
For multipart data, construct a YEncHeader with the whole-file size, a nonzero partNumber,
optional totalParts, and one-based inclusive begin/end offsets. Supply only that part's
bytes to the writer. It emits a part checksum and can include an optional whole-file checksum.
The reader validates each individual range, not gaps, overlaps, or ordering across parts;
assembly remains the caller's responsibility.
Reader and writer checksums are finalized unsigned CRC32 values, unlike the low-level codecs.
ExpectedChecksum exposes an advertised whole-file CRC; it remains unverified metadata
for a partial range. ExpectedPartChecksum, when present, is checked against the current part.
Absent checksums are accepted. Malformed framing, truncation, size mismatches and applicable
checksum mismatches throw FormatException. Discard partial output on failure; an I/O,
format or mid-operation cancellation failure invalidates the reader or writer.
YEncOptions defaults to a 16 KB encoded-line limit, a 1 GB decoded limit per block/part,
Latin-1 filenames and CRLF output. Writer payload widths are 60 through 998; the configured
line limit must also accommodate a final escape pair's extra byte. Readers accept CRLF or LF.
Options are copied at construction. Set FileNameEncoding explicitly for UTF-8 filenames;
unrepresentable or invalid filenames cause errors instead of replacement characters.
Treat filenames as untrusted metadata, never as ready-to-use filesystem paths.
| 4.x | 5.0 |
|---|---|
HtmlTokenizerState.AttributeValueQuoted |
AttributeValueDoubleQuoted or AttributeValueSingleQuoted. The numeric values of the members that follow have changed, so don't persist them. |
HtmlToHtml.FilterHtml (obsolete) |
Removed. Use HtmlTagCallback with an allow-list, or a dedicated sanitizer such as HtmlSanitizer. |
| — | HtmlToHtml.NoScriptHandling, HtmlTokenizer.ScriptingEnabled |
| — | HtmlTokenizer.MaxElementDepth, HtmlToHtml.MaxElementDepth (4096 by default) |
<noscript>is now filtered. By default (HtmlNoScriptHandling.Unwrap),HtmlToHtmltokenizes the content of<noscript>elements as markup, passes it to yourHtmlTagCallbackand removes the<noscript>tags themselves. In 4.x the content was copied verbatim without being passed to the callback, which let tags inside<noscript>bypass filtering when the output was rendered with scripting disabled (as is typical for email).HtmlNoScriptHandling.ScriptingEnabledrestores the 4.x behavior; only use it if the output is always rendered with scripting enabled.- Tokenization matches browsers.
HtmlTokenizernow conforms to the html5lib tokenizer tests. YourHtmlTagCallbackmay now see tags that 4.x treated as text (and vice versa), CDATA sections are only recognized inside SVG and MathML, and<img/onerror=...>has anonerrorattribute. If you filter HTML withHtmlToHtml, this makes the filtering more reliable; review any code that matched the old quirks. - Character references. Numeric character references follow the HTML specification (any number of
digits, an optional
;, out-of-range values become U+FFFD), and legacy named references without a;in attribute values are left alone when followed by=or an alphanumeric (as inhref="?a=1¬=2"). This also affectsHtmlEntityDecoderandHtmlUtils.HtmlDecode (). - Element depth limit. Once more than
MaxElementDepthdistinct elements are open, the rest of the input is returned as character data, whichHtmlToHtmlwrites as encoded text. HtmlDocTypeTokensetters now throw anArgumentExceptionfor values that can't be written as a well-formed DOCTYPE (for example, a name that contains whitespace or>).
These APIs were removed without a direct replacement (most were already obsolete in 4.x):
| 4.x | 5.0 |
|---|---|
QEncoder |
Removed. Use Rfc2047.EncodePhrase () or Rfc2047.EncodeText () to encode header text. |
HexEncoder, HexDecoder |
Removed from the public API; internal implementations are named Rfc2231Encoder and Rfc2231Decoder |
TnefPropertyTag.Puid |
TnefPropertyTag.PuidA or TnefPropertyTag.PuidW |
The obsolete members of MimeReader, DkimSigner, ArcSigner, DkimSignerBase, HtmlToHtml and
IX509CertificateDatabase are covered in the sections above.
MimeKit 5.0 replaces the MimeKit.Tnef API. The reader was rewritten against the published
[MS-OXTNEF], [MS-OXCDATA] and [MS-OXCMAIL] specifications, a TnefMessage object model was added, and
TnefPart.ConvertToMessage () was replaced by a spec-driven MIME converter. The
TNEF porting checklist summarizes this section.
MimeKit 5.0 offers three levels of TNEF API. Most 4.x code reached for TnefReader only because there
was nothing between it and ConvertToMessage (), and can now use a higher level.
| If your 4.x code... | Use in 5.0 |
|---|---|
called TnefPart.ConvertToMessage () or TnefPart.ExtractAttachments () |
TnefPart.LoadTnefMessage () + TnefMessage.ConvertToMime () |
| walked attributes to collect the subject, recipients, bodies or attachments | TnefMessage (Properties, Recipients, TextBody/HtmlBody/RtfBody, Attachments) |
| needed attribute-level detail (offsets, checksums, unknown attributes, streaming) | TnefReader + TnefPropertyReader |
| needed to produce TNEF (4.x could not) | TnefWriter + TnefPropertyWriter (see Write a TNEF stream) |
TnefMessage buffers bodies and attachment content in memory, subject to the limits in TnefOptions.
TnefReader streams and does not buffer.
| 4.x | 5.0 |
|---|---|
TnefComplianceMode (Loose, Strict) |
Removed. The reader is always "loose"; see Compliance. |
TnefComplianceStatus (flags) |
Removed. Use ITnefComplianceLogger, TnefComplianceIssue and TnefComplianceViolation. |
TnefPropertyId.InternetCPID |
TnefPropertyId.InternetCodepage |
TnefPropertyTag.InternetCPID |
TnefPropertyTag.InternetCodepage |
TnefPropertyTag.Puid |
Removed (obsolete); use TnefPropertyTag.PuidA or TnefPropertyTag.PuidW. |
New types: TnefOptions, ITnefComplianceLogger, TnefComplianceIssue, TnefComplianceViolation,
TnefMessage, TnefAttachment, TnefRecipient, TnefRecipientType,
TnefMessageBody, TnefMessageBodyFormat, TnefPropertySet, TnefProperty, TnefPropertySetGuid,
TnefConversionOptions, TnefConversionResult, TnefConversionLoss and TnefConversionLossKind.
TnefAttributeType (internal in 4.x) is now public.
TnefNameId gained static fields for well-known named properties (for example TnefNameId.Location).
| 4.x | 5.0 |
|---|---|
MimeMessage ConvertToMessage () |
using var tnef = part.LoadTnefMessage (); then using var result = tnef.ConvertToMime (); and use result.Message |
IEnumerable<MimeEntity> ExtractAttachments () |
result.Message.BodyParts or result.Message.Attachments (see below), or TnefMessage.Attachments |
| — | Task<TnefMessage> LoadTnefMessageAsync (TnefOptions?, CancellationToken) |
The same members were removed from and added to ITnefPart.
LoadTnefMessage ()returns aTnefMessagethat is independent of theTnefPartand must be disposed.ConvertToMime ()returns aTnefConversionResult. Disposing it disposesresult.Message. To keep the message, take a reference toresult.Messageand do not dispose the result.- The converted
MimeMessagedoes not depend on theTnefMessage, so theTnefMessagemay be disposed as soon asConvertToMime ()returns.
ConvertToMime () follows [MS-OXCMAIL]. Differences from 4.x ConvertToMessage () that may affect
callers:
- PidTagMimeSkeleton. If the message has a MIME skeleton (common for mail that passed through Exchange), the skeleton's MIME structure and headers are used and filled in with the TNEF bodies and attachments. 4.x ignored it.
- Headers. The Received headers from
PidTagTransportMessageHeaderscome first, then the headers derived from MAPI properties, then the headers stored inPS_INTERNET_HEADERSnamed properties. No header is fabricated: there is no generated Message-Id and no empty From or Subject header. - Dates.
PT_SYSTIMEvalues are UTC (see TnefPropertyReader), so the Date header no longer depends on the time zone of the machine that ran the conversion. - Structure. When there is more than one body, the bodies become a
multipart/alternative(text/plain, thentext/html). Which bodies are used is decided by the [MS-OXBBODY] best body algorithm (PidTagNativeBody, thenPidTagRtfInSync). When the best body is RTF, both alternatives are generated from the compressed RTF withRtfToTextandRtfToHtml([MS-OXCMAIL] 2.1.3.3.5); thePidTagBodyandPidTagHtmlvalues are not used. Otherwise the plain text and HTML bodies are used as they are, and the out-of-date RTF body is dropped. Atext/rtfpart is never produced. In 4.x,ConvertToMessage ()added every body property it found (text/rtf,text/htmlandtext/plain) to themultipart/alternative, in the order the properties appeared in the TNEF stream, without considering which body was authoritative. The inline attachments that the HTML body references are grouped with the body in amultipart/related([MS-OXCMAIL] 2.1.3.3.6). The other attachments follow in amultipart/mixed. A message with no body and one attachment is still wrapped in amultipart/mixed. - Inline attachments. Inline status follows the [MS-OXCMAIL] 2.1.3.4.1 "best body" rules. When there
is an HTML body, attachments flagged
afRenderedInBodythat the HTML references bycid:orContent-Locationare inline. When the best body is RTF, the attachments are matched, inPidTagRenderingPositionorder, with the RTF's\objattphplaceholders ([MS-OXRTFEX] 2.2.3.4, [MS-OXCMAIL] 2.1.3.4.1.1). Hidden attachments and those with a rendering position of -1 are skipped. Attachments whose content is an image a browser can display (PNG, JPEG, GIF, BMP or WebP) are given aContent-Id, are referenced by an<img src="cid:...">in the generated HTML, and are inline. If the numbers of placeholders and attachments differ, the images are appended to the end of the HTML body instead. Other attachments, including OLE objects, are not inline. In 4.x, OLE attachments were always inline. If the RTF encapsulates HTML ([MS-OXRTFEX]), the extracted HTML is the HTML body, and the HTML rules apply to it instead of the placeholders. Inline attachments getContent-Disposition: inline, soMimeMessage.Attachmentsdoes not list them. - RTF attachment placeholders. By default, the generated plain-text body has nothing at an attachment
placeholder, and the HTML body has an image or nothing. Set
TnefConversionOptions.AttachmentPlaceholderCallbackto return text (for example, the attachment's file name) to insert at the placeholder. The text is HTML-encoded in the HTML body, where it is used only for attachments that are not displayed as images. - OLE objects. OLE attachments are kept as-is unless
TnefConversionOptions.OleObjectConverteris set. ATnefOleObjectConvertercan render an OLE object as an image. The image replaces the OLE attachment and is named after its display name ([MS-OXCMAIL] 2.1.3.4.4). If the converter returnsnullor content that is not an image, the OLE attachment is kept. Any OLE attachment that is kept is reported as aTnefConversionLossKind.OleObjectNotRenderedloss. - Embedded messages. As in 4.x, embedded messages are added as
application/ms-tnefTnefParts by default. SetTnefConversionOptions.ConvertEmbeddedMessages = trueto getmessage/rfc822MessageParts instead. - Losses. Anything that could not be represented in MIME is listed in
result.Losses(TnefConversionLoss.KindandDescription) instead of being silently dropped. - Unsafe content types. An attachment that claims to be
multipart/*,message/*,application/applefileorapplication/mac-binhex40, or that has an invalid MIME type, is emitted asapplication/octet-stream.
4.x ExtractAttachments () returned the body alternatives and the attachments as a flat list. To get
the same set of entities:
using var tnef = part.LoadTnefMessage ();
using var result = tnef.ConvertToMime ();
// Everything (bodies + attachments), like 4.x ExtractAttachments ():
foreach (var entity in result.Message.BodyParts) {
// ...
}
// Only the attachments (Content-Disposition: attachment):
foreach (var attachment in result.Message.Attachments) {
// ...
}The entities belong to result.Message. Either finish with them before result is disposed, or do not
dispose result until you are done with them.
If you only need the raw attachment bytes, skip MIME conversion entirely and use
TnefMessage.Attachments (see Extract attachments).
TnefReader is now sealed.
| 4.x | 5.0 | Notes |
|---|---|---|
TnefReader (Stream) |
TnefReader (Stream stream, TnefOptions? options = null, bool leaveOpen = false) |
|
TnefReader (Stream, int defaultMessageCodepage, TnefComplianceMode) |
new TnefReader (stream, new TnefOptions { DefaultCodepage = codepage }) |
Compliance mode has no replacement; see Compliance. |
MaxNestingDepth |
TnefOptions.MaxNestingDepth |
|
bool ReadNextAttribute () |
bool Read (CancellationToken = default) / ReadAsync |
|
AttributeLevel |
Level |
|
AttributeTag |
Tag |
|
| — | AttributeType |
Type encoded in the attribute id. |
AttributeRawValueLength |
Length |
|
AttributeRawValueStreamOffset |
StreamOffset + 9 |
StreamOffset is now the offset of the attribute header (1-byte level, 4-byte id, 4-byte length). |
StreamOffset (int, current read position) |
StreamOffset (long, start of current attribute) |
Meaning changed. Offsets of embedded readers are relative to the outermost stream. |
int ReadAttributeRawValue (byte[], int, int) |
Stream OpenValueStream () then Read |
|
TnefPropertyReader property |
GetPropertyReader () |
Only valid for MapiProperties, Attachment and RecipientTable attributes; throws InvalidOperationException otherwise. |
AttachmentKey (short) |
LegacyKey (ushort) |
|
MessageCodepage |
Codepage |
An attOemCodepage of 0 is now ignored, and an unsupported one keeps TnefOptions.DefaultCodepage rather than switching to windows-1252. If the stream has no attOemCodepage, the message's PidTagInternetCodepage is used from the point where it is read ([MS-OXTNEF] 2.3.3.2). |
TnefVersion |
Read the TnefAttributeTag.TnefVersion attribute with ReadValueAsInt32 (). |
A non-0x00010000 version is reported as UnsupportedVersion. |
ComplianceMode |
— | Removed. |
ComplianceStatus |
ComplianceLogger |
See Compliance. |
ResetComplianceStatus () |
— | Removed; use a new logger. |
| — | ComplianceOptions |
A TnefComplianceOptions; its MaxIssuesPerViolation caps repeated reports. Set this when reading untrusted input. |
| — | Depth |
Embedded-message nesting depth (0 for the outermost stream). |
| — | Options |
The TnefOptions the reader was created with. |
Close () |
Dispose () |
|
Dispose () |
Dispose () |
Disposes the stream unless leaveOpen is true. |
In 4.x, the values of legacy attributes such as attSubject or attDateSent were read through
reader.TnefPropertyReader.ReadValueAs* (). In 5.0 they are read directly from the reader:
| 4.x | 5.0 |
|---|---|
reader.TnefPropertyReader.ReadValueAsString () |
reader.ReadValueAsString () |
reader.TnefPropertyReader.ReadValueAsInt16 () |
reader.ReadValueAsInt16 () |
reader.TnefPropertyReader.ReadValueAsInt32 () |
reader.ReadValueAsInt32 () |
reader.TnefPropertyReader.ReadValueAsDateTime () |
reader.ReadValueAsDateTime () |
reader.TnefPropertyReader.ReadValueAsBytes () |
reader.ReadValueAsBytes () |
reader.TnefPropertyReader.ReadValue () |
Choose the typed method from reader.AttributeType. |
Each has an *Async twin. Strings and byte arrays may only be read once.
- Nothing in the stream causes an exception. 4.x threw
TnefExceptionin strict mode andEndOfStreamException/TnefExceptionfrom several paths in loose mode. 5.0 reports aTnefComplianceIssueand recovers. A truncated stream simply stops returning attributes. - A wrong signature does not throw from
Read (). It reportsInvalidSignatureand returnsfalse. (TnefMessage.Load ()does throwTnefExceptionin this case.) - Late message attributes. A message-level attribute after the first attachment-level attribute is
reported as
MessageAttributeAfterAttachment. - The reader is only valid on its own attribute.
TnefPropertyReaders and value streams obtained for one attribute throwInvalidOperationExceptiononceRead ()has advanced past it.
TnefPropertyReader is now sealed, and is obtained by calling TnefReader.GetPropertyReader ().
| 4.x | 5.0 | Notes |
|---|---|---|
PropertyTag |
Tag |
|
| — | PropertyType |
The value type, Tag.ValueTnefType. |
IsMultiValuedProperty |
IsMultiValued |
|
IsNamedProperty |
Name.HasValue (or Tag.IsNamed) |
|
PropertyNameId (TnefNameId) |
Name (TnefNameId?) |
null for properties that are not named. |
IsObjectProperty |
PropertyType == TnefPropertyType.Object |
|
IsEmbeddedMessage |
IsEmbeddedMessage |
Now detects the IID_IMessage prefix, so it no longer depends on AttachMethod having been read first. |
ValueCount, PropertyCount, RowCount |
unchanged | |
RawValueLength, RawValueStreamOffset |
— | Removed. Use OpenValueStream ().Length for variable-length values. |
ValueType |
— | Removed. Use PropertyType, or ReadValue ()?.GetType (). |
| — | IsValueConsumed |
Whether a variable-length value has already been read. |
bool ReadNextRow () |
ReadNextRow (CancellationToken = default) / ReadNextRowAsync |
|
bool ReadNextProperty () |
ReadNextProperty (CancellationToken = default) / ReadNextPropertyAsync |
Now positions on the first value; see below. |
bool ReadNextValue () |
ReadNextValue (CancellationToken = default) / ReadNextValueAsync |
Advances to the second and later values. |
Stream GetRawValueReadStream () |
Stream OpenValueStream () |
|
int ReadRawValue (byte[], int, int) |
OpenValueStream ().Read (...) |
|
int ReadTextValue (char[], int, int) |
ReadValueAsString () |
Or wrap OpenValueStream () in a StreamReader. |
TnefReader GetEmbeddedMessageReader () |
TnefReader OpenEmbeddedMessage () |
Shares the parent's options and logger; dispose it when done. |
ReadValue () |
ReadValue (CancellationToken = default) / ReadValueAsync |
Currency now returns decimal; see below. |
ReadValueAsString (), ReadValueAsBytes () |
unchanged, plus a CancellationToken and *Async twins |
May only be called once per value. |
ReadValueAsBoolean/Int16/Int32/Int64/Float/Double/DateTime/Guid () |
unchanged | May be called any number of times. |
| — | ReadProperty (), ReadPropertySet (), ReadRowsAsPropertySets () (+ *Async) |
Materialize the current property, the remaining properties, or every row. |
-
Cursor position. After
ReadNextProperty ()returnstrue, the reader is already positioned on the property's first value (ifValueCount > 0). Read all values with:do { var value = prop.ReadValue (); } while (prop.ReadNextValue ());
Code that called
ReadNextValue ()before the first read (while (prop.ReadNextValue ()) { ... }) skips the first value in 5.0 and must be rewritten as ado/whileloop. -
Single-use variable-length values. Strings, binary values and objects can be read once (via
ReadValue,ReadValueAsString,ReadValueAsBytes,OpenValueStreamorOpenEmbeddedMessage). A second read throwsInvalidOperationException. Fixed-width values can be read repeatedly. -
Stale readers throw. Any use after the owning
TnefReaderadvances to another attribute throwsInvalidOperationException. -
PT_SYSTIMEis UTC. 4.x usedDateTime.FromFileTime(local time). 5.0 returnsDateTimeKind.Utcvalues. Remove any.ToUniversalTime ()calls added to compensate. -
PT_CURRENCYis scaled. [MS-OXCDATA] defines currency as a 64-bit integer scaled by 10,000.ReadValue ()returns adecimal(4.x returned the rawlong), and the numericReadValueAs* ()methods return the scaled value (4.xReadValueAsInt64 ()returned the raw value). -
Size limits. Values larger than
TnefOptions.MaxPropertyValueLength, or pastTnefOptions.MaxTotalDataBytes, are reported asDataSizeLimitExceededand read as empty. -
No exceptions for malformed data. Problems are reported to the reader's
ComplianceLoggerand the property reader stops returning properties when the rest of the attribute cannot be interpreted.
4.x accumulated a TnefComplianceStatus bit field in loose mode and threw TnefException in strict mode.
5.0 reports each problem as a TnefComplianceIssue to an ITnefComplianceLogger. Each issue carries the
Violation, the StreamOffset, the embedded-message Depth, the AttributeTag and PropertyTag it was
found in, a Severity, a Description and Remarks.
4.x TnefComplianceStatus |
5.0 TnefComplianceViolation |
|---|---|
Compliant |
No issues logged. |
AttributeOverflow |
InvalidAttributeLength or TruncatedStream |
InvalidAttribute |
UnknownAttribute, AttributeLevelMismatch |
InvalidAttributeChecksum |
AttributeChecksumMismatch |
InvalidAttributeLength |
InvalidAttributeLength |
InvalidAttributeLevel |
InvalidAttributeLevel, MessageAttributeAfterAttachment |
InvalidAttributeValue |
InvalidAttributeValue, InvalidValueCount, InvalidNamedPropertyKind |
InvalidDate |
InvalidDate |
InvalidMessageClass |
InvalidMessageClass |
InvalidMessageCodepage |
InvalidMessageCodepage |
InvalidPropertyLength |
InvalidPropertyLength, InvalidPropertyCount |
InvalidRowCount |
InvalidRowCount |
InvalidTnefSignature |
InvalidSignature |
InvalidTnefVersion |
UnsupportedVersion |
NestingTooDeep |
NestingTooDeep |
StreamTruncated |
TruncatedStream |
UnsupportedPropertyType |
UnsupportedPropertyType |
| — | TooManyAttachments, DataSizeLimitExceeded (resource limits from TnefOptions) |
| — | TooManyComplianceIssues (the reader's per-violation cap was reached) |
| 4.x pattern | 5.0 replacement |
|---|---|
TnefComplianceMode.Loose + check reader.ComplianceStatus afterwards |
Set reader.ComplianceLogger to a logger that collects issues, and inspect it afterwards. |
TnefComplianceMode.Strict (throw on first problem) |
Set a logger whose Log throws, e.g. throw new TnefException (issue.Violation, issue.Description). |
reader.ResetComplianceStatus () |
Clear your logger's state. |
status.HasFlag (TnefComplianceStatus.X) |
issues.Any (i => i.Violation == TnefComplianceViolation.X) |
When reading untrusted streams, bound the number of issues with reader.ComplianceOptions.MaxIssuesPerViolation
or have your logger stop recording after a limit of its own. To get a logger into TnefMessage, create the
reader yourself and call TnefMessage.Load (TnefReader) (see Collect compliance issues).
| 4.x | 5.0 |
|---|---|
TnefException (TnefComplianceStatus, string?) |
TnefException (TnefComplianceViolation, string?) |
TnefException (TnefComplianceStatus, string?, Exception?) |
TnefException (TnefComplianceViolation, string?, Exception?) |
Error (TnefComplianceStatus) |
Violation (TnefComplianceViolation) |
MimeKit itself only throws TnefException from TnefMessage.Load[Async] () and
TnefPart.LoadTnefMessage[Async] (), when the stream does not begin with the TNEF signature.
The following types are unchanged apart from additions: TnefAttachFlags, TnefAttachMethod,
TnefAttributeLevel, TnefAttributeTag, TnefNameId, TnefNameIdKind, TnefPropertyId (except
InternetCPID), TnefPropertyTag (except InternetCPID and Puid), TnefPropertyType,
RtfCompressedToRtf and RtfCompressionMode.
One behavioural change applies to RtfCompressedToRtf. In 4.x, a stream whose compression type was
neither LZFu nor MELA was passed through as if it were uncompressed. In 5.0, such a stream is
malformed ([MS-OXRTFCP] 2.1.3.1.1), so the filter produces no output. Check CompressionMode
to detect it. ConvertToMime drops an RTF body like this and reports a
TnefConversionLossKind.InvalidRtfBody loss. When a compressed RTF body has a CRC mismatch, it is
still converted, and an RtfChecksumMismatch loss is reported.
All recipes assume using MimeKit; using MimeKit.Tnef;.
Before (4.x):
var message = tnefPart.ConvertToMessage ();After (5.0):
MimeMessage message;
using (var tnef = tnefPart.LoadTnefMessage ()) {
var result = tnef.ConvertToMime ();
foreach (var loss in result.Losses)
Console.WriteLine ("TNEF conversion loss: {0}", loss);
// Keep the message; do not dispose the result.
message = result.Message;
}Async:
using (var tnef = await tnefPart.LoadTnefMessageAsync (null, cancellationToken)) {
var result = tnef.ConvertToMime (null, cancellationToken);
// ...
}ConvertToMime () has no async twin because all of the content is already in memory.
Before (4.x):
foreach (var attachment in tnefPart.ExtractAttachments ()) {
if (attachment is MimePart part) {
using (var output = File.Create (part.FileName))
part.Content.DecodeTo (output);
}
}After (5.0), without MIME conversion:
using (var tnef = tnefPart.LoadTnefMessage ()) {
foreach (var attachment in tnef.Attachments) {
if (!attachment.HasContent || attachment.IsEmbeddedMessage)
continue;
// FileName comes from an untrusted source; sanitize it before using it as a path.
var fileName = Path.GetFileName (attachment.FileName ?? "attachment.dat");
using (var input = attachment.OpenRead ())
using (var output = File.Create (fileName))
input.CopyTo (output);
}
}Embedded messages are read with attachment.LoadEmbeddedMessage (), which returns another TnefMessage.
Before (4.x), walking attributes:
using (var reader = new TnefReader (stream, 0, TnefComplianceMode.Loose)) {
while (reader.ReadNextAttribute ()) {
if (reader.AttributeLevel != TnefAttributeLevel.Message || reader.AttributeTag != TnefAttributeTag.MapiProperties)
continue;
var prop = reader.TnefPropertyReader;
while (prop.ReadNextProperty ()) {
if (prop.PropertyTag.Id == TnefPropertyId.Subject)
subject = prop.ReadValueAsString ();
}
}
}After (5.0), with the object model:
using (var tnef = TnefMessage.Load (stream)) {
var subject = tnef.Subject;
var sentRepresenting = tnef.Properties.GetString (TnefPropertyTag.SentRepresentingEmailAddressW);
var submitted = tnef.Properties.GetDateTime (TnefPropertyTag.ClientSubmitTime); // UTC
if (tnef.Properties.TryGetValue (TnefNameId.Location, out var location))
Console.WriteLine ("Location: {0}", location.Value);
}TnefPropertySet lookups by TnefPropertyTag match the property id and accept either string type
(String8/Unicode).
After (5.0), with the reader:
using (var reader = new TnefReader (stream)) {
while (reader.Read ()) {
if (reader.Level != TnefAttributeLevel.Message || reader.Tag != TnefAttributeTag.MapiProperties)
continue;
var prop = reader.GetPropertyReader ();
while (prop.ReadNextProperty ()) {
if (prop.Tag.Id == TnefPropertyId.Subject && prop.ValueCount > 0)
subject = prop.ReadValueAsString ();
}
}
}Before (4.x):
while (prop.ReadNextProperty ()) {
if (prop.IsMultiValuedProperty) {
while (prop.ReadNextValue ())
values.Add (prop.ReadValue ());
}
}After (5.0):
while (prop.ReadNextProperty ()) {
if (prop.IsMultiValued && prop.ValueCount > 0) {
do {
values.Add (prop.ReadValue ());
} while (prop.ReadNextValue ());
}
}Or materialize it: var property = prop.ReadProperty (); and then property.TryGetValues<string> (out var strings).
Before (4.x):
var prop = reader.TnefPropertyReader;
while (prop.ReadNextRow ()) {
while (prop.ReadNextProperty ()) {
switch (prop.PropertyTag.Id) {
case TnefPropertyId.DisplayName: name = prop.ReadValueAsString (); break;
case TnefPropertyId.EmailAddress: addr = prop.ReadValueAsString (); break;
}
}
}After (5.0), with the object model:
foreach (var recipient in tnef.Recipients)
Console.WriteLine ("{0}: {1} <{2}> ({3})", recipient.RecipientType, recipient.DisplayName, recipient.EmailAddress, recipient.AddressType);After (5.0), with the reader (the loop shape is unchanged; only the member names differ):
var prop = reader.GetPropertyReader ();
while (prop.ReadNextRow ()) {
while (prop.ReadNextProperty ()) {
switch (prop.Tag.Id) {
case TnefPropertyId.DisplayName: name = prop.ReadValueAsString (); break;
case TnefPropertyId.EmailAddress: addr = prop.ReadValueAsString (); break;
}
}
}Or IReadOnlyList<TnefPropertySet> rows = prop.ReadRowsAsPropertySets ();.
Before (4.x), the bodies were found by walking MapiProperties and decompressing RtfCompressed with
RtfCompressedToRtf. After (5.0):
if (tnef.HtmlBody != null) {
var html = tnef.HtmlBody.GetText ();
} else if (tnef.RtfBody != null) {
using (var rtf = tnef.RtfBody.OpenDecodedRead ()) {
// decompressed RTF bytes
}
} else if (tnef.TextBody != null) {
var text = tnef.TextBody.GetText ();
}Before (4.x):
if (prop.IsEmbeddedMessage) {
using (var embedded = prop.GetEmbeddedMessageReader ()) {
// ...
}
}After (5.0), with the reader (the depth limit is TnefOptions.MaxNestingDepth):
if (prop.IsEmbeddedMessage) {
using (var embedded = prop.OpenEmbeddedMessage ()) {
while (embedded.Read ()) {
// ...
}
}
}After (5.0), with the object model:
foreach (var attachment in tnef.Attachments) {
if (attachment.IsEmbeddedMessage) {
using (var embedded = attachment.LoadEmbeddedMessage ())
Console.WriteLine ("Embedded: {0}", embedded.Subject);
}
}Before (4.x):
using (var reader = new TnefReader (stream, 0, TnefComplianceMode.Loose)) {
while (reader.ReadNextAttribute ()) {
// ...
}
if (reader.ComplianceStatus != TnefComplianceStatus.Compliant)
Console.WriteLine ("TNEF problems: {0}", reader.ComplianceStatus);
}After (5.0):
class TnefIssueCollector : ITnefComplianceLogger
{
public List<TnefComplianceIssue> Issues { get; } = new List<TnefComplianceIssue> ();
public void Log (in TnefComplianceIssue issue)
{
Issues.Add (issue);
}
}
var collector = new TnefIssueCollector ();
using (var reader = new TnefReader (stream, leaveOpen: true) { ComplianceLogger = collector }) {
reader.ComplianceOptions.MaxIssuesPerViolation = 16;
using (var tnef = TnefMessage.Load (reader)) {
// ...
}
}
foreach (var issue in collector.Issues)
Console.WriteLine (issue); // violation, offset, attribute/property and descriptionBefore (4.x):
var reader = new TnefReader (stream, 0, TnefComplianceMode.Strict);After (5.0):
class StrictTnefLogger : ITnefComplianceLogger
{
public void Log (in TnefComplianceIssue issue)
{
throw new TnefException (issue.Violation, issue.Description);
}
}
var reader = new TnefReader (stream) { ComplianceLogger = new StrictTnefLogger () };You can also throw only for some violations, for example only when
issue.Severity >= MimeComplianceSeverity.Major.
4.x had only MaxNestingDepth. 5.0 adds:
var options = new TnefOptions {
DefaultCodepage = 1252,
MaxNestingDepth = 8, // default 32
MaxPropertyValueLength = 16 * 1024 * 1024, // default 32 MB
MaxTotalDataBytes = 64L * 1024 * 1024, // default 64 MB
MaxAttachments = 256 // default 1024
};
using (var tnef = tnefPart.LoadTnefMessage (options)) {
// ...
}4.x could only read TNEF. 5.0 adds TnefWriter (attributes) and TnefPropertyWriter (MAPI properties).
The writer emits the signature, attTnefVersion and attOemCodepage itself, computes every length,
count, checksum and padding, and throws InvalidOperationException when attributes are written out of
the order required by [MS-OXTNEF] (message attributes, then attMsgProps, then for each attachment
attAttachRenderData ... attAttachment).
using (var output = File.Create ("winmail.dat"))
using (var writer = new TnefWriter (output, 1252)) {
writer.WriteAttribute (TnefAttributeTag.MessageClass, "IPM.Note");
using (var properties = writer.OpenPropertyWriter (TnefAttributeTag.MapiProperties)) {
properties.WritePropertyTag (TnefPropertyTag.SubjectW);
properties.WriteValue ("Quarterly report");
properties.WritePropertyTag (TnefPropertyTag.RtfCompressed);
using (var rtf = properties.OpenRtfCompressedStream ())
rtf.Write (rtfBytes, 0, rtfBytes.Length);
// Named properties are assigned ids (0x8000 and up) by the writer.
properties.WritePropertyTag (new TnefNameId (TnefPropertySetGuid.PublicStrings, "Keywords"), TnefPropertyType.Unicode | TnefPropertyType.MultiValued);
properties.WriteValue ("finance");
properties.WriteValue ("q3");
}
writer.WriteAttribute (TnefAttributeTag.AttachRenderData, renderData);
using (var properties = writer.OpenPropertyWriter (TnefAttributeTag.Attachment)) {
properties.WritePropertyTag (TnefPropertyTag.AttachMethod);
properties.WriteValue ((int) TnefAttachMethod.ByValue);
properties.WritePropertyTag (TnefPropertyTag.AttachLongFilenameW);
properties.WriteValue ("report.pdf");
properties.WritePropertyTag (TnefPropertyTag.AttachDataBin);
using (var content = properties.OpenValueStream ())
pdfStream.CopyTo (content);
}
}Use TnefPropertyWriter.OpenEmbeddedMessage () on an Object property (PidTagAttachDataObject) to write an
embedded message, and TnefPropertyWriter.WriteProperty (TnefProperty) to copy a property from a
TnefMessage or TnefPropertySet unchanged. TnefWriter has WriteAttributeAsync and FlushAsync;
property values are buffered in memory, so TnefPropertyWriter is synchronous.
- Replace
TnefPart.ConvertToMessage ()withLoadTnefMessage ()+ConvertToMime (); dispose theTnefMessage; decide who ownsresult.Message; consider loggingresult.Losses. - Replace
TnefPart.ExtractAttachments ()withresult.Message.BodyParts/Attachments, or withTnefMessage.Attachmentswhen MIME conversion is not needed. - Replace
new TnefReader (stream, codepage, mode)withnew TnefReader (stream, new TnefOptions { DefaultCodepage = codepage }). - Rename:
ReadNextAttribute→Read,AttributeLevel→Level,AttributeTag→Tag,AttributeRawValueLength→Length,MessageCodepage→Codepage,AttachmentKey→LegacyKey,TnefPropertyReader(property) →GetPropertyReader (),Close ()→Dispose (). - Replace
ReadAttributeRawValue/GetRawValueReadStream/ReadRawValue/ReadTextValuewithOpenValueStream ()or theReadValueAs* ()methods. - Move legacy attribute reads from
reader.TnefPropertyReader.ReadValueAs* ()toreader.ReadValueAs* (). - Rename:
PropertyTag→Tag,PropertyNameId→Name(now nullable),IsNamedProperty→Name.HasValue,IsMultiValuedProperty→IsMultiValued,GetEmbeddedMessageReader→OpenEmbeddedMessage. - Rewrite every
while (prop.ReadNextValue ())value loop asdo { ... } while (prop.ReadNextValue ()). - Make sure each string, binary or object value is read only once.
- Do not use a
TnefPropertyReaderor value stream after callingRead ()again. - Remove compensation for local-time
PT_SYSTIMEvalues; they are now UTC. - Review
PT_CURRENCYhandling: values are now scaled by 1/10,000 (decimalfromReadValue ()). - Replace
ComplianceMode/ComplianceStatus/ResetComplianceStatuswith anITnefComplianceLoggerand map flags using the violation table. - Replace
TnefException.ErrorwithTnefException.Violation. - Remove
try/catchblocks that existed only to survive malformed TNEF (EndOfStreamException,TnefExceptionfrom reading); keep the one aroundTnefMessage.Load ()for non-TNEF input. - Rename
InternetCPID→InternetCodepage; replaceTnefPropertyTag.PuidwithPuidA/PuidW. - For untrusted input, set
TnefOptionslimits andTnefComplianceOptions.MaxIssuesPerViolation. - Re-run any tests that compared converted MIME output: header order, dates, inline dispositions and the absence of generated headers have changed as described in TnefPart.
- Update references to the
MimeKitLitepackage toMimeKit.Core, and any assembly-qualified type names, binding redirects or trimmer descriptors fromMimeKittoMimeKit.Core. - Add
using MimeKit.Cryptography;whereverMimeMessage.Sign (),Encrypt ()orSignAndEncrypt ()is called. - Register an S/MIME context with
CryptographyContext.Register ()during startup, and callCryptographyModule.Initialize ()if messages might be parsed beforeCryptographyContextis used. - Change visitors that override the
Visit*methods for signed or encrypted entities to derive fromCryptographicMimeVisitor. - Pass
DigestAlgorithm.Sha1explicitly if you depend on SHA-1 signatures; otherwise expect SHA-256. - Move digest-preference code from
CryptographyContext/ICryptographyContexttoOpenPgpContext. - Remove the
DbConnectionparameter fromX509CertificateDatabaseoverrides and useCreateCommand (). - Remove serialization constructors and
GetObjectData ()overrides from subclasses of MimeKit exceptions. - Replace your
IDkimPublicKeyLocatorwith anIDnsResolverthat returnsDnsTxtResponseresults, distinguishing NXDOMAIN from temporary failures. - Change
DkimVerifier.Verify ()callers to checkresult.Status, and removetry/catchblocks that treated exceptions as failed signatures. - Pass the private key to the
DkimSigner,ArcSignerandDkimSignerBaseconstructors. - Rename
ExperimentalMimeParsertoMimeParser; make sureMimeParsersubclasses call the base implementation of anyOn*method they override and don't callReadHeaders (),ReadEntity ()orReadMessage (). - Update
MimeReadersubclasses: the newOnMboxMarkerRead ()signature,OnMultipartBoundaryEnd ()andOnMultipartEndBoundaryEnd (), and no more trimming of the newline before a boundary. - Set
FormatOptions.EnsureNewLinewhen appending messages to an mbox file. - Handle
ArgumentExceptionfrom the msg-id setters andnullinstead of""for empty msg-ids. - Don't rely on leading or trailing whitespace in address names, and remove
MimeEntity.WriteTo ()/WriteToAsync ()overrides. - Use
MailboxAddress.GetAddress (false)/GetDomain (false)where you relied on parsed IDN-encoded domains being decoded, and compareGetAddress (true)where the A-label and Unicode forms should be equal. - Replace
HtmlTokenizerState.AttributeValueQuoted, and stop persistingHtmlTokenizerStatevalues. - Review
HtmlTagCallbackimplementations for<noscript>content, which is now passed to the callback, and for the tokenizer changes. - Replace
HtmlToHtml.FilterHtml,QEncoder,HexEncoderandTnefPropertyTag.Puid. - Port TNEF code using the TNEF porting checklist.
- Update message/entity
Dispose (bool)overrides to perform only local cleanup, tolerate retry after failure, and no longer assume children are usable before the base call. See disposing messages and entities.