예제 #1
0
파일: test_models.py 프로젝트: ntung/app
def test_suggested_emails_for_user_who_cannot_create_new_alias(flask_client):
    # make sure user is not in trial
    user = User.create(
        email="[email protected]",
        password="******",
        name="Test User",
        activated=True,
        trial_end=None,
    )

    db.session.commit()

    # make sure user runs out of quota to create new email
    for i in range(MAX_NB_EMAIL_FREE_PLAN):
        GenEmail.create_new(user_id=user.id, prefix="test")
    db.session.commit()

    suggested_email, other_emails = user.suggested_emails(website_name="test")

    # the suggested email is chosen from existing GenEmail
    assert GenEmail.get_by(email=suggested_email)

    # all other emails are generated emails
    for email in other_emails:
        assert GenEmail.get_by(email=email)
예제 #2
0
def test_custom_mode(flask_client):
    user = User.create(email="[email protected]",
                       password="******",
                       name="Test User",
                       activated=True)
    db.session.commit()

    # create api_key
    api_key = ApiKey.create(user.id, "for test")
    db.session.commit()

    # without note
    r = flask_client.post(
        url_for("api.new_random_alias", hostname="www.test.com", mode="uuid"),
        headers={"Authentication": api_key.code},
    )

    assert r.status_code == 201
    # extract the uuid part
    alias = r.json["alias"]
    uuid_part = alias[:len(alias) - len(EMAIL_DOMAIN) - 1]
    assert is_valid_uuid(uuid_part)

    # with note
    r = flask_client.post(
        url_for("api.new_random_alias", hostname="www.test.com", mode="uuid"),
        headers={"Authentication": api_key.code},
        json={"note": "test note"},
    )

    assert r.status_code == 201
    alias = r.json["alias"]
    ge = GenEmail.get_by(email=alias)
    assert ge.note == "test note"
예제 #3
0
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
예제 #4
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("ony premium user can choose custom alias", "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")

        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):
                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)
                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id

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

    return render_template("dashboard/custom_alias.html", **locals())
예제 #5
0
def alias_log(alias):
    gen_email = GenEmail.get_by(email=alias)

    # sanity check
    if not gen_email:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    if gen_email.user_id != current_user.id:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    return render_template("dashboard/alias_log.html",
                           logs=get_alias_log(gen_email),
                           alias=alias)
예제 #6
0
def alias_contact_manager(alias, forward_email_id=None):
    gen_email = GenEmail.get_by(email=alias)

    # sanity check
    if not gen_email:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    if gen_email.user_id != current_user.id:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    new_contact_form = NewContactForm()

    if request.method == "POST":
        if request.form.get("form-name") == "create":
            if new_contact_form.validate():
                contact_email = new_contact_form.email.data

                # generate a reply_email, make sure it is unique
                # not use while to avoid infinite loop
                for _ in range(1000):
                    reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
                    if not ForwardEmail.get_by(reply_email=reply_email):
                        break

                website_email = get_email_part(contact_email)

                # already been added
                if ForwardEmail.get_by(
                    gen_email_id=gen_email.id, website_email=website_email
                ):
                    flash(f"{website_email} is already added", "error")
                    return redirect(
                        url_for("dashboard.alias_contact_manager", alias=alias)
                    )

                forward_email = ForwardEmail.create(
                    gen_email_id=gen_email.id,
                    website_email=website_email,
                    website_from=contact_email,
                    reply_email=reply_email,
                )

                LOG.d("create reverse-alias for %s", contact_email)
                db.session.commit()
                flash(
                    f"Reverse alias for {contact_email} is created successfully",
                    "success",
                )

                return redirect(
                    url_for(
                        "dashboard.alias_contact_manager",
                        alias=alias,
                        forward_email_id=forward_email.id,
                    )
                )
        elif request.form.get("form-name") == "delete":
            forward_email_id = request.form.get("forward-email-id")
            forward_email = ForwardEmail.get(forward_email_id)

            if not forward_email:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(url_for("dashboard.alias_contact_manager", alias=alias))
            elif forward_email.gen_email_id != gen_email.id:
                flash("You cannot delete reverse-alias", "warning")
                return redirect(url_for("dashboard.alias_contact_manager", alias=alias))

            contact_name = forward_email.website_from
            ForwardEmail.delete(forward_email_id)
            db.session.commit()

            flash(
                f"Reverse-alias for {contact_name} has been deleted successfully",
                "success",
            )

            return redirect(url_for("dashboard.alias_contact_manager", alias=alias))

    # make sure highlighted forward_email is at array start
    forward_emails = gen_email.forward_emails

    if forward_email_id:
        forward_emails = sorted(
            forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True
        )

    return render_template(
        "dashboard/alias_contact_manager.html",
        forward_emails=forward_emails,
        alias=gen_email.email,
        new_contact_form=new_contact_form,
        forward_email_id=forward_email_id,
    )
