Пример #1
0
def import_from_csv(batch_import: BatchImport, user: User, lines):
    reader = csv.DictReader(lines)

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

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

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

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

        mailboxes = []

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

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

                mailboxes.append(mailbox.id)

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

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

        for i in range(1, len(mailboxes)):
            alias_mailbox = AliasMailbox.create(
                alias_id=alias.id,
                mailbox_id=mailboxes[i],
            )
            db.session.commit()
            LOG.d("Create %s", alias_mailbox)
Пример #2
0
def send_email_with_rate_control(
    user: User,
    alert_type: str,
    to_email: str,
    subject,
    plaintext,
    html=None,
    max_nb_alert=MAX_ALERT_24H,
    nb_day=1,
) -> bool:
    """Same as send_email with rate control over alert_type.
    Make sure no more than `max_nb_alert` emails are sent over the period of `nb_day` days

    Return true if the email is sent, otherwise False
    """
    to_email = sanitize_email(to_email)
    min_dt = arrow.now().shift(days=-1 * nb_day)
    nb_alert = (SentAlert.query.filter_by(
        alert_type=alert_type,
        to_email=to_email).filter(SentAlert.created_at > min_dt).count())

    if nb_alert >= max_nb_alert:
        LOG.warning(
            "%s emails were sent to %s in the last %s days, alert type %s",
            nb_alert,
            to_email,
            nb_day,
            alert_type,
        )
        return False

    SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email)
    db.session.commit()
    send_email(to_email, subject, plaintext, html)
    return True
Пример #3
0
def resend_activation():
    form = ResendActivationForm(request.form)

    if form.validate_on_submit():
        user = User.filter_by(email=sanitize_email(form.email.data)).first()

        if not user:
            flash("There is no such email", "warning")
            return render_template("auth/resend_activation.html", form=form)

        if user.activated:
            flash("Your account was already activated, please login",
                  "success")
            return redirect(url_for("auth.login"))

        # user is not activated
        LOG.d("user %s is not activated", user)
        flash(
            "An activation email has been sent to you. Please check your inbox/spam folder.",
            "warning",
        )
        send_activation_email(user, request.args.get("next"))
        return render_template("auth/register_waiting_activation.html")

    return render_template("auth/resend_activation.html", form=form)
Пример #4
0
def send_email_at_most_times(
    user: User,
    alert_type: str,
    to_email: str,
    subject,
    plaintext,
    html=None,
    max_times=1,
) -> bool:
    """Same as send_email with rate control over alert_type.
    Sent at most `max_times`
    This is used to inform users about a warning.

    Return true if the email is sent, otherwise False
    """
    to_email = sanitize_email(to_email)
    nb_alert = SentAlert.query.filter_by(
        alert_type=alert_type, to_email=to_email
    ).count()

    if nb_alert >= max_times:
        LOG.warning(
            "%s emails were sent to %s alert type %s",
            nb_alert,
            to_email,
            alert_type,
        )
        return False

    SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email)
    db.session.commit()
    send_email(to_email, subject, plaintext, html)
    return True
Пример #5
0
def get_email_domain_part(address):
    """
    Get the domain part from email
    [email protected] -> cd.com
    """
    address = sanitize_email(address)
    return address[address.find("@") + 1 :]
Пример #6
0
def create_mailbox():
    """
    Create a new mailbox. User needs to verify the mailbox via an activation email.
    Input:
        email: in body
    Output:
        the new mailbox dict
    """
    user = g.user
    mailbox_email = sanitize_email(request.get_json().get("email"))

    if not is_valid_email(mailbox_email):
        return jsonify(error=f"{mailbox_email} invalid"), 400
    elif mailbox_already_used(mailbox_email, user):
        return jsonify(error=f"{mailbox_email} already used"), 400
    elif not email_can_be_used_as_mailbox(mailbox_email):
        return (
            jsonify(
                error=
                f"{mailbox_email} cannot be used. Please note a mailbox cannot "
                f"be a disposable email address"),
            400,
        )
    else:
        new_mailbox = Mailbox.create(email=mailbox_email, user_id=user.id)
        db.session.commit()

        send_verification_email(user, new_mailbox)

        return (
            jsonify(mailbox_to_dict(new_mailbox)),
            201,
        )
