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 generate_reply_email(contact_email: str) -> str: """ generate a reply_email (aka reverse-alias), make sure it isn't used by any contact """ # shorten email to avoid exceeding the 64 characters # from https://tools.ietf.org/html/rfc5321#section-4.5.3 # "The maximum total length of a user name or other local-part is 64 # octets." if contact_email: # control char: 4 chars (ra+, +) # random suffix: max 10 chars # maximum: 64 # make sure contact_email can be ascii-encoded contact_email = convert_to_id(contact_email) contact_email = contact_email.lower().strip().replace(" ", "") contact_email = contact_email[:45] contact_email = contact_email.replace("@", ".at.") # not use while to avoid infinite loop for _ in range(1000): if contact_email: random_length = random.randint(5, 10) reply_email = ( f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}" ) else: random_length = random.randint(10, 50) reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): return reply_email raise Exception("Cannot generate reply email")
def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error="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, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip() alias_suffix = data.get("alias_suffix", "").strip() note = data.get("note") alias_prefix = convert_to_id(alias_prefix) if not verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create( user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note ) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_alias), 201
def generate_oauth_client_id(client_name) -> str: oauth_client_id = convert_to_id(client_name) + "-" + random_string() # check that the client does not exist yet if not Client.get_by(oauth_client_id=oauth_client_id): LOG.debug("generate oauth_client_id %s", oauth_client_id) return oauth_client_id # Rerun the function LOG.warning("client_id %s already exists, generate a new client_id", oauth_client_id) return generate_oauth_client_id(client_name)
def verify_prefix_suffix(user: User, alias_prefix, alias_suffix) -> bool: """verify if user could create an alias with the given prefix and suffix""" if not alias_prefix or not alias_suffix: # should be caught on frontend return False user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() # alias_domain_prefix is either a .random_word or "" alias_domain_prefix, alias_domain = alias_suffix.split("@", 1) # alias_domain must be either one of user custom domains or built-in domains if alias_domain not in user.available_alias_domains(): LOG.exception("wrong alias suffix %s, user %s", alias_suffix, user) return False # SimpleLogin domain case: # 1) alias_suffix must start with "." and # 2) alias_domain_prefix must come from the word list if (alias_domain in user.available_sl_domains() and alias_domain not in user_custom_domains # when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty and not DISABLE_ALIAS_SUFFIX): if not alias_domain_prefix.startswith("."): LOG.exception("User %s submits a wrong alias suffix %s", user, alias_suffix) return False random_word_part = alias_domain_prefix[1:] if not word_exist(random_word_part): LOG.exception( "alias suffix %s needs to start with a random word, user %s", alias_suffix, user, ) return False else: if alias_domain not in user_custom_domains: if not DISABLE_ALIAS_SUFFIX: LOG.exception("wrong alias suffix %s, user %s", alias_suffix, user) return False if alias_domain not in user.available_sl_domains(): LOG.exception("wrong alias suffix %s, user %s", alias_suffix, user) return False return True
def normalize_reply_email(reply_email: str) -> str: """Handle the case where reply email contains *strange* char that was wrongly generated in the past""" if not reply_email.isascii(): reply_email = convert_to_id(reply_email) ret = [] # drop all control characters like shift, separator, etc for c in reply_email: if c not in _ALLOWED_CHARS: ret.append("_") else: ret.append(c) return "".join(ret)
def verify_prefix_suffix(user, alias_prefix, alias_suffix) -> bool: """verify if user could create an alias with the given prefix and suffix""" if not alias_prefix or not alias_suffix: # should be caught on frontend return False user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] # alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, one of the default ALIAS_DOMAINS if DISABLE_ALIAS_SUFFIX: if (alias_domain not in user_custom_domains and alias_domain not in ALIAS_DOMAINS): LOG.exception("wrong alias suffix %s, user %s", alias_suffix, user) return False else: if alias_domain not in user_custom_domains: LOG.exception("wrong alias suffix %s, user %s", alias_suffix, user) return False else: if not alias_suffix.startswith("."): LOG.exception("User %s submits a wrong alias suffix %s", user, alias_suffix) return False full_alias = alias_prefix + alias_suffix if not email_belongs_to_alias_domains(full_alias): LOG.exception( "Alias suffix should end with one of the alias domains %s", user, alias_suffix, ) return False random_word_part = alias_suffix[1:alias_suffix.find("@")] if not word_exist(random_word_part): LOG.exception( "alias suffix %s needs to start with a random word, user %s", alias_suffix, user, ) return False return True
def suggested_emails(self, website_name) -> (str, [str]): """return suggested email and other email choices """ website_name = convert_to_id(website_name) all_gen_emails = [ge.email for ge in GenEmail.filter_by(user_id=self.id)] if self.can_create_new_alias(): suggested_gen_email = GenEmail.create_new(self, prefix=website_name).email else: # pick an email from the list of gen emails suggested_gen_email = random.choice(all_gen_emails) return ( suggested_gen_email, list(set(all_gen_emails).difference({suggested_gen_email})), )
def generate_reply_email(contact_email: str, user: User) -> str: """ generate a reply_email (aka reverse-alias), make sure it isn't used by any contact """ # shorten email to avoid exceeding the 64 characters # from https://tools.ietf.org/html/rfc5321#section-4.5.3 # "The maximum total length of a user name or other local-part is 64 # octets." # todo: turns this to False after Dec 20 2020 include_sender_in_reverse_alias = True # user has chosen an option explicitly if user.include_sender_in_reverse_alias is not None: include_sender_in_reverse_alias = user.include_sender_in_reverse_alias if include_sender_in_reverse_alias and contact_email: # control char: 4 chars (ra+, +) # random suffix: max 10 chars # maximum: 64 # make sure contact_email can be ascii-encoded contact_email = convert_to_id(contact_email) contact_email = contact_email.lower().strip().replace(" ", "") contact_email = contact_email[:45] contact_email = contact_email.replace("@", ".at.") contact_email = convert_to_alphanumeric(contact_email) # not use while to avoid infinite loop for _ in range(1000): if include_sender_in_reverse_alias and contact_email: random_length = random.randint(5, 10) reply_email = ( f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}" ) else: random_length = random.randint(20, 50) reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): return reply_email raise Exception("Cannot generate reply email")
def verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains) -> bool: """verify if user could create an alias with the given prefix and suffix""" alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) if not alias_prefix: # should be caught on frontend return False # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] # alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, EMAIL_DOMAIN if DISABLE_ALIAS_SUFFIX: if alias_domain not in user_custom_domains and alias_domain != EMAIL_DOMAIN: LOG.error("wrong alias suffix %s, user %s", alias_suffix, user) return False else: if alias_domain not in user_custom_domains: LOG.error("wrong alias suffix %s, user %s", alias_suffix, user) return False else: if not alias_suffix.startswith("."): LOG.error("User %s submits a wrong alias suffix %s", user, alias_suffix) return False if not alias_suffix.endswith(EMAIL_DOMAIN): LOG.error( "Alias suffix should end with default alias domain %s", user, alias_suffix, ) return False random_word_part = alias_suffix[1 : alias_suffix.find("@")] if not word_exist(random_word_part): LOG.error( "alias suffix %s needs to start with a random word, user %s", alias_suffix, user, ) return False return True
def generate_reply_email(contact_email: str, user: User) -> str: """ generate a reply_email (aka reverse-alias), make sure it isn't used by any contact """ # shorten email to avoid exceeding the 64 characters # from https://tools.ietf.org/html/rfc5321#section-4.5.3 # "The maximum total length of a user name or other local-part is 64 # octets." include_sender_in_reverse_alias = False # user has set this option explicitly if user.include_sender_in_reverse_alias is not None: include_sender_in_reverse_alias = user.include_sender_in_reverse_alias if include_sender_in_reverse_alias and contact_email: # make sure contact_email can be ascii-encoded contact_email = convert_to_id(contact_email) contact_email = sanitize_email(contact_email) contact_email = contact_email[:45] contact_email = contact_email.replace("@", ".at.") contact_email = convert_to_alphanumeric(contact_email) # not use while to avoid infinite loop for _ in range(1000): if include_sender_in_reverse_alias and contact_email: random_length = random.randint(5, 10) reply_email = ( # do not use the ra+ anymore # f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}" f"{contact_email}_{random_string(random_length)}@{EMAIL_DOMAIN}" ) else: random_length = random.randint(20, 50) # do not use the ra+ anymore # reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}" reply_email = f"{random_string(random_length)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): return reply_email raise Exception("Cannot generate reply email")
def custom_alias(): # check if user has the right to create custom alias if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash("ony premium user can choose custom alias", "warning") return redirect(url_for("dashboard.index")) error = "" if request.method == "POST": if request.form.get("form-name") == "non-custom-domain-name": email_prefix = request.form.get("email-prefix") email_prefix = convert_to_id(email_prefix) email_suffix = request.form.get("email-suffix") if not email_prefix: error = "alias prefix cannot be empty" else: full_email = f"{email_prefix}.{email_suffix}@{EMAIL_DOMAIN}" # check if email already exists if GenEmail.get_by(email=full_email) or DeletedAlias.get_by( email=full_email): error = "email already chosen, please choose another one" else: # create the new alias LOG.d("create custom alias %s for user %s", full_email, current_user) gen_email = GenEmail.create(email=full_email, user_id=current_user.id) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) elif request.form.get("form-name") == "custom-domain-name": custom_domain_id = request.form.get("custom-domain-id") email = request.form.get("email") custom_domain = CustomDomain.get(custom_domain_id) if not custom_domain: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif custom_domain.user_id != current_user.id: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif not custom_domain.verified: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) full_email = f"{email}@{custom_domain.domain}" if GenEmail.get_by(email=full_email): error = f"{full_email} already exist, please choose another one" else: LOG.d( "create custom alias %s for custom domain %s", full_email, custom_domain.domain, ) gen_email = GenEmail.create( email=full_email, user_id=current_user.id, custom_domain_id=custom_domain.id, ) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) email_suffix = random_word() return render_template( "dashboard/custom_alias.html", error=error, email_suffix=email_suffix, EMAIL_DOMAIN=EMAIL_DOMAIN, custom_domains=current_user.verified_custom_domains(), )
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(): """ Return what options user has when creating new alias. Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README optional recommendation: optional custom can_create_custom: boolean existing: array of existing aliases """ user = g.user hostname = request.args.get("hostname") ret = { "existing": [ge.email for ge in GenEmail.query.filter_by(user_id=user.id)], "can_create_custom": user.can_create_new_alias(), } # recommendation alias if exist if hostname: # put the latest used alias first q = (db.session.query(AliasUsedOn, GenEmail, User).filter( AliasUsedOn.gen_email_id == GenEmail.id, GenEmail.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 ret["custom"] = {} 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["custom"]["suggestion"] = domain_name else: ret["custom"]["suggestion"] = "" # maybe better to make sure the suffix is never used before # but this is ok as there's a check when creating a new custom alias ret["custom"]["suffixes"] = [f".{random_word()}@{EMAIL_DOMAIN}"] for custom_domain in user.verified_custom_domains(): ret["custom"]["suffixes"].append("@" + custom_domain.domain) # custom domain should be put first ret["custom"]["suffixes"] = list(reversed(ret["custom"]["suffixes"])) return jsonify(ret)
def new_custom_alias(): """ Currently used by Safari extension. Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ LOG.warning("/alias/custom/new is obsolete") user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "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, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") alias_suffix = data.get("alias_suffix", "").strip().lower().replace(" ", "") note = data.get("note") alias_prefix = convert_to_id(alias_prefix) if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 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.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 alias = Alias.create(user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note) if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) if domain: LOG.d("set alias %s to domain %s", full_alias, domain) alias.custom_domain_id = domain.id db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return jsonify(alias=full_alias, **serialize_alias_info(get_alias_info(alias))), 201
def new_custom_alias_v3(): """ Create a new custom alias Same as v2 but accept a list of mailboxes as input Input: alias_prefix, for ex "www_groupon_com" signed_suffix, either [email protected] or @my-domain.com mailbox_ids: list of int optional "hostname" in args optional "note" optional "name" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "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, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") signed_suffix = data.get("signed_suffix", "").strip() mailbox_ids = data.get("mailbox_ids") note = data.get("note") name = data.get("name") if name: name = name.replace("\n", "") alias_prefix = convert_to_id(alias_prefix) if not check_alias_prefix(alias_prefix): return jsonify(error="alias prefix invalid format or too long"), 400 # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != user.id or not mailbox.verified: return jsonify(error="Errors with Mailbox"), 400 mailboxes.append(mailbox) if not mailboxes: return jsonify(error="At least one mailbox must be selected"), 400 # 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.warning("Alias creation time expired for %s", user) return jsonify( error="Alias creation time is expired, please retry"), 412 except Exception: LOG.warning("Alias suffix is tampered, user %s", user) return jsonify(error="Tampered suffix"), 400 if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 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.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 custom_domain_id = None if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) if domain: custom_domain_id = domain.id alias = Alias.create( user_id=user.id, email=full_alias, note=note, name=name or None, mailbox_id=mailboxes[0].id, custom_domain_id=custom_domain_id, ) db.session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return ( jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))), 201, )
def new_custom_alias_v2(): """ Create a new custom alias Same as v1 but signed_suffix is actually the suffix with signature, e.g. [email protected] Input: alias_prefix, for ex "www_groupon_com" signed_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "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, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") signed_suffix = data.get("signed_suffix", "").strip() note = data.get("note") alias_prefix = convert_to_id(alias_prefix) # 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.warning("Alias creation time expired for %s", user) return jsonify( error="Alias creation time is expired, please retry"), 412 except Exception: LOG.warning("Alias suffix is tampered, user %s", user) return jsonify(error="Tampered suffix"), 400 if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 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.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 custom_domain_id = None if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) # check if the alias is currently in the domain trash if domain and DomainDeletedAlias.get_by(domain_id=domain.id, email=full_alias): LOG.d( f"Alias {full_alias} is currently in the {domain.domain} trash. " ) return jsonify(error=f"alias {full_alias} in domain trash"), 409 if domain: custom_domain_id = domain.id alias = Alias.create( user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note, custom_domain_id=custom_domain_id, ) db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return ( jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))), 201, )
def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args Output: 201 if success 409 if alias already exists """ user = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create custom alias", user) return ( jsonify( error="You have created 3 custom aliases, please upgrade to create more" ), 400, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() alias_prefix = data["alias_prefix"] alias_suffix = data["alias_suffix"] # make sure alias_prefix is not empty alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) if not alias_prefix: # should be checked on frontend LOG.d("user %s submits empty alias prefix %s", user, alias_prefix) return jsonify(error="alias prefix cannot be empty"), 400 # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() if alias_suffix.startswith("@"): custom_domain = alias_suffix[1:] if custom_domain not in user_custom_domains: LOG.d("user %s submits wrong custom domain %s ", user, custom_domain) return jsonify(error="error"), 400 else: if not alias_suffix.startswith("."): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 if not alias_suffix.endswith(EMAIL_DOMAIN): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 random_letters = alias_suffix[1 : alias_suffix.find("@")] if len(random_letters) < 5: LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create(user_id=user.id, email=full_alias) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_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 # List of (is_custom_domain, alias-suffix, time-signed alias-suffix) suffixes = available_suffixes(user) # custom domain should be put first ret["suffixes"] = [ {"suffix": suffix[1], "signed_suffix": suffix[2]} for suffix in suffixes ] return jsonify(ret)
def suggested_names(self) -> (str, [str]): """return suggested name and other name choices """ other_name = convert_to_id(self.name) return self.name, [other_name, "Anonymous", "whoami"]
def options_v2(): """ Return what options user has when creating new alias. Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README can_create: bool suffixes: [str] prefix_suggestion: str existing: [str] recommendation: Optional dict alias: str hostname: str """ LOG.exception("/v2/alias/options is obsolete") user = g.user hostname = request.args.get("hostname") ret = { "existing": [ ge.email for ge in Alias.query.filter_by(user_id=user.id, enabled=True) ], "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 # maybe better to make sure the suffix is never used before # but this is ok as there's a check when creating a new custom alias for domain in ALIAS_DOMAINS: if DISABLE_ALIAS_SUFFIX: ret["suffixes"].append(f"@{domain}") else: ret["suffixes"].append(f".{random_word()}@{domain}") for custom_domain in user.verified_custom_domains(): ret["suffixes"].append("@" + custom_domain.domain) # custom domain should be put first ret["suffixes"] = list(reversed(ret["suffixes"])) return jsonify(ret)