<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- generator="FeedCreator 1.8" --><?xml-stylesheet href="https://www.splitbrain.org/lib/exe/css.php?s=feed" type="text/css"?><rss version="2.0">
    <channel xmlns:g="http://base.google.com/ns/1.0">
        <title>Andreas Gohr: Weblog [splitbrain.org]</title>
        <description>Weblog on technology, programming and personal stuff by Andreas Gohr.</description>
        <link>https://www.splitbrain.org/</link>
        <lastBuildDate>Sun, 05 Apr 2026 20:21:50 +0000</lastBuildDate>
        <generator>FeedCreator 1.8</generator>
        <image>
            <url>https://www.splitbrain.org/_media/wiki/favicon.ico</url>
            <title>splitbrain.org</title>
            <link>https://www.splitbrain.org/</link>
        </image>
        <item>
            <title>Auto-Delete old Mails from GMail</title>
            <link>https://www.splitbrain.org/blog/2026-03/08-auto_delete_old_mails_from_gmail</link>
            <description>
&lt;h1 class="sectionedit1" id="auto-delete_old_mails_from_gmail"&gt;Auto-Delete old Mails from GMail&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
My inbox is a mess. While I manage to maintain a somewhat close to “Inbox Zero” at work, my personal GMail&amp;#039;s inbox currently sits at 2,215  emails &lt;img src="https://www.splitbrain.org/lib/images/smileys/eek.svg" class="icon smiley" alt="8-O" /&gt;. A large amount of those mails are notifications of some kind, like from Github for example.
&lt;/p&gt;

&lt;p&gt;
GMail has some mechanisms to automatically sort mails and I do make use of that, but those filters are applied when the mail arrives only.
&lt;/p&gt;

&lt;p&gt;
For notifications I want to see the mail. So it should land in my inbox. But when I have ignored it for a certain time, it&amp;#039;s pretty sure I don&amp;#039;t need it anymore. So I wanted a way to automatically delete certain mail threads that are older than a specified number of days.
&lt;/p&gt;

&lt;p&gt;
So here is what I came up with using &lt;a href="https://developers.google.com/apps-script/" class="urlextern" title="https://developers.google.com/apps-script/"&gt;Google Apps Script&lt;/a&gt;.
&lt;/p&gt;
&lt;pre class="code file javascript"&gt;&lt;span class="coMULTI"&gt;/**
 * Main script to schedule
 *
 * Add one cleanEmails line per type of threads to clean
 */&lt;/span&gt;
&lt;span class="kw1"&gt;function&lt;/span&gt; main&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
  cleanEmails&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="kw2"&gt;true&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; &lt;span class="st0"&gt;'&amp;lt;notifications@github.com&amp;gt;'&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; &lt;span class="nu0"&gt;7&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;
&amp;nbsp;
&amp;nbsp;
&lt;span class="coMULTI"&gt;/**
 * Find old thread based on given conditions
 * 
 * @param {bool} clean Set to false to not actually delete the mails
 * @param {string} searchString The string to search for
 * @param {int} minAgeDays The minimum age a thread (newest message) has to have
 * @param 
 */&lt;/span&gt;
&lt;span class="kw1"&gt;function&lt;/span&gt; cleanEmails&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;clean&lt;span class="sy0"&gt;,&lt;/span&gt; searchString&lt;span class="sy0"&gt;,&lt;/span&gt; minAgeDays&lt;span class="sy0"&gt;,&lt;/span&gt; headerField &lt;span class="sy0"&gt;=&lt;/span&gt; &lt;span class="st0"&gt;'from'&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
&amp;nbsp;
  &lt;span class="kw1"&gt;const&lt;/span&gt; threads &lt;span class="sy0"&gt;=&lt;/span&gt; findRecentEmails&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;searchString&lt;span class="sy0"&gt;,&lt;/span&gt; minAgeDays&lt;span class="sy0"&gt;,&lt;/span&gt; headerField&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
  console.&lt;span class="me1"&gt;log&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;`Found $&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;threads.&lt;span class="me1"&gt;length&lt;/span&gt;&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt; threads`&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&amp;nbsp;
  threads.&lt;span class="me1"&gt;forEach&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;thread&lt;span class="sy0"&gt;,&lt;/span&gt; i&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="sy0"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
    logThread&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;thread&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
    &lt;span class="kw1"&gt;if&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;clean&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
      thread.&lt;span class="me1"&gt;moveToTrash&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
      console.&lt;span class="me1"&gt;log&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="st0"&gt;'moved to Trash'&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
    &lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;
  &lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;
&amp;nbsp;
&lt;span class="coMULTI"&gt;/**
 * Log a thread to console
 * 
 * @param {Thread} The gmail thread to log
 */&lt;/span&gt;
&lt;span class="kw1"&gt;function&lt;/span&gt; logThread&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;thread&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; firstMsg &lt;span class="sy0"&gt;=&lt;/span&gt; thread.&lt;span class="me1"&gt;getMessages&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#91;&lt;/span&gt;&lt;span class="nu0"&gt;0&lt;/span&gt;&lt;span class="br0"&gt;&amp;#93;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt; &lt;span class="co1"&gt;// newest message in thread&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; lastMsgDate &lt;span class="sy0"&gt;=&lt;/span&gt; thread.&lt;span class="me1"&gt;getLastMessageDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; dateStr &lt;span class="sy0"&gt;=&lt;/span&gt; Utilities.&lt;span class="me1"&gt;formatDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;
    lastMsgDate&lt;span class="sy0"&gt;,&lt;/span&gt;
    Session.&lt;span class="me1"&gt;getScriptTimeZone&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt;
    &lt;span class="st0"&gt;'yyyy-MM-dd HH:mm'&lt;/span&gt;
  &lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&amp;nbsp;
  console.&lt;span class="me1"&gt;log&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;`&lt;span class="kw4"&gt;Date&lt;/span&gt;&lt;span class="sy0"&gt;:&lt;/span&gt; $&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;dateStr&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; From&lt;span class="sy0"&gt;:&lt;/span&gt; $&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;firstMsg.&lt;span class="me1"&gt;getFrom&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; Subject&lt;span class="sy0"&gt;:&lt;/span&gt; $&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;thread.&lt;span class="me1"&gt;getFirstMessageSubject&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;`&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;