Пример #7
0
def create_contact_route(alias_id):
    """
    Create contact for an alias
    Input:
        alias_id: in url
        contact: in body
    Output:
        201 if success
        409 if contact already added


    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    user = g.user
    alias: Alias = Alias.get(alias_id)

    if alias.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    contact_addr = data.get("contact")

    if not contact_addr:
        return jsonify(error="Contact cannot be empty"), 400

    full_address: EmailAddress = address.parse(contact_addr)
    if not full_address:
        return jsonify(error=f"invalid contact email {contact_addr}"), 400

    contact_name, contact_email = full_address.display_name, full_address.address

    contact_email = sanitize_email(contact_email, not_lower=True)

    # already been added
    contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
    if contact:
        return jsonify(**serialize_contact(contact, existed=True)), 200

    try:
        contact = Contact.create(
            user_id=alias.user_id,
            alias_id=alias.id,
            website_email=contact_email,
            name=contact_name,
            reply_email=generate_reply_email(contact_email, user),
        )
    except CannotCreateContactForReverseAlias:
        return jsonify(
            error="You can't create contact for a reverse alias"), 400

    LOG.d("create reverse-alias for %s %s", contact_addr, alias)
    Session.commit()

    return jsonify(**serialize_contact(contact)), 201
Пример #8
0
def auth_activate():
    """
    User enters the activation code to confirm their account.
    Input:
        email
        code
    Output:
        200: user account is now activated, user can login now
        400: wrong email, code
        410: wrong code too many times

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    email = sanitize_email(data.get("email"))
    code = data.get("code")

    user = User.get_by(email=email)

    # do not use a different message to avoid exposing existing email
    if not user or user.activated:
        # Trigger rate limiter
        g.deduct_limit = True
        return jsonify(error="Wrong email or code"), 400

    account_activation = AccountActivation.get_by(user_id=user.id)
    if not account_activation:
        # Trigger rate limiter
        g.deduct_limit = True
        return jsonify(error="Wrong email or code"), 400

    if account_activation.code != code:
        # decrement nb tries
        account_activation.tries -= 1
        db.session.commit()
        # Trigger rate limiter
        g.deduct_limit = True

        if account_activation.tries == 0:
            AccountActivation.delete(account_activation.id)
            db.session.commit()
            return jsonify(error="Too many wrong tries"), 410

        return jsonify(error="Wrong email or code"), 400

    LOG.debug("activate user %s", user)
    user.activated = True
    AccountActivation.delete(account_activation.id)
    db.session.commit()

    return jsonify(msg="Account is activated, user can login now"), 200
Пример #9
0
def create_contact_route(alias_id):
    """
    Create contact for an alias
    Input:
        alias_id: in url
        contact: in body
    Output:
        201 if success
        409 if contact already added


    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    user = g.user
    alias: Alias = Alias.get(alias_id)

    if alias.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    contact_addr = data.get("contact")

    if not contact_addr:
        return jsonify(error="Contact cannot be empty"), 400

    contact_name, contact_email = parseaddr_unicode(contact_addr)

    if not is_valid_email(contact_email):
        return jsonify(error=f"invalid contact email {contact_email}"), 400

    contact_email = sanitize_email(contact_email)

    # already been added
    contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
    if contact:
        return jsonify(**serialize_contact(contact, existed=True)), 200

    contact = Contact.create(
        user_id=alias.user_id,
        alias_id=alias.id,
        website_email=contact_email,
        name=contact_name,
        reply_email=generate_reply_email(contact_email, user),
    )

    LOG.d("create reverse-alias for %s %s", contact_addr, alias)
    db.session.commit()

    return jsonify(**serialize_contact(contact)), 201
Пример #10
0
def auth_google():
    """
    Authenticate user with Facebook
    Input:
        google_token: Google access token
        device: to create an ApiKey associated with this device
    Output:
        200 and user info containing:
        {
            name: "John Wick",
            mfa_enabled: true,
            mfa_key: "a long string",
            api_key: "a long string"
        }

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    google_token = data.get("google_token")
    device = data.get("device")

    cred = google.oauth2.credentials.Credentials(token=google_token)

    build = googleapiclient.discovery.build("oauth2", "v2", credentials=cred)

    user_info = build.userinfo().get().execute()
    email = sanitize_email(user_info.get("email"))

    user = User.get_by(email=email)

    if not user:
        if DISABLE_REGISTRATION:
            return jsonify(error="registration is closed"), 400
        if not email_can_be_used_as_mailbox(email) or personal_email_already_used(
            email
        ):
            return jsonify(error=f"cannot use {email} as personal inbox"), 400

        LOG.d("create Google user with %s", user_info)
        user = User.create(email=email, name="", activated=True)
        db.session.commit()
        email_utils.send_welcome_email(user)

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

    return jsonify(**auth_payload(user, device)), 200