예제 #7
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,
    )
예제 #8
0
    def handle_forward(self, envelope, smtp: SMTP, msg: Message) -> str:
        """return *status_code message*"""
        alias = envelope.rcpt_tos[0].lower()  # alias@SL

        gen_email = GenEmail.get_by(email=alias)
        if not gen_email:
            LOG.d(
                "alias %s not exist. Try to see if it can be created on the fly",
                alias)

            # try to see if alias could be created on-the-fly
            on_the_fly = False

            # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
            if email_belongs_to_alias_domains(alias):
                if "/" in alias or "+" in alias or "#" in alias:
                    if "/" in alias:
                        sep = "/"
                    elif "+" in alias:
                        sep = "+"
                    else:
                        sep = "#"

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

                    directory = Directory.get_by(name=directory_name)

                    # Only premium user can use the directory feature
                    if directory:
                        dir_user = directory.user
                        if dir_user.is_premium():
                            LOG.d("create alias %s for directory %s", alias,
                                  directory)
                            on_the_fly = True

                            gen_email = GenEmail.create(
                                email=alias,
                                user_id=directory.user_id,
                                directory_id=directory.id,
                            )
                            db.session.commit()
                        else:
                            LOG.error(
                                "User %s is not premium anymore and cannot create alias with directory",
                                dir_user,
                            )
                            send_cannot_create_directory_alias(
                                dir_user, alias, directory_name)

            # 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
            if not on_the_fly:
                alias_domain = get_email_domain_part(alias)
                custom_domain = CustomDomain.get_by(domain=alias_domain)

                # Only premium user can continue using the catch-all feature
                if custom_domain and custom_domain.catch_all:
                    domain_user = custom_domain.user
                    if domain_user.is_premium():
                        LOG.d("create alias %s for domain %s", alias,
                              custom_domain)
                        on_the_fly = True

                        gen_email = GenEmail.create(
                            email=alias,
                            user_id=custom_domain.user_id,
                            custom_domain_id=custom_domain.id,
                            automatic_creation=True,
                        )
                        db.session.commit()
                    else:
                        LOG.error(
                            "User %s is not premium anymore and cannot create alias with domain %s",
                            domain_user,
                            alias_domain,
                        )
                        send_cannot_create_domain_alias(
                            domain_user, alias, alias_domain)

            if not on_the_fly:
                LOG.d("alias %s cannot be created on-the-fly, return 510",
                      alias)
                return "510 Email not exist"

        user_email = gen_email.user.email

        website_email = get_email_part(msg["From"])

        forward_email = ForwardEmail.get_by(gen_email_id=gen_email.id,
                                            website_email=website_email)
        if not forward_email:
            LOG.debug(
                "create forward email for alias %s and website email %s",
                alias,
                website_email,
            )

            # generate a reply_email, make sure it is unique
            # not use while to avoid infinite loop
            for _ in range(1000):
                reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
                if not ForwardEmail.get_by(reply_email=reply_email):
                    break

            forward_email = ForwardEmail.create(
                gen_email_id=gen_email.id,
                website_email=website_email,
                website_from=msg["From"],
                reply_email=reply_email,
            )
            db.session.commit()

        forward_log = ForwardEmailLog.create(forward_id=forward_email.id)

        if gen_email.enabled:
            # add custom header
            add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")

            # remove reply-to header if present
            delete_header(msg, "Reply-To")

            # change the from header so the sender comes from @SL
            # so it can pass DMARC check
            # replace the email part in from: header
            from_header = (get_email_name(msg["From"]) + " - " +
                           website_email.replace("@", " at ") +
                           f" <{forward_email.reply_email}>")
            msg.replace_header("From", from_header)
            LOG.d("new from header:%s", from_header)

            # add List-Unsubscribe header
            unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
            add_or_replace_header(msg, "List-Unsubscribe",
                                  f"<{unsubscribe_link}>")
            add_or_replace_header(msg, "List-Unsubscribe-Post",
                                  "List-Unsubscribe=One-Click")

            add_dkim_signature(msg, EMAIL_DOMAIN)

            LOG.d(
                "Forward mail from %s to %s, mail_options %s, rcpt_options %s ",
                website_email,
                user_email,
                envelope.mail_options,
                envelope.rcpt_options,
            )

            # smtp.send_message has UnicodeEncodeErroremail issue
            # encode message raw directly instead
            msg_raw = msg.as_string().encode()
            smtp.sendmail(
                forward_email.reply_email,
                user_email,
                msg_raw,
                envelope.mail_options,
                envelope.rcpt_options,
            )
        else:
            LOG.d("%s is disabled, do not forward", gen_email)
            forward_log.blocked = True

        db.session.commit()
        return "250 Message accepted for delivery"
