~ruther/guix-config

a178510f63e4e391479c8eee6cb87e6c062861f5 — Rutherther 8 days ago fbe79d4
chore: update
M Makefile => Makefile +3 -1
@@ 51,4 51,6 @@ system-repl: channels-lock.scm
	$(GUIX) repl -L modules -i ./repl-init.scm $(EXTRA_ARGS)

channels-lock.scm: channels.scm
	guix time-machine -C channels.scm $(EXTRA_TIMEMACHINE_ARGS) -- describe -f channels $(EXTRA_ARGS) > channels-lock.scm
	guix time-machine -C channels.scm $(EXTRA_TIMEMACHINE_ARGS) -- \
    describe -f channels $(EXTRA_ARGS) > channels-lock.scm \
    || (rm channels-lock.scm && exit 1)

M config.scm => config.scm +77 -93
@@ 5,6 5,7 @@
(define-module (config))

(use-modules
 (guix gexp)
 (nongnu packages linux)
 (nongnu system linux-initrd)
 (nongnu packages firmware)


@@ 19,11 20,18 @@
 (guix utils)
 (guix packages)
 (guix build-system gnu)
 (guix build-system trivial)
 (gnu system accounts)
 (ruther services system)
 (ruther services bind)
 (ruther services nix-gl)
 (ruther bootloader grub))
 (ruther services nix)
 (ruther services auto-login)
 (ruther services netbird)
 (ruther packages netbird)
 (ruther bootloader grub)
 (dfsg contrib services tailscale))

(use-service-modules desktop sddm xorg base nix pm virtualization vpn
                     sound dbus cups containers security-token networking
                     ssh databases linux docker)


@@ 86,6 94,9 @@
    'pluto-sdr
    (file->udev-rule "53-adi-plutosdr-usb.rules" (local-file "udev/53-adi-plutosdr-usb.rules")))
   (udev-rules-service
    'android
    (file->udev-rule "51-android.rules" (local-file "udev/51-android.rules")))
   (udev-rules-service
    'ftdi
    (file->udev-rule "51-ftdi.rules" (local-file "udev/51-ftdi.rules")))))



@@ 123,7 134,9 @@
                (endpoint "159.195.49.157:51820")
                (keep-alive 25)
                (public-key "ZOVjmgUak67kLhNVgZwyb0bro3Yi4vCJbGArv+35IWQ=")
                (allowed-ips '("192.168.32.0/24")))))))))
                (allowed-ips '("192.168.32.0/24")))))))
   (service tailscaled-service-type)
   (service netbird-service-type)))

(define %ruther/laptop-gui-essential-services
  (list


@@ 175,7 188,7 @@

(define %ruther/base-laptop-os
  (operating-system
    (kernel linux-6.18)
    (kernel linux-7.2)
    (kernel-arguments
     (cons* "amdgpu.dcdebugmask=0x10"
            %default-kernel-arguments))


@@ 236,10 249,10 @@
       (program
        (file-append inetutils "/bin/traceroute"))
       (capabilities "cap_net_raw=eip"))
      (privileged-program
       (program
        (file-append wireshark-patched "/bin/dumpcap"))
       (capabilities "cap_net_raw,cap_net_admin=eip"))
      ;; (privileged-program
      ;;  (program
      ;;   (file-append wireshark-patched "/bin/dumpcap"))
      ;;  (capabilities "cap_net_raw,cap_net_admin=eip"))
      %default-privileged-programs))

    ;; This is where we specify system-wide packages.


@@ 251,10 264,11 @@
                       zip unzip
                       wget curl
                       vim
                       nix
                       polkit ;; get pktty
                       fwupd-nonfree
                       wireshark-patched)
                       netbird
                       ;; wireshark-patched
                       )
                      %base-packages))

    (services


@@ 278,13 292,6 @@
                           (home "/home/fbw"))
                          %default-guix-shared-users))))

              ;; Prevent me from going to Codeberg too often.
              (simple-service 'disable-codeberg
                              hosts-service-type
                              (list
                               (host "127.0.0.1" "www.codeberg.org")
                               (host "127.0.0.1" "codeberg.org")))

              ;; I give up! Let the download binaries run.
              (extra-special-file "/lib64/ld-linux-x86-64.so.2"
                                  (file-append glibc "/lib/ld-linux-x86-64.so.2"))


