<?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>DEV Community: Casey Marlin</title>
    <description>The latest articles on DEV Community by Casey Marlin (@caseymarlin).</description>
    <link>https://dev.to/caseymarlin</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064883%2Ff7e21414-4f93-40ed-9b0e-6d48f15fb858.png</url>
      <title>DEV Community: Casey Marlin</title>
      <link>https://dev.to/caseymarlin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/caseymarlin"/>
    <language>en</language>
    <item>
      <title>What's actually inside an image's DPI tag (and why changing it is lossless)</title>
      <dc:creator>Casey Marlin</dc:creator>
      <pubDate>Sun, 06 Sep 2026 02:56:22 +0000</pubDate>
      <link>https://dev.to/caseymarlin/whats-actually-inside-an-images-dpi-tag-and-why-changing-it-is-lossless-57pb</link>
      <guid>https://dev.to/caseymarlin/whats-actually-inside-an-images-dpi-tag-and-why-changing-it-is-lossless-57pb</guid>
      <description>&lt;p&gt;A print shop rejected a 4000 × 2667 px photo last week because the file "was only 72 DPI". The photo had more than enough pixels for the 8 × 10 in print they were making — at 300 DPI that size only needs 2400 × 3000. Nothing about the image was wrong. One integer in the file header was.&lt;/p&gt;

&lt;p&gt;This post is about where that integer lives, byte by byte, and why rewriting it doesn't touch a single pixel.&lt;/p&gt;

&lt;p&gt;DPI is metadata, not resolution&lt;br&gt;
"Resolution" gets used for two unrelated things:&lt;/p&gt;

&lt;p&gt;Pixel dimensions — how many samples the image has. 4000 × 2667. This is the actual information content.&lt;br&gt;
Density tag — a hint that says "when you put this on paper, space the pixels N to the inch". 72, 96, 300, whatever.&lt;br&gt;
Screens ignore the density tag completely. Browsers map image pixels to CSS pixels and never read it. The tag only matters when something is about to lay ink on a physical surface — or when a form-validation script on an upload portal reads it and refuses your file.&lt;/p&gt;

&lt;p&gt;Print size is always pixels ÷ DPI. A 3000 px wide image tagged 72 DPI "wants" to be 41.7 inches wide. The same file tagged 300 DPI "wants" to be 10 inches. Same pixels. Same sharpness at the same physical size. Different suggestion.&lt;/p&gt;

&lt;p&gt;Where the tag lives: three formats, three places&lt;br&gt;
JPEG — JFIF APP0 segment&lt;br&gt;
Right after the SOI marker (FF D8), most JPEGs carry an APP0 segment with the JFIF header:&lt;/p&gt;

&lt;p&gt;FF E0            APP0 marker&lt;br&gt;
00 10            length (16)&lt;br&gt;
4A 46 49 46 00   "JFIF\0"&lt;br&gt;
01 02            version 1.02&lt;br&gt;
01               density units: 0 = none, 1 = dots/inch, 2 = dots/cm&lt;br&gt;
01 2C            X density (0x012C = 300)&lt;br&gt;
01 2C            Y density&lt;br&gt;
00 00            thumbnail w/h&lt;br&gt;
That's it. Two big-endian uint16 values and a units byte. Rewriting "72 DPI" to "300 DPI" means changing 00 48 to 01 2C at a fixed offset and making sure the units byte is 01. The entropy-coded scan data that follows — the actual compressed pixels — is untouched.&lt;/p&gt;

&lt;p&gt;JPEG — EXIF XResolution / YResolution&lt;br&gt;
Cameras usually write an APP1 EXIF segment too, and it has its own resolution fields: tags 0x011A (XResolution) and 0x011B (YResolution) as RATIONAL values (two uint32s: numerator / denominator), plus 0x0128 ResolutionUnit (2 = inch, 3 = cm).&lt;/p&gt;

