Blog

Stabilitätsumgebung

Nachdem mich die ständigen Unannehmlichkeiten in Folge von Homebrew-Updates mürbe gemacht haben, liefert PEP 704 den Anlass, endlich eine virtuelle Umgebung für meine Django-Projekte einzurichten:

$ python3 -m venv virtualdjango $ source virtualdjango/bin/activate (virtualdjango) $ pip install Django (virtualdjango) $ pip install Django (virtualdjango) $ pip install uwsgi (virtualdjango) $ pip install psycopg2 (virtualdjango) $ pip install keyring (virtualdjango) $ pip install gnupg (virtualdjango) $ pip freeze > ~/Sites/virtualdjango_requirements.txt (virtualdjango) $ deactivate

Die Aktivierung der virtuellen Umgebung scheitert zwar im ersten Anlauf, weil ich auf die glorreiche Idee gekommen bin, in .zshrc das Alias hash für eines meiner Skripte zu definieren, und das activate-Skript die Existenz der gleichnamigen Shell-Funktion überprüft:

if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then hash -r 2> /dev/null fi

Anschließend beschränkt sich die Nutzung der neuen Umgebung aber – zu meiner Überraschung – auf die Anpassung einer einzigen Zeile in ~/Sites/djangoapp/manage.py

#!/Users/snafu/virtualdjango/bin/python3

– sowie die Überarbeitung des LaunchAgent-Konfiguration für uwsgi:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>KeepAlive</key> <true/> <key>Label</key> <string>net.janeden.uwsgi</string> <key>ProgramArguments</key> <array> <string>/Users/snafu/virtualdjango/bin/uwsgi</string> <string>--uid</string> <string>_www</string> <string>--gid</string> <string>_www</string> <string>--master</string> <string>--die-on-term</string> <string>--autoload</string> <string>--logto</string> <string>/Users/snafu/Library/Logs/Django/uwsgi.log</string> <string>--emperor</string> <string>/Users/snafu/Sites/djangoapp</string> </array> <key>RunAtLoad</key> <true/> <key>WorkingDirectory</key> <string>/Users/snafu/virtualdjango</string> </dict> </plist>

Dank Python 3.11 ist die virtuelle Django-Umgebung nicht nur stabiler, sondern auch substantiell schneller:

