Files
crm/mail.py
T
MachielandClaude Opus 5.5 bffa053c05 v0.0.1: eerste versie van de Van Emous CRM
Flask + SQLite, in de Van Emous-huisstijl (tokens uit het design system, GSAP-effecten
volgens de motion-regels, uit bij prefers-reduced-motion).

- Leads en contactpersonen: status, toewijzen, verwachte waarde, omzetten naar klant,
  tijdlijn, bestanden, taken/opvolging
- Relaties (zoals accounts in EspoCRM): klantbundels (huishouden/familie) en partners met
  type (leverancier, aannemer, ...); leveranciers van producten worden automatisch partners
- API voor formulieren (POST /api/leads) met dubbele-leaddetectie, bestanden en
  configurator-ontwerpen (house-design JSON)
- Import van CSV/JSON (leads, relaties, producten) met kolomkoppeling; configurator-formaat
- Producten met SKU als vaste sleutel, mutatiehistorie, POST /api/products, CSV-export;
  offerteregels houden hun eigen prijs
- Offertes met btw per tarief en afdrukversie (PDF via de browser)
- Inbox via IMAP/SMTP, gekoppeld aan contacten
- Gebruikers (beheerder/medewerker) en klantportaal (offertes accepteren, bestanden, berichten)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:56:55 +02:00

117 lines
4.1 KiB
Python

"""IMAP ophalen en SMTP versturen, alleen met de standaardbibliotheek."""
import email
import html
import imaplib
import re
import smtplib
from datetime import datetime, timedelta
from email.message import EmailMessage
from email.policy import default as policy
from email.utils import formatdate, make_msgid, parseaddr, parsedate_to_datetime
MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
def _text(msg):
part = msg.get_body(preferencelist=('plain', 'html'))
if part is None:
return ''
try:
content = part.get_content()
except Exception:
content = (part.get_payload(decode=True) or b'').decode('utf-8', 'replace')
if part.get_content_type() == 'text/html':
content = re.sub(r'(?is)<(script|style|head).*?</\1>', '', content)
content = re.sub(r'(?i)<br\s*/?>|</p>|</div>|</tr>|</h\d>|</li>', '\n', content)
content = html.unescape(re.sub(r'<[^>]+>', '', content))
content = re.sub(r'[ \t]+\n', '\n', content)
return re.sub(r'\n{3,}', '\n\n', content).strip()
def _connect_imap(cfg):
host, port = cfg['imap_host'], int(cfg.get('imap_port') or 993)
if port == 993:
conn = imaplib.IMAP4_SSL(host, port, timeout=30)
else:
conn = imaplib.IMAP4(host, port, timeout=30)
conn.starttls()
conn.login(cfg['imap_user'], cfg.get('imap_pass', ''))
return conn
def fetch(cfg, known, limit=200):
"""Haal berichten van de laatste `imap_days` dagen op die nog niet in `known` staan."""
folder = cfg.get('imap_folder') or 'INBOX'
days = int(cfg.get('imap_days') or 30)
since = datetime.now() - timedelta(days=days)
since_str = f'{since.day:02d}-{MONTHS[since.month - 1]}-{since.year}'
conn = _connect_imap(cfg)
try:
typ, _ = conn.select(f'"{folder}"', readonly=True)
if typ != 'OK':
raise RuntimeError(f'Map "{folder}" niet gevonden')
_, data = conn.uid('search', None, 'SINCE', since_str)
uids = data[0].split()[-limit:]
out = []
for uid in uids:
key = f'{folder}:{uid.decode()}'
if key in known:
continue
_, msgdata = conn.uid('fetch', uid, '(BODY.PEEK[])')
if not msgdata or not isinstance(msgdata[0], tuple):
continue
msg = email.message_from_bytes(msgdata[0][1], policy=policy)
name, addr = parseaddr(str(msg.get('From', '')))
try:
date = parsedate_to_datetime(msg['Date']).astimezone().strftime('%Y-%m-%d %H:%M:%S')
except Exception:
date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
out.append({
'uid': key,
'message_id': str(msg.get('Message-ID', '')),
'from_name': name,
'from_addr': addr.lower(),
'to_addr': str(msg.get('To', '')),
'subject': str(msg.get('Subject', '')) or '(geen onderwerp)',
'body': _text(msg),
'date': date,
})
return out
finally:
try:
conn.logout()
except Exception:
pass
def send(cfg, to, subject, body, in_reply_to=None):
host = cfg.get('smtp_host')
if not host:
raise RuntimeError('Geen SMTP-server ingesteld')
user = cfg.get('smtp_user') or cfg.get('imap_user', '')
password = cfg.get('smtp_pass') or cfg.get('imap_pass', '')
port = int(cfg.get('smtp_port') or 587)
msg = EmailMessage()
msg['From'] = cfg.get('smtp_from') or user
msg['To'] = to
msg['Subject'] = subject
msg['Date'] = formatdate(localtime=True)
msg['Message-ID'] = make_msgid()
if in_reply_to:
msg['In-Reply-To'] = in_reply_to
msg['References'] = in_reply_to
msg.set_content(body)
if port == 465:
server = smtplib.SMTP_SSL(host, port, timeout=30)
else:
server = smtplib.SMTP(host, port, timeout=30)
server.starttls()
with server:
if user:
server.login(user, password)
server.send_message(msg)
return msg['Message-ID']