I ended up figuring out a solution.
What I did was to add a custom token to the first message that goes to the original email. That custom token was something like "forcedEmailAddress", which had a value of the original email address.
Then I used a pipeline to intercept FillEmail (which I was already doing for another purpose - allowing HTML in my tokens). In that pipeline, I'd check for the presence of my custom token, and if it was there, I'd change the value of message.To.
Here's the .config for the pipeline:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<SendEmail>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.FillEmail, Sitecore.EmailCampaign.Cm">
<patch:attribute name="type">MyNamespace.CustomFillEmail, MyNamespace.Foundation.Exm</patch:attribute>
</processor>
</SendEmail>
</pipelines>
</sitecore>
</configuration>
Here's my pipeline code:
using System.Web;
using Sitecore.EDS.Core.Dispatch;
using Sitecore.EmailCampaign.Cm.Pipelines.SendEmail;
using Sitecore.ExM.Framework.Diagnostics;
using Sitecore.Modules.EmailCampaign.Core.Crypto;
using Sitecore.Modules.EmailCampaign.Messages;
namespace MyNamespace.Foundation.Exm.Pipelines
{
public class CustomFillEmail
{
private readonly IStringCipher _cipher;
private readonly ILogger _logger;
public CustomFillEmail(IStringCipher cipher, ILogger logger)
{
_cipher = cipher;
_logger = logger;
}
public void Process(SendMessageArgs args)
{
if (args.EcmMessage == null)
return;
GetMailFiller(args)?.FillEmail();
}
private MailFiller GetMailFiller(SendMessageArgs args)
{
var ecmMessage1 = (HtmlMailBase) args.EcmMessage;
if (ecmMessage1 != null)
// here we return custom mail filler
return new CustomHtmlMailFiller(ecmMessage1, args, _logger, _cipher);
var ecmMessage2 = (MailMessageItem) args.EcmMessage;
return ecmMessage2 != null ? new MailMessageFiller(ecmMessage2, args, _cipher) : null;
}
}
public class CustomHtmlMailFiller : HtmlMailFiller
{
private readonly HtmlMailBase _htmlMailBase;
public CustomHtmlMailFiller(HtmlMailBase message, SendMessageArgs args, ILogger logger, IStringCipher cipher) :
base(message, args, logger, cipher)
{
if (message.CustomPersonTokens.TryGetValue(Constants.ForcedEmailRecipientAddress, out var forcedEmailRecipientAddress))
message.To = forcedEmailRecipientAddress.ToString();
_htmlMailBase = message;
}
protected override void FillBody()
{
var body = _htmlMailBase.Body;
Email.ContentType = MessageContentType.Html;
Email.HtmlBody = HttpUtility.HtmlDecode(_htmlMailBase.ReplaceTokens(body));
Email.PlainTextBody = _htmlMailBase.ReplaceTokens(_htmlMailBase.AlternateText);
}
}
}
My message sender class:
using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Analytics;
using Sitecore.Analytics.Tracking;
using Sitecore.Configuration;
using Sitecore.EmailCampaign.Cd.Services;
using Sitecore.EmailCampaign.Model.Messaging;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using Sitecore.XConnect.Client.Configuration;
using Sitecore.XConnect.Collection.Model;
using MyNamespace.Portal.Domain.Model.Member;
namespace MyNamespace.Foundation.Exm
{
public class MessageSender
{
private readonly IClientApiService _clientApiService;
public MessageSender(IClientApiService clientApiService)
{
_clientApiService = clientApiService;
}
public void Send(Member member, Guid messageId, IDictionary<string, object> dictionaryOfTokens, bool forceEmailAddressFromMember = false, string language = "en")
{
if (Tracker.IsActive)
{
var trackerContact = Tracker.Current.Contact;
if (trackerContact?.Identifiers != null)
{
using (var client = SitecoreXConnectClientConfiguration.GetClient())
{
var contactIdentifier = trackerContact.Identifiers.FirstOrDefault(id => id.Source == Constants.ContactIdentifierSource);
if (contactIdentifier == null)
{
return;
}
var reference = new IdentifiedContactReference(contactIdentifier.Source, contactIdentifier.Identifier);
var facets = new[] { PersonalInformation.DefaultFacetKey,EmailAddressList.DefaultFacetKey };
var expand = new ContactExpandOptions(facets);
var contact = client.Get(reference, expand);
var personal = contact.Personal();
if (contact.Personal() == null) personal = new PersonalInformation();
personal.Birthdate = member.DateOfBirth;
personal.FirstName = member.GivenNames;
personal.LastName = member.Surname;
personal.Title = member.Title;
personal.Gender = member.Gender.ToString();
client.SetPersonal(contact, personal);
var emails = contact.Emails();
if (contact.Emails() == null) emails = new EmailAddressList(new EmailAddress(member.EmailAddress, true), Constants.EmailPreferredKey);
emails.PreferredEmail.SmtpAddress = member.EmailAddress;
client.SetEmails(contact, emails);
client.Submit();
if (Factory.CreateObject("tracking/contactManager", true) is ContactManager manager)
{
manager.RemoveFromSession(Tracker.Current.Contact.ContactId); // Use tracker ID, not xConnect ID
Tracker.Current.Session.Contact = manager.LoadContact(Tracker.Current.Contact.ContactId);
}
if (forceEmailAddressFromMember)
{
if(dictionaryOfTokens == null) dictionaryOfTokens = new Dictionary<string, object>();
dictionaryOfTokens.Add(Constants.ForcedEmailRecipientAddress, member.EmailAddress);
}
var messageInfo = new AutomatedMessage
{
MessageId = messageId,
ContactIdentifier = contact.Identifiers.FirstOrDefault(id => id.Source == Constants.ContactIdentifierSource),
CustomTokens = dictionaryOfTokens,
TargetLanguage = language
};
_clientApiService.SendAutomatedMessage(messageInfo);
}
}
}
}
}
}
And I call the message sender like this:
var messageSender = new MessageSender(_clientApiService);
if (isEmailDirty)
{
messageSender.Send(member,Guid.Parse(_siteSettings.UpdatedEmailAddressMessage.ToString()),null,true);
}