&amp;nbsp;
&lt;span class="coMULTI"&gt;/**
 * Find old thread based on given conditions
 * 
 * @param {string} searchString The string to search for
 * @param {int} minAgeDays The minimum age a thread (newest message) has to have
 * @param 
 */&lt;/span&gt;
&lt;span class="kw1"&gt;function&lt;/span&gt; findRecentEmails&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;searchString&lt;span class="sy0"&gt;,&lt;/span&gt; minAgeDays&lt;span class="sy0"&gt;,&lt;/span&gt; headerField &lt;span class="sy0"&gt;=&lt;/span&gt; &lt;span class="st0"&gt;'from'&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;
  &lt;span class="co1"&gt;// Compute the cutoff date (messages must be older than this)&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; cutoffDate &lt;span class="sy0"&gt;=&lt;/span&gt; &lt;span class="kw1"&gt;new&lt;/span&gt; &lt;span class="kw4"&gt;Date&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="kw4"&gt;Date&lt;/span&gt;.&lt;span class="me1"&gt;now&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="sy0"&gt;-&lt;/span&gt; minAgeDays &lt;span class="sy0"&gt;*&lt;/span&gt; &lt;span class="nu0"&gt;24&lt;/span&gt; &lt;span class="sy0"&gt;*&lt;/span&gt; &lt;span class="nu0"&gt;60&lt;/span&gt; &lt;span class="sy0"&gt;*&lt;/span&gt; &lt;span class="nu0"&gt;60&lt;/span&gt; &lt;span class="sy0"&gt;*&lt;/span&gt; &lt;span class="nu0"&gt;1000&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; cutoffFormatted &lt;span class="sy0"&gt;=&lt;/span&gt; Utilities.&lt;span class="me1"&gt;formatDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;
    cutoffDate&lt;span class="sy0"&gt;,&lt;/span&gt;
    Session.&lt;span class="me1"&gt;getScriptTimeZone&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt;
    &lt;span class="st0"&gt;'yyyy/MM/dd'&lt;/span&gt;
  &lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&amp;nbsp;
  &lt;span class="co1"&gt;// Build Gmail search query:&lt;/span&gt;
  &lt;span class="co1"&gt;//   - headerField:searchString  → search in that header&lt;/span&gt;
  &lt;span class="co1"&gt;//   - before:YYYY/MM/DD         → older than cutoff&lt;/span&gt;
  &lt;span class="co1"&gt;//   - -is:starred               → skip starred&lt;/span&gt;
  &lt;span class="kw1"&gt;const&lt;/span&gt; query &lt;span class="sy0"&gt;=&lt;/span&gt; `$&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;headerField&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="sy0"&gt;:&lt;/span&gt;$&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;searchString&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt; before&lt;span class="sy0"&gt;:&lt;/span&gt;$&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;cutoffFormatted&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt; &lt;span class="sy0"&gt;-&lt;/span&gt;is&lt;span class="sy0"&gt;:&lt;/span&gt;starred`&lt;span class="sy0"&gt;;&lt;/span&gt;
  console.&lt;span class="me1"&gt;log&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;`Query&lt;span class="sy0"&gt;:&lt;/span&gt; $&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;query&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;`&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&amp;nbsp;
  &lt;span class="co1"&gt;// get the 50 newest threads with their newest message older than the cut-off date&lt;/span&gt;
  &lt;span class="kw1"&gt;return&lt;/span&gt; GmailApp.&lt;span class="me1"&gt;search&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;query&lt;span class="sy0"&gt;,&lt;/span&gt; &lt;span class="nu0"&gt;0&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; &lt;span class="nu0"&gt;75&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;
          .&lt;span class="me1"&gt;filter&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;thread &lt;span class="sy0"&gt;=&amp;gt;&lt;/span&gt; thread.&lt;span class="me1"&gt;getLastMessageDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="sy0"&gt;&amp;lt;&lt;/span&gt; cutoffDate&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;
          .&lt;span class="me1"&gt;sort&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;a&lt;span class="sy0"&gt;,&lt;/span&gt; b&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="sy0"&gt;=&amp;gt;&lt;/span&gt; b.&lt;span class="me1"&gt;getLastMessageDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt; &lt;span class="sy0"&gt;-&lt;/span&gt; a.&lt;span class="me1"&gt;getLastMessageDate&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;
          .&lt;span class="me1"&gt;slice&lt;/span&gt;&lt;span class="br0"&gt;&amp;#40;&lt;/span&gt;&lt;span class="nu0"&gt;0&lt;/span&gt;&lt;span class="sy0"&gt;,&lt;/span&gt; &lt;span class="nu0"&gt;50&lt;/span&gt;&lt;span class="br0"&gt;&amp;#41;&lt;/span&gt;&lt;span class="sy0"&gt;;&lt;/span&gt;
&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;
The script is scheduled to run &lt;code&gt;main()&lt;/code&gt; every hour. For now it only handles my Github notification mails, but more will follow. The &lt;code&gt;cleanEmails()&lt;/code&gt; function takes four parameters:
&lt;/p&gt;
&lt;ol&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;code&gt;clean&lt;/code&gt;: set it to false if you the method to run but not delete anything. The found threads will be printed to the console.&lt;/div&gt;
&lt;/li&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;code&gt;searchString&lt;/code&gt;: this is what identifies the threads to delete, for example the sender&amp;#039;s email address&lt;/div&gt;
&lt;/li&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;code&gt;minAgeDays&lt;/code&gt;: defines how old the youngest message in the thread has to be to be considered for deletion&lt;/div&gt;
&lt;/li&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;code&gt;headerField&lt;/code&gt;: defaults to &lt;code&gt;from&lt;/code&gt; but you sometimes might want to search the subject instead.&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
Each call to &lt;code&gt;cleanEmails()&lt;/code&gt; will delete the newest 50 threads matching your criteria. And by delete I mean move them to the “Trash” folder – GMail will delete them automatically after 30 days. This adds a bit of safety if something goes wrong.
&lt;/p&gt;

