コード例 #1
0
def facebook_callback():
    # user clicks on cancel
    if "error" in request.args:
        flash("Please use another sign in method then", "warning")
        return redirect("/")

    facebook = OAuth2Session(
        FACEBOOK_CLIENT_ID,
        state=session["oauth_state"],
        scope=_scope,
        redirect_uri=_redirect_uri,
    )
    facebook = facebook_compliance_fix(facebook)
    token = facebook.fetch_token(
        _token_url,
        client_secret=FACEBOOK_CLIENT_SECRET,
        authorization_response=request.url,
    )

    # Fetch a protected resource, i.e. user profile
    # {
    #     "email": "*****@*****.**",
    #     "id": "1234",
    #     "name": "First Last",
    #     "picture": {
    #         "data": {
    #             "url": "long_url"
    #         }
    #     }
    # }
    facebook_user_data = facebook.get(
        "https://graph.facebook.com/me?fields=id,name,email,picture{url}"
    ).json()

    email = facebook_user_data.get("email")

    # user choose to not share email, cannot continue
    if not email:
        flash("In order to use SimpleLogin, you need to give us a valid email",
              "warning")
        return redirect(url_for("auth.register"))

    user = User.get_by(email=email)

    picture_url = facebook_user_data.get("picture", {}).get("data",
                                                            {}).get("url")

    if user:
        if picture_url and not user.profile_picture_id:
            LOG.d("set user profile picture to %s", picture_url)
            file = create_file_from_url(picture_url)
            user.profile_picture_id = file.id
            db.session.commit()

    # create user
    else:
        if DISABLE_REGISTRATION:
            flash("Registration is closed", "error")
            return redirect(url_for("auth.login"))

        if not can_be_used_as_personal_email(email) or email_already_used(
                email):
            flash(f"You cannot use {email} as your personal inbox.", "error")
            return redirect(url_for("auth.login"))

        LOG.d("create facebook user with %s", facebook_user_data)
        user = User.create(email=email.lower(),
                           name=facebook_user_data["name"],
                           activated=True)

        if picture_url:
            LOG.d("set user profile picture to %s", picture_url)
            file = create_file_from_url(picture_url)
            user.profile_picture_id = file.id

        db.session.commit()
        login_user(user)
        email_utils.send_welcome_email(user)

        flash(f"Welcome to SimpleLogin {user.name}!", "success")

    next_url = None
    # The activation link contains the original page, for ex authorize page
    if "facebook_next_url" in session:
        next_url = session["facebook_next_url"]
        LOG.debug("redirect user to %s", next_url)

        # reset the next_url to avoid user getting redirected at each login :)
        session.pop("facebook_next_url", None)

    if not SocialAuth.get_by(user_id=user.id, social="facebook"):
        SocialAuth.create(user_id=user.id, social="facebook")
        db.session.commit()

    return after_login(user, next_url)
コード例 #2
0
ファイル: email_utils.py プロジェクト: mobornak/app
def send_email(
    to_email,
    subject,
    plaintext,
    html=None,
    unsubscribe_link=None,
    unsubscribe_via_email=False,
):
    to_email = sanitize_email(to_email)
    if NOT_SEND_EMAIL:
        LOG.d(
            "send email with subject '%s' to '%s', plaintext: %s",
            subject,
            to_email,
            plaintext,
        )
        return

    LOG.d("send email to %s, subject %s", to_email, subject)

    if POSTFIX_SUBMISSION_TLS:
        smtp = SMTP(POSTFIX_SERVER, 587)
        smtp.starttls()
    else:
        smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT or 25)

    msg = MIMEMultipart("alternative")
    msg.attach(MIMEText(plaintext))

    if not html:
        LOG.d("Use plaintext as html")
        html = plaintext.replace("\n", "<br>")
    msg.attach(MIMEText(html, "html"))

    msg["Subject"] = subject
    msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>"
    msg["To"] = to_email

    msg_id_header = make_msgid()
    msg["Message-ID"] = msg_id_header

    date_header = formatdate()
    msg["Date"] = date_header

    if unsubscribe_link:
        add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
        if not unsubscribe_via_email:
            add_or_replace_header(
                msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
            )

    # add DKIM
    email_domain = SUPPORT_EMAIL[SUPPORT_EMAIL.find("@") + 1 :]
    add_dkim_signature(msg, email_domain)

    msg_raw = to_bytes(msg)

    transaction = TransactionalEmail.get_by(email=to_email)
    if not transaction:
        transaction = TransactionalEmail.create(email=to_email, commit=True)

    # use a different envelope sender for each transactional email (aka VERP)
    smtp.sendmail(TRANSACTIONAL_BOUNCE_EMAIL.format(transaction.id), to_email, msg_raw)
コード例 #3
0
ファイル: authorize.py プロジェクト: stephensong/app
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
                        )

                        # 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
                        )
                        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))
コード例 #4
0
def fake_data():
    LOG.d("create fake data")
    # Remove db if exist
    if os.path.exists("db.sqlite"):
        LOG.d("remove existing db file")
        os.remove("db.sqlite")

    # Create all tables
    db.create_all()

    # Create a user
    user = User.create(
        email="*****@*****.**",
        name="John Wick",
        password="******",
        activated=True,
        is_admin=True,
        otp_secret="base32secret3232",
    )
    db.session.commit()

    # Create a subscription for user
    Subscription.create(
        user_id=user.id,
        cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
        update_url="https://checkout.paddle.com/subscription/update?user=1234",
        subscription_id="123",
        event_time=arrow.now(),
        next_bill_date=arrow.now().shift(days=10).date(),
        plan=PlanEnum.monthly,
    )
    db.session.commit()

    api_key = ApiKey.create(user_id=user.id, name="Chrome")
    api_key.code = "code"

    GenEmail.create_new(user.id, "e1@")
    GenEmail.create_new(user.id, "e2@")
    GenEmail.create_new(user.id, "e3@")

    CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
    CustomDomain.create(user_id=user.id,
                        domain="very-long-domain.com.net.org",
                        verified=True)
    db.session.commit()

    # Create a client
    client1 = Client.create_new(name="Demo", user_id=user.id)
    client1.oauth_client_id = "client-id"
    client1.oauth_client_secret = "client-secret"
    client1.published = True
    db.session.commit()

    RedirectUri.create(client_id=client1.id, uri="https://ab.com")

    client2 = Client.create_new(name="Demo 2", user_id=user.id)
    client2.oauth_client_id = "client-id2"
    client2.oauth_client_secret = "client-secret2"
    client2.published = True
    db.session.commit()

    db.session.commit()
