imap-agent-cli 0.1.4__tar.gz → 0.1.6__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.
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/.github/workflows/publish.yml +3 -2
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/AGENTS.md +12 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/PKG-INFO +22 -2
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/README.md +21 -1
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/pyproject.toml +1 -1
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/__init__.py +1 -1
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/cli.py +143 -2
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/config.py +3 -0
- imap_agent_cli-0.1.6/src/imap_agent_cli/imap_client.py +731 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/mime.py +10 -3
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/models.py +1 -0
- imap_agent_cli-0.1.6/src/imap_agent_cli/search.py +75 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/skill.py +54 -19
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/test_cli.py +56 -0
- imap_agent_cli-0.1.6/tests/test_imap_client.py +150 -0
- imap_agent_cli-0.1.6/tests/test_live_no_seen.py +37 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/test_pymap_integration.py +1 -0
- imap_agent_cli-0.1.6/tests/test_search.py +115 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/test_skill.py +2 -0
- imap_agent_cli-0.1.4/src/imap_agent_cli/imap_client.py +0 -303
- imap_agent_cli-0.1.4/src/imap_agent_cli/search.py +0 -30
- imap_agent_cli-0.1.4/tests/test_search.py +0 -34
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/.gitignore +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/LICENSE +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/imap_agent_cli.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/spec.md +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/errors.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/src/imap_agent_cli/render.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/__init__.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/_bootstrap.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/pymap_server_runner.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/test_config.py +0 -0
- {imap_agent_cli-0.1.4 → imap_agent_cli-0.1.6}/tests/test_mime.py +0 -0
|
@@ -29,13 +29,16 @@ The core safety boundary is strict: never add support for sending email, deletin
|
|
|
29
29
|
- Keep README and skill language platform-neutral and agent-tool-neutral. Avoid shell-specific syntax unless explicitly documenting a shell-specific example.
|
|
30
30
|
- If changing installed-skill behavior or wording, update `src/imap_agent_cli/skill.py` and `tests/test_skill.py` together.
|
|
31
31
|
- If changing MIME parsing, body rendering, draft creation, or search behavior, add focused tests for the contract being changed.
|
|
32
|
+
- If changing search output, preserve metadata-first behavior; search should not fetch full message bodies.
|
|
32
33
|
|
|
33
34
|
## Safety-Sensitive Implementation Notes
|
|
34
35
|
|
|
35
36
|
- Reads must avoid changing message state. Preserve no-seen fetch behavior.
|
|
37
|
+
- `ssl_mode=required` and `ssl_mode=preferred` must never send credentials over plaintext. Plaintext login is allowed only with `ssl_mode=disabled`.
|
|
36
38
|
- Draft creation is allowed only by appending a new MIME message to the detected or configured Drafts folder.
|
|
37
39
|
- Reply drafts should preserve appropriate reply headers such as `In-Reply-To` and `References`.
|
|
38
40
|
- Attachment downloads require explicit user/agent intent and an output directory.
|
|
41
|
+
- Attachment downloads should preflight target filenames before writing, so a failed multi-attachment download does not partially write files.
|
|
39
42
|
- Folder-wide or all-folder operations should remain bounded by defaults and overridable limits.
|
|
40
43
|
- HTML email is untrusted input. Sanitize before returning HTML and keep Markdown conversion intentionally lossy but predictable.
|
|
41
44
|
|
|
@@ -63,6 +66,14 @@ uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
|
63
66
|
|
|
64
67
|
Set `IMAP_AGENT_CLI_TEST_PYMAP=1` in the shell before running the integration test. The test starts `pymap dict --demo-data` locally and logs in with demo credentials.
|
|
65
68
|
|
|
69
|
+
Run the optional live no-seen test only when intentionally validating a configured mailbox:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
python -m unittest tests.test_live_no_seen -v
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Set `IMAP_AGENT_CLI_LIVE_TEST=1` first. The test reads one unread message and verifies flags do not change; it skips when no unread message is available.
|
|
76
|
+
|
|
66
77
|
Build the package:
|
|
67
78
|
|
|
68
79
|
```text
|
|
@@ -73,6 +84,7 @@ uv build --no-sources
|
|
|
73
84
|
|
|
74
85
|
- Package metadata lives in `pyproject.toml`.
|
|
75
86
|
- The GitHub Actions publish workflow is `.github/workflows/publish.yml`.
|
|
87
|
+
- Publishing runs when a version tag such as `0.1.4` is pushed, or when the workflow is manually dispatched.
|
|
76
88
|
- PyPI Trusted Publishing uses:
|
|
77
89
|
- Project: `imap-agent-cli`
|
|
78
90
|
- Owner: `pseudosavant`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imap-agent-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Agent-first IMAP CLI for safe email search, read, attachment download, and draft creation
|
|
5
5
|
Project-URL: Homepage, https://github.com/pseudosavant/imap-agent-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/pseudosavant/imap-agent-cli
|
|
@@ -100,6 +100,7 @@ IMAP_AGENT_CLI_PORT=993
|
|
|
100
100
|
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
101
101
|
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
102
102
|
IMAP_AGENT_CLI_TLS=true
|
|
103
|
+
IMAP_AGENT_CLI_SSL_MODE=required
|
|
103
104
|
IMAP_AGENT_CLI_DRAFTS_FOLDER=Drafts
|
|
104
105
|
```
|
|
105
106
|
|
|
@@ -121,6 +122,12 @@ The config file is stored at:
|
|
|
121
122
|
|
|
122
123
|
Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
|
|
123
124
|
|
|
125
|
+
Validate setup without reading message bodies:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
imap-agent-cli config check
|
|
129
|
+
```
|
|
130
|
+
|
|
124
131
|
### Agent Skill
|
|
125
132
|
|
|
126
133
|
The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
|
|
@@ -176,7 +183,10 @@ These examples are mostly useful for validating configuration or debugging what
|
|
|
176
183
|
```text
|
|
177
184
|
imap-agent-cli folders
|
|
178
185
|
imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
|
|
179
|
-
imap-agent-cli
|
|
186
|
+
imap-agent-cli search --folder INBOX --from "Justin" --to "me@example.com" --max-results 10
|
|
187
|
+
imap-agent-cli search --folder INBOX --has-attachments --max-results 10 --max-scan 100
|
|
188
|
+
imap-agent-cli read --folder INBOX --uid 12345 --body-format html --include-attachments none
|
|
189
|
+
imap-agent-cli thread --folder INBOX --uid 12345 --include-body latest --body-format plain
|
|
180
190
|
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
181
191
|
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
182
192
|
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
@@ -219,6 +229,14 @@ uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
|
219
229
|
|
|
220
230
|
The integration test starts `pymap dict --demo-data` locally and logs in with `demouser` / `demopass`.
|
|
221
231
|
|
|
232
|
+
Run the opt-in live IMAP no-seen test only when you intentionally want to validate the configured mailbox:
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
python -m unittest tests.test_live_no_seen -v
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Set `IMAP_AGENT_CLI_LIVE_TEST=1` in your shell first. The test reads one unread message with no-seen fetch behavior and verifies the flags do not change. If no unread message exists, it skips.
|
|
239
|
+
|
|
222
240
|
### Project Status
|
|
223
241
|
|
|
224
242
|
This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
|
|
@@ -227,6 +245,8 @@ This repo is under active development. The behavior target is defined in [`spec.
|
|
|
227
245
|
|
|
228
246
|
The GitHub Actions workflow is `.github/workflows/publish.yml`.
|
|
229
247
|
|
|
248
|
+
Publishing runs when a version tag such as `0.1.4` is pushed. The workflow can also be run manually.
|
|
249
|
+
|
|
230
250
|
PyPI Trusted Publishing values:
|
|
231
251
|
|
|
232
252
|
```text
|
|
@@ -67,6 +67,7 @@ IMAP_AGENT_CLI_PORT=993
|
|
|
67
67
|
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
68
68
|
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
69
69
|
IMAP_AGENT_CLI_TLS=true
|
|
70
|
+
IMAP_AGENT_CLI_SSL_MODE=required
|
|
70
71
|
IMAP_AGENT_CLI_DRAFTS_FOLDER=Drafts
|
|
71
72
|
```
|
|
72
73
|
|
|
@@ -88,6 +89,12 @@ The config file is stored at:
|
|
|
88
89
|
|
|
89
90
|
Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
|
|
90
91
|
|
|
92
|
+
Validate setup without reading message bodies:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
imap-agent-cli config check
|
|
96
|
+
```
|
|
97
|
+
|
|
91
98
|
### Agent Skill
|
|
92
99
|
|
|
93
100
|
The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
|
|
@@ -143,7 +150,10 @@ These examples are mostly useful for validating configuration or debugging what
|
|
|
143
150
|
```text
|
|
144
151
|
imap-agent-cli folders
|
|
145
152
|
imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
|
|
146
|
-
imap-agent-cli
|
|
153
|
+
imap-agent-cli search --folder INBOX --from "Justin" --to "me@example.com" --max-results 10
|
|
154
|
+
imap-agent-cli search --folder INBOX --has-attachments --max-results 10 --max-scan 100
|
|
155
|
+
imap-agent-cli read --folder INBOX --uid 12345 --body-format html --include-attachments none
|
|
156
|
+
imap-agent-cli thread --folder INBOX --uid 12345 --include-body latest --body-format plain
|
|
147
157
|
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
148
158
|
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
149
159
|
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
@@ -186,6 +196,14 @@ uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
|
186
196
|
|
|
187
197
|
The integration test starts `pymap dict --demo-data` locally and logs in with `demouser` / `demopass`.
|
|
188
198
|
|
|
199
|
+
Run the opt-in live IMAP no-seen test only when you intentionally want to validate the configured mailbox:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
python -m unittest tests.test_live_no_seen -v
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Set `IMAP_AGENT_CLI_LIVE_TEST=1` in your shell first. The test reads one unread message with no-seen fetch behavior and verifies the flags do not change. If no unread message exists, it skips.
|
|
206
|
+
|
|
189
207
|
### Project Status
|
|
190
208
|
|
|
191
209
|
This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
|
|
@@ -194,6 +212,8 @@ This repo is under active development. The behavior target is defined in [`spec.
|
|
|
194
212
|
|
|
195
213
|
The GitHub Actions workflow is `.github/workflows/publish.yml`.
|
|
196
214
|
|
|
215
|
+
Publishing runs when a version tag such as `0.1.4` is pushed. The workflow can also be run manually.
|
|
216
|
+
|
|
197
217
|
PyPI Trusted Publishing values:
|
|
198
218
|
|
|
199
219
|
```text
|
|
@@ -31,12 +31,15 @@ Setup:
|
|
|
31
31
|
IMAP_AGENT_CLI_USERNAME
|
|
32
32
|
IMAP_AGENT_CLI_PASSWORD
|
|
33
33
|
IMAP_AGENT_CLI_TLS
|
|
34
|
+
IMAP_AGENT_CLI_SSL_MODE
|
|
34
35
|
|
|
35
36
|
Common workflows:
|
|
36
37
|
imap-agent-cli folders
|
|
37
38
|
imap-agent-cli search --folder INBOX --from "Justin" --max-results 10
|
|
39
|
+
imap-agent-cli search --folder INBOX --to "me@example.com" --has-attachments --max-results 10
|
|
38
40
|
imap-agent-cli read --folder INBOX --uid 12345 --body-format metadata
|
|
39
|
-
imap-agent-cli read --folder INBOX --uid 12345 --body-format html --max-body-chars 12000
|
|
41
|
+
imap-agent-cli read --folder INBOX --uid 12345 --body-format html --include-attachments none --max-body-chars 12000
|
|
42
|
+
imap-agent-cli thread --folder INBOX --uid 12345 --include-body latest
|
|
40
43
|
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
41
44
|
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
42
45
|
imap-agent-cli draft create --to person@example.com --subject "Subject" --body-file ./draft.txt
|
|
@@ -54,6 +57,7 @@ Commands:
|
|
|
54
57
|
folders list folders
|
|
55
58
|
search search messages
|
|
56
59
|
read read a message by folder and UID
|
|
60
|
+
thread inspect bounded thread context
|
|
57
61
|
attachments list or download attachments
|
|
58
62
|
draft create new or reply drafts
|
|
59
63
|
|
|
@@ -131,6 +135,21 @@ def _max_body_chars(args: argparse.Namespace, payload: dict[str, Any] | None = N
|
|
|
131
135
|
return load_config().defaults.max_body_chars
|
|
132
136
|
|
|
133
137
|
|
|
138
|
+
def _include_attachments(value: Any) -> str:
|
|
139
|
+
text = str(value or "metadata").strip().lower()
|
|
140
|
+
if text in {"false", "none", "no", "0"}:
|
|
141
|
+
return "none"
|
|
142
|
+
if text in {"true", "metadata", "yes", "1"}:
|
|
143
|
+
return "metadata"
|
|
144
|
+
raise AppError("invalid_request", "--include-attachments must be none or metadata.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _check_item(name: str, ok: bool, **details: Any) -> dict[str, Any]:
|
|
148
|
+
item: dict[str, Any] = {"name": name, "ok": ok}
|
|
149
|
+
item.update(details)
|
|
150
|
+
return item
|
|
151
|
+
|
|
152
|
+
|
|
134
153
|
def _body_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> str:
|
|
135
154
|
if getattr(args, "body", None) is not None:
|
|
136
155
|
return args.body
|
|
@@ -168,6 +187,56 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
168
187
|
path = init_config(from_env=args.from_env)
|
|
169
188
|
write_json({"created": True, "path": str(path)})
|
|
170
189
|
return 0
|
|
190
|
+
if args.config_command == "check":
|
|
191
|
+
checks: list[dict[str, Any]] = []
|
|
192
|
+
report: dict[str, Any] = {"ok": False, "checks": checks}
|
|
193
|
+
try:
|
|
194
|
+
config = load_config()
|
|
195
|
+
checks.append(_check_item("config_loaded", True, path=str(config.path)))
|
|
196
|
+
profile = resolve_profile(
|
|
197
|
+
config,
|
|
198
|
+
getattr(args, "profile", None),
|
|
199
|
+
host=getattr(args, "host", None),
|
|
200
|
+
port=getattr(args, "port", None),
|
|
201
|
+
username=getattr(args, "username", None),
|
|
202
|
+
password=_read_password(args),
|
|
203
|
+
tls=getattr(args, "tls", None),
|
|
204
|
+
ssl_mode=getattr(args, "ssl_mode", None),
|
|
205
|
+
)
|
|
206
|
+
report["profile"] = {
|
|
207
|
+
"name": profile.name,
|
|
208
|
+
"host": profile.host,
|
|
209
|
+
"port": profile.port,
|
|
210
|
+
"username": profile.username,
|
|
211
|
+
"password_env": profile.password_env,
|
|
212
|
+
"has_password": bool(profile.password),
|
|
213
|
+
"tls": profile.tls,
|
|
214
|
+
"ssl_mode": profile.ssl_mode,
|
|
215
|
+
}
|
|
216
|
+
checks.append(_check_item("profile_resolved", True))
|
|
217
|
+
checks.append(_check_item("password_available", bool(profile.password), password_env=profile.password_env))
|
|
218
|
+
with ImapSession(profile, config.defaults) as session:
|
|
219
|
+
checks.append(_check_item("login", True, security=session.security))
|
|
220
|
+
capabilities = session.capabilities()
|
|
221
|
+
checks.append(_check_item("capabilities", True, values=capabilities))
|
|
222
|
+
folders = session.folders()["folders"]
|
|
223
|
+
checks.append(_check_item("folders", True, count=len(folders)))
|
|
224
|
+
default_folder = config.defaults.default_folder
|
|
225
|
+
selectable = {str(folder["name"]) for folder in folders if folder.get("selectable")}
|
|
226
|
+
default_ok = default_folder in selectable
|
|
227
|
+
checks.append(_check_item("default_folder", default_ok, folder=default_folder))
|
|
228
|
+
if default_ok:
|
|
229
|
+
session._select(default_folder, readonly=True)
|
|
230
|
+
try:
|
|
231
|
+
drafts_folder = session.resolve_drafts_folder()
|
|
232
|
+
checks.append(_check_item("drafts_folder", True, folder=drafts_folder))
|
|
233
|
+
except AppError as exc:
|
|
234
|
+
checks.append(_check_item("drafts_folder", False, error=exc.message))
|
|
235
|
+
except AppError as exc:
|
|
236
|
+
checks.append(_check_item(exc.code, False, error=exc.message))
|
|
237
|
+
report["ok"] = all(item["ok"] for item in checks)
|
|
238
|
+
write_json(report)
|
|
239
|
+
return 0 if report["ok"] else 1
|
|
171
240
|
if args.config_command == "show":
|
|
172
241
|
write_json(config_status(load_config()))
|
|
173
242
|
return 0
|
|
@@ -223,6 +292,7 @@ def cmd_search(args: argparse.Namespace) -> int:
|
|
|
223
292
|
folder = args.folder or payload.get("folder") or config.defaults.default_folder
|
|
224
293
|
scope = payload.get("scope") or ("all" if args.all_folders else "recursive" if args.recursive else "folder")
|
|
225
294
|
max_results = int(args.max_results or payload.get("max_results") or config.defaults.max_results)
|
|
295
|
+
max_scan = int(args.max_scan or payload.get("max_scan") or config.defaults.max_scan)
|
|
226
296
|
with _session(args) as session:
|
|
227
297
|
write_json(
|
|
228
298
|
session.search(
|
|
@@ -230,9 +300,22 @@ def cmd_search(args: argparse.Namespace) -> int:
|
|
|
230
300
|
scope=str(scope),
|
|
231
301
|
subject=args.subject or payload.get("subject"),
|
|
232
302
|
sender=args.sender or payload.get("from"),
|
|
303
|
+
recipient=args.recipient or payload.get("to"),
|
|
304
|
+
message_id=args.message_id or payload.get("message_id"),
|
|
305
|
+
text=args.text or payload.get("text"),
|
|
233
306
|
since=args.since or payload.get("since"),
|
|
234
307
|
before=args.before or payload.get("before"),
|
|
308
|
+
unseen=bool(args.unseen or payload.get("unseen", False)),
|
|
309
|
+
seen=bool(args.seen or payload.get("seen", False)),
|
|
310
|
+
answered=bool(args.answered or payload.get("answered", False)),
|
|
311
|
+
flagged=bool(args.flagged or payload.get("flagged", False)),
|
|
312
|
+
larger=args.larger or payload.get("larger"),
|
|
313
|
+
smaller=args.smaller or payload.get("smaller"),
|
|
314
|
+
has_attachments=bool(args.has_attachments or payload.get("has_attachments", False)),
|
|
235
315
|
max_results=max_results,
|
|
316
|
+
max_scan=max_scan,
|
|
317
|
+
sort=str(args.sort or payload.get("sort") or "uid"),
|
|
318
|
+
order=str(args.order or payload.get("order") or "desc"),
|
|
236
319
|
)
|
|
237
320
|
)
|
|
238
321
|
return 0
|
|
@@ -247,8 +330,38 @@ def cmd_read(args: argparse.Namespace) -> int:
|
|
|
247
330
|
config = load_config()
|
|
248
331
|
body_format = args.body_format or payload.get("body_format") or config.defaults.body_format
|
|
249
332
|
max_body_chars = int(args.max_body_chars or payload.get("max_body_chars") or config.defaults.max_body_chars)
|
|
333
|
+
include_attachments = _include_attachments(args.include_attachments or payload.get("include_attachments") or "metadata")
|
|
334
|
+
with _session(args) as session:
|
|
335
|
+
write_json(
|
|
336
|
+
session.read(
|
|
337
|
+
folder=str(folder),
|
|
338
|
+
uid=int(uid),
|
|
339
|
+
body_format=str(body_format),
|
|
340
|
+
max_body_chars=max_body_chars,
|
|
341
|
+
include_attachments=include_attachments,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_thread(args: argparse.Namespace) -> int:
|
|
348
|
+
if not args.folder or args.uid is None:
|
|
349
|
+
raise AppError("invalid_request", "thread requires --folder and --uid.")
|
|
350
|
+
config = load_config()
|
|
351
|
+
max_body_chars = int(args.max_body_chars or config.defaults.max_body_chars)
|
|
352
|
+
max_scan = int(args.max_scan or config.defaults.max_scan)
|
|
250
353
|
with _session(args) as session:
|
|
251
|
-
write_json(
|
|
354
|
+
write_json(
|
|
355
|
+
session.thread(
|
|
356
|
+
folder=args.folder,
|
|
357
|
+
uid=args.uid,
|
|
358
|
+
max_messages=args.max_messages,
|
|
359
|
+
max_scan=max_scan,
|
|
360
|
+
include_body=args.include_body,
|
|
361
|
+
body_format=args.body_format,
|
|
362
|
+
max_body_chars=max_body_chars,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
252
365
|
return 0
|
|
253
366
|
|
|
254
367
|
|
|
@@ -362,6 +475,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
362
475
|
config_init = config_sub.add_parser("init", help="create a starter config file")
|
|
363
476
|
config_init.add_argument("--from-env", action="store_true")
|
|
364
477
|
config_init.set_defaults(func=cmd_config)
|
|
478
|
+
config_check = config_sub.add_parser("check", help="validate profile configuration and IMAP connectivity")
|
|
479
|
+
_common_profile_args(config_check)
|
|
480
|
+
config_check.set_defaults(func=cmd_config)
|
|
365
481
|
config_show = config_sub.add_parser("show", help="show resolved config metadata without secrets")
|
|
366
482
|
config_show.set_defaults(func=cmd_config)
|
|
367
483
|
config_default = config_sub.add_parser("set-default-profile", help="set the default profile")
|
|
@@ -406,9 +522,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
406
522
|
search.add_argument("--all-folders", action="store_true")
|
|
407
523
|
search.add_argument("--subject")
|
|
408
524
|
search.add_argument("--from", dest="sender")
|
|
525
|
+
search.add_argument("--to", dest="recipient")
|
|
526
|
+
search.add_argument("--message-id")
|
|
527
|
+
search.add_argument("--text")
|
|
409
528
|
search.add_argument("--since")
|
|
410
529
|
search.add_argument("--before")
|
|
530
|
+
search.add_argument("--unseen", action="store_true")
|
|
531
|
+
search.add_argument("--seen", action="store_true")
|
|
532
|
+
search.add_argument("--answered", action="store_true")
|
|
533
|
+
search.add_argument("--flagged", action="store_true")
|
|
534
|
+
search.add_argument("--larger", type=int)
|
|
535
|
+
search.add_argument("--smaller", type=int)
|
|
536
|
+
search.add_argument("--has-attachments", action="store_true")
|
|
411
537
|
search.add_argument("--max-results", type=int)
|
|
538
|
+
search.add_argument("--max-scan", type=int)
|
|
539
|
+
search.add_argument("--sort", choices=["uid", "date"], default=None)
|
|
540
|
+
search.add_argument("--order", choices=["asc", "desc"], default=None)
|
|
412
541
|
search.set_defaults(func=cmd_search)
|
|
413
542
|
|
|
414
543
|
read = sub.add_parser("read", help="read a message by folder and UID without marking it read")
|
|
@@ -418,8 +547,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
418
547
|
read.add_argument("--uid", type=int)
|
|
419
548
|
read.add_argument("--body-format", choices=["html", "markdown", "plain", "raw-html", "metadata"])
|
|
420
549
|
read.add_argument("--max-body-chars", type=int)
|
|
550
|
+
read.add_argument("--include-attachments", choices=["none", "metadata", "false", "true"])
|
|
421
551
|
read.set_defaults(func=cmd_read)
|
|
422
552
|
|
|
553
|
+
thread = sub.add_parser("thread", help="inspect bounded thread context for a message")
|
|
554
|
+
_common_profile_args(thread)
|
|
555
|
+
thread.add_argument("--folder", required=True)
|
|
556
|
+
thread.add_argument("--uid", required=True, type=int)
|
|
557
|
+
thread.add_argument("--max-messages", type=int, default=5)
|
|
558
|
+
thread.add_argument("--max-scan", type=int)
|
|
559
|
+
thread.add_argument("--include-body", choices=["none", "latest", "all"], default="none")
|
|
560
|
+
thread.add_argument("--body-format", choices=["html", "markdown", "plain", "raw-html", "metadata"], default="metadata")
|
|
561
|
+
thread.add_argument("--max-body-chars", type=int)
|
|
562
|
+
thread.set_defaults(func=cmd_thread)
|
|
563
|
+
|
|
423
564
|
attachments = sub.add_parser("attachments", help="list attachment metadata for a message")
|
|
424
565
|
_common_profile_args(attachments)
|
|
425
566
|
attachments.add_argument("--folder")
|
|
@@ -67,6 +67,7 @@ def _env_profile() -> Profile | None:
|
|
|
67
67
|
return None
|
|
68
68
|
defaults = Defaults(
|
|
69
69
|
max_results=_env_int(f"{ENV_PREFIX}MAX_RESULTS", 25),
|
|
70
|
+
max_scan=_env_int(f"{ENV_PREFIX}MAX_SCAN", 250),
|
|
70
71
|
max_body_chars=_env_int(f"{ENV_PREFIX}MAX_BODY_CHARS", 12000),
|
|
71
72
|
connect_timeout_seconds=_env_int(f"{ENV_PREFIX}CONNECT_TIMEOUT_SECONDS", 15),
|
|
72
73
|
read_timeout_seconds=_env_int(f"{ENV_PREFIX}READ_TIMEOUT_SECONDS", 30),
|
|
@@ -229,6 +230,7 @@ profile = "default"
|
|
|
229
230
|
format = "json"
|
|
230
231
|
default_folder = "INBOX"
|
|
231
232
|
max_results = 25
|
|
233
|
+
max_scan = 250
|
|
232
234
|
max_body_chars = 12000
|
|
233
235
|
connect_timeout_seconds = 15
|
|
234
236
|
read_timeout_seconds = 30
|
|
@@ -288,6 +290,7 @@ def add_profile(
|
|
|
288
290
|
raw["defaults"].setdefault("format", "json")
|
|
289
291
|
raw["defaults"].setdefault("default_folder", "INBOX")
|
|
290
292
|
raw["defaults"].setdefault("max_results", 25)
|
|
293
|
+
raw["defaults"].setdefault("max_scan", 250)
|
|
291
294
|
raw["defaults"].setdefault("max_body_chars", 12000)
|
|
292
295
|
raw["defaults"].setdefault("connect_timeout_seconds", 15)
|
|
293
296
|
raw["defaults"].setdefault("read_timeout_seconds", 30)
|