Пример #11
0
def sanitize_alias_address_name():
    count = 0
    # using Alias.all() will take all the memory
    for alias in Alias.yield_per_query():
        if count % 1000 == 0:
            LOG.d("process %s", count)

        count += 1
        if sanitize_email(alias.email) != alias.email:
            LOG.e("Alias %s email not sanitized", alias)

        if alias.name and "\n" in alias.name:
            alias.name = alias.name.replace("\n", "")
            Session.commit()
            LOG.e("Alias %s name contains linebreak %s", alias, alias.name)
Пример #12
0
def auth_facebook():
    """
    Authenticate user with Facebook
    Input:
        facebook_token: facebook access token
        device: to create an ApiKey associated with this device
    Output:
        200 and user info containing:
        {
            name: "John Wick",
            mfa_enabled: true,
            mfa_key: "a long string",
            api_key: "a long string"
        }

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    facebook_token = data.get("facebook_token")
    device = data.get("device")

    graph = facebook.GraphAPI(access_token=facebook_token)
    user_info = graph.get_object("me", fields="email,name")
    email = sanitize_email(user_info.get("email"))

    user = User.get_by(email=email)

    if not user:
        if DISABLE_REGISTRATION:
            return jsonify(error="registration is closed"), 400
        if not email_can_be_used_as_mailbox(email) or personal_email_already_used(
            email
        ):
            return jsonify(error=f"cannot use {email} as personal inbox"), 400

        LOG.d("create facebook user with %s", user_info)
        user = User.create(email=email, name=user_info["name"], activated=True)
        db.session.commit()
        email_utils.send_welcome_email(user)

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

    return jsonify(**auth_payload(user, device)), 200
Пример #13
0
def handle_batch_import(batch_import: BatchImport):
    user = batch_import.user

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

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

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

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

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

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

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

        alias = Alias.create(
            user_id=user.id,
            email=full_alias,
            note=note,
            mailbox_id=user.default_mailbox_id,
            custom_domain_id=custom_domain.id,
            batch_import_id=batch_import.id,
        )
        db.session.commit()
        LOG.d("Create %s", alias)
Пример #14
0
def auth_register():
    """
    User signs up - will need to activate their account with an activation code.
    Input:
        email
        password
    Output:
        200: user needs to confirm their account

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    email = sanitize_email(data.get("email"))
    password = data.get("password")

    if DISABLE_REGISTRATION:
        return jsonify(error="registration is closed"), 400
    if not email_can_be_used_as_mailbox(email) or personal_email_already_used(
            email):
        return jsonify(error=f"cannot use {email} as personal inbox"), 400

    if not password or len(password) < 8:
        return jsonify(error="password too short"), 400

    if len(password) > 100:
        return jsonify(error="password too long"), 400

    LOG.d("create user %s", email)
    user = User.create(email=email, name="", password=password)
    Session.flush()

    # create activation code
    code = "".join([str(random.randint(0, 9)) for _ in range(6)])
    AccountActivation.create(user_id=user.id, code=code)
    Session.commit()

    send_email(
        email,
        "Just one more step to join SimpleLogin",
        render("transactional/code-activation.txt.jinja2", code=code),
        render("transactional/code-activation.html", code=code),
    )

    return jsonify(msg="User needs to confirm their account"), 200
Пример #15
0
def send_email_with_rate_control(
    user: User,
    alert_type: str,
    to_email: str,
    subject,
    plaintext,
    html=None,
    max_nb_alert=MAX_ALERT_24H,
    nb_day=1,
    ignore_smtp_error=False,
    retries=0,
) -> bool:
    """Same as send_email with rate control over alert_type.
    Make sure no more than `max_nb_alert` emails are sent over the period of `nb_day` days

    Return true if the email is sent, otherwise False
    """
    to_email = sanitize_email(to_email)
    min_dt = arrow.now().shift(days=-1 * nb_day)
    nb_alert = (SentAlert.filter_by(
        alert_type=alert_type,
        to_email=to_email).filter(SentAlert.created_at > min_dt).count())

    if nb_alert >= max_nb_alert:
        LOG.w(
            "%s emails were sent to %s in the last %s days, alert type %s",
            nb_alert,
            to_email,
            nb_day,
            alert_type,
        )
        return False

    SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email)
    Session.commit()

    if ignore_smtp_error:
        try:
            send_email(to_email, subject, plaintext, html, retries=retries)
        except SMTPException:
            LOG.w("Cannot send email to %s, subject %s", to_email, subject)
    else:
        send_email(to_email, subject, plaintext, html, retries=retries)

    return True
