Example #1
0
def delete_alias(alias: Alias, user: User):
    """
    Delete an alias and add it to either global or domain trash
    Should be used instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create
    """
    # save deleted alias to either global or domain trash
    if alias.custom_domain_id:
        if not DomainDeletedAlias.get_by(
            email=alias.email, domain_id=alias.custom_domain_id
        ):
            LOG.debug("add %s to domain %s trash", alias, alias.custom_domain_id)
            db.session.add(
                DomainDeletedAlias(
                    user_id=user.id, email=alias.email, domain_id=alias.custom_domain_id
                )
            )
            db.session.commit()
    else:
        if not DeletedAlias.get_by(email=alias.email):
            LOG.d("add %s to global trash", alias)
            db.session.add(DeletedAlias(email=alias.email))
            db.session.commit()

    Alias.query.filter(Alias.id == alias.id).delete()
    db.session.commit()
Example #2
0
def import_from_csv(batch_import: BatchImport, user: User, lines):
    reader = csv.DictReader(lines)

    for row in reader:
        try:
            full_alias = sanitize_email(row["alias"])
            note = row["note"]
        except KeyError:
            LOG.warning("Cannot parse row %s", row)
            continue

        alias_domain = get_email_domain_part(full_alias)
        custom_domain = CustomDomain.get_by(domain=alias_domain)

        if (not custom_domain or not custom_domain.verified
                or custom_domain.user_id != user.id):
            LOG.debug("domain %s can't be used %s", alias_domain, user)
            continue

        if (Alias.get_by(email=full_alias)
                or DeletedAlias.get_by(email=full_alias)
                or DomainDeletedAlias.get_by(email=full_alias)):
            LOG.d("alias already used %s", full_alias)
            continue

        mailboxes = []

        if "mailboxes" in row:
            for mailbox_email in row["mailboxes"].split():
                mailbox_email = sanitize_email(mailbox_email)
                mailbox = Mailbox.get_by(email=mailbox_email)

                if not mailbox or not mailbox.verified or mailbox.user_id != user.id:
                    LOG.d("mailbox %s can't be used %s", mailbox, user)
                    continue

                mailboxes.append(mailbox.id)

        if len(mailboxes) == 0:
            mailboxes = [user.default_mailbox_id]

        alias = Alias.create(
            user_id=user.id,
            email=full_alias,
            note=note,
            mailbox_id=mailboxes[0],
            custom_domain_id=custom_domain.id,
            batch_import_id=batch_import.id,
        )
        db.session.commit()
        db.session.flush()
        LOG.d("Create %s", alias)

        for i in range(1, len(mailboxes)):
            alias_mailbox = AliasMailbox.create(
                alias_id=alias.id,
                mailbox_id=mailboxes[i],
            )
            db.session.commit()
            LOG.d("Create %s", alias_mailbox)
Example #3
0
def try_auto_create_directory(address: str) -> Optional[Alias]:
    """
    Try to create an alias with directory
    """
    # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
    if email_belongs_to_alias_domains(address):
        # if there's no directory separator in the alias, no way to auto-create it
        if "/" not in address and "+" not in address and "#" not in address:
            return None

        # alias contains one of the 3 special directory separator: "/", "+" or "#"
        if "/" in address:
            sep = "/"
        elif "+" in address:
            sep = "+"
        else:
            sep = "#"

        directory_name = address[:address.find(sep)]
        LOG.d("directory_name %s", directory_name)

        directory = Directory.get_by(name=directory_name)
        if not directory:
            return None

        dir_user: User = directory.user

        if not dir_user.can_create_new_alias():
            send_cannot_create_directory_alias(dir_user, address,
                                               directory_name)
            return None

        # if alias has been deleted before, do not auto-create it
        if DeletedAlias.get_by(email=address):
            LOG.warning(
                "Alias %s was deleted before, cannot auto-create using directory %s, user %s",
                address,
                directory_name,
                dir_user,
            )
            return None

        LOG.d("create alias %s for directory %s", address, directory)

        alias = Alias.create(
            email=address,
            user_id=directory.user_id,
            directory_id=directory.id,
            mailbox_id=dir_user.default_mailbox_id,
        )

        db.session.commit()
        return alias