コード例 #5
0
ファイル: domain_detail.py プロジェクト: simple-login/app
def domain_detail(custom_domain_id):
    custom_domain: CustomDomain = CustomDomain.get(custom_domain_id)
    mailboxes = current_user.mailboxes()

    if not custom_domain or custom_domain.user_id != current_user.id:
        flash("You cannot see this page", "warning")
        return redirect(url_for("dashboard.index"))

    if request.method == "POST":
        if request.form.get("form-name") == "switch-catch-all":
            custom_domain.catch_all = not custom_domain.catch_all
            Session.commit()

            if custom_domain.catch_all:
                flash(
                    f"The catch-all has been enabled for {custom_domain.domain}",
                    "success",
                )
            else:
                flash(
                    f"The catch-all has been disabled for {custom_domain.domain}",
                    "warning",
                )
            return redirect(
                url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)
            )
        elif request.form.get("form-name") == "set-name":
            if request.form.get("action") == "save":
                custom_domain.name = request.form.get("alias-name").replace("\n", "")
                Session.commit()
                flash(
                    f"Default alias name for Domain {custom_domain.domain} has been set",
                    "success",
                )
            else:
                custom_domain.name = None
                Session.commit()
                flash(
                    f"Default alias name for Domain {custom_domain.domain} has been removed",
                    "info",
                )

            return redirect(
                url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)
            )
        elif request.form.get("form-name") == "switch-random-prefix-generation":
            custom_domain.random_prefix_generation = (
                not custom_domain.random_prefix_generation
            )
            Session.commit()

            if custom_domain.random_prefix_generation:
                flash(
                    f"Random prefix generation has been enabled for {custom_domain.domain}",
                    "success",
                )
            else:
                flash(
                    f"Random prefix generation has been disabled for {custom_domain.domain}",
                    "warning",
                )
            return redirect(
                url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)
            )
        elif request.form.get("form-name") == "update":
            mailbox_ids = request.form.getlist("mailbox_ids")
            # 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.domain_detail", custom_domain_id=custom_domain.id
                        )
                    )
                mailboxes.append(mailbox)

            if not mailboxes:
                flash("You must select at least 1 mailbox", "warning")
                return redirect(
                    url_for(
                        "dashboard.domain_detail", custom_domain_id=custom_domain.id
                    )
                )

            # first remove all existing domain-mailboxes links
            DomainMailbox.filter_by(domain_id=custom_domain.id).delete()
            Session.flush()

            for mailbox in mailboxes:
                DomainMailbox.create(domain_id=custom_domain.id, mailbox_id=mailbox.id)

            Session.commit()
            flash(f"{custom_domain.domain} mailboxes has been updated", "success")

            return redirect(
                url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)
            )

        elif request.form.get("form-name") == "delete":
            name = custom_domain.domain
            LOG.d("Schedule deleting %s", custom_domain)

            # Schedule delete domain job
            LOG.w("schedule delete domain job for %s", custom_domain)
            Job.create(
                name=JOB_DELETE_DOMAIN,
                payload={"custom_domain_id": custom_domain.id},
                run_at=arrow.now(),
                commit=True,
            )

            flash(
                f"{name} scheduled for deletion."
                f"You will receive a confirmation email when the deletion is finished",
                "success",
            )

            if custom_domain.is_sl_subdomain:
                return redirect(url_for("dashboard.subdomain_route"))
            else:
                return redirect(url_for("dashboard.custom_domain"))

    nb_alias = Alias.filter_by(custom_domain_id=custom_domain.id).count()

    return render_template("dashboard/domain_detail/info.html", **locals())
コード例 #6
0
ファイル: cron.py プロジェクト: goddess5321/app
def stats_before(moment: Arrow) -> Stats:
    """return the stats before a specific moment, ignoring all stats come from users in IGNORED_EMAILS
    """
    # nb user
    q = User.query
    for ie in IGNORED_EMAILS:
        q = q.filter(~User.email.contains(ie), User.created_at < moment)

    nb_user = q.count()
    LOG.d("total number user %s", nb_user)

    # nb alias
    q = db.session.query(Alias, User).filter(Alias.user_id == User.id,
                                             Alias.created_at < moment)
    for ie in IGNORED_EMAILS:
        q = q.filter(~User.email.contains(ie))

    nb_alias = q.count()
    LOG.d("total number alias %s", nb_alias)

    # email log stats
    q = (db.session.query(EmailLog).join(User,
                                         EmailLog.user_id == User.id).filter(
                                             EmailLog.created_at < moment, ))
    for ie in IGNORED_EMAILS:
        q = q.filter(~User.email.contains(ie))

    nb_spam = nb_bounced = nb_forward = nb_block = nb_reply = 0
    for email_log in q.yield_per(500):
        if email_log.bounced:
            nb_bounced += 1
        elif email_log.is_spam:
            nb_spam += 1
        elif email_log.is_reply:
            nb_reply += 1
        elif email_log.blocked:
            nb_block += 1
        else:
            nb_forward += 1

    LOG.d(
        "nb_forward %s, nb_block %s, nb_reply %s, nb_bounced %s, nb_spam %s",
        nb_forward,
        nb_block,
        nb_reply,
        nb_bounced,
        nb_spam,
    )

    nb_premium = Subscription.query.filter(
        Subscription.created_at < moment).count()
    nb_premium += AppleSubscription.query.filter(
        AppleSubscription.created_at < moment).count()

    nb_custom_domain = CustomDomain.query.filter(
        CustomDomain.created_at < moment).count()

    nb_app = Client.query.filter(Client.created_at < moment).count()

    data = locals()
    # to keep only Stats field
    data = {
        k: v
        for (k, v) in data.items()
        if k in vars(Stats)["__dataclass_fields__"].keys()
    }
    return Stats(**data)
コード例 #7
0
from app.models import (
    Client,
    User,
    ClientUser,
    GenEmail,
    RedirectUri,
    Subscription,
    PlanEnum,
    ApiKey,
    CustomDomain,
)
from app.monitor.base import monitor_bp
from app.oauth.base import oauth_bp

if SENTRY_DSN:
    LOG.d("enable sentry")
    sentry_sdk.init(dsn=SENTRY_DSN, integrations=[FlaskIntegration()])

# the app is served behin nginx which uses http and not https
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"


def create_app() -> Flask:
    app = Flask(__name__)
    app.url_map.strict_slashes = False

    app.config["SQLALCHEMY_DATABASE_URI"] = DB_URI
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.secret_key = FLASK_SECRET

    app.config["TEMPLATES_AUTO_RELOAD"] = True
コード例 #8
0
def sanity_check():
    """
    #TODO: investigate why DNS sometimes not working
    Different sanity checks
    - detect if there's mailbox that's using a invalid domain
    """
    mailbox_ids = (db.session.query(Mailbox.id).filter(
        Mailbox.verified == True, Mailbox.disabled == False).all())
    mailbox_ids = [e[0] for e in mailbox_ids]

    # iterate over id instead of mailbox directly
    # as a mailbox can be deleted during the sleep time
    for mailbox_id in mailbox_ids:
        mailbox = Mailbox.get(mailbox_id)
        # a mailbox has been deleted
        if not mailbox:
            continue

        # hack to not query DNS too often
        sleep(1)

        if not email_domain_can_be_used_as_mailbox(mailbox.email):
            mailbox.nb_failed_checks += 1
            nb_email_log = nb_email_log_for_mailbox(mailbox)
            log_func = LOG.warning

            # send a warning
            if mailbox.nb_failed_checks == 5:
                if mailbox.user.email != mailbox.email:
                    send_email(
                        mailbox.user.email,
                        f"Mailbox {mailbox.email} is disabled",
                        render("transactional/disable-mailbox-warning.txt",
                               mailbox=mailbox),
                        render(
                            "transactional/disable-mailbox-warning.html",
                            mailbox=mailbox,
                        ),
                    )

            # alert if too much fail and nb_email_log > 100
            if mailbox.nb_failed_checks > 10 and nb_email_log > 100:
                log_func = LOG.exception
                mailbox.disabled = True

                if mailbox.user.email != mailbox.email:
                    send_email(
                        mailbox.user.email,
                        f"Mailbox {mailbox.email} is disabled",
                        render("transactional/disable-mailbox.txt",
                               mailbox=mailbox),
                        render("transactional/disable-mailbox.html",
                               mailbox=mailbox),
                    )

            log_func(
                "issue with mailbox %s domain. #alias %s, nb email log %s",
                mailbox,
                mailbox.nb_alias(),
                nb_email_log,
            )
        else:  # reset nb check
            mailbox.nb_failed_checks = 0

        db.session.commit()

    for user in User.filter_by(activated=True).all():
        if user.email.lower().strip().replace(" ", "") != user.email:
            LOG.exception("%s does not have sanitized email", user)

    for alias in Alias.query.all():
        if alias.email.lower().strip().replace(" ", "") != alias.email:
            LOG.exception("Alias %s email not sanitized", alias)

    for contact in Contact.query.all():
        if contact.reply_email.lower().strip().replace(
                " ", "") != contact.reply_email:
            LOG.exception("Contact %s reply-email not sanitized", contact)

    for mailbox in Mailbox.query.all():
        if mailbox.email.lower().strip().replace(" ", "") != mailbox.email:
            LOG.exception("Mailbox %s address not sanitized", mailbox)

    LOG.d("Finish sanity check")
