So today's post isn't necessarily that interesting - but I try to live by the rule of blogging everything that causes me trouble. Earlier this week I blogged about creating a "Random Comic Book Character" API. In the post, I ended with a simple HTML demo that displayed a random character on page load. Initially my plan had been to send an email every day with the character but I decided against that as I figured it would be too much trouble. Today I obviously decided I must like trouble as I went ahead and did it. And yes, it went as well as you can imagine.
So first, a quick recap. The core API is based one OpenWhisk action. It handles making multiple calls to the Comic Vine API and massaging them down into one set of data. So in theory, all I needed to do was take some of the "character JSON into HTML" logic from the desktop demo and just use that as an email. Easy!
So I know HTML email is a complete and utter shit show but I knew if I kept things simple and used inline styles I'd probably be ok. I had used a Google Font for my desktop demo so I figured I'd try that too and it wouldn't hurt it if failed. So here's the action I built:
const helper = require('sendgrid').mail;
function main(args) {
let SG_KEY = args.SG_KEY;
let char = args.character;
let from_email = new helper.Email('raymondcamden@gmail.com','Raymond Camden');
let to_email = new helper.Email('raymondcamden@gmail.com','Raymond Camden');
let subject = 'Your Daily Comic Book Character: '+char.name;
/*
Generate the HTML for the email first.
*/
let friendsTemplate = '';
let enemiesTemplate = '';
let powersTemplate = '';
let teamsTemplate = '';
let creatorsTemplate = '';
//need to find female (don't think a default exists)
let defaultMaleImage = 'https://comicvine.gamespot.com/api/image/scale_large/1-male-good-large.jpg';
let image = '';
if(!char.image) {
image = defaultMaleImage;
} else if(char.image && !char.image.super_url) {
image = defaultMaleImage;
} else {
image = char.image.super_url;
}
return new Promise( (resolve, reject) => {
let publisher = 'None';
if(char.publisher && char.publisher.name) publisher = char.publisher.name;
/*
If no description, copy deck over. deck can be blank too though
also sometimes its <br/>, sometimes <p>.</p>
*/
if(char.description && (char.description === '<br/>' || char.description === '<p>.</p>')) delete char.description;
if(!char.description && !char.deck) {
char.description = 'No description.';
} else if(!char.description) {
char.description = char.deck;
}
if(char.character_friends.length) {
friendsTemplate = `<h2 style="font-family: 'Banger', cursive;">Friends</h2><ul>`;
char.character_friends.forEach((friend) => {
friendsTemplate += `<li style="font-family: 'Banger', cursive;"><a href="${friend.site_detail_url}">${friend.name}</a></li>`;
});
friendsTemplate += '</ul>';
}
if(char.character_enemies.length) {
enemiesTemplate = `<h2 style="font-family: 'Banger', cursive;">Enemies</h2><ul>`;
char.character_enemies.forEach((enemy) => {
enemiesTemplate += `<li style="font-family: 'Banger', cursive;"><a href="${enemy.site_detail_url}" target="_new">${enemy.name}</a></li>`;
});
enemiesTemplate += '</ul>';
}
if(char.powers.length) {
powersTemplate = `<h2 style="font-family: 'Banger', cursive;">Powers</h2><ul>`;
char.powers.forEach((power) => {
powersTemplate += `<li style="font-family: 'Banger', cursive;">${power.name}</li>`;
});
powersTemplate += '</ul>';
}
if(char.teams.length) {
teamsTemplate = `<h2 style="font-family: 'Banger', cursive;">Teams</h2><ul>`;
char.teams.forEach((team) => {
teamsTemplate += `<li style="font-family: 'Banger', cursive;"><a href="${team.site_detail_url}" target="_new">${team.name}</a></li>`;
});
teamsTemplate += '</ul>';
}
if(char.creators.length) {
creatorsTemplate = `<h2 style="font-family: 'Banger', cursive;">Creators</h2><ul>`;
char.creators.forEach((creator) => {
creatorsTemplate += `<li style="font-family: 'Banger', cursive;"><a href="${creator.site_detail_url}" target="_new">${creator.name}</a></li>`;
});
creatorsTemplate += '</ul>';
}
let mainTemplate = `
<html>
<head>
<meta name="viewport" content="width=device-width">
<link href="https://fonts.googleapis.com/css?family=Bangers" rel="stylesheet">
</head>
<body style="background-color: #ffeb3b;padding: 10px">
<h1 style="font-family: 'Banger', cursive;">${char.name}</h1>
<p style="font-family: 'Banger', cursive;">
<strong>Publisher:</strong> ${publisher}<br/>
<strong>First Issue:</strong> <a href="${char.first_issue.site_detail_url}" target="_new">${char.first_issue.volume.name} ${char.first_issue.issue_number} (${char.first_issue.cover_date})</a><br/>
</p>
<a href="${char.site_detail_url}" target="_new"><img style="max-width:500px" title="Character Image" src="${image}"></a>
<p style="font-family: 'Banger', cursive;">${char.description}</p>
${creatorsTemplate}
${powersTemplate}
${teamsTemplate}
${friendsTemplate}
${enemiesTemplate}
</body>
</html>
`;
let mailContent = new helper.Content('text/html', mainTemplate);
let mail = new helper.Mail(from_email, subject, to_email, mailContent);
let sg = require('sendgrid')(SG_KEY);
var request = sg.emptyRequest({
method: 'POST',
path: '/v3/mail/send',
body: mail.toJSON()
});
sg.API(request, function(error, response) {
if(error) {
console.log('error in sg', error.response.body);
reject({error:error.message})
} else {
console.log('it should be well');
resolve({success:1});
}
});
});
}
That's a big chunk of code, but honestly, most of it is the same from the desktop demo. I expect a character to be passed in, I render it, and then use SendGrid to email it. I created a sequence that joined my actions together and fired it off and... it worked! Mostly...
It's not the prettiest email, but outside of the image not loading, it worked. Clicking the "load image" thing worked fine so I thought - ok - let me figure out why the image isn't loading by default.
Everything ended happily ever after.
Ok, no. So GMail has a setting for external images but I already had it on:
I clicked the "Learn more" link because I always want to learn more, and discovered this gem:
Note: If Gmail thinks a sender or message is suspicious, you won’t see images automatically. Instead, you'll be asked if you want to see the image.
Err... ok. I did a tiny bit of research into determining if there was something I could do to make GMail think email from me via SendGrid was super duper safe, but it seemed like a lot of work. I then decided - why not try encoding the image via base64 and data URLs. That should be easy, right?
Of course not - it took me a good hour just to get the data right. Mainly because I missed a little tip in the Request docs that say you have to disable encoding if you're working with binary data. That didn't frustrate me at all.
Eventually I got the base64 text right (verified via a regular HTML file) and I did my test. And nothing. No error, but the image just didn't show up. I went into dev tools and GMail had simply removed it. I have no idea why. From what I saw from Googling (everything comes back to the big G), GMail does support data URLs, but it never worked for me. Hell, I even tried the weird CID/attachment thing and that didn't work either. If folks want to see what I tried before I reverted, you can find the ocde here: https://github.com/cfjedimaster/Serverless-Examples/blob/master/comicvine/dailyEmail.fail.js
On a whim, I decided, why not take a look at the email on my phone.
AND OF COURSE, ON THE MOBILE DEVICE, IT LOADED THE EXTERNAL IMAGE AUTOMATICALLY.
sigh
But just for fun, it decided to not support the remote font.
I removed the font styles from the text and it ended up ok.
The final version of the code is up here - https://github.com/cfjedimaster/Serverless-Examples/blob/master/comicvine/dailyEmail.js. The final bit was to make a cron trigger, a rule, and then see how well I did my cron when, and if, I get the email tomorrow morning.