diff --git a/home-manager/machines.nix b/home-manager/machines.nix index 2b7dff7e..e50490b8 100644 --- a/home-manager/machines.nix +++ b/home-manager/machines.nix @@ -142,6 +142,7 @@ in hera.default = makeConfig "hera" (on-my-machines ++ [ ./roles/fetch-banking-timer.nix ./roles/weechat + ./roles/mail-sort.nix ./roles/mail2rss.nix ./roles/headless-mpd.nix ./roles/headless.nix diff --git a/home-manager/roles/mail-sort.nix b/home-manager/roles/mail-sort.nix new file mode 100644 index 00000000..c98542d1 --- /dev/null +++ b/home-manager/roles/mail-sort.nix @@ -0,0 +1,105 @@ +{ pkgs, lib, config, ... }: +let + quick-sync = pkgs.writeShellScript "quick-mail-and-todo-sync" '' + ${pkgs.isync}/bin/mbsync hera:INBOX,Code,Move/todo + ${pkgs.fd}/bin/fd -tf . ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mscan -f "E-Mail from %f: %S" | xargs -I '{}' ${pkgs.taskwarrior}/bin/task add '"{}"' + ${pkgs.mblaze}/bin/mlist ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mflag -S + ${pkgs.mblaze}/bin/mlist ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mrefile ${unsorted} + ${pkgs.notmuch}/bin/notmuch new + ''; + lists = pkgs.privateValue { sortLists = [ ]; stupidLists = [ ]; notifications = [ ]; } "mail/filters"; + maildir = config.accounts.email.maildirBasePath; + # mhdr -h List-ID -d Maildir/hera/Archiv/unsortiert | sort | sed 's/^.*<\(.*\)>$/\1/' | uniq | xargs -I '{}' sh -c "notmuch count List:{} | sed 's/$/: {}/'" | sort + # To find candidates + archiveSuffix = "hera/Archiv"; + unsortedSuffix = "${archiveSuffix}/unsortiert"; + unsorted = "${maildir}/${unsortedSuffix}"; + archive = "${maildir}/${archiveSuffix}"; + filter = rec { + mailToFolder = name: + toFolder (lib.concatStringsSep "." (lib.splitString "@" name)); + toFolder = name: + lib.concatStringsSep "/" (lib.reverseList (lib.splitString "." name)); + simple = filter: target: { inherit filter target; }; + notifications = notify: + simple "from:${notify}" "notifications/${mailToFolder notify}"; + stupidList = list: simple "to:${list}" "list/${mailToFolder list}"; + simpleSortList = listName: + simple "List:${listName}" "list/${toFolder listName}"; + }; + myFilters = builtins.map filter.simpleSortList lists.sortLists + ++ builtins.map filter.stupidList lists.stupidLists + ++ builtins.map filter.notifications lists.notifications; + sortMail = pkgs.writeHaskellScript + { + name = "sort-mail-archive"; + bins = [ pkgs.notmuch pkgs.coreutils pkgs.mblaze pkgs.findutils ]; + imports = [ + "Text.Megaparsec" + "Text.Megaparsec.Char" + "Text.Megaparsec.Char.Lexer" + "qualified Data.List.NonEmpty as NE" + "qualified Data.Text as T" + "System.Environment (setEnv)" + ]; + } '' + reScan = notmuch "new" "--quiet" + + findFilterMail :: (Text,Text) -> IO (Maybe (LByteString, Text, Text)) + findFilterMail (filter_, target) = do + files <- notmuch "search" "--output" "files" (toString filter_) "folder:${unsortedSuffix}" |> capture + pure $ if (LBS.length files > 0) then Just (files, filter_, target) else Nothing + + executeFilterMail :: (LByteString, Text, Text) -> IO () + executeFilterMail (files, filter_, target) = do + say [i|Sorting "#{filter_}" into #{target}|] + writeOutput files |> mscan + mmkdir ([i|${archive}/#{target}|] :: String) + writeOutput files |> mrefile ([i|${archive}/#{target}|] :: String) + + myFilters :: [(Text,Text)] + myFilters = [${ + lib.concatStringsSep "," + ( + builtins.map ({ filter, target }: ''("${filter}","${target}")'') + myFilters + ) + }] + + filtersFromTo :: Text -> Maybe (Text,Text) + filtersFromTo = filtersFromField "to" [toToName] + toToName :: Text -> Maybe Text + toToName (T.splitOn "@" -> [name, "maralorn.de"]) + | not (T.isInfixOf "randy" name) = Just . ("to/" <>) . T.intercalate "_" . T.splitOn "." $ name + toToName _ = Nothing + filtersFromField :: Text -> [Text-> Maybe Text] -> Text -> Maybe (Text,Text) + filtersFromField field filters text = fmap ([i|#{field}:#{text}|],) . viaNonEmpty Relude.head . mapMaybe ($ text) $ filters + filtersFromListIDs :: Text -> Maybe (Text,Text) + filtersFromListIDs = filtersFromField "List" [githubNameFolderFromId, gitlabNameFolderFromId] + githubNameFolderFromId :: Text -> Maybe Text + githubNameFolderFromId (reverse . T.splitOn "." -> ("com":"github":org:name)) = Just [i|github/#{org}/#{T.intercalate "_" $ reverse name}|] + githubNameFolderFromId _ = Nothing + gitlabNameFolderFromId :: Text -> Maybe Text + gitlabNameFolderFromId (reverse . T.splitOn "." -> ("de":"ccc":"darmstadt":"git":org:name1:name)) = Just [i|cda-gitlab/#{org}/#{T.intercalate "_" . toList . Relude.tail $ NE.reverse (name1:|name)}|] + gitlabNameFolderFromId _ = Nothing + + type Parser = Parsec Text Text + listId :: Parser Text + listId = manyTill anySingle (char '<') *> (toText <$> manyTill anySingle (char '>')) + + main = do + setEnv "MBLAZE_PAGER" "cat" + setEnv "NOTMUCH_CONFIG" "${config.home.sessionVariables.NOTMUCH_CONFIG or ""}" + reScan + (listIDs,tos) <- concurrently (mhdr "-h" "List-ID" "-d" "${unsorted}" |> capture) (mhdr "-h" "To" "-d" "${unsorted}" "-A" |> capture) + let listFilters = mapMaybe filtersFromListIDs . sortNub . mapMaybe (parseMaybe listId) . lines . decodeUtf8 $ listIDs + toFilters = mapMaybe filtersFromTo . sortNub . fmap (\x -> maybe x Relude.id $ parseMaybe listId x) . lines . decodeUtf8 $ tos + applicableFilters <- catMaybes <$> forConcurrently (listFilters <> myFilters <> toFilters) findFilterMail + for_ applicableFilters executeFilterMail + reScan + ''; +in +{ + services.mbsync.postExec = "${sortMail}/bin/sort-mail-archive"; + accounts.email.accounts = lib.mkIf pkgs.withSecrets { hera.imapnotify.onNotify = lib.mkForce "${quick-sync}"; }; +} diff --git a/home-manager/roles/mail.nix b/home-manager/roles/mail.nix index a219a2db..2035a859 100644 --- a/home-manager/roles/mail.nix +++ b/home-manager/roles/mail.nix @@ -4,104 +4,11 @@ let name = "Malte Brandy"; mail = "malte.brandy@maralorn.de"; alternates = pkgs.privateValue [ ] "mail/alternates"; - lists = pkgs.privateValue { sortLists = [ ]; stupidLists = [ ]; notifications = [ ]; } "mail/filters"; - quick-sync = pkgs.writeShellScript "quick-mail-sync" '' - ${pkgs.isync}/bin/mbsync hera:INBOX,Code,Move/todo - ${pkgs.fd}/bin/fd -tf . ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mscan -f "E-Mail from %f: %S" | xargs -I '{}' ${pkgs.taskwarrior}/bin/task add '"{}"' - ${pkgs.mblaze}/bin/mlist ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mflag -S - ${pkgs.mblaze}/bin/mlist ${maildir}/hera/Move/todo | ${pkgs.mblaze}/bin/mrefile ${unsorted} + quick-sync = pkgs.writeShellScriptBin "quick-mail-sync" '' + ${pkgs.isync}/bin/mbsync hera:INBOX,Code ${pkgs.notmuch}/bin/notmuch new ''; maildir = config.accounts.email.maildirBasePath; - # mhdr -h List-ID -d Maildir/hera/Archiv/unsortiert | sort | sed 's/^.*<\(.*\)>$/\1/' | uniq | xargs -I '{}' sh -c "notmuch count List:{} | sed 's/$/: {}/'" | sort - # To find candidates - archiveSuffix = "hera/Archiv"; - unsortedSuffix = "${archiveSuffix}/unsortiert"; - unsorted = "${maildir}/${unsortedSuffix}"; - archive = "${maildir}/${archiveSuffix}"; - filter = rec { - mailToFolder = name: - toFolder (lib.concatStringsSep "." (lib.splitString "@" name)); - toFolder = name: - lib.concatStringsSep "/" (lib.reverseList (lib.splitString "." name)); - simple = filter: target: { inherit filter target; }; - notifications = notify: - simple "from:${notify}" "notifications/${mailToFolder notify}"; - stupidList = list: simple "to:${list}" "list/${mailToFolder list}"; - simpleSortList = listName: - simple "List:${listName}" "list/${toFolder listName}"; - }; - myFilters = builtins.map filter.simpleSortList lists.sortLists - ++ builtins.map filter.stupidList lists.stupidLists - ++ builtins.map filter.notifications lists.notifications; - sortMail = pkgs.writeHaskellScript - { - name = "sort-mail-archive"; - bins = [ pkgs.notmuch pkgs.coreutils pkgs.mblaze pkgs.findutils ]; - imports = [ - "Text.Megaparsec" - "Text.Megaparsec.Char" - "Text.Megaparsec.Char.Lexer" - "qualified Data.List.NonEmpty as NE" - "qualified Data.Text as T" - "System.Environment (setEnv)" - ]; - } '' - reScan = notmuch "new" "--quiet" - - findFilterMail :: (Text,Text) -> IO (Maybe (LByteString, Text, Text)) - findFilterMail (filter_, target) = do - files <- notmuch "search" "--output" "files" (toString filter_) "folder:${unsortedSuffix}" |> capture - pure $ if (LBS.length files > 0) then Just (files, filter_, target) else Nothing - - executeFilterMail :: (LByteString, Text, Text) -> IO () - executeFilterMail (files, filter_, target) = do - say [i|Sorting "#{filter_}" into #{target}|] - writeOutput files |> mscan - mmkdir ([i|${archive}/#{target}|] :: String) - writeOutput files |> mrefile ([i|${archive}/#{target}|] :: String) - - myFilters :: [(Text,Text)] - myFilters = [${ - lib.concatStringsSep "," - ( - builtins.map ({ filter, target }: ''("${filter}","${target}")'') - myFilters - ) - }] - - filtersFromTo :: Text -> Maybe (Text,Text) - filtersFromTo = filtersFromField "to" [toToName] - toToName :: Text -> Maybe Text - toToName (T.splitOn "@" -> [name, "maralorn.de"]) - | not (T.isInfixOf "randy" name) = Just . ("to/" <>) . T.intercalate "_" . T.splitOn "." $ name - toToName _ = Nothing - filtersFromField :: Text -> [Text-> Maybe Text] -> Text -> Maybe (Text,Text) - filtersFromField field filters text = fmap ([i|#{field}:#{text}|],) . viaNonEmpty Relude.head . mapMaybe ($ text) $ filters - filtersFromListIDs :: Text -> Maybe (Text,Text) - filtersFromListIDs = filtersFromField "List" [githubNameFolderFromId, gitlabNameFolderFromId] - githubNameFolderFromId :: Text -> Maybe Text - githubNameFolderFromId (reverse . T.splitOn "." -> ("com":"github":org:name)) = Just [i|github/#{org}/#{T.intercalate "_" $ reverse name}|] - githubNameFolderFromId _ = Nothing - gitlabNameFolderFromId :: Text -> Maybe Text - gitlabNameFolderFromId (reverse . T.splitOn "." -> ("de":"ccc":"darmstadt":"git":org:name1:name)) = Just [i|cda-gitlab/#{org}/#{T.intercalate "_" . toList . Relude.tail $ NE.reverse (name1:|name)}|] - gitlabNameFolderFromId _ = Nothing - - type Parser = Parsec Text Text - listId :: Parser Text - listId = manyTill anySingle (char '<') *> (toText <$> manyTill anySingle (char '>')) - - main = do - setEnv "MBLAZE_PAGER" "cat" - setEnv "NOTMUCH_CONFIG" "${config.home.sessionVariables.NOTMUCH_CONFIG or ""}" - reScan - (listIDs,tos) <- concurrently (mhdr "-h" "List-ID" "-d" "${unsorted}" |> capture) (mhdr "-h" "To" "-d" "${unsorted}" "-A" |> capture) - let listFilters = mapMaybe filtersFromListIDs . sortNub . mapMaybe (parseMaybe listId) . lines . decodeUtf8 $ listIDs - toFilters = mapMaybe filtersFromTo . sortNub . fmap (\x -> maybe x Relude.id $ parseMaybe listId x) . lines . decodeUtf8 $ tos - applicableFilters <- catMaybes <$> forConcurrently (listFilters <> myFilters <> toFilters) findFilterMail - for_ applicableFilters executeFilterMail - reScan - ''; in { @@ -109,46 +16,12 @@ in enable = true; frequency = "*:0/30"; verbose = false; - postExec = "${sortMail}/bin/sort-mail-archive"; }; systemd.user.timers.mbsync.Timer.RandomizedDelaySec = "10m"; accounts.email.accounts = pkgs.privateValue { } "mail/accounts"; systemd.user.services = let - mkService = name: account: - let - configjs = pkgs.writeText "config.js" '' - var child_process = require('child_process'); - - function getStdout(cmd) { - var stdout = child_process.execSync(cmd); - return stdout.toString().trim(); - } - - exports.host = "${account.imap.host}" - exports.port = 993 - exports.tls = true; - exports.tlsOptions = { "rejectUnauthorized": false }; - exports.username = "${account.userName}"; - exports.password = getStdout("${toString account.passwordCommand}"); - exports.onNotify = "${quick-sync}" - exports.boxes = [ "Inbox", "Code" ]; - ''; - in - { - Unit.Description = "Run imapnotify for imap account ${name}"; - Service = { - ExecStart = "${pkgs.imapnotify}/bin/imapnotify -c ${configjs}"; - Restart = "always"; - RestartSec = "1min"; - }; - Install.WantedBy = [ "default.target" ]; - }; - mkServiceWithName = name: account: { - name = "imapnotify-${name}-inbox"; - value = mkService name account; - }; hasImapHost = name: account: account.imap != null; mkWatchService = name: account: { name = "watch-${name}-maildir"; @@ -166,9 +39,8 @@ in }; }; in - lib.mapAttrs' mkWatchService - (lib.filterAttrs hasImapHost config.accounts.email.accounts) // lib.mapAttrs' mkServiceWithName - (lib.filterAttrs hasImapHost config.accounts.email.accounts) // { + lib.mapAttrs' mkWatchService (lib.filterAttrs hasImapHost config.accounts.email.accounts) // + { mbsync.Service = { Environment = "PATH=${pkgs.coreutils}/bin"; Restart = "on-failure"; @@ -176,29 +48,30 @@ in }; }; - programs.msmtp.enable = true; - programs.mbsync.enable = true; - programs.notmuch = { - enable = config.accounts.email.accounts != { }; - hooks.postInsert = '' - ${pkgs.notmuch}/bin/notmuch tag +deleted -- "folder:/Trash/ (not tag:deleted)" - ${pkgs.notmuch}/bin/notmuch tag -deleted -- "(not folder:/Trash/) tag:deleted" - ${pkgs.notmuch}/bin/notmuch tag +spam -- "folder:/Junk|Spam|SPAM/ (not tag:spam)" - ${pkgs.notmuch}/bin/notmuch tag -spam -- "(not folder:/Junk|Spam|SPAM/) tag:spam" - ''; - new = { - tags = [ ]; - ignore = [ ".isyncuidmap.db" ]; + programs = { + msmtp.enable = true; + mbsync.enable = true; + notmuch = { + enable = config.accounts.email.accounts != { }; + hooks.postInsert = '' + ${pkgs.notmuch}/bin/notmuch tag +deleted -- "folder:/Trash/ (not tag:deleted)" + ${pkgs.notmuch}/bin/notmuch tag -deleted -- "(not folder:/Trash/) tag:deleted" + ${pkgs.notmuch}/bin/notmuch tag +spam -- "folder:/Junk|Spam|SPAM/ (not tag:spam)" + ${pkgs.notmuch}/bin/notmuch tag -spam -- "(not folder:/Junk|Spam|SPAM/) tag:spam" + ''; + new = { + tags = [ ]; + ignore = [ ".isyncuidmap.db" ]; + }; + maildir.synchronizeFlags = true; }; - maildir.synchronizeFlags = true; }; home = { - packages = [ sortMail ]; + packages = [ quick-sync ]; file = let - mutt_alternates = "@maralorn.de " - + (builtins.concatStringsSep " " alternates); + mutt_alternates = "@maralorn.de " + (builtins.concatStringsSep " " alternates); show-sidebar = pkgs.writeText "show-sidebar" '' set sidebar_visible=yes bind index sidebar-prev diff --git a/private b/private index 0ced3f6d..e60d0d6c 160000 --- a/private +++ b/private @@ -1 +1 @@ -Subproject commit 0ced3f6ddbd29f4a4a4d43b8751a079209285658 +Subproject commit e60d0d6c41d564d3d955ea6e0e58f24abd626a6a