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:
1 parent
f54b55674c
commit
b6907e5343
2 files changed
+183
-4
No files matched your search
@@ -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
@@ -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>
|
||||
Reference in new issue
Block a user