diff options
| -rwxr-xr-x | dot.local-bin-nm--tagger | 140 | ||||
| -rwxr-xr-x | dot.local-bin-sm | 9 |
2 files changed, 146 insertions, 3 deletions
diff --git a/dot.local-bin-nm--tagger b/dot.local-bin-nm--tagger new file mode 100755 index 0000000..f2de0d2 --- /dev/null +++ b/dot.local-bin-nm--tagger @@ -0,0 +1,140 @@ +#!/usr/bin/env python3.7 +"""Tagging script based on mail folder names for Notmuch + +This scripts iterates through all messages with ``tag:new``, and replaces that +by tags based on the name of the maildir folder(s) the message is in. By +default, it takes the first and last component to form the name. This suits my +personal pre-Notmuch mail setup, with hierarchical IMAP folders, where the tuple +(account, last_folder) is unique for almost all folders. + +My maildirs have the account name as first component and the IMAP part as the +rest. So, for example, ``.maildir/Chaotikum/INBOX/Notmuch`` gets the tags +``Chaotikum`` and ``Notmuch``. + +Additionally, folder names can be mapped to other tag names. This can come in +handy when you need to use Exchange and its default names, yet want to keep all +sent messages—whether they are in ``.maildir/Acc/Sent`` or in +``.maildir/Exchange/Sent Messages``—under ``tag:sent``. + +See :py:func:`read_config` for configuration details. + +""" + +import json +import logging as log +from pathlib import Path +import notmuch + +DEFAULT_CONFIG = { + "folder_separator": ".", + "keep": [], + "transforms": { + "Drafts": "draft", + "INBOX": "inbox", + "Junk": "spam", + "Sent": "sent", + }, +} +"""Default config, overridden with the user-provided one""" + + +def read_config(): + """Returns the user’s config, with ``DEFAULT_CONFIG`` as base. + + The config is read from ``$XDG_CONFIG_HOME/nm-tagger/config.json`` and shall + contain three keys: + + ``folder_separator`` + The character that separates logical folder names, or + the special value ``#FILESYSTEM#``. + + For example, ``.`` means that the Maildir directories are named like + ``.maildir/This.INBOX.is.deeply.nested``. + + The value ``#FILESYSTEM#`` means to use the file system directories (on + most systems, this has the same effect as ``/``). + + ``keep`` + A list of folder names that are assigned as tags, even if they are + neither at the begin nor end. + + ``transforms`` + A dictionary mapping folder names to tags. + + """ + try: + from xdg import BaseDirectory + + config_path_base = Path(BaseDirectory.load_first_config("nm-tagger")) + except ImportError: + from os import environ + + config_path_base = environ["XDG_CONFIG_HOME"] + if config_path_base != "": + config_path_base = Path(config_path_base) + else: + config_path_base = Path.home() / ".config" / "nm-tagger" + except KeyError: + config_path_base = Path.home() / ".config" / "nm-tagger" + + config_path = config_path_base / "config.json" + + try: + with open(config_path) as fp: + config = json.load(fp) + log.info("Loaded config from %s", config_path) + log.debug("Config: %s", config) + except FileNotFoundError: + log.warn("Config file %s not found!", config_path) + config = {} + return {**DEFAULT_CONFIG, **config} + + +def main(): + """Entry point""" + log.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=log.DEBUG) + + config = read_config() + + with notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) as db: + top = Path(db.get_path()) + log.info("Opened notmuch db at %s", top) + new_messages_query = notmuch.Query(db, "tag:new") + log.info("Found %d messages to process", new_messages_query.count_messages()) + new_messages = set(new_messages_query.search_messages()) + + db.begin_atomic() + + for m in new_messages: + mid = m.get_message_id() + _paths = {Path(p).relative_to(top) for p in m.get_filenames()} + if config["folder_separator"] == "#FILESYSTEM#": + paths = {p.parts[:-2] for p in _paths} + else: + paths = { + p.parent.parent.as_posix().split(config["folder_separator"]) + for p in _paths + } + log.debug("paths of %s: (%d) %s", mid, len(paths), str(paths)) + tags = set() + for _p in paths: + p = tuple(map(lambda c: config["transforms"].get(c, c), _p)) + tags.add(p[0]) + tags.add(p[-1]) + for c in p[1:-1]: + if c in config["keep"]: + tags.add(c) + log.debug("tags for %s: %s", mid, str(tags)) + try: + m.freeze() + for t in tags: + m.add_tag(t) + m.remove_tag("new") + finally: + m.thaw() + + db.end_atomic() + + +if __name__ == "__main__": + main() diff --git a/dot.local-bin-sm b/dot.local-bin-sm index cdea8e4..c519299 100755 --- a/dot.local-bin-sm +++ b/dot.local-bin-sm @@ -3,10 +3,13 @@ set -e set -x mbsync -a || true notmuch new +[ -d ~/.maildir/GMail ] && (cd ~/.maildir/GMail && gmi sync) new=$(notmuch search --output=messages tag:new | xargs) [ -z "$new" ] && exit -afew -tnvv +notmuch config set query.new "$new" +notmuch tag -new -- tag:new and folder:GMail/mail +nm-tagger export MAILSEQ="${MBLAZE:-$HOME/.mblaze}/seq.sm" export MBLAZE_PAGER="" -msearch -- --exclude=false $new | mthread | mseq -S | mscan -f '%u%r %3n %10d %17f %8F %t %i%S' -type mu 2> /dev/null && mu index || true +msearch -- --exclude=false query:new | mthread | mseq -S | mscan -f '%u%r %3n %10d %17f %8F %t %i%S' +type mu > /dev/null && mu index || true |
