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 @@
+
+
+
+
+
+
+
+
+ {{ 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 @@
+
+
+
+
+
+
+
+
+
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