diff --git a/README.md b/README.md index b6fe6bf9d092d137ab74f6a44b5872ef40d762b7..1449852c16da55f7db2f44f9e5a557285e0d5d21 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,16 @@ 4. For html files, make sure that traslation strings are not directly assigned to an attribute. Use a tmp variable to call `t()` and then assign this variable to the attribute. 4. Commit the updated `translationfiles/templates/email-recovery.pot` 5. Translate in [Weblate](https://i18n.e.foundation/projects/ecloud/email-recovery/) + +## Recovery Email verification +1. Recovery Email Verification Diagram +![Recovery Verification Steps](recovery-verification-steps.png) + +2. Add VerifyMail API key in config using occ +`occ config:system:set verify_mail_api_key --value='[ADD API KEY]'` + +3. Run command to set Create Popular domain + +`occ email-recovery:create-popular-domains` + + diff --git a/appinfo/info.xml b/appinfo/info.xml index 90788aa2cef4d95db3e34db9eb469584f64298c3..c2004f4db8e2ced7d914b1cb9f5058f3fb6dea04 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,5 +22,6 @@ OCA\EmailRecovery\Command\UpdateBlacklistedDomains + OCA\EmailRecovery\Command\CreatePopularDomain diff --git a/l10n/de.js b/l10n/de.js index f3d97fcffdd6118a58c01a5165eb28cf1722d789..7463cfca3307e3ffd9a70fb208427684c518b53b 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/de.json b/l10n/de.json index b96b52da53b388d26f84864c9ddbb079e7c720db..5fd1977e03f4a92fd73844f9708dcb4f42240662 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/de_DE.js b/l10n/de_DE.js index 8d14855f20c2baf9dd47c58294accd1d0fe1524b..b228711dca76464ab459d605eebd85c7d1ccd8fc 100644 --- a/l10n/de_DE.js +++ b/l10n/de_DE.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json index f91465169820eeb96f1cda45ff9fe10c6cde81b0..45ba9a578e047fd9cc8631a874ac959838be8202 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/en.js b/l10n/en.js index f8174ea5f42220ffe395a2717ca348940effa26a..3ef0c606891805a9f73027178eaab56e99fc26c8 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -31,6 +31,9 @@ OC.L10N.register( "Please set a recovery email address.": "Please set a recovery email address.", "The domain of this email address is blacklisted. Please provide another recovery address.": "The domain of this email address is blacklisted. Please provide another recovery address.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.", + "The email could not be verified. Please try again later.": "The email could not be verified. Please try again later.", + "The email address is disposable. Please provide another recovery address." : "The email address is disposable. Please provide another recovery address.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/en.json b/l10n/en.json index 239b92c7132848c25b91e75063ffa25dabd3791b..6ca785f042e9b49f64ae3e68cd88aaee94197136 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -32,7 +32,10 @@ "Please set a recovery email address.": "Please set a recovery email address.", "The domain of this email address is blacklisted. Please provide another recovery address.": "The domain of this email address is blacklisted. Please provide another recovery address.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.", + "The email could not be verified. Please try again later.": "The email could not be verified. Please try again later.", + "The email address is disposable. Please provide another recovery address." : "The email address is disposable. Please provide another recovery address.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "pluralForm": "nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/es.js b/l10n/es.js index 45eccc5f02e90404129d4b42d873417e417728ee..66500df2102383c9c3f43928daa7f9a41eb5990e 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Por favor configura un correo electrónico para recuperación.", "The domain of this email address is blacklisted. Please provide another recovery address.": "El dominio de esta dirección de correo electrónico está en lista negra. Por favor, proporciona otra dirección de recuperación.", "Too many verification emails.": "Demasiados correos electrónicos de verificación.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "EEsta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "EEsta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad.", + "The email could not be verified. Please try again later.": "No se ha podido verificar el correo electrónico. Inténtelo de nuevo más tarde.", + "The email address is disposable. Please provide another recovery address." : "La dirección de correo electrónico es desechable. Por favor, proporcione otra dirección de recuperación.", + "The email address is not deliverable. Please provide another recovery address.": "La dirección de correo electrónico no se puede entregar. Por favor, proporcione otra dirección de recuperación." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/es.json b/l10n/es.json index b078c42a8abd36e907f1c0c3f747a36ebba3a0e6..74af0bcd001d0b9049a5d60d048a76bbb3dd6d16 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Por favor configura un correo electrónico para recuperación.", "The domain of this email address is blacklisted. Please provide another recovery address.": "El dominio de esta dirección de correo electrónico está en lista negra. Por favor, proporciona otra dirección de recuperación.", "Too many verification emails.": "Demasiados correos electrónicos de verificación.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Esta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Esta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad.", + "The email could not be verified. Please try again later.": "No se ha podido verificar el correo electrónico. Inténtelo de nuevo más tarde.", + "The email address is disposable. Please provide another recovery address." : "La dirección de correo electrónico es desechable. Por favor, proporcione otra dirección de recuperación.", + "The email address is not deliverable. Please provide another recovery address.": "La dirección de correo electrónico no se puede entregar. Por favor, proporcione otra dirección de recuperación." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/fr.js b/l10n/fr.js index 7fa0cc03d2c4142acd03054c20a45ce2823f6626..8a5e027b8cb3497cb1ea662c33a26e46731deebe 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Merci d'ajouter une adresse e-mail de récupération.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Le domain de cette adresse e-mail est sur liste noire. Merci de bien vouloir fournir une autre adresse de récupération.", "Too many verification emails.": "Trop de courriels de vérification.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", + "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", + "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", + "The email address is not deliverable. Please provide another recovery address.": "L'adresse électronique ne peut être délivrée. Veuillez fournir une autre adresse de recouvrement." }, "nplurals=2; plural=n > 1;"); diff --git a/l10n/fr.json b/l10n/fr.json index d660d412f7c53ee74c9e654261f7c72df2994d79..ed352922bacb3e159bf444bbcafab5ddf806e9b3 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Merci d'ajouter une adresse e-mail de récupération.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Le domain de cette adresse e-mail est sur liste noire. Merci de bien vouloir fournir une autre adresse de récupération.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", + "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", + "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", + "The email address is not deliverable. Please provide another recovery address.": "L'adresse électronique ne peut être délivrée. Veuillez fournir une autre adresse de recouvrement." },"pluralForm" :"nplurals=2; plural=n > 1;" } \ No newline at end of file diff --git a/l10n/it.js b/l10n/it.js index af51b4109610213b6fb1bdfc0dbc85b15f2eed81..53f5fa64b921e69485d3358bc5af96ec1d300859 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Imposta un indirizzo e-mail di recovery.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Il dominio cui appartiene questo indirizzo e-mail è contenuto in una black list. Inserisci un indirizzo di recovery differente.", "Too many verification emails.": "Troppe e-mail di verifica.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità.", + "The email could not be verified. Please try again later.": "Non è stato possibile verificare l'e-mail. Si prega di riprovare più tardi.", + "The email address is disposable. Please provide another recovery address." : "L'indirizzo e-mail è monouso. Si prega di fornire un altro indirizzo di recupero.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/it.json b/l10n/it.json index 84fd9e6d8cfd79dab0bffd5fe7da88ec4a19e6c6..5e9165fc0ff6c7d6e0a4d6f258350a31c68fb0b6 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Imposta un indirizzo e-mail di recovery.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Il dominio cui appartiene questo indirizzo e-mail è contenuto in una black list. Inserisci un indirizzo di recovery differente.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità.", + "The email could not be verified. Please try again later.": "Non è stato possibile verificare l'e-mail. Si prega di riprovare più tardi.", + "The email address is disposable. Please provide another recovery address." : "L'indirizzo e-mail è monouso. Si prega di fornire un altro indirizzo di recupero.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php new file mode 100644 index 0000000000000000000000000000000000000000..a19f96b3a07712a4519a02a73396c13db09e9c7c --- /dev/null +++ b/lib/Command/CreatePopularDomain.php @@ -0,0 +1,45 @@ +domainService = $domainService; + $this->logger = $logger; + $this->appData = $appData; + } + + protected function configure() { + $this + ->setName(Application::APP_ID.':create-popular-domains') + ->setDescription('Fetch popular email domains and create a popular_domain.json file'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $this->domainService->updatePopularDomainsFile(); + $output->writeln('Popular domains list created successfully.'); + } catch (\Throwable $th) { + $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); + $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); + return Command::FAILURE; + } + + return Command::SUCCESS; + } +} diff --git a/lib/Command/UpdateBlacklistedDomains.php b/lib/Command/UpdateBlacklistedDomains.php index b0bbed834579126e66b6e8be992a2239a1b452d6..b677f6804f70454e1acb4f15f966c7d382b69362 100644 --- a/lib/Command/UpdateBlacklistedDomains.php +++ b/lib/Command/UpdateBlacklistedDomains.php @@ -5,20 +5,20 @@ declare(strict_types=1); namespace OCA\EmailRecovery\Command; use OCA\EmailRecovery\AppInfo\Application; -use OCA\EmailRecovery\Service\BlackListService; +use OCA\EmailRecovery\Service\DomainService; use OCP\ILogger; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class UpdateBlacklistedDomains extends Command { - private BlackListService $blackListService; + private DomainService $domainService; private ILogger $logger; - public function __construct(BlackListService $blackListService, ILogger $logger) { + public function __construct(DomainService $domainService, ILogger $logger) { parent::__construct(); - $this->blackListService = $blackListService; + $this->domainService = $domainService; $this->logger = $logger; } @@ -28,7 +28,7 @@ class UpdateBlacklistedDomains extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { try { - $this->blackListService->updateBlacklistedDomains(); + $this->domainService->updateBlacklistedDomains(); $output->writeln('Updated blacklisted domains for creation.'); } catch (\Throwable $th) { $this->logger->error('Error while updating blacklisted domains. ' . $th->getMessage()); diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php deleted file mode 100644 index 020c58fac70e3e4112cb8aab125e17070d6d3f96..0000000000000000000000000000000000000000 --- a/lib/Service/BlackListService.php +++ /dev/null @@ -1,123 +0,0 @@ -logger = $logger; - $this->l10nFactory = $l10nFactory; - $this->appData = $appData; - $this->appName = $appName; - } - /** - * Check if an email domain is blacklisted against a JSON list of disposable email domains. - * - * @param string $email The email address to check. - * @return bool True if the email domain is blacklisted, false otherwise. - */ - public function isBlacklistedEmail(string $email): bool { - if (!$this->ensureDocumentsFolder()) { - return false; - } - $blacklistedDomains = $this->getBlacklistedDomainData(); - if (empty($blacklistedDomains)) { - return false; - } - $emailParts = explode('@', $email); - $emailDomain = strtolower(end($emailParts)); - return in_array($emailDomain, $blacklistedDomains); - } - /** - * Update the blacklisted domains data by fetching it from a URL and saving it locally. - * - * @return void - */ - public function updateBlacklistedDomains(): void { - $blacklisted_domain_url = self::BLACKLISTED_DOMAINS_URL; - $json_data = file_get_contents($blacklisted_domain_url); - $this->setBlacklistedDomainsData($json_data); - } - /** - * Store blacklisted domain data in a file within AppData. - * - * @param string $data The data to be stored in the file. - * @return void - */ - private function setBlacklistedDomainsData(string $data): void { - $file = $this->getBlacklistedDomainsFile(); - $file->putContent($data); - } - /** - * Retrieve the blacklisted domain file path - * - * @return ISimpleFile - */ - private function getBlacklistedDomainsFile(): ISimpleFile { - try { - $currentFolder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $currentFolder = $this->appData->newFolder('/'); - } - $filename = self::BLACKLISTED_DOMAINS_FILE_NAME; - if ($currentFolder->fileExists($filename)) { - return $currentFolder->getFile($filename); - } - return $currentFolder->newFile($filename); - } - /** - * Retrieve the blacklisted domain data. - * - * @return array The array of blacklisted domains. - */ - public function getBlacklistedDomainData(): array { - $document = self::BLACKLISTED_DOMAINS_FILE_NAME; - $file = $this->getBlacklistedDomainsFile(); - try { - $blacklistedDomainsInJson = $file->getContent(); - if (empty($blacklistedDomainsInJson)) { - return []; - } - return json_decode($blacklistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } catch (NotFoundException $e) { - $this->logger->warning('Blacklisted domains file ' . $document . ' not found!'); - return []; - } catch (\Throwable $e) { - $this->logger->warning('Error decoding blacklisted domains file ' . $document . ': ' . $e->getMessage()); - return []; - } - } - - /** - * Ensure the specified folder exists within AppData. - * - * @return bool - */ - private function ensureDocumentsFolder(): bool { - try { - $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $this->logger->error($this->appName . ' AppData folder not found!'); - return false; - } catch (\RuntimeException $e) { - $this->logger->error($this->appName . ' AppData folder not found! Runtime Error: ' . $e->getMessage()); - return false; - } - return true; - } -} diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php new file mode 100644 index 0000000000000000000000000000000000000000..ca5f43c8ab4a954187c4517ff706d0f7bfd9b436 --- /dev/null +++ b/lib/Service/DomainService.php @@ -0,0 +1,191 @@ +logger = $logger; + $this->appData = $appData; + $this->httpClient = $httpClient; + $this->appName = $appName; + $this->l = $l; + } + + // ------------------------------- + // Public Methods + // ------------------------------- + + /** + * Check if an email belongs to a popular domain. + */ + public function isPopularDomain(string $email, IL10N $l): bool { + $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE, $l); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a blacklisted domain. + */ + public function isBlacklistedDomain(string $email, IL10N $l): bool { + $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE, $l); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a custom blacklist domain. + */ + public function isDomainInCustomBlacklist(string $email, IL10N $l): bool { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); + return $this->isDomainInList($email, $domains); + } + + /** + * Update blacklisted domains by fetching from the external source. + */ + public function updateBlacklistedDomains(): void { + $this->updateDomainsFromUrl(self::BLACKLISTED_DOMAINS_URL, self::BLACKLISTED_DOMAINS_FILE); + } + + /** + * Add a custom disposable domain to the list. + */ + public function addCustomDisposableDomain(string $domain, IL10N $l, array $relatedDomains = []): void { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); + $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); + $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); + } + + // ------------------------------- + // Private Helper Methods + // ------------------------------- + + /** + * Check if an email's domain is in the given list of domains. + */ + private function isDomainInList(string $email, array $domains): bool { + if (empty($domains)) { + return false; + } + $emailDomain = strtolower(explode('@', $email)[1]); + return in_array($emailDomain, $domains, true); + } + + /** + * Fetch and update domains from an external URL. + */ + private function updateDomainsFromUrl(string $url, string $filename): void { + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + $this->saveDomainsToFile($filename, $data); + } catch (RequestException $e) { + $this->logger->error("Failed to fetch domains from $url: " . $e->getMessage()); + } catch (\Throwable $e) { + $this->logger->error("Unexpected error while updating domains: " . $e->getMessage()); + } + } + + /** + * Get domains from a file. + */ +private function getDomainsFromFile(string $filename, IL10N $l): array { + try { + // Attempt to get and read the file + $file = $this->getDomainsFile($filename); + $content = $file->getContent(); + + // Decode JSON content + return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (NotFoundException $e) { + // File not found, treat as no domains configured + $this->logger->warning("File $filename not found. Returning an empty domain list."); + return []; + } catch (\Throwable $e) { + // Other errors indicate a serious issue (e.g., unreadable file) + $this->logger->error("Error reading $filename: " . $e->getMessage()); + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + } +} + /** + * Save domains to a file. + */ + private function saveDomainsToFile(string $filename, array $domains): void { + $file = $this->getDomainsFile($filename); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + /** + * Get or create a file in AppData. + */ + private function getDomainsFile(string $filename): ISimpleFile { + try { + $folder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('/'); + } + + if ($folder->fileExists($filename)) { + return $folder->getFile($filename); + } + return $folder->newFile($filename); + } + + public function updatePopularDomainsFile(): void { + try { + $domains = $this->fetchPopularDomains(); + $file = $this->getDomainsFile(self::POPULAR_DOMAINS_FILE); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->logger->info('Popular domains list updated successfully.'); + } catch (\Throwable $e) { + $this->logger->error('Error while updating popular domains file: ' . $e->getMessage()); + throw new \RuntimeException('Failed to update popular domains file.', 0, $e); + } + } + + /** + * Fetch popular email domains from an external source. + * + * @return array List of popular domains. + */ + private function fetchPopularDomains(): array { + $url = self::POPULAR_DOMAINS_URL; + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); + } + + return $data; + } catch (RequestException $e) { + $this->logger->error('HTTP request failed: ' . $e->getMessage()); + throw new \RuntimeException('Failed to fetch popular domains.'); + } catch (\Throwable $th) { + $this->logger->error('Unexpected error: ' . $th->getMessage()); + throw $th; + } + } +} diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 0fe18447e926ecdf79a2c14559dfea77d0ab4826..74a4f6194560df99b767d4e8b6933414831d3919 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -24,6 +24,8 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; +use OCP\ICacheFactory; +use OCP\Http\Client\IClientService; class RecoveryEmailService { private ILogger $logger; @@ -35,15 +37,23 @@ class RecoveryEmailService { private IURLGenerator $urlGenerator; private Defaults $themingDefaults; private IVerificationToken $verificationToken; + private ICacheFactory $cacheFactory; private CurlService $curl; + private IClientService $httpClientService; private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; - private BlackListService $blackListService; + private const CACHE_KEY = 'recovery_email_rate_limit'; + private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; + private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; + private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; + private $cache; + + private DomainService $domainService; private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClientService $httpClientService) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -55,8 +65,11 @@ class RecoveryEmailService { $this->themingDefaults = $themingDefaults; $this->verificationToken = $verificationToken; $this->curl = $curlService; - $this->blackListService = $blackListService; + $this->domainService = $domainService; + $this->httpClientService = $httpClientService; $this->l = $l; + $this->cacheFactory = $cacheFactory; // Initialize the cache factory + $this->cache = $this->cacheFactory->createDistributed(self::CACHE_KEY); // Initialize the cache $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { @@ -107,37 +120,184 @@ class RecoveryEmailService { return true; } - public function validateRecoveryEmail(string $recoveryEmail, string $username = '', string $language = 'en') : bool { - $email = ''; - if ($username != '') { - $user = $this->userManager->get($username); - $email = $user->getEMailAddress(); - } + public function validateRecoveryEmail(string $recoveryEmail, string $username = '', string $language = 'en'): bool { + // Fetch user email if username is provided + $email = $this->getUserEmail($username); + $l = $this->l10nFactory->get($this->appName, $language); - if (!empty($recoveryEmail)) { - if (!$this->isValidEmailFormat($recoveryEmail)) { - $this->logger->info("User $username's requested recovery email does not match email format"); - throw new InvalidRecoveryEmailException($l->t('Invalid Recovery Email')); + $this->enforceBasicRecoveryEmailRules($recoveryEmail, $username, $email, $l); + + $apiKey = $this->config->getSystemValue('verify_mail_api_key', ''); + + if (empty($apiKey)) { + $this->logger->info('VerifyMail API Key is not configured.'); + } + + // Check if it's a popular domain and not custom blacklist, then verify the email + if ($this->domainService->isPopularDomain($recoveryEmail, $l) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2, $l); + $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey, $l); + } + + // Verify the domain using the API + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_DOMAIN, 15, $l); + $domain = substr(strrchr($recoveryEmail, "@"), 1); + $this->verifyDomainWithApi($domain, $username, $apiKey, $l); + + return true; + } + private function getUserEmail(string $username): string { + if (empty($username)) { + return ''; + } + $user = $this->userManager->get($username); + return $user->getEMailAddress(); + } + + private function enforceBasicRecoveryEmailRules(string $recoveryEmail, string $username, string $email, IL10N $l): void { + if (empty($recoveryEmail)) { + return; + } + + if (!$this->isValidEmailFormat($recoveryEmail)) { + $this->logger->info("User $username's requested recovery email does not match email format"); + throw new InvalidRecoveryEmailException($l->t('Invalid Recovery Email')); + } + + if (!empty($email) && strcmp($recoveryEmail, $email) === 0) { + $this->logger->info("User ID $username's requested recovery email is the same as email"); + throw new SameRecoveryEmailAsEmailException($l->t('Error! User email address cannot be saved as recovery email address!')); + } + + if ($this->isRecoveryEmailTaken($username, $recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email address is already taken"); + throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); + } + + if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email address is disallowed."); + throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); + } + + if ($this->domainService->isBlacklistedDomain($recoveryEmail, $l)) { + $this->logger->info("User ID $username's requested recovery email address domain is blacklisted."); + throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); + } + } + + private function retryApiCall(callable $callback, IL10N $l, int $maxRetries = 10, int $initialInterval = 1000): void { + $retryInterval = $initialInterval; // Initial retry interval in milliseconds + $retries = 0; + + while ($retries < $maxRetries) { + try { + // Execute the API call + $result = $callback(); + + // If successful, return immediately + return; + } catch (\Exception $e) { + // Check for rate-limiting (HTTP 429) + if ($e instanceof \RuntimeException && $e->getCode() === 429) { + $retries++; + + if ($retries >= $maxRetries) { + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + } + + $this->logger->warning("Received 429 status code, retrying in $retryInterval ms..."); + usleep($retryInterval * 1000); // Convert to microseconds + $retryInterval *= 2; // Exponential backoff + continue; // Retry only on 429 errors + } + + // For other exceptions, log and rethrow immediately without retrying + $this->logger->error("API call failed on the first attempt. Error: " . $e->getMessage()); + throw $e; } - if ($email != '' && strcmp($recoveryEmail, $email) === 0) { - $this->logger->info("User ID $username's requested recovery email is the same as email"); - throw new SameRecoveryEmailAsEmailException($l->t('Error! User email address cannot be saved as recovery email address!')); + } + + // Shouldn't reach here since retries are handled above + throw new \RuntimeException("API call failed unexpectedly after maximum retries."); + } + + private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey, IL10N $l): void { + $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); + + $this->retryApiCall(function () use ($url, $username, $l) { + try { + $httpClient = $this->httpClientService->newClient(); + // Make the API request + $response = $httpClient->get($url, [ + 'timeout' => 5, // Timeout for the API call + ]); + + // Process response, handle errors (e.g., disposable email, non-deliverable email) + $responseBody = $response->getBody(); // Get the response body as a string + $data = json_decode($responseBody, true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is disposable."); + throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); + } + + if (!$data['deliverable_email'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address is not deliverable."); + throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); + } + } catch (\Exception $e) { + // Optionally handle specific exceptions if needed here (e.g., timeouts, network errors) + $this->logger->error("Error while validating email for user $username: " . $e->getMessage()); + throw $e; // Re-throw if necessary } - if ($this->isRecoveryEmailTaken($username, $recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address is already taken"); - throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); + }, $l, // Pass the IL10N object + 10, // Optional: Max retries (default is 10, override if necessary) + 1000); + } + + + + private function verifyDomainWithApi(string $domain, string $username, string $apiKey, IL10N $l): void { + $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); + + $this->retryApiCall(function () use ($url, $username, $domain, $l) { + $httpClient = $this->httpClientService->newClient(); + // Make the API request + $response = $httpClient->get($url, [ + 'timeout' => 5, // Timeout for the API call + ]); + + // Process response, handle errors (e.g., disposable email, non-deliverable email) + $responseBody = $response->getBody(); // Get the response body as a string + $data = json_decode($responseBody, true); + + + // Check if the data is properly structured + if (!$data || !is_array($data)) { + throw new \RuntimeException("Invalid response received while verifying domain: " . $response); } - if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address is disallowed."); - throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); + + // Handle response data + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); + $this->domainService->addCustomDisposableDomain($domain, $l, $data['related_domains'] ?? []); + throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); } - if ($this->blackListService->isBlacklistedEmail($recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address domain is blacklisted. Please provide another recovery address."); - throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); + + if (!$data['mx'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address domain is not valid."); + $this->domainService->addCustomDisposableDomain($domain, $l, $data['related_domains'] ?? []); + throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); } - } - return true; + + $this->logger->info("User ID $username's requested recovery email address domain is valid."); + }, $l, // Pass the IL10N object + 10, // Optional: Max retries (default is 10, override if necessary) + 1000); } + + + public function isRecoveryEmailDomainDisallowed(string $recoveryEmail): bool { $recoveryEmail = strtolower($recoveryEmail); $emailParts = explode('@', $recoveryEmail); @@ -307,4 +467,42 @@ class RecoveryEmailService { public function isValidEmailFormat(string $recoveryEmail): bool { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } + + private function ensureRealTimeRateLimit(string $key, int $rateLimit, IL10N $l, int $maxRetries = 10): void { + $now = microtime(true); + $attempts = 0; // Track the number of attempts + $requests = $this->cache->get($key) ?? []; + + // Filter out requests older than the sliding window of 1 second + $requests = array_filter($requests, function ($timestamp) use ($now) { + return ($now - $timestamp) <= 1; + }); + + // If we exceed the rate limit, delay until the next available slot + while (count($requests) >= $rateLimit) { + $oldestRequest = min($requests); + $delay = 1 - ($now - $oldestRequest); // Time to wait until the sliding window resets + + if ($delay > 0) { + usleep((int)($delay * 1000000)); // Sleep for the calculated delay + } + + // Update current time after delay and re-check + $now = microtime(true); + $requests = array_filter($requests, function ($timestamp) use ($now) { + return ($now - $timestamp) <= 1; + }); + + // Increment attempts and check for max retries + $attempts++; + if ($attempts >= $maxRetries) { + $this->logger->info("Rate limit exceeded after $maxRetries attempts. Please try again later."); + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + } + } + + // Add the current request timestamp + $requests[] = $now; + $this->cache->set($key, $requests, 2); + } } diff --git a/recovery-verification-steps.png b/recovery-verification-steps.png new file mode 100644 index 0000000000000000000000000000000000000000..2b4542e30043383902a727544b5ebc9c64aab4bb Binary files /dev/null and b/recovery-verification-steps.png differ