proton2m365 0.1.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 (54) hide show
  1. proton2m365-0.1.0/.gitignore +14 -0
  2. proton2m365-0.1.0/CHANGELOG.md +22 -0
  3. proton2m365-0.1.0/LICENSE +21 -0
  4. proton2m365-0.1.0/PKG-INFO +172 -0
  5. proton2m365-0.1.0/README.md +142 -0
  6. proton2m365-0.1.0/SECURITY.md +23 -0
  7. proton2m365-0.1.0/pyproject.toml +59 -0
  8. proton2m365-0.1.0/src/proton2m365/__init__.py +1 -0
  9. proton2m365-0.1.0/src/proton2m365/auth.py +255 -0
  10. proton2m365-0.1.0/src/proton2m365/cli.py +1342 -0
  11. proton2m365-0.1.0/src/proton2m365/content.py +195 -0
  12. proton2m365-0.1.0/src/proton2m365/doctor.py +219 -0
  13. proton2m365-0.1.0/src/proton2m365/graph_store.py +341 -0
  14. proton2m365-0.1.0/src/proton2m365/imap_store.py +554 -0
  15. proton2m365-0.1.0/src/proton2m365/lease.py +111 -0
  16. proton2m365-0.1.0/src/proton2m365/locks.py +80 -0
  17. proton2m365-0.1.0/src/proton2m365/pipeline.py +974 -0
  18. proton2m365-0.1.0/src/proton2m365/progress.py +60 -0
  19. proton2m365-0.1.0/src/proton2m365/proton_export.py +765 -0
  20. proton2m365-0.1.0/src/proton2m365/receipt.py +330 -0
  21. proton2m365-0.1.0/src/proton2m365/sample_export.py +122 -0
  22. proton2m365-0.1.0/src/proton2m365/state.py +577 -0
  23. proton2m365-0.1.0/src/proton2m365/status.py +211 -0
  24. proton2m365-0.1.0/src/proton2m365/targets.py +75 -0
  25. proton2m365-0.1.0/tests/__init__.py +0 -0
  26. proton2m365-0.1.0/tests/conftest.py +49 -0
  27. proton2m365-0.1.0/tests/fixtures/SYNTHETIC.txt +2 -0
  28. proton2m365-0.1.0/tests/snapshots/doctor.txt +22 -0
  29. proton2m365-0.1.0/tests/snapshots/plan-dry-run.txt +23 -0
  30. proton2m365-0.1.0/tests/snapshots/plan.txt +25 -0
  31. proton2m365-0.1.0/tests/snapshots/replan-exclude.txt +27 -0
  32. proton2m365-0.1.0/tests/snapshots/sample-export.txt +0 -0
  33. proton2m365-0.1.0/tests/snapshots/scan.txt +34 -0
  34. proton2m365-0.1.0/tests/snapshots/status.txt +5 -0
  35. proton2m365-0.1.0/tests/test_auth.py +237 -0
  36. proton2m365-0.1.0/tests/test_check_publish.py +199 -0
  37. proton2m365-0.1.0/tests/test_cli_safety.py +218 -0
  38. proton2m365-0.1.0/tests/test_content.py +120 -0
  39. proton2m365-0.1.0/tests/test_docs.py +31 -0
  40. proton2m365-0.1.0/tests/test_doctor.py +160 -0
  41. proton2m365-0.1.0/tests/test_graph_store.py +251 -0
  42. proton2m365-0.1.0/tests/test_homebrew.py +47 -0
  43. proton2m365-0.1.0/tests/test_imap_store.py +542 -0
  44. proton2m365-0.1.0/tests/test_lease.py +96 -0
  45. proton2m365-0.1.0/tests/test_locks.py +45 -0
  46. proton2m365-0.1.0/tests/test_mapping.py +73 -0
  47. proton2m365-0.1.0/tests/test_pipeline.py +1218 -0
  48. proton2m365-0.1.0/tests/test_plan.py +289 -0
  49. proton2m365-0.1.0/tests/test_progress.py +66 -0
  50. proton2m365-0.1.0/tests/test_proton_export.py +346 -0
  51. proton2m365-0.1.0/tests/test_snapshots.py +77 -0
  52. proton2m365-0.1.0/tests/test_state.py +334 -0
  53. proton2m365-0.1.0/tests/test_status.py +73 -0
  54. proton2m365-0.1.0/tests/test_targets.py +46 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ .gstack/
