openmail 0.1.5__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 (67) hide show
  1. openmail/__init__.py +6 -0
  2. openmail/assistants/__init__.py +35 -0
  3. openmail/assistants/classify_emails.py +83 -0
  4. openmail/assistants/compose_email.py +43 -0
  5. openmail/assistants/detect_phishing_for_email.py +61 -0
  6. openmail/assistants/evaluate_sender_trust_for_email.py +59 -0
  7. openmail/assistants/extract_tasks_from_emails.py +126 -0
  8. openmail/assistants/generate_follow_up_for_email.py +54 -0
  9. openmail/assistants/natural_language_query.py +699 -0
  10. openmail/assistants/prioritize_emails.py +89 -0
  11. openmail/assistants/reply.py +58 -0
  12. openmail/assistants/reply_suggestions.py +46 -0
  13. openmail/assistants/rewrite_email.py +50 -0
  14. openmail/assistants/summarize_attachments_for_email.py +101 -0
  15. openmail/assistants/summarize_thread_emails.py +55 -0
  16. openmail/assistants/summary.py +44 -0
  17. openmail/assistants/summary_multi.py +57 -0
  18. openmail/assistants/translate_email.py +54 -0
  19. openmail/auth/__init__.py +6 -0
  20. openmail/auth/base.py +34 -0
  21. openmail/auth/no_auth.py +19 -0
  22. openmail/auth/oauth2.py +58 -0
  23. openmail/auth/password.py +26 -0
  24. openmail/config.py +26 -0
  25. openmail/email_assistant.py +418 -0
  26. openmail/email_manager.py +777 -0
  27. openmail/email_query.py +279 -0
  28. openmail/errors.py +16 -0
  29. openmail/imap/__init__.py +5 -0
  30. openmail/imap/attachment_parts.py +55 -0
  31. openmail/imap/bodystructure.py +296 -0
  32. openmail/imap/client.py +806 -0
  33. openmail/imap/fetch_response.py +115 -0
  34. openmail/imap/inline_cid.py +106 -0
  35. openmail/imap/pagination.py +16 -0
  36. openmail/imap/parser.py +298 -0
  37. openmail/imap/query.py +233 -0
  38. openmail/llm/__init__.py +3 -0
  39. openmail/llm/claude.py +35 -0
  40. openmail/llm/costs.py +108 -0
  41. openmail/llm/gemini.py +34 -0
  42. openmail/llm/gpt.py +33 -0
  43. openmail/llm/groq.py +36 -0
  44. openmail/llm/model.py +126 -0
  45. openmail/llm/xai.py +35 -0
  46. openmail/logger.py +20 -0
  47. openmail/models/__init__.py +20 -0
  48. openmail/models/attachment.py +128 -0
  49. openmail/models/message.py +113 -0
  50. openmail/models/subscription.py +45 -0
  51. openmail/models/task.py +24 -0
  52. openmail/py.typed +0 -0
  53. openmail/smtp/__init__.py +7 -0
  54. openmail/smtp/builder.py +41 -0
  55. openmail/smtp/client.py +218 -0
  56. openmail/smtp/templates.py +16 -0
  57. openmail/subscription/__init__.py +7 -0
  58. openmail/subscription/detector.py +58 -0
  59. openmail/subscription/parser.py +32 -0
  60. openmail/subscription/service.py +237 -0
  61. openmail/types.py +30 -0
  62. openmail/utils/__init__.py +39 -0
  63. openmail/utils/utils.py +295 -0
  64. openmail-0.1.5.dist-info/METADATA +180 -0
  65. openmail-0.1.5.dist-info/RECORD +67 -0
  66. openmail-0.1.5.dist-info/WHEEL +4 -0
  67. openmail-0.1.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from openmail.auth.base import AuthContext
