Implement IMimeMessageData (#1326)

* Implement IMimeMessageData

* 1

* Update src/WireMock.Net.MimePart/Util/MimeKitUtils.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* v1

* v2

* e

* ?

* fix

* if (Array.TrueForAll(_funcs, func => func(value).IsPerfect()))

* Update src/WireMock.Net.Shared/Util/IMimeKitUtils.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.MimePart/Models/MimeEntityDataWrapper.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Models.Mime.IMimeMessageData? BodyAsMimeMessage { get; }

* Update src/WireMock.Net.MimePart/Util/MimeKitUtils.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.MimePart/Models/MimePartDataWrapper.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.MimePart/Models/MimeMessageDataWrapper.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.Shared/Util/IMimeKitUtils.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stef Heyenrath
2025-07-12 09:54:18 +02:00
committed by GitHub
parent 6c61f87ef3
commit b0076b4e81
18 changed files with 851 additions and 49 deletions

View File

@@ -1,8 +1,8 @@
// Copyright © WireMock.Net
using System;
using MimeKit;
using WireMock.Matchers.Helpers;
using WireMock.Models.Mime;
using WireMock.Util;
namespace WireMock.Matchers;
@@ -12,7 +12,7 @@ namespace WireMock.Matchers;
/// </summary>
public class MimePartMatcher : IMimePartMatcher
{
private readonly Func<MimePart, MatchResult>[] _funcs;
private readonly Func<IMimePartData, MatchResult>[] _funcs;
/// <inheritdoc />
public string Name => nameof(MimePartMatcher);
@@ -52,21 +52,21 @@ public class MimePartMatcher : IMimePartMatcher
_funcs =
[
mp => ContentTypeMatcher?.IsMatch(GetContentTypeAsString(mp.ContentType)) ?? MatchScores.Perfect,
mp => ContentDispositionMatcher?.IsMatch(mp.ContentDisposition.ToString().Replace("Content-Disposition: ", string.Empty)) ?? MatchScores.Perfect,
mp => ContentTransferEncodingMatcher?.IsMatch(mp.ContentTransferEncoding.ToString().ToLowerInvariant()) ?? MatchScores.Perfect,
mp => ContentDispositionMatcher?.IsMatch(mp.ContentDisposition?.ToString()?.Replace("Content-Disposition: ", string.Empty)) ?? MatchScores.Perfect,
mp => ContentTransferEncodingMatcher?.IsMatch(mp.ContentTransferEncoding.ToLowerInvariant()) ?? MatchScores.Perfect,
MatchOnContent
];
}
/// <inheritdoc />
public MatchResult IsMatch(object value)
public MatchResult IsMatch(IMimePartData value)
{
var score = MatchScores.Mismatch;
Exception? exception = null;
try
{
if (value is MimePart mimePart && Array.TrueForAll(_funcs, func => func(mimePart).IsPerfect()))
if (Array.TrueForAll(_funcs, func => func(value).IsPerfect()))
{
score = MatchScores.Perfect;
}
@@ -85,7 +85,7 @@ public class MimePartMatcher : IMimePartMatcher
return "NotImplemented";
}
private MatchResult MatchOnContent(MimePart mimePart)
private MatchResult MatchOnContent(IMimePartData mimePart)
{
if (ContentMatcher == null)
{
@@ -94,10 +94,10 @@ public class MimePartMatcher : IMimePartMatcher
var bodyParserSettings = new BodyParserSettings
{
Stream = mimePart.Content.Open(),
Stream = mimePart.Open(),
ContentType = GetContentTypeAsString(mimePart.ContentType),
DeserializeJson = true,
ContentEncoding = null, // mimePart.ContentType.CharsetEncoding.ToString(),
ContentEncoding = null, // mimePart.ContentType?.CharsetEncoding.ToString(),
DecompressGZipAndDeflate = true
};
@@ -105,8 +105,8 @@ public class MimePartMatcher : IMimePartMatcher
return BodyDataMatchScoreCalculator.CalculateMatchScore(bodyData, ContentMatcher);
}
private static string? GetContentTypeAsString(ContentType? contentType)
private static string? GetContentTypeAsString(IContentTypeData? contentType)
{
return contentType?.ToString().Replace("Content-Type: ", string.Empty);
return contentType?.ToString()?.Replace("Content-Type: ", string.Empty);
}
}

View File

@@ -0,0 +1,63 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Linq;
using MimeKit;
using Stef.Validation;
using WireMock.Models.Mime;
namespace WireMock.Models;
/// <summary>
/// A wrapper class that implements the IContentDispositionData interface
/// by wrapping a ContentDisposition object.
/// </summary>
/// <remarks>
/// This class provides a simplified, read-only view of a ContentDisposition.
/// </remarks>
public class ContentDispositionDataWrapper : IContentDispositionData
{
private readonly ContentDisposition _contentDisposition;
/// <summary>
/// Initializes a new instance of the <see cref="ContentDispositionDataWrapper"/> class.
/// </summary>
/// <param name="contentDisposition">The ContentDisposition to wrap.</param>
public ContentDispositionDataWrapper(ContentDisposition contentDisposition)
{
_contentDisposition = Guard.NotNull(contentDisposition);
Parameters = _contentDisposition.Parameters.Select(p => p.ToString()).ToList();
}
/// <inheritdoc/>
public string Disposition => _contentDisposition.Disposition;
/// <inheritdoc/>
public bool IsAttachment => _contentDisposition.IsAttachment;
/// <inheritdoc/>
public IList<string> Parameters { get; private set; }
/// <inheritdoc/>
public string FileName => _contentDisposition.FileName;
/// <inheritdoc/>
public DateTimeOffset? CreationDate => _contentDisposition.CreationDate;
/// <inheritdoc/>
public DateTimeOffset? ModificationDate => _contentDisposition.ModificationDate;
/// <inheritdoc/>
public DateTimeOffset? ReadDate => _contentDisposition.ReadDate;
/// <inheritdoc/>
public long? Size => _contentDisposition.Size;
/// <inheritdoc/>
public override string ToString()
{
return _contentDisposition.ToString();
}
}

View File

@@ -0,0 +1,65 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MimeKit;
using Stef.Validation;
using WireMock.Models.Mime;
namespace WireMock.Models;
/// <summary>
/// A wrapper class that implements the <see cref="IContentTypeData"/> interface by wrapping a <see cref="ContentType"/> object.
/// </summary>
/// <remarks>
/// This class provides a simplified, read-only view of a <see cref="ContentType"/>.
/// </remarks>
public class ContentTypeDataWrapper : IContentTypeData
{
private readonly ContentType _contentType;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeDataWrapper"/> class.
/// </summary>
/// <param name="contentType">The ContentType to wrap.</param>
public ContentTypeDataWrapper(ContentType contentType)
{
_contentType = Guard.NotNull(contentType);
Parameters = _contentType.Parameters.Select(p => p.ToString()).ToList();
}
/// <inheritdoc/>
public string MediaType => _contentType.MediaType;
/// <inheritdoc/>
public string MediaSubtype => _contentType.MediaSubtype;
/// <inheritdoc/>
public IList<string> Parameters { get; private set; }
/// <inheritdoc/>
public string Boundary => _contentType.Boundary;
/// <inheritdoc/>
public string Charset => _contentType.Charset;
/// <inheritdoc/>
public Encoding CharsetEncoding => _contentType.CharsetEncoding;
/// <inheritdoc/>
public string Format => _contentType.Format;
/// <inheritdoc/>
public string MimeType => _contentType.MimeType;
/// <inheritdoc/>
public string Name => _contentType.Name;
/// <inheritdoc/>
public override string ToString()
{
return _contentType.ToString();
}
}

View File

@@ -0,0 +1,61 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Linq;
using MimeKit;
using Stef.Validation;
using WireMock.Models.Mime;
namespace WireMock.Models;
/// <summary>
/// A wrapper class that implements the <see cref="IMimeEntityData" /> interface by wrapping an <see cref="IMimeEntity" /> interface.
/// </summary>
/// <remarks>
/// This class provides a simplified, read-only view of an <see cref="IMimeEntity"/>.
/// </remarks>
public class MimeEntityDataWrapper : IMimeEntityData
{
private readonly IMimeEntity _entity;
/// <summary>
/// Initializes a new instance of the <see cref="MimeEntityDataWrapper"/> class.
/// </summary>
/// <param name="entity">The MIME entity to wrap.</param>
public MimeEntityDataWrapper(IMimeEntity entity)
{
_entity = Guard.NotNull(entity);
ContentDisposition = _entity.ContentDisposition != null ? new ContentDispositionDataWrapper(_entity.ContentDisposition) : null;
ContentType = _entity.ContentType != null ? new ContentTypeDataWrapper(_entity.ContentType) : null;
Headers = _entity.Headers.Select(h => h.ToString()).ToList();
}
/// <inheritdoc/>
public IList<string> Headers { get; private set; }
/// <inheritdoc/>
public IContentDispositionData? ContentDisposition { get; private set; }
/// <inheritdoc/>
public IContentTypeData? ContentType { get; private set; }
/// <inheritdoc/>
public Uri ContentBase => _entity.ContentBase;
/// <inheritdoc/>
public Uri ContentLocation => _entity.ContentLocation;
/// <inheritdoc/>
public string ContentId => _entity.ContentId;
/// <inheritdoc/>
public bool IsAttachment => _entity.IsAttachment;
/// <inheritdoc/>
public override string ToString()
{
return _entity.ToString()!;
}
}

View File

@@ -0,0 +1,140 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Linq;
using MimeKit;
using Stef.Validation;
using WireMock.Models.Mime;
namespace WireMock.Models;
/// <summary>
/// A wrapper class that implements the <see cref="IMimeMessageData" /> interface by wrapping an <see cref="IMimeMessage" /> interface.
/// </summary>
/// <remarks>
/// This class provides a simplified, read-only view of an <see cref="IMimeMessage"/>.
/// </remarks>
internal class MimeMessageDataWrapper : IMimeMessageData
{
private readonly IMimeMessage _message;
/// <summary>
/// Initializes a new instance of the <see cref="MimeMessageDataWrapper"/> class.
/// </summary>
/// <param name="message">The MIME message to wrap.</param>
public MimeMessageDataWrapper(IMimeMessage message)
{
_message = Guard.NotNull(message);
Bcc = _message.Bcc.Select(h => h.ToString()).ToList();
Cc = _message.Cc.Select(h => h.ToString()).ToList();
From = _message.From.Select(h => h.ToString()).ToList();
Headers = _message.Headers.Select(h => h.ToString()).ToList();
References = _message.References.ToList();
ReplyTo = _message.ReplyTo.Select(h => h.ToString()).ToList();
ResentBcc = _message.ResentBcc.Select(h => h.ToString()).ToList();
ResentCc = _message.ResentCc.Select(h => h.ToString()).ToList();
ResentFrom = _message.ResentFrom.Select(h => h.ToString()).ToList();
ResentReplyTo = _message.ResentReplyTo.Select(h => h.ToString()).ToList();
ResentTo = _message.ResentTo.Select(h => h.ToString()).ToList();
To = _message.To.Select(h => h.ToString()).ToList();
Body = new MimeEntityDataWrapper(_message.Body);
BodyParts = _message.BodyParts.OfType<MimePart>().Select(mp => new MimePartDataWrapper(mp)).ToList<IMimePartData>();
Attachments = _message.Attachments.Select(me => new MimeEntityDataWrapper(me)).ToList<IMimeEntityData>();
}
/// <inheritdoc/>
public IList<string> Headers { get; private set; }
/// <inheritdoc/>
public int Importance => (int)_message.Importance;
/// <inheritdoc/>
public int Priority => (int)_message.Priority;
/// <inheritdoc/>
public int XPriority => (int)_message.XPriority;
/// <inheritdoc/>
public string Sender => _message.Sender.Address;
/// <inheritdoc/>
public string ResentSender => _message.ResentSender.ToString();
/// <inheritdoc/>
public IList<string> From { get; private set; }
/// <inheritdoc/>
public IList<string> ResentFrom { get; private set; }
/// <inheritdoc/>
public IList<string> ReplyTo { get; private set; }
/// <inheritdoc/>
public IList<string> ResentReplyTo { get; private set; }
/// <inheritdoc/>
public IList<string> To { get; private set; }
/// <inheritdoc/>
public IList<string> ResentTo { get; private set; }
/// <inheritdoc/>
public IList<string> Cc { get; private set; }
/// <inheritdoc/>
public IList<string> ResentCc { get; private set; }
/// <inheritdoc/>
public IList<string> Bcc { get; private set; }
/// <inheritdoc/>
public IList<string> ResentBcc { get; private set; }
/// <inheritdoc/>
public string Subject => _message.Subject;
/// <inheritdoc/>
public DateTimeOffset Date => _message.Date;
/// <inheritdoc/>
public DateTimeOffset ResentDate => _message.ResentDate;
/// <inheritdoc/>
public IList<string> References { get; private set; }
/// <inheritdoc/>
public string InReplyTo => _message.InReplyTo;
/// <inheritdoc/>
public string MessageId => _message.MessageId;
/// <inheritdoc/>
public string ResentMessageId => _message.ResentMessageId;
/// <inheritdoc/>
public Version MimeVersion => _message.MimeVersion;
/// <inheritdoc/>
public IMimeEntityData Body { get; private set; }
/// <inheritdoc/>
public string TextBody => _message.TextBody;
/// <inheritdoc/>
public string HtmlBody => _message.HtmlBody;
/// <inheritdoc/>
public IList<IMimePartData> BodyParts { get; private set; }
/// <inheritdoc/>
public IList<IMimeEntityData> Attachments { get; private set; }
/// <inheritdoc/>
public override string ToString()
{
return _message.ToString();
}
}

View File

@@ -0,0 +1,64 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.IO;
using MimeKit;
using Stef.Validation;
using WireMock.Models.Mime;
namespace WireMock.Models;
/// <summary>
/// A wrapper class that implements the <see cref="IMimePartData" /> interface by wrapping an <see cref="IMimePart"/> interface.
/// </summary>
/// <remarks>
/// This class provides a simplified, read-only view of an <see cref="IMimePart"/>.
/// </remarks>
public class MimePartDataWrapper : MimeEntityDataWrapper, IMimePartData
{
private readonly IMimePart _part;
/// <summary>
/// Initializes a new instance of the <see cref="MimePartDataWrapper"/> class.
/// </summary>
/// <param name="part">The MIME part to wrap.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="part"/> is <see langword="null"/>.
/// </exception>
public MimePartDataWrapper(IMimePart part) : base(part)
{
_part = Guard.NotNull(part);
}
/// <inheritdoc/>
public string ContentDescription => _part.ContentDescription;
/// <inheritdoc/>
public int? ContentDuration => _part.ContentDuration;
/// <inheritdoc/>
public string ContentMd5 => _part.ContentMd5;
/// <inheritdoc/>
public string ContentTransferEncoding => _part.ContentTransferEncoding.ToString();
/// <inheritdoc/>
public string FileName => _part.FileName;
/// <inheritdoc/>
public IDictionary<string, object?> Content => new Dictionary<string, object?>()
{
{ nameof(MimePart.Content.Encoding), _part.Content.Encoding },
{ nameof(MimePart.Content.NewLineFormat), _part.Content.NewLineFormat },
{ nameof(MimePart.Content.Stream), _part.Content.Stream }
};
/// <inheritdoc/>
public Stream Open() => _part.Content.Open();
/// <inheritdoc/>
public override string ToString()
{
return _part.ToString()!;
}
}

View File

@@ -9,6 +9,8 @@ using System.Text;
using MimeKit;
using Stef.Validation;
using WireMock.Http;
using WireMock.Models;
using WireMock.Models.Mime;
using WireMock.Types;
namespace WireMock.Util;
@@ -16,13 +18,13 @@ namespace WireMock.Util;
internal class MimeKitUtils : IMimeKitUtils
{
/// <inheritdoc />
public object LoadFromStream(Stream stream)
public IMimeMessageData LoadFromStream(Stream stream)
{
return MimeMessage.Load(Guard.NotNull(stream));
return new MimeMessageDataWrapper(MimeMessage.Load(Guard.NotNull(stream)));
}
/// <inheritdoc />
public bool TryGetMimeMessage(IRequestMessage requestMessage, [NotNullWhen(true)] out object? mimeMessage)
public bool TryGetMimeMessage(IRequestMessage requestMessage, [NotNullWhen(true)] out IMimeMessageData? mimeMessageData)
{
Guard.NotNull(requestMessage);
@@ -44,26 +46,14 @@ internal class MimeKitUtils : IMimeKitUtils
var fixedBytes = FixBytes(bytes, contentTypeHeader[0]);
mimeMessage = LoadFromStream(new MemoryStream(fixedBytes));
mimeMessageData = LoadFromStream(new MemoryStream(fixedBytes));
return true;
}
mimeMessage = null;
mimeMessageData = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<object> GetBodyParts(object mimeMessage)
{
if (mimeMessage is not MimeMessage mm)
{
throw new ArgumentException($"The mimeMessage must be of type {nameof(MimeMessage)}", nameof(mimeMessage));
}
return mm.BodyParts
.OfType<MimePart>()
.ToArray();
}
private static bool StartsWithMultiPart(WireMockList<string> contentTypeHeader)
{