feat(projekt-matching): Cfg dataclass, IMAP mailbox with keyword/trash handling, SMTP send
This commit is contained in:
20
projekt-matching/projektmatch/config.py
Normal file
20
projekt-matching/projektmatch/config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Runtime configuration passed from Langflow component inputs."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cfg:
|
||||||
|
imap_user: str = "chancen@destengs.com"
|
||||||
|
imap_password: str = ""
|
||||||
|
espo_base: str = ""
|
||||||
|
espo_api_key: str = ""
|
||||||
|
notify_to: str = ""
|
||||||
|
alert_to: str = ""
|
||||||
|
threshold: int = 85
|
||||||
|
vllm_base: str = ""
|
||||||
|
vllm_model: str = ""
|
||||||
|
flow2_id: str = ""
|
||||||
|
langflow_api_key: str = ""
|
||||||
|
langflow_base: str = "http://127.0.0.1:7860"
|
||||||
|
data_dir: str = "/app/langflow"
|
||||||
|
crm_web_base: str = "https://crm.creature-go.com"
|
||||||
78
projekt-matching/projektmatch/mailer.py
Normal file
78
projekt-matching/projektmatch/mailer.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""IMAP mailbox handling and SMTP sending for chancen@destengs.com."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import email
|
||||||
|
import imaplib
|
||||||
|
import smtplib
|
||||||
|
import ssl
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from email.policy import default as default_policy
|
||||||
|
|
||||||
|
IMAP_HOST, IMAP_PORT = "mail.destengs.com", 993
|
||||||
|
SMTP_HOST, SMTP_PORT = "mail.destengs.com", 587
|
||||||
|
KEYWORD = "$ProjektChecked"
|
||||||
|
|
||||||
|
|
||||||
|
class MailBox:
|
||||||
|
def __init__(self, user, password, conn=None):
|
||||||
|
self.conn = conn or imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
|
||||||
|
if conn is None:
|
||||||
|
self.conn.login(user, password)
|
||||||
|
self.conn.select("INBOX")
|
||||||
|
self._trash = None
|
||||||
|
|
||||||
|
def unchecked_uids(self):
|
||||||
|
typ, data = self.conn.uid("SEARCH", None, f"UNKEYWORD {KEYWORD}")
|
||||||
|
if typ != "OK" or not data or not data[0]:
|
||||||
|
return []
|
||||||
|
return [u.decode() for u in data[0].split()]
|
||||||
|
|
||||||
|
def fetch(self, uid):
|
||||||
|
typ, data = self.conn.uid("FETCH", uid, "(BODY.PEEK[])")
|
||||||
|
return email.message_from_bytes(data[0][1], policy=default_policy)
|
||||||
|
|
||||||
|
def flag_checked(self, uid):
|
||||||
|
self.conn.uid("STORE", uid, "+FLAGS", f"({KEYWORD})")
|
||||||
|
|
||||||
|
def trash_folder(self):
|
||||||
|
if self._trash:
|
||||||
|
return self._trash
|
||||||
|
typ, data = self.conn.list()
|
||||||
|
candidates = []
|
||||||
|
for raw in data or []:
|
||||||
|
line = raw.decode() if isinstance(raw, bytes) else str(raw)
|
||||||
|
name = line.rsplit(" ", 1)[-1].strip('"')
|
||||||
|
if "\\Trash" in line.split(")")[0]:
|
||||||
|
self._trash = name
|
||||||
|
return name
|
||||||
|
candidates.append(name)
|
||||||
|
for cand in ("Trash", "INBOX.Trash"):
|
||||||
|
if cand in candidates:
|
||||||
|
self._trash = cand
|
||||||
|
return cand
|
||||||
|
self._trash = "Trash"
|
||||||
|
return self._trash
|
||||||
|
|
||||||
|
def move_to_trash(self, uid):
|
||||||
|
folder = self.trash_folder()
|
||||||
|
typ, _ = self.conn.uid("MOVE", uid, folder)
|
||||||
|
if typ != "OK":
|
||||||
|
self.conn.uid("COPY", uid, folder)
|
||||||
|
self.conn.uid("STORE", uid, "+FLAGS", "(\\Deleted)")
|
||||||
|
self.conn.expunge()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
self.conn.logout()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(user, password, to, subject, body):
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["From"], msg["To"], msg["Subject"] = user, to, subject
|
||||||
|
msg.set_content(body)
|
||||||
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server:
|
||||||
|
server.starttls(context=ssl.create_default_context())
|
||||||
|
server.login(user, password)
|
||||||
|
server.send_message(msg)
|
||||||
53
projekt-matching/tests/test_mailer.py
Normal file
53
projekt-matching/tests/test_mailer.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import email
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from projektmatch import mailer
|
||||||
|
|
||||||
|
|
||||||
|
def fake_conn():
|
||||||
|
conn = mock.Mock()
|
||||||
|
conn.uid.return_value = ("OK", [b"3 7"])
|
||||||
|
conn.list.return_value = ("OK", [
|
||||||
|
b'(\\HasNoChildren) "." "INBOX"',
|
||||||
|
b'(\\HasNoChildren \\Trash) "." "Trash"'])
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def test_unchecked_uids_search():
|
||||||
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
||||||
|
assert box.unchecked_uids() == ["3", "7"]
|
||||||
|
box.conn.uid.assert_called_with("SEARCH", None,
|
||||||
|
"UNKEYWORD $ProjektChecked")
|
||||||
|
|
||||||
|
|
||||||
|
def test_flag_checked():
|
||||||
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
||||||
|
box.flag_checked("3")
|
||||||
|
box.conn.uid.assert_called_with("STORE", "3", "+FLAGS",
|
||||||
|
"($ProjektChecked)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_trash_folder_by_special_use():
|
||||||
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
||||||
|
assert box.trash_folder() == "Trash"
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_uses_move_then_fallback():
|
||||||
|
conn = fake_conn()
|
||||||
|
box = mailer.MailBox("u", "p", conn=conn)
|
||||||
|
conn.uid.return_value = ("OK", [b""])
|
||||||
|
box.move_to_trash("3")
|
||||||
|
assert conn.uid.call_args_list[-1].args[:2] == ("MOVE", "3")
|
||||||
|
conn.uid.side_effect = [("NO", [b""]), ("OK", [b""]), ("OK", [b""])]
|
||||||
|
box.move_to_trash("4")
|
||||||
|
assert conn.expunge.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_mail_starttls():
|
||||||
|
with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp:
|
||||||
|
server = smtp.return_value.__enter__.return_value
|
||||||
|
mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body")
|
||||||
|
server.starttls.assert_called_once()
|
||||||
|
server.login.assert_called_once_with("u@x", "pw")
|
||||||
|
msg = server.send_message.call_args.args[0]
|
||||||
|
assert msg["Subject"] == "Subj" and msg["To"] == "to@x"
|
||||||
Reference in New Issue
Block a user