Пример #16
0
def login():
    next_url = request.args.get("next")

    if current_user.is_authenticated:
        if next_url:
            LOG.debug("user is already authenticated, redirect to %s",
                      next_url)
            return redirect(next_url)
        else:
            LOG.d("user is already authenticated, redirect to dashboard")
            return redirect(url_for("dashboard.index"))

    form = LoginForm(request.form)

    show_resend_activation = False

    if form.validate_on_submit():
        user = User.filter_by(email=sanitize_email(form.email.data)).first()

        if not user or not user.check_password(form.password.data):
            # Trigger rate limiter
            g.deduct_limit = True
            form.password.data = None
            flash("Email or password incorrect", "error")
        elif user.disabled:
            flash(
                "Your account is disabled. Please contact SimpleLogin team to re-enable your account.",
                "error",
            )
        elif not user.activated:
            show_resend_activation = True
            flash(
                "Please check your inbox for the activation email. You can also have this email re-sent",
                "error",
            )
        else:
            return after_login(user, next_url)

    return render_template(
        "auth/login.html",
        form=form,
        next_url=next_url,
        show_resend_activation=show_resend_activation,
    )
Пример #17
0
def generate_reply_email(contact_email: str, user: User) -> str:
    """
    generate a reply_email (aka reverse-alias), make sure it isn't used by any contact
    """
    # shorten email to avoid exceeding the 64 characters
    # from https://tools.ietf.org/html/rfc5321#section-4.5.3
    # "The maximum total length of a user name or other local-part is 64
    #    octets."

    include_sender_in_reverse_alias = False

    # user has chosen an option explicitly
    if user.include_sender_in_reverse_alias is not None:
        include_sender_in_reverse_alias = user.include_sender_in_reverse_alias

    if include_sender_in_reverse_alias and contact_email:
        # control char: 4 chars (ra+, +)
        # random suffix: max 10 chars
        # maximum: 64

        # make sure contact_email can be ascii-encoded
        contact_email = convert_to_id(contact_email)
        contact_email = sanitize_email(contact_email)
        contact_email = contact_email[:45]
        contact_email = contact_email.replace("@", ".at.")
        contact_email = convert_to_alphanumeric(contact_email)

    # not use while to avoid infinite loop
    for _ in range(1000):
        if include_sender_in_reverse_alias and contact_email:
            random_length = random.randint(5, 10)
            reply_email = (
                f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}"
            )
        else:
            random_length = random.randint(20, 50)
            reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}"

        if not Contact.get_by(reply_email=reply_email):
            return reply_email

    raise Exception("Cannot generate reply email")
