Blog

Untergangswarteschlange 4

Nachdem der Klimawandel seine vermeintlich unanfechtbare apocalyptic pole position jahrelang ausbauen konnte, übernahm Anfang 2020 eine Pandemie die Führung und verteidigte sie etwa zwei Jahre, bevor sich der nukleare Weltkrieg im Februar 2022 eindrucksvoll zurückmeldete. Mit einem weltweiten Einsatz von Nuklearwaffen würde nicht nur die Verbreitung des Coronavirus nachhaltig begrenzt, sondern auch die globale Erwärmung dergestalt umgekehrt, dass trotz einer signifikanten Reduktion der Weltbevölkerung Hungerkrisen drohen.

Fehlerbenachrichtigung

Kaum hat Rspamd sein statistisches Lernziel von 200 Tokens erreicht und mit der Bayes-gestützten Bewertung neuer Nachrichten begonnen, erscheinen beunruhigende Fehlermeldungen:

learn error: all learn conditions denied learning spam in default classifier learn error: all learn conditions denied learning ham in default classifier

Diese Meldungen gibt es offenbar erst seit Rspamd 3.1, und sie irritieren nicht nur mich. Aktiviert man das Debug-Logging, klingt das Problem wesentlich weniger dramatisch –

rspamd_stat_classifier_is_skipped: learn condition for classifier bayes returned: already in class spam; probability 100.00%; skip classifier

