abstractcore 2.19.2__tar.gz → 2.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. {abstractcore-2.19.2 → abstractcore-2.20.0}/PKG-INFO +15 -1
  2. {abstractcore-2.19.2 → abstractcore-2.20.0}/README.md +11 -0
  3. abstractcore-2.20.0/abstractcore/comms/__init__.py +5 -0
  4. abstractcore-2.20.0/abstractcore/comms/email/__init__.py +156 -0
  5. abstractcore-2.20.0/abstractcore/comms/email/bodystructure.py +253 -0
  6. abstractcore-2.20.0/abstractcore/comms/email/client.py +1310 -0
  7. abstractcore-2.20.0/abstractcore/comms/email/context.py +106 -0
  8. abstractcore-2.20.0/abstractcore/comms/email/errors.py +370 -0
  9. abstractcore-2.20.0/abstractcore/comms/email/imap_codec.py +267 -0
  10. abstractcore-2.20.0/abstractcore/comms/email/legacy.py +265 -0
  11. abstractcore-2.20.0/abstractcore/comms/email/limits.py +151 -0
  12. abstractcore-2.20.0/abstractcore/comms/email/models.py +653 -0
  13. abstractcore-2.20.0/abstractcore/comms/email/oauth.py +597 -0
  14. abstractcore-2.20.0/abstractcore/comms/email/policy.py +322 -0
  15. abstractcore-2.20.0/abstractcore/comms/email/search.py +209 -0
  16. abstractcore-2.20.0/abstractcore/comms/email/store.py +546 -0
  17. abstractcore-2.20.0/abstractcore/comms/email/vault.py +323 -0
  18. abstractcore-2.20.0/abstractcore/config/email_cli.py +571 -0
  19. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/main.py +5 -0
  20. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/manager.py +80 -8
  21. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/console/themes.py +1 -1
  22. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/console/web.py +340 -3
  23. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/app.py +5 -0
  24. abstractcore-2.20.0/abstractcore/server/email_routes.py +422 -0
  25. abstractcore-2.20.0/abstractcore/testing/__init__.py +2 -0
  26. abstractcore-2.20.0/abstractcore/testing/mailserver.py +1131 -0
  27. abstractcore-2.20.0/abstractcore/tools/comms_tools.py +1056 -0
  28. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/email_digests.py +8 -32
  29. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/inventory.py +20 -3
  30. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/risk_facts.py +4 -0
  31. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/version.py +1 -1
  32. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/PKG-INFO +15 -1
  33. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/SOURCES.txt +19 -0
  34. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/requires.txt +3 -0
  35. {abstractcore-2.19.2 → abstractcore-2.20.0}/pyproject.toml +11 -1
  36. abstractcore-2.20.0/tests/test_email_digests.py +24 -0
  37. abstractcore-2.19.2/abstractcore/tools/comms_tools.py +0 -1722
  38. abstractcore-2.19.2/tests/test_email_digests.py +0 -40
  39. {abstractcore-2.19.2 → abstractcore-2.20.0}/LICENSE +0 -0
  40. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/__init__.py +0 -0
  41. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/__init__.py +0 -0
  42. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/__main__.py +0 -0
  43. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/app_config_utils.py +0 -0
  44. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/deepsearch.py +0 -0
  45. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/extractor.py +0 -0
  46. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/intent.py +0 -0
  47. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/judge.py +0 -0
  48. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/apps/summarizer.py +0 -0
  49. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/architectures/__init__.py +0 -0
  50. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/architectures/detection.py +0 -0
  51. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/architectures/enums.py +0 -0
  52. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/architectures/response_postprocessing.py +0 -0
  53. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/architectures/thinking_controls.py +0 -0
  54. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/OCRA.ttf +0 -0
  55. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/OCRB.ttf +0 -0
  56. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/OCRBL.ttf +0 -0
  57. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/README.md +0 -0
  58. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/abstractframework_identity.json +0 -0
  59. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/architecture_formats.json +0 -0
  60. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/bloc-schema.jsonld +0 -0
  61. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/model_capabilities.json +0 -0
  62. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/model_capabilities.schema.json +0 -0
  63. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/model_downloads_catalog.json +0 -0
  64. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/model_downloads_catalog.schema.json +0 -0
  65. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/semantic_models.md +0 -0
  66. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/assets/session_schema.json +0 -0
  67. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/__init__.py +0 -0
  68. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/errors.py +0 -0
  69. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/host.py +0 -0
  70. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/music_selectors.py +0 -0
  71. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/registry.py +0 -0
  72. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/scene3d_selectors.py +0 -0
  73. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/types.py +0 -0
  74. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/capabilities/vision_catalog.py +0 -0
  75. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/__init__.py +0 -0
  76. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/analytics.py +0 -0
  77. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/cache.py +0 -0
  78. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/config.py +0 -0
  79. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/exceptions.py +0 -0
  80. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/glyph_processor.py +0 -0
  81. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/optimizer.py +0 -0
  82. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/orchestrator.py +0 -0
  83. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/pil_text_renderer.py +0 -0
  84. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/quality.py +0 -0
  85. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/text_formatter.py +0 -0
  86. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/compression/vision_compressor.py +0 -0
  87. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/__init__.py +0 -0
  88. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/capability_defaults.py +0 -0
  89. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/engines.py +0 -0
  90. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/host_jobs.py +0 -0
  91. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/model_catalog.py +0 -0
  92. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/model_materializer.py +0 -0
  93. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/model_verify.py +0 -0
  94. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/models_engines_cli.py +0 -0
  95. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/provider_profiles.py +0 -0
  96. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/recommendations.py +0 -0
  97. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/route_engines.py +0 -0
  98. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/config/vision_config.py +0 -0
  99. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/console/__init__.py +0 -0
  100. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/console/theme_sync.py +0 -0
  101. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/__init__.py +0 -0
  102. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/bloc_kv.py +0 -0
  103. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/bloc_metadata.py +0 -0
  104. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/cached_session.py +0 -0
  105. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/degeneration.py +0 -0
  106. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/endpoint_damping.py +0 -0
  107. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/enums.py +0 -0
  108. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/factory.py +0 -0
  109. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/file_blocs.py +0 -0
  110. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/file_boxes.py +0 -0
  111. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/generate_contract.py +0 -0
  112. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/interface.py +0 -0
  113. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/multimodal_generation.py +0 -0
  114. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/output_specs.py +0 -0
  115. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/retry.py +0 -0
  116. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/session.py +0 -0
  117. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/core/types.py +0 -0
  118. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/download.py +0 -0
  119. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/embeddings/__init__.py +0 -0
  120. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/embeddings/manager.py +0 -0
  121. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/embeddings/models.py +0 -0
  122. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/endpoint/__init__.py +0 -0
  123. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/endpoint/app.py +0 -0
  124. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/events/__init__.py +0 -0
  125. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/exceptions/__init__.py +0 -0
  126. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/__init__.py +0 -0
  127. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/client.py +0 -0
  128. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/factory.py +0 -0
  129. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/naming.py +0 -0
  130. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/stdio_client.py +0 -0
  131. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/mcp/tool_source.py +0 -0
  132. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/__init__.py +0 -0
  133. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/auto_handler.py +0 -0
  134. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/base.py +0 -0
  135. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/capabilities.py +0 -0
  136. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/delivery.py +0 -0
  137. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/enrichment.py +0 -0
  138. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/handlers/__init__.py +0 -0
  139. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/handlers/anthropic_handler.py +0 -0
  140. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/handlers/local_handler.py +0 -0
  141. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/handlers/openai_handler.py +0 -0
  142. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/openai_parts.py +0 -0
  143. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/pdf_routing.py +0 -0
  144. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/__init__.py +0 -0
  145. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/audio_processor.py +0 -0
  146. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/direct_pdf_processor.py +0 -0
  147. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/glyph_pdf_processor.py +0 -0
  148. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/image_processor.py +0 -0
  149. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/office_processor.py +0 -0
  150. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/pdf_processor.py +0 -0
  151. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/text_processor.py +0 -0
  152. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/processors/video_processor.py +0 -0
  153. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/types.py +0 -0
  154. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/utils/__init__.py +0 -0
  155. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/utils/image_scaler.py +0 -0
  156. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/utils/video_frames.py +0 -0
  157. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/media/vision_fallback.py +0 -0
  158. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/__init__.py +0 -0
  159. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/basic_deepsearch.py +0 -0
  160. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/basic_extractor.py +0 -0
  161. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/basic_intent.py +0 -0
  162. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/basic_judge.py +0 -0
  163. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/processing/basic_summarizer.py +0 -0
  164. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/__init__.py +0 -0
  165. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/_http.py +0 -0
  166. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/anthropic_provider.py +0 -0
  167. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/base.py +0 -0
  168. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/endpoint_context.py +0 -0
  169. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/generation_cancel.py +0 -0
  170. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/generation_progress.py +0 -0
  171. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/hf_residency.py +0 -0
  172. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/huggingface_provider.py +0 -0
  173. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/lmstudio_provider.py +0 -0
  174. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_model_rules.py +0 -0
  175. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_native_cache.py +0 -0
  176. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_native_session.py +0 -0
  177. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_prefill_observer.py +0 -0
  178. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_provider.py +0 -0
  179. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_qwen4.py +0 -0
  180. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_residency.py +0 -0
  181. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_runtime.py +0 -0
  182. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/mlx_vision_addon.py +0 -0
  183. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/model_capabilities.py +0 -0
  184. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/model_config_fingerprint.py +0 -0
  185. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/ollama_provider.py +0 -0
  186. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/openai_compatible_provider.py +0 -0
  187. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/openai_provider.py +0 -0
  188. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/openrouter_provider.py +0 -0
  189. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/portkey_provider.py +0 -0
  190. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/process_residency.py +0 -0
  191. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/registry.py +0 -0
  192. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/speculation.py +0 -0
  193. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/streaming.py +0 -0
  194. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/tokenizer_fingerprint.py +0 -0
  195. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/vllm_provider.py +0 -0
  196. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/providers/weights_fingerprint.py +0 -0
  197. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/py.typed +0 -0
  198. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/__init__.py +0 -0
  199. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/__main__.py +0 -0
  200. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/audio_endpoints.py +0 -0
  201. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/camera_endpoints.py +0 -0
  202. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/capability_generation.py +0 -0
  203. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/console_routes.py +0 -0
  204. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/credentials.py +0 -0
  205. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/first_run.py +0 -0
  206. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/host_routes.py +0 -0
  207. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/scene3d_endpoints.py +0 -0
  208. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/session_routes.py +0 -0
  209. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/server/vision_endpoints.py +0 -0
  210. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/structured/__init__.py +0 -0
  211. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/structured/handler.py +0 -0
  212. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/structured/retry.py +0 -0
  213. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/structured/schema_compat.py +0 -0
  214. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/__init__.py +0 -0
  215. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/abstractignore.py +0 -0
  216. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/arg_canonicalizer.py +0 -0
  217. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/arg_coercion.py +0 -0
  218. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/browser_tools.py +0 -0
  219. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/code_analysis.py +0 -0
  220. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/common_tools.py +0 -0
  221. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/core.py +0 -0
  222. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/fetch_url_ssrf.py +0 -0
  223. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/handler.py +0 -0
  224. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/parser.py +0 -0
  225. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/process_tree.py +0 -0
  226. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/registry.py +0 -0
  227. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/shell_session.py +0 -0
  228. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/shell_tools.py +0 -0
  229. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/syntax_rewriter.py +0 -0
  230. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/tag_rewriter.py +0 -0
  231. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/telegram_tdlib.py +0 -0
  232. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/telegram_tools.py +0 -0
  233. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/tools/wire_naming.py +0 -0
  234. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/__init__.py +0 -0
  235. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/async_stream.py +0 -0
  236. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/cli.py +0 -0
  237. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/context_calibration.py +0 -0
  238. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/context_estimate.py +0 -0
  239. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/data_registry.py +0 -0
  240. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/display.py +0 -0
  241. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/file_filters.py +0 -0
  242. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/host_profile.py +0 -0
  243. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/hostinfo.py +0 -0
  244. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/identity.py +0 -0
  245. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/install_settings.py +0 -0
  246. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/jsonish.py +0 -0
  247. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/memory.py +0 -0
  248. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/message_preprocessor.py +0 -0
  249. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/model_cache.py +0 -0
  250. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/model_fit.py +0 -0
  251. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/residency.py +0 -0
  252. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/self_fixes.py +0 -0
  253. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/structured_logging.py +0 -0
  254. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/token_utils.py +0 -0
  255. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/trace_export.py +0 -0
  256. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/truncation.py +0 -0
  257. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/vlm_token_calculator.py +0 -0
  258. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/windows_dll.py +0 -0
  259. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore/utils/workspace_paths.py +0 -0
  260. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/dependency_links.txt +0 -0
  261. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/entry_points.txt +0 -0
  262. {abstractcore-2.19.2 → abstractcore-2.20.0}/abstractcore.egg-info/top_level.txt +0 -0
  263. {abstractcore-2.19.2 → abstractcore-2.20.0}/setup.cfg +0 -0
  264. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_abstractendpoint_singleton_provider.py +0 -0
  265. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_agentic_cli_compatibility.py +0 -0
  266. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_basic_session.py +0 -0
  267. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv.py +0 -0
  268. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_cache_dtype.py +0 -0
  269. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_endpoint.py +0 -0
  270. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_engine_fingerprint.py +0 -0
  271. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_model_config_fingerprint.py +0 -0
  272. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_tokenizer_fingerprint.py +0 -0
  273. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_kv_weights_fingerprint.py +0 -0
  274. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_bloc_metadata_json_extract.py +0 -0
  275. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_cached_session_kv_mode.py +0 -0
  276. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_cached_session_kv_thinking_prefix.py +0 -0
  277. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_capabilities_registry.py +0 -0
  278. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_capabilities_registry_preferred_backends_from_config.py +0 -0
  279. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_complete_integration.py +0 -0
  280. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_comprehensive_events.py +0 -0
  281. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_console_web.py +0 -0
  282. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_core_components.py +0 -0
  283. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_display_rule.py +0 -0
  284. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_embedding_model_token_warnings.py +0 -0
  285. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_enhanced_prompt.py +0 -0
  286. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_environment_variable_tool_call_tags.py +0 -0
  287. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_factory.py +0 -0
  288. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_file_bloc_store_unit.py +0 -0
  289. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_file_boxes_cached_session.py +0 -0
  290. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_final_accuracy.py +0 -0
  291. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_final_comprehensive.py +0 -0
  292. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_final_graceful_errors.py +0 -0
  293. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_generate_contract.py +0 -0
  294. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_generate_response_reasoning_property_unit.py +0 -0
  295. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_generate_with_outputs.py +0 -0
  296. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_graceful_fallback.py +0 -0
  297. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_import_debug.py +0 -0
  298. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_import_safety.py +0 -0
  299. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_incremental_thinking_tag_stripper.py +0 -0
  300. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_install_hints_no_bare_packages.py +0 -0
  301. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_install_settings.py +0 -0
  302. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_integrated_functionality.py +0 -0
  303. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_llms_sources.py +0 -0
  304. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_mcp_integration.py +0 -0
  305. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_mcp_stdio_client.py +0 -0
  306. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_multimodal_generate_output.py +0 -0
  307. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_openai_prompt_cache_key_passthrough.py +0 -0
  308. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_output_specs.py +0 -0
  309. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_packaging_assets.py +0 -0
  310. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_packaging_extras.py +0 -0
  311. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_prompt_cache_api.py +0 -0
  312. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_prompt_cache_bloc_composition.py +0 -0
  313. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_prompt_cache_bloc_e2e_gate.py +0 -0
  314. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_prompt_cache_control_plane.py +0 -0
  315. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_prompt_cache_prepare_modules_thinking_http.py +0 -0
  316. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_retry_observability.py +0 -0
  317. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_retry_strategy.py +0 -0
  318. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_seed_determinism.py +0 -0
  319. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_seed_temperature_basic.py +0 -0
  320. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_sensory_prompting.py +0 -0
  321. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_server_prompt_cache_control_plane_proxy.py +0 -0
  322. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_server_prompt_cache_key_passthrough.py +0 -0
  323. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_session_reasoning_persistence_unit.py +0 -0
  324. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_structured_output_truncation_retry.py +0 -0
  325. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_text_only_model_experience.py +0 -0
  326. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_unload_memory.py +0 -0
  327. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_user_scenario_validation.py +0 -0
  328. {abstractcore-2.19.2 → abstractcore-2.20.0}/tests/test_voice_host_credentials.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.19.2