Пример #18
0
def auth_login():
    """
    Authenticate user
    Input:
        email
        password
        device: to create an ApiKey associated with this device
    Output:
        200 and user info containing:
        {
            name: "John Wick",
            mfa_enabled: true,
            mfa_key: "a long string",
            api_key: "a long string"
        }

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    email = sanitize_email(data.get("email"))
    password = data.get("password")
    device = data.get("device")

    user = User.filter_by(email=email).first()

    if not user or not user.check_password(password):
        # Trigger rate limiter
        g.deduct_limit = True
        return jsonify(error="Email or password incorrect"), 400
    elif user.disabled:
        return jsonify(error="Account disabled"), 400
    elif not user.activated:
        return jsonify(error="Account not activated"), 400
    elif user.fido_enabled():
        # allow user who has TOTP enabled to continue using the mobile app
        if not user.enable_otp:
            return jsonify(
                error="Currently we don't support FIDO on mobile yet"), 403

    return jsonify(**auth_payload(user, device)), 200
Пример #19
0
def forgot_password():
    form = ForgotPasswordForm(request.form)

    if form.validate_on_submit():
        email = sanitize_email(form.email.data)
        flash(
            "If your email is correct, you are going to receive an email to reset your password",
            "success",
        )

        user = User.get_by(email=email)

        if user:
            send_reset_password_email(user)
            return redirect(url_for("auth.forgot_password"))

        # Trigger rate limiter
        g.deduct_limit = True

    return render_template("auth/forgot_password.html", form=form)
Пример #20
0
def forgot_password():
    """
    User forgot password
    Input:
        email
    Output:
        200 and a reset password email is sent to user
        400 if email not exist

    """
    data = request.get_json()
    if not data or not data.get("email"):
        return jsonify(error="request body must contain email"), 400

    email = sanitize_email(data.get("email"))

    user = User.get_by(email=email)

    if user:
        send_reset_password_email(user)

    return jsonify(ok=True)
Пример #21
0
def auth_reactivate():
    """
    User asks for another activation code
    Input:
        email
    Output:
        200: user is going to receive an email for activate their account

    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    email = sanitize_email(data.get("email"))
    user = User.get_by(email=email)

    # do not use a different message to avoid exposing existing email
    if not user or user.activated:
        return jsonify(error="Something went wrong"), 400

    account_activation = AccountActivation.get_by(user_id=user.id)
    if account_activation:
        AccountActivation.delete(account_activation.id)
        db.session.commit()

    # create activation code
    code = "".join([str(random.randint(0, 9)) for _ in range(6)])
    AccountActivation.create(user_id=user.id, code=code)
    db.session.commit()

    send_email(
        email,
        "Just one more step to join SimpleLogin",
        render("transactional/code-activation.txt", code=code),
        render("transactional/code-activation.html", code=code),
    )

    return jsonify(msg="User needs to confirm their account"), 200
Пример #22
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
                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") == "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,
    )
Пример #23
0
def update_mailbox(mailbox_id):
    """
    Update mailbox
    Input:
        mailbox_id: in url
        (optional) default: in body. Set a mailbox as the default mailbox.
        (optional) email: in body. Change a mailbox email.
        (optional) cancel_email_change: in body. Cancel mailbox email change.
    Output:
        200 if updated successfully

    """
    user = g.user
    mailbox = Mailbox.get(mailbox_id)

    if not mailbox or mailbox.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    data = request.get_json() or {}
    changed = False
    if "default" in data:
        is_default = data.get("default")
        if is_default:
            if not mailbox.verified:
                return (
                    jsonify(
                        error=
                        "Unverified mailbox cannot be used as default mailbox"
                    ),
                    400,
                )
            user.default_mailbox_id = mailbox.id
            changed = True

    if "email" in data:
        new_email = sanitize_email(data.get("email"))

        if mailbox_already_used(new_email, user):
            return jsonify(error=f"{new_email} already used"), 400
        elif not email_can_be_used_as_mailbox(new_email):
            return (
                jsonify(
                    error=
                    f"{new_email} cannot be used. Please note a mailbox cannot "
                    f"be a disposable email address"),
                400,
            )

        try:
            verify_mailbox_change(user, mailbox, new_email)
        except SMTPRecipientsRefused:
            return jsonify(
                error=f"Incorrect mailbox, please recheck {new_email}"), 400
        else:
            mailbox.new_email = new_email
            changed = True

    if "cancel_email_change" in data:
        cancel_email_change = data.get("cancel_email_change")
        if cancel_email_change:
            mailbox.new_email = None
            changed = True

    if changed:
        Session.commit()

    return jsonify(updated=True), 200
Пример #24
0
def github_callback():
    # user clicks on cancel
    if "error" in request.args:
        flash("Please use another sign in method then", "warning")
        return redirect("/")

    github = OAuth2Session(
        GITHUB_CLIENT_ID,
        state=session["oauth_state"],
        scope=["user:email"],
        redirect_uri=_redirect_uri,
    )
    github.fetch_token(
        _token_url,
        client_secret=GITHUB_CLIENT_SECRET,
        authorization_response=request.url,
    )

    # a dict with "name", "login"
    github_user_data = github.get("https://api.github.com/user").json()

    # return list of emails
    # {
    #     'email': '*****@*****.**',
    #     'primary': False,
    #     'verified': True,
    #     'visibility': None
    # }
    emails = github.get("https://api.github.com/user/emails").json()

    # only take the primary email
    email = None

    for e in emails:
        if e.get("verified") and e.get("primary"):
            email = e.get("email")
            break

    if not email:
        LOG.e(f"cannot get email for github user {github_user_data} {emails}")
        flash(
            "Cannot get a valid email from Github, please another way to login/sign up",
            "error",
        )
        return redirect(url_for("auth.login"))

    email = sanitize_email(email)
    user = User.get_by(email=email)

    if not user:
        flash(
            "Sorry you cannot sign up via Github, please use email/password sign-up instead",
            "error",
        )
        return redirect(url_for("auth.register"))

    if not SocialAuth.get_by(user_id=user.id, social="github"):
        SocialAuth.create(user_id=user.id, social="github")
        Session.commit()

    # The activation link contains the original page, for ex authorize page
    next_url = request.args.get("next") if request.args else None

    return after_login(user, next_url)
