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.
- openmail/__init__.py +6 -0
- openmail/assistants/__init__.py +35 -0
- openmail/assistants/classify_emails.py +83 -0
- openmail/assistants/compose_email.py +43 -0
- openmail/assistants/detect_phishing_for_email.py +61 -0
- openmail/assistants/evaluate_sender_trust_for_email.py +59 -0
- openmail/assistants/extract_tasks_from_emails.py +126 -0
- openmail/assistants/generate_follow_up_for_email.py +54 -0
- openmail/assistants/natural_language_query.py +699 -0
- openmail/assistants/prioritize_emails.py +89 -0
- openmail/assistants/reply.py +58 -0
- openmail/assistants/reply_suggestions.py +46 -0
- openmail/assistants/rewrite_email.py +50 -0
- openmail/assistants/summarize_attachments_for_email.py +101 -0
- openmail/assistants/summarize_thread_emails.py +55 -0
- openmail/assistants/summary.py +44 -0
- openmail/assistants/summary_multi.py +57 -0
- openmail/assistants/translate_email.py +54 -0
- openmail/auth/__init__.py +6 -0
- openmail/auth/base.py +34 -0
- openmail/auth/no_auth.py +19 -0
- openmail/auth/oauth2.py +58 -0
- openmail/auth/password.py +26 -0
- openmail/config.py +26 -0
- openmail/email_assistant.py +418 -0
- openmail/email_manager.py +777 -0
- openmail/email_query.py +279 -0
- openmail/errors.py +16 -0
- openmail/imap/__init__.py +5 -0
- openmail/imap/attachment_parts.py +55 -0
- openmail/imap/bodystructure.py +296 -0
- openmail/imap/client.py +806 -0
- openmail/imap/fetch_response.py +115 -0
- openmail/imap/inline_cid.py +106 -0
- openmail/imap/pagination.py +16 -0
- openmail/imap/parser.py +298 -0
- openmail/imap/query.py +233 -0
- openmail/llm/__init__.py +3 -0
- openmail/llm/claude.py +35 -0
- openmail/llm/costs.py +108 -0
- openmail/llm/gemini.py +34 -0
- openmail/llm/gpt.py +33 -0
- openmail/llm/groq.py +36 -0
- openmail/llm/model.py +126 -0
- openmail/llm/xai.py +35 -0
- openmail/logger.py +20 -0
- openmail/models/__init__.py +20 -0
- openmail/models/attachment.py +128 -0
- openmail/models/message.py +113 -0
- openmail/models/subscription.py +45 -0
- openmail/models/task.py +24 -0
- openmail/py.typed +0 -0
- openmail/smtp/__init__.py +7 -0
- openmail/smtp/builder.py +41 -0
- openmail/smtp/client.py +218 -0
- openmail/smtp/templates.py +16 -0
- openmail/subscription/__init__.py +7 -0
- openmail/subscription/detector.py +58 -0
- openmail/subscription/parser.py +32 -0
- openmail/subscription/service.py +237 -0
- openmail/types.py +30 -0
- openmail/utils/__init__.py +39 -0
- openmail/utils/utils.py +295 -0
- openmail-0.1.5.dist-info/METADATA +180 -0
- openmail-0.1.5.dist-info/RECORD +67 -0
- openmail-0.1.5.dist-info/WHEEL +4 -0
- openmail-0.1.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
PRIORITIZE_EMAILS_PROMPT = """
|
|
12
|
+
You are an assistant that assigns a priority score to each email.
|
|
13
|
+
|
|
14
|
+
Consider:
|
|
15
|
+
- Urgency and explicit deadlines.
|
|
16
|
+
- Importance of the sender (e.g., manager, key client vs. newsletter).
|
|
17
|
+
- Clear requests for action or decision.
|
|
18
|
+
- Relevance to ongoing work or commitments.
|
|
19
|
+
|
|
20
|
+
Instructions:
|
|
21
|
+
- Each email is identified by an Email ID.
|
|
22
|
+
- For each email, you must output:
|
|
23
|
+
- id: the Email ID (copied exactly as given)
|
|
24
|
+
- score: a numeric priority score between 0.0 and 1.0
|
|
25
|
+
- 1.0 = extremely urgent and important
|
|
26
|
+
- 0.0 = trivial / ignorable
|
|
27
|
+
- Base the score only on the email content and metadata provided.
|
|
28
|
+
- Do not omit any email; produce one item for every Email ID.
|
|
29
|
+
|
|
30
|
+
Emails:
|
|
31
|
+
{email_blocks}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EmailPriorityItem(BaseModel):
|
|
36
|
+
id: str = Field(
|
|
37
|
+
description="Opaque ID that identifies one email exactly as given in the prompt."
|
|
38
|
+
)
|
|
39
|
+
score: float = Field(
|
|
40
|
+
ge=0.0,
|
|
41
|
+
le=1.0,
|
|
42
|
+
description="Priority score between 0.0 (trivial) and 1.0 (critical).",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EmailPrioritySchema(BaseModel):
|
|
47
|
+
items: List[EmailPriorityItem] = Field(
|
|
48
|
+
description=(
|
|
49
|
+
"List of priority results. Each item contains an email id and a score. "
|
|
50
|
+
"There must be one item per email."
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def llm_prioritize_emails(
|
|
56
|
+
messages: Sequence[EmailMessage],
|
|
57
|
+
*,
|
|
58
|
+
provider: str,
|
|
59
|
+
model_name: str,
|
|
60
|
+
) -> Tuple[List[float], Dict[str, Any]]:
|
|
61
|
+
"""
|
|
62
|
+
Assign a priority score to multiple emails at once.
|
|
63
|
+
"""
|
|
64
|
+
if not messages:
|
|
65
|
+
return [], {}
|
|
66
|
+
|
|
67
|
+
id_list = [f"e{i + 1}" for i in range(len(messages))]
|
|
68
|
+
id_to_index = {id_: i for i, id_ in enumerate(id_list)}
|
|
69
|
+
|
|
70
|
+
email_blocks = "\n\n".join(
|
|
71
|
+
f"Email ID: {email_id}\n{build_email_context(msg)}"
|
|
72
|
+
for email_id, msg in zip(id_list, messages)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
prompt = PRIORITIZE_EMAILS_PROMPT.format(
|
|
76
|
+
email_blocks=email_blocks,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
chain = get_model(provider, model_name, EmailPrioritySchema)
|
|
80
|
+
result, llm_call_info = chain(prompt)
|
|
81
|
+
|
|
82
|
+
# Preserve order of input messages
|
|
83
|
+
scores: List[float] = [0.0] * len(messages)
|
|
84
|
+
for item in result.items:
|
|
85
|
+
idx = id_to_index.get(item.id)
|
|
86
|
+
if idx is not None:
|
|
87
|
+
scores[idx] = item.score
|
|
88
|
+
|
|
89
|
+
return scores, llm_call_info
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
EMAIL_REPLY_PROMPT = """
|
|
12
|
+
You are an assistant that drafts concise, polite email replies.
|
|
13
|
+
|
|
14
|
+
The previous suggested reply (for reference or editing):
|
|
15
|
+
{previous_reply}
|
|
16
|
+
|
|
17
|
+
The user's instruction about how to change or generate the reply:
|
|
18
|
+
{reply_context}
|
|
19
|
+
|
|
20
|
+
Instructions (follow all):
|
|
21
|
+
- Either improve/refine the previous reply, or write a new one if needed.
|
|
22
|
+
- Follow the user's instruction above as much as possible.
|
|
23
|
+
- Be professional but friendly.
|
|
24
|
+
- Keep it short and to the point.
|
|
25
|
+
- Do NOT explain what you are doing.
|
|
26
|
+
- Output ONLY the email reply body text (no surrounding quotes).
|
|
27
|
+
|
|
28
|
+
Email context:
|
|
29
|
+
{email_context}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmailReplySchema(BaseModel):
|
|
34
|
+
reply: str = Field(description="A concise reply body for the email.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def llm_concise_reply_for_email(
|
|
38
|
+
reply_context: str,
|
|
39
|
+
msg: EmailMessage,
|
|
40
|
+
*,
|
|
41
|
+
provider: str,
|
|
42
|
+
model_name: str,
|
|
43
|
+
previous_reply: Optional[str] = None,
|
|
44
|
+
) -> Tuple[str, dict[str, Any]]:
|
|
45
|
+
"""
|
|
46
|
+
Generate a concise email reply using the LLM pipeline.
|
|
47
|
+
"""
|
|
48
|
+
chain = get_model(provider, model_name, EmailReplySchema)
|
|
49
|
+
email_context = build_email_context(msg)
|
|
50
|
+
result, llm_call_info = chain(
|
|
51
|
+
EMAIL_REPLY_PROMPT.format(
|
|
52
|
+
previous_reply=previous_reply or "",
|
|
53
|
+
reply_context=reply_context,
|
|
54
|
+
email_context=email_context,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return result.reply, llm_call_info
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
EMAIL_REPLY_SUGGESTION_PROMPT = """
|
|
12
|
+
You are an assistant that generates brief suggestions for how a user could reply to an email.
|
|
13
|
+
|
|
14
|
+
Instructions:
|
|
15
|
+
- Provide 2 to 3 distinct reply strategies.
|
|
16
|
+
- Each suggestion must be short (4 to 10 words).
|
|
17
|
+
- Describe the intent of the reply, NOT the reply text itself.
|
|
18
|
+
- No numbering, no bullets, no quotes, no commentary.
|
|
19
|
+
- Separate each suggestion with a blank line.
|
|
20
|
+
|
|
21
|
+
Email context:
|
|
22
|
+
{email_context}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EmailReplySuggestionsSchema(BaseModel):
|
|
27
|
+
suggestions: List[str] = Field(description="2-3 concise suggestions describing how to reply.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def llm_reply_suggestions_for_email(
|
|
31
|
+
msg: EmailMessage,
|
|
32
|
+
*,
|
|
33
|
+
provider: str,
|
|
34
|
+
model_name: str,
|
|
35
|
+
) -> Tuple[List[str], dict[str, Any]]:
|
|
36
|
+
"""
|
|
37
|
+
Generate 2-3 short reply suggestions using the LLM pipeline.
|
|
38
|
+
"""
|
|
39
|
+
chain = get_model(provider, model_name, EmailReplySuggestionsSchema)
|
|
40
|
+
email_context = build_email_context(msg)
|
|
41
|
+
result, llm_call_info = chain(
|
|
42
|
+
EMAIL_REPLY_SUGGESTION_PROMPT.format(
|
|
43
|
+
email_context=email_context,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return result.suggestions, llm_call_info
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
|
|
9
|
+
REWRITE_EMAIL_PROMPT = """
|
|
10
|
+
You are an assistant that rewrites email drafts while preserving the original meaning.
|
|
11
|
+
|
|
12
|
+
Instructions:
|
|
13
|
+
- Rewrite the email using the requested style.
|
|
14
|
+
- Preserve all important facts, commitments, and dates.
|
|
15
|
+
- Keep the structure of an email (greeting, body, closing) if present.
|
|
16
|
+
- Do not add new information that is not implied by the original.
|
|
17
|
+
- Do not include any explanation; only output the rewritten email text.
|
|
18
|
+
|
|
19
|
+
Requested style:
|
|
20
|
+
{style}
|
|
21
|
+
|
|
22
|
+
Original email:
|
|
23
|
+
{draft}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RewriteEmailSchema(BaseModel):
|
|
28
|
+
rewritten_email: str = Field(
|
|
29
|
+
description="The full email text rewritten in the requested style."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def llm_rewrite_email(
|
|
34
|
+
draft_text: str,
|
|
35
|
+
style: str,
|
|
36
|
+
*,
|
|
37
|
+
provider: str,
|
|
38
|
+
model_name: str,
|
|
39
|
+
) -> Tuple[str, dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
Rewrite an email draft according to a requested style.
|
|
42
|
+
"""
|
|
43
|
+
chain = get_model(provider, model_name, RewriteEmailSchema)
|
|
44
|
+
result, llm_call_info = chain(
|
|
45
|
+
REWRITE_EMAIL_PROMPT.format(
|
|
46
|
+
style=style,
|
|
47
|
+
draft=draft_text,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return result.rewritten_email, llm_call_info
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import looks_binary, safe_decode
|
|
10
|
+
|
|
11
|
+
ATTACHMENT_SUMMARY_PROMPT = """
|
|
12
|
+
You are an assistant that summarizes file attachments from an email.
|
|
13
|
+
|
|
14
|
+
Instructions:
|
|
15
|
+
- For each attachment, provide a concise summary (3-6 sentences).
|
|
16
|
+
- Focus on key points, decisions, and any important data.
|
|
17
|
+
- Do not copy large passages verbatim.
|
|
18
|
+
- If the content is not meaningful (e.g. very short or empty), say so briefly.
|
|
19
|
+
|
|
20
|
+
Below are the attachments with their content.
|
|
21
|
+
|
|
22
|
+
Attachments:
|
|
23
|
+
{attachments_context}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AttachmentSummarySchema(BaseModel):
|
|
28
|
+
filename: str = Field(description="Filename of the attachment.")
|
|
29
|
+
summary: str = Field(description="Concise summary of the attachment's contents.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AttachmentSummariesSchema(BaseModel):
|
|
33
|
+
attachments: List[AttachmentSummarySchema] = Field(
|
|
34
|
+
description="List of summaries for each attachment."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_attachments_context(message: EmailMessage) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Build a text context representing all attachments.
|
|
41
|
+
|
|
42
|
+
Adjust attribute names here to match your actual attachment model.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
attachments = message.attachments
|
|
46
|
+
parts: List[str] = []
|
|
47
|
+
|
|
48
|
+
for idx, att in enumerate(attachments, start=1):
|
|
49
|
+
filename = att.filename
|
|
50
|
+
# Try common attributes for text content
|
|
51
|
+
data = att.data
|
|
52
|
+
decoded = safe_decode(data)
|
|
53
|
+
|
|
54
|
+
if not decoded:
|
|
55
|
+
parts.append(
|
|
56
|
+
f"--- Attachment #{idx} ---\n"
|
|
57
|
+
f"Filename: {filename}\n"
|
|
58
|
+
f"Content: [non-text or empty bytes]\n"
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if looks_binary(decoded):
|
|
63
|
+
parts.append(
|
|
64
|
+
f"--- Attachment #{idx} ---\n"
|
|
65
|
+
f"Filename: {filename}\n"
|
|
66
|
+
f"Content: [binary data - not summarized]\n"
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if len(decoded) > 4000:
|
|
71
|
+
decoded = decoded[:4000] + "\n...[truncated]..."
|
|
72
|
+
|
|
73
|
+
parts.append(
|
|
74
|
+
f"--- Attachment #{idx} ---\n" f"Filename: {filename}\n" f"Content:\n{decoded}\n"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return "\n".join(parts)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def llm_summarize_attachments_for_email(
|
|
81
|
+
message: EmailMessage,
|
|
82
|
+
*,
|
|
83
|
+
provider: str,
|
|
84
|
+
model_name: str,
|
|
85
|
+
) -> Tuple[Dict[str, str], dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
Summarize each attachment in an email.
|
|
88
|
+
"""
|
|
89
|
+
attachments = message.attachments
|
|
90
|
+
if not attachments:
|
|
91
|
+
return {}, {}
|
|
92
|
+
|
|
93
|
+
attachments_context = _build_attachments_context(message)
|
|
94
|
+
|
|
95
|
+
chain = get_model(provider, model_name, AttachmentSummariesSchema)
|
|
96
|
+
result, llm_call_info = chain(
|
|
97
|
+
ATTACHMENT_SUMMARY_PROMPT.format(attachments_context=attachments_context)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
summaries: Dict[str, str] = {att.filename: att.summary for att in result.attachments}
|
|
101
|
+
return summaries, llm_call_info
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
THREAD_SUMMARY_PROMPT = """
|
|
12
|
+
You are an assistant that summarizes email conversation threads.
|
|
13
|
+
|
|
14
|
+
Instructions:
|
|
15
|
+
- Read the entire thread carefully.
|
|
16
|
+
- Provide a concise summary (5-10 sentences).
|
|
17
|
+
- Focus on:
|
|
18
|
+
- Overall context and purpose of the thread.
|
|
19
|
+
- Key decisions that have been made.
|
|
20
|
+
- Open questions that still need answers.
|
|
21
|
+
- Action items and who is responsible for them (if specified).
|
|
22
|
+
- Write in clear, professional language.
|
|
23
|
+
- Do not include greetings or sign-offs.
|
|
24
|
+
|
|
25
|
+
Email thread (oldest to newest):
|
|
26
|
+
{thread_context}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ThreadSummarySchema(BaseModel):
|
|
31
|
+
summary: str = Field(
|
|
32
|
+
description="Concise summary of the email thread, including context, decisions, open questions, and action items."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def llm_summarize_thread_emails(
|
|
37
|
+
messages: Sequence[EmailMessage],
|
|
38
|
+
*,
|
|
39
|
+
provider: str,
|
|
40
|
+
model_name: str,
|
|
41
|
+
) -> Tuple[str, dict[str, Any]]:
|
|
42
|
+
"""
|
|
43
|
+
Summarize a sequence of emails representing a thread.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
parts: List[str] = []
|
|
47
|
+
for idx, msg in enumerate(messages, start=1):
|
|
48
|
+
ctx = build_email_context(msg)
|
|
49
|
+
parts.append(f"--- Email #{idx} ---\n{ctx}\n")
|
|
50
|
+
|
|
51
|
+
thread_context = "\n".join(parts)
|
|
52
|
+
|
|
53
|
+
chain = get_model(provider, model_name, ThreadSummarySchema)
|
|
54
|
+
result, llm_call_info = chain(THREAD_SUMMARY_PROMPT.format(thread_context=thread_context))
|
|
55
|
+
return result.summary, llm_call_info
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
EMAIL_SUMMARY_PROMPT = """
|
|
12
|
+
You are an assistant that summarizes emails for a busy user.
|
|
13
|
+
|
|
14
|
+
Instructions (follow all):
|
|
15
|
+
- Summarize the email in 2-4 sentences.
|
|
16
|
+
- Capture the main points, action items, and any dates/deadlines.
|
|
17
|
+
- Do NOT include any meta commentary, just the summary text.
|
|
18
|
+
|
|
19
|
+
Email context:
|
|
20
|
+
{email_context}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EmailSummarySchema(BaseModel):
|
|
25
|
+
summary: str = Field(description="A concise summary of a single email.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def llm_summarize_single_email(
|
|
29
|
+
msg: EmailMessage,
|
|
30
|
+
*,
|
|
31
|
+
provider: str,
|
|
32
|
+
model_name: str,
|
|
33
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
34
|
+
"""
|
|
35
|
+
Generate a concise email reply using the LLM pipeline.
|
|
36
|
+
"""
|
|
37
|
+
chain = get_model(provider, model_name, EmailSummarySchema)
|
|
38
|
+
email_context = build_email_context(msg)
|
|
39
|
+
result, llm_call_info = chain(
|
|
40
|
+
EMAIL_SUMMARY_PROMPT.format(
|
|
41
|
+
email_context=email_context,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
return result.summary, llm_call_info
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
EMAIL_MULTI_SUMMARY_PROMPT = """
|
|
12
|
+
You are helping a user triage multiple emails.
|
|
13
|
+
|
|
14
|
+
You will see several emails, each labeled with a UID.
|
|
15
|
+
|
|
16
|
+
Instructions (follow all):
|
|
17
|
+
- Write ONE short paragraph.
|
|
18
|
+
- Clearly state which emails are IMPORTANT or URGENT.
|
|
19
|
+
- Briefly mention the other emails as less important.
|
|
20
|
+
- When referring to an IMPORTANT email, you MUST mention its UID exactly
|
|
21
|
+
as 'UID=<number>' so the UI can link directly to it.
|
|
22
|
+
- Keep the paragraph compact and easy to scan.
|
|
23
|
+
|
|
24
|
+
Emails:
|
|
25
|
+
{emails_block}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EmailMultiSummarySchema(BaseModel):
|
|
30
|
+
summary: str = Field(description="One paragraph summarizing and prioritizing multiple emails.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def llm_summarize_many_emails(
|
|
34
|
+
messages: Sequence[EmailMessage],
|
|
35
|
+
*,
|
|
36
|
+
provider: str,
|
|
37
|
+
model_name: str,
|
|
38
|
+
) -> Tuple[Optional[str], Dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Generate a concise email reply using the LLM pipeline.
|
|
41
|
+
"""
|
|
42
|
+
chain = get_model(provider, model_name, EmailMultiSummarySchema)
|
|
43
|
+
|
|
44
|
+
blocks: List[str] = []
|
|
45
|
+
for msg in messages:
|
|
46
|
+
email_context = build_email_context(msg)
|
|
47
|
+
blocks.append(email_context)
|
|
48
|
+
|
|
49
|
+
emails_block = "\n---\n".join(blocks)
|
|
50
|
+
|
|
51
|
+
result, llm_call_info = chain(
|
|
52
|
+
EMAIL_MULTI_SUMMARY_PROMPT.format(
|
|
53
|
+
emails_block=emails_block,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return result.summary, llm_call_info
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
|
|
9
|
+
TRANSLATE_EMAIL_PROMPT = """
|
|
10
|
+
You are a translation assistant.
|
|
11
|
+
|
|
12
|
+
Instructions:
|
|
13
|
+
- Translate the text into the target language.
|
|
14
|
+
- Preserve tone and level of formality as much as reasonable.
|
|
15
|
+
- Do not add explanations, comments, or notes—only output the translated text.
|
|
16
|
+
- Keep formatting (line breaks, bullet points) where it makes sense.
|
|
17
|
+
|
|
18
|
+
Target language: {target_language}
|
|
19
|
+
{source_line}
|
|
20
|
+
|
|
21
|
+
Text to translate:
|
|
22
|
+
{text}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TranslateEmailSchema(BaseModel):
|
|
27
|
+
translated_text: str = Field(description="The translated text in the target language.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def llm_translate_email(
|
|
31
|
+
text: str,
|
|
32
|
+
*,
|
|
33
|
+
target_language: str,
|
|
34
|
+
source_language: Optional[str],
|
|
35
|
+
provider: str,
|
|
36
|
+
model_name: str,
|
|
37
|
+
) -> Tuple[str, dict[str, Any]]:
|
|
38
|
+
"""
|
|
39
|
+
Translate arbitrary text to a target language.
|
|
40
|
+
"""
|
|
41
|
+
if source_language:
|
|
42
|
+
source_line = f"Source language: {source_language}"
|
|
43
|
+
else:
|
|
44
|
+
source_line = "Source language: auto-detect"
|
|
45
|
+
|
|
46
|
+
chain = get_model(provider, model_name, TranslateEmailSchema)
|
|
47
|
+
result, llm_call_info = chain(
|
|
48
|
+
TRANSLATE_EMAIL_PROMPT.format(
|
|
49
|
+
target_language=target_language,
|
|
50
|
+
source_line=source_line,
|
|
51
|
+
text=text,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return result.translated_text, llm_call_info
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from openmail.auth.base import AuthContext, IMAPAuth, SMTPAuth
|
|
2
|
+
from openmail.auth.no_auth import NoAuth
|
|
3
|
+
from openmail.auth.oauth2 import OAuth2Auth
|
|
4
|
+
from openmail.auth.password import PasswordAuth
|
|
5
|
+
|
|
6
|
+
__all__ = ["SMTPAuth", "IMAPAuth", "AuthContext", "PasswordAuth", "OAuth2Auth", "NoAuth"]
|
openmail/auth/base.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class AuthContext:
|
|
9
|
+
"""
|
|
10
|
+
Extra context an auth method may need.
|
|
11
|
+
(Keep this minimal; extend later.)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
host: str
|
|
15
|
+
port: int
|
|
16
|
+
username: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class SMTPAuth(Protocol):
|
|
21
|
+
"""
|
|
22
|
+
Something that knows how to authenticate an smtplib.SMTP-like object.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def apply_smtp(self, server, ctx: AuthContext) -> None: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class IMAPAuth(Protocol):
|
|
30
|
+
"""
|
|
31
|
+
Something that knows how to authenticate an imaplib.IMAP4-like object.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def apply_imap(self, conn, ctx: AuthContext) -> None: ...
|
openmail/auth/no_auth.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from openmail.auth.base import AuthContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class NoAuth:
|
|
10
|
+
"""
|
|
11
|
+
Represents 'no authentication required'. Useful for SMTP relays
|
|
12
|
+
or IMAP/SMTP servers that don't require login for this client.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def apply_smtp(self, server, ctx: AuthContext) -> None:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
def apply_imap(self, conn, ctx: AuthContext) -> None:
|
|
19
|
+
return
|
openmail/auth/oauth2.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from openmail.auth.base import AuthContext
|
|
8
|
+
from openmail.errors import AuthError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class OAuth2Auth:
|
|
13
|
+
"""
|
|
14
|
+
XOAUTH2-based auth. You provide a function that returns a fresh access token.
|
|
15
|
+
- token_provider() -> access_token (string)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
username: str
|
|
19
|
+
token_provider: Callable[..., str]
|
|
20
|
+
|
|
21
|
+
def _raw_xoauth2(self, access_token: str) -> str:
|
|
22
|
+
return f"user={self.username}\x01auth=Bearer {access_token}\x01\x01"
|
|
23
|
+
|
|
24
|
+
def apply_imap(self, conn, ctx: AuthContext) -> None:
|
|
25
|
+
try:
|
|
26
|
+
token = self.token_provider()
|
|
27
|
+
if not token:
|
|
28
|
+
raise AuthError("OAuth2 token provider returned empty token")
|
|
29
|
+
|
|
30
|
+
auth_bytes = self._raw_xoauth2(token).encode("utf-8")
|
|
31
|
+
|
|
32
|
+
def auth_cb(_):
|
|
33
|
+
return auth_bytes
|
|
34
|
+
|
|
35
|
+
typ, data = conn.authenticate("XOAUTH2", auth_cb)
|
|
36
|
+
if typ != "OK":
|
|
37
|
+
raise AuthError(f"IMAP XOAUTH2 auth failed (non-OK response: {typ}, {data})")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise AuthError(f"IMAP XOAUTH2 auth failed: {e}") from e
|
|
40
|
+
|
|
41
|
+
def apply_smtp(self, server, ctx: AuthContext) -> None:
|
|
42
|
+
"""
|
|
43
|
+
smtplib doesn't expose a single 'authenticate XOAUTH2' helper,
|
|
44
|
+
so we send AUTH XOAUTH2 with a base64-encoded initial response.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
token = self.token_provider()
|
|
48
|
+
if not token:
|
|
49
|
+
raise AuthError("OAuth2 token provider returned empty token")
|
|
50
|
+
|
|
51
|
+
raw = self._raw_xoauth2(token)
|
|
52
|
+
auth_b64 = base64.b64encode(raw.encode("utf-8")).decode("ascii")
|
|
53
|
+
|
|
54
|
+
code, resp = server.docmd("AUTH", "XOAUTH2 " + auth_b64)
|
|
55
|
+
if code != 235:
|
|
56
|
+
raise AuthError(f"SMTP XOAUTH2 auth failed: {code} {resp!r}")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise AuthError(f"SMTP XOAUTH2 auth failed: {e}") from e
|