Files
website/public/posts/google-groups-spam/index.html
T
2026-03-29 17:20:43 +02:00

619 lines
27 KiB
HTML
Executable File

<!doctype html>
<html
lang="de-DE"
dir="ltr"
class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="language" content="de-DE">
<script>
(function () {
const storedTheme = localStorage.getItem("theme");
const systemPrefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
const theme = storedTheme || (systemPrefersLight ? "light" : "dark");
document.documentElement.setAttribute("data-theme", theme);
})();
</script>
<title>Google Groups Spam | Demians Blog</title>
<meta
name="description"
content="
Understanding the Google Groups Spam Problem Over the past year, I&rsquo;ve noticed a significant increase in spam messages originating from Google Groups. This …
">
<link rel="canonical" href="https://pyte.dev/posts/google-groups-spam/">
<meta name="robots" content="index, follow">
<meta property="og:type" content="article">
<meta property="og:title" content="Google Groups Spam | Demians Blog">
<meta property="og:description" content="Understanding the Google Groups Spam Problem Over the past year, I&rsquo;ve noticed a significant increase in spam messages originating from Google Groups. This …">
<meta property="og:url" content="https://pyte.dev/posts/google-groups-spam/">
<meta property="og:site_name" content="Demians Blog"><meta property="og:image" content="https://pyte.dev/assets/patrick.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"><meta property="article:published_time" content="2025-10-10T09:26:56&#43;02:00">
<meta
property="article:modified_time"
content="2025-10-10T09:26:56&#43;02:00">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Google Groups Spam | Demians Blog">
<meta name="twitter:description" content="Understanding the Google Groups Spam Problem Over the past year, I&rsquo;ve noticed a significant increase in spam messages originating from Google Groups. This …"><meta name="twitter:image" content="https://pyte.dev/assets/patrick.png">
<script type="application/ld+json">
"{\"@context\":\"https://schema.org\",\"@type\":\"BlogPosting\",\"author\":{\"@type\":\"Person\",\"email\":\"demian (at) pyte (dot) dev\",\"name\":\"Demians Blog\"},\"dateModified\":\"2025-10-10T09:26:56+02:00\",\"datePublished\":\"2025-10-10T09:26:56+02:00\",\"description\":\"Understanding the Google Groups Spam Problem Over the past year, I\\u0026rsquo;ve noticed a significant increase in spam messages originating from Google Groups. This …\",\"headline\":\"Google Groups Spam\",\"image\":\"https://pyte.dev/assets/patrick.png\",\"mainEntityOfPage\":{\"@id\":\"https://pyte.dev/posts/google-groups-spam/\",\"@type\":\"WebPage\"},\"publisher\":{\"@type\":\"Organization\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://pyte.dev/assets/patrick.png\"},\"name\":\"Demians Blog\"}}"</script>
<script type="application/ld+json">"{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"item\":\"https://pyte.dev/\",\"name\":\"Demians Blog\",\"position\":1},{\"@type\":\"ListItem\",\"item\":\"https://pyte.dev/posts/\",\"name\":\"Posts\",\"position\":2},{\"@type\":\"ListItem\",\"item\":\"https://pyte.dev/posts/google-groups-spam/\",\"name\":\"Google Groups Spam\",\"position\":3}]}"</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet">
<link
rel="stylesheet"
href="/css/theme.min.86a32b729f656fc6c119ed69193950db815a3ad9fdc967b32c76d602f11449a4.css"
integrity="sha256-hqMrcp9lb8bBGe1pGTlQ24FaOtn9yWezLHbWAvEUSaQ=">
<link
rel="stylesheet"
href="/css/syntax-dark.min.3e403a03e3af837b3829e9b6f01fc7792bda7cb7f5056f5e9786109545c6b2e1.css"
integrity="sha256-PkA6A&#43;Ovg3s4Kem28B/HeSvafLf1BW9el4YQlUXGsuE="
id="syntax-dark-theme"
class="syntax-theme"><link
rel="stylesheet"
href="/css/syntax-light.min.d0d33b879698595e6b2c0f75f0cea95a8517fb0150570cd0ee4dc42e25c8d147.css"
integrity="sha256-0NM7h5aYWV5rLA918M6pWoUX&#43;wFQVwzQ7k3ELiXI0Uc="
id="syntax-light-theme"
class="syntax-theme"
disabled><script>
(function () {
const storedTheme = localStorage.getItem("theme");
const systemPrefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
const theme = storedTheme || (systemPrefersLight ? "light" : "dark");
const syntaxDark = document.getElementById("syntax-dark-theme");
const syntaxLight = document.getElementById("syntax-light-theme");
if (theme === "light") {
if (syntaxDark) syntaxDark.disabled = true;
if (syntaxLight) syntaxLight.disabled = false;
} else {
if (syntaxDark) syntaxDark.disabled = false;
if (syntaxLight) syntaxLight.disabled = true;
}
const observer = new MutationObserver(() => {
const currentTheme = document.documentElement.getAttribute("data-theme");
if (currentTheme === "light") {
if (syntaxDark) syntaxDark.disabled = true;
if (syntaxLight) syntaxLight.disabled = false;
} else {
if (syntaxDark) syntaxDark.disabled = false;
if (syntaxLight) syntaxLight.disabled = true;
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
})();
</script>
<link
rel="stylesheet"
href="/css/bundle.min.783a79746a859af9be598cbc33fba2a1087434b23768524277b07ef28336f113.css"
integrity="sha256-eDp5dGqFmvm&#43;WYy8M/uioQh0NLI3aFJCd7B&#43;8oM28RM=">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/logo-transparent/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/logo-transparent/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/logo-transparent/apple-touch-icon.png">
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/favicon/logo-transparent/android-chrome-192x192.png">
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/favicon/logo-transparent/android-chrome-512x512.png">
<link rel="manifest" href="/favicon/logo-transparent/site.webmanifest">
</head>
<body class="flex flex-col min-h-screen">
<a href="#main-content" class="skip-to-main" aria-label="Skip to main content"
>Skip to main content</a
>
<header class="sticky-header">
<div class="header-container">
<nav class="header-nav" role="navigation" aria-label="Main navigation">
<div class="header-content">
<div class="header-logo">
<a href="/" class="logo-link" aria-label="Home - Demians Blog">
Demians Blog
</a>
</div>
<div class="header-menu">
<ul class="menu-list">
<li class="menu-item">
<a
href="/"
class="menu-link ">
Home
</a>
</li>
<li class="menu-item">
<a
href="/posts/"
class="menu-link ">
Blog
</a>
</li>
</ul>
<button id="search-toggle" class="search-toggle" aria-label="Search" type="button">
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" type="button">
<svg class="icon-sun" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
<svg class="icon-moon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
</button>
</div>
</div>
</nav>
</div>
</header>
<main id="main-content" class="flex-1" role="main">
<div class="single-post-wrapper">
<article class="single-post">
<header class="post-header">
<h1 class="post-title-main">Google Groups Spam</h1>
<div class="post-meta">
<div class="post-meta-info">
<time datetime="2025-10-10T09:26:56&#43;02:00" class="post-date">
<svg class="meta-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
October 10, 2025
</time>
<span class="meta-separator"></span>
<span class="post-word-count">
<svg class="meta-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
722 words
</span>
<span class="meta-separator"></span>
<span class="post-reading-time">
<svg class="meta-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
4 min
</span>
</div>
</div>
</header>
<div class="post-content-main">
<h1 id="understanding-the-google-groups-spam-problem">Understanding the Google Groups Spam Problem</h1>
<p>Over the past year, I&rsquo;ve noticed a significant increase in spam messages originating from Google Groups. This issue stems from the way Google Groups is designed: It gives spammers an easy way to distribute large volumes of unwanted mail using legitimate Google mail servers, which makes filtering much harder.</p>
<h2 id="why-it-happens">Why it Happens</h2>
<p>There are a few fundamental problems with how Google Groups works that make it particularly attractive to spammers:</p>
<ul>
<li>
<p><strong>No opt-in required</strong>: Spammers can freely add any email address to a Google Group without the recipient&rsquo;s consent.</p>
</li>
<li>
<p><strong>Unsubscribing is often impossible</strong>: Many spam groups are set to private, meaning that victims cannot even access the group page to unsubscribe.</p>
</li>
<li>
<p><strong>Amplified spam through auto-responders</strong>: Some of these groups include automated mail systems (like ticketing or vacation responders). When these systems reply to the initial spam message, the responses are redistributed to all group members multiplying the traffic in unwanted mails.</p>
</li>
</ul>
<h2 id="the-challenge-and-approach">The Challenge and Approach</h2>
<p>While analyzing these messages, I discovered that several legitimate organizations also use Google Groups to distribute newsletters or announcements. That means simply blocking all mail coming from Google Groups would cause false positives and disrupt valid communication.</p>
<p>To handle this more effectively, I developed a solution that integrates directly with Rspamd, our spam filtering system:</p>
<ul>
<li>
<p><strong>Custom Lua plugin</strong>: Detects messages originating from Google Groups and assigns a custom symbol.</p>
</li>
<li>
<p><strong>Composite rules</strong>: Use this symbol, combined with other spam indicators, to decide whether a message should be classified as spam.</p>
</li>
</ul>
<p>This approach allows us to target the abusive patterns specifically, without penalizing legitimate use of Google Groups.</p>
<h2 id="technical-configuration">Technical Configuration</h2>
<p>To tackle the Google Groups spam issue, I built a small custom Lua plugin for Rspamd that detects messages originating from Google Groups and assigns a custom symbol to them. Once tagged, we can use composite rules to decide whether a message should be treated as spam based on additional indicators.</p>
<h3 id="step-1-create-the-custom-lua-plugin">Step 1: Create the Custom Lua Plugin</h3>
<p>Start by creating a new file at <code>/etc/rspamd/plugins.d/kits_header_google_group.lua</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="n">rspamd_config</span><span class="p">:</span><span class="n">register_symbol</span><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;KITS_HEADER_GOOGLE_GROUP&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">score</span> <span class="o">=</span> <span class="mf">0.1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">group</span> <span class="o">=</span> <span class="s2">&#34;headers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Message contains X-Google-Group-Id header or List-Unsubscribe header with googlegroups&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">callback</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="c1">-- Check for X-Google-Group-Id header</span>
</span></span><span class="line"><span class="cl"> <span class="kr">if</span> <span class="n">task</span><span class="p">:</span><span class="n">get_header</span><span class="p">(</span><span class="s1">&#39;X-Google-Group-Id&#39;</span><span class="p">)</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl"> <span class="kr">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">-- Check for List-Unsubscribe header containing &#39;googlegroups&#39;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">local</span> <span class="n">list_unsubscribe</span> <span class="o">=</span> <span class="n">task</span><span class="p">:</span><span class="n">get_header</span><span class="p">(</span><span class="s1">&#39;List-Unsubscribe&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="kr">if</span> <span class="n">list_unsubscribe</span> <span class="ow">and</span> <span class="n">string.find</span><span class="p">(</span><span class="n">list_unsubscribe</span><span class="p">:</span><span class="n">lower</span><span class="p">(),</span> <span class="s1">&#39;googlegroups&#39;</span><span class="p">)</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl"> <span class="kr">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kr">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"> <span class="kr">end</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This plugin checks for either of the following headers:</p>
<ul>
<li><code>X-Google-Group-Id</code></li>
<li><code>List-Unsubscribe</code> containing the string <code>googlegroups</code></li>
</ul>
<p>If either is present, the message is tagged with the symbol <code>KITS_HEADER_GOOGLE_GROUP</code>.</p>
<h3 id="step-2-enable-the-plugin">Step 2: Enable the Plugin</h3>
<p>Next, register a module with the same name by creating an empty configuration file at <code>/etc/rspamd/modules.d/kits_header_google_group.conf</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Empty config to enable lua plugin</span>
</span></span><span class="line"><span class="cl">kits_header_google_group <span class="o">{</span> <span class="o">}</span>
</span></span></code></pre></div><p>At this point, every email that originates from a Google Group will be tagged with the symbol <code>KITS_HEADER_GOOGLE_GROUP</code> and adds a score of 0.1.</p>
<h3 id="step-3-create-composite-rules">Step 3: Create Composite Rules</h3>
<p>The next step is to define composite rules that evaluate whether a tagged message is likely to be spam. Create the following entries in <code>/etc/rspamd/override.d/composite.conf</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Google Group origin with bulk or freemail origin</span>
</span></span><span class="line"><span class="cl">KITS_GOOGLE_GROUP_BAD <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="nv">expression</span> <span class="o">=</span> <span class="s2">&#34;KITS_HEADER_GOOGLE_GROUP and (DCC_REJECT | FUZZY_BULK | FREEMAIL_FROM)&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nv">score</span> <span class="o">=</span> 8.0<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Google Group origin with bulk and freemail origin</span>
</span></span><span class="line"><span class="cl">KITS_GOOGLE_GROUP_WORST <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="nv">expression</span> <span class="o">=</span> <span class="s2">&#34;KITS_HEADER_GOOGLE_GROUP and (DCC_REJECT | FUZZY_BULK ) and FREEMAIL_FROM&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nv">score</span> <span class="o">=</span> 20.0<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>These rules assign higher scores to messages that originate from Google Groups and match known spam indicators such as <code>DCC_REJECT</code>, <code>FUZZY_BULK</code>, or <code>FREEMAIL_FROM</code>.</p>
<blockquote class="blockquote-regular">
<p>The naming scheme, scores, and logic here are tailored to my environment. Always adjust the scoring and expressions to fit your setup and verify changes before applying them.</p>
</blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>After deploying this configuration, the amount of spam originating from Google Groups dropped noticeably. Legitimate messages from companies still passed through correctly, while unwanted group spam was effectively flagged or quarantined by Rspamd.</p>
<p>If you&rsquo;re running a mail server, fighting spam is one of the more tedious and ongoing challenges. Thankfully, with Rspamd, we have some of the best and most flexible spam-fighting tools available.</p>
<p>Battling spam will always be a cat-and-mouse game, and I&rsquo;m sure spammers will find new and clever ways to distribute unwanted mail sooner rather than later, but Rspamd will be here to help. I hope to share more useful posts in the future about keeping our mail systems clean with Rspamd.</p>
</div>
<nav class="post-navigation" aria-label="Post navigation">
<a
href="/posts/blocking-invalid-rcpt-postfix/"
class="nav-link nav-prev"
aria-label="Previous post: Blocking Invalid Recipients Before They Reach Your Exchange Server">
<span class="nav-label">Previous</span>
<span class="nav-title">Blocking Invalid Recipients Before They Reach Your Exchange Server</span>
</a>
<a
href="/posts/spaceship-distrobox/"
class="nav-link nav-next"
aria-label="Next post: Spaceship Distrobox">
<span class="nav-label">Next</span>
<span class="nav-title">Spaceship Distrobox</span>
</a>
</nav>
</article>
<aside class="post-toc" id="post-toc" aria-label="Table of contents">
<button
class="toc-toggle"
id="toc-toggle"
aria-expanded="true"
aria-controls="toc-content"
aria-label="Toggle table of contents">
<svg
class="toc-burger-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
<span class="toc-toggle-text">Table of Contents</span>
<svg
class="toc-chevron-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="toc-content" id="toc-content">
<nav class="toc-nav" aria-label="Table of contents">
<nav id="TableOfContents">
<ul>
<li><a href="#why-it-happens">Why it Happens</a></li>
<li><a href="#the-challenge-and-approach">The Challenge and Approach</a></li>
<li><a href="#technical-configuration">Technical Configuration</a>
<ul>
<li><a href="#step-1-create-the-custom-lua-plugin">Step 1: Create the Custom Lua Plugin</a></li>
<li><a href="#step-2-enable-the-plugin">Step 2: Enable the Plugin</a></li>
<li><a href="#step-3-create-composite-rules">Step 3: Create Composite Rules</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</nav>
</nav>
</div>
</aside>
</div>
</main>
<footer>
<footer class="site-footer">
<div class="footer-content">
<p class="footer-text">
&copy;
2026
Demians Blog.
Built with Hugo and Mana ❤️
</p>
<div class="footer-social">
<div class="social-links">
<a
href="https://github.com/pyte1"
target="_blank"
rel="noopener noreferrer"
class="social-link"
aria-label="GitHub">
<svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd"></path>
</svg>
</a>
<a href="mailto:demian%20%28at%29%20pyte%20%28dot%29%20dev" class="social-link" aria-label="Email">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</a>
</div>
</div>
</div>
</footer>
</footer>
<div id="search-modal" class="search-modal" aria-hidden="true" role="dialog" aria-label="Search">
<div class="search-modal-backdrop" id="search-modal-backdrop"></div>
<div class="search-modal-container">
<div class="search-input-wrapper">
<svg class="search-input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
type="search"
id="search-input"
class="search-input"
placeholder="Search..."
autocomplete="off"
aria-label="Search input">
<button class="search-input-clear" id="search-input-clear" aria-label="Clear search">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
<button class="search-modal-close" id="search-modal-close" aria-label="Close search">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="search-results" class="search-results"></div>
</div>
</div>
<button id="scroll-to-top" class="scroll-to-top" aria-label="Scroll to top" type="button">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
</svg>
</button>
<script src="/js/main.min.e60ab79dca7b920b4dc5cf3163ad5ce8794839b60f27778db65782f087be3e27.js" defer></script>
</body>
</html>