graphmail 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. graphmail-1.0.0/.gitignore +13 -0
  2. graphmail-1.0.0/LICENSE +21 -0
  3. graphmail-1.0.0/PKG-INFO +202 -0
  4. graphmail-1.0.0/README.md +157 -0
  5. graphmail-1.0.0/pyproject.toml +126 -0
  6. graphmail-1.0.0/src/graphmail/__init__.py +3 -0
  7. graphmail-1.0.0/src/graphmail/auth.py +411 -0
  8. graphmail-1.0.0/src/graphmail/cli.py +461 -0
  9. graphmail-1.0.0/src/graphmail/envfile.py +46 -0
  10. graphmail-1.0.0/src/graphmail/export.py +193 -0
  11. graphmail-1.0.0/src/graphmail/graph.py +318 -0
  12. graphmail-1.0.0/src/graphmail/importer.py +284 -0
  13. graphmail-1.0.0/src/graphmail/index.py +217 -0
  14. graphmail-1.0.0/src/graphmail/lock.py +132 -0
  15. graphmail-1.0.0/src/graphmail/pipeline.py +241 -0
  16. graphmail-1.0.0/src/graphmail/py.typed +0 -0
  17. graphmail-1.0.0/src/graphmail/runlog.py +123 -0
  18. graphmail-1.0.0/src/graphmail/text.py +42 -0
  19. graphmail-1.0.0/tests/conftest.py +28 -0
  20. graphmail-1.0.0/tests/fixtures/folder_inbox.json +8 -0
  21. graphmail-1.0.0/tests/fixtures/messages_page1.json +30 -0
  22. graphmail-1.0.0/tests/fixtures/messages_page2.json +16 -0
  23. graphmail-1.0.0/tests/fixtures/oauth_token_response.json +8 -0
  24. graphmail-1.0.0/tests/helpers.py +14 -0
  25. graphmail-1.0.0/tests/mockgraph.py +532 -0
  26. graphmail-1.0.0/tests/spec/examples.json +198 -0
  27. graphmail-1.0.0/tests/spec/graph_mail_v1.json +3068 -0
  28. graphmail-1.0.0/tests/test_auth.py +547 -0
  29. graphmail-1.0.0/tests/test_cli.py +655 -0
  30. graphmail-1.0.0/tests/test_e2e_local.py +232 -0
  31. graphmail-1.0.0/tests/test_envfile.py +73 -0
  32. graphmail-1.0.0/tests/test_export.py +272 -0
  33. graphmail-1.0.0/tests/test_helpers.py +114 -0
  34. graphmail-1.0.0/tests/test_importer.py +516 -0
  35. graphmail-1.0.0/tests/test_index.py +193 -0
  36. graphmail-1.0.0/tests/test_lock.py +230 -0
  37. graphmail-1.0.0/tests/test_mime.py +167 -0
  38. graphmail-1.0.0/tests/test_pagination.py +252 -0
  39. graphmail-1.0.0/tests/test_pipeline.py +370 -0
  40. graphmail-1.0.0/tests/test_runlog.py +156 -0
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ .cache/
3
+ build/
4
+ dist/
5
+ logs/
6
+ archive/
7
+ index.db
8
+ token.json
9
+ email_index.xlsx
10
+ __pycache__/
11
+ *.py[cod]
12
+ *.egg-info/
13
+ *.env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 graphmail contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: graphmail
3
+ Version: 1.0.0
4
+ Summary: Extract emails from Microsoft Graph and local .eml files into a local archive, with an Excel index.
5
+ Project-URL: Homepage, https://github.com/off-signal/graphmail
6
+ Author: graphmail contributors
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: archive,email,eml,microsoft-graph,outlook
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Communications :: Email
22
+ Classifier: Topic :: System :: Archiving
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: beautifulsoup4>=4.12
26
+ Requires-Dist: openpyxl>=3.1
27
+ Requires-Dist: requests>=2.31
28
+ Requires-Dist: tqdm>=4.66
29
+ Provides-Extra: dev
30
+ Requires-Dist: coverage>=7; extra == 'dev'
31
+ Requires-Dist: jsonschema>=4; extra == 'dev'
32
+ Requires-Dist: mypy>=1.11; extra == 'dev'
33
+ Requires-Dist: pytest-cov; extra == 'dev'
34
+ Requires-Dist: pytest>=8; extra == 'dev'
35
+ Requires-Dist: pyyaml>=6; extra == 'dev'
36
+ Requires-Dist: responses>=0.25; extra == 'dev'
37
+ Requires-Dist: ruff>=0.6; extra == 'dev'
38
+ Requires-Dist: types-beautifulsoup4; extra == 'dev'
39
+ Requires-Dist: types-jsonschema; extra == 'dev'
40
+ Requires-Dist: types-openpyxl; extra == 'dev'
41
+ Requires-Dist: types-pyyaml; extra == 'dev'
42
+ Requires-Dist: types-requests; extra == 'dev'
43
+ Requires-Dist: types-tqdm; extra == 'dev'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Graphmail
47
+
48
+ Archive your Outlook/Microsoft 365 mail into files you own.
49
+
50
+ `graphmail` pulls messages from Microsoft Graph (or imports local `.eml` files) and writes each one as a plain `.eml` file into a folder tree, indexed in SQLite, with an optional Excel workbook.
51
+
52
+ - `.eml` files in folders, openable in any mail client.
53
+ - The index and workbook are rebuilt on demand from the `.eml` tree, so the files are the only source of truth.
54
+ - Excel hyperlinks use relative paths, so the workbook keeps working as long as you move or sync the workbook and `archive/` together. Keep them side by side (links are resolved relative to the workbook's own folder, so moving only one of the two breaks them).
55
+
56
+ ## Requirements
57
+
58
+ - Python ≥ 3.10
59
+ - Any OS (pure Python)
60
+ - A Microsoft 365 / Outlook account and an Azure/Entra app registration
61
+ - [uv](https://docs.astral.sh/uv/) (if you're developing graphmail; see [Development](#development))
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install graphmail
67
+ ```
68
+
69
+ ## Quick start
70
+
71
+ One-time setup:
72
+
73
+ 1. **Register an app** in the [Entra/Azure admin center](https://entra.microsoft.com/):
74
+ - **Authentication:** add platform [*Mobile and desktop apps*](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-app-configuration) -> add redirect URI `http://localhost` ([the port is ignored](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url); this enables browser login).
75
+ - Optionally also add `https://login.microsoftonline.com/common/oauth2/nativeclient` if you want the `--manual` paste fallback.
76
+ - Leave it as a **public client** (no secret needed for desktop use).
77
+ - **API permissions** -> Microsoft Graph delegated: [`Mail.Read`](https://learn.microsoft.com/en-us/graph/permissions-reference), `User.Read`, `offline_access`. Admin-consent if your tenant requires it.
78
+
79
+ 2. **Create a credentials file** (chmod 600):
80
+
81
+ ```bash
82
+ # ~/.config/graphmail.env
83
+ GRAPHMAIL_CLIENT_ID=your-app-id
84
+ GRAPHMAIL_TENANT_ID=common # or your tenant id
85
+ # GRAPHMAIL_CLIENT_SECRET=... # only for confidential clients
86
+ ```
87
+
88
+ Pass `--env-file ~/.config/graphmail.env` to any command. Real environment variables still win if set.
89
+
90
+ 3. **Log in** (opens your browser; the redirect is caught automatically):
91
+
92
+ ```bash
93
+ graphmail --env-file ~/.config/graphmail.env login
94
+ ```
95
+
96
+ Then archive mail:
97
+
98
+ ```bash
99
+ graphmail fetch --folders Inbox
100
+ graphmail fetch --folders all --recursive
101
+ graphmail fetch --folders Inbox --sender someone@example.com
102
+
103
+ graphmail import ~/old-mail
104
+
105
+ graphmail export -o email_index.xlsx --group-by folder
106
+ ```
107
+
108
+ ## Features
109
+
110
+ - `fetch` pulls messages from Graph folders. `--folders` takes display names (case-insensitive), raw Graph folder ids, or `all`. `--recursive` walks subfolders. `--sender` filters by address. Re-running resumes where it left off and skips anything already archived.
111
+ - `import` copies a directory of local `.eml` files into the archive and indexes them.
112
+ - `reindex` rebuilds `index.db` from `archive/`'s own `.eml` files (moves/renames are picked up; Graph-fetched rows are untouched).
113
+ - `export` generates an Excel workbook from the index, grouped by folder, year, or as one big sheet.
114
+ - `login` runs the OAuth flow or just refreshes the cached token, no fetching.
115
+
116
+ Behaviour worth knowing:
117
+
118
+ - **Ctrl-C** finishes the current message and stops; a second Ctrl-C aborts immediately.
119
+ - **Resume:** interrupted fetches pick up where they stopped. Failed messages are retried next run; `fetch` exits non-zero if any failed.
120
+ - **Throttling:** Graph 429 / `Retry-After` is respected automatically.
121
+ - **Dedup:** exact duplicates (same received time + content) are skipped.
122
+
123
+ ## A note on stability
124
+
125
+ This tool was refactored from a handful of battle-tested one-off scripts I created last year. The core behaviours were pinned with characterization tests *before* the refactor, so the everyday fetch/dedup/resume/export paths are safe to use.
126
+
127
+ What's new since the refactor (i.e. not battle-tested the same way):
128
+
129
+ - System-browser login with PKCE (the old scripts only had the paste-the-code flow)
130
+ - SQLite index (`index.db`) replacing ad-hoc JSON caches
131
+ - `import` and `reindex` commands for local `.eml` files
132
+ - Single-job lock so overlapping cron runs can't corrupt the archive
133
+ - Per-run log files under `archive/logs/`
134
+ - `--env-file` for unattended jobs
135
+ - Excel export with relative, portable hyperlinks
136
+ - Cross-platform polish
137
+
138
+ If you hit a bug or a quirk, [open an issue](https://github.com/off-signal/graphmail/issues).
139
+
140
+ ## Cron
141
+
142
+ Mutating commands (`fetch`, `import`, `reindex`) are safe unattended:
143
+
144
+ - Single-job lock: a second run exits immediately with code **2** (vs **1** = "ran but some mail failed"). Stale locks from crashed processes are cleaned up.
145
+ - Run log per job under `archive/logs/<command>-<timestamp>.log`, with `archive/logs/latest-<command>.log` pointing at the most recent run of that command.
146
+ - `--quiet` silences progress but keeps the one-line `=== done ===` summary on stdout (cron `MAILTO` stays useful).
147
+ - Token refresh has a network timeout; a stalled endpoint can't hang the job forever.
148
+
149
+ Log in interactively **once** to seed `token.json`, then let cron use the refresh token from there on.
150
+
151
+ ```cron
152
+ SHELL=/bin/sh
153
+ 15 3 * * * cd /data/mail-archive && \
154
+ graphmail --env-file /etc/graphmail.env --quiet fetch --folders all \
155
+ >> /data/mail-archive/cron.out 2>&1
156
+ ```
157
+
158
+ ## Exit codes
159
+
160
+ | Code | Meaning |
161
+ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | 0 | success |
163
+ | 1 | failed: `fetch` had one or more failed messages (retried next run); `import`/`reindex` failed on every file; or a setup error (auth, missing source/index) |
164
+ | 2 | another graphmail job already holds the lock |
165
+ | 130 | aborted (Ctrl-C) |
166
+
167
+ Note the asymmetry: `fetch` exits `1` if *anything* failed, but `import` and `reindex` only exit `1` when *nothing* was imported, a partially successful run still exits `0` on reindex.
168
+
169
+ ## Files on disk
170
+
171
+ Everything below is relative to the directory you run graphmail from. (cd into your mail directory first)
172
+
173
+ | Path | What |
174
+ | ---------------------------------------- | --------------------------------------------------------------------------------------- |
175
+ | `archive/<Folder>/*.eml` | the archive; fetch uses the Outlook folder name, import uses `Imported_<UTC timestamp>` |
176
+ | `archive/logs/<command>-<timestamp>.log` | one log per run; the filename says which command |
177
+ | `archive/logs/latest-<command>.log` | pointer to that command's most recent run |
178
+ | `archive/.graphmail.lock` | single-job lock; exists only while a job runs |
179
+ | `index.db` | SQLite index (rebuild with `reindex`) |
180
+ | `token.json` | cached OAuth token (keep private) |
181
+ | `email_index.xlsx` | Excel view, regenerated on demand |
182
+
183
+ ## Development
184
+
185
+ Clone and sync the dev tooling (the `dev` extra; requires [uv](https://docs.astral.sh/uv/)):
186
+
187
+ ```bash
188
+ git clone https://github.com/off-signal/graphmail && cd graphmail
189
+ uv sync --locked --extra dev
190
+ ```
191
+
192
+ The same checks CI runs:
193
+
194
+ ```bash
195
+ uv run ruff check src tests tools
196
+ uv run mypy
197
+ uv run pytest --cov=graphmail --cov-report=term --cov-fail-under=90
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT; see [LICENSE](https://raw.githubusercontent.com/off-signal/graphmail/main/LICENSE).
@@ -0,0 +1,157 @@
1
+ # Graphmail
2
+
3
+ Archive your Outlook/Microsoft 365 mail into files you own.
4
+
5
+ `graphmail` pulls messages from Microsoft Graph (or imports local `.eml` files) and writes each one as a plain `.eml` file into a folder tree, indexed in SQLite, with an optional Excel workbook.
6
+
7
+ - `.eml` files in folders, openable in any mail client.
8
+ - The index and workbook are rebuilt on demand from the `.eml` tree, so the files are the only source of truth.
9
+ - Excel hyperlinks use relative paths, so the workbook keeps working as long as you move or sync the workbook and `archive/` together. Keep them side by side (links are resolved relative to the workbook's own folder, so moving only one of the two breaks them).
10
+
11
+ ## Requirements
12
+
13
+ - Python ≥ 3.10
14
+ - Any OS (pure Python)
15
+ - A Microsoft 365 / Outlook account and an Azure/Entra app registration
16
+ - [uv](https://docs.astral.sh/uv/) (if you're developing graphmail; see [Development](#development))
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install graphmail
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ One-time setup:
27
+
28
+ 1. **Register an app** in the [Entra/Azure admin center](https://entra.microsoft.com/):
29
+ - **Authentication:** add platform [*Mobile and desktop apps*](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-app-configuration) -> add redirect URI `http://localhost` ([the port is ignored](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url); this enables browser login).
30
+ - Optionally also add `https://login.microsoftonline.com/common/oauth2/nativeclient` if you want the `--manual` paste fallback.
31
+ - Leave it as a **public client** (no secret needed for desktop use).
32
+ - **API permissions** -> Microsoft Graph delegated: [`Mail.Read`](https://learn.microsoft.com/en-us/graph/permissions-reference), `User.Read`, `offline_access`. Admin-consent if your tenant requires it.
33
+
34
+ 2. **Create a credentials file** (chmod 600):
35
+
36
+ ```bash
37
+ # ~/.config/graphmail.env
38
+ GRAPHMAIL_CLIENT_ID=your-app-id
39
+ GRAPHMAIL_TENANT_ID=common # or your tenant id
40
+ # GRAPHMAIL_CLIENT_SECRET=... # only for confidential clients
41
+ ```
42
+
43
+ Pass `--env-file ~/.config/graphmail.env` to any command. Real environment variables still win if set.
44
+
45
+ 3. **Log in** (opens your browser; the redirect is caught automatically):
46
+
47
+ ```bash
48
+ graphmail --env-file ~/.config/graphmail.env login
49
+ ```
50
+
51
+ Then archive mail:
52
+
53
+ ```bash
54
+ graphmail fetch --folders Inbox
55
+ graphmail fetch --folders all --recursive
56
+ graphmail fetch --folders Inbox --sender someone@example.com
57
+
58
+ graphmail import ~/old-mail
59
+
60
+ graphmail export -o email_index.xlsx --group-by folder
61
+ ```
62
+
63
+ ## Features
64
+
65
+ - `fetch` pulls messages from Graph folders. `--folders` takes display names (case-insensitive), raw Graph folder ids, or `all`. `--recursive` walks subfolders. `--sender` filters by address. Re-running resumes where it left off and skips anything already archived.
66
+ - `import` copies a directory of local `.eml` files into the archive and indexes them.
67
+ - `reindex` rebuilds `index.db` from `archive/`'s own `.eml` files (moves/renames are picked up; Graph-fetched rows are untouched).
68
+ - `export` generates an Excel workbook from the index, grouped by folder, year, or as one big sheet.
69
+ - `login` runs the OAuth flow or just refreshes the cached token, no fetching.
70
+
71
+ Behaviour worth knowing:
72
+
73
+ - **Ctrl-C** finishes the current message and stops; a second Ctrl-C aborts immediately.
74
+ - **Resume:** interrupted fetches pick up where they stopped. Failed messages are retried next run; `fetch` exits non-zero if any failed.
75
+ - **Throttling:** Graph 429 / `Retry-After` is respected automatically.
76
+ - **Dedup:** exact duplicates (same received time + content) are skipped.
77
+
78
+ ## A note on stability
79
+
80
+ This tool was refactored from a handful of battle-tested one-off scripts I created last year. The core behaviours were pinned with characterization tests *before* the refactor, so the everyday fetch/dedup/resume/export paths are safe to use.
81
+
82
+ What's new since the refactor (i.e. not battle-tested the same way):
83
+
84
+ - System-browser login with PKCE (the old scripts only had the paste-the-code flow)
85
+ - SQLite index (`index.db`) replacing ad-hoc JSON caches
86
+ - `import` and `reindex` commands for local `.eml` files
87
+ - Single-job lock so overlapping cron runs can't corrupt the archive
88
+ - Per-run log files under `archive/logs/`
89
+ - `--env-file` for unattended jobs
90
+ - Excel export with relative, portable hyperlinks
91
+ - Cross-platform polish
92
+
93
+ If you hit a bug or a quirk, [open an issue](https://github.com/off-signal/graphmail/issues).
94
+
95
+ ## Cron
96
+
97
+ Mutating commands (`fetch`, `import`, `reindex`) are safe unattended:
98
+
99
+ - Single-job lock: a second run exits immediately with code **2** (vs **1** = "ran but some mail failed"). Stale locks from crashed processes are cleaned up.
100
+ - Run log per job under `archive/logs/<command>-<timestamp>.log`, with `archive/logs/latest-<command>.log` pointing at the most recent run of that command.
101
+ - `--quiet` silences progress but keeps the one-line `=== done ===` summary on stdout (cron `MAILTO` stays useful).
102
+ - Token refresh has a network timeout; a stalled endpoint can't hang the job forever.
103
+
104
+ Log in interactively **once** to seed `token.json`, then let cron use the refresh token from there on.
105
+
106
+ ```cron
107
+ SHELL=/bin/sh
108
+ 15 3 * * * cd /data/mail-archive && \
109
+ graphmail --env-file /etc/graphmail.env --quiet fetch --folders all \
110
+ >> /data/mail-archive/cron.out 2>&1
111
+ ```
112
+
113
+ ## Exit codes
114
+
115
+ | Code | Meaning |
116
+ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | 0 | success |
118
+ | 1 | failed: `fetch` had one or more failed messages (retried next run); `import`/`reindex` failed on every file; or a setup error (auth, missing source/index) |
119
+ | 2 | another graphmail job already holds the lock |
120
+ | 130 | aborted (Ctrl-C) |
121
+
122
+ Note the asymmetry: `fetch` exits `1` if *anything* failed, but `import` and `reindex` only exit `1` when *nothing* was imported, a partially successful run still exits `0` on reindex.
123
+
124
+ ## Files on disk
125
+
126
+ Everything below is relative to the directory you run graphmail from. (cd into your mail directory first)
127
+
128
+ | Path | What |
129
+ | ---------------------------------------- | --------------------------------------------------------------------------------------- |
130
+ | `archive/<Folder>/*.eml` | the archive; fetch uses the Outlook folder name, import uses `Imported_<UTC timestamp>` |
131
+ | `archive/logs/<command>-<timestamp>.log` | one log per run; the filename says which command |
132
+ | `archive/logs/latest-<command>.log` | pointer to that command's most recent run |
133
+ | `archive/.graphmail.lock` | single-job lock; exists only while a job runs |
134
+ | `index.db` | SQLite index (rebuild with `reindex`) |
135
+ | `token.json` | cached OAuth token (keep private) |
136
+ | `email_index.xlsx` | Excel view, regenerated on demand |
137
+
138
+ ## Development
139
+
140
+ Clone and sync the dev tooling (the `dev` extra; requires [uv](https://docs.astral.sh/uv/)):
141
+
142
+ ```bash
143
+ git clone https://github.com/off-signal/graphmail && cd graphmail
144
+ uv sync --locked --extra dev
145
+ ```
146
+
147
+ The same checks CI runs:
148
+
149
+ ```bash
150
+ uv run ruff check src tests tools
151
+ uv run mypy
152
+ uv run pytest --cov=graphmail --cov-report=term --cov-fail-under=90
153
+ ```
154
+
155
+ ## License
156
+
157
+ MIT; see [LICENSE](https://raw.githubusercontent.com/off-signal/graphmail/main/LICENSE).
@@ -0,0 +1,126 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "graphmail"
7
+ version = "1.0.0"
8
+ description = "Extract emails from Microsoft Graph and local .eml files into a local archive, with an Excel index."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "graphmail contributors" }]
13
+ keywords = ["microsoft-graph", "outlook", "email", "archive", "eml"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Communications :: Email",
27
+ "Topic :: System :: Archiving",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = [
31
+ "requests>=2.31",
32
+ "openpyxl>=3.1",
33
+ "beautifulsoup4>=4.12",
34
+ "tqdm>=4.66",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=8",
40
+ "pytest-cov",
41
+ "coverage>=7",
42
+ "responses>=0.25",
43
+ "ruff>=0.6",
44
+ "mypy>=1.11",
45
+ "types-requests",
46
+ "types-beautifulsoup4",
47
+ "types-openpyxl",
48
+ "types-tqdm",
49
+ "pyyaml>=6",
50
+ "types-PyYAML",
51
+ "jsonschema>=4",
52
+ "types-jsonschema",
53
+ ]
54
+
55
+ [project.urls]
56
+ Homepage = "https://github.com/off-signal/graphmail"
57
+
58
+ [project.scripts]
59
+ graphmail = "graphmail.cli:main"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["src/graphmail"]
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
66
+ exclude = ["**/__pycache__"]
67
+
68
+ # ruff
69
+ [tool.ruff]
70
+ line-length = 100
71
+ target-version = "py310"
72
+ src = ["src", "tests"]
73
+ extend-exclude = ["build", "dist"]
74
+ cache-dir = ".cache/ruff"
75
+
76
+ [tool.ruff.lint]
77
+ select = [
78
+ "E", # pycodestyle errors
79
+ "W", # pycodestyle warnings
80
+ "F", # pyflakes
81
+ "I", # isort
82
+ "UP", # pyupgrade
83
+ "B", # flake8-bugbear
84
+ "SIM", # flake8-simplify
85
+ "C4", # comprehensions
86
+ "PTH", # pathlib
87
+ "ARG", # unused arguments
88
+ "T20", # no stray prints in library code (CLI reports via cli layer)
89
+ ]
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "src/graphmail/cli.py" = ["T20"]
93
+ "src/graphmail/pipeline.py" = ["T20"]
94
+ "src/graphmail/importer.py" = ["T20"]
95
+ "tests/**" = ["ARG", "T20"]
96
+ "tools/**" = ["T20"]
97
+
98
+ [tool.ruff.lint.isort]
99
+ known-first-party = ["graphmail"]
100
+
101
+ # mypy
102
+ [tool.mypy]
103
+ files = ["src", "tests", "tools"]
104
+ python_version = "3.10"
105
+ strict = true
106
+ warn_unreachable = true
107
+ pretty = true
108
+ show_error_codes = true
109
+ mypy_path = "src:tests"
110
+ explicit_package_bases = true
111
+ cache_dir = ".cache/mypy"
112
+
113
+ # pytest
114
+ [tool.pytest.ini_options]
115
+ testpaths = ["tests"]
116
+ addopts = "-q --strict-markers"
117
+ cache_dir = ".cache/pytest"
118
+ markers = [
119
+ "characterization: pins current behavior of the legacy script",
120
+ ]
121
+
122
+ [tool.coverage.run]
123
+ source = ["src"]
124
+ omit = ["tests/*"]
125
+ concurrency = ["thread"]
126
+ data_file = ".cache/coverage/.coverage"
@@ -0,0 +1,3 @@
1
+ """graphmail: extract Microsoft Graph / .eml emails into a local archive."""
2
+
3
+ __version__ = "1.0.0"