コード例 #9
0
def setting():
    form = SettingForm()
    promo_form = PromoCodeForm()
    change_email_form = ChangeEmailForm()

    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-email":
            if change_email_form.validate():
                # whether user can proceed with the email update
                new_email_valid = True
                if (sanitize_email(change_email_form.email.data) !=
                        current_user.email and not pending_email):
                    new_email = sanitize_email(change_email_form.email.data)

                    # check if this email is not already used
                    if personal_email_already_used(new_email) or Alias.get_by(
                            email=new_email):
                        flash(f"Email {new_email} already used", "error")
                        new_email_valid = False
                    elif not email_can_be_used_as_mailbox(new_email):
                        flash(
                            "You cannot use this email address as your personal inbox.",
                            "error",
                        )
                        new_email_valid = False
                    # a pending email change with the same email exists from another user
                    elif EmailChange.get_by(new_email=new_email):
                        other_email_change: EmailChange = EmailChange.get_by(
                            new_email=new_email)
                        LOG.warning(
                            "Another user has a pending %s with the same email address. Current user:%s",
                            other_email_change,
                            current_user,
                        )

                        if other_email_change.is_expired():
                            LOG.d("delete the expired email change %s",
                                  other_email_change)
                            EmailChange.delete(other_email_change.id)
                            db.session.commit()
                        else:
                            flash(
                                "You cannot use this email address as your personal inbox.",
                                "error",
                            )
                            new_email_valid = False

                    if new_email_valid:
                        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",
                        )
                        return redirect(url_for("dashboard.setting"))
        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(user_id=current_user.id, 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("Your profile has been updated", "success")
                    return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "change-password":
            flash(
                "You are going to receive an email containing instructions to change your password",
                "success",
            )
            send_reset_password_email(current_user)
            return redirect(url_for("dashboard.setting"))

        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")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "delete-account":
            LOG.warning("Delete account %s", current_user)
            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")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get(
                "form-name") == "change-random-alias-default-domain":
            default_domain = request.form.get("random-alias-default-domain")

            if default_domain:
                sl_domain: SLDomain = SLDomain.get_by(domain=default_domain)
                if sl_domain:
                    if sl_domain.premium_only and not current_user.is_premium(
                    ):
                        flash("You cannot use this domain", "error")
                        return redirect(url_for("dashboard.setting"))

                    current_user.default_alias_public_domain_id = sl_domain.id
                    current_user.default_alias_custom_domain_id = None
                else:
                    custom_domain = CustomDomain.get_by(domain=default_domain)
                    if custom_domain:
                        # sanity check
                        if (custom_domain.user_id != current_user.id
                                or not custom_domain.verified):
                            LOG.exception("%s cannot use domain %s",
                                          current_user, default_domain)
                        else:
                            current_user.default_alias_custom_domain_id = (
                                custom_domain.id)
                            current_user.default_alias_public_domain_id = None

            else:
                current_user.default_alias_custom_domain_id = None
                current_user.default_alias_public_domain_id = None

            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "change-sender-format":
            sender_format = int(request.form.get("sender-format"))
            if SenderFormatEnum.has_value(sender_format):
                current_user.sender_format = sender_format
                db.session.commit()
                flash("Your sender format preference has been updated",
                      "success")
            db.session.commit()
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "replace-ra":
            choose = request.form.get("replace-ra")
            if choose == "on":
                current_user.replace_reverse_alias = True
            else:
                current_user.replace_reverse_alias = False
            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "sender-in-ra":
            choose = request.form.get("enable")
            if choose == "on":
                current_user.include_sender_in_reverse_alias = True
            else:
                current_user.include_sender_in_reverse_alias = False
            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

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

            for alias in Alias.filter_by(
                    user_id=current_user.id).all():  # type: Alias
                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"
                },
            )
        elif request.form.get("form-name") == "export-alias":
            data = [["alias", "note", "enabled"]]
            for alias in Alias.filter_by(
                    user_id=current_user.id).all():  # type: Alias
                data.append([alias.email, alias.note, alias.enabled])

            si = StringIO()
            cw = csv.writer(si)
            cw.writerows(data)
            output = make_response(si.getvalue())
            output.headers[
                "Content-Disposition"] = "attachment; filename=aliases.csv"
            output.headers["Content-type"] = "text/csv"
            return output

    manual_sub = ManualSubscription.get_by(user_id=current_user.id)
    coinbase_sub = CoinbaseSubscription.get_by(user_id=current_user.id)

    return render_template(
        "dashboard/setting.html",
        form=form,
        PlanEnum=PlanEnum,
        SenderFormatEnum=SenderFormatEnum,
        promo_form=promo_form,
        change_email_form=change_email_form,
        pending_email=pending_email,
        AliasGeneratorEnum=AliasGeneratorEnum,
        manual_sub=manual_sub,
        coinbase_sub=coinbase_sub,
        FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN,
    )
コード例 #10
0
ファイル: new_custom_alias.py プロジェクト: maniacs-oss/app-1
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")
    alias_prefix = convert_to_id(alias_prefix)

    # 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,
    )
