The CreatorCon Call for Content is officially open! Get started here.

Automating Attachment Download – Automated Email Delivery and Instance Cleanup

yashwanth_p
Tera Contributor

Email-Based Multi-Record Attachment Downloader

 

Picture this: You’re juggling multiple incidents or requests, and each one has attachments you need. Clicking through every record and downloading files one by one? Frustrating, time-consuming, and amajor time drain. 

 

What if one click could handle it all? No messy downloads. No time drain. Just clean, automated delivery straight to your inbox.

 

This solution does exactly that. From the List View, select your records, click “Download Attachments,” and the system:

  • Creates a ZIP for each record,
  • Combines them into a master ZIP,
  • Emails the link to you, ready to download.

All in one seamless flow, with duplicate-safe handling and automatic cleanup — so nothing clutters your instance.

 

 

Why It Makes Work Easier

 

The motivation was simple:

  • Free users from repetitive, manual attachment downloads.
  • Keep all processing on the server, ensuring the UI stays fast.
  • Automatically handle duplicates and edge cases.
  • Provide a solution that’s reusable, maintainable, and scalable.

In short, it’s about making attachments effortless while keeping everything neat and professional.

 

 

Core Components

 

ComponentTypePurpose
Download AttachmentsList View UI ActionInitiates the processor for selected records
exportAttachmentsViaEmailProcessorCreates record-specific ZIPs, builds master ZIP, triggers notifications
schedule.attachment.download
Event
Triggers the Script Action to Queue the downloads for midnight if no records are selected
Trigger ProcessorScript ActionOptional automation via RESTMessage or GlideHTTPRequest
master.zip.notificationEventTriggers the Zip Attachment notification when the master ZIP is ready
Zip AttachmentNotificationSends the download link via email
Delete Master ZIP AttachmentsScheduled JobCleans up standalone master ZIPs in the Attachments -  sys_attachment table.
Script IncludeOptional Global Script IncludeCan call the processor logic directly instead of using RESTMessage

 

⚠️ Notes:

 

  • The processor script used here functions only within the Global Scope and is not compatible with Scoped Applications.
  • A new processor record can be added through a background script or by temporarily adjusting ACLs

  • Implementing duplicate file handling is essential to prevent runtime errors during ZIP creation

 

 

UI Action — List View Button

function list_attachment_send() {
    var selected = g_list.getChecked();
	
    if (!selected) {
        var downloadAll = confirm("Do you want to download all record files?");
        if (!downloadAll) return;
    }

    var url = 'exportAttachmentsViaEmail.do?sysparm_table=' + g_list.getTableName() +
              '&sysparm_sys_id_list=' + selected +
              '&sysparm_from_uia=true&sysparm_user=' + g_user.userID;
	
    var newWin = top.window.open(url, '_blank');
    setTimeout(function() { if (newWin) newWin.close(); }, 500);
    alert('An email will be sent containing the requested attachments.');
}

 

  • Confirms user intent when no records are selected.
  • Sends record IDs and table name to the processor.
  • Alerts users that attachments will be emailed.

 

 

Processor — exportAttachmentsViaEmail