Server Software: nginx 1.23.1 / Python 3.9 nginx 1.23.3 / Python 3.11
Server Port: 80 80
Document Path: / /
Document Length: 65820 bytes 65820 bytes
Concurrency Level: 50 50
Time taken for tests: 1.595 seconds 0.999 seconds
Complete requests: 500 500
Failed requests: 0 0
Total transferred: 33065000 bytes 33065000 bytes
HTML transferred: 32910000 bytes 32910000 bytes
Requests per second: 313.49 [#/sec] (mean) 500.27 [#/sec] (mean)
Time per request: 159.492 [ms] (mean) 99.945 [ms] (mean)
Time per request: 3.190 [ms] (mean, across all concurrent requests) 1.999 [ms] (mean, across all concurrent requests)
Transfer rate: 20245.53 [Kbytes/sec] received 31108.88 [Kbytes/sec] received

Fernsuche

Die Kombination von leistungsfähiger Suchfunktion und undifferenzierter Ordnerstruktur ermöglicht es mir, auf eine ordnerübergreifende Suche weitgehend zu verzichten. Für besondere Anlässe – wie die Suche nach einer mehrere Jahre zurückliegenden Konversation – greife ich auf notmuch zurück:

brew install notmuch brew install notmuch-mutt # ~/.muttrc macro index <F8> "<enter-command>unset wait_key<enter><shell-escape>/opt/homebrew/bin/notmuch-mutt --prompt search<enter><change-folder-readonly>/Users/snafu/.cache/notmuch/mutt/results<enter>" "search mail (using notmuch)" macro index <F9> "<enter-command>unset wait_key<enter><pipe-message>/opt/homebrew/bin/notmuch-mutt thread<enter><change-folder-readonly>/Users/snafu/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" "search and reconstruct owning thread (using notmuch)"

Weil isync/mbsync über den von notmuch verwendeten Nachrichtenindex stolpert (Maildir error: found subfolder '.notmuch/xapian', but store 'personal-local' does not specify SubFolders style), muss auch .mbsyncrc ergänzt werden:

# ~/.mbsyncrc MaildirStore personal-local Path ~/PersonalMail/ Inbox ~/PersonalMail/INBOX Subfolders Verbatim

Das Perl-Skript notmuch-mutt create[s] a virtual maildir folder with search results whenever a search is made, und abgesehen von der geringfügig anderen Syntax für Suchbegriffe ist die Integration von mutt und notmuch sehr gelungen.

Absenderausrichtung

Einige Monate, nachdem ich das Thema DMARC für mich abgeschlossen habe, rächt sich mein oberflächliches Verständnis der DMARC-Prinzipien. Als ich zum ersten Mal einen Blick auf einen DMARC-Report von Google für lists.eden.one werfe, bin ich sowohl erschüttert als auch verwirrt:

<record> <row> <source_ip>123.123.123.123</source_ip> <count>1</count> <policy_evaluated> <disposition>none</disposition> <dkim>pass</dkim> <spf>fail</spf> </policy_evaluated> </row> <identifiers> <header_from>lists.eden.one</header_from> </identifiers> <auth_results> <dkim> <domain>lists.eden.one</domain> <result>pass</result> <selector>s42</selector> </dkim> <spf> <domain>eden.one</domain> <result>pass</result> </spf> </auth_results> </record>

Warum ist der SPF-Test gleichzeitig erfolgreich und nicht erfolgreich? Einen ersten Hinweis liefert der etwas detailliertere Report eines anderen Providers:

<record> <row> <source_ip>123.123.123.123</source_ip> <count>1</count> <policy_evaluated> <disposition>none</disposition> <dkim>pass</dkim> <spf>fail</spf> </policy_evaluated> </row> <identifiers> <header_from>lists.eden.one</header_from> <envelope_from>eden.one</envelope_from> </identifiers> <auth_results> <dkim> <domain>janeden.net</domain> <selector>s42</selector> <result>pass</result> </dkim> <spf> <domain>eden.one</domain> <scope>mfrom</scope> <result>pass</result> </spf> </auth_results> </record>

Die Ursache ist offensichtlich die Abweichung zwischen header_from und envelope_from durch die Verwendung des Sender Rewriting Scheme auf meinem Mailserver. Während der SPF-Test selbst lediglich den relevanten DNS-Eintrag für die Briefumschlagsdomain (eden.one) berücksichtigt, prüft der DMARC-Mechanismus in Bezug auf SPF, ob header_from und envelope_from zueinander passen (Under DMARC a message can fail even if it passes SPF or DKIM, but fails alignment.). Weil meine DMARC-Policy für SPF (und DKIM) eine strikte Übereinstimmung fordert –

<policy_published> <domain>lists.eden.one</domain> <adkim>s</adkim> <aspf>s</aspf> <p>quarantine</p> <sp>quarantine</sp> <pct>75</pct> </policy_published>

– muss dieser alignment check für SPF scheitern. DKIM ist nicht betroffen, weil Listenmails eine eigene DKIM-Signatur für lists.eden.one erhalten. Für Subdomains wie lists.eden.one lässt sich das SPF-Problem lösen, indem ich auf eine entspanntere Policy wechsele:

<policy_published> <domain>lists.eden.one</domain> <adkim>s</adkim> <aspf>r</aspf> <p>quarantine</p> <sp>quarantine</sp> <pct>75</pct> </policy_published> <record> <row> <source_ip>217.160.240.138</source_ip> <count>1</count> <policy_evaluated> <disposition>none</disposition> <dkim>pass</dkim> <spf>pass</spf> </policy_evaluated> </row>

Für andere Domains (z.B. janeden.net) käme nur ein Verzicht auf SRS in Frage, um die Abweichung von header_from und envelope_from zu eliminieren –

<record> <row> <source_ip>123.123.123.123</source_ip> <count>1</count> <policy_evaluated> <disposition>none</disposition> <dkim>pass</dkim> <spf>pass</spf> </policy_evaluated> </row> <identifiers> <header_from>janeden.net</header_from> </identifiers> <auth_results> <dkim> <domain>janeden.net</domain> <result>pass</result> <selector>s42</selector> </dkim> <spf> <domain>janeden.net</domain> <result>pass</result> </spf> </auth_results> </record>

– was aber neue Probleme für die vielen über meinen Mailserver weitergeleiteten E-Mails aufwürfe. Ich tröste mich damit, dass das passende DKIM-Alignment eine hinreichende Bedingung für ein positives DMARC-Ergebnis ist.

Listenzitatdarstellung

Seit mehr als zehn Jahren beobachte ich mit einer Mischung aus zusammengebissenen Zähnen und Gleichmut, dass Listen innerhalb von Zitaten (<blockquote><ol><li>...</li></ol></blockquote>) auf meiner Website sehr unschön dargestellt werden. Dafür verantwortlich sind die folgenden CSS-Anweisungen, die die Zitatbegrenzungslinie zerhacken:

blockquote > ul, blockquote > ol { padding: 1em 2em 1em 2em; border-width: 0px 0px 0px 1px; border-color: #369; border-style: solid; } ul, ol { margin-left: 3em; margin-bottom: 0.8em; }

Es spricht nicht für meinen gestalterischen Willen, dass ich die triviale Lösung –

blockquote > ul, blockquote > ol { padding: 1em 2em 1em 2em; margin-left: 0em; margin-bottom: 0em; border-width: 0px 0px 0px 1px; border-color: #369; border-style: solid; } blockquote > ul > li, blockquote > ol > li { margin-left: 3em; } ul, ol { margin-left: 3em; margin-bottom: 0.8em; }

– erst heute implementiere.

Missionarisch

Der gestrenge Herr Reiter mahnt zum neuen Jahr –

Friends of GnuPG,
a happy new year to all of you!

Now I am taking Andrew (hi) as an example to send a reminder why using text/plain format only mails is a good idea on this (and other mailing lists).

Am Samstag 17 Dezember 2022 19:54:39 schrieb Andrew Gallagher via Gnupg-users:
> <html><head><meta http-equiv="content-type" content="text/html;
> charset=utf-8"></head><body dir="auto">I’ve been

Because HTML can have a lot of active contents, a number of people I know sanitize email that have text/html parts. Some ignore such emails completely.

In the past I know that Werner ignored (most) emails with text/html.

There are more advanted [sic!] to text/plain mails:

  • people can better chose how their email client is displaying the contents, for instance the font size and color.
  • it saves energy because of less bytes transmitted and backuped (and indexed, archived and searched).

– und verweist auf die

productivity gap between people that use full fledged and customised emails clients to those with only web and mobile clients. As email is one of the working decentralised communication solutions, I think we should value it more and thus help people to learn about the productivity of an email client that they can fully control (on their hardware) and customize to have one unified interface to several communities.

Der missionarische Eifer meiner Mitgeeks rührt mich sehr.

Winterputz

Ein knappes halbes Jahr nach der Veröffentlichung von Vim 9.0 widme ich mich endlich dem Upgrade von .vimrc auf Vim9 Script. Auf den ersten Blick sind das neue Kommentarzeichen, die geänderte Variablendeklaration und die vereinfachte Funktionssyntax keine großen Herausforderungen, aber der Teufel steckt im Detail. Die Angabe von ranges für Kommandos erfordert nun ein Präfix (:), und in many places ist Weißraum obligatorisch (während er beim Setzen von Optionen – set formatoptions+=n – nach wie vor untersagt ist).

Ich nutze die Gelegenheit, um meine überbordende .vimrc mit Hilfe von filetype plugins etwas zu verschlanken und bei dieser Gelegenheit die unterschiedlichen Mappings für Markup-Sprachen zu vereinheitlichen:

# ~/.vim/ftplugins/markdown.vim vnoremap <buffer> <silent> ,b <Esc>`>a**<Esc>`<i**<Esc> vnoremap <buffer> <silent> ,a <Esc>`>a]()<Esc>P`<i[<Esc> # ~/.vim/ftplugins/html.vim vnoremap <buffer> ,a <Esc>`>a</a><Esc>`<i<a href="<Esc>pa"><Esc> vnoremap <buffer> ,b <Esc>`>a</strong><Esc>`<i<strong><Esc>

Besonders tückisch ist, dass html.vim auch für Markdown-Buffer geladen wird (mit sehr verwirrenden Konsequenzen), wenn man nicht frühzeitig eingreift:

# ~/.vim/ftplugins/html.vim if &ft == "markdown" finish endif

Ebenfalls nicht trivial ist die Handhabung des substitute-Kommandos im virtual mode. Einfache Funktionen könnten im Prinzip unmittelbar als Mapping realisiert werden:

def g:AddLinebreaks() silent s/.\zs\n\ze./\<br \/\>\r/ge nohl enddef vnoremap <f3> :call AddLinebreaks()<CR>

Interessant wird es, wenn ein Kommando innerhalb der Funktion die Markierung als Ganze behandeln soll, während das substitute-Kommando auf jeder Zeile operieren muss:

def g:EncloseParagraphs() execute "normal `>a</p>\<Esc>`<i<p>\<Esc>" silent :%s/\%V\n\{2,}\%V/<\/p>\r\r<p>/ge nohl enddef vnoremap <buffer> <silent> ,p :<C-U>call EncloseParagraphs()<CR>

Mittels <C-U> wird verhindert, dass die gesamte Funktion (einschließlich des execute-Kommandos) für jede Zeile innerhalb der Markierung ausgeführt wird. Das substitute-Präfix :% verschafft diesem Kommando dann eine maximale Reichweite (alle Zeilen des Buffers), die schließlich durch das Musteratom \%V wieder auf die ursprüngliche Markierung begrenzt wird.

Diep Pham bringt es auf den Punkt:

Q: Is it worth converting all my vim scripts to Vim9 script?
A: IMO, no. The syntax looks a little better to read and write, but not by a huge margin. But if you have some free time, why not?

Raketenprotokollerweiterung

Im Windschatten von Mastodon und angesichts des Digital Markets Act rückt Paul Sawers auf TechCrunch das Matrix-Protokoll ins Rampenlicht und überhöht die (unfertige, halbherzige, komplizierte) Matrix-Unterstützung von Rocket.Chat ein wenig:

Back in May, open source enterprise messaging platform Rocket.Chat revealed that it would be transitioning to the Matrix protocol. While this process is still ongoing, this represented a major coup for the Matrix movement, given that Rocket.Chat claims some 12 million users across major organizations such as Audi, Continental and Germany’s national railway company, The Deutsche Bahn.

In diesem Sinne leitete Apple mit iTunes for Windows auch den Wechsel auf die Windows-Plattform ein.

Textmatrix

Nach einer gewissen Abklingzeit versuche ich mich noch einmal an der Installation eines CLI-Clients für Matrix und siehe da – ein rabiates Upgrade der Xcode Command Line Tools löst das Problem:

sudo rm -rf /Library/Developer/CommandLineTools sudo xcode-select --install brew tap aaronraimist/tap brew cask install gomuks

Um die bestehenden (verschlüsselten) Konversationen lesen zu können, muss ich lediglich die Raumschlüssel aus Element exportieren und in gomuks importieren (/import /Users/snafu/Desktop/room_keys.txt; /clearcache). Die anschließende Verifikation der neuen gomuks-Session anhand ihres Fingerabdrucks (/fingerprint) funktioniert natürlich etwas anders als mit einem GUI-Client:

To find the manual verify option in Element, click on that user in the list of users or click their profile picture in the timeline. That will open their profile. Then click on the device you want to verify and then click the manually verify button.

Mein Dank gilt Aaron Raimist für die Zapfanlage und den individuellen Support.

Nachrichtenverwalter 2

Die Mailman Suite zeichnet sich nicht nur durch eine komplexe Architektur, sondern auch durch viele kleine und leicht zu behebende Fehler aus. Neben der falschen Platzierung von Konfigurationsdateien, über die ich bei der Installation gestolpert bin, funktioniert auch der Export von Listenmitgliedern per Kommandozeile (mailman members listname@domain --output list_members.txt) nur dann einwandfrei, wenn die display names sich auf den ASCII-Zeichensatz beschränken. Andernfalls: =?utf-8?q?J=C3=BCrgen?=.

Mark Sapiro stellt natürlich innerhalb weniger Stunden einen Patch bereit, muss aber kurz darauf ein Python-Skript zur Korrektur von display names nachliefern, die mit Mailman < 3.3.3 erstellt (und daher rfc2047-encoded gespeichert) wurden. Er räumt auch freimütig ein, dass eine Bearbeitung von display names eigentlich via Postorius möglich sein sollte. Aber:

Understood, but there are a couple of things at work here. One is that this particular task is a one-off the correct the result of a bug, but perhaps more significantly, mailman developers (at least me) are comfortable with withlist (it's been around since MM 2.1) and if I can do it that way, it reduces the motivation to make something more friendly for the normal admin. Unfortunately doing things easily that way requires a lot of specialized knowledge of Mailman internals, so most admins need help with it.

Diese Graubärtigkeit ist mir sehr sympathisch, auch wenn sie mit meiner dauerhaften Abhängigkeit von mailman-users[at]mailman3.org einhergeht. Immerhin hat das heutige Upgrade auf die aktuellen Mailman-Komponenten ohne Rücksprache mit Mark funktioniert:

systemctl stop mailman3 systemctl stop mailmanweb su - mailman pip3 install -U mailman postorius django-mailman3 hyperkitty mailman-web mailman-web migrate mailman-web compress mailman-web collectstatic mailman-web compilemessages exit systemctl start mailman3 systemctl start mailmanweb

Unwiderstehlich 6

Offenbar gab es im LastPass-Management einen Fuck Everything-Moment und man hat entschieden, die lauwarmen Reaktionen auf diverse Vorfälle nicht länger zu tolerieren.

Mit der umfassenden Übermittlung von teilweise unverschlüsselten oder wenigstens entschlüsselbaren Kundendaten an Dritte sollte die Diskussion darüber, welcher Passwortmanager die Prinzipien von Openness und Sharing am konsequentesten vertritt, beendet sein (zumal ein kanadischer Konkurrent keinen Hehl aus seiner übervorsichtigen Verschlüsselungsarchitektur macht). Auch die Presse und Sicherheitsexpertinnen können nicht umhin, die Bemühungen von LastPass endlich ernst zu nehmen.

Für weitergehende Ambitionen bleibt eigentlich nur der unbegrenzte Erwartungshorizont der Nutzerinnen:

Here’s what I want from a password manager:

  1. Stores my passwords in the caldera of an active volcano.
  2. I can access it with a 6-digit PIN from any computer on the Internet.

I don’t understand why this is so hard for the industry to get right.

Ignorance is bliss

Dem Präsidenten des Neusser Bürger-Schützen-Vereins e.V., Martin Flecken, ist es auf unerklärliche Weise gelungen, sämtliche Debatten zu Genderfragen in den vergangenen Jahren zu ignorieren. Die Reaktionen auf die Entscheidung, weiterhin keine Frauen als Vereinsmitglieder zuzulassen, kommentiert er verwundert:

Ich dachte, über so ein sachliches Thema kann man ja eigentlich nicht so heftig diskutieren.

Diesen Grad der Weltabgewandtheit sollte man eigentlich nur bei Karthäusermönchen erwarten.

Datenreinigungssonntagsausflug

Ich bewundere Dr. Drangs disziplinierte und detaillierte Dokumentation seiner Datenreinigungsabenteuer und kann sie manchmal sogar in kleinem Rahmen nachstellen. Für die Extraktion von Namen und E-Mail-Adressen aus einer VCF-Datei benötigt man nämlich nicht unbedingt ein komplexes Python-Modul. Die doppelte Anwendung des regulären Ausdrucks

FN:(.*)\nEMAIL.*pref:(.*)

in Verbindung mit den BBEdit-Befehlen Extract und Replace all (\1 <\2>) funktioniert einwandfrei, und Kontakte ohne primäre E-Mail-Adresse sind auch rasch extrahiert (FN:(.*)\n(?!EMAIL)).

Fehlbelegungskorrektur

Trotz meiner viel beschworenen Begeisterung für das maximum viable product vim habe ich bis heute die falsche Tastenbelegung genutzt, weil die entsprechenden Anweisungen –

# .bashrc set -o vi # .zshrc bindkey -v

– einen Blindflug zwischen insert mode und normal mode initiieren. Das muss natürlich nicht sein:

brew install zsh-vi-mode echo "source $(brew --prefix)/opt/zsh-vi-mode/share/zsh-vi-mode/zsh-vi-mode.plugin.zsh" >> .zshrc

Terminated

Der jüngste Versuch von Skynet, sich wieder ins apokalyptische Spiel zu bringen, war eindeutig zu zaghaft und wurde im Keim erstickt.

Wachstumsschmerzen

Der Twitter-Besitzer sorgt weiterhin fürsorglich für das Wachstum der Mastodon-Userbase, aber nicht alle Neuankömmlinge sind uneingeschränkt begeistert von ihrem neuen Habitat. Während Mastodons großzügiger Umgang mit Plattenplatz und Bandbreite vornehmlich Administratorinnen betrifft (und zusätzliche Umsätze für Amazon, Spendenaufrufe sowie neue Community-Projekte und alternative ActivityPub-Implementierungen generiert), tangieren Angriffe durch Trolle oder Staaten, unerfahrene Admins, diverse Sicherheitslücken, eine sehr öffentliche Implementierung von unverschlüsselten Direktnachrichten, das Thema Moderation (und der Digital Services Act), das strenge CW-Regiment für nahezu alle Posts und die Abwesenheit guter Apps auch die Nutzerinnen.

Ich fühle mich trotzdem wohl.

Offline 3

Gemini-Nutzerinnen sind tendenziell etwas älter und stehen der Entwicklung des World Wide Web trotz ihrer Netzaffinität eher krückstockfuchtelnd gegenüber. Ein skurriler Ausdruck dieser Haltung ist die als Achtsamkeit verbrämte Idee, moderne Computer wenigstens zeitweise ohne Internet-Zugang (Offline First) zu nutzen.

Völlig fremd ist mir diese Marotte nicht, und im Gegensatz zur iOS-App Offline Pages hat Lionel Dricots AV 98-Fork Offpunk einen sehr soliden Cache (sowie eine im Vergleich zu Amfora ziemlich rustikale Oberfläche). In dieser Umgebung ist der tiefenentspannte Level 3 der Internet-Abstinenz schwer zu erreichen, zumal sich gut gestaltete Webseiten auch auf anderen Wegen lokal bereitstellen lassen (wget -r -np https://learnvimscriptthehardway.stevelosh.com).

Interessanter als experimentelle Browser ist Solderpunks Hinweis auf msmtpq, das mit msmtp ausgeliefert wird und einen asynchronen (queue-basierten) E-Mail-Versand ermöglicht. Seit der Erstellung der Skripte hat sich in der Welt und bei den ping-Parametern viel geändert, und so muss ich kleine Änderungen vornehmen –

# old ping -qnc2 -w10 debian.org >/dev/null 2>&1 || return 1 ping -qnc1 -w4 8.8.8.8 >/dev/null 2>&1 || return 1 # new ping -qnc2 -W10 debian.org >/dev/null 2>&1 || return 1 ping -qnc1 -W4 8.8.8.8 >/dev/null 2>&1 || return 1

– bevor ich Mutt die neuen Verhältnisse nahe bringen kann:

# .muttrc set sendmail = "/opt/homebrew/bin/msmtpq -a profile --read-envelope-from" set sendmail_wait = -1

Anders als Internetausdrucker analoge Konservative ist sich Solderpunk seiner anachronistischen Tendenzen übrigens durchaus bewusst:

Apologies to everybody who grew up on dialup, for whom this approach is familiar and obvious and doesn't need careful explanation, but a lot of younger folks will have never used email this way in their life.

Fairerweise muss man festhalten, dass die Internetabspeicherei unter WIFIonICE- und 10 MBit/s-Upstream-Bedingungen auch 2022 noch ihre Berechtigung hat, und die Verantwortlichen großer Technologie- und Medienkonzerne jungen Menschen weltweit zutrauen, Offline-Werkzeuge wie youtube-dl oder mediathekviewweb.de massenhaft zu nutzen.

Hypertexthund 2

Wie bei vielen Sündenfällen folgt auch beim Versand von HTML-Mails die Strafe relativ rasch. markdown2html erweist sich als unfähig, mit mehreren unsauber formatierten Zitatebenen umzugehen und gibt unter anderem die CSS-Klasse .quotesubsequent sichtbar im Mailtext aus. Um wenigstens wohlgeformtes HTML zu versenden, greife ich einen Hinweis von Akkana Peck auf:

Write the [HTML] file and exit the editor. Then, in the Compose menu, type Ctrl-T to edit the attachment type. Change the type from text/plain to text/html.

Mit folgenden Ergänzungen in .vimrc und .muttrc lässt sich der Redaktionsprozess etwas vereinfachen:

# .vimrc " insert HTML template nnoremap ,d :0r ~/signature_html<CR>4G<CR>i function EncloseParagraphs() execute "normal `>a</p>\<Esc>`<i<p>\<Esc>" %s/\%V\n\{2,}\%V/<\/p>\r\r<p>/ge nohl endfunction " <C-U> is required to keep the called function from being executed for each " line of the selected range vnoremap <buffer> <silent> ,p :<C-U>call EncloseParagraphs()<CR> function AddLinebreaks() '<,'>s:\(.\)\zs\n\ze\(.\):\<br \/\>\r:ge nohl endfunction " select email content (visual mode) and add linebreaks/paragraphs nnoremap ,f kV5G:<C-U>call AddLinebreaks()<CR>gv:<C-U>call EncloseParagraphs()<CR> # .muttrc macro compose ,t "<edit-type>^Utext/html; charset=utf-8<enter>y"

Ein Nachteil dieser Vorgehensweise ist die voreingestellte Zeichenkodierung, während mutt flexibel die am wenigsten anspruchsvolle Kodierung (in meinem Fall us-ascii, iso-8859-1 oder utf-8) verwendet, und die Beschränkung auf eine HTML-Version. Die Lösung ist offensichtlich ein Skript, das (wie markdown2html) als Filter funktioniert und meinen E-Mails eine HTML-Version hinzufügt:

#!/opt/homebrew/bin/python3 import re import sys import html def convert_to_html(text_input): html_signature = '[redacted]' parts = re.split(r'^-- $', text_input, 1, flags=re.MULTILINE) body = parts[0] body = html.escape(body) # add linebreaks (with lookaround for overlapping searches) body = re.sub(r'(?<=.)\n(?=.)', r'<br />', body) # add paragraphs body = re.sub(r'\n{2,}', r'</p>\n\n<p>', body) body = f'<p>{body}</p>' # color quoted paragraphs body = re.sub(r'<p>((&gt; *){3,})', r'<p style="color:#009900;">\1', body) body = re.sub(r'<p>((&gt; *){2})', r'<p style="color:#ff002d;">\1', body) body = re.sub(r'<p>(&gt; *)', r'<p style="color:#3366ff;">\1', body) html_body = f'<html><body style="font:14px Helvetica;">{body}{html_signature}</body></html>' return html_body def main(): html_output = convert_to_html(sys.stdin.read()) if html_output: print(f'text/html\n\n{html_output}') if __name__ == '__main__': main()

In .vimrc sind nun nur noch die üblichen Staubwedel eingetragen, und .muttrc benötigt eine kleine Anpassung der attribution-Variablen, um die Einfärbung von zitierten Absätzen optimal zu unterstützen:

# .vimrc function CleanMail() " remove excess blank/quoted lines in mail messages silent %s/\(^[> ]*$\n\)\{2,}/\1/ge " remove quotes in lines below an attribution (to support HTML paragraph " coloring via html_mail.py) silent %s/^\(\(> *\)*On.*wrote:\n\)\(> *\)\+$/\1/ge nohl normal 1G endfunction autocmd FileType mail call CleanMail() " properly wrap lines vnoremap ,r gq nnoremap ,r gqap " remove e-mail thread below cursor (but leave signature untouched) map ,e VG16kdzz # .muttrc set attribution="On %d, %n wrote:\n"

Es ist eben nicht so, dass reguläre Ausdrücke stets die Anzahl der Probleme verdoppeln.

Downsizing

Trotz der Entscheidung, nicht mein gesamtes Weblog in eine Gemini-Kapsel zu überführen, habe ich probeweise einige Posts konvertiert. Zum Einsatz kamen dabei das Python-Skript html2gemini.py

#!/opt/homebrew/bin/python3 import sys import pypandoc from md2gemini import md2gemini import html def convert_to_gemini(html_input): markdown_output = pypandoc.convert_text(html_input, format="html", to="gfm-raw_html") gemini_output = md2gemini(markdown_output, links="copy") # work around md2gemini bug gemini_output = gemini_output.replace('\r\n', '\n') return gemini_output def main(): gemini_output = convert_to_gemini(sys.stdin.read()) if gemini_output: print(gemini_output) if __name__ == '__main__': main()

– und ein regulärer Ausdruck:

vnoremap <f7> :!html2gemini.py<CR> vnoremap <f8> :s/<p class="code">\(\_.\{-}\)<\/p>/```\r\1\r```/<CR>

Erste Erkenntnis: Allzu viele Links unter einzelnen Paragraphen stören den Lesefluss in Gemini-Clients erheblich (obwohl ich die Linkdichte im Vergleich zu den HTML-Versionen der Posts bereits reduziert habe).

Kleinnetz

Seit dem Aufstieg der Mastodon-Plattform wird dort vielfach eine allgemeine Renaissance von Blogs/RSS und Mailinglisten beschworen (unter anderem durch den RSS-Erfinder). Unter den optimistischen Geeks, die sich Gedanken um die Langlebigkeit ihrer digitalen Äußerungen und Werkzeuge machen, sind viele Anhängerinnen des Small Web, das sich seit 2019 auf eine moderne Gopher-Variante namens Gemini stützt und nicht an einem globalen Erfolg nach dem Muster des World Wide Web interessiert ist. Gemini strebt stattdessen mit dem Verzicht auf individuelle Gestaltung (CSS) und Dynamisierung (JavaScript) nach einer maximum power to weight ratio, die für die breite Öffentlichkeit weniger erstrebenswert ist. Außerdem legt die Spezifikation besonderen Wert auf Privatsphäre, indem Einfallstore für schamloses Tracking weitgehend ausgeschlossen werden. Mit anderen Worten: Gemini ist das Ergebnis einer Kombination von Gopher mit den Erfahrungen aus 30 Jahren WWW-Entwicklung aus der Perspektive einer überzeugten Gopher-Nutzerin. Der programmatische Minimalismus des Gemini-Protokolls geht in Bezug auf den Medientyp text/gemini bzw. die Auszeichnungssprache Gemtext sehr weit – außer Überschriften, Listen, Zitaten, Code und Links sind keine Auszeichnungselemente vorgesehen, und über die Darstellung dieser Elemente entscheidet allein die Client-Software. Im Vergleich zu Gemtext wirkt selbst Markdown barock.

Die Beschränkungen von Gemtext stellen ein gewisses Problem dar für den Transfer bestehender Weblogs in das Small Web. Mir ist die Rückbesinnung auf Dokumente, die unabhängig von einem Browser gut lesbar sind und dennoch ohne Zwischenschritte veröffentlicht werden können, natürlich sehr sympathisch, aber der Verzicht auf inline links ist nicht mit der Verlinkungspraxis in meinen Blogposts kompatibel. Erste Versuche zur Konversion dieses Weblogs mit Pandoc (pandoc --from html --to markdown) und md2gemini hinterlassen außerdem Spuren von Markup (wie {lang="en"}), die ich für hunderte von Posts entfernen (oder im Fall von <p class="code"> in die Gemtext-Syntax überführen) müsste. Den Weg zu einer vollständigen Gemini-Version seines umfangreichen Weblogs bezeichnet selbst Lionel Dricot als a lot more complex than planned – eine mehr als deutliche Warnung.

Für einen parallelen Publikationsprozess HTML/Gemtext gibt es verschiedene Systeme, von handgedengelten Skripten über spezifische Content-Generatoren bis hin zu Hugo, dessen komplexe Konfiguration meinem Django-basierten Redaktionssystem ziemlich nahe kommt. Ich entscheide mich deshalb für eine Erweiterung des Django-Modells –

# models.py class Page(models.Model): title = models.CharField(blank=True, max_length=400) content = models.TextField(blank=True) geminicontent = models.TextField(blank=True) [...]

– in Verbindung mit einer angepassten Variante meines Backrezeptes, die neben den einzelnen Posts eine Indexseite für die Gemini-Kapsel generiert:

# /djangproject/mysite/management/command/make_gemini.py def write_file(filename, content, extension='gmi'): global changed_pages filename = '{0}{1}.{2}'.format(BASEDIR, filename, extension) path = os.path.dirname(filename) # PostgreSQL sends \r\n linefeeds, Python reads \n from file content = content.replace('\r\n', '\n') changed = False if os.path.exists(filename): existing_file = io.open(filename, encoding='utf-8') existing_content = existing_file.read() existing_file.close() if existing_content != content: changed = True else: changed = True if changed: changed_pages += f'Added or changed page was written to to {filename}\n' if not os.path.exists(path): os.makedirs(path) encoded_content = content.encode('utf-8') gmi_file = open(filename, 'wb') gmi_file.write(encoded_content) gmi_file.close() def render_page(page): rendered_page = render_to_string('post.gmi', { 'requested_page': page, 'title' : page.title, 'content' : page.geminicontent, }) return rendered_page class Command(BaseCommand): help = "Create Gemini site" def handle(self, **options): global changed_pages index_content = '' for page in Page.objects.filter(mother_id=11923).exclude(geminicontent='').order_by('-create_date'): rendered_page = render_page(page) filename = reverse('blogentry_alpha', kwargs = { 'page_url' : page.url_name, 'year' : page.create_date.year, 'month' : page.create_date.month }) write_file(filename, rendered_page) index_content += f'=> {filename}.gmi {page.title}\n' if not changed_pages: changed_pages = 'No pages have been changed.' # Update index page only if pages have been changed else: rendered_index_page = render_to_string('index.gmi', { 'content' : index_content }) write_file('/index', rendered_index_page) scriptmail.send_message('Gemini Site updated', changed_pages)

Der Einsatz eines komplexen Web-Frameworks zur Erstellung einfacher Textdokumente entspricht nicht unbedingt dem Geist der Gemini-Spezifikation, andererseits ist die Nutzung vorhandener und gut funktionierender Technologie viel nachhaltiger als eine Investition in immer neue Werkzeuge. Dafür nehme ich sogar die redundante Speicherung für jedes Crossposting in Kauf.

Die Motivation für eine Gemini-Kapsel ist rein technischer und philosophischer Natur. Die FAQ-Sammlung des Projektes und verschiedene Posts heben stets die Vorteile eines nahezu ausschließlich textbasierten Mediums hervor, was den Nischencharakter des Small Web dauerhaft sicherstellt – auch die Gemini-Kurzanleitung von Jason McBrayer formuliert bei aller Euphorie recht defensiv:

The main thing to know is that you’re going to get a much more stripped-down experience compared to the modern WWW, but that’s okay! Some of the choices made to keep Gemini simple may seem too extreme, compared to even a bare-bones web site, but there are hidden benefits that won’t be obvious at first.

Viele neue Leserinnen wird mein – ohnehin vor allem von Bots frequentiertes – Blog auf diesem Weg also nicht finden. Lionel Dricot betrachtet die Beschränkung auf einen kleinen Personenkreis und wenige Kapseln als einen wesentlichen Vorteil:

That’s why on Gemini, we have no content. We have conversations. We have humans writing because they want to. Because they need to. We may have answers. We may start conversations over email. We are slowly interacting. The purpose is to write, to express, not having the feeling of being read.

Etwas weniger weihevoll ausgedrückt: Es besteht mit etwas Disziplin eine gewisse Chance, das gesamte Small Web durchzulesen und dabei nette, gleichgesinnte Menschen kennenzulernen. Oder unverschlüsselt die sensibelsten Informationen zu publizieren, unterhalb des Radars aller staatlichen und kommerziellen Datensammler – Schreiben unter implizitem Ausschluss der Öffentlichkeit.

Wie nicht anders zu erwarten, gibt es ähnlich viele Software-Projekte für Gemini wie Nutzerinnen. Als Clients verwende ich Lagrange (GUI/Desktop), Amfora (CLI/Desktop) und Elaho (iOS). Auch wenn mir die GUI von Elaho sehr gut gefällt – Lagrange erinnert etwas an NeXTSTEP – ist Amfora natürlich die angemessene Art, sich im Small Web zu bewegen.

Serverseitig entscheide ich mich für Agate, das ich als Binärpaket installiere und manuell als systemd-Service konfiguriere:

# /etc/systemd/system/gemini.service [Unit] Description=Agate gemini server [Service] User=www-data WorkingDirectory=/var/gemini/ ExecStart=/bin/sh -c "agate --hostname eden.one --lang de-DE" Restart=always RestartSec=1 StandardOutput=syslog StandardError=syslog SyslogIdentifier=gemini [Install] WantedBy=multi-user.target # /etc/rsyslog.d/gemini.conf if $programname == 'gemini' then /var/log/gemini.log & stop # /etc/logrotate.d/geminilogs /var/log/gemini.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm sharedscripts }

Nach dem üblichen –

systemctl daemon-reload systemctl restart rsyslog systemctl enable gemini systemctl start gemini

– liefert Agate die Kapsel aus. Meine Pendelei zwischen alten und neuen Netztechnologien ist im Stadium der Synthese angelangt.

Nachrichtenverwalter

Meine Freizeitgestaltung unterliegt einer klaren Pendelbewegung, und nach modernen und sogar populären Kommunikationstechnologien wende ich mich wieder einem sehr alten Medium zu, denn: Chat is indeed faster. But email creates evidence. Der Versand von E-Mails an eine überschaubare Anzahl von Empfängerinnen mittels einer einzigen Adresse lässt sich entweder mit einer Zeile in /etc/postfix/virtual bewerkstelligen, oder mit der Mailman Suite, bestehend aus den Projekten Mailman Core, Postorius, Hyperkitty, Mailmanclient, Hyperkitty Mailman plugin und Django-mailman3. Als wäre die Architektur nicht einschüchternd genug, gibt es zwei distinkte Einstiegspunkte in die Dokumentation, von denen nur einer zu einer stabilen Installationsleitung führt. Folgt man dieser Anleitung ohne jede Abweichung, denkt an das percent-encoding der Passwörter und konfiguriert die gewünschte Subdomain samt DKIM-Schlüssel und DNS-Einträgen für SPF, DKIM und DMARC (denn einige Mailprovider ignorieren die Subdomain Policy des bestehenden DMARC-Eintrags), gelangt man erstaunlich rasch zu einer funktionierenden Mailman-Instanz. Die Verwendung des Listennamens als Absenderadresse (DMARC mitigation) erzeugt in Verbindung mit use_esld = false; in /etc/rspamd/local.d/dkim_signing.conf (um die Reduktion von lists.eden.one auf die eSLD eden.one im Rahmen der DKIM-Signatur zu verhindern) wohlgeformte E-Mails, die allerdings unter Umständen mit der DKIM-Signatur der Absenderin verunziert sind:

Authentication-Results: posteo.de; dmarc=pass (p=quarantine dis=none) header.from=lists.eden.one Authentication-Results: posteo.de; spf=pass smtp.mailfrom=eden.one Authentication-Results: posteo.de; dkim=pass (2048-bit key) header.d=lists.eden.one header.i=@lists.eden.one header.b=RohmQgN4; dkim=fail reason="signature verification failed" (2048-bit key) header.d=eden.one header.i=@eden.one header.b=Io5jc3FE; dkim-atps=neutral

Dieses Problem lässt sich mit dem Parameter remove_dkim_headers: yes in /etc/mailman3/mailman.cfg (um die ursprüngliche DKIM-Signatur zu entfernen) leicht beheben:

Authentication-Results: posteo.de; dmarc=pass (p=quarantine dis=none) header.from=lists.eden.one Authentication-Results: posteo.de; spf=pass smtp.mailfrom=eden.one Authentication-Results: posteo.de; dkim=pass (2048-bit key) header.d=lists.eden.one header.i=@lists.eden.one header.b=0VbRo4SC; dkim-atps=neutral

Im nächsten Schritt soll die Kommunikation zwischen Mailman und Postfix, die standardmäßig unverschlüsselt und unauthentifiziert über Port 25 erfolgt, abgesichert werden:

# /etc/mailman3/mailman.cfg [mta] smtp_user: mailman_sender smtp_pass: mypassword smtp_secure_mode: starttls smtp_port: 587 smtp_verify_cert: no

Weil von diesem Moment an reject_sender_login_mismatch und smtpd_sender_login_maps greifen, benötigt mailman_sender einen Freibrief (und keinen Hack in mailman/src/mailman/mta/postfix.py, um die jeweils genutzten Listenadressen einzeln nachzutragen):

# /etc/postfix/controlled_envelope_senders @lists.eden.one mailman_sender

Zum Schluss verschiebe ich /opt/mailman/mm/mailman-hyperkitty.cfg nach /etc/mailman3/mailman-hyperkitty.cfg und vollziehe die Änderung in /etc/mailman3/mailman.cfg nach:

[archiver.hyperkitty] class: mailman_hyperkitty.Archiver enable: yes configuration: /etc/mailman3/mailman-hyperkitty.cfg

Nach diesen harmlosen Optimierungen verfalle ich auf die verhängnisvolle Idee, mailman-web und uwsgi aus Effizienzerwägungen über ein file socket kommunizieren zu lassen. Die initialen Änderungen in /etc/mailman3/uwsgi.ini und /etc/nginx/sites-enabled/mailman sind rasch erledigt, wenn man nicht vergisst, nginx die relevanten Umgebungsvariablen via uwsgi_params zu vermitteln (andernfalls: KeyError: 'REQUEST_METHOD'):

# /etc/mailman3/uwsgi.ini [uwsgi] # http-socket = 0.0.0.0:8000 socket = /opt/mailman/mm/var/mailman.sock chmod-socket = 664 # /etc/nginx/sites-enabled/mailman location / { #proxy_pass http://127.0.0.1:8000; uwsgi_pass unix:/opt/mailman/mm/var/mailman.sock; include /etc/mailman3/uwsgi_params; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; }

Um mailman-web von den neuen Verhältnissen zu überzeugen, müssen zwei weitere Einstellungen angepasst werden:

# /etc/mailman3/settings.py # override http://localhost:8000 in mailman_web/settings/mailman.py POSTORIUS_TEMPLATE_BASE_URL = 'https://lists.eden.one' # /etc/mailman3/mailman-hyperkitty.cfg [general] # base_url: https://127.0.0.1:8000/archives/ base_url: https://lists.eden.one/archives/

Nun funktioniert alles – außer der Archivierung von Nachrichten mit Hyperkitty (obwohl sie zuvor, mit http-socket, kein Problem war):

# /opt/mailman/web/logs/mailmanweb.log PermissionError at /archives/list/testlist@lists.eden.one/thread/T6MCILAVQYV5QIOGHJB3JNJ26Z7E2Z35/reattach-suggest [Errno 13] Permission denied: 'fulltext_index'

Es stellt sich heraus, dass das Verzeichnis fulltext_index durch das Whoosh-Backend der Haystack-Komponente referenziert wird. Ich probiere es mit einer vollständigen Pfadangabe –

# /etc/mailman3/settings.py # override the faulty PATH in mailman_web/settings/mailman.py HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', 'PATH': "/opt/mailman/fulltext_index", }, }

– und ernte einen Teilerfolg (eine andere Fehlermeldung):

ERROR 2022-12-01 09:18:00,111 1337915 hyperkitty.views.mailman Access to the archiving API endpoint was forbidden from IP 123.456.789.012, your MAILMAN_ARCHIVER_FROM setting may be misconfigured

Offenbar eine weitere Nebenwirkung des Wechsels von http-socket zu socket, die sich durch die Angabe der öffentlichen IP des Mailservers beheben lässt:

# /etc/mailman3/settings.py # MAILMAN_ARCHIVER_FROM = ('127.0.0.1', '::1') MAILMAN_ARCHIVER_FROM = ('123.456.789.012', '::1')

Natürlich frage ich mich langsam, ob ein socket wirklich so viel effizienter ist, aber genau in diesem Moment enden die Fehlermeldungen, und Hyperkitty arbeitet wie gewünscht: Threads lassen sich im Archiv suchen und neu zusammenfügen, und kombinierbare Threads werden sogar automatisch vorgeschlagen, wenn sich deren Betreffzeilen gleichen. Fröhliches reattachment führt allerdings dazu, dass Hyperkitty auf den Archivseiten aller Testlisten vergeblich nach anzuzeigenden Threads sucht (hyperkitty.models.thread.Thread.DoesNotExist: Thread matching query does not exist.), aber mein Debugging-Konto für diesen Monat ist schon weit überzogen.

Unterstützung bei der tastenden Fehlersuche erhalte ich ausnahmsweise nicht über eine der vielen Stackexchange-Seiten, sondern über mailman-users[at]mailman3.org, die zu meiner großen Befriedigung weniger sauber konfiguriert ist als meine Listen:

Authentication-Results: posteo.de; dmarc=pass (p=none dis=none) header.from=mailman3.org Authentication-Results: posteo.de; dkim=pass (1024-bit key) header.d=mailman3.org header.i=@mailman3.org header.b=0eJkGPKA; dkim=fail reason="signature verification failed" (2048-bit key) header.d=eden.one header.i=@eden.one header.b=l4srDqkL; dkim-atps=neutral

mailman-users[at]mailman3.org wird exklusiv von Mark Sapiro betreut, der meine Anmerkung zum Hyperkitty-Konfigurationspfad unverzüglich umsetzt und für neue Admins entmutigende Hinweise wie diesen bereithält:

Note in the above, django-admin is the command you use to invoke Django's management which may be django-admin, manage.py, mailman-web or something else depending on how Mailman is installed.

Jedes Projekt sollte einen so freundlichen und geduldigen resident sage haben. Sehr angenehm ist auch der minimale Platzbedarf der Mailman-Datenbanken (< 20KB), vor allem im Vergleich zu gewissen raumgreifenden Sozialanwendungen. Allerdings trägt die Mailman Suite mit ihren verschiedenen Django-Anwendungen allein etwa zwei Prozentpunkte zur Systemlast bei (~ 5% CPU, ~ 15% RAM), obwohl Reichweite und Aktivität meiner Testlisten im nicht messbaren Bereich liegen.