Skip to content

Commit

Permalink
simplify email logic
Browse files Browse the repository at this point in the history
  • Loading branch information
brassy-endomorph committed Feb 6, 2025
1 parent 5ed8ac3 commit dd6b93a
Show file tree
Hide file tree
Showing 9 changed files with 22 additions and 123 deletions.
6 changes: 2 additions & 4 deletions hushline/model/field_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
44 changes: 2 additions & 42 deletions hushline/routes/forms.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import enum
from typing import Self

from flask_wtf import FlaskForm
from wtforms import (
Field,
HiddenField,
PasswordField,
RadioField,
SelectField,
SelectMultipleField,
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
Expand Down Expand Up @@ -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

Expand Down
45 changes: 16 additions & 29 deletions hushline/routes/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions hushline/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
39 changes: 0 additions & 39 deletions hushline/static/js/client-side-encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand All @@ -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);
Expand Down Expand Up @@ -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";
}
});

Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ def make_message(user: User) -> Message:
msg,
str(uuid4()),
False,
False,
)
db.session.add(field_value)
db.session.commit()
Expand Down
2 changes: 0 additions & 2 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions tests/test_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit dd6b93a

Please sign in to comment.