&lt;p&gt;
With the script running every hour it will take a while until all my old Github notifications are gone, but that&amp;#039;s fine. I heard that GMail can become sluggish when deleting too many mails at once.
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dappscript" title="Show pages matching 'appscript'" class="wikilink1"&gt;appscript&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dgmail" title="Show pages matching 'gmail'" class="wikilink1"&gt;gmail&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dautomation" title="Show pages matching 'automation'" class="wikilink1"&gt;automation&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Djavascript" title="Show pages matching 'javascript'" class="wikilink1"&gt;javascript&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;4361-4446&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2017-01/30-save_gmail_attachments_to_google_drive" class="wikilink1" title="blog:2017-01:30-save_gmail_attachments_to_google_drive" data-wiki-id="blog:2017-01:30-save_gmail_attachments_to_google_drive"&gt;Save Gmail Attachments to Google Drive&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/09-gmail_backup_notmuch_gui" class="wikilink1" title="blog:2026-01:09-gmail_backup_notmuch_gui" data-wiki-id="blog:2026-01:09-gmail_backup_notmuch_gui"&gt;Gmail Backup and notmuch GUI&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/02-password_hasher_pwa" class="wikilink1" title="blog:2026-01:02-password_hasher_pwa" data-wiki-id="blog:2026-01:02-password_hasher_pwa"&gt;Password Hasher PWA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-03/08-auto_delete_old_mails_from_gmail#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Sun, 08 Mar 2026 12:50:07 +0000</pubDate>
        </item>
        <item>
            <title>Old Game, New Clothes</title>
            <link>https://www.splitbrain.org/blog/2026-03/02-old_game_new_clothes</link>
            <description>
&lt;h1 class="sectionedit1" id="old_game_new_clothes"&gt;Old Game, New Clothes&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
&lt;a href="https://splitbrain.github.io/zignum/" class="media" title="https://splitbrain.github.io/zignum/"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-03/zignum.png?w=400&amp;amp;tok=0495d7" class="mediaright" align="right" loading="lazy" title="ZigNum" alt="ZigNum" width="400" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
Back in university in 2003 we had to do a group project. I don&amp;#039;t remember which course it was, but the assignment was pretty open. Basically as long as you coded something, you passed.
&lt;/p&gt;

&lt;p&gt;
I had just switched from my trusty &lt;a href="https://en.wikipedia.org/wiki/Palm_Tungsten" class="interwiki iw_wp" title="https://en.wikipedia.org/wiki/Palm_Tungsten"&gt;Palm Tungsten E2&lt;/a&gt; PDA to a Linux based &lt;a href="https://en.wikipedia.org/wiki/Sharp_Zaurus" class="interwiki iw_wp" title="https://en.wikipedia.org/wiki/Sharp_Zaurus"&gt;Sharp Zaurus&lt;/a&gt; and I was missing a little puzzle game I enjoyed playing on the Palm. So my friend Frank and I decided to implement our own version.
&lt;/p&gt;

&lt;p&gt;
The Zaurus used QT as the graphics framework so we made it in C++ and QT. It was a fun project and it not only worked on the Zaurus but also on desktop. Frank even managed to compile it for Windows.
&lt;/p&gt;

&lt;p&gt;
The time of PDAs was soon over and the age of the smartphone had begun. From time to time I thought about creating an Android app of that game. Unfortunately I had lost the original source code, so I asked Frank if he still had it and luckily he found it on an old disk.
&lt;/p&gt;

&lt;p&gt;
Good thing he sent the code to my gmail account where it got archived forever. Because that was back in 2011 and I never came around to actually make that app.
&lt;/p&gt;

&lt;p&gt;
But recently I remembered. And since we&amp;#039;re living in the future (a much shittier version of the future than I had hoped, but the future nonetheless) porting code is relatively simple now.
&lt;/p&gt;

&lt;p&gt;
So I spent an afternoon with Claude Code, having it analyze the original code, come up with some initial &lt;a href="https://github.com/splitbrain/zignum/commit/4fb02a0eb1512c49cb498937c15abb1e8ccf7d22" class="urlextern" title="https://github.com/splitbrain/zignum/commit/4fb02a0eb1512c49cb498937c15abb1e8ccf7d22"&gt;requirements&lt;/a&gt; and a final &lt;a href="https://github.com/splitbrain/zignum/commit/f8da5d790e517e008bdf93a987b4cd2427e216ac" class="urlextern" title="https://github.com/splitbrain/zignum/commit/f8da5d790e517e008bdf93a987b4cd2427e216ac"&gt;implementation plan&lt;/a&gt;. Then I let it run and it basically one-shot a working implementation &lt;img src="https://www.splitbrain.org/lib/images/smileys/surprised.svg" class="icon smiley" alt=":-O" /&gt;.
&lt;/p&gt;

&lt;p&gt;
There are many valid criticisms on LLMs and their billionaire owners. But that LLMs don&amp;#039;t work isn&amp;#039;t one of them…
&lt;/p&gt;

&lt;p&gt;
I iterated a bit on the implementation and now have a modern version of the old game and it even includes a network mode.
&lt;/p&gt;

&lt;p&gt;
The game uses modern ES6 JavaScript modules and web components, the networking is based on WebRTC (using &lt;a href="https://peerjs.com/" class="urlextern" title="https://peerjs.com/"&gt;PeerJS&lt;/a&gt;), it has an automatic dark mode, you can install it as an offline app thanks to PWA technology and does not even need a build system. Another tiny part of the future that doesn&amp;#039;t suck.
&lt;/p&gt;