예제 #9
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())
예제 #10
0
    def handle_forward(self, envelope, smtp: SMTP, msg: EmailMessage) -> str:
        """return *status_code message*"""
        alias = envelope.rcpt_tos[0]  # alias@SL

        gen_email = GenEmail.get_by(email=alias)
        if not gen_email:
            LOG.d("alias %s not exist")
            return "510 Email not exist"

        user_email = gen_email.user.email

        website_email = get_email_part(msg["From"])

        forward_email = ForwardEmail.get_by(
            gen_email_id=gen_email.id, website_email=website_email
        )
        if not forward_email:
            LOG.debug(
                "create forward email for alias %s and website email %s",
                alias,
                website_email,
            )

            # generate a reply_email, make sure it is unique
            # not use while to avoid infinite loop
            for _ in range(1000):
                reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
                if not ForwardEmail.get_by(reply_email=reply_email):
                    break

            forward_email = ForwardEmail.create(
                gen_email_id=gen_email.id,
                website_email=website_email,
                website_from=msg["From"],
                reply_email=reply_email,
            )
            db.session.commit()

        forward_log = ForwardEmailLog.create(forward_id=forward_email.id)

        if gen_email.enabled:
            # add custom header
            add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")

            # remove reply-to header if present
            if msg["Reply-To"]:
                LOG.d("Delete reply-to header %s", msg["Reply-To"])
                del msg["Reply-To"]

            # change the from header so the sender comes from @SL
            # so it can pass DMARC check
            # replace the email part in from: header
            from_header = (
                get_email_name(msg["From"])
                + " - "
                + website_email.replace("@", " at ")
                + f" <{forward_email.reply_email}>"
            )
            msg.replace_header("From", from_header)
            LOG.d("new from header:%s", from_header)

            # add List-Unsubscribe header
            unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
            add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
            add_or_replace_header(
                msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
            )

            add_dkim_signature(msg, EMAIL_DOMAIN)

            LOG.d(
                "Forward mail from %s to %s, mail_options %s, rcpt_options %s ",
                website_email,
                user_email,
                envelope.mail_options,
                envelope.rcpt_options,
            )

            # smtp.send_message has UnicodeEncodeErroremail issue
            # encode message raw directly instead
            msg_raw = msg.as_string().encode()
            smtp.sendmail(
                forward_email.reply_email,
                user_email,
                msg_raw,
                envelope.mail_options,
                envelope.rcpt_options,
            )
        else:
            LOG.d("%s is disabled, do not forward", gen_email)
            forward_log.blocked = True

        db.session.commit()
        return "250 Message accepted for delivery"
