Add full GDPR email lifecycle: confirmation, report, and deletion notice

- Attendee immediately receives a data-retention notice on email capture,
  explaining purpose, deletion deadline (10 July 2026), and how to reply
  KEEP if they want their contact retained
- Admin page: Send Full Report to All button (purple) triggers the PDF
  report to all collected addresses after the conference
- Admin page: Delete and Notify All button (red) emails each address a
  deletion confirmation then wipes emails.json, completing the GDPR cycle
- send_deletion_confirmation() notifies under GDPR Article 17 before data
  is removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 4.6 committed 2026-06-07 16:26:44 +02:00
1 parent f54b55674c
commit b6907e5343
2 files changed
+183 -4

No files matched your search

+150 -3
View File
@@ -894,10 +894,126 @@ def generate_analysis_pdf():
return bytes(pdf.output())
def send_confirmation_email(to_email):
"""Sends an immediate data-retention notice to the attendee. Runs in a background thread."""
if not MAIL_HOST or not MAIL_USER or not MAIL_PASS:
print(f'[email] SMTP not configured, skipping confirmation to {to_email}')
return
try:
msg = MIMEMultipart()
msg['From'] = f'HiActs Poll - TXproducts <{MAIL_USER}>'
msg['To'] = to_email
msg['Subject'] = 'Hi-Acts Poll: your data and your full report'
body_html = f"""
<div style="font-family:sans-serif;max-width:560px;margin:0 auto;color:#2d3748">
<div style="background:linear-gradient(135deg,#0f2744,#1a4a8a);padding:28px 32px;border-radius:10px 10px 0 0">
<p style="color:#f5c842;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;margin:0 0 8px">Hi-Acts 3rd Network Meeting</p>
<h1 style="color:white;font-size:22px;margin:0;line-height:1.3">Thank you for participating</h1>
</div>
<div style="background:#f8fafc;padding:28px 32px;border:1px solid #e2e8f0;border-radius:0 0 10px 10px">
<p>We have received your email address and will send you the full Innovation Challenge Arena analysis report after the event.</p>
<div style="background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:16px 20px;margin:20px 0">
<p style="font-weight:700;color:#92400e;margin:0 0 8px">Your data and your rights</p>
<p style="color:#78350f;font-size:13px;margin:0;line-height:1.7">
Your email address is held by TXproducts GmbH solely for the purpose of sending you this report.
It will not be shared with or sold to any third party.
It will be permanently deleted by <strong>10 July 2026</strong> unless you ask us to keep it.
</p>
</div>
<p style="font-size:14px;color:#4a5568;">
<strong>If you would like TXproducts to retain your contact details</strong> for future updates
and communications, simply reply to this email with the word <strong>KEEP</strong>.
If you do nothing, your email will be deleted by 10 July 2026 as stated.
</p>
<p style="font-size:14px;color:#4a5568;">
To request immediate deletion or to exercise any of your rights under GDPR,
contact us at <a href="mailto:mblack@txproducts.de" style="color:#2563eb;">mblack@txproducts.de</a>.
</p>
<p style="color:#8a9baf;font-size:12px;margin-top:24px;border-top:1px solid #e2e8f0;padding-top:16px">
TXproducts GmbH | mblack@txproducts.de<br>
Data processed under GDPR Article 6(1)(a) on the basis of your consent.
</p>
</div>
</div>
"""
msg.attach(MIMEText(body_html, 'html'))
with smtplib.SMTP(MAIL_HOST, MAIL_PORT, timeout=15) as server:
server.ehlo()
server.starttls()
server.login(MAIL_USER, MAIL_PASS)
server.send_message(msg)
print(f'[email] Confirmation sent to {to_email}')
except Exception as e:
print(f'[email] Failed to send confirmation to {to_email}: {e}')
def send_deletion_confirmation(to_email):
"""Notifies the attendee that their email has been deleted. Call before wiping emails.json."""
if not MAIL_HOST or not MAIL_USER or not MAIL_PASS:
print(f'[email] SMTP not configured, skipping deletion notice to {to_email}')
return
try:
msg = MIMEMultipart()
msg['From'] = f'HiActs Poll - TXproducts <{MAIL_USER}>'
msg['To'] = to_email
msg['Subject'] = 'Hi-Acts Poll: your email address has been deleted'
body_html = f"""
<div style="font-family:sans-serif;max-width:560px;margin:0 auto;color:#2d3748">
<div style="background:linear-gradient(135deg,#0f2744,#1a4a8a);padding:28px 32px;border-radius:10px 10px 0 0">
<p style="color:#f5c842;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;margin:0 0 8px">Hi-Acts 3rd Network Meeting</p>
<h1 style="color:white;font-size:22px;margin:0;line-height:1.3">Your data has been deleted</h1>
</div>
<div style="background:#f8fafc;padding:28px 32px;border:1px solid #e2e8f0;border-radius:0 0 10px 10px">
<p>This is to confirm that your email address has been permanently deleted from the
Hi-Acts Innovation Challenge Arena poll system operated by TXproducts GmbH.</p>
<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:8px;padding:16px 20px;margin:20px 0">
<p style="font-weight:700;color:#166534;margin:0 0 6px">Deletion confirmed</p>
<p style="color:#15803d;font-size:13px;margin:0;line-height:1.7">
Your email address is no longer stored in any TXproducts system related to this event.
No further emails will be sent to this address from the Hi-Acts poll.
</p>
</div>
<p style="font-size:14px;color:#4a5568;">
If you have any questions or concerns, contact us at
<a href="mailto:mblack@txproducts.de" style="color:#2563eb;">mblack@txproducts.de</a>.
</p>
<p style="color:#8a9baf;font-size:12px;margin-top:24px;border-top:1px solid #e2e8f0;padding-top:16px">
TXproducts GmbH | mblack@txproducts.de<br>
This deletion was carried out in accordance with GDPR Article 17 (Right to Erasure).
</p>
</div>
</div>
"""
msg.attach(MIMEText(body_html, 'html'))
with smtplib.SMTP(MAIL_HOST, MAIL_PORT, timeout=15) as server:
server.ehlo()
server.starttls()
server.login(MAIL_USER, MAIL_PASS)
server.send_message(msg)
print(f'[email] Deletion confirmation sent to {to_email}')
except Exception as e:
print(f'[email] Failed to send deletion notice to {to_email}: {e}')
def send_analysis_report(to_email):
"""Generates the PDF and emails it to the given address. Runs in a background thread."""
if not MAIL_HOST or not MAIL_USER or not MAIL_PASS:
print(f'[email] SMTP not configured — cannot send to {to_email}')
print(f'[email] SMTP not configured, skipping report to {to_email}')
return
try:
@@ -1053,18 +1169,49 @@ def question_view(q_id):
return render_template('question_view.html', question=q, questions=QUESTIONS)
# Receives an email address, stores it, then sends the analysis PDF in the background
# Receives an email address, stores it, and sends an immediate data-retention notice
@app.route('/api/email', methods=['POST'])
def capture_email():
data = request.json
email = (data.get('email') or '').strip()
if email and '@' in email and '.' in email:
store_email(email)
t = threading.Thread(target=send_analysis_report, args=(email,), daemon=True)
t = threading.Thread(target=send_confirmation_email, args=(email,), daemon=True)
t.start()
return jsonify({'ok': True})
# Sends the full PDF analysis report to every collected email address (run this after the conference)
@app.route('/admin/send_all_reports', methods=['POST'])
def send_all_reports():
emails = load_emails()
if not emails:
return jsonify({'ok': False, 'message': 'No emails collected yet'})
for email in emails:
t = threading.Thread(target=send_analysis_report, args=(email,), daemon=True)
t.start()
return jsonify({'ok': True, 'message': f'Sending report to {len(emails)} address(es)'})
# Sends a deletion confirmation to every stored email, then wipes emails.json
@app.route('/admin/delete_and_notify', methods=['POST'])
def delete_and_notify():
emails = load_emails()
if not emails:
return jsonify({'ok': False, 'message': 'No emails to delete'})
def notify_then_delete():
for email in emails:
send_deletion_confirmation(email)
with open(EMAILS_FILE, 'w') as f:
json.dump([], f)
print(f'[email] emails.json cleared after notifying {len(emails)} address(es)')
t = threading.Thread(target=notify_then_delete, daemon=True)
t.start()
return jsonify({'ok': True, 'message': f'Notifying {len(emails)} address(es) then deleting data'})
# Downloads all poll responses as a CSV file — one row per respondent
@app.route('/admin/export')
def export_csv():
+33 -1
View File
@@ -247,7 +247,14 @@
<div class="section-title">Emails collected (full report requests)</div>
<div style="background:white;border-radius:12px;border:1px solid #e8eef4;padding:16px 20px;">
{% if emails %}
<div style="font-size:13px;color:#4a5568;margin-bottom:10px;">{{ emails | length }} address{{ 'es' if emails | length != 1 else '' }} collected</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
<div style="font-size:13px;color:#4a5568;">{{ emails | length }} address{{ 'es' if emails | length != 1 else '' }} collected</div>
<div style="display:flex;gap:8px;">
<button onclick="sendAllReports()" style="background:#7c3aed;color:white;border:none;border-radius:7px;padding:6px 14px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;">Send Full Report to All</button>
<button onclick="deleteAndNotify()" style="background:#e53e3e;color:white;border:none;border-radius:7px;padding:6px 14px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;">Delete and Notify All</button>
</div>
</div>
<div id="sendReportMsg" style="display:none;font-size:12px;color:#059669;margin-bottom:8px;font-weight:600;"></div>
{% for email in emails %}
<div style="font-size:13px;color:#1a365d;padding:6px 0;border-bottom:1px solid #f0f4f8;font-family:'SF Mono','Fira Code',monospace;">{{ email }}</div>
{% endfor %}
@@ -290,6 +297,31 @@
if (!confirm('Reset ALL poll data? This cannot be undone.')) return;
fetch('/admin/reset', { method: 'POST' }).then(() => location.reload());
}
/* Sends the full PDF analysis report to every collected email address */
function sendAllReports() {
if (!confirm('Send the full analysis PDF to all collected email addresses? This cannot be undone.')) return;
fetch('/admin/send_all_reports', { method: 'POST' })
.then(r => r.json())
.then(data => {
const msg = document.getElementById('sendReportMsg');
msg.textContent = data.message;
msg.style.display = 'block';
});
}
/* Emails a deletion confirmation to every address, then wipes emails.json */
function deleteAndNotify() {
if (!confirm('This will email every address a deletion confirmation and then permanently delete all stored emails. Are you sure?')) return;
fetch('/admin/delete_and_notify', { method: 'POST' })
.then(r => r.json())
.then(data => {
const msg = document.getElementById('sendReportMsg');
msg.textContent = data.message;
msg.style.display = 'block';
setTimeout(() => location.reload(), 4000);
});
}
</script>
</body>
</html>