&lt;p&gt;
If you want to give it a try, here you go: &lt;a href="https://splitbrain.github.io/zignum/" class="urlextern" title="https://splitbrain.github.io/zignum/"&gt;ZigNum&lt;/a&gt;. I find the computer quite difficult to beat – despite the rather naive “AI” approach we picked back in 2003. But maybe I&amp;#039;m just rusty.
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dgame" title="Show pages matching 'game'" class="wikilink1"&gt;game&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dllm" title="Show pages matching 'llm'" class="wikilink1"&gt;llm&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dai" title="Show pages matching 'ai'" class="wikilink1"&gt;ai&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dzignum" title="Show pages matching 'zignum'" class="wikilink1"&gt;zignum&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Djs" title="Show pages matching 'js'" class="wikilink1"&gt;js&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dpwa" title="Show pages matching 'pwa'" class="wikilink1"&gt;pwa&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;2586-2661&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/02-password_hasher_pwa" class="wikilink1" title="blog:2026-01:02-password_hasher_pwa" data-wiki-id="blog:2026-01:02-password_hasher_pwa"&gt;Password Hasher PWA&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2023-08/15-using_sqlite_as_vector_store_in_php" class="wikilink1" title="blog:2023-08:15-using_sqlite_as_vector_store_in_php" data-wiki-id="blog:2023-08:15-using_sqlite_as_vector_store_in_php"&gt;Using SQLite as Vector Store in PHP&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2019-11/03-detective_day_berlin_2019" class="wikilink1" title="blog:2019-11:03-detective_day_berlin_2019" data-wiki-id="blog:2019-11:03-detective_day_berlin_2019"&gt;Detective Day Berlin 2019&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-03/02-old_game_new_clothes#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Mon, 02 Mar 2026 17:13:31 +0000</pubDate>
        </item>
        <item>
            <title>LAN Overview 2026</title>
            <link>https://www.splitbrain.org/blog/2026-02/07-lan_overview_2026</link>
            <description>
&lt;h1 class="sectionedit1" id="lan_overview_2026"&gt;LAN Overview 2026&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
The last time that I blogged about my personal &lt;abbr title="Local Area Network"&gt;LAN&lt;/abbr&gt; setup was more than &lt;a href="https://www.splitbrain.org/blog/2015-10/03-network_upgrade" class="wikilink1" title="blog:2015-10:03-network_upgrade" data-wiki-id="blog:2015-10:03-network_upgrade"&gt;a decade ago&lt;/a&gt;. Meanwhile we moved into our own house and one of the major things we did, was adding Ethernet to every room.
&lt;/p&gt;

&lt;p&gt;
So let&amp;#039;s have a look at the current state:
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://www.splitbrain.org/_media/blog/2026-02/network.png" class="media" title="blog:2026-02:network.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-02/network.png?w=800&amp;amp;tok=edb836" class="media" loading="lazy" alt="" width="800" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
This only shows the devices connected by Ethernet in detail. I added a few placeholders for the various devices connected by WiFi. Zigbee and Bluetooth devices are also omitted. I probably forgot something, too.
&lt;/p&gt;

&lt;p&gt;
I am connected to the Internet via a 1GBit/s (TV) cable connection. It&amp;#039;s great when it works, but unfortunately the service isn&amp;#039;t the most reliable. To get Vodafone support help me faster, I opted to connect their shitty modem instead of my own Fritzbox to the line. This way they can run their access tests and don&amp;#039;t have me unplug (and thus mutilate my network) everytime I have them on the phone. The modem runs as a simple forwarder.
&lt;/p&gt;

&lt;p&gt;
I am still using a Fritzbox to run &lt;abbr title="Domain Name System"&gt;DNS&lt;/abbr&gt; and DHCP but often think about replacing it with a less consumery device where I have more control. But so far I have been too lazy. I also like their mesh network mechanism for the repeaters.
&lt;/p&gt;

&lt;p&gt;
Everything else is pretty straight forward. I haven&amp;#039;t dropped PowerLan completely - it&amp;#039;s more reliable for extending the network out to the shed than WiFi.
&lt;/p&gt;

&lt;p&gt;
The diagram was created using &lt;a href="https://github.com/stan-smith/FossFLOW" class="urlextern" title="https://github.com/stan-smith/FossFLOW"&gt;FossFLOW&lt;/a&gt; and I vibecoded a &lt;a href="https://github.com/splitbrain/isoico" class="urlextern" title="https://github.com/splitbrain/isoico"&gt;little tool&lt;/a&gt; to create the icons I was missing using the gpt-image-1 &lt;abbr title="Application Programming Interface"&gt;API&lt;/abbr&gt;.
&lt;/p&gt;

&lt;p&gt;
For my own reference in the future, here&amp;#039;s the FossFLOW file: &lt;a href="https://www.splitbrain.org/_media/blog/2026-02/network.json" class="media mediafile mf_json" title="blog:2026-02:network.json (1.2 MB)"&gt;network.json&lt;/a&gt;
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dnetwork" title="Show pages matching 'network'" class="wikilink1"&gt;network&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dlan" title="Show pages matching 'lan'" class="wikilink1"&gt;lan&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dhouse" title="Show pages matching 'house'" class="wikilink1"&gt;house&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dinfrastructure" title="Show pages matching 'infrastructure'" class="wikilink1"&gt;infrastructure&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dpowerlan" title="Show pages matching 'powerlan'" class="wikilink1"&gt;powerlan&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Drouter" title="Show pages matching 'router'" class="wikilink1"&gt;router&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;1703-1801&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2015-10/03-network_upgrade" class="wikilink1" title="blog:2015-10:03-network_upgrade" data-wiki-id="blog:2015-10:03-network_upgrade"&gt;Network Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2010-11/07-lan_upgrade" class="wikilink1" title="blog:2010-11:07-lan_upgrade" data-wiki-id="blog:2010-11:07-lan_upgrade"&gt;LAN Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2017-11/12-network_trouble" class="wikilink1" title="blog:2017-11:12-network_trouble" data-wiki-id="blog:2017-11:12-network_trouble"&gt;Network Trouble&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-02/07-lan_overview_2026#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Sat, 07 Feb 2026 18:02:14 +0000</pubDate>
        </item>
        <item>
            <title>Appy - An AppImage Installer</title>
            <link>https://www.splitbrain.org/blog/2026-01/20-appy_an_appimage_installer</link>
            <description>
