Take the community feedback survey now.

KennyG
Aug 11, 2025
  402
(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 MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025

Raising and Retrieving Custom Events in Application Insights

Following on from Minesh's insight on how to extend Extending Application Insights in an Optimizely PaaS CMS Solution , I'd like to share another w...

Mark Stott | Sep 25, 2025

Extending Application Insights in an Optimizely PaaS CMS Solution

As part of a recent Optimizely DXP project, I needed richer telemetry from the Content Delivery API than the default Application Insights integrati...

Minesh Shah (Netcel) | Sep 24, 2025

Master Language Switcher for Optimizely CMS 12

Managing multiple languages in Optimizely CMS 12 can sometimes be challenging — especially when you need to promote a new language branch to be the...

Adnan Zameer | Sep 24, 2025 |