&lt;p&gt;So a JPEG can carry the density twice, and they can disagree. Photoshop reads EXIF first. Windows Explorer reads... it depends on the version. A proper tool rewrites both so every reader agrees.&lt;/p&gt;

&lt;p&gt;PNG — the pHYs chunk&lt;br&gt;
PNG stores it in an optional pHYs chunk before IDAT:&lt;/p&gt;

&lt;p&gt;00 00 00 09      length (9)&lt;br&gt;
70 48 59 73      "pHYs"&lt;br&gt;
00 00 2E 23      pixels per unit, X (0x2E23 = 11811)&lt;br&gt;
00 00 2E 23      pixels per unit, Y&lt;br&gt;
01               unit: 1 = metre&lt;br&gt;
xx xx xx xx      CRC32&lt;br&gt;
Notice PNG doesn't store inches. It stores pixels per metre. 300 DPI = 300 / 0.0254 = 11811 px/m. That's why a PNG "set to 300 DPI" sometimes reads back as 299.99 — the metre value is an integer and 300 doesn't divide cleanly.&lt;/p&gt;

&lt;p&gt;If the chunk is absent (most screenshots, most web-exported PNGs), there is no DPI. Any number a viewer shows you is that viewer's default, not a property of the file.&lt;/p&gt;

&lt;p&gt;Why rewriting it is lossless&lt;br&gt;
In all three cases the density fields are in a header segment or chunk that sits before and separate from the compressed image data. Changing them:&lt;/p&gt;

&lt;p&gt;doesn't decode the pixels&lt;br&gt;
doesn't re-encode the pixels&lt;br&gt;
doesn't touch JPEG quantization tables or Huffman data&lt;br&gt;
doesn't touch PNG IDAT chunks&lt;br&gt;
The only thing you have to get right is the PNG CRC (recompute it over the chunk type + data) and, for JPEG, not shifting any offsets — the fields are fixed-width so an in-place overwrite is safe.&lt;/p&gt;

&lt;p&gt;Contrast with what "Image → Resize → 300 DPI" does in most editors: it decodes, optionally resamples, and re-encodes. For JPEG that's a generation of quality loss you never needed to take.&lt;/p&gt;

&lt;p&gt;Reading pHYs in ~15 lines of JavaScript&lt;br&gt;
async function readPngDpi(file) {&lt;br&gt;
  const buf = new Uint8Array(await file.arrayBuffer());&lt;br&gt;
  const dv = new DataView(buf.buffer);&lt;br&gt;
  let off = 8; // skip PNG signature&lt;br&gt;
  while (off &amp;lt; buf.length) {&lt;br&gt;
    const len = dv.getUint32(off);&lt;br&gt;
    const type = String.fromCharCode(...buf.slice(off + 4, off + 8));&lt;br&gt;
    if (type === "pHYs") {&lt;br&gt;
      const ppuX = dv.getUint32(off + 8);&lt;br&gt;
      const unit = buf[off + 16];&lt;br&gt;
      return unit === 1 ? Math.round(ppuX * 0.0254) : null; // px/m → DPI&lt;br&gt;
    }&lt;br&gt;
    if (type === "IDAT") return null; // pHYs must precede IDAT&lt;br&gt;
    off += 12 + len; // length + type + data + CRC&lt;br&gt;
  }&lt;br&gt;
  return null;&lt;br&gt;
}&lt;br&gt;
Writing is the same walk plus a CRC32 and an ArrayBuffer splice. JFIF is even simpler — it's a fixed offset if the APP0 segment is where it usually is (and you should verify the JFIF\0 signature rather than assume).&lt;/p&gt;

&lt;p&gt;The trap on the other side&lt;br&gt;
Because the tag is so easy to change, "increase image DPI" tools are everywhere, and half of them quietly imply that raising the number makes a small image printable. It doesn't. An 800 × 600 px image tagged 300 DPI is a 2.67 × 2 in print. Tagging it 600 makes it a 1.33 × 1 in print. The pixels are the ceiling; the tag is just where you set the ruler.&lt;/p&gt;

