From cbe2c26d84fb354ddd3da0b5c043e99d08764ecd Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Thu, 11 Jul 2024 15:58:45 +0530 Subject: [PATCH] Tests for personal and work email addresses --- funnel/models/email_address.py | 15 +++++++++++++ funnel/views/account.py | 39 ++++++++++++++++++++++++++++++++++ requirements/base.in | 1 + requirements/base.txt | 22 ++++++++++--------- requirements/dev.txt | 8 +++---- requirements/test.txt | 4 ++-- 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/funnel/models/email_address.py b/funnel/models/email_address.py index 6c31e6e6e..056fb95fd 100644 --- a/funnel/models/email_address.py +++ b/funnel/models/email_address.py @@ -5,11 +5,13 @@ import hashlib import unicodedata import warnings +from collections.abc import MutableMapping from datetime import datetime from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self, cast, overload import base58 import idna +from mxsniff import mxsniff from pyisemail import is_email from pyisemail.diagnosis import BaseDiagnosis from sqlalchemy import event, inspect @@ -408,6 +410,19 @@ def is_available_for(self, owner: Account | None) -> bool: return False return True + def is_public_provider(self, cache: MutableMapping | None = None) -> bool: + """ + Check if this email address is from a known public provider (Gmail, etc). + + This performs a DNS lookup for unknown domains. Providing a cache is highly + recommended. The cache object must implement the + :class:`~collections.abc.MutableMapping` API. + """ + if not self.domain: + raise ValueError("Can't lookup a removed email address.") + lookup = mxsniff(self.domain, cache=cache) + return lookup['public'] + @delivery_state.transition(None, delivery_state.SENT) def mark_sent(self) -> None: """Record fact of an email message being sent to this address.""" diff --git a/funnel/views/account.py b/funnel/views/account.py index 76758a351..fe1c5f02f 100644 --- a/funnel/views/account.py +++ b/funnel/views/account.py @@ -211,6 +211,45 @@ def user_not_likely_throwaway(obj: Account) -> bool: return obj.is_verified or bool(obj.phone) +@Account.features('has_work_email', cached_property=True) +def account_has_work_email(obj: Account) -> bool: + """Confirm the user has a work email address associated with their account.""" + if not obj.emails: + return False + # TODO: Provide cache + return any(not ae.email_address.is_public_provider() for ae in obj.emails) + + +@Account.features('has_personal_email', cached_property=True) +def account_has_personal_email(obj: Account) -> bool: + """Confirm the user has a personal email address associated with their account.""" + if not obj.emails: + return False + # TODO: Provide cache + return any(ae.email_address.is_public_provider() for ae in obj.emails) + + +@Account.features('may_need_to_add_email', cached_property=True) +def account_may_need_to_add_email(obj: Account) -> bool: + """Check if the user missing work or personal email addresses.""" + if not obj.emails: + return True + has_work_email = False + has_personal_email = False + for ae in obj.emails: + # TODO: Provide cache + is_public = ae.email_address.is_public_provider() + if is_public: + has_personal_email = True + if has_work_email: + return False + else: + has_work_email = True + if has_personal_email: + return False + return True + + _quoted_str_re = re.compile('"(.*?)"') _quoted_ua_re = re.compile(r'"(.*?)"\s*;\s*v\s*=\s*"(.*?)",?\s*') _fake_ua_re = re.compile( diff --git a/requirements/base.in b/requirements/base.in index 2f3ff33e4..8d58f19f9 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -37,6 +37,7 @@ lazy-loader linkify-it-py markdown-it-py mdit-py-plugins +mxsniff oauth2client passlib phonenumbers diff --git a/requirements/base.txt b/requirements/base.txt index dadf2052a..642c64bd9 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:5f90855f3a570ce53e6e6a8535075ec897910b30 +# SHA1:c6a3d6e9d18a2218ada8d272e4e27ac8855b5f0a # # This file is autogenerated by pip-compile-multi # To update, run: @@ -66,7 +66,7 @@ cachelib==0.9.0 # via flask-caching cachetools==5.3.3 # via premailer -certifi==2024.6.2 +certifi==2024.7.4 # via # httpcore # httpx @@ -138,7 +138,7 @@ flask-executor==1.0.0 # via -r requirements/base.in flask-flatpages==0.8.2 # via -r requirements/base.in -flask-mailman==1.1.0 +flask-mailman==1.1.1 # via -r requirements/base.in flask-migrate==4.0.7 # via @@ -269,7 +269,9 @@ multidict==6.0.5 # aiohttp # yarl mxsniff==0.3.5 - # via baseframe + # via + # -r requirements/base.in + # baseframe mypy-extensions==1.0.0 # via typing-inspect oauth2client==4.1.3 @@ -287,17 +289,17 @@ packaging==24.1 # marshmallow passlib==1.7.4 # via -r requirements/base.in -phonenumbers==8.13.39 +phonenumbers==8.13.40 # via -r requirements/base.in -playwright==1.44.0 +playwright==1.45.0 # via -r requirements/base.in premailer==3.10.0 # via -r requirements/base.in progressbar2==4.4.2 # via -r requirements/base.in -psycopg[binary]==3.1.19 +psycopg[binary]==3.2.1 # via -r requirements/base.in -psycopg-binary==3.1.19 +psycopg-binary==3.2.1 # via psycopg pyasn1==0.6.0 # via @@ -406,7 +408,7 @@ semantic-version==2.10.0 # via # baseframe # coaster -sentry-sdk==2.7.1 +sentry-sdk==2.9.0 # via baseframe six==1.16.0 # via @@ -449,7 +451,7 @@ tuspy==1.0.3 # via pyvimeo tweepy==4.14.0 # via -r requirements/base.in -twilio==9.2.2 +twilio==9.2.3 # via -r requirements/base.in types-python-dateutil==2.9.0.20240316 # via arrow diff --git a/requirements/dev.txt b/requirements/dev.txt index b5318e320..d2944fa16 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -32,7 +32,7 @@ gherkin-official==24.0.0 # via reformat-gherkin icdiff==2.0.7 # via pytest-icdiff -identify==2.5.36 +identify==2.6.0 # via pre-commit isort==5.13.2 # via pylint @@ -80,7 +80,7 @@ pyupgrade==3.16.0 # via -r requirements/dev.in reformat-gherkin==3.0.1 # via -r requirements/dev.in -ruff==0.5.0 +ruff==0.5.1 # via -r requirements/dev.in tokenize-rt==5.2.0 # via pyupgrade @@ -118,7 +118,7 @@ types-pillow==10.2.0.20240520 # via -r requirements/dev.in types-pyopenssl==24.1.0.20240425 # via types-redis -types-pytest-lazy-fixture==0.6.3.20240310 +types-pytest-lazy-fixture==0.6.3.20240707 # via -r requirements/dev.in types-pytz==2024.1.0.20240417 # via -r requirements/dev.in @@ -128,7 +128,7 @@ types-redis==4.6.0.20240425 # via -r requirements/dev.in types-requests==2.32.0.20240622 # via -r requirements/dev.in -types-setuptools==70.1.0.20240627 +types-setuptools==70.3.0.20240710 # via types-cffi types-zxcvbn==4.4.1.20240106 # via -r requirements/dev.in diff --git a/requirements/test.txt b/requirements/test.txt index e1de4ccc5..4c9c3f214 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -58,7 +58,7 @@ pytest-dotenv==0.5.2 # via -r requirements/test.in pytest-env==1.1.3 # via -r requirements/test.in -pytest-playwright==0.5.0 +pytest-playwright==0.5.1 # via -r requirements/test.in pytest-pretty==1.2.0 # via -r requirements/test.in @@ -78,7 +78,7 @@ sttable==0.0.1 # via -r requirements/test.in text-unidecode==1.3 # via python-slugify -tomlkit==0.12.5 +tomlkit==0.13.0 # via -r requirements/test.in typeguard==4.3.0 # via -r requirements/test.in