You've got

vars,name,subscribed,address {},AJ ONeal,True, {},Ryan Burnette,True, {},John Bobjohn,False, ...

You want

"AJ ONeal" <>, "Ryan Burnette" <>, ...

So... Whaddayado?

TL;DR: Parse, Loop, Map, Join, & Print


'use strict';

// Important: I happen to be writing these 3 lines as if this were in node.js,
//            but those are the only 3 node-specific lines. It's all VanillaJS.

// Read in the file as UTF-8 (not ASCII, since you may have internationals)
var fs = require('fs');
var filepath = process.argv[2] || './export.csv';
var csv = fs.readFileSync(filepath, 'utf8');

// Remove the trailing newline
csv = csv.trim()

// Map over split rows, and map over split fields, trimming along the way
var rows = txt.split(/\n/).map(function (line) {
  var cols = line.trim().split(',');
  return (col) {
    return col.trim();

// Throw away the header

// Get an array of formatted names
var list = (cols) {
  var subscribed = cols[2];
  // null out the unsubscribed
  if ('True' !== subscribed) { return null; }

  var name = cols[1];
  var email = cols[3].toLowerCase();

  // '<>'
  var addr = '<' + email + '>';

  // '"AJ ONeal" <>'
  if (name) {
    addr = '"' + name + '" ' + addr;

  return addr;
}).filter(Boolean); // Drop the nulls

// Print the long string in Gmail-friendly copy/paste-able form
console.log(list.join(', '));

Wanna know why and how? Read on, reader!

Simple CSV

CSV can have many complex rules... and complex exceptions to those rules.

There's kinda-sorta a standard, but everyone follows it differently. If you need a complex CSV parser, Golang probably has you best option.

Fortunately, however, Mailgun uses a very dumb CSV generator (yes, in this case "dumb" is a good thing) with very simple rules.

Parsing Rows (Lines)

Try this:


Two things to notice there:

First, the use of /\n/ instead of '\n'. This matches not just a single literal ASCII "linefeed" byte, but rather any type of newline (which can be different between Windows, Mac, Linux, and "web") - so '\r\n', '\r', '\n', etc.

Second, you get an extra empty line:

[ "foo,bar"
, "baz,qux"
, ""

So, instead, we'll just add a little .trim() to nix the trailing end-of-file newline:



[ "foo,bar"
, "baz,qux"


So that's line parsing. Now let's try individual fields from the rows:

Parsing Fields (Columns)


Hopefully there are no surprises here:

[ "foo", "bar", "", "baz" ]

Hopefully with that you can see that constructing a simple CSV parser is just a matter of .trim(), .split() and .forEach() (or .map()).

var rows = [];
"foo,bar\nbaz,qux\n".trim().split(/\n/).forEach(function (row) {
  var cols = row.trim().split(",");

Lastly, there's the issue of the header.

The Header

The header, if any, will be your very first row.

This is where .shift() comes in:

// Get all rows, including the header
var lines = "First,Last,Email\nAJ,ONeal,\n".trim().split(/\n/);

// Remove the header from the *top* of the array
var header = lines.shift();

As you can surmise, .shift() is the opposite, no, inverse... maybe contra-positive (?)... of .pop(). It "pops from the top", as it were.

Bonus: Parsing Vars

If you're a cool modern hip dude or dudette you're too cool for schema-bound datasets like a CSV.

MailGun's got your back. The first field is a "bag" (as we say in typed-language parlance) titled Vars, which is just JSON. If you happen to actually be using it you could construct a dynamic list of headers like this:

// We'll use an object as a "map" to keep count of occurences of the extra headers
var xheadersMap = {};

var lines = csvtxt.trim().split(/\n/);
var headers = lines.shift().trim().split(",");

var data = (row) {
  var fields = row.trim().split(",");
  var json = JSON.parse(fields[0]); // the first field is json

  // keep track of how often each possible header is used
  Object.keys(json).forEach(function (k) {
    // add one to the previous count (or 0)
    xheadersMap[k] = (xheadersMap[k] || 0) + 1;

  return fields;

And you could then flatten out the CSV like this:

var xheaders = Object.keys(xheadersMap).sort(function (a, b) {
  return xheadersMap[b] - xheadersMap[a]; // sort most frequent to least frequest, by count
headers = headers.concat(xheaders);

lines = (fields) {
  var json = JSON.parse(fields.shift()); // shift to remove the first field (still json)
  xheaders.forEach(function (k) {
    fields.push(json[k] || '');

  return fields.join(",");


Well, that's all folks. Now go spam your friends... I guess.

By AJ ONeal

If you loved this and want more like it, sign up!

Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )