<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: SparkPost</title>
    <description>The latest articles on Forem by SparkPost (@sparkpost).</description>
    <link>https://forem.com/sparkpost</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F24%2F5c22a580-ac66-4867-88f5-854abe62d947.jpg</url>
      <title>Forem: SparkPost</title>
      <link>https://forem.com/sparkpost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sparkpost"/>
    <language>en</language>
    <item>
      <title>End-To-End Email Testing With MailSlurp</title>
      <dc:creator>Jose</dc:creator>
      <pubDate>Fri, 09 Feb 2018 14:01:20 +0000</pubDate>
      <link>https://forem.com/sparkpost/end-to-end-email-testing-with-mailslurp-57mk</link>
      <guid>https://forem.com/sparkpost/end-to-end-email-testing-with-mailslurp-57mk</guid>
      <description>&lt;h3&gt;
  
  
  How To Conduct End-To-End Testing With MailSlurp
&lt;/h3&gt;

&lt;p&gt;If you send email from an application, you probably have tests that ensure your code is attempting to send that email. But do you have a right way of testing whether that email got sent? &lt;a href="https://www.mailslurp.com/"&gt;MailSlurp&lt;/a&gt; is an awesome &lt;em&gt;free&lt;/em&gt; tool that helps you do that. It allows you to easily generate random inboxes to send email to and then confirm delivery through its API.&lt;/p&gt;

&lt;p&gt;In this post, we will use the MailSlurp &lt;a href="https://www.npmjs.com/package/mailslurp-client"&gt;Javascript SDK&lt;/a&gt; to write an end-to-end test for a node function that sends an email. We’ll be using &lt;a href="https://sparkpost.com"&gt;SparkPost&lt;/a&gt; to send the email. If you aren’t already sending email, here’s a &lt;a href="https://www.sparkpost.com/docs/getting-started/getting-started-sparkpost/"&gt;good place to start (for free)&lt;/a&gt;. For our test we’ll use &lt;a href="https://facebook.github.io/jest/"&gt;Jest&lt;/a&gt;, but any framework should work just fine.&lt;/p&gt;

&lt;p&gt;Our code will be using a few es2015 features, so be sure to be running a version of node that supports them (version &lt;a href="http://node.green/"&gt;6.4.0&lt;/a&gt; or above should be fine). We’ll be installing dependencies using &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt; version 5.6.0.&lt;/p&gt;

&lt;p&gt;All the code for this &lt;a href="https://github.com/jgzamora/mailslurp-sparkpost"&gt;demo&lt;/a&gt; can be found here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Set Up
&lt;/h3&gt;

&lt;p&gt;Alright, let’s get started! The first step is to get set up with a MailSlurp account. It’s quick and free: &lt;a href="https://www.mailslurp.com/sign-up"&gt;Sign up&lt;/a&gt; for MailSlurp. Next, log in to your dashboard and grab your API key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mVBg44H3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/dm7goc8dt9qj1laipbvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mVBg44H3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/dm7goc8dt9qj1laipbvs.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have your API Key, we can get started with the test setup. Make sure you have these dependencies in your package.json – here’s what ours looks like, using the SparkPost client library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sparkpost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mailslurp-client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.1.1514850454"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^22.1.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run &lt;code&gt;npm install&lt;/code&gt; Here is our full &lt;a href="https://github.com/jgzamora/mailslurp-sparkpost/blob/master/package.json"&gt;package.json&lt;/a&gt;. Normally packages like Mailslurp client and jest should go under “dev-dependencies”, but we kept it simple for this demo.&lt;/p&gt;

&lt;p&gt;Now from the top of our test file, we require Mailslurp-client and our &lt;code&gt;sendEmail&lt;/code&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test file: mailslurp.test.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MailslurpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mailslurp-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./sendEmail.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// our SparkPost email function&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You would import whatever function you use to send email.&lt;/p&gt;

&lt;p&gt;Right after, we initialize the MailSlurp client. This is also a good spot to store your API key in a variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// initialize the MailSlurp Client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slurp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;MailslurpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InboxcontrollerApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Sign up at MailSlurp.com to get one of these for free.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slurpKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MS_APIKEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We used &lt;a href="https://medium.com/ibm-watson-data-lab/environment-variables-or-keeping-your-secrets-secret-in-a-node-js-app-99019dfff716"&gt;environment&lt;/a&gt; variables to access ours; if you store credentials a different way, that’s ok too. Our slurpKey variable will be the first parameter we pass to every MailSlurp client method.&lt;/p&gt;

&lt;p&gt;Next step is to create an inbox we can send to with the MailSlurp client’s &lt;code&gt;createRandomInboxUsingPOST&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SendEmail Tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;inboxId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Creates the inbox and set address &amp;amp; inboxId for use in tests&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slurp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createRandomInboxUsingPOST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slurpKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the email address we will be sending to&lt;/span&gt;
        &lt;span class="nx"&gt;inboxId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// UUID used to identify the inbox&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

 &lt;span class="cm"&gt;/* tests will go here */&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important things here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute this code before you run your tests&lt;/li&gt;
&lt;li&gt;Store both address &amp;amp; id from the response as variables, so you can send to and access your inbox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now for the actual test we want to send an email (&lt;code&gt;sendEmail()&lt;/code&gt; function for us) and then use the MailSlurp client’s &lt;code&gt;getEmailsForInboxUsingGET&lt;/code&gt; method to get the email from the inbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email delivers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// increase the default jest timeout to wait for delivery&lt;/span&gt;
    &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// pass address given to us by MailSlurp&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;total_accepted_recipients&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// make sure SparkPost accepted the request&lt;/span&gt;
        &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total_accepted_recipients&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Get the list of emails from the inbox&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slurp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEmailsForInboxUsingGET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slurpKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inboxId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;minCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// minimum number of emails to wait for, default wait time is 60s&lt;/span&gt;
          &lt;span class="p"&gt;})&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// console.log(payload) // uncomment this to see the full email&lt;/span&gt;
            &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailSlurp Test Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Email delivery can take more than a few seconds, so make sure your test waits around long enough for the email to be delivered. In the code snippet above, we change the timeout threshold to handle that. Note that we are passing &lt;code&gt;address&lt;/code&gt; to our &lt;code&gt;sendEmail&lt;/code&gt; function, and then passing &lt;code&gt;inboxId&lt;/code&gt; to &lt;code&gt;getEmailsForInboxUsingGET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this test we first asserted that MailSlurp returned an array of emails with a length of 1 (we only sent one email). Then to make sure it was the email we sent, we asserted that its subject was ‘MailSlurp Test Email’ as defined in &lt;a href="https://github.com/jgzamora/mailslurp-sparkpost/blob/master/sendEmail.js"&gt;sendEmail.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s it! Run the test however you usually do. Ours is set up with npm scripts in the package.json, which runs with &lt;code&gt;npm test&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;p&gt;There is plenty of room for more assertions in our example test. When using an email delivery service like SparkPost in combination with complex HTML templates, an email’s body can get pretty complicated. To make assertions about an email’s actual content, we suggest using an advanced email body parser. Once you are able to extract the HTML/text content from the body, you could easily use &lt;a href="https://facebook.github.io/jest/docs/en/snapshot-testing.html"&gt;snapshot tests&lt;/a&gt; to ensure you are sending the content you want.&lt;/p&gt;

&lt;p&gt;If this was interesting to you, you should check out the rest of &lt;a href="https://www.mailslurp.com/"&gt;MailSlurp&lt;/a&gt; features. You can reach us on &lt;a href="https://twitter.com/SparkPost"&gt;Twitter&lt;/a&gt; for any questions. And you can also send 15,000 emails a month for free on &lt;a href="https://pages.sparkpost.com/bl-account-signup-generic.html?src=Blog&amp;amp;pc=bl-rfi-generic-2017&amp;amp;utm_source=blog&amp;amp;utm_medium=blog&amp;amp;utm_campaign=all&amp;amp;utm_content=signup"&gt;SparkPost&lt;/a&gt;! Special thanks to &lt;a href="https://github.com/jackmahoney"&gt;Jack Mahoney&lt;/a&gt; for building MailSlurp and answering my questions for this post.&lt;/p&gt;

&lt;p&gt;–&lt;a href="https://twitter.com/josegzamora"&gt;Jose&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The post &lt;a href="https://www.sparkpost.com/blog/email-testing-mailslurp/"&gt;End-To-End Email Testing With MailSlurp&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>email</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How To Send Faxes Via Email Using SparkPost, Twilio &amp; Cloudinary</title>
      <dc:creator>Avi Goldman</dc:creator>
      <pubDate>Fri, 12 Jan 2018 14:08:50 +0000</pubDate>
      <link>https://forem.com/sparkpost/how-to-send-faxes-via-email-using-sparkpost-twilio--cloudinary-5e5i</link>
      <guid>https://forem.com/sparkpost/how-to-send-faxes-via-email-using-sparkpost-twilio--cloudinary-5e5i</guid>
      <description>&lt;h3&gt;
  
  
  No Fax Machine? No Problem! SparkPost, Twilio, and Cloudinary Are Here To Save The Day!
&lt;/h3&gt;

&lt;p&gt;I don’t know about you, but I don’t have a fax machine. Once in a blue moon, I have to send or receive a fax, so I was excited when Twilio shared an awesome way to receive faxes straight to your email using SparkPost. But that is only half the battle. I decided to build the ability to send faxes from an email so I would never need a fax machine again!&lt;/p&gt;

&lt;p&gt;We’ll build a function so we can send a PDF attachment to FAX_NUMBER@YOUR_DOMAIN, which will automatically fax the PDF to the phone number before the at sign (a.k.a. the local part, for all you &lt;a href="https://www.meetup.com/emailgeeksSF/" rel="noopener noreferrer"&gt;email geeks&lt;/a&gt; out there). To do this, we’ll use SparkPost’s inbound functionality, Twilio’s Fax API, and Cloudinary to glue them together. We’ll receive an email to a Twilio function, pull off the attached PDF, save it to Cloudinary, and send it as a fax.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sign Up and Configure Your SparkPost Account
&lt;/h3&gt;

&lt;p&gt;The first thing you’ll need is a &lt;a href="https://app.sparkpost.com/join" rel="noopener noreferrer"&gt;SparkPost account&lt;/a&gt; and a domain you want to use to receive mail (a.k.a. an &lt;a href="https://developers.sparkpost.com/api/inbound-domains.html" rel="noopener noreferrer"&gt;inbound domain&lt;/a&gt;). You’ll also need to create an API key with permissions to read and write&lt;code&gt;inbound domains&lt;/code&gt; and &lt;code&gt;relay webhooks&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.27.28-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.27.28-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, add the &lt;a href="https://www.sparkpost.com/docs/tech-resources/inbound-email-relay-webhook/#add-mx-records" rel="noopener noreferrer"&gt;SparkPost MX&lt;/a&gt; records for your inbound domain. Once you&lt;a href="https://mxtoolbox.com/" rel="noopener noreferrer"&gt; verify&lt;/a&gt; that they are set up correctly, run the following cURL to add your inbound domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.sparkpost.com/api/v1/inbound-domains &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "domain": "YOUR_DOMAIN" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sign Up For Cloudinary
&lt;/h3&gt;

&lt;p&gt;Next, we’ll need a &lt;a href="https://cloudinary.com/users/register/free" rel="noopener noreferrer"&gt;Cloudinary account&lt;/a&gt; to store the PDF we’ll fax. If you don’t know, Cloudinary is a powerful solution for storing, manipulating, and delivering all your media. Grab your cloud name, API key, and API secret and put them somewhere safe for later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.41.58-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.41.58-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create &amp;amp; Configure Twilio
&lt;/h3&gt;

&lt;p&gt;We’ll use Twilio to consume the incoming emails and send the faxes. To get started, &lt;a href="https://www.twilio.com/try-twilio" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for an account, and &lt;a href="https://www.twilio.com/login?g=%2Fconsole%2Fphone-numbers%2Fsearch%2Fbuy%2Fresults&amp;amp;t=3a4d26ea80f7766974e80da9f2835d8e4df28f63a511efd5ae0093c5cb47e0d9" rel="noopener noreferrer"&gt;buy a phone number&lt;/a&gt; that can send and receive faxes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.43.40-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.43.40-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twilio’s serverless functions are perfect for this project. This isn’t a stateful application and it doesn’t need to be running all the time. Using one, we can quickly get set up and run our application.&lt;/p&gt;

&lt;p&gt;We’ll need the following NPM modules for our function:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.45.46-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.45.46-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s also add in our environment variables for Cloudinary and enable Twilio’s option to pass through our &lt;code&gt;ACCOUNT_SID&lt;/code&gt; and &lt;code&gt;AUTH_TOKEN&lt;/code&gt; so we can use their client library without worrying about our credentials:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.48.59-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-3.48.59-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;

&lt;p&gt;First things first, let’s write our basic Twilio function and pull in our dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudinary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MailParser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mailparser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toArray&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;twilio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTwilioClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;cloudinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cloud_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;api_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_SECRET&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;messages&lt;/code&gt; variable is an object full of the messages SparkPost handed off to us. We’ll need to loop through the messages, pull off the attachment, save it to Cloudinary, and send off the fax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after the cloudinary config&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pickPhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;pickPdfAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;uploadAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendFax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parsing The Email
&lt;/h3&gt;

&lt;p&gt;You can see an example the &lt;a href="https://developers.sparkpost.com/api/relay-webhooks.html#header-example-payloads" rel="noopener noreferrer"&gt;payload SparkPost sends&lt;/a&gt; in the API documentation. The important parts for us are the &lt;code&gt;rcpt_to&lt;/code&gt;and the&lt;br&gt;
&lt;code&gt;content&lt;/code&gt; which will contain the raw email. These live inside &lt;code&gt;message.msys.relay_message&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can pull out the phone number by splitting off the local part from the &lt;code&gt;rcpt_to&lt;/code&gt; value – everything before the &lt;code&gt;@&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickPhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relay_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rcpt_to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll also need to pull the PDF off of the message. To do this, we’ll parse the RFC 822 value using the &lt;code&gt;mailparser&lt;/code&gt; library and return the content of the first PDF attached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickPdfAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relay_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_rfc822_is_base64&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isBase64&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_rfc822&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_rfc822&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailParser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;streamAttachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attachment&lt;/span&gt;
    &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Saving The Attachment
&lt;/h3&gt;

&lt;p&gt;Assuming we get an attachment, we’ll need to put it in an accessible spot for Twilio to pull it from. Enter Cloudinary. Using their Node library we can easily pipe the PDF and get the publicly accessible URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_stream&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sending The Fax
&lt;/h3&gt;

&lt;p&gt;The last step for our code is to send the fax! Twilio makes this really easy. We need three values: the number we are sending to, the number we are sending from, and the media url. We’ll use the number we bought earlier to send for the “from” number. And we should have a phone number from the local part of the email and the media url from Cloudinary!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendFax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;faxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;YOUR_PHONE_NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create A Relay Webhook
&lt;/h3&gt;

&lt;p&gt;The last piece of the puzzle it to tie our inbound domain from SparkPost to our Twilio function. Let’s create a relay webhook to pass all mail sent to our inbound domain onto our Twilio function. Copy your function path and pass it into this cURL request to create the relay webhook. Make sure that “Check for valid Twilio signature” is unchecked so that SparkPost can access it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-4.13.11-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2018%2F01%2FScreen-Shot-2018-01-10-at-4.13.11-PM.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.sparkpost.com/api/v1/relay-webhooks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "target": "YOUR_TWILIO_PATH", "match": { "domain": "YOUR_INBOUND_DOMAIN", "protocol": "SMTP" }, "name": "Email-to-Fax" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send The Fax!! 🎉
&lt;/h3&gt;

&lt;p&gt;Send an email over to FAX_NUMBER@YOUR_DOMAIN with your fax attached as a PDF and it should go through!&lt;/p&gt;

&lt;p&gt;You can set up receiving faxes for a fully working email fax machine with some &lt;a href="https://www.twilio.com/blog/2017/12/fax-to-email-twilio-functions-sparkpost.html" rel="noopener noreferrer"&gt;guidance&lt;/a&gt; from our friend &lt;a href="https://twitter.com/kolencherry" rel="noopener noreferrer"&gt;Patrick&lt;/a&gt; at &lt;a href="https://www.twilio.com/" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt;! Feel free to &lt;a href="https://twitter.com/theavigoldman" rel="noopener noreferrer"&gt;reach&lt;/a&gt; out if you’ve got any questions and have fun faxing!&lt;/p&gt;

&lt;p&gt;-Avi&lt;/p&gt;

&lt;p&gt;P.S. Before you launch this to the world, you’ll probably want to validate the phone numbers you’re sending to and add some security to who can send. I’d suggest a token in the email that you verify in the Twilio function.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The post &lt;a href="https://www.sparkpost.com/blog/send-faxes-via-email/" rel="noopener noreferrer"&gt;How To Send Faxes Via Email Using SparkPost, Twilio &amp;amp; Cloudinary&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com" rel="noopener noreferrer"&gt;SparkPost&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>twilio</category>
      <category>cloundinary</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Announcing HEML: An Open Source Framework for Email</title>
      <dc:creator>Avi Goldman</dc:creator>
      <pubDate>Fri, 27 Oct 2017 13:05:21 +0000</pubDate>
      <link>https://forem.com/sparkpost/announcing-heml-an-open-source-framework-for-email-cek</link>
      <guid>https://forem.com/sparkpost/announcing-heml-an-open-source-framework-for-email-cek</guid>
      <description>&lt;p&gt;Today I am excited to announce the first release of &lt;a href="https://heml.io/" rel="noopener noreferrer"&gt;HEML,&lt;/a&gt; an open source markup language for crafting clean, responsive emails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why
&lt;/h3&gt;

&lt;p&gt;If you’re not familiar with writing HTML emails, &lt;a href="https://www.sparkpost.com/blog/topol-html-email-templates/" rel="noopener noreferrer"&gt;it can be a painful process.&lt;/a&gt; A few months ago I started to play with some exciting interactive developments on the front end. I found the bottleneck wasn’t sending or testing the email, but simply building the email itself. At our next company hackathon, I took the opportunity to solve this problem, and thus HEML was born.&lt;/p&gt;

&lt;p&gt;Each element in HEML renders into email-ready HTML so that you can send without worry. HEML also works to iron out CSS bugs and limitations of different email clients. An excellent example of one such bug is an obscure issue in Lotus Notes where if you use RGB decimal value, the entire style declaration will be ignored. HEML will handle that bug for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our Goals
&lt;/h3&gt;

&lt;p&gt;We wanted HEML to help make email more accessible to developers. The idea is for developers to jump in quickly and build their emails without wrestling with Outlook (or any other email inbox). To do this, we focused on three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native feel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We wanted HEML to look and feel like HTML. As a result, it mirrors HTML as closely as possible and uses plain ol’ CSS for styling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward Thinking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HEML doesn’t limit you from taking advantage of all that HTML and CSS can do. It encourages progressive enhancements. &lt;a href="https://emails.hteumeuleu.com/do-emails-need-to-look-exactly-the-same-in-every-client-5c0ec5ca541d" rel="noopener noreferrer"&gt;Email doesn’t have to look the same everywhere.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extendable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HEML lets you create your custom elements, share them, and pull in other elements made by the community.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using HEML
&lt;/h3&gt;

&lt;p&gt;There are a couple of different ways to use HEML.&lt;/p&gt;

&lt;p&gt;Get started quickly using our editor at &lt;a href="https://heml.io/editor" rel="noopener noreferrer"&gt;heml.io/editor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F10%2Fezgif-5-df93edf292.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F10%2Fezgif-5-df93edf292.gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To use it locally, install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g heml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your HEML email in &lt;code&gt;email.heml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;heml&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;subject&amp;gt;&lt;/span&gt;Email Rox!&lt;span class="nt"&gt;&amp;lt;/subject&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SkyBlue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DarkViolet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;container&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;marquee&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello world &lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;/marquee&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/container&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/heml&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heml develop email.heml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That starts a development server that will auto-reload your browser whenever you make a change.&lt;/p&gt;

&lt;p&gt;Once you’re ready to send your email into the wild, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heml build email.heml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates an &lt;code&gt;email.html&lt;/code&gt; file that is ready to be sent.&lt;/p&gt;

&lt;p&gt;Give it a spin!&lt;/p&gt;

&lt;p&gt;This is our take on a difficult problem. It doesn’t solve every problem presented by email, but it can help you create solutions for your unique email challenges. There has been amazing work done to simplify this challenge by &lt;a href="https://mjml.io/" rel="noopener noreferrer"&gt;MJML&lt;/a&gt;,&lt;a href="https://foundation.zurb.com/emails.html" rel="noopener noreferrer"&gt;Foundation for Email&lt;/a&gt;, and many others. We hope you find this equally as helpful!&lt;/p&gt;

&lt;p&gt;So give it a try! Hopefully, it makes your life easier. If you have any feedback, suggestions, or bugs, &lt;a href="https://github.com/SparkPost/heml/issues" rel="noopener noreferrer"&gt;let us know&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy Coding!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The post &lt;a href="https://www.sparkpost.com/blog/heml-open-source/" rel="noopener noreferrer"&gt;Announcing HEML: An Open Source Framework for Email&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com" rel="noopener noreferrer"&gt;SparkPost&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>heml</category>
      <category>opensource</category>
      <category>email</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Creating the Next Generation of Interactive Emails with Rebel Mail</title>
      <dc:creator>Jennifer Lacey</dc:creator>
      <pubDate>Wed, 18 Oct 2017 13:05:32 +0000</pubDate>
      <link>https://forem.com/sparkpost/creating-the-next-generation-of-interactive-emails-with-rebel-mail-2le</link>
      <guid>https://forem.com/sparkpost/creating-the-next-generation-of-interactive-emails-with-rebel-mail-2le</guid>
      <description>&lt;p&gt;&lt;em&gt;**We are excited to announce our new partnership with Rebel. Rebel is disrupting the email marketing space and allows users to send interactive emails with no plug-ins required. This guest blog post, written by Manisha Shah highlights Rebel’s ease of use and their competitive edge in the market.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrate your email channel and app with in-email interactivity.
&lt;/h3&gt;

&lt;p&gt;Email is one of the oldest communication protocols on the web. Unfortunately for the billions of people that use it to communicate, up until a few years ago, almost nothing has dramatically improved the user experience. After years of very little innovation, interactive email has created a flurry of exciting changes.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.rebelmail.com/"&gt;Rebel&lt;/a&gt;, we don’t see stodgy, old email clients that are a nightmare to develop against. We see a brand new surface for your application to live on. Interactive email allows you to show a user the exact action you want them to take, without the need for a click or leave their inbox.&lt;/p&gt;

&lt;p&gt;Taking advantage of these new opportunities for user actions is easier said than done. Coding normal emails remains a challenge for most developers due to the deep fragmentation in email clients. That’s why we believe the best way to take advantage is through our interactive email API.&lt;/p&gt;