3
+ Version: 2.20.0
4
4
  Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
6
  Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
@@ -48,6 +48,8 @@ Requires-Dist: fastapi<1.0.0,>=0.100.0
48
48
  Requires-Dist: uvicorn[standard]<1.0.0,>=0.23.0
49
49
  Requires-Dist: python-multipart<1.0.0,>=0.0.6
50
50
  Requires-Dist: sse-starlette<2.0.0,>=1.6.0
51
+ Requires-Dist: cryptography>=41.0.0
52
+ Requires-Dist: keyring>=24.0.0
51
53
  Requires-Dist: abstractvoice>=0.13.2
52
54
  Requires-Dist: abstractvision>=0.3.33
53
55
  Requires-Dist: abstractmusic>=0.1.15; python_version >= "3.10"
@@ -173,6 +175,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
173
175
  Requires-Dist: responses>=0.23.0; extra == "test"
174
176
  Requires-Dist: tomli>=2.0.0; python_version < "3.11" and extra == "test"
175
177
  Requires-Dist: jinja2<4.0.0,>=3.0.0; extra == "test"
178
+ Requires-Dist: aiosmtpd<2.0.0,>=1.4.4; extra == "test"
176
179
  Provides-Extra: docs
177
180
  Requires-Dist: mkdocs>=1.5.0; extra == "docs"
