def test_suggested_emails_for_user_who_cannot_create_new_alias(flask_client): # make sure user is not in trial user = User.create( email="[email protected]", password="******", name="Test User", activated=True, trial_end=None, ) db.session.commit() # make sure user runs out of quota to create new email for i in range(MAX_NB_EMAIL_FREE_PLAN): GenEmail.create_new(user_id=user.id, prefix="test") db.session.commit() suggested_email, other_emails = user.suggested_emails(website_name="test") # the suggested email is chosen from existing GenEmail assert GenEmail.get_by(email=suggested_email) # all other emails are generated emails for email in other_emails: assert GenEmail.get_by(email=email)
def test_custom_mode(flask_client): user = User.create(email="[email protected]", password="******", name="Test User", activated=True) db.session.commit() # create api_key api_key = ApiKey.create(user.id, "for test") db.session.commit() # without note r = flask_client.post( url_for("api.new_random_alias", hostname="www.test.com", mode="uuid"), headers={"Authentication": api_key.code}, ) assert r.status_code == 201 # extract the uuid part alias = r.json["alias"] uuid_part = alias[:len(alias) - len(EMAIL_DOMAIN) - 1] assert is_valid_uuid(uuid_part) # with note r = flask_client.post( url_for("api.new_random_alias", hostname="www.test.com", mode="uuid"), headers={"Authentication": api_key.code}, json={"note": "test note"}, ) assert r.status_code == 201 alias = r.json["alias"] ge = GenEmail.get_by(email=alias) assert ge.note == "test note"
def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error="You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip() alias_suffix = data.get("alias_suffix", "").strip() note = data.get("note") alias_prefix = convert_to_id(alias_prefix) if not verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create( user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note ) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_alias), 201
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash("ony premium user can choose custom alias", "warning") return redirect(url_for("dashboard.index")) user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()] # List of (is_custom_domain, alias-suffix) suffixes = [] # put custom domain first for alias_domain in user_custom_domains: suffixes.append((True, "@" + alias_domain)) # then default domain for domain in ALIAS_DOMAINS: suffixes.append( ( False, ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain, ) ) if request.method == "POST": alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") if verify_prefix_suffix( current_user, alias_prefix, alias_suffix, user_custom_domains ): full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) flash( f"Alias {full_alias} already exists, please choose another one", "warning", ) else: gen_email = GenEmail.create(user_id=current_user.id, email=full_alias) db.session.commit() flash(f"Alias {full_alias} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template("dashboard/custom_alias.html", **locals())
def alias_log(alias): gen_email = GenEmail.get_by(email=alias) # sanity check if not gen_email: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if gen_email.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) return render_template("dashboard/alias_log.html", logs=get_alias_log(gen_email), alias=alias)
def alias_contact_manager(alias, forward_email_id=None): gen_email = GenEmail.get_by(email=alias) # sanity check if not gen_email: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if gen_email.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_email = new_contact_form.email.data # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break website_email = get_email_part(contact_email) # already been added if ForwardEmail.get_by( gen_email_id=gen_email.id, website_email=website_email ): flash(f"{website_email} is already added", "error") return redirect( url_for("dashboard.alias_contact_manager", alias=alias) ) forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=contact_email, reply_email=reply_email, ) LOG.d("create reverse-alias for %s", contact_email) db.session.commit() flash( f"Reverse alias for {contact_email} is created successfully", "success", ) return redirect( url_for( "dashboard.alias_contact_manager", alias=alias, forward_email_id=forward_email.id, ) ) elif request.form.get("form-name") == "delete": forward_email_id = request.form.get("forward-email-id") forward_email = ForwardEmail.get(forward_email_id) if not forward_email: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.alias_contact_manager", alias=alias)) elif forward_email.gen_email_id != gen_email.id: flash("You cannot delete reverse-alias", "warning") return redirect(url_for("dashboard.alias_contact_manager", alias=alias)) contact_name = forward_email.website_from ForwardEmail.delete(forward_email_id) db.session.commit() flash( f"Reverse-alias for {contact_name} has been deleted successfully", "success", ) return redirect(url_for("dashboard.alias_contact_manager", alias=alias)) # make sure highlighted forward_email is at array start forward_emails = gen_email.forward_emails if forward_email_id: forward_emails = sorted( forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True ) return render_template( "dashboard/alias_contact_manager.html", forward_emails=forward_emails, alias=gen_email.email, new_contact_form=new_contact_form, forward_email_id=forward_email_id, )
def setting(): form = SettingForm() promo_form = PromoCodeForm() email_change = EmailChange.get_by(user_id=current_user.id) if email_change: pending_email = email_change.new_email else: pending_email = None if request.method == "POST": if request.form.get("form-name") == "update-profile": if form.validate(): profile_updated = False # update user info if form.name.data != current_user.name: current_user.name = form.name.data db.session.commit() profile_updated = True if form.profile_picture.data: file_path = random_string(30) file = File.create(path=file_path) s3.upload_from_bytesio( file_path, BytesIO(form.profile_picture.data.read())) db.session.flush() LOG.d("upload file %s to s3", file) current_user.profile_picture_id = file.id db.session.commit() profile_updated = True if profile_updated: flash(f"Your profile has been updated", "success") if (form.email.data and form.email.data != current_user.email and not pending_email): new_email = form.email.data # check if this email is not used by other user, or as alias if (User.get_by(email=new_email) or GenEmail.get_by(email=new_email) or DeletedAlias.get_by(email=new_email)): flash(f"Email {new_email} already used", "error") elif new_email.endswith(EMAIL_DOMAIN): flash( "You cannot use alias as your personal inbox. Nice try though 😉", "error", ) else: email_change = EmailChange.create( user_id=current_user.id, code=random_string( 60), # todo: make sure the code is unique new_email=new_email, ) db.session.commit() send_change_email_confirmation(current_user, email_change) flash( "A confirmation email is on the way, please check your inbox", "success", ) elif request.form.get("form-name") == "change-password": send_reset_password_email(current_user) elif request.form.get("form-name") == "notification-preference": choose = request.form.get("notification") if choose == "on": current_user.notification = True else: current_user.notification = False db.session.commit() flash("Your notification preference has been updated", "success") elif request.form.get("form-name") == "delete-account": User.delete(current_user.id) db.session.commit() flash("Your account has been deleted", "success") logout_user() return redirect(url_for("auth.register")) elif request.form.get("form-name") == "change-alias-generator": scheme = int(request.form.get("alias-generator-scheme")) if AliasGeneratorEnum.has_value(scheme): current_user.alias_generator = scheme db.session.commit() flash("Your preference has been updated", "success") elif request.form.get("form-name") == "export-data": data = { "email": current_user.email, "name": current_user.name, "aliases": [], "apps": [], "custom_domains": [], } for alias in GenEmail.filter_by( user_id=current_user.id).all(): # type: GenEmail data["aliases"].append( dict(email=alias.email, enabled=alias.enabled)) for custom_domain in CustomDomain.filter_by( user_id=current_user.id).all(): data["custom_domains"].append(custom_domain.domain) for app in Client.filter_by( user_id=current_user.id): # type: Client data["apps"].append( dict(name=app.name, home_url=app.home_url, published=app.published)) return Response( json.dumps(data), mimetype="text/json", headers={ "Content-Disposition": "attachment;filename=data.json" }, ) return redirect(url_for("dashboard.setting")) return render_template( "dashboard/setting.html", form=form, PlanEnum=PlanEnum, promo_form=promo_form, pending_email=pending_email, AliasGeneratorEnum=AliasGeneratorEnum, )
def handle_forward(self, envelope, smtp: SMTP, msg: Message) -> str: """return *status_code message*""" alias = envelope.rcpt_tos[0].lower() # alias@SL gen_email = GenEmail.get_by(email=alias) if not gen_email: LOG.d( "alias %s not exist. Try to see if it can be created on the fly", alias) # try to see if alias could be created on-the-fly on_the_fly = False # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format if email_belongs_to_alias_domains(alias): if "/" in alias or "+" in alias or "#" in alias: if "/" in alias: sep = "/" elif "+" in alias: sep = "+" else: sep = "#" directory_name = alias[:alias.find(sep)] LOG.d("directory_name %s", directory_name) directory = Directory.get_by(name=directory_name) # Only premium user can use the directory feature if directory: dir_user = directory.user if dir_user.is_premium(): LOG.d("create alias %s for directory %s", alias, directory) on_the_fly = True gen_email = GenEmail.create( email=alias, user_id=directory.user_id, directory_id=directory.id, ) db.session.commit() else: LOG.error( "User %s is not premium anymore and cannot create alias with directory", dir_user, ) send_cannot_create_directory_alias( dir_user, alias, directory_name) # try to create alias on-the-fly with custom-domain catch-all feature # check if alias is custom-domain alias and if the custom-domain has catch-all enabled if not on_the_fly: alias_domain = get_email_domain_part(alias) custom_domain = CustomDomain.get_by(domain=alias_domain) # Only premium user can continue using the catch-all feature if custom_domain and custom_domain.catch_all: domain_user = custom_domain.user if domain_user.is_premium(): LOG.d("create alias %s for domain %s", alias, custom_domain) on_the_fly = True gen_email = GenEmail.create( email=alias, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, ) db.session.commit() else: LOG.error( "User %s is not premium anymore and cannot create alias with domain %s", domain_user, alias_domain, ) send_cannot_create_domain_alias( domain_user, alias, alias_domain) if not on_the_fly: LOG.d("alias %s cannot be created on-the-fly, return 510", alias) return "510 Email not exist" user_email = gen_email.user.email website_email = get_email_part(msg["From"]) forward_email = ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email) if not forward_email: LOG.debug( "create forward email for alias %s and website email %s", alias, website_email, ) # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=msg["From"], reply_email=reply_email, ) db.session.commit() forward_log = ForwardEmailLog.create(forward_id=forward_email.id) if gen_email.enabled: # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") # remove reply-to header if present delete_header(msg, "Reply-To") # change the from header so the sender comes from @SL # so it can pass DMARC check # replace the email part in from: header from_header = (get_email_name(msg["From"]) + " - " + website_email.replace("@", " at ") + f" <{forward_email.reply_email}>") msg.replace_header("From", from_header) LOG.d("new from header:%s", from_header) # add List-Unsubscribe header unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click") add_dkim_signature(msg, EMAIL_DOMAIN) LOG.d( "Forward mail from %s to %s, mail_options %s, rcpt_options %s ", website_email, user_email, envelope.mail_options, envelope.rcpt_options, ) # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( forward_email.reply_email, user_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) else: LOG.d("%s is disabled, do not forward", gen_email) forward_log.blocked = True db.session.commit() return "250 Message accepted for delivery"
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash( "You have reached free plan limit, please upgrade to create new aliases", "warning", ) return redirect(url_for("dashboard.index")) user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()] # List of (is_custom_domain, alias-suffix) suffixes = [] # put custom domain first for alias_domain in user_custom_domains: suffixes.append((True, "@" + alias_domain)) # then default domain for domain in ALIAS_DOMAINS: suffixes.append( ( False, ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain, ) ) if request.method == "POST": alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") alias_note = request.form.get("note") if verify_prefix_suffix( current_user, alias_prefix, alias_suffix, user_custom_domains ): full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by( email=full_alias ): LOG.d("full alias already used %s", full_alias) flash( f"Alias {full_alias} already exists, please choose another one", "warning", ) else: gen_email = GenEmail.create( user_id=current_user.id, email=full_alias, note=alias_note ) # get the custom_domain_id if alias is created with a custom domain alias_domain = get_email_domain_part(full_alias) custom_domain = CustomDomain.get_by(domain=alias_domain) if custom_domain: gen_email.custom_domain_id = custom_domain.id db.session.commit() flash(f"Alias {full_alias} has been created", "success") return redirect( url_for("dashboard.index", highlight_gen_email_id=gen_email.id) ) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template("dashboard/custom_alias.html", **locals())
def handle_forward(self, envelope, smtp: SMTP, msg: EmailMessage) -> str: """return *status_code message*""" alias = envelope.rcpt_tos[0] # alias@SL gen_email = GenEmail.get_by(email=alias) if not gen_email: LOG.d("alias %s not exist") return "510 Email not exist" user_email = gen_email.user.email website_email = get_email_part(msg["From"]) forward_email = ForwardEmail.get_by( gen_email_id=gen_email.id, website_email=website_email ) if not forward_email: LOG.debug( "create forward email for alias %s and website email %s", alias, website_email, ) # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=msg["From"], reply_email=reply_email, ) db.session.commit() forward_log = ForwardEmailLog.create(forward_id=forward_email.id) if gen_email.enabled: # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") # remove reply-to header if present if msg["Reply-To"]: LOG.d("Delete reply-to header %s", msg["Reply-To"]) del msg["Reply-To"] # change the from header so the sender comes from @SL # so it can pass DMARC check # replace the email part in from: header from_header = ( get_email_name(msg["From"]) + " - " + website_email.replace("@", " at ") + f" <{forward_email.reply_email}>" ) msg.replace_header("From", from_header) LOG.d("new from header:%s", from_header) # add List-Unsubscribe header unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header( msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click" ) add_dkim_signature(msg, EMAIL_DOMAIN) LOG.d( "Forward mail from %s to %s, mail_options %s, rcpt_options %s ", website_email, user_email, envelope.mail_options, envelope.rcpt_options, ) # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( forward_email.reply_email, user_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) else: LOG.d("%s is disabled, do not forward", gen_email) forward_log.blocked = True db.session.commit() return "250 Message accepted for delivery"
def custom_alias(): # check if user has the right to create custom alias if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash("ony premium user can choose custom alias", "warning") return redirect(url_for("dashboard.index")) error = "" if request.method == "POST": if request.form.get("form-name") == "non-custom-domain-name": email_prefix = request.form.get("email-prefix") email_prefix = convert_to_id(email_prefix) email_suffix = request.form.get("email-suffix") if not email_prefix: error = "alias prefix cannot be empty" else: full_email = f"{email_prefix}.{email_suffix}@{EMAIL_DOMAIN}" # check if email already exists if GenEmail.get_by(email=full_email) or DeletedAlias.get_by( email=full_email): error = "email already chosen, please choose another one" else: # create the new alias LOG.d("create custom alias %s for user %s", full_email, current_user) gen_email = GenEmail.create(email=full_email, user_id=current_user.id) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) elif request.form.get("form-name") == "custom-domain-name": custom_domain_id = request.form.get("custom-domain-id") email = request.form.get("email") custom_domain = CustomDomain.get(custom_domain_id) if not custom_domain: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif custom_domain.user_id != current_user.id: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif not custom_domain.verified: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) full_email = f"{email}@{custom_domain.domain}" if GenEmail.get_by(email=full_email): error = f"{full_email} already exist, please choose another one" else: LOG.d( "create custom alias %s for custom domain %s", full_email, custom_domain.domain, ) gen_email = GenEmail.create( email=full_email, user_id=current_user.id, custom_domain_id=custom_domain.id, ) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) email_suffix = random_word() return render_template( "dashboard/custom_alias.html", error=error, email_suffix=email_suffix, EMAIL_DOMAIN=EMAIL_DOMAIN, custom_domains=current_user.verified_custom_domains(), )
def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args Output: 201 if success 409 if alias already exists """ user = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create custom alias", user) return ( jsonify( error="You have created 3 custom aliases, please upgrade to create more" ), 400, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() alias_prefix = data["alias_prefix"] alias_suffix = data["alias_suffix"] # make sure alias_prefix is not empty alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) if not alias_prefix: # should be checked on frontend LOG.d("user %s submits empty alias prefix %s", user, alias_prefix) return jsonify(error="alias prefix cannot be empty"), 400 # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() if alias_suffix.startswith("@"): custom_domain = alias_suffix[1:] if custom_domain not in user_custom_domains: LOG.d("user %s submits wrong custom domain %s ", user, custom_domain) return jsonify(error="error"), 400 else: if not alias_suffix.startswith("."): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 if not alias_suffix.endswith(EMAIL_DOMAIN): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 random_letters = alias_suffix[1 : alias_suffix.find("@")] if len(random_letters) < 5: LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create(user_id=user.id, email=full_alias) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_alias), 201
def authorize(): """ Redirected from client when user clicks on "Login with Server". This is a GET request with the following field in url - client_id - (optional) state - response_type: must be code """ oauth_client_id = request.args.get("client_id") state = request.args.get("state") scope = request.args.get("scope") redirect_uri = request.args.get("redirect_uri") response_mode = request.args.get("response_mode") nonce = request.args.get("nonce") try: response_types: [ResponseType] = get_response_types(request) except ValueError: return ( "response_type must be code, token, id_token or certain combination of these." " Please see /.well-known/openid-configuration to see what response_type are supported ", 400, ) if set(response_types) not in SUPPORTED_OPENID_FLOWS: return ( f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}", 400, ) if not redirect_uri: LOG.d("no redirect uri") return "redirect_uri must be set", 400 client = Client.get_by(oauth_client_id=oauth_client_id) if not client: final_redirect_uri = ( f"{redirect_uri}?error=invalid_client_id&client_id={oauth_client_id}" ) return redirect(final_redirect_uri) # check if redirect_uri is valid # allow localhost by default hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1": # support custom scheme for mobile app if scheme == "http": final_redirect_uri = f"{redirect_uri}?error=http_not_allowed" return redirect(final_redirect_uri) if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri): final_redirect_uri = f"{redirect_uri}?error=unknown_redirect_uri" return redirect(final_redirect_uri) # redirect from client website if request.method == "GET": if current_user.is_authenticated: suggested_email, other_emails, email_suffix = None, [], None suggested_name, other_names = None, [] # user has already allowed this client client_user: ClientUser = ClientUser.get_by( client_id=client.id, user_id=current_user.id) user_info = {} if client_user: LOG.debug("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() else: suggested_email, other_emails = current_user.suggested_emails( client.name) suggested_name, other_names = current_user.suggested_names() user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] # List of (is_custom_domain, alias-suffix) suffixes = [] # put custom domain first for alias_domain in user_custom_domains: suffixes.append((True, "@" + alias_domain)) # then default domain for domain in ALIAS_DOMAINS: suffixes.append(( False, ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain, )) return render_template( "oauth/authorize.html", Scope=Scope, EMAIL_DOMAIN=EMAIL_DOMAIN, **locals(), ) else: # after user logs in, redirect user back to this page return render_template( "oauth/authorize_nonlogin_user.html", client=client, next=request.url, Scope=Scope, ) else: # POST - user allows or denies if request.form.get("button") == "deny": LOG.debug("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.debug("User %s allows Client %s", current_user, client) client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id) # user has already allowed this client, user cannot change information if client_user: LOG.d("user %s has already allowed client %s", current_user, client) else: alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") gen_email = None # user creates a new alias, not using suggested alias if alias_prefix: # should never happen as this is checked on the front-end if not current_user.can_create_new_alias(): raise Exception( f"User {current_user} cannot create custom email") user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] from app.dashboard.views.custom_alias import verify_prefix_suffix if verify_prefix_suffix(current_user, alias_prefix, alias_suffix, user_custom_domains): full_alias = alias_prefix + alias_suffix if GenEmail.get_by( email=full_alias) or DeletedAlias.get_by( email=full_alias): LOG.error("alias %s already used, very rare!", full_alias) flash(f"Alias {full_alias} already used", "error") return redirect(request.url) else: gen_email = GenEmail.create( user_id=current_user.id, email=full_alias, mailbox_id=current_user.default_mailbox_id, ) # get the custom_domain_id if alias is created with a custom domain alias_domain = get_email_domain_part(full_alias) custom_domain = CustomDomain.get_by( domain=alias_domain) if custom_domain: gen_email.custom_domain_id = custom_domain.id db.session.flush() flash(f"Alias {full_alias} has been created", "success") # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return redirect(request.url) # User chooses one of the suggestions else: chosen_email = request.form.get("suggested-email") # todo: add some checks on chosen_email if chosen_email != current_user.email: gen_email = GenEmail.get_by(email=chosen_email) if not gen_email: gen_email = GenEmail.create( email=chosen_email, user_id=current_user.id, mailbox_id=current_user.default_mailbox_id, ) db.session.flush() suggested_name = request.form.get("suggested-name") custom_name = request.form.get("custom-name") use_default_avatar = request.form.get("avatar-choice") == "default" client_user = ClientUser.create(client_id=client.id, user_id=current_user.id) if gen_email: client_user.gen_email_id = gen_email.id if custom_name: client_user.name = custom_name elif suggested_name != current_user.name: client_user.name = suggested_name if use_default_avatar: # use default avatar LOG.d("use default avatar for user %s client %s", current_user, client) client_user.default_avatar = True db.session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = {} if state: redirect_args["state"] = state else: LOG.warning( "more security reason, state should be added. client %s", client) if scope: redirect_args["scope"] = scope auth_code = None if ResponseType.CODE in response_types: # Create authorization code auth_code = AuthorizationCode.create( client_id=client.id, user_id=current_user.id, code=random_string(), scope=scope, redirect_uri=redirect_uri, response_type=response_types_to_str(response_types), ) db.session.add(auth_code) redirect_args["code"] = auth_code.code oauth_token = None if ResponseType.TOKEN in response_types: # create access-token oauth_token = OauthToken.create( client_id=client.id, user_id=current_user.id, scope=scope, redirect_uri=redirect_uri, access_token=generate_access_token(), response_type=response_types_to_str(response_types), ) db.session.add(oauth_token) redirect_args["access_token"] = oauth_token.access_token if ResponseType.ID_TOKEN in response_types: redirect_args["id_token"] = make_id_token( client_user, nonce, oauth_token.access_token if oauth_token else None, auth_code.code if auth_code else None, ) db.session.commit() # should all params appended the url using fragment (#) or query fragment = False if response_mode and response_mode == "fragment": fragment = True # if response_types contain "token" => implicit flow => should use fragment # except if client sets explicitly response_mode if not response_mode: if ResponseType.TOKEN in response_types: fragment = True # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))
def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: """return *status_code message*""" alias = rcpt_to.lower() # alias@SL gen_email = GenEmail.get_by(email=alias) if not gen_email: LOG.d("alias %s not exist. Try to see if it can be created on the fly", alias) gen_email = try_auto_create(alias) if not gen_email: LOG.d("alias %s cannot be created on-the-fly, return 510", alias) return "510 Email not exist" mailbox = gen_email.mailbox mailbox_email = mailbox.email # create PGP email if needed if mailbox.pgp_finger_print: LOG.d("Encrypt message using mailbox %s", mailbox) msg = prepare_pgp_message(msg, mailbox.pgp_finger_print) forward_email = get_or_create_forward_email(msg["From"], gen_email) forward_log = ForwardEmailLog.create(forward_id=forward_email.id) if gen_email.enabled: # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") # remove reply-to & sender header if present delete_header(msg, "Reply-To") delete_header(msg, "Sender") # change the from header so the sender comes from @SL # so it can pass DMARC check # replace the email part in from: header website_from_header = msg["From"] website_email = get_email_part(website_from_header) from_header = ( get_email_name(website_from_header) + ("" if get_email_name(website_from_header) == "" else " - ") + website_email.replace("@", " at ") + f" <{forward_email.reply_email}>" ) add_or_replace_header(msg, "From", from_header) LOG.d("new from header:%s", from_header) # append alias into the TO header if it's not present in To or CC if should_append_alias(msg, alias): LOG.d("append alias %s to TO header %s", alias, msg["To"]) if msg["To"]: to_header = msg["To"] + "," + alias else: to_header = alias add_or_replace_header(msg, "To", to_header) # add List-Unsubscribe header unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header( msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click" ) add_dkim_signature(msg, EMAIL_DOMAIN) LOG.d( "Forward mail from %s to %s, mail_options %s, rcpt_options %s ", website_email, mailbox_email, envelope.mail_options, envelope.rcpt_options, ) # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( forward_email.reply_email, mailbox_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) else: LOG.d("%s is disabled, do not forward", gen_email) forward_log.blocked = True db.session.commit() return "250 Message accepted for delivery"
def authorize(): """ Redirected from client when user clicks on "Login with Server". This is a GET request with the following field in url - client_id - (optional) state - response_type: must be code """ oauth_client_id = request.args.get("client_id") state = request.args.get("state") scope = request.args.get("scope") redirect_uri = request.args.get("redirect_uri") response_mode = request.args.get("response_mode") nonce = request.args.get("nonce") try: response_types: [ResponseType] = get_response_types(request) except ValueError: return ( "response_type must be code, token, id_token or certain combination of these." " Please see /.well-known/openid-configuration to see what response_type are supported ", 400, ) if set(response_types) not in SUPPORTED_OPENID_FLOWS: return ( f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}", 400, ) if not redirect_uri: LOG.d("no redirect uri") return "redirect_uri must be set", 400 client = Client.get_by(oauth_client_id=oauth_client_id) if not client: final_redirect_uri = ( f"{redirect_uri}?error=invalid_client_id&client_id={oauth_client_id}" ) return redirect(final_redirect_uri) # check if redirect_uri is valid # allow localhost by default hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1": # support custom scheme for mobile app if scheme == "http": final_redirect_uri = f"{redirect_uri}?error=http_not_allowed" return redirect(final_redirect_uri) if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri): final_redirect_uri = f"{redirect_uri}?error=unknown_redirect_uri" return redirect(final_redirect_uri) # redirect from client website if request.method == "GET": if current_user.is_authenticated: suggested_email, other_emails, email_suffix = None, [], None suggested_name, other_names = None, [] # user has already allowed this client client_user: ClientUser = ClientUser.get_by( client_id=client.id, user_id=current_user.id ) user_info = {} if client_user: LOG.debug("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() else: suggested_email, other_emails = current_user.suggested_emails( client.name ) suggested_name, other_names = current_user.suggested_names() email_suffix = random_word() return render_template( "oauth/authorize.html", client=client, user_info=user_info, client_user=client_user, Scope=Scope, suggested_email=suggested_email, personal_email=current_user.email, suggested_name=suggested_name, other_names=other_names, other_emails=other_emails, email_suffix=email_suffix, EMAIL_DOMAIN=EMAIL_DOMAIN, ) else: # after user logs in, redirect user back to this page return render_template( "oauth/authorize_nonlogin_user.html", client=client, next=request.url, Scope=Scope, ) else: # user allows or denies if request.form.get("button") == "deny": LOG.debug("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.debug("User %s allows Client %s", current_user, client) client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id) # user has already allowed this client, user cannot change information if client_user: LOG.d("user %s has already allowed client %s", current_user, client) else: email_suffix = request.form.get("email-suffix") custom_email_prefix = request.form.get("custom-email-prefix") chosen_email = request.form.get("suggested-email") suggested_name = request.form.get("suggested-name") custom_name = request.form.get("custom-name") use_default_avatar = request.form.get("avatar-choice") == "default" gen_email = None if custom_email_prefix: # check if user can generate custom email if not current_user.can_create_new_alias(): raise Exception(f"User {current_user} cannot create custom email") email = f"{convert_to_id(custom_email_prefix)}.{email_suffix}@{EMAIL_DOMAIN}" LOG.d("create custom email alias %s for user %s", email, current_user) if GenEmail.get_by(email=email) or DeletedAlias.get_by(email=email): LOG.error("email %s already used, very rare!", email) flash(f"alias {email} already used", "error") return redirect(request.url) gen_email = GenEmail.create(email=email, user_id=current_user.id) db.session.flush() else: # user picks an email from suggestion if chosen_email != current_user.email: gen_email = GenEmail.get_by(email=chosen_email) if not gen_email: gen_email = GenEmail.create( email=chosen_email, user_id=current_user.id ) db.session.flush() client_user = ClientUser.create( client_id=client.id, user_id=current_user.id ) if gen_email: client_user.gen_email_id = gen_email.id if custom_name: LOG.d( "use custom name %s for user %s client %s", custom_name, current_user, client, ) client_user.name = custom_name elif suggested_name != current_user.name: LOG.d( "use another name %s for user %s client %s", custom_name, current_user, client, ) client_user.name = suggested_name if use_default_avatar: # use default avatar LOG.d("use default avatar for user %s client %s", current_user, client) client_user.default_avatar = True db.session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = {} if state: redirect_args["state"] = state else: LOG.warning( "more security reason, state should be added. client %s", client ) if scope: redirect_args["scope"] = scope auth_code = None if ResponseType.CODE in response_types: # Create authorization code auth_code = AuthorizationCode.create( client_id=client.id, user_id=current_user.id, code=random_string(), scope=scope, redirect_uri=redirect_uri, response_type=response_types_to_str(response_types), ) db.session.add(auth_code) redirect_args["code"] = auth_code.code oauth_token = None if ResponseType.TOKEN in response_types: # create access-token oauth_token = OauthToken.create( client_id=client.id, user_id=current_user.id, scope=scope, redirect_uri=redirect_uri, access_token=generate_access_token(), response_type=response_types_to_str(response_types), ) db.session.add(oauth_token) redirect_args["access_token"] = oauth_token.access_token if ResponseType.ID_TOKEN in response_types: redirect_args["id_token"] = make_id_token( client_user, nonce, oauth_token.access_token if oauth_token else None, auth_code.code if auth_code else None, ) db.session.commit() # should all params appended the url using fragment (#) or query fragment = False if response_mode and response_mode == "fragment": fragment = True # if response_types contain "token" => implicit flow => should use fragment # except if client sets explicitly response_mode if not response_mode: if ResponseType.TOKEN in response_types: fragment = True # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))