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