langchain-atomicmail 0.3.14__py3-none-any.whl

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 (44) hide show
  1. atomicmail/__init__.py +32 -0
  2. atomicmail/auth_http.py +177 -0
  3. atomicmail/cli.py +236 -0
  4. atomicmail/config.py +112 -0
  5. atomicmail/constants.py +32 -0
  6. atomicmail/credentials.py +158 -0
  7. atomicmail/help.py +118 -0
  8. atomicmail/jmap_request.py +918 -0
  9. atomicmail/jwt_utils.py +34 -0
  10. atomicmail/mcp_server.py +341 -0
  11. atomicmail/pow.py +71 -0
  12. atomicmail/session.py +595 -0
  13. atomicmail/shared_assets.py +67 -0
  14. atomicmail/vendor/shared/consts.json +11 -0
  15. atomicmail/vendor/shared/fixtures/pow_vectors.json +32 -0
  16. atomicmail/vendor/shared/help/fragments/inbox_cron_agent_prompt.md +1 -0
  17. atomicmail/vendor/shared/help/fragments/post_register_cron_reminder.md +5 -0
  18. atomicmail/vendor/shared/help/readme_stub.md +3 -0
  19. atomicmail/vendor/shared/help/topics/auth.md +8 -0
  20. atomicmail/vendor/shared/help/topics/cron.md +217 -0
  21. atomicmail/vendor/shared/help/topics/installation.md +35 -0
  22. atomicmail/vendor/shared/help/topics/jmap_cheatsheet.md +19 -0
  23. atomicmail/vendor/shared/help/topics/multi_account.md +9 -0
  24. atomicmail/vendor/shared/help/topics/overview.md +27 -0
  25. atomicmail/vendor/shared/help/topics/presets.md +12 -0
  26. atomicmail/vendor/shared/help/topics/tools.md +16 -0
  27. atomicmail/vendor/shared/help/topics/troubleshooting.md +6 -0
  28. atomicmail/vendor/shared/manifest.json +31 -0
  29. atomicmail/vendor/shared/messages/errors.json +68 -0
  30. atomicmail/vendor/shared/messages/hints.json +8 -0
  31. atomicmail/vendor/shared/presets/list_inbox.json +46 -0
  32. atomicmail/vendor/shared/presets/reply.json +97 -0
  33. atomicmail/vendor/shared/presets/send_mail.json +70 -0
  34. atomicmail/vendor/shared/presets/send_mail_attachment.json +92 -0
  35. atomicmail/vendor/shared/presets/send_mail_blob_attachment.json +74 -0
  36. atomicmail/vendor/shared/skill/SKILL.template.md +202 -0
  37. atomicmail/vendor/shared/skill/manifest.json +89 -0
  38. langchain_atomicmail/__init__.py +23 -0
  39. langchain_atomicmail/toolkit.py +15 -0
  40. langchain_atomicmail/tools.py +251 -0
  41. langchain_atomicmail-0.3.14.dist-info/METADATA +136 -0
  42. langchain_atomicmail-0.3.14.dist-info/RECORD +44 -0
  43. langchain_atomicmail-0.3.14.dist-info/WHEEL +5 -0
  44. langchain_atomicmail-0.3.14.dist-info/top_level.txt +2 -0
