feat: init AI email summarization bot
- Multi-account IMAP email polling with UID tracking - DeepSeek API integration with JSON Mode structured output - Telegram notification with formatted MarkdownV2 message - YAML config with dataclass-based type validation - Graceful shutdown on SIGINT/SIGTERM - 60s default polling interval
This commit is contained in:
86
src/email_client.py
Normal file
86
src/email_client.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import imaplib
|
||||
import email
|
||||
from email.header import decode_header
|
||||
from email.utils import parsedate_to_datetime
|
||||
from typing import Optional
|
||||
from src.config import EmailAccount
|
||||
|
||||
|
||||
class Email:
|
||||
def __init__(self, uid: bytes, subject: str, sender: str, body: str, date: str):
|
||||
self.uid = uid
|
||||
self.subject = subject
|
||||
self.sender = sender
|
||||
self.body = body
|
||||
self.date = date
|
||||
|
||||
|
||||
def _decode_str(s: str) -> str:
|
||||
parts = decode_header(s)
|
||||
result = []
|
||||
for part, charset in parts:
|
||||
if isinstance(part, bytes):
|
||||
try:
|
||||
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||
except LookupError:
|
||||
result.append(part.decode("utf-8", errors="replace"))
|
||||
else:
|
||||
result.append(part)
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def _get_text_from_msg(msg) -> str:
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
if content_type == "text/plain":
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode("utf-8", errors="replace")
|
||||
elif content_type == "text/html":
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode("utf-8", errors="replace")
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode("utf-8", errors="replace")
|
||||
return ""
|
||||
|
||||
|
||||
def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
|
||||
conn = imaplib.IMAP4_SSL(account.imap_server, account.imap_port)
|
||||
conn.login(account.username, account.password)
|
||||
conn.select("INBOX")
|
||||
|
||||
_, data = conn.uid("SEARCH", None, "UNSEEN")
|
||||
uids = data[0].split() if data[0] else []
|
||||
emails = []
|
||||
|
||||
for uid in uids:
|
||||
_, msg_data = conn.uid("FETCH", uid, "RFC822")
|
||||
if msg_data[0] is None:
|
||||
continue
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
subject = _decode_str(msg.get("Subject", ""))
|
||||
sender = _decode_str(msg.get("From", ""))
|
||||
date_str = msg.get("Date", "")
|
||||
body = _get_text_from_msg(msg)
|
||||
|
||||
emails.append(Email(uid=uid, subject=subject, sender=sender, body=body, date=date_str))
|
||||
|
||||
conn.logout()
|
||||
return emails
|
||||
|
||||
|
||||
def mark_as_seen(account: EmailAccount, uids: list[bytes]):
|
||||
if not uids:
|
||||
return
|
||||
conn = imaplib.IMAP4_SSL(account.imap_server, account.imap_port)
|
||||
conn.login(account.username, account.password)
|
||||
conn.select("INBOX")
|
||||
for uid in uids:
|
||||
conn.uid("STORE", uid, "+FLAGS", "\\Seen")
|
||||
conn.logout()
|
||||
Reference in New Issue
Block a user