&lt;p&gt;It’s the only way of generating interactive emails on the fly, personalized to the user you are sending to. You don’t need to know the latest hacks to get the email rendering perfectly or the quirks of gathering data from inside an email client. All you need to know is the information you want to get from a user, and with a simple JSON request, you can get back ready-to-send interactive email HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  But what does that really mean?
&lt;/h3&gt;

&lt;p&gt;If the above sounds fanciful and far-fetched to you, you’re not alone. However, it is that simple. All that you need to do is describe the functionality that you want to achieve in JSON and our API will convert it to HTML for you.&lt;/p&gt;

&lt;p&gt;To prove that point we’re going to build a quick email that asks a user for some feedback on your app. It will contain stars for rating 1-5 and a text input for collecting general feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API call structure.
&lt;/h3&gt;

&lt;p&gt;For this example, we’ll be looking at an abbreviated API call (I’ve taken out some of the options so that the information is clear), that will return to you HTML with two form fields, and a submit button.&lt;/p&gt;

&lt;p&gt;As you can see below the JSON call includes an array at &lt;code&gt;context.components.body.formfields&lt;/code&gt;, in that array there are two objects. Each describes one piece of functionality. One for the rating module and one for the text area. Here you place all the information you want to be covered displayed in the email and adjust and field-specific settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"context": {
    "components": [
      {
        "type": "form",
        "body": {
          "submitUrl": "http://rebelmail.com/review/",
          "id": "review-form",
          "formFields": [
            {
              "type": "rating",
              "variation": "star",
              "fluid": true,
              "layout": "stack",
              "name": "star-rating",
              "label": "How was the espresso?",
              "options": [
                {
                  "value": "1",
                  "text": "&amp;amp;#9733;"
                },
                {
                  "value": "2",
                  "text": "&amp;amp;#9733;"
                },
                {
                  "value": "3",
                  "text": "&amp;amp;#9733;"
                },
                {
                  "value": "4",
                  "text": "&amp;amp;#9733;"
                },
                {
                  "value": "5",
                  "text": "&amp;amp;#9733;"
                }
              ]
            },
            {
              "type": "textArea",
              "layout": "stack",
              "name": "comments",
              "label": "Anything else we should know?",
              "placeholder": "Leave your comments here...",
              "maxlength": "2000"

            }
          ],
          "submit": {
            "text": "Leave Your Review"
          }
        }
      }
    ],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt; &lt;/p&gt;

&lt;p&gt;The above example is abbreviated for illustrative purposes, but generally, our API allows you to set the values and styles of any element inside our interactive modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get back.
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;/campaigns/:id/html&lt;/code&gt; endpoint will return to you HTML, completely ready to be delivered. No need to do anything to it, just send it out. Here is the email from the full request above as seen in Apple Mail:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VVwWcK-g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/10/Screen-Shot-2017-10-16-at-1.51.51-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VVwWcK-g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/10/Screen-Shot-2017-10-16-at-1.51.51-PM.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Interactive email analytics.
&lt;/h3&gt;

&lt;p&gt;Of course what good is new technology if you can’t tell if it’s adding value to your business. That’s why we built a robust analytics platform for recording every possible action a user takes within an email. If a user clicks a radio button or star rating, we record it. If they open the email on Outlook and see a beautifully rendered static fallback message, we record it.&lt;/p&gt;

&lt;p&gt;All that actionable data is only good if you can consume it. That’s why we created a number of ways to have our data pushed to your servers (such as a webhook).&lt;/p&gt;

&lt;h3&gt;
  
  
  Go forth and interact.
&lt;/h3&gt;

&lt;p&gt;These kinds of forms are one excellent way to allow users to interact with your app from within an email client, and there is a myriad of ways to apply this same technology. Responding to comments, prompting inactive users to post again for the first time, or allowing users to complete portions of their onboarding while verifying their email address, are all possible.&lt;/p&gt;

&lt;p&gt;You can do even more than forms with our flexible API, Thanks to the various modules we have created, your users can take quizzes, research or filter products, and even shop directly in the inbox. All while our robust analytics allows you to gather every action and use it for things like progressive profiling and user recommendations.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/interactive-emails-rebelmail/"&gt;Creating the Next Generation of Interactive Emails with Rebel Mail&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>email</category>
      <category>api</category>
      <category>json</category>
    </item>
    <item>
      <title>Using the SparkPost Metrics API with C#</title>
      <dc:creator>Mike Hillyer</dc:creator>
      <pubDate>Mon, 16 Oct 2017 13:02:48 +0000</pubDate>
      <link>https://forem.com/sparkpost/using-the-sparkpost-metrics-api-with-c-83f</link>
      <guid>https://forem.com/sparkpost/using-the-sparkpost-metrics-api-with-c-83f</guid>
      <description>&lt;p&gt;*Today’s blog is written by &lt;a href="https://twitter.com/darrencauthon"&gt;Darren Cauthon&lt;/a&gt;, community member and original author of the &lt;a href="https://github.com/SparkPost/csharp"&gt;SparkPost C# Library&lt;/a&gt;. Thanks for the post, Darren! We hope you enjoy.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Use the SparkPost Metrics API with C
&lt;/h3&gt;

&lt;p&gt;In our first two tutorials on using SparkPost with C#, we learned how to &lt;a href="https://dev.to/sparkpost/getting-started-with-c-and-sparkpost"&gt;send an email from a C# application&lt;/a&gt; and &lt;a href="https://dev.to/sparkpost/using-sparkpost-templates-with-c-c8h"&gt;how to manipulate email templates using C# code&lt;/a&gt;. You’ll want to start there to get the SparkPost C# library installed via NuGet, it’s all good stuff.&lt;/p&gt;

&lt;p&gt;But most of the time when you send an email from your app, you’ll also want to know what happened to that message: was it delivered? Did your user open it or even click on link in the message body? Visibility into message disposition and performance is crucial for many apps.&lt;/p&gt;

&lt;p&gt;That’s why the SparkPost C# library includes methods for getting important metrics about the emails that you send. Today, we’re going to look at how they work.&lt;/p&gt;

&lt;p&gt;There are two mechanisms for getting these analytics: pushing and pulling. With pushing (via a webhook), you can get your metrics streamed in near real-time as SparkPost posts the information to a web endpoint that you provide. A future tutorial will provide some help on setting up a webhook endpoint in a standard .Net MVC application.&lt;/p&gt;

&lt;p&gt;Today, however, we’ll focus on pull-style API-based queries for your metrics via the C# wrapper. You can query the SparkPost API for just about any type of information you’d want about the emails you send within the past few weeks, and the API will return the information you requested.&lt;/p&gt;

&lt;p&gt;This tutorial will also provide some insight into the library’s design, and how you can use it for other SparkPost API calls you’d want to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  A SparkPost Metrics API Quick Start
&lt;/h3&gt;

&lt;p&gt;When using the C# SparkPost library, the first place to check is the &lt;a href="https://developers.sparkpost.com/api/"&gt;SparkPost API documentation&lt;/a&gt;. The C# library was written to mirror the SparkPost web API as closely as possible. Naturally, the &lt;a href="https://developers.sparkpost.com/api/metrics.html"&gt;API docs include specific information about metrics&lt;/a&gt;. Look there to see what sorts of metrics you might want to gather.&lt;/p&gt;

&lt;p&gt;Let’s try the first method in the docs: Gathering metrics by domain. Let’s build a sample request that will return the number of accepted and bounced emails that were sent to Gmail or Yahoo Mail. Here is C# code that will return your results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var client = new Client("YOUR API KEY");
client.CustomSettings.SendingMode = SendingModes.Sync; 

var query = new MetricsQuery {
    From = DateTime.Now.AddDays(-7),
    Metrics = new[] { "count_accepted", "count_bounce" },
    Domains = new[] { "gmail.com", "yahoo.com" },
};

var response = await client.Metrics.GetDeliverabilityByDomain(query);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are your results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yjw3q0AY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/10/Screen-Shot-2017-10-13-at-2.31.52-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yjw3q0AY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/10/Screen-Shot-2017-10-13-at-2.31.52-PM.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s really that easy. All of the metrics data you want, for this and any of the other API metric methods, will be returned in this form. The only real question is what data you want, so check out the SparkPost API documentation and make the query you want!&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Keep the SparkPost C# Library Up-To-Date with API Changes
&lt;/h3&gt;

&lt;p&gt;One of the difficulties in using a library for a service like SparkPost is keeping up-to-date with changes. SparkPost is always adding new features to its API, but are those new features reflected in the library? Sometimes there is a lag between when API developers add features and when library authors add support.&lt;/p&gt;

&lt;p&gt;The C# library tries to mitigate those issues by allowing you to extend the library, yourself. The example above showed how to use the MetricsQuery, but you can pass any object to use as a query—so long as it matches the style and naming of the SparkPost service.&lt;/p&gt;

&lt;p&gt;Here is a concrete example: SparkPost developers added a new API feature, the ability to pass a “limit to limit the size of your result. That feature was not available when the MetricsQuery class was written, so it does not have Limit available. So does that mean you can’t use Limit in C#? No, that’s still available to you – just have to pass it yourself. Like with an anonymous object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var client = new Client("YOUR API KEY");
client.CustomSettings.SendingMode = SendingModes.Sync; 

var response = client.Metrics.GetDeliverabilityByDomain(new {
  From = DateTime.Now.AddDays(-0),
  Metrics = "count_accepted,count_bounce",
  Domains = "gmail.com,yahoo.com",
  Limit = 10
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another method would be to extend the MetricsQuery and add an “int Limit { get; set; } property. But the main point to notice is that so long as you stay consistent with the SparkPost documentation and naming, this library can extend itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SparkPost C# Library Is Open Source
&lt;/h3&gt;

&lt;p&gt;I’m the original author of the &lt;a href="https://github.com/SparkPost/csharp"&gt;SparkPost library&lt;/a&gt;, but there are more than a dozen developers who have contributed code to this library, like this Metrics library, which was contributed by Aaron Sherber. Each developer on this library was either a SparkPost employee or a customer of SparkPost, all of us with an interest in making this library work well.&lt;/p&gt;

&lt;p&gt;As the SparkPost API expands, this library will expand with it. And we’re accepting contributions from anybody who wants to help!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/metrics-api-csharp/"&gt;Using the SparkPost Metrics API with C#&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>c</category>
      <category>csharp</category>
      <category>sdk</category>
      <category>metrics</category>
    </item>
    <item>
      <title>Tracking Recipient Preferences With The User Agent Header in Elixir</title>
      <dc:creator>Ewan Dennis</dc:creator>
      <pubDate>Fri, 13 Oct 2017 13:01:32 +0000</pubDate>
      <link>https://forem.com/sparkpost/tracking-recipient-preferences-with-the-user-agent-header-in-elixir-2j1</link>
      <guid>https://forem.com/sparkpost/tracking-recipient-preferences-with-the-user-agent-header-in-elixir-2j1</guid>
      <description>&lt;h3&gt;
  
  
  Tracking Recipient Preferences With The User Agent Header in Elixir
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Note: this user agent header post illustrates itself using code written in Elixir. If you prefer, you can &lt;a href="https://www.sparkpost.com/blog/preferences-user-agent-header-php/"&gt;read the PHP version&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Much has been made of the relative commercial value of particular groups of people. From super consumers to influencers, &lt;a href="https://www.theguardian.com/technology/2016/nov/02/mobile-web-browsing-desktop-smartphones-tablets"&gt;iPhone users&lt;/a&gt; to desktop holdouts, learning about your recipients’ preferences is &lt;a href="http://uk.businessinsider.com/gap-android-ios-conversion-rates-revenue-per-session-shrinks-iphone-moovweb-2015-6?r=US&amp;amp;IR=T"&gt;clearly important&lt;/a&gt;. In these days of deep personalization, it’s also just nice to know a little more about your customer base. Luckily this is a pretty easy job with SparkPost message events.&lt;/p&gt;

&lt;p&gt;In this post, I’ll review the content of the &lt;code&gt;User-Agent&lt;/code&gt; header, then walk through the process of receiving tracking events from SparkPost’s webhooks facility, parsing your recipients’ User Agent header and using the results to build a simple but extensible report for tracking Operating System preferences. I’ll be using Elixir for the example code in this article but most of the concepts are transferrable to other languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  SparkPost Webhook Engagement Events
&lt;/h3&gt;

&lt;p&gt;SparkPost webhooks offer a low-latency way for your apps to receive detailed tracking events for your email traffic. We’ve written previously about &lt;a href="https://www.sparkpost.com/blog/webhooks-beyond-the-basics/"&gt;how to use them&lt;/a&gt; and &lt;a href="https://www.sparkpost.com/blog/webhooks-infrastructure/"&gt;how they’re built&lt;/a&gt; so you can read some background material if you need to.&lt;/p&gt;

&lt;p&gt;We’ll be focusing on just the &lt;code&gt;click&lt;/code&gt; event here. Each time a recipient clicks on a tracked link in your email, SparkPost generates a &lt;code&gt;click&lt;/code&gt; event that you can receive by webhook. You can grab a sample click event directly from the SparkPost API &lt;a href="https://api.sparkpost.com/api/v1/webhooks/events/samples?events=click"&gt;here&lt;/a&gt;. The most interesting field for our purposes is naturally &lt;code&gt;msys.track_event.user_agent&lt;/code&gt; which contains the full User-Agent header sent by your recipient’s email client when they clicked the link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ "msys": { "track_event": { // ... "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 
(KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36" // ... } } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Grokking The User Agent
&lt;/h3&gt;

&lt;p&gt;Ok so we can almost pick out the important details from that little blob of text. For the dedicated, there’s &lt;a href="http://www.rfcreader.com/#rfc7231_line2082"&gt;a specification&lt;/a&gt; but it’s a tough read. Broadly speaking, we can extract details about the user’s browser, OS and “device from their user agent string.&lt;/p&gt;

&lt;p&gt;For example, from my own user agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6P Build/N4F26O) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…you can tell I’m an Android user with a Huawei Nexus 6P device (and that it’s bang up-to-date ;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat: user agent spoofing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some of you might be concerned about the information our user agent shares with the services we use. As is your right, you can use a browser plugin (&lt;a href="https://chrome.google.com/webstore/detail/user-agent-switcher-for-c/djflhoibgkdhkhhcedjiklpkjnoahfmg"&gt;Chrome&lt;/a&gt;, &lt;a href="https://addons.mozilla.org/en-gb/firefox/addon/user-agent-switcher/"&gt;Firefox&lt;/a&gt;) or built-in browser dev tools to change your user agent string to something less revealing. Some services on the web will alter your experience based on your user agent though so it’s important to know the impact these tools might have on you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Harvesting User Agents From SparkPost Tracking Events
&lt;/h3&gt;

&lt;p&gt;Alright, enough theory. Let’s build out a little webhook service to receive, process and stash user agent details for each click tracked through our SparkPost account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elixir And The Web: Phoenix
&lt;/h3&gt;

&lt;p&gt;The de facto standard way to build web services in Elixir is the &lt;a href="http://phoenixframework.org/"&gt;Phoenix Framework&lt;/a&gt;. If you’re interested in a Phoenix getting started guide, the &lt;a href="https://hexdocs.pm/phoenix/overview.html"&gt;docs are excellent&lt;/a&gt; and the &lt;a href="https://hexdocs.pm/phoenix/up_and_running.html"&gt;Up and Running guide&lt;/a&gt; in particular is a great place to start.&lt;/p&gt;

&lt;p&gt;We’ll assume you already have a basic Phoenix application and focus on adding an HTTP endpoint to accept SparkPost webhook event batches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plug: Composable Modules For The Web&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Elixir comes with a specification called â€˜Plug’ (&lt;a href="https://github.com/elixir-plug/plug"&gt;defined here&lt;/a&gt;) which makes it easy to build up layers of so-called middleware on an HTTP service. The simplest form of plug is a function that accepts a connection and a set of options. We’ll use this form to build up our webhook consumer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling SparkPost Webhooks Requests
&lt;/h3&gt;

&lt;p&gt;Our first task is to create a “pipeline”, which is a sequence of transformations that a connection goes through. A pipeline in Phoenix is just a convenient way to compose a sequence of plugs and apply them to some group of incoming requests.&lt;/p&gt;

&lt;p&gt;We’ll first create a “webhook pipeline and then add plugs to it to handle the various tasks in our service. All this happens in our application’s Router module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create a new plug pipeline to handle SparkPost webhooks requests pipeline :webhook do
# Use the accepts plug plug :accepts, ["json"] end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read more about Phoenix routing and plug pipelines in the &lt;a href="https://hexdocs.pm/phoenix/routing.html"&gt;routing section of the Phoenix docs&lt;/a&gt;. For now, it’s important to realize that each Phoenix application includes an endpoint module which is responsible for setting up basic request processing. This includes automatic JSON parsing, which we’ll rely on here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unpacking SparkPost Events
&lt;/h3&gt;

&lt;p&gt;Our event structure contains a certain amount of nesting which we can now strip out in preparation for consuming the tasty details inside. This is a job for our very first plug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Define a new plug to extract nested event fields def 
unpack_events(conn, _) do first_key_fn = &amp;amp;hd(Map.keys(&amp;amp;1))   
cleanevents = conn.params["_json"] |&amp;gt; Enum.map(fn evt -&amp;gt; 
evt["msys"] end) |&amp;gt; Enum.map(fn evt -&amp;gt; evt[first_key_fn.(evt)] 
end) assign(conn, :events, cleanevents) end # Add the new plug 
to our pipeline pipeline :webhook do plug 
:accepts, ["json"] plug :unpack_events end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a little magic going on here. Our endpoint applies the JSON parser plug to all requests before our pipeline starts. Our &lt;code&gt;unpack_events&lt;/code&gt; plug can then rely upon the &lt;code&gt;_json&lt;/code&gt; param left on the connection JSON parser.&lt;/p&gt;

&lt;p&gt;The rest of &lt;code&gt;unpack_events&lt;/code&gt; is just extracting the contents of the &lt;code&gt;msys&lt;/code&gt; key on each event and the contents of the first key in that object. Finally, our &lt;code&gt;unpack_events&lt;/code&gt; plug stored our unpacked events on a connection param for later plugs to pick up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering Events
&lt;/h3&gt;

&lt;p&gt;Now lets retain just the click events (when we register our webhook with SparkPost later, we can also ask it to send only click events):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def filter_event_types(conn, types) do good_event? = 
&amp;amp;Enum.member?(types, &amp;amp;1["type"]) assign(conn, :events, 
Enum.filter(conn.assigns[:events], good_event?)) end # Apply event 
filtering after unpacking pipeline :webhook do plug :accepts, 
["json"] plug :unpack_events plug :filter_event_types ['click'] end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plug leaves our filtered events on the &lt;code&gt;:events&lt;/code&gt; connection param. &lt;code&gt;filter_event_types&lt;/code&gt; accepts a list of types we care about.&lt;/p&gt;

&lt;p&gt;There’s a lot of detail in a single event. It &lt;em&gt;might&lt;/em&gt; be a good idea to pare things down to just the fields we care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def filter_event_fields(conn, kv) do pick_fields = &amp;amp;Map.take(&amp;amp;1, 
kv) assign(conn, :events, Enum.map(conn.assigns[:events], 
pick_fields)) end # JSON -&amp;gt; unpack -&amp;gt; filter on clicks -&amp;gt; extract 
user_agent fields pipeline :webhook do plug :accepts, ["json"]   
plug :unpack_events plug :filter_event_types, ['click'] plug 
:filter_event_fields, ['user_agent'] end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After The Plug Pipeline: The Controller&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To finish up our webhook request handling, we need a &lt;em&gt;controller&lt;/em&gt; which works after the plug pipeline to process to request and produce a response for the client. Here’s a skeleton Controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule TrackUserAgentsWeb.ApiController do use 
TrackUserAgentsWeb, :controller def webhook(conn, _params) do     
json conn, %{"ok": true} end end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can wire &lt;code&gt;ApiController.webhook/2&lt;/code&gt; to our router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scope "/webhook", TrackUserAgentsWeb do pipe_through :webhook     
post "/", ApiController, :webhook end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we register our web service with SparkPost as a webhook consumer, it’ll make HTTP requests to it containing a JSON payload of events. Now our service has a &lt;code&gt;/webhook&lt;/code&gt; endpoint that accepts JSON, cuts our event batch down to size and responds with a happy little “ok!”.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Our Progress
&lt;/h3&gt;

&lt;p&gt;We can test our service by sending a test batch to it. Luckily, the SparkPost API will &lt;a href="https://developers.sparkpost.com/api/webhooks.html#webhooks-events-samples-get"&gt;generate a test batch&lt;/a&gt; for you on request.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grab a sample webhooks event batch from the SparkPost API: Note: this step uses &lt;a href="https://curl.haxx.se/"&gt;cURL&lt;/a&gt; and &lt;a href="https://stedolan.github.io/jq/"&gt;jq&lt;/a&gt;. You can skip the jq part and remove the results key from the JSON file yourself.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl https://api.sparkpost.com/api/v1/webhooks/events/samples | jq .results &amp;gt; batch.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Start our Phoenix service:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mix phx.server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Send our test batch to the service:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl -XPOST -H "Content-type: application/json" -d @batch.json http://localhost:4000/webhook&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Parsing User-Agent
&lt;/h3&gt;

&lt;p&gt;Now we’re ready to enrich our events with new information. We’ll parse the user agent string and extract the OS using the &lt;a href="https://hex.pm/packages/ua_inspector"&gt;ua_inspector&lt;/a&gt; module. We can easily add this step to the API plug pipeline in our router:&lt;/p&gt;

&lt;p&gt;Note: If you’re following along, remember to add &lt;code&gt;ua_inspector&lt;/code&gt; as a dependency in mix.exs and configure it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def os_name(ua) do if not Map.has_key?(ua, :os) do "unknown" 
else case ua.os do %UAInspector.Result.OS{} -&amp;gt; ua.os.name 
_ -&amp;gt; "unknown" end end end def parse_user_agents(conn, _) do 
events = conn.assigns[:events] |&amp;gt; Enum.map(&amp;amp;Map.put(&amp;amp;1, 
"user_agent", UAInspector.parse(&amp;amp;1["user_agent"]))) |&amp;gt; 
Enum.map(&amp;amp;Map.put(&amp;amp;1, "os", os_name(&amp;amp;1["ua"]))) assign(conn, 
:events, events) end # JSON -&amp;gt; unpack -&amp;gt; filter on clicks -&amp;gt; extract 
user_agent -&amp;gt; filter nulls -&amp;gt; parse pipeline :webhook do plug 
:accepts, ["json"] plug :unpack_events plug :filter_event_types, 
['click'] plug :filter_event_fields, ['user_agent'] plug 
:parse_user_agents end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: not all user agent strings will contain the detail we want (or even make sense at all) so we label all odd-shaped clicks with “OS: unknown”.&lt;/p&gt;

&lt;p&gt;Alright, now we have an array of events containing only interesting fields and with an extra “os field to boot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Report-Ready Summary Data
&lt;/h3&gt;

&lt;p&gt;At this point, we could just list each event and call our report done. However, we’ve come to expect some summarisation in our reports, to simplify the task of understanding. We’re interested in OS trends in our email recipients, which suggests that we should aggregate our results: collect summaries indexed by OS. Maybe we’d even use a &lt;a href="https://developers.google.com/chart/interactive/docs/gallery/piechart"&gt;Google Charts pie chart&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We could stop there citing “exercise for the reader but I always find that frustrating so instead, &lt;a href="https://github.com/SparkPost/elixir-webhook-sample"&gt;here’s a batteries-included implementation&lt;/a&gt; which stores click events summaries in PostgreSQL and renders a simple report using Google Charts.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Exercise For The Reader
&lt;/h3&gt;

&lt;p&gt;I know, I said I wouldn’t do this. Bear with me: if you were paying attention to the implementation steps above, you might have noticed several re-usable elements. Specifically, I drew a few filtering and reporting parameters out for re-use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event type filters&lt;/li&gt;
&lt;li&gt;event field filters&lt;/li&gt;
&lt;li&gt;event “enrichment functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With minimal effort, you could add, filter on and group the &lt;code&gt;campaign_id&lt;/code&gt; field to see OS preference broken down by email campaign. You could also use it as a basis for updating your own user database from bounce events with &lt;code&gt;type=bounce, fields=rcpt_to,bounce_class&lt;/code&gt; and so on.&lt;/p&gt;

&lt;p&gt;I hope this short walkthrough gave some practical insight on using SparkPost webhooks. With a little experimentation, the project could be made to fit into plenty of use cases and I’d be more than happy to accept contributions on that theme. If you’d like to talk more about the user agent header, your own event processing needs, SparkPost webhooks, Elixir or anything else, come find us on &lt;a href="http://slack.sparkpost.com"&gt;Slack&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/user-agent-header-elixir/"&gt;Tracking Recipient Preferences With The User Agent Header in Elixir&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>elixir</category>
      <category>sdk</category>
      <category>metrics</category>
    </item>
    <item>
      <title>Community Spotlight: CodeNewbie</title>
      <dc:creator>Jennifer Lacey</dc:creator>
      <pubDate>Wed, 11 Oct 2017 13:01:51 +0000</pubDate>
      <link>https://forem.com/sparkpost/community-spotlight-codenewbie-1ej</link>
      <guid>https://forem.com/sparkpost/community-spotlight-codenewbie-1ej</guid>
      <description>&lt;p&gt;Have you ever embarked on an adventure to learn a completely new skill set? Did it make you feel overwhelmed with no idea where to start? For many of our community members, that feeling is more common than you’d think. Tons of developers or coders didn’t originally start out on that path, and today we’re chatting with someone who’s making big waves and providing a ton of resources for newbies in the coding community.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meet Saron
&lt;/h3&gt;

&lt;p&gt;Not too long ago, Saron was also new to coding. She worked for a few different start-ups, but became more and more interested in the product side of things – which required a more technical background. So Saron did what any motivated self-starter does – she vowed to teach herself. From there, she started her own bootcamp and conference to help others!&lt;/p&gt;

&lt;p&gt;Today, Saron has built the CodeNewbie community, a growing network of over 15,000 people who join in on weekly twitter chats, listen to podcasts, attend &lt;a href="http://codelandconf.com/"&gt;CodeLand&lt;/a&gt; (check out &lt;a href="https://www.sparkpost.com/blog/running-technical-workshop/"&gt;Cole’s experience teaching a workshop&lt;/a&gt;) and exchange ideas with others working on projects in tech. Saron was gracious enough to share some insights on the community she’s built, her favorite part about her job, and advice for those just starting out. I hope you enjoy!&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter CodeNewbie
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What’s your background in the tech world, how did you get started down this path?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I first got interested in tech after reading the Steve Jobs book. It was the first time I saw the connection between tech and more liberal artsy topics like design and psychology. I then read everything I could find about tech and startups, and eventually worked at a few startups. I worked primarily on the business side of things, but I was always more interested in the product. Because I didn’t have a technical background of any kind, I felt that the only way I could have a say in the product was if I invested in learning how to code. So I quit my job, taught myself for a few months then enrolled in a coding bootcamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did you start CodeNewbie?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I started my coding bootcamp, I was surprised to find that I not only learned a lot, but gained a lot of confidence in what I already knew. I realized that when I was learning on my own, I’d been internalizing the natural failure that comes with coding, and interpreting my struggles as my own stupidity. But in my class, I saw that everyone was struggling! It removed so much of the emotional burden that I could actually focus on learning. The true benefit of the bootcamp was the community. But finding that community is hard if you don’t have $11K and can’t quit your job. So I wanted to create a way for everyone to have a supportive community, and that community is #CodeNewbie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In your opinion, how important is community in the programming world?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s everything. Community is how you get out of your own way. It’s how you get jobs, opportunities, make friends, build partnerships, create meaningful change. I’m here today as a result of the incredibly kind community around me, and that’s what I hope to provide for others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did you start the CodeNewbie podcast?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I decided to do the CodeNewbie Podcast, I’d been doing the #CodeNewbie Twitter chat for about a year. I loved the chat. It was such a great way to connect people and start conversations. But it’s not a great medium for diving deep into a topic or a story. Since I’d previously worked at NPR, I thought that a podcast was a great way to focus on one story, one problem, one idea, and really explore it. Audio is such a flexible medium because it’s one of the few you can consume while partaking in other activities. I felt it was a great way to provide a weekly dose of knowledge and inspiration in the lives of our community members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which episode is your favorite so far and why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is tough! I’d have to say &lt;a href="https://www.codenewbie.org/podcast/building-community-in-a-virtual-world-moderation-tools-in-vr"&gt;Season 1, Episode 2:&lt;/a&gt; “Building community in a virtual world: Moderation tools in VR Cameron Brown. On a content level, I personally learned so much in that conversation and really appreciated the passion from Cameron. You can hear in his voice how much he loves creating a safe space for all. He talks so matter of factly about topics like diversity and inclusion, and how obviously important they are, in a way that I’ve never heard from a developer that absolutely blew me away. On a production level, I got to be more creative with the intro, so mixing that was lots of fun.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are 150 episodes on the CodeNewbie podcast. For new listeners, where should they start?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on their goal. If they’re looking to learn more about CodeNewbie in general and what we’re all about, definitely checkout the episode where my husband interviewed me. For job-hunting inspiration, I loved &lt;a href="https://www.codenewbie.org/podcast/getting-my-first-developer-job"&gt;Paola’s incredibly powerful interview&lt;/a&gt; on her journey to finding a job. To hear how tech and society collide, I thought Carina’s &lt;a href="https://www.codenewbie.org/podcast/algorithms"&gt;episode on algorithms&lt;/a&gt; was brilliant. For an inspiring story on persistence and dedication, definitely checkout &lt;a href="https://www.codenewbie.org/podcast/truck-driver"&gt;George Moore’s episode&lt;/a&gt; “Truck Driver.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are some great resources for all the CodeNewbies out there?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are so many! The obvious ones are Codecademy, Coursera, Khan Academy. But there are also great communities like Girl Develop It, Women Who Code, RailsBridge, Hacker Hours, PyLadies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you have any advice for people who want to begin their own journey in programming but don’t know how to start?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes! Nothing is going to make sense for awhile. It’s going to take a lot of beating your head against the wall, not really knowing how useful what you’re learning is going to be, and not building anything that seems relevant to a job before things start to click. That’s just part of the process. I think newbies obsess over starting with the right thing and seeing immediate results, and this results in people quitting much sooner than they should. So I’d say, pick a language with a large and supportive community (ruby, python, javascript are great for these reasons), pick a free resource that doesn’t require much setup (I usually recommend Codecademy), start and then *stick with it.* Over time, things will fall into place.&lt;/p&gt;

&lt;p&gt;Questions for Saron? Join in on her &lt;a href="https://www.codenewbie.org/chat"&gt;weekly Twitter chats&lt;/a&gt; on Wednesdays at 6PM PST/9PM EST! Questions for us? Reach out on &lt;a href="http://twitter.com/sparkpost"&gt;Twitter&lt;/a&gt; or find us in our &lt;a href="http://slack.sparkpost.com"&gt;community slack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/community-spotlight-codenewbie/"&gt;Community Spotlight: CodeNewbie&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>community</category>
      <category>codenewbie</category>
      <category>interview</category>
    </item>
    <item>
      <title>How to Draw an Owl with React: A Migration Story</title>
      <dc:creator>not shocked</dc:creator>
      <pubDate>Fri, 06 Oct 2017 13:05:44 +0000</pubDate>
      <link>https://forem.com/sparkpost/how-to-draw-an-owl-with-react-a-migration-story-537</link>
      <guid>https://forem.com/sparkpost/how-to-draw-an-owl-with-react-a-migration-story-537</guid>
      <description>

&lt;p&gt;A few months ago, &lt;a href="https://www.sparkpost.com/blog/react-angular-edition/"&gt;I wrote about our adventures&lt;/a&gt; building &lt;a href="https://tools.sparkpost.com/spf/inspector"&gt;a brand new project&lt;/a&gt; using React. Our existing SparkPost app was built many moons ago in the ancient framework known as AngularJS. In that earlier post, I said (and I quote), “So what about that 50,000-line Angular app? We won’t be migrating it to React, at least not directly.” Well, we’re migrating it to React. Directly.&lt;/p&gt;

&lt;p&gt;It wasn’t that I lied, but let’s say I was a victim of my own lofty idealism. Shortly after that article was published, I started in on my plan to improve the Angular app. After almost two weeks of struggling to get Bower out of the project, I was immediately rethinking all of my ideals and life choices. I’d tried one thing and it was hard. It was time to leave ideals behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pivot
&lt;/h3&gt;

&lt;p&gt;In all seriousness, we considered a couple of other wild ideas like writing React inside of Angular or migrating everything one page at a time. Ultimately we realized that the fastest and safest way to rebuild our entire app in a brand new framework was to do exactly what I’d publicly ridiculed: direct migration, i.e. a parallel rebuild. The big question was would SparkPost get behind this big idea?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pitch
&lt;/h3&gt;

&lt;p&gt;Before I go on, I should explain something very clearly. The chance to completely rewrite your existing app from scratch is a rare occurrence in the world of engineering. If you come away from this article thinking, “I want to totally rewrite OUR app!”, you should know that you’re going to need a Very Good Pitch (and some flexible decision makers willing to carve you a good chunk of time).&lt;/p&gt;

&lt;p&gt;That said, there’s tremendous value in the chance to rethink and rebuild your entire user interface as a singular, consistent unit instead of continuing to stack more cards on top of a shakier and shakier house. SparkPost got on board with the rebuild and we started in on a plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Plan
&lt;/h3&gt;

&lt;p&gt;So how do you rebuild an entire web application?&lt;/p&gt;

&lt;p&gt;Step 1: Catalog and build a solid foundation of reusable components, design tools, and critical features like authentication.&lt;/p&gt;

&lt;p&gt;Step 2: &lt;a href="http://knowyourmeme.com/memes/how-to-draw-an-owl"&gt;Draw the rest of the [censored] owl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To be honest, our original plan was only slightly more fleshed out than that, and we’ve shifted and adapted it a lot along the way. Here are a few of the pieces we’ve been most happy about and that have helped us prepare for maximum owl-drawing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus:&lt;/strong&gt;  Our entire application team has always been “full-stack”, bouncing from back end to front end and back. As we’ve grown we’ve recognized a need for a core group of folks who can completely focus on a project like this front-end rebuild. That kind of focus helps us maximize efficiency and create the best foundation possible. It also allows us to be a  bridge to the rest of the team about how this new app is built and, eventually, how they can contribute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modular design&lt;/strong&gt; : Jon Ambas, our front-end engineer and design lead (and my main partner in the early days of dreaming up this plan), has been all-in on creating a fully-featured component library to make our work in React a lot simpler and more visually consistent. We now use &lt;a href="https://sparkpost.github.io/matchbox/?selectedKind=Welcome&amp;amp;selectedStory=to%20Storybook&amp;amp;full=0&amp;amp;down=1&amp;amp;left=1&amp;amp;panelRight=0&amp;amp;downPanel=storybook%2Factions%2Factions-panel"&gt;Matchbox&lt;/a&gt; throughout the app and rarely have to think about CSS for anything except custom corner cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Openness:&lt;/strong&gt; Client-side code is already public when it runs in your browser. Therefore, I saw no reason to keep the code private in a repository.  We don’t expect anyone will use this code but us (please don’t). It’s already been nice to be able to point to examples of things we’re doing and link directly to the code in GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing and Monitoring:&lt;/strong&gt; This has been a big goal of the rebuild work we’re doing. Our Selenium-driven functional tests in Angular were taking hours to run and constantly failing until we finally turned them off completely. In the new app, we’re writing lots of tests (ðŸ‘‹ snapshots) but we decided early on that we’ll never be able to predict every bug with test cases. For that reason, we’re also investing in real-time error logging and monitoring. No more waiting for a customer to report an error introduced in a deploy that went out 8 hours ago. Fast stable tests supplemented by error monitoring is our new way forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Preserve and improve” as a redesign strategy:&lt;/strong&gt; In other words, we’ve preserved the architecture of the app. Leaving most things generally in the places you’d expect to find them. Avoid incredibly drastic changes that would require lots of approvals, user testing, etc. We’ve also taken every opportunity we’ve had to make easy, obvious improvements as we rebuild each part of the app. The result so far feels like an incredibly fresh coat of paint on a familiar base. Which will hopefully help us when we roll the new app out to our existing customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rollout:&lt;/strong&gt; Incremental rollout, to be specific, is the last big part of our very big plan and probably the least-well-defined at the moment. We know we want to incrementally roll the app out to users (randomly and/or by allowing users to opt-in) so we can test out how it works for real use-cases before we unleash it on everyone. We’re still looking at options for this, but getting the rollout right will be one of the most important parts of our plan overall.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Point
&lt;/h3&gt;

&lt;p&gt;The takeaway here is that you should be skeptical of anything I say, especially if it sounds good or idealistic. When it comes to something like application development, being practical is almost always better than being pure.&lt;/p&gt;

&lt;p&gt;Happy owl-drawing!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/react-migration-story/"&gt;How to Draw an Owl with React: A Migration Story&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;


</description>
      <category>react</category>
      <category>howto</category>
      <category>angular</category>
      <category>refactor</category>
    </item>
    <item>
      <title>Using SparkPost Templates with C#</title>
      <dc:creator>Mike Hillyer</dc:creator>
      <pubDate>Mon, 02 Oct 2017 13:05:01 +0000</pubDate>
      <link>https://forem.com/sparkpost/using-sparkpost-templates-with-c-c8h</link>
      <guid>https://forem.com/sparkpost/using-sparkpost-templates-with-c-c8h</guid>
      <description>

</description>
      <category>csharp</category>
      <category>email</category>
      <category>howto</category>
      <category>c</category>
    </item>
    <item>
      <title>Email Events On Your Terms: Webhooks, Databases, AWS, and more!</title>
      <dc:creator>Dave Gray</dc:creator>
      <pubDate>Fri, 29 Sep 2017 13:05:00 +0000</pubDate>
      <link>https://forem.com/sparkpost/email-events-on-your-terms-webhooks-databases-aws-and-more-29l</link>
      <guid>https://forem.com/sparkpost/email-events-on-your-terms-webhooks-databases-aws-and-more-29l</guid>
      <description>&lt;p&gt;How can Webhooks be easier, and searching event data (AKA Message Events) maybe even greater? We’ll try to answer in this post and open source some code along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fevent-data-captioned-web.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fevent-data-captioned-web.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shouting “Show me the data! will earn you funny looks from most people, but not from us here at SparkPost. We are all about the data, both &lt;a href="https://www.sparkpost.com/blog/better-onboarding-experience/" rel="noopener noreferrer"&gt;internally&lt;/a&gt; as we decide what to build, and externally when we’re delivering event data to you via &lt;a href="https://www.sparkpost.com/blog/webhooks-beyond-the-basics/" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt; or &lt;a href="https://www.sparkpost.com/blog/sparkpost-message-events-api/" rel="noopener noreferrer"&gt;Message Events&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fshow-me-the-data-captioned-web.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fshow-me-the-data-captioned-web.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tom Cruise may actually want to see the money, but for our customers, data is king. Many of them make heavy use of our Webhooks (push model) to receive batches of event data via HTTP POST. Others prefer to use our &lt;a href="https://developers.sparkpost.com/api/message-events.html" rel="noopener noreferrer"&gt;Message Events&lt;/a&gt;endpoint, which is a pull model – you’re querying the same events, although data retention is limited to 10 days, as of this writing.&lt;/p&gt;

&lt;p&gt;Now I don’t know about you, but whenever I hear that something is limited, the first thing I want to do is find a way around that limitation. The second thing is to show other people how I did it. In this post, I’m going to show you how to bypass our Message Events data retention limit by rolling your own low-cost queryable event database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building Blocks of a Service
&lt;/h3&gt;

&lt;p&gt;The vision here is to ingest batches of event data, delivered by SparkPost’s Webhooks, and then be able to query that data, ideally for free. At least for cheap. Luckily, there are &lt;a href="https://www.sparkpost.com/docs/tech-resources/webhook-data-streams/" rel="noopener noreferrer"&gt;published best practices&lt;/a&gt; for doing the first part. One way to keep costs down (at least initially) is to use the &lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;AWS free tier,&lt;/a&gt; which is the way we’ll go in this post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fi-cant-believe-you-like-free-stuff-too-web.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fi-cant-believe-you-like-free-stuff-too-web.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, I’ll walk through the services I ended up using, and then briefly discuss what else I tried along the way, and why that didn’t make the cut. Almost everything in this system is defined and deployed &lt;a href="https://github.com/SparkPost/event-data/blob/master/event-data.yaml" rel="noopener noreferrer"&gt;using CloudFormation&lt;/a&gt;, along with pieces from the &lt;a href="https://aws.amazon.com/blogs/compute/introducing-simplified-serverless-application-deplyoment-and-management/" rel="noopener noreferrer"&gt;AWS Serverless Application Model&lt;/a&gt; (SAM). Under the hood, this uses API Gateway as an HTTP listener, and Node.js Lambda functions to “do stuff when requests are received or in response to other interesting events. More on that later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fwebhooks-apig-lambda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fwebhooks-apig-lambda.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;According to the best practices linked above, we need to return 200 OK ASAP, before doing any processing of the request body, where the event data is. So we’ll run &lt;a href="https://github.com/SparkPost/event-data/blob/431c843854ef4220ad0bf9f25e800f3f1b1ccd0b/index.js#L9" rel="noopener noreferrer"&gt;a Lambda&lt;/a&gt;to extract the event data and batch id from the HTTP request and save it to S3. At this point, we’re capturing the data but can’t-do a whole lot with it just yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Databases and Event Data
&lt;/h3&gt;

&lt;p&gt;There are all sorts of options out there when it comes to databases. I chose RDS PostgreSQL since it’s a (somewhat) managed service that’s eligible for the AWS free tier. Also, I’m already familiar with it, and had some &lt;a href="https://github.com/SparkPost/event-data/blob/master/sql/auto-partitioner.sql" rel="noopener noreferrer"&gt;automatic partitioning&lt;/a&gt; code lying around that would be better as open source.&lt;/p&gt;

&lt;p&gt;Now seems like a good time to talk about what didn’t make the cut, especially since there were so many interesting options to choose from. The first database-y thing I considered was &lt;a href="https://aws.amazon.com/athena/" rel="noopener noreferrer"&gt;Athena&lt;/a&gt;, which would let us query directly against S3. Right out of the gate, unfortunately, there’s a snag: Athena isn’t eligible for the free tier, it’s priced based on the amount of data scanned by each query. We get a raw JSON feed from the Webhook, so optimizing the storage of that data to be cost-effective to the query would be its own project.&lt;/p&gt;

&lt;p&gt;Another database I didn’t use is &lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;Dynamo&lt;/a&gt;, which would have been super convenient since AWS SAM bakes in support for it. Event data in combination with the types of queries the system needed to support isn’t a great fit for Dynamo though since it doesn’t allow the number of secondary indexes we’d need in order to efficiently support the wide range of queries that Message Events provides. Dynamo would definitely have been the low-stress option. Using RDS meant I had to poke around a bit more in AWS networking land than I had planned to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting the Data Dots
&lt;/h3&gt;

&lt;p&gt;Our event data is stored in S3, and we’ve chosen a database. Triggers aren’t just for databases, thankfully, and S3 lets you configure Lambda functions to run for various types of events. We’ll fire &lt;a href="https://github.com/SparkPost/event-data/blob/431c843854ef4220ad0bf9f25e800f3f1b1ccd0b/index.js#L72" rel="noopener noreferrer"&gt;our next Lambda&lt;/a&gt; when a file is created in the bucket that our Webhook listener writes to. It’ll read the batch of event data, and load it into our database, which closes the loop. We’re now asynchronously loading event data sent via Webhook into our database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Flambda-s3-pg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Flambda-s3-pg.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only missing piece now is a way to search for specific types of events. We can implement this using AWS SAM as well, which gives us some nice shortcuts. &lt;a href="https://github.com/SparkPost/event-data/blob/431c843854ef4220ad0bf9f25e800f3f1b1ccd0b/index.js#L184" rel="noopener noreferrer"&gt;This last Lambda&lt;/a&gt; is essentially a translator between query parameters and SQL. There are quite a few options for query builders in Node, and I picked &lt;a href="https://hiddentao.com/squel/" rel="noopener noreferrer"&gt;Squel.js&lt;/a&gt;, which was a good balance between simplicity, dependencies, and features.&lt;/p&gt;

&lt;p&gt;This system now achieves what it set out to – we’re storing event data provided via Webhook, following best practices, and can query the data using a familiar interface. And if you need to, it’s straightforward to customize by updating the query_events Lambda to add new ways to pull out the data you need, and indexes can be added to the database to make those custom queries faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Tho, and What Next?
&lt;/h3&gt;

&lt;p&gt;SparkPost sends a lot of data along with our events. For example, transmission metadata lets our customers include things like their own internal user id with each email. Event data such as opens and clicks will now include that user id, making it easier to tie things together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fthat-metadata-really-tied-the-systems-together-web.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2Fthat-metadata-really-tied-the-systems-together-web.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because every customer uses features like metadata differently, it’s nigh impossible for us to give everyone exactly the type of search options they’d like. Running your own event database means you’re free to implement custom search parameters. Many of our larger customers already have systems like this, whether it’s a third party tool or something they built themselves. This project aims to lower the barriers to entry, so anyone with a moderate level of familiarity with AWS and the command line can operate their own event database more easily.&lt;/p&gt;

&lt;p&gt;There are a few things I’d like to do next, for example, setting up authentication on the various endpoints, since as things are now, they’re open to the public. I discuss a solution to this in the repo, since exposing your customer’s email addresses to the public is a no-no.&lt;/p&gt;

&lt;p&gt;I’d also like to perform some volume testing on this system. The free tier RDS database in this setup has 20GB of storage, I’m curious to see how quickly that would fill up. It would also be nice to complete the CloudFormation conversion. Currently, the database is managed separately from the CF stack, and creating the required tables and stored procedures requires punching a hole through the firewall, er, security group. It would be nice to standardize and automate that step as well, instead of requiring mouse clicks in the AWS console.&lt;/p&gt;

&lt;p&gt;Thanks for reading! Give us a shout on &lt;a href="https://twitter.com/sparkpost" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and star, fork or submit a PR on &lt;a href="https://github.com/SparkPost/event-data" rel="noopener noreferrer"&gt;Github&lt;/a&gt;if you enjoyed the post. We’d love to hear about what you build!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/webhooks-databases-aws/" rel="noopener noreferrer"&gt;Email Events On Your Terms: Webhooks, Databases, AWS, and more!&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com" rel="noopener noreferrer"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>aws</category>
      <category>data</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Calm your Suppression List Anxieties with Python</title>
      <dc:creator>SteveT</dc:creator>
      <pubDate>Fri, 22 Sep 2017 13:05:36 +0000</pubDate>
      <link>https://forem.com/sparkpost/calm-your-suppression-list-anxieties-with-python-d92</link>
      <guid>https://forem.com/sparkpost/calm-your-suppression-list-anxieties-with-python-d92</guid>
      <description>&lt;h3&gt;
  
  
  “It takes many good deeds to build a good reputation, and only one bad one to lose it.”
&lt;/h3&gt;

&lt;p&gt;– Benjamin Franklin&lt;/p&gt;

&lt;p&gt;From our handy &lt;a href="https://www.sparkpost.com/docs/getting-started/getting-started-sparkpost/#important-coming-from-other-email-services"&gt;Getting Started Guide&lt;/a&gt;, you know how important it is to bring your suppression list from your old provider with you. Ben Franklin was right – your &lt;a href="https://www.sparkpost.com/blog/email-suppression/"&gt;email reputation&lt;/a&gt; will catch a nasty cold if you send to stale, unsubscribed, bounced addresses. This affects whether your messages to your real subscribers are accepted, now and in the future, so it’s best to heed the doctor’s advice.&lt;/p&gt;

&lt;p&gt;In this article, we’ll set up an easy-to-use tool to manage your suppression lists. If you want some more background on the “what and “why of suppressions, &lt;a href="https://www.sparkpost.com/docs/user-guide/using-suppression-lists/"&gt;this article&lt;/a&gt; is a good starting point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrubbing up
&lt;/h3&gt;

&lt;p&gt;The exported suppression lists we see coming from old providers are often dirty. Duplicate entries, invalid entries with more than one @ sign, invalid characters, telephone numbers instead of email addresses, you name it. We’ve seen files with weird characters in various obscure international alphabets. This might make you consider just amputating those lists, but we’ll explore techniques to preserve as much of them as we can.&lt;/p&gt;

&lt;p&gt;Lists can be large, reaching nearly a million entries. Working with small blocks manually is going to take forever. We are going to need a robot surgeon!&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan for treatment
&lt;/h3&gt;

&lt;p&gt;Let’s set out our needs, and translate them into design goals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make it easy to get started and simple to use.&lt;/li&gt;
&lt;li&gt;Make best efforts to understand your file format, even if it appears to contain weird characters.&lt;/li&gt;
&lt;li&gt;Check and upload any size of a list without manual work.&lt;/li&gt;
&lt;li&gt;Check the input files up front, with helpful warnings as we go.&lt;/li&gt;
&lt;li&gt;Checks should be thorough and fast. If there are faults, we want to know exactly where they are in the file, and what’s wrong. Specifically, we need to:

&lt;ul&gt;
&lt;li&gt;Ensure email addresses are well-formed (i.e. follow the RFCs)&lt;/li&gt;
&lt;li&gt;Check the other field values, such as transactional / non_transactional flags.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;A “check everything but don’t change anything mode, to make it easy to find and fix faulty input data.&lt;/li&gt;
&lt;li&gt;Allow retrieval of your whole suppression list back from SparkPost, or select time-bounded portions.

&lt;ul&gt;
&lt;li&gt;Have time-zone awareness, while accepting times in your locale. In particular, remember that start and end times could fall on either side of a daylight savings time change.&lt;/li&gt;
&lt;li&gt;Keep it simple. The API supports searching by domain, source, type, description etc – however, that can be done by filtering the retrieved file afterward. If you want these features, &lt;a href="https://github.com/tuck1s/sparkySuppress/issues/new"&gt;raise an issue&lt;/a&gt; on the Git repository and we’ll look at it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Work across both master account and subaccounts.&lt;/li&gt;
&lt;li&gt;Make it easy to supply defaults for missing/optional file information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leads us on to making a tool with the following options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check&lt;/strong&gt; the format of your files (prior to import). Always a good idea to bring your suppressions with you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt; your suppression list in SparkPost (i.e. create if your list is currently empty).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve&lt;/strong&gt; your suppression list from SparkPost, for example, if you want to get suppressions back into your upstream campaign management tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt; your suppression list from SparkPost, i.e. clean your suppression list out. Maybe you uploaded some entries by mistake. We hope that’s a rare use-case, but it’s there for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Time to operate
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tuck1s/sparkySuppress"&gt;sparkySuppress&lt;/a&gt; is a tool written in Python to help you manage your suppression list. The &lt;a href="https://github.com/tuck1s/sparkySuppress"&gt;Github repo&lt;/a&gt; includes a comprehensive README file. There’s some help with getting Python 3 installed &lt;a href="https://www.sparkpost.com/blog/sparkpost-message-events-api/"&gt;here&lt;/a&gt; if you need it.&lt;/p&gt;

&lt;p&gt;You can configure sparkySuppress with the &lt;code&gt;sparkpost.ini&lt;/code&gt; file, which is used to set up things you change infrequently, such as your API key, timezone, batch sizes and so on. You can leave everything except API key set to default if you like.&lt;/p&gt;

&lt;p&gt;Email addresses from input files are checked as we go, using the excellent &lt;a href="https://github.com/JoshData/python-email-validator"&gt;email_validator&lt;/a&gt; library. We use this to give comprehensive reporting in case of faulty addresses, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Line 2 ! bad@email@address.com The email address is not valid. It must have exactly one @-sign.
Line 3 ! invalid.email@~{}gmail.com The domain name ~{}gmail.com contains invalid characters (Codepoint U+007E not allowed at position 1 in '~{}gmail.com').
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ! marks the entry as having an error. We’ll mark entries that have recoverable problems with a warning w like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Line 1 w need valid transactional &amp;amp; non_transactional flags: {'recipient': 'test.recip@gmail.com'}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  An excellent character
&lt;/h3&gt;

&lt;p&gt;Text files are not as simple as they appear! Unusual file character encoding can be an obstacle, particularly when you don’t have control over how the suppression list export was created in the first place.&lt;/p&gt;

&lt;p&gt;UTF-8 is the most modern and capable encoding, but some systems may not use it. Output files exported from some older versions of Excel will be in Latin-1 for example, rather than UTF-8.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FileCharacterEncodings&lt;/code&gt; setting in the &lt;code&gt;sparkpost.ini&lt;/code&gt; provides an easy way to control how your input file will be processed. The tool reads your file using each encoding in turn, and if it finds anomalies, will try in the next encoding and so on. So if you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FileCharacterEncodings=utf-8,utf-16,ascii,latin-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will see the tool trying each encoding until it finds one that reads the whole file without error. You can select any of the standard encodings shown &lt;a href="https://docs.python.org/3.6/library/codecs.html#standard-encodings"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="err"&gt;./sparkySuppress.py&lt;/span&gt; &lt;span class="err"&gt;check&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;utf-8&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1125&lt;/span&gt; &lt;span class="err"&gt;'utf-8'&lt;/span&gt; &lt;span class="err"&gt;codec&lt;/span&gt; &lt;span class="err"&gt;can't&lt;/span&gt; &lt;span class="err"&gt;decode&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt; &lt;span class="err"&gt;0x9a&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;position&lt;/span&gt; &lt;span class="err"&gt;7198:&lt;/span&gt; &lt;span class="err"&gt;invalid&lt;/span&gt; &lt;span class="err"&gt;start&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;utf-16&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt; &lt;span class="err"&gt;UTF-16&lt;/span&gt; &lt;span class="err"&gt;stream&lt;/span&gt; &lt;span class="err"&gt;does&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;start&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;BOM&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;ascii&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1125&lt;/span&gt; &lt;span class="err"&gt;'ascii'&lt;/span&gt; &lt;span class="err"&gt;codec&lt;/span&gt; &lt;span class="err"&gt;can't&lt;/span&gt; &lt;span class="err"&gt;decode&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt; &lt;span class="err"&gt;0x9a&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;position&lt;/span&gt; &lt;span class="err"&gt;7198:&lt;/span&gt; &lt;span class="err"&gt;ordinal&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;range(128)&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;latin-1&lt;/span&gt;
        &lt;span class="err"&gt;File&lt;/span&gt; &lt;span class="err"&gt;reads&lt;/span&gt; &lt;span class="err"&gt;OK.&lt;/span&gt;

&lt;span class="err"&gt;Lines&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;file:&lt;/span&gt; &lt;span class="err"&gt;8496&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your first encoding in the list is used when you’re retrieving entries back from SparkPost into a file.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good performance
&lt;/h3&gt;

&lt;p&gt;Delete is a bit special – it uses multi-threading because deletes have to be done one per call. Update and retrieve work fast when single-threaded, as each call handles a batch. You should experience good performance with the default batch size and thread settings, but you can tweak them if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practicing your medicine
&lt;/h3&gt;

&lt;p&gt;In case you don’t have data from your old provider yet, here’s a &lt;a href="https://www.sparkpost.com/blog/recipient-suppression-lists-python/"&gt;tool for creating suppression lists&lt;/a&gt; that you can use to create a dummy file to practice on.&lt;/p&gt;

&lt;p&gt;That’s about it! You are now a skilled suppression list surgeon. You’ll soon have your campaigns in excellent shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  And finally…
&lt;/h3&gt;

&lt;p&gt;If you are exploring this tool and want to give the author feedback, you’re welcome to visit our &lt;a href="http://slack.sparkpost.com"&gt;Community Slack channel&lt;/a&gt;– there’s a channel just for Python, #python. Alternatively, open a Github project &lt;a href="https://github.com/tuck1s/sparkySuppress/issues/new"&gt;issue&lt;/a&gt; or &lt;a href="https://github.com/tuck1s/sparkySuppress/compare"&gt;pull-request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you don’t like Python (whut?) there are some lower-level command-line SparkPost projects that provide a thin “wrapper over the API and can be used to manipulate suppression lists. Check out &lt;a href="https://github.com/SparkPost/node-sparkpost-cli"&gt;Node.js&lt;/a&gt; and &lt;a href="https://github.com/SparkPost/sparkpost-cli"&gt;Go&lt;/a&gt; and if you want to know more about the API and UI for suppression lists, &lt;a href="https://www.sparkpost.com/blog/how-to-view-and-validate-your-suppression-lists/"&gt;here’s a good place to start&lt;/a&gt;. There’s also a &lt;a href="https://www.sparkpost.com/docs/tech-resources/download-suppression-list/"&gt;node.js tool&lt;/a&gt; to retrieve your list back again from SparkPost for checking.&lt;/p&gt;

&lt;p&gt;If you prefer point-and-click, the SparkPost user interface has a built-in &lt;a href="https://app.sparkpost.com/lists/suppressions"&gt;Lists/Suppressions&lt;/a&gt; upload feature. This gives you a nice example template and is ideal when you have perfectly formatted files that aren’t too large, with a maximum of 10,000 recipients per file.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/suppression-list-python/"&gt;Calm your Suppression List Anxieties with Python&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>email</category>
      <category>cli</category>
    </item>
    <item>
      <title>Archiving Emails: A How-To Guide for Tracking Sent Mail</title>
      <dc:creator>jeff-goldstein</dc:creator>
      <pubDate>Wed, 20 Sep 2017 13:05:36 +0000</pubDate>
      <link>https://forem.com/sparkpost/archiving-emails-a-how-to-guide-for-tracking-sent-mail</link>
      <guid>https://forem.com/sparkpost/archiving-emails-a-how-to-guide-for-tracking-sent-mail</guid>
      <description>&lt;p&gt;Not every email sender needs the ability to archive their emails, but for some segments, like Financial or Health Care, tracking sent emails is often a requirement. So I decided to create a sample program that does just that. As with most of my blog posts, I have a &lt;a href="https://github.com/jeff-goldstein/SparkPost-Archive-Tools" rel="noopener noreferrer"&gt;corresponding Github repository&lt;/a&gt; you can take a look at if you’re interested.&lt;/p&gt;

&lt;p&gt;You are more than welcome to use this repository as a starting point to your project or modify and update the repository for others to leverage.&lt;/p&gt;

&lt;p&gt;There are a couple of different ways to support archiving. For this post, I decided to focus on just one method, the “Archive function within the SparkPost SMTP call. Another approach would be to store the template and the data needed to merge into that template. I may address that approach with some sample code in a future post.&lt;/p&gt;

&lt;p&gt;But let’s get back to the approach that we’re covering today. Here is a graphic of the three distinct steps in this process; starting with the email creation down to the archiving of the email body and supporting components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2FScreen-Shot-2017-09-11-at-2.54.02-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.sparkpost.com%2Fuploads%2F2017%2F09%2FScreen-Shot-2017-09-11-at-2.54.02-PM.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a 32,000 foot level, those steps manifest into three development steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up an inbound relay Webhook within SparkPost. Relay Webhooks are a way to instruct SparkPost to accept inbound emails on your behalf and forward it to you over HTTP for your own consumption.&lt;/li&gt;
&lt;li&gt;Create a tool that will receive the Relay Webhook information (the inbound email) in the form of a JSON payload.&lt;/li&gt;
&lt;li&gt;Send an email via the SMTP protocol adding the SparkPost SMTP API header. The SMTP API header will contain the email address of that you will use for your archiving domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 1 and 2 share the “chicken or the egg syndrome. They’re so intertwined you can’t do one without the other so it may not matter which you do first, but I’m going address the setup first because you can’t do full unit testing without getting DNS and the Webhooks in place.&lt;/p&gt;

&lt;p&gt;“&lt;a href="https://www.sparkpost.com/docs/tech-resources/inbound-email-relay-webhook/" rel="noopener noreferrer"&gt;Enabling Inbound Email Relaying &amp;amp; Relay Webhooks&lt;/a&gt; will walk you through the Relay Webhook setup process, but I have a few helpful hints to add. I’ll walk through the steps of the document at a high level, then add some examples that you may find useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up an Inbox Relay Webhook
&lt;/h3&gt;

&lt;p&gt;The first step in the document mentioned above is to set up mx records to have the email for a given domain forwarded to SparkPost for processing. The second step in the aforementioned document has you creating an Inbound Domain using the SparkPost &lt;em&gt;inbound-domain&lt;/em&gt; RESTful API. The body of the call is straightforward with one &lt;em&gt;key/value&lt;/em&gt; pair, with the key named “domain”. For my example, the &lt;em&gt;value&lt;/em&gt; matches my MX entry for the domain named &lt;em&gt;geekwithapersonality.com&lt;/em&gt; (no judgements on the domain name, please). Thus the body of the API call is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"geekwithapersonality.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no UI functionality within SparkPost to set this up, so you must use a client like Postman or an application like CURL. SparkPost does have a &lt;a href="https://app.getpostman.com/run-collection/81ee1dd2790d7952b76a" rel="noopener noreferrer"&gt;Postman collection&lt;/a&gt; that you can leverage, which makes this step extremely easy. Next, you need to tie that inbound-domain to where your application is running. As a footnote, there can only be one relay-webhook application per inbound-domain. In this step you will call the &lt;em&gt;relay-webhooks&lt;/em&gt; RESTful API, with a body similar to this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Replies Webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.geekwithapersonality.com/cgi-bin/inbox-relay.php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auth_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SMTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"geekwithapersonality.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the domain key/value pair uses the same domain value, &lt;em&gt;geekwithapersonality&lt;/em&gt;, as the entry employed in the RESTful API inbound-domain call. This step is the one that ties the two together. So any emails that go to the domain &lt;em&gt;geekwithapersonality.com&lt;/em&gt; will be directed to SparkPost, then translated into a JSON structure for processing. Again, there is no SparkPost UI component for this step, so you need to use something like Postman or CURL to execute this step. Unless you’re an ESP processing inbound messages for many clients, this is typically a very rare task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Collector to Process and Store the Archive Email
&lt;/h3&gt;

&lt;p&gt;Even though the code is sitting in &lt;a href="https://github.com/jeff-goldstein/SparkPost-Archive-Tools" rel="noopener noreferrer"&gt;Github&lt;/a&gt;, I’ll describe the components and the logic for each piece. Overall, this code was fairly straightforward to write. I decide to archive the following components of each email in separate files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All the headers used to send the email. Because the archive email went to a different location than the original email, these headers are NOT 100% equivalent to the original email, but they are close.&lt;/li&gt;
&lt;li&gt;The full JSON structure.&lt;/li&gt;
&lt;li&gt;The text body with the original &lt;em&gt;To&lt;/em&gt; email address, along with the &lt;em&gt;From&lt;/em&gt; and &lt;em&gt;Subject&lt;/em&gt; appended to the top of the text file.&lt;/li&gt;
&lt;li&gt;The HTML body with the original &lt;em&gt;To&lt;/em&gt; email address along with the &lt;em&gt;From&lt;/em&gt; and &lt;em&gt;Subject&lt;/em&gt; appended to the top of the HTML file.&lt;/li&gt;
&lt;li&gt;The rfc822 compliant email. This allows an application to grab the whole file and resend it if necessary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this sample code, I placed each group of files in their own directory and logged each Archive email processed. Beyond placing the data into a directory, I am NOT archiving these items into something more enterprise level like S3 or HP Autonomy. I’ll leave that process for another time, but there are so many CMS or archiving tools out there that creating a dozen different hooks into archiving tools would only scratch the surface. I also want the process to be as fast as possible, so I think the archiving tool should only be loosely coupled and either run off an index/queue/stack of processed items or perhaps the archiving tool will simply look for new directories and relocate them to a more permanent location.&lt;/p&gt;

&lt;p&gt;In order to create a unique directory for each archive, I created directories with a combination of the name of the original email recipient address along with a timestamp.&lt;/p&gt;

&lt;p&gt;The structure of the inbound webhook structure may change over time but at the moment the current structure can be found within the &lt;a href="https://developers.sparkpost.com/api/relay-webhooks.html" rel="noopener noreferrer"&gt;Relay Webhooks Example Payloads&lt;/a&gt; documentation of SparkPost.&lt;/p&gt;

&lt;p&gt;Here are the json values that I grabbed in order to either store them separately or process them further:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;msys/relay_message/content/to: This is the email address for the original target of the content.&lt;/li&gt;
&lt;li&gt;msys/relay_message/rcpt_to: This is the email address used to send the archive message.&lt;/li&gt;
&lt;li&gt;msys/relay_message/friendly_from: This is the from address presented in the original email address.&lt;/li&gt;
&lt;li&gt;msys/relay_message/content/subject: Subject of the email.&lt;/li&gt;
&lt;li&gt;msys/relay_message/content/headers: Once pulled, I format these into a nice html (ok, semi-nice) format for storage and reference.&lt;/li&gt;
&lt;li&gt;msys/relay_message/content/html: The HTML body of the email.&lt;/li&gt;
&lt;li&gt;msys/relay_message/content/text: The text body of the email.&lt;/li&gt;
&lt;li&gt;msys/relay_message/content/email_rfc822: The full email body in rfc822 standard. It’s save as an eml file which allows most email clients to open directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once I get all of this data, I process the headers into an html format. From there, I add the &lt;em&gt;To&lt;/em&gt;, &lt;em&gt;From&lt;/em&gt; and &lt;em&gt;Subject&lt;/em&gt; to the HTML and Text bodies and save everything. Finally, I create a log entry for that specific entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send an Archived Copy of the Email
&lt;/h3&gt;

&lt;p&gt;The Archiving functionality can only be initiated by using the &lt;a href="https://developer.sparkpost.com/api/smtp-api.html" rel="noopener noreferrer"&gt;SMTP API&lt;/a&gt; (which, by the way, is a horribly named industry standard for an added header for your SMTP call). For SparkPost, the SMTP API is actually a JSON formatted header called &lt;em&gt;X-MSYS-API&lt;/em&gt;. In that header, you are able to set items like campaign id, metadata, IP Pools, CC and BCC email addresses in addition to a list of Archiving email addresses. Here is a sample &lt;em&gt;X-MSYS-API&lt;/em&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;X-MSYS-API:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"campaign_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jeffs BBQ Campaign"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"home_address_region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“West”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"smoker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SMB"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"archive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="err"&gt;“archive@mail.geekwithapersonality.com”&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"open_tracking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"click_tracking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"transactional"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ip_pool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sp_shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn’t a complete sample with all of the options available, but illustrates what a typical header looks like. Notice that the original recipient’s email address isn’t in the &lt;em&gt;X-MSYS-API&lt;/em&gt; header, but you’ll get it in the Reply Webhook.&lt;/p&gt;

&lt;p&gt;In the Github repository, I have added a test PHP application that calls SMTP by using the PHPMailer framework. While calling SMTP and adding in the X-MSYS-API header with an archive email address is probably the best approach for thorough unit testing, it isn’t exactly necessary. Any email sent to the domain you are targeting will get processed the same way as an archive email. So I did most of my unit testing by sending an email to my &lt;em&gt;geekwithapersonality&lt;/em&gt; domain, either by a Postman transmission SparkPost API call or simply sending an email via my MAC email client. Neither is really an archive, but the inbox relay doesn’t care and treats all emails the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voila! Archiving Emails is Simple!
&lt;/h3&gt;

&lt;p&gt;Those are the three steps to set up archiving. If you are using SMTP to send emails to SparkPost, you’re probably already using the X-MSYS-API headers, so just adding the archive entry will be easy for you. Then all you need to do is point email for your archiving domain to SparkPost and create a simple app to capture the data.&lt;/p&gt;

&lt;p&gt;I hope this was helpful – if you have any questions on archiving emails, email templates, etc, please feel free to &lt;a href="//mailto:jeff.goldstein@sparkpost.com"&gt;reach out to me&lt;/a&gt; directly.&lt;/p&gt;

&lt;p&gt;Happy Sending!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/archiving-emails/" rel="noopener noreferrer"&gt;Archiving Emails: A How-To Guide for Tracking Sent Mail&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com" rel="noopener noreferrer"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>developer</category>
      <category>features</category>
      <category>sparkpost</category>
      <category>webhooks</category>
    </item>
  </channel>
</rss>
