How to Automatically Save Gmail Attachments to Google Drive with Apps Script

7 min readUpdated

A working Apps Script that finds Gmail attachments matching a search query and saves them into a Drive folder automatically — no add-on, no third-party tool.

Share:TelegramX

Quick answer

GmailApp.search(query) finds matching email threads, message.getAttachments() pulls the files out of each message, and folder.createFile(attachment) saves them into a Drive folder — three built-in Apps Script services, no add-on install required. Add a time-driven trigger and it runs on its own.

Step 1 — Write the search-and-save function

function saveAttachments() {
  const FOLDER_ID = 'YOUR_DRIVE_FOLDER_ID';
  const folder = DriveApp.getFolderById(FOLDER_ID);

  // Only unread invoices from the last day, as an example query
  const threads = GmailApp.search('has:attachment newer_than:1d label:invoices', 0, 20);

  threads.forEach(thread => {
    thread.getMessages().forEach(message => {
      const attachments = message.getAttachments({
        includeInlineImages: false,
        includeAttachments: true,
      });
      attachments.forEach(attachment => {
        folder.createFile(attachment);
      });
    });
  });
}

What each call actually does, per the official Apps Script reference:

  • GmailApp.search(query) — accepts a normal Gmail search query string, same syntax you'd type into Gmail's own search bar (has:attachment, newer_than:1d, label:invoices, from:someone@example.com, and so on). A second overload, search(query, start, max), adds pagination — max caps how many threads come back in one call
  • message.getAttachments(options) — includeInlineImages and includeAttachments both default to true. Setting includeInlineImages: false is what keeps a signature logo or an inline image embedded in the email body from being saved as if it were a real attachment
  • folder.createFile(blob) — a GmailAttachment is itself a Blob, so it can be passed straight into createFile() without any conversion step

Step 2 — Test manually, then automate

Run saveAttachments once from the Apps Script editor (▶ Run) to grant authorization and confirm files land in the right folder. Then add a time-driven trigger (clock icon → Add Trigger → saveAttachments → Time-driven → hourly or daily) so it keeps running without you opening the script again.

Step 3 — Avoid saving the same attachment twice

The example above will re-save every matching attachment on every run unless you narrow the search or mark messages as handled. Two common approaches:

// Option A: use a Gmail label as a processed-marker
const threads = GmailApp.search('has:attachment label:invoices -label:processed');
threads.forEach(thread => {
  thread.getMessages().forEach(message => {
    message.getAttachments({ includeInlineImages: false }).forEach(a => folder.createFile(a));
  });
  thread.addLabel(GmailApp.getUserLabelByName('processed'));
});

Option B is narrowing the query to a moving time window (newer_than:1d in the original example) combined with a trigger that runs at least that often — simpler, but less precise than a label if the trigger ever misses a run.

Common mistakes

Saving inline images as attachments by accident. Without includeInlineImages: false, every logo or tracking pixel embedded in an HTML email shows up in the Drive folder alongside real attachments. This is the single most common complaint in this exact pattern.

Search query too broad, script times out. GmailApp.search() with no date or label filter against a large mailbox can return thousands of threads — combined with getAttachments() and createFile() calls per message, this easily exceeds Apps Script's 6-minute execution limit. Always scope the query (a label, a sender, a date range) rather than searching the whole mailbox.

Duplicate files piling up in the Drive folder. Covered in Step 3 above — without a processed-marker or a narrowing time window, the same attachment gets re-saved (as a separate duplicate file, not overwritten) on every trigger run.

Large attachments failing silently. createFile() is still subject to Apps Script's normal per-call quotas — very large files (video, uncompressed images) or a high volume of them in one run can hit execution-time or response-size limits before they hit any Drive storage limit. If this happens, batch the run into smaller time windows rather than trying to process everything at once.

FAQ

Can I organize saved attachments into subfolders by sender or date instead of one flat folder?

Yes — create or look up a subfolder inside the loop, keyed on whatever you want to organize by (message.getFrom(), or the message date formatted with Utilities.formatDate()), and call createFile() on that subfolder instead of the top-level one.

Does this need the Gmail API enabled separately, or does the built-in GmailApp service handle everything?

The built-in GmailApp service handles everything shown here — no need to enable the separate advanced Gmail API service in Apps Script's project settings unless you need something GmailApp doesn't expose (batch operations, certain metadata). For search-and-save, GmailApp alone is enough.

What if I want to save attachments from a shared/group inbox, not my personal Gmail?

If it's a Google Workspace shared mailbox you have delegate access to, GmailApp operates through the account context the script runs under — a script bound to that shared account's own Google Sheets or run under that account's authorization can access it the same way. It cannot reach into someone else's private inbox just because you can send from it.

Is there a size or count limit on how many attachments this can process per run?

Not a hard limit specific to this pattern, but it's governed by the general Apps Script constraints: 6 minutes per execution, and Drive's own per-file and per-account storage quotas. For a high-volume mailbox, scope the search narrowly (as in Step 3) and let the trigger run more frequently on a small window rather than doing one large sweep — this is the same batching principle covered in importing API data into Google Sheets, where the same execution-time ceiling applies.


Last fact-checked: August 28, 2026. Method signatures verified against the official GmailApp and GmailMessage Apps Script references.

Related articles