Automating Attachment Download – Automated Email Delivery and Instance Cleanup
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
10 hours ago
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
Component | Type | Purpose |
Download Attachments | List View UI Action | Initiates the processor for selected records |
exportAttachmentsViaEmail | Processor | Creates 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 Processor | Script Action | Optional automation via RESTMessage or GlideHTTPRequest |
master.zip.notification | Event | Triggers the Zip Attachment notification when the master ZIP is ready |
Zip Attachment | Notification | Sends the download link via email |
Delete Master ZIP Attachments | Scheduled Job | Cleans up standalone master ZIPs in the Attachments - sys_attachment table. |
Script Include | Optional Global Script Include | Can 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:
Sending email notifications once the master ZIP is ready.
Scheduling background processing for large or unselected datasets.
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
9 hours ago
Curious about other attachment management options? Don’t miss:
Form View → Step-by-step download of attachments from individual records for better visibility: Form View Immediate Download
List View (Immediate ZIP) → Bundle attachments from multiple records into a single ZIP for faster access: List View Immediate Download