&lt;h1 class="sectionedit1" id="appy_-_an_appimage_installer"&gt;Appy - An AppImage Installer&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
I nerd-sniped myself today. All I wanted to do was installing &lt;a href="https://github.com/dweymouth/supersonic" class="urlextern" title="https://github.com/dweymouth/supersonic"&gt;SuperSonic&lt;/a&gt;, a subsonic compatible music player written in go. One of their recommended ways to run it is an AppImage.
&lt;/p&gt;

&lt;p&gt;
An AppImage is basically an ELF executable which contains a SquashFS with the application and all dependencies it needs. It&amp;#039;s a simple way to distribute a Linux binary.
&lt;/p&gt;

&lt;p&gt;
Running an AppImage is super simple. You download it, you make it executable you start it.
&lt;/p&gt;

&lt;p&gt;
But for graphical tools, you probably want it integrated into your system&amp;#039;s menu with a nice icon. For that you need to copy the AppImage to a more permanent location than your download folder, create a &lt;code&gt;.desktop&lt;/code&gt; file in &lt;code&gt;.local/share/applications&lt;/code&gt; and also place an icon in &lt;code&gt;.local/share/icons&lt;/code&gt;. Now you need to remember the &lt;code&gt;.desktop&lt;/code&gt; specification and also need to find a proper icon for your app. All not very difficult but tedious.
&lt;/p&gt;

&lt;p&gt;
So I looked into available AppImage “installers” that would automate this process. I wasn&amp;#039;t happy with what I found. It all seemed more complicated than what I wanted.
&lt;/p&gt;

&lt;p&gt;
So I spent a few hours co-working with Claude to come up with my own solution called “Appy”. It&amp;#039;s a single go binary which can install and register itself as default handler for &lt;code&gt;.AppImage&lt;/code&gt; files.
&lt;/p&gt;

&lt;p&gt;
Once installed, you can download the AppImage, double click it in your file manager and get a single button to “install” it.
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://www.splitbrain.org/_detail/blog/2026-01/appy.png?id=blog%3A2026-01%3A20-appy_an_appimage_installer" class="media" title="blog:2026-01:appy.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-01/appy.png" class="mediacenter" loading="lazy" alt="" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
You click the button, the AppImage is copied to &lt;code&gt;~/Applications&lt;/code&gt;, chmod&amp;#039;ed and the icon and desktop file are extracted from the AppImage itself and installed at the right places. And when you later want to “uninstall” the app again, you right click it in your &lt;code&gt;~/Applications&lt;/code&gt; folder, select “Open with… Appy” and you can uninstall it with another click of a button.
&lt;/p&gt;

&lt;p&gt;
You can find &lt;a href="https://github.com/splitbrain/appy" class="urlextern" title="https://github.com/splitbrain/appy"&gt;Appy at Github&lt;/a&gt;.
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dgolang" title="Show pages matching 'golang'" class="wikilink1"&gt;golang&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dappimage" title="Show pages matching 'appimage'" class="wikilink1"&gt;appimage&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dlinux" title="Show pages matching 'linux'" class="wikilink1"&gt;linux&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dinstaller" title="Show pages matching 'installer'" class="wikilink1"&gt;installer&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;1948-2027&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2025-12/01-my_first_nixos_upgrade" class="wikilink1" title="blog:2025-12:01-my_first_nixos_upgrade" data-wiki-id="blog:2025-12:01-my_first_nixos_upgrade"&gt;My first NixOS Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2025-08/26-off-site_backup_with_borgmatic" class="wikilink1" title="blog:2025-08:26-off-site_backup_with_borgmatic" data-wiki-id="blog:2025-08:26-off-site_backup_with_borgmatic"&gt;Off-Site Backup with Borgmatic&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2025-08/03-diy_nas_on_nixos" class="wikilink1" title="blog:2025-08:03-diy_nas_on_nixos" data-wiki-id="blog:2025-08:03-diy_nas_on_nixos"&gt;DIY NAS on NixOS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/20-appy_an_appimage_installer#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Tue, 20 Jan 2026 21:14:46 +0000</pubDate>
        </item>
        <item>
            <title>Gmail Backup and notmuch GUI</title>
            <link>https://www.splitbrain.org/blog/2026-01/09-gmail_backup_notmuch_gui</link>
            <description>
&lt;h1 class="sectionedit1" id="gmail_backup_and_notmuch_gui"&gt;Gmail Backup and notmuch GUI&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
I am still working adding services to my &lt;a href="https://www.splitbrain.org/blog/2025-08/03-diy_nas_on_nixos" class="wikilink1" title="blog:2025-08:03-diy_nas_on_nixos" data-wiki-id="blog:2025-08:03-diy_nas_on_nixos"&gt;NAS&lt;/a&gt; and one thing that was on my todo list for a while was a proper GMail backup.
&lt;/p&gt;

&lt;p&gt;
You never know when the powers that be deny you access to your mails for some obscure reasons. One guard is using my own domain. When I a can&amp;#039;t use GMail anymore, I can just use my own mail server or a different provider. But it would suck losing all my old mail.
&lt;/p&gt;

&lt;/div&gt;
&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;section&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;Gmail Backup and notmuch GUI&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;gmail_backup_and_notmuch_gui&amp;quot;,&amp;quot;codeblockOffset&amp;quot;:0,&amp;quot;secid&amp;quot;:1,&amp;quot;range&amp;quot;:&amp;quot;1-470&amp;quot;} --&gt;
&lt;h2 class="sectionedit2" id="backup"&gt;Backup&lt;/h2&gt;
&lt;div class="level2"&gt;

&lt;p&gt;
I don&amp;#039;t necessarily need a running IMAP server to access the backup. But the backup should be in a format that allows my to point an IMAP server to the backup and be ready to go. That means I want the mails in a Maildir directory.
&lt;/p&gt;

