// Gmail · Drive · Apps Script

Save Gmail attachments to a Drive folder in Gmail.

A Google Apps Script that saves Gmail attachments to a specific Drive folder, with filters to skip inline images, signature logos, and calendar invites that getAttachments() quietly bundles in.

I need to automatically pull file attachments out of Gmail and store them in a Drive folder without manually saving each one.

The script

copy · paste · trigger
saveAttachments.gs
Apps Script
// saveAttachments.gs — run on a time trigger or manually
// Saves qualifying Gmail attachments to a named Drive folder.
function saveAttachmentsToDrive() {
  var folderName = 'Gmail Attachments';
  var searchQuery = 'has:attachment newer_than:1d';

  var folders = DriveApp.getFoldersByName(folderName);
  var folder = folders.hasNext()
    ? folders.next()
    : DriveApp.createFolder(folderName);

  var threads = GmailApp.search(searchQuery);
  for (var i = 0; i < threads.length; i++) {
    var messages = threads[i].getMessages();
    for (var j = 0; j < messages.length; j++) {
      var attachments = messages[j].getAttachments();
      for (var k = 0; k < attachments.length; k++) {
        var att = attachments[k];
        var ct = att.getContentType();
        if (ct === 'application/ics') continue;
        if (ct.indexOf('image/') === 0 && att.getSize() < 20000) continue;
        folder.createFile(att);
      }
    }
  }
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

What getAttachments() actually returns

The Gmail API's getAttachments() method gives you everything attached to the MIME envelope: PDFs, spreadsheets, Word docs, but also every inline image in every HTML signature, and the .ics calendar invite that Google Calendar automatically appends to meeting emails. The first time I ran a naive version of this script against a busy inbox I ended up with 400 tiny PNG files of company logos, an .ics for every calendar event in the last week, and exactly zero of the invoices I was trying to collect.

The fix is to filter on content type before writing to Drive. The script above skips application/ics unconditionally. For images it applies a size heuristic: anything under 20,000 bytes is almost certainly a signature icon or a spacer pixel, not a real attachment. That threshold holds well in practice; a genuine photograph or scanned document is rarely under 20 KB. Adjust the number if your domain sends small image attachments legitimately.

Note that getAttachments() does not expose inline images that are referenced via a Content-ID header but hosted remotely (tracked pixel style). Those never appear in the list, so you don't need to filter them. The ones you do need to filter are locally-attached inline parts, which Gmail encodes as standard MIME attachments with an image/* content type.

Targeting the right messages with search operators

GmailApp.search() accepts the same operators as the Gmail search box: has:attachment newer_than:1d, label:invoices has:attachment, from:vendor@example.com has:attachment. The 100-thread limit per search call is the main operational constraint — if you're processing more than 100 threads per day, you need to paginate using the two-argument form GmailApp.search(query, start, max) and loop until the result set is empty.

Using newer_than:1d on a daily time trigger means you'll reprocess the same messages that arrived in the last 24 hours on every run, which creates duplicate files in Drive. The cleaner pattern is to apply a Gmail label after processing a thread (thread.addLabel()) and exclude that label from the search query with -label:saved-to-drive. That way each message is processed exactly once regardless of trigger timing.

The search operator is:unread is tempting as a proxy for new messages, but it breaks the moment someone reads a thread before the trigger fires. Label-based deduplication is more reliable.

Setting up a time trigger without touching the UI

You can register the trigger programmatically so anyone who copies the script doesn't have to click through the Apps Script editor triggers panel:

Add a one-time setup function: function createTrigger() { ScriptApp.newTrigger('saveAttachmentsToDrive').timeBased().everyDays(1).atHour(7).create(); }. Run it once, then delete it or leave it — calling it again creates a duplicate trigger, which doubles your Drive writes.

The script requires two OAuth scopes: https://www.googleapis.com/auth/gmail.readonly and https://www.googleapis.com/auth/drive. Apps Script infers these from the service calls you make, so you don't declare them manually. On first run the authorization dialog will ask you to approve both. If you later add GmailApp.search() with a modify operator (like addLabel), the scope widens to gmail.modify and the authorization prompt fires again for any existing users of the script.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Why are duplicate files appearing in my Drive folder?
The script re-runs against the same messages each time unless you mark processed threads. Add a Gmail label after saving (thread.addLabel(GmailApp.getUserLabelByName('saved-to-drive'))) and include -label:saved-to-drive in your search query. Create the label in Gmail first or use GmailApp.createLabel() once in a setup function.
Can I save attachments to a subfolder organized by sender or date?
Yes. Replace the single folder.createFile(att) call with logic that creates or fetches a subfolder first: var sub = getOrCreateSubfolder(folder, messages[j].getFrom()). DriveApp.getFoldersByName() searches across all of Drive, not just children of a specific folder, so use folder.getFoldersByName() on the parent folder object to scope the lookup correctly.
The script times out before finishing. How do I handle a large backlog?
Apps Script has a 6-minute execution limit. For large backlogs, process one thread per run and store the last-processed thread index in PropertiesService (PropertiesService.getScriptProperties().setProperty('lastIndex', i)). Read it back at the start of each run and resume from there. Combine this with a short trigger interval (every 5 minutes) to drain the backlog incrementally.
How do I skip Google Workspace native files like Docs or Sheets that show up as attachments?
Call att.isGoogleType() before saving. It returns true for Docs, Sheets, Slides, and Forms attached as Drive links. Those can't be written to Drive via createFile() anyway — the call throws — so checking isGoogleType() and skipping is both the correct filter and the error prevention.