cpanel-mail-mcp 0.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodrigo Sauceda
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,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: cpanel-mail-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server for IMAP/SMTP email — multi-account, attachments, ICS calendar invites, drafts, Save-to-Sent.
5
+ Author-email: Rodrigo Sauceda <rodrigo.sauceda@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rsauceda/cpanel-mail-mcp
8
+ Project-URL: Repository, https://github.com/rsauceda/cpanel-mail-mcp
9
+ Project-URL: Issues, https://github.com/rsauceda/cpanel-mail-mcp/issues
10
+ Keywords: mcp,email,imap,smtp,cpanel,calendar,ics,attachments
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.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Email
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp[cli]>=1.2.0
24
+ Requires-Dist: python-dotenv>=1.0.0
25
+ Dynamic: license-file
26
+
27
+ # cpanel-mail-mcp
28
+
29
+ MCP server for IMAP/SMTP email accounts — works with cPanel, Gmail
30
+ (app passwords), Outlook, Fastmail, iCloud, or any provider that speaks
31
+ plain IMAP + SMTP.
32
+
33
+ ## Features
34
+
35
+ * **Multi-account** — manage multiple email accounts from different providers
36
+ * **Read, search, list** — full IMAP support with folder browsing
37
+ * **Send emails** — plain text, HTML, or both (multipart/alternative)
38
+ * **Attachments** — send via file path or base64-encoded inline data
39
+ * **Download attachments** — extract attachments from received emails as base64
40
+ * **Calendar invites** — send proper ICS invitations with Accept/Decline buttons
41
+ * **Save to Sent** — automatically saves sent emails to the Sent folder via IMAP
42
+ * **Optional send gate** — configurable confirmation code to prevent accidental sends
43
+ * **International folders** — handles UTF-7 encoded folder names (German, etc.)
44
+ * **Compact MCP surface** — one `email` tool with lazy action discovery to reduce
45
+ client context use
46
+
47
+ ## Install
48
+
49
+ ### With `uvx` (recommended, no venv setup)
50
+
51
+ ```bash
52
+ claude mcp add cpanel-mail \
53
+ -e CPANEL_USER=you@example.com \
54
+ -e CPANEL_PASS='your_password' \
55
+ -e CPANEL_SMTP_HOST=mail.example.com \
56
+ -e CPANEL_IMAP_HOST=mail.example.com \
57
+ -- uvx cpanel-mail-mcp
58
+ ```
59
+
60
+ ### With `pipx`
61
+
62
+ ```bash
63
+ pipx install cpanel-mail-mcp
64
+ claude mcp add cpanel-mail \
65
+ -e CPANEL_USER=you@example.com -e CPANEL_PASS='...' \
66
+ -e CPANEL_SMTP_HOST=mail.example.com -e CPANEL_IMAP_HOST=mail.example.com \
67
+ -- cpanel-mail-mcp
68
+ ```
69
+
70
+ ### Development install (from a git checkout)
71
+
72
+ ```bash
73
+ git clone https://github.com/rsauceda/cpanel-mail-mcp
74
+ cd cpanel-mail-mcp
75
+ python -m venv .venv && source .venv/bin/activate
76
+ pip install -e .
77
+ cp .env.example .env # edit
78
+ cpanel-mail-mcp # runs the stdio server
79
+ # or: python -m cpanel_mail_mcp
80
+ ```
81
+
82
+ ## Configuration
83
+
84
+ Three ways, in priority order:
85
+
86
+ ### 1. `EMAIL_ACCOUNTS_JSON` (best for multi-account)
87
+
88
+ ```bash
89
+ export EMAIL_ACCOUNTS_JSON='[
90
+ {"name":"work","user":"me@work.com","password":"...",
91
+ "smtp_host":"mail.work.com","imap_host":"mail.work.com",
92
+ "sent_folder":"INBOX.Sent"},
93
+ {"name":"gmail","user":"me@gmail.com","password":"app_pass",
94
+ "smtp_host":"smtp.gmail.com","smtp_port":587,
95
+ "imap_host":"imap.gmail.com","sent_folder":"[Gmail]/Sent Mail"}
96
+ ]'
97
+ ```
98
+
99
+ ### 2. `EMAIL_ACCOUNTS_FILE`
100
+
101
+ Point to a JSON file with the same shape.
102
+
103
+ ### 3. Legacy single-account (`CPANEL_*`)
104
+
105
+ Backwards compatible with 0.1.x installs. See [`.env.example`](.env.example).
106
+
107
+ ### Account fields
108
+
109
+ | field | required | default |
110
+ |-----------------|----------|-----------|
111
+ | `name` | no | `user` |
112
+ | `user` | **yes** | — |
113
+ | `password` | **yes** | — |
114
+ | `smtp_host` | **yes** | — |
115
+ | `smtp_port` | no | `465` |
116
+ | `imap_host` | **yes** | — |
117
+ | `imap_port` | no | `993` |
118
+ | `sent_folder` | no | `Sent` |
119
+ | `drafts_folder` | no | `Drafts` |
120
+ | `save_to_sent` | no | `true` |
121
+ | `from_name` | no | — |
122
+
123
+ ### Loading a dev `.env`
124
+
125
+ The server auto-loads `.env` from the current working directory. Point it
126
+ elsewhere with:
127
+
128
+ ```bash
129
+ export EMAIL_ENV_FILE=/absolute/path/to/.env
130
+ ```
131
+
132
+ ## Usage — the `email` tool
133
+
134
+ The server exposes **one** MCP tool named `email`. Call it with an `action`
135
+ string and a `params` dict. Discover actions with `action='help'` — the
136
+ default when `action` is omitted:
137
+
138
+ ```json
139
+ { "action": "help" }
140
+ ```
141
+
142
+ ### Actions at a glance
143
+
144
+ | action | purpose |
145
+ |-----------------------|--------------------------------------------------|
146
+ | `help` | list all actions and their signatures |
147
+ | `list_accounts` | see configured accounts (no secrets returned) |
148
+ | `list_folders` | list IMAP folders (UTF-7 decoded) |
149
+ | `list_recent` | last N messages of a folder |
150
+ | `search` | IMAP search by FROM/TO/SUBJECT/BODY/TEXT |
151
+ | `read` | read a message by UID (optionally with attachments) |
152
+ | `download_attachments`| fetch attachments as base64 |
153
+ | `send` | send an email (text/HTML/attachments) |
154
+ | `save_draft` | append a draft to the account's Drafts folder |
155
+ | `send_invite` | send an ICS calendar invite |
156
+
157
+ ### Send an email with an attachment
158
+
159
+ ```json
160
+ {
161
+ "action": "send",
162
+ "params": {
163
+ "to": "someone@example.com",
164
+ "subject": "Report",
165
+ "text": "See attached.",
166
+ "html": "<p>See <b>attached</b>.</p>",
167
+ "attachments": [
168
+ {"path": "/tmp/report.pdf"},
169
+ {"name": "note.txt", "content": "hi from inline"}
170
+ ]
171
+ }
172
+ }
173
+ ```
174
+
175
+ Attachment shapes accepted:
176
+ - `{"path": "/local/file.pdf", "name": "renamed.pdf"?}` — read from disk
177
+ - `{"name": "x.bin", "content_base64": "..."}` — inline base64
178
+ - `{"name": "x.txt", "content": "hello", "mime": "text/plain"?}` — inline text
179
+
180
+ ### Read a message and its attachments
181
+
182
+ ```json
183
+ {
184
+ "action": "read",
185
+ "params": {"uid": "42", "folder": "INBOX", "include_attachments": true}
186
+ }
187
+ ```
188
+
189
+ ### Download only specific attachments
190
+
191
+ ```json
192
+ {
193
+ "action": "download_attachments",
194
+ "params": {"uid": "42", "filenames": ["report.pdf"]}
195
+ }
196
+ ```
197
+
198
+ ### Send a calendar invite
199
+
200
+ ```json
201
+ {
202
+ "action": "send_invite",
203
+ "params": {
204
+ "to": "guest@example.com",
205
+ "subject": "Kickoff",
206
+ "start": "2026-07-25 09:00",
207
+ "end": "2026-07-25 10:00",
208
+ "location": "Zoom link here",
209
+ "description": "quarter review"
210
+ }
211
+ }
212
+ ```
213
+
214
+ Datetimes accept ISO 8601 (`2026-07-25T09:00:00-06:00`) or
215
+ `YYYY-MM-DD HH:MM`. Naive times are assumed UTC.
216
+
217
+ ### Save a draft
218
+
219
+ ```json
220
+ {"action": "save_draft", "params": {"subject": "todo", "text": "..."}}
221
+ ```
222
+
223
+ ### Search
224
+
225
+ ```json
226
+ {"action": "search", "params": {"query": "invoice", "field": "SUBJECT"}}
227
+ ```
228
+
229
+ ### Pick an account (multi-account setup)
230
+
231
+ Any action accepts an optional `account` param; omit it to use the first
232
+ configured account:
233
+
234
+ ```json
235
+ {"action": "send", "params": {"account": "work", "to": "...", "text": "..."}}
236
+ ```
237
+
238
+ ## Send gate (recommended when an agent has this tool)
239
+
240
+ Prevent accidental sends by requiring a shared secret:
241
+
242
+ ```bash
243
+ export EMAIL_SEND_CONFIRMATION_CODE=please-send
244
+ ```
245
+
246
+ Every `send` / `send_invite` call must include `confirm: "please-send"` in
247
+ params, or the server refuses.
248
+
249
+ ## Security notes
250
+
251
+ * `.env` is git-ignored. Never commit it.
252
+ * Prefer a **dedicated mailbox** (e.g. `mcp@yourdomain.com`) with a small quota.
253
+ * If your provider supports **app passwords** (Gmail, Fastmail, iCloud), use one
254
+ instead of your primary password.
255
+ * MCP env vars end up in `~/.claude.json` on your machine — treat that file
256
+ like a keychain.
257
+
258
+ ## License
259
+
260
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,234 @@
1
+ # cpanel-mail-mcp
2
+
3
+ MCP server for IMAP/SMTP email accounts — works with cPanel, Gmail
4
+ (app passwords), Outlook, Fastmail, iCloud, or any provider that speaks
5
+ plain IMAP + SMTP.
6
+
7
+ ## Features
8
+
9
+ * **Multi-account** — manage multiple email accounts from different providers
10
+ * **Read, search, list** — full IMAP support with folder browsing
11
+ * **Send emails** — plain text, HTML, or both (multipart/alternative)
12
+ * **Attachments** — send via file path or base64-encoded inline data
13
+ * **Download attachments** — extract attachments from received emails as base64
14
+ * **Calendar invites** — send proper ICS invitations with Accept/Decline buttons
15
+ * **Save to Sent** — automatically saves sent emails to the Sent folder via IMAP
16
+ * **Optional send gate** — configurable confirmation code to prevent accidental sends
17
+ * **International folders** — handles UTF-7 encoded folder names (German, etc.)
18
+ * **Compact MCP surface** — one `email` tool with lazy action discovery to reduce
19
+ client context use
20
+
21
+ ## Install
22
+
23
+ ### With `uvx` (recommended, no venv setup)
24
+
25
+ ```bash
26
+ claude mcp add cpanel-mail \
27
+ -e CPANEL_USER=you@example.com \
28
+ -e CPANEL_PASS='your_password' \
29
+ -e CPANEL_SMTP_HOST=mail.example.com \
30
+ -e CPANEL_IMAP_HOST=mail.example.com \
31
+ -- uvx cpanel-mail-mcp
32
+ ```
33
+
34
+ ### With `pipx`
35
+
36
+ ```bash
37
+ pipx install cpanel-mail-mcp
38
+ claude mcp add cpanel-mail \
39
+ -e CPANEL_USER=you@example.com -e CPANEL_PASS='...' \
40
+ -e CPANEL_SMTP_HOST=mail.example.com -e CPANEL_IMAP_HOST=mail.example.com \
41
+ -- cpanel-mail-mcp
42
+ ```
43
+
44
+ ### Development install (from a git checkout)
45
+
46
+ ```bash
47
+ git clone https://github.com/rsauceda/cpanel-mail-mcp
48
+ cd cpanel-mail-mcp
49
+ python -m venv .venv && source .venv/bin/activate
50
+ pip install -e .
51
+ cp .env.example .env # edit
52
+ cpanel-mail-mcp # runs the stdio server
53
+ # or: python -m cpanel_mail_mcp
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Three ways, in priority order:
59
+
60
+ ### 1. `EMAIL_ACCOUNTS_JSON` (best for multi-account)
61
+
62
+ ```bash
63
+ export EMAIL_ACCOUNTS_JSON='[
64
+ {"name":"work","user":"me@work.com","password":"...",
65
+ "smtp_host":"mail.work.com","imap_host":"mail.work.com",
66
+ "sent_folder":"INBOX.Sent"},
67
+ {"name":"gmail","user":"me@gmail.com","password":"app_pass",
68
+ "smtp_host":"smtp.gmail.com","smtp_port":587,
69
+ "imap_host":"imap.gmail.com","sent_folder":"[Gmail]/Sent Mail"}
70
+ ]'
71
+ ```
72
+
73
+ ### 2. `EMAIL_ACCOUNTS_FILE`
74
+
75
+ Point to a JSON file with the same shape.
76
+
77
+ ### 3. Legacy single-account (`CPANEL_*`)
78
+
79
+ Backwards compatible with 0.1.x installs. See [`.env.example`](.env.example).
80
+
81
+ ### Account fields
82
+
83
+ | field | required | default |
84
+ |-----------------|----------|-----------|
85
+ | `name` | no | `user` |
86
+ | `user` | **yes** | — |
87
+ | `password` | **yes** | — |
88
+ | `smtp_host` | **yes** | — |
89
+ | `smtp_port` | no | `465` |
90
+ | `imap_host` | **yes** | — |
91
+ | `imap_port` | no | `993` |
92
+ | `sent_folder` | no | `Sent` |
93
+ | `drafts_folder` | no | `Drafts` |
94
+ | `save_to_sent` | no | `true` |
95
+ | `from_name` | no | — |
96
+
97
+ ### Loading a dev `.env`
98
+
99
+ The server auto-loads `.env` from the current working directory. Point it
100
+ elsewhere with:
101
+
102
+ ```bash
103
+ export EMAIL_ENV_FILE=/absolute/path/to/.env
104
+ ```
105
+
106
+ ## Usage — the `email` tool
107
+
108
+ The server exposes **one** MCP tool named `email`. Call it with an `action`
109
+ string and a `params` dict. Discover actions with `action='help'` — the
110
+ default when `action` is omitted:
111
+
112
+ ```json
113
+ { "action": "help" }
114
+ ```
115
+
116
+ ### Actions at a glance
117
+
118
+ | action | purpose |
119
+ |-----------------------|--------------------------------------------------|
120
+ | `help` | list all actions and their signatures |
121
+ | `list_accounts` | see configured accounts (no secrets returned) |
122
+ | `list_folders` | list IMAP folders (UTF-7 decoded) |
123
+ | `list_recent` | last N messages of a folder |
124
+ | `search` | IMAP search by FROM/TO/SUBJECT/BODY/TEXT |
125
+ | `read` | read a message by UID (optionally with attachments) |
126
+ | `download_attachments`| fetch attachments as base64 |
127
+ | `send` | send an email (text/HTML/attachments) |
128
+ | `save_draft` | append a draft to the account's Drafts folder |
129
+ | `send_invite` | send an ICS calendar invite |
130
+
131
+ ### Send an email with an attachment
132
+
133
+ ```json
134
+ {
135
+ "action": "send",
136
+ "params": {
137
+ "to": "someone@example.com",
138
+ "subject": "Report",
139
+ "text": "See attached.",
140
+ "html": "<p>See <b>attached</b>.</p>",
141
+ "attachments": [
142
+ {"path": "/tmp/report.pdf"},
143
+ {"name": "note.txt", "content": "hi from inline"}
144
+ ]
145
+ }
146
+ }
147
+ ```
148
+
149
+ Attachment shapes accepted:
150
+ - `{"path": "/local/file.pdf", "name": "renamed.pdf"?}` — read from disk
151
+ - `{"name": "x.bin", "content_base64": "..."}` — inline base64
152
+ - `{"name": "x.txt", "content": "hello", "mime": "text/plain"?}` — inline text
153
+
154
+ ### Read a message and its attachments
155
+
156
+ ```json
157
+ {
158
+ "action": "read",
159
+ "params": {"uid": "42", "folder": "INBOX", "include_attachments": true}
160
+ }
161
+ ```
162
+
163
+ ### Download only specific attachments
164
+
165
+ ```json
166
+ {
167
+ "action": "download_attachments",
168
+ "params": {"uid": "42", "filenames": ["report.pdf"]}
169
+ }
170
+ ```
171
+
172
+ ### Send a calendar invite
173
+
174
+ ```json
175
+ {
176
+ "action": "send_invite",
177
+ "params": {
178
+ "to": "guest@example.com",
179
+ "subject": "Kickoff",
180
+ "start": "2026-07-25 09:00",
181
+ "end": "2026-07-25 10:00",
182
+ "location": "Zoom link here",
183
+ "description": "quarter review"
184
+ }
185
+ }
186
+ ```
187
+
188
+ Datetimes accept ISO 8601 (`2026-07-25T09:00:00-06:00`) or
189
+ `YYYY-MM-DD HH:MM`. Naive times are assumed UTC.
190
+
191
+ ### Save a draft
192
+
193
+ ```json
194
+ {"action": "save_draft", "params": {"subject": "todo", "text": "..."}}
195
+ ```
196
+
197
+ ### Search
198
+
199
+ ```json
200
+ {"action": "search", "params": {"query": "invoice", "field": "SUBJECT"}}
201
+ ```
202
+
203
+ ### Pick an account (multi-account setup)
204
+
205
+ Any action accepts an optional `account` param; omit it to use the first
206
+ configured account:
207
+
208
+ ```json
209
+ {"action": "send", "params": {"account": "work", "to": "...", "text": "..."}}
210
+ ```
211
+
212
+ ## Send gate (recommended when an agent has this tool)
213
+
214
+ Prevent accidental sends by requiring a shared secret:
215
+
216
+ ```bash
217
+ export EMAIL_SEND_CONFIRMATION_CODE=please-send
218
+ ```
219
+
220
+ Every `send` / `send_invite` call must include `confirm: "please-send"` in
221
+ params, or the server refuses.
222
+
223
+ ## Security notes
224
+
225
+ * `.env` is git-ignored. Never commit it.
226
+ * Prefer a **dedicated mailbox** (e.g. `mcp@yourdomain.com`) with a small quota.
227
+ * If your provider supports **app passwords** (Gmail, Fastmail, iCloud), use one
228
+ instead of your primary password.
229
+ * MCP env vars end up in `~/.claude.json` on your machine — treat that file
230
+ like a keychain.
231
+
232
+ ## License
233
+
234
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "cpanel-mail-mcp"
3
+ version = "0.2.0"
4
+ description = "MCP server for IMAP/SMTP email — multi-account, attachments, ICS calendar invites, drafts, Save-to-Sent."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [{ name = "Rodrigo Sauceda", email = "rodrigo.sauceda@gmail.com" }]
9
+ keywords = ["mcp", "email", "imap", "smtp", "cpanel", "calendar", "ics", "attachments"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Communications :: Email",
20
+ ]
21
+ dependencies = [
22
+ "mcp[cli]>=1.2.0",
23
+ "python-dotenv>=1.0.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/rsauceda/cpanel-mail-mcp"
28
+ Repository = "https://github.com/rsauceda/cpanel-mail-mcp"
29
+ Issues = "https://github.com/rsauceda/cpanel-mail-mcp/issues"
30
+
31
+ [project.scripts]
32
+ cpanel-mail-mcp = "cpanel_mail_mcp.server:main"
33
+
34
+ [build-system]
35
+ requires = ["setuptools>=68"]
36
+ build-backend = "setuptools.build_meta"
37
+
38
+ [tool.setuptools]
39
+ package-dir = {"" = "src"}
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """cpanel-mail-mcp — MCP server for IMAP/SMTP email accounts."""
2
+ from .server import main, mcp
3
+
4
+ __version__ = "0.2.0"
5
+ __all__ = ["main", "mcp", "__version__"]
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,103 @@
1
+ """Account discovery from environment.
2
+
3
+ Priority:
4
+ 1. `EMAIL_ACCOUNTS_JSON` — JSON array of account dicts.
5
+ 2. `EMAIL_ACCOUNTS_FILE` — path to a JSON file with the same array.
6
+ 3. Legacy `CPANEL_USER`/`CPANEL_PASS`/... single account.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Account:
18
+ name: str
19
+ user: str
20
+ password: str
21
+ smtp_host: str
22
+ smtp_port: int
23
+ imap_host: str
24
+ imap_port: int
25
+ sent_folder: str
26
+ drafts_folder: str
27
+ save_to_sent: bool
28
+ from_name: str | None
29
+
30
+
31
+ def _from_dict(d: dict) -> Account:
32
+ if not d.get("user") or not d.get("password"):
33
+ raise ValueError(f"account {d.get('name')!r} missing user/password")
34
+ host = d.get("host") or ""
35
+ return Account(
36
+ name=str(d.get("name") or d["user"]),
37
+ user=d["user"],
38
+ password=d["password"],
39
+ smtp_host=d.get("smtp_host") or host,
40
+ smtp_port=int(d.get("smtp_port", 465)),
41
+ imap_host=d.get("imap_host") or host,
42
+ imap_port=int(d.get("imap_port", 993)),
43
+ sent_folder=d.get("sent_folder", "Sent"),
44
+ drafts_folder=d.get("drafts_folder", "Drafts"),
45
+ save_to_sent=bool(d.get("save_to_sent", True)),
46
+ from_name=d.get("from_name"),
47
+ )
48
+
49
+
50
+ def _legacy() -> Account | None:
51
+ user = os.environ.get("CPANEL_USER")
52
+ pw = os.environ.get("CPANEL_PASS")
53
+ if not user or not pw:
54
+ return None
55
+ return _from_dict(
56
+ {
57
+ "name": os.environ.get("CPANEL_ACCOUNT_NAME", "default"),
58
+ "user": user,
59
+ "password": pw,
60
+ "smtp_host": os.environ.get("CPANEL_SMTP_HOST", ""),
61
+ "smtp_port": int(os.environ.get("CPANEL_SMTP_PORT", "465")),
62
+ "imap_host": os.environ.get("CPANEL_IMAP_HOST", ""),
63
+ "imap_port": int(os.environ.get("CPANEL_IMAP_PORT", "993")),
64
+ "sent_folder": os.environ.get("CPANEL_SENT_FOLDER", "INBOX.Sent"),
65
+ "drafts_folder": os.environ.get("CPANEL_DRAFTS_FOLDER", "INBOX.Drafts"),
66
+ "save_to_sent": os.environ.get("CPANEL_SAVE_TO_SENT", "true").lower() != "false",
67
+ "from_name": os.environ.get("CPANEL_FROM_NAME"),
68
+ }
69
+ )
70
+
71
+
72
+ def load_accounts() -> dict[str, Account]:
73
+ accts: list[Account] = []
74
+ raw = os.environ.get("EMAIL_ACCOUNTS_JSON")
75
+ if raw:
76
+ accts.extend(_from_dict(d) for d in json.loads(raw))
77
+ else:
78
+ path = os.environ.get("EMAIL_ACCOUNTS_FILE")
79
+ if path:
80
+ accts.extend(_from_dict(d) for d in json.loads(Path(path).read_text()))
81
+ if not accts:
82
+ legacy = _legacy()
83
+ if legacy:
84
+ accts.append(legacy)
85
+ if not accts:
86
+ raise RuntimeError(
87
+ "No email accounts configured. Set EMAIL_ACCOUNTS_JSON, "
88
+ "EMAIL_ACCOUNTS_FILE, or the legacy CPANEL_USER/CPANEL_PASS/... env vars."
89
+ )
90
+ out: dict[str, Account] = {}
91
+ for a in accts:
92
+ if a.name in out:
93
+ raise ValueError(f"duplicate account name: {a.name!r}")
94
+ out[a.name] = a
95
+ return out
96
+
97
+
98
+ def get_account(accounts: dict[str, Account], name: str | None) -> Account:
99
+ if not name:
100
+ return next(iter(accounts.values()))
101
+ if name in accounts:
102
+ return accounts[name]
103
+ raise ValueError(f"unknown account {name!r}. Available: {list(accounts)}")