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.
Files changed (29) hide show
  1. imap_agent_cli-0.1.4/AGENTS.md +88 -0
  2. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/PKG-INFO +88 -40
  3. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/README.md +87 -39
  4. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/pyproject.toml +1 -1
  5. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/__init__.py +1 -1
  6. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/cli.py +96 -19
  7. imap_agent_cli-0.1.4/tests/test_cli.py +101 -0
  8. imap_agent_cli-0.1.3/tests/test_cli.py +0 -52
  9. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/.github/workflows/publish.yml +0 -0
  10. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/.gitignore +0 -0
  11. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/LICENSE +0 -0
  12. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/imap_agent_cli.py +0 -0
  13. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/spec.md +0 -0
  14. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/config.py +0 -0
  15. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/errors.py +0 -0
  16. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/imap_client.py +0 -0
  17. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/mime.py +0 -0
  18. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/models.py +0 -0
  19. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/render.py +0 -0
  20. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/search.py +0 -0
  21. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/src/imap_agent_cli/skill.py +0 -0
  22. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/__init__.py +0 -0
  23. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/_bootstrap.py +0 -0
  24. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/pymap_server_runner.py +0 -0
  25. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_config.py +0 -0
  26. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_mime.py +0 -0
  27. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_pymap_integration.py +0 -0
  28. {imap_agent_cli-0.1.3 → imap_agent_cli-0.1.4}/tests/test_search.py +0 -0
  29. {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
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
- ## Status
40
+ ## Using imap-agent-cli
41
41
 
42
- This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
42
+ ### Quick Start
43
+
44
+ Install or update the `imap` agent skill:
43
45
 
44
- ## Quick Start
46
+ ```text
47
+ uvx imap-agent-cli install-skill
48
+ ```
45
49
 
46
- Single-profile env-var setup:
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
- Local development:
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
- uv run ./imap_agent_cli.py --help
60
- uv run ./imap_agent_cli.py folders
61
- uv run ./imap_agent_cli.py search --subject invoice
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
- Packaged execution:
72
+ Install it as a persistent `uv` tool when you want to call `imap-agent-cli` directly:
65
73
 
66
74
  ```text
67
- uvx imap-agent-cli --help
75
+ uv tool install imap-agent-cli
76
+ uv tool update-shell
77
+ imap-agent-cli --help
68
78
  ```
69
79
 
70
- ## Examples
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 folders
74
- imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
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
- ## Config
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
- Create a starter config:
95
+ For a single account, environment variables are enough:
87
96
 
88
97
  ```text
89
- imap-agent-cli config init
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
- Add a named profile:
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
- Config path:
116
+ The config file is stored at:
100
117
 
101
118
  ```text
102
119
  ~/.imap-agent-cli/config.toml
103
120
  ```
104
121
 
105
- Secrets should stay in environment variables, not the config file.
122
+ Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
106
123
 
107
- ## Agent Skill
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, for example:
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
- ## Safety Boundary
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
- ## Testing
172
+ ### Example Commands
154
173
 
155
- No-network unit tests:
174
+ These examples are mostly useful for validating configuration or debugging what an agentic tool is doing:
156
175
 
157
176
  ```text
158
- python -m unittest discover -v
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
- Package build:
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
- The spec targets `pymap` for future local IMAP integration tests on Windows without Docker.
204
+ ### Testing
168
205
 
169
- Opt-in local IMAP integration test:
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
- ## Publishing
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
- ## Status
7
+ ## Using imap-agent-cli
8
8
 
9
- This repo is under active development. The behavior target is defined in [`spec.md`](./spec.md).
9
+ ### Quick Start
10
+
11
+ Install or update the `imap` agent skill:
10
12
 
11
- ## Quick Start
13
+ ```text
14
+ uvx imap-agent-cli install-skill
15
+ ```
12
16
 
13
- Single-profile env-var setup:
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
- Local development:
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
- uv run ./imap_agent_cli.py --help
27
- uv run ./imap_agent_cli.py folders
28
- uv run ./imap_agent_cli.py search --subject invoice
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
- Packaged execution:
39
+ Install it as a persistent `uv` tool when you want to call `imap-agent-cli` directly:
32
40
 
33
41
  ```text
34
- uvx imap-agent-cli --help
42
+ uv tool install imap-agent-cli
43
+ uv tool update-shell
44
+ imap-agent-cli --help
35
45
  ```
36
46
 
37
- ## Examples
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 folders
41
- imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
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
- ## Config
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
- Create a starter config:
62
+ For a single account, environment variables are enough:
54
63
 
55
64
  ```text
56
- imap-agent-cli config init
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
- Add a named profile:
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
- Config path:
83
+ The config file is stored at:
67
84
 
68
85
  ```text
69
86
  ~/.imap-agent-cli/config.toml
70
87
  ```
71
88
 
72
- Secrets should stay in environment variables, not the config file.
89
+ Keep secrets in environment variables. The config file should reference password environment variable names, not contain passwords.
73
90
 
74
- ## Agent Skill
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, for example:
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
- ## Safety Boundary
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
- ## Testing
139
+ ### Example Commands
121
140
 
122
- No-network unit tests:
141
+ These examples are mostly useful for validating configuration or debugging what an agentic tool is doing:
123
142
 
124
143
  ```text
125
- python -m unittest discover -v
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
- Package build:
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
- The spec targets `pymap` for future local IMAP integration tests on Windows without Docker.
171
+ ### Testing
135
172
 
136
- Opt-in local IMAP integration test:
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
- ## Publishing
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "imap-agent-cli"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Agent-first IMAP CLI for safe email search, read, attachment download, and draft creation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """Agent-first IMAP CLI."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.4"
@@ -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(prog="imap-agent-cli")
291
- parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
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