6
+ from openmail.errors import AuthError
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class PasswordAuth:
11
+ username: str
12
+ password: str
13
+
14
+ def apply_smtp(self, server, ctx: AuthContext) -> None:
15
+ try:
16
+ server.login(self.username, self.password)
17
+ except Exception as e:
18
+ raise AuthError(f"SMTP login failed: {e}") from e
19
+
20
+ def apply_imap(self, conn, ctx: AuthContext) -> None:
21
+ try:
22
+ typ, _ = conn.login(self.username, self.password)
23
+ if typ != "OK":
24
+ raise AuthError("IMAP login failed (non-OK response)")
25
+ except Exception as e:
26
+ raise AuthError(f"IMAP login failed: {e}") from e
openmail/config.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from openmail.auth import IMAPAuth, SMTPAuth
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class SMTPConfig:
11
+ host: str
12
+ port: int = 587
13
+ use_starttls: bool = True
14
+ use_ssl: bool = False
15
+ timeout: float = 30.0
16
+ from_email: Optional[str] = None
17
+ auth: Optional[SMTPAuth] = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class IMAPConfig:
22
+ host: str
23
+ port: int = 993
24
+ use_ssl: bool = True
25
+ timeout: float = 30.0
26
+ auth: Optional[IMAPAuth] = None
@@ -0,0 +1,418 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from email.message import EmailMessage as PyEmailMessage
5
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
6
+
7
+ from openmail.assistants import (
8
+ llm_classify_emails,
9
+ llm_compose_email,
10
+ llm_concise_reply_for_email,
11
+ llm_detect_phishing_for_email,
12
+ llm_easy_imap_query_from_nl,
13
+ llm_evaluate_sender_trust_for_email,
14
+ llm_extract_tasks_from_emails,
15
+ llm_generate_follow_up_for_email,
16
+ llm_prioritize_emails,
17
+ llm_reply_suggestions_for_email,
18
+ llm_rewrite_email,
19
+ llm_summarize_attachments_for_email,
20
+ llm_summarize_many_emails,
21
+ llm_summarize_single_email,
22
+ llm_summarize_thread_emails,
23
+ llm_translate_email,
24
+ )
25
+ from openmail.email_query import EmailQuery
26
+ from openmail.models import EmailMessage, Task
27
+
28
+
29
+ @dataclass
30
+ class EmailAssistantProfile:
31
+ """
32
+ Generic profile for personalizing email behavior.
33
+ Designed to be reusable across domains (CRM, helpdesk, etc.).
34
+ """
35
+
36
+ name: Optional[str] = None
37
+ role: Optional[str] = None
38
+ company: Optional[str] = None
39
+ tone: Optional[str] = None # e.g. "formal", "friendly", "concise"
40
+ signature: Optional[str] = None # default signature block
41
+ locale: Optional[str] = None # e.g. "en-US"
42
+ extra_context: Optional[str] = None # free-form org / domain context
43
+
44
+ def generate_prompt(self):
45
+ """Convert profile dataclass fields into a structured prompt string."""
46
+ parts = []
47
+ if self.name:
48
+ parts.append(f"You are {self.name}.")
49
+ if self.role:
50
+ parts.append(f"Your role is {self.role}.")
51
+ if self.company:
52
+ parts.append(f"You represent {self.company}.")
53
+ if self.tone:
54
+ parts.append(f"Use a {self.tone} tone.")
55
+ if self.locale:
56
+ parts.append(f"Locale: {self.locale}.")
57
+ if self.extra_context:
58
+ parts.append(f"Context: {self.extra_context}")
59
+
60
+ return " ".join(parts).strip()
61
+
62
+
63
+ class EmailAssistant:
64
+
65
+ def __init__(
66
+ self,
67
+ profile: Optional[EmailAssistantProfile] = None,
68
+ ) -> None:
69
+ self.profile = profile
70
+
71
+ def generate_reply_suggestions(
72
+ self,
73
+ message: EmailMessage,
74
+ *,
75
+ provider: str,
76
+ model_name: str,
77
+ ) -> Tuple[List[str], Dict[str, Any]]:
78
+ """
79
+ Generate reply suggestions for an email.
80
+ """
81
+ return llm_reply_suggestions_for_email(
82
+ message,
83
+ provider=provider,
84
+ model_name=model_name,
85
+ )
86
+
87
+ def generate_reply(
88
+ self,
89
+ reply_context: str,
90
+ message: EmailMessage,
91
+ *,
92
+ previous_reply: Optional[str] = None,
93
+ provider: str,
94
+ model_name: str,
95
+ ) -> Tuple[str, Dict[str, Any]]:
96
+ """
97
+ Generate a reply to an email.
98
+ """
99
+ persona = self.profile.generate_prompt() if self.profile else ""
100
+ enriched_context = f"{persona}\n\n{reply_context}".strip()
101
+
102
+ return llm_concise_reply_for_email(
103
+ enriched_context,
104
+ message,
105
+ provider=provider,
106
+ model_name=model_name,
107
+ previous_reply=previous_reply,
108
+ )
109
+
110
+ def summarize_email(
111
+ self,
112
+ message: EmailMessage,
113
+ *,
114
+ provider: str,
115
+ model_name: str,
116
+ ) -> Tuple[str, Dict[str, Any]]:
117
+ """
118
+ Summarize a single email.
119
+ """
120
+ return llm_summarize_single_email(
121
+ message,
122
+ provider=provider,
123
+ model_name=model_name,
124
+ )
125
+
126
+ def summarize_multi_emails(
127
+ self,
128
+ messages: Sequence[EmailMessage],
129
+ *,
130
+ provider: str,
131
+ model_name: str,
132
+ ) -> Tuple[str, Dict[str, Any]]:
133
+ """
134
+ Summarize a list of emails.
135
+ """
136
+ if not messages:
137
+ return "No emails selected.", {}
138
+
139
+ return llm_summarize_many_emails(
140
+ messages,
141
+ provider=provider,
142
+ model_name=model_name,
143
+ )
144
+
145
+ def summarize_thread(
146
+ self,
147
+ thread_messages: Sequence[EmailMessage],
148
+ *,
149
+ provider: str,
150
+ model_name: str,
151
+ ) -> Tuple[str, Dict[str, Any]]:
152
+ """
153
+ Summarize an email conversation thread (ordered sequence of messages).
154
+ Highlight key decisions, open questions, and next steps.
155
+ """
156
+ if not thread_messages:
157
+ return "No emails in thread.", {}
158
+
159
+ return llm_summarize_thread_emails(
160
+ thread_messages,
161
+ provider=provider,
162
+ model_name=model_name,
163
+ )
164
+
165
+ def search_emails(
166
+ self,
167
+ user_request: str,
168
+ *,
169
+ provider: str,
170
+ model_name: str,
171
+ mailbox: str = "INBOX",
172
+ ) -> Tuple[EmailQuery, Dict[str, Any]]:
173
+ """
174
+ Turn a natural-language request like:
175
+ "find unread security alerts from Google last week"
176
+ into an EmailQuery + llm_call_info.
177
+ """
178
+ return llm_easy_imap_query_from_nl(
179
+ user_request,
180
+ provider=provider,
181
+ model_name=model_name,
182
+ mailbox=mailbox,
183
+ )
184
+
185
+ def classify_emails(
186
+ self,
187
+ messages: Sequence[EmailMessage],
188
+ classes: Sequence[str],
189
+ *,
190
+ provider: str,
191
+ model_name: str,
192
+ ) -> Tuple[List[str], Dict[str, Any]]:
193
+ """
194
+ Classify multiple emails at once.
195
+ """
196
+ if not messages:
197
+ return [], {}
198
+
199
+ return llm_classify_emails(
200
+ messages,
201
+ classes=classes,
202
+ provider=provider,
203
+ model_name=model_name,
204
+ )
205
+
206
+ def prioritize_emails(
207
+ self,
208
+ messages: Sequence[EmailMessage],
209
+ *,
210
+ provider: str,
211
+ model_name: str,
212
+ ) -> Tuple[List[float], Dict[str, Any]]:
213
+ """
214
+ Assign a priority score to multiple emails at once.
215
+ """
216
+ if not messages:
217
+ return [], {}
218
+
219
+ return llm_prioritize_emails(
220
+ messages,
221
+ provider=provider,
222
+ model_name=model_name,
223
+ )
224
+
225
+ def generate_follow_up(
226
+ self,
227
+ message: EmailMessage,
228
+ *,
229
+ provider: str,
230
+ model_name: str,
231
+ ) -> Tuple[str, Dict[str, Any]]:
232
+ """
233
+ Generate a follow-up email to a previous message
234
+ (e.g. when there was no response or action).
235
+ """
236
+ return llm_generate_follow_up_for_email(
237
+ message,
238
+ provider=provider,
239
+ model_name=model_name,
240
+ )
241
+
242
+ def compose_email(
243
+ self,
244
+ instructions: str,
245
+ *,
246
+ provider: str,
247
+ model_name: str,
248
+ ) -> Tuple[str, str, Dict[str, Any]]:
249
+ """
250
+ Compose a new email (subject and body) from natural language instructions
251
+ or bullet points.
252
+ """
253
+ persona = self.profile.generate_prompt() if self.profile else ""
254
+ enriched_instructions = f"{persona}\n\n{instructions}".strip()
255
+
256
+ return llm_compose_email(
257
+ enriched_instructions,
258
+ provider=provider,
259
+ model_name=model_name,
260
+ )
261
+
262
+ def rewrite_email(
263
+ self,
264
+ draft_text: str,
265
+ style: str,
266
+ *,
267
+ provider: str,
268
+ model_name: str,
269
+ ) -> Tuple[str, Dict[str, Any]]:
270
+ """
271
+ Rewrite an email draft according to a requested style.
272
+ """
273
+ return llm_rewrite_email(
274
+ draft_text,
275
+ style,
276
+ provider=provider,
277
+ model_name=model_name,
278
+ )
279
+
280
+ def translate_email(
281
+ self,
282
+ text: str,
283
+ target_language: str,
284
+ *,
285
+ provider: str,
286
+ model_name: str,
287
+ source_language: Optional[str] = None,
288
+ ) -> Tuple[str, Dict[str, Any]]:
289
+ """
290
+ Translate an email or arbitrary text into a target language.
291
+
292
+ target_language: e.g. "en", "es", "fr-FR".
293
+ source_language: optional; if None, LLM may auto-detect.
294
+ """
295
+ return llm_translate_email(
296
+ text,
297
+ target_language=target_language,
298
+ source_language=source_language,
299
+ provider=provider,
300
+ model_name=model_name,
301
+ )
302
+
303
+ def extract_tasks(
304
+ self,
305
+ messages: Sequence[EmailMessage],
306
+ *,
307
+ provider: str,
308
+ model_name: str,
309
+ ) -> Tuple[List[Task], Dict[str, Any]]:
310
+ """
311
+ Extract action items / tasks from one or more emails, using a generic
312
+ Task structure that can be mapped into different domains
313
+ (task managers, CRMs, ticketing systems, etc.).
314
+ """
315
+ if not messages:
316
+ return [], {}
317
+
318
+ return llm_extract_tasks_from_emails(
319
+ messages,
320
+ provider=provider,
321
+ model_name=model_name,
322
+ )
323
+
324
+ def summarize_attachments(
325
+ self,
326
+ message: EmailMessage,
327
+ *,
328
+ provider: str,
329
+ model_name: str,
330
+ ) -> Tuple[Dict[str, str], Dict[str, Any]]:
331
+ """
332
+ Summarize each attachment in an email.
333
+ """
334
+ return llm_summarize_attachments_for_email(
335
+ message,
336
+ provider=provider,
337
+ model_name=model_name,
338
+ )
339
+
340
+ def detect_missing_attachment(self, message: PyEmailMessage) -> bool:
341
+ """
342
+ Heuristically detect if the email text implies an attachment
343
+ should be present, but there is no actual attachment.
344
+ """
345
+ # 1) Check if there are any actual attachments.
346
+ has_attachments = any(
347
+ part.get_content_disposition() == "attachment" for part in message.iter_attachments()
348
+ )
349
+
350
+ if has_attachments:
351
+ return False
352
+
353
+ # 2) Extract a reasonable body representation.
354
+ body_text = ""
355
+ try:
356
+ # Prefer text/plain, fall back to text/html if needed.
357
+ payload = message.get_body(preferencelist=("plain", "html"))
358
+ if payload is not None:
359
+ body_text = payload.get_content() or ""
360
+ else:
361
+ # Fallback: some messages are simple / not multipart
362
+ if message.get_content_type().startswith("text/"):
363
+ body_text = message.get_content() or ""
364
+ except Exception:
365
+ # Be robust to weird encodings / structures
366
+ body_text = ""
367
+
368
+ body_lower = body_text.lower()
369
+
370
+ # 3) Look for common phrases that imply an attachment.
371
+ trigger_phrases = [
372
+ "see attached",
373
+ "see the attached",
374
+ "attached file",
375
+ "attached document",
376
+ "i've attached",
377
+ "i have attached",
378
+ "please find attached",
379
+ ]
380
+
381
+ mentions_attachment = any(phrase in body_lower for phrase in trigger_phrases)
382
+
383
+ return mentions_attachment and not has_attachments
384
+
385
+ def detect_phishing(
386
+ self,
387
+ message: EmailMessage,
388
+ *,
389
+ provider: str,
390
+ model_name: str,
391
+ ) -> Tuple[bool, Dict[str, Any]]:
392
+ """
393
+ Detect whether an email is likely to be a phishing attempt.
394
+ """
395
+ return llm_detect_phishing_for_email(
396
+ message,
397
+ provider=provider,
398
+ model_name=model_name,
399
+ )
400
+
401
+ def evaluate_sender_trust(
402
+ self,
403
+ message: EmailMessage,
404
+ *,
405
+ provider: str,
406
+ model_name: str,
407
+ ) -> Tuple[float, Dict[str, Any]]:
408
+ """
409
+ Evaluate how trustworthy the sender looks based on:
410
+ - email address / domain
411
+ - content patterns
412
+ - known orgs, signatures, etc.
413
+ """
414
+ return llm_evaluate_sender_trust_for_email(
415
+ message,
416
+ provider=provider,
417
+ model_name=model_name,
418
+ )