Enabling Document Previews in SugarCRM

Author: Yuri Gee

Date: 22 Oct 2025

10 minute read time

This article demonstrates how to enable attachment previews in the SugarCRM Email module for formats such as Word documents (.docx), images (.jpeg, .png), and text files (.txt, .csv), with the potential to extend support to PDFs, Excel (.xlsx), PowerPoint (.pptx), and ZIP and other file types.

There are three main strategies for implementing previews, each with its own advantages and trade-offs:

  • using trusted client-side JavaScript libraries that support commercial use,
  • deploying the same or similar libraries on the backend to stream file chunks and avoid full file downloads, or
  • integrating external/internal preview services like Cloud Drive or AI summarization services for enhanced rendering.

This example focuses on a basic .docx preview using the open-source Mammoth.js library, along with support for image and text formats, and browser-native PDF rendering where applicable. The content is displayed through a dedicated Preview HTML field in the Emails module which must be enabled via Studio (or potentially any related field or dashlet that can be added). The preview is rendered inside a sandboxed iframe with no script access or interaction with the parent page, loading content either as sanitized HTML or via Blob URLs for image files.

Preview examples for common file types

When an attachment is single-clicked, its preview will be displayed; double-clicking will initiate the download.

Preview of a large DOCX file with 83 pages and 14MB size.

Example of a large document file

Image and DOCX preview example

Image and DOCX preview example

CSV text file preview.

 CSV text file preview

Implementation details

The code example is located within the following files.

custom/modules/Emails/clients/base/fields/email-attachments/email-attachments.js

