<?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: Rodrigo Fuentealba Cartes</title>
    <description>The latest articles on DEV Community by Rodrigo Fuentealba Cartes (@devrfc83).</description>
    <link>https://dev.to/devrfc83</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%2F4204183%2F03e465f4-a63f-4406-b393-a84dbaf038eb.png</url>
      <title>DEV Community: Rodrigo Fuentealba Cartes</title>
      <link>https://dev.to/devrfc83</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devrfc83"/>
    <language>en</language>
    <item>
      <title>I Built a Lightweight SMTP-to-Mailgun Bridge in Go (In Just Two Days)</title>
      <dc:creator>Rodrigo Fuentealba Cartes</dc:creator>
      <pubDate>Sat, 24 Oct 2026 08:22:15 +0000</pubDate>
      <link>https://dev.to/devrfc83/i-built-a-lightweight-smtp-to-mailgun-bridge-in-go-in-just-two-days-10n1</link>
      <guid>https://dev.to/devrfc83/i-built-a-lightweight-smtp-to-mailgun-bridge-in-go-in-just-two-days-10n1</guid>
      <description>&lt;p&gt;If you've ever tried to plug a legacy application, an old monitoring script, or an enterprise tool into a modern email delivery service like Mailgun, you've probably hit a brick wall: &lt;strong&gt;your app only speaks SMTP&lt;/strong&gt;, but many cloud providers block outbound port 25 entirely. &lt;/p&gt;

&lt;p&gt;Instead of dealing with heavy traditional mail transfer agents (MTAs) like Postfix or Exim just to relay transactional emails, I decided to build something simpler, lighter, and modern. Meet &lt;strong&gt;Mailgun Passport&lt;/strong&gt;, a minimal SMTP-to-Mailgun bridge written in Go.&lt;/p&gt;

&lt;p&gt;I coded this project from scratch in a little over two days to solve a very specific pain point. Here is how it's built, the technical hurdles I solved along the way, and why it's structured for production from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Keeping it Simple
&lt;/h2&gt;