&lt;p&gt;The honest workflow: check pixel dimensions ÷ target DPI ≥ print size. If yes, rewrite the tag and stop. If no, you need more pixels — a larger original, a rescan, or an actual upscaler (which is a different, lossy operation).&lt;/p&gt;

&lt;p&gt;I put this in a browser tool&lt;br&gt;
I got tired of opening Photoshop to change one integer, so I built &lt;a href="https://setdpi.com" rel="noopener noreferrer"&gt;SetDPI&lt;/a&gt; — it does the JFIF / EXIF / pHYs rewrite described above entirely in the browser with a FileReader and a Blob. Nothing is uploaded; you can load the page and go offline. There's also a &lt;a href="https://setdpi.com/dpi-checker/" rel="noopener noreferrer"&gt;DPI checker&lt;/a&gt; that reads all three fields and tells you when they disagree or when the PNG simply has no pHYs chunk, instead of inventing 72.&lt;/p&gt;

&lt;p&gt;If you're building anything that handles user-uploaded images, the takeaway is smaller than the tool: read the density fields, don't trust them, and never resample a file that only needed its label fixed.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>images</category>
    </item>
    <item>
      <title>CSV to vCard: the version minefield (and how to stop imports garbling names)</title>
      <dc:creator>Casey Marlin</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:12:49 +0000</pubDate>
      <link>https://dev.to/caseymarlin/csv-to-vcard-the-version-minefield-and-how-to-stop-imports-garbling-names-4ho0</link>
      <guid>https://dev.to/caseymarlin/csv-to-vcard-the-version-minefield-and-how-to-stop-imports-garbling-names-4ho0</guid>
      <description>&lt;p&gt;Every phone and both big contact clouds will happily &lt;em&gt;export&lt;/em&gt; your&lt;br&gt;
address book — and then be remarkably picky about what they re-import.&lt;br&gt;
If you've ever moved contacts via a spreadsheet and ended up with garbled&lt;br&gt;
names or missing phone labels, the culprit is almost always the same:&lt;br&gt;
&lt;strong&gt;which vCard you generated&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a vCard actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.vcf&lt;/code&gt; is plain text, one &lt;code&gt;BEGIN:VCARD … END:VCARD&lt;/code&gt; block per person.&lt;br&gt;
A well-behaved 3.0 card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN:VCARD
VERSION:3.0
N:García;José;;;
FN:José García
TEL;TYPE=CELL:+34600000001
EMAIL;TYPE=INTERNET:jose@example.com
ORG:Acme GmbH
END:VCARD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two name fields, and you need both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;N&lt;/code&gt;&lt;/strong&gt; is structured — &lt;code&gt;Family;Given;Middle;Prefix;Suffix&lt;/code&gt;. The
semicolons are separators, so a semicolon &lt;em&gt;inside&lt;/em&gt; a name must be
escaped as &lt;code&gt;\;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FN&lt;/code&gt;&lt;/strong&gt; is the display name, and it's &lt;em&gt;required&lt;/em&gt; in 3.0/4.0. Omit it
and some importers silently drop the card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the fact almost nobody uses: &lt;strong&gt;one .vcf can hold your entire address&lt;br&gt;
book&lt;/strong&gt; — just concatenate the blocks. No zips, no file-per-contact.&lt;/p&gt;
&lt;h2&gt;
  
  
  The version minefield
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Non-ASCII handling&lt;/th&gt;
&lt;th&gt;In 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2.1&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QUOTED-PRINTABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everything imports it; nothing should still write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1998&lt;/td&gt;
&lt;td&gt;plain UTF-8&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The safe target.&lt;/strong&gt; Google Contacts exports it; iOS/iCloud, Android, Outlook all import it cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;plain UTF-8&lt;/td&gt;
&lt;td&gt;Nicest spec, patchiest importer support — still&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you've ever opened a .vcf and seen &lt;code&gt;=E4=BD=A0=E5=A5=BD&lt;/code&gt; where a name&lt;br&gt;
should be, that's vCard 2.1's quoted-printable encoding — the #1 source&lt;br&gt;
of "my contacts imported as gibberish". The fix is boring: &lt;strong&gt;generate&lt;br&gt;
3.0, in UTF-8, and move on.&lt;/strong&gt; 4.0 gains you nothing for a contact dump&lt;br&gt;
and costs you compatibility.&lt;/p&gt;
&lt;h2&gt;
  
  
  From CSV rows to cards