Пример #25
0
def google_callback():
    # user clicks on cancel
    if "error" in request.args:
        flash("please use another sign in method then", "warning")
        return redirect("/")

    google = OAuth2Session(
        GOOGLE_CLIENT_ID,
        # some how Google Login fails with oauth_state KeyError
        # state=session["oauth_state"],
        scope=_scope,
        redirect_uri=_redirect_uri,
    )
    google.fetch_token(
        _token_url,
        client_secret=GOOGLE_CLIENT_SECRET,
        authorization_response=request.url,
    )

    # Fetch a protected resource, i.e. user profile
    # {
    #     "email": "*****@*****.**",
    #     "family_name": "First name",
    #     "given_name": "Last name",
    #     "id": "1234",
    #     "locale": "en",
    #     "name": "First Last",
    #     "picture": "http://profile.jpg",
    #     "verified_email": true
    # }
    google_user_data = google.get(
        "https://www.googleapis.com/oauth2/v1/userinfo").json()

    email = sanitize_email(google_user_data["email"])
    user = User.get_by(email=email)

    picture_url = google_user_data.get("picture")

    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(user, picture_url)
            user.profile_picture_id = file.id
            db.session.commit()
    else:
        flash(
            "Sorry you cannot sign up via Google, please use email/password sign-up instead",
            "error",
        )
        return redirect(url_for("auth.register"))

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

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

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

    return after_login(user, next_url)