– und Vsevolod Stakhov betrachtet die Meldung auch als einfache Benachrichtigung über das gewünschte Verhalten der Software. Erst nach längerer Diskussion räumt er ein, dass Benachrichtigungen und Fehlermeldungen keine Synonyme sind (Well, I'll think about possibilities to distinguish real failures from notices, thank you for your input.). Aber ich sollte mich nicht beklagen: Der Anteil erkannter Spam-Nachrichten steigt dank statistical module zügig auf 40% (ohne erkennbare false positives).

Einzugsmarkierung

Nachdem ich mich leichtfertig über PEP 8 hinweggesetzt habe, fehlt mir für die Komposition von Python-Skripten in vim eigentlich nur noch eine saubere Markierung von Einzügen, die sich aber mit listchars sehr schön realisieren

set listchars=trail:·,precedes:«,extends:»,eol:↲,tab:▸\

– und mit :set list/:set list! schalten lässt.

Punk is not Dead

Ärgerlicherweise ignoriert vim sehr eindeutige Anweisungen in .vimrc

:set noexpandtab :set softtabstop=0 :set shiftwidth=4 :set tabstop=4

– immer dann, wenn es auf einheitliche Handhabung von whitespace wirklich ankommt. Glücklicherweise lässt sich der Hintergrund dieses unbotmäßigen Verhaltens rasch ausleuchten:

:verbose set expandtab? noexpandtab Last set from ~/.vimrc line 34 :set ft=python :verbose set expandtab? expandtab Last set from /opt/homebrew/Cellar/vim/8.2.4400/share/vim/vim82/ftplugin/python.vim line 119

Und was findet sich in und um Zeile 119?

if !exists("g:python_recommended_style") || g:python_recommended_style != 0 " As suggested by PEP8. setlocal expandtab tabstop=4 softtabstop=4 shiftwidth=4 endif

Selten war ein Verstoß gegen Konventionen so leicht zu realisieren:

let g:python_recommended_style=0

Achtstücksauber

Die Empfehlung von Rspamd, für Textnachrichten die Kodierung quoted-printable (statt base64) zu verwenden, ist mehr als 10 Jahre nach dem RFC6152 und angesichts der Verbreitung von 8bit clean MTAs etwas antiquiert. Ohne explizite Angabe eines Content-Transfer-Encoding mit dem Parameter cte entscheidet sich die Funktion set_content() – wie mutt – selbständig zwischen 7bit und 8bit. mutt geht allerdings noch einen Schritt weiter und wählt auch für den Content-Type den einfachsten passenden Zeichensatz, während die EmailMessage-Klasse selbst für ASCII-Nachrichten stur UTF-8 angibt.

Verfrühter Frühjahrsputz

Dank des Homebrew- und pip-gestützten Setups gestalten sich regelmäßige Updates sehr einfach, auch wenn jede Django-Aktualisierung nach längerer Zeit (3.1.7 → 4.0.2) kleinere Anpassungen erfordert. Der vermeintlich viel harmlosere Umstieg von Python 3.9.2. auf 3.9.9 zeitigt dagegen Bad Gateway-Meldungen von nginx, weil uwsgi die gewohnte Python-Version vermisst. Wie schon vor einem Jahr muss uwsgi – nach einer vollständigen Deinstallation – erst mit Homebrew (brew install uwsgi) und dann mit pip3 (pip3 install uwsgi) installiert werden, um sich wieder zur konstruktiven Mitarbeit bewegen zu lassen. Immerhin beschert mir das initiale brew upgrade ein taufrisches und großzügiges mutt 2.2.1.

Fingerabdruckautomatik

Es hat nicht lange gedauert von der Einrichtung eines Aktualisierungsmechanismus für SSL-/TLS-Zertifikate bis zum Wunsch, auch die Aktualisierung der Zertifikatsfingerabdrücke zu automatisieren.

Im Gegensatz zum openssl-Einzeiler ist die Erzeugung von Fingerprints mit Python nicht ganz trivial. Glücklicherweise gibt es ein großzügig lizenziertes code snippet, auf das ich mich stützen kann. Das Snippet liest den Dateiinhalt im Binärmodus und verwendet die base64-Funktion base64.decodebytes() zur Umwandlung in das binäre DER-Format. Mit der neueren Funktion base64.b64decode() lassen sich grundsätzlich Bytestrings und ASCII-Strings umwandeln, dennoch ist der Binärmodus auch für Textdateien die sicherere Wahl:

def get_fingerprint(): lines = [] with open(pem_path, 'rb') as pem: for line in pem: if line.rstrip() == b'-----BEGIN CERTIFICATE-----': continue elif line.rstrip() == b'-----END CERTIFICATE-----': continue else: lines.append(line.strip()) cert = b''.join(lines) der_cert = base64.b64decode(cert) fingerprint = hashlib.sha256(der_cert).hexdigest() fingerprint_upper = fingerprint.upper() fingerprint_colonized = ':'.join(fingerprint_upper[i:i+2] for i in range(0, len(fingerprint_upper), 2)) print(fingerprint_colonized) return fingerprint, fingerprint_colonized

Die Extraktion des eigentlichen Zertifikates ist etwas umständlich, weil zunächst die vornehmlich für Menschen relevanten Begrenzungszeilen zwischen den Elementen der Zertifikatskette und die Zeilenumbrüche entfernt werden müssen. Anschließend wird das Zertifikat binarisiert, es wird ein Hash erzeugt und in hexadezimale Schreibweise gewandelt sowie in eine zusätzliche Doppelpunkt-Notation überführt.

Mit den beiden so erzeugten Zeichenketten können .msmtprc und .offlineimaprc aktualisiert werden. Was auf den ersten Blick aussieht wie eine passende Aufgabe für das Tandem fileinput und re, erweist sich als etwas schwieriger: Sowohl msmtp als auch offlineimap akzeptieren keine Kommentare am Zeilenende bzw. interpretieren sie als Teil des Parameterwertes. Ich muss die Markierung für die zu aktualisierenden Fingerprints daher in die vorangehende Zeile schreiben, was die Verwendung des zeilenbasierten fileinput-Moduls ausschließt.

def update_fingerprint_files(fp_msmtp, fp_offlineimap): input = [ (msmtp_path, re.compile('(#auto-updated fingerprint\ntls_fingerprint )(.*)'), fp_msmtp), (offlineimap_path, re.compile('(#auto-updated fingerprint\ncert_fingerprint = )(.*)'), fp_offlineimap) ] results = [] for file, pattern, fingerprint in input: with open(file, 'r') as current_file: content = current_file.read() if not pattern.search(content): result = f'Malformed configuration file! Please check {file} manually.' elif pattern.search(content).group(2) == fingerprint: result = f'File {file} was up to date.' else: result = f'File {file} was updated ("{pattern.search(content).group(2)}" replaced by "{fingerprint}").' updated_content = pattern.sub(rf'\g<1>{fingerprint}', content) current_file.close() with open(file, 'w') as updated_file: updated_file.write(updated_content) print(result) results.append(result) return results

Es wird jeweils die gesamte Konfigurationsdatei (bis zu 121 Zeilen!) eingelesen – an dieser Stelle zahlt sich aus, dass mein aktueller Client mit mehr als 640KB Arbeitsspeicher ausgestattet ist. Enthält die Datei den gesuchten Ausdruck (pattern), werden der neue und der bereits vorhandene Fingerprint verglichen. Nur bei einer Abweichung wird die gesamte Datei (mit dem neuen Fingerprint) geschrieben. Bemerkenswert ist hier höchstens die Kombination von string literal und format string, mit der ich sowohl ein Element meines Regex-Musters als auch die Variable fingerprint aufgreifen kann. Der erzielte Effizienzgewinn (Zeitaufwand: 2 Stunden, Einsparung: maximal 3 Minuten alle drei Monate) ist noch niedriger als an anderer Stelle, so dass ich frühestens in 10 Jahren objektiv von meinem Skript profitieren werde. Nicht zu vernachlässigen ist allerdings die unbezahlbare Befriedigung über eine elegante Lösung und natürlich der kollaterale Erkenntnisgewinn.

Maskenerlaubnis

Die legendäre rheinische Liberalität (Jede Jeck is anders) bewährt sich auch in der Pandemie:

Außerdem wird ab Weiberfastnacht die noch in Einkaufsstraßen geltende Maskenpflicht ausgesetzt. Freiwillig dürfen die Masken aber getragen werden.

Schlüsselnachbestellung

Auch wenn junge, moderne Admins ihre SSL-Zertifikate natürlich längst automatisiert erhalten, lade ich sie bisher noch von Hand bei meinem Provider herunter und schicke sie dann in unterschiedlicher Bündelung auf meinen Server:

cat eden_personal.cer eden_intermediate.cer > eden.one_public.cer cat eden_private.key eden_personal.cer eden_intermediate.cer > eden.one_chain.pem scp eden_private.key root@server:/etc/ssl/private/eden.one_private.key scp eden.one_chain.pem root@server:/etc/ssl/private/eden.one_chain.pem scp eden.one_public.cer root@server:/etc/ssl/certs/eden.one_public.cer

Während eden.one_private.key und eden.one_public.cer von nginx und Dovecot genutzt werden, empfiehlt Postfix die Bündelung in eden.one_chain.pem:

Storing a key and its associated certificate chain in separate files is not recommended, because this is prone to race conditions during key rollover, as there is no way to update multiple files atomically.

Wenn ich nicht gerade meinen privaten Schlüssel zwischen Download und Upload verschludere

Ihr persönlicher Private Key (Privater Schlüssel) wird aus Sicherheitsgründen nicht in Ihrem IONOS-Konto hinterlegt. Wenn Sie nicht mehr im Besitz Ihres Private Keys sind, müssen Sie das Zertifikat neu ausstellen lassen. Damit erhalten Sie dann einen neuen Private Key.

– erzeugt die jährliche Schlüsselnachbestellung keinen erwähnenswerten Aufwand. Aber nachdem mittlerweile sämtliche Tutorials mit SSL/TLS-Bezug die Verwendung von Let's Encrypt unterstellen/voraussetzen/nahelegen, sehe ich mir den Certbot doch einmal an.

Die Installation erfolgt über die alternative Paketverwaltung Snap, die ihrerseits erst installiert werden muss:

apt-get install snap snap install core; snap refresh core snap install --classic certbot ln -s /snap/bin/certbot /usr/bin/certbot

Weil ich keine Anpassung meiner ausgefeilten nginx-Konfiguration wünsche, lasse ich mir nur die Zertifikate ausstellen:

certbot certonly --nginx

Nach diesem unscheinbaren Befehl wird mir die Ausstellung von Zertifikaten für alle in nginx konfigurierten Domains angeboten, oder jedenfalls würde sie mir angeboten, wenn ich in der Direktive server_name nicht willkürlich und grundlos Kommata zwischen einige der Domainnamen gesetzt hätte:

# Do NOT do this! server_name www.domain1.de domain1.de domain2.org, www.domain2.org, domain3.net www.domain3.net www.domain5.net, domain5.net, www.eden.one eden.one;

Der Certbot geht kreativ mit dieser fehlerhaften Anweisung um und verschweigt einfach die zwischen zwei Kommata befindlichen Domains. Nach erfolgter Korrektur und Auswahl der gewünschten Domains erzeugt er eine angemessen restriktive Verzeichnisstruktur unter /etc/letsencrypt, in der Zertifikate und Schlüssel wie folgt abgelegt werden:

Certificate is saved at: /etc/letsencrypt/live/[domain]/fullchain.pem Key is saved at: /etc/letsencrypt/live/[domain]/privkey.pem

Außerdem wird eine tägliche Timer Unit angelegt, die die Zertifikate rechtzeitig vor Ablauf ihrer dreimonatigen Gültigkeit erneuert. Ich muss lediglich die entsprechenden Dateipfade in /etc/nginx/nginx.conf, /etc/dovecot/conf.d/10-ssl.conf und /etc/postfix/main.cf eintragen und kann alles weitere dem Certbot überlassen. Da Let's Encrypt in der Regel keine Wildcard-Zertifikate ausstellt und iOS den MX-Eintrag und die Zertifikatsdomäne abgleicht, verwende ich für Dovecot und Postfix die Domäne mail.eden.one, für nginx eden.one.

Allerdings weiche ich mit der Konfiguration –

# /etc/postfix/main.cf smtpd_tls_chain_files = /etc/letsencrypt/live/mail.eden.one/privkey.pem, /etc/letsencrypt/live/mail.eden.one/fullchain.pem

– von der oben genannten Empfehlung ab und riskiere race conditions mit möglicherweise dauerhaften Folgen:

Storing the private key in the same file as the corresponding certificate is more reliable. With the key and certificate in separate files, there is a chance that during key rollover a Postfix process might load a private key and certificate from separate files that don't match. Various operational errors may even result in a persistent broken configuration in which the certificate does not match the private key.

Einerseits klingt das beunruhigend, andererseits ist Postfix derart sicherheitsorientiert, dass auch die unvermeidliche Einbindung von OpenSSL kritisch kommentiert wird:

NOTE: By turning on TLS support in Postfix, you not only get the ability to encrypt mail and to authenticate remote SMTP clients or servers. You also turn on hundreds of thousands of lines of OpenSSL library code. Assuming that OpenSSL is written as carefully as Wietse's own code, every 1000 lines introduces one additional bug into Postfix.

Ich entscheide mich für das Risiko und eine langfristig wartungsarme SSL/TLS-Konfiguration. Lediglich für meinen lokalen Mailstack muss ich im Abstand von drei Monaten die Fingerprints der frischen Zertifikate erzeugen

openssl x509 -noout -fingerprint -sha256 -in /etc/letsencrypt/live/mail.eden.one/fullchain.pem

– und in .offlineimaprc bzw. .msmtprc eintragen.

Für mein Sicherungsskript sind die Zugriffsrechte einiger Dateien unter /etc/letsencrypt übrigens zu restriktiv. Die privaten Schlüssel zur Kommunikation mit der Let's Encrypt-API sind auch für root nur lesbar, so dass gesicherte Kopien nicht mehr aktualisiert werden können. Ich hoffe, dass mein Workaround (chmod 600 private_key.json) nicht mit der Certbot-Funktionsweise kollidiert. Jedenfalls habe ich nicht vor, die Schlüssel zu löschen oder zu modifizieren.

Paketabhängigkeiten

Es ist überraschend riskant, eine nicht mehr benötigte SASL-Bibliothek wieder zu entfernen:

apt-get remove libsasl2-2 [...] The following packages will be REMOVED: dirmngr gnupg gpg-wks-client libldap-2.4-2 libsasl2-2 postfix postfix-policyd-spf-python

Offenbar bestehen zwischen Cyrus/saslauthd und Postfix – trotz aller Animositäten – weiterhin sehr enge Bindungen.

Unabhängigkeitsmail 3: Dovecot

Obwohl mein erweiterter Mailstack funktional nichts mehr zu wünschen übrig lässt, geht mir die gegen Cyrus gerichtete Empfehlung der Postfix-Community (Dovecot provides an alternative that may be worth considering.) nicht aus dem Kopf, und nachdem ich so weit gekommen bin, traue ich mir auch den Betrieb eines eigenen IMAP-Servers zu.

Die Dovecot-Dokumentation ist nur auf den ersten Blick etwas einschüchternd, und mit Hilfe des Ubuntu-Wikis komme ich gut voran. Für den Heimbedarf (weniger als 5 Benutzerkonten, keine serverseitigen Mailfilter) werden drei Pakete benötigt:

apt-get install dovecot-core dovecot-imapd dovecot-lmtpd

Die Konfiguration erfolgt vollständig in /etc/dovecot/conf.d/. Im Gegensatz zu Rspamd haben die Dovecot-Maintainer umfangreich kommentierte Beispieldateien bereitgestellt, aus denen allerdings nur wenige Parameter benötigt werden

# 10-auth.conf disable_plaintext_auth = yes auth_default_realm = eden.one auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ !include auth-passwdfile.conf.ext

Keine Klartext-Passwörter ohne SSL, Benutzernamen ohne Domäne/Realm werden um eden.one ergänzt, die Kreativität bei der Wahl von Benutzernamen wird zum Schutz vor SQL-Injektionen beschränkt, und die Passwörter liegen in einer Textdatei.

# 10-mail.conf mail_location = maildir:/var/mail/virtual/%u namespace inbox { type = private inbox = yes } mail_uid = vmail mail_gid = vmail mail_privileged_group = mail auth_socket_path = /var/run/dovecot/auth-userdb mailbox_list_index = yes

Dovecot soll Nachrichten im Maildir-Format speichern, die Inbox jeder Nutzerin als privaten Bereich betrachten und stellvertretend für alle virtuellen Nutzerinnen als Systemnutzerin vmail agieren. Der Socket für den Authentifizierungsdienst wird verwirrenderweise hier und nicht in 10-auth.conf oder in 10-master.conf definiert.

# 10-master.conf default_internal_user = dovecot service imap-login { inet_listener imap { port = 143 } inet_listener imaps { port = 993 ssl = yes } } service lmtp { unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 group = postfix user = postfix } } service auth { unix_listener auth-userdb { mode = 0660 user = vmail group = vmail } unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } }

Dovecot lauscht auf zwei IMAP-Ports (wobei auch Anfragen auf Port 143 durch die entsprechenden Parameter in 10-auth.conf (s.o.) und 10-ssl.conf (s.u.) zur Verschlüsselung gezwungen werden). Über Unix-Sockets stellt Dovecot einen LMTP-Server und den Authentifizierungsservice für IMAP-Logins (Typ: userdb) und für Postfix (Typ: client) zur Verfügung.

# 10-ssl.conf ssl = required ssl_cert = </etc/ssl/certs/mycert.pem ssl_key = </etc/ssl/private/mykey.key ssl_client_ca_dir = /etc/ssl/certs

Jede Verbindung wird verschlüsselt, und zwar mit Hilfe der SSL-Zertifikate für meine Domain.

# 15-lda.conf postmaster_address = postmaster@eden.one hostname = mail.eden.one

Auch bei der internen LMTP-Kommunikation mit Postfix soll Dovecot sich als mail.eden.one vorstellen –

Received: from mail.eden.one by mail.eden.one with LMTP id AXunOntwEWKkcA0Ap6K1+g (envelope-from <jane.doe@gmail.com>) for <user@eden.one>; Sat, 12 Feb 2022 22:34:35 +0000

– obwohl diese Angabe keinen Menschen außer mir interessiert und auch mich nicht unbedingt interessieren müsste. Relevanter sind die entsprechenden MUA/MTA-Einstellungen, die immerhin sichtbar werden, wenn eine exzentrische Empfängerin die E-Mail-Header einblendet.

# 15-mailboxes.conf namespace inbox { mailbox Drafts { special_use = \Drafts auto = subscribe } mailbox Junk { special_use = \Junk auto = subscribe } mailbox Trash { special_use = \Trash auto = subscribe } mailbox Archive { special_use = \Archive auto = subscribe } mailbox Sent { special_use = \Sent auto = subscribe } mailbox virtual/Flagged { special_use = \Flagged comment = All my flagged messages } }

Die genannten Standardpostfächer sollen automatisch angelegt und abonniert werden.

# auth-passwdfile.conf.ext passdb { driver = passwd-file args = scheme=SHA256-CRYPT username_format=%u /etc/dovecot/users } userdb { driver = passwd-file args = username_format=%u /etc/dovecot/users override_fields = home=/home/virtual/%u }

Als Passwort- und Nutzerdatei dient eine schlanke Textdatei ohne UID/GID (vgl. mail_uid und mail_gid in 10-mail.conf) und ohne individuelles Homeverzeichnis.

Nach dem Abschluss der Dovecot-Konfiguration muss ein Passwort-Hash generiert (doveadm pw -s SHA256-CRYPT) und mit dem zugehörigen Benutzernamen in /etc/dovecot/users geschrieben werden (alle übrigen Felder bleiben leer):

user@eden.one:{SHA256-CRYPT}$5$lpskns12qTM5P2Zm$TMTAV6IYrvMUG8sT16sK6LZBiDMa3LWHXebtKrSEhr1::::::

Postfix muss instruiert werden, auf den von Dovecot bereitgestellten Socket (statt auf saslauthd) zu lauschen:

# /etc/postfix/main.cf smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth

Um das Postfach der Nutzerin user@eden.one in /var/mail/virtual/user@eden.one zu befüllen, wird eine der virtual_alias_domains zur virtuellen Mailbox-Domäne befördert, um eingehende E-Mails lokal über den oben definierten LMTP-Socket auszuliefern:

# /etc/postfix/main.cf virtual_mailbox_domains = eden.one virtual_mailbox_maps = hash:/etc/postfix/mailbox_users virtual_transport = lmtp:unix:private/dovecot-lmtp

Mit virtual_mailbox_maps kann Postfix die Verbindung zwischen einer Adresse und einem Dateipfad zur Ablage von Nachrichten herstellen. In meiner Konfiguration enthält mailbox_users dagegen lediglich die von Dovecot bereitgestellten Nutzerinnen/Postfächer:

user@eden.one OK

Praktischerweise funktionieren die Weiterleitungen in virtual_alias_maps auch nach der Umstellung für die Mailbox-Domäne. Es können nicht nur Nachrichten an externe Empfängerinnen geschickt werden, sondern auch eine Kombination lokaler Postfächer und externer Empfängerinnen ist möglich (sales_marketing@eden.one user@eden.one, jane.doe@gmail.com). Mit

service dovecot restart postmap /etc/postfix/mailbox_users service postfix restart

wird der vollständige Mailstack in Betrieb genommen, und nach der Freischaltung der IMAP-Ports 143 und 993 in der Firewall reduziert sich meine Abhängigkeit theoretisch um einen Anbieter. Praktisch wäre es ausgesprochen leichtsinnig, sämtliche Dienste von nur einem – wenn auch sehr professionellen – Unternehmen zu beziehen, und so stütze ich mich weiterhin auf Pairdomains, IONOS, Posteo und Apple.

Hafenwechsel

Wenn man sich an die Weboberfläche für die Hardware-Firewall eines professionellen Hosting-Providers gewöhnt hat, vergisst man sehr leicht die manuelle Anpassung von Firewall-Regeln unter CentOS –

firewall-cmd --zone=public --permanent --add-port 1183/tcp firewall-cmd --reload

– bei einem kleineren Hoster und wundert sich, warum ssh -p 1183 user@server.com trotz entsprechender Portänderung in /etc/ssh/sshd-config immer wieder scheitert.

More Leniency

In den Release Notes für mutt 2.2 gibt Kevin McCarthy nicht nur seinen Rückzug aus der Maintainer-Rolle bekannt, sondern auch eine kopernikanische Wende im Projekt:

$rfc2047_parameters enabled by default

If you've ever looked at the raw headers of an email and seen text like =?iso-8859-1?q?blah=20blah=20blah?=, that is RFC2047 encoding. It is used to encode non-ascii text in an email header, but has specific rules about where it can be used.

One of the places it shouldn't be used is for attachment names (usually stored inside Content-Disposition:, although historically it could be in other places.) RFC2231 encoding was created for that purpose.

Unfortunately, even though it's explicitly listed as prohibited by the RFC, a few mail clients still do so. You've seen this if you tried to save an attachment and saw a name like the one above.

Mutt has had $rfc2047_parameters for a long time to deal with the problem, but it was set off by default, because.... the behavior was prohibited. However, because the option name is fairly obsure, this often left users (even technically proficient ones) confused and blaming Mutt. So starting this release, I've turned it on by default.

I guess there may be a tiny chance someone wants to send an attachment named =?iso-8859-1?q?blah=20blah=20blah?=, but I think that's much less likely than normal users encountering the incorrect encoding.

Offenbar war ich einer der letzten Menschen, die den Parameter $rfc2047_parameters selbst auf den einzig sinnvollen Wert (yes) setzen mussten.

Secure Keyboard Focus

Mehrere Wochen war meine Produktivität auf Grund einer unscheinbaren Option der Terminal-Anwendung spürbar beeinträchtigt. Secure Keyboard Entry soll keystroke logging erschweren, es hat aber seit macOS Monterey auch den Effekt, dass der Befehl open den Fokus nicht auf die jeweils geöffnete Anwendung verlagert.

Das Verhalten betrifft nicht nur das Terminal (in dem ich open vor allem nutze, um E-Mail-Anhänge zu öffnen), sondern auch LaunchBar, was auf den Focus-Modus als Ursache hindeutet und das Debugging deutlich verlängert.

Reputationspflege 7: Schlangenmail

Unter den Augen des gestrengen Rspamd muss ich einige Python-Skripte mit E-Mail-Funktion umarbeiten: Grundlose MIME multipart messages

msg = MIMEMultipart() msg.attach(MIMEText(body, 'plain'))

– werden mit dem Symbol CTYPE_MIXED_BOGUS (1) markiert, während –

msg = MIMEText(body, 'plain')

– ein wohlwollendes MIME_GOOD (-0.1) nach sich zieht. Außerdem lerne ich, dass Postfix seit Version 2.6 mit Rücksicht auf fragile DKIM-Signaturen fehlende Header nicht mehr automatisch ergänzt – mit gravierenden Folgen (MISSING_MID (2.5), MISSING_DATE (1)) für meine skriptbasierten E-Mails. Zwar könnte ich Postfix nun anweisen, wieder für vollständige Header zu sorgen (always_add_missing_headers = yes). Dadurch entstünden allerdings Message-IDs der Form 20220219215732.2CE497F8AA@mail.eden.one, die einen falschen Eindruck vom Geburtsort der Nachrichten vermitteln.

Manuell in mutt komponierte E-Mails tragen nämlich IDs der Form YhHNelJ2KZ2Lni7x@client.eden.one, basierend auf dem internen ID-Algorithmus mutt_gen_msgid() und der Einstellung set hostname="client.eden.one" in .muttrc. Die Python-Funktion email.utils.make_msgid() erzeugt mit dem Parameterwert domain='client.eden.one' entsprechende Herkunftsnachweise, wenn auch mit einer anderen ID-Struktur. Der gleichnamige msmtp-Parameter domain sorgt in Verbindung mit dem Hostnamen client.eden.one übrigens für Konsistenz zwischen Message-ID und Received-Header, während der vergleichbare Parameter der Python-Funktion smtplib.SMTP() local_hostname heißt. Etwas verwirrend, aber funktional gleichwertig:

# mutt und msmtp Received: from client.eden.one (cable-XX-XX-XXX-X.nc.de [XX.XX.XXX.X]) Message-ID: <YhHNelJ2KZ2Lni7x@client.eden.one> # Python Received: from client.eden.one (cable-XX-XX-XXX-X.nc.de [XX.XX.XXX.X]) Message-ID: <164535102753.2523.10851843159073092003@client.eden.one>

Schließlich löse ich auch noch das Rätsel des MIME_BASE64_TEXT (0.1): Jede automatisch generierte E-Mail, die sich nicht auf den ASCII-Zeichensatz beschränkt, wird seitens des Python-Moduls email.mime base64-kodiert. Rspamd hält quoted-printable für angemessener und vergibt das obige Symbol. Bis vor einigen Jahren hätte eine entsprechende Änderung eine manuelle Anpassung der MIME-Header erfordert, mit der set_content()-Funktion der EmailMessage-Klasse (seit Python 3.6) und ihrem Parameter cte ist es sehr viel einfacher. Die verschiedenen Anpassungen nehme ich zum Anlass, den E-Mail-Versand aller Skripte an das Modul scriptmail zu delegieren:

#!/usr/bin/env python3 import os import datetime import __main__ as main import smtplib from email.message import EmailMessage from email.headerregistry import Address from email.utils import make_msgid import keyring smtp_server = 'mail.eden.one' smtp_user = 'user@eden.one' port = 587 recipient = Address("Script Master", "user", "eden.one") sender = Address("Script Status", "noreply", "eden.one") localhost = 'client.eden.one' def send_message(subject, body): now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') subject = f'{os.path.basename(main.__file__)}: {subject}' body = f'{body}\n\n{now}' msg = EmailMessage() msg['From'] = sender msg['To'] = recipient msg['Subject'] = subject msg['Message-ID'] = make_msgid(domain=localhost) msg.set_content(body, cte='quoted-printable') smtp_password = keyring.get_password(smtp_server, smtp_user) s = smtplib.SMTP(host=smtp_server, local_hostname=localhost, port=port) s.starttls() s.login(smtp_user, smtp_password) s.send_message(msg) del msg

Die Funktion send_message() ergänzt vor dem Versand den Subject-Header um den Namen des aufrufenden Skripts und den Body um einen Zeitstempel. Das SMTP-Passwort wird aus dem macOS-Schlüsselbund entnommen.

Gezügelte Neugier

Seit mehreren Jahren verzichte ich auf Informationen über die Besucherinnen dieser Website, und spätestens seit dem Schrems II-Urteil des Europäischen Gerichtshofs (auf das sich in den vergangenen Wochen österreichische und französische Datenschutzbehörden stützten) muss ich meine Neugier auch aus rechtlichen Gründen zügeln.

Schafsherdenpelzthemen

Obwohl Themen besser klingt als Herde, sind wir für Google immer noch das Produkt.

Easpa cumhachta

Wenn Führungskräfte mit unzureichender Gestaltungsmacht oder mangelnder Durchsetzungsfähigkeit hadern, können sie sich mit einem Blick nach Nordirland trösten:

Zwischen Irland und der britischen Provinz Nordirland ist ein offener Streit über die Umsetzung des Brexit-Vertrages ausgebrochen. Der nordirische Landwirtschaftsminister Edwin Poots ordnete an, ab Mitternacht alle Kontrollen von Waren von der britischen Insel zu stoppen. Allerdings war unklar, ob die Beamten die Anweisung umsetzen würden.