Example #4
0
def handle_batch_import(batch_import: BatchImport):
    user = batch_import.user

    batch_import.processed = True
    db.session.commit()

    LOG.debug("Start batch import for %s %s", batch_import, user)
    file_url = s3.get_url(batch_import.file.path)

    LOG.d("Download file %s from %s", batch_import.file, file_url)
    r = requests.get(file_url)
    lines = [l.decode() for l in r.iter_lines()]
    reader = csv.DictReader(lines)

    for row in reader:
        try:
            full_alias = row["alias"].lower().strip().replace(" ", "")
            note = row["note"]
        except KeyError:
            LOG.warning("Cannot parse row %s", row)
            continue

        alias_domain = get_email_domain_part(full_alias)
        custom_domain = CustomDomain.get_by(domain=alias_domain)

        if (
            not custom_domain
            or not custom_domain.verified
            or custom_domain.user_id != user.id
        ):
            LOG.debug("domain %s can't be used %s", alias_domain, user)
            continue

        if (
            Alias.get_by(email=full_alias)
            or DeletedAlias.get_by(email=full_alias)
            or DomainDeletedAlias.get_by(email=full_alias)
        ):
            LOG.d("alias already used %s", full_alias)
            continue

        alias = Alias.create(
            user_id=user.id,
            email=full_alias,
            note=note,
            mailbox_id=user.default_mailbox_id,
            custom_domain_id=custom_domain.id,
            batch_import_id=batch_import.id,
        )
        db.session.commit()
        LOG.d("Create %s", alias)
Example #5
0
def test_delete_alias(flask_client):
    user = User.create(email="[email protected]",
                       password="******",
                       name="Test User",
                       activated=True)
    db.session.commit()
    alias = Alias.create(user_id=user.id,
                         email="*****@*****.**",
                         mailbox_id=user.default_mailbox_id)
    db.session.commit()
    assert Alias.get_by(email="*****@*****.**")

    delete_alias(alias, user)
    assert Alias.get_by(email="*****@*****.**") is None
    assert DeletedAlias.get_by(email=alias.email)
Example #6
0
def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
    """Try to create an alias with catch-all domain"""

    # try to create alias on-the-fly with custom-domain catch-all feature
    # check if alias is custom-domain alias and if the custom-domain has catch-all enabled
    alias_domain = get_email_domain_part(address)
    custom_domain = CustomDomain.get_by(domain=alias_domain)

    if not custom_domain:
        return None

    # custom_domain exists
    if not custom_domain.catch_all:
        return None

    # custom_domain has catch-all enabled
    domain_user: User = custom_domain.user

    if not domain_user.can_create_new_alias():
        send_cannot_create_domain_alias(domain_user, address, alias_domain)
        return None

    # if alias has been deleted before, do not auto-create it
    if DeletedAlias.get_by(email=address):
        LOG.warning(
            "Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
            address,
            custom_domain,
            domain_user,
        )
        return None

    LOG.d("create alias %s for domain %s", address, custom_domain)

    alias = Alias.create(
        email=address,
        user_id=custom_domain.user_id,
        custom_domain_id=custom_domain.id,
        automatic_creation=True,
        mailbox_id=domain_user.default_mailbox_id,
    )

    db.session.commit()
    return alias