Пример #26
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.is_(True), Mailbox.disabled.is_(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_can_be_used_as_mailbox(mailbox.email):
            mailbox.nb_failed_checks += 1
            nb_email_log = nb_email_log_for_mailbox(mailbox)

            # 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:
                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.warning(
                "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 sanitize_email(user.email) != user.email:
            LOG.exception("%s does not have sanitized email", user)

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

        if alias.name and "\n" in alias.name:
            alias.name = alias.name.replace("\n", "")
            db.session.commit()
            LOG.exception("Alias %s name contains linebreak %s", alias, alias.name)

    contact_email_sanity_date = arrow.get("2021-01-12")
    for contact in Contact.query.all():
        if sanitize_email(contact.reply_email) != contact.reply_email:
            LOG.exception("Contact %s reply-email not sanitized", contact)

        if (
            sanitize_email(contact.website_email) != contact.website_email
            and contact.created_at > contact_email_sanity_date
        ):
            LOG.exception("Contact %s website-email not sanitized", contact)

        if not contact.invalid_email and not is_valid_email(contact.website_email):
            LOG.exception("%s invalid email", contact)
            contact.invalid_email = True
            db.session.commit()

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

    for contact in Contact.query.all():
        if normalize_reply_email(contact.reply_email) != contact.reply_email:
            LOG.exception(
                "Contact %s reply email is not normalized %s",
                contact,
                contact.reply_email,
            )

    for domain in CustomDomain.query.all():
        if domain.name and "\n" in domain.name:
            LOG.exception("Domain %s name contain linebreak %s", domain, domain.name)

    migrate_domain_trash()
    set_custom_domain_for_alias()

    LOG.d("Finish sanity check")
Пример #27
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":
            # 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") == "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") == "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,
    )
Пример #28
0
def alias_contact_manager(alias_id):
    highlight_contact_id = None
    if request.args.get("highlight_contact_id"):
        highlight_contact_id = int(request.args.get("highlight_contact_id"))

    alias = Alias.get(alias_id)

    page = 0
    if request.args.get("page"):
        page = int(request.args.get("page"))

    query = request.args.get("query") or ""

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

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

    new_contact_form = NewContactForm()

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

                try:
                    contact_name, contact_email = parseaddr_unicode(
                        contact_addr)
                    contact_email = sanitize_email(contact_email)
                except Exception:
                    flash(f"{contact_addr} is invalid", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                        ))

                if not is_valid_email(contact_email):
                    flash(f"{contact_email} is invalid", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                        ))

                contact = Contact.get_by(alias_id=alias.id,
                                         website_email=contact_email)
                # already been added
                if contact:
                    flash(f"{contact_email} is already added", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                            highlight_contact_id=contact.id,
                        ))

                contact = Contact.create(
                    user_id=alias.user_id,
                    alias_id=alias.id,
                    website_email=contact_email,
                    name=contact_name,
                    reply_email=generate_reply_email(contact_email,
                                                     current_user),
                )

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

                return redirect(
                    url_for(
                        "dashboard.alias_contact_manager",
                        alias_id=alias_id,
                        highlight_contact_id=contact.id,
                    ))
        elif request.form.get("form-name") == "delete":
            contact_id = request.form.get("contact-id")
            contact = Contact.get(contact_id)

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

            delete_contact_email = contact.website_email
            Contact.delete(contact_id)
            db.session.commit()

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

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

        elif request.form.get("form-name") == "search":
            query = request.form.get("query")
            return redirect(
                url_for(
                    "dashboard.alias_contact_manager",
                    alias_id=alias_id,
                    query=query,
                    highlight_contact_id=highlight_contact_id,
                ))

    contact_infos = get_contact_infos(alias, page, query=query)
    last_page = len(contact_infos) < PAGE_LIMIT

    # if highlighted contact isn't included, fetch it
    # make sure highlighted contact is at array start
    contact_ids = [contact_info.contact.id for contact_info in contact_infos]
    if highlight_contact_id and highlight_contact_id not in contact_ids:
        contact_infos = (get_contact_infos(
            alias, contact_id=highlight_contact_id, query=query) +
                         contact_infos)

    return render_template(
        "dashboard/alias_contact_manager.html",
        contact_infos=contact_infos,
        alias=alias,
        new_contact_form=new_contact_form,
        highlight_contact_id=highlight_contact_id,
        page=page,
        last_page=last_page,
        query=query,
    )
Пример #29
0
def register():
    if current_user.is_authenticated:
        LOG.d("user is already authenticated, redirect to dashboard")
        flash("You are already logged in", "warning")
        return redirect(url_for("dashboard.index"))

    if config.DISABLE_REGISTRATION:
        flash("Registration is closed", "error")
        return redirect(url_for("auth.login"))

    form = RegisterForm(request.form)
    next_url = request.args.get("next")

    if form.validate_on_submit():
        # only check if hcaptcha is enabled
        if HCAPTCHA_SECRET:
            # check with hCaptcha
            token = request.form.get("h-captcha-response")
            params = {"secret": HCAPTCHA_SECRET, "response": token}
            hcaptcha_res = requests.post("https://hcaptcha.com/siteverify",
                                         data=params).json()
            # return something like
            # {'success': True,
            #  'challenge_ts': '2020-07-23T10:03:25',
            #  'hostname': '127.0.0.1'}
            if not hcaptcha_res["success"]:
                LOG.warning(
                    "User put wrong captcha %s %s",
                    form.email.data,
                    hcaptcha_res,
                )
                flash("Wrong Captcha", "error")
                return render_template(
                    "auth/register.html",
                    form=form,
                    next_url=next_url,
                    HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
                )

        email = sanitize_email(form.email.data)
        if not email_can_be_used_as_mailbox(email):
            flash("You cannot use this email address as your personal inbox.",
                  "error")

        else:
            if personal_email_already_used(email):
                flash(f"Email {email} already used", "error")
            else:
                LOG.debug("create user %s", email)
                user = User.create(
                    email=email,
                    name="",
                    password=form.password.data,
                    referral=get_referral(),
                )
                db.session.commit()

                try:
                    send_activation_email(user, next_url)
                except Exception:
                    flash("Invalid email, are you sure the email is correct?",
                          "error")
                    return redirect(url_for("auth.register"))

                return render_template("auth/register_waiting_activation.html")

    return render_template(
        "auth/register.html",
        form=form,
        next_url=next_url,
        HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
    )
Пример #30
0
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)