From 79cf8a6b4a5a327a69baa5379f1ce026706e04d7 Mon Sep 17 00:00:00 2001 From: Tnix Date: Mon, 19 Aug 2024 01:30:08 +1200 Subject: [PATCH] feat: email templates & SMTP --- .env.example | 13 +++++++ email_templates/_base.html | 23 ++++++++++++ email_templates/_base.txt | 11 ++++++ email_templates/_lockdown.html | 9 +++++ email_templates/_lockdown.txt | 3 ++ email_templates/email_changed.html | 4 +++ email_templates/email_changed.txt | 4 +++ email_templates/locked.html | 6 ++++ email_templates/locked.txt | 8 +++++ email_templates/mfa_added.html | 4 +++ email_templates/mfa_added.txt | 4 +++ email_templates/mfa_removed.html | 4 +++ email_templates/mfa_removed.txt | 4 +++ email_templates/password_changed.html | 4 +++ email_templates/password_changed.txt | 4 +++ email_templates/recovery.html | 12 +++++++ email_templates/recovery.txt | 6 ++++ email_templates/verify.html | 12 +++++++ email_templates/verify.txt | 6 ++++ security.py | 50 ++++++++++++++++++++++++++- test.html | 26 ++++++++++++++ 21 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 email_templates/_base.html create mode 100644 email_templates/_base.txt create mode 100644 email_templates/_lockdown.html create mode 100644 email_templates/_lockdown.txt create mode 100644 email_templates/email_changed.html create mode 100644 email_templates/email_changed.txt create mode 100644 email_templates/locked.html create mode 100644 email_templates/locked.txt create mode 100644 email_templates/mfa_added.html create mode 100644 email_templates/mfa_added.txt create mode 100644 email_templates/mfa_removed.html create mode 100644 email_templates/mfa_removed.txt create mode 100644 email_templates/password_changed.html create mode 100644 email_templates/password_changed.txt create mode 100644 email_templates/recovery.html create mode 100644 email_templates/recovery.txt create mode 100644 email_templates/verify.html create mode 100644 email_templates/verify.txt create mode 100644 test.html diff --git a/.env.example b/.env.example index 9c74d34..de92fd4 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,19 @@ INTERNAL_API_TOKEN="" # used for authenticating internal API requests (gives ac CAPTCHA_SITEKEY= CAPTCHA_SECRET= +EMAIL_SMTP_HOST= +EMAIL_SMTP_PORT= +EMAIL_SMTP_TLS= +EMAIL_SMTP_USERNAME= +EMAIL_SMTP_PASSWORD= +EMAIL_FROM_NAME= +EMAIL_FROM_ADDRESS= +EMAIL_PLATFORM_NAME="Meower" +EMAIL_PLATFORM_LOGO="" +EMAIL_PLATFORM_BRAND="Meower Media" +EMAIL_PLATFORM_FRONTEND="https://meower.org" +EMAIL_PLATFORM_SUPPORT="support@meower.org" + GRPC_AUTH_ADDRESS="0.0.0.0:5000" GRPC_AUTH_TOKEN= diff --git a/email_templates/_base.html b/email_templates/_base.html new file mode 100644 index 0000000..d9740d1 --- /dev/null +++ b/email_templates/_base.html @@ -0,0 +1,23 @@ + + + +
+
+ {{ env['EMAIL_PLATFORM_NAME'] }} Logo +
+
+ {{ subject }} + Hey {{ name }}! + {% block body %}{% endblock %} + + {# We include the platform brand in the _lockdown.html file as well #} + {# because doing extends seems to cut-off the rest of the file. #} + {% if include_lockdown %} + {% extends "_lockdown.html" %} + {% else %} + - {{ env['EMAIL_PLATFORM_BRAND'] }} + {% endif %} +
+
+ + \ No newline at end of file diff --git a/email_templates/_base.txt b/email_templates/_base.txt new file mode 100644 index 0000000..18a9958 --- /dev/null +++ b/email_templates/_base.txt @@ -0,0 +1,11 @@ +Hey {{ name }}! + +{% block body %}{% endblock %} + +{# We include the platform brand in the _lockdown.txt file as well #} +{# because doing extends seems to cut-off the rest of the file. #} +{% if include_lockdown %} +{% extends "_lockdown.txt" %} +{% else %} +- {{ env['EMAIL_PLATFORM_BRAND'] }} +{% endif %} \ No newline at end of file diff --git a/email_templates/_lockdown.html b/email_templates/_lockdown.html new file mode 100644 index 0000000..05dfde2 --- /dev/null +++ b/email_templates/_lockdown.html @@ -0,0 +1,9 @@ +If you didn't request this, please click the button below within the next 24 hours to revert this change and secure your account. + + This wasn't me! + +- {{ env['EMAIL_PLATFORM_BRAND'] }} \ No newline at end of file diff --git a/email_templates/_lockdown.txt b/email_templates/_lockdown.txt new file mode 100644 index 0000000..0f46b00 --- /dev/null +++ b/email_templates/_lockdown.txt @@ -0,0 +1,3 @@ +If you didn't request this, please follow this link within the next 24 hours to revert this change and secure your account: {{ env['EMAIL_PLATFORM_FRONTEND'] }}/emails/lockdown#{{ token }} + +- {{ env['EMAIL_PLATFORM_BRAND'] }} \ No newline at end of file diff --git a/email_templates/email_changed.html b/email_templates/email_changed.html new file mode 100644 index 0000000..48292a3 --- /dev/null +++ b/email_templates/email_changed.html @@ -0,0 +1,4 @@ +{% extends "_base.html" %} +{% block body %} + The email on your {{ env['EMAIL_PLATFORM_NAME'] }} account was recently changed. +{% endblock %} \ No newline at end of file diff --git a/email_templates/email_changed.txt b/email_templates/email_changed.txt new file mode 100644 index 0000000..8e0a969 --- /dev/null +++ b/email_templates/email_changed.txt @@ -0,0 +1,4 @@ +{% extends "_base.txt" %} +{% block body %} +The email on your {{ env['EMAIL_PLATFORM_NAME'] }} account was recently changed. +{% endblock %} \ No newline at end of file diff --git a/email_templates/locked.html b/email_templates/locked.html new file mode 100644 index 0000000..13613af --- /dev/null +++ b/email_templates/locked.html @@ -0,0 +1,6 @@ +{% extends "_base.html" %} +{% block body %} + Your {{ env['EMAIL_PLATFORM_NAME'] }} account has been locked because we believe it may have been compromised. This can happen if your {{ env['EMAIL_PLATFORM_NAME'] }} password is weak, you used the same password on another website and that website was hacked, or you accidentally gave an access token to someone else. + You will be required to reset your password using this email address ({{ address }}) before logging back in to {{ env['EMAIL_PLATFORM_NAME'] }}. + If you have any questions, please reach out to {{ env['EMAIL_PLATFORM_SUPPORT'] }}. +{% endblock %} \ No newline at end of file diff --git a/email_templates/locked.txt b/email_templates/locked.txt new file mode 100644 index 0000000..52dcec3 --- /dev/null +++ b/email_templates/locked.txt @@ -0,0 +1,8 @@ +{% extends "_base.txt" %} +{% block body %} +Your {{ env['EMAIL_PLATFORM_NAME'] }} account has been locked because we believe it may have been compromised. This can happen if your {{ env['EMAIL_PLATFORM_NAME'] }} password is weak, you used the same password on another website and that website was hacked, or you accidentally gave an access token to someone else. + +You will be required to reset your password using this email address ({{ address }}) before logging back in to {{ env['EMAIL_PLATFORM_NAME'] }}. + +If you have any questions, please reach out to {{ env['EMAIL_PLATFORM_SUPPORT'] }}. +{% endblock %} \ No newline at end of file diff --git a/email_templates/mfa_added.html b/email_templates/mfa_added.html new file mode 100644 index 0000000..2b3d011 --- /dev/null +++ b/email_templates/mfa_added.html @@ -0,0 +1,4 @@ +{% extends "_base.html" %} +{% block body %} + A multi-factor authenticator was recently added to your {{ env['EMAIL_PLATFORM_NAME'] }} account. +{% endblock %} \ No newline at end of file diff --git a/email_templates/mfa_added.txt b/email_templates/mfa_added.txt new file mode 100644 index 0000000..619d8c9 --- /dev/null +++ b/email_templates/mfa_added.txt @@ -0,0 +1,4 @@ +{% extends "_base.txt" %} +{% block body %} +A multi-factor authenticator was recently added to your {{ env['EMAIL_PLATFORM_NAME'] }} account. +{% endblock %} \ No newline at end of file diff --git a/email_templates/mfa_removed.html b/email_templates/mfa_removed.html new file mode 100644 index 0000000..e2f2e51 --- /dev/null +++ b/email_templates/mfa_removed.html @@ -0,0 +1,4 @@ +{% extends "_base.html" %} +{% block body %} + A multi-factor authenticator was recently removed from your {{ env['EMAIL_PLATFORM_NAME'] }} account. +{% endblock %} \ No newline at end of file diff --git a/email_templates/mfa_removed.txt b/email_templates/mfa_removed.txt new file mode 100644 index 0000000..735ad0b --- /dev/null +++ b/email_templates/mfa_removed.txt @@ -0,0 +1,4 @@ +{% extends "_base.txt" %} +{% block body %} +A multi-factor authenticator was recently removed from your {{ env['EMAIL_PLATFORM_NAME'] }} account. +{% endblock %} \ No newline at end of file diff --git a/email_templates/password_changed.html b/email_templates/password_changed.html new file mode 100644 index 0000000..ce19c8c --- /dev/null +++ b/email_templates/password_changed.html @@ -0,0 +1,4 @@ +{% extends "_base.html" %} +{% block body %} + The password to your {{ env['EMAIL_PLATFORM_NAME'] }} account was recently changed. +{% endblock %} \ No newline at end of file diff --git a/email_templates/password_changed.txt b/email_templates/password_changed.txt new file mode 100644 index 0000000..a9b4cb4 --- /dev/null +++ b/email_templates/password_changed.txt @@ -0,0 +1,4 @@ +{% extends "_base.txt" %} +{% block body %} +The password to your {{ env['EMAIL_PLATFORM_NAME'] }} account was recently changed. +{% endblock %} \ No newline at end of file diff --git a/email_templates/recovery.html b/email_templates/recovery.html new file mode 100644 index 0000000..8be3de8 --- /dev/null +++ b/email_templates/recovery.html @@ -0,0 +1,12 @@ +{% extends "_base.html" %} +{% block body %} + To reset your {{ env['EMAIL_PLATFORM_NAME'] }} account password, please click the button below. + If you didn't request this, please ignore this email, no further action is required. + + Reset Password + +{% endblock %} \ No newline at end of file diff --git a/email_templates/recovery.txt b/email_templates/recovery.txt new file mode 100644 index 0000000..79fe129 --- /dev/null +++ b/email_templates/recovery.txt @@ -0,0 +1,6 @@ +{% extends "_base.txt" %} +{% block body %} +To reset your {{ env['EMAIL_PLATFORM_NAME'] }} account password, please follow this link: {{ env['EMAIL_PLATFORM_FRONTEND'] }}/emails/recovery#{{ token }} + +If you didn't request this, please ignore this email, no further action is required. +{% endblock %} \ No newline at end of file diff --git a/email_templates/verify.html b/email_templates/verify.html new file mode 100644 index 0000000..d698b37 --- /dev/null +++ b/email_templates/verify.html @@ -0,0 +1,12 @@ +{% extends "_base.html" %} +{% block body %} + To confirm adding your email address ({{ email }}) to your {{ env['EMAIL_PLATFORM_NAME'] }} account, please click the button below. + If you didn't request this, please ignore this email, no further action is required. + + Verify Email Address + +{% endblock %} \ No newline at end of file diff --git a/email_templates/verify.txt b/email_templates/verify.txt new file mode 100644 index 0000000..e7a8fca --- /dev/null +++ b/email_templates/verify.txt @@ -0,0 +1,6 @@ +{% extends "_base.txt" %} +{% block body %} +To confirm adding your email address ({{ email }}) to your {{ env['EMAIL_PLATFORM_NAME'] }} account, please follow this link: {{ env['EMAIL_PLATFORM_FRONTEND'] }}/emails/verify#{{ token }} + +If you didn't request this, please ignore this email, no further action is required. +{% endblock %} \ No newline at end of file diff --git a/security.py b/security.py index 6f5db22..ba425f6 100644 --- a/security.py +++ b/security.py @@ -1,6 +1,9 @@ from hashlib import sha256 from typing import Optional -import time, requests, os, uuid, secrets, bcrypt, msgpack +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.utils import formataddr +import time, requests, os, uuid, secrets, bcrypt, msgpack, jinja2, smtplib from database import db, rdb from utils import log @@ -43,6 +46,10 @@ TOKEN_BYTES = 64 +email_file_loader = jinja2.FileSystemLoader("email_templates") +email_env = jinja2.Environment(loader=email_file_loader) + + class UserFlags: SYSTEM = 1 DELETED = 2 @@ -536,3 +543,44 @@ def hash_password(password: str) -> str: def check_password_hash(password: str, hashed_password: str) -> bool: return bcrypt.checkpw(password.encode(), hashed_password.encode()) + + +def send_email(template: str, to_name: str, to_address: str, token: Optional[str] = ""): + txt_tmpl = email_env.get_template(f"{template}.txt") + html_tmpl = email_env.get_template(f"{template}.html") + + message = MIMEMultipart("alternative") + message["From"] = formataddr((os.environ["EMAIL_FROM_NAME"], os.environ["EMAIL_FROM_ADDRESS"])) + message["To"] = formataddr((to_name, to_address)) + + match template: + case "verify": + message["Subject"] = "Verify your email address" + case "recovery": + message["Subject"] = "Reset your password" + case "email_changed": + message["Subject"] = "Your email has been changed" + case "password_changed": + message["Subject"] = "Your password has been changed" + case "mfa_added": + message["Subject"] = "Multi-factor authenticator added" + case "mfa_removed": + message["Subject"] = "Multi-factor authenticator removed" + case "locked": + message["Subject"] = "Your account has been locked" + + data = { + "subject": message["Subject"], + "name": to_name, + "address": to_address, + "token": token, + "env": os.environ + } + message.attach(MIMEText(txt_tmpl.render(data), "plain")) + message.attach(MIMEText(html_tmpl.render(data), "html")) + + with smtplib.SMTP(os.environ["EMAIL_SMTP_HOST"], int(os.environ["EMAIL_SMTP_PORT"])) as server: + if os.getenv("EMAIL_SMTP_TLS"): + server.starttls() + server.login(os.environ["EMAIL_SMTP_USERNAME"], os.environ["EMAIL_SMTP_PASSWORD"]) + server.sendmail(os.environ["EMAIL_FROM_ADDRESS"], to_address, message.as_string()) diff --git a/test.html b/test.html new file mode 100644 index 0000000..3d62a0c --- /dev/null +++ b/test.html @@ -0,0 +1,26 @@ + + + +
+
+  Logo +
+
+ Your email has been changed + Hey ! + + The email on your account was recently changed. + + + + + + If you didn't request this, please click the button below within the next 24 hours to revert this change and secure your account. + + This wasn't me! + +- Meower Media \ No newline at end of file