Example #7
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        LOG.warning("user %s tries to create custom alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [
        cd.domain for cd in current_user.verified_custom_domains()
    ]
    suffixes = available_suffixes_more_info(current_user)
    at_least_a_premium_domain = False
    for suffix in suffixes:
        if not suffix.is_custom and suffix.is_premium:
            at_least_a_premium_domain = True
            break

    mailboxes = current_user.mailboxes()

    if request.method == "POST":
        alias_prefix = request.form.get("prefix").strip().lower().replace(
            " ", "")
        signed_suffix = request.form.get("suffix")
        mailbox_ids = request.form.getlist("mailboxes")
        alias_note = request.form.get("note")

        if not check_alias_prefix(alias_prefix):
            flash(
                "Only lowercase letters, numbers, dashes (-) and underscores (_) "
                "are currently supported for alias prefix. Cannot be more than 40 letters",
                "error",
            )
            return redirect(url_for("dashboard.custom_alias"))

        # 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 != current_user.id
                    or not mailbox.verified):
                flash("Something went wrong, please retry", "warning")
                return redirect(url_for("dashboard.custom_alias"))
            mailboxes.append(mailbox)

        if not mailboxes:
            flash("At least one mailbox must be selected", "error")
            return redirect(url_for("dashboard.custom_alias"))

        # 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", current_user)
            flash("Alias creation time is expired, please retry", "warning")
            return redirect(url_for("dashboard.custom_alias"))
        except Exception:
            LOG.warning("Alias suffix is tampered, user %s", current_user)
            flash("Unknown error, refresh the page", "error")
            return redirect(url_for("dashboard.custom_alias"))

        if verify_prefix_suffix(current_user, alias_prefix, alias_suffix):
            full_alias = alias_prefix + alias_suffix

            general_error_msg = f"{full_alias} cannot be used"

            if Alias.get_by(email=full_alias):
                alias = Alias.get_by(email=full_alias)
                if alias.user_id == current_user.id:
                    flash(f"You already have this alias {full_alias}", "error")
                else:
                    flash(general_error_msg, "error")
            elif DomainDeletedAlias.get_by(email=full_alias):
                domain_deleted_alias: DomainDeletedAlias = DomainDeletedAlias.get_by(
                    email=full_alias)
                custom_domain = domain_deleted_alias.domain
                if domain_deleted_alias.user_id == current_user.id:
                    flash(
                        f"You have deleted this alias before. You can restore it on "
                        f"{custom_domain.domain} 'Deleted Alias' page",
                        "error",
                    )
                else:
                    # should never happen as user can only choose their domains
                    LOG.exception(
                        "Deleted Alias %s does not belong to user %s",
                        domain_deleted_alias,
                    )

            elif DeletedAlias.get_by(email=full_alias):
                flash(general_error_msg, "error")

            else:
                custom_domain_id = None
                # get the custom_domain_id if alias is created with a custom domain
                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):
                        flash(
                            f"Alias {full_alias} is currently in the {domain.domain} trash. "
                            f"Please remove it from the trash in order to re-create it.",
                            "warning",
                        )
                        return redirect(url_for("dashboard.custom_alias"))

                    if domain:
                        custom_domain_id = domain.id

                try:
                    alias = Alias.create(
                        user_id=current_user.id,
                        email=full_alias,
                        note=alias_note,
                        mailbox_id=mailboxes[0].id,
                        custom_domain_id=custom_domain_id,
                    )
                    db.session.flush()
                except IntegrityError:
                    LOG.warning("Alias %s already exists", full_alias)
                    db.session.rollback()
                    flash("Unknown error, please retry", "error")
                    return redirect(url_for("dashboard.custom_alias"))

                for i in range(1, len(mailboxes)):
                    AliasMailbox.create(
                        alias_id=alias.id,
                        mailbox_id=mailboxes[i].id,
                    )

                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_alias_id=alias.id))
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template(
        "dashboard/custom_alias.html",
        user_custom_domains=user_custom_domains,
        suffixes=suffixes,
        at_least_a_premium_domain=at_least_a_premium_domain,
        mailboxes=mailboxes,
    )
Example #8
0
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))
Example #9
0
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,
    )