(function process(g_request, g_response, g_processor) {
    var table = g_request.getParameter('sysparm_table');
    var idList = g_request.getParameter('sysparm_sys_id_list');

    /* */
    var fromUIaction = g_request.getParameter('sysparm_from_uia');
    var user = g_request.getParameter('sysparm_user');
    if (fromUIaction == 'true' && !idList) {
        // gs.eventQueue('schedule.attachment.download', null, user, table);

        var dt = new GlideDateTime();
        dt.addSeconds(60);
        gs.eventQueueScheduled('schedule.attachment.download', '', user, table, dt);
        gs.log("After event trigger");
        return;
    }
    gs.log("triggered by script action");
    /* */

    var sysIds = [];

    var sa = new GlideSysAttachment();
    var baosMaster = new Packages.java.io.ByteArrayOutputStream();
    var mainOut = new Packages.java.util.zip.ZipOutputStream(baosMaster);

    // --- Step 1: Get sys_ids with attachments and avoid duplicate filenames ---
    var attGR = new GlideRecord('sys_attachment');
    attGR.addQuery('table_name', table);
    if (idList) {
        attGR.addQuery('table_sys_id', 'IN', idList);
    }
    attGR.query();

    var recordMeta = {};
    var attMap = {};
    while (attGR.next()) {
        var recId = attGR.getValue('table_sys_id');
        recId = recId.toString().trim();

        // Track unique sysIds
        if (sysIds.indexOf(recId) === -1) sysIds.push(recId);

        if (!attMap[recId]) attMap[recId] = [];

        var attBytes = sa.getBytes(attGR);
        if (attBytes && attBytes.length > 0) {
            var fileName = attGR.getValue('file_name');
            var originalName = fileName;
            var counter = 1;

            // Avoid duplicate filenames for the same record
            while (attMap[recId].some(function(a) {
                    return a.file_name === fileName;
                })) {
                counter++;
                var extIndex = originalName.lastIndexOf(".");
                if (extIndex > -1) {
                    fileName = originalName.substring(0, extIndex) + "_" + counter + originalName.substring(extIndex);
                } else {
                    fileName = originalName + "_" + counter;
                }
            }


            attMap[recId].push({
                file_name: fileName,
                bytes: attBytes
            });
        }
    }

    // if (sysIds.length === 0) {
    //     gs.info("No attachments found for the given table/IDs. Exiting.");
    //     return;
    // }

    gs.log("YP_After Step 1");

    // --- Step 2: Preload record metadata (number, job_title, location) ---
    var rec = new GlideRecord(table);
    rec.addQuery('sys_id', 'IN', sysIds);
    rec.query();
    while (rec.next()) {
        var sysid = rec.getUniqueValue();
        recordMeta[sysid] = {
            number: rec.getValue('number') || sysid,
            jobTitle: rec.caller_id.title ? rec.caller_id.title.getDisplayValue().replace(/\s+/g, "_") : '',
            location: rec.caller_id.location ? rec.caller_id.location.getDisplayValue().replace(/\s+/g, "_") : ''
        };
    }

    gs.log("YP_After Step 2");

    // --- Step 3: Create record-specific ZIPs and add to master ---
    for (var i = 0; i < sysIds.length; i++) {
        var sysid = sysIds[i];
        if (!attMap[sysid] || !recordMeta[sysid]) continue;

        var meta = recordMeta[sysid];
        var zipFileName = meta.number +
            (meta.jobTitle ? '_' + meta.jobTitle : '') +
            (meta.location ? '_' + meta.location : '') + '.zip';

        var baos = new Packages.java.io.ByteArrayOutputStream();
        var zipFileOut = new Packages.java.util.zip.ZipOutputStream(baos);

        var files = attMap[sysid];

        gs.log("Processing sysid: " + sysid + ", files: " + files.length);
        for (var j = 0; j < files.length; j++) {
            var f = files[j];
            gs.log("File: " + f.file_name + ", bytes length: " + f.bytes.length);
        }

        for (var j = 0; j < files.length; j++) {
            var f = files[j];
            zipFileOut.putNextEntry(new Packages.java.util.zip.ZipEntry(f.file_name));
            zipFileOut.write(f.bytes, 0, f.bytes.length);
            zipFileOut.closeEntry();
        }
        zipFileOut.close();

        var recordBytes = baos.toByteArray();
        if (recordBytes.length > 0) {
            mainOut.putNextEntry(new Packages.java.util.zip.ZipEntry(zipFileName));
            mainOut.write(recordBytes, 0, recordBytes.length);
            mainOut.closeEntry();
        }
    }

    mainOut.close(); // finish writing master ZIP
    var masterBytes = baosMaster.toByteArray();

    gs.log("YP_After Step 3");

    // --- Step 4: Only create master ZIP if it has entries ---
    var hasEntries = false;
    if (masterBytes) {
        var bais = new Packages.java.io.ByteArrayInputStream(masterBytes);
        var zis = new Packages.java.util.zip.ZipInputStream(bais);
        while (zis.getNextEntry() !== null) {
            hasEntries = true; // at least one entry exists
            break; // no need to continue looping
        }
    }

    if (hasEntries) {
        var mainZipName = 'Attachments.zip';
        var newAttachmentGR = new GlideRecord('sys_attachment');
        newAttachmentGR.initialize();
        newAttachmentGR.file_name = mainZipName;
        newAttachmentGR.content_type = 'application/zip';
        var attachmentSysId = sa.write(newAttachmentGR, mainZipName, 'application/zip', masterBytes);
        gs.eventQueue('master.zip.notification', null, user, attachmentSysId);
    } else {
        gs.info("No attachments found to include in master ZIP.");
		gs.eventQueue('master.zip.notification', null, gs.getUserID(), '');
    }

})(g_request, g_response, g_processor);

 

 

