aboutsummaryrefslogtreecommitdiff
path: root/dot.local-bin-nm--tagger
blob: 5c07f6de254732a98de80c9ea95ea21538fb8434 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
"""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 os import environ
from pathlib import Path
from tqdm import tqdm
import notmuch2

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:
        config_home = environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
        config_path_base = Path(config_home) / "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=environ.get("NM_TAGGER_LOG_LEVEL", log.INFO),
    )

    config = read_config()

    with notmuch2.Database(mode=notmuch2.Database.MODE.READ_WRITE) as db:
        top = db.path
        mail_root = Path(db.config.get("database.mail_root", top))
        log.info("Opened notmuch db at %s for mail root %s", top, mail_root)
        new_messages = db.messages("tag:new", omit_excluded=db.EXCLUDE.FALSE)
        count = db.count_messages("tag:new", omit_excluded=db.EXCLUDE.FALSE)
        log.info("Found %d messages to process", count)

        with db.atomic():
            for m in tqdm(new_messages, total=count):
                mid = m.messageid
                _paths = {p.relative_to(mail_root) for p in m.filenames()}
                if config["folder_separator"] == "#FILESYSTEM#":
                    paths = {p.parts[:-2] for p in _paths}
                else:
                    paths = {
                        tuple(
                            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))
                with m.frozen():
                    for t in tags:
                        m.tags.add(t)
                    m.tags.discard("new")


if __name__ == "__main__":
    main()