Example #10
0
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
Example #11
0
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
    hostname, scheme = get_host_name_and_scheme(redirect_uri)
    if hostname != "localhost" and hostname != "127.0.0.1":
        # 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.debug("user %s has already allowed client %s",
                          current_user, client)
                user_info = client_user.get_user_info()
            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()
                ]
                # List of (is_custom_domain, alias-suffix, time-signed alias-suffix)
                suffixes = 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 request.form.get("button") == "deny":
            LOG.debug("User %s denies Client %s", current_user, client)
            final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
            return redirect(final_redirect_uri)

        LOG.debug("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")

                # 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",
                                current_user)
                    flash("Alias creation time is expired, please retry",
                          "warning")
                    return redirect(request.url)
                except Exception:
                    LOG.exception("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.exception("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,
                        )

                        # get the custom_domain_id if alias is created with a custom domain
                        if alias_suffix.startswith("@"):
                            alias_domain = alias_suffix[1:]
                            domain = CustomDomain.get_by(domain=alias_domain)
                            if domain:
                                alias.custom_domain_id = domain.id

                        db.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,
                        )
                        db.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

            db.session.flush()
            LOG.d("create client-user for client %s, user %s", client,
                  current_user)

        redirect_args = {}

        if state:
            redirect_args["state"] = state
        else:
            LOG.warning(
                "more security reason, state should be added. client %s",
                client)

        if scope:
            redirect_args["scope"] = scope

        auth_code = None
        if ResponseType.CODE in response_types:
            # Create authorization code
            auth_code = AuthorizationCode.create(
                client_id=client.id,
                user_id=current_user.id,
                code=random_string(),
                scope=scope,
                redirect_uri=redirect_uri,
                response_type=response_types_to_str(response_types),
            )
            db.session.add(auth_code)
            redirect_args["code"] = auth_code.code

        oauth_token = None
        if ResponseType.TOKEN in response_types:
            # create access-token
            oauth_token = OauthToken.create(
                client_id=client.id,
                user_id=current_user.id,
                scope=scope,
                redirect_uri=redirect_uri,
                access_token=generate_access_token(),
                response_type=response_types_to_str(response_types),
            )
            db.session.add(oauth_token)
            redirect_args["access_token"] = oauth_token.access_token

        if ResponseType.ID_TOKEN in response_types:
            redirect_args["id_token"] = make_id_token(
                client_user,
                nonce,
                oauth_token.access_token if oauth_token else None,
                auth_code.code if auth_code else None,
            )

        db.session.commit()

        # should all params appended the url using fragment (#) or query
        fragment = False

        if response_mode and response_mode == "fragment":
            fragment = True

        # if response_types contain "token" => implicit flow => should use fragment
        # except if client sets explicitly response_mode
        if not response_mode:
            if ResponseType.TOKEN in response_types:
                fragment = True

        # construct redirect_uri with redirect_args
        return redirect(construct_url(redirect_uri, redirect_args, fragment))
Example #12
0
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,
    )
