imap-agent-cli 0.1.1__tar.gz → 0.1.2__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.1 → imap_agent_cli-0.1.2}/PKG-INFO +27 -1
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/README.md +26 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/pyproject.toml +1 -1
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/__init__.py +1 -1
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/cli.py +20 -0
- imap_agent_cli-0.1.2/src/imap_agent_cli/skill.py +185 -0
- imap_agent_cli-0.1.2/tests/test_skill.py +76 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/.github/workflows/publish.yml +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/.gitignore +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/LICENSE +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/imap_agent_cli.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/spec.md +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/config.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/errors.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/imap_client.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/mime.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/models.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/render.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/src/imap_agent_cli/search.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/__init__.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/_bootstrap.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/pymap_server_runner.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/test_cli.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/test_config.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/test_mime.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/test_pymap_integration.py +0 -0
- {imap_agent_cli-0.1.1 → imap_agent_cli-0.1.2}/tests/test_search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imap-agent-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -104,6 +104,32 @@ Config path:
|
|
|
104
104
|
|
|
105
105
|
Secrets should stay in environment variables, not the config file.
|
|
106
106
|
|
|
107
|
+
## Codex Skill
|
|
108
|
+
|
|
109
|
+
Install or update the user-scoped Codex skill:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
imap-agent-cli install-skill
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This writes:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
~/.agents/skills/imap/SKILL.md
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Remove the managed skill:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
imap-agent-cli remove-skill
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use `--skills-dir PATH` to install into a nonstandard skills directory, for example:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
imap-agent-cli install-skill --skills-dir C:\Users\PaulEllis\.agents\skills
|
|
131
|
+
```
|
|
132
|
+
|
|
107
133
|
## Safety Boundary
|
|
108
134
|
|
|
109
135
|
Allowed:
|
|
@@ -71,6 +71,32 @@ Config path:
|
|
|
71
71
|
|
|
72
72
|
Secrets should stay in environment variables, not the config file.
|
|
73
73
|
|
|
74
|
+
## Codex Skill
|
|
75
|
+
|
|
76
|
+
Install or update the user-scoped Codex skill:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
imap-agent-cli install-skill
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This writes:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
~/.agents/skills/imap/SKILL.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Remove the managed skill:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
imap-agent-cli remove-skill
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use `--skills-dir PATH` to install into a nonstandard skills directory, for example:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
imap-agent-cli install-skill --skills-dir C:\Users\PaulEllis\.agents\skills
|
|
98
|
+
```
|
|
99
|
+
|
|
74
100
|
## Safety Boundary
|
|
75
101
|
|
|
76
102
|
Allowed:
|
|
@@ -12,6 +12,7 @@ from .errors import AppError, ConfigError
|
|
|
12
12
|
from .imap_client import ImapSession
|
|
13
13
|
from .mime import create_draft_message, header_value, parse_message
|
|
14
14
|
from .render import write_error, write_json
|
|
15
|
+
from .skill import install_skill, remove_skill
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _read_json_arg(value: str) -> dict[str, Any]:
|
|
@@ -138,6 +139,16 @@ def cmd_profiles(args: argparse.Namespace) -> int:
|
|
|
138
139
|
return 0
|
|
139
140
|
|
|
140
141
|
|
|
142
|
+
def cmd_install_skill(args: argparse.Namespace) -> int:
|
|
143
|
+
write_json(install_skill(Path(args.skills_dir) if args.skills_dir else None))
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_remove_skill(args: argparse.Namespace) -> int:
|
|
148
|
+
write_json(remove_skill(Path(args.skills_dir) if args.skills_dir else None, force=args.force))
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
141
152
|
def cmd_folders(args: argparse.Namespace) -> int:
|
|
142
153
|
with _session(args) as session:
|
|
143
154
|
write_json(session.folders())
|
|
@@ -308,6 +319,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
308
319
|
profiles = sub.add_parser("profiles")
|
|
309
320
|
profiles.set_defaults(func=cmd_profiles)
|
|
310
321
|
|
|
322
|
+
install_skill_parser = sub.add_parser("install-skill")
|
|
323
|
+
install_skill_parser.add_argument("--skills-dir")
|
|
324
|
+
install_skill_parser.set_defaults(func=cmd_install_skill)
|
|
325
|
+
|
|
326
|
+
remove_skill_parser = sub.add_parser("remove-skill")
|
|
327
|
+
remove_skill_parser.add_argument("--skills-dir")
|
|
328
|
+
remove_skill_parser.add_argument("--force", action="store_true")
|
|
329
|
+
remove_skill_parser.set_defaults(func=cmd_remove_skill)
|
|
330
|
+
|
|
311
331
|
folders = sub.add_parser("folders")
|
|
312
332
|
_common_profile_args(folders)
|
|
313
333
|
folders.set_defaults(func=cmd_folders)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import AppError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SKILL_NAME = "imap"
|
|
11
|
+
MANAGED_MARKER = "<!-- managed-by: imap-agent-cli -->"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SKILL_MD = f"""---
|
|
15
|
+
name: imap
|
|
16
|
+
description: Use when the user asks Codex to search, read, summarize, inspect, or draft email using the local imap-agent-cli tool. Covers safe IMAP-only email access, folder enumeration, attachment download, and draft creation; must not send, delete, move, archive, flag, label, or mark messages read/unread.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{MANAGED_MARKER}
|
|
20
|
+
|
|
21
|
+
# IMAP Email Access
|
|
22
|
+
|
|
23
|
+
Use `imap-agent-cli` for safe agentic email workflows over IMAP. The tool can search and read email, list folders, inspect/download attachments, and create drafts. It cannot send email and must not be used for destructive or metadata-mutating mailbox actions.
|
|
24
|
+
|
|
25
|
+
## Safety Rules
|
|
26
|
+
|
|
27
|
+
- Never send email. There is no SMTP support.
|
|
28
|
+
- Never delete, move, archive, label, flag, star, mark read, or mark unread messages.
|
|
29
|
+
- Prefer metadata/search before reading message bodies.
|
|
30
|
+
- Do not print large or sensitive email bodies unless the user asks to see the content.
|
|
31
|
+
- Use `--body-format metadata` when you only need headers and attachment metadata.
|
|
32
|
+
- Use `--body-format plain` for summaries unless HTML structure matters.
|
|
33
|
+
- Download attachments only when the user asks and provides or accepts an output directory.
|
|
34
|
+
- Create drafts only with `draft create` or `draft reply`.
|
|
35
|
+
|
|
36
|
+
## Command Form
|
|
37
|
+
|
|
38
|
+
Prefer the published CLI:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
$env:UV_LINK_MODE = "copy"
|
|
42
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli <command>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If working inside the `imap-agent-cli` repository, this local form is also valid:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
uv run ./imap_agent_cli.py <command>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The CLI uses these environment variables for the default profile:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
IMAP_AGENT_CLI_HOST
|
|
55
|
+
IMAP_AGENT_CLI_PORT
|
|
56
|
+
IMAP_AGENT_CLI_USERNAME
|
|
57
|
+
IMAP_AGENT_CLI_PASSWORD
|
|
58
|
+
IMAP_AGENT_CLI_TLS
|
|
59
|
+
IMAP_AGENT_CLI_DRAFTS_FOLDER
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Folder Listing
|
|
63
|
+
|
|
64
|
+
Use this to discover folder names and Drafts naming:
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli folders
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Return only relevant folder names/counts to the user unless they ask for the full list.
|
|
71
|
+
|
|
72
|
+
## Search
|
|
73
|
+
|
|
74
|
+
Search defaults to `INBOX` when no folder is provided:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --folder INBOX --max-results 10
|
|
78
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --folder INBOX --subject "invoice" --max-results 10
|
|
79
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --folder INBOX --from "Justin" --max-results 10
|
|
80
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --folder INBOX --since 2026-01-01 --before 2026-02-01 --max-results 10
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If a server-side `--from` search times out, enumerate recent messages with a bounded `--max-results` and filter locally by the returned `from` metadata.
|
|
84
|
+
|
|
85
|
+
For child folders:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --folder Projects --recursive --max-results 25
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For all folders, use sparingly because large mailboxes can be slow:
|
|
92
|
+
|
|
93
|
+
```powershell
|
|
94
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli search --all-folders --subject "contract" --max-results 25
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Read
|
|
98
|
+
|
|
99
|
+
Read a message by folder and UID from search results:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli read --folder INBOX --uid 12345 --body-format metadata
|
|
103
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli read --folder INBOX --uid 12345 --body-format plain --max-body-chars 12000
|
|
104
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli read --folder INBOX --uid 12345 --body-format html
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `metadata` first when confirming identity, attachments, or headers. Use `plain` when summarizing. Use `html` only when formatting or links matter.
|
|
108
|
+
|
|
109
|
+
## Attachments
|
|
110
|
+
|
|
111
|
+
List attachment metadata:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli attachments --folder INBOX --uid 12345
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Download requires an explicit output directory:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli attachments download --folder INBOX --uid 12345 --part-id 1 --output-dir C:\\tmp\\email-attachments
|
|
121
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli attachments download --folder INBOX --uid 12345 --all --output-dir C:\\tmp\\email-attachments
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Report saved paths to the user.
|
|
125
|
+
|
|
126
|
+
## Drafts
|
|
127
|
+
|
|
128
|
+
Create a new draft:
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli draft create --to person@example.com --subject "Subject" --body "Draft body"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Create a reply draft from an existing message:
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
uvx --refresh-package imap-agent-cli imap-agent-cli draft reply --folder INBOX --uid 12345 --body "Draft reply body"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For longer draft bodies, write a temporary body file and pass `--body-file`. Tell the user a draft was created; do not imply it was sent.
|
|
141
|
+
|
|
142
|
+
## Output Handling
|
|
143
|
+
|
|
144
|
+
All CLI payloads are JSON on stdout. Diagnostics and errors are on stderr. Parse JSON before deciding what to show the user, and summarize high-signal fields instead of dumping raw output.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def default_skills_dir() -> Path:
|
|
149
|
+
return Path.home() / ".agents" / "skills"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def skill_dir(skills_dir: Path | None = None) -> Path:
|
|
153
|
+
return (skills_dir or default_skills_dir()) / SKILL_NAME
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def install_skill(skills_dir: Path | None = None) -> dict[str, Any]:
|
|
157
|
+
target = skill_dir(skills_dir)
|
|
158
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
skill_path = target / "SKILL.md"
|
|
160
|
+
previous = skill_path.read_text(encoding="utf-8") if skill_path.exists() else ""
|
|
161
|
+
updated = previous != SKILL_MD
|
|
162
|
+
skill_path.write_text(SKILL_MD, encoding="utf-8")
|
|
163
|
+
return {
|
|
164
|
+
"installed": True,
|
|
165
|
+
"updated": updated,
|
|
166
|
+
"skill": SKILL_NAME,
|
|
167
|
+
"path": str(skill_path),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def remove_skill(skills_dir: Path | None = None, *, force: bool = False) -> dict[str, Any]:
|
|
172
|
+
target = skill_dir(skills_dir)
|
|
173
|
+
skill_path = target / "SKILL.md"
|
|
174
|
+
if not target.exists():
|
|
175
|
+
return {"removed": False, "skill": SKILL_NAME, "path": str(target), "reason": "not_installed"}
|
|
176
|
+
if not skill_path.exists():
|
|
177
|
+
raise AppError("invalid_request", f"refusing to remove '{target}' because SKILL.md is missing.")
|
|
178
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
179
|
+
if MANAGED_MARKER not in content and not force:
|
|
180
|
+
raise AppError(
|
|
181
|
+
"invalid_request",
|
|
182
|
+
f"refusing to remove '{target}' because it is not marked as managed by imap-agent-cli; use --force to override.",
|
|
183
|
+
)
|
|
184
|
+
shutil.rmtree(target)
|
|
185
|
+
return {"removed": True, "skill": SKILL_NAME, "path": str(target)}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 main
|
|
13
|
+
from imap_agent_cli.errors import AppError
|
|
14
|
+
from imap_agent_cli.skill import MANAGED_MARKER, install_skill, remove_skill
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkillTests(unittest.TestCase):
|
|
18
|
+
def test_install_skill_creates_and_updates_managed_skill(self) -> None:
|
|
19
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
20
|
+
root = Path(tmp)
|
|
21
|
+
first = install_skill(root)
|
|
22
|
+
self.assertTrue(first["installed"])
|
|
23
|
+
self.assertTrue(first["updated"])
|
|
24
|
+
skill_path = root / "imap" / "SKILL.md"
|
|
25
|
+
self.assertTrue(skill_path.exists())
|
|
26
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
27
|
+
self.assertIn("name: imap", content)
|
|
28
|
+
self.assertIn(MANAGED_MARKER, content)
|
|
29
|
+
self.assertIn("draft reply", content)
|
|
30
|
+
|
|
31
|
+
second = install_skill(root)
|
|
32
|
+
self.assertTrue(second["installed"])
|
|
33
|
+
self.assertFalse(second["updated"])
|
|
34
|
+
|
|
35
|
+
def test_remove_skill_removes_only_managed_skill(self) -> None:
|
|
36
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
37
|
+
root = Path(tmp)
|
|
38
|
+
install_skill(root)
|
|
39
|
+
removed = remove_skill(root)
|
|
40
|
+
self.assertTrue(removed["removed"])
|
|
41
|
+
self.assertFalse((root / "imap").exists())
|
|
42
|
+
|
|
43
|
+
def test_remove_skill_refuses_unmanaged_skill_without_force(self) -> None:
|
|
44
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
45
|
+
root = Path(tmp)
|
|
46
|
+
skill_dir = root / "imap"
|
|
47
|
+
skill_dir.mkdir(parents=True)
|
|
48
|
+
(skill_dir / "SKILL.md").write_text("---\nname: imap\n---\ncustom\n", encoding="utf-8")
|
|
49
|
+
with self.assertRaises(AppError):
|
|
50
|
+
remove_skill(root)
|
|
51
|
+
removed = remove_skill(root, force=True)
|
|
52
|
+
self.assertTrue(removed["removed"])
|
|
53
|
+
|
|
54
|
+
def test_cli_install_skill_outputs_json(self) -> None:
|
|
55
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
56
|
+
stdout = StringIO()
|
|
57
|
+
with patch("sys.stdout", stdout):
|
|
58
|
+
code = main(["install-skill", "--skills-dir", tmp])
|
|
59
|
+
self.assertEqual(code, 0)
|
|
60
|
+
payload = json.loads(stdout.getvalue())
|
|
61
|
+
self.assertEqual(payload["skill"], "imap")
|
|
62
|
+
self.assertTrue((Path(tmp) / "imap" / "SKILL.md").exists())
|
|
63
|
+
|
|
64
|
+
def test_cli_remove_skill_outputs_json(self) -> None:
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
66
|
+
install_skill(Path(tmp))
|
|
67
|
+
stdout = StringIO()
|
|
68
|
+
with patch("sys.stdout", stdout):
|
|
69
|
+
code = main(["remove-skill", "--skills-dir", tmp])
|
|
70
|
+
self.assertEqual(code, 0)
|
|
71
|
+
payload = json.loads(stdout.getvalue())
|
|
72
|
+
self.assertTrue(payload["removed"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
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
|