@@ 294,20 301,48 @@
              ;; Let's use GUI apps from Nixpkgs, because, why not?
              nixos-opengl-driver-service

              (service nix-service-type
              ;; Prefer the builder's LAN address, then fall back to WireGuard.
              (extra-special-file
               "/root/.ssh/config"
               (mixed-text-file
                "root-ssh-config"
                "Host edge-ruther-builder\n"
                "  HostName edge-ruther.local\n"
                "  HostKeyAlias edge-ruther.local\n"
                "  BatchMode yes\n"
                "  ConnectTimeout 5\n"
                "  ProxyUseFdpass yes\n"
                "  ProxyCommand "
                (file-append bash "/bin/sh")
                " -c '"
                (file-append netcat-openbsd "/bin/nc")
                " -F -w 2 edge-ruther.local %p || exec "
                (file-append netcat-openbsd "/bin/nc")
                " -F -w 10 192.168.32.12 %p'\n"))

              (service nix-service-type/raised-nofile
                       (nix-configuration
                        (extra-config
                         '("experimental-features = nix-command flakes\n"
                           "extra-platforms = i686-linux aarch64-linux\n"
                           "keep-outputs = true\n"
                           "keep-derivations = true\n"))))
                           "keep-derivations = true\n"

                           "extra-trusted-public-keys = edge-1:sGPnKl9kRZ1Tv8xqC8qdMfdpPpeZ0HDE38/eEmGOCGk=\n"
                           "fallback = true\n"

                           "builders = ssh-ng://nixremote@edge-ruther-builder x86_64-linux /root/.ssh/id_ed25519 8 2 big-parallel,kvm\n"
                           "builders-use-substitutes = true\n"))))
              ;; TODO: contribute this to the nix service
              (simple-service 'nix-etc-d etc-profile-d-service-type
                              (list
                               (file-append nix "/etc/profile.d/nix-daemon.sh")
                               (file-append nix "/etc/profile.d/nix.sh")))

              ;; Vivado or Matlab can crash because they open too many files
              ;; Vivado or Matlab can crash because they open too many files.
              ;; Note: @nixbld limits are set on the nix-daemon shepherd
              ;; service itself (see (ruther services nix)); PAM limits don't
              ;; apply because nixbld users never log in.
              (service pam-limits-service-type
                       (list
                        (pam-limits-entry "@wheel" 'hard 'nofile '50000)


@@ 322,7 357,9 @@
                                                  (local-file "keys/nonguix-signing-key.pub")
                                                  (local-file "keys/rpi-local-key.pub")
                                                  (local-file "keys/orv2-local-key.pub")))
                                (substitute-urls '("https://substitutes.nonguix.org"))))
                                ;; (substitute-urls '("https://nonguix-proxy.ditigal.xyz"))
                                (substitute-urls '("https://substitutes.nonguix.org"))
                                ))

              (service earlyoom-service-type
                       (earlyoom-configuration


@@ 355,8 392,9 @@
               (mingetty-service-type config => (if (string=? (mingetty-configuration-tty config) "tty1")
                                                    (mingetty-configuration
                                                     (inherit config)
                                                     (auto-login "ruther")
                                                     (login-pause? #t))
                                                     (auto-login "auto")
                                                     (login-program (make-auto-login-program #:users '("ruther" "fbw")))
                                                     (login-pause? #f))
                                                    config))
               (elogind-service-type config => (elogind-configuration
                                                (handle-lid-switch-external-power 'ignore)))


@@ 380,93 418,39 @@
             (ice-9 match)
             (guix records))

(define-record-type <non-grafted>
  (non-grafted derivation)
  non-grafted?
  (derivation non-grafted-derivation))

(define-gexp-compiler (non-grafted-compiler (non-grafted <non-grafted>) system target)
  (mlet* %store-monad ((drv (non-grafted-derivation non-grafted)))
    (without-grafting
     (gexp->derivation "non-grafted"
                       #~(symlink #$drv #$output)))))

(define-record-type* <drv-name>
  drv-name-record drv-name
  drv-name?
  (derivation drv-name-derivation)
  (override-grafts? drv-name-override-grafts?
                    (default #f)))

(define-gexp-compiler (drv-name-compiler (drv-name <drv-name>) system target)
  (let ((drv-name-store (derivation->drv-name (drv-name-derivation drv-name))))
    (if (drv-name-override-grafts? drv-name)
        (without-grafting drv-name-store)
        drv-name-store)))
(define (non-grafted file)
  (computed-file
   "non-grafted"
   #~(symlink
      #$(with-parameters
            ((%graft? #f))
          file)
      #$output)))

(define <operating-system> (@@ (gnu system) <operating-system>))
(define-gexp-compiler (os-compiler (os <operating-system>) system target)
  (operating-system-derivation os))

;; I don't know why the inputs aren't getting grafted here, but they aren't, that's good here.
(define (make-non-grafted-package pkgs)
  (package
    (name "non-grafted-pkg")
    (version "0")
    (synopsis #f)
    (source #f)
    (description #f)
    (home-page #f)
    (license #f)
    (build-system (@ (guix build-system trivial) trivial-build-system))
    ;; (inputs pkgs)
    (arguments
     (list
      #:builder #~(begin
                    (use-modules
                     (srfi srfi-19))

                    (mkdir #$output)

                    (for-each (lambda (pkg)
                                (symlink
                                 pkg
                                 (string-append #$output "/" (basename pkg))))
                              '#$pkgs)

                    (symlink #$guile-3.0:debug (string-append #$output "/" "guile-debug"))
                    (symlink #$(libc-utf8-locales-for-target):static (string-append #$output "/" "guile-debug"))

                    ;; (for-each (lambda (pkg)
                    ;;             (unless (file-exists? (string-append #$output "/" (basename pkg)))
                    ;;               (symlink
                    ;;                pkg
                    ;;                (string-append #$output "/" (basename pkg)))))
                    ;;           (map cdr %build-inputs))
                    )))))

;; Takes an operating system and gc roots its derivation
(define (operating-system-with-build-inputs os)
  (operating-system
    (inherit os)
    (services (operating-system-user-services os))
    (services
     (cons*
      (simple-service 'gc-root-system-derivation
                      gc-root-service-type
                      (list
                       (drv-name (operating-system-derivation os) #t)
                       (drv-name (operating-system-derivation os) #f)
                       (non-grafted (operating-system-derivation os))
                       (non-grafted os)
                       (libc-utf8-locales-for-target)
                       guile-3.0
                       (make-non-grafted-package
                        (list
                         guile-3.0
                         ;; `(,guile-3.0 "debug")
                         (libc-utf8-locales-for-target)
                         texinfo))))
                       ;; (map
                       ;;  non-grafted
                       ;;  (list
                       ;;   guile-3.0
                       ;;   (libc-utf8-locales-for-target)
                       ;;   texinfo))
                       ))
      (operating-system-user-services os)))))

(operating-system-with-build-inputs %ruther/base-laptop-os)
;; (operating-system-with-build-inputs %ruther/base-laptop-os)
%ruther/base-laptop-os

M home/dotfiles/foot.ini => home/dotfiles/foot.ini +1 -1
@@ 1,1 1,1 @@
font=Hack:size=12
\ No newline at end of file
font=Comic Shanns Mono:size=14
\ No newline at end of file

M home/dotfiles/waybar/config.jsonc => home/dotfiles/waybar/config.jsonc +26 -0
@@ 16,6 16,7 @@
    "modules-center": [
    ],
    "modules-right": [
        "custom/dnd",
        "idle_inhibitor",
        "pulseaudio",
        "network",


@@ 51,6 52,18 @@
        "tooltip": true,
        "tooltip-format": "{app}: {title}"
    },
    "custom/dnd": {
        "exec": "if makoctl mode | grep -qx do-not-disturb; then echo '{\"alt\":\"on\",\"class\":\"on\",\"tooltip\":\"Do not disturb: on\"}'; else echo '{\"alt\":\"off\",\"class\":\"off\",\"tooltip\":\"Do not disturb: off\"}'; fi",
        "return-type": "json",
        "format": "{icon}",
        "format-icons": {
            "on": "",
            "off": ""
        },
        "on-click": "makoctl mode -t do-not-disturb; pkill -RTMIN+8 waybar",
        "signal": 8,
        "interval": 60
    },
    "idle_inhibitor": {
        "format": "{icon}",
        "format-icons": {


@@ 155,6 168,7 @@
    "modules-center": [
    ],
    "modules-right": [
        "custom/dnd",
        "idle_inhibitor",
        "pulseaudio",
        "network",


@@ 191,6 205,18 @@
        "tooltip": true,
        "tooltip-format": "{app}: {title}"
    },
    "custom/dnd": {
        "exec": "if makoctl mode | grep -qx do-not-disturb; then echo '{\"alt\":\"on\",\"class\":\"on\",\"tooltip\":\"Do not disturb: on\"}'; else echo '{\"alt\":\"off\",\"class\":\"off\",\"tooltip\":\"Do not disturb: off\"}'; fi",
        "return-type": "json",
        "format": "{icon}",
        "format-icons": {
            "on": "",
            "off": ""
        },
        "on-click": "makoctl mode -t do-not-disturb; pkill -RTMIN+8 waybar",
        "signal": 8,
        "interval": 60
    },
    "idle_inhibitor": {
        "format": "{icon}",
        "format-icons": {

M home/dotfiles/waybar/style.css => home/dotfiles/waybar/style.css +5 -0
@@ 84,6 84,7 @@ button:hover {
#custom-media,
#tray,
#mode,
#custom-dnd,
#idle_inhibitor,
#scratchpad,
#power-profiles-daemon,


@@ 135,6 136,10 @@ button:hover {
    padding-right: 15px;
}

#custom-dnd.on {
    color: #f53c3c;
}

label:focus {
    background-color: #000000;
}

M home/home-configuration.scm => home/home-configuration.scm +2 -18
@@ 119,21 119,6 @@
                         (find-files source "\\.(otf|otc)$"))
               #t))))))))

(define (apply-patches pkg patches)
  (let ((src (package-source pkg))
        (source-patches
         (if (list? patches)
             patches
             (list patches))))
    (package
      (inherit pkg)
      (source
       (origin
         (inherit src)
         (patches
          (append source-patches
                  (origin-patches src))))))))

(home-environment
 (packages
  (append


@@ 149,8 134,7 @@
    dwm

    offlineimap
    (apply-patches msmtp
                   (local-file "./patches/msmtp-from-utf8-encoding.patch"))
    msmtp
    (emacs-for-tty)
    (emacs-packages))
   (specifications->packages


@@ 263,7 247,7 @@ $XDG_DATA_DIRS")
         (service home-gtk-service-type
                  (let ((gtk-theme (make-graphite-gtk-theme #:theme "orange" #:color "dark"))
                        (icon-theme (make-tela-circle-icon-theme #:variant "orange"))
                        (cursor-theme (make-catppuccin-cursors #:flavor "frappe" #:accent "sapphire")))
                        (cursor-theme (make-catppuccin-cursors #:flavor "latte" #:accent "maroon")))
                    (home-gtk-configuration
                     (gtk-theme
                      (gtk-theme-configuration

M home/modules/ruther/home/dwl/config.h => home/modules/ruther/home/dwl/config.h +46 -0
@@ 287,3 287,49 @@ static const Button buttons[] = {
	{ MODKEY, BTN_MIDDLE, togglefloating, {0} },
	{ MODKEY, BTN_RIGHT,  moveresize,     {.ui = CurResize} },
};

bool applycustomrules(const char *appid, const char *title, Rule *r)
{
  static int opened_librewolf = 0; // appid librewolf
  static int opened_firefox = 0; // appid Firefox
  static int opened_vesktop = 0; // appid vesktop
  static int opened_element = 0; // appid Element

  if (strstr(appid, "librewolf")) {
    if (opened_librewolf < 4) {
      r->tags = 1 << (opened_librewolf / 2);
      r->allow_activation = 1;
      opened_librewolf++;
      return true;
    }
  }

  if (strstr(appid, "Firefox")) {
    if (opened_firefox < 2) {
      r->tags = 1 << 7;
      r->allow_activation = 1;
      opened_firefox++;
      return true;
    }
  }

  if (strstr(appid, "Element")) {
    if (opened_element < 2) {
      r->tags = 1 << 8;
      r->allow_activation = 1;
      opened_element++;
      return true;
    }
  }

  if (strstr(appid, "vesktop")) {
    if (opened_vesktop < 2) {
      r->tags = 1 << 8;
      r->allow_activation = 1;
      opened_vesktop++;
      return true;
    }
  }

  return false;
}

M home/modules/ruther/home/dwl/scripts.scm => home/modules/ruther/home/dwl/scripts.scm +6 -1
@@ 76,7 76,12 @@
       "start"
       "wlr-services"
       (getenv "WAYLAND_DISPLAY")
       (getenv "DISPLAY"))))
       (getenv "DISPLAY"))

      (system "librewolf &")
      (system "firefox &")
      (system "element-desktop &")
      (system "dev.vencord.Vesktop &")))

(define (start-dwl-gexp dwl waybar)
  #~(begin

M home/modules/ruther/home/dwl/wm.scm => home/modules/ruther/home/dwl/wm.scm +3 -3
@@ 120,14 120,14 @@
         (method git-fetch)
         (uri (git-reference
               (url "https://git.ditigal.xyz/~ruther/dwl")
               (commit "2daeeb4ffd1b7b382b80976a14f0ae5f23b920b9")))
               (commit "c5333e44d94f9d3cf439ac7e257d64f3f3014cfb")))
         (file-name (git-file-name name "0.8-dev"))
         (sha256
          (base32 "1r5k6vzmgwwlwd3giym1fy3y633i770a838fbzrc41afnbwrzz8s"))))
          (base32 "1cbhan45jznpgwgrn9xab5605i8l4ldrlswwrj5a5pg7w4a1y2cv"))))
      (inputs
       (modify-inputs (package-inputs base)
         (append foot)
         (append rofi-wayland)
         (append rofi)
         (append swaylock)
         (append mew)
         (append dmenu-wl)

D home/patches/msmtp-from-utf8-encoding.patch => home/patches/msmtp-from-utf8-encoding.patch +0 -119
@@ 1,119 0,0 @@
From d4e93269f280a57863f55f30a910afabaf424c24 Mon Sep 17 00:00:00 2001
From: Martin Lambers <marlam@marlam.de>
Date: Sat, 28 Sep 2024 13:13:37 +0200
Subject: [PATCH] Properly encode non-ASCII strings in the From header

This fixes Github issue #162.
---
 src/msmtp.c |  4 +++-
 src/tools.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/tools.h |  8 ++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/src/msmtp.c b/src/msmtp.c
index 95297a6..75a4a29 100644
--- a/src/msmtp.c
+++ b/src/msmtp.c
@@ -4187,8 +4187,10 @@ int main(int argc, char *argv[])
         {
             if (account->from_full_name)
             {
+                char *enc_name = encode_for_header(account->from_full_name);
                 fprintf(prepend_header_tmpfile, "From: %s <%s>\n",
-                        account->from_full_name, account->from);
+                        enc_name, account->from);
+                free(enc_name);
             }
             else
             {
diff --git a/src/tools.c b/src/tools.c
index d029f24..11718d3 100644
--- a/src/tools.c
+++ b/src/tools.c
@@ -50,9 +50,11 @@
 #include <unistd.h>
 #ifdef ENABLE_NLS
 # include <locale.h>
+# include <langinfo.h>
 #endif
 
 #include "xalloc.h"
+#include "base64.h"
 #include "tools.h"
 
 
@@ -907,3 +909,57 @@ int check_hostname_matches_domain(const char *hostname, const char *domain)
     return (hostname[hostname_len - 1 - domain_len] == '.'
             && strcasecmp(hostname + (hostname_len - domain_len), domain) == 0) ? 1 : 0;
 }
+
+
+/*
+ * encode_for_header()
+ *
+ * see tools.h
+ */
+
+char *encode_for_header(const char *s)
+{
+    int needsEncoding = 0;
+    for (int i = 0; s[i]; i++)
+    {
+        if (s[i] < 32 || s[i] >= 127)
+        {
+            needsEncoding = 1;
+            break;
+        }
+    }
+    if (needsEncoding)
+    {
+        /* create a string of the form "=?ENCODING?B?BASE64STRING?=" */
+        size_t s_len = strlen(s);
+        size_t b64_s_len = BASE64_LENGTH(s_len);
+        char* encoding = xstrdup(
+#ifdef ENABLE_NLS
+                nl_langinfo(CODESET)
+#else
+                "UTF-8";
+#endif
+                );
+        size_t e_len = strlen(encoding);
+        size_t enc_len = 2 + e_len + 3 + b64_s_len + 3;
+        char *enc = xmalloc(enc_len + 1);
+        size_t i = 0;
+        enc[i++] = '=';
+        enc[i++] = '?';
+        for (size_t j = 0; j < e_len; j++)
+        {
+            enc[i++] = tolower(encoding[j]);
+        }
+        free(encoding);
+        enc[i++] = '?';
+        enc[i++] = 'B';
+        enc[i++] = '?';
+        base64_encode(s, s_len, enc + i, enc_len - i + 1);
+        strcat(enc, "?=");
+        return enc;
+    }
+    else
+    {
+        return xstrdup(s);
+    }
+}
diff --git a/src/tools.h b/src/tools.h
index 86ba9ff..ebdeb73 100644
--- a/src/tools.h
+++ b/src/tools.h
@@ -259,4 +259,12 @@ void split_mail_address(const char *address, char **local_part, char **domain_pa
  */
 int check_hostname_matches_domain(const char *hostname, const char *domain);
 
+/*
+ * encode_for_header()
+ *
+ * Encode a string so that it can be used in a mail header.
+ * The result is allocated and needs to be freed.
+ */
+char *encode_for_header(const char *s);
+
 #endif

M home/patches/offlineimap3.patch => home/patches/offlineimap3.patch +167 -11
@@ 1,11 1,167 @@
--- a/offlineimap/localeval.py	2025-05-04 12:19:37.739860684 +0200
+++ b/offlineimap/localeval.py	2025-05-04 12:26:07.838113251 +0200
@@ -26,6 +26,8 @@
         self.namespace = {}
 
         if path is not None:
+            import importlib.machinery
+
             # FIXME: limit opening files owned by current user with rights set
             # to fixed mode 644.
             foo = open(path, 'r')
From 1c4f9b4f05de575cd99ac5bad68c6e0bbad25374 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodolfo=20Garc=C3=ADa=20Pe=C3=B1as=20=28kix=29?=
 <kix@kix.es>
Date: Wed, 13 May 2026 18:35:54 +0200
Subject: [PATCH] Enhance STARTTLS handling with fallback for non-standard IMAP
 servers

- Implement tolerance for non-standard IMAP servers that do not provide reliable CAPABILITY after STARTTLS.
- Introduce configuration option 'allow_nonstandard_capabilities' to enable fallback using pre-TLS capabilities.
---
 offlineimap/imapserver.py      | 110 +++++++++++++++++++++++++++------
 offlineimap/repository/IMAP.py |   7 +++
 2 files changed, 98 insertions(+), 19 deletions(-)

diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py
index fd866867..b29b5d6c 100644
--- a/offlineimap/imapserver.py
+++ b/offlineimap/imapserver.py
@@ -363,32 +363,95 @@ def __gsshandler(self, token):
             return None

     def __start_tls(self, imapobj):
-        if 'STARTTLS' in imapobj.capabilities and not self.usessl:
-            self.ui.debug('imap', 'Using STARTTLS connection')
-            # imaplib2.starttls() unconditionally calls _get_capabilities() right
-            # after the TLS handshake (before setting _tls_established=True).
-            # Servers like Protonmail Bridge do not respond to CAPABILITY at
-            # that stage, causing a 60-second hang.  We bypass it by temporarily
-            # substituting a no-op; imaplib2 still sets _tls_established=True
-            # normally, and acquireconnection() already guards its own
-            # post-login CAPABILITY refresh against that flag (see below).
-            _orig_get_cap = imapobj._get_capabilities
-            imapobj._get_capabilities = lambda: None
+        """Upgrade connection to TLS if STARTTLS is configured.
+
+        - Uses pre-TLS capabilities only to decide whether to perform STARTTLS.
+        - After STARTTLS, forces a CAPABILITY command and replaces internal
+          capabilities with the post-TLS response.
+        - If no reliable post-TLS CAPABILITY is available:
+          - In strict mode (allow_nonstandard_capabilities = no): abort.
+          - In tolerant mode (allow_nonstandard_capabilities = yes):
+            reuse pre-TLS capabilities, removing LOGINDISABLED.
+        """
+
+        # If the repository does not want STARTTLS, do nothing.
+        if not self.starttls or self.usessl:
+            return
+
+        # Pre-TLS capabilities (from initial banner / CAPABILITY).
+        caps_pre = set(getattr(imapobj, '_offlineimap_capabilities_pre_tls',
+                               getattr(imapobj, 'capabilities', [])))
+
+        # If the server does not advertise STARTTLS, respect that.
+        if 'STARTTLS' not in caps_pre:
+            return
+
+        # Execute STARTTLS.
+        self.ui.debug('imap', 'Using STARTTLS connection')
+
+        # Disable _get_capabilities() during starttls() to prevent imaplib2
+        # from issuing CAPABILITY before we can do it ourselves.
+        _orig_get_cap = imapobj._get_capabilities
+        imapobj._get_capabilities = lambda: None
+        try:
             try:
                 imapobj.starttls()
             except imapobj.error as e:
                 err = "Failed to start TLS connection: %s" % str(e)
                 raise OfflineImapError(err, OfflineImapError.ERROR.REPO,
                                        exc_info()[2])
-            finally:
-                # Always restore the original method, even on success.
-                imapobj._get_capabilities = _orig_get_cap
-        elif self.starttls and not self.usessl:
+        finally:
+            # Always restore the original method, even on error.
+            imapobj._get_capabilities = _orig_get_cap
+
+        # At this point, the socket is encrypted. Now we must refresh CAPABILITY.
+        caps_post = None
+        try:
+            typ, data = imapobj.capability()
+            if typ == 'OK' and data:
+                # data is a list of bytes, e.g. [b'IMAP4rev1 IDLE AUTH=PLAIN']
+                line = data[0]
+                if isinstance(line, bytes):
+                    line = line.decode('ascii', 'ignore')
+                caps_post = set(line.upper().split())
+        except Exception:
+            caps_post = None
+
+        if caps_post is not None:
+            # Standard path: completely replace capabilities
+            imapobj.capabilities = caps_post
+            imapobj._offlineimap_capabilities_post_tls = caps_post
+            return
+
+        # If we reach here, we do not have reliable post-TLS capabilities.
+        # Decide based on repository configuration.
+        allow_nonstandard = getattr(self.repos,
+                                    'allow_nonstandard_capabilities', False)
+
+        if not allow_nonstandard:
+            # Strict mode: abort with clear error.
             raise OfflineImapError(
-                "STARTTLS requested but server does not advertise STARTTLS capability. "
-                "Aborting to prevent sending password in plaintext (possible MITM attack). "
-                "If you REALLY want an insecure connection, set 'starttls = no'.",
-                OfflineImapError.ERROR.REPO)
+                "Server did not provide valid CAPABILITY after STARTTLS; "
+                "set 'allow_nonstandard_capabilities = yes' in repository "
+                "configuration to enable a non-standard fallback.",
+                OfflineImapError.ERROR.REPO
+            )
+
+        # Tolerant mode: best-effort using caps_pre.
+        caps_fallback = set(caps_pre)
+        if 'LOGINDISABLED' in caps_fallback:
+            # We assume that after STARTTLS, LOGINDISABLED no longer applies,
+            # so we remove it to allow LOGIN/AUTH configured by the user.
+            caps_fallback.remove('LOGINDISABLED')
+
+        imapobj.capabilities = caps_fallback
+        imapobj._offlineimap_capabilities_post_tls = caps_fallback
+
+        self.ui.warn(
+            "Server did not provide CAPABILITY after STARTTLS; "
+            "falling back to pre-TLS capabilities without LOGINDISABLED "
+            "due to allow_nonstandard_capabilities = yes."
+        )

     # All __authn_* procedures are helpers that do authentication.
     # They are class methods that take one parameter, IMAP object.
@@ -605,6 +668,15 @@ def acquireconnection(self):
             self.lastowner[imapobj] = curThread.ident
             self.connectionlock.release()

+            # Store the pre-TLS capabilities (from banner and initial CAPABILITY).
+            # This will be used only to decide STARTTLS and, in case of non-standard
+            # fallback, to reconstruct an approximate post-TLS capability list.
+            try:
+                caps_pre = set(getattr(imapobj, 'capabilities', []))
+            except Exception:
+                caps_pre = set()
+            imapobj._offlineimap_capabilities_pre_tls = caps_pre
+
             # Verify that the connection is still alive before returning it
             # to the caller.  If not, clean up and recursively call
             # acquireconnection() to get a new one.
diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py
index b1afe861..db64dae1 100644
--- a/offlineimap/repository/IMAP.py
+++ b/offlineimap/repository/IMAP.py
@@ -38,6 +38,13 @@ def __init__(self, reposname, account):
         self.idlefolders = None
         BaseRepository.__init__(self, reposname, account)
         # self.ui is being set by the BaseRepository
+
+        # Allow tolerance for non-standard IMAP servers that do not provide
+        # reliable CAPABILITY after STARTTLS. Default is strict (False).
+        # If set to True, heuristics using pre-TLS capabilities are applied
+        # as a fallback.
+        self.allow_nonstandard_capabilities = self.getconfboolean(
+            'allow_nonstandard_capabilities', False)
         self._host = None
         # Must be set before calling imapserver.IMAPServer(self)
         self.oauth2_request_url = None

M isos/simple.scm => isos/simple.scm +1 -1
@@ 77,7 77,7 @@
                            (package-version guix))))


  (kernel linux-6.13)
  (kernel linux-lts)
  (firmware (cons* linux-firmware
                   %base-firmware))


A modules/ruther/services/auto-login.scm => modules/ruther/services/auto-login.scm +49 -0
@@ 0,0 1,49 @@
(define-module (ruther services auto-login)
  #:use-module (guix gexp)
  #:use-module (gnu packages admin)
  #:use-module (gnu packages base)
  #:export (make-auto-login-program))

(define* (make-auto-login-program #:key (users '()))
  (program-file
   "auto-login"
   #~(begin
       (define users (list #$@users))
       (define marker "/tmp/.auto-login-done")
       (define stty-bin #$(file-append coreutils "/bin/stty"))

       (define (show-menu!)
         (display "\nAuto-login — choose account:\n")
         (let loop ((us users) (i 0))
           (when (pair? us)
             (format #t "  ~a) ~a\n" (integer->char (+ i (char->integer #\a))) (car us))
             (loop (cdr us) (+ i 1))))
         (display "> "))

       (define (read-key)
         (dynamic-wind
           (lambda () (system* stty-bin "-echo" "cbreak"))
           (lambda () (read-char))
           (lambda () (system* stty-bin "echo" "-cbreak"))))

       (define (read-choice)
         (show-menu!)
         (let* ((ch  (read-key))
                (idx (- (char->integer ch) (char->integer #\a))))
           (newline)
           (if (and (>= idx 0) (< idx (length users)))
               (list-ref users idx)
               (begin
                 (display "Invalid choice, try again.\n")
                 (read-choice)))))

       (let ((login-bin #$(file-append shadow "/bin/login"))
             (args       (filter (lambda (a) (not (or (string=? a "-f")
                                                      (string=? a "auto"))))
                                (cdr (command-line)))))  ; strip -f and dummy username injected by mingetty
         (if (file-exists? marker)
             (apply execlp login-bin login-bin args)
             (let ((user (read-choice)))
               (call-with-output-file marker
                 (lambda (p) (display (getpid) p)))
               (apply execlp login-bin login-bin "-f" user args)))))))

A modules/ruther/services/nix.scm => modules/ruther/services/nix.scm +58 -0
@@ 0,0 1,58 @@
;; Custom nix-daemon service that raises its NOFILE rlimit.
;;
;; The stock (gnu services nix) nix-daemon is started by shepherd with the
;; default 1024/4096 open-files rlimit.  Build processes run as @nixbld are
;; spawned as children of the daemon, so they inherit that limit — PAM limits
;; for @nixbld are ineffective because nixbld users never log in.
;;
;; This module re-exports a service type that inherits from nix-service-type
;; but replaces its shepherd-root-service-type extension with one that passes
;; #:resource-limits to make-forkexec-constructor.

(define-module (ruther services nix)
  #:use-module (srfi srfi-1)
  #:use-module (guix gexp)
  #:use-module (gnu services)
  #:use-module (gnu services nix)
  #:use-module (gnu services shepherd)
  #:export (nix-service-type/raised-nofile))

;; The nix-configuration-* accessors are not exported from (gnu services nix),
;; so reach in with @@.
(define nix-configuration-package
  (@@ (gnu services nix) nix-configuration-package))
(define nix-configuration-build-directory
  (@@ (gnu services nix) nix-configuration-build-directory))
(define nix-configuration-extra-options
  (@@ (gnu services nix) nix-configuration-extra-options))

(define (nix-shepherd-service/raised-nofile config)
  (let ((package (nix-configuration-package config))
        (build-directory (nix-configuration-build-directory config))
        (extra-options (nix-configuration-extra-options config)))
    (list
     (shepherd-service
      (provision '(nix-daemon))
      (documentation "Run nix-daemon with a raised NOFILE rlimit.")
      (requirement '(user-processes file-system-/nix/store))
      (start #~(make-forkexec-constructor
                (list (string-append #$package "/bin/nix-daemon")
                      #$@extra-options)
                #:environment-variables
                (list (string-append "TMPDIR=" #$build-directory)
                      "PATH=/run/current-system/profile/bin")
                #:resource-limits
                '((nofile 1048576 1048576))))
      (respawn? #f)
      (stop #~(make-kill-destructor))))))

(define nix-service-type/raised-nofile
  (service-type
   (inherit nix-service-type)
   (extensions
    (cons (service-extension shepherd-root-service-type
                             nix-shepherd-service/raised-nofile)
          (filter (lambda (ext)
                    (not (eq? (service-extension-target ext)
                              shepherd-root-service-type)))
                  (service-type-extensions nix-service-type))))))

A udev/51-android.rules => udev/51-android.rules +1 -0
@@ 0,0 1,1 @@
SUBSYSTEMS=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="4ee1", MODE="0666", TAG+="uaccess", SYMLINK+="android%n"
\ No newline at end of file