예제 #11
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(),
    )
예제 #12
0
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
예제 #13
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)
                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,
                    ))

            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")
            alias_suffix = request.form.get("suffix")

            gen_email = 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")

                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, user_custom_domains):
                    full_alias = alias_prefix + alias_suffix

                    if GenEmail.get_by(
                            email=full_alias) or DeletedAlias.get_by(
                                email=full_alias):
                        LOG.error("alias %s already used, very rare!",
                                  full_alias)
                        flash(f"Alias {full_alias} already used", "error")
                        return redirect(request.url)
                    else:
                        gen_email = GenEmail.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
                        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.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:
                    gen_email = GenEmail.get_by(email=chosen_email)
                    if not gen_email:
                        gen_email = GenEmail.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 gen_email:
                client_user.gen_email_id = gen_email.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))
예제 #14
0
def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
    """return *status_code message*"""
    alias = rcpt_to.lower()  # alias@SL

    gen_email = GenEmail.get_by(email=alias)
    if not gen_email:
        LOG.d("alias %s not exist. Try to see if it can be created on the fly", alias)
        gen_email = try_auto_create(alias)
        if not gen_email:
            LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
            return "510 Email not exist"

    mailbox = gen_email.mailbox
    mailbox_email = mailbox.email

    # create PGP email if needed
    if mailbox.pgp_finger_print:
        LOG.d("Encrypt message using mailbox %s", mailbox)
        msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)

    forward_email = get_or_create_forward_email(msg["From"], gen_email)
    forward_log = ForwardEmailLog.create(forward_id=forward_email.id)

    if gen_email.enabled:
        # add custom header
        add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")

        # remove reply-to & sender header if present
        delete_header(msg, "Reply-To")
        delete_header(msg, "Sender")

        # change the from header so the sender comes from @SL
        # so it can pass DMARC check
        # replace the email part in from: header
        website_from_header = msg["From"]
        website_email = get_email_part(website_from_header)
        from_header = (
            get_email_name(website_from_header)
            + ("" if get_email_name(website_from_header) == "" else " - ")
            + website_email.replace("@", " at ")
            + f" <{forward_email.reply_email}>"
        )
        add_or_replace_header(msg, "From", from_header)
        LOG.d("new from header:%s", from_header)

        # append alias into the TO header if it's not present in To or CC
        if should_append_alias(msg, alias):
            LOG.d("append alias %s  to TO header %s", alias, msg["To"])
            if msg["To"]:
                to_header = msg["To"] + "," + alias
            else:
                to_header = alias

            add_or_replace_header(msg, "To", to_header)

        # add List-Unsubscribe header
        unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
        add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
        add_or_replace_header(
            msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
        )

        add_dkim_signature(msg, EMAIL_DOMAIN)

        LOG.d(
            "Forward mail from %s to %s, mail_options %s, rcpt_options %s ",
            website_email,
            mailbox_email,
            envelope.mail_options,
            envelope.rcpt_options,
        )

        # smtp.send_message has UnicodeEncodeErroremail issue
        # encode message raw directly instead
        msg_raw = msg.as_string().encode()
        smtp.sendmail(
            forward_email.reply_email,
            mailbox_email,
            msg_raw,
            envelope.mail_options,
            envelope.rcpt_options,
        )
    else:
        LOG.d("%s is disabled, do not forward", gen_email)
        forward_log.blocked = True

    db.session.commit()
    return "250 Message accepted for delivery"
예제 #15
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))