({
    extendsFrom: 'BaseEmailsEmailAttachmentsField',
    //extendsFrom: 'BaseEmailAttachmentsField',

    //custom/modules/Emails/clients/base/fields/email-attachments/email-attachments.js 
    // 1. Add to Content-Security-Policy to allow: frame-src blob:
    // 2. Upload or reference trusted libraries for DOCX processing within the custom/include/javascript directory. 
    //    Ensure the libraries are compatible with your codebase and licensed for use in commercial applications.
    // 3. Enable the Email module in Studio and add a new HTML field html_preview2_c beside the attachments field in the record view
    //    -> create modules/Emails/metadata/studio.php and run Quick Repair and Rebuild

    // Notes:
    // - This code does not support file chunking; large files (over 20MB) may crash the browser and should be handled server-side
    // - Clicking multiple attachments can trigger repeated fetches
    // - Despite browser caching may still have long previews to reload; monitor memory usage carefully
    // - Additional libraries can be integrated for PDF, ZIP, XLSX, PPTX, and other common formats
    // - Make sure to reinforce sanitizing and validation 

    // Tip: Currently, a single click triggers the preview; double-click initiates the download instead. 
    // If you want both preview and download functionality on each click, make sure to configure accordingly 
    // events: { 'click [data-click-action=preview]': '_previewFile', 'dblclick [data-action=download]': '_downloadFile'},
    events: { 
        'click [data-click-action=preview]': '_handleSingleClick',
        'dblclick [data-action=custom-download]': '_handleDoubleClick'
    },

    clickTimeout: null,
    
    _handleSingleClick: function(event) {
        clearTimeout(this.clickTimeout);
        const url = this.$(event.currentTarget).data('url');
        if (this.disposed === true || _.isEmpty(url)) return;

        this.clickTimeout = setTimeout(() => this._previewFile(url), 300);
    },

    _handleDoubleClick: function(event) {
        clearTimeout(this.clickTimeout);
        this._downloadFile(event);
    },

    initialize: function(options) {
        this._super('initialize', [options]);
    },

    _render: function() {
        this._super('_render');
    },

    _downloadFile: function(event) {
        this._super('_downloadFile', [event]);
    },

   _previewFile: function(url) {
        if (this.disposed === true || _.isEmpty(url)) return;

        const match = url.match(/Notes\/([^\/]+)\//);
        const noteId = match ? match[1] : null;
        if (!noteId) {
            console.error('Previewer: Unable to extract Note ID from URL.');
            return;
        }

        const targetSpan = document.querySelector('span.htmlareafield[name="preview2_c"]');
        if (!targetSpan) {
            console.error('Previewer: Target span not found.');
            return;
        }

        const injectIframe = (src, isHTML = false, compact = true) => {
            const iframe = document.createElement('iframe');
            iframe.width = '100%';
            iframe.height = compact ? '50' : '400';
            iframe.style.border = 'none';
            iframe.setAttribute('sandbox', '');
            iframe.setAttribute('referrerpolicy', 'no-referrer');

            const sanitizedHTML = isHTML ? DOMPurify.sanitize(src?.trim() || '<html><body></body></html>'/*, {
                    ALLOWED_TAGS: ['html', 'body', 'div', 'p', 'span', 'img', 'a', 'ul', 'ol', 'li', 'br', 'strong', 'em'],
                    ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'width', 'height', 'style'],
                    RETURN_TRUSTED_TYPE: false
                }*/) : null;

            iframe.style.display = isHTML && (!sanitizedHTML || sanitizedHTML.replace(/<[^>]+>/g, '').trim() === '') ? 'none' : 'block';

            const blobUrl = isHTML ? URL.createObjectURL(new Blob([sanitizedHTML], { type: 'text/html' })) : null;
            iframe.src = isHTML ? blobUrl : src;

            const cleanupBlob = () => {
                if (blobUrl) URL.revokeObjectURL(blobUrl);
                else if (!isHTML && typeof src === 'string' && src.startsWith('blob:')) URL.revokeObjectURL(src);
            };
            iframe.onload = cleanupBlob;
            iframe.onerror = cleanupBlob;

            if (targetSpan) { 
                if (targetSpan.firstChild?.tagName === 'IFRAME') targetSpan.firstChild.src = 'about:blank';
                targetSpan.innerHTML = '';
                targetSpan.appendChild(iframe);
            }
        };

        const loadMammoth = (callback) => {
            if (window.mammoth) {
                callback();
                return;
            }
            const script = document.createElement('script');
            script.src = 'custom/include/javascript/mammoth/mammoth.browser.min.js';
            script.onload = callback;
            script.onerror = () => console.error('Previewer: Failed to load Mammoth.js');
            document.head.appendChild(script);
        };

        const fetchDocxFile = (fileUrl, callback) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', fileUrl, true);
            xhr.responseType = 'arraybuffer';
            xhr.onload = () => {
                if (xhr.status === 200) callback(xhr.response);
                else console.error('Previewer: Failed to fetch DOCX file. Status:', xhr.status);
            };
            xhr.onerror = () => console.error('Previewer: Network error while fetching DOCX file.');
            xhr.send();
        };

        const handlePreview = (noteBean) => {
            const filename = noteBean.get('filename') || '';
            const mimeType = noteBean.get('file_mime_type') || '';
            const extension = filename.split('.').pop().toLowerCase();
            const fileSize = noteBean.get('file_size') || 0;

            if (fileSize > 20 * 1024 * 1024) { // 20MB limit
                console.warn('Previewer: File too large to preview safely.');
                injectIframe('<html><body><p>File too large to preview.</p></body></html>', true);
                return;
            }

            const fileUrl = App.api.buildFileURL({module: 'Notes', id: noteBean.get('id'), field: 'filename' });

            const isDocx = mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || extension === 'docx';
            const isImage = mimeType.startsWith('image/') || ['jpg', 'jpeg', 'png'].includes(extension);
            const isPdf = mimeType === 'application/pdf' || extension === 'pdf';

            injectIframe('<html><body><p>Loading preview...</p></body></html>', true);
            if (isDocx) {
                loadMammoth(() => {
                    fetchDocxFile(fileUrl, (arrayBuffer) => {
                        mammoth.convertToHtml({ arrayBuffer })
                            .then(result => {
                                injectIframe(result.value, true, false);
                                result.value = null;
                                arrayBuffer = null; 
                            })
                            .catch(err => console.error('Previewer: Mammoth conversion error:', err));
                    });
                });
                return;
            } 

            fetch(fileUrl).then(response => {
                    const contentType = response.headers.get('Content-Type') || '';
                    return response.blob().then(blob => ({ blob, contentType }));
                })
                .then(({ blob, contentType }) => {
                    const blobType = blob.type;

                    if ((contentType.startsWith('image/') && blobType.startsWith('image/') && isImage) ||
                        (contentType === 'application/pdf' && blobType === 'application/pdf' && isPdf)) {
                            const blobUrl = URL.createObjectURL(blob);
                            //NOTE: add file validation as needed to ensure the content is appropriate and secure before proceeding.
                            //Note: Chrome/Edge may block PDF blobs or data URIs from rendering in sandboxed iframes. 
                            // Consider using a dedicated PDF library for reliable display.
                            injectIframe(blobUrl, false, false);
                            return;
                    }

                    blob.text().then(text => {
                         const escape = s => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
                         //NOTE: Ensure the data is properly sanitized and validated within injectIframe as necessary.
                         injectIframe(`<html><body><pre>${escape(text)}</pre></body></html>`, true, false);
                    }).catch(err => {
                        console.warn('Previewer: Failed to read blob as text.', err);
                        injectIframe('<html><body><p>Preview not available for this file type.</p></body></html>', true);
                    });
                })
                .catch(err => { console.error('Previewer: Failed to fetch or process file.', err); });
        };

        const noteBean = App.data.createBean('Notes', { id: noteId });
        noteBean.fetch({
            success: () => handlePreview(noteBean),
            error: err => console.error('Previewer: Failed to fetch Note record.', err)
        });
    }
});
 