178
181
  Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
@@ -452,6 +455,7 @@ You can also persist settings (including API keys) via the config CLI:
452
455
  - Embeddings and semantic search → [Embeddings](docs/embeddings.md)
453
456
  - Observability: global event bus + interaction traces → [Architecture](docs/architecture.md), [API Reference (Events)](docs/api-reference.md#eventtype), [Interaction Tracing](docs/interaction-tracing.md)
454
457
  - MCP (Model Context Protocol): discover tools from MCP servers (HTTP/stdio) → [MCP](docs/mcp.md)
458
+ - Email: one IMAP/SMTP account (password or OAuth2), encrypted credentials, recipient policy, send limits, read-only mailbox and email tools → [Email](docs/email.md)
455
459
  - OpenAI-compatible server: one `/v1` gateway for chat + optional `/v1/images/*` and `/v1/audio/*` endpoints → [Server](docs/server.md)
456
460
 
457
461
  ## Tool calling (passthrough by default)
@@ -721,6 +725,15 @@ abstractcore engines status --probe # Ollama / LM Studio / MLX / ll
721
725
  abstractcore engines install ollama --dry-run # the exact install command
722
726
  ```
723
727
 
728
+ Email (see [Email](docs/email.md)):
729
+
730
+ ```bash
731
+ abstractcore email connect --address me@example.com \
732
+ --imap-host imap.example.com --smtp-host smtp.example.com --password <value>
733
+ abstractcore email test
734
+ abstractcore email status
735
+ ```
736
+
724
737
  ## Built-in CLI apps
725
738
 
726
739
  AbstractCore also ships with ready-to-use CLI apps:
@@ -759,6 +772,7 @@ Core features:
759
772
  - [Capabilities](docs/capabilities.md) — supported features and current limitations
760
773
  - [Interaction Tracing](docs/interaction-tracing.md) — inspect prompts/responses/usage for observability
761
774
  - [MCP](docs/mcp.md) — consume MCP tool servers (HTTP/stdio) as tool sources
775
+ - [Email](docs/email.md) — connect a mailbox, recipient policy, send limits, email tools and the `abstractcore.comms.email` library
762
776
 
763
777
  Reference and internals:
764
778
  - [Architecture](docs/architecture.md) — system overview + event system
@@ -270,6 +270,7 @@ You can also persist settings (including API keys) via the config CLI:
270
270
  - Embeddings and semantic search → [Embeddings](docs/embeddings.md)
271
271
  - Observability: global event bus + interaction traces → [Architecture](docs/architecture.md), [API Reference (Events)](docs/api-reference.md#eventtype), [Interaction Tracing](docs/interaction-tracing.md)
272
272
  - MCP (Model Context Protocol): discover tools from MCP servers (HTTP/stdio) → [MCP](docs/mcp.md)
273
+ - Email: one IMAP/SMTP account (password or OAuth2), encrypted credentials, recipient policy, send limits, read-only mailbox and email tools → [Email](docs/email.md)
273
274
  - OpenAI-compatible server: one `/v1` gateway for chat + optional `/v1/images/*` and `/v1/audio/*` endpoints → [Server](docs/server.md)
274
275
 
275
276
  ## Tool calling (passthrough by default)
@@ -539,6 +540,15 @@ abstractcore engines status --probe # Ollama / LM Studio / MLX / ll
539
540
  abstractcore engines install ollama --dry-run # the exact install command
540
541
  ```
541
542
 
543
+ Email (see [Email](docs/email.md)):
544
+
545
+ ```bash
546
+ abstractcore email connect --address me@example.com \
547
+ --imap-host imap.example.com --smtp-host smtp.example.com --password <value>
548
+ abstractcore email test
549
+ abstractcore email status
550
+ ```
551
+
542
552
  ## Built-in CLI apps
543
553
 
544
554
  AbstractCore also ships with ready-to-use CLI apps:
@@ -577,6 +587,7 @@ Core features:
577
587
  - [Capabilities](docs/capabilities.md) — supported features and current limitations
578
588
  - [Interaction Tracing](docs/interaction-tracing.md) — inspect prompts/responses/usage for observability
579
589
  - [MCP](docs/mcp.md) — consume MCP tool servers (HTTP/stdio) as tool sources
590
+ - [Email](docs/email.md) — connect a mailbox, recipient policy, send limits, email tools and the `abstractcore.comms.email` library
580
591
 
581
592
  Reference and internals:
582
593
  - [Architecture](docs/architecture.md) — system overview + event system
@@ -0,0 +1,5 @@
1
+ """Communication libraries (typed, host-agnostic).
2
+
3
+ `abstractcore.comms.email` is the one implementation of mail in the framework: the core
4
+ CLI, the core tools, the consoles and hosts such as the gateway all use it.
5
+ """
@@ -0,0 +1,156 @@
1
+ """`abstractcore.comms.email` — the framework's one mail implementation.
2
+
3
+ from abstractcore.comms.email import (
4
+ EmailAccount, ImapSettings, SmtpSettings, EmailSecret, EmailClient,
5
+ EmailAccountStore, RecipientPolicy, SendLimits, SearchCriteria, OutgoingMessage,
6
+ )
7
+
8
+ account = EmailAccount.build(
9
+ address="me@example.test",
10
+ imap=ImapSettings.build("imap.example.test", security="ssl"),
11
+ smtp=SmtpSettings.build("smtp.example.test", security="starttls"),
12
+ )
13
+ store = EmailAccountStore() # the local AbstractCore settings
14
+ store.connect(account, EmailSecret("app-password"))
15
+ ctx = store.context()
16
+ ctx.client().search(SearchCriteria.build(unseen=True, since="7d"), limit=20)
17
+ ctx.send(OutgoingMessage(to=("me@example.test",), subject="Hello", text="Hi"))
18
+
19
+ Modules: `models` (typed settings), `client` (IMAP read-only + SMTP, verified TLS),
20
+ `bodystructure` (the MIME structure used to fetch only the parts needed),
21
+ `policy` (recipient allowlist / denylist), `limits` (send rate limits), `vault` (encrypted
22
+ credentials), `oauth` (OAuth2 / XOAUTH2), `store` (settings + secret + status), `context`
23
+ (the guarded send), `errors` (typed errors), `legacy` (one-time import of pre-2.20 settings).
24
+ """
25
+
26
+ from .client import DEFAULT_MAX_MESSAGE_BYTES, MAX_LIST_LIMIT, EmailClient, tls_context
27
+ from .context import EmailContext, guarded_send
28
+ from .errors import (
29
+ EmailAgentToolsOff,
30
+ EmailAttachmentNotFound,
31
+ EmailAuthFailed,
32
+ EmailDisabled,
33
+ EmailError,
34
+ EmailInvalidMessage,
35
+ EmailInvalidSettings,
36
+ EmailMailboxMissing,
37
+ EmailMessageNotFound,
38
+ EmailMessageTooLarge,
39
+ EmailNotConfigured,
40
+ EmailOAuthFailed,
41
+ EmailOAuthPending,
42
+ EmailOAuthReauthorize,
43
+ EmailPolicyRefused,
44
+ EmailProtocolError,
45
+ EmailQuotaExceeded,
46
+ EmailRateLimited,
47
+ EmailReadOnlyViolation,
48
+ EmailRecipientRefused,
49
+ EmailSecretUnavailable,
50
+ EmailServerError,
51
+ EmailTlsFailed,
52
+ EmailTransient,
53
+ EmailUnreachable,
54
+ )
55
+ from .limits import SendRateLimiter
56
+ from .models import (
57
+ Attachment,
58
+ AttachmentInfo,
59
+ EmailAccount,
60
+ EmailSecret,
61
+ FetchResult,
62
+ ImapSettings,
63
+ MailCursor,
64
+ MessageDetail,
65
+ MessageSummary,
66
+ OAuthSettings,
67
+ OutgoingMessage,
68
+ SendLimits,
69
+ SendResult,
70
+ SmtpSettings,
71
+ )
72
+ from .oauth import (
73
+ BUILTIN_CLIENTS,
74
+ DeviceAuthorization,
75
+ LoopbackAuthorization,
76
+ OAuthTokenClient,
77
+ OAuthTokenProvider,
78
+ TokenSet,
79
+ builtin_client,
80
+ provider_preset,
81
+ resolve_oauth_client,
82
+ xoauth2_string,
83
+ )
84
+ from .policy import PolicyDecision, RecipientPolicy, evaluate, normalize_address, normalize_domain, parse_recipients
85
+ from .search import SearchCriteria
86
+ from .store import EmailAccountStore, EmailSettings
87
+ from .vault import SecretVault
88
+
89
+ __all__ = [
90
+ "DEFAULT_MAX_MESSAGE_BYTES",
91
+ "MAX_LIST_LIMIT",
92
+ "BUILTIN_CLIENTS",
93
+ "Attachment",
94
+ "AttachmentInfo",
95
+ "DeviceAuthorization",
96
+ "EmailAccount",
97
+ "EmailAccountStore",
98
+ "EmailAgentToolsOff",
99
+ "EmailAttachmentNotFound",
100
+ "EmailAuthFailed",
101
+ "EmailClient",
102
+ "EmailContext",
103
+ "EmailDisabled",
104
+ "EmailError",
105
+ "EmailInvalidMessage",
106
+ "EmailInvalidSettings",
107
+ "EmailMailboxMissing",
108
+ "EmailMessageNotFound",
109
+ "EmailMessageTooLarge",
110
+ "EmailNotConfigured",
111
+ "EmailOAuthFailed",
112
+ "EmailOAuthPending",
113
+ "EmailOAuthReauthorize",
114
+ "EmailPolicyRefused",
115
+ "EmailProtocolError",
116
+ "EmailQuotaExceeded",
117
+ "EmailRateLimited",
118
+ "EmailReadOnlyViolation",
119
+ "EmailRecipientRefused",
120
+ "EmailSecret",
121
+ "EmailSecretUnavailable",
122
+ "EmailServerError",
123
+ "EmailSettings",
124
+ "EmailTlsFailed",
125
+ "EmailTransient",
126
+ "EmailUnreachable",
127
+ "FetchResult",
128
+ "ImapSettings",
129
+ "LoopbackAuthorization",
130
+ "MailCursor",
131
+ "MessageDetail",
132
+ "MessageSummary",
133
+ "OAuthSettings",
134
+ "OAuthTokenClient",
135
+ "OAuthTokenProvider",
136
+ "OutgoingMessage",
137
+ "PolicyDecision",
138
+ "RecipientPolicy",
139
+ "SearchCriteria",
140
+ "SecretVault",
141
+ "SendLimits",
142
+ "SendRateLimiter",
143
+ "SendResult",
144
+ "SmtpSettings",
145
+ "TokenSet",
146
+ "builtin_client",
147
+ "evaluate",
148
+ "guarded_send",
149
+ "normalize_address",
150
+ "normalize_domain",
151
+ "parse_recipients",
152
+ "provider_preset",
153
+ "resolve_oauth_client",
154
+ "tls_context",
155
+ "xoauth2_string",
156
+ ]
@@ -0,0 +1,253 @@
1
+ """IMAP BODYSTRUCTURE (RFC 3501 section 7.4.2): the MIME tree of a message without its content.
2
+
3
+ Reading a message fetches its structure first, then only the parts it needs by section
4
+ (`BODY.PEEK[1.2]`): the text/plain and text/html bodies for `read_email`, one part for an
5
+ attachment download. Attachments are listed from the structure (name, type, size) without
6
+ downloading them.
7
+
8
+ The parser follows the RFC grammar (parenthesised lists, strings, NIL); it never looks for
9
+ words in free text. Section numbers follow RFC 3501 section 6.4.5: the parts of a multipart
10
+ body are numbered 1, 2, ... (nested: 2.1, 2.2); a message that is not multipart has its body at
11
+ section 1; the parts inside an attached message (message/rfc822 at section 2) are 2.1, 2.2, ...
12
+ (its body is 2.1 when it is not multipart).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import binascii
19
+ import email.header
20
+ import email.utils
21
+ import quopri
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class MimePart:
28
+ """One node of the structure. `size` is the part's size on the wire (encoded octets)."""
29
+
30
+ section: str
31
+ content_type: str
32
+ params: Dict[str, str] = field(default_factory=dict)
33
+ content_id: str = ""
34
+ encoding: str = ""
35
+ size: int = 0
36
+ disposition: str = ""
37
+ disposition_params: Dict[str, str] = field(default_factory=dict)
38
+ children: Tuple["MimePart", ...] = ()
39
+ # message/rfc822 only: the body of the attached message.
40
+ message_body: Optional["MimePart"] = None
41
+
42
+ @property
43
+ def multipart(self) -> bool:
44
+ return self.content_type.startswith("multipart/")
45
+
46
+ @property
47
+ def charset(self) -> str:
48
+ return self.params.get("charset", "")
49
+
50
+ @property
51
+ def filename(self) -> str:
52
+ return self.disposition_params.get("filename") or self.params.get("name") or ""
53
+
54
+ @property
55
+ def is_attachment(self) -> bool:
56
+ """Same rule as reading the whole message did: disposition `attachment`, or a file name."""
57
+
58
+ if self.multipart:
59
+ return False
60
+ return self.disposition == "attachment" or bool(self.filename)
61
+
62
+
63
+ def _text(tok: Any) -> str:
64
+ if tok is None:
65
+ return ""
66
+ if isinstance(tok, (bytes, bytearray)):
67
+ return bytes(tok).decode("utf-8", errors="replace")
68
+ return str(tok)
69
+
70
+
71
+ def _decode_words(value: str) -> str:
72
+ """RFC 2047 encoded words (`=?utf-8?B?...?=`) inside a parameter value, if any."""
73
+
74
+ if "=?" not in value:
75
+ return value
76
+ try:
77
+ return str(email.header.make_header(email.header.decode_header(value)))
78
+ except (ValueError, LookupError, UnicodeDecodeError, binascii.Error):
79
+ return value
80
+
81
+
82
+ def _params(tok: Any) -> Dict[str, str]:
83
+ """A parameter list `("NAME" "value" ...)` -> {name: decoded value}.
84
+
85
+ RFC 2231 forms (`filename*`, continuations `filename*0*`) are decoded with the standard
86
+ library; RFC 2047 encoded words are decoded too.
87
+ """
88
+
89
+ if not isinstance(tok, list):
90
+ return {}
91
+ pairs: List[Tuple[str, str]] = []
92
+ for i in range(0, len(tok) - 1, 2):
93
+ key = _text(tok[i]).strip().lower()
94
+ if key:
95
+ value = _text(tok[i + 1])
96
+ if not key.endswith("*"):
97
+ # decode_params unquotes; keep the value as the server sent it.
98
+ value = '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
99
+ pairs.append((key, value))
100
+ if not pairs:
101
+ return {}
102
+ try:
103
+ decoded = email.utils.decode_params([("", "")] + pairs)[1:]
104
+ except Exception: # noqa: BLE001 - a malformed parameter list reads as "no parameters"
105
+ decoded = [(k.rstrip("*"), v.strip('"')) for k, v in pairs]
106
+ out: Dict[str, str] = {}
107
+ for key, value in decoded:
108
+ if isinstance(value, tuple):
109
+ charset, language, encoded = value
110
+ text = email.utils.collapse_rfc2231_value((charset, language, email.utils.unquote(encoded)))
111
+ else:
112
+ text = _decode_words(email.utils.unquote(str(value)))
113
+ out[str(key).lower()] = str(text)
114
+ return out
115
+
116
+
117
+ def _disposition(tok: Any) -> Tuple[str, Dict[str, str]]:
118
+ if isinstance(tok, list) and tok:
119
+ return _text(tok[0]).strip().lower(), _params(tok[1] if len(tok) > 1 else None)
120
+ return "", {}
121
+
122
+
123
+ def _int(tok: Any) -> int:
124
+ try:
125
+ return int(_text(tok).strip())
126
+ except ValueError:
127
+ return 0
128
+
129
+
130
+ def _part_at(tok: Any, section: str) -> MimePart:
131
+ """The part described by `tok`, located at `section`."""
132
+
133
+ if not isinstance(tok, list) or not tok:
134
+ raise ValueError("BODYSTRUCTURE part is not a list")
135
+ if isinstance(tok[0], list):
136
+ # multipart: (part)(part)... "SUBTYPE" [params [disposition [language [location]]]]
137
+ children: List[MimePart] = []
138
+ i = 0
139
+ while i < len(tok) and isinstance(tok[i], list):
140
+ children.append(_part_at(tok[i], f"{section}.{len(children) + 1}" if section else str(len(children) + 1)))
141
+ i += 1
142
+ subtype = _text(tok[i]).lower() if i < len(tok) else "mixed"
143
+ params = _params(tok[i + 1]) if i + 1 < len(tok) else {}
144
+ disp, dparams = _disposition(tok[i + 2]) if i + 2 < len(tok) else ("", {})
145
+ return MimePart(
146
+ section=section,
147
+ content_type=f"multipart/{subtype}",
148
+ params=params,
149
+ disposition=disp,
150
+ disposition_params=dparams,
151
+ children=tuple(children),
152
+ )
153
+ # single part: type subtype params id description encoding size ...
154
+ if len(tok) < 7:
155
+ raise ValueError("BODYSTRUCTURE part is too short")
156
+ ctype = f"{_text(tok[0]).lower()}/{_text(tok[1]).lower()}"
157
+ rest = list(tok[7:])
158
+ message_body: Optional[MimePart] = None
159
+ if ctype == "message/rfc822" and len(rest) >= 3:
160
+ # envelope, body, lines
161
+ message_body = _message_body(rest[1], section)
162
+ rest = rest[3:]
163
+ elif ctype.startswith("text/") and rest:
164
+ rest = rest[1:] # lines
165
+ # extension data: md5 disposition language location
166
+ disp, dparams = _disposition(rest[1]) if len(rest) > 1 else ("", {})
167
+ return MimePart(
168
+ section=section,
169
+ content_type=ctype,
170
+ params=_params(tok[2]),
171
+ content_id=_text(tok[3]).strip(),
172
+ encoding=_text(tok[5]).strip().lower(),
173
+ size=_int(tok[6]),
174
+ disposition=disp,
175
+ disposition_params=dparams,
176
+ message_body=message_body,
177
+ )
178
+
179
+
180
+ def _message_body(tok: Any, prefix: str) -> MimePart:
181
+ """The body of a message (the top level, or an attached message at section `prefix`)."""
182
+
183
+ if isinstance(tok, list) and tok and isinstance(tok[0], list):
184
+ return _part_at(tok, prefix)
185
+ return _part_at(tok, f"{prefix}.1" if prefix else "1")
186
+
187
+
188
+ def parse_bodystructure(tok: Any) -> MimePart:
189
+ """The root of a message's structure from the parsed `BODYSTRUCTURE` value."""
190
+
191
+ return _message_body(tok, "")
192
+
193
+
194
+ def walk(part: MimePart) -> Iterator[MimePart]:
195
+ """Depth-first, in document order. An attached message (message/rfc822 with disposition
196
+ `attachment`) is one attachment; an inline one is walked into."""
197
+
198
+ yield part
199
+ for child in part.children:
200
+ yield from walk(child)
201
+ if part.message_body is not None and not part.is_attachment:
202
+ yield from walk(part.message_body)
203
+
204
+
205
+ def body_parts(root: MimePart) -> List[MimePart]:
206
+ """The text/plain and text/html parts that are not attachments, in document order."""
207
+
208
+ return [
209
+ p
210
+ for p in walk(root)
211
+ if not p.multipart
212
+ and p.message_body is None
213
+ and not p.is_attachment
214
+ and p.content_type in ("text/plain", "text/html")
215
+ ]
216
+
217
+
218
+ def attachment_parts(root: MimePart) -> List[MimePart]:
219
+ return [p for p in walk(root) if p.is_attachment]
220
+
221
+
222
+ def decode_transfer(data: bytes, encoding: str) -> bytes:
223
+ """Undo the Content-Transfer-Encoding of a part's wire bytes."""
224
+
225
+ enc = (encoding or "").strip().lower()
226
+ if enc == "base64":
227
+ try:
228
+ return base64.b64decode(data, validate=False)
229
+ except (binascii.Error, ValueError):
230
+ return base64.b64decode(data + b"=" * (-len(data) % 4), validate=False)
231
+ if enc == "quoted-printable":
232
+ return quopri.decodestring(data)
233
+ return bytes(data)
234
+
235
+
236
+ def decode_text(data: bytes, part: MimePart) -> str:
237
+ raw = decode_transfer(data, part.encoding)
238
+ charset = part.charset or "utf-8"
239
+ try:
240
+ return raw.decode(charset, errors="replace")
241
+ except LookupError:
242
+ return raw.decode("utf-8", errors="replace")
243
+
244
+
245
+ __all__ = [
246
+ "MimePart",
247
+ "attachment_parts",
248
+ "body_parts",
249
+ "decode_text",
250
+ "decode_transfer",
251
+ "parse_bodystructure",
252
+ "walk",
253
+ ]