Sending Email with Attachments with Optimizely Forms in 2025
Most articles on adding attachments to Optimizely Forms emails are from around 2019 and don’t cover the most recent versions. This post walks through my 2025 implementation and the lessons learned. Your mileage may vary.
The Problem
Optimizely Forms’ built-in SendEmailAfterSubmissionActor sends clean, simple emails after a form is submitted. When you use the File Upload Form Element Block, the actor includes links in the email to where files are stored in the CMS.
That’s fine for internal users with CMS access. But if your recipients are external — customers, partners, vendors — those links won’t work for them.
What I needed was:
-
Direct access to the MimeMessage object.
-
Ability to attach uploaded files directly to the outgoing email.
Why Subclassing Won’t Work
Optimizely’s SendEmailAfterSubmissionActor only allows limited subclassing.
Many of the key operations are private, not protected, and MimeMessage is never handed back for customization.
That means no way to simply “hook in” and add attachments — which is why I opted to:
-
Decompile the original actor.
-
Copy the logic wholesale.
-
Insert my changes where needed.
The Approach
The original actor:
-
Sends plain text or HTML emails (TextPart only).
-
Has no concept of multipart MIME or file attachments.
The modified actor:
-
Builds a Multipart("mixed") message.
-
Adds the original body as a TextPart.
-
Calls a new helper method AddFileAttachments(multipart), to append uploaded files.
-
Resolves uploaded files from CMS blobs, determines their content type, and attaches them to the outgoing email.
Attachments Support
Attachment handling includes:
-
Detecting file upload fields from the form submission (FriendlyNameInfo and field naming patterns like file, upload, attachment).
-
Resolving blob references via IBlobFactory and IUrlResolver.
-
Determining content type based on file extension.
-
Embedding attachments as MimePart instances with base64 encoding.
Dependency Additions
To make this work, I added:
-
IFormRepository – retrieve form field metadata.
-
IBlobFactory & IUrlResolver – fetch binary data for uploaded files.
-
A DetermineContentType helper – ensures attachments have correct MIME types.
Body Construction Changes
Original
Used:
message.Body = new TextPart(subtype) { Text = this.RewriteUrls(content) };
Modified
Wraps the body in a Multipart("mixed") so it can contain both the body and attachments:
Key New Methods
Attachment Processing
protected virtual void AddFileAttachments(Multipart multipart)
{
var friendlyNameInfos = _formRepository.Service.GetFriendlyNameInfos(this.FormIdentity);
foreach (var submissionDataItem in this.SubmissionData.Data)
{
var friendlyNameInfo = friendlyNameInfos
.FirstOrDefault(f => string.Equals(f.ElementId, submissionDataItem.Key, StringComparison.OrdinalIgnoreCase));
if (friendlyNameInfo != null && IsFileUploadField(friendlyNameInfo, submissionDataItem.Value))
{
var fileUploads = submissionDataItem.Value.ToString().Split("|");
foreach (var fileUpload in fileUploads)
{
AttachFileFromBlob(multipart, fileUpload, friendlyNameInfo.FriendlyName);
}
}
}
}
Note: If the form allows multiple uploads, they come through as a single string separated by |.
File Attachment Helper
protected virtual void AttachFileFromBlob(Multipart multipart, string blobIdentifier, string friendlyName)
{
Blob blob = null;
IContent contentData = _urlResolver.Service.Route(new UrlBuilder(blobIdentifier));
if (contentData is MediaData media)
{
blob = _blobFactory.Service.GetBlob(media.BinaryData.ID);
}
if (blob != null)
{
var memoryStream = new MemoryStream();
blob.OpenRead().CopyTo(memoryStream);
memoryStream.Position = 0;
var fileName = blobIdentifier.Split(new[] { "#@" }, StringSplitOptions.None).Last();
var contentType = DetermineContentType(blob, fileName);
multipart.Add(new MimePart(contentType)
{
Content = new MimeContent(memoryStream, ContentEncoding.Default),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = fileName
});
}
}
The original filename is appended to the identifier after #@ — this extracts it for the email attachment.
Determine Content Type Helper
protected virtual string DetermineContentType(Blob blob, string fileName)
{
// Try to get content type from blob
// This might be available in blob metadata depending on your implementation
// Fall back to determining from file extension
string extension = Path.GetExtension(fileName)?.ToLowerInvariant();
return extension switch
{
".pdf" => "application/pdf",
".doc" => "application/msword",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls" => "application/vnd.ms-excel",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".txt" => "text/plain",
".csv" => "text/csv",
".zip" => "application/zip",
".xml" => "application/xml",
".json" => "application/json",
_ => "application/octet-stream"
};
}
SendMessage() Changes
Original:
MimeMessage message = new MimeMessage();
message.Subject = str1;
string subtype = this._sendMessageInHTMLFormat ? "html" : "plain";
message.Body = (MimeEntity) new TextPart(subtype, new object[1]
{
(object) this.RewriteUrls(content)
});
Modified:
MimeMessage message = new MimeMessage();
message.Subject = str1;
// Create multipart message to support attachments
var multipart = new Multipart("mixed");
string subtype = this._sendMessageInHTMLFormat ? "html" : "plain";
multipart.Add(new TextPart(subtype, new object[1]
{
(object) this.RewriteUrls(content)
}));
// Add file attachments
this.AddFileAttachments(multipart);
message.Body = multipart;
Wiring It Up
To replace the out-of-the-box actor, you’ll likely need to do it in your Dependency Injection setup.
In my case, I didn’t replace it globally — we already had a subclassed actor for applying different HTML templates, so I had that subclass inherit from the new replacement instead of the original.
Takeaways
-
Original actor: Simple, reliable, zero attachment support.
-
Modified actor: Same foundation, plus multipart MIME and attachment handling.
If you need to extend built-in actors in Optimizely Forms beyond what the public API offers, sometimes you’ll have to work at the code level.
Caution: After any update to the Forms package, check if SendEmailAfterSubmissionActor has changed — you may need to re-sync your version or take advantage of new core features.
Have any thoughts or opinions? Let me know in the comments!
Comments