Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions press/api/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def signup(email: str, product: str | None = None, referrer: str | None = None)
{"email": email, "referrer_id": referrer, "product_trial": product},
"name",
)

if not account_request:
account_request_doc = frappe.get_doc(
{
Expand Down
3 changes: 2 additions & 1 deletion press/api/tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ class TestAccountApi(TestCase):
def tearDown(self):
frappe.db.rollback()

def _fake_signup(self, email: str = "[email protected]") -> Mock:
def _fake_signup(self, email: str | None = None) -> Mock:
"""Call press.api.account.signup without sending verification mail."""
email = email or "user@" + frappe.generate_hash(length=5) + ".com"
with patch.object(AccountRequest, "send_verification_email") as mock_send_email:
signup(email)
return mock_send_email
Expand Down
1 change: 1 addition & 0 deletions press/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@

fixtures = [
"Agent Job Type",
"Email Provider",
"Press Job Type",
"Frappe Version",
"MariaDB Variable",
Expand Down
22 changes: 20 additions & 2 deletions press/press/doctype/account_request/account_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from frappe.model.document import Document
from frappe.utils import get_url, random_string

from press.utils import get_country_info, is_valid_email_address, log_error
from press.decorators import settings
from press.utils import disposable_emails, get_country_info, is_valid_email_address, log_error
from press.utils.otp import generate_otp
from press.utils.telemetry import capture

Expand Down Expand Up @@ -111,9 +112,26 @@ def before_insert(self):
else:
self.is_us_eu = False

def validate(self):
def before_validate(self):
self.email = self.email.strip()

def validate(self):
self.disallow_disposable_emails()

@settings.enabled("disallow_disposable_emails")
def disallow_disposable_emails(self):
"""
Disallow temporary email providers for account requests. Throws
validation error if a temporary email provider is detected.
"""
if not self.email:
return
if disposable_emails.is_disposable(self.email):
frappe.throw(
"Temporary email providers are not allowed.",
frappe.ValidationError,
)

def after_insert(self):
# Telemetry: Only capture if it's not a saas signup or invited by parent team. Also don't capture if user already have a team
if not (
Expand Down
28 changes: 27 additions & 1 deletion press/press/doctype/account_request/test_account_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import random
from unittest.mock import patch

import frappe
Expand Down Expand Up @@ -41,4 +42,29 @@ def create_test_account_request(


class TestAccountRequest(FrappeTestCase):
pass
def test_bare(self):
account_request = frappe.get_doc(
{
"doctype": "Account Request",
"email": frappe.mock("email"),
}
)

self.assertIsNotNone(account_request.insert())

def test_temporary_email_provider(self):
frappe.db.set_value("Press Settings", "Press Settings", "disallow_disposable_emails", 1)
with patch("press.utils.disposable_emails.domains") as disposable_domains:
disposable_domains.return_value = [frappe.mock("domain_name")]
domains = disposable_domains()
domain = domains[random.randint(0, len(domains) - 1)]

account_request = frappe.get_doc(
{
"doctype": "Account Request",
"email": "hello@" + domain,
}
)

with self.assertRaises(frappe.ValidationError):
account_request.insert()
9 changes: 8 additions & 1 deletion press/press/doctype/press_settings/press_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
"enable_email_pre_verification",
"execute_incident_action",
"enable_server_snapshot_recovery",
"disallow_disposable_emails",
"section_break_jstu",
"enable_app_grouping",
"default_apps",
Expand Down Expand Up @@ -1486,11 +1487,17 @@
"fieldname": "set_redis_password",
"fieldtype": "Check",
"label": "Set Redis Password"
},
{
"default": "1",
"fieldname": "disallow_disposable_emails",
"fieldtype": "Check",
"label": "Disallow disposable emails"
}
],
"issingle": 1,
"links": [],
"modified": "2025-11-03 10:28:59.626610",
"modified": "2025-11-10 11:17:06.839232",
"modified_by": "Administrator",
"module": "Press",
"name": "Press Settings",
Expand Down
1 change: 1 addition & 0 deletions press/press/doctype/press_settings/press_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class PressSettings(Document):
disable_auto_retry: DF.Check
disable_frappe_auth: DF.Check
disable_physical_backup: DF.Check
disallow_disposable_emails: DF.Check
docker_registry_namespace: DF.Data | None
docker_registry_password: DF.Data | None
docker_registry_url: DF.Data | None
Expand Down
Empty file added press/press_utils/__init__.py
Empty file.
Empty file.
37 changes: 37 additions & 0 deletions press/utils/disposable_emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import requests
from frappe.utils import caching


@caching.redis_cache(ttl=60 * 60)
def domains() -> list[str]:
"""
Retrieve a list of disposable email domains.

:return: A list of disposable email domains.
"""
uri = "https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt"
domains_response = requests.get(uri)
domains_str = domains_response.content.decode("utf-8")
return domains_str.splitlines()


@caching.redis_cache(ttl=60 * 60)
def is_disposable_provider(domain: str) -> bool:
"""
Determine if a given domain is a disposable email provider.

:param domain: The domain to check.
:return: True if the domain is disposable, False otherwise.
"""
return domain in domains()


def is_disposable(email: str) -> bool:
"""
Determine if an email address is from a disposable email provider.

:param email: The email address to check.
:return: True if the email is disposable, False otherwise.
"""
domain = email.split("@").pop()
return is_disposable_provider(domain)
Loading