Email Delivery — Automating Attachment ZIPs and Cleanup

 

This section handles:

  1. Sending email notifications once the master ZIP is ready.

  2. Scheduling background processing for large or unselected datasets.

  3. Cleaning up unlinked master ZIPs to avoid storage bloat.

 

Script Action

 

Method 1: RESTMessage

(function() {
    var base = gs.getProperty('glide.servlet.uri');  
    var selected = "";
    var url = base + 'exportAttachmentsViaEmail.do?sysparm_table=' + event.parm2 +
              '&sysparm_sys_id_list=' + selected + '&sysparm_from_uia=false&sysparm_user=' + event.parm1;

    var rm = new sn_ws.RESTMessageV2();
    rm.setHttpMethod("GET");
    rm.setEndpoint(url);
    rm.setBasicAuth("username", "password");

    try {
        var response = rm.execute();
        gs.info("Processor called: " + url + " Status: " + response.getStatusCode());
    } catch (ex) {
        gs.error("Error calling processor: " + ex);
    }
})();

 

Method 2: GlideHTTPRequest

 (function() {
     var base = gs.getProperty('glide.servlet.uri');
     var url = base + 'exportAttachmentsViaEmail.do?sysparm_table=incident&sysparm_sys_id_list=' + "" +
               '&sysparm_from_uia=false&sysparm_user=' + gs.getUserID();
     var request = new GlideHTTPRequest(url);
     request.setBasicAuth("username","password");
     var response = request.get();
     gs.info("Processor called: " + url + " | Status: " + response.getStatusCode());
})();

 

Method 3 : Script Include

Other than these two methods, the Processor Script can be stored in a Script Include, and that can be triggered in Script Action.

 

Scheduled Job — Cleanup Old ZIPs

var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.addQuery('file_name','Attachments.zip');
attachmentGR.addQuery('table_sys_id','');
attachmentGR.query();
attachmentGR.deleteMultiple();

 

How Everything Works

 

  • Gathers attachments for selected records.
  • Creates individual ZIPs per record, handles duplicates.
  • Combines them into a master ZIP.
  • Sends the master ZIP link via email.
  • Can schedule processing for midnight if no records are selected.
  • Cleans up unlinked master ZIPs automatically.

 

Performance Thoughts

 

  • Runs asynchronously — no UI slowdown.

  • Handles multiple records efficiently.

  • Easily scalable with scheduled or chunk-based processing.

  • Clean execution with minimal system footprint.

 

Final Outcome

 

  • One button click.
  • A master ZIP containing all requested attachments.
  • Email delivery straight to the user.
  • Clean, automated, and efficient — no leftover files.

 

 

Hope this helps!
Regards,
Yashwanth Pepakayala.

1 REPLY 1

yashwanth_p
Tera Contributor

Curious about other attachment management options? Don’t miss: