def test_available_suffixes(flask_client): user = login(flask_client) CustomDomain.create(user_id=user.id, domain="test.com", verified=True) assert len(get_available_suffixes(user)) > 0 # first suffix is custom domain first_suffix = get_available_suffixes(user)[0] assert first_suffix.is_custom assert first_suffix.suffix == "@test.com" assert first_suffix.signed_suffix.startswith("@test.com")
def test_available_suffixes_default_domain(flask_client): user = login(flask_client) sl_domain = SLDomain.query.first() CustomDomain.create(user_id=user.id, domain="test.com", verified=True, commit=True) user.default_alias_public_domain_id = sl_domain.id # first suffix is SL Domain first_suffix = get_available_suffixes(user)[0] assert first_suffix.suffix.endswith(f"@{sl_domain.domain}") user.default_alias_public_domain_id = None # first suffix is custom domain first_suffix = get_available_suffixes(user)[0] assert first_suffix.suffix == "@test.com"
def options_v4(): """ Return what options user has when creating new alias. Same as v3 but return time-based signed-suffix in addition to suffix. To be used with /v2/alias/custom/new Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README can_create: bool suffixes: [[suffix, signed_suffix]] prefix_suggestion: str recommendation: Optional dict alias: str hostname: str """ user = g.user hostname = request.args.get("hostname") ret = { "can_create": user.can_create_new_alias(), "suffixes": [], "prefix_suggestion": "", } # recommendation alias if exist if hostname: # put the latest used alias first q = (Session.query(AliasUsedOn, Alias, User).filter( AliasUsedOn.alias_id == Alias.id, Alias.user_id == user.id, AliasUsedOn.hostname == hostname, ).order_by(desc(AliasUsedOn.created_at))) r = q.first() if r: _, alias, _ = r LOG.d("found alias %s %s %s", alias, hostname, user) ret["recommendation"] = { "alias": alias.email, "hostname": hostname } # custom alias suggestion and suffix if hostname: # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon ext = tldextract.extract(hostname) prefix_suggestion = ext.domain prefix_suggestion = convert_to_id(prefix_suggestion) ret["prefix_suggestion"] = prefix_suggestion suffixes = get_available_suffixes(user) # custom domain should be put first ret["suffixes"] = list([suffix.suffix, suffix.signed_suffix] for suffix in suffixes) return jsonify(ret)
def test_available_suffixes_random_prefix_generation(flask_client): user = login(flask_client) CustomDomain.create(user_id=user.id, domain="test.com", verified=True, commit=True) cd2 = CustomDomain.create( user_id=user.id, domain="test2.com", verified=True, commit=True ) user.default_alias_custom_domain_id = cd2.id # first suffix is test2.com first_suffix = get_available_suffixes(user)[0] assert first_suffix.suffix == "@test2.com" cd2.random_prefix_generation = True # e.g. [email protected] first_suffix = get_available_suffixes(user)[0] assert first_suffix.suffix.endswith("@test2.com") assert first_suffix.suffix.startswith(".")
def authorize(): """ Redirected from client when user clicks on "Login with Server". This is a GET request with the following field in url - client_id - (optional) state - response_type: must be code """ oauth_client_id = request.args.get("client_id") state = request.args.get("state") scope = request.args.get("scope") redirect_uri = request.args.get("redirect_uri") response_mode = request.args.get("response_mode") nonce = request.args.get("nonce") try: response_types: [ResponseType] = get_response_types(request) except ValueError: return ( "response_type must be code, token, id_token or certain combination of these." " Please see /.well-known/openid-configuration to see what response_type are supported ", 400, ) if set(response_types) not in SUPPORTED_OPENID_FLOWS: return ( f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}", 400, ) if not redirect_uri: LOG.d("no redirect uri") return "redirect_uri must be set", 400 client = Client.get_by(oauth_client_id=oauth_client_id) if not client: final_redirect_uri = ( f"{redirect_uri}?error=invalid_client_id&client_id={oauth_client_id}" ) return redirect(final_redirect_uri) # check if redirect_uri is valid # allow localhost by default # allow any redirect_uri if the app isn't approved hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1" and client.approved: # support custom scheme for mobile app if scheme == "http": final_redirect_uri = f"{redirect_uri}?error=http_not_allowed" return redirect(final_redirect_uri) if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri): final_redirect_uri = f"{redirect_uri}?error=unknown_redirect_uri" return redirect(final_redirect_uri) # redirect from client website if request.method == "GET": if current_user.is_authenticated: suggested_email, other_emails, email_suffix = None, [], None suggested_name, other_names = None, [] # user has already allowed this client client_user: ClientUser = ClientUser.get_by( client_id=client.id, user_id=current_user.id) user_info = {} if client_user: LOG.d("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() # redirect user to the client page redirect_args = construct_redirect_args( client, client_user, nonce, redirect_uri, response_types, scope, state, ) fragment = get_fragment(response_mode, response_types) # construct redirect_uri with redirect_args return redirect( construct_url(redirect_uri, redirect_args, fragment)) else: suggested_email, other_emails = current_user.suggested_emails( client.name) suggested_name, other_names = current_user.suggested_names() user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] suffixes = get_available_suffixes(current_user) return render_template( "oauth/authorize.html", Scope=Scope, EMAIL_DOMAIN=EMAIL_DOMAIN, **locals(), ) else: # after user logs in, redirect user back to this page return render_template( "oauth/authorize_nonlogin_user.html", client=client, next=request.url, Scope=Scope, ) else: # POST - user allows or denies if not current_user.is_authenticated or not current_user.is_active: LOG.i( "Attempt to validate a OAUth allow request by an unauthenticated user" ) return redirect(url_for("auth.login", next=request.url)) if request.form.get("button") == "deny": LOG.d("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.d("User %s allows Client %s", current_user, client) client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id) # user has already allowed this client, user cannot change information if client_user: LOG.d("user %s has already allowed client %s", current_user, client) else: alias_prefix = request.form.get("prefix") signed_suffix = request.form.get("suffix") alias = None # user creates a new alias, not using suggested alias if alias_prefix: # should never happen as this is checked on the front-end if not current_user.can_create_new_alias(): raise Exception( f"User {current_user} cannot create custom email") alias_prefix = alias_prefix.strip().lower().replace(" ", "") if not check_alias_prefix(alias_prefix): flash( "Only lowercase letters, numbers, dashes (-), dots (.) and underscores (_) " "are currently supported for alias prefix. Cannot be more than 40 letters", "error", ) return redirect(request.url) # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.w("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(request.url) except Exception: LOG.w("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(request.url) user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] from app.dashboard.views.custom_alias import verify_prefix_suffix if verify_prefix_suffix(current_user, alias_prefix, alias_suffix): full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.e("alias %s already used, very rare!", full_alias) flash(f"Alias {full_alias} already used", "error") return redirect(request.url) else: alias = Alias.create( user_id=current_user.id, email=full_alias, mailbox_id=current_user.default_mailbox_id, ) Session.flush() flash(f"Alias {full_alias} has been created", "success") # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return redirect(request.url) # User chooses one of the suggestions else: chosen_email = request.form.get("suggested-email") # todo: add some checks on chosen_email if chosen_email != current_user.email: alias = Alias.get_by(email=chosen_email) if not alias: alias = Alias.create( email=chosen_email, user_id=current_user.id, mailbox_id=current_user.default_mailbox_id, ) Session.flush() suggested_name = request.form.get("suggested-name") custom_name = request.form.get("custom-name") use_default_avatar = request.form.get("avatar-choice") == "default" client_user = ClientUser.create(client_id=client.id, user_id=current_user.id) if alias: client_user.alias_id = alias.id if custom_name: client_user.name = custom_name elif suggested_name != current_user.name: client_user.name = suggested_name if use_default_avatar: # use default avatar LOG.d("use default avatar for user %s client %s", current_user, client) client_user.default_avatar = True Session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = construct_redirect_args(client, client_user, nonce, redirect_uri, response_types, scope, state) fragment = get_fragment(response_mode, response_types) # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))
def new_random_alias(): """ Create a new random alias Input: (Optional) note Output: 201 if success """ user = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create new random alias", user) return ( jsonify( error= f"You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) note = None data = request.get_json(silent=True) if data: note = data.get("note") alias = None # custom alias suggestion and suffix hostname = request.args.get("hostname") if hostname and user.include_website_in_one_click_alias: LOG.d("Use %s to create new alias", hostname) # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon ext = tldextract.extract(hostname) prefix_suggestion = ext.domain prefix_suggestion = convert_to_id(prefix_suggestion) suffixes = get_available_suffixes(user) # use the first suffix suggested_alias = prefix_suggestion + suffixes[0].suffix alias = Alias.get_by(email=suggested_alias) # cannot use this alias as it belongs to another user if alias and not alias.user_id == user.id: LOG.d("%s belongs to another user", alias) alias = None elif alias and alias.user_id == user.id: # make sure alias was created for this website if AliasUsedOn.get_by(alias_id=alias.id, hostname=hostname, user_id=alias.user_id): LOG.d("Use existing alias %s", alias) else: LOG.d("%s wasn't created for this website %s", alias, hostname) alias = None elif not alias: LOG.d("create new alias %s", suggested_alias) try: alias = Alias.create( user_id=user.id, email=suggested_alias, note=note, mailbox_id=user.default_mailbox_id, commit=True, ) except AliasInTrashError: LOG.i("Alias %s is in trash", suggested_alias) alias = None if not alias: scheme = user.alias_generator mode = request.args.get("mode") if mode: if mode == "word": scheme = AliasGeneratorEnum.word.value elif mode == "uuid": scheme = AliasGeneratorEnum.uuid.value else: return jsonify( error=f"{mode} must be either word or uuid"), 400 alias = Alias.create_new_random(user=user, scheme=scheme, note=note) Session.commit() if hostname and not AliasUsedOn.get_by(alias_id=alias.id, hostname=hostname): AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) Session.commit() return ( jsonify(alias=alias.email, **serialize_alias_info_v2(get_alias_info_v2(alias))), 201, )
def options_v5(): """ Return what options user has when creating new alias. Same as v4 but uses a better format. To be used with /v2/alias/custom/new Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README can_create: bool suffixes: [ { suffix: "suffix", signed_suffix: "signed_suffix" } ] prefix_suggestion: str recommendation: Optional dict alias: str hostname: str """ user = g.user hostname = request.args.get("hostname") ret = { "can_create": user.can_create_new_alias(), "suffixes": [], "prefix_suggestion": "", } # recommendation alias if exist if hostname: # put the latest used alias first q = ( db.session.query(AliasUsedOn, Alias, User) .filter( AliasUsedOn.alias_id == Alias.id, Alias.user_id == user.id, AliasUsedOn.hostname == hostname, ) .order_by(desc(AliasUsedOn.created_at)) ) r = q.first() if r: _, alias, _ = r LOG.d("found alias %s %s %s", alias, hostname, user) ret["recommendation"] = {"alias": alias.email, "hostname": hostname} # custom alias suggestion and suffix if hostname: # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon domain_name = hostname if "." in hostname: parts = hostname.split(".") domain_name = parts[-2] domain_name = convert_to_id(domain_name) ret["prefix_suggestion"] = domain_name suffixes = get_available_suffixes(user) # custom domain should be put first ret["suffixes"] = [ {"suffix": suffix.suffix, "signed_suffix": suffix.signed_suffix} for suffix in suffixes ] return jsonify(ret)