custom/modules/Emails/clients/base/fields/email-attachments/detail.hbs

{{!--custom/modules/Emails/clients/base/fields/email-attachments/detail.hbs
--}}
{{#each value}}
    <div class="square-pill">
        {{#if file_url}}
            <a href="javascript:void(0);" data-url="{{file_url}}" data-action="custom-download" data-click-action="preview">
                <span class="ellipsis-value ellipsis_inline" title="{{name}}">{{name}}</span>
                <span class="ellipsis-extra">({{file_size}})</span>
            </a>
        {{else}}
            <div rel="tooltip" title="{{str 'ERR_NO_SUCH_FILE' ../module}}">
                <span class="ellipsis-value ellipsis_inline" title="{{name}}">{{name}}</span>
                <span class="ellipsis-extra">({{file_size}})</span>
            </div>
        {{/if}}
    </div>
{{/each}}
 

Additionally, make sure the Content Security Policy in Admin settings permits blob: sources for iframes.

CSP iframe configuration to support blobs

In Admin, navigate to Studio → Emails → Fields, then create an HTML field named preview2_c and add it to the Record View layout.

Preview HTML field in Emails module

The Mammoth library should be placed at custom/include/javascript/mammoth/mammoth.browser.min.js. To ensure the file is preserved accurately, it’s recommended to include it either as part of a package or upload the raw file directly without modification.

Additional considerations

When implementing attachment previews, there are several important technical and usability factors to keep in mind. First, ensure your Content Security Policy (CSP) includes frame-src blob: to allow rendering of Blob-based previews within sandboxed iframes.

Next, upload or reference trusted JavaScript libraries for DOCX processing (e.g. Mammoth.js) within the custom/include/javascript directory. Ensure this and any other library used is reliable, compatible with your codebase, and appropriately licensed for commercial use if applicable.

In the Studio, enable the Email module and add a new HTML field (e.g., html_preview2_c) adjacent to the attachments field in the record view. To register this field, create or update modules/Emails/metadata/studio.php and run a Quick Repair and Rebuild.

Please note that this implementation does not currently support file chunking large files (the code sets a 20MB limit) and repeated previews of such files on the same page may cause browser crashes and should be better handled server-side. Additionally, clicking multiple attachments in quick succession may trigger repeated fetch requests, causing multiple files to load into browser memory. Even with browser caching, preview load times can remain high, so it's important to monitor memory usage closely. You can also integrate additional libraries to support formats like PDF, ZIP, XLSX, and PPTX.

Ensure strict sanitization and validation of all content to maintain security. Currently, HTML is sanitized using DOMPurify, unsupported file types are displayed as plain text, and images embedded in blobs are rendered via the browser’s native parser.

Tip: By default, a single click triggers the preview, while a double-click initiates the download. If you want to change this, be sure to configure that events behavior explicitly.

Keep the feedback coming — we love hearing from you!

Parents Reply Children
  • Hi Abhijeet, you're very welcome! There's still an opportunity to leverage the browser's built-in PDF viewer — it just requires rendering the PDF using an <iframe> or <embed> tag and ensuring that verified PDF links from Sugar servers are permitted. Alternatively, libraries like PDF.js (https://mozilla.github.io/pdf.js/) and others can be explored. I believe these libraries should support rendering PDFs as blobs within a sandboxed iframe, offering more control and flexibility.