Example #13
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()]
    # List of (is_custom_domain, alias-suffix)
    suffixes = []

    # put custom domain first
    for alias_domain in user_custom_domains:
        suffixes.append((True, "@" + alias_domain))

    # then default domain
    for domain in ALIAS_DOMAINS:
        suffixes.append(
            (
                False,
                ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain,
            )
        )

    if request.method == "POST":
        alias_prefix = request.form.get("prefix")
        alias_suffix = request.form.get("suffix")
        alias_note = request.form.get("note")

        if verify_prefix_suffix(
            current_user, alias_prefix, alias_suffix, user_custom_domains
        ):
            full_alias = alias_prefix + alias_suffix

            if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
                email=full_alias
            ):
                LOG.d("full alias already used %s", full_alias)
                flash(
                    f"Alias {full_alias} already exists, please choose another one",
                    "warning",
                )
            else:
                gen_email = GenEmail.create(
                    user_id=current_user.id, email=full_alias, note=alias_note
                )

                # get the custom_domain_id if alias is created with a custom domain
                alias_domain = get_email_domain_part(full_alias)
                custom_domain = CustomDomain.get_by(domain=alias_domain)
                if custom_domain:
                    gen_email.custom_domain_id = custom_domain.id

                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_gen_email_id=gen_email.id)
                )
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template("dashboard/custom_alias.html", **locals())
Example #14
0
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
    hostname, scheme = get_host_name_and_scheme(redirect_uri)
    if hostname != "localhost" and hostname != "127.0.0.1":
        # 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.debug("user %s has already allowed client %s", current_user, client)
                user_info = client_user.get_user_info()
            else:
                suggested_email, other_emails = current_user.suggested_emails(
                    client.name
                )
                suggested_name, other_names = current_user.suggested_names()
                email_suffix = random_word()

            return render_template(
                "oauth/authorize.html",
                client=client,
                user_info=user_info,
                client_user=client_user,
                Scope=Scope,
                suggested_email=suggested_email,
                personal_email=current_user.email,
                suggested_name=suggested_name,
                other_names=other_names,
                other_emails=other_emails,
                email_suffix=email_suffix,
                EMAIL_DOMAIN=EMAIL_DOMAIN,
            )
        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:  # user allows or denies
        if request.form.get("button") == "deny":
            LOG.debug("User %s denies Client %s", current_user, client)
            final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
            return redirect(final_redirect_uri)

        LOG.debug("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:
            email_suffix = request.form.get("email-suffix")
            custom_email_prefix = request.form.get("custom-email-prefix")
            chosen_email = request.form.get("suggested-email")

            suggested_name = request.form.get("suggested-name")
            custom_name = request.form.get("custom-name")

            use_default_avatar = request.form.get("avatar-choice") == "default"

            gen_email = None
            if custom_email_prefix:
                # check if user can generate custom email
                if not current_user.can_create_new_alias():
                    raise Exception(f"User {current_user} cannot create custom email")

                email = f"{convert_to_id(custom_email_prefix)}.{email_suffix}@{EMAIL_DOMAIN}"
                LOG.d("create custom email alias %s for user %s", email, current_user)

                if GenEmail.get_by(email=email) or DeletedAlias.get_by(email=email):
                    LOG.error("email %s already used, very rare!", email)
                    flash(f"alias {email} already used", "error")
                    return redirect(request.url)

                gen_email = GenEmail.create(email=email, user_id=current_user.id)
                db.session.flush()
            else:  # user picks an email from suggestion
                if chosen_email != current_user.email:
                    gen_email = GenEmail.get_by(email=chosen_email)
                    if not gen_email:
                        gen_email = GenEmail.create(
                            email=chosen_email, user_id=current_user.id
                        )
                        db.session.flush()

            client_user = ClientUser.create(
                client_id=client.id, user_id=current_user.id
            )
            if gen_email:
                client_user.gen_email_id = gen_email.id

            if custom_name:
                LOG.d(
                    "use custom name %s for user %s client %s",
                    custom_name,
                    current_user,
                    client,
                )
                client_user.name = custom_name
            elif suggested_name != current_user.name:
                LOG.d(
                    "use another name %s for user %s client %s",
                    custom_name,
                    current_user,
                    client,
                )
                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

            db.session.flush()
            LOG.d("create client-user for client %s, user %s", client, current_user)

        redirect_args = {}

        if state:
            redirect_args["state"] = state
        else:
            LOG.warning(
                "more security reason, state should be added. client %s", client
            )

        if scope:
            redirect_args["scope"] = scope

        auth_code = None
        if ResponseType.CODE in response_types:
            # Create authorization code
            auth_code = AuthorizationCode.create(
                client_id=client.id,
                user_id=current_user.id,
                code=random_string(),
                scope=scope,
                redirect_uri=redirect_uri,
                response_type=response_types_to_str(response_types),
            )
            db.session.add(auth_code)
            redirect_args["code"] = auth_code.code

        oauth_token = None
        if ResponseType.TOKEN in response_types:
            # create access-token
            oauth_token = OauthToken.create(
                client_id=client.id,
                user_id=current_user.id,
                scope=scope,
                redirect_uri=redirect_uri,
                access_token=generate_access_token(),
                response_type=response_types_to_str(response_types),
            )
            db.session.add(oauth_token)
            redirect_args["access_token"] = oauth_token.access_token

        if ResponseType.ID_TOKEN in response_types:
            redirect_args["id_token"] = make_id_token(
                client_user,
                nonce,
                oauth_token.access_token if oauth_token else None,
                auth_code.code if auth_code else None,
            )

        db.session.commit()

        # should all params appended the url using fragment (#) or query
        fragment = False

        if response_mode and response_mode == "fragment":
            fragment = True

        # if response_types contain "token" => implicit flow => should use fragment
        # except if client sets explicitly response_mode
        if not response_mode:
            if ResponseType.TOKEN in response_types:
                fragment = True

        # construct redirect_uri with redirect_args
        return redirect(construct_url(redirect_uri, redirect_args, fragment))
Example #15
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        LOG.d("%s can't create new alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [
        cd.domain for cd in current_user.verified_custom_domains()
    ]
    alias_suffixes = get_alias_suffixes(current_user)
    at_least_a_premium_domain = False
    for alias_suffix in alias_suffixes:
        if not alias_suffix.is_custom and alias_suffix.is_premium:
            at_least_a_premium_domain = True
            break

    alias_suffixes_with_signature = [
        (alias_suffix, signer.sign(alias_suffix.serialize()).decode())
        for alias_suffix in alias_suffixes
    ]

    mailboxes = current_user.mailboxes()

    if request.method == "POST":
        alias_prefix = request.form.get("prefix").strip().lower().replace(
            " ", "")
        signed_alias_suffix = request.form.get("signed-alias-suffix")
        mailbox_ids = request.form.getlist("mailboxes")
        alias_note = request.form.get("note")

        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)

        # 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 != current_user.id
                    or not mailbox.verified):
                flash("Something went wrong, please retry", "warning")
                return redirect(request.url)
            mailboxes.append(mailbox)

        if not mailboxes:
            flash("At least one mailbox must be selected", "error")
            return redirect(request.url)

        # hypothesis: user will click on the button in the 600 secs
        try:
            signed_alias_suffix_decoded = signer.unsign(signed_alias_suffix,
                                                        max_age=600).decode()
            alias_suffix: AliasSuffix = AliasSuffix.deserialize(
                signed_alias_suffix_decoded)
        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)

        if verify_prefix_suffix(current_user, alias_prefix,
                                alias_suffix.suffix):
            full_alias = alias_prefix + alias_suffix.suffix

            if ".." in full_alias:
                flash("Your alias can't contain 2 consecutive dots (..)",
                      "error")
                return redirect(request.url)

            try:
                validate_email(full_alias,
                               check_deliverability=False,
                               allow_smtputf8=False)
            except EmailNotValidError as e:
                flash(str(e), "error")
                return redirect(request.url)

            general_error_msg = f"{full_alias} cannot be used"

            if Alias.get_by(email=full_alias):
                alias = Alias.get_by(email=full_alias)
                if alias.user_id == current_user.id:
                    flash(f"You already have this alias {full_alias}", "error")
                else:
                    flash(general_error_msg, "error")
            elif DomainDeletedAlias.get_by(email=full_alias):
                domain_deleted_alias: DomainDeletedAlias = DomainDeletedAlias.get_by(
                    email=full_alias)
                custom_domain = domain_deleted_alias.domain
                if domain_deleted_alias.user_id == current_user.id:
                    flash(
                        f"You have deleted this alias before. You can restore it on "
                        f"{custom_domain.domain} 'Deleted Alias' page",
                        "error",
                    )
                else:
                    # should never happen as user can only choose their domains
                    LOG.e(
                        "Deleted Alias %s does not belong to user %s",
                        domain_deleted_alias,
                    )

            elif DeletedAlias.get_by(email=full_alias):
                flash(general_error_msg, "error")

            else:
                try:
                    alias = Alias.create(
                        user_id=current_user.id,
                        email=full_alias,
                        note=alias_note,
                        mailbox_id=mailboxes[0].id,
                    )
                    Session.flush()
                except IntegrityError:
                    LOG.w("Alias %s already exists", full_alias)
                    Session.rollback()
                    flash("Unknown error, please retry", "error")
                    return redirect(url_for("dashboard.custom_alias"))

                for i in range(1, len(mailboxes)):
                    AliasMailbox.create(
                        alias_id=alias.id,
                        mailbox_id=mailboxes[i].id,
                    )

                Session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_alias_id=alias.id))
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template(
        "dashboard/custom_alias.html",
        user_custom_domains=user_custom_domains,
        alias_suffixes_with_signature=alias_suffixes_with_signature,
        at_least_a_premium_domain=at_least_a_premium_domain,
        mailboxes=mailboxes,
    )
