Guide

Save Email Attachments to Google Drive

Invoices, signed PDFs, and receipts land in your inbox and need to live in Google Drive. Most teams reach for a no-code connector and hit per-file fees plus monthly run caps. The Nylas CLI lists each message's attachments as JSON and downloads them locally; the Google Drive API stores each one in two short requests. This guide builds an email-to-Drive pipeline you control, filing every attachment into the folder you choose on a schedule.

Written by Prem Keshari Senior SRE

Reviewed by Qasim Muhammad

VerifiedCLI 3.1.17 · Gmail, Outlook · last tested June 9, 2026

Command references used in this guide: nylas email search, nylas email list, and the attachments subcommands.

How do I find email messages that have attachments?

Find messages with files by running nylas email search with the --has-attachment flag, which returns only messages that carry at least one attachment. Add --json to get structured output you can pipe into a loop, and scope the window with --after so you process a known date range instead of the whole mailbox. Each result includes the message ID you need next.

The search runs against whichever grant the CLI has configured, so it works the same for Gmail, Outlook, and the four other supported providers without provider-specific code. A practical filter is sender plus date: most attachment workflows file documents from one source, such as a billing system or a scanning service. The --limit flag defaults to 20 and auto-paginates past 200, so a daily run with --limit 200 covers a busy inbox. Capturing the message IDs to a file keeps the download step idempotent on reruns.

# Find messages with attachments from the last day, as JSON
nylas email search "*" --has-attachment \
  --from "billing@vendor.com" --after 2026-06-08 \
  --json --limit 200 > messages.json

# Keep just the message IDs for the next step
jq -r '.[].id' messages.json > message-ids.txt

How do I list and download each attachment?

List a message's files with nylas email attachments list <message-id>, which returns each attachment's ID, filename, content type, and size. Then download each one with nylas email attachments download <attachment-id> <message-id>. The -o flag sets the output path; without it the tool writes to the original filename. Looping over the IDs from the search step pulls every file to local disk.

Adding --json to the list command gives you the filename and content type per attachment, which the Drive upload needs for its metadata and MIME boundary. A 25 MB attachment is the Gmail per-message ceiling, so most files download in under a second; the size field lets you skip anything over a threshold before spending bandwidth. Sanitize filenames before writing them to disk, since a crafted name field could contain path separators. Write each file into a per-message directory so two messages with the same filename never collide.

mkdir -p downloads
while read -r MSG_ID; do
  # List attachments for this message as JSON
  nylas email attachments list "$MSG_ID" --json > atts.json

  jq -c '.[]' atts.json | while read -r att; do
    ATT_ID=$(echo "$att" | jq -r '.id')
    NAME=$(echo "$att"   | jq -r '.filename // "file.bin"' | tr -d '/')
    nylas email attachments download "$ATT_ID" "$MSG_ID" \
      -o "downloads/${MSG_ID}-${NAME}"
  done
done < message-ids.txt

How do I upload a file to the Google Drive API?

Upload in two requests, the pattern curl handles cleanly: POST the file's metadata to https://www.googleapis.com/drive/v3/files to create the entry, then PATCH its bytes to https://www.googleapis.com/upload/drive/v3/files/<id>?uploadType=media. (curl's -F sends multipart/form-data, which Drive's multipart endpoint rejects, so the two-step media upload is the reliable shell path.) Both calls are documented in Google's Manage uploads guide.

Simple media upload handles everyday attachments; Google recommends resumable uploads above 5 MB, and the same files.create method covers both per the files.create reference. You authenticate with an OAuth 2.0 access token carrying the drive.file scope, which limits the app to files it creates — see the Drive auth guide. Set parents to a folder ID to file everything in one place, and the token lives separately from the mailbox grant the tool manages.

FOLDER_ID="your-drive-folder-id"
upload_to_drive() {
  local path="$1" name="$2"
  # 1. Create the file's metadata (jq builds the JSON), capture the new ID
  local meta id
  meta=$(jq -n --arg n "$name" --arg p "$FOLDER_ID" '{name: $n, parents: [$p]}')
  id=$(curl -s -X POST "https://www.googleapis.com/drive/v3/files" \
    -H "Authorization: Bearer $GOOGLE_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$meta" | jq -r '.id')
  # 2. Upload the bytes to that file (simple media upload)
  curl -s -X PATCH \
    "https://www.googleapis.com/upload/drive/v3/files/$id?uploadType=media" \
    -H "Authorization: Bearer $GOOGLE_TOKEN" \
    --data-binary "@$path"
}

for f in downloads/*; do
  upload_to_drive "$f" "$(basename "$f")"
done

What message metadata should I attach to each upload?

Attach the sender, subject, and received date so a filed document stays traceable to its source email. Drive supports an appProperties object — up to 30 custom key-value pairs per file — for exactly this kind of private metadata. Pull the fields from the message JSON you already saved, then merge them into the upload's metadata part alongside the name and parent folder.

Custom appProperties keys and values are limited to 124 bytes combined per pair, so store IDs and short strings rather than full email bodies. Tagging the originating message ID lets a rerun query Drive and skip files it already uploaded, which is the de-duplication hook for a scheduled job. You can also prefix the filename with the ISO date so the folder sorts chronologically without opening each file. Keeping provenance in appProperties means an auditor can answer “which email did this PDF come from?” months later from the file's own record.

# Build a metadata part that carries email provenance
build_meta() {
  local name="$1" msg_id="$2" sender="$3"
  jq -nc \
    --arg name "$name" --arg folder "$FOLDER_ID" \
    --arg msg "$msg_id" --arg from "$sender" \
    '{name:$name, parents:[$folder],
      appProperties:{sourceMessageId:$msg, sender:$from}}'
}

Why run the pipeline on a schedule instead of by hand?

Run it on a schedule because attachments arrive continuously and a manual export misses anything received between runs. A daily cron that searches --after yesterday, downloads, and uploads keeps Drive current with zero clicks. Scoping each run to a one-day window means you process a small, predictable batch instead of re-scanning the entire mailbox every night.

For near-real-time filing, drive the same download-and-upload step from a message.created webhook so a new attachment lands in Drive within seconds of arriving. The processing code is identical; only the trigger changes. Store each uploaded file's sourceMessageId in appProperties and query Drive before uploading, so a retried run after a network failure never creates duplicates. Cron handles the common case in three lines of crontab; the webhook path is worth the extra setup only when minutes matter, such as a finance team that reconciles invoices the moment they land.

# crontab -e — file yesterday's attachments every morning at 6am
0 6 * * * /usr/local/bin/email-to-drive.sh >> ~/drive-sync.log 2>&1

Next steps