From dd6b93a4db421ed7e4cffcb3ae6ec75c05ca846e Mon Sep 17 00:00:00 2001 From: brassy endomorph Date: Thu, 6 Feb 2025 10:21:56 +0000 Subject: [PATCH] simplify email logic --- hushline/model/field_value.py | 6 +-- hushline/routes/forms.py | 44 +------------------ hushline/routes/profile.py | 45 +++++++------------- hushline/settings/common.py | 4 +- hushline/static/js/client-side-encryption.js | 39 ----------------- tests/conftest.py | 1 - tests/test_fields.py | 2 - tests/test_inbox.py | 2 - tests/test_profile.py | 2 - 9 files changed, 22 insertions(+), 123 deletions(-) diff --git a/hushline/model/field_value.py b/hushline/model/field_value.py index bbdebb482..182fbd937 100644 --- a/hushline/model/field_value.py +++ b/hushline/model/field_value.py @@ -44,18 +44,16 @@ class FieldValue(Model): _value: Mapped[str] = mapped_column(db.Text) encrypted: Mapped[bool] = mapped_column(default=False) - def __init__( # noqa: PLR0913 + def __init__( self, field_definition: "FieldDefinition", message: "Message", value: str, encrypted: bool, - client_side_encrypted: bool, ) -> None: self.field_definition = field_definition self.message = message self.encrypted = encrypted - self.client_side_encrypted = client_side_encrypted # set the value AFTER setting the encrypted flag self.value = value @@ -73,7 +71,7 @@ def value(self, value: str | list[str]) -> None: if isinstance(value, list): value = "\n".join(value) - if self.encrypted and not self.client_side_encrypted: + if self.encrypted and not value.startswith("-----BEGIN PGP MESSAGE-----"): # Encrypt with PGP # Pad the value to hide the length of the plaintext diff --git a/hushline/routes/forms.py b/hushline/routes/forms.py index fea7d051a..720dadfd1 100644 --- a/hushline/routes/forms.py +++ b/hushline/routes/forms.py @@ -1,10 +1,5 @@ -import enum -from typing import Self - from flask_wtf import FlaskForm from wtforms import ( - Field, - HiddenField, PasswordField, RadioField, SelectField, @@ -12,7 +7,7 @@ StringField, TextAreaField, ) -from wtforms.validators import DataRequired, Length, Optional, ValidationError +from wtforms.validators import DataRequired, Length, Optional from wtforms.widgets import CheckboxInput, ListWidget from hushline.forms import ComplexPassword @@ -57,48 +52,13 @@ class LoginForm(FlaskForm): password = PasswordField("Password", validators=[DataRequired()]) -@enum.unique -class EmailEncryptionType(enum.Enum): - SHOULD_ENCRYPT = "should_encrypt" - ALREADY_ENCRYPTED = "already_encrypted" - SAFE_AS_PLAINTEXT = "safe_as_plaintext" - - @classmethod - def parse(cls, string: str) -> Self: - for val in cls: - if val.value == string: - return val - raise ValueError(f"Not a valid {cls.__name__}: {string}") - - class DynamicMessageForm: def __init__(self, fields: list[FieldDefinition]): self.fields = fields # Create a custom form class for this instance of CustomMessageForm class F(FlaskForm): - client_side_encrypted = HiddenField( - "Client Side Encrypted", - validators=[DataRequired()], - render_kw={"id": "clientSideEncrypted", "value": "false"}, - ) - email_body = HiddenField( - "Email Body", validators=[Optional(), Length(max=10240 * len(fields))] - ) - email_encryption_type = HiddenField( - "Email Encryption Type", - validators=[DataRequired()], - render_kw={ - "id": "emailEncryptionType", - "value": EmailEncryptionType.SHOULD_ENCRYPT.value, - }, - ) - - def validate_email_encryption_type(self, field: Field) -> None: - try: - EmailEncryptionType.parse(field.data) - except ValueError: - raise ValidationError(f"Not a valid input: {field.data}") + pass self.F = F diff --git a/hushline/routes/profile.py b/hushline/routes/profile.py index eabd9bd89..66a84ce18 100644 --- a/hushline/routes/profile.py +++ b/hushline/routes/profile.py @@ -22,7 +22,7 @@ Username, ) from hushline.routes.common import do_send_email, validate_captcha -from hushline.routes.forms import DynamicMessageForm, EmailEncryptionType +from hushline.routes.forms import DynamicMessageForm from hushline.safe_template import safe_render_template @@ -95,32 +95,12 @@ def submit_message(username: str) -> Response | str: current_app.logger.debug(f"Form submitted: {form.data}") - email_body = ( - "There was an error creating an encrypted email body. " - "Login to Hush Line to view this message." - ) - match form.email_encryption_type.data: - case EmailEncryptionType.SHOULD_ENCRYPT.value: - # if we should encrypt at this point, something went wrong - # but this is semi-expected - pass - case EmailEncryptionType.ALREADY_ENCRYPTED.value: - if (form.encrypted_email_body.data or "").startswith( - "-----BEGIN PGP MESSAGE-----" - ): - email_body = form.email_body.data - else: - current_app.logger.error("Email body is not a PGP message") - case EmailEncryptionType.SAFE_AS_PLAINTEXT.value: - email_body = form.email_body.data - case x: - raise NotImplementedError(f"Encryption type not handled: {x}") - # Create a message message = Message(username_id=uname.id) db.session.add(message) - db.session.commit() + db.session.flush() + extracted_fields = [] # Add the field values for data in dynamic_form.field_data(): field_name: str = data["name"] # type: ignore @@ -131,16 +111,23 @@ def submit_message(username: str) -> Response | str: message, value, field_definition.encrypted, - form.client_side_encrypted.data == "true", ) db.session.add(field_value) - db.session.commit() + db.session.flush() + extracted_fields.append((field_definition.label, field_value.value)) + + db.session.commit() + + if uname.user.enable_email_notifications: + if not uname.user.email_include_message_content: + email_body = "You have a new Hush Line message. Login to read it." + else: + email_body = "" + for name, value in extracted_fields: + email_body += f"\n\n{name}\n\n{value}\n\n==============" - if isinstance(value, list): - value = "\n".join(value) + do_send_email(uname.user, email_body.strip()) - if uname.user.enable_email_notifications and email_body: - do_send_email(uname.user, email_body) flash("👍 Message submitted successfully.") session["reply_slug"] = message.reply_slug current_app.logger.debug("Message sent and now redirecting") diff --git a/hushline/settings/common.py b/hushline/settings/common.py index 640a19139..1b3454b46 100644 --- a/hushline/settings/common.py +++ b/hushline/settings/common.py @@ -234,10 +234,10 @@ def handle_pgp_key_form(user: User, form: PGPKeyForm) -> Response: db.session.commit() else: flash("⛔️ Invalid PGP key format or import failed.") - return redirect(url_for(".notifications")) + return redirect(url_for(".encryption")) flash("👍 PGP key updated successfully.") - return redirect(url_for(".notifications")) + return redirect(url_for(".encryption")) def create_profile_forms( diff --git a/hushline/static/js/client-side-encryption.js b/hushline/static/js/client-side-encryption.js index fbe13faf9..c528eab6c 100644 --- a/hushline/static/js/client-side-encryption.js +++ b/hushline/static/js/client-side-encryption.js @@ -44,14 +44,6 @@ async function encryptMessage(publicKeyArmored, message) { } } -function loadEmailSettings() { - const elem = document.getElementById("userEmailSettings"); - if (!elem) { - console.error("Email settings element not found"); - } - return JSON.parse(elem.innerHTML); -} - function getFieldValue(field) { if ( field.tagName === "INPUT" || @@ -78,46 +70,16 @@ function getFieldLabel(field) { document.addEventListener("DOMContentLoaded", function () { const form = document.getElementById("messageForm"); - const clientSideEncryptedEl = document.getElementById("clientSideEncrypted"); const emailEncryptionType = document.getElementById("emailEncryptionType"); const publicKeyArmored = document.getElementById("publicKey") ? document.getElementById("publicKey").value : ""; - const emailSettings = loadEmailSettings(); - form.addEventListener("submit", async function (event) { event.preventDefault(); const emailBodyEl = document.getElementById("email_body"); - if (emailSettings.sendEmail && emailSettings.includeContent) { - // Build an email body with all fields - let emailBody = ""; - document.querySelectorAll(".form-field").forEach(async (field) => { - const value = getFieldValue(field); - const label = getFieldLabel(field); - - emailBody += `# ${label}\n\n${value}\n\n====================\n\n`; - }); - const encryptedEmailBody = await encryptMessage( - publicKeyArmored, - emailBody, - ); - if (encryptedEmailBody) { - emailBodyEl.value = encryptedEmailBody; - emailEncryptionType.value = "already_encrypted"; - } else { - console.error("Client-side encryption failed for email body"); - } - } else if (emailSettings.sendEmail) { - emailBodyEl.value = - "You have a new Hush Line message. Log in to view it."; - emailEncryptionType.value = "safe_as_plaintext"; - } else { - emailEncryptionType.value = "safe_as_plaintext"; - } - // Loop through all encrypted fields and encrypt them document.querySelectorAll(".encrypted-field").forEach(async (field) => { const value = getFieldValue(field); @@ -155,7 +117,6 @@ document.addEventListener("DOMContentLoaded", function () { fieldContainer.appendChild(textarea); } else { console.error("Client-side encryption failed for field:", field.name); - clientSideEncryptedEl.value = "false"; } }); diff --git a/tests/conftest.py b/tests/conftest.py index fd803fe42..fbd6188c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -294,7 +294,6 @@ def make_message(user: User) -> Message: msg, str(uuid4()), False, - False, ) db.session.add(field_value) db.session.commit() diff --git a/tests/test_fields.py b/tests/test_fields.py index 5ce55802e..ae0c77626 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -113,7 +113,6 @@ def test_field_value_encryption(user: User) -> None: message=message, value="this is a test value", encrypted=field_definition.encrypted, - client_side_encrypted=False, ) db.session.add(field_value) db.session.commit() @@ -150,7 +149,6 @@ def test_field_value_unencryption(user: User) -> None: message=message, value="this is a test value", encrypted=field_definition.encrypted, - client_side_encrypted=False, ) db.session.add(field_value) db.session.commit() diff --git a/tests/test_inbox.py b/tests/test_inbox.py index b50301797..ca082ea0f 100644 --- a/tests/test_inbox.py +++ b/tests/test_inbox.py @@ -20,7 +20,6 @@ def test_delete_own_message(client: FlaskClient, user: User) -> None: message, "test_value", field_def.encrypted, - False, ) db.session.add(field_value) db.session.commit() @@ -59,7 +58,6 @@ def test_cannot_delete_other_user_message( other_user_message, "test_value", field_def.encrypted, - False, ) db.session.add(field_value) db.session.commit() diff --git a/tests/test_profile.py b/tests/test_profile.py index 442023ba2..c98b389cc 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -66,7 +66,6 @@ def test_profile_submit_message(client: FlaskClient, user: User) -> None: data={ "field_0": msg_contact_method, "field_1": msg_content, - "client_side_encrypted": "false", "captcha_answer": get_captcha_from_session(client, user.primary_username.username), }, follow_redirects=True, @@ -96,7 +95,6 @@ def test_profile_submit_message_to_alias( data={ "field_0": msg_contact_method, "field_1": msg_content, - "client_side_encrypted": "false", "captcha_answer": get_captcha_from_session(client, user.primary_username.username), }, follow_redirects=True,