Example #16
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [
        cd.domain for cd in current_user.verified_custom_domains()
    ]
    # List of (is_custom_domain, alias-suffix, time-signed alias-suffix)
    suffixes = available_suffixes(current_user)

    mailboxes = current_user.mailboxes()

    if request.method == "POST":
        alias_prefix = request.form.get("prefix").strip().lower()
        signed_suffix = request.form.get("suffix")
        mailbox_ids = request.form.getlist("mailboxes")
        alias_note = request.form.get("note")

        # 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 != current_user.id
                    or not mailbox.verified):
                flash("Something went wrong, please retry", "warning")
                return redirect(url_for("dashboard.custom_alias"))
            mailboxes.append(mailbox)

        if not mailboxes:
            flash("At least one mailbox must be selected", "error")
            return redirect(url_for("dashboard.custom_alias"))

        # 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", current_user)
            flash("Alias creation time is expired, please retry", "warning")
            return redirect(url_for("dashboard.custom_alias"))
        except Exception:
            LOG.error("Alias suffix is tampered, user %s", current_user)
            flash("Unknown error, refresh the page", "error")
            return redirect(url_for("dashboard.custom_alias"))

        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.d("full alias already used %s", full_alias)
                flash(
                    f"Alias {full_alias} already exists, please choose another one",
                    "warning",
                )
            else:
                custom_domain_id = None
                # get the custom_domain_id if alias is created with a custom domain
                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):
                        flash(
                            f"Alias {full_alias} is currently in the {domain.domain} trash. "
                            f"Please remove it from the trash in order to re-create it.",
                            "warning",
                        )
                        return redirect(url_for("dashboard.custom_alias"))

                    if domain:
                        custom_domain_id = domain.id

                alias = Alias.create(
                    user_id=current_user.id,
                    email=full_alias,
                    note=alias_note,
                    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()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_alias_id=alias.id))
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template(
        "dashboard/custom_alias.html",
        user_custom_domains=user_custom_domains,
        suffixes=suffixes,
        mailboxes=mailboxes,
    )
Example #17
0
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(),
    )
Example #18
0
def setting():
    form = SettingForm()
    promo_form = PromoCodeForm()

    email_change = EmailChange.get_by(user_id=current_user.id)
    if email_change:
        pending_email = email_change.new_email
    else:
        pending_email = None

    if request.method == "POST":
        if request.form.get("form-name") == "update-profile":
            if form.validate():
                profile_updated = False
                # update user info
                if form.name.data != current_user.name:
                    current_user.name = form.name.data
                    db.session.commit()
                    profile_updated = True

                if form.profile_picture.data:
                    file_path = random_string(30)
                    file = File.create(path=file_path)

                    s3.upload_from_bytesio(
                        file_path, BytesIO(form.profile_picture.data.read()))

                    db.session.flush()
                    LOG.d("upload file %s to s3", file)

                    current_user.profile_picture_id = file.id
                    db.session.commit()
                    profile_updated = True

                if profile_updated:
                    flash(f"Your profile has been updated", "success")

                if (form.email.data and form.email.data != current_user.email
                        and not pending_email):
                    new_email = form.email.data

                    # check if this email is not used by other user, or as alias
                    if (User.get_by(email=new_email)
                            or GenEmail.get_by(email=new_email)
                            or DeletedAlias.get_by(email=new_email)):
                        flash(f"Email {new_email} already used", "error")
                    elif new_email.endswith(EMAIL_DOMAIN):
                        flash(
                            "You cannot use alias as your personal inbox. Nice try though 😉",
                            "error",
                        )
                    else:
                        email_change = EmailChange.create(
                            user_id=current_user.id,
                            code=random_string(
                                60),  # todo: make sure the code is unique
                            new_email=new_email,
                        )
                        db.session.commit()
                        send_change_email_confirmation(current_user,
                                                       email_change)
                        flash(
                            "A confirmation email is on the way, please check your inbox",
                            "success",
                        )

        elif request.form.get("form-name") == "change-password":
            send_reset_password_email(current_user)

        elif request.form.get("form-name") == "notification-preference":
            choose = request.form.get("notification")
            if choose == "on":
                current_user.notification = True
            else:
                current_user.notification = False
            db.session.commit()
            flash("Your notification preference has been updated", "success")

        elif request.form.get("form-name") == "delete-account":
            User.delete(current_user.id)
            db.session.commit()
            flash("Your account has been deleted", "success")
            logout_user()
            return redirect(url_for("auth.register"))

        elif request.form.get("form-name") == "change-alias-generator":
            scheme = int(request.form.get("alias-generator-scheme"))
            if AliasGeneratorEnum.has_value(scheme):
                current_user.alias_generator = scheme
                db.session.commit()
            flash("Your preference has been updated", "success")

        elif request.form.get("form-name") == "export-data":
            data = {
                "email": current_user.email,
                "name": current_user.name,
                "aliases": [],
                "apps": [],
                "custom_domains": [],
            }

            for alias in GenEmail.filter_by(
                    user_id=current_user.id).all():  # type: GenEmail
                data["aliases"].append(
                    dict(email=alias.email, enabled=alias.enabled))

            for custom_domain in CustomDomain.filter_by(
                    user_id=current_user.id).all():
                data["custom_domains"].append(custom_domain.domain)

            for app in Client.filter_by(
                    user_id=current_user.id):  # type: Client
                data["apps"].append(
                    dict(name=app.name,
                         home_url=app.home_url,
                         published=app.published))

            return Response(
                json.dumps(data),
                mimetype="text/json",
                headers={
                    "Content-Disposition": "attachment;filename=data.json"
                },
            )

        return redirect(url_for("dashboard.setting"))

    return render_template(
        "dashboard/setting.html",
        form=form,
        PlanEnum=PlanEnum,
        promo_form=promo_form,
        pending_email=pending_email,
        AliasGeneratorEnum=AliasGeneratorEnum,
    )
