Product layer on top of the pure engine: a wizard to connect the apps that feed the brain, an encrypted credential vault (Fernet/PBKDF2; app logins never enter brain.db), and 3 working connectors. Connector data flows through the same credential-scrub path as the engine. Atlas Corporation proprietary.
57 lines
2.5 KiB
Python
57 lines
2.5 KiB
Python
# SecondBrain — (c) 2026 Atlas Corporation. All Rights Reserved. Proprietary; see LICENSE.
|
|
"""Connector: Email over IMAP. Use an app password (not your main password).
|
|
Message bodies pass through the engine scrub, so any secrets inside emails are redacted."""
|
|
import imaplib, email
|
|
from email.header import decode_header, make_header
|
|
from .base import Connector, Field, register, feed
|
|
|
|
@register
|
|
class ImapEmailConnector(Connector):
|
|
id = "email"
|
|
name = "Email (IMAP)"
|
|
blurb = "Pull recent messages from any IMAP mailbox (Gmail, Outlook, self-hosted…)."
|
|
fields = [
|
|
Field("host", "IMAP host", default="imap.gmail.com"),
|
|
Field("port", "IMAP port", default="993"),
|
|
Field("user", "Username / email address"),
|
|
Field("password", "App password", secret=True),
|
|
Field("folder", "Folder", default="INBOX"),
|
|
Field("limit", "Max recent messages", default="200"),
|
|
]
|
|
|
|
def _open(self, cfg):
|
|
M = imaplib.IMAP4_SSL(cfg["host"], int(cfg.get("port") or 993))
|
|
M.login(cfg["user"], cfg["password"])
|
|
return M
|
|
|
|
def test(self, cfg):
|
|
M = self._open(cfg); M.select(cfg.get("folder", "INBOX"), readonly=True); M.logout()
|
|
return "login ok"
|
|
|
|
def sync(self, cfg):
|
|
M = self._open(cfg)
|
|
M.select(cfg.get("folder", "INBOX"), readonly=True)
|
|
_, data = M.search(None, "ALL")
|
|
ids = data[0].split()[-int(cfg.get("limit") or 200):]
|
|
items = []
|
|
for i in ids:
|
|
_, d = M.fetch(i, "(RFC822)")
|
|
if not d or not isinstance(d[0], tuple):
|
|
continue
|
|
msg = email.message_from_bytes(d[0][1])
|
|
def H(h):
|
|
try: return str(make_header(decode_header(msg.get(h, "") or "")))
|
|
except Exception: return msg.get(h, "") or ""
|
|
body = ""
|
|
if msg.is_multipart():
|
|
for p in msg.walk():
|
|
if p.get_content_type() == "text/plain":
|
|
try: body = (p.get_payload(decode=True) or b"").decode("utf-8", "ignore"); break
|
|
except Exception: pass
|
|
else:
|
|
try: body = (msg.get_payload(decode=True) or b"").decode("utf-8", "ignore")
|
|
except Exception: body = str(msg.get_payload())
|
|
items.append({"text": f"[email from {H('From')}] {H('Subject')}\n{body[:1500]}",
|
|
"project": "email", "ref": H('From')[:80], "ts": H('Date'), "role": "email"})
|
|
M.logout()
|
|
return feed(items, "events")
|