mcp-server-notmuch 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_server_notmuch-0.1.0/.github/workflows/ci.yml +39 -0
- mcp_server_notmuch-0.1.0/.gitignore +9 -0
- mcp_server_notmuch-0.1.0/LICENSE +21 -0
- mcp_server_notmuch-0.1.0/Makefile +50 -0
- mcp_server_notmuch-0.1.0/PKG-INFO +328 -0
- mcp_server_notmuch-0.1.0/README.md +302 -0
- mcp_server_notmuch-0.1.0/config.example.toml +90 -0
- mcp_server_notmuch-0.1.0/logo.jpg +0 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/__init__.py +1 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/__main__.py +5 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/attachments.py +397 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/cli.py +85 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/compose.py +405 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/config.py +261 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/export.py +151 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/message.py +309 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/notmuch.py +186 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/pending.py +136 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/render.py +305 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/server.py +850 -0
- mcp_server_notmuch-0.1.0/mcpnotmuch/threads.py +207 -0
- mcp_server_notmuch-0.1.0/pyproject.toml +49 -0
- mcp_server_notmuch-0.1.0/tests/conftest.py +425 -0
- mcp_server_notmuch-0.1.0/tests/test_attachments.py +140 -0
- mcp_server_notmuch-0.1.0/tests/test_compose.py +475 -0
- mcp_server_notmuch-0.1.0/tests/test_config.py +106 -0
- mcp_server_notmuch-0.1.0/tests/test_export.py +119 -0
- mcp_server_notmuch-0.1.0/tests/test_find_attachments.py +104 -0
- mcp_server_notmuch-0.1.0/tests/test_message.py +207 -0
- mcp_server_notmuch-0.1.0/tests/test_notmuch_integration.py +126 -0
- mcp_server_notmuch-0.1.0/tests/test_pending.py +91 -0
- mcp_server_notmuch-0.1.0/tests/test_protocol.py +547 -0
- mcp_server_notmuch-0.1.0/tests/test_threads.py +114 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install notmuch
|
|
18
|
+
run: |
|
|
19
|
+
sudo apt-get update
|
|
20
|
+
sudo apt-get install -y notmuch poppler-utils ghostscript
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install uv
|
|
28
|
+
run: pip install uv
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: uv pip install --prerelease=allow --system -e '.[dev]'
|
|
32
|
+
|
|
33
|
+
- name: Lint
|
|
34
|
+
run: |
|
|
35
|
+
ruff format --check .
|
|
36
|
+
ruff check .
|
|
37
|
+
|
|
38
|
+
- name: Test
|
|
39
|
+
run: pytest
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hagen Paul Pfeifer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.DELETE_ON_ERROR:
|
|
2
|
+
MAKEFLAGS += --no-builtin-rules
|
|
3
|
+
|
|
4
|
+
VENV := .venv
|
|
5
|
+
PYTHON := $(VENV)/bin/python3
|
|
6
|
+
UV := uv
|
|
7
|
+
|
|
8
|
+
.PHONY: all
|
|
9
|
+
all:
|
|
10
|
+
$(MAKE) fmt
|
|
11
|
+
$(MAKE) lint
|
|
12
|
+
$(MAKE) test
|
|
13
|
+
|
|
14
|
+
.PHONY: venv
|
|
15
|
+
venv:
|
|
16
|
+
test -d $(VENV) || $(UV) venv $(VENV)
|
|
17
|
+
$(UV) pip install --prerelease=allow -e '.[dev]' --python $(PYTHON)
|
|
18
|
+
|
|
19
|
+
.PHONY: fmt
|
|
20
|
+
fmt: venv
|
|
21
|
+
$(VENV)/bin/ruff format .
|
|
22
|
+
|
|
23
|
+
.PHONY: lint
|
|
24
|
+
lint: venv
|
|
25
|
+
$(VENV)/bin/ruff format --check .
|
|
26
|
+
$(VENV)/bin/ruff check .
|
|
27
|
+
|
|
28
|
+
.PHONY: test
|
|
29
|
+
test: venv
|
|
30
|
+
$(VENV)/bin/pytest
|
|
31
|
+
|
|
32
|
+
.PHONY: clean
|
|
33
|
+
clean:
|
|
34
|
+
find . -name '__pycache__' -type d -prune -exec rm -rf {} +
|
|
35
|
+
rm -rf .pytest_cache .ruff_cache *.egg-info
|
|
36
|
+
|
|
37
|
+
.PHONY: distclean
|
|
38
|
+
distclean: clean
|
|
39
|
+
rm -rf $(VENV)
|
|
40
|
+
|
|
41
|
+
.PHONY: help
|
|
42
|
+
help:
|
|
43
|
+
@echo "Targets:"
|
|
44
|
+
@echo " all fmt + lint + test (default)"
|
|
45
|
+
@echo " venv create .venv and install the package with dev deps"
|
|
46
|
+
@echo " fmt format code with ruff"
|
|
47
|
+
@echo " lint check formatting and lint with ruff"
|
|
48
|
+
@echo " test run pytest"
|
|
49
|
+
@echo " clean remove caches and build artifacts"
|
|
50
|
+
@echo " distclean clean + remove .venv"
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-server-notmuch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing a local notmuch email database, read-first
|
|
5
|
+
Project-URL: Homepage, https://github.com/hgn/mcp-server-notmuch
|
|
6
|
+
Author: Hagen Paul Pfeifer
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: email,mcp,notmuch
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Communications :: Email
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: html2text>=2024.2.26
|
|
19
|
+
Requires-Dist: mcp==2.0.0b2
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
|
+
Provides-Extra: image
|
|
24
|
+
Requires-Dist: pillow>=10; extra == 'image'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# mcp-server-notmuch
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="logo.jpg" alt="mcp-server-notmuch logo" width="200"/>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
An MCP server that exposes a local [notmuch](https://notmuchmail.org/) email
|
|
34
|
+
database to an LLM client such as Claude. It is read-first: searching,
|
|
35
|
+
reading, and understanding mail is always available; writing anything
|
|
36
|
+
(drafts, tags, exported files) requires an explicit opt-in flag and is
|
|
37
|
+
confined to clearly bounded locations.
|
|
38
|
+
|
|
39
|
+
**It never sends mail.** There is no send capability anywhere in this
|
|
40
|
+
codebase, in any mode, with any flag. Drafts are written to a local maildir
|
|
41
|
+
for you to review and send yourself in a real mail client.
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
44
|
+
|
|
45
|
+
- Searches and reads your mail (threads, single messages, attachments,
|
|
46
|
+
calendar invites, office documents, images) via the real `notmuch` CLI.
|
|
47
|
+
- Understands scopes: a named, pre-configured notmuch query (e.g. "personal
|
|
48
|
+
mail" vs. "mailing lists") that every search is confined to unless you ask
|
|
49
|
+
otherwise.
|
|
50
|
+
- Answers "what's still unanswered" and "who owes me a reply" (`mail_pending`),
|
|
51
|
+
"has this come up before" (`mail_related_threads`), and gives a
|
|
52
|
+
token-cheap overview of a long thread before you read all of it
|
|
53
|
+
(`mail_thread_overview`).
|
|
54
|
+
- Optionally composes and revises plain-text drafts (`--allow-drafts`),
|
|
55
|
+
tags messages (`--allow-tags`), or exports attachments and a
|
|
56
|
+
[Gource](https://gource.io/) visualization of your mailbox history to a
|
|
57
|
+
directory you name (`--allow-export DIR`).
|
|
58
|
+
|
|
59
|
+
## What it does not do
|
|
60
|
+
|
|
61
|
+
- It does not send mail. Ever.
|
|
62
|
+
- It does not modify your mail in any way unless you pass `--allow-tags`
|
|
63
|
+
(tagging) or `--allow-drafts` (writing a new file into a drafts maildir).
|
|
64
|
+
Neither flag lets it touch existing messages' content.
|
|
65
|
+
- It does not read or write outside the notmuch database, the configured
|
|
66
|
+
drafts maildir, and (only with `--allow-export`) the configured export
|
|
67
|
+
directory.
|
|
68
|
+
- It does not require or use the notmuch2 Python bindings, so no compiler
|
|
69
|
+
is needed to install it.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
### From PyPI (once published)
|
|
74
|
+
|
|
75
|
+
```console
|
|
76
|
+
$ uvx --prerelease=allow mcp-server-notmuch --help
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### From source
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ git clone https://github.com/hgn/mcp-server-notmuch
|
|
83
|
+
$ cd mcp-server-notmuch
|
|
84
|
+
$ uv pip install --prerelease=allow -e .
|
|
85
|
+
$ mcp-server-notmuch --help
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `--prerelease=allow` is required because this project pins a
|
|
89
|
+
pre-release of the `mcp` SDK (see [SDK version](#sdk-version) below); it is
|
|
90
|
+
not optional.
|
|
91
|
+
|
|
92
|
+
### System requirements
|
|
93
|
+
|
|
94
|
+
- `notmuch` (the CLI, not just the library) on `PATH` or pointed to via
|
|
95
|
+
`notmuch.binary` in the config.
|
|
96
|
+
- `poppler-utils` (`pdftotext`) to read PDF attachments. Without it,
|
|
97
|
+
`mail_read_attachment` on a PDF names the missing package.
|
|
98
|
+
- `pandoc` or `libreoffice` to read office documents (doc/docx/odt/rtf).
|
|
99
|
+
Without either, the error names both options.
|
|
100
|
+
- Optionally, `Pillow` (`pip install 'mcp-server-notmuch[image]'`) to let
|
|
101
|
+
oversized image attachments be downscaled instead of refused.
|
|
102
|
+
- Optionally, [Gource](https://gource.io/) to actually play back the log
|
|
103
|
+
`mail_export_gource` writes.
|
|
104
|
+
|
|
105
|
+
## MCP client configuration
|
|
106
|
+
|
|
107
|
+
### Claude Code
|
|
108
|
+
|
|
109
|
+
Read-only (the default: search and read tools only):
|
|
110
|
+
|
|
111
|
+
```console
|
|
112
|
+
$ claude mcp add notmuch -- uvx --prerelease=allow mcp-server-notmuch
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With drafts enabled (also allows revising/tagging as needed):
|
|
116
|
+
|
|
117
|
+
```console
|
|
118
|
+
$ claude mcp add notmuch -- uvx --prerelease=allow mcp-server-notmuch --allow-drafts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or by hand in `.mcp.json`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"notmuch": {
|
|
127
|
+
"command": "uvx",
|
|
128
|
+
"args": ["--prerelease=allow", "mcp-server-notmuch", "--allow-drafts"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Claude Desktop
|
|
135
|
+
|
|
136
|
+
Add to `claude_desktop_config.json` (read-only default):
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"notmuch": {
|
|
142
|
+
"command": "uvx",
|
|
143
|
+
"args": ["--prerelease=allow", "mcp-server-notmuch"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
With drafts enabled:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"notmuch": {
|
|
155
|
+
"command": "uvx",
|
|
156
|
+
"args": ["--prerelease=allow", "mcp-server-notmuch", "--allow-drafts"]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Configuration
|
|
163
|
+
|
|
164
|
+
The server reads `$XDG_CONFIG_HOME/mcp-server-notmuch/config.toml`
|
|
165
|
+
(`~/.config/mcp-server-notmuch/config.toml` if `XDG_CONFIG_HOME` is unset),
|
|
166
|
+
or a path given with `--config`. A missing file is not an error: the server
|
|
167
|
+
falls back to the system `notmuch` binary and a single built-in scope
|
|
168
|
+
`all` with an empty query. Once a file exists, `[scopes]` is authoritative
|
|
169
|
+
and `all` is no longer implied.
|
|
170
|
+
|
|
171
|
+
See [`config.example.toml`](config.example.toml) for a fully commented
|
|
172
|
+
reference file. Summary of every key:
|
|
173
|
+
|
|
174
|
+
| Section | Key | Default | Meaning |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| `[notmuch]` | `binary` | `"notmuch"` | Path or bare name of the notmuch binary. |
|
|
177
|
+
| | `config` | notmuch's own default | Path passed as `NOTMUCH_CONFIG`. |
|
|
178
|
+
| `[limits]` | `default_limit` | `20` | Rows returned when a tool call omits `limit`. |
|
|
179
|
+
| | `max_limit` | `100` | Hard ceiling on `limit`, whatever is requested. |
|
|
180
|
+
| | `max_body_chars` | `40000` | Message body truncation point. |
|
|
181
|
+
| | `max_attachment_chars` | `100000` | Attachment/office/calendar text truncation point. |
|
|
182
|
+
| | `max_image_bytes` | `5242880` | Image size ceiling; downscaled with Pillow if larger, else refused. |
|
|
183
|
+
| `[scopes]` | `default` | *(required once `[scopes]` exists)* | Scope used when a tool call omits `scope`. |
|
|
184
|
+
| `[scopes.<name>]` | `query` | — | A notmuch query ANDed with every search using this scope. |
|
|
185
|
+
| | `description` | `""` | Shown by `mail_list_scopes`. |
|
|
186
|
+
| `[drafts]` | `maildir` | unset | Root of a maildir (`cur/`, `new/`, `tmp/`) for `mail_create_draft`. |
|
|
187
|
+
| | `from` | unset | `From:` header on every draft. |
|
|
188
|
+
| | `signature` | unset | Plain-text signature file, appended on request. |
|
|
189
|
+
| | `wrap_columns` | `72` | Hard-wrap width for drafted plain text. |
|
|
190
|
+
| | `max_total_attachment_bytes` | `26214400` (25 MiB) | Ceiling on draft attachments' combined size. |
|
|
191
|
+
| `[identity]` | `addresses` | notmuch's `user.primary_email`/`user.other_email` | Your own address(es); used to exclude yourself from reply-all and to detect `mail_pending direction="waiting"`. |
|
|
192
|
+
|
|
193
|
+
`mail_create_draft`/`mail_update_draft` refuse to run unless both
|
|
194
|
+
`drafts.maildir` and `drafts.from` are set. `mail_pending
|
|
195
|
+
direction="waiting"` needs `[identity] addresses` (or a readable notmuch
|
|
196
|
+
`user.primary_email`) to know which address is "you".
|
|
197
|
+
|
|
198
|
+
Scope resolution: a tool's `scope` argument names a configured scope; its
|
|
199
|
+
query is ANDed with the caller's query, both sides parenthesized
|
|
200
|
+
(`(scope_query) and (user_query)`), so an `or` on either side cannot leak
|
|
201
|
+
past the other. An unknown scope name is an error listing the configured
|
|
202
|
+
scopes; `scope="all"` is never silently unfiltered unless you define a
|
|
203
|
+
scope literally named `all`.
|
|
204
|
+
|
|
205
|
+
## Tiers and tools
|
|
206
|
+
|
|
207
|
+
Four tiers. The read tier is always registered. The other three are
|
|
208
|
+
registered **only** when their flag is passed — there is no
|
|
209
|
+
"registered but refused" state, an unauthorized tool is simply absent from
|
|
210
|
+
the tool list a client sees.
|
|
211
|
+
|
|
212
|
+
| Flag | Registers |
|
|
213
|
+
|---|---|
|
|
214
|
+
| *(none)* | Read tier: search, read, list, prepare — nothing is written. |
|
|
215
|
+
| `--allow-drafts` | Draft tier: compose and revise local plain-text drafts. |
|
|
216
|
+
| `--allow-tags` | Tag tier: add/remove tags on existing messages. |
|
|
217
|
+
| `--allow-export DIR` | Export tier: write attachments/a Gource log into `DIR`. |
|
|
218
|
+
|
|
219
|
+
### Read tier (always on)
|
|
220
|
+
|
|
221
|
+
| Tool | Purpose |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `mail_search` | Search threads or messages, paged, with a truncation notice. |
|
|
224
|
+
| `mail_read_thread` | Every message in a thread, oldest first. |
|
|
225
|
+
| `mail_thread_overview` | One line per message (date/size/from), tree or flat layout, before reading a long thread in full. |
|
|
226
|
+
| `mail_related_threads` | Heuristic "has this come up before" (subject + participant overlap). |
|
|
227
|
+
| `mail_pending` | Threads you owe a reply on, or threads you're waiting on a reply to. |
|
|
228
|
+
| `mail_read_message` | A single message's headers and body. |
|
|
229
|
+
| `mail_count` | Cheap message/thread count for a query. |
|
|
230
|
+
| `mail_list_addresses` | Resolve a name to the real address(es) behind it. |
|
|
231
|
+
| `mail_list_attachments` | List one message's attachments. |
|
|
232
|
+
| `mail_read_attachment` | Read one attachment: text, PDF, image, office document, or calendar invite. |
|
|
233
|
+
| `mail_find_attachments` | Find attachments across a whole search (e.g. "all PDFs from 2025"). |
|
|
234
|
+
| `mail_list_scopes` | List the configured scopes. |
|
|
235
|
+
| `mail_prepare_reply` | Derive reply/reply-all/forward headers and quoted/forwarded body; writes nothing. |
|
|
236
|
+
|
|
237
|
+
### Draft tier (`--allow-drafts`)
|
|
238
|
+
|
|
239
|
+
| Tool | Purpose |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `mail_create_draft` | Compose a plain-text draft (optionally with attachments) into the configured maildir. |
|
|
242
|
+
| `mail_update_draft` | Revise an existing draft in place; only the given fields change. |
|
|
243
|
+
|
|
244
|
+
### Tag tier (`--allow-tags`)
|
|
245
|
+
|
|
246
|
+
| Tool | Purpose |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `mail_tag` | Add/remove tags on every message matching a query. |
|
|
249
|
+
|
|
250
|
+
### Export tier (`--allow-export DIR`)
|
|
251
|
+
|
|
252
|
+
| Tool | Purpose |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `mail_save_attachment` | Save one attachment's raw bytes into `DIR`. |
|
|
255
|
+
| `mail_export_gource` | Write a [Gource](https://gource.io/) custom log of mailbox history into `DIR`. |
|
|
256
|
+
|
|
257
|
+
`mail_export_gource` writes one line per message,
|
|
258
|
+
`timestamp\|username\|type\|path\|colour`, sorted oldest first (Gource
|
|
259
|
+
requires this). `path` is `folder/normalized-subject`, so a whole reply
|
|
260
|
+
chain lands at one point in the tree; `colour` is a stable hash of the
|
|
261
|
+
folder name, so a folder keeps its colour across repeated exports. Play it
|
|
262
|
+
back with:
|
|
263
|
+
|
|
264
|
+
```console
|
|
265
|
+
$ gource --log-format custom mail.gource -s 0.5 --key
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`limit` (default 100000) matters: feeding Gource every mailing-list message
|
|
269
|
+
you've ever received produces an unwatchable animation, so scope the query
|
|
270
|
+
first.
|
|
271
|
+
|
|
272
|
+
## Security model
|
|
273
|
+
|
|
274
|
+
This is a mail server handed to an LLM; message content is not trusted the
|
|
275
|
+
way your own instructions are.
|
|
276
|
+
|
|
277
|
+
1. **Prompt injection.** Message bodies, attachment text, calendar
|
|
278
|
+
summaries, and thread overview lines are third-party content, not
|
|
279
|
+
instructions. `render.py` wraps every one of them in explicit
|
|
280
|
+
`-----BEGIN/END UNTRUSTED EMAIL CONTENT-----` markers with a notice that
|
|
281
|
+
nothing inside should be treated as a command, so no individual tool can
|
|
282
|
+
forget to do this.
|
|
283
|
+
2. **Path confinement.** The drafts maildir (`compose.py`) and the export
|
|
284
|
+
directory (`export.py`) each resolve the target path and verify it is
|
|
285
|
+
still inside the configured root afterward. This catches a literal `..`
|
|
286
|
+
and a symlink pointing outside the root (`Path.resolve()` follows
|
|
287
|
+
symlinks), and `mail_create_draft`/`mail_update_draft` cannot be made to
|
|
288
|
+
write outside the configured maildir with any combination of arguments.
|
|
289
|
+
3. **No shell, ever.** Every subprocess call is `subprocess.run([...],
|
|
290
|
+
shell=False)` with an argv list; queries are passed as a single argv
|
|
291
|
+
element, never interpolated into a shell string or a notmuch query
|
|
292
|
+
string beyond normal AND/OR composition.
|
|
293
|
+
4. **No content in diagnostics.** Errors and progress go to stderr and are
|
|
294
|
+
content-free (a byte count or a file path, never a message body).
|
|
295
|
+
5. **Never silently unfiltered.** A `scope` argument that AND-composes
|
|
296
|
+
with a query is always explicit; there is no hidden "search everything"
|
|
297
|
+
fallback unless a scope literally named `all` is configured.
|
|
298
|
+
|
|
299
|
+
## notmuch query syntax
|
|
300
|
+
|
|
301
|
+
`query`/`scope` arguments accept full notmuch search syntax: `from:`,
|
|
302
|
+
`to:`, `subject:`, `tag:`, `date:` ranges, boolean `and`/`or`/`not`, and
|
|
303
|
+
more. See **notmuch-search-terms(7)** (`man notmuch-search-terms`) for the
|
|
304
|
+
complete reference.
|
|
305
|
+
|
|
306
|
+
## SDK version
|
|
307
|
+
|
|
308
|
+
Targets MCP spec **2026-07-28** and pins `mcp==2.0.0b2`, a pre-release of
|
|
309
|
+
the Python SDK built for that spec. Once the spec and a matching stable SDK
|
|
310
|
+
release ship, this pin moves to the stable release; until then, every
|
|
311
|
+
install (`uv pip install`, `uvx`) needs `--prerelease=allow`.
|
|
312
|
+
|
|
313
|
+
## Development
|
|
314
|
+
|
|
315
|
+
```console
|
|
316
|
+
$ make # fmt + lint + test
|
|
317
|
+
$ make test # pytest (skips cleanly if notmuch is not installed)
|
|
318
|
+
$ make lint # ruff format --check + ruff check
|
|
319
|
+
$ make help # list all targets
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Tests build a small crafted maildir and run real `notmuch` commands against
|
|
323
|
+
it; nothing touches your real mail. CI runs on Python 3.11, 3.12 and 3.13
|
|
324
|
+
with notmuch installed via `apt`.
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT, see [LICENSE](LICENSE).
|