&lt;p&gt;The core idea is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen:&lt;/strong&gt; A local SMTP server accepts incoming mail (e.g., on &lt;code&gt;127.0.0.1:587&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse:&lt;/strong&gt; It intercepts the raw bytes, parses the MIME structure, and maps SMTP envelope recipients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward:&lt;/strong&gt; It translates everything cleanly into a call to Mailgun’s HTTP API (&lt;code&gt;/v3/{domain}/messages&lt;/code&gt; or raw MIME).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; Go 1.25+ (for top-tier concurrency, small binaries, and great standard libraries).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP Server:&lt;/strong&gt; &lt;a href="https://github.com/emersion/go-smtp" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/emersion/go-smtp&lt;/code&gt;&lt;/a&gt;—the gold standard for building Go mail servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIME Parsing:&lt;/strong&gt; &lt;a href="https://github.com/jhillyerd/enmime" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/jhillyerd/enmime&lt;/code&gt;&lt;/a&gt; to cleanly extract text, HTML, headers, and attachments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging:&lt;/strong&gt; Go's built-in structured logger (&lt;code&gt;log/slog&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two Technical Challenges I Solved
&lt;/h2&gt;

&lt;p&gt;Building an email bridge exposes you to the quirky realities of email RFCs. Here are two highlights from the codebase:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Bridging SMTP Envelopes and MIME Headers (&lt;code&gt;RCPT TO&lt;/code&gt; vs &lt;code&gt;Bcc&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In SMTP, recipients are passed via the envelope command (&lt;code&gt;RCPT TO&lt;/code&gt;), which might not match the &lt;code&gt;To&lt;/code&gt; or &lt;code&gt;Cc&lt;/code&gt; headers inside the MIME message (especially for blind carbon copies, &lt;code&gt;Bcc&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;In &lt;code&gt;parser.go&lt;/code&gt;, the application intelligently reconciles this: if a recipient from the SMTP envelope isn't explicitly listed in the MIME headers, it treats them as a &lt;code&gt;Bcc&lt;/code&gt; automatically, ensuring no recipient gets dropped during the translation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;applyEnvelopeRecipients&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="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bcc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RcptTo&lt;/span&gt;&lt;span class="o"&gt;...&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="c"&gt;// ... maps envelope RCPT TO into Bcc safely if missing from headers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bulletproof Systemd Hardening
&lt;/h3&gt;

&lt;p&gt;Because this service runs on Linux servers, security matters. The companion systemd service file is heavily sandboxed using modern Linux capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privilege separation:&lt;/strong&gt; Runs under a dedicated low-privilege user (&lt;code&gt;mailgun-passport&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port binding without root:&lt;/strong&gt; Uses &lt;code&gt;AmbientCapabilities=CAP_NET_BIND_SERVICE&lt;/code&gt; so it can bind to privileged port 587 without running as &lt;code&gt;root&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem lockdown:&lt;/strong&gt; Enforces &lt;code&gt;ProtectSystem=strict&lt;/code&gt; and isolates temporary paths.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started &amp;amp; Testing Locally
&lt;/h2&gt;

&lt;p&gt;Clone the repository and spin it up in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="o"&gt;[&lt;/span&gt;https://github.com/devrfc83/mailgun-passport.git]&lt;span class="o"&gt;(&lt;/span&gt;https://github.com/devrfc83/mailgun-passport.git&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;mailgun-passport

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env and add your MAILGUN_API_KEY and MAILGUN_DOMAIN&lt;/span&gt;

go run ./cmd/mailgun-passport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test it locally using &lt;strong&gt;swaks&lt;/strong&gt; (the Swiss Army knife for SMTP transactions):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swaks &lt;span class="nt"&gt;--to&lt;/span&gt; dest@example.com &lt;span class="nt"&gt;--from&lt;/span&gt; you@yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server&lt;/span&gt; 127.0.0.1:587 &lt;span class="nt"&gt;--port&lt;/span&gt; 587 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Subject: Hello from Passport"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"It works!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This is just &lt;strong&gt;v0.1.0&lt;/strong&gt;, built over a single weekend to solve a real infrastructure problem. It already supports native RPM/DEB packaging, structured logging, flexible configuration via &lt;code&gt;.env&lt;/code&gt; or systemd, and dual parsing/MIME modes.&lt;/p&gt;

&lt;p&gt;Check out the repository on GitHub: &lt;strong&gt;&lt;a href="https://github.com/devrfc83/mailgun-passport" rel="noopener noreferrer"&gt;devrfc83/mailgun-passport&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If you encounter edge cases with legacy clients, find bugs, or have ideas for improvement, PRs, issues, and stars are very welcome!&lt;/p&gt;




&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;Hi! I'm &lt;strong&gt;Rodrigo Fuentealba Cartes&lt;/strong&gt;, an entrepreneur, Bachelor of Engineering Sciences and Computer and Informatics Engineer. For more than two decades I've combined data analysis with software engineering to take projects from idea to production. Now I offer my services through a small company named &lt;a href="https://www.seadragon.cl/" rel="noopener noreferrer"&gt;Sea Dragon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm also a &lt;a href="https://www.devrfc83.com/en/chess/" rel="noopener noreferrer"&gt;chess&lt;/a&gt; &lt;a href="https://www.devrfc83.com/en/chess/analysis/" rel="noopener noreferrer"&gt;player&lt;/a&gt;, musician, motorcyclist, amateur radio operator (license CA5NFS), and a low performance athlete.&lt;/p&gt;

&lt;p&gt;You can find me around the web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.devrfc83.com/" rel="noopener noreferrer"&gt;https://www.devrfc83.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/devrfc83" rel="noopener noreferrer"&gt;@devrfc83&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instagram:&lt;/strong&gt; &lt;a href="https://instagram.com/devrfc83" rel="noopener noreferrer"&gt;@devrfc83&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>devops</category>
      <category>go</category>
    </item>
  </channel>
</rss>
