def test_can_be_used_as_personal_email(flask_client): # default alias domain assert not email_domain_can_be_used_as_mailbox("*****@*****.**") assert not email_domain_can_be_used_as_mailbox("*****@*****.**") assert email_domain_can_be_used_as_mailbox("*****@*****.**") # custom domain user = User.create(email="[email protected]", password="******", name="Test User", activated=True) db.session.commit() CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) db.session.commit() assert not email_domain_can_be_used_as_mailbox("*****@*****.**") # disposable domain assert not email_domain_can_be_used_as_mailbox("*****@*****.**") assert not email_domain_can_be_used_as_mailbox("*****@*****.**") # subdomain will not work assert not email_domain_can_be_used_as_mailbox("*****@*****.**") # valid domains should not be affected assert email_domain_can_be_used_as_mailbox("*****@*****.**") assert email_domain_can_be_used_as_mailbox("*****@*****.**") assert email_domain_can_be_used_as_mailbox("*****@*****.**")
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).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) # 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.verified = False else: log_func = LOG.warning 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")
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: user.default_mailbox_id = mailbox.id changed = True if "email" in data: new_email = data.get("email").lower().strip() if mailbox_already_used(new_email, user): return jsonify(error=f"{new_email} already used"), 400 elif not email_domain_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: db.session.commit() return jsonify(updated=True), 200
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 = user_info.get("email").strip().lower() user = User.get_by(email=email) if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 if not email_domain_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.lower(), 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
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 = user_info.get("email").strip().lower() user = User.get_by(email=email) if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 if not email_domain_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.lower(), 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
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 = data.get("email").strip().lower() password = data.get("password") if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 if not email_domain_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 LOG.debug("create user %s", email) user = User.create(email=email, name="", password=password) db.session.flush() # 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, f"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
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(): email = form.email.data.strip().lower() if not email_domain_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", form.email.data) user = User.create( email=email, name="", password=form.password.data, referral=get_referral(), ) db.session.commit() try: send_activation_email(user, next_url) except: 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)
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 - id - email - verified """ user = g.user mailbox_email = request.get_json().get("email").lower().strip().replace( " ", "") if mailbox_already_used(mailbox_email, user): return jsonify(error=f"{mailbox_email} already used"), 400 elif not email_domain_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( id=new_mailbox.id, email=new_mailbox.email, verified=new_mailbox.verified, default=user.default_mailbox_id == new_mailbox.id, ), 201, )
def sanity_check(): """ #TODO: investigate why DNS sometimes not working Different sanity checks - detect if there's mailbox that's using a invalid domain """ for mailbox in Mailbox.filter_by(verified=True).all(): # 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) # 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 else: log_func = LOG.warning 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() != 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")
def mailbox_detail_route(mailbox_id): mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != current_user.id: flash("You cannot see this page", "warning") return redirect(url_for("dashboard.index")) change_email_form = ChangeEmailForm() if mailbox.new_email: pending_email = mailbox.new_email else: pending_email = None if request.method == "POST": if ( request.form.get("form-name") == "update-email" and change_email_form.validate_on_submit() ): new_email = change_email_form.email.data.lower().strip() if new_email != mailbox.email and not pending_email: # check if this email is not already used if mailbox_already_used(new_email, current_user) or Alias.get_by( email=new_email ): flash(f"Email {new_email} already used", "error") elif not email_domain_can_be_used_as_mailbox(new_email): flash("You cannot use this email address as your mailbox", "error") else: mailbox.new_email = new_email db.session.commit() try: verify_mailbox_change(current_user, mailbox, new_email) except SMTPRecipientsRefused: flash( f"Incorrect mailbox, please recheck {mailbox.email}", "error", ) else: flash( f"You are going to receive an email to confirm {new_email}.", "success", ) return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) ) elif request.form.get("form-name") == "force-spf": if not ENFORCE_SPF: flash("SPF enforcement globally not enabled", "error") return redirect(url_for("dashboard.index")) mailbox.force_spf = ( True if request.form.get("spf-status") == "on" else False ) db.session.commit() flash( "SPF enforcement was " + "enabled" if request.form.get("spf-status") else "disabled" + " successfully", "success", ) return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) ) elif request.form.get("form-name") == "pgp": if request.form.get("action") == "save": if not current_user.is_premium(): flash("Only premium plan can add PGP Key", "warning") return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) ) mailbox.pgp_public_key = request.form.get("pgp") try: mailbox.pgp_finger_print = load_public_key(mailbox.pgp_public_key) except PGPException: flash("Cannot add the public key, please verify it", "error") else: db.session.commit() flash("Your PGP public key is saved successfully", "success") return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) ) elif request.form.get("action") == "remove": # Free user can decide to remove their added PGP key mailbox.pgp_public_key = None mailbox.pgp_finger_print = None db.session.commit() flash("Your PGP public key is removed successfully", "success") return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) ) spf_available = ENFORCE_SPF return render_template("dashboard/mailbox_detail.html", **locals())
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 = form.email.data.strip().lower() if not email_domain_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: 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, )
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 (change_email_form.email.data.lower().strip() != current_user.email and not pending_email): new_email = change_email_form.email.data.strip().lower() # 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_domain_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(f"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: public_domain = PublicDomain.get_by(domain=default_domain) if public_domain: # make sure only default_random_alias_domain_id or default_random_alias_public_domain_id is set current_user.default_random_alias_public_domain_id = ( public_domain.id) current_user.default_random_alias_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: # make sure only default_random_alias_domain_id or # default_random_alias_public_domain_id is set current_user.default_random_alias_domain_id = ( custom_domain.id) current_user.default_random_alias_public_domain_id = None else: current_user.default_random_alias_domain_id = None current_user.default_random_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") == "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" }, ) manual_sub = ManualSubscription.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, FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN, )
def mailbox_route(): mailboxes = (Mailbox.query.filter_by(user_id=current_user.id).order_by( Mailbox.created_at.desc()).all()) new_mailbox_form = NewMailboxForm() if request.method == "POST": if request.form.get("form-name") == "delete": mailbox_id = request.form.get("mailbox-id") mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != current_user.id: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.mailbox_route")) if mailbox.id == current_user.default_mailbox_id: flash("You cannot delete default mailbox", "error") return redirect(url_for("dashboard.mailbox_route")) email = mailbox.email Mailbox.delete(mailbox_id) db.session.commit() flash(f"Mailbox {email} has been deleted", "success") return redirect(url_for("dashboard.mailbox_route")) if request.form.get("form-name") == "set-default": mailbox_id = request.form.get("mailbox-id") mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != current_user.id: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.mailbox_route")) if mailbox.id == current_user.default_mailbox_id: flash("This mailbox is already default one", "error") return redirect(url_for("dashboard.mailbox_route")) if not mailbox.verified: flash("Cannot set unverified mailbox as default", "error") return redirect(url_for("dashboard.mailbox_route")) current_user.default_mailbox_id = mailbox.id db.session.commit() flash(f"Mailbox {mailbox.email} is set as Default Mailbox", "success") return redirect(url_for("dashboard.mailbox_route")) elif request.form.get("form-name") == "create": if not current_user.is_premium(): flash("Only premium plan can add additional mailbox", "warning") return redirect(url_for("dashboard.mailbox_route")) if new_mailbox_form.validate(): mailbox_email = new_mailbox_form.email.data.lower() if mailbox_already_used(mailbox_email, current_user): flash(f"{mailbox_email} already used", "error") elif not email_domain_can_be_used_as_mailbox(mailbox_email): flash(f"You cannot use {mailbox_email}.", "error") else: new_mailbox = Mailbox.create(email=mailbox_email, user_id=current_user.id) db.session.commit() s = Signer(MAILBOX_SECRET) mailbox_id_signed = s.sign(str(new_mailbox.id)).decode() verification_url = (URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}") send_email( mailbox_email, f"Please confirm your email {mailbox_email}", render( "transactional/verify-mailbox.txt", user=current_user, link=verification_url, mailbox_email=mailbox_email, ), render( "transactional/verify-mailbox.html", user=current_user, link=verification_url, mailbox_email=mailbox_email, ), ) flash( f"You are going to receive an email to confirm {mailbox_email}.", "success", ) return redirect( url_for("dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id)) return render_template( "dashboard/mailbox.html", mailboxes=mailboxes, new_mailbox_form=new_mailbox_form, EMAIL_DOMAIN=EMAIL_DOMAIN, ALIAS_DOMAINS=ALIAS_DOMAINS, )