How to convert a Mailgun export to Gmail-friendly list
Published 2019-4-6You've got
vars,name,subscribed,address {},AJ ONeal,True,solderjs@gmail.com {},Ryan Burnette,True,rburnt@example.com {},John Bobjohn,False,bobjohn@example.com ...
You want
"AJ ONeal" <solderjs@gmail.com>, "Ryan Burnette" <rburnt@example.com>, ...
So... Whaddayado?
TL;DR: Parse, Loop, Map, Join, & Print
mailgun-csv-to-email-list.js
:
'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 cols.map(function (col) {
return col.trim();
});
});
// Throw away the header
rows.shift();
// Get an array of formatted names
var list = rows.map(function (cols) {
var subscribed = cols[2];
// null out the unsubscribed
if ('True' !== subscribed) { return null; }
var name = cols[1];
var email = cols[3].toLowerCase();
// '<solderjs@gmail.com>'
var addr = '<' + email + '>';
// '"AJ ONeal" <solderjs@gmail.com>'
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:
"foo,bar\nbaz,qux\n".split(/\n/)
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\nbaz,qux\n".trim().split(/\n/)
Tada!
[ "foo,bar"
, "baz,qux"
]
Better?
So that's line parsing. Now let's try individual fields from the rows:
Parsing Fields (Columns)
"foo,bar,,baz".split(",")
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(",");
rows.push(cols);
});
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,solderjs@gmail.com\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 = lines.map(function (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 = data.map(function (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(",");
});
console.log(headers.join(","));
console.log(lines.join("\n"));
Well, that's all folks. Now go spam your friends... I guess.
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )