Skip to content
// Gmail · Sheets · Apps Script

Parse CSV attachments from Gmail into Google Sheets.

A Google Apps Script that finds CSV attachments in Gmail, parses them with Utilities.parseCsv (handling quoted newlines and Windows-1252 encoding), and writes rows directly into a target Sheet.

I need a script that pulls CSV files attached to specific Gmail messages and writes their rows into a Google Sheet automatically, without corrupting quoted fields that contain commas or line breaks.

The script

copy · paste · trigger
importGmailCsv.gs
Apps Script
// importGmailCsv.gs — search Gmail, parse first CSV attachment, append to Sheet
function importGmailCsv() {
  var QUERY = 'subject:"weekly export" has:attachment newer_than:7d';
  var SHEET_ID = 'YOUR_SPREADSHEET_ID';
  var SHEET_NAME = 'Imports';
  var CHARSET = 'windows-1252';

  var threads = GmailApp.search(QUERY, 0, 10);
  var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);

  for (var t = 0; t < threads.length; t++) {
    var messages = threads[t].getMessages();
    for (var m = 0; m < messages.length; m++) {
      var attachments = messages[m].getAttachments();
      for (var a = 0; a < attachments.length; a++) {
        var att = attachments[a];
        if (att.getName().slice(-4).toLowerCase() !== '.csv') continue;
        var csv = att.getDataAsString(CHARSET);
        var rows = Utilities.parseCsv(csv);
        sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
             .setValues(rows);
      }
    }
  }
}

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

Walkthrough

Why split(',') will eventually wreck your data

CSV is deceptively simple until a field contains a comma or a newline. RFC 4180 says those fields are wrapped in double-quotes, and a hand-rolled split(',') has no idea those quotes exist. The first time I hit this was an address export where 'Suite 400, Floor 3' lived in a single column — split blew it into two columns and shifted every subsequent field one position to the right, silently.

Utilities.parseCsv is the correct tool. It is Apps Script's built-in RFC 4180 parser: it handles quoted commas, quoted newlines, and escaped quotes (two consecutive double-quotes inside a quoted field). You hand it a string and it returns a two-dimensional array of strings. No regex, no edge-case wrangling.

Encoding: the silent damage Excel exports cause

getDataAsString() defaults to UTF-8. That is correct for CSVs generated by anything modern. Excel on Windows, however, still defaults to Windows-1252 (also called CP-1252) when you 'Save As CSV'. Windows-1252 encodes characters like the euro sign (€), curly quotes, and accented letters differently from UTF-8. If you call getDataAsString() without a charset argument on a Windows-1252 file, those bytes decode as garbage or throw an exception entirely.

The fix is one argument: att.getDataAsString('windows-1252'). If you control the export and know it is UTF-8, you can drop that argument or pass 'UTF-8' explicitly. When the source is an external vendor and you are not sure, request a sample file, run Utilities.base64Encode on a few bytes around a suspect character, and compare against a Windows-1252 byte table. It is a ten-minute one-time check.

A note on the BOM (byte-order mark): some UTF-8 exports prepend three invisible bytes (EF BB BF) to signal the encoding. Utilities.parseCsv will include them in the first cell value as a garbage prefix. If your header row's first column name looks wrong in the sheet, strip the BOM manually: csv = csv.replace('', '').

Scoping the Gmail search and avoiding re-imports

GmailApp.search accepts the same query syntax as the Gmail search box. newer_than:7d limits the thread scan to the last week; without it the script re-reads your entire inbox on every run. Add has:attachment to skip threads with no attachments at the query level, not in the loop.

The script as written appends every matching attachment on every run, which means re-importing the same file if you run it twice in a week. The practical guard is to label processed threads: call thread.addLabel(GmailApp.getUserLabelByName('csv-imported')) after processing, then add -label:csv-imported to your QUERY string. One label, no database, no duplicates.

GmailApp.search returns at most 500 threads per call. The second and third arguments (start, max) let you paginate: start at 0, max at 10 is conservative for a weekly job. If the volume is higher, loop with an increasing start offset until the returned array is shorter than max.

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
My CSV has a header row and I don't want it repeated in the sheet on every run. How do I skip it?
Slice the rows array before writing: pass rows.slice(1) to setValues instead of rows. If you also need to write the header once when the sheet is empty, check sheet.getLastRow() === 0 and write rows[0] separately on the first run.
Utilities.parseCsv throws 'Cannot call method parseCsv of undefined'. What is wrong?
This happens when the attachment's getDataAsString returns an empty string, usually because the charset argument is wrong and the decode failed silently in older Apps Script runtimes. Log att.getBytes().length before calling getDataAsString to confirm the attachment has content, then verify the charset. On newer runtimes, a bad charset will throw a more descriptive error instead.
The script times out when there are many messages. What is the limit?
Apps Script imposes a 6-minute execution ceiling on triggered functions and 30 seconds on manually run functions. If you are processing large batches, break the work across runs: write the last processed thread index to PropertiesService (PropertiesService.getScriptProperties().setProperty('lastIndex', t)), read it at the start of the next run, and pick up from there. This is sometimes called a continuation pattern.
Can I run this on a schedule without opening the spreadsheet manually?
Yes. In the Apps Script editor go to Triggers (clock icon on the left sidebar), add a time-driven trigger pointing at importGmailCsv, and set the interval. Hourly or daily is typical for email exports. The trigger runs under your account so it inherits your Gmail and Sheets permissions — no extra OAuth setup needed beyond the initial manual authorization run.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.