3
+ .pytest_cache/
4
+ .venv/
5
+ *.egg-info/
6
+ *.log
7
+ # migration data: never commit (scripts/publish-files.txt has the same never-list)
8
+ *.sqlite*
9
+ *.bak-v*
10
+ *.receipt.*
11
+ verify-report*.csv
12
+ token_cache*
13
+ upload-probe.log
14
+ upload-trial.log
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to proton2m365. Versions follow the git tags (`vX.Y.Z`).
4
+
5
+ ## v0.1.0 (2026-11-07)
6
+
7
+ First public release. Microsoft 365 work and school accounts are supported; Outlook.com and Microsoft 365
8
+ Personal/Family are experimental.
9
+
10
+ - Move a Proton Mail Export Tool backup into Microsoft 365 over IMAP and Microsoft Graph: received dates, read
11
+ state, folders (from Proton's metadata) and labels as categories are kept; drafts, Trash and Spam are left out
12
+ unless included; stopping and resuming never uploads a message twice.
13
+ - `migrate EXPORT` runs the whole thing in one resumable command; `scan`, `plan`, `upload`, `tag`, `verify` and
14
+ `prune` remain available one at a time, each ending with a `Next:` line.
15
+ - `verify` ends with one outcome (PASSED, SAMPLE OK, INCOMPLETE, FAILED) and writes a receipt
16
+ (`.receipt.html`, `.txt`, `.json`) that accounts for every export file; `--all` also finds duplicate copies,
17
+ and `--content` compares text, attachments, HTML text and links with the export.
18
+ - Bring your own sign-in app (docs/setup-own-app.md); device-code sign-in over SSH; each state file is bound to
19
+ one mailbox; a lease in the mailbox keeps two computers from uploading into it at once.
20
+ - `plan --exclude-label`, `--exclude-file`, `--map-folder` and `--map-label`, saved and reused by a re-plan.
21
+ - Adaptive upload pacing, `selftest`, `doctor --issue`, `sample-export`, `--json` on every command, stable
22
+ `P2M-` error codes and exit codes, and tested on Linux, macOS and Windows.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Cilley
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,172 @@
1
+ Metadata-Version: 2.5
2
+ Name: proton2m365
3
+ Version: 0.1.0
4
+ Summary: Move a Proton Mail Export Tool backup into Microsoft 365 or Outlook.com, with a receipt for every message.
5
+ Project-URL: Homepage, https://github.com/jonathan-opmodi/proton2m365
6
+ Project-URL: Documentation, https://github.com/jonathan-opmodi/proton2m365#readme
7
+ Project-URL: Issues, https://github.com/jonathan-opmodi/proton2m365/issues
8
+ Project-URL: Changelog, https://github.com/jonathan-opmodi/proton2m365/blob/main/CHANGELOG.md
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: email migration,eml,imap,microsoft 365,outlook,proton,proton mail
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications :: Email
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: httpx>=0.28.1
26
+ Requires-Dist: imapclient>=4.1.0
27
+ Requires-Dist: msal-extensions>=1.3.1
28
+ Requires-Dist: msal>=1.39.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # proton2m365
32
+
33
+ Move a **Proton Mail Export Tool** backup into **Microsoft 365** or **Outlook.com**, without Proton Mail
34
+ Bridge or a paid Proton plan, and get a receipt that accounts for every message.
35
+
36
+ - Keeps each message's original received date and read/unread state.
37
+ - Puts mail in the right folder (Inbox, Sent Items, Archive, your own folders) using Proton's metadata.
38
+ - Turns Proton labels into Outlook categories, and stars into a category or a flag.
39
+ - Can stop and resume at any time without uploading anything twice.
40
+ - Ends with `verify`: one outcome (PASSED, SAMPLE OK, INCOMPLETE or FAILED) and a receipt file listing every
41
+ export file as migrated, intentionally skipped (and why), or unresolved.
42
+
43
+ Status: v0.1. Microsoft 365 work and school accounts are tested live. **Outlook.com and Microsoft 365
44
+ Personal/Family are experimental** until the live checks in [docs/how-it-works.md](docs/how-it-works.md#outlookcom)
45
+ have passed. Support is best effort through GitHub issues.
46
+
47
+ ## Prerequisites
48
+
49
+ - Python 3.12 or newer, and either [pipx](https://pipx.pypa.io) or [uv](https://docs.astral.sh/uv/).
50
+ - macOS: `brew install pipx` (or `brew install uv`)
51
+ - Windows: install Python from python.org, then `py -m pip install --user pipx` (or `winget install astral-sh.uv`)
52
+ - Linux: your distribution's `pipx` package (or the uv installer)
53
+ - A Proton Mail export made with Proton's **Export Tool** (the folder that contains `labels.json` and pairs of
54
+ `.eml` and `.metadata.json` files). Unzip it first.
55
+ - The mailbox you are moving into: a Microsoft 365 work or school account, or an Outlook.com account.
56
+ - **Your own sign-in app** for migrating (not needed to try it). proton2m365 does not ship one, so your mail is only
57
+ ever reached through an app you control. Registering it is a one-time setup, see
58
+ [docs/setup-own-app.md](docs/setup-own-app.md):
59
+ - work or school account: an administrator of your organization registers it, about 10 minutes;
60
+ - personal account (Outlook.com, Microsoft 365 Family): Microsoft lets only organizations register apps, so
61
+ you first create a free Azure account (it asks for a card to verify you; the app itself costs nothing),
62
+ about 20 minutes.
63
+
64
+ ## Try it (no sign-in, nothing written to any mailbox)
65
+
66
+ ```sh
67
+ pipx install proton2m365
68
+ proton2m365 sample-export ./sample
69
+ proton2m365 scan ./sample
70
+ proton2m365 plan --dry-run ./sample
71
+ ```
72
+
73
+ With uv instead of pipx, run each command as `uvx proton2m365 ...` and skip the install line.
74
+
75
+ `plan --dry-run` shows what would be uploaded: 34 of the sample's 40 messages, which folders they would go
76
+ to, which labels become categories, and the 6 that are skipped and why. Try it on your own export next; it
77
+ still writes nothing anywhere.
78
+
79
+ ## Migrate your export
80
+
81
+ First register your sign-in app and put its client ID and tenant in `~/.config/proton2m365/config.toml`
82
+ ([docs/setup-own-app.md](docs/setup-own-app.md)). Then:
83
+
84
+ ```sh
85
+ proton2m365 selftest # live checks with synthetic messages; cleans up after itself
86
+ proton2m365 scan /path/to/export
87
+ proton2m365 plan /path/to/export # writes ./state.sqlite; add --exclude-label "Name" to leave mail out
88
+ proton2m365 upload # hours for a large export; safe to stop and run again
89
+ proton2m365 tag # applies categories and flags
90
+ proton2m365 verify --all # the outcome, and the receipt
91
+ ```
92
+
93
+ Or all of it in one command, which asks before uploading and can be run again to continue where it stopped:
94
+
95
+ ```sh
96
+ proton2m365 migrate /path/to/export # add --exclude-label "Name" to leave mail out
97
+ ```
98
+
99
+ Every command ends with a `Next:` line naming the command to run next. `verify` writes three receipt files next
100
+ to the state file: `state.receipt.html` (open it in a browser, or print it), `state.receipt.txt` and the full
101
+ per-message `state.receipt.json`. Keep your Proton export until you have read the receipt.
102
+
103
+ ### Signing in
104
+
105
+ The first online command opens your browser. Choose the mailbox you are moving **into**, and approve the
106
+ permissions your app asks for. Over SSH or without a display, proton2m365 uses a device code instead (it may ask twice:
107
+ once for Microsoft Graph, once for IMAP). Each state file remembers its mailbox and refuses any other.
108
+
109
+ ## What you will see in Outlook
110
+
111
+ | From Proton | In Outlook |
112
+ |---|---|
113
+ | Inbox, Sent, Archive | Inbox, Sent Items, Archive |
114
+ | your folders (`Projects/Alpha`) | the same folders, created for you |
115
+ | labels | categories (with the nearest Outlook color) |
116
+ | starred | a "Starred" category, or a flag with `plan --starred-as-flag` |
117
+ | received date, read/unread | kept |
118
+ | drafts, Trash, Spam | skipped (Trash and Spam can be included with `--include-trash`, `--include-spam`) |
119
+
120
+ Want it somewhere else? `plan --map-folder "Projects=Clients"` moves a folder (and its subfolders), and
121
+ `--map-label "Work=Office"` renames a category (`"Work="` drops it). The same mappings can live in
122
+ `~/.config/proton2m365/config.toml` under `[map.folders]` and `[map.labels]`.
123
+
124
+ Each uploaded message carries an added `X-Proton-Id` header, which is how proton2m365 finds it again. While
125
+ `upload` or `prune` runs, a folder `_proton2m365_lease` holds one small message so that a second computer does
126
+ not upload the same mail; it is emptied when the run ends.
127
+
128
+ ## What is not preserved
129
+
130
+ Proton's conversation grouping, snooze and scheduled sends, and labels you exclude. Messages larger than the
131
+ mailbox's size limit (35 MB by default) are skipped and listed. `verify` checks dates, read state, flags,
132
+ categories, drafts and folders; `verify --content` also downloads the checked messages and compares their text,
133
+ attachments, HTML text and links with the export (`verify --all --content` downloads the whole mailbox). The
134
+ receipt says which of these were done.
135
+
136
+ ## If it stops
137
+
138
+ Run the same command again: nothing that already arrived is sent twice. `proton2m365 status` shows where
139
+ things stand without signing in. To start over, delete the state file (and its `.receipt.*` files) and run
140
+ `plan` again; mail already uploaded is detected by its `X-Proton-Id` header.
141
+
142
+ ### Exit codes
143
+
144
+ | Code | Meaning |
145
+ |---|---|
146
+ | 0 | done (verify: PASSED) |
147
+ | 1 | unexpected error: a crash log was written; attach `proton2m365 doctor --issue` output to an issue |
148
+ | 2 | usage, configuration, sign-in or connection error (the message starts with a `P2M-` code, see [docs/errors.md](docs/errors.md)) |
149
+ | 3 | mailbox full |
150
+ | 10 | verify: SAMPLE OK, some messages not checked yet (`verify --all`) |
151
+ | 11 | incomplete: messages still need work (the `Next:` line says which command) |
152
+ | 12 | failed: a check did not match, or a selftest check failed |
153
+ | 13 | interrupted: run the same command again to resume |
154
+ | 14 | verify would have PASSED, but its receipt could not be saved |
155
+
156
+ ## Why not Thunderbird or a paid EML converter?
157
+
158
+ They can move `.eml` files, and that may be all you need. As far as we know none of them reads Proton's
159
+ metadata files, so Proton's folders, labels and read state are not carried over, and nothing tells you
160
+ afterwards whether every message arrived. proton2m365 exists for those things.
161
+ Tools built on Proton Mail Bridge (imapsync and others) need a paid Proton plan and a running Bridge.
162
+
163
+ ## More
164
+
165
+ - [How it works](docs/how-it-works.md): export format, folder routing, why IMAP plus Graph
166
+ - [Errors](docs/errors.md): every `P2M-` code and what to do
167
+ - [Upgrading](docs/upgrading.md): pinning a version, resuming across versions
168
+ - [Using your own Entra app](docs/setup-own-app.md)
169
+ - [Security](SECURITY.md): what stays on your computer
170
+ - [Contributing](CONTRIBUTING.md)
171
+
172
+ MIT licensed. Not affiliated with Proton AG or Microsoft.
@@ -0,0 +1,142 @@
1
+ # proton2m365
2
+
3
+ Move a **Proton Mail Export Tool** backup into **Microsoft 365** or **Outlook.com**, without Proton Mail
4
+ Bridge or a paid Proton plan, and get a receipt that accounts for every message.
5
+
6
+ - Keeps each message's original received date and read/unread state.
7
+ - Puts mail in the right folder (Inbox, Sent Items, Archive, your own folders) using Proton's metadata.
8
+ - Turns Proton labels into Outlook categories, and stars into a category or a flag.
9
+ - Can stop and resume at any time without uploading anything twice.
10
+ - Ends with `verify`: one outcome (PASSED, SAMPLE OK, INCOMPLETE or FAILED) and a receipt file listing every
11
+ export file as migrated, intentionally skipped (and why), or unresolved.
12
+
13
+ Status: v0.1. Microsoft 365 work and school accounts are tested live. **Outlook.com and Microsoft 365
14
+ Personal/Family are experimental** until the live checks in [docs/how-it-works.md](docs/how-it-works.md#outlookcom)
15
+ have passed. Support is best effort through GitHub issues.
16
+
17
+ ## Prerequisites
18
+
19
+ - Python 3.12 or newer, and either [pipx](https://pipx.pypa.io) or [uv](https://docs.astral.sh/uv/).
20
+ - macOS: `brew install pipx` (or `brew install uv`)
21
+ - Windows: install Python from python.org, then `py -m pip install --user pipx` (or `winget install astral-sh.uv`)
22
+ - Linux: your distribution's `pipx` package (or the uv installer)
23
+ - A Proton Mail export made with Proton's **Export Tool** (the folder that contains `labels.json` and pairs of
24
+ `.eml` and `.metadata.json` files). Unzip it first.
25
+ - The mailbox you are moving into: a Microsoft 365 work or school account, or an Outlook.com account.
26
+ - **Your own sign-in app** for migrating (not needed to try it). proton2m365 does not ship one, so your mail is only
27
+ ever reached through an app you control. Registering it is a one-time setup, see
28
+ [docs/setup-own-app.md](docs/setup-own-app.md):
29
+ - work or school account: an administrator of your organization registers it, about 10 minutes;
30
+ - personal account (Outlook.com, Microsoft 365 Family): Microsoft lets only organizations register apps, so
31
+ you first create a free Azure account (it asks for a card to verify you; the app itself costs nothing),
32
+ about 20 minutes.
33
+
34
+ ## Try it (no sign-in, nothing written to any mailbox)
35
+
36
+ ```sh
37
+ pipx install proton2m365
38
+ proton2m365 sample-export ./sample
39
+ proton2m365 scan ./sample
40
+ proton2m365 plan --dry-run ./sample
41
+ ```
42
+
43
+ With uv instead of pipx, run each command as `uvx proton2m365 ...` and skip the install line.
44
+
45
+ `plan --dry-run` shows what would be uploaded: 34 of the sample's 40 messages, which folders they would go
46
+ to, which labels become categories, and the 6 that are skipped and why. Try it on your own export next; it
47
+ still writes nothing anywhere.
48
+
49
+ ## Migrate your export
50
+
51
+ First register your sign-in app and put its client ID and tenant in `~/.config/proton2m365/config.toml`
52
+ ([docs/setup-own-app.md](docs/setup-own-app.md)). Then:
53
+
54
+ ```sh
55
+ proton2m365 selftest # live checks with synthetic messages; cleans up after itself
56
+ proton2m365 scan /path/to/export
57
+ proton2m365 plan /path/to/export # writes ./state.sqlite; add --exclude-label "Name" to leave mail out
58
+ proton2m365 upload # hours for a large export; safe to stop and run again
59
+ proton2m365 tag # applies categories and flags
60
+ proton2m365 verify --all # the outcome, and the receipt
61
+ ```
62
+
63
+ Or all of it in one command, which asks before uploading and can be run again to continue where it stopped:
64
+
65
+ ```sh
66
+ proton2m365 migrate /path/to/export # add --exclude-label "Name" to leave mail out
67
+ ```
68
+
69
+ Every command ends with a `Next:` line naming the command to run next. `verify` writes three receipt files next
70
+ to the state file: `state.receipt.html` (open it in a browser, or print it), `state.receipt.txt` and the full
71
+ per-message `state.receipt.json`. Keep your Proton export until you have read the receipt.
72
+
73
+ ### Signing in
74
+
75
+ The first online command opens your browser. Choose the mailbox you are moving **into**, and approve the
76
+ permissions your app asks for. Over SSH or without a display, proton2m365 uses a device code instead (it may ask twice:
77
+ once for Microsoft Graph, once for IMAP). Each state file remembers its mailbox and refuses any other.
78
+
79
+ ## What you will see in Outlook
80
+
81
+ | From Proton | In Outlook |
82
+ |---|---|
83
+ | Inbox, Sent, Archive | Inbox, Sent Items, Archive |
84
+ | your folders (`Projects/Alpha`) | the same folders, created for you |
85
+ | labels | categories (with the nearest Outlook color) |
86
+ | starred | a "Starred" category, or a flag with `plan --starred-as-flag` |
87
+ | received date, read/unread | kept |
88
+ | drafts, Trash, Spam | skipped (Trash and Spam can be included with `--include-trash`, `--include-spam`) |
89
+
90
+ Want it somewhere else? `plan --map-folder "Projects=Clients"` moves a folder (and its subfolders), and
91
+ `--map-label "Work=Office"` renames a category (`"Work="` drops it). The same mappings can live in
92
+ `~/.config/proton2m365/config.toml` under `[map.folders]` and `[map.labels]`.
93
+
94
+ Each uploaded message carries an added `X-Proton-Id` header, which is how proton2m365 finds it again. While
95
+ `upload` or `prune` runs, a folder `_proton2m365_lease` holds one small message so that a second computer does
96
+ not upload the same mail; it is emptied when the run ends.
97
+
98
+ ## What is not preserved
99
+
100
+ Proton's conversation grouping, snooze and scheduled sends, and labels you exclude. Messages larger than the
101
+ mailbox's size limit (35 MB by default) are skipped and listed. `verify` checks dates, read state, flags,
102
+ categories, drafts and folders; `verify --content` also downloads the checked messages and compares their text,
103
+ attachments, HTML text and links with the export (`verify --all --content` downloads the whole mailbox). The
104
+ receipt says which of these were done.
105
+
106
+ ## If it stops
107
+
108
+ Run the same command again: nothing that already arrived is sent twice. `proton2m365 status` shows where
109
+ things stand without signing in. To start over, delete the state file (and its `.receipt.*` files) and run
110
+ `plan` again; mail already uploaded is detected by its `X-Proton-Id` header.
111
+
112
+ ### Exit codes
113
+
114
+ | Code | Meaning |
115
+ |---|---|
116
+ | 0 | done (verify: PASSED) |
117
+ | 1 | unexpected error: a crash log was written; attach `proton2m365 doctor --issue` output to an issue |
118
+ | 2 | usage, configuration, sign-in or connection error (the message starts with a `P2M-` code, see [docs/errors.md](docs/errors.md)) |
119
+ | 3 | mailbox full |
120
+ | 10 | verify: SAMPLE OK, some messages not checked yet (`verify --all`) |
121
+ | 11 | incomplete: messages still need work (the `Next:` line says which command) |
122
+ | 12 | failed: a check did not match, or a selftest check failed |
123
+ | 13 | interrupted: run the same command again to resume |
124
+ | 14 | verify would have PASSED, but its receipt could not be saved |
125
+
126
+ ## Why not Thunderbird or a paid EML converter?
127
+
128
+ They can move `.eml` files, and that may be all you need. As far as we know none of them reads Proton's
129
+ metadata files, so Proton's folders, labels and read state are not carried over, and nothing tells you
130
+ afterwards whether every message arrived. proton2m365 exists for those things.
131
+ Tools built on Proton Mail Bridge (imapsync and others) need a paid Proton plan and a running Bridge.
132
+
133
+ ## More
134
+
135
+ - [How it works](docs/how-it-works.md): export format, folder routing, why IMAP plus Graph
136
+ - [Errors](docs/errors.md): every `P2M-` code and what to do
137
+ - [Upgrading](docs/upgrading.md): pinning a version, resuming across versions
138
+ - [Using your own Entra app](docs/setup-own-app.md)
139
+ - [Security](SECURITY.md): what stays on your computer
140
+ - [Contributing](CONTRIBUTING.md)
141
+
142
+ MIT licensed. Not affiliated with Proton AG or Microsoft.
@@ -0,0 +1,23 @@
1
+ # Security
2
+
3
+ ## What leaves your computer
4
+
5
+ Only calls to Microsoft: sign-in (login.microsoftonline.com), Microsoft Graph and Outlook IMAP. proton2m365
6
+ sends no telemetry and talks to no other service.
7
+
8
+ ## What stays on your computer, and holds mailbox data
9
+
10
+ | File | Holds | Protection |
11
+ |---|---|---|
12
+ | token cache (`~/.config/proton2m365/token_cache.bin`, or the OS secret store) | sign-in tokens | macOS Keychain, Windows DPAPI or Linux libsecret; otherwise an owner-only file (0600 on macOS and Linux) |
13
+ | state file (`state.sqlite` by default) and its `.bak-v*` upgrade backups | message IDs, folder and category names, the mailbox address | your file permissions |
14
+ | `<state>.receipt.json`, `.receipt.txt`, `.receipt.html`, `verify-report.csv` | the same, per message | owner-only (0600) where the OS supports it; on Windows they inherit your profile's permissions |
15
+ | `crash-*.log` | a traceback, which can include addresses | owner-only (0600) |
16
+
17
+ `doctor --issue` prints a report with names and addresses replaced by hashes; attach that to issues, never the
18
+ files above.
19
+
20
+ ## Reporting a vulnerability
21
+
22
+ Please use GitHub's private vulnerability reporting on this repository rather than a public issue. Support is
23
+ best effort; expect a reply within a couple of weeks.
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "proton2m365"
3
+ dynamic = ["version"]
4
+ description = "Move a Proton Mail Export Tool backup into Microsoft 365 or Outlook.com, with a receipt for every message."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = ["proton", "proton mail", "microsoft 365", "outlook", "email migration", "imap", "eml"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: End Users/Desktop",
14
+ "Intended Audience :: System Administrators",
15
+ "Operating System :: MacOS",
16
+ "Operating System :: Microsoft :: Windows",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Communications :: Email",
22
+ "Topic :: Utilities",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.28.1",
26
+ "imapclient>=4.1.0",
27
+ "msal>=1.39.0",
28
+ "msal-extensions>=1.3.1",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/jonathan-opmodi/proton2m365"
33
+ Documentation = "https://github.com/jonathan-opmodi/proton2m365#readme"
34
+ Issues = "https://github.com/jonathan-opmodi/proton2m365/issues"
35
+ Changelog = "https://github.com/jonathan-opmodi/proton2m365/blob/main/CHANGELOG.md"
36
+
37
+ [project.scripts]
38
+ proton2m365 = "proton2m365.cli:main"
39
+
40
+ [dependency-groups]
41
+ dev = ["pytest>=8"]
42
+
43
+ [build-system]
44
+ requires = ["hatchling", "hatch-vcs"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.hatch.version]
48
+ source = "vcs"
49
+ # Builds without git metadata (an sdist, a shallow checkout) still get a version.
50
+ fallback-version = "0.0.0+unknown"
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = ["src", "tests", "README.md", "LICENSE", "CHANGELOG.md", "SECURITY.md", "pyproject.toml"]
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/proton2m365"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ """proton2m365: move a Proton Mail export into a Microsoft 365 mailbox."""