From d9db59b1dbdf8739d8b4b29d24a69c2c5ec0c4b8 Mon Sep 17 00:00:00 2001
From: Reid 'arrdem' McKenzie
Date: Fri, 2 Jun 2023 23:58:29 -0600
Subject: [PATCH] Simplify emails with a spool
---
.../python/tentacles/blueprints/user_ui.py | 18 ++++++++---
.../tentacles/src/python/tentacles/schema.sql | 13 ++++++++
.../tentacles/src/python/tentacles/store.py | 32 ++++++++++++++++++-
.../templates/verification_email.html.j2 | 4 +--
.../tentacles/src/python/tentacles/workers.py | 29 ++++-------------
5 files changed, 67 insertions(+), 29 deletions(-)
diff --git a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
index 3280e02..c9bfeae 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
@@ -84,16 +84,26 @@ def post_register():
break
- if res := ctx.db.try_create_user(
+ if user := ctx.db.try_create_user(
username, email, salt(request.form["password"]), group_id, status_id
):
- id, status = res
- if status == -1:
+ if user.status_id == -2:
+ ctx.db.create_email(
+ user.id,
+ "Tentacles email confirmation",
+ render_template(
+ "verification_email.html.j2",
+ username=user.name,
+ token_id=user.verification_token,
+ base_url=request.root_url,
+ ),
+ )
+
flash(
"Please check your email for a verification request",
category="success",
)
- return render_template("register.html.j2")
+ return render_template("register.html.j2")
except Exception as e:
log.exception("Error encountered while registering a user...")
diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql
index b2ec1db..20f5067 100644
--- a/projects/tentacles/src/python/tentacles/schema.sql
+++ b/projects/tentacles/src/python/tentacles/schema.sql
@@ -1,3 +1,5 @@
+----------------------------------------------------------------------------------------------------
+-- User structures
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT
, name TEXT
@@ -100,3 +102,14 @@ CREATE TABLE IF NOT EXISTS jobs (
, FOREIGN KEY(file_id) REFERENCES files(id)
, FOREIGN KEY(printer_id) REFERENCES printer(id)
);
+
+----------------------------------------------------------------------------------------------------
+-- Emails are used for notifications
+CREATE TABLE IF NOT EXISTS email_spool (
+ id INTEGER PRIMARY KEY AUTOINCREMENT
+ , user_id INTEGER NOT NULL
+ , subject TEXT
+ , body TEXT
+ , sent_at TEXT
+ , FOREIGN KEY(user_id) REFERENCES users(id)
+);
diff --git a/projects/tentacles/src/python/tentacles/store.py b/projects/tentacles/src/python/tentacles/store.py
index ade7a30..3454a53 100644
--- a/projects/tentacles/src/python/tentacles/store.py
+++ b/projects/tentacles/src/python/tentacles/store.py
@@ -103,7 +103,7 @@ class Store(object):
digest.update(password.encode("utf-8"))
digest = digest.hexdigest()
return self._conn.execute(
- "INSERT INTO users (name, email, hash, group_id, status_id) VALUES (?, ?, ?, ?, ?) RETURNING id, status_id",
+ "INSERT INTO users (name, email, hash, group_id, status_id) VALUES (?, ?, ?, ?, ?) RETURNING *",
[username, email, digest, group_id, status_id],
).fetchone()
@@ -521,3 +521,33 @@ class Store(object):
@requires_conn
def delete_job(self, uid: int, jid: int):
self._conn.execute("DELETE FROM jobs WHERE user_id = ? and id = ?", [uid, jid])
+
+ ################################################################################
+ # Emails (notifications)
+
+ @requires_conn
+ def create_email(self, uid: int, subject: str, body: str):
+ return self._conn.execute(
+ "INSERT INTO email_spool (user_id, subject, body) VALUES (?1, ?2, ?3) RETURNING id",
+ [uid, subject, body],
+ ).fetchone()
+
+ @requires_conn
+ def send_email(self, eid: int):
+ return self._conn.execute(
+ "UPDATE email_spool SET sent_at = datetime('now') WHERE id = ?1", [eid]
+ )
+
+ @requires_conn
+ def poll_spool(self, limit: int = 16):
+ return self._conn.execute(
+ """
+ SELECT s.id as `id`, u.email as `to`, subject, body
+ FROM email_spool s
+ INNER JOIN users u
+ ON s.user_id = u.id
+ WHERE s.sent_at IS NULL
+ LIMIT ?1
+ """,
+ [limit],
+ ).fetchall()
diff --git a/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2 b/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2
index 814f148..88b46de 100644
--- a/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2
+++ b/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2
@@ -21,11 +21,11 @@
Welcome {{ username }}!
- Before you can use your account, please confirm your email address by clicking this link or pasting the following text into your browser's navigation bar.
+ Before you can use your account, please confirm your email address by clicking this link or pasting the following text into your browser's navigation bar.
- {{ base_url }}/user/verify?token={{token_id}}
+ {{ base_url }}user/verify?token={{token_id}}
diff --git a/projects/tentacles/src/python/tentacles/workers.py b/projects/tentacles/src/python/tentacles/workers.py
index 0e89f6c..410efca 100644
--- a/projects/tentacles/src/python/tentacles/workers.py
+++ b/projects/tentacles/src/python/tentacles/workers.py
@@ -254,35 +254,21 @@ def pull_jobs(app: App, store: Store) -> None:
log.exception("Oop")
-def send_verifications(app, store: Store):
+def send_emails(app, store: Store):
with closing(
FastMailSMTP(
app.config.get("fastmail", {}).get("username"),
app.config.get("fastmail", {}).get("key"),
)
) as fm:
- for user in store.list_unverified_users():
+ for message in store.poll_spool():
fm.send_message(
from_addr="root@tirefireind.us",
- to_addrs=[user.email],
- subject="Email verification from tentacles",
- msg=render_template(
- "verification_email.html.j2",
- base_url=app.config.get("base_url"),
- username=user.username,
- token_id=user.verification_token,
- ),
+ to_addrs=[message.to],
+ subject=message.subject,
+ msg=message.body,
)
-
-
-def send_approvals(app, store: Store):
- with closing(
- FastMailSMTP(
- app.config.get("fastmail", {}).get("username"),
- app.config.get("fastmail", {}).get("key"),
- )
- ):
- pass
+ store.send_email(message.id)
@corn_job(timedelta(seconds=5))
@@ -293,8 +279,7 @@ def run_worker(app: App, db_factory):
push_jobs(app, store)
revoke_jobs(app, store)
pull_jobs(app, store)
- send_verifications(app, store)
- send_approvals(app, store)
+ send_emails(app, store)
def create_workers(app, db_factory: Callable[[App], Store]) -> Event: