v0.0.5: gedeelde mailboxen (info@, facturen@, nieuwsbrief@, persoonlijk) met eigen IMAP/SMTP, afzender, rol en toegang per gebruiker; inbox per mailbox en versturen vanuit een gekozen mailbox; routines (jobs.py: types met @jobs.register, interval, instellingen, logboek, slot tegen dubbel draaien; app.py scheduler/tick) voor e-mail ophalen, nieuwsbrieven versturen en opschonen; meldingen (bel, bureaubladmelding); nieuwsbrieven met segmenten, testmail, inplannen en versturen per batch over één verbinding, afmeldlink met List-Unsubscribe en bounceverwerking; oude mailinstellingen worden de eerste mailbox
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8f6e1ab443
commit
cad838f1f4
+26
-51
@@ -3,13 +3,35 @@
|
||||
{% macro field(key, label, type='text', placeholder='', full=false) %}
|
||||
<div class="{{ 'col-12' if full else 'col-md-6 col-xl-4' }}"><label class="form-label" for="{{ key }}">{{ label }}</label><input class="form-control" id="{{ key }}" type="{{ type }}" name="{{ key }}" value="{{ s.get(key, '') }}" placeholder="{{ placeholder }}"></div>
|
||||
{% endmacro %}
|
||||
{% macro security(key) %}
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="{{ key }}">Beveiliging</label>
|
||||
<select class="form-select" id="{{ key }}" name="{{ key }}">{% for v, label in SECURITY %}<option value="{{ v }}" {{ 'selected' if s.get(key, '') == v }}>{{ label }}</option>{% endfor %}</select></div>
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
<div class="head"><div><div class="kicker">Beheer</div><h1>Instellingen</h1></div></div>
|
||||
|
||||
<div class="card" id="mail"><div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||
<div><h2>Mailboxen</h2>
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Elke mailbox (info@, facturen@, nieuwsbrief@ of een persoonlijke) heeft een eigen account. Een mailbox
|
||||
zonder gekozen gebruikers is gedeeld met iedereen. Ophalen gaat via de routine "E-mail ophalen" (<a href="{{ url_for('routines') }}">Routines</a>).</p></div>
|
||||
<a class="btn btn-primary" href="{{ url_for('mailbox_edit') }}">Mailbox toevoegen</a>
|
||||
</div>
|
||||
{% if mailboxes %}
|
||||
<div class="table-responsive"><table class="table table-hover table-stack">
|
||||
<thead><tr><th>Naam</th><th>Adres</th><th>Rol</th><th>Toegang</th><th>Laatst opgehaald</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in mailboxes %}
|
||||
<tr class="link" onclick="location='{{ url_for('mailbox_edit', mbid=m.id) }}'">
|
||||
<td class="stack-title"><a href="{{ url_for('mailbox_edit', mbid=m.id) }}">{{ m.name }}</a></td>
|
||||
<td data-label="Adres">{{ m.imap_user or m.smtp_user }}</td>
|
||||
<td data-label="Rol">{{ PURPOSE.get(m.purpose, m.purpose) }}</td>
|
||||
<td data-label="Toegang">{{ m.access or 'Iedereen' }}</td>
|
||||
<td data-label="Laatst opgehaald" class="muted">{{ m.last_sync|dt if m.last_sync else '—' }}</td>
|
||||
<td data-label="Status">{% if not m.active %}<span class="chip verloren">Inactief</span>{% elif m.last_error %}<span class="chip nieuw" title="{{ m.last_error }}">Fout</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}<p class="empty">Nog geen mailbox.</p>{% endif %}
|
||||
</div></div>
|
||||
|
||||
<form method="post">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Bedrijfsgegevens</h2>
|
||||
@@ -37,43 +59,6 @@
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="card" id="mail"><div class="card-body">
|
||||
<h2>E-mail koppelen</h2>
|
||||
<p class="hint" style="margin:-.4rem 0 1rem">Eén gedeelde mailbox (bijv. info@) voor de inbox en het versturen vanuit de CRM. Bij OVHcloud
|
||||
(Zimbra of MX Plan) is de gebruikersnaam het volledige e-mailadres; de afzender moet dat adres (of een alias ervan) zijn.
|
||||
Gmail werkt met een app-wachtwoord. Wachtwoorden staan in de database: houd de map <code>data/</code> privé.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="mail-provider">Aanbieder</label>
|
||||
<select class="form-select" id="mail-provider" aria-describedby="mail-provider-hint">
|
||||
<option value="">Kies om de servers in te vullen…</option>
|
||||
{% for key, p in PROVIDERS.items() %}<option value="{{ key }}" data-preset="{{ p|tojson|forceescape }}">{{ p.label }}</option>{% endfor %}
|
||||
</select>
|
||||
<p class="hint" id="mail-provider-hint">Vult server, poort en beveiliging in; daarna kun je alles nog aanpassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Ontvangen (IMAP)</h3>
|
||||
<div class="row g-3">
|
||||
{{ field('imap_host', 'Server', placeholder='imap.mail.ovh.net') }}
|
||||
{{ field('imap_port', 'Poort', 'number', '993') }}
|
||||
{{ security('imap_security') }}
|
||||
{{ field('imap_user', 'Gebruikersnaam', placeholder='info@vanemouswoningen.nl') }}
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="imap_pass">Wachtwoord</label><input class="form-control" id="imap_pass" type="password" name="imap_pass" placeholder="{{ '•••••••• (ingesteld)' if s.imap_pass else '' }}" autocomplete="new-password"></div>
|
||||
{{ field('imap_folder', 'Map', placeholder='INBOX') }}
|
||||
{{ field('imap_days', 'Aantal dagen terug ophalen', 'number', '30') }}
|
||||
</div>
|
||||
<h3>Versturen (SMTP)</h3>
|
||||
<div class="row g-3">
|
||||
{{ field('smtp_host', 'Server', placeholder='smtp.mail.ovh.net') }}
|
||||
{{ field('smtp_port', 'Poort', 'number', '465') }}
|
||||
{{ security('smtp_security') }}
|
||||
{{ field('smtp_user', 'Gebruikersnaam', placeholder='leeg = zelfde als IMAP') }}
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_pass">Wachtwoord</label><input class="form-control" id="smtp_pass" type="password" name="smtp_pass" placeholder="{{ '•••••••• (ingesteld)' if s.smtp_pass else 'leeg = zelfde als IMAP' }}" autocomplete="new-password"></div>
|
||||
<div class="col-md-6 col-xl-4"><label class="form-label" for="smtp_sent_folder">Kopie in map</label><input class="form-control" id="smtp_sent_folder" name="smtp_sent_folder" value="{{ s.smtp_sent_folder or '' }}" placeholder="leeg = geen kopie" aria-describedby="sent-hint"><p class="hint" id="sent-hint">Bij Zimbra/MX Plan <code>Sent</code>, zodat verstuurde mail ook in de webmail staat. Gmail doet dit zelf.</p></div>
|
||||
{{ field('smtp_from', 'Afzender', placeholder='Van Emous <info@vanemouswoningen.nl>', full=true) }}
|
||||
</div>
|
||||
<div class="actions"><button class="btn btn-outline-primary" name="action" value="mailtest">Opslaan en verbinding testen</button></div>
|
||||
</div></div>
|
||||
|
||||
<div class="card"><div class="card-body">
|
||||
<h2>Formulieren en API</h2>
|
||||
<div class="row g-3">
|
||||
@@ -145,14 +130,4 @@ const { ok, id } = await res.json();{% endfilter %}</pre>
|
||||
<button class="btn btn-outline-secondary btn-sm">Nieuwe API-sleutel maken</button>
|
||||
</form>
|
||||
</div></div>
|
||||
<script>
|
||||
document.getElementById('mail-provider').addEventListener('change', (e) => {
|
||||
const opt = e.target.selectedOptions[0];
|
||||
if (!opt || !opt.dataset.preset) return;
|
||||
Object.entries(JSON.parse(opt.dataset.preset)).forEach(([key, value]) => {
|
||||
const el = document.getElementById(key);
|
||||
if (el) el.value = value;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user