diff --git a/Src/Directory.Build.props b/Src/Directory.Build.props
index bceadac..7af3bbf 100644
--- a/Src/Directory.Build.props
+++ b/Src/Directory.Build.props
@@ -8,8 +8,8 @@
Copyright 2011-$(CurrentYear) axuno, MailMergeLib Project maintainers and contributers
https://github.com/axuno/MailMergeLib.git
true
- 5.12.0
- 5.12.0
+ 5.12.2
+ 5.12.2
5.0.0.0
latest
true
diff --git a/Src/MailMergeLib.Tests/MailMergeLib.Tests.csproj b/Src/MailMergeLib.Tests/MailMergeLib.Tests.csproj
index a0821d0..a36c2da 100644
--- a/Src/MailMergeLib.Tests/MailMergeLib.Tests.csproj
+++ b/Src/MailMergeLib.Tests/MailMergeLib.Tests.csproj
@@ -8,7 +8,6 @@
MailMergeLib.Tests
../MailMergeLib/MailMergeLib.snk
true
- true
true
enable
latest
diff --git a/Src/MailMergeLib.Tests/Message_Html.cs b/Src/MailMergeLib.Tests/Message_Html.cs
index b79c3bd..63a12df 100644
--- a/Src/MailMergeLib.Tests/Message_Html.cs
+++ b/Src/MailMergeLib.Tests/Message_Html.cs
@@ -31,10 +31,10 @@ public void MimeMessageSize()
var mimeMessage = mmm.GetMimeMessage(null);
var size = MailMergeLib.Tools.CalcMessageSize(mimeMessage);
- Assert.That(size > 0, Is.True);
+ Assert.That(size, Is.GreaterThan(0));
}
- Assert.That(MailMergeLib.Tools.CalcMessageSize(null) == 0, Is.True);
+ Assert.That(MailMergeLib.Tools.CalcMessageSize(null), Is.EqualTo(0));
}
[Test]
@@ -105,16 +105,24 @@ public void HtmlMailMergeWithInlineAndAtt()
Assert.Multiple(() =>
{
- Assert.That(((MailboxAddress) msg.From.First()).Address == dataItem.SenderAddr, Is.True);
- Assert.That(((MailboxAddress) msg.To.First()).Address == dataItem.MailboxAddr, Is.True);
- Assert.That(((MailboxAddress) msg.To.First()).Name == dataItem.Name, Is.True);
- Assert.That(msg.Headers[HeaderId.Organization] == mmm.Config.Organization, Is.True);
- Assert.That(msg.Priority == mmm.Config.Priority, Is.True);
- Assert.That(msg.Attachments.FirstOrDefault(a => ((MimePart) a).FileName == "Log file from {Date:yyyy-MM-dd}.log".Replace("{Date:yyyy-MM-dd}", dataItem.Date.ToString("yyyy-MM-dd"))) != null, Is.True);
- Assert.That(msg.Subject == mmm.Subject.Replace("{Date:yyyy-MM-dd}", dataItem.Date.ToString("yyyy-MM-dd")), Is.True);
- Assert.That(msg.HtmlBody.Contains(dataItem.Success ? "succeeded" : "failed"), Is.True);
- Assert.That(msg.TextBody.Contains(dataItem.Success ? "succeeded" : "failed"), Is.True);
- Assert.That(msg.BodyParts.Any(bp => bp.ContentDisposition?.Disposition == ContentDisposition.Inline && bp.ContentType.IsMimeType("image", "jpeg")), Is.True);
+ Assert.That(((MailboxAddress) msg.From.First()).Address, Is.EqualTo(dataItem.SenderAddr));
+ Assert.That(((MailboxAddress) msg.To.First()).Address, Is.EqualTo(dataItem.MailboxAddr));
+ Assert.That(((MailboxAddress) msg.To.First()).Name, Is.EqualTo(dataItem.Name));
+ Assert.That(msg.Headers[HeaderId.Organization], Is.EqualTo(mmm.Config.Organization));
+ Assert.That(msg.Priority, Is.EqualTo(mmm.Config.Priority));
+ Assert.That(
+ msg.Attachments.FirstOrDefault(a =>
+ ((MimePart) a).FileName ==
+ "Log file from {Date:yyyy-MM-dd}.log".Replace("{Date:yyyy-MM-dd}",
+ dataItem.Date.ToString("yyyy-MM-dd"))), Is.Not.EqualTo(null));
+ Assert.That(msg.Subject,
+ Is.EqualTo(mmm.Subject.Replace("{Date:yyyy-MM-dd}", dataItem.Date.ToString("yyyy-MM-dd"))));
+ Assert.That(msg.HtmlBody, Does.Contain(dataItem.Success ? "succeeded" : "failed"));
+ Assert.That(msg.TextBody, Does.Contain(dataItem.Success ? "succeeded" : "failed"));
+ Assert.That(
+ msg.BodyParts.Any(bp =>
+ bp.ContentDisposition?.Disposition == ContentDisposition.Inline &&
+ bp.ContentType.IsMimeType("image", "jpeg")), Is.True);
});
MailMergeMessage.DisposeFileStreams(msg);
@@ -134,12 +142,12 @@ public void HtmlStreamAttachments()
mmm.StreamAttachments.Add(new StreamAttachment(stream, streamAttFilename, "text/plain"));
}
- Assert.That(mmm.StreamAttachments.Count == 1, Is.True);
+ Assert.That(mmm.StreamAttachments.Count, Is.EqualTo(1));
mmm.StreamAttachments.Clear();
- Assert.That(mmm.StreamAttachments.Count == 0, Is.True);
+ Assert.That(mmm.StreamAttachments.Count, Is.EqualTo(0));
mmm.StreamAttachments = streamAttachments;
- Assert.That(mmm.StreamAttachments.Count == 2, Is.True);
+ Assert.That(mmm.StreamAttachments.Count, Is.EqualTo(2));
}
@@ -199,8 +207,8 @@ public void HtmlMailMergeWithMoreEqualInlineAtt()
Assert.Multiple(() =>
{
- Assert.That(new HtmlParser().ParseDocument((string) msg.HtmlBody).All.Count(m => m is IHtmlImageElement) == 3, Is.True);
- Assert.That(msg.BodyParts.Count(bp => bp.ContentDisposition?.Disposition == ContentDisposition.Inline && bp.ContentType.IsMimeType("image", "jpeg")) == 1, Is.True);
+ Assert.That(new HtmlParser().ParseDocument((string) msg.HtmlBody).All.Count(m => m is IHtmlImageElement), Is.EqualTo(3));
+ Assert.That(msg.BodyParts.Count(bp => bp.ContentDisposition?.Disposition == ContentDisposition.Inline && bp.ContentType.IsMimeType("image", "jpeg")), Is.EqualTo(1));
});
MailMergeMessage.DisposeFileStreams(msg);
@@ -341,4 +349,42 @@ public void SearchAndReplaceFilename(string text, string expected)
var result = mmm.SearchAndReplaceVarsInFilename(text, dataItem);
Assert.That(result, Is.EqualTo(expected));
}
+
+ [Test]
+ public void SimpleHtmlContent()
+ {
+ using var mmm = new MailMergeMessage("subject", "plain text", "
{Name}{Value:0.00}");
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.To, "john@specimen.com"));
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.From, "no-reply@specimen.com"));
+ var dataItem = new { Name = "John", Value = 2 };
+ var msg = mmm.GetMimeMessage(dataItem);
+ Assert.That(msg.HtmlBody, Does.Contain(dataItem.Name));
+ }
+
+ [Test]
+ public void HtmlBodyBuilder()
+ {
+ using var mmm = new MailMergeMessage("subject", "plain text", "{Name}{Value:0.00}");
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.To, "john@specimen.com"));
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.From, "no-reply@specimen.com"));
+ var dataItem = new { Name = "John", Value = 2 };
+ var bb = new HtmlBodyBuilder(mmm, dataItem);
+
+ Assert.That(bb.DocHtml, Does.Contain(dataItem.Name));
+ }
+
+ [TestCase("John", 0, "John: Nothing")]
+ [TestCase("John", 2, "John: Double")]
+ [TestCase("John", 3, "John: More")]
+ public void ConditionalHtmlContent(string name, int value, string expected)
+ {
+ // Note: The ConditionalFormatter makes use of characters <, >, =, &, ? and :
+ // which must not be encoded to <, >, & etc.
+ using var mmm = new MailMergeMessage("subject", "plain text", "{Name}: {Value:cond:<1?Nothing|=2?Double|More}");
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.To, "john@specimen.com"));
+ mmm.MailMergeAddresses.Add(new MailMergeAddress(MailAddressType.From, "no-reply@specimen.com"));
+ var dataItem = new { Name = name, Value = value };
+ var msg = mmm.GetMimeMessage(dataItem);
+ Assert.That(msg.HtmlBody, Does.Contain(expected));
+ }
}
diff --git a/Src/MailMergeLib/BodyBuilderBase.cs b/Src/MailMergeLib/BodyBuilderBase.cs
index d753e64..3f83928 100644
--- a/Src/MailMergeLib/BodyBuilderBase.cs
+++ b/Src/MailMergeLib/BodyBuilderBase.cs
@@ -25,7 +25,7 @@ protected BodyBuilderBase()
public ContentEncoding TextTransferEncoding { get; set; }
///
- /// Gets the ready made body part for a mail message.
+ /// Gets the ready-made body part for a mail message.
///
public abstract MimeEntity GetBodyPart();
-}
\ No newline at end of file
+}
diff --git a/Src/MailMergeLib/HtmlBodyBuilder.cs b/Src/MailMergeLib/HtmlBodyBuilder.cs
index 0bc98a9..d8f9443 100644
--- a/Src/MailMergeLib/HtmlBodyBuilder.cs
+++ b/Src/MailMergeLib/HtmlBodyBuilder.cs
@@ -38,10 +38,15 @@ public HtmlBodyBuilder(MailMergeMessage mailMergeMessage, object? dataItem)
_dataItem = dataItem;
BinaryTransferEncoding = mailMergeMessage.Config.BinaryTransferEncoding;
+ // We need to replace placeholders in the HTML text before parsing as HTML
+ // because SmartFormat extensions may use characters like '<', '&' or '>' in placeholders.
+ // These characters would be encoded to HTML entities by AngleSharp
+ // and thus not be interpreted correctly in SmartFormat.
+ var htmlFormatted = mailMergeMessage.SearchAndReplaceVars(mailMergeMessage.HtmlText, dataItem);
// Create a new parser front-end (can be re-used)
var parser = new HtmlParser();
- //Just get the DOM representation
- _htmlDocument = parser.ParseDocument(mailMergeMessage.HtmlText);
+ // Just get the DOM representation
+ _htmlDocument = parser.ParseDocument(htmlFormatted);
}
///
@@ -55,7 +60,7 @@ public HtmlBodyBuilder(MailMergeMessage mailMergeMessage, object? dataItem)
public string DocHtml => _htmlDocument.ToHtml();
///
- /// Gets the ready made body part for a mail message either
+ /// Gets the ready-made body part for a mail message either
/// - as TextPart, if there are no inline attachments
/// - as MultipartRelated with a TextPart and one or more MimeParts of type inline attachments
///
@@ -89,17 +94,12 @@ public override MimeEntity GetBodyPart()
ReplaceImgSrcByCid();
- // replace placeholders only in the HTML Body, because e.g.
- // in the header there may be CSS definitions with curly brace which collide with SmartFormat {placeholders}
- if (_htmlDocument.Body != null)
- _htmlDocument.Body.InnerHtml =
- _mailMergeMessage.SearchAndReplaceVars(_htmlDocument.Body.InnerHtml, _dataItem) ?? string.Empty;
-
var htmlTextPart = new TextPart("html")
{
ContentTransferEncoding = TextTransferEncoding
};
- htmlTextPart.SetText(CharacterEncoding, DocHtml); // MimeKit.ContentType.Charset is set using CharacterEncoding
+ // MimeKit.ContentType.Charset is set using CharacterEncoding
+ htmlTextPart.SetText(CharacterEncoding, _htmlDocument.ToHtml());
htmlTextPart.ContentId = MimeUtils.GenerateMessageId();
if (!InlineAtt.Any())
@@ -214,30 +214,25 @@ private void ReplaceImgSrcByCid()
var filename = _mailMergeMessage.SearchAndReplaceVarsInFilename(srcUri.LocalPath, _dataItem);
try
{
- if (filename != null)
+ if (!fileList.TryGetValue(filename, out var cidForExistingFile))
+ {
+ var fileInfo = new FileInfo(filename);
+ var contentType = MimeTypes.GetMimeType(filename);
+ var cid = MimeUtils.GenerateMessageId();
+ InlineAtt.Add(new FileAttachment(fileInfo.FullName,
+ MakeCid(string.Empty, cid, fileInfo.Extension), contentType));
+ srcAttr.Value = MakeCid("cid:", cid, fileInfo.Extension);
+ fileList.Add(fileInfo.FullName, cid);
+ }
+ else
{
- if (!fileList.ContainsKey(filename))
- {
- var fileInfo = new FileInfo(filename);
- var contentType = MimeTypes.GetMimeType(filename);
- var cid = MimeUtils.GenerateMessageId();
- InlineAtt.Add(new FileAttachment(fileInfo.FullName,
- MakeCid(string.Empty, cid, fileInfo.Extension), contentType));
- srcAttr.Value = MakeCid("cid:", cid, fileInfo.Extension);
- fileList.Add(fileInfo.FullName, cid);
- }
- else
- {
- var cidForExistingFile = fileList[filename];
- var fileInfo = new FileInfo(filename);
- srcAttr.Value = MakeCid("cid:", cidForExistingFile, fileInfo.Extension);
- }
+ var fileInfo = new FileInfo(filename);
+ srcAttr.Value = MakeCid("cid:", cidForExistingFile, fileInfo.Extension);
}
}
catch
{
- BadInlineFiles.Add(filename ?? "(null)");
- continue;
+ BadInlineFiles.Add(filename);
}
}
}
@@ -246,11 +241,11 @@ private void ReplaceImgSrcByCid()
/// Makes the content identifier (CID)
///
/// i.e. normally "cid:"
- /// unique indentifier
+ /// unique identifier
/// file extension, so that content type can be easily identified. May be string.empty
///
private static string MakeCid(string prefix, string contentId, string fileExt)
{
return prefix + contentId + fileExt.Replace('.', '-');
}
-}
\ No newline at end of file
+}
diff --git a/Src/MailMergeLib/MailMergeMessage.cs b/Src/MailMergeLib/MailMergeMessage.cs
index 58a8ea4..cd27602 100644
--- a/Src/MailMergeLib/MailMergeMessage.cs
+++ b/Src/MailMergeLib/MailMergeMessage.cs
@@ -340,6 +340,10 @@ private MailSmartFormatter GetConfiguredMailSmartFormatter(bool invokedFromConst
var currentSmartSettings = invokedFromConstructor
? new SmartSettings()
: SmartFormatter.Settings;
+ // Parse content of