imap-agent-cli 0.1.3__tar.gz → 0.1.4__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/AGENTS.md +88 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/PKG-INFO +88 -40
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/README.md +87 -39
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/pyproject.toml +1 -1
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/__init__.py +1 -1
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/cli.py +96 -19
- imap_agent_cli-0.1.4/tests/test_cli.py +101 -0
- imap_agent_cli-0.1.3/tests/test_cli.py +0 -52
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/.github/workflows/publish.yml +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/.gitignore +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/LICENSE +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/imap_agent_cli.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/spec.md +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/config.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/errors.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/imap_client.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/mime.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/models.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/render.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/search.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/skill.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/__init__.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/_bootstrap.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/pymap_server_runner.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_config.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_mime.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_pymap_integration.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_search.py +0 -0
- {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_skill.py +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Project Purpose
|
|
4
|
+
|
|
5
|
+
`imap-agent-cli` is a Python CLI for safe agentic email access over generic IMAP. It can search mailboxes, read messages without marking them read, inspect or download attachments when explicitly requested, and append messages to Drafts.
|
|
6
|
+
|
|
7
|
+
The core safety boundary is strict: never add support for sending email, deleting messages, moving messages, archiving messages, labeling messages, flagging messages, starring messages, marking messages read/unread, or creating/deleting/renaming folders.
|
|
8
|
+
|
|
9
|
+
## Repository Layout
|
|
10
|
+
|
|
11
|
+
- `imap_agent_cli.py` is the root local-development wrapper with PEP 723 metadata.
|
|
12
|
+
- `src/imap_agent_cli/cli.py` owns argument parsing and command dispatch.
|
|
13
|
+
- `src/imap_agent_cli/config.py` owns env-var and profile config behavior.
|
|
14
|
+
- `src/imap_agent_cli/imap_client.py` owns IMAP interactions.
|
|
15
|
+
- `src/imap_agent_cli/mime.py` owns MIME parsing and draft composition.
|
|
16
|
+
- `src/imap_agent_cli/render.py` owns body format rendering and sanitization.
|
|
17
|
+
- `src/imap_agent_cli/search.py` owns search criteria construction.
|
|
18
|
+
- `src/imap_agent_cli/skill.py` owns the installed `imap` skill text.
|
|
19
|
+
- `tests/` contains unit tests and the opt-in local `pymap` integration test.
|
|
20
|
+
- `spec.md` is the product behavior target.
|
|
21
|
+
|
|
22
|
+
## Development Rules
|
|
23
|
+
|
|
24
|
+
- Keep the PyPI package name, GitHub repository name, and console command aligned as `imap-agent-cli`.
|
|
25
|
+
- Keep the root wrapper thin; application logic belongs under `src/imap_agent_cli/`.
|
|
26
|
+
- Preserve stable JSON payloads on stdout. Diagnostics, warnings, progress, and errors must go to stderr.
|
|
27
|
+
- Do not print secrets, credentials, full message bodies, or attachment contents in logs.
|
|
28
|
+
- Keep credentials in environment variables or config references to environment variables. Do not add config examples that store passwords directly.
|
|
29
|
+
- Keep README and skill language platform-neutral and agent-tool-neutral. Avoid shell-specific syntax unless explicitly documenting a shell-specific example.
|
|
30
|
+
- If changing installed-skill behavior or wording, update `src/imap_agent_cli/skill.py` and `tests/test_skill.py` together.
|
|
31
|
+
- If changing MIME parsing, body rendering, draft creation, or search behavior, add focused tests for the contract being changed.
|
|
32
|
+
|
|
33
|
+
## Safety-Sensitive Implementation Notes
|
|
34
|
+
|
|
35
|
+
- Reads must avoid changing message state. Preserve no-seen fetch behavior.
|
|
36
|
+
- Draft creation is allowed only by appending a new MIME message to the detected or configured Drafts folder.
|
|
37
|
+
- Reply drafts should preserve appropriate reply headers such as `In-Reply-To` and `References`.
|
|
38
|
+
- Attachment downloads require explicit user/agent intent and an output directory.
|
|
39
|
+
- Folder-wide or all-folder operations should remain bounded by defaults and overridable limits.
|
|
40
|
+
- HTML email is untrusted input. Sanitize before returning HTML and keep Markdown conversion intentionally lossy but predictable.
|
|
41
|
+
|
|
42
|
+
## Common Commands
|
|
43
|
+
|
|
44
|
+
Run the CLI locally:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
uv run ./imap_agent_cli.py --help
|
|
48
|
+
uv run ./imap_agent_cli.py folders
|
|
49
|
+
uv run ./imap_agent_cli.py search --subject invoice
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run no-network tests:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
python -m unittest discover -v
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run the optional local IMAP integration test:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
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
|
+
|
|
66
|
+
Build the package:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
uv build --no-sources
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Publishing Notes
|
|
73
|
+
|
|
74
|
+
- Package metadata lives in `pyproject.toml`.
|
|
75
|
+
- The GitHub Actions publish workflow is `.github/workflows/publish.yml`.
|
|
76
|
+
- PyPI Trusted Publishing uses:
|
|
77
|
+
- Project: `imap-agent-cli`
|
|
78
|
+
- Owner: `pseudosavant`
|
|
79
|
+
- Repository: `imap-agent-cli`
|
|
80
|
+
- Workflow: `publish.yml`
|
|
81
|
+
- Environment: `pypi`
|
|
82
|
+
|
|
83
|
+
## Before Finishing Changes
|
|
84
|
+
|
|
85
|
+
- Run the smallest relevant tests for the change.
|
|
86
|
+
- For broad behavior changes, run `python -m unittest discover -v`.
|
|
87
|
+
- For packaging or release changes, also run `uv build --no-sources`.
|
|
88
|
+
- Check `git status --short` and mention any uncommitted or unverified work.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imap-agent-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -37,79 +37,98 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
It can inspect mailboxes and append messages to Drafts. It cannot send email, delete messages, move messages, archive messages, change labels, alter flags, or mark messages read/unread.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Using imap-agent-cli
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
### Quick Start
|
|
43
|
+
|
|
44
|
+
Install or update the `imap` agent skill:
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
```text
|
|
47
|
+
uvx imap-agent-cli install-skill
|
|
48
|
+
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
Configure the default IMAP account with environment variables:
|
|
47
51
|
|
|
48
52
|
```text
|
|
49
53
|
IMAP_AGENT_CLI_HOST=imap.example.com
|
|
50
54
|
IMAP_AGENT_CLI_PORT=993
|
|
51
55
|
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
52
|
-
IMAP_AGENT_CLI_PASSWORD
|
|
56
|
+
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
53
57
|
IMAP_AGENT_CLI_TLS=true
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
Then ask your agentic tool to use the `imap` skill for mailbox search, email reading, attachment inspection/download, or draft creation.
|
|
61
|
+
|
|
62
|
+
### Use From PyPI
|
|
63
|
+
|
|
64
|
+
Use the published CLI directly with `uvx`:
|
|
57
65
|
|
|
58
66
|
```text
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
uvx imap-agent-cli --help
|
|
68
|
+
uvx imap-agent-cli folders
|
|
69
|
+
uvx imap-agent-cli search --subject invoice
|
|
62
70
|
```
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
Install it as a persistent `uv` tool when you want to call `imap-agent-cli` directly:
|
|
65
73
|
|
|
66
74
|
```text
|
|
67
|
-
|
|
75
|
+
uv tool install imap-agent-cli
|
|
76
|
+
uv tool update-shell
|
|
77
|
+
imap-agent-cli --help
|
|
68
78
|
```
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
Restart your shell after `uv tool update-shell` if `imap-agent-cli` is not found.
|
|
81
|
+
|
|
82
|
+
Or install it into a Python environment:
|
|
71
83
|
|
|
72
84
|
```text
|
|
73
|
-
imap-agent-cli
|
|
74
|
-
imap-agent-cli
|
|
75
|
-
imap-agent-cli read --folder INBOX --uid 12345 --body-format html
|
|
76
|
-
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
77
|
-
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
78
|
-
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
79
|
-
imap-agent-cli draft reply --folder INBOX --uid 12345 --body "Thanks. I will review this."
|
|
85
|
+
python -m pip install imap-agent-cli
|
|
86
|
+
imap-agent-cli --help
|
|
80
87
|
```
|
|
81
88
|
|
|
82
89
|
All command payloads are JSON on stdout. Diagnostics and errors go to stderr.
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
Use `imap-agent-cli --about` for project URL and license attribution. Use `imap-agent-cli --version` to print only the version number.
|
|
92
|
+
|
|
93
|
+
### Configuration
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
For a single account, environment variables are enough:
|
|
87
96
|
|
|
88
97
|
```text
|
|
89
|
-
imap
|
|
98
|
+
IMAP_AGENT_CLI_HOST=imap.example.com
|
|
99
|
+
IMAP_AGENT_CLI_PORT=993
|
|
100
|
+
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
101
|
+
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
102
|
+
IMAP_AGENT_CLI_TLS=true
|
|
103
|
+
IMAP_AGENT_CLI_DRAFTS_FOLDER=Drafts
|
|
90
104
|
```
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
`IMAP_AGENT_CLI_DRAFTS_FOLDER` is optional. When omitted, the CLI tries to auto-detect the Drafts folder.
|
|
107
|
+
|
|
108
|
+
For multiple accounts, create a config file:
|
|
93
109
|
|
|
94
110
|
```text
|
|
111
|
+
imap-agent-cli config init
|
|
95
112
|
imap-agent-cli config add-profile work --host imap.example.com --port 993 --username me@example.com --password-env IMAP_AGENT_CLI_WORK_PASSWORD
|
|
96
113
|
imap-agent-cli config set-default-profile work
|
|
97
114
|
```
|
|
98
115
|
|
|
99
|
-
|
|
116
|
+
The config file is stored at:
|
|
100
117
|
|
|
101
118
|
```text
|
|
102
119
|
~/.imap-agent-cli/config.toml
|
|
103
120
|
```
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
|
|
106
123
|
|
|
107
|
-
|
|
124
|
+
### Agent Skill
|
|
125
|
+
|
|
126
|
+
The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
|
|
108
127
|
|
|
109
128
|
Install or update the user-scoped `imap` skill:
|
|
110
129
|
|
|
111
130
|
```text
|
|
112
|
-
imap-agent-cli install-skill
|
|
131
|
+
uvx imap-agent-cli install-skill
|
|
113
132
|
```
|
|
114
133
|
|
|
115
134
|
This writes:
|
|
@@ -121,16 +140,16 @@ This writes:
|
|
|
121
140
|
Remove the managed skill:
|
|
122
141
|
|
|
123
142
|
```text
|
|
124
|
-
imap-agent-cli remove-skill
|
|
143
|
+
uvx imap-agent-cli remove-skill
|
|
125
144
|
```
|
|
126
145
|
|
|
127
|
-
Use `--skills-dir PATH` to install into a nonstandard skills directory
|
|
146
|
+
Use `--skills-dir PATH` to install into a nonstandard skills directory:
|
|
128
147
|
|
|
129
148
|
```text
|
|
130
|
-
imap-agent-cli install-skill --skills-dir ~/.agents/skills
|
|
149
|
+
uvx imap-agent-cli install-skill --skills-dir ~/.agents/skills
|
|
131
150
|
```
|
|
132
151
|
|
|
133
|
-
|
|
152
|
+
### Safety Boundary
|
|
134
153
|
|
|
135
154
|
Allowed:
|
|
136
155
|
|
|
@@ -150,32 +169,61 @@ Not allowed:
|
|
|
150
169
|
- mark read/unread
|
|
151
170
|
- create/delete/rename folders
|
|
152
171
|
|
|
153
|
-
|
|
172
|
+
### Example Commands
|
|
154
173
|
|
|
155
|
-
|
|
174
|
+
These examples are mostly useful for validating configuration or debugging what an agentic tool is doing:
|
|
156
175
|
|
|
157
176
|
```text
|
|
158
|
-
|
|
177
|
+
imap-agent-cli folders
|
|
178
|
+
imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
|
|
179
|
+
imap-agent-cli read --folder INBOX --uid 12345 --body-format html
|
|
180
|
+
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
181
|
+
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
182
|
+
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
183
|
+
imap-agent-cli draft reply --folder INBOX --uid 12345 --body "Thanks. I will review this."
|
|
159
184
|
```
|
|
160
185
|
|
|
161
|
-
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
### Local Development
|
|
189
|
+
|
|
190
|
+
Run the CLI from the repository:
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
uv run ./imap_agent_cli.py --help
|
|
194
|
+
uv run ./imap_agent_cli.py folders
|
|
195
|
+
uv run ./imap_agent_cli.py search --subject invoice
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Build the package:
|
|
162
199
|
|
|
163
200
|
```text
|
|
164
201
|
uv build --no-sources
|
|
165
202
|
```
|
|
166
203
|
|
|
167
|
-
|
|
204
|
+
### Testing
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
Run no-network unit tests:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
python -m unittest discover -v
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Run the opt-in local IMAP integration test:
|
|
213
|
+
|
|
214
|
+
Set `IMAP_AGENT_CLI_TEST_PYMAP=1` in your shell, then run:
|
|
170
215
|
|
|
171
216
|
```text
|
|
172
|
-
$env:IMAP_AGENT_CLI_TEST_PYMAP = "1"
|
|
173
217
|
uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
174
218
|
```
|
|
175
219
|
|
|
176
220
|
The integration test starts `pymap dict --demo-data` locally and logs in with `demouser` / `demopass`.
|
|
177
221
|
|
|
178
|
-
|
|
222
|
+
### Project Status
|
|
223
|
+
|
|
224
|
+
This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
|
|
225
|
+
|
|
226
|
+
### Publishing
|
|
179
227
|
|
|
180
228
|
The GitHub Actions workflow is `.github/workflows/publish.yml`.
|
|
181
229
|
|
|
@@ -4,79 +4,98 @@
|
|
|
4
4
|
|
|
5
5
|
It can inspect mailboxes and append messages to Drafts. It cannot send email, delete messages, move messages, archive messages, change labels, alter flags, or mark messages read/unread.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Using imap-agent-cli
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Quick Start
|
|
10
|
+
|
|
11
|
+
Install or update the `imap` agent skill:
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
```text
|
|
14
|
+
uvx imap-agent-cli install-skill
|
|
15
|
+
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Configure the default IMAP account with environment variables:
|
|
14
18
|
|
|
15
19
|
```text
|
|
16
20
|
IMAP_AGENT_CLI_HOST=imap.example.com
|
|
17
21
|
IMAP_AGENT_CLI_PORT=993
|
|
18
22
|
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
19
|
-
IMAP_AGENT_CLI_PASSWORD
|
|
23
|
+
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
20
24
|
IMAP_AGENT_CLI_TLS=true
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
Then ask your agentic tool to use the `imap` skill for mailbox search, email reading, attachment inspection/download, or draft creation.
|
|
28
|
+
|
|
29
|
+
### Use From PyPI
|
|
30
|
+
|
|
31
|
+
Use the published CLI directly with `uvx`:
|
|
24
32
|
|
|
25
33
|
```text
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
uvx imap-agent-cli --help
|
|
35
|
+
uvx imap-agent-cli folders
|
|
36
|
+
uvx imap-agent-cli search --subject invoice
|
|
29
37
|
```
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
Install it as a persistent `uv` tool when you want to call `imap-agent-cli` directly:
|
|
32
40
|
|
|
33
41
|
```text
|
|
34
|
-
|
|
42
|
+
uv tool install imap-agent-cli
|
|
43
|
+
uv tool update-shell
|
|
44
|
+
imap-agent-cli --help
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
Restart your shell after `uv tool update-shell` if `imap-agent-cli` is not found.
|
|
48
|
+
|
|
49
|
+
Or install it into a Python environment:
|
|
38
50
|
|
|
39
51
|
```text
|
|
40
|
-
imap-agent-cli
|
|
41
|
-
imap-agent-cli
|
|
42
|
-
imap-agent-cli read --folder INBOX --uid 12345 --body-format html
|
|
43
|
-
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
44
|
-
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
45
|
-
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
46
|
-
imap-agent-cli draft reply --folder INBOX --uid 12345 --body "Thanks. I will review this."
|
|
52
|
+
python -m pip install imap-agent-cli
|
|
53
|
+
imap-agent-cli --help
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
All command payloads are JSON on stdout. Diagnostics and errors go to stderr.
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
Use `imap-agent-cli --about` for project URL and license attribution. Use `imap-agent-cli --version` to print only the version number.
|
|
59
|
+
|
|
60
|
+
### Configuration
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
For a single account, environment variables are enough:
|
|
54
63
|
|
|
55
64
|
```text
|
|
56
|
-
imap
|
|
65
|
+
IMAP_AGENT_CLI_HOST=imap.example.com
|
|
66
|
+
IMAP_AGENT_CLI_PORT=993
|
|
67
|
+
IMAP_AGENT_CLI_USERNAME=me@example.com
|
|
68
|
+
IMAP_AGENT_CLI_PASSWORD=your-password-or-app-password
|
|
69
|
+
IMAP_AGENT_CLI_TLS=true
|
|
70
|
+
IMAP_AGENT_CLI_DRAFTS_FOLDER=Drafts
|
|
57
71
|
```
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
`IMAP_AGENT_CLI_DRAFTS_FOLDER` is optional. When omitted, the CLI tries to auto-detect the Drafts folder.
|
|
74
|
+
|
|
75
|
+
For multiple accounts, create a config file:
|
|
60
76
|
|
|
61
77
|
```text
|
|
78
|
+
imap-agent-cli config init
|
|
62
79
|
imap-agent-cli config add-profile work --host imap.example.com --port 993 --username me@example.com --password-env IMAP_AGENT_CLI_WORK_PASSWORD
|
|
63
80
|
imap-agent-cli config set-default-profile work
|
|
64
81
|
```
|
|
65
82
|
|
|
66
|
-
|
|
83
|
+
The config file is stored at:
|
|
67
84
|
|
|
68
85
|
```text
|
|
69
86
|
~/.imap-agent-cli/config.toml
|
|
70
87
|
```
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
### Agent Skill
|
|
92
|
+
|
|
93
|
+
The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
|
|
75
94
|
|
|
76
95
|
Install or update the user-scoped `imap` skill:
|
|
77
96
|
|
|
78
97
|
```text
|
|
79
|
-
imap-agent-cli install-skill
|
|
98
|
+
uvx imap-agent-cli install-skill
|
|
80
99
|
```
|
|
81
100
|
|
|
82
101
|
This writes:
|
|
@@ -88,16 +107,16 @@ This writes:
|
|
|
88
107
|
Remove the managed skill:
|
|
89
108
|
|
|
90
109
|
```text
|
|
91
|
-
imap-agent-cli remove-skill
|
|
110
|
+
uvx imap-agent-cli remove-skill
|
|
92
111
|
```
|
|
93
112
|
|
|
94
|
-
Use `--skills-dir PATH` to install into a nonstandard skills directory
|
|
113
|
+
Use `--skills-dir PATH` to install into a nonstandard skills directory:
|
|
95
114
|
|
|
96
115
|
```text
|
|
97
|
-
imap-agent-cli install-skill --skills-dir ~/.agents/skills
|
|
116
|
+
uvx imap-agent-cli install-skill --skills-dir ~/.agents/skills
|
|
98
117
|
```
|
|
99
118
|
|
|
100
|
-
|
|
119
|
+
### Safety Boundary
|
|
101
120
|
|
|
102
121
|
Allowed:
|
|
103
122
|
|
|
@@ -117,32 +136,61 @@ Not allowed:
|
|
|
117
136
|
- mark read/unread
|
|
118
137
|
- create/delete/rename folders
|
|
119
138
|
|
|
120
|
-
|
|
139
|
+
### Example Commands
|
|
121
140
|
|
|
122
|
-
|
|
141
|
+
These examples are mostly useful for validating configuration or debugging what an agentic tool is doing:
|
|
123
142
|
|
|
124
143
|
```text
|
|
125
|
-
|
|
144
|
+
imap-agent-cli folders
|
|
145
|
+
imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
|
|
146
|
+
imap-agent-cli read --folder INBOX --uid 12345 --body-format html
|
|
147
|
+
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
148
|
+
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
149
|
+
imap-agent-cli draft create --to person@example.com --subject "Hello" --body "Draft only."
|
|
150
|
+
imap-agent-cli draft reply --folder INBOX --uid 12345 --body "Thanks. I will review this."
|
|
126
151
|
```
|
|
127
152
|
|
|
128
|
-
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
### Local Development
|
|
156
|
+
|
|
157
|
+
Run the CLI from the repository:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
uv run ./imap_agent_cli.py --help
|
|
161
|
+
uv run ./imap_agent_cli.py folders
|
|
162
|
+
uv run ./imap_agent_cli.py search --subject invoice
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Build the package:
|
|
129
166
|
|
|
130
167
|
```text
|
|
131
168
|
uv build --no-sources
|
|
132
169
|
```
|
|
133
170
|
|
|
134
|
-
|
|
171
|
+
### Testing
|
|
135
172
|
|
|
136
|
-
|
|
173
|
+
Run no-network unit tests:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
python -m unittest discover -v
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Run the opt-in local IMAP integration test:
|
|
180
|
+
|
|
181
|
+
Set `IMAP_AGENT_CLI_TEST_PYMAP=1` in your shell, then run:
|
|
137
182
|
|
|
138
183
|
```text
|
|
139
|
-
$env:IMAP_AGENT_CLI_TEST_PYMAP = "1"
|
|
140
184
|
uv run --extra test python -m unittest tests.test_pymap_integration -v
|
|
141
185
|
```
|
|
142
186
|
|
|
143
187
|
The integration test starts `pymap dict --demo-data` locally and logs in with `demouser` / `demopass`.
|
|
144
188
|
|
|
145
|
-
|
|
189
|
+
### Project Status
|
|
190
|
+
|
|
191
|
+
This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
|
|
192
|
+
|
|
193
|
+
### Publishing
|
|
146
194
|
|
|
147
195
|
The GitHub Actions workflow is `.github/workflows/publish.yml`.
|
|
148
196
|
|
|
@@ -15,6 +15,68 @@ from .render import write_error, write_json
|
|
|
15
15
|
from .skill import install_skill, remove_skill
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
PROJECT_URL = "https://github.com/pseudosavant/imap-agent-cli"
|
|
19
|
+
LICENSE_NAME = "MIT"
|
|
20
|
+
|
|
21
|
+
TOP_LEVEL_HELP = f"""imap-agent-cli - safe IMAP email access for agentic tools
|
|
22
|
+
|
|
23
|
+
Safety:
|
|
24
|
+
Can: list folders, search, read without marking read, inspect/download attachments, create Drafts
|
|
25
|
+
Cannot: send, delete, move, archive, label, flag, mark read/unread
|
|
26
|
+
|
|
27
|
+
Setup:
|
|
28
|
+
Required env vars for the default profile:
|
|
29
|
+
IMAP_AGENT_CLI_HOST
|
|
30
|
+
IMAP_AGENT_CLI_PORT
|
|
31
|
+
IMAP_AGENT_CLI_USERNAME
|
|
32
|
+
IMAP_AGENT_CLI_PASSWORD
|
|
33
|
+
IMAP_AGENT_CLI_TLS
|
|
34
|
+
|
|
35
|
+
Common workflows:
|
|
36
|
+
imap-agent-cli folders
|
|
37
|
+
imap-agent-cli search --folder INBOX --from "Justin" --max-results 10
|
|
38
|
+
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
|
|
40
|
+
imap-agent-cli attachments --folder INBOX --uid 12345
|
|
41
|
+
imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 2 --output-dir ./email-attachments
|
|
42
|
+
imap-agent-cli draft create --to person@example.com --subject "Subject" --body-file ./draft.txt
|
|
43
|
+
imap-agent-cli draft reply --folder INBOX --uid 12345 --body-file ./reply.txt
|
|
44
|
+
|
|
45
|
+
Output:
|
|
46
|
+
stdout is JSON payload only for operational commands
|
|
47
|
+
stderr is diagnostics/errors only
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
config initialize and manage profile configuration
|
|
51
|
+
profiles list configured profile names
|
|
52
|
+
install-skill install or update the imap agent skill
|
|
53
|
+
remove-skill remove the managed imap agent skill
|
|
54
|
+
folders list folders
|
|
55
|
+
search search messages
|
|
56
|
+
read read a message by folder and UID
|
|
57
|
+
attachments list or download attachments
|
|
58
|
+
draft create new or reply drafts
|
|
59
|
+
|
|
60
|
+
More help:
|
|
61
|
+
imap-agent-cli <command> --help
|
|
62
|
+
imap-agent-cli --about
|
|
63
|
+
imap-agent-cli --version
|
|
64
|
+
|
|
65
|
+
Project:
|
|
66
|
+
{PROJECT_URL}
|
|
67
|
+
License:
|
|
68
|
+
{LICENSE_NAME}
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
ABOUT_TEXT = f"""imap-agent-cli {__version__}
|
|
72
|
+
|
|
73
|
+
Safe IMAP email access for agentic tools.
|
|
74
|
+
|
|
75
|
+
Project: {PROJECT_URL}
|
|
76
|
+
License: {LICENSE_NAME}
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
18
80
|
def _read_json_arg(value: str) -> dict[str, Any]:
|
|
19
81
|
if value == "-":
|
|
20
82
|
text = sys.stdin.read()
|
|
@@ -287,21 +349,25 @@ def cmd_draft_reply(args: argparse.Namespace) -> int:
|
|
|
287
349
|
|
|
288
350
|
|
|
289
351
|
def build_parser() -> argparse.ArgumentParser:
|
|
290
|
-
parser = argparse.ArgumentParser(
|
|
291
|
-
|
|
352
|
+
parser = argparse.ArgumentParser(
|
|
353
|
+
prog="imap-agent-cli",
|
|
354
|
+
description="Safe IMAP email access for agentic tools.",
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
357
|
+
parser.add_argument("--about", action="store_true", help="show project and license information and exit")
|
|
292
358
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
293
359
|
|
|
294
|
-
config = sub.add_parser("config")
|
|
360
|
+
config = sub.add_parser("config", help="initialize and manage profile configuration")
|
|
295
361
|
config_sub = config.add_subparsers(dest="config_command", required=True)
|
|
296
|
-
config_init = config_sub.add_parser("init")
|
|
362
|
+
config_init = config_sub.add_parser("init", help="create a starter config file")
|
|
297
363
|
config_init.add_argument("--from-env", action="store_true")
|
|
298
364
|
config_init.set_defaults(func=cmd_config)
|
|
299
|
-
config_show = config_sub.add_parser("show")
|
|
365
|
+
config_show = config_sub.add_parser("show", help="show resolved config metadata without secrets")
|
|
300
366
|
config_show.set_defaults(func=cmd_config)
|
|
301
|
-
config_default = config_sub.add_parser("set-default-profile")
|
|
367
|
+
config_default = config_sub.add_parser("set-default-profile", help="set the default profile")
|
|
302
368
|
config_default.add_argument("name")
|
|
303
369
|
config_default.set_defaults(func=cmd_config)
|
|
304
|
-
config_add = config_sub.add_parser("add-profile")
|
|
370
|
+
config_add = config_sub.add_parser("add-profile", help="add or update a named profile")
|
|
305
371
|
config_add.add_argument("name")
|
|
306
372
|
config_add.add_argument("--host", required=True)
|
|
307
373
|
config_add.add_argument("--port", type=int, default=993)
|
|
@@ -312,27 +378,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
312
378
|
config_add.add_argument("--ssl-mode", choices=["required", "preferred", "disabled"], default="required")
|
|
313
379
|
config_add.add_argument("--drafts-folder", default="")
|
|
314
380
|
config_add.set_defaults(func=cmd_config)
|
|
315
|
-
config_remove = config_sub.add_parser("remove-profile")
|
|
381
|
+
config_remove = config_sub.add_parser("remove-profile", help="remove a named profile from config")
|
|
316
382
|
config_remove.add_argument("name")
|
|
317
383
|
config_remove.set_defaults(func=cmd_config)
|
|
318
384
|
|
|
319
|
-
profiles = sub.add_parser("profiles")
|
|
385
|
+
profiles = sub.add_parser("profiles", help="list configured profile names")
|
|
320
386
|
profiles.set_defaults(func=cmd_profiles)
|
|
321
387
|
|
|
322
|
-
install_skill_parser = sub.add_parser("install-skill")
|
|
388
|
+
install_skill_parser = sub.add_parser("install-skill", help="install or update the imap agent skill")
|
|
323
389
|
install_skill_parser.add_argument("--skills-dir")
|
|
324
390
|
install_skill_parser.set_defaults(func=cmd_install_skill)
|
|
325
391
|
|
|
326
|
-
remove_skill_parser = sub.add_parser("remove-skill")
|
|
392
|
+
remove_skill_parser = sub.add_parser("remove-skill", help="remove the managed imap agent skill")
|
|
327
393
|
remove_skill_parser.add_argument("--skills-dir")
|
|
328
394
|
remove_skill_parser.add_argument("--force", action="store_true")
|
|
329
395
|
remove_skill_parser.set_defaults(func=cmd_remove_skill)
|
|
330
396
|
|
|
331
|
-
folders = sub.add_parser("folders")
|
|
397
|
+
folders = sub.add_parser("folders", help="list folders and folder metadata")
|
|
332
398
|
_common_profile_args(folders)
|
|
333
399
|
folders.set_defaults(func=cmd_folders)
|
|
334
400
|
|
|
335
|
-
search = sub.add_parser("search")
|
|
401
|
+
search = sub.add_parser("search", help="search messages by folder, subject, sender, or date range")
|
|
336
402
|
_common_profile_args(search)
|
|
337
403
|
search.add_argument("--json", dest="json_input")
|
|
338
404
|
search.add_argument("--folder")
|
|
@@ -345,7 +411,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
345
411
|
search.add_argument("--max-results", type=int)
|
|
346
412
|
search.set_defaults(func=cmd_search)
|
|
347
413
|
|
|
348
|
-
read = sub.add_parser("read")
|
|
414
|
+
read = sub.add_parser("read", help="read a message by folder and UID without marking it read")
|
|
349
415
|
_common_profile_args(read)
|
|
350
416
|
read.add_argument("--json", dest="json_input")
|
|
351
417
|
read.add_argument("--folder")
|
|
@@ -354,13 +420,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
354
420
|
read.add_argument("--max-body-chars", type=int)
|
|
355
421
|
read.set_defaults(func=cmd_read)
|
|
356
422
|
|
|
357
|
-
attachments = sub.add_parser("attachments")
|
|
423
|
+
attachments = sub.add_parser("attachments", help="list attachment metadata for a message")
|
|
358
424
|
_common_profile_args(attachments)
|
|
359
425
|
attachments.add_argument("--folder")
|
|
360
426
|
attachments.add_argument("--uid", type=int)
|
|
361
427
|
attachments.set_defaults(func=cmd_attachments)
|
|
362
428
|
attachments_sub = attachments.add_subparsers(dest="attachment_command")
|
|
363
|
-
download = attachments_sub.add_parser("download")
|
|
429
|
+
download = attachments_sub.add_parser("download", help="download selected attachments to a directory")
|
|
364
430
|
_common_profile_args(download)
|
|
365
431
|
download.add_argument("--folder", required=True)
|
|
366
432
|
download.add_argument("--uid", required=True, type=int)
|
|
@@ -371,9 +437,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
371
437
|
download.add_argument("--overwrite", action="store_true")
|
|
372
438
|
download.set_defaults(func=cmd_attachments_download)
|
|
373
439
|
|
|
374
|
-
draft = sub.add_parser("draft")
|
|
440
|
+
draft = sub.add_parser("draft", help="create new or reply drafts")
|
|
375
441
|
draft_sub = draft.add_subparsers(dest="draft_command", required=True)
|
|
376
|
-
create = draft_sub.add_parser("create")
|
|
442
|
+
create = draft_sub.add_parser("create", help="append a new message to Drafts")
|
|
377
443
|
_common_profile_args(create)
|
|
378
444
|
create.add_argument("--json", dest="json_input")
|
|
379
445
|
create.add_argument("--to", action="append")
|
|
@@ -387,7 +453,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
387
453
|
create.add_argument("--drafts-folder")
|
|
388
454
|
create.set_defaults(func=cmd_draft_create)
|
|
389
455
|
|
|
390
|
-
reply = draft_sub.add_parser("reply")
|
|
456
|
+
reply = draft_sub.add_parser("reply", help="append a reply draft for an existing message")
|
|
391
457
|
_common_profile_args(reply)
|
|
392
458
|
reply.add_argument("--json", dest="json_input")
|
|
393
459
|
reply.add_argument("--folder")
|
|
@@ -407,6 +473,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
407
473
|
|
|
408
474
|
|
|
409
475
|
def main(argv: list[str] | None = None) -> int:
|
|
476
|
+
if argv is None:
|
|
477
|
+
argv = sys.argv[1:]
|
|
478
|
+
if not argv or argv in (["--help"], ["-h"]):
|
|
479
|
+
sys.stdout.write(TOP_LEVEL_HELP)
|
|
480
|
+
return 0
|
|
481
|
+
if argv == ["--about"]:
|
|
482
|
+
sys.stdout.write(ABOUT_TEXT)
|
|
483
|
+
return 0
|
|
484
|
+
if argv == ["--version"]:
|
|
485
|
+
sys.stdout.write(f"{__version__}\n")
|
|
486
|
+
return 0
|
|
410
487
|
parser = build_parser()
|
|
411
488
|
args = parser.parse_args(argv)
|
|
412
489
|
try:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
from tests import _bootstrap # noqa: F401
|
|
11
|
+
|
|
12
|
+
from imap_agent_cli import __version__
|
|
13
|
+
from imap_agent_cli.cli import build_parser, main
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CliTests(unittest.TestCase):
|
|
17
|
+
def test_help_parser_has_expected_commands(self) -> None:
|
|
18
|
+
parser = build_parser()
|
|
19
|
+
parsed = parser.parse_args(["search", "--subject", "invoice"])
|
|
20
|
+
self.assertEqual(parsed.command, "search")
|
|
21
|
+
self.assertEqual(parsed.subject, "invoice")
|
|
22
|
+
|
|
23
|
+
def test_no_args_prints_agent_quick_reference(self) -> None:
|
|
24
|
+
stdout = StringIO()
|
|
25
|
+
stderr = StringIO()
|
|
26
|
+
with patch("sys.stdout", stdout), patch("sys.stderr", stderr):
|
|
27
|
+
code = main([])
|
|
28
|
+
output = stdout.getvalue()
|
|
29
|
+
self.assertEqual(code, 0)
|
|
30
|
+
self.assertEqual(stderr.getvalue(), "")
|
|
31
|
+
self.assertIn("safe IMAP email access for agentic tools", output)
|
|
32
|
+
self.assertIn("Safety:", output)
|
|
33
|
+
self.assertIn("IMAP_AGENT_CLI_HOST", output)
|
|
34
|
+
self.assertIn("imap-agent-cli search --folder INBOX", output)
|
|
35
|
+
self.assertIn("stdout is JSON payload only", output)
|
|
36
|
+
self.assertIn("https://github.com/pseudosavant/imap-agent-cli", output)
|
|
37
|
+
self.assertIn("License:\n MIT", output)
|
|
38
|
+
|
|
39
|
+
def test_top_level_help_prints_agent_quick_reference(self) -> None:
|
|
40
|
+
stdout = StringIO()
|
|
41
|
+
with patch("sys.stdout", stdout):
|
|
42
|
+
code = main(["--help"])
|
|
43
|
+
output = stdout.getvalue()
|
|
44
|
+
self.assertEqual(code, 0)
|
|
45
|
+
self.assertIn("Common workflows:", output)
|
|
46
|
+
self.assertIn("imap-agent-cli <command> --help", output)
|
|
47
|
+
|
|
48
|
+
def test_about_prints_project_and_license(self) -> None:
|
|
49
|
+
stdout = StringIO()
|
|
50
|
+
with patch("sys.stdout", stdout):
|
|
51
|
+
code = main(["--about"])
|
|
52
|
+
self.assertEqual(code, 0)
|
|
53
|
+
self.assertEqual(
|
|
54
|
+
stdout.getvalue(),
|
|
55
|
+
f"""imap-agent-cli {__version__}
|
|
56
|
+
|
|
57
|
+
Safe IMAP email access for agentic tools.
|
|
58
|
+
|
|
59
|
+
Project: https://github.com/pseudosavant/imap-agent-cli
|
|
60
|
+
License: MIT
|
|
61
|
+
""",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def test_version_prints_only_semver(self) -> None:
|
|
65
|
+
stdout = StringIO()
|
|
66
|
+
with patch("sys.stdout", stdout):
|
|
67
|
+
code = main(["--version"])
|
|
68
|
+
self.assertEqual(code, 0)
|
|
69
|
+
self.assertEqual(stdout.getvalue(), f"{__version__}\n")
|
|
70
|
+
|
|
71
|
+
def test_config_init_outputs_json(self) -> None:
|
|
72
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
73
|
+
config_path = Path(tmp) / "config.toml"
|
|
74
|
+
with patch("imap_agent_cli.cli.init_config", return_value=config_path):
|
|
75
|
+
stdout = StringIO()
|
|
76
|
+
with patch("sys.stdout", stdout):
|
|
77
|
+
code = main(["config", "init"])
|
|
78
|
+
self.assertEqual(code, 0)
|
|
79
|
+
payload = json.loads(stdout.getvalue())
|
|
80
|
+
self.assertTrue(payload["created"])
|
|
81
|
+
self.assertEqual(payload["path"], str(config_path))
|
|
82
|
+
|
|
83
|
+
def test_read_missing_folder_errors_to_stderr(self) -> None:
|
|
84
|
+
stderr = StringIO()
|
|
85
|
+
with patch("sys.stderr", stderr):
|
|
86
|
+
code = main(["read", "--uid", "1"])
|
|
87
|
+
self.assertEqual(code, 1)
|
|
88
|
+
payload = json.loads(stderr.getvalue())
|
|
89
|
+
self.assertEqual(payload["error"]["code"], "invalid_request")
|
|
90
|
+
|
|
91
|
+
def test_attachments_missing_folder_errors_to_stderr(self) -> None:
|
|
92
|
+
stderr = StringIO()
|
|
93
|
+
with patch("sys.stderr", stderr):
|
|
94
|
+
code = main(["attachments"])
|
|
95
|
+
self.assertEqual(code, 1)
|
|
96
|
+
payload = json.loads(stderr.getvalue())
|
|
97
|
+
self.assertEqual(payload["error"]["code"], "invalid_request")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
unittest.main()
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import tempfile
|
|
5
|
-
import unittest
|
|
6
|
-
from io import StringIO
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from unittest.mock import patch
|
|
9
|
-
|
|
10
|
-
from tests import _bootstrap # noqa: F401
|
|
11
|
-
|
|
12
|
-
from imap_agent_cli.cli import build_parser, main
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class CliTests(unittest.TestCase):
|
|
16
|
-
def test_help_parser_has_expected_commands(self) -> None:
|
|
17
|
-
parser = build_parser()
|
|
18
|
-
parsed = parser.parse_args(["search", "--subject", "invoice"])
|
|
19
|
-
self.assertEqual(parsed.command, "search")
|
|
20
|
-
self.assertEqual(parsed.subject, "invoice")
|
|
21
|
-
|
|
22
|
-
def test_config_init_outputs_json(self) -> None:
|
|
23
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
24
|
-
config_path = Path(tmp) / "config.toml"
|
|
25
|
-
with patch("imap_agent_cli.cli.init_config", return_value=config_path):
|
|
26
|
-
stdout = StringIO()
|
|
27
|
-
with patch("sys.stdout", stdout):
|
|
28
|
-
code = main(["config", "init"])
|
|
29
|
-
self.assertEqual(code, 0)
|
|
30
|
-
payload = json.loads(stdout.getvalue())
|
|
31
|
-
self.assertTrue(payload["created"])
|
|
32
|
-
self.assertEqual(payload["path"], str(config_path))
|
|
33
|
-
|
|
34
|
-
def test_read_missing_folder_errors_to_stderr(self) -> None:
|
|
35
|
-
stderr = StringIO()
|
|
36
|
-
with patch("sys.stderr", stderr):
|
|
37
|
-
code = main(["read", "--uid", "1"])
|
|
38
|
-
self.assertEqual(code, 1)
|
|
39
|
-
payload = json.loads(stderr.getvalue())
|
|
40
|
-
self.assertEqual(payload["error"]["code"], "invalid_request")
|
|
41
|
-
|
|
42
|
-
def test_attachments_missing_folder_errors_to_stderr(self) -> None:
|
|
43
|
-
stderr = StringIO()
|
|
44
|
-
with patch("sys.stderr", stderr):
|
|
45
|
-
code = main(["attachments"])
|
|
46
|
-
self.assertEqual(code, 1)
|
|
47
|
-
payload = json.loads(stderr.getvalue())
|
|
48
|
-
self.assertEqual(payload["error"]["code"], "invalid_request")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if __name__ == "__main__":
|
|
52
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|