&lt;/h2&gt;

&lt;p&gt;The annoying part isn't the serializer — it's that every ecosystem's CSV&lt;br&gt;
headers differ. Google's export says &lt;code&gt;Given Name&lt;/code&gt; / &lt;code&gt;Family Name&lt;/code&gt;,&lt;br&gt;
Outlook's says &lt;code&gt;First Name&lt;/code&gt; / &lt;code&gt;Last Name&lt;/code&gt;, and your hand-made sheet says&lt;br&gt;
whatever it says. Any converter is really a &lt;em&gt;column-mapping&lt;/em&gt; problem&lt;br&gt;
with 25 lines of formatting bolted on:&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;esc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;??&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;;&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/,/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&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;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN:VCARD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VERSION:3.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`N:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;;;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`FN:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;`TEL;TYPE=CELL:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;`TEL;TYPE=WORK:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;`EMAIL;TYPE=INTERNET:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;`ORG:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;END:VCARD&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&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;vcf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&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;card&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire rules are the same as iCalendar, because the formats share ancestry&lt;br&gt;
(RFC 2425's directory profile): &lt;strong&gt;CRLF line endings&lt;/strong&gt;, long lines folded&lt;br&gt;
at 75 octets with a space starting each continuation line.&lt;/p&gt;

&lt;p&gt;Phone numbers: keep E.164 (&lt;code&gt;+34600000001&lt;/code&gt;) if you can. Importers don't&lt;br&gt;
validate numbers, but sync engines dedupe far better when the format is&lt;br&gt;
consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before you point it at your phone
&lt;/h2&gt;

&lt;p&gt;My test loop: import the file into &lt;strong&gt;Google Contacts&lt;/strong&gt; first — it&lt;br&gt;
reports how many cards it read, and can undo. Then sync, AirDrop the&lt;br&gt;
.vcf to an iPhone, or open it in Android's Contacts app. If Google reads&lt;br&gt;
297 of 300 cards, the missing three almost always have an unescaped&lt;br&gt;
semicolon or a blank &lt;code&gt;FN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the mapping-table part I ended up building&lt;br&gt;
&lt;a href="https://csv-to-vcf.com" rel="noopener noreferrer"&gt;a browser-based CSV ⇆ vCard converter&lt;/a&gt; with an&lt;br&gt;
editable column-mapping step — it reads your headers, guesses the&lt;br&gt;
mapping, and lets you correct the guess before writing 3.0 cards. Free&lt;br&gt;
and client-side; nothing uploads.&lt;/p&gt;

&lt;p&gt;But honestly, for a one-off with clean columns, the 25 lines above will&lt;br&gt;
do. The format survived 30 years by being boring — write the boring&lt;br&gt;
version of it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Bulk-adding events to a calendar app: the .ics format from scratch</title>
      <dc:creator>Casey Marlin</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:41:41 +0000</pubDate>
      <link>https://dev.to/caseymarlin/bulk-adding-events-to-a-calendar-app-the-ics-format-from-scratch-578g</link>
      <guid>https://dev.to/caseymarlin/bulk-adding-events-to-a-calendar-app-the-ics-format-from-scratch-578g</guid>
      <description>&lt;p&gt;Last month I had a spreadsheet with ~40 shifts — date, start, end, who's&lt;br&gt;
on call — and needed them in Google Calendar. The obvious route is&lt;br&gt;
Google's CSV import. It exists, and it half-works: dates get parsed&lt;br&gt;
US-style (&lt;code&gt;03/04&lt;/code&gt; is March 4th whether you meant that or not), there's no&lt;br&gt;
timezone column, all-day events hinge on a magic &lt;code&gt;All Day Event&lt;/code&gt; column,&lt;br&gt;
and malformed rows are dropped without a word. Apple Calendar won't take&lt;br&gt;
CSV at all.&lt;/p&gt;

&lt;p&gt;The format every calendar app &lt;em&gt;does&lt;/em&gt; agree on is &lt;strong&gt;iCalendar&lt;/strong&gt; — the&lt;br&gt;
&lt;code&gt;.ics&lt;/code&gt; file, RFC 5545. And it's just text, so you can generate it&lt;br&gt;
yourself.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal .ics file
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//demo//csv-export//EN
BEGIN:VEVENT
UID:20260901-0900-standup@example.com
DTSTAMP:20260812T120000Z
DTSTART;TZID=Europe/Berlin:20260901T090000
DTEND;TZID=Europe/Berlin:20260901T100000
SUMMARY:Team standup
END:VEVENT
END:VCALENDAR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One &lt;code&gt;VEVENT&lt;/code&gt; block per spreadsheet row. Three properties are mandatory:&lt;br&gt;
&lt;code&gt;UID&lt;/code&gt;, &lt;code&gt;DTSTAMP&lt;/code&gt;, &lt;code&gt;DTSTART&lt;/code&gt;. A useful side effect of &lt;code&gt;UID&lt;/code&gt;: calendar apps&lt;br&gt;
dedupe on it, so re-importing a corrected file doesn't create 40&lt;br&gt;
duplicates. Derive it from the row data — don't randomize it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dates are where imports go to die
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.ics&lt;/code&gt; has three flavors of timestamp, and mixing them up is the #1&lt;br&gt;
source of "my events are six hours off":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DTSTART:20260901T090000Z&lt;/code&gt; — trailing &lt;code&gt;Z&lt;/code&gt; means UTC.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DTSTART;TZID=Europe/Berlin:20260901T090000&lt;/code&gt; — wall-clock time in a
named IANA timezone. Usually what you want.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DTSTART:20260901T090000&lt;/code&gt; — no &lt;code&gt;Z&lt;/code&gt;, no &lt;code&gt;TZID&lt;/code&gt;: "floating" time,
rendered in whatever timezone the viewer's calendar uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All-day events are their own trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DTSTART;VALUE=DATE:20260901
DTEND;VALUE=DATE:20260902
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DTEND&lt;/code&gt; is &lt;strong&gt;exclusive&lt;/strong&gt; — a one-day event ends on the &lt;em&gt;next&lt;/em&gt; date.&lt;br&gt;
Skip the +1 and multi-day events render one day short.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two rules nobody reads the spec for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Line endings are CRLF.&lt;/strong&gt; Most parsers forgive plain &lt;code&gt;\n&lt;/code&gt;; Outlook&lt;br&gt;
historically hasn't. Join with &lt;code&gt;\r\n&lt;/code&gt; and stop worrying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Lines fold at 75 octets.&lt;/strong&gt; Long &lt;code&gt;DESCRIPTION&lt;/code&gt; values get split, and&lt;br&gt;
each continuation line starts with a single space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESCRIPTION:Quarterly review with the plat
 form team — bring the Q3 numbers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's 75 &lt;em&gt;octets&lt;/em&gt;, not characters — emoji and CJK text hit the limit&lt;br&gt;
sooner because UTF-8 is multibyte. And inside TEXT values, &lt;code&gt;,&lt;/code&gt; &lt;code&gt;;&lt;/code&gt; &lt;code&gt;\&lt;/code&gt;&lt;br&gt;
must be backslash-escaped; literal newlines become &lt;code&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up in JavaScript
&lt;/h2&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;esc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;??&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;,;&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;$1&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&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;fold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;75&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;line&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;out&lt;/span&gt; &lt;span class="o"&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;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;74&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ch&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="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt; &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN:VEVENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`UID:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@yourdomain.example`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`DTSTAMP:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;-:&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\.\d{3}&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`DTSTART;TZID=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`DTEND;TZID=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`SUMMARY:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;END:VEVENT&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;ics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN:VCALENDAR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VERSION:2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRODID:-//you//csv-to-ics//EN&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;rows&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;event&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;END:VCALENDAR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve it as &lt;code&gt;text/calendar&lt;/code&gt;, or hand it to&lt;br&gt;
&lt;code&gt;URL.createObjectURL(new Blob([ics]))&lt;/code&gt; for a download link. Recurring&lt;br&gt;
events are one property away (&lt;code&gt;RRULE:FREQ=WEEKLY;BYDAY=MO,WE&lt;/code&gt;) — a&lt;br&gt;
rabbit hole worth its own post.&lt;/p&gt;

&lt;p&gt;I eventually packaged all of this — column mapping, timezone selection,&lt;br&gt;
folding, escaping, the all-day off-by-one — into&lt;br&gt;
&lt;a href="https://csvtoics.app" rel="noopener noreferrer"&gt;csvtoics.app&lt;/a&gt;, a free browser-based converter&lt;br&gt;
(File API end to end, nothing uploads). As of this week it also reads&lt;br&gt;
.xlsx directly — SheetJS in the browser — so the Save-As-CSV step, and&lt;br&gt;
the locale date-mangling that comes with it, is gone entirely. Useful if&lt;br&gt;
you have a spreadsheet right now and no appetite for the spec.&lt;/p&gt;

&lt;p&gt;Either way: after generating, import into Google Calendar &lt;em&gt;and&lt;/em&gt; open the&lt;br&gt;
file in Apple Calendar or Outlook once. The strict parsers are the ones&lt;br&gt;
that find your bugs.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What's actually inside an .eml file (and how to read one without Outlook)</title>
      <dc:creator>Casey Marlin</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:03:55 +0000</pubDate>
      <link>https://dev.to/caseymarlin/whats-actually-inside-an-eml-file-and-how-to-read-one-without-outlook-3jp1</link>
      <guid>https://dev.to/caseymarlin/whats-actually-inside-an-eml-file-and-how-to-read-one-without-outlook-3jp1</guid>
      <description>&lt;p&gt;Someone forwards you an email as a &lt;code&gt;.eml&lt;/code&gt; attachment. You double-click it,&lt;br&gt;
and… nothing useful happens. On Windows it tries to launch Outlook; on a&lt;br&gt;
work laptop with no mail client you get a wall of raw text. So what &lt;em&gt;is&lt;/em&gt;&lt;br&gt;
this file, and how do you read it programmatically?&lt;/p&gt;
&lt;h2&gt;
  
  
  An .eml file is just text
&lt;/h2&gt;

&lt;p&gt;Despite the scary-looking contents, a &lt;code&gt;.eml&lt;/code&gt; file is a plain-text file in&lt;br&gt;
&lt;strong&gt;MIME format&lt;/strong&gt; (RFC 5322 + RFC 2045). Open one in a text editor and the&lt;br&gt;
top looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;From&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Alice &amp;lt;alice@example.com&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;To&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Bob &amp;lt;bob@example.com&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Q3 numbers&lt;/span&gt;
&lt;span class="nt"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Tue, 12 Aug 2026 09:14:00 +0000&lt;/span&gt;
&lt;span class="nt"&gt;MIME-Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; 1.0&lt;/span&gt;
&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; multipart/mixed; boundary="_boundary_42"&lt;/span&gt;

--_boundary_42
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable

&amp;lt;p&amp;gt;Here are the numbers you asked for.&amp;lt;/p&amp;gt;

--_boundary_42
Content-Type: application/pdf; name="report.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"

JVBERi0xLjQKJc... (base64 continues)
--_boundary_42--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Headers&lt;/strong&gt; come first, one per line, until the first blank line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Content-Type: multipart/...; boundary="..."&lt;/code&gt;&lt;/strong&gt; means the body is
split into parts, separated by &lt;code&gt;--&amp;lt;boundary&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each part has its own headers and its own &lt;strong&gt;encoding&lt;/strong&gt; — usually
&lt;code&gt;quoted-printable&lt;/code&gt; for text and &lt;code&gt;base64&lt;/code&gt; for attachments.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reading it in JavaScript
&lt;/h2&gt;

&lt;p&gt;The nice thing about MIME being plain text is that you can parse it in the&lt;br&gt;
browser with the File API — no server, no upload. The skeleton:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readEml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Split headers from body at the first blank line&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&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;rawHeaders&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;splitOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Parse headers into a map (careful: values can wrap onto&lt;/span&gt;
  &lt;span class="c1"&gt;//    continuation lines that start with whitespace)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawHeaders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. If multipart, split the body on the boundary and recurse&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&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;boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/boundary="&lt;/span&gt;&lt;span class="se"&gt;?([^&lt;/span&gt;&lt;span class="sr"&gt;";&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctype&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="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;boundary&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;boundary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&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;parseMimePart&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;headers&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parts&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 two places people get bitten:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header folding.&lt;/strong&gt; A long &lt;code&gt;Subject:&lt;/code&gt; can wrap onto the next line if that
line starts with a space or tab. Un-fold before you parse, or you'll
truncate subjects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoded words.&lt;/strong&gt; Non-ASCII headers look like
&lt;code&gt;=?UTF-8?B?SsOpc3Vz?=&lt;/code&gt; (RFC 2047). Decode them or "José" turns into
line noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attachments are just the base64 part decoded into a &lt;code&gt;Blob&lt;/code&gt;, which you can&lt;br&gt;
hand straight to a download link:&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;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                              &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The gotchas that make it a real project
&lt;/h2&gt;

&lt;p&gt;A toy parser handles the happy path. A robust one has to deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.emlx&lt;/code&gt; (Apple Mail) — an &lt;code&gt;.eml&lt;/code&gt; wrapped with a length prefix and a
plist footer you have to strip.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.msg&lt;/code&gt; (Outlook) — &lt;strong&gt;not&lt;/strong&gt; MIME at all; it's an OLE compound binary
file that needs a completely different parser.&lt;/li&gt;
&lt;li&gt;Inline images referenced by &lt;code&gt;cid:&lt;/code&gt; in the HTML body, which live as
separate MIME parts.&lt;/li&gt;
&lt;li&gt;Rendering untrusted HTML safely (sandboxed iframe, scripts stripped) so
a phishing sample can't run anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I got tired of re-solving this, so I built a free browser-based viewer that&lt;br&gt;
handles all of the above —&lt;br&gt;
&lt;a href="https://emltool.com" rel="noopener noreferrer"&gt;emltool.com&lt;/a&gt; — everything is parsed locally with the&lt;br&gt;
File API, nothing is uploaded. Source of truth if you just need to &lt;em&gt;open&lt;/em&gt;&lt;br&gt;
an &lt;code&gt;.eml&lt;/code&gt;/&lt;code&gt;.emlx&lt;/code&gt;/&lt;code&gt;.msg&lt;/code&gt; file without installing anything.&lt;/p&gt;

&lt;p&gt;Hope the format breakdown saves someone an afternoon of squinting at&lt;br&gt;
base64. If you're implementing your own parser, the RFC 2047 encoded-word&lt;br&gt;
decoding is the part worth testing hardest.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>email</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