&lt;p&gt;
For the backup I am using GMail&amp;#039;s IMAP server. To fetch your mails by IMAP you need to enable IMAP, have 2fa enabled (as you should in general) and create an app password. You also want to hide some of the default tags from IMAP. Basically only the INBOX, your sent mail and all your custom tags should be exposed. This avoids downloading a bunch of duplicates.
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://www.splitbrain.org/_detail/blog/2026-01/gmail.png?id=blog%3A2026-01%3A09-gmail_backup_notmuch_gui" class="media" title="blog:2026-01:gmail.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-01/gmail.png?w=800&amp;amp;tok=d85fa1" class="mediacenter" loading="lazy" title="The GMail settings" alt="The GMail settings" width="800" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
To do the actual syncing, I decided to use the mbsync command from the isync package. More specifically I am using the &lt;a href="https://github.com/theohbrothers/docker-isync" class="urlextern" title="https://github.com/theohbrothers/docker-isync"&gt;theohbrothers/docker-isync&lt;/a&gt; Docker image.
&lt;/p&gt;

&lt;p&gt;
The image will run one sync, then exit. I added a sleep after execution and then let Docker&amp;#039;s restart policy taking care of running the next sync.
&lt;/p&gt;
&lt;dl class="code"&gt;
&lt;dt&gt;&lt;a href="https://www.splitbrain.org/_export/code/blog/2026-01/09-gmail_backup_notmuch_gui?codeblock=0" title="Download Snippet" class="mediafile mf_yml"&gt;compose.yml&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;pre class="code yaml"&gt;&lt;span class="co4"&gt;services&lt;/span&gt;:&lt;span class="co4"&gt;
  mbsync&lt;/span&gt;:&lt;span class="co3"&gt;
    image&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;theohbrothers/docker-isync:latest&lt;span class="co3"&gt;
    container_name&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;mbsync&lt;span class="co3"&gt;
    restart&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;unless-stopped&lt;span class="co3"&gt;
    user&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;&lt;span class="st0"&gt;&amp;quot;1000:100&amp;quot;&lt;/span&gt;&lt;span class="co4"&gt;
    environment&lt;/span&gt;:&lt;span class="co3"&gt;
      GMAIL_USER&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;$&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;GMAIL_USER&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="co3"&gt;
      GMAIL_PASS&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;$&lt;span class="br0"&gt;&amp;#123;&lt;/span&gt;GMAIL_PASS&lt;span class="br0"&gt;&amp;#125;&lt;/span&gt;&lt;span class="co4"&gt;
    volumes&lt;/span&gt;&lt;span class="sy2"&gt;:
&lt;/span&gt;      - ./volumes/vmail:/mail
      - ./config/mbsyncrc:/mbsyncrc:ro&lt;span class="co3"&gt;
    command&lt;/span&gt;&lt;span class="sy2"&gt;: &lt;/span&gt;&amp;lt;
      sh -c 'while true; do
               echo &lt;span class="st0"&gt;&amp;quot;[$(date)] Running sync...&amp;quot;&lt;/span&gt;;
               /sync;
               echo &lt;span class="st0"&gt;&amp;quot;[$(date)] Sync finished. Sleeping...&amp;quot;&lt;/span&gt;;
               sleep 1800;
             done'&lt;/pre&gt;
&lt;/dd&gt;&lt;/dl&gt;

&lt;p&gt;
The configuration for the synchronization is relatively straight if forward. I added a bunch of comments for your and my understanding:
&lt;/p&gt;
&lt;dl class="code"&gt;
&lt;dt&gt;&lt;a href="https://www.splitbrain.org/_export/code/blog/2026-01/09-gmail_backup_notmuch_gui?codeblock=1" title="Download Snippet" class="mediafile mf_"&gt;config/mbsyncrc&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;pre class="code"&gt;### Remote Config ###
IMAPStore gmail-remote
Host imap.gmail.com
Port 993
UserCmd &amp;quot;echo $GMAIL_USER&amp;quot;   # from environment
PassCmd &amp;quot;echo $GMAIL_PASS&amp;quot;   # from environment
AuthMechs LOGIN
TLSType IMAPS
PipelineDepth 1              # Avoid quota errors
Timeout 60                   # More graceful timeouts

### Local Config ###
MaildirStore gmail-local
Path /mail/
Inbox /mail/INBOX
SubFolders Verbatim

### Sync Settings ###
Channel gmail
Far :gmail-remote:
Near :gmail-local:
Patterns *
SyncState *
CopyArrivalDate yes          # Don&amp;#039;t mess up message timestamps when moving them between folders.
Sync Pull                    # Download changes only, don&amp;#039;t sync local changes up to the server.
Create Near                  # Automatically create new folders in the local copy.
Remove Near                  # Automatically remove deleted folders from the local copy.
Expunge Near                 # Expunge deleted messages from the local copy.&lt;/pre&gt;
&lt;/dd&gt;&lt;/dl&gt;

&lt;p&gt;
With the appropriate user and app password in a &lt;code&gt;.env&lt;/code&gt; file, this will create your backup (the first run can take a loooong time).
&lt;/p&gt;

&lt;/div&gt;
&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;section&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;Backup&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;backup&amp;quot;,&amp;quot;codeblockOffset&amp;quot;:0,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;471-3303&amp;quot;} --&gt;
&lt;h2 class="sectionedit3" id="introducing_notmore"&gt;Introducing notmore.&lt;/h2&gt;
&lt;div class="level2"&gt;

&lt;p&gt;
Next I wanted a way to look at my backup. Of course I could set up an IMAP server and point a mail client to it, but that seems relatively involved. What I wanted was a quick way to search through the Maildir.
&lt;/p&gt;

&lt;p&gt;
A quick web search revealed &lt;a href="https://notmuchmail.org/" class="urlextern" title="https://notmuchmail.org/"&gt;notmuch&lt;/a&gt; - a xapian based mail search engine (and more that I am not using). It is super fast and simple to use. But it&amp;#039;s CLI only. I wanted something with a web interface.
&lt;/p&gt;

&lt;p&gt;
The general recommendation is &lt;a href="https://github.com/DavidMStraub/netviel" class="urlextern" title="https://github.com/DavidMStraub/netviel"&gt;netviel&lt;/a&gt;. A python/JavaScript interface to notmuch. Unfortunately it seems to be mostly abandoned with the last release 3 years ago and I had a really hard time getting it to work on modern Python versions within a Docker container.
&lt;/p&gt;

