mseep-email-client 0.1.1__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.
- email_client/__init__.py +8 -0
- email_client/server.py +482 -0
- mseep_email_client-0.1.1.dist-info/METADATA +14 -0
- mseep_email_client-0.1.1.dist-info/RECORD +8 -0
- mseep_email_client-0.1.1.dist-info/WHEEL +5 -0
- mseep_email_client-0.1.1.dist-info/entry_points.txt +2 -0
- mseep_email_client-0.1.1.dist-info/licenses/LICENSE +21 -0
- mseep_email_client-0.1.1.dist-info/top_level.txt +1 -0
email_client/__init__.py
ADDED
email_client/server.py
ADDED
@@ -0,0 +1,482 @@
|
|
1
|
+
from typing import Any
|
2
|
+
import asyncio
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
import email
|
5
|
+
import imaplib
|
6
|
+
import smtplib
|
7
|
+
import logging
|
8
|
+
from email.mime.text import MIMEText
|
9
|
+
from email.mime.multipart import MIMEMultipart
|
10
|
+
import os
|
11
|
+
from dotenv import load_dotenv
|
12
|
+
from mcp.server.models import InitializationOptions
|
13
|
+
import mcp.types as types
|
14
|
+
from mcp.server import NotificationOptions, Server
|
15
|
+
import mcp.server.stdio
|
16
|
+
|
17
|
+
# Configure logging
|
18
|
+
logging.basicConfig(
|
19
|
+
level=logging.DEBUG,
|
20
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
21
|
+
filename='email_client.log'
|
22
|
+
)
|
23
|
+
|
24
|
+
# Load environment variables from .env file
|
25
|
+
load_dotenv()
|
26
|
+
|
27
|
+
# Email configuration
|
28
|
+
EMAIL_CONFIG = {
|
29
|
+
"email": os.getenv("EMAIL_ADDRESS", "your.email@gmail.com"),
|
30
|
+
"password": os.getenv("EMAIL_PASSWORD", "your-app-specific-password"),
|
31
|
+
"imap_server": os.getenv("IMAP_SERVER", "imap.gmail.com"),
|
32
|
+
"smtp_server": os.getenv("SMTP_SERVER", "smtp.gmail.com"),
|
33
|
+
"smtp_port": int(os.getenv("SMTP_PORT", "587"))
|
34
|
+
}
|
35
|
+
|
36
|
+
# Constants
|
37
|
+
SEARCH_TIMEOUT = 60 # seconds
|
38
|
+
MAX_EMAILS = 100
|
39
|
+
|
40
|
+
server = Server("email")
|
41
|
+
|
42
|
+
def format_email_summary(msg_data: tuple) -> dict:
|
43
|
+
"""Format an email message into a summary dict with basic information."""
|
44
|
+
email_body = email.message_from_bytes(msg_data[0][1])
|
45
|
+
|
46
|
+
return {
|
47
|
+
"id": msg_data[0][0].split()[0].decode(), # Get the email ID
|
48
|
+
"from": email_body.get("From", "Unknown"),
|
49
|
+
"date": email_body.get("Date", "Unknown"),
|
50
|
+
"subject": email_body.get("Subject", "No Subject"),
|
51
|
+
}
|
52
|
+
|
53
|
+
def format_email_content(msg_data: tuple) -> dict:
|
54
|
+
"""Format an email message into a dict with full content."""
|
55
|
+
email_body = email.message_from_bytes(msg_data[0][1])
|
56
|
+
|
57
|
+
# Extract body content
|
58
|
+
body = ""
|
59
|
+
if email_body.is_multipart():
|
60
|
+
# Handle multipart messages
|
61
|
+
for part in email_body.walk():
|
62
|
+
if part.get_content_type() == "text/plain":
|
63
|
+
body = part.get_payload(decode=True).decode()
|
64
|
+
break
|
65
|
+
elif part.get_content_type() == "text/html":
|
66
|
+
# If no plain text found, use HTML content
|
67
|
+
if not body:
|
68
|
+
body = part.get_payload(decode=True).decode()
|
69
|
+
else:
|
70
|
+
# Handle non-multipart messages
|
71
|
+
body = email_body.get_payload(decode=True).decode()
|
72
|
+
|
73
|
+
return {
|
74
|
+
"from": email_body.get("From", "Unknown"),
|
75
|
+
"to": email_body.get("To", "Unknown"),
|
76
|
+
"date": email_body.get("Date", "Unknown"),
|
77
|
+
"subject": email_body.get("Subject", "No Subject"),
|
78
|
+
"content": body
|
79
|
+
}
|
80
|
+
|
81
|
+
async def search_emails_async(mail: imaplib.IMAP4_SSL, search_criteria: str) -> list[dict]:
|
82
|
+
"""Asynchronously search emails with timeout."""
|
83
|
+
loop = asyncio.get_event_loop()
|
84
|
+
try:
|
85
|
+
_, messages = await loop.run_in_executor(None, lambda: mail.search(None, search_criteria))
|
86
|
+
if not messages[0]:
|
87
|
+
return []
|
88
|
+
|
89
|
+
email_list = []
|
90
|
+
for num in messages[0].split()[:MAX_EMAILS]: # Limit to MAX_EMAILS
|
91
|
+
_, msg_data = await loop.run_in_executor(None, lambda: mail.fetch(num, '(RFC822)'))
|
92
|
+
email_list.append(format_email_summary(msg_data))
|
93
|
+
|
94
|
+
return email_list
|
95
|
+
except Exception as e:
|
96
|
+
raise Exception(f"Error searching emails: {str(e)}")
|
97
|
+
|
98
|
+
async def get_email_content_async(mail: imaplib.IMAP4_SSL, email_id: str) -> dict:
|
99
|
+
"""Asynchronously get full content of a specific email."""
|
100
|
+
loop = asyncio.get_event_loop()
|
101
|
+
try:
|
102
|
+
_, msg_data = await loop.run_in_executor(None, lambda: mail.fetch(email_id, '(RFC822)'))
|
103
|
+
return format_email_content(msg_data)
|
104
|
+
except Exception as e:
|
105
|
+
raise Exception(f"Error fetching email content: {str(e)}")
|
106
|
+
|
107
|
+
async def count_emails_async(mail: imaplib.IMAP4_SSL, search_criteria: str) -> int:
|
108
|
+
"""Asynchronously count emails matching the search criteria."""
|
109
|
+
loop = asyncio.get_event_loop()
|
110
|
+
try:
|
111
|
+
_, messages = await loop.run_in_executor(None, lambda: mail.search(None, search_criteria))
|
112
|
+
return len(messages[0].split()) if messages[0] else 0
|
113
|
+
except Exception as e:
|
114
|
+
raise Exception(f"Error counting emails: {str(e)}")
|
115
|
+
|
116
|
+
async def send_email_async(
|
117
|
+
to_addresses: list[str],
|
118
|
+
subject: str,
|
119
|
+
content: str,
|
120
|
+
cc_addresses: list[str] | None = None
|
121
|
+
) -> None:
|
122
|
+
"""Asynchronously send an email."""
|
123
|
+
try:
|
124
|
+
# Create message
|
125
|
+
msg = MIMEMultipart()
|
126
|
+
msg['From'] = EMAIL_CONFIG["email"]
|
127
|
+
msg['To'] = ', '.join(to_addresses)
|
128
|
+
if cc_addresses:
|
129
|
+
msg['Cc'] = ', '.join(cc_addresses)
|
130
|
+
msg['Subject'] = subject
|
131
|
+
|
132
|
+
# Add body
|
133
|
+
msg.attach(MIMEText(content, 'plain', 'utf-8'))
|
134
|
+
|
135
|
+
# Connect to SMTP server and send email
|
136
|
+
def send_sync():
|
137
|
+
with smtplib.SMTP(EMAIL_CONFIG["smtp_server"], EMAIL_CONFIG["smtp_port"]) as server:
|
138
|
+
server.set_debuglevel(1) # Enable debug output
|
139
|
+
logging.debug(f"Connecting to {EMAIL_CONFIG['smtp_server']}:{EMAIL_CONFIG['smtp_port']}")
|
140
|
+
|
141
|
+
# Start TLS
|
142
|
+
logging.debug("Starting TLS")
|
143
|
+
server.starttls()
|
144
|
+
|
145
|
+
# Login
|
146
|
+
logging.debug(f"Logging in as {EMAIL_CONFIG['email']}")
|
147
|
+
server.login(EMAIL_CONFIG["email"], EMAIL_CONFIG["password"])
|
148
|
+
|
149
|
+
# Send email
|
150
|
+
all_recipients = to_addresses + (cc_addresses or [])
|
151
|
+
logging.debug(f"Sending email to: {all_recipients}")
|
152
|
+
result = server.send_message(msg, EMAIL_CONFIG["email"], all_recipients)
|
153
|
+
|
154
|
+
if result:
|
155
|
+
# send_message returns a dict of failed recipients
|
156
|
+
raise Exception(f"Failed to send to some recipients: {result}")
|
157
|
+
|
158
|
+
logging.debug("Email sent successfully")
|
159
|
+
|
160
|
+
# Run the synchronous send function in the executor
|
161
|
+
loop = asyncio.get_event_loop()
|
162
|
+
await loop.run_in_executor(None, send_sync)
|
163
|
+
|
164
|
+
except Exception as e:
|
165
|
+
logging.error(f"Error in send_email_async: {str(e)}")
|
166
|
+
raise
|
167
|
+
|
168
|
+
@server.list_tools()
|
169
|
+
async def handle_list_tools() -> list[types.Tool]:
|
170
|
+
"""
|
171
|
+
List available tools.
|
172
|
+
Each tool specifies its arguments using JSON Schema validation.
|
173
|
+
"""
|
174
|
+
return [
|
175
|
+
types.Tool(
|
176
|
+
name="search-emails",
|
177
|
+
description="Search emails within a date range and/or with specific keywords",
|
178
|
+
inputSchema={
|
179
|
+
"type": "object",
|
180
|
+
"properties": {
|
181
|
+
"start_date": {
|
182
|
+
"type": "string",
|
183
|
+
"description": "Start date in YYYY-MM-DD format (optional)",
|
184
|
+
},
|
185
|
+
"end_date": {
|
186
|
+
"type": "string",
|
187
|
+
"description": "End date in YYYY-MM-DD format (optional)",
|
188
|
+
},
|
189
|
+
"keyword": {
|
190
|
+
"type": "string",
|
191
|
+
"description": "Keyword to search in email subject and body (optional)",
|
192
|
+
},
|
193
|
+
"folder": {
|
194
|
+
"type": "string",
|
195
|
+
"description": "Folder to search in ('inbox' or 'sent', defaults to 'inbox')",
|
196
|
+
"enum": ["inbox", "sent"],
|
197
|
+
},
|
198
|
+
},
|
199
|
+
},
|
200
|
+
),
|
201
|
+
types.Tool(
|
202
|
+
name="get-email-content",
|
203
|
+
description="Get the full content of a specific email by its ID",
|
204
|
+
inputSchema={
|
205
|
+
"type": "object",
|
206
|
+
"properties": {
|
207
|
+
"email_id": {
|
208
|
+
"type": "string",
|
209
|
+
"description": "The ID of the email to retrieve",
|
210
|
+
},
|
211
|
+
},
|
212
|
+
"required": ["email_id"],
|
213
|
+
},
|
214
|
+
),
|
215
|
+
types.Tool(
|
216
|
+
name="count-daily-emails",
|
217
|
+
description="Count emails received for each day in a date range",
|
218
|
+
inputSchema={
|
219
|
+
"type": "object",
|
220
|
+
"properties": {
|
221
|
+
"start_date": {
|
222
|
+
"type": "string",
|
223
|
+
"description": "Start date in YYYY-MM-DD format",
|
224
|
+
},
|
225
|
+
"end_date": {
|
226
|
+
"type": "string",
|
227
|
+
"description": "End date in YYYY-MM-DD format",
|
228
|
+
},
|
229
|
+
},
|
230
|
+
"required": ["start_date", "end_date"],
|
231
|
+
},
|
232
|
+
),
|
233
|
+
types.Tool(
|
234
|
+
name="send-email",
|
235
|
+
description="CONFIRMATION STEP: Actually send the email after user confirms the details. Before calling this, first show the email details to the user for confirmation. Required fields: recipients (to), subject, and content. Optional: CC recipients.",
|
236
|
+
inputSchema={
|
237
|
+
"type": "object",
|
238
|
+
"properties": {
|
239
|
+
"to": {
|
240
|
+
"type": "array",
|
241
|
+
"items": {"type": "string"},
|
242
|
+
"description": "List of recipient email addresses (confirmed)",
|
243
|
+
},
|
244
|
+
"subject": {
|
245
|
+
"type": "string",
|
246
|
+
"description": "Confirmed email subject",
|
247
|
+
},
|
248
|
+
"content": {
|
249
|
+
"type": "string",
|
250
|
+
"description": "Confirmed email content",
|
251
|
+
},
|
252
|
+
"cc": {
|
253
|
+
"type": "array",
|
254
|
+
"items": {"type": "string"},
|
255
|
+
"description": "List of CC recipient email addresses (optional, confirmed)",
|
256
|
+
},
|
257
|
+
},
|
258
|
+
"required": ["to", "subject", "content"],
|
259
|
+
},
|
260
|
+
),
|
261
|
+
]
|
262
|
+
|
263
|
+
@server.call_tool()
|
264
|
+
async def handle_call_tool(
|
265
|
+
name: str, arguments: dict | None
|
266
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
267
|
+
"""
|
268
|
+
Handle tool execution requests.
|
269
|
+
Tools can search emails and return results.
|
270
|
+
"""
|
271
|
+
if not arguments:
|
272
|
+
arguments = {}
|
273
|
+
|
274
|
+
try:
|
275
|
+
if name == "send-email":
|
276
|
+
to_addresses = arguments.get("to", [])
|
277
|
+
subject = arguments.get("subject", "")
|
278
|
+
content = arguments.get("content", "")
|
279
|
+
cc_addresses = arguments.get("cc", [])
|
280
|
+
|
281
|
+
if not to_addresses:
|
282
|
+
return [types.TextContent(
|
283
|
+
type="text",
|
284
|
+
text="At least one recipient email address is required."
|
285
|
+
)]
|
286
|
+
|
287
|
+
try:
|
288
|
+
logging.info("Attempting to send email")
|
289
|
+
logging.info(f"To: {to_addresses}")
|
290
|
+
logging.info(f"Subject: {subject}")
|
291
|
+
logging.info(f"CC: {cc_addresses}")
|
292
|
+
|
293
|
+
async with asyncio.timeout(SEARCH_TIMEOUT):
|
294
|
+
await send_email_async(to_addresses, subject, content, cc_addresses)
|
295
|
+
return [types.TextContent(
|
296
|
+
type="text",
|
297
|
+
text="Email sent successfully! Check email_client.log for detailed logs."
|
298
|
+
)]
|
299
|
+
except asyncio.TimeoutError:
|
300
|
+
logging.error("Operation timed out while sending email")
|
301
|
+
return [types.TextContent(
|
302
|
+
type="text",
|
303
|
+
text="Operation timed out while sending email."
|
304
|
+
)]
|
305
|
+
except Exception as e:
|
306
|
+
error_msg = str(e)
|
307
|
+
logging.error(f"Failed to send email: {error_msg}")
|
308
|
+
return [types.TextContent(
|
309
|
+
type="text",
|
310
|
+
text=f"Failed to send email: {error_msg}\n\nPlease check:\n1. Email and password are correct in .env\n2. SMTP settings are correct\n3. Less secure app access is enabled (for Gmail)\n4. Using App Password if 2FA is enabled"
|
311
|
+
)]
|
312
|
+
|
313
|
+
# Connect to IMAP server using predefined credentials
|
314
|
+
mail = imaplib.IMAP4_SSL(EMAIL_CONFIG["imap_server"])
|
315
|
+
mail.login(EMAIL_CONFIG["email"], EMAIL_CONFIG["password"])
|
316
|
+
|
317
|
+
if name == "search-emails":
|
318
|
+
# 选择文件夹
|
319
|
+
folder = arguments.get("folder", "inbox") # 默认选择收件箱
|
320
|
+
if folder == "sent":
|
321
|
+
mail.select('"[Gmail]/Sent Mail"') # 对于 Gmail
|
322
|
+
else:
|
323
|
+
mail.select("inbox")
|
324
|
+
|
325
|
+
# Get optional parameters
|
326
|
+
start_date = arguments.get("start_date")
|
327
|
+
end_date = arguments.get("end_date")
|
328
|
+
keyword = arguments.get("keyword")
|
329
|
+
|
330
|
+
# If no dates provided, default to last 7 days
|
331
|
+
if not start_date:
|
332
|
+
start_date = datetime.now() - timedelta(days=7)
|
333
|
+
start_date = start_date.strftime("%d-%b-%Y")
|
334
|
+
else:
|
335
|
+
start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%d-%b-%Y")
|
336
|
+
|
337
|
+
if not end_date:
|
338
|
+
end_date = datetime.now().strftime("%d-%b-%Y")
|
339
|
+
else:
|
340
|
+
# Convert end_date to datetime object once
|
341
|
+
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
|
342
|
+
end_date = end_date_obj.strftime("%d-%b-%Y")
|
343
|
+
|
344
|
+
# Build search criteria
|
345
|
+
if start_date == end_date:
|
346
|
+
# If searching for a single day
|
347
|
+
search_criteria = f'ON "{start_date}"'
|
348
|
+
else:
|
349
|
+
# Calculate next day using the already converted end_date_obj
|
350
|
+
next_day = (end_date_obj + timedelta(days=1)).strftime("%d-%b-%Y")
|
351
|
+
search_criteria = f'SINCE "{start_date}" BEFORE "{next_day}"'
|
352
|
+
|
353
|
+
if keyword:
|
354
|
+
# Fix: Properly combine keyword search with date criteria
|
355
|
+
keyword_criteria = f'(OR SUBJECT "{keyword}" BODY "{keyword}")'
|
356
|
+
search_criteria = f'({keyword_criteria} {search_criteria})'
|
357
|
+
|
358
|
+
logging.debug(f"Search criteria: {search_criteria}") # Add debug logging
|
359
|
+
|
360
|
+
try:
|
361
|
+
async with asyncio.timeout(SEARCH_TIMEOUT):
|
362
|
+
email_list = await search_emails_async(mail, search_criteria)
|
363
|
+
|
364
|
+
if not email_list:
|
365
|
+
return [types.TextContent(
|
366
|
+
type="text",
|
367
|
+
text="No emails found matching the criteria."
|
368
|
+
)]
|
369
|
+
|
370
|
+
# Format the results as a table
|
371
|
+
result_text = "Found emails:\n\n"
|
372
|
+
result_text += "ID | From | Date | Subject\n"
|
373
|
+
result_text += "-" * 80 + "\n"
|
374
|
+
|
375
|
+
for email in email_list:
|
376
|
+
result_text += f"{email['id']} | {email['from']} | {email['date']} | {email['subject']}\n"
|
377
|
+
|
378
|
+
result_text += "\nUse get-email-content with an email ID to view the full content of a specific email."
|
379
|
+
|
380
|
+
return [types.TextContent(
|
381
|
+
type="text",
|
382
|
+
text=result_text
|
383
|
+
)]
|
384
|
+
|
385
|
+
except asyncio.TimeoutError:
|
386
|
+
return [types.TextContent(
|
387
|
+
type="text",
|
388
|
+
text="Search operation timed out. Please try with a more specific search criteria."
|
389
|
+
)]
|
390
|
+
|
391
|
+
elif name == "get-email-content":
|
392
|
+
email_id = arguments.get("email_id")
|
393
|
+
if not email_id:
|
394
|
+
return [types.TextContent(
|
395
|
+
type="text",
|
396
|
+
text="Email ID is required."
|
397
|
+
)]
|
398
|
+
|
399
|
+
try:
|
400
|
+
async with asyncio.timeout(SEARCH_TIMEOUT):
|
401
|
+
email_content = await get_email_content_async(mail, email_id)
|
402
|
+
|
403
|
+
result_text = (
|
404
|
+
f"From: {email_content['from']}\n"
|
405
|
+
f"To: {email_content['to']}\n"
|
406
|
+
f"Date: {email_content['date']}\n"
|
407
|
+
f"Subject: {email_content['subject']}\n"
|
408
|
+
f"\nContent:\n{email_content['content']}"
|
409
|
+
)
|
410
|
+
|
411
|
+
return [types.TextContent(
|
412
|
+
type="text",
|
413
|
+
text=result_text
|
414
|
+
)]
|
415
|
+
|
416
|
+
except asyncio.TimeoutError:
|
417
|
+
return [types.TextContent(
|
418
|
+
type="text",
|
419
|
+
text="Operation timed out while fetching email content."
|
420
|
+
)]
|
421
|
+
|
422
|
+
elif name == "count-daily-emails":
|
423
|
+
start_date = datetime.strptime(arguments["start_date"], "%Y-%m-%d")
|
424
|
+
end_date = datetime.strptime(arguments["end_date"], "%Y-%m-%d")
|
425
|
+
|
426
|
+
result_text = "Daily email counts:\n\n"
|
427
|
+
result_text += "Date | Count\n"
|
428
|
+
result_text += "-" * 30 + "\n"
|
429
|
+
|
430
|
+
current_date = start_date
|
431
|
+
while current_date <= end_date:
|
432
|
+
date_str = current_date.strftime("%d-%b-%Y")
|
433
|
+
search_criteria = f'(ON "{date_str}")'
|
434
|
+
|
435
|
+
try:
|
436
|
+
async with asyncio.timeout(SEARCH_TIMEOUT):
|
437
|
+
count = await count_emails_async(mail, search_criteria)
|
438
|
+
result_text += f"{current_date.strftime('%Y-%m-%d')} | {count}\n"
|
439
|
+
except asyncio.TimeoutError:
|
440
|
+
result_text += f"{current_date.strftime('%Y-%m-%d')} | Timeout\n"
|
441
|
+
|
442
|
+
current_date += timedelta(days=1)
|
443
|
+
|
444
|
+
return [types.TextContent(
|
445
|
+
type="text",
|
446
|
+
text=result_text
|
447
|
+
)]
|
448
|
+
|
449
|
+
else:
|
450
|
+
raise ValueError(f"Unknown tool: {name}")
|
451
|
+
|
452
|
+
except Exception as e:
|
453
|
+
return [types.TextContent(
|
454
|
+
type="text",
|
455
|
+
text=f"Error: {str(e)}"
|
456
|
+
)]
|
457
|
+
finally:
|
458
|
+
try:
|
459
|
+
mail.close()
|
460
|
+
mail.logout()
|
461
|
+
except:
|
462
|
+
pass
|
463
|
+
|
464
|
+
async def main():
|
465
|
+
# Run the server using stdin/stdout streams
|
466
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
467
|
+
await server.run(
|
468
|
+
read_stream,
|
469
|
+
write_stream,
|
470
|
+
InitializationOptions(
|
471
|
+
server_name="email",
|
472
|
+
server_version="0.1.0",
|
473
|
+
capabilities=server.get_capabilities(
|
474
|
+
notification_options=NotificationOptions(),
|
475
|
+
experimental_capabilities={},
|
476
|
+
),
|
477
|
+
),
|
478
|
+
)
|
479
|
+
|
480
|
+
if __name__ == "__main__":
|
481
|
+
asyncio.run(main())
|
482
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: mseep-email_client
|
3
|
+
Version: 0.1.1
|
4
|
+
Summary: Email search and management tool using MCP protocol
|
5
|
+
Author-email: mseep <support@skydeck.ai>
|
6
|
+
Requires-Python: >=3.12
|
7
|
+
Description-Content-Type: text/plain
|
8
|
+
License-File: LICENSE
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
10
|
+
Requires-Dist: mcp>=1.1.2
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
12
|
+
Dynamic: license-file
|
13
|
+
|
14
|
+
Package managed by MseeP.ai
|
@@ -0,0 +1,8 @@
|
|
1
|
+
email_client/__init__.py,sha256=VAtgRH_6BN5yJ0H_PKgtEIOvEKLNyObj_Dflvzyxluw,153
|
2
|
+
email_client/server.py,sha256=W0jS5pSMSypu73dN-ficp3JlPY-2dvvYNhinII6dous,18951
|
3
|
+
mseep_email_client-0.1.1.dist-info/licenses/LICENSE,sha256=m_oniMdXFxy0QcK0EItJvbgyPCfltZH43P2YF2PPel8,1067
|
4
|
+
mseep_email_client-0.1.1.dist-info/METADATA,sha256=-rM8HD8XEwZ1PryLv746dp8be5hVUyRfSgclZ_LtKHw,389
|
5
|
+
mseep_email_client-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
mseep_email_client-0.1.1.dist-info/entry_points.txt,sha256=Wrdh3ByhKYjXV3U7gGwNSwgVq0PJGvwSYxKzJBALmVU,51
|
7
|
+
mseep_email_client-0.1.1.dist-info/top_level.txt,sha256=CVGSEJ0RIrALXhHil0q3wE1HoTZdqf1DM1PRXhgQTxc,13
|
8
|
+
mseep_email_client-0.1.1.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Zilong Xue
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
email_client
|