Example #19
0
def mailbox_detail_route(mailbox_id):
    mailbox = Mailbox.get(mailbox_id)
    if not mailbox or mailbox.user_id != current_user.id:
        flash("You cannot see this page", "warning")
        return redirect(url_for("dashboard.index"))

    change_email_form = ChangeEmailForm()

    if mailbox.new_email:
        pending_email = mailbox.new_email
    else:
        pending_email = None

    if request.method == "POST":
        if (
            request.form.get("form-name") == "update-email"
            and change_email_form.validate_on_submit()
        ):
            new_email = change_email_form.email.data
            if new_email != mailbox.email and not pending_email:
                # check if this email is not already used
                if (
                    mailbox_already_used(new_email, current_user)
                    or Alias.get_by(email=new_email)
                    or DeletedAlias.get_by(email=new_email)
                ):
                    flash(f"Email {new_email} already used", "error")
                elif not email_domain_can_be_used_as_mailbox(new_email):
                    flash("You cannot use this email address as your mailbox", "error")
                else:
                    mailbox.new_email = new_email
                    db.session.commit()

                    s = Signer(MAILBOX_SECRET)
                    mailbox_id_signed = s.sign(str(mailbox.id)).decode()
                    verification_url = (
                        URL
                        + "/dashboard/mailbox/confirm_change"
                        + f"?mailbox_id={mailbox_id_signed}"
                    )

                    try:
                        send_email(
                            new_email,
                            f"Confirm mailbox change on SimpleLogin",
                            render(
                                "transactional/verify-mailbox-change.txt",
                                user=current_user,
                                link=verification_url,
                                mailbox_email=mailbox.email,
                                mailbox_new_email=new_email,
                            ),
                            render(
                                "transactional/verify-mailbox-change.html",
                                user=current_user,
                                link=verification_url,
                                mailbox_email=mailbox.email,
                                mailbox_new_email=new_email,
                            ),
                        )
                    except SMTPRecipientsRefused:
                        flash(
                            f"Incorrect mailbox, please recheck {mailbox.email}",
                            "error",
                        )
                    else:
                        flash(
                            f"You are going to receive an email to confirm {new_email}.",
                            "success",
                        )
                    return redirect(
                        url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
                    )
        elif request.form.get("form-name") == "force-spf":
            if not ENFORCE_SPF:
                flash("SPF enforcement globally not enabled", "error")
                return redirect(url_for("dashboard.index"))

            mailbox.force_spf = (
                True if request.form.get("spf-status") == "on" else False
            )
            db.session.commit()
            flash(
                "SPF enforcement was " + "enabled"
                if request.form.get("spf-status")
                else "disabled" + " succesfully",
                "success",
            )
            return redirect(
                url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
            )
        elif request.form.get("form-name") == "pgp":
            if request.form.get("action") == "save":
                if not current_user.is_premium():
                    flash("Only premium plan can add PGP Key", "warning")
                    return redirect(
                        url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
                    )

                mailbox.pgp_public_key = request.form.get("pgp")
                try:
                    mailbox.pgp_finger_print = load_public_key(mailbox.pgp_public_key)
                except PGPException:
                    flash("Cannot add the public key, please verify it", "error")
                else:
                    db.session.commit()
                    flash("Your PGP public key is saved successfully", "success")
                    return redirect(
                        url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
                    )
            elif request.form.get("action") == "remove":
                # Free user can decide to remove their added PGP key
                mailbox.pgp_public_key = None
                mailbox.pgp_finger_print = None
                db.session.commit()
                flash("Your PGP public key is removed successfully", "success")
                return redirect(
                    url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
                )

    spf_available = ENFORCE_SPF
    return render_template("dashboard/mailbox_detail.html", **locals())