&lt;p&gt;
So instead I decided to roll my own. I vibe engineered a PHP based search interface that wraps around the notmuch command line interface. It let&amp;#039;s you search using notmuch&amp;#039;s comprehensive syntax, displays the result threads and allows you to drill down into the individual mails. Attachments can also be downloaded through the interface. Simple, not more.
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://www.splitbrain.org/_detail/blog/2026-01/notmore-home.png?id=blog%3A2026-01%3A09-gmail_backup_notmuch_gui" class="media" title="blog:2026-01:notmore-home.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-01/notmore-home.png?w=400&amp;amp;tok=2f7c14" class="media" loading="lazy" alt="" width="400" /&gt;&lt;/a&gt;
&lt;a href="https://www.splitbrain.org/_detail/blog/2026-01/notmore-search.png?id=blog%3A2026-01%3A09-gmail_backup_notmuch_gui" class="media" title="blog:2026-01:notmore-search.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-01/notmore-search.png?w=400&amp;amp;tok=e24597" class="media" loading="lazy" alt="" width="400" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
The tool is aptly named “notmore” and &lt;a href="https://github.com/splitbrain/notmore" class="urlextern" title="https://github.com/splitbrain/notmore"&gt;available on Github&lt;/a&gt;.
&lt;/p&gt;

&lt;p&gt;
The repo also builds a ready to use Docker image. You can just point it at the Maildir created by the sync job above.
&lt;/p&gt;

&lt;p&gt;
My full docker compose setup can be found in my &lt;a href="https://github.com/splitbrain/infra-nas-docker/tree/main/mail" class="urlextern" title="https://github.com/splitbrain/infra-nas-docker/tree/main/mail"&gt;github repository&lt;/a&gt; for the NAS docker setup.
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit4"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dgmail" title="Show pages matching 'gmail'" class="wikilink1"&gt;gmail&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dbackup" title="Show pages matching 'backup'" class="wikilink1"&gt;backup&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dmail" title="Show pages matching 'mail'" class="wikilink1"&gt;mail&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dprogramming" title="Show pages matching 'programming'" class="wikilink1"&gt;programming&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:4,&amp;quot;range&amp;quot;:&amp;quot;4907-4984&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2008-06/11-inbox_zero" class="wikilink1" title="blog:2008-06:11-inbox_zero" data-wiki-id="blog:2008-06:11-inbox_zero"&gt;Inbox Zero&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-03/08-auto_delete_old_mails_from_gmail" class="wikilink1" title="blog:2026-03:08-auto_delete_old_mails_from_gmail" data-wiki-id="blog:2026-03:08-auto_delete_old_mails_from_gmail"&gt;Auto-Delete old Mails from GMail&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/02-password_hasher_pwa" class="wikilink1" title="blog:2026-01:02-password_hasher_pwa" data-wiki-id="blog:2026-01:02-password_hasher_pwa"&gt;Password Hasher PWA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;section&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;Introducing notmore.&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;introducing_notmore&amp;quot;,&amp;quot;codeblockOffset&amp;quot;:2,&amp;quot;secid&amp;quot;:3,&amp;quot;range&amp;quot;:&amp;quot;3304-&amp;quot;} --&gt;&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/09-gmail_backup_notmuch_gui#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Fri, 09 Jan 2026 17:10:31 +0000</pubDate>
        </item>
        <item>
            <title>Password Hasher PWA</title>
            <link>https://www.splitbrain.org/blog/2026-01/02-password_hasher_pwa</link>
            <description>
&lt;h1 class="sectionedit1" id="password_hasher_pwa"&gt;Password Hasher PWA&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
Nearly &lt;a href="https://www.splitbrain.org/blog/2007-09/11-better_password_security_with_firefox" class="wikilink1" title="blog:2007-09:11-better_password_security_with_firefox" data-wiki-id="blog:2007-09:11-better_password_security_with_firefox"&gt;two decades ago&lt;/a&gt; I wrote about a mechanism to have different passwords for every web site, without a central password manager storing your passwords. All you needed is an algorithm and a master password.
&lt;/p&gt;

&lt;p&gt;
I am still using the mechanism&lt;sup&gt;&lt;a href="#fn__1" id="fnt__1" class="fn_top"&gt;1)&lt;/a&gt;&lt;/sup&gt;. But the original Firefox extension is abandoned, the Chrome extension that replaced it no longer works with recent Chrome changes and the Android App I was using is no longer working either.
&lt;/p&gt;

&lt;p&gt;
I felt unwell today and spent the day in bed. Time enough to get coding (or rather &lt;a href="https://simonwillison.net/2025/Oct/7/vibe-engineering/" class="urlextern" title="https://simonwillison.net/2025/Oct/7/vibe-engineering/"&gt;vibe engineering&lt;/a&gt;). Based on the original algorithm, I built a PWA (Progressive Web App), of course updated to modern JavaScript (using the SubtleCrypto &lt;abbr title="Application Programming Interface"&gt;API&lt;/abbr&gt;, a web component, the clipboard &lt;abbr title="Application Programming Interface"&gt;API&lt;/abbr&gt;, modern nested &lt;abbr title="Cascading Style Sheets"&gt;CSS&lt;/abbr&gt;, etc.) - not even a build system was needed for this.
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://www.splitbrain.org/_detail/blog/2026-01/passhash.png?id=blog%3A2026-01%3A02-password_hasher_pwa" class="media" title="blog:2026-01:passhash.png"&gt;&lt;img src="https://www.splitbrain.org/_media/blog/2026-01/passhash.png?w=600&amp;amp;tok=c0ad6e" class="mediacenter" loading="lazy" alt="" width="600" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
The app can be installed on Android and will show up in the share menu. When I need a password, I can simply share the current &lt;abbr title="Uniform Resource Locator"&gt;URL&lt;/abbr&gt; with the app and it will prefill the site tag. I enter the master pass, press enter and the password is in my clipboard, ready to use. On desktop I can use a simple bookmarklet that does the same. 
&lt;/p&gt;

