Take the community feedback survey now.

KennyG
Aug 11, 2025
  283
(0 votes)

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:

  1. Decompile the original actor.

  2. Copy the logic wholesale.

  3. 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:

var multipart = new Multipart("mixed");
multipart.Add(new TextPart(subtype) { Text = this.RewriteUrls(content) });
this.AddFileAttachments(multipart);
message.Body = multipart;

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!

Aug 11, 2025

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely Developer - We Hacked the Future: Netcel's Opal Hackathon Adventure

Ever wondered what happens when you mix  AI ,  creativity , and a dash of competitive spirit? Welcome to the  Opal Hackathon 2025 —where we rolled ...

Graham Carr | Aug 31, 2025

Simple Feature Flags In Optimizely CMS

The Problem I was working with a CMS 11 client who wanted to introduce two new third party content sources. These would be synchronized into new...

Mark Stott | Aug 31, 2025

SQL addon for Optimizely CMS 12 updated with new features

First released in 2021, the SQL Studio addon for Optimizely CMS lets you query your database directly from the edit UI. The latest update adds...

Tomas Hensrud Gulla | Aug 29, 2025 |

🎓 New Certification Icons Are Now Live on World!

If you’ve passed an Optimizely Academy certification, you’ll now see something new beside your name on World —  fresh certification icons are live...

Satata Satez | Aug 29, 2025