From d87a209b9efd2e32827aca20c66af7f94ce9e6c1 Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Fri, 18 Sep 2026 03:43:48 +0530 Subject: fix(communication/neomutt): fixed mbsync syncing with different modes Now there are two channels created for every account quick and full variants, quick only gives new emails and full does the whole sync. neomutt-sync can now be told which channel to use to sync with. --- .../neomutt/account/flavors/gmail.nix | 1 - modules/communication/neomutt/lib.nix | 20 ++++ modules/communication/neomutt/mail/mbsync.nix | 104 +++++++++++++++++++- modules/communication/neomutt/neomutt-sync.sh | 108 ++++++++++++++------- 4 files changed, 196 insertions(+), 37 deletions(-) create mode 100644 modules/communication/neomutt/lib.nix (limited to 'modules') diff --git a/modules/communication/neomutt/account/flavors/gmail.nix b/modules/communication/neomutt/account/flavors/gmail.nix index ba39c58..9cb51b2 100644 --- a/modules/communication/neomutt/account/flavors/gmail.nix +++ b/modules/communication/neomutt/account/flavors/gmail.nix @@ -21,7 +21,6 @@ "[Gmail]/Drafts" "[Gmail]/Sent Mail" "[Gmail]/Spam" - "[Gmail]/Trash" ]; extraConfig.channel = { Sync = lib.mkDefault "All"; diff --git a/modules/communication/neomutt/lib.nix b/modules/communication/neomutt/lib.nix new file mode 100644 index 0000000..253ada8 --- /dev/null +++ b/modules/communication/neomutt/lib.nix @@ -0,0 +1,20 @@ +# some basic common utility functions for neomutt +{ lib, ... }: +{ + mbsyncNamesFromName = + name: + let + channelNameBase = lib.replaceStrings [ "@" "." ] [ "-" "_" ] name; + in + { + base = channelNameBase; + channels = { + full = "${channelNameBase}-full"; + quick = "${channelNameBase}-quick"; + }; + servers = { + remote = "${channelNameBase}-remote"; + local = "${channelNameBase}-local"; + }; + }; +} diff --git a/modules/communication/neomutt/mail/mbsync.nix b/modules/communication/neomutt/mail/mbsync.nix index 28b7b2c..1d21377 100644 --- a/modules/communication/neomutt/mail/mbsync.nix +++ b/modules/communication/neomutt/mail/mbsync.nix @@ -1,7 +1,107 @@ -# Mailbox synchronization -{ config, lib, ... }: +{ + config, + lib, + ... +}@inputs: +let + genValue = + v: + if lib.isList v then + lib.concatStringsSep " " (map genValue v) + else if lib.isBool v then + lib.hm.booleans.yesNo v + else if lib.isInt v then + toString v + else if lib.isString v then + if builtins.match ".* .*" v != null then ''"${lib.escape [ ''"'' ] v}"'' else v + else + throw "Unsupported mbsync value"; + + genSection = name: attrs: '' + ${name} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "${n} ${genValue v}") attrs)} + ''; + + genAccount = + a: + let + mbsyncNames = (import ../lib.nix inputs).mbsyncNamesFromName a.name; + + quick = { + Far = ":${mbsyncNames.servers.remote}:\"INBOX\""; + Near = ":${mbsyncNames.servers.local}:\"INBOX\""; + Create = "Near"; + Expunge = "None"; + Sync = [ + "Pull" + "New" + ]; + SyncState = "*"; + }; + + full = { + Far = ":${mbsyncNames.servers.remote}:"; + Near = ":${mbsyncNames.servers.local}:"; + Patterns = a.mbsync.patterns; + Create = "Near"; + Expunge = "Both"; + Sync = "All"; + SyncState = "*"; + }; + in + genSection "IMAPAccount ${mbsyncNames.base}" ( + { + Host = a.imap.host; + User = a.userName; + PipelineDepth = 50; + TLSType = + if !a.imap.tls.enable then + "None" + else if a.imap.tls.useStartTls then + "STARTTLS" + else + "IMAPS"; + } + // lib.optionalAttrs (a.passwordCommand != null) { + PassCmd = toString a.passwordCommand; + } + // lib.optionalAttrs (a.imap.tls.certificatesFile != null) { + CertificateFile = toString a.imap.tls.certificatesFile; + } + // lib.optionalAttrs (a.imap.port != null) { + Port = a.imap.port; + } + ) + + + "\n" + + genSection "IMAPStore ${mbsyncNames.servers.remote}" { + Account = mbsyncNames.base; + } + + + "\n" + + genSection "MaildirStore ${mbsyncNames.servers.local}" { + Path = "${a.maildir.absPath}/"; + Inbox = "${a.maildir.absPath}/${a.folders.inbox}"; + SubFolders = "Verbatim"; + } + + + "\n" + + genSection "Channel ${mbsyncNames.channels.quick}" quick + + + "\n" + + genSection "Channel ${mbsyncNames.channels.full}" full + + + "\n"; + + accounts = lib.filter (a: a.enable && a.mbsync.enable) ( + lib.attrValues config.accounts.email.accounts + ); + +in { config = lib.mkIf config.communication.neomutt.enable { programs.mbsync.enable = true; + + xdg.configFile."mbsync/.mbsyncrc".text = lib.concatStringsSep "\n" (map genAccount accounts); }; } diff --git a/modules/communication/neomutt/neomutt-sync.sh b/modules/communication/neomutt/neomutt-sync.sh index 67e007d..ff43039 100644 --- a/modules/communication/neomutt/neomutt-sync.sh +++ b/modules/communication/neomutt/neomutt-sync.sh @@ -1,14 +1,31 @@ #!/usr/bin/env bash -# Sync mail with mbsync and index new messages with notmuch, -# showing progress via a dialog gauge. +# Sync one mbsync channel and index new messages with notmuch. + +set -uo pipefail + +MBSYNCRC="${MBSYNCRC:-$HOME/.config/mbsync/.mbsyncrc}" +TITLE="[󰇮 SYNCING MAIL]" DIALOGRC=$(mktemp) +MBSYNC_LOG=$(mktemp) NOTMUCH_LOG=$(mktemp) -TITLE="[󰇮 SYNCING MAIL]" + export DIALOGRC -trap 'rm -f "$DIALOGRC" "$NOTMUCH_LOG"' EXIT # Clean up everything on exit -cat << 'EOF' > "$DIALOGRC" +trap 'rm -f "$DIALOGRC" "$MBSYNC_LOG" "$NOTMUCH_LOG"' EXIT + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " + echo + echo "Example:" + echo " $0 arpitchakladar-gmail_com-quick" + echo " $0 arpitchakladar-gmail_com-full" + exit 2 +fi + +CHANNEL="$1" + +cat <<'EOF' > "$DIALOGRC" use_shadow = ON use_colors = ON screen_color = (WHITE,BLACK,OFF) @@ -19,47 +36,70 @@ border_color = (CYAN,BLACK,ON) border2_color = (BLUE,BLACK,ON) shadow_color = (BLACK,BLACK,ON) -# Active buttons: magenta emphasis, matching Base16 base0E button_active_color = (MAGENTA,BLACK,ON) button_key_active_color = (MAGENTA,BLACK,ON) button_label_active_color = (MAGENTA,BLACK,ON) -# Inactive buttons: cyan secondary emphasis, matching Base16 base0C button_inactive_color = (CYAN,BLACK,OFF) button_key_inactive_color = (CYAN,BLACK,OFF) button_label_inactive_color = (CYAN,BLACK,OFF) EOF -# Run the sync and indexing progress bar ( - # mbsync phase - script -q -e -c "mbsync -a" /dev/null | awk -v RS='\r' ' - { - c_str = "" - b_str = "" - for(i=1; i<=NF; i++) { - if ($i == "C:") c_str = $(i+1) - if ($i == "B:") b_str = $(i+1) - } - if (b_str != "") { - split(b_str, arr, "/") - if (arr[2] > 0) { - percent = int((arr[1] / arr[2]) * 100) - printf "XXX\n%d\nSyncing | Channel: %s | Mailbox: %s\nXXX\n", percent, c_str, b_str - } - } - fflush() - }' - - # notmuch phase - printf "XXX\n100\nIndexing new mail with notmuch...\nXXX\n" - notmuch new > "$NOTMUCH_LOG" 2>&1 + printf "XXX\n0\nSyncing: %s...\nXXX\n" "$CHANNEL" + + mbsync -c "$MBSYNCRC" "$CHANNEL" > "$MBSYNC_LOG" 2>&1 & + PID=$! + + # Keep the gauge alive while mbsync runs. + while kill -0 "$PID" 2>/dev/null; do + printf "XXX\n50\nSyncing: %s...\nXXX\n" "$CHANNEL" + sleep 0.5 + done + + wait "$PID" + MBSYNC_STATUS=$? + + if (( MBSYNC_STATUS != 0 )); then + printf "XXX\n100\nSync failed: %s\nXXX\n" "$CHANNEL" + exit "$MBSYNC_STATUS" + fi + + printf "XXX\n90\nIndexing new mail with notmuch...\nXXX\n" + + notmuch new --quiet > "$NOTMUCH_LOG" 2>&1 + NOTMUCH_STATUS=$? + + printf "XXX\n100\nDone.\nXXX\n" + + if (( NOTMUCH_STATUS != 0 )); then + exit "$NOTMUCH_STATUS" + fi ) | dialog --title "$TITLE" --gauge "Initializing..." 8 80 0 -# Format the captured notmuch output into a single clean line -CLEAN_OUT=$(tr '\n' ' ' < "$NOTMUCH_LOG" | sed 's/ */ /g') +STATUS=${PIPESTATUS[0]} + +MBSYNC_OUT=$( + tr '\n' ' ' < "$MBSYNC_LOG" | + sed 's/ */ /g' +) + +NOTMUCH_OUT=$( + tr '\n' ' ' < "$NOTMUCH_LOG" | + sed 's/ */ /g' +) -# Display final results and wait for user input -dialog --title "$TITLE" --msgbox "Done!\n$CLEAN_OUT" 8 80 +if (( STATUS == 0 )); then + dialog \ + --title "$TITLE" \ + --msgbox "Done!\n\nChannel: $CHANNEL\n\nmbsync: ${MBSYNC_OUT:-OK}\n\nnotmuch: ${NOTMUCH_OUT:-OK}" \ + 12 80 +else + dialog \ + --title "$TITLE" \ + --msgbox "Sync failed!\n\nChannel: $CHANNEL\n\nmbsync: ${MBSYNC_OUT:-no output}\n\nnotmuch: ${NOTMUCH_OUT:-not run}" \ + 12 80 +fi clear +exit "$STATUS" -- cgit v1.2.3