Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
75f45d9365 | |||
ead425e0c2 | |||
6c910d62c5 | |||
99ffd1ec0c | |||
eda940f8b2 | |||
1dad582523 | |||
e516266a27 | |||
850fc95477 | |||
d172825900 |
@ -74,7 +74,7 @@ Setting up DKIM is highly recommended to reduce the chance your emails ending up
|
|||||||
First you need to generate a private and public key for DKIM:
|
First you need to generate a private and public key for DKIM:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl genrsa -out dkim.key 1024
|
openssl genrsa -out dkim.key -traditional 1024
|
||||||
openssl rsa -in dkim.key -pubout -out dkim.pub.key
|
openssl rsa -in dkim.key -pubout -out dkim.pub.key
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -511,10 +511,13 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:7777;
|
proxy_pass http://localhost:7777;
|
||||||
|
proxy_set_header Host $host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: If `/etc/nginx/sites-enabled/default` exists, delete it or certbot will fail due to the conflict. The `simplelogin` file should be the only file in `sites-enabled`.
|
||||||
|
|
||||||
Reload Nginx with the command below
|
Reload Nginx with the command below
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -214,6 +214,20 @@ class UserAdmin(SLModelView):
|
|||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
|
@action(
|
||||||
|
"remove trial",
|
||||||
|
"Stop trial period",
|
||||||
|
"Remove trial for this user?",
|
||||||
|
)
|
||||||
|
def stop_trial(self, ids):
|
||||||
|
for user in User.filter(User.id.in_(ids)):
|
||||||
|
user.trial_end = None
|
||||||
|
|
||||||
|
flash(f"Stopped trial for {user}", "success")
|
||||||
|
AdminAuditLog.stop_trial(current_user.id, user.id)
|
||||||
|
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
"disable_otp_fido",
|
"disable_otp_fido",
|
||||||
"Disable OTP & FIDO",
|
"Disable OTP & FIDO",
|
||||||
|
@ -201,10 +201,10 @@ def get_alias_infos_with_pagination_v3(
|
|||||||
q = q.order_by(Alias.pinned.desc())
|
q = q.order_by(Alias.pinned.desc())
|
||||||
q = q.order_by(latest_activity.desc())
|
q = q.order_by(latest_activity.desc())
|
||||||
|
|
||||||
q = list(q.limit(page_limit).offset(page_id * page_size))
|
q = q.limit(page_limit).offset(page_id * page_size)
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q:
|
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in list(q):
|
||||||
ret.append(
|
ret.append(
|
||||||
AliasInfo(
|
AliasInfo(
|
||||||
alias=alias,
|
alias=alias,
|
||||||
@ -358,7 +358,6 @@ def construct_alias_query(user: User):
|
|||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
).label("nb_forward"),
|
).label("nb_forward"),
|
||||||
func.max(EmailLog.created_at).label("latest_email_log_created_at"),
|
|
||||||
)
|
)
|
||||||
.join(EmailLog, Alias.id == EmailLog.alias_id, isouter=True)
|
.join(EmailLog, Alias.id == EmailLog.alias_id, isouter=True)
|
||||||
.filter(Alias.user_id == user.id)
|
.filter(Alias.user_id == user.id)
|
||||||
@ -366,14 +365,6 @@ def construct_alias_query(user: User):
|
|||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
|
|
||||||
alias_contact_subquery = (
|
|
||||||
Session.query(Alias.id, func.max(Contact.id).label("max_contact_id"))
|
|
||||||
.join(Contact, Alias.id == Contact.alias_id, isouter=True)
|
|
||||||
.filter(Alias.user_id == user.id)
|
|
||||||
.group_by(Alias.id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Session.query(
|
Session.query(
|
||||||
Alias,
|
Alias,
|
||||||
@ -385,23 +376,7 @@ def construct_alias_query(user: User):
|
|||||||
)
|
)
|
||||||
.options(joinedload(Alias.hibp_breaches))
|
.options(joinedload(Alias.hibp_breaches))
|
||||||
.options(joinedload(Alias.custom_domain))
|
.options(joinedload(Alias.custom_domain))
|
||||||
.join(Contact, Alias.id == Contact.alias_id, isouter=True)
|
.join(EmailLog, Alias.last_email_log_id == EmailLog.id, isouter=True)
|
||||||
.join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
|
.join(Contact, EmailLog.contact_id == Contact.id, isouter=True)
|
||||||
.filter(Alias.id == alias_activity_subquery.c.id)
|
.filter(Alias.id == alias_activity_subquery.c.id)
|
||||||
.filter(Alias.id == alias_contact_subquery.c.id)
|
|
||||||
.filter(
|
|
||||||
or_(
|
|
||||||
EmailLog.created_at
|
|
||||||
== alias_activity_subquery.c.latest_email_log_created_at,
|
|
||||||
and_(
|
|
||||||
# no email log yet for this alias
|
|
||||||
alias_activity_subquery.c.latest_email_log_created_at.is_(None),
|
|
||||||
# to make sure only 1 contact is returned in this case
|
|
||||||
or_(
|
|
||||||
Contact.id == alias_contact_subquery.c.max_contact_id,
|
|
||||||
alias_contact_subquery.c.max_contact_id.is_(None),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
@ -31,6 +31,7 @@ from app.models import Alias, Contact, Mailbox, AliasMailbox
|
|||||||
@deprecated
|
@deprecated
|
||||||
@api_bp.route("/aliases", methods=["GET", "POST"])
|
@api_bp.route("/aliases", methods=["GET", "POST"])
|
||||||
@require_api_auth
|
@require_api_auth
|
||||||
|
@limiter.limit("10/minute", key_func=lambda: g.user.id)
|
||||||
def get_aliases():
|
def get_aliases():
|
||||||
"""
|
"""
|
||||||
Get aliases
|
Get aliases
|
||||||
@ -72,10 +73,8 @@ def get_aliases():
|
|||||||
|
|
||||||
|
|
||||||
@api_bp.route("/v2/aliases", methods=["GET", "POST"])
|
@api_bp.route("/v2/aliases", methods=["GET", "POST"])
|
||||||
@limiter.limit(
|
|
||||||
"5/minute",
|
|
||||||
)
|
|
||||||
@require_api_auth
|
@require_api_auth
|
||||||
|
@limiter.limit("50/minute", key_func=lambda: g.user.id)
|
||||||
def get_aliases_v2():
|
def get_aliases_v2():
|
||||||
"""
|
"""
|
||||||
Get aliases
|
Get aliases
|
||||||
|
@ -17,9 +17,14 @@ from app.models import PlanEnum, AppleSubscription
|
|||||||
_MONTHLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.monthly"
|
_MONTHLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.monthly"
|
||||||
_YEARLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.yearly"
|
_YEARLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.yearly"
|
||||||
|
|
||||||
|
# SL Mac app used to be in SL account
|
||||||
_MACAPP_MONTHLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.monthly"
|
_MACAPP_MONTHLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.monthly"
|
||||||
_MACAPP_YEARLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.yearly"
|
_MACAPP_YEARLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.yearly"
|
||||||
|
|
||||||
|
# SL Mac app is moved to Proton account
|
||||||
|
_MACAPP_MONTHLY_PRODUCT_ID_NEW = "me.proton.simplelogin.macos.premium.monthly"
|
||||||
|
_MACAPP_YEARLY_PRODUCT_ID_NEW = "me.proton.simplelogin.macos.premium.yearly"
|
||||||
|
|
||||||
# Apple API URL
|
# Apple API URL
|
||||||
_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
|
_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||||
_PROD_URL = "https://buy.itunes.apple.com/verifyReceipt"
|
_PROD_URL = "https://buy.itunes.apple.com/verifyReceipt"
|
||||||
@ -263,7 +268,11 @@ def apple_update_notification():
|
|||||||
plan = (
|
plan = (
|
||||||
PlanEnum.monthly
|
PlanEnum.monthly
|
||||||
if transaction["product_id"]
|
if transaction["product_id"]
|
||||||
in (_MONTHLY_PRODUCT_ID, _MACAPP_MONTHLY_PRODUCT_ID)
|
in (
|
||||||
|
_MONTHLY_PRODUCT_ID,
|
||||||
|
_MACAPP_MONTHLY_PRODUCT_ID,
|
||||||
|
_MACAPP_MONTHLY_PRODUCT_ID_NEW,
|
||||||
|
)
|
||||||
else PlanEnum.yearly
|
else PlanEnum.yearly
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -517,7 +526,11 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
|
|||||||
plan = (
|
plan = (
|
||||||
PlanEnum.monthly
|
PlanEnum.monthly
|
||||||
if latest_transaction["product_id"]
|
if latest_transaction["product_id"]
|
||||||
in (_MONTHLY_PRODUCT_ID, _MACAPP_MONTHLY_PRODUCT_ID)
|
in (
|
||||||
|
_MONTHLY_PRODUCT_ID,
|
||||||
|
_MACAPP_MONTHLY_PRODUCT_ID,
|
||||||
|
_MACAPP_MONTHLY_PRODUCT_ID_NEW,
|
||||||
|
)
|
||||||
else PlanEnum.yearly
|
else PlanEnum.yearly
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ def user_to_dict(user: User) -> dict:
|
|||||||
"in_trial": user.in_trial(),
|
"in_trial": user.in_trial(),
|
||||||
"max_alias_free_plan": user.max_alias_for_free_account(),
|
"max_alias_free_plan": user.max_alias_for_free_account(),
|
||||||
"connected_proton_address": None,
|
"connected_proton_address": None,
|
||||||
|
"can_create_reverse_alias": user.can_create_contacts(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.CONNECT_WITH_PROTON:
|
if config.CONNECT_WITH_PROTON:
|
||||||
@ -58,6 +59,7 @@ def user_info():
|
|||||||
- in_trial
|
- in_trial
|
||||||
- max_alias_free
|
- max_alias_free
|
||||||
- is_connected_with_proton
|
- is_connected_with_proton
|
||||||
|
- can_create_reverse_alias
|
||||||
"""
|
"""
|
||||||
user = g.user
|
user = g.user
|
||||||
|
|
||||||
|
@ -489,7 +489,34 @@ def setup_nameservers():
|
|||||||
|
|
||||||
NAMESERVERS = setup_nameservers()
|
NAMESERVERS = setup_nameservers()
|
||||||
|
|
||||||
DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = False
|
DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = os.environ.get(
|
||||||
|
"DISABLE_CREATE_CONTACTS_FOR_FREE_USERS", False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Expect format hits,seconds:hits,seconds...
|
||||||
|
# Example 1,10:4,60 means 1 in the last 10 secs or 4 in the last 60 secs
|
||||||
|
def getRateLimitFromConfig(
|
||||||
|
env_var: string, default: string = ""
|
||||||
|
) -> list[tuple[int, int]]:
|
||||||
|
value = os.environ.get(env_var, default)
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
entries = [entry for entry in value.split(":")]
|
||||||
|
limits = []
|
||||||
|
for entry in entries:
|
||||||
|
fields = entry.split(",")
|
||||||
|
limit = (int(fields[0]), int(fields[1]))
|
||||||
|
limits.append(limit)
|
||||||
|
return limits
|
||||||
|
|
||||||
|
|
||||||
|
ALIAS_CREATE_RATE_LIMIT_FREE = getRateLimitFromConfig(
|
||||||
|
"ALIAS_CREATE_RATE_LIMIT_FREE", "10,900:50,3600"
|
||||||
|
)
|
||||||
|
ALIAS_CREATE_RATE_LIMIT_PAID = getRateLimitFromConfig(
|
||||||
|
"ALIAS_CREATE_RATE_LIMIT_PAID", "50,900:200,3600"
|
||||||
|
)
|
||||||
PARTNER_API_TOKEN_SECRET = os.environ.get("PARTNER_API_TOKEN_SECRET") or (
|
PARTNER_API_TOKEN_SECRET = os.environ.get("PARTNER_API_TOKEN_SECRET") or (
|
||||||
FLASK_SECRET + "partnerapitoken"
|
FLASK_SECRET + "partnerapitoken"
|
||||||
)
|
)
|
||||||
|
@ -51,14 +51,6 @@ def email_validator():
|
|||||||
return _check
|
return _check
|
||||||
|
|
||||||
|
|
||||||
def user_can_create_contacts(user: User) -> bool:
|
|
||||||
if user.is_premium():
|
|
||||||
return True
|
|
||||||
if user.flags & User.FLAG_FREE_DISABLE_CREATE_ALIAS == 0:
|
|
||||||
return True
|
|
||||||
return not config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS
|
|
||||||
|
|
||||||
|
|
||||||
def create_contact(user: User, alias: Alias, contact_address: str) -> Contact:
|
def create_contact(user: User, alias: Alias, contact_address: str) -> Contact:
|
||||||
"""
|
"""
|
||||||
Create a contact for a user. Can be restricted for new free users by enabling DISABLE_CREATE_CONTACTS_FOR_FREE_USERS.
|
Create a contact for a user. Can be restricted for new free users by enabling DISABLE_CREATE_CONTACTS_FOR_FREE_USERS.
|
||||||
@ -82,7 +74,7 @@ def create_contact(user: User, alias: Alias, contact_address: str) -> Contact:
|
|||||||
if contact:
|
if contact:
|
||||||
raise ErrContactAlreadyExists(contact)
|
raise ErrContactAlreadyExists(contact)
|
||||||
|
|
||||||
if not user_can_create_contacts(user):
|
if not user.can_create_contacts():
|
||||||
raise ErrContactErrorUpgradeNeeded()
|
raise ErrContactErrorUpgradeNeeded()
|
||||||
|
|
||||||
contact = Contact.create(
|
contact = Contact.create(
|
||||||
@ -327,6 +319,6 @@ def alias_contact_manager(alias_id):
|
|||||||
last_page=last_page,
|
last_page=last_page,
|
||||||
query=query,
|
query=query,
|
||||||
nb_contact=nb_contact,
|
nb_contact=nb_contact,
|
||||||
can_create_contacts=user_can_create_contacts(current_user),
|
can_create_contacts=current_user.can_create_contacts(),
|
||||||
csrf_form=csrf_form,
|
csrf_form=csrf_form,
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,7 @@ from app.models import (
|
|||||||
AliasMailbox,
|
AliasMailbox,
|
||||||
DomainDeletedAlias,
|
DomainDeletedAlias,
|
||||||
)
|
)
|
||||||
|
from app.utils import CSRFValidationForm
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/custom_alias", methods=["GET", "POST"])
|
@dashboard_bp.route("/custom_alias", methods=["GET", "POST"])
|
||||||
@ -48,9 +49,13 @@ def custom_alias():
|
|||||||
at_least_a_premium_domain = True
|
at_least_a_premium_domain = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
csrf_form = CSRFValidationForm()
|
||||||
mailboxes = current_user.mailboxes()
|
mailboxes = current_user.mailboxes()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
alias_prefix = request.form.get("prefix").strip().lower().replace(" ", "")
|
alias_prefix = request.form.get("prefix").strip().lower().replace(" ", "")
|
||||||
signed_alias_suffix = request.form.get("signed-alias-suffix")
|
signed_alias_suffix = request.form.get("signed-alias-suffix")
|
||||||
mailbox_ids = request.form.getlist("mailboxes")
|
mailbox_ids = request.form.getlist("mailboxes")
|
||||||
@ -164,4 +169,5 @@ def custom_alias():
|
|||||||
alias_suffixes=alias_suffixes,
|
alias_suffixes=alias_suffixes,
|
||||||
at_least_a_premium_domain=at_least_a_premium_domain,
|
at_least_a_premium_domain=at_least_a_premium_domain,
|
||||||
mailboxes=mailboxes,
|
mailboxes=mailboxes,
|
||||||
|
csrf_form=csrf_form,
|
||||||
)
|
)
|
||||||
|
@ -52,16 +52,13 @@ def get_stats(user: User) -> Stats:
|
|||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/", methods=["GET", "POST"])
|
@dashboard_bp.route("/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
@limiter.limit(
|
@limiter.limit(
|
||||||
ALIAS_LIMIT,
|
ALIAS_LIMIT,
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
exempt_when=lambda: request.form.get("form-name") != "create-random-email",
|
exempt_when=lambda: request.form.get("form-name") != "create-random-email",
|
||||||
)
|
)
|
||||||
@limiter.limit(
|
@limiter.limit("10/minute", methods=["GET"], key_func=lambda: current_user.id)
|
||||||
"5/minute",
|
|
||||||
methods=["GET"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@parallel_limiter.lock(
|
@parallel_limiter.lock(
|
||||||
name="alias_creation",
|
name="alias_creation",
|
||||||
only_when=lambda: request.form.get("form-name") == "create-random-email",
|
only_when=lambda: request.form.get("form-name") == "create-random-email",
|
||||||
|
@ -583,6 +583,26 @@ def email_can_be_used_as_mailbox(email_address: str) -> bool:
|
|||||||
LOG.d("MX Domain %s %s is invalid mailbox domain", mx_domain, domain)
|
LOG.d("MX Domain %s %s is invalid mailbox domain", mx_domain, domain)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
existing_user = User.get_by(email=email_address)
|
||||||
|
if existing_user and existing_user.disabled:
|
||||||
|
LOG.d(
|
||||||
|
f"User {existing_user} is disabled. {email_address} cannot be used for other mailbox"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for existing_user in (
|
||||||
|
User.query()
|
||||||
|
.join(Mailbox, User.id == Mailbox.user_id)
|
||||||
|
.filter(Mailbox.email == email_address)
|
||||||
|
.group_by(User.id)
|
||||||
|
.all()
|
||||||
|
):
|
||||||
|
if existing_user.disabled:
|
||||||
|
LOG.d(
|
||||||
|
f"User {existing_user} is disabled and has a mailbox with {email_address}. Id cannot be used for other mailbox"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ def quarantine_dmarc_failed_forward_email(alias, contact, envelope, msg) -> Emai
|
|||||||
refused_email = RefusedEmail.create(
|
refused_email = RefusedEmail.create(
|
||||||
full_report_path=s3_report_path, user_id=alias.user_id, flush=True
|
full_report_path=s3_report_path, user_id=alias.user_id, flush=True
|
||||||
)
|
)
|
||||||
return EmailLog.create(
|
email_log = EmailLog.create(
|
||||||
user_id=alias.user_id,
|
user_id=alias.user_id,
|
||||||
mailbox_id=alias.mailbox_id,
|
mailbox_id=alias.mailbox_id,
|
||||||
contact_id=contact.id,
|
contact_id=contact.id,
|
||||||
@ -142,6 +142,7 @@ def quarantine_dmarc_failed_forward_email(alias, contact, envelope, msg) -> Emai
|
|||||||
blocked=True,
|
blocked=True,
|
||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
|
return email_log
|
||||||
|
|
||||||
|
|
||||||
def apply_dmarc_policy_for_reply_phase(
|
def apply_dmarc_policy_for_reply_phase(
|
||||||
|
@ -27,7 +27,7 @@ from sqlalchemy.orm import deferred
|
|||||||
from sqlalchemy.sql import and_
|
from sqlalchemy.sql import and_
|
||||||
from sqlalchemy_utils import ArrowType
|
from sqlalchemy_utils import ArrowType
|
||||||
|
|
||||||
from app import config
|
from app import config, rate_limiter
|
||||||
from app import s3
|
from app import s3
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.dns_utils import get_mx_domains
|
from app.dns_utils import get_mx_domains
|
||||||
@ -235,6 +235,7 @@ class AuditLogActionEnum(EnumE):
|
|||||||
download_provider_complaint = 8
|
download_provider_complaint = 8
|
||||||
disable_user = 9
|
disable_user = 9
|
||||||
enable_user = 10
|
enable_user = 10
|
||||||
|
stop_trial = 11
|
||||||
|
|
||||||
|
|
||||||
class Phase(EnumE):
|
class Phase(EnumE):
|
||||||
@ -907,7 +908,11 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
return sub
|
return sub
|
||||||
|
|
||||||
def verified_custom_domains(self) -> List["CustomDomain"]:
|
def verified_custom_domains(self) -> List["CustomDomain"]:
|
||||||
return CustomDomain.filter_by(user_id=self.id, ownership_verified=True).all()
|
return (
|
||||||
|
CustomDomain.filter_by(user_id=self.id, ownership_verified=True)
|
||||||
|
.order_by(CustomDomain.domain.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
def mailboxes(self) -> List["Mailbox"]:
|
def mailboxes(self) -> List["Mailbox"]:
|
||||||
"""list of mailbox that user own"""
|
"""list of mailbox that user own"""
|
||||||
@ -1113,6 +1118,13 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
|
|
||||||
return random_words(1)
|
return random_words(1)
|
||||||
|
|
||||||
|
def can_create_contacts(self) -> bool:
|
||||||
|
if self.is_premium():
|
||||||
|
return True
|
||||||
|
if self.flags & User.FLAG_FREE_DISABLE_CREATE_ALIAS == 0:
|
||||||
|
return True
|
||||||
|
return not config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<User {self.id} {self.name} {self.email}>"
|
return f"<User {self.id} {self.name} {self.email}>"
|
||||||
|
|
||||||
@ -1488,6 +1500,8 @@ class Alias(Base, ModelMixin):
|
|||||||
TSVector(), sa.Computed("to_tsvector('english', note)", persisted=True)
|
TSVector(), sa.Computed("to_tsvector('english', note)", persisted=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
last_email_log_id = sa.Column(sa.Integer, default=None, nullable=True)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("ix_video___ts_vector__", ts_vector, postgresql_using="gin"),
|
Index("ix_video___ts_vector__", ts_vector, postgresql_using="gin"),
|
||||||
# index on note column using pg_trgm
|
# index on note column using pg_trgm
|
||||||
@ -1506,6 +1520,7 @@ class Alias(Base, ModelMixin):
|
|||||||
def mailboxes(self):
|
def mailboxes(self):
|
||||||
ret = [self.mailbox]
|
ret = [self.mailbox]
|
||||||
for m in self._mailboxes:
|
for m in self._mailboxes:
|
||||||
|
if m.id is not self.mailbox.id:
|
||||||
ret.append(m)
|
ret.append(m)
|
||||||
|
|
||||||
ret = [mb for mb in ret if mb.verified]
|
ret = [mb for mb in ret if mb.verified]
|
||||||
@ -1555,6 +1570,15 @@ class Alias(Base, ModelMixin):
|
|||||||
flush = kw.pop("flush", False)
|
flush = kw.pop("flush", False)
|
||||||
|
|
||||||
new_alias = cls(**kw)
|
new_alias = cls(**kw)
|
||||||
|
user = User.get(new_alias.user_id)
|
||||||
|
if user.is_premium():
|
||||||
|
limits = config.ALIAS_CREATE_RATE_LIMIT_PAID
|
||||||
|
else:
|
||||||
|
limits = config.ALIAS_CREATE_RATE_LIMIT_FREE
|
||||||
|
# limits is array of (hits,days)
|
||||||
|
for limit in limits:
|
||||||
|
key = f"alias_create_{limit[1]}d:{user.id}"
|
||||||
|
rate_limiter.check_bucket_limit(key, limit[0], limit[1])
|
||||||
|
|
||||||
email = kw["email"]
|
email = kw["email"]
|
||||||
# make sure email is lowercase and doesn't have any whitespace
|
# make sure email is lowercase and doesn't have any whitespace
|
||||||
@ -2037,6 +2061,20 @@ class EmailLog(Base, ModelMixin):
|
|||||||
def get_dashboard_url(self):
|
def get_dashboard_url(self):
|
||||||
return f"{config.URL}/dashboard/refused_email?highlight_id={self.id}"
|
return f"{config.URL}/dashboard/refused_email?highlight_id={self.id}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, *args, **kwargs):
|
||||||
|
commit = kwargs.pop("commit", False)
|
||||||
|
email_log = super().create(*args, **kwargs)
|
||||||
|
Session.flush()
|
||||||
|
if "alias_id" in kwargs:
|
||||||
|
sql = "UPDATE alias SET last_email_log_id = :el_id WHERE id = :alias_id"
|
||||||
|
Session.execute(
|
||||||
|
sql, {"el_id": email_log.id, "alias_id": kwargs["alias_id"]}
|
||||||
|
)
|
||||||
|
if commit:
|
||||||
|
Session.commit()
|
||||||
|
return email_log
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<EmailLog {self.id}>"
|
return f"<EmailLog {self.id}>"
|
||||||
|
|
||||||
@ -3322,6 +3360,15 @@ class AdminAuditLog(Base):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stop_trial(cls, admin_user_id: int, user_id: int):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.stop_trial.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def disable_otp_fido(
|
def disable_otp_fido(
|
||||||
cls, admin_user_id: int, user_id: int, had_otp: bool, had_fido: bool
|
cls, admin_user_id: int, user_id: int, had_otp: bool, had_fido: bool
|
||||||
|
40
app/app/rate_limiter.py
Normal file
40
app/app/rate_limiter.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import newrelic.agent
|
||||||
|
import redis.exceptions
|
||||||
|
import werkzeug.exceptions
|
||||||
|
from limits.storage import RedisStorage
|
||||||
|
|
||||||
|
from app.log import LOG
|
||||||
|
|
||||||
|
lock_redis: Optional[RedisStorage] = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_redis_concurrent_lock(redis: RedisStorage):
|
||||||
|
global lock_redis
|
||||||
|
lock_redis = redis
|
||||||
|
|
||||||
|
|
||||||
|
def check_bucket_limit(
|
||||||
|
lock_name: Optional[str] = None,
|
||||||
|
max_hits: int = 5,
|
||||||
|
bucket_seconds: int = 3600,
|
||||||
|
):
|
||||||
|
# Calculate current bucket time
|
||||||
|
int_time = int(datetime.utcnow().timestamp())
|
||||||
|
bucket_id = int_time - (int_time % bucket_seconds)
|
||||||
|
bucket_lock_name = f"bl:{lock_name}:{bucket_id}"
|
||||||
|
if not lock_redis:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
value = lock_redis.incr(bucket_lock_name, bucket_seconds)
|
||||||
|
if value > max_hits:
|
||||||
|
LOG.i(f"Rate limit hit for {bucket_lock_name} -> {value}/{max_hits}")
|
||||||
|
newrelic.agent.record_custom_event(
|
||||||
|
"BucketRateLimit",
|
||||||
|
{"lock_name": lock_name, "bucket_seconds": bucket_seconds},
|
||||||
|
)
|
||||||
|
raise werkzeug.exceptions.TooManyRequests()
|
||||||
|
except (redis.exceptions.RedisError, AttributeError):
|
||||||
|
LOG.e("Cannot connect to redis")
|
@ -2,6 +2,7 @@ import flask
|
|||||||
import limits.storage
|
import limits.storage
|
||||||
|
|
||||||
from app.parallel_limiter import set_redis_concurrent_lock
|
from app.parallel_limiter import set_redis_concurrent_lock
|
||||||
|
from app.rate_limiter import set_redis_concurrent_lock as rate_limit_set_redis
|
||||||
from app.session import RedisSessionStore
|
from app.session import RedisSessionStore
|
||||||
|
|
||||||
|
|
||||||
@ -10,12 +11,14 @@ def initialize_redis_services(app: flask.Flask, redis_url: str):
|
|||||||
storage = limits.storage.RedisStorage(redis_url)
|
storage = limits.storage.RedisStorage(redis_url)
|
||||||
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
|
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
|
||||||
set_redis_concurrent_lock(storage)
|
set_redis_concurrent_lock(storage)
|
||||||
|
rate_limit_set_redis(storage)
|
||||||
elif redis_url.startswith("redis+sentinel://"):
|
elif redis_url.startswith("redis+sentinel://"):
|
||||||
storage = limits.storage.RedisSentinelStorage(redis_url)
|
storage = limits.storage.RedisSentinelStorage(redis_url)
|
||||||
app.session_interface = RedisSessionStore(
|
app.session_interface = RedisSessionStore(
|
||||||
storage.storage, storage.storage_slave, app
|
storage.storage, storage.storage_slave, app
|
||||||
)
|
)
|
||||||
set_redis_concurrent_lock(storage)
|
set_redis_concurrent_lock(storage)
|
||||||
|
rate_limit_set_redis(storage)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Tried to set_redis_session with an invalid redis url: ${redis_url}"
|
f"Tried to set_redis_session with an invalid redis url: ${redis_url}"
|
||||||
|
@ -49,11 +49,11 @@ def random_string(length=10, include_digits=False):
|
|||||||
|
|
||||||
def convert_to_id(s: str):
|
def convert_to_id(s: str):
|
||||||
"""convert a string to id-like: remove space, remove special accent"""
|
"""convert a string to id-like: remove space, remove special accent"""
|
||||||
s = s.replace(" ", "")
|
|
||||||
s = s.lower()
|
s = s.lower()
|
||||||
s = unidecode(s)
|
s = unidecode(s)
|
||||||
|
s = s.replace(" ", "")
|
||||||
|
|
||||||
return s
|
return s[:256]
|
||||||
|
|
||||||
|
|
||||||
_ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-."
|
_ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-."
|
||||||
|
@ -388,7 +388,7 @@ Input:
|
|||||||
- (Optional but recommended) `hostname` passed in query string
|
- (Optional but recommended) `hostname` passed in query string
|
||||||
- Request Message Body in json (`Content-Type` is `application/json`)
|
- Request Message Body in json (`Content-Type` is `application/json`)
|
||||||
- alias_prefix: string. The first part of the alias that user can choose.
|
- alias_prefix: string. The first part of the alias that user can choose.
|
||||||
- signed_suffix: should be one of the suffixes returned in the `GET /api/v4/alias/options` endpoint.
|
- signed_suffix: should be one of the suffixes returned in the `GET /api/v5/alias/options` endpoint.
|
||||||
- mailbox_ids: list of mailbox_id that "owns" this alias
|
- mailbox_ids: list of mailbox_id that "owns" this alias
|
||||||
- (Optional) note: alias note
|
- (Optional) note: alias note
|
||||||
- (Optional) name: alias name
|
- (Optional) name: alias name
|
||||||
|
@ -192,7 +192,6 @@ amigos
|
|||||||
amines
|
amines
|
||||||
amnion
|
amnion
|
||||||
amoeba
|
amoeba
|
||||||
amoral
|
|
||||||
amount
|
amount
|
||||||
amours
|
amours
|
||||||
ampere
|
ampere
|
||||||
@ -215,7 +214,6 @@ animus
|
|||||||
anions
|
anions
|
||||||
ankles
|
ankles
|
||||||
anklet
|
anklet
|
||||||
annals
|
|
||||||
anneal
|
anneal
|
||||||
annoys
|
annoys
|
||||||
annual
|
annual
|
||||||
@ -364,7 +362,6 @@ auntie
|
|||||||
aureus
|
aureus
|
||||||
aurora
|
aurora
|
||||||
author
|
author
|
||||||
autism
|
|
||||||
autumn
|
autumn
|
||||||
avails
|
avails
|
||||||
avatar
|
avatar
|
||||||
@ -638,14 +635,12 @@ bigwig
|
|||||||
bijoux
|
bijoux
|
||||||
bikers
|
bikers
|
||||||
biking
|
biking
|
||||||
bikini
|
|
||||||
bilges
|
bilges
|
||||||
bilked
|
bilked
|
||||||
bilker
|
bilker
|
||||||
billed
|
billed
|
||||||
billet
|
billet
|
||||||
billow
|
billow
|
||||||
bimbos
|
|
||||||
binary
|
binary
|
||||||
binder
|
binder
|
||||||
binged
|
binged
|
||||||
@ -710,8 +705,6 @@ blocks
|
|||||||
blokes
|
blokes
|
||||||
blonde
|
blonde
|
||||||
blonds
|
blonds
|
||||||
bloods
|
|
||||||
bloody
|
|
||||||
blooms
|
blooms
|
||||||
bloops
|
bloops
|
||||||
blotch
|
blotch
|
||||||
@ -817,8 +810,6 @@ bounds
|
|||||||
bounty
|
bounty
|
||||||
bovine
|
bovine
|
||||||
bovver
|
bovver
|
||||||
bowels
|
|
||||||
bowers
|
|
||||||
bowing
|
bowing
|
||||||
bowled
|
bowled
|
||||||
bowleg
|
bowleg
|
||||||
@ -827,10 +818,8 @@ bowman
|
|||||||
bowmen
|
bowmen
|
||||||
bowwow
|
bowwow
|
||||||
boxcar
|
boxcar
|
||||||
boxers
|
|
||||||
boxier
|
boxier
|
||||||
boxing
|
boxing
|
||||||
boyish
|
|
||||||
braced
|
braced
|
||||||
bracer
|
bracer
|
||||||
braces
|
braces
|
||||||
@ -861,7 +850,6 @@ breach
|
|||||||
breads
|
breads
|
||||||
breaks
|
breaks
|
||||||
breams
|
breams
|
||||||
breast
|
|
||||||
breath
|
breath
|
||||||
breech
|
breech
|
||||||
breeds
|
breeds
|
||||||
@ -872,9 +860,6 @@ brevet
|
|||||||
brewed
|
brewed
|
||||||
brewer
|
brewer
|
||||||
briars
|
briars
|
||||||
bribed
|
|
||||||
briber
|
|
||||||
bribes
|
|
||||||
bricks
|
bricks
|
||||||
bridal
|
bridal
|
||||||
brides
|
brides
|
||||||
@ -926,13 +911,7 @@ buffed
|
|||||||
buffer
|
buffer
|
||||||
buffet
|
buffet
|
||||||
bugged
|
bugged
|
||||||
bugger
|
|
||||||
bugled
|
|
||||||
bugler
|
|
||||||
bugles
|
|
||||||
builds
|
builds
|
||||||
bulged
|
|
||||||
bulges
|
|
||||||
bulked
|
bulked
|
||||||
bulled
|
bulled
|
||||||
bullet
|
bullet
|
||||||
@ -1340,8 +1319,6 @@ clingy
|
|||||||
clinic
|
clinic
|
||||||
clinks
|
clinks
|
||||||
clique
|
clique
|
||||||
cloaca
|
|
||||||
cloaks
|
|
||||||
cloche
|
cloche
|
||||||
clocks
|
clocks
|
||||||
clomps
|
clomps
|
||||||
@ -1448,7 +1425,6 @@ comply
|
|||||||
compos
|
compos
|
||||||
conchs
|
conchs
|
||||||
concur
|
concur
|
||||||
condom
|
|
||||||
condor
|
condor
|
||||||
condos
|
condos
|
||||||
coneys
|
coneys
|
||||||
@ -1568,8 +1544,6 @@ cranes
|
|||||||
cranks
|
cranks
|
||||||
cranky
|
cranky
|
||||||
cranny
|
cranny
|
||||||
crapes
|
|
||||||
crappy
|
|
||||||
crated
|
crated
|
||||||
crater
|
crater
|
||||||
crates
|
crates
|
||||||
@ -1585,7 +1559,6 @@ crazes
|
|||||||
creaks
|
creaks
|
||||||
creaky
|
creaky
|
||||||
creams
|
creams
|
||||||
creamy
|
|
||||||
crease
|
crease
|
||||||
create
|
create
|
||||||
creche
|
creche
|
||||||
@ -1594,8 +1567,6 @@ credos
|
|||||||
creeds
|
creeds
|
||||||
creeks
|
creeks
|
||||||
creels
|
creels
|
||||||
creeps
|
|
||||||
creepy
|
|
||||||
cremes
|
cremes
|
||||||
creole
|
creole
|
||||||
crepes
|
crepes
|
||||||
@ -1728,9 +1699,6 @@ dainty
|
|||||||
daises
|
daises
|
||||||
damage
|
damage
|
||||||
damask
|
damask
|
||||||
dammed
|
|
||||||
dammit
|
|
||||||
damned
|
|
||||||
damped
|
damped
|
||||||
dampen
|
dampen
|
||||||
damper
|
damper
|
||||||
@ -1754,7 +1722,6 @@ darers
|
|||||||
daring
|
daring
|
||||||
darken
|
darken
|
||||||
darker
|
darker
|
||||||
darkie
|
|
||||||
darkly
|
darkly
|
||||||
darned
|
darned
|
||||||
darner
|
darner
|
||||||
@ -1763,8 +1730,6 @@ darter
|
|||||||
dashed
|
dashed
|
||||||
dasher
|
dasher
|
||||||
dashes
|
dashes
|
||||||
daters
|
|
||||||
dating
|
|
||||||
dative
|
dative
|
||||||
daubed
|
daubed
|
||||||
dauber
|
dauber
|
||||||
@ -1921,7 +1886,6 @@ dharma
|
|||||||
dhotis
|
dhotis
|
||||||
diadem
|
diadem
|
||||||
dialog
|
dialog
|
||||||
diaper
|
|
||||||
diatom
|
diatom
|
||||||
dibble
|
dibble
|
||||||
dicier
|
dicier
|
||||||
@ -1943,7 +1907,6 @@ digits
|
|||||||
diking
|
diking
|
||||||
diktat
|
diktat
|
||||||
dilate
|
dilate
|
||||||
dildos
|
|
||||||
dilute
|
dilute
|
||||||
dimity
|
dimity
|
||||||
dimmed
|
dimmed
|
||||||
@ -2058,7 +2021,6 @@ dotted
|
|||||||
double
|
double
|
||||||
doubly
|
doubly
|
||||||
doubts
|
doubts
|
||||||
douche
|
|
||||||
doughy
|
doughy
|
||||||
dourer
|
dourer
|
||||||
dourly
|
dourly
|
||||||
@ -2139,15 +2101,6 @@ duenna
|
|||||||
duffed
|
duffed
|
||||||
duffer
|
duffer
|
||||||
dugout
|
dugout
|
||||||
dulcet
|
|
||||||
dulled
|
|
||||||
duller
|
|
||||||
dumber
|
|
||||||
dumbly
|
|
||||||
dumbos
|
|
||||||
dumdum
|
|
||||||
dumped
|
|
||||||
dumper
|
|
||||||
dunces
|
dunces
|
||||||
dunged
|
dunged
|
||||||
dunked
|
dunked
|
||||||
@ -2285,7 +2238,6 @@ endows
|
|||||||
endued
|
endued
|
||||||
endues
|
endues
|
||||||
endure
|
endure
|
||||||
enemas
|
|
||||||
energy
|
energy
|
||||||
enfold
|
enfold
|
||||||
engage
|
engage
|
||||||
@ -2333,7 +2285,6 @@ erects
|
|||||||
ermine
|
ermine
|
||||||
eroded
|
eroded
|
||||||
erodes
|
erodes
|
||||||
erotic
|
|
||||||
errand
|
errand
|
||||||
errant
|
errant
|
||||||
errata
|
errata
|
||||||
@ -2344,7 +2295,6 @@ eructs
|
|||||||
erupts
|
erupts
|
||||||
escape
|
escape
|
||||||
eschew
|
eschew
|
||||||
escort
|
|
||||||
escrow
|
escrow
|
||||||
escudo
|
escudo
|
||||||
espied
|
espied
|
||||||
@ -2363,7 +2313,6 @@ ethnic
|
|||||||
etudes
|
etudes
|
||||||
euchre
|
euchre
|
||||||
eulogy
|
eulogy
|
||||||
eunuch
|
|
||||||
eureka
|
eureka
|
||||||
evaded
|
evaded
|
||||||
evader
|
evader
|
||||||
@ -2392,7 +2341,6 @@ exempt
|
|||||||
exerts
|
exerts
|
||||||
exeunt
|
exeunt
|
||||||
exhale
|
exhale
|
||||||
exhort
|
|
||||||
exhume
|
exhume
|
||||||
exiled
|
exiled
|
||||||
exiles
|
exiles
|
||||||
@ -2415,7 +2363,6 @@ extant
|
|||||||
extend
|
extend
|
||||||
extent
|
extent
|
||||||
extols
|
extols
|
||||||
extort
|
|
||||||
extras
|
extras
|
||||||
exuded
|
exuded
|
||||||
exudes
|
exudes
|
||||||
@ -2440,7 +2387,6 @@ faeces
|
|||||||
faerie
|
faerie
|
||||||
faffed
|
faffed
|
||||||
fagged
|
fagged
|
||||||
faggot
|
|
||||||
failed
|
failed
|
||||||
faille
|
faille
|
||||||
fainer
|
fainer
|
||||||
@ -2473,18 +2419,10 @@ faring
|
|||||||
farmed
|
farmed
|
||||||
farmer
|
farmer
|
||||||
farrow
|
farrow
|
||||||
farted
|
|
||||||
fascia
|
fascia
|
||||||
fasted
|
fasted
|
||||||
fasten
|
fasten
|
||||||
faster
|
faster
|
||||||
father
|
|
||||||
fathom
|
|
||||||
fating
|
|
||||||
fatsos
|
|
||||||
fatten
|
|
||||||
fatter
|
|
||||||
fatwas
|
|
||||||
faucet
|
faucet
|
||||||
faults
|
faults
|
||||||
faulty
|
faulty
|
||||||
@ -2532,7 +2470,6 @@ fesses
|
|||||||
festal
|
festal
|
||||||
fester
|
fester
|
||||||
feting
|
feting
|
||||||
fetish
|
|
||||||
fetter
|
fetter
|
||||||
fettle
|
fettle
|
||||||
feudal
|
feudal
|
||||||
@ -2617,9 +2554,7 @@ flaked
|
|||||||
flakes
|
flakes
|
||||||
flambe
|
flambe
|
||||||
flamed
|
flamed
|
||||||
flamer
|
|
||||||
flames
|
flames
|
||||||
flange
|
|
||||||
flanks
|
flanks
|
||||||
flared
|
flared
|
||||||
flares
|
flares
|
||||||
@ -2754,8 +2689,6 @@ franks
|
|||||||
frappe
|
frappe
|
||||||
frauds
|
frauds
|
||||||
frayed
|
frayed
|
||||||
freaks
|
|
||||||
freaky
|
|
||||||
freely
|
freely
|
||||||
freest
|
freest
|
||||||
freeze
|
freeze
|
||||||
@ -2795,8 +2728,6 @@ fryers
|
|||||||
frying
|
frying
|
||||||
ftpers
|
ftpers
|
||||||
ftping
|
ftping
|
||||||
fucked
|
|
||||||
fucker
|
|
||||||
fuddle
|
fuddle
|
||||||
fudged
|
fudged
|
||||||
fudges
|
fudges
|
||||||
@ -2891,10 +2822,7 @@ gasbag
|
|||||||
gashed
|
gashed
|
||||||
gashes
|
gashes
|
||||||
gasket
|
gasket
|
||||||
gasman
|
|
||||||
gasmen
|
|
||||||
gasped
|
gasped
|
||||||
gassed
|
|
||||||
gasses
|
gasses
|
||||||
gateau
|
gateau
|
||||||
gather
|
gather
|
||||||
@ -3104,7 +3032,6 @@ grimed
|
|||||||
grimes
|
grimes
|
||||||
grimly
|
grimly
|
||||||
grinds
|
grinds
|
||||||
gringo
|
|
||||||
griped
|
griped
|
||||||
griper
|
griper
|
||||||
gripes
|
gripes
|
||||||
@ -3186,8 +3113,6 @@ gypsum
|
|||||||
gyrate
|
gyrate
|
||||||
gyving
|
gyving
|
||||||
habits
|
habits
|
||||||
hacked
|
|
||||||
hacker
|
|
||||||
hackle
|
hackle
|
||||||
hadith
|
hadith
|
||||||
haggis
|
haggis
|
||||||
@ -3195,8 +3120,6 @@ haggle
|
|||||||
hailed
|
hailed
|
||||||
hairdo
|
hairdo
|
||||||
haired
|
haired
|
||||||
hajjes
|
|
||||||
hajjis
|
|
||||||
halest
|
halest
|
||||||
haling
|
haling
|
||||||
halite
|
halite
|
||||||
@ -3223,11 +3146,8 @@ happen
|
|||||||
haptic
|
haptic
|
||||||
harass
|
harass
|
||||||
harden
|
harden
|
||||||
harder
|
|
||||||
hardly
|
|
||||||
harems
|
harems
|
||||||
haring
|
haring
|
||||||
harked
|
|
||||||
harlot
|
harlot
|
||||||
harmed
|
harmed
|
||||||
harped
|
harped
|
||||||
@ -3407,7 +3327,6 @@ hoofed
|
|||||||
hoofer
|
hoofer
|
||||||
hookah
|
hookah
|
||||||
hooked
|
hooked
|
||||||
hooker
|
|
||||||
hookup
|
hookup
|
||||||
hooped
|
hooped
|
||||||
hoopla
|
hoopla
|
||||||
@ -3459,8 +3378,6 @@ huffed
|
|||||||
hugely
|
hugely
|
||||||
hugest
|
hugest
|
||||||
hugged
|
hugged
|
||||||
hulled
|
|
||||||
huller
|
|
||||||
humane
|
humane
|
||||||
humans
|
humans
|
||||||
humble
|
humble
|
||||||
@ -3667,8 +3584,6 @@ jacket
|
|||||||
jading
|
jading
|
||||||
jagged
|
jagged
|
||||||
jaguar
|
jaguar
|
||||||
jailed
|
|
||||||
jailer
|
|
||||||
jalopy
|
jalopy
|
||||||
jammed
|
jammed
|
||||||
jangle
|
jangle
|
||||||
@ -3689,8 +3604,6 @@ jejune
|
|||||||
jelled
|
jelled
|
||||||
jellos
|
jellos
|
||||||
jennet
|
jennet
|
||||||
jerked
|
|
||||||
jerkin
|
|
||||||
jersey
|
jersey
|
||||||
jested
|
jested
|
||||||
jester
|
jester
|
||||||
@ -3814,11 +3727,7 @@ kidded
|
|||||||
kidder
|
kidder
|
||||||
kiddie
|
kiddie
|
||||||
kiddos
|
kiddos
|
||||||
kidnap
|
|
||||||
kidney
|
kidney
|
||||||
killed
|
|
||||||
killer
|
|
||||||
kilned
|
|
||||||
kilted
|
kilted
|
||||||
kilter
|
kilter
|
||||||
kimono
|
kimono
|
||||||
@ -3827,15 +3736,11 @@ kinder
|
|||||||
kindle
|
kindle
|
||||||
kindly
|
kindly
|
||||||
kingly
|
kingly
|
||||||
kinked
|
|
||||||
kiosks
|
kiosks
|
||||||
kipped
|
kipped
|
||||||
kipper
|
kipper
|
||||||
kirsch
|
kirsch
|
||||||
kismet
|
kismet
|
||||||
kissed
|
|
||||||
kisser
|
|
||||||
kisses
|
|
||||||
kiting
|
kiting
|
||||||
kitsch
|
kitsch
|
||||||
kitted
|
kitted
|
||||||
@ -3847,10 +3752,6 @@ kluges
|
|||||||
klutzy
|
klutzy
|
||||||
knacks
|
knacks
|
||||||
knaves
|
knaves
|
||||||
kneads
|
|
||||||
kneels
|
|
||||||
knells
|
|
||||||
knifed
|
|
||||||
knifes
|
knifes
|
||||||
knight
|
knight
|
||||||
knives
|
knives
|
||||||
@ -4210,8 +4111,6 @@ lunges
|
|||||||
lupine
|
lupine
|
||||||
lupins
|
lupins
|
||||||
luring
|
luring
|
||||||
lurked
|
|
||||||
lurker
|
|
||||||
lusher
|
lusher
|
||||||
lushes
|
lushes
|
||||||
lushly
|
lushly
|
||||||
@ -4608,7 +4507,6 @@ muggle
|
|||||||
mukluk
|
mukluk
|
||||||
mulcts
|
mulcts
|
||||||
mulish
|
mulish
|
||||||
mullah
|
|
||||||
mulled
|
mulled
|
||||||
mullet
|
mullet
|
||||||
mumble
|
mumble
|
||||||
@ -4721,9 +4619,6 @@ nickel
|
|||||||
nicker
|
nicker
|
||||||
nickle
|
nickle
|
||||||
nieces
|
nieces
|
||||||
niggas
|
|
||||||
niggaz
|
|
||||||
nigger
|
|
||||||
niggle
|
niggle
|
||||||
nigher
|
nigher
|
||||||
nights
|
nights
|
||||||
@ -4736,7 +4631,6 @@ ninjas
|
|||||||
ninths
|
ninths
|
||||||
nipped
|
nipped
|
||||||
nipper
|
nipper
|
||||||
nipple
|
|
||||||
nitric
|
nitric
|
||||||
nitwit
|
nitwit
|
||||||
nixing
|
nixing
|
||||||
@ -4781,15 +4675,6 @@ nozzle
|
|||||||
nuance
|
nuance
|
||||||
nubbin
|
nubbin
|
||||||
nubile
|
nubile
|
||||||
nuclei
|
|
||||||
nudest
|
|
||||||
nudged
|
|
||||||
nudges
|
|
||||||
nudism
|
|
||||||
nudist
|
|
||||||
nudity
|
|
||||||
nugget
|
|
||||||
nuking
|
|
||||||
numbed
|
numbed
|
||||||
number
|
number
|
||||||
numbly
|
numbly
|
||||||
@ -4804,7 +4689,6 @@ nutter
|
|||||||
nuzzle
|
nuzzle
|
||||||
nybble
|
nybble
|
||||||
nylons
|
nylons
|
||||||
nympho
|
|
||||||
nymphs
|
nymphs
|
||||||
oafish
|
oafish
|
||||||
oaring
|
oaring
|
||||||
@ -4885,7 +4769,6 @@ opting
|
|||||||
option
|
option
|
||||||
opuses
|
opuses
|
||||||
oracle
|
oracle
|
||||||
orally
|
|
||||||
orange
|
orange
|
||||||
orated
|
orated
|
||||||
orates
|
orates
|
||||||
@ -4897,7 +4780,6 @@ ordeal
|
|||||||
orders
|
orders
|
||||||
ordure
|
ordure
|
||||||
organs
|
organs
|
||||||
orgasm
|
|
||||||
orgies
|
orgies
|
||||||
oriels
|
oriels
|
||||||
orient
|
orient
|
||||||
@ -4993,10 +4875,6 @@ pander
|
|||||||
panels
|
panels
|
||||||
panics
|
panics
|
||||||
panned
|
panned
|
||||||
panted
|
|
||||||
pantie
|
|
||||||
pantos
|
|
||||||
pantry
|
|
||||||
papacy
|
papacy
|
||||||
papaya
|
papaya
|
||||||
papers
|
papers
|
||||||
@ -5078,7 +4956,6 @@ pebble
|
|||||||
pebbly
|
pebbly
|
||||||
pecans
|
pecans
|
||||||
pecked
|
pecked
|
||||||
pecker
|
|
||||||
pectic
|
pectic
|
||||||
pectin
|
pectin
|
||||||
pedalo
|
pedalo
|
||||||
@ -5151,9 +5028,6 @@ phenom
|
|||||||
phials
|
phials
|
||||||
phlegm
|
phlegm
|
||||||
phloem
|
phloem
|
||||||
phobia
|
|
||||||
phobic
|
|
||||||
phoebe
|
|
||||||
phoned
|
phoned
|
||||||
phones
|
phones
|
||||||
phoney
|
phoney
|
||||||
@ -5228,9 +5102,6 @@ piques
|
|||||||
piracy
|
piracy
|
||||||
pirate
|
pirate
|
||||||
pirogi
|
pirogi
|
||||||
pissed
|
|
||||||
pisser
|
|
||||||
pisses
|
|
||||||
pistes
|
pistes
|
||||||
pistil
|
pistil
|
||||||
pistol
|
pistol
|
||||||
@ -5311,8 +5182,6 @@ pogrom
|
|||||||
points
|
points
|
||||||
pointy
|
pointy
|
||||||
poised
|
poised
|
||||||
poises
|
|
||||||
poison
|
|
||||||
pokers
|
pokers
|
||||||
pokeys
|
pokeys
|
||||||
pokier
|
pokier
|
||||||
@ -5422,7 +5291,6 @@ preyed
|
|||||||
priced
|
priced
|
||||||
prices
|
prices
|
||||||
pricey
|
pricey
|
||||||
pricks
|
|
||||||
prided
|
prided
|
||||||
prides
|
prides
|
||||||
priers
|
priers
|
||||||
@ -5602,14 +5470,9 @@ rabbit
|
|||||||
rabble
|
rabble
|
||||||
rabies
|
rabies
|
||||||
raceme
|
raceme
|
||||||
racers
|
|
||||||
racial
|
|
||||||
racier
|
racier
|
||||||
racily
|
racily
|
||||||
racing
|
racing
|
||||||
racism
|
|
||||||
racist
|
|
||||||
racked
|
|
||||||
racket
|
racket
|
||||||
radars
|
radars
|
||||||
radial
|
radial
|
||||||
@ -5661,8 +5524,6 @@ rapers
|
|||||||
rapids
|
rapids
|
||||||
rapier
|
rapier
|
||||||
rapine
|
rapine
|
||||||
raping
|
|
||||||
rapist
|
|
||||||
rapped
|
rapped
|
||||||
rappel
|
rappel
|
||||||
rapper
|
rapper
|
||||||
@ -5747,7 +5608,6 @@ recoup
|
|||||||
rectal
|
rectal
|
||||||
rector
|
rector
|
||||||
rectos
|
rectos
|
||||||
rectum
|
|
||||||
recurs
|
recurs
|
||||||
recuse
|
recuse
|
||||||
redact
|
redact
|
||||||
@ -5891,7 +5751,6 @@ resume
|
|||||||
retail
|
retail
|
||||||
retain
|
retain
|
||||||
retake
|
retake
|
||||||
retard
|
|
||||||
retell
|
retell
|
||||||
retest
|
retest
|
||||||
retied
|
retied
|
||||||
@ -6125,8 +5984,6 @@ sadden
|
|||||||
sadder
|
sadder
|
||||||
saddle
|
saddle
|
||||||
sadhus
|
sadhus
|
||||||
sadism
|
|
||||||
sadist
|
|
||||||
safari
|
safari
|
||||||
safely
|
safely
|
||||||
safest
|
safest
|
||||||
@ -6364,16 +6221,6 @@ severs
|
|||||||
sewage
|
sewage
|
||||||
sewers
|
sewers
|
||||||
sewing
|
sewing
|
||||||
sexier
|
|
||||||
sexily
|
|
||||||
sexing
|
|
||||||
sexism
|
|
||||||
sexist
|
|
||||||
sexpot
|
|
||||||
sextet
|
|
||||||
sexton
|
|
||||||
sexual
|
|
||||||
shabby
|
|
||||||
shacks
|
shacks
|
||||||
shaded
|
shaded
|
||||||
shades
|
shades
|
||||||
@ -6383,10 +6230,7 @@ shaggy
|
|||||||
shaken
|
shaken
|
||||||
shaker
|
shaker
|
||||||
shakes
|
shakes
|
||||||
shalom
|
|
||||||
shaman
|
shaman
|
||||||
shamed
|
|
||||||
shames
|
|
||||||
shandy
|
shandy
|
||||||
shanks
|
shanks
|
||||||
shanty
|
shanty
|
||||||
@ -6432,7 +6276,6 @@ shirks
|
|||||||
shirrs
|
shirrs
|
||||||
shirts
|
shirts
|
||||||
shirty
|
shirty
|
||||||
shitty
|
|
||||||
shiver
|
shiver
|
||||||
shoals
|
shoals
|
||||||
shoats
|
shoats
|
||||||
@ -6575,9 +6418,6 @@ slangy
|
|||||||
slants
|
slants
|
||||||
slated
|
slated
|
||||||
slates
|
slates
|
||||||
slaved
|
|
||||||
slaver
|
|
||||||
slaves
|
|
||||||
slayed
|
slayed
|
||||||
slayer
|
slayer
|
||||||
sleaze
|
sleaze
|
||||||
@ -6672,7 +6512,6 @@ snarks
|
|||||||
snarky
|
snarky
|
||||||
snarls
|
snarls
|
||||||
snarly
|
snarly
|
||||||
snatch
|
|
||||||
snazzy
|
snazzy
|
||||||
sneaks
|
sneaks
|
||||||
sneaky
|
sneaky
|
||||||
@ -6716,7 +6555,6 @@ socket
|
|||||||
sodded
|
sodded
|
||||||
sodden
|
sodden
|
||||||
sodium
|
sodium
|
||||||
sodomy
|
|
||||||
soever
|
soever
|
||||||
soften
|
soften
|
||||||
softer
|
softer
|
||||||
@ -7468,7 +7306,6 @@ torrid
|
|||||||
torsos
|
torsos
|
||||||
tortes
|
tortes
|
||||||
tossed
|
tossed
|
||||||
tosser
|
|
||||||
tosses
|
tosses
|
||||||
tossup
|
tossup
|
||||||
totals
|
totals
|
||||||
@ -7686,7 +7523,6 @@ unhook
|
|||||||
unhurt
|
unhurt
|
||||||
unions
|
unions
|
||||||
unique
|
unique
|
||||||
unisex
|
|
||||||
unison
|
unison
|
||||||
united
|
united
|
||||||
unites
|
unites
|
||||||
@ -7793,7 +7629,6 @@ vacant
|
|||||||
vacate
|
vacate
|
||||||
vacuum
|
vacuum
|
||||||
vagary
|
vagary
|
||||||
vagina
|
|
||||||
vaguer
|
vaguer
|
||||||
vainer
|
vainer
|
||||||
vainly
|
vainly
|
||||||
@ -7930,9 +7765,6 @@ votive
|
|||||||
vowels
|
vowels
|
||||||
vowing
|
vowing
|
||||||
voyage
|
voyage
|
||||||
voyeur
|
|
||||||
vulgar
|
|
||||||
vulvae
|
|
||||||
wabbit
|
wabbit
|
||||||
wacker
|
wacker
|
||||||
wackos
|
wackos
|
||||||
@ -7975,7 +7807,6 @@ wander
|
|||||||
wangle
|
wangle
|
||||||
waning
|
waning
|
||||||
wanked
|
wanked
|
||||||
wanker
|
|
||||||
wanner
|
wanner
|
||||||
wanted
|
wanted
|
||||||
wanton
|
wanton
|
||||||
|
@ -1944,7 +1944,6 @@ dosage
|
|||||||
dose
|
dose
|
||||||
dotted
|
dotted
|
||||||
doubling
|
doubling
|
||||||
douche
|
|
||||||
dove
|
dove
|
||||||
down
|
down
|
||||||
dowry
|
dowry
|
||||||
@ -3015,7 +3014,6 @@ groom
|
|||||||
groove
|
groove
|
||||||
grooving
|
grooving
|
||||||
groovy
|
groovy
|
||||||
grope
|
|
||||||
ground
|
ground
|
||||||
grouped
|
grouped
|
||||||
grout
|
grout
|
||||||
@ -3135,7 +3133,6 @@ happiness
|
|||||||
happy
|
happy
|
||||||
harbor
|
harbor
|
||||||
hardcopy
|
hardcopy
|
||||||
hardcore
|
|
||||||
hardcover
|
hardcover
|
||||||
harddisk
|
harddisk
|
||||||
hardened
|
hardened
|
||||||
@ -6553,7 +6550,6 @@ swimmer
|
|||||||
swimming
|
swimming
|
||||||
swimsuit
|
swimsuit
|
||||||
swimwear
|
swimwear
|
||||||
swinger
|
|
||||||
swinging
|
swinging
|
||||||
swipe
|
swipe
|
||||||
swirl
|
swirl
|
||||||
|
File diff suppressed because it is too large
Load Diff
29
app/migrations/versions/2024_020110_818b0a956205_.py
Normal file
29
app/migrations/versions/2024_020110_818b0a956205_.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 818b0a956205
|
||||||
|
Revises: 4bc54632d9aa
|
||||||
|
Create Date: 2024-02-01 10:43:46.253184
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '818b0a956205'
|
||||||
|
down_revision = '4bc54632d9aa'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('alias', sa.Column('last_email_log_id', sa.Integer(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('alias', 'last_email_log_id')
|
||||||
|
# ### end Alembic commands ###
|
44
app/oneshot/backfill_email_log.py
Normal file
44
app/oneshot/backfill_email_log.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from app.models import Alias
|
||||||
|
from app.db import Session
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="Backfill alias", description="Backfill alias las use"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--start_alias_id", default=0, type=int, help="Initial alias_id"
|
||||||
|
)
|
||||||
|
parser.add_argument("-e", "--end_alias_id", default=0, type=int, help="Last alias_id")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
alias_id_start = args.start_alias_id
|
||||||
|
max_alias_id = args.end_alias_id
|
||||||
|
if max_alias_id == 0:
|
||||||
|
max_alias_id = Session.query(func.max(Alias.id)).scalar()
|
||||||
|
|
||||||
|
print(f"Checking alias {alias_id_start} to {max_alias_id}")
|
||||||
|
step = 1000
|
||||||
|
el_query = "SELECT alias_id, MAX(id) from email_log where alias_id>=:start AND alias_id < :end GROUP BY alias_id"
|
||||||
|
alias_query = "UPDATE alias set last_email_log_id = :el_id where id = :alias_id"
|
||||||
|
updated = 0
|
||||||
|
start_time = time.time()
|
||||||
|
for batch_start in range(alias_id_start, max_alias_id, step):
|
||||||
|
rows = Session.execute(el_query, {"start": batch_start, "end": batch_start + step})
|
||||||
|
for row in rows:
|
||||||
|
Session.execute(alias_query, {"alias_id": row[0], "el_id": row[1]})
|
||||||
|
Session.commit()
|
||||||
|
updated += 1
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
time_per_alias = elapsed / (updated + 1)
|
||||||
|
last_batch_id = batch_start + step
|
||||||
|
remaining = max_alias_id - last_batch_id
|
||||||
|
time_remaining = (max_alias_id - last_batch_id) * time_per_alias
|
||||||
|
hours_remaining = time_remaining / 3600.0
|
||||||
|
print(
|
||||||
|
f"\rAlias {batch_start}/{max_alias_id} {updated} {hours_remaining:.2f}hrs remaining"
|
||||||
|
)
|
||||||
|
print("")
|
@ -20,6 +20,7 @@ exclude = '''
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
ignore-init-module-imports = true
|
ignore-init-module-imports = true
|
||||||
|
exclude = [".venv", "migrations"]
|
||||||
|
|
||||||
[tool.djlint]
|
[tool.djlint]
|
||||||
indent = 2
|
indent = 2
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
{{ otp_token_form.csrf_token }}
|
{{ otp_token_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="create" />
|
<input type="hidden" name="form-name" value="create" />
|
||||||
<div class="font-weight-bold mt-5">Token</div>
|
<div class="font-weight-bold mt-5">Token</div>
|
||||||
<div class="small-text mb-3">Please enter the 2FA code from your 2FA authenticator</div>
|
<div class="small-text mb-3">Please enter the 2FA code from your authenticator app</div>
|
||||||
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
||||||
{{ render_field_errors(otp_token_form.token) }}
|
{{ render_field_errors(otp_token_form.token) }}
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<h1 class="card-title">Create new account</h1>
|
<h1 class="card-title">Create new account</h1>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Email address</label>
|
<label class="form-label">Email address</label>
|
||||||
{{ form.email(class="form-control", type="email", placeholder="YourName@protonmail.com") }}
|
{{ form.email(class="form-control", type="email", placeholder="username@proton.me") }}
|
||||||
<div class="small-text alert alert-info" style="margin-top: 1px">
|
<div class="small-text alert alert-info" style="margin-top: 1px">
|
||||||
Emails sent to your alias will be forwarded to this email address.
|
Emails sent to your alias will be forwarded to this email address.
|
||||||
<br>
|
<br>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<div class="card-body p-6 text-center">
|
<div class="card-body p-6 text-center">
|
||||||
<h1 class="h4">An email to validate your email is on its way.</h1>
|
<h1 class="h4">An email to validate your email is on its way.</h1>
|
||||||
<p>Please check your inbox/spam folder.</p>
|
<p>Please check your inbox/spam folder.</p>
|
||||||
|
<p>Make sure to mark the message as not spam so that future messages come to your normal inbox</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -59,6 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if can_create_contacts %}
|
||||||
|
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-12 col-lg-6 pt-1">
|
<div class="col-12 col-lg-6 pt-1">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@ -79,6 +81,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="col-12 col-lg-6 pt-1">
|
<div class="col-12 col-lg-6 pt-1">
|
||||||
<div class="float-right d-flex">
|
<div class="float-right d-flex">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
|
<b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
|
||||||
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc.
|
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc.
|
||||||
<br />
|
<br />
|
||||||
If you add your own domain, this restriction is removed, and you can fully customize the alias.
|
If you add your own domain (or subdomain), this restriction is removed, and you can fully customize the alias.
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,6 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col p-1">
|
<div class="col p-1">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<button type="submit" id="create" class="btn btn-primary mt-1">Create</button>
|
<button type="submit" id="create" class="btn btn-primary mt-1">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="h3">Two Factor Authentication - TOTP</h1>
|
<h1 class="h3">Two Factor Authentication - TOTP</h1>
|
||||||
<p>
|
<p>
|
||||||
You will need to use a 2FA application like Google Authenticator or Authy on your phone or PC and scan the following QR Code:
|
You will need to use a 2FA application like Proton Pass or Aegis on your phone or PC and scan the following QR Code:
|
||||||
</p>
|
</p>
|
||||||
<canvas id="qr"></canvas>
|
<canvas id="qr"></canvas>
|
||||||
<script>
|
<script>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div>{{ notification.message | safe }}</div>
|
<div>{{ notification.message | safe }}</div>
|
||||||
<form method="post"
|
<form method="post"
|
||||||
class="float-right mt-3"
|
class="float-right mt-3"
|
||||||
onsubmit="return confirm('This operation is not reversible, please confirm');">
|
onsubmit="return confirm('This operation is irreversible, please confirm');">
|
||||||
<button class="btn btn-outline-danger">Delete</button>
|
<button class="btn btn-outline-danger">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<br />
|
<br />
|
||||||
For generic questions, i.e. not related to your account, we recommend to post the question on
|
For generic questions, i.e. not related to your account, we recommend to post the question on
|
||||||
our
|
our
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a> or <a href="https://forum.simplelogin.io/">our official forum</a>
|
||||||
where our community can help answer the question
|
where our community can help answer the question
|
||||||
and other people with the same question can find the answer there.
|
and other people with the same question can find the answer there.
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
{% extends "default.html" %}
|
{% extends "default.html" %}
|
||||||
|
|
||||||
{% set active_page = "dashboard" %}
|
{% set active_page = "dashboard" %}
|
||||||
{% block title %}Block an alias{% endblock %}
|
{% block title %}Deactivate an alias{% endblock %}
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="h3">Block alias</h1>
|
<h1 class="h3">Deactivate alias</h1>
|
||||||
<p>
|
<p>
|
||||||
You are about to block the alias
|
You are about to deactivate the alias
|
||||||
<a href="mailto:{{ alias }}" target="_blank">{{ alias }}</a>
|
<a href="mailto:{{ alias }}" target="_blank">{{ alias }}</a>
|
||||||
</p>
|
</p>
|
||||||
<p>After this, you will stop receiving all emails sent to this alias, please confirm.</p>
|
<p>
|
||||||
|
After this, you will stop receiving all emails sent to this alias, please confirm. You will always be able to re-activate it untill you will decide to delete it.
|
||||||
|
</p>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<button class="btn btn-warning">Confirm</button>
|
<button class="btn btn-warning">Confirm</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -43,9 +43,8 @@ Note, if you are a paying Proton Mail user, you automatically receive the premiu
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
For any question, feedback or feature request, please join our
|
For any question or feedback, please join our <a href="https://forum.simplelogin.io/">official forum</a>.
|
||||||
<a href="https://github.com/simple-login/app/discussions">GitHub forum</a>
|
If you want to request a feature, please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
|
||||||
.
|
|
||||||
You can also join our
|
You can also join our
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
||||||
or follow our
|
or follow our
|
||||||
|
@ -13,7 +13,8 @@ SimpleLogin is also available on Android and iOS so you can manage your aliases
|
|||||||
|
|
||||||
Note, if you are a paying Proton Mail user, you automatically receive the premium version of SimpleLogin.
|
Note, if you are a paying Proton Mail user, you automatically receive the premium version of SimpleLogin.
|
||||||
|
|
||||||
For any question, feedback or feature request, please join our GitHub forum.
|
For any question or feedback, please join our official forum.
|
||||||
|
If you want to request a feature, please submit it on our GitHub repo.
|
||||||
You can also join our Reddit or follow our Twitter.
|
You can also join our Reddit or follow our Twitter.
|
||||||
|
|
||||||
Best,
|
Best,
|
||||||
@ -26,7 +27,8 @@ Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
|
|||||||
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
|
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
|
||||||
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
|
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
|
||||||
iOS: https://apps.apple.com/app/id1494359858
|
iOS: https://apps.apple.com/app/id1494359858
|
||||||
Github forum: https://github.com/simple-login/app/discussions
|
Github repo: https://github.com/simple-login/app/discussions
|
||||||
|
Official forum: https://forum.simplelogin.io/
|
||||||
Reddit: https://www.reddit.com/r/Simplelogin/
|
Reddit: https://www.reddit.com/r/Simplelogin/
|
||||||
Twitter: https://twitter.com/simple_login
|
Twitter: https://twitter.com/simple_login
|
||||||
|
|
||||||
|
@ -71,9 +71,10 @@ Please note that you can't create more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
For any question, feedback or feature request, please join our
|
For any question or feedback,
|
||||||
<a href="https://github.com/simple-login/app/discussions">GitHub forum</a>
|
please join our <a href="https://forum.simplelogin.io/">official forum</a>.
|
||||||
.
|
If you want to request a feature,
|
||||||
|
please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
|
||||||
You can also join our
|
You can also join our
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
||||||
or follow our
|
or follow our
|
||||||
|
@ -26,6 +26,8 @@ No worries: all aliases you create during this period will continue to work norm
|
|||||||
|
|
||||||
At any time, you can reach out to us by simply replying to this email.
|
At any time, you can reach out to us by simply replying to this email.
|
||||||
|
|
||||||
For any question, feedback or feature request, please join our GitHub forum at https://github.com/simple-login/app/discussions
|
For any question or feedback, please join our official forum at https://forum.simplelogin.io/
|
||||||
|
|
||||||
|
If you want to request a feature, please submit it on our GitHub repo at https://github.com/simple-login/app/discussions
|
||||||
|
|
||||||
You can also join our Reddit at https://www.reddit.com/r/Simplelogin/ follow our Twitter at https://twitter.com/simplelogin
|
You can also join our Reddit at https://www.reddit.com/r/Simplelogin/ follow our Twitter at https://twitter.com/simplelogin
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
{{ render_text("Thank you for choosing SimpleLogin.") }}
|
{{ render_text("Thank you for choosing SimpleLogin.") }}
|
||||||
{{ render_text("To get started, please confirm that <b>" + email + "</b> is your email address by clicking on the button below within 1 hour.") }}
|
{{ render_text("To get started, please confirm that <b>" + email + "</b> is your email address by clicking on the button below within 1 hour.") }}
|
||||||
|
{{ render_text("If it wasn't you, maybe someone entered your email by mistake. In this case you can ignore this mail.") }}
|
||||||
{{ render_button("Verify email", activation_link) }}
|
{{ render_button("Verify email", activation_link) }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
|
@ -4,4 +4,6 @@
|
|||||||
Thank you for choosing SimpleLogin.
|
Thank you for choosing SimpleLogin.
|
||||||
|
|
||||||
To get started, please confirm that {{email}} is your email address using this link {{activation_link}} within 1 hour.
|
To get started, please confirm that {{email}} is your email address using this link {{activation_link}} within 1 hour.
|
||||||
|
|
||||||
|
If it wasn't you, maybe someone entered your email by mistake. In this case you can ignore this mail.
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Your have tried to register multiple times to {{ service }}, and this is against the terms of service of SimpleLogin. Please don't do that anymore.
|
You have tried to register multiple times to {{ service }}, and this is against the terms of service of SimpleLogin. Please don't do that anymore.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<a href='https://simplelogin.io/' aria-label="SimpleLogin">
|
<a href='https://simplelogin.io/' aria-label="SimpleLogin">
|
||||||
<img src="/static/logo-white.svg"
|
<img src="/static/logo-white.svg"
|
||||||
height="30px"
|
height="30px"
|
||||||
class="mb-3"
|
class="mt-3 mb-3"
|
||||||
alt="SimpleLogin logo">
|
alt="SimpleLogin logo">
|
||||||
</a>
|
</a>
|
||||||
<!-- End Logo -->
|
<!-- End Logo -->
|
||||||
@ -17,8 +17,7 @@
|
|||||||
SimpleLogin is an <a href="https://github.com/simple-login">open source</a> email alias solution to protect your email address.
|
SimpleLogin is an <a href="https://github.com/simple-login">open source</a> email alias solution to protect your email address.
|
||||||
</p>
|
</p>
|
||||||
<p class="small text-white">
|
<p class="small text-white">
|
||||||
SimpleLogin is the product of SimpleLogin SAS, registered in France under the SIREN number 884302134.
|
SimpleLogin is the product of <a href="https://proton.me">Proton AG</a>, registered in Switzerland under number CHE-354.686.492.
|
||||||
SimpleLogin SAS is part of <a href="https://proton.me">Proton AG</a>.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,12 +37,6 @@
|
|||||||
alt="GitHub">
|
alt="GitHub">
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item "
|
|
||||||
href="https://github.com/simple-login/app/blob/master/docs/api.md">
|
|
||||||
API Docs
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
href="https://status.simplelogin.io/">Status</a>
|
href="https://status.simplelogin.io/">Status</a>
|
||||||
@ -61,18 +54,10 @@
|
|||||||
<a class="list-group-item text-white footer-item"
|
<a class="list-group-item text-white footer-item"
|
||||||
href="https://simplelogin.io/blog/">Blog</a>
|
href="https://simplelogin.io/blog/">Blog</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item"
|
|
||||||
href="https://simplelogin.io/job/">Join Us</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item"
|
<a class="list-group-item text-white footer-item"
|
||||||
href="https://simplelogin.io/about/">About Us</a>
|
href="https://simplelogin.io/about/">About Us</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item"
|
|
||||||
href="https://github.com/simple-login/app/projects/1">Roadmap</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item"
|
<a class="list-group-item text-white footer-item"
|
||||||
href="https://simplelogin.io/contact/">Contact Us</a>
|
href="https://simplelogin.io/contact/">Contact Us</a>
|
||||||
@ -106,37 +91,9 @@
|
|||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
href="https://simplelogin.io/docs/">Documentation</a>
|
href="https://simplelogin.io/docs/">Documentation</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 col-lg-2 mb-4">
|
|
||||||
<h3 class="h4 text-white">Comparisons</h3>
|
|
||||||
<ul class="list-group list-group-transparent list-group-white list-group-flush list-group-borderless mb-0 footer-list-group">
|
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
href="https://simplelogin.io/blog/email-alias-vs-plus-sign/">
|
href="https://forum.simplelogin.io">Forum</a>
|
||||||
vs Plus Sign (+) Trick
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item"
|
|
||||||
href="https://simplelogin.io/blog/vs-firefox-relay/">
|
|
||||||
vs
|
|
||||||
Firefox Relay
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item"
|
|
||||||
href="https://simplelogin.io/blog/vs-burner-mail/">
|
|
||||||
vs
|
|
||||||
Burner Mail
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="list-group-item text-white footer-item"
|
|
||||||
href="https://simplelogin.io/blog/alternative-33mail/">
|
|
||||||
vs
|
|
||||||
33mail
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,7 +83,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://github.com/simple-login/app/discussions"
|
<a href="https://github.com/simple-login/app/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
Github repo
|
||||||
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<a href="https://forum.simplelogin.io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
Forum
|
Forum
|
||||||
|
@ -106,7 +106,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://github.com/simple-login/app/discussions"
|
<a href="https://forum.simplelogin.io/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
Forum
|
Forum
|
||||||
|
@ -40,14 +40,16 @@ def test_get_notifications(flask_client):
|
|||||||
def test_mark_notification_as_read(flask_client):
|
def test_mark_notification_as_read(flask_client):
|
||||||
user, api_key = get_new_user_and_api_key()
|
user, api_key = get_new_user_and_api_key()
|
||||||
|
|
||||||
Notification.create(id=1, user_id=user.id, message="Test message 1")
|
notif_id = Notification.create(
|
||||||
|
user_id=user.id, message="Test message 1", flush=True
|
||||||
|
).id
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
url_for("api.mark_as_read", notification_id=1),
|
url_for("api.mark_as_read", notification_id=notif_id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
notification = Notification.first()
|
notification = Notification.filter_by(id=notif_id).first()
|
||||||
assert notification.read
|
assert notification.read
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from app.api.serializer import get_alias_infos_with_pagination_v3
|
from app.api.serializer import get_alias_infos_with_pagination_v3
|
||||||
from app.config import PAGE_LIMIT
|
from app.config import PAGE_LIMIT
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import Alias, Mailbox, Contact
|
from app.models import Alias, Mailbox, Contact, EmailLog
|
||||||
from tests.utils import create_new_user
|
from tests.utils import create_new_user, random_email
|
||||||
|
|
||||||
|
|
||||||
def test_get_alias_infos_with_pagination_v3(flask_client):
|
def test_get_alias_infos_with_pagination_v3(flask_client):
|
||||||
@ -155,3 +155,46 @@ def test_get_alias_infos_pinned_alias(flask_client):
|
|||||||
# pinned alias isn't included in the search
|
# pinned alias isn't included in the search
|
||||||
alias_infos = get_alias_infos_with_pagination_v3(user, query="no match")
|
alias_infos = get_alias_infos_with_pagination_v3(user, query="no match")
|
||||||
assert len(alias_infos) == 0
|
assert len(alias_infos) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_alias_infos_with_no_last_email_log(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||||
|
assert len(alias_infos) == 1
|
||||||
|
row = alias_infos[0]
|
||||||
|
assert row.alias.id == user.newsletter_alias_id
|
||||||
|
assert row.latest_contact is None
|
||||||
|
assert row.latest_email_log is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_alias_infos_with_email_log_no_contact():
|
||||||
|
user = create_new_user()
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=user.newsletter_alias_id,
|
||||||
|
website_email="a@a.com",
|
||||||
|
reply_email=random_email(),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=user.newsletter_alias_id,
|
||||||
|
website_email="unused@a.com",
|
||||||
|
reply_email=random_email(),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
EmailLog.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=user.newsletter_alias_id,
|
||||||
|
contact_id=contact.id,
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||||
|
assert len(alias_infos) == 1
|
||||||
|
row = alias_infos[0]
|
||||||
|
assert row.alias.id == user.newsletter_alias_id
|
||||||
|
assert row.latest_contact is not None
|
||||||
|
assert row.latest_contact.id == contact.id
|
||||||
|
assert row.latest_email_log is not None
|
||||||
|
alias = Alias.get(id=user.newsletter_alias_id)
|
||||||
|
assert row.latest_email_log.id == alias.last_email_log_id
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from app import config
|
from app import config
|
||||||
|
from app.db import Session
|
||||||
from app.models import User, PartnerUser
|
from app.models import User, PartnerUser
|
||||||
from app.proton.utils import get_proton_partner
|
from app.proton.utils import get_proton_partner
|
||||||
from tests.api.utils import get_new_user_and_api_key
|
from tests.api.utils import get_new_user_and_api_key
|
||||||
@ -23,6 +24,7 @@ def test_user_in_trial(flask_client):
|
|||||||
"profile_picture_url": None,
|
"profile_picture_url": None,
|
||||||
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
||||||
"connected_proton_address": None,
|
"connected_proton_address": None,
|
||||||
|
"can_create_reverse_alias": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -52,9 +54,24 @@ def test_user_linked_to_proton(flask_client):
|
|||||||
"profile_picture_url": None,
|
"profile_picture_url": None,
|
||||||
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
||||||
"connected_proton_address": partner_email,
|
"connected_proton_address": partner_email,
|
||||||
|
"can_create_reverse_alias": user.can_create_contacts(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_create_reverse_alias(flask_client):
|
||||||
|
user, api_key = get_new_user_and_api_key()
|
||||||
|
user.trial_end = None
|
||||||
|
Session.flush()
|
||||||
|
config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = True
|
||||||
|
|
||||||
|
r = flask_client.get(
|
||||||
|
url_for("api.user_info"), headers={"Authentication": api_key.code}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert not r.json["can_create_reverse_alias"]
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_api_key(flask_client):
|
def test_wrong_api_key(flask_client):
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
url_for("api.user_info"), headers={"Authentication": "Invalid code"}
|
url_for("api.user_info"), headers={"Authentication": "Invalid code"}
|
||||||
|
17
app/tests/models/test_alias.py
Normal file
17
app/tests/models/test_alias.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from app.db import Session
|
||||||
|
from app.models import Alias, Mailbox, AliasMailbox
|
||||||
|
from tests.utils import create_new_user, random_email
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicated_mailbox_is_returned_only_once():
|
||||||
|
user = create_new_user()
|
||||||
|
other_mailbox = Mailbox.create(user_id=user.id, email=random_email(), verified=True)
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
AliasMailbox.create(mailbox_id=other_mailbox.id, alias_id=alias.id)
|
||||||
|
AliasMailbox.create(mailbox_id=user.default_mailbox_id, alias_id=alias.id)
|
||||||
|
Session.flush()
|
||||||
|
alias_mailboxes = alias.mailboxes
|
||||||
|
assert len(alias_mailboxes) == 2
|
||||||
|
alias_mailbox_id = [mailbox.id for mailbox in alias_mailboxes]
|
||||||
|
assert user.default_mailbox_id in alias_mailbox_id
|
||||||
|
assert other_mailbox.id in alias_mailbox_id
|
@ -49,10 +49,26 @@ from app.models import (
|
|||||||
VerpType,
|
VerpType,
|
||||||
AliasGeneratorEnum,
|
AliasGeneratorEnum,
|
||||||
SLDomain,
|
SLDomain,
|
||||||
|
Mailbox,
|
||||||
)
|
)
|
||||||
|
|
||||||
# flake8: noqa: E101, W191
|
# flake8: noqa: E101, W191
|
||||||
from tests.utils import login, load_eml_file, create_new_user, random_domain
|
from tests.utils import (
|
||||||
|
login,
|
||||||
|
load_eml_file,
|
||||||
|
create_new_user,
|
||||||
|
random_email,
|
||||||
|
random_domain,
|
||||||
|
random_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module(module):
|
||||||
|
config.SKIP_MX_LOOKUP_ON_CHECK = True
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module(module):
|
||||||
|
config.SKIP_MX_LOOKUP_ON_CHECK = False
|
||||||
|
|
||||||
|
|
||||||
def test_get_email_domain_part():
|
def test_get_email_domain_part():
|
||||||
@ -68,10 +84,6 @@ def test_email_belongs_to_alias_domains():
|
|||||||
assert not can_create_directory_for_address("hey@d3.test")
|
assert not can_create_directory_for_address("hey@d3.test")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
|
||||||
"GITHUB_ACTIONS_TEST" in os.environ,
|
|
||||||
reason="this test requires DNS lookup that does not work on Github CI",
|
|
||||||
)
|
|
||||||
def test_can_be_used_as_personal_email(flask_client):
|
def test_can_be_used_as_personal_email(flask_client):
|
||||||
# default alias domain
|
# default alias domain
|
||||||
assert not email_can_be_used_as_mailbox("ab@sl.local")
|
assert not email_can_be_used_as_mailbox("ab@sl.local")
|
||||||
@ -94,6 +106,27 @@ def test_can_be_used_as_personal_email(flask_client):
|
|||||||
assert email_can_be_used_as_mailbox("abcd@gmail.com")
|
assert email_can_be_used_as_mailbox("abcd@gmail.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_user_prevents_email_from_being_used_as_mailbox():
|
||||||
|
email = f"user_{random_token(10)}@mailbox.test"
|
||||||
|
assert email_can_be_used_as_mailbox(email)
|
||||||
|
user = create_new_user(email)
|
||||||
|
user.disabled = True
|
||||||
|
Session.flush()
|
||||||
|
assert not email_can_be_used_as_mailbox(email)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_user_with_secondary_mailbox_prevents_email_from_being_used_as_mailbox():
|
||||||
|
email = f"user_{random_token(10)}@mailbox.test"
|
||||||
|
assert email_can_be_used_as_mailbox(email)
|
||||||
|
user = create_new_user()
|
||||||
|
Mailbox.create(user_id=user.id, email=email)
|
||||||
|
Session.flush()
|
||||||
|
assert email_can_be_used_as_mailbox(email)
|
||||||
|
user.disabled = True
|
||||||
|
Session.flush()
|
||||||
|
assert not email_can_be_used_as_mailbox(email)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_header():
|
def test_delete_header():
|
||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
assert msg._headers == []
|
assert msg._headers == []
|
||||||
@ -154,13 +187,14 @@ def test_parse_full_address():
|
|||||||
|
|
||||||
def test_send_email_with_rate_control(flask_client):
|
def test_send_email_with_rate_control(flask_client):
|
||||||
user = create_new_user()
|
user = create_new_user()
|
||||||
|
email = random_email()
|
||||||
|
|
||||||
for _ in range(MAX_ALERT_24H):
|
for _ in range(MAX_ALERT_24H):
|
||||||
assert send_email_with_rate_control(
|
assert send_email_with_rate_control(
|
||||||
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
user, "test alert type", email, "subject", "plaintext"
|
||||||
)
|
)
|
||||||
assert not send_email_with_rate_control(
|
assert not send_email_with_rate_control(
|
||||||
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
user, "test alert type", email, "subject", "plaintext"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user