SharezoneApp / sharezone-app

Sharezone is a collaborative school organization app for iOS, Android, macOS and web with +500,000 downloads. Built with Flutter & Firebase.
https://sharezone.net
European Union Public License 1.2
254 stars 46 forks source link

Send email to users because privacy policy chagned #1606

Open nilsreichardt opened 1 month ago

nilsreichardt commented 1 month ago

Draft of the email:

Hi đź‘‹

ich bin Nils, einer der GrĂĽnder von Sharezone. Ich schreibe dir, weil du ein Konto bei Sharezone hast und wir verpflichtet sind, dich ĂĽber rechtliche Ă„nderungen zu informieren.

Du bekommst sicher jeden Tag ein paar E-Mails; deshalb werde ich mich kurz fassen.

Was ändert sich?

Datenschutzerklärung: Wir haben unsere Datenschutzerklärung komplett überarbeitet. Sie ist jetzt transparenter und ausführlicher.

Allgemeine Nutzungsbedingungen: Wir fĂĽhren neue Allgemeine Nutzungsbedingungen ein, die die Nutzung von Sharezone regeln.

Neue Rechtsform: Sharezone firmiert ab sofort nicht mehr unter "Sander, Jonas; Reichardt, Nils; Weuthen, Felix „Sharezone“ GbR", sondern unter “Sharezone UG (haftungsbeschränkt)”.

Alle Änderungen werden sofort wirksam. Solltest du mit den Änderungen nicht einverstanden sein oder Sharezone nicht mehr verwenden, kannst du dein Konto löschen.

Solltest du weitere Fragen oder Ă„nderungswĂĽnsche haben, kannst du einfach auf diese E-Mail antworten.

Viele GrĂĽĂźe Nils

nilsreichardt commented 1 month ago
Source code of the script ```ts import * as fs from "fs"; import { EmailSender } from "../utils/email_sender"; interface FirebaseAuthUser { localId: string; email: string | undefined; createdAt: number; } const logFilePath = "send_legal_update.log"; const html = `

Hi đź‘‹

Ich bin Nils, einer der GrĂĽnder von Sharezone. Ich schreibe dir, weil du ein Konto bei Sharezone hast und wir verpflichtet sind, dich ĂĽber rechtliche Ă„nderungen zu informieren.

Du bekommst sicher jeden Tag ein paar E-Mails, deshalb werde ich versuchen, mich kurz zu fassen.

Was ändert sich?

Datenschutzerklärung

Wir haben unsere Datenschutzerklärung komplett überarbeitet. Sie ist jetzt transparenter und ausführlicher. Du findest die neue Datenschutzerklärung hier.

Allgemeine Nutzungsbedingungen

Wir fĂĽhren neue Allgemeine Nutzungsbedingungen ein, die die Nutzung von Sharezone regeln. Du findest die Allgemeine Nutzungsbedingungen hier.

Neue Rechtsform

Sharezone firmiert ab sofort nicht mehr unter "Sander, Jonas; Reichardt, Nils; Weuthen, Felix „Sharezone“ GbR", sondern unter “Sharezone UG (haftungsbeschränkt)”.

Alle Änderungen werden sofort wirksam. Solltest du mit den Änderungen nicht einverstanden sein oder Sharezone nicht mehr verwenden, kannst du dein Konto löschen.

Solltest du weitere Fragen oder Ă„nderungswĂĽnsche haben, kannst du einfach auf diese E-Mail antworten.

Viele GrĂĽĂźe
Nils

Impressum | Datenschutzerklärung | Allgemeine Nutzungsbedingungen
`; const text = `Hi 👋 ich bin Nils, einer der Gründer von Sharezone. Ich schreibe dir, weil du ein Konto bei Sharezone hast und wir verpflichtet sind, dich über rechtliche Änderungen zu informieren. Du bekommst sicher jeden Tag ein paar E-Mails, deshalb werde ich versuchen, mich kurz zu fassen. ## Was ändert sich? Datenschutzerklärung: Wir haben unsere Datenschutzerklärung komplett überarbeitet. Sie ist jetzt transparenter und ausführlicher. Allgemeine Nutzungsbedingungen: Wir führen neue Allgemeine Nutzungsbedingungen ein, die die Nutzung von Sharezone regeln. Neue Rechtsform: Sharezone firmiert ab sofort nicht mehr unter "Sander, Jonas; Reichardt, Nils; Weuthen, Felix „Sharezone“ GbR", sondern unter “Sharezone UG (haftungsbeschränkt)”. Alle Änderungen werden sofort wirksam. Solltest du mit den Änderungen nicht einverstanden sein oder Sharezone nicht mehr verwenden, kannst du dein Konto löschen. Solltest du weitere Fragen oder Änderungswünsche haben, kannst du einfach auf diese E-Mail antworten. Viele Grüße Nils`; const emailSender = new EmailSender({ secretKey: `${process.env.SCW_SECRET_KEY}`, projectId: `${process.env.SCW_PROJECT_ID}`, }); async function main() { ensureLoggingFileExists(); let counter = 0; const users = getAuthUsers(); for (const user of users) { console.log( `Sending email to user ${user.email} (${++counter}/${users.length})`, ); if (user.email == null) { continue; } const hasPrivateEmail = user.email.includes("@privaterelay.appleid.com"); if (hasPrivateEmail) { // We can't send emails to private relay email addresses. continue; } if (new Date(user.createdAt) >= new Date("2024-04-25")) { // User already accepted the new terms. continue; } try { await sendEmailToUser(user, emailSender); logSuccess(user); } catch (error) { logError(user, error); } } } async function sendEmailToUser( user: FirebaseAuthUser, emailSender: EmailSender, ) { await emailSender.sendEmail({ receiver: user.email!, senderName: "Nils von Sharezone", html: html, text: text, subject: "Neue Datenschutzerklärung, Nutzungsbedingungen und Rechtsform bei Sharezone", }); } function ensureLoggingFileExists() { if (!fs.existsSync(logFilePath)) { fs.writeFileSync(logFilePath, ""); } } function getAuthUsers(): FirebaseAuthUser[] { // return [ // { // createdAt: new Date("2024-01-01").getTime(), // email: "...@gmail.com", // localId: "1", // }, // ]; const json = JSON.parse(fs.readFileSync("users.json", "utf8")); return json.users; } function logSuccess(user: FirebaseAuthUser) { fs.appendFileSync(logFilePath, `${user.localId};sent\n`); } function logError(user: FirebaseAuthUser, error: any) { fs.appendFileSync(logFilePath, `${user.localId};failed="${error.message}\n"`); console.error(`Failed to send email to user ${user.email}: ${error.message}`); } // eslint-disable-next-line @typescript-eslint/no-floating-promises main(); ```
Script to group emails by domain ```py import json from collections import defaultdict # Load the JSON data from the file with open('users.json', 'r') as file: data = json.load(file) # Initialize a dictionary to store the counts of each domain domain_counts = defaultdict(int) # Iterate through the users and count the domains for user in data['users']: email = user.get('email') if email: domain = email.split('@')[-1] domain_counts[domain] += 1 # Sort the results by count in descending order sorted_domain_counts = sorted(domain_counts.items(), key=lambda x: x[1], reverse=True) # Print the results for domain, count in sorted_domain_counts: print(f"@{domain}: {count:,}") ```
nilsreichardt commented 1 month ago

Tip for the next time: Check if the user still exists. Otherwise, you email a user who deleted their account between the time point where you did the export and where you actually sent the email => user thinks you still have the user in the database.