&lt;p&gt;
This makes the system independent from any mobile apps or browser extensions. Good for another 20 years &lt;img src="https://www.splitbrain.org/lib/images/smileys/smile.svg" class="icon smiley" alt=":-)" /&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;a href="https://splitbrain.github.io/passhash/" class="urlextern" title="https://splitbrain.github.io/passhash/"&gt;Live Setup&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li class="level1"&gt;&lt;div class="li"&gt; &lt;a href="https://github.com/splitbrain/passhash" class="urlextern" title="https://github.com/splitbrain/passhash"&gt;Github Repository&lt;/a&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dpasswords" title="Show pages matching 'passwords'" class="wikilink1"&gt;passwords&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dprogramming" title="Show pages matching 'programming'" class="wikilink1"&gt;programming&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Djavascript" title="Show pages matching 'javascript'" class="wikilink1"&gt;javascript&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dpwa" title="Show pages matching 'pwa'" class="wikilink1"&gt;pwa&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;1640-1724&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2008-07/28-working_with_password_hashes_in_php" class="wikilink1" title="blog:2008-07:28-working_with_password_hashes_in_php" data-wiki-id="blog:2008-07:28-working_with_password_hashes_in_php"&gt;Working with Password Hashes in PHP&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-03/08-auto_delete_old_mails_from_gmail" class="wikilink1" title="blog:2026-03:08-auto_delete_old_mails_from_gmail" data-wiki-id="blog:2026-03:08-auto_delete_old_mails_from_gmail"&gt;Auto-Delete old Mails from GMail&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-03/02-old_game_new_clothes" class="wikilink1" title="blog:2026-03:02-old_game_new_clothes" data-wiki-id="blog:2026-03:02-old_game_new_clothes"&gt;Old Game, New Clothes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;div class="footnotes"&gt;
&lt;div class="fn"&gt;&lt;sup&gt;&lt;a href="#fnt__1" id="fn__1" class="fn_bot"&gt;1)&lt;/a&gt;&lt;/sup&gt; 
&lt;div class="content"&gt;haven&amp;#039;t found a &lt;a href="https://www.splitbrain.org/blog/2025-08/17-password_manager_woes" class="wikilink1" title="blog:2025-08:17-password_manager_woes" data-wiki-id="blog:2025-08:17-password_manager_woes"&gt;self hosted password manager&lt;/a&gt; I like, yet&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2026-01/02-password_hasher_pwa#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Fri, 02 Jan 2026 19:22:47 +0000</pubDate>
        </item>
        <item>
            <title>Twenty Years…</title>
            <link>https://www.splitbrain.org/blog/2025-12/15-twenty_years</link>
            <description>
&lt;h1 class="sectionedit1" id="twenty_years"&gt;Twenty Years…&lt;/h1&gt;
&lt;div class="level1"&gt;

&lt;p&gt;
…of GitHub commits. And it seems this year, I beat the highscore from 2016 &#128562;!
&lt;/p&gt;

&lt;p&gt;
&lt;img src="https://www.splitbrain.org/_media/blog/2025-12/contributions.png" class="media" loading="lazy" alt="" /&gt;
&lt;/p&gt;

&lt;p&gt;
Image made with &lt;a href="https://github-contributions.vercel.app/" class="urlextern" title="https://github-contributions.vercel.app/"&gt;GitHub Contributions Chart Generator&lt;/a&gt;
&lt;/p&gt;
&lt;div class="inline dataplugin_entry blogtags sectionedit2"&gt;&lt;dl&gt;&lt;dt class="tags"&gt;Tags&lt;span class="sep"&gt;: &lt;/span&gt;&lt;/dt&gt;&lt;dd class="tags"&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dgithub" title="Show pages matching 'github'" class="wikilink1"&gt;github&lt;/a&gt;&lt;span class="sep"&gt;, &lt;/span&gt;&lt;a href="https://www.splitbrain.org/tags?dataflt%5B0%5D=tags_%3Dcharts" title="Show pages matching 'charts'" class="wikilink1"&gt;charts&lt;/a&gt;&lt;/dd&gt;&lt;/dl&gt;&lt;/div&gt;&lt;!-- EDIT{&amp;quot;target&amp;quot;:&amp;quot;plugin_data&amp;quot;,&amp;quot;hid&amp;quot;:&amp;quot;data_entry&amp;quot;,&amp;quot;secid&amp;quot;:2,&amp;quot;range&amp;quot;:&amp;quot;259-318&amp;quot;} --&gt;&lt;dl class="blogrelated"&gt;&lt;dt&gt;Similar posts:&lt;/dt&gt;&lt;dd&gt;&lt;ul&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2018-05/06_thirteen_years" class="wikilink1" title="blog:2018-05:06_thirteen_years" data-wiki-id="blog:2018-05:06_thirteen_years"&gt;Thirteen Years…&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2016-09/02-some_dokuwiki_plugin_updates" class="wikilink1" title="blog:2016-09:02-some_dokuwiki_plugin_updates" data-wiki-id="blog:2016-09:02-some_dokuwiki_plugin_updates"&gt;Some DokuWiki Plugin Updates&lt;/a&gt;&lt;/li&gt;
&lt;li class="level1"&gt;&lt;a href="https://www.splitbrain.org/blog/2011-06/19-automate_github_pull_requests" class="wikilink1" title="blog:2011-06:19-automate_github_pull_requests" data-wiki-id="blog:2011-06:19-automate_github_pull_requests"&gt;Automate Github Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;&lt;/dl&gt;
&lt;/div&gt;
&lt;p align="right"&gt;&lt;a href="https://www.splitbrain.org/blog/2025-12/15-twenty_years#comments"&gt;Add or read comments to this article &amp;raquo;&amp;raquo;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;small&gt;Copyright © 2026 Andreas Gohr&lt;br /&gt;This feed is for personal, non commercial use in the subscriber's feedreader only.&lt;br /&gt;All contents (especially texts and images) are protected by copyright law and may not be republished outside of splitbrain.org without prior consent. Texts may be quoted in extracts under fair use policy. [digital fingerprint: sb97741286f601b4a0d496dc8bae242e6d]&lt;/small&gt;&lt;/p&gt;</description>
            <author>andi@undisclosed.example.com (andi)</author>
            <pubDate>Mon, 15 Dec 2025 13:53:46 +0000</pubDate>
        </item>
    </channel>
</rss>