Compare commits

...

9 Commits

Author SHA1 Message Date
6c910d62c5 4.38.2 2024-02-06 12:00:07 +00:00
99ffd1ec0c 4.38.0 2024-02-03 16:55:23 +00:00
eda940f8b2 4.37.2 2024-01-27 12:00:07 +00:00
1dad582523 4.37.1 2024-01-25 12:00:08 +00:00
e516266a27 4.37.0 2024-01-18 12:00:07 +00:00
850fc95477 4.36.8 2023-12-28 12:00:07 +00:00
d172825900 4.36.7 2023-12-21 12:00:09 +00:00
026865e5bf 4.36.6 2023-12-17 14:56:57 +00:00
add94ef2a2 4.36.5 2023-11-30 12:00:09 +00:00
45 changed files with 369 additions and 370412 deletions

View File

@ -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:
```bash
openssl genrsa -out dkim.key 1024
openssl genrsa -out dkim.key -traditional 1024
openssl rsa -in dkim.key -pubout -out dkim.pub.key
```
@ -510,11 +510,14 @@ server {
server_name app.mydomain.com;
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
```bash

View File

@ -214,6 +214,20 @@ class UserAdmin(SLModelView):
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(
"disable_otp_fido",
"Disable OTP & FIDO",

View File

@ -31,6 +31,7 @@ from app.models import Alias, Contact, Mailbox, AliasMailbox
@deprecated
@api_bp.route("/aliases", methods=["GET", "POST"])
@require_api_auth
@limiter.limit("10/minute", key_func=lambda: g.user.id)
def get_aliases():
"""
Get aliases
@ -72,10 +73,8 @@ def get_aliases():
@api_bp.route("/v2/aliases", methods=["GET", "POST"])
@limiter.limit(
"5/minute",
)
@require_api_auth
@limiter.limit("50/minute", key_func=lambda: g.user.id)
def get_aliases_v2():
"""
Get aliases

View File

@ -17,9 +17,14 @@ from app.models import PlanEnum, AppleSubscription
_MONTHLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.monthly"
_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_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
_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
_PROD_URL = "https://buy.itunes.apple.com/verifyReceipt"
@ -263,7 +268,11 @@ def apple_update_notification():
plan = (
PlanEnum.monthly
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
)
@ -517,7 +526,11 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
plan = (
PlanEnum.monthly
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
)

View File

@ -32,6 +32,7 @@ def user_to_dict(user: User) -> dict:
"in_trial": user.in_trial(),
"max_alias_free_plan": user.max_alias_for_free_account(),
"connected_proton_address": None,
"can_create_reverse_alias": user.can_create_contacts(),
}
if config.CONNECT_WITH_PROTON:
@ -58,6 +59,7 @@ def user_info():
- in_trial
- max_alias_free
- is_connected_with_proton
- can_create_reverse_alias
"""
user = g.user

View File

@ -179,6 +179,7 @@ AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3"
BUCKET = os.environ.get("BUCKET")
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
AWS_ENDPOINT_URL = os.environ.get("AWS_ENDPOINT_URL", None)
# Paddle
try:
@ -488,7 +489,34 @@ def 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 (
FLASK_SECRET + "partnerapitoken"
)

View File

@ -51,14 +51,6 @@ def email_validator():
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:
"""
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:
raise ErrContactAlreadyExists(contact)
if not user_can_create_contacts(user):
if not user.can_create_contacts():
raise ErrContactErrorUpgradeNeeded()
contact = Contact.create(
@ -327,6 +319,6 @@ def alias_contact_manager(alias_id):
last_page=last_page,
query=query,
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,
)

View File

@ -24,6 +24,7 @@ from app.models import (
AliasMailbox,
DomainDeletedAlias,
)
from app.utils import CSRFValidationForm
@dashboard_bp.route("/custom_alias", methods=["GET", "POST"])
@ -48,9 +49,13 @@ def custom_alias():
at_least_a_premium_domain = True
break
csrf_form = CSRFValidationForm()
mailboxes = current_user.mailboxes()
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(" ", "")
signed_alias_suffix = request.form.get("signed-alias-suffix")
mailbox_ids = request.form.getlist("mailboxes")
@ -164,4 +169,5 @@ def custom_alias():
alias_suffixes=alias_suffixes,
at_least_a_premium_domain=at_least_a_premium_domain,
mailboxes=mailboxes,
csrf_form=csrf_form,
)

View File

@ -52,16 +52,13 @@ def get_stats(user: User) -> Stats:
@dashboard_bp.route("/", methods=["GET", "POST"])
@login_required
@limiter.limit(
ALIAS_LIMIT,
methods=["POST"],
exempt_when=lambda: request.form.get("form-name") != "create-random-email",
)
@limiter.limit(
"5/minute",
methods=["GET"],
)
@login_required
@limiter.limit("10/minute", methods=["GET"], key_func=lambda: current_user.id)
@parallel_limiter.lock(
name="alias_creation",
only_when=lambda: request.form.get("form-name") == "create-random-email",

View File

@ -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)
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

View File

@ -27,7 +27,7 @@ from sqlalchemy.orm import deferred
from sqlalchemy.sql import and_
from sqlalchemy_utils import ArrowType
from app import config
from app import config, rate_limiter
from app import s3
from app.db import Session
from app.dns_utils import get_mx_domains
@ -235,6 +235,7 @@ class AuditLogActionEnum(EnumE):
download_provider_complaint = 8
disable_user = 9
enable_user = 10
stop_trial = 11
class Phase(EnumE):
@ -1113,6 +1114,13 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
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):
return f"<User {self.id} {self.name} {self.email}>"
@ -1506,7 +1514,8 @@ class Alias(Base, ModelMixin):
def mailboxes(self):
ret = [self.mailbox]
for m in self._mailboxes:
ret.append(m)
if m.id is not self.mailbox.id:
ret.append(m)
ret = [mb for mb in ret if mb.verified]
ret = sorted(ret, key=lambda mb: mb.email)
@ -1555,6 +1564,15 @@ class Alias(Base, ModelMixin):
flush = kw.pop("flush", False)
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"]
# make sure email is lowercase and doesn't have any whitespace
@ -3322,6 +3340,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
def disable_otp_fido(
cls, admin_user_id: int, user_id: int, had_otp: bool, had_fido: bool

38
app/app/rate_limiter.py Normal file
View File

@ -0,0 +1,38 @@
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
bucket_id = int(datetime.utcnow().timestamp()) % 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:
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")

View File

@ -2,6 +2,7 @@ import flask
import limits.storage
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
@ -10,12 +11,14 @@ def initialize_redis_services(app: flask.Flask, redis_url: str):
storage = limits.storage.RedisStorage(redis_url)
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
set_redis_concurrent_lock(storage)
rate_limit_set_redis(storage)
elif redis_url.startswith("redis+sentinel://"):
storage = limits.storage.RedisSentinelStorage(redis_url)
app.session_interface = RedisSessionStore(
storage.storage, storage.storage_slave, app
)
set_redis_concurrent_lock(storage)
rate_limit_set_redis(storage)
else:
raise RuntimeError(
f"Tried to set_redis_session with an invalid redis url: ${redis_url}"

View File

@ -13,17 +13,29 @@ from app.config import (
LOCAL_FILE_UPLOAD,
UPLOAD_DIR,
URL,
AWS_ENDPOINT_URL,
)
if not LOCAL_FILE_UPLOAD:
_session = boto3.Session(
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
region_name=AWS_REGION,
)
from app.log import LOG
def upload_from_bytesio(key: str, bs: BytesIO, content_type="string"):
_s3_client = None
def _get_s3client():
global _s3_client
if _s3_client is None:
args = {
"aws_access_key_id": AWS_ACCESS_KEY_ID,
"aws_secret_access_key": AWS_SECRET_ACCESS_KEY,
"region_name": AWS_REGION,
}
if AWS_ENDPOINT_URL:
args["endpoint_url"] = AWS_ENDPOINT_URL
_s3_client = boto3.client("s3", **args)
return _s3_client
def upload_from_bytesio(key: str, bs: BytesIO, content_type="application/octet-stream"):
bs.seek(0)
if LOCAL_FILE_UPLOAD:
@ -34,7 +46,8 @@ def upload_from_bytesio(key: str, bs: BytesIO, content_type="string"):
f.write(bs.read())
else:
_session.resource("s3").Bucket(BUCKET).put_object(
_get_s3client().put_object(
Bucket=BUCKET,
Key=key,
Body=bs,
ContentType=content_type,
@ -52,7 +65,8 @@ def upload_email_from_bytesio(path: str, bs: BytesIO, filename):
f.write(bs.read())
else:
_session.resource("s3").Bucket(BUCKET).put_object(
_get_s3client().put_object(
Bucket=BUCKET,
Key=path,
Body=bs,
# Support saving a remote file using Http header
@ -67,12 +81,9 @@ def download_email(path: str) -> Optional[str]:
file_path = os.path.join(UPLOAD_DIR, path)
with open(file_path, "rb") as f:
return f.read()
resp = (
_session.resource("s3")
.Bucket(BUCKET)
.get_object(
Key=path,
)
resp = _get_s3client().get_object(
Bucket=BUCKET,
Key=path,
)
if not resp or "Body" not in resp:
return None
@ -88,8 +99,7 @@ def get_url(key: str, expires_in=3600) -> str:
if LOCAL_FILE_UPLOAD:
return URL + "/static/upload/" + key
else:
s3_client = _session.client("s3")
return s3_client.generate_presigned_url(
return _get_s3client().generate_presigned_url(
ExpiresIn=expires_in,
ClientMethod="get_object",
Params={"Bucket": BUCKET, "Key": key},
@ -100,5 +110,15 @@ def delete(path: str):
if LOCAL_FILE_UPLOAD:
os.remove(os.path.join(UPLOAD_DIR, path))
else:
o = _session.resource("s3").Bucket(BUCKET).Object(path)
o.delete()
_get_s3client().delete_object(Bucket=BUCKET, Key=path)
def create_bucket_if_not_exists():
s3client = _get_s3client()
buckets = s3client.list_buckets()
for bucket in buckets["Buckets"]:
if bucket["Name"] == BUCKET:
LOG.i("Bucket already exists")
return
s3client.create_bucket(Bucket=BUCKET)
LOG.i(f"Bucket {BUCKET} created")

View File

@ -49,11 +49,11 @@ def random_string(length=10, include_digits=False):
def convert_to_id(s: str):
"""convert a string to id-like: remove space, remove special accent"""
s = s.replace(" ", "")
s = s.lower()
s = unidecode(s)
s = s.replace(" ", "")
return s
return s[:256]
_ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-."

View File

@ -388,7 +388,7 @@ Input:
- (Optional but recommended) `hostname` passed in query string
- Request Message Body in json (`Content-Type` is `application/json`)
- 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
- (Optional) note: alias note
- (Optional) name: alias name

View File

@ -192,7 +192,6 @@ amigos
amines
amnion
amoeba
amoral
amount
amours
ampere
@ -215,7 +214,6 @@ animus
anions
ankles
anklet
annals
anneal
annoys
annual
@ -364,7 +362,6 @@ auntie
aureus
aurora
author
autism
autumn
avails
avatar
@ -638,14 +635,12 @@ bigwig
bijoux
bikers
biking
bikini
bilges
bilked
bilker
billed
billet
billow
bimbos
binary
binder
binged
@ -710,8 +705,6 @@ blocks
blokes
blonde
blonds
bloods
bloody
blooms
bloops
blotch
@ -817,8 +810,6 @@ bounds
bounty
bovine
bovver
bowels
bowers
bowing
bowled
bowleg
@ -827,10 +818,8 @@ bowman
bowmen
bowwow
boxcar
boxers
boxier
boxing
boyish
braced
bracer
braces
@ -861,7 +850,6 @@ breach
breads
breaks
breams
breast
breath
breech
breeds
@ -872,9 +860,6 @@ brevet
brewed
brewer
briars
bribed
briber
bribes
bricks
bridal
brides
@ -926,13 +911,7 @@ buffed
buffer
buffet
bugged
bugger
bugled
bugler
bugles
builds
bulged
bulges
bulked
bulled
bullet
@ -1340,8 +1319,6 @@ clingy
clinic
clinks
clique
cloaca
cloaks
cloche
clocks
clomps
@ -1448,7 +1425,6 @@ comply
compos
conchs
concur
condom
condor
condos
coneys
@ -1568,8 +1544,6 @@ cranes
cranks
cranky
cranny
crapes
crappy
crated
crater
crates
@ -1585,7 +1559,6 @@ crazes
creaks
creaky
creams
creamy
crease
create
creche
@ -1594,8 +1567,6 @@ credos
creeds
creeks
creels
creeps
creepy
cremes
creole
crepes
@ -1728,9 +1699,6 @@ dainty
daises
damage
damask
dammed
dammit
damned
damped
dampen
damper
@ -1754,7 +1722,6 @@ darers
daring
darken
darker
darkie
darkly
darned
darner
@ -1763,8 +1730,6 @@ darter
dashed
dasher
dashes
daters
dating
dative
daubed
dauber
@ -1921,7 +1886,6 @@ dharma
dhotis
diadem
dialog
diaper
diatom
dibble
dicier
@ -1943,7 +1907,6 @@ digits
diking
diktat
dilate
dildos
dilute
dimity
dimmed
@ -2058,7 +2021,6 @@ dotted
double
doubly
doubts
douche
doughy
dourer
dourly
@ -2139,15 +2101,6 @@ duenna
duffed
duffer
dugout
dulcet
dulled
duller
dumber
dumbly
dumbos
dumdum
dumped
dumper
dunces
dunged
dunked
@ -2285,7 +2238,6 @@ endows
endued
endues
endure
enemas
energy
enfold
engage
@ -2333,7 +2285,6 @@ erects
ermine
eroded
erodes
erotic
errand
errant
errata
@ -2344,7 +2295,6 @@ eructs
erupts
escape
eschew
escort
escrow
escudo
espied
@ -2363,7 +2313,6 @@ ethnic
etudes
euchre
eulogy
eunuch
eureka
evaded
evader
@ -2392,7 +2341,6 @@ exempt
exerts
exeunt
exhale
exhort
exhume
exiled
exiles
@ -2415,7 +2363,6 @@ extant
extend
extent
extols
extort
extras
exuded
exudes
@ -2440,7 +2387,6 @@ faeces
faerie
faffed
fagged
faggot
failed
faille
fainer
@ -2473,18 +2419,10 @@ faring
farmed
farmer
farrow
farted
fascia
fasted
fasten
faster
father
fathom
fating
fatsos
fatten
fatter
fatwas
faucet
faults
faulty
@ -2532,7 +2470,6 @@ fesses
festal
fester
feting
fetish
fetter
fettle
feudal
@ -2617,9 +2554,7 @@ flaked
flakes
flambe
flamed
flamer
flames
flange
flanks
flared
flares
@ -2754,8 +2689,6 @@ franks
frappe
frauds
frayed
freaks
freaky
freely
freest
freeze
@ -2795,8 +2728,6 @@ fryers
frying
ftpers
ftping
fucked
fucker
fuddle
fudged
fudges
@ -2891,10 +2822,7 @@ gasbag
gashed
gashes
gasket
gasman
gasmen
gasped
gassed
gasses
gateau
gather
@ -3104,7 +3032,6 @@ grimed
grimes
grimly
grinds
gringo
griped
griper
gripes
@ -3186,8 +3113,6 @@ gypsum
gyrate
gyving
habits
hacked
hacker
hackle
hadith
haggis
@ -3195,8 +3120,6 @@ haggle
hailed
hairdo
haired
hajjes
hajjis
halest
haling
halite
@ -3223,11 +3146,8 @@ happen
haptic
harass
harden
harder
hardly
harems
haring
harked
harlot
harmed
harped
@ -3407,7 +3327,6 @@ hoofed
hoofer
hookah
hooked
hooker
hookup
hooped
hoopla
@ -3459,8 +3378,6 @@ huffed
hugely
hugest
hugged
hulled
huller
humane
humans
humble
@ -3667,8 +3584,6 @@ jacket
jading
jagged
jaguar
jailed
jailer
jalopy
jammed
jangle
@ -3689,8 +3604,6 @@ jejune
jelled
jellos
jennet
jerked
jerkin
jersey
jested
jester
@ -3814,11 +3727,7 @@ kidded
kidder
kiddie
kiddos
kidnap
kidney
killed
killer
kilned
kilted
kilter
kimono
@ -3827,15 +3736,11 @@ kinder
kindle
kindly
kingly
kinked
kiosks
kipped
kipper
kirsch
kismet
kissed
kisser
kisses
kiting
kitsch
kitted
@ -3847,10 +3752,6 @@ kluges
klutzy
knacks
knaves
kneads
kneels
knells
knifed
knifes
knight
knives
@ -4210,8 +4111,6 @@ lunges
lupine
lupins
luring
lurked
lurker
lusher
lushes
lushly
@ -4608,7 +4507,6 @@ muggle
mukluk
mulcts
mulish
mullah
mulled
mullet
mumble
@ -4721,9 +4619,6 @@ nickel
nicker
nickle
nieces
niggas
niggaz
nigger
niggle
nigher
nights
@ -4736,7 +4631,6 @@ ninjas
ninths
nipped
nipper
nipple
nitric
nitwit
nixing
@ -4781,15 +4675,6 @@ nozzle
nuance
nubbin
nubile
nuclei
nudest
nudged
nudges
nudism
nudist
nudity
nugget
nuking
numbed
number
numbly
@ -4804,7 +4689,6 @@ nutter
nuzzle
nybble
nylons
nympho
nymphs
oafish
oaring
@ -4885,7 +4769,6 @@ opting
option
opuses
oracle
orally
orange
orated
orates
@ -4897,7 +4780,6 @@ ordeal
orders
ordure
organs
orgasm
orgies
oriels
orient
@ -4993,10 +4875,6 @@ pander
panels
panics
panned
panted
pantie
pantos
pantry
papacy
papaya
papers
@ -5078,7 +4956,6 @@ pebble
pebbly
pecans
pecked
pecker
pectic
pectin
pedalo
@ -5151,9 +5028,6 @@ phenom
phials
phlegm
phloem
phobia
phobic
phoebe
phoned
phones
phoney
@ -5228,9 +5102,6 @@ piques
piracy
pirate
pirogi
pissed
pisser
pisses
pistes
pistil
pistol
@ -5311,8 +5182,6 @@ pogrom
points
pointy
poised
poises
poison
pokers
pokeys
pokier
@ -5422,7 +5291,6 @@ preyed
priced
prices
pricey
pricks
prided
prides
priers
@ -5602,14 +5470,9 @@ rabbit
rabble
rabies
raceme
racers
racial
racier
racily
racing
racism
racist
racked
racket
radars
radial
@ -5661,8 +5524,6 @@ rapers
rapids
rapier
rapine
raping
rapist
rapped
rappel
rapper
@ -5747,7 +5608,6 @@ recoup
rectal
rector
rectos
rectum
recurs
recuse
redact
@ -5891,7 +5751,6 @@ resume
retail
retain
retake
retard
retell
retest
retied
@ -6125,8 +5984,6 @@ sadden
sadder
saddle
sadhus
sadism
sadist
safari
safely
safest
@ -6364,16 +6221,6 @@ severs
sewage
sewers
sewing
sexier
sexily
sexing
sexism
sexist
sexpot
sextet
sexton
sexual
shabby
shacks
shaded
shades
@ -6383,10 +6230,7 @@ shaggy
shaken
shaker
shakes
shalom
shaman
shamed
shames
shandy
shanks
shanty
@ -6432,7 +6276,6 @@ shirks
shirrs
shirts
shirty
shitty
shiver
shoals
shoats
@ -6575,9 +6418,6 @@ slangy
slants
slated
slates
slaved
slaver
slaves
slayed
slayer
sleaze
@ -6672,7 +6512,6 @@ snarks
snarky
snarls
snarly
snatch
snazzy
sneaks
sneaky
@ -6716,7 +6555,6 @@ socket
sodded
sodden
sodium
sodomy
soever
soften
softer
@ -7468,7 +7306,6 @@ torrid
torsos
tortes
tossed
tosser
tosses
tossup
totals
@ -7686,7 +7523,6 @@ unhook
unhurt
unions
unique
unisex
unison
united
unites
@ -7793,7 +7629,6 @@ vacant
vacate
vacuum
vagary
vagina
vaguer
vainer
vainly
@ -7930,9 +7765,6 @@ votive
vowels
vowing
voyage
voyeur
vulgar
vulvae
wabbit
wacker
wackos
@ -7975,7 +7807,6 @@ wander
wangle
waning
wanked
wanker
wanner
wanted
wanton

View File

@ -1944,7 +1944,6 @@ dosage
dose
dotted
doubling
douche
dove
down
dowry
@ -3015,7 +3014,6 @@ groom
groove
grooving
groovy
grope
ground
grouped
grout
@ -3135,7 +3133,6 @@ happiness
happy
harbor
hardcopy
hardcore
hardcover
harddisk
hardened
@ -6553,7 +6550,6 @@ swimmer
swimming
swimsuit
swimwear
swinger
swinging
swipe
swirl

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ exclude = '''
[tool.ruff]
ignore-init-module-imports = true
exclude = [".venv", "migrations"]
[tool.djlint]
indent = 2

View File

@ -407,8 +407,10 @@ def jinja2_filter(app):
@app.context_processor
def inject_stage_and_region():
now = arrow.now()
return dict(
YEAR=arrow.now().year,
YEAR=now.year,
NOW=now,
URL=URL,
SENTRY_DSN=SENTRY_FRONT_END_DSN,
VERSION=SHA1,

View File

@ -15,7 +15,7 @@
{{ otp_token_form.csrf_token }}
<input type="hidden" name="form-name" value="create" />
<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") }}
{{ render_field_errors(otp_token_form.token) }}
<div class="form-check">

View File

@ -9,7 +9,7 @@
<h1 class="card-title">Create new account</h1>
<div class="form-group">
<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">
Emails sent to your alias will be forwarded to this email address.
<br>

View File

@ -7,6 +7,7 @@
<div class="card-body p-6 text-center">
<h1 class="h4">An email to validate your email is on its way.</h1>
<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>
{% endblock %}

View File

@ -86,7 +86,7 @@
</head>
<body>
<div class="page">
{% if current_user.is_authenticated and current_user.should_show_upgrade_button() %}
{% if NOW.timestamp < 1701475201 and current_user.is_authenticated and current_user.should_show_upgrade_button() %}
<div class="alert alert-success text-center mb-0" role="alert">
Black Friday: $20 for the first year instead of $30. Available until December 1st.

View File

@ -59,26 +59,29 @@
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-12 col-lg-6 pt-1">
<form method="post">
<input type="hidden" name="form-name" value="create" />
{{ new_contact_form.csrf_token }}
{{ new_contact_form.email(class="form-control", placeholder="First Last <email@example.com>", autofocus=True) }}
{{ render_field_errors(new_contact_form.email) }}
<div class="small-text">Where do you want to send the email?</div>
{% if can_create_contacts %}
{% if can_create_contacts %}
<button class="btn btn-primary mt-2">Create reverse-alias</button>
{% else %}
<button disabled
title="Upgrade to premium to create reverse-aliases"
class="btn btn-primary mt-2">
Create reverse-alias
</button>
{% endif %}
</form>
</div>
<div class="row mb-5">
<div class="col-12 col-lg-6 pt-1">
<form method="post">
<input type="hidden" name="form-name" value="create" />
{{ new_contact_form.csrf_token }}
{{ new_contact_form.email(class="form-control", placeholder="First Last <email@example.com>", autofocus=True) }}
{{ render_field_errors(new_contact_form.email) }}
<div class="small-text">Where do you want to send the email?</div>
{% if can_create_contacts %}
<button class="btn btn-primary mt-2">Create reverse-alias</button>
{% else %}
<button disabled
title="Upgrade to premium to create reverse-aliases"
class="btn btn-primary mt-2">
Create reverse-alias
</button>
{% endif %}
</form>
</div>
{% endif %}
<div class="col-12 col-lg-6 pt-1">
<div class="float-right d-flex">
<form method="post">

View File

@ -17,7 +17,7 @@
<b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc.
<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 />
</div>
</div>
@ -93,6 +93,7 @@
</div>
<div class="row">
<div class="col p-1">
{{ csrf_form.csrf_token }}
<button type="submit" id="create" class="btn btn-primary mt-1">Create</button>
</div>
</div>

View File

@ -12,7 +12,7 @@
<div class="card-body">
<h1 class="h3">Two Factor Authentication - TOTP</h1>
<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>
<canvas id="qr"></canvas>
<script>

View File

@ -10,7 +10,7 @@
<div>{{ notification.message | safe }}</div>
<form method="post"
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>
</form>
</div>

View File

@ -57,19 +57,22 @@
{% endblock %}
{% block default_content %}
<div class="alert alert-info">
Black Friday Deal: 33% off on the yearly plan for the <b>first</b> year ($20 instead of $30).
<br>
Please use this coupon code
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="BF2023">BF2023</em> during the checkout.
<br>
<img src="/static/images/coupon.png" class="m-2" style="max-width: 300px">
<br>
Available until December 1, 2023.
</div>
{% if NOW.timestamp < 1701475201 %}
<div class="alert alert-info">
Black Friday Deal: 33% off on the yearly plan for the <b>first</b> year ($20 instead of $30).
<br>
Please use this coupon code
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="BF2023">BF2023</em> during the checkout.
<br>
<img src="/static/images/coupon.png" class="m-2" style="max-width: 300px">
<br>
Available until December 1, 2023.
</div>
{% endif %}
<div class="pb-8">
<div class="text-center mx-md-auto mb-8 mt-6">
<h1>Upgrade to unlock premium features</h1>

View File

@ -18,7 +18,7 @@
<br />
For generic questions, i.e. not related to your account, we recommend to post the question on
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
and other people with the same question can find the answer there.
</div>

View File

@ -1,17 +1,19 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Block an alias{% endblock %}
{% block title %}Deactivate an alias{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Block alias</h1>
<h1 class="h3">Deactivate alias</h1>
<p>
You are about to block the alias
You are about to deactivate the alias
<a href="mailto:{{ alias }}" target="_blank">{{ alias }}</a>
</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">
<button class="btn btn-warning">Confirm</button>
</form>

View File

@ -43,9 +43,8 @@ Note, if you are a paying Proton Mail user, you automatically receive the premiu
{% endcall %}
{% call text() %}
For any question, feedback or feature request, please join our
<a href="https://github.com/simple-login/app/discussions">GitHub forum</a>
.
For any question or feedback, 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
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
or follow our

View File

@ -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.
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.
Best,
@ -26,7 +27,8 @@ Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
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/
Twitter: https://twitter.com/simple_login

View File

@ -71,9 +71,10 @@ Please note that you can't create more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases
{% endif %}
{% call text() %}
For any question, feedback or feature request, please join our
<a href="https://github.com/simple-login/app/discussions">GitHub forum</a>
.
For any question or feedback,
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
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
or follow our

View File

@ -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.
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

View File

@ -4,6 +4,7 @@
{{ 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("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_text('Thanks,
<br />

View File

@ -4,4 +4,6 @@
Thank you for choosing SimpleLogin.
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 %}

View File

@ -7,7 +7,7 @@
{% endcall %}
{% 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 %}
{% call text() %}

View File

@ -9,7 +9,7 @@
<a href='https://simplelogin.io/' aria-label="SimpleLogin">
<img src="/static/logo-white.svg"
height="30px"
class="mb-3"
class="mt-3 mb-3"
alt="SimpleLogin logo">
</a>
<!-- 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.
</p>
<p class="small text-white">
SimpleLogin is the product of SimpleLogin SAS, registered in France under the SIREN number 884302134.
SimpleLogin SAS is part of <a href="https://proton.me">Proton AG</a>.
SimpleLogin is the product of <a href="https://proton.me">Proton AG</a>, registered in Switzerland under number CHE-354.686.492.
</p>
</div>
</div>
@ -38,12 +37,6 @@
alt="GitHub">
</a>
</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>
<a class="list-group-item text-white footer-item "
href="https://status.simplelogin.io/">Status</a>
@ -61,18 +54,10 @@
<a class="list-group-item text-white footer-item"
href="https://simplelogin.io/blog/">Blog</a>
</li>
<li>
<a class="list-group-item text-white footer-item"
href="https://simplelogin.io/job/">Join Us</a>
</li>
<li>
<a class="list-group-item text-white footer-item"
href="https://simplelogin.io/about/">About Us</a>
</li>
<li>
<a class="list-group-item text-white footer-item"
href="https://github.com/simple-login/app/projects/1">Roadmap</a>
</li>
<li>
<a class="list-group-item text-white footer-item"
href="https://simplelogin.io/contact/">Contact Us</a>
@ -106,37 +91,9 @@
<a class="list-group-item text-white footer-item "
href="https://simplelogin.io/docs/">Documentation</a>
</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>
<a class="list-group-item text-white footer-item"
href="https://simplelogin.io/blog/email-alias-vs-plus-sign/">
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>
<a class="list-group-item text-white footer-item "
href="https://forum.simplelogin.io">Forum</a>
</li>
</ul>
</div>

View File

@ -83,7 +83,15 @@
</a>
</div>
<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"
rel="noopener noreferrer">
Forum

View File

@ -106,7 +106,7 @@
</a>
</div>
<div class="dropdown-item">
<a href="https://github.com/simple-login/app/discussions"
<a href="https://forum.simplelogin.io/"
target="_blank"
rel="noopener noreferrer">
Forum

View File

@ -1,6 +1,7 @@
from flask import url_for
from app import config
from app.db import Session
from app.models import User, PartnerUser
from app.proton.utils import get_proton_partner
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,
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
"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,
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
"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):
r = flask_client.get(
url_for("api.user_info"), headers={"Authentication": "Invalid code"}

View 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

View File

@ -49,10 +49,25 @@ from app.models import (
VerpType,
AliasGeneratorEnum,
SLDomain,
Mailbox,
)
# 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_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():
@ -68,10 +83,6 @@ def test_email_belongs_to_alias_domains():
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):
# default alias domain
assert not email_can_be_used_as_mailbox("ab@sl.local")
@ -94,6 +105,27 @@ def test_can_be_used_as_personal_email(flask_client):
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():
msg = EmailMessage()
assert msg._headers == []