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.
- email_code_finder-0.1.0/MANIFEST.in +4 -0
- email_code_finder-0.1.0/PKG-INFO +253 -0
- email_code_finder-0.1.0/README.md +227 -0
- email_code_finder-0.1.0/pyproject.toml +60 -0
- email_code_finder-0.1.0/setup.cfg +4 -0
- email_code_finder-0.1.0/src/email_code_finder/__init__.py +17 -0
- email_code_finder-0.1.0/src/email_code_finder/client.py +167 -0
- email_code_finder-0.1.0/src/email_code_finder/finder.py +170 -0
- email_code_finder-0.1.0/src/email_code_finder/py.typed +0 -0
- email_code_finder-0.1.0/src/email_code_finder.egg-info/PKG-INFO +253 -0
- email_code_finder-0.1.0/src/email_code_finder.egg-info/SOURCES.txt +14 -0
- email_code_finder-0.1.0/src/email_code_finder.egg-info/dependency_links.txt +1 -0
- email_code_finder-0.1.0/src/email_code_finder.egg-info/requires.txt +3 -0
- email_code_finder-0.1.0/src/email_code_finder.egg-info/top_level.txt +1 -0
- email_code_finder-0.1.0/tests/test_client.py +82 -0
- email_code_finder-0.1.0/tests/test_finder.py +89 -0
|
@@ -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
|
+
[](https://pypi.org/project/email-code-finder/)
|
|
30
|
+
[](https://pypi.org/project/email-code-finder/)
|
|
31
|
+
[](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
|
+
[](https://pypi.org/project/email-code-finder/)
|
|
4
|
+
[](https://pypi.org/project/email-code-finder/)
|
|
5
|
+
[](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,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"
|