atomicmail/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """Atomic Mail Python shared foundation package."""
2
+
3
+ from .config import ResolvedAgentConfig, resolve_agent_config_from_env
4
+ from .credentials import (
5
+ CredentialStore,
6
+ Credentials,
7
+ SkillFiles,
8
+ default_files_from_out_dir,
9
+ )
10
+ from .help import help
11
+ from .jmap_request import JmapAttachmentInput, JmapRequestResult, jmap_request, run_jmap_request
12
+ from .mcp_server import handle_tool_call
13
+ from .session import AgentSession, RegisterResult, create_agent_session, register
14
+
15
+ __all__ = [
16
+ "AgentSession",
17
+ "Credentials",
18
+ "CredentialStore",
19
+ "JmapRequestResult",
20
+ "JmapAttachmentInput",
21
+ "RegisterResult",
22
+ "ResolvedAgentConfig",
23
+ "SkillFiles",
24
+ "default_files_from_out_dir",
25
+ "help",
26
+ "handle_tool_call",
27
+ "jmap_request",
28
+ "run_jmap_request",
29
+ "register",
30
+ "create_agent_session",
31
+ "resolve_agent_config_from_env",
32
+ ]
@@ -0,0 +1,177 @@
1
+ """Auth-service HTTP helpers: challenge -> session -> capability."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Mapping
8
+ from urllib.error import HTTPError
9
+ from urllib.request import Request, urlopen
10
+
11
+ from .jwt_utils import decode_jwt_payload
12
+ from .pow import solve_pow
13
+
14
+
15
+ @dataclass
16
+ class ChallengeResponse:
17
+ challengeJWT: str
18
+ challenge: str
19
+ difficulty: int
20
+
21
+
22
+ @dataclass
23
+ class SessionResponse:
24
+ sessionJWT: str
25
+ apiKey: str | None = None
26
+
27
+
28
+ def fetch_challenge(auth_url: str) -> ChallengeResponse:
29
+ base = auth_url.rstrip("/")
30
+ status, text, headers = _http_post(f"{base}/api/v1/challenge")
31
+ if status < 200 or status >= 300:
32
+ raise ValueError(
33
+ f"auth-service /api/v1/challenge returned {status}: {text}"
34
+ )
35
+
36
+ challenge_jwt = _read_bearer_token(
37
+ headers.get("Authorization"),
38
+ "Challenge response missing Authorization bearer token.",
39
+ )
40
+ payload = decode_jwt_payload(challenge_jwt)
41
+ challenge = payload.get("jti")
42
+ difficulty = payload.get("difficulty")
43
+ if not isinstance(challenge, str) or not isinstance(difficulty, (int, float)):
44
+ raise ValueError("Challenge JWT payload malformed (missing jti or difficulty).")
45
+
46
+ return ChallengeResponse(
47
+ challengeJWT=challenge_jwt,
48
+ challenge=challenge,
49
+ difficulty=int(difficulty),
50
+ )
51
+
52
+
53
+ def exchange_session(
54
+ auth_url: str,
55
+ *,
56
+ challenge_jwt: str,
57
+ pow_hex: str,
58
+ nonce: str,
59
+ api_key: str | None = None,
60
+ username: str | None = None,
61
+ ) -> SessionResponse:
62
+ base = auth_url.rstrip("/")
63
+ payload: dict[str, str] = {"powHex": pow_hex, "nonce": nonce}
64
+ if api_key:
65
+ payload["apiKey"] = api_key
66
+ if username:
67
+ payload["username"] = username
68
+
69
+ status, text, headers = _http_post(
70
+ f"{base}/api/v1/session",
71
+ headers={"Authorization": f"Bearer {challenge_jwt}"},
72
+ json_body=payload,
73
+ )
74
+ if status < 200 or status >= 300:
75
+ raise ValueError(f"auth-service /api/v1/session returned {status}: {text}")
76
+
77
+ session_jwt = _read_bearer_token(
78
+ headers.get("Authorization"),
79
+ "Session response missing Authorization bearer token.",
80
+ )
81
+
82
+ data: dict[str, object] = {}
83
+ if text.strip():
84
+ try:
85
+ parsed = json.loads(text)
86
+ except json.JSONDecodeError as err:
87
+ raise ValueError(
88
+ "auth-service /api/v1/session returned non-JSON body."
89
+ ) from err
90
+ if not isinstance(parsed, dict):
91
+ raise ValueError("auth-service /api/v1/session returned non-JSON body.")
92
+ data = parsed
93
+
94
+ api_key_out = data.get("apiKey")
95
+ return SessionResponse(
96
+ sessionJWT=session_jwt,
97
+ apiKey=api_key_out if isinstance(api_key_out, str) else None,
98
+ )
99
+
100
+
101
+ def fetch_capability(auth_url: str, session_jwt: str) -> str:
102
+ base = auth_url.rstrip("/")
103
+ status, text, headers = _http_post(
104
+ f"{base}/api/v1/capability",
105
+ headers={"Authorization": f"Bearer {session_jwt}"},
106
+ )
107
+ if status < 200 or status >= 300:
108
+ raise ValueError(f"auth-service /api/v1/capability returned {status}: {text}")
109
+
110
+ return _read_bearer_token(
111
+ headers.get("Authorization"),
112
+ "Capability response missing Authorization bearer token.",
113
+ )
114
+
115
+
116
+ def perform_pow_and_session(
117
+ *,
118
+ auth_url: str,
119
+ scrypt_salt: str,
120
+ api_key: str | None = None,
121
+ username: str | None = None,
122
+ on_pow_progress: Callable[[int], None] | None = None,
123
+ ) -> SessionResponse:
124
+ challenge = fetch_challenge(auth_url)
125
+ solved = solve_pow(
126
+ challenge=challenge.challenge,
127
+ difficulty=challenge.difficulty,
128
+ salt=scrypt_salt,
129
+ on_progress=on_pow_progress,
130
+ )
131
+ return exchange_session(
132
+ auth_url,
133
+ challenge_jwt=challenge.challengeJWT,
134
+ pow_hex=solved.powHex,
135
+ nonce=solved.nonce,
136
+ api_key=api_key,
137
+ username=username,
138
+ )
139
+
140
+
141
+ def _read_bearer_token(header_value: str | None, missing_error: str) -> str:
142
+ if not header_value:
143
+ raise ValueError(missing_error)
144
+
145
+ raw = header_value.strip()
146
+ prefix = "bearer "
147
+ if not raw.lower().startswith(prefix):
148
+ raise ValueError("Authorization header must use Bearer scheme.")
149
+
150
+ token = raw[len(prefix) :].strip()
151
+ if not token:
152
+ raise ValueError("Authorization header must use Bearer scheme.")
153
+ return token
154
+
155
+
156
+ def _http_post(
157
+ url: str,
158
+ *,
159
+ headers: Mapping[str, str] | None = None,
160
+ json_body: object | None = None,
161
+ ) -> tuple[int, str, Mapping[str, str]]:
162
+ req_headers = dict(headers or {})
163
+ body_bytes: bytes | None = None
164
+ if json_body is not None:
165
+ body_bytes = json.dumps(json_body).encode("utf-8")
166
+ req_headers.setdefault("Content-Type", "application/json")
167
+
168
+ req = Request(url, data=body_bytes, headers=req_headers, method="POST")
169
+ try:
170
+ with urlopen(req) as response:
171
+ return (
172
+ int(response.getcode()),
173
+ response.read().decode("utf-8"),
174
+ response.headers,
175
+ )
176
+ except HTTPError as err:
177
+ return err.code, err.read().decode("utf-8", errors="replace"), err.headers
atomicmail/cli.py ADDED
@@ -0,0 +1,236 @@
1
+ """Atomic Mail CLI adapter for Python library."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Mapping, Sequence
9
+
10
+ from .help import HELP_TOPIC_LIST, help as get_help, normalize_help_topic
11
+ from .jmap_request import DEFAULT_JMAP_USING, JmapAttachmentInput, jmap_request
12
+ from .shared_assets import try_read_shared_json
13
+ from .session import register
14
+
15
+
16
+ def _shared_errors() -> dict[str, str]:
17
+ loaded = try_read_shared_json("messages/errors.json")
18
+ if isinstance(loaded, dict):
19
+ return {k: v for k, v in loaded.items() if isinstance(k, str) and isinstance(v, str)}
20
+ return {}
21
+
22
+
23
+ _ERRORS = _shared_errors()
24
+
25
+
26
+ def _error(key: str, fallback: str) -> str:
27
+ return _ERRORS.get(key, fallback)
28
+
29
+
30
+ def _error_template(key: str, fallback: str, values: Mapping[str, str | int]) -> str:
31
+ out = _ERRORS.get(key, fallback)
32
+ for name, value in values.items():
33
+ out = out.replace(f"{{{name}}}", str(value))
34
+ return out
35
+
36
+
37
+ def _parse_user_vars_json(raw: str) -> dict[str, str]:
38
+ try:
39
+ value = json.loads(raw)
40
+ except json.JSONDecodeError as err:
41
+ raise ValueError(
42
+ _error_template(
43
+ "vars_invalid_json_template",
44
+ "--vars is not valid JSON: {details}",
45
+ {"details": str(err)},
46
+ )
47
+ ) from err
48
+ if not isinstance(value, dict):
49
+ raise ValueError(_error("vars_not_object", "--vars must be a JSON object of { VAR_NAME: string }."))
50
+ out: dict[str, str] = {}
51
+ for key, item in value.items():
52
+ if not isinstance(key, str) or not key:
53
+ raise ValueError(
54
+ _error_template(
55
+ "vars_key_invalid_template",
56
+ "--vars key '{key}' must match /^[A-Z][A-Z0-9_]*$/.",
57
+ {"key": str(key)},
58
+ )
59
+ )
60
+ if not key[0].isalpha() or not key[0].isupper():
61
+ raise ValueError(
62
+ _error_template(
63
+ "vars_key_invalid_template",
64
+ "--vars key '{key}' must match /^[A-Z][A-Z0-9_]*$/.",
65
+ {"key": key},
66
+ )
67
+ )
68
+ if any((not ch.isdigit()) and (not ch.isupper()) and ch != "_" for ch in key[1:]):
69
+ raise ValueError(
70
+ _error_template(
71
+ "vars_key_invalid_template",
72
+ "--vars key '{key}' must match /^[A-Z][A-Z0-9_]*$/.",
73
+ {"key": key},
74
+ )
75
+ )
76
+ if not isinstance(item, str):
77
+ raise ValueError(
78
+ _error_template(
79
+ "vars_value_not_string_template",
80
+ "--vars value for '{key}' must be a string.",
81
+ {"key": key},
82
+ )
83
+ )
84
+ out[key] = item
85
+ return out
86
+
87
+
88
+ def _build_parser() -> argparse.ArgumentParser:
89
+ parser = argparse.ArgumentParser(prog="atomicmail", description="Atomic Mail Python CLI")
90
+ subparsers = parser.add_subparsers(dest="command")
91
+
92
+ register_cmd = subparsers.add_parser(
93
+ "register", help="PoW signup or API-key login and persist credentials"
94
+ )
95
+ register_mode = register_cmd.add_mutually_exclusive_group(required=True)
96
+ register_mode.add_argument("--username", help="Desired username (5-21 characters).")
97
+ register_mode.add_argument("--api-key", help="Existing API key for login.")
98
+ register_cmd.add_argument("--credentials-dir", help="Credential directory for this command.")
99
+ register_cmd.add_argument(
100
+ "--forced",
101
+ action="store_true",
102
+ help="Allow replacing existing credentials for a different username (username mode only).",
103
+ )
104
+
105
+ jmap_cmd = subparsers.add_parser("jmap_request", help="Send a JMAP request")
106
+ jmap_cmd.add_argument("--credentials-dir", help="Credential directory for this command.")
107
+ jmap_ops = jmap_cmd.add_mutually_exclusive_group(required=True)
108
+ jmap_ops.add_argument("--ops", help="Inline JMAP JSON.")
109
+ jmap_ops.add_argument("--ops-file", help="JMAP ops file path or bundled preset name.")
110
+ jmap_cmd.add_argument(
111
+ "--using",
112
+ help="Comma-separated capability URNs used when ops does not provide using.",
113
+ )
114
+ jmap_cmd.add_argument("--vars", help="JSON object with VAR_NAME -> string placeholder values.")
115
+ jmap_cmd.add_argument(
116
+ "--attachment",
117
+ action="append",
118
+ dest="attachments",
119
+ help="Attachment path; repeat for multiple files.",
120
+ )
121
+ jmap_cmd.add_argument(
122
+ "--attachment-path-base",
123
+ help="Base directory for resolving relative attachment paths.",
124
+ )
125
+ jmap_cmd.add_argument(
126
+ "--dry-run",
127
+ action="store_true",
128
+ help="Resolve envelope and print request without sending it.",
129
+ )
130
+
131
+ help_cmd = subparsers.add_parser("help", help="Print Atomic Mail help topic")
132
+ help_cmd.add_argument("--topic", help="Help topic; omit for overview.")
133
+
134
+ return parser
135
+
136
+
137
+ def _cmd_register(args: argparse.Namespace) -> int:
138
+ if args.api_key and args.forced:
139
+ raise ValueError("--forced can only be used with --username.")
140
+
141
+ result = register(
142
+ username=args.username,
143
+ api_key=args.api_key,
144
+ credentials_dir=args.credentials_dir,
145
+ forced=bool(args.forced),
146
+ )
147
+ sys.stdout.write(json.dumps(result.__dict__, indent=2) + "\n")
148
+ return 0
149
+
150
+
151
+ def _cmd_jmap_request(args: argparse.Namespace) -> int:
152
+ attachments: list[JmapAttachmentInput] | None = None
153
+ if args.attachments:
154
+ attachments = [JmapAttachmentInput(path=item) for item in args.attachments]
155
+ if args.dry_run and attachments:
156
+ raise ValueError(
157
+ _error(
158
+ "cli_dry_run_with_attachment",
159
+ "--dry-run cannot be combined with --attachment.",
160
+ )
161
+ )
162
+
163
+ using = list(DEFAULT_JMAP_USING)
164
+ if args.using:
165
+ using = [item.strip() for item in args.using.split(",") if item.strip()]
166
+
167
+ vars_map: dict[str, str] | None = None
168
+ if args.vars is not None:
169
+ vars_map = _parse_user_vars_json(args.vars)
170
+
171
+ result = jmap_request(
172
+ ops=args.ops,
173
+ ops_file=args.ops_file,
174
+ vars=vars_map,
175
+ dry_run=bool(args.dry_run),
176
+ attachments=attachments,
177
+ attachment_path_base=args.attachment_path_base,
178
+ using=using,
179
+ credentials_dir=args.credentials_dir,
180
+ )
181
+ sys.stdout.write(result.bodyText if result.bodyText.endswith("\n") else f"{result.bodyText}\n")
182
+ if result.ok:
183
+ return 0
184
+ sys.stderr.write(f"Error: JMAP request failed (HTTP {result.status}): {result.bodyText}\n")
185
+ return 1
186
+
187
+
188
+ def _cmd_help(args: argparse.Namespace) -> int:
189
+ topic = args.topic
190
+ if topic is not None and normalize_help_topic(topic) == "readme":
191
+ # Python package does not bundle npm README lookup;
192
+ # we intentionally return the same shared stub text as get_help("readme").
193
+ sys.stdout.write(get_help("readme") + "\n")
194
+ return 0
195
+
196
+ sys.stdout.write(get_help(topic) + "\n")
197
+ return 0
198
+
199
+
200
+ def main(argv: Sequence[str] | None = None) -> int:
201
+ parser = _build_parser()
202
+ args = parser.parse_args(list(argv) if argv is not None else None)
203
+
204
+ if not args.command:
205
+ parser.print_help(sys.stderr)
206
+ return 2
207
+
208
+ try:
209
+ if args.command == "register":
210
+ return _cmd_register(args)
211
+ if args.command == "jmap_request":
212
+ return _cmd_jmap_request(args)
213
+ if args.command == "help":
214
+ return _cmd_help(args)
215
+ message = _error_template(
216
+ "cli_unknown_command_template",
217
+ "Unknown command: {cmd}",
218
+ {"cmd": str(args.command)},
219
+ )
220
+ sys.stderr.write(f"Error: {message}\n")
221
+ return 2
222
+ except ValueError as err:
223
+ sys.stderr.write(f"Error: {err}\n")
224
+ return 2
225
+ except Exception as err: # pragma: no cover - defensive CLI adapter guard
226
+ sys.stderr.write(f"Error: {err}\n")
227
+ return 1
228
+
229
+
230
+ def console_main() -> None:
231
+ raise SystemExit(main())
232
+
233
+
234
+ def help_topics_for_cli() -> str:
235
+ """Expose topics for tests/help text parity checks."""
236
+ return ", ".join(HELP_TOPIC_LIST)
atomicmail/config.py ADDED
@@ -0,0 +1,112 @@
1
+ """Configuration resolution for auth/api/credential defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal, Mapping
9
+
10
+ from .constants import (
11
+ DEFAULT_API_URL,
12
+ DEFAULT_AUTH_URL,
13
+ DEFAULT_POW_SCRYPT_SALT_HEX,
14
+ )
15
+ from .credentials import SkillFiles, default_files_from_out_dir, try_read_credentials
16
+
17
+ ConfigSource = Literal["credentials-file", "env", "mixed", "defaults"]
18
+
19
+
20
+ @dataclass
21
+ class ResolvedAgentConfig:
22
+ authUrl: str
23
+ apiUrl: str
24
+ scryptSalt: str
25
+ apiKey: str | None
26
+ inboxId: str | None
27
+ credentialDir: str
28
+ files: SkillFiles
29
+ source: ConfigSource
30
+
31
+
32
+ def resolve_credential_dir() -> str:
33
+ from_env = os.getenv("ATOMIC_MAIL_CREDENTIALS_DIR")
34
+ if from_env:
35
+ return from_env
36
+
37
+ home = os.getenv("HOME") or os.getenv("USERPROFILE")
38
+ if not home:
39
+ raise ValueError(
40
+ "Cannot determine default credential directory: HOME and USERPROFILE "
41
+ "are both unset. Set ATOMIC_MAIL_CREDENTIALS_DIR explicitly."
42
+ )
43
+ home_base = home.rstrip("/\\")
44
+ return f"{home_base}/.atomicmail"
45
+
46
+
47
+ def expand_credential_dir_input(dir_value: str | None = None) -> str:
48
+ raw = dir_value or os.getenv("ATOMIC_MAIL_CREDENTIALS_DIR") or "~/.atomicmail"
49
+ if raw == "~":
50
+ return str(Path.home())
51
+ return str(Path(raw).expanduser().resolve())
52
+
53
+
54
+ def _normalize_url(url: str) -> str:
55
+ return url.rstrip("/")
56
+
57
+
58
+ def _pick_env(env: Mapping[str, str], key: str) -> str | None:
59
+ value = env.get(key)
60
+ return value if value else None
61
+
62
+
63
+ def resolve_agent_config_from_env(
64
+ env: Mapping[str, str] | None = None,
65
+ credential_dir: str | None = None,
66
+ ) -> ResolvedAgentConfig:
67
+ current_env = env or os.environ
68
+ resolved_credential_dir = (
69
+ expand_credential_dir_input(credential_dir)
70
+ if credential_dir is not None
71
+ else resolve_credential_dir()
72
+ )
73
+ files = default_files_from_out_dir(resolved_credential_dir)
74
+ file_creds = try_read_credentials(files.credentialsFile)
75
+
76
+ env_auth_url = _pick_env(current_env, "ATOMIC_MAIL_AUTH_URL")
77
+ env_api_url = _pick_env(current_env, "ATOMIC_MAIL_API_URL")
78
+ env_salt = _pick_env(current_env, "ATOMIC_MAIL_SCRYPT_SALT")
79
+ env_api_key = _pick_env(current_env, "ATOMIC_MAIL_API_KEY")
80
+
81
+ auth_url = env_auth_url or (file_creds.authUrl if file_creds else None) or DEFAULT_AUTH_URL
82
+ api_url = env_api_url or (file_creds.apiUrl if file_creds else None) or DEFAULT_API_URL
83
+ scrypt_salt = (
84
+ env_salt
85
+ or (file_creds.scryptSalt if file_creds else None)
86
+ or DEFAULT_POW_SCRYPT_SALT_HEX
87
+ )
88
+ api_key = env_api_key or (file_creds.apiKey if file_creds else None)
89
+ inbox_id = file_creds.inboxId if file_creds else None
90
+
91
+ using_file = file_creds is not None
92
+ using_env = any((env_auth_url, env_api_url, env_salt, env_api_key))
93
+
94
+ if using_file and using_env:
95
+ source: ConfigSource = "mixed"
96
+ elif using_file:
97
+ source = "credentials-file"
98
+ elif using_env:
99
+ source = "env"
100
+ else:
101
+ source = "defaults"
102
+
103
+ return ResolvedAgentConfig(
104
+ authUrl=_normalize_url(auth_url),
105
+ apiUrl=_normalize_url(api_url),
106
+ scryptSalt=scrypt_salt,
107
+ apiKey=api_key,
108
+ inboxId=inbox_id,
109
+ credentialDir=resolved_credential_dir,
110
+ files=files,
111
+ source=source,
112
+ )
@@ -0,0 +1,32 @@
1
+ """Cross-language constants loaded from shared assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .shared_assets import try_read_shared_json
6
+
7
+ _DEFAULTS = {
8
+ "DEFAULT_POW_SCRYPT_SALT_HEX": "0b980734412c292d6549110276b604ab1dea4883bd460d77d1b984adf8bca083",
9
+ "DEFAULT_AUTH_URL": "https://auth.atomicmail.ai",
10
+ "DEFAULT_API_URL": "https://api.atomicmail.ai",
11
+ "ONE_SEC_MS": 1_000,
12
+ "ONE_MIN_MS": 60_000,
13
+ "ONE_HOUR_MS": 3_600_000,
14
+ "ONE_DAY_MS": 86_400_000,
15
+ "ONE_MONTH_MS": 2_592_000_000,
16
+ "ONE_YEAR_MS": 31_536_000_000,
17
+ }
18
+
19
+ _SHARED = try_read_shared_json("consts.json") or {}
20
+
21
+ DEFAULT_POW_SCRYPT_SALT_HEX = _SHARED.get(
22
+ "DEFAULT_POW_SCRYPT_SALT_HEX", _DEFAULTS["DEFAULT_POW_SCRYPT_SALT_HEX"]
23
+ )
24
+ DEFAULT_AUTH_URL = _SHARED.get("DEFAULT_AUTH_URL", _DEFAULTS["DEFAULT_AUTH_URL"])
25
+ DEFAULT_API_URL = _SHARED.get("DEFAULT_API_URL", _DEFAULTS["DEFAULT_API_URL"])
26
+
27
+ ONE_SEC_MS = int(_SHARED.get("ONE_SEC_MS", _DEFAULTS["ONE_SEC_MS"]))
28
+ ONE_MIN_MS = int(_SHARED.get("ONE_MIN_MS", _DEFAULTS["ONE_MIN_MS"]))
29
+ ONE_HOUR_MS = int(_SHARED.get("ONE_HOUR_MS", _DEFAULTS["ONE_HOUR_MS"]))
30
+ ONE_DAY_MS = int(_SHARED.get("ONE_DAY_MS", _DEFAULTS["ONE_DAY_MS"]))
31
+ ONE_MONTH_MS = int(_SHARED.get("ONE_MONTH_MS", _DEFAULTS["ONE_MONTH_MS"]))
32
+ ONE_YEAR_MS = int(_SHARED.get("ONE_YEAR_MS", _DEFAULTS["ONE_YEAR_MS"]))