secondbrain/connectors/imap_email.py
Atlas eb5b67dee2 Add connectors + setup wizard (files / IMAP email / Nextcloud-WebDAV) + encrypted vault
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.
2026-07-01 21:50:20 +02:00

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")