EXM 3.4 removes href from link tags and src from img tags in some cases.
This happens through the use of Sitecore.Modules.EmailCampaign.Core.Links.LinksManager, which calls ChangeLink based on a parameter function (which is what will change the actual link). However, if this function returns null, the entire matched link (including href=" / src=") will be matched with the original link itself - thus removing the important attribute part of the link tag.
The functions returns null if the pipeline is aborted, but this is wrong, it should have returned the link itself instead. This for instance happens for anchor links. This bug also affects the auto embedded image opened image.
These bugs are fixed in EXM 3.5, so upgrading will fix it.
If that's not an option, we fixed it by replacing the mail type in order to overwrite this unwanted behaviour - we only use ABTestMessage, but it's inherited from WebPageMail and HtmlMailBase, so you can replace any of these you need. What it looks like for us now is this:
ABTestMessage:
public class ABTestMessage : Sitecore.Modules.EmailCampaign.Messages.ABTestMessage
{
private CoreFactory _coreFactory;
protected ABTestMessage(Item item) : this(item, CoreFactory.Instance)
{
}
internal ABTestMessage(Item item, CoreFactory coreFactory) : base(item)
{
this._coreFactory = coreFactory;
}
//needed to instantiate correct type
public new static ABTestMessage FromItem(Item item)
{
if (item == null || !ABTestMessage.IsCorrectMessageItem(item))
{
return null;
}
return new ABTestMessage(item);
}
//needed to clone to the correct type
public override object Clone()
{
ABTestMessage aBTestMessage = new ABTestMessage(base.InnerItem);
this.CloneFields(aBTestMessage);
return aBTestMessage;
}
//edited with the only purpose of the inline delegate returning link instead of null if the pipeline was aborted
public override void CollectRelativeFiles(bool preview = false)
{
if (this.ManagerRoot.Settings.EmbedImages)
{
base.CollectRelativeFiles(preview);
return;
}
try
{
this._relativeFiles = new List<FileInMemory>();
if (this.Body != null)
{
DateTime utcNow = DateTime.UtcNow;
LinksManager linksManager = new LinksManager(this.Body, LinkType.Src | LinkType.Css);
this.Body = linksManager.Replace(delegate (string link)
{
GenerateLinkPipelineArgs generateLinkPipelineArgs = new GenerateLinkPipelineArgs(link, this, preview, this.ManagerRoot.Settings.WebsiteSiteConfigurationName);
this.PipelineHelper.RunPipeline("modifyImageLink", generateLinkPipelineArgs);
if (!generateLinkPipelineArgs.Aborted)
{
return generateLinkPipelineArgs.GeneratedUrl;
}
return link;
});
Util.TraceTimeDiff("Modify 'src' links", utcNow);
}
}
catch (Exception ex)
{
this.Warnings.Add(ex.Message);
Logger.Instance.LogError(ex);
}
}
//edited with the only purpose of the inline delegate returning link instead of null if the pipeline was aborted
protected override string CorrectHtml(string html, bool preview)
{
HtmlHelper htmlHelper = new HtmlHelper(html);
htmlHelper.CleanHtml();
DateTime utcNow = DateTime.UtcNow;
htmlHelper.InsertStyleSheets();
Util.TraceTimeDiff("Insert style sheets", utcNow);
utcNow = DateTime.UtcNow;
LinksManager linksManager = new LinksManager(htmlHelper.Html, LinkType.Href);
html = linksManager.Replace(delegate (string link)
{
GenerateLinkPipelineArgs generateLinkPipelineArgs = new GenerateLinkPipelineArgs(link, this, preview, ManagerRoot.Settings.WebsiteSiteConfigurationName);
PipelineHelper.RunPipeline("modifyHyperlink", generateLinkPipelineArgs);
if (!generateLinkPipelineArgs.Aborted)
{
return generateLinkPipelineArgs.GeneratedUrl;
}
return link;
});
Util.TraceTimeDiff("Modify 'href' links", utcNow);
utcNow = DateTime.UtcNow;
html = HtmlHelper.EncodeSrc(html);
Util.TraceTimeDiff("Encode 'src' links", utcNow);
return html;
}
//needed by the overriden CollectRelativeFiles - we just take it from base via reflection
private List<FileInMemory> _relativeFiles
{
get
{
return (List<FileInMemory>) typeof(HtmlMailBase).GetField("_relativeFiles", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
}
set
{
typeof(HtmlMailBase).GetField("_relativeFiles", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(this, value);
}
}
}
Then you need to overwrite the TypeResolver to use your new implementation:
public class TypeResolver : Sitecore.Modules.EmailCampaign.Core.TypeResolver
{
public override MessageItem GetCorrectMessageObject(Item item)
{
if (SMS.IsCorrectMessageItem(item))
{
return SMS.FromItemEx(item);
}
if (ABTestMessage.IsCorrectMessageItem(item))
{
return ABTestMessage.FromItem(item);
}
return base.GetCorrectMessageObject(item);
}
}
And lastly you need to configure the use of the resolver in a .config file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<TypeResolver type="Sitecore.Modules.EmailCampaign.Core.TypeResolver,Sitecore.EmailCampaign" singleInstance="true">
<patch:attribute name="type">YOURNAMESPACE.TypeResolver, YOURASSEMBLY</patch:attribute>
</TypeResolver>
</sitecore>
</configuration>