email-code-finder 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.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include src *.py
4
+ recursive-include src *.typed
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: email-code-finder
3
+ Version: 0.1.0
4
+ Summary: Extract any regex-matchable value (2FA/OTP codes, links, tokens, text) from an IMAP mailbox without deleting messages.
5
+ Author-email: Erik Melias <erikmelias@phac.com.br>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/erikmelias/email-code-finder
8
+ Project-URL: Repository, https://github.com/erikmelias/email-code-finder
9
+ Project-URL: Issues, https://github.com/erikmelias/email-code-finder/issues
10
+ Keywords: imap,email,regex,extract,scraping,2fa,otp,authentication,token,code
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Communications :: Email
21
+ Classifier: Topic :: Security
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+
27
+ # email-code-finder
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/email-code-finder.svg)](https://pypi.org/project/email-code-finder/)
30
+ [![Python](https://img.shields.io/pypi/pyversions/email-code-finder.svg)](https://pypi.org/project/email-code-finder/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
32
+
33
+ Wait for an email to arrive in an IMAP mailbox and extract **any value you can
34
+ describe with a regular expression** — a 2FA/OTP code, a confirmation link, a
35
+ tracking number, an order ID, a token, or any piece of text. The library
36
+ connects over IMAP, waits for the message whose subject you specify, runs your
37
+ regex against the body, and returns the captured value — **without ever deleting
38
+ your messages.**
39
+
40
+ > **Not just 2FA.** One-time codes are the most common use case, but the engine
41
+ > is generic: if the value is somewhere in the email body and a regex can
42
+ > capture it, this library can fetch it. The `regex_pattern` you provide is the
43
+ > only thing that decides *what* gets extracted.
44
+
45
+ Examples of what you can extract:
46
+
47
+ | Goal | Example `subject_to_find` | Example `regex_pattern` |
48
+ | --- | --- | --- |
49
+ | 2FA / OTP code | `Your verification code` | `\b(\d{6})\b` |
50
+ | Confirmation link | `Confirm your email` | `href="(https://[^"]*/confirm[^"]*)"` |
51
+ | Order / tracking ID | `Your order shipped` | `Tracking:\s*([A-Z0-9]{10,})` |
52
+ | Arbitrary text | `Your invoice` | `Invoice No\.\s*(\S+)` |
53
+
54
+ - **No third-party dependencies** — standard library only.
55
+ - **Non-destructive** — emails are flagged as read, never deleted.
56
+ - **Provider-aware** — Gmail, Outlook/Office 365, Yahoo, iCloud, or any custom
57
+ IMAP host.
58
+ - **Pluggable notifications** — pass a callback to surface progress in your UI.
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install email-code-finder
66
+ ```
67
+
68
+ Requires Python 3.8+.
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ from email_code_finder import EmailCodeFinder
74
+
75
+ config = {
76
+ "provider": "gmail",
77
+ "user_email": "user@example.com",
78
+ "password": "your-app-specific-password",
79
+ "subject_to_find": "Your verification code",
80
+ "regex_pattern": r"(?s)token-2fa-text\"?>.*?<b>(.*?)</b>.*?</div>",
81
+ "max_wait_time_seconds": 180,
82
+ "check_interval_seconds": 6,
83
+ }
84
+
85
+ finder = EmailCodeFinder(config=config, notify_callback=print)
86
+ code = finder.wait_for_code()
87
+
88
+ if code:
89
+ print(f"Got the code: {code}")
90
+ else:
91
+ print("Timed out without receiving a code.")
92
+ ```
93
+
94
+ Or load the configuration from a JSON file:
95
+
96
+ ```python
97
+ finder = EmailCodeFinder(config_path="config.json")
98
+ code = finder.wait_for_code()
99
+ ```
100
+
101
+ A runnable example lives in [`examples/basic_usage.py`](examples/basic_usage.py).
102
+
103
+ ## Configuration
104
+
105
+ Configuration can be passed as a `dict` or stored in a JSON file (see
106
+ [`examples/config.example.json`](examples/config.example.json)).
107
+
108
+ | Key | Type | Required | Description |
109
+ | --- | --- | --- | --- |
110
+ | `user_email` | str | ✅ | Full email address used to authenticate. |
111
+ | `password` | str | ✅ | Account or **app-specific** password (see Security). |
112
+ | `subject_to_find` | str | ✅ | Subject line of the email carrying the code. A `Fwd:` variant is matched too. |
113
+ | `regex_pattern` | str | ✅ | Regex whose **first capture group** is the code. |
114
+ | `provider` | str | ➖ | `gmail`, `outlook`, `office365`, `yahoo`, `icloud`, `kinghost`. If omitted, the host is derived from your email domain (`imap.<domain>`). |
115
+ | `imap_server` | str | ➖ | Explicit IMAP host; overrides `provider` detection. |
116
+ | `max_wait_time_seconds` | int | ➖ | How long to wait before giving up. Default `180`. |
117
+ | `check_interval_seconds` | int | ➖ | Delay between inbox polls. Default `6`. |
118
+
119
+ ### Writing the regex
120
+
121
+ `regex_pattern` is matched with `re.DOTALL`; the value returned is **capture
122
+ group 1**. For a code wrapped in `<b>123456</b>` you might use:
123
+
124
+ ```text
125
+ <b>(\d{6})</b>
126
+ ```
127
+
128
+ Test your pattern against a real email body before relying on it.
129
+
130
+ ## API
131
+
132
+ ### `EmailCodeFinder(config=None, config_path="config.json", notify_callback=None)`
133
+
134
+ - `config` — configuration dict. When `None`, it is loaded from `config_path`.
135
+ - `config_path` — path to a JSON config file (used only when `config` is `None`).
136
+ - `notify_callback` — optional `Callable(message: str)` invoked for user-facing
137
+ events (waiting, code found, timeout, error). Must be thread-safe.
138
+
139
+ Missing required keys raise `ValueError`.
140
+
141
+ #### `wait_for_code() -> Optional[str]`
142
+
143
+ Connects, polls the inbox until the code arrives or the timeout elapses, and
144
+ returns the code (or `None`). On success the matching email is flagged as read.
145
+ The connection is always closed when the call returns.
146
+
147
+ ### `ImapEmailClient`
148
+
149
+ Lower-level IMAP wrapper exposed for advanced use (`connect`, `get_max_uid`,
150
+ `search_unread_by_subject`, `fetch_body`, `extract_code`, `mark_as_read`,
151
+ `logout`). It performs **no destructive operations**.
152
+
153
+ ## How matching works: timing & the UID baseline
154
+
155
+ Understanding the sequence is important to use the library correctly.
156
+
157
+ 1. **Baseline.** The moment `wait_for_code()` connects, it reads the highest
158
+ existing message UID in the inbox and stores it as a *baseline*. IMAP UIDs
159
+ only ever increase, so this is a precise "everything up to here is old" mark.
160
+ 2. **Polling.** Every `check_interval_seconds` it searches the inbox for
161
+ **unread** messages whose subject matches `subject_to_find` (a `Fwd:`
162
+ variant is matched too).
163
+ 3. **New-only filter.** Any match with a UID **less than or equal to** the
164
+ baseline is skipped — it was already there before you started waiting, so it
165
+ is treated as stale. Only genuinely new messages are inspected.
166
+ 4. **Extraction.** The regex runs against the body of each new match. The first
167
+ message that yields a capture group wins: that value is returned and the
168
+ message is flagged as read.
169
+ 5. **Timeout.** If nothing matches within `max_wait_time_seconds`, the call
170
+ returns `None`.
171
+
172
+ This UID baseline replaces the old, dangerous behaviour of *deleting* the inbox
173
+ to "clean up" previous codes. Your existing emails are never touched.
174
+
175
+ ### ⏱️ Critical: start waiting *before* the email is sent
176
+
177
+ Because the baseline is taken at the start, an email that arrives **before** you
178
+ call `wait_for_code()` will be at or below the baseline and therefore ignored.
179
+ Trigger the action that generates the email **after** (or concurrently with)
180
+ starting the wait:
181
+
182
+ ```python
183
+ import threading
184
+ from email_code_finder import EmailCodeFinder
185
+
186
+ finder = EmailCodeFinder(config=config)
187
+
188
+ # Run wait_for_code() first (in a thread), THEN trigger the email.
189
+ result = {}
190
+ waiter = threading.Thread(target=lambda: result.update(code=finder.wait_for_code()))
191
+ waiter.start()
192
+
193
+ trigger_login() # the action that makes the provider send the email
194
+ waiter.join()
195
+ print(result["code"])
196
+ ```
197
+
198
+ ### Tuning the delays
199
+
200
+ | Setting | What it controls | Guidance |
201
+ | --- | --- | --- |
202
+ | `max_wait_time_seconds` | Total time to wait before giving up. | Set it above the worst-case email delivery time. Mail can take anywhere from a few seconds to a couple of minutes; `180` (3 min) is a safe default. |
203
+ | `check_interval_seconds` | Pause between inbox polls. | Lower = the code is picked up sooner, but more IMAP requests. `6` is a good balance. Avoid going below `2–3` so you don't hit provider rate limits or get your IP throttled. |
204
+
205
+ The call returns **as soon as** a matching code is found — the interval is only
206
+ the upper bound on how long after arrival you notice it, not a fixed wait.
207
+
208
+ ## Security
209
+
210
+ > ⚠️ **This library handles mailbox credentials. Read this section.**
211
+
212
+ - **Use app-specific passwords**, not your main account password. Gmail,
213
+ Outlook, Yahoo and iCloud all support them and most require them when 2FA is
214
+ enabled on the account.
215
+ - **Never commit `config.json`.** It is listed in `.gitignore`. Prefer loading
216
+ secrets from environment variables or a secret manager in production, e.g.:
217
+
218
+ ```python
219
+ import os
220
+ config["password"] = os.environ["EMAIL_PASSWORD"]
221
+ ```
222
+
223
+ - **Connections use TLS** (`IMAP4_SSL` on port 993) with certificate
224
+ verification via `ssl.create_default_context()`. Do not disable verification.
225
+ - **Least privilege.** If your provider supports it, use a dedicated mailbox or
226
+ an account scoped only to receiving these codes.
227
+ - **Logging.** Extracted codes are written to logs at `DEBUG`/`INFO` level for
228
+ troubleshooting. Keep your log level and log storage appropriately restricted,
229
+ and avoid `DEBUG` in production if logs are shared.
230
+ - **Regex from untrusted input.** If `regex_pattern` ever comes from an
231
+ untrusted source, beware of catastrophic backtracking (ReDoS). Prefer simple,
232
+ anchored patterns.
233
+
234
+ ## Limitations
235
+
236
+ - IMAP only; POP3 and provider-specific APIs are not supported.
237
+ - Reads from `INBOX` only.
238
+ - The code must be extractable from the email body via a single regex group.
239
+
240
+ ## Development
241
+
242
+ ```bash
243
+ git clone https://github.com/erikmelias/email-code-finder.git
244
+ cd email-code-finder
245
+ pip install -e ".[dev]"
246
+ pytest
247
+ ```
248
+
249
+ Tests use a mocked IMAP client and make no network connections.
250
+
251
+ ## License
252
+
253
+ [MIT](LICENSE) © Erik Melias
@@ -0,0 +1,227 @@
1
+ # email-code-finder
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/email-code-finder.svg)](https://pypi.org/project/email-code-finder/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/email-code-finder.svg)](https://pypi.org/project/email-code-finder/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ Wait for an email to arrive in an IMAP mailbox and extract **any value you can
8
+ describe with a regular expression** — a 2FA/OTP code, a confirmation link, a
9
+ tracking number, an order ID, a token, or any piece of text. The library
10
+ connects over IMAP, waits for the message whose subject you specify, runs your
11
+ regex against the body, and returns the captured value — **without ever deleting
12
+ your messages.**
13
+
14
+ > **Not just 2FA.** One-time codes are the most common use case, but the engine
15
+ > is generic: if the value is somewhere in the email body and a regex can
16
+ > capture it, this library can fetch it. The `regex_pattern` you provide is the
17
+ > only thing that decides *what* gets extracted.
18
+
19
+ Examples of what you can extract:
20
+
21
+ | Goal | Example `subject_to_find` | Example `regex_pattern` |
22
+ | --- | --- | --- |
23
+ | 2FA / OTP code | `Your verification code` | `\b(\d{6})\b` |
24
+ | Confirmation link | `Confirm your email` | `href="(https://[^"]*/confirm[^"]*)"` |
25
+ | Order / tracking ID | `Your order shipped` | `Tracking:\s*([A-Z0-9]{10,})` |
26
+ | Arbitrary text | `Your invoice` | `Invoice No\.\s*(\S+)` |
27
+
28
+ - **No third-party dependencies** — standard library only.
29
+ - **Non-destructive** — emails are flagged as read, never deleted.
30
+ - **Provider-aware** — Gmail, Outlook/Office 365, Yahoo, iCloud, or any custom
31
+ IMAP host.
32
+ - **Pluggable notifications** — pass a callback to surface progress in your UI.
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install email-code-finder
40
+ ```
41
+
42
+ Requires Python 3.8+.
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from email_code_finder import EmailCodeFinder
48
+
49
+ config = {
50
+ "provider": "gmail",
51
+ "user_email": "user@example.com",
52
+ "password": "your-app-specific-password",
53
+ "subject_to_find": "Your verification code",
54
+ "regex_pattern": r"(?s)token-2fa-text\"?>.*?<b>(.*?)</b>.*?</div>",
55
+ "max_wait_time_seconds": 180,
56
+ "check_interval_seconds": 6,
57
+ }
58
+
59
+ finder = EmailCodeFinder(config=config, notify_callback=print)
60
+ code = finder.wait_for_code()
61
+
62
+ if code:
63
+ print(f"Got the code: {code}")
64
+ else:
65
+ print("Timed out without receiving a code.")
66
+ ```
67
+
68
+ Or load the configuration from a JSON file:
69
+
70
+ ```python
71
+ finder = EmailCodeFinder(config_path="config.json")
72
+ code = finder.wait_for_code()
73
+ ```
74
+
75
+ A runnable example lives in [`examples/basic_usage.py`](examples/basic_usage.py).
76
+
77
+ ## Configuration
78
+
79
+ Configuration can be passed as a `dict` or stored in a JSON file (see
80
+ [`examples/config.example.json`](examples/config.example.json)).
81
+
82
+ | Key | Type | Required | Description |
83
+ | --- | --- | --- | --- |
84
+ | `user_email` | str | ✅ | Full email address used to authenticate. |
85
+ | `password` | str | ✅ | Account or **app-specific** password (see Security). |
86
+ | `subject_to_find` | str | ✅ | Subject line of the email carrying the code. A `Fwd:` variant is matched too. |
87
+ | `regex_pattern` | str | ✅ | Regex whose **first capture group** is the code. |
88
+ | `provider` | str | ➖ | `gmail`, `outlook`, `office365`, `yahoo`, `icloud`, `kinghost`. If omitted, the host is derived from your email domain (`imap.<domain>`). |
89
+ | `imap_server` | str | ➖ | Explicit IMAP host; overrides `provider` detection. |
90
+ | `max_wait_time_seconds` | int | ➖ | How long to wait before giving up. Default `180`. |
91
+ | `check_interval_seconds` | int | ➖ | Delay between inbox polls. Default `6`. |
92
+
93
+ ### Writing the regex
94
+
95
+ `regex_pattern` is matched with `re.DOTALL`; the value returned is **capture
96
+ group 1**. For a code wrapped in `<b>123456</b>` you might use:
97
+
98
+ ```text
99
+ <b>(\d{6})</b>
100
+ ```
101
+
102
+ Test your pattern against a real email body before relying on it.
103
+
104
+ ## API
105
+
106
+ ### `EmailCodeFinder(config=None, config_path="config.json", notify_callback=None)`
107
+
108
+ - `config` — configuration dict. When `None`, it is loaded from `config_path`.
109
+ - `config_path` — path to a JSON config file (used only when `config` is `None`).
110
+ - `notify_callback` — optional `Callable(message: str)` invoked for user-facing
111
+ events (waiting, code found, timeout, error). Must be thread-safe.
112
+
113
+ Missing required keys raise `ValueError`.
114
+
115
+ #### `wait_for_code() -> Optional[str]`
116
+
117
+ Connects, polls the inbox until the code arrives or the timeout elapses, and
118
+ returns the code (or `None`). On success the matching email is flagged as read.
119
+ The connection is always closed when the call returns.
120
+
121
+ ### `ImapEmailClient`
122
+
123
+ Lower-level IMAP wrapper exposed for advanced use (`connect`, `get_max_uid`,
124
+ `search_unread_by_subject`, `fetch_body`, `extract_code`, `mark_as_read`,
125
+ `logout`). It performs **no destructive operations**.
126
+
127
+ ## How matching works: timing & the UID baseline
128
+
129
+ Understanding the sequence is important to use the library correctly.
130
+
131
+ 1. **Baseline.** The moment `wait_for_code()` connects, it reads the highest
132
+ existing message UID in the inbox and stores it as a *baseline*. IMAP UIDs
133
+ only ever increase, so this is a precise "everything up to here is old" mark.
134
+ 2. **Polling.** Every `check_interval_seconds` it searches the inbox for
135
+ **unread** messages whose subject matches `subject_to_find` (a `Fwd:`
136
+ variant is matched too).
137
+ 3. **New-only filter.** Any match with a UID **less than or equal to** the
138
+ baseline is skipped — it was already there before you started waiting, so it
139
+ is treated as stale. Only genuinely new messages are inspected.
140
+ 4. **Extraction.** The regex runs against the body of each new match. The first
141
+ message that yields a capture group wins: that value is returned and the
142
+ message is flagged as read.
143
+ 5. **Timeout.** If nothing matches within `max_wait_time_seconds`, the call
144
+ returns `None`.
145
+
146
+ This UID baseline replaces the old, dangerous behaviour of *deleting* the inbox
147
+ to "clean up" previous codes. Your existing emails are never touched.
148
+
149
+ ### ⏱️ Critical: start waiting *before* the email is sent
150
+
151
+ Because the baseline is taken at the start, an email that arrives **before** you
152
+ call `wait_for_code()` will be at or below the baseline and therefore ignored.
153
+ Trigger the action that generates the email **after** (or concurrently with)
154
+ starting the wait:
155
+
156
+ ```python
157
+ import threading
158
+ from email_code_finder import EmailCodeFinder
159
+
160
+ finder = EmailCodeFinder(config=config)
161
+
162
+ # Run wait_for_code() first (in a thread), THEN trigger the email.
163
+ result = {}
164
+ waiter = threading.Thread(target=lambda: result.update(code=finder.wait_for_code()))
165
+ waiter.start()
166
+
167
+ trigger_login() # the action that makes the provider send the email
168
+ waiter.join()
169
+ print(result["code"])
170
+ ```
171
+
172
+ ### Tuning the delays
173
+
174
+ | Setting | What it controls | Guidance |
175
+ | --- | --- | --- |
176
+ | `max_wait_time_seconds` | Total time to wait before giving up. | Set it above the worst-case email delivery time. Mail can take anywhere from a few seconds to a couple of minutes; `180` (3 min) is a safe default. |
177
+ | `check_interval_seconds` | Pause between inbox polls. | Lower = the code is picked up sooner, but more IMAP requests. `6` is a good balance. Avoid going below `2–3` so you don't hit provider rate limits or get your IP throttled. |
178
+
179
+ The call returns **as soon as** a matching code is found — the interval is only
180
+ the upper bound on how long after arrival you notice it, not a fixed wait.
181
+
182
+ ## Security
183
+
184
+ > ⚠️ **This library handles mailbox credentials. Read this section.**
185
+
186
+ - **Use app-specific passwords**, not your main account password. Gmail,
187
+ Outlook, Yahoo and iCloud all support them and most require them when 2FA is
188
+ enabled on the account.
189
+ - **Never commit `config.json`.** It is listed in `.gitignore`. Prefer loading
190
+ secrets from environment variables or a secret manager in production, e.g.:
191
+
192
+ ```python
193
+ import os
194
+ config["password"] = os.environ["EMAIL_PASSWORD"]
195
+ ```
196
+
197
+ - **Connections use TLS** (`IMAP4_SSL` on port 993) with certificate
198
+ verification via `ssl.create_default_context()`. Do not disable verification.
199
+ - **Least privilege.** If your provider supports it, use a dedicated mailbox or
200
+ an account scoped only to receiving these codes.
201
+ - **Logging.** Extracted codes are written to logs at `DEBUG`/`INFO` level for
202
+ troubleshooting. Keep your log level and log storage appropriately restricted,
203
+ and avoid `DEBUG` in production if logs are shared.
204
+ - **Regex from untrusted input.** If `regex_pattern` ever comes from an
205
+ untrusted source, beware of catastrophic backtracking (ReDoS). Prefer simple,
206
+ anchored patterns.
207
+
208
+ ## Limitations
209
+
210
+ - IMAP only; POP3 and provider-specific APIs are not supported.
211
+ - Reads from `INBOX` only.
212
+ - The code must be extractable from the email body via a single regex group.
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ git clone https://github.com/erikmelias/email-code-finder.git
218
+ cd email-code-finder
219
+ pip install -e ".[dev]"
220
+ pytest
221
+ ```
222
+
223
+ Tests use a mocked IMAP client and make no network connections.
224
+
225
+ ## License
226
+
227
+ [MIT](LICENSE) © Erik Melias
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "email-code-finder"
7
+ version = "0.1.0"
8
+ description = "Extract any regex-matchable value (2FA/OTP codes, links, tokens, text) from an IMAP mailbox without deleting messages."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Erik Melias", email = "erikmelias@phac.com.br" },
15
+ ]
16
+ keywords = [
17
+ "imap",
18
+ "email",
19
+ "regex",
20
+ "extract",
21
+ "scraping",
22
+ "2fa",
23
+ "otp",
24
+ "authentication",
25
+ "token",
26
+ "code",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Intended Audience :: Developers",
31
+ "Operating System :: OS Independent",
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.8",
34
+ "Programming Language :: Python :: 3.9",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Topic :: Communications :: Email",
39
+ "Topic :: Security",
40
+ ]
41
+ # No third-party runtime dependencies: only the Python standard library is used.
42
+ dependencies = []
43
+
44
+ [project.optional-dependencies]
45
+ dev = ["pytest>=7.0"]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/erikmelias/email-code-finder"
49
+ Repository = "https://github.com/erikmelias/email-code-finder"
50
+ Issues = "https://github.com/erikmelias/email-code-finder/issues"
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.setuptools.package-data]
56
+ email_code_finder = ["py.typed"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """email-code-finder: read one-time 2FA/OTP codes from an IMAP mailbox.
2
+
3
+ Public API::
4
+
5
+ from email_code_finder import EmailCodeFinder
6
+
7
+ finder = EmailCodeFinder(config_path="config.json", notify_callback=print)
8
+ code = finder.wait_for_code()
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .client import ImapEmailClient
14
+ from .finder import EmailCodeFinder
15
+
16
+ __all__ = ["EmailCodeFinder", "ImapEmailClient"]
17
+ __version__ = "0.1.0"