コード例 #11
0
ファイル: new_custom_alias.py プロジェクト: maniacs-oss/app-1
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

    """
    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
コード例 #12
0
ファイル: new_custom_alias.py プロジェクト: maniacs-oss/app-1
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,
    )
コード例 #13
0
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
    address = alias.email
    email_log: EmailLog = EmailLog.create(contact_id=contact.id,
                                          bounced=True,
                                          user_id=contact.user_id)
    db.session.commit()

    nb_bounced = EmailLog.filter_by(contact_id=contact.id,
                                    bounced=True).count()
    disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"

    # <<< Store the bounced email >>>
    # generate a name for the email
    random_name = str(uuid.uuid4())

    full_report_path = f"refused-emails/full-{random_name}.eml"
    s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()),
                                 random_name)

    file_path = None
    mailbox = alias.mailbox
    orig_msg = get_orig_message_from_bounce(msg)
    if not orig_msg:
        # Some MTA does not return the original message in bounce message
        # nothing we can do here
        LOG.warning(
            "Cannot parse original message from bounce message %s %s %s %s",
            alias,
            user,
            contact,
            full_report_path,
        )
    else:
        file_path = f"refused-emails/{random_name}.eml"
        s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()),
                                     random_name)
        # <<< END Store the bounced email >>>

        mailbox_id = int(orig_msg[_MAILBOX_ID_HEADER])
        mailbox = Mailbox.get(mailbox_id)
        if not mailbox or mailbox.user_id != user.id:
            LOG.exception(
                "Tampered message mailbox_id %s, %s, %s, %s %s",
                mailbox_id,
                user,
                alias,
                contact,
                full_report_path,
            )
            # use the alias default mailbox
            mailbox = alias.mailbox

    refused_email = RefusedEmail.create(path=file_path,
                                        full_report_path=full_report_path,
                                        user_id=user.id)
    db.session.flush()

    email_log.refused_email_id = refused_email.id
    email_log.bounced_mailbox_id = mailbox.id
    db.session.commit()

    LOG.d("Create refused email %s", refused_email)

    refused_email_url = (URL + f"/dashboard/refused_email?highlight_id=" +
                         str(email_log.id))

    # inform user if this is the first bounced email
    if nb_bounced == 1:
        LOG.d(
            "Inform user %s about bounced email sent by %s to alias %s",
            user,
            contact.website_email,
            address,
        )
        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Email from {contact.website_email} to {address} cannot be delivered to your inbox",
            render(
                "transactional/bounced-email.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/bounced-email.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )
    # disable the alias the second time email is bounced
    elif nb_bounced >= 2:
        LOG.d(
            "Bounce happens again with alias %s from %s. Disable alias now ",
            address,
            contact.website_email,
        )
        if alias.cannot_be_disabled:
            LOG.warning("%s cannot be disabled", alias)
        else:
            alias.enabled = False
        db.session.commit()

        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Alias {address} has been disabled due to second undelivered email from {contact.website_email}",
            render(
                "transactional/automatic-disable-alias.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/automatic-disable-alias.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )
コード例 #14
0
            "===>> New message, mail from %s, rctp tos %s ",
            envelope.mail_from,
            envelope.rcpt_tos,
        )

        if POSTFIX_SUBMISSION_TLS:
            smtp = SMTP(POSTFIX_SERVER, 587)
            smtp.starttls()
        else:
            smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT or 25)

        app = new_app()
        with app.app_context():
            return handle(envelope, smtp)


if __name__ == "__main__":
    controller = Controller(MailHandler(), hostname="0.0.0.0", port=20381)

    controller.start()
    LOG.d("Start mail controller %s %s", controller.hostname, controller.port)

    if LOAD_PGP_EMAIL_HANDLER:
        LOG.warning("LOAD PGP keys")
        app = create_app()
        with app.app_context():
            load_pgp_public_keys()

    while True:
        time.sleep(2)
コード例 #15
0
ファイル: custom_alias.py プロジェクト: theRealBithive/app
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")

            # verify email_suffix: do not verify when DISABLE_ALIAS_SUFFIX is set
            if not DISABLE_ALIAS_SUFFIX:
                # email suffix must be in the format ".{word}"
                if email_suffix[0] != "." or not word_exist(email_suffix[1:]):
                    flash(
                        "nice try :). The suffix is there so no one can take all the *nice* aliases though",
                        "warning",
                    )
                    return redirect(url_for("dashboard.custom_alias"))

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

            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 = "" if DISABLE_ALIAS_SUFFIX else "." + 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(),
    )
コード例 #16
0
ファイル: email_handler.py プロジェクト: nibblehole/app
async def handle_forward(envelope, smtp: SMTP, msg: Message,
                         rcpt_to: str) -> List[Tuple[bool, str]]:
    """return an array of SMTP status (is_success, smtp_status)
    is_success indicates whether an email has been delivered and
    smtp_status is the SMTP Status ("250 Message accepted", "550 Non-existent email address", etc)
    """
    address = rcpt_to.lower().strip().replace(" ", "")  # alias@SL

    alias = Alias.get_by(email=address)
    if not alias:
        LOG.d("alias %s not exist. Try to see if it can be created on the fly",
              address)
        alias = try_auto_create(address)
        if not alias:
            LOG.d("alias %s cannot be created on-the-fly, return 550", address)
            return [(False, "550 SL E3 Email not exist")]

    mail_from = envelope.mail_from.lower().strip()
    for mb in alias.mailboxes:
        # email send from a mailbox to alias
        if mb.email.lower().strip() == mail_from:
            LOG.warning("cycle email sent from %s to %s", mb, alias)
            handle_email_sent_to_ourself(alias, mb, msg, alias.user)
            return [(True, "250 Message accepted for delivery")]

    contact = get_or_create_contact(msg["From"], envelope.mail_from, alias)
    email_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id)
    db.session.commit()

    if not alias.enabled:
        LOG.d("%s is disabled, do not forward", alias)
        email_log.blocked = True

        db.session.commit()
        # do not return 5** to allow user to receive emails later when alias is enabled
        return [(True, "250 Message accepted for delivery")]

    user = alias.user

    ret = []
    mailboxes = alias.mailboxes

    # no valid mailbox
    if not mailboxes:
        return [(False, "550 SL E16 invalid mailbox")]

    # no need to create a copy of message
    if len(mailboxes) == 1:
        mailbox = mailboxes[0]
        ret.append(await
                   forward_email_to_mailbox(alias, msg, email_log, contact,
                                            envelope, smtp, mailbox, user))
    # create a copy of message for each forward
    else:
        for mailbox in mailboxes:
            ret.append(await
                       forward_email_to_mailbox(alias, copy(msg), email_log,
                                                contact, envelope, smtp,
                                                mailbox, user))

    return ret
コード例 #17
0
ファイル: cron.py プロジェクト: goddess5321/app
    db.session.commit()

    for user in User.filter_by(activated=True).all():
        if user.email.lower() != user.email:
            LOG.exception("%s does not have lowercase email", user)

    for mailbox in Mailbox.filter_by(verified=True).all():
        if mailbox.email.lower() != mailbox.email:
            LOG.exception("%s does not have lowercase email", mailbox)

    LOG.d("Finish sanity check")


if __name__ == "__main__":
    LOG.d("Start running cronjob")
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-j",
        "--job",
        help="Choose a cron job to run",
        type=str,
        choices=[
            "stats",
            "notify_trial_end",
            "notify_manual_subscription_end",
            "notify_premium_end",
            "delete_refused_emails",
            "poll_apple_subscription",
            "sanity_check",
        ],
コード例 #18
0
ファイル: email_handler.py プロジェクト: morristech/app-1
def handle_spam(
        contact: Contact,
        alias: Alias,
        msg: Message,
        user: User,
        mailbox: Mailbox,
        email_log: EmailLog,
        is_reply=False,  # whether the email is in forward or reply phase
):
    # Store the report & original email
    orig_msg = get_orig_message_from_spamassassin_report(msg)
    # generate a name for the email
    random_name = str(uuid.uuid4())

    full_report_path = f"spams/full-{random_name}.eml"
    s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()),
                                 random_name)

    file_path = None
    if orig_msg:
        file_path = f"spams/{random_name}.eml"
        s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()),
                                     random_name)

    refused_email = RefusedEmail.create(path=file_path,
                                        full_report_path=full_report_path,
                                        user_id=user.id)
    db.session.flush()

    email_log.refused_email_id = refused_email.id
    db.session.commit()

    LOG.d("Create spam email %s", refused_email)

    refused_email_url = (URL + f"/dashboard/refused_email?highlight_id=" +
                         str(email_log.id))
    disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"

    if is_reply:
        LOG.d(
            "Inform %s (%s) about spam email sent from alias %s to %s",
            mailbox,
            user,
            alias,
            contact,
        )
        send_email_with_rate_control(
            user,
            ALERT_SPAM_EMAIL,
            mailbox.email,
            f"Email from {contact.website_email} to {alias.email} is detected as spam",
            render(
                "transactional/spam-email-reply-phase.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
            ),
            render(
                "transactional/spam-email-reply-phase.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
            ),
        )
    else:
        # inform user
        LOG.d(
            "Inform %s (%s) about spam email sent by %s to alias %s",
            mailbox,
            user,
            contact,
            alias,
        )
        send_email_with_rate_control(
            user,
            ALERT_SPAM_EMAIL,
            mailbox.email,
            f"Email from {contact.website_email} to {alias.email} is detected as spam",
            render(
                "transactional/spam-email.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
            ),
            render(
                "transactional/spam-email.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
            ),
        )
コード例 #19
0
ファイル: custom_alias.py プロジェクト: simple-login/app
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,
    )
コード例 #20
0
ファイル: email_handler.py プロジェクト: morristech/app-1
def replace_header_when_forward(msg: Message, alias: Alias, header: str):
    """
    Replace CC or To header by Reply emails in forward phase
    """
    addrs = get_addrs_from_header(msg, header)

    # Nothing to do
    if not addrs:
        return

    new_addrs: [str] = []

    for addr in addrs:
        contact_name, contact_email = parseaddr_unicode(addr)

        # no transformation when alias is already in the header
        if contact_email == alias.email:
            new_addrs.append(addr)
            continue

        contact = Contact.get_by(alias_id=alias.id,
                                 website_email=contact_email)
        if contact:
            # update the contact name if needed
            if contact.name != contact_name:
                LOG.d(
                    "Update contact %s name %s to %s",
                    contact,
                    contact.name,
                    contact_name,
                )
                contact.name = contact_name
                db.session.commit()
        else:
            LOG.debug(
                "create contact for alias %s and email %s, header %s",
                alias,
                contact_email,
                header,
            )

            reply_email = generate_reply_email()

            contact = Contact.create(
                user_id=alias.user_id,
                alias_id=alias.id,
                website_email=contact_email,
                name=contact_name,
                reply_email=reply_email,
                is_cc=header.lower() == "cc",
            )
            db.session.commit()

        new_addrs.append(contact.new_addr())

    if new_addrs:
        new_header = ",".join(new_addrs)
        LOG.d("Replace %s header, old: %s, new: %s", header, msg[header],
              new_header)
        add_or_replace_header(msg, header, new_header)
    else:
        LOG.d("Delete %s header, old value %s", header, msg[header])
        delete_header(msg, header)
コード例 #21
0
    def paddle():
        LOG.debug(
            "paddle callback %s %s %s %s %s",
            request.form.get("alert_name"),
            request.form.get("email"),
            request.form.get("customer_name"),
            request.form.get("subscription_id"),
            request.form.get("subscription_plan_id"),
        )

        # make sure the request comes from Paddle
        if not paddle_utils.verify_incoming_request(dict(request.form)):
            LOG.error("request not coming from paddle. Request data:%s",
                      dict(request.form))
            return "KO", 400

        if (request.form.get("alert_name") == "subscription_created"
            ):  # new user subscribes
            user_email = request.form.get("email")
            user = User.get_by(email=user_email)

            if (int(request.form.get("subscription_plan_id")) ==
                    PADDLE_MONTHLY_PRODUCT_ID):
                plan = PlanEnum.monthly
            else:
                plan = PlanEnum.yearly

            sub = Subscription.get_by(user_id=user.id)

            if not sub:
                LOG.d("create a new sub")
                Subscription.create(
                    user_id=user.id,
                    cancel_url=request.form.get("cancel_url"),
                    update_url=request.form.get("update_url"),
                    subscription_id=request.form.get("subscription_id"),
                    event_time=arrow.now(),
                    next_bill_date=arrow.get(
                        request.form.get("next_bill_date"),
                        "YYYY-MM-DD").date(),
                    plan=plan,
                )
            else:
                LOG.d("update existing sub %s", sub)
                sub.cancel_url = request.form.get("cancel_url")
                sub.update_url = request.form.get("update_url")
                sub.subscription_id = request.form.get("subscription_id")
                sub.event_time = arrow.now()
                sub.next_bill_date = arrow.get(
                    request.form.get("next_bill_date"), "YYYY-MM-DD").date()
                sub.plan = plan

            LOG.debug("User %s upgrades!", user)

            db.session.commit()

        elif request.form.get("alert_name") == "subscription_updated":
            subscription_id = request.form.get("subscription_id")
            LOG.debug("Update subscription %s", subscription_id)

            sub: Subscription = Subscription.get_by(
                subscription_id=subscription_id)
            sub.event_time = arrow.now()
            sub.next_bill_date = arrow.get(request.form.get("next_bill_date"),
                                           "YYYY-MM-DD").date()

            db.session.commit()

        elif request.form.get("alert_name") == "subscription_cancelled":
            subscription_id = request.form.get("subscription_id")
            LOG.debug("Cancel subscription %s", subscription_id)

            sub: Subscription = Subscription.get_by(
                subscription_id=subscription_id)
            sub.cancelled = True

            db.session.commit()

        return "OK"
コード例 #22
0
ファイル: email_handler.py プロジェクト: morristech/app-1
async def forward_email_to_mailbox(
    alias,
    msg: Message,
    email_log: EmailLog,
    contact: Contact,
    envelope,
    smtp: SMTP,
    mailbox,
    user,
) -> (bool, str):
    LOG.d("Forward %s -> %s -> %s", contact, alias, mailbox)

    # sanity check: make sure mailbox is not actually an alias
    if get_email_domain_part(alias.email) == get_email_domain_part(
            mailbox.email):
        LOG.exception(
            "Mailbox has the same domain as alias. %s -> %s -> %s",
            contact,
            alias,
            mailbox,
        )
        return False, "550 SL E14"

    # Spam check
    spam_status = ""
    is_spam = False

    if SPAMASSASSIN_HOST:
        start = time.time()
        spam_score = await get_spam_score(msg)
        LOG.d(
            "%s -> %s - spam score %s in %s seconds",
            contact,
            alias,
            spam_score,
            time.time() - start,
        )
        email_log.spam_score = spam_score
        db.session.commit()

        if (user.max_spam_score and spam_score > user.max_spam_score) or (
                not user.max_spam_score and spam_score > MAX_SPAM_SCORE):
            is_spam = True
            spam_status = "Spam detected by SpamAssassin server"
    else:
        is_spam, spam_status = get_spam_info(msg,
                                             max_score=user.max_spam_score)

    if is_spam:
        LOG.warning("Email detected as spam. Alias: %s, from: %s", alias,
                    contact)
        email_log.is_spam = True
        email_log.spam_status = spam_status
        db.session.commit()

        handle_spam(contact, alias, msg, user, mailbox, email_log)
        return False, "550 SL E1 Email detected as spam"

    # create PGP email if needed
    if mailbox.pgp_finger_print and user.is_premium(
    ) and not alias.disable_pgp:
        LOG.d("Encrypt message using mailbox %s", mailbox)
        try:
            msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)
        except PGPException:
            LOG.exception("Cannot encrypt message %s -> %s. %s %s", contact,
                          alias, mailbox, user)
            # so the client can retry later
            return False, "421 SL E12 Retry later"

    # add custom header
    add_or_replace_header(msg, _DIRECTION, "Forward")

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

    delete_header(msg, _IP_HEADER)
    add_or_replace_header(msg, _MAILBOX_ID_HEADER, str(mailbox.id))
    add_or_replace_header(msg, _EMAIL_LOG_ID_HEADER, str(email_log.id))
    add_or_replace_header(msg, _MESSAGE_ID,
                          make_msgid(str(email_log.id), EMAIL_DOMAIN))

    # change the from header so the sender comes from @SL
    # so it can pass DMARC check
    # replace the email part in from: header
    contact_from_header = msg["From"]
    new_from_header = contact.new_addr()
    add_or_replace_header(msg, "From", new_from_header)
    LOG.d("new_from_header:%s, old header %s", new_from_header,
          contact_from_header)

    # replace CC & To emails by reply-email for all emails that are not alias
    replace_header_when_forward(msg, alias, "Cc")
    replace_header_when_forward(msg, alias, "To")

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

        add_or_replace_header(msg, "To", to_header.strip())

    # add List-Unsubscribe header
    if UNSUBSCRIBER:
        unsubscribe_link = f"mailto:{UNSUBSCRIBER}?subject={alias.id}="
        add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
    else:
        unsubscribe_link = f"{URL}/dashboard/unsubscribe/{alias.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 ",
        contact.website_email,
        mailbox.email,
        envelope.mail_options,
        envelope.rcpt_options,
    )

    # smtp.send_message has UnicodeEncodeErroremail issue
    # encode message raw directly instead
    smtp.sendmail(
        contact.reply_email,
        mailbox.email,
        msg.as_bytes(),
        envelope.mail_options,
        envelope.rcpt_options,
    )

    db.session.commit()
    return True, "250 Message accepted for delivery"
コード例 #23
0
ファイル: email_utils.py プロジェクト: wwwK/app
def should_disable(alias: Alias) -> bool:
    """Disable an alias if it has too many bounces recently"""
    # Bypass the bounce rule
    if alias.cannot_be_disabled:
        LOG.warning("%s cannot be disabled", alias)
        return False

    yesterday = arrow.now().shift(days=-1)
    nb_bounced_last_24h = (
        db.session.query(EmailLog)
        .join(Contact, EmailLog.contact_id == Contact.id)
        .filter(
            EmailLog.bounced.is_(True),
            EmailLog.is_reply.is_(False),
            EmailLog.created_at > yesterday,
        )
        .filter(Contact.alias_id == alias.id)
        .count()
    )
    # if more than 12 bounces in 24h -> disable alias
    if nb_bounced_last_24h > 12:
        LOG.d("more than 12 bounces in the last 24h, disable alias %s", alias)
        return True

    # if more than 5 bounces but has bounces last week -> disable alias
    elif nb_bounced_last_24h > 5:
        one_week_ago = arrow.now().shift(days=-8)
        nb_bounced_7d_1d = (
            db.session.query(EmailLog)
            .join(Contact, EmailLog.contact_id == Contact.id)
            .filter(
                EmailLog.bounced.is_(True),
                EmailLog.is_reply.is_(False),
                EmailLog.created_at > one_week_ago,
                EmailLog.created_at < yesterday,
            )
            .filter(Contact.alias_id == alias.id)
            .count()
        )
        if nb_bounced_7d_1d > 1:
            LOG.debug(
                "more than 5 bounces in the last 24h and more than 1 bounces in the last 7 days, "
                "disable alias %s",
                alias,
            )
            return True
    else:
        # if bounces at least 9 days in the last 10 days -> disable alias
        query = (
            db.session.query(
                func.date(EmailLog.created_at).label("date"),
                func.count(EmailLog.id).label("count"),
            )
            .join(Contact, EmailLog.contact_id == Contact.id)
            .filter(Contact.alias_id == alias.id)
            .filter(
                EmailLog.created_at > arrow.now().shift(days=-10),
                EmailLog.bounced.is_(True),
                EmailLog.is_reply.is_(False),
            )
            .group_by("date")
        )

        if query.count() >= 9:
            LOG.d(
                "Bounces every day for at least 9 days in the last 10 days, disable alias %s",
                alias,
            )
            return True

    return False
コード例 #24
0
ファイル: email_handler.py プロジェクト: morristech/app-1
async def handle_reply(envelope, smtp: SMTP, msg: Message,
                       rcpt_to: str) -> (bool, str):
    """
    return whether an email has been delivered and
    the smtp status ("250 Message accepted", "550 Non-existent email address", etc)
    """
    reply_email = rcpt_to.lower().strip()

    # reply_email must end with EMAIL_DOMAIN
    if not reply_email.endswith(EMAIL_DOMAIN):
        LOG.warning(f"Reply email {reply_email} has wrong domain")
        return False, "550 SL E2"

    contact = Contact.get_by(reply_email=reply_email)
    if not contact:
        LOG.warning(f"No such forward-email with {reply_email} as reply-email")
        return False, "550 SL E4 Email not exist"

    alias = contact.alias
    address: str = contact.alias.email
    alias_domain = address[address.find("@") + 1:]

    # alias must end with one of the ALIAS_DOMAINS or custom-domain
    if not email_belongs_to_alias_domains(alias.email):
        if not CustomDomain.get_by(domain=alias_domain):
            return False, "550 SL E5"

    user = alias.user
    mail_from = envelope.mail_from.lower().strip()

    # bounce email initiated by Postfix
    # can happen in case emails cannot be delivered to user-email
    # in this case Postfix will try to send a bounce report to original sender, which is
    # the "reply email"
    if mail_from == "<>":
        LOG.warning(
            "Bounce when sending to alias %s from %s, user %s",
            alias,
            contact,
            user,
        )

        handle_bounce(contact, alias, msg, user)
        return False, "550 SL E6"

    mailbox = Mailbox.get_by(email=mail_from, user_id=user.id)
    if not mailbox or mailbox not in alias.mailboxes:
        # only mailbox can send email to the reply-email
        handle_unknown_mailbox(envelope, msg, reply_email, user, alias)
        return False, "550 SL E7"

    if ENFORCE_SPF and mailbox.force_spf:
        ip = msg[_IP_HEADER]
        if not spf_pass(ip, envelope, mailbox, user, alias,
                        contact.website_email, msg):
            # cannot use 4** here as sender will retry. 5** because that generates bounce report
            return True, "250 SL E11"

    email_log = EmailLog.create(contact_id=contact.id,
                                is_reply=True,
                                user_id=contact.user_id)

    # Spam check
    spam_status = ""
    is_spam = False

    # do not use user.max_spam_score here
    if SPAMASSASSIN_HOST:
        start = time.time()
        spam_score = await get_spam_score(msg)
        LOG.d(
            "%s -> %s - spam score %s in %s seconds",
            alias,
            contact,
            spam_score,
            time.time() - start,
        )
        email_log.spam_score = spam_score
        if spam_score > MAX_REPLY_PHASE_SPAM_SCORE:
            is_spam = True
            spam_status = "Spam detected by SpamAssassin server"
    else:
        is_spam, spam_status = get_spam_info(
            msg, max_score=MAX_REPLY_PHASE_SPAM_SCORE)

    if is_spam:
        LOG.exception(
            "Reply phase - email sent from %s to %s detected as spam", alias,
            contact)

        email_log.is_spam = True
        email_log.spam_status = spam_status
        db.session.commit()

        handle_spam(contact,
                    alias,
                    msg,
                    user,
                    mailbox,
                    email_log,
                    is_reply=True)
        return False, "550 SL E15 Email detected as spam"

    delete_header(msg, _IP_HEADER)

    delete_header(msg, "DKIM-Signature")
    delete_header(msg, "Received")

    # make the email comes from alias
    from_header = alias.email
    # add alias name from alias
    if alias.name:
        LOG.d("Put alias name in from header")
        from_header = formataddr((alias.name, alias.email))
    elif alias.custom_domain:
        LOG.d("Put domain default alias name in from header")

        # add alias name from domain
        if alias.custom_domain.name:
            from_header = formataddr((alias.custom_domain.name, alias.email))

    add_or_replace_header(msg, "From", from_header)

    # some email providers like ProtonMail adds automatically the Reply-To field
    # make sure to delete it
    delete_header(msg, "Reply-To")

    # remove sender header if present as this could reveal user real email
    delete_header(msg, "Sender")
    delete_header(msg, "X-Sender")

    replace_header_when_reply(msg, alias, "To")
    replace_header_when_reply(msg, alias, "Cc")

    add_or_replace_header(
        msg,
        _MESSAGE_ID,
        make_msgid(str(email_log.id), get_email_domain_part(alias.email)),
    )
    add_or_replace_header(msg, _EMAIL_LOG_ID_HEADER, str(email_log.id))

    add_or_replace_header(msg, _DIRECTION, "Reply")

    # Received-SPF is injected by postfix-policyd-spf-python can reveal user original email
    delete_header(msg, "Received-SPF")

    LOG.d(
        "send email from %s to %s, mail_options:%s,rcpt_options:%s",
        alias.email,
        contact.website_email,
        envelope.mail_options,
        envelope.rcpt_options,
    )

    # replace "*****@*****.**" by the contact email in the email body
    # as this is usually included when replying
    if user.replace_reverse_alias:
        if msg.is_multipart():
            for part in msg.walk():
                if part.get_content_maintype() != "text":
                    continue
                part = replace_str_in_msg(part, reply_email,
                                          contact.website_email)

        else:
            msg = replace_str_in_msg(msg, reply_email, contact.website_email)

    if alias_domain in ALIAS_DOMAINS:
        add_dkim_signature(msg, alias_domain)
    # add DKIM-Signature for custom-domain alias
    else:
        custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
        if custom_domain.dkim_verified:
            add_dkim_signature(msg, alias_domain)

    # create PGP email if needed
    if contact.pgp_finger_print and user.is_premium():
        LOG.d("Encrypt message for contact %s", contact)
        try:
            msg = prepare_pgp_message(msg, contact.pgp_finger_print)
        except PGPException:
            LOG.exception("Cannot encrypt message %s -> %s. %s %s", alias,
                          contact, mailbox, user)
            # to not save the email_log
            db.session.rollback()
            # return 421 so the client can retry later
            return False, "421 SL E13 Retry later"

    try:
        smtp.sendmail(
            alias.email,
            contact.website_email,
            msg.as_bytes(),
            envelope.mail_options,
            envelope.rcpt_options,
        )
    except Exception:
        # to not save the email_log
        db.session.rollback()

        LOG.exception("Cannot send email from %s to %s", alias, contact)
        send_email(
            mailbox.email,
            f"Email cannot be sent to {contact.email} from {alias.email}",
            render(
                "transactional/reply-error.txt",
                user=user,
                alias=alias,
                contact=contact,
                contact_domain=get_email_domain_part(contact.email),
            ),
            render(
                "transactional/reply-error.html",
                user=user,
                alias=alias,
                contact=contact,
                contact_domain=get_email_domain_part(contact.email),
            ),
        )

    db.session.commit()
    return True, "250 Message accepted for delivery"
コード例 #25
0
ファイル: setting.py プロジェクト: ricocf/app
def setting():
    form = SettingForm()
    promo_form = PromoCodeForm()
    change_email_form = ChangeEmailForm()

    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-email":
            if change_email_form.validate():
                # whether user can proceed with the email update
                new_email_valid = True
                if (sanitize_email(change_email_form.email.data) !=
                        current_user.email and not pending_email):
                    new_email = sanitize_email(change_email_form.email.data)

                    # check if this email is not already used
                    if personal_email_already_used(new_email) or Alias.get_by(
                            email=new_email):
                        flash(f"Email {new_email} already used", "error")
                        new_email_valid = False
                    elif not email_can_be_used_as_mailbox(new_email):
                        flash(
                            "You cannot use this email address as your personal inbox.",
                            "error",
                        )
                        new_email_valid = False
                    # a pending email change with the same email exists from another user
                    elif EmailChange.get_by(new_email=new_email):
                        other_email_change: EmailChange = EmailChange.get_by(
                            new_email=new_email)
                        LOG.warning(
                            "Another user has a pending %s with the same email address. Current user:%s",
                            other_email_change,
                            current_user,
                        )

                        if other_email_change.is_expired():
                            LOG.d("delete the expired email change %s",
                                  other_email_change)
                            EmailChange.delete(other_email_change.id)
                            db.session.commit()
                        else:
                            flash(
                                "You cannot use this email address as your personal inbox.",
                                "error",
                            )
                            new_email_valid = False

                    if new_email_valid:
                        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",
                        )
                        return redirect(url_for("dashboard.setting"))
        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(user_id=current_user.id, 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("Your profile has been updated", "success")
                    return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "change-password":
            flash(
                "You are going to receive an email containing instructions to change your password",
                "success",
            )
            send_reset_password_email(current_user)
            return redirect(url_for("dashboard.setting"))

        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")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "delete-account":
            # Schedule delete account job
            LOG.warning("schedule delete account job for %s", current_user)
            Job.create(
                name=JOB_DELETE_ACCOUNT,
                payload={"user_id": current_user.id},
                run_at=arrow.now(),
                commit=True,
            )

            flash(
                "Your account deletion has been scheduled. "
                "You'll receive an email when the deletion is finished",
                "success",
            )
            return redirect(url_for("dashboard.setting"))

        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")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get(
                "form-name") == "change-random-alias-default-domain":
            default_domain = request.form.get("random-alias-default-domain")

            if default_domain:
                sl_domain: SLDomain = SLDomain.get_by(domain=default_domain)
                if sl_domain:
                    if sl_domain.premium_only and not current_user.is_premium(
                    ):
                        flash("You cannot use this domain", "error")
                        return redirect(url_for("dashboard.setting"))

                    current_user.default_alias_public_domain_id = sl_domain.id
                    current_user.default_alias_custom_domain_id = None
                else:
                    custom_domain = CustomDomain.get_by(domain=default_domain)
                    if custom_domain:
                        # sanity check
                        if (custom_domain.user_id != current_user.id
                                or not custom_domain.verified):
                            LOG.exception("%s cannot use domain %s",
                                          current_user, default_domain)
                        else:
                            current_user.default_alias_custom_domain_id = (
                                custom_domain.id)
                            current_user.default_alias_public_domain_id = None

            else:
                current_user.default_alias_custom_domain_id = None
                current_user.default_alias_public_domain_id = None

            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "random-alias-suffix":
            scheme = int(request.form.get("random-alias-suffix-generator"))
            if AliasSuffixEnum.has_value(scheme):
                current_user.random_alias_suffix = scheme
                db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "change-sender-format":
            sender_format = int(request.form.get("sender-format"))
            if SenderFormatEnum.has_value(sender_format):
                current_user.sender_format = sender_format
                current_user.sender_format_updated_at = arrow.now()
                db.session.commit()
                flash("Your sender format preference has been updated",
                      "success")
            db.session.commit()
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "replace-ra":
            choose = request.form.get("replace-ra")
            if choose == "on":
                current_user.replace_reverse_alias = True
            else:
                current_user.replace_reverse_alias = False
            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "sender-in-ra":
            choose = request.form.get("enable")
            if choose == "on":
                current_user.include_sender_in_reverse_alias = True
            else:
                current_user.include_sender_in_reverse_alias = False
            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "expand-alias-info":
            choose = request.form.get("enable")
            if choose == "on":
                current_user.expand_alias_info = True
            else:
                current_user.expand_alias_info = False
            db.session.commit()
            flash("Your preference has been updated", "success")
            return redirect(url_for("dashboard.setting"))

        elif request.form.get("form-name") == "export-data":
            return redirect(url_for("api.export_data"))
        elif request.form.get("form-name") == "export-alias":
            return redirect(url_for("api.export_aliases"))

    manual_sub = ManualSubscription.get_by(user_id=current_user.id)
    apple_sub = AppleSubscription.get_by(user_id=current_user.id)
    coinbase_sub = CoinbaseSubscription.get_by(user_id=current_user.id)

    return render_template(
        "dashboard/setting.html",
        form=form,
        PlanEnum=PlanEnum,
        SenderFormatEnum=SenderFormatEnum,
        promo_form=promo_form,
        change_email_form=change_email_form,
        pending_email=pending_email,
        AliasGeneratorEnum=AliasGeneratorEnum,
        manual_sub=manual_sub,
        apple_sub=apple_sub,
        coinbase_sub=coinbase_sub,
        FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN,
        ALIAS_RAND_SUFFIX_LENGTH=ALIAS_RANDOM_SUFFIX_LENGTH,
    )
コード例 #26
0
ファイル: email_handler.py プロジェクト: morristech/app-1
def spf_pass(
    ip: str,
    envelope,
    mailbox: Mailbox,
    user: User,
    alias: Alias,
    contact_email: str,
    msg: Message,
) -> bool:
    if ip:
        LOG.d("Enforce SPF")
        try:
            r = spf.check2(i=ip, s=envelope.mail_from.lower(), h=None)
        except Exception:
            LOG.exception("SPF error, mailbox %s, ip %s", mailbox.email, ip)
        else:
            # TODO: Handle temperr case (e.g. dns timeout)
            # only an absolute pass, or no SPF policy at all is 'valid'
            if r[0] not in ["pass", "none"]:
                LOG.warning(
                    "SPF fail for mailbox %s, reason %s, failed IP %s",
                    mailbox.email,
                    r[0],
                    ip,
                )
                send_email_with_rate_control(
                    user,
                    ALERT_SPF,
                    mailbox.email,
                    f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address",
                    render(
                        "transactional/spf-fail.txt",
                        name=user.name,
                        alias=alias.email,
                        ip=ip,
                        mailbox_url=URL +
                        f"/dashboard/mailbox/{mailbox.id}#spf",
                        to_email=contact_email,
                        subject=msg["Subject"],
                        time=arrow.now(),
                    ),
                    render(
                        "transactional/spf-fail.html",
                        name=user.name,
                        alias=alias.email,
                        ip=ip,
                        mailbox_url=URL +
                        f"/dashboard/mailbox/{mailbox.id}#spf",
                        to_email=contact_email,
                        subject=msg["Subject"],
                        time=arrow.now(),
                    ),
                )
                return False

    else:
        LOG.warning(
            "Could not find %s header %s -> %s",
            _IP_HEADER,
            mailbox.email,
            contact_email,
        )

    return True
コード例 #27
0
ファイル: alias_utils.py プロジェクト: Charismaticzone/app-2
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 can_create_directory_for_address(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

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

            mailboxes = directory.mailboxes

            alias = Alias.create(
                email=address,
                user_id=directory.user_id,
                directory_id=directory.id,
                mailbox_id=mailboxes[0].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()
            return alias
        except AliasInTrashError:
            LOG.warning(
                "Alias %s was deleted before, cannot auto-create using directory %s, user %s",
                address,
                directory_name,
                dir_user,
            )
            return None
        except IntegrityError:
            LOG.warning("Alias %s already exists", address)
            db.session.rollback()
            alias = Alias.get_by(email=address)
            return alias
コード例 #28
0
ファイル: email_handler.py プロジェクト: morristech/app-1
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
    disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"

    # Store the bounced email
    # generate a name for the email
    random_name = str(uuid.uuid4())

    full_report_path = f"refused-emails/full-{random_name}.eml"
    s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()),
                                 random_name)

    file_path = None
    mailbox = None
    email_log: EmailLog = None
    orig_msg = get_orig_message_from_bounce(msg)
    if not orig_msg:
        # Some MTA does not return the original message in bounce message
        # nothing we can do here
        LOG.warning(
            "Cannot parse original message from bounce message %s %s %s %s",
            alias,
            user,
            contact,
            full_report_path,
        )
    else:
        file_path = f"refused-emails/{random_name}.eml"
        s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()),
                                     random_name)
        try:
            mailbox_id = int(orig_msg[_MAILBOX_ID_HEADER])
        except TypeError:
            LOG.exception(
                "cannot parse mailbox from original message header %s",
                orig_msg[_MAILBOX_ID_HEADER],
            )
        else:
            mailbox = Mailbox.get(mailbox_id)
            if not mailbox or mailbox.user_id != user.id:
                LOG.exception(
                    "Tampered message mailbox_id %s, %s, %s, %s %s",
                    mailbox_id,
                    user,
                    alias,
                    contact,
                    full_report_path,
                )
                # cannot use this tampered mailbox, reset it
                mailbox = None

        # try to get the original email_log
        try:
            email_log_id = int(orig_msg[_EMAIL_LOG_ID_HEADER])
        except TypeError:
            LOG.exception(
                "cannot parse email log from original message header %s",
                orig_msg[_EMAIL_LOG_ID_HEADER],
            )
        else:
            email_log = EmailLog.get(email_log_id)

    refused_email = RefusedEmail.create(path=file_path,
                                        full_report_path=full_report_path,
                                        user_id=user.id)
    db.session.flush()
    LOG.d("Create refused email %s", refused_email)

    if not mailbox:
        LOG.debug("Try to get mailbox from bounce report")
        try:
            mailbox_id = int(get_header_from_bounce(msg, _MAILBOX_ID_HEADER))
        except Exception:
            LOG.exception("cannot get mailbox-id from bounce report, %s",
                          refused_email)
        else:
            mailbox = Mailbox.get(mailbox_id)
            if not mailbox or mailbox.user_id != user.id:
                LOG.exception(
                    "Tampered message mailbox_id %s, %s, %s, %s %s",
                    mailbox_id,
                    user,
                    alias,
                    contact,
                    full_report_path,
                )
                mailbox = None

    if not email_log:
        LOG.d("Try to get email log from bounce report")
        try:
            email_log_id = int(
                get_header_from_bounce(msg, _EMAIL_LOG_ID_HEADER))
        except Exception:
            LOG.exception("cannot get email log id from bounce report, %s",
                          refused_email)
        else:
            email_log = EmailLog.get(email_log_id)

    # use the default mailbox as the last option
    if not mailbox:
        LOG.warning("Use %s default mailbox %s", alias, refused_email)
        mailbox = alias.mailbox

    # create a new email log as the last option
    if not email_log:
        LOG.warning("cannot get the original email_log, create a new one")
        email_log: EmailLog = EmailLog.create(contact_id=contact.id,
                                              user_id=contact.user_id)

    email_log.bounced = True
    email_log.refused_email_id = refused_email.id
    email_log.bounced_mailbox_id = mailbox.id
    db.session.commit()

    refused_email_url = (URL + f"/dashboard/refused_email?highlight_id=" +
                         str(email_log.id))

    nb_bounced = EmailLog.filter_by(contact_id=contact.id,
                                    bounced=True).count()
    if nb_bounced >= 2 and alias.cannot_be_disabled:
        LOG.warning("%s cannot be disabled", alias)

    # inform user if this is the first bounced email
    if nb_bounced == 1 or (nb_bounced >= 2 and alias.cannot_be_disabled):
        LOG.d(
            "Inform user %s about bounced email sent by %s to alias %s",
            user,
            contact.website_email,
            alias,
        )
        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Email from {contact.website_email} to {alias.email} cannot be delivered to your inbox",
            render(
                "transactional/bounced-email.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/bounced-email.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )
    # disable the alias the second time email is bounced
    elif nb_bounced >= 2:
        LOG.d(
            "Bounce happens again with alias %s from %s. Disable alias now ",
            alias,
            contact.website_email,
        )
        alias.enabled = False
        db.session.commit()

        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Alias {alias.email} has been disabled due to second undelivered email from {contact.website_email}",
            render(
                "transactional/automatic-disable-alias.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/automatic-disable-alias.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )
コード例 #29
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()
    ]
    suffixes = get_available_suffixes(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 (-), dots (.) 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,
    )
コード例 #30
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))