def compute_metric2() -> Metric2: now = arrow.now() _24h_ago = now.shift(days=-1) nb_referred_user_paid = 0 for user in User.filter(User.referral_id.isnot(None)): if user.is_paid(): nb_referred_user_paid += 1 return Metric2.create( date=now, # user stats nb_user=User.count(), nb_activated_user=User.filter_by(activated=True).count(), # subscription stats nb_premium=Subscription.filter( Subscription.cancelled.is_(False)).count(), nb_cancelled_premium=Subscription.filter( Subscription.cancelled.is_(True)).count(), # todo: filter by expires_date > now nb_apple_premium=AppleSubscription.count(), nb_manual_premium=ManualSubscription.filter( ManualSubscription.end_at > now, ManualSubscription.is_giveaway.is_(False), ).count(), nb_coinbase_premium=CoinbaseSubscription.filter( CoinbaseSubscription.end_at > now).count(), # referral stats nb_referred_user=User.filter(User.referral_id.isnot(None)).count(), nb_referred_user_paid=nb_referred_user_paid, nb_alias=Alias.count(), # email log stats nb_forward_last_24h=EmailLog.filter( EmailLog.created_at > _24h_ago).filter_by(bounced=False, is_spam=False, is_reply=False, blocked=False).count(), nb_bounced_last_24h=EmailLog.filter( EmailLog.created_at > _24h_ago).filter_by(bounced=True).count(), nb_total_bounced_last_24h=Bounce.filter( Bounce.created_at > _24h_ago).count(), nb_reply_last_24h=EmailLog.filter( EmailLog.created_at > _24h_ago).filter_by(is_reply=True).count(), nb_block_last_24h=EmailLog.filter( EmailLog.created_at > _24h_ago).filter_by(blocked=True).count(), # other stats nb_verified_custom_domain=CustomDomain.filter_by( verified=True).count(), nb_subdomain=CustomDomain.filter_by(is_sl_subdomain=True).count(), nb_directory=Directory.count(), nb_deleted_directory=DeletedDirectory.count(), nb_deleted_subdomain=DeletedSubdomain.count(), nb_app=Client.count(), commit=True, )
def export_data(): """ Get user data Output: Alias, custom domain and app info """ user = g.user data = { "email": user.email, "name": user.name, "aliases": [], "apps": [], "custom_domains": [], } for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias data["aliases"].append(dict(email=alias.email, enabled=alias.enabled)) for custom_domain in CustomDomain.filter_by(user_id=user.id).all(): data["custom_domains"].append(custom_domain.domain) for app in Client.filter_by(user_id=user.id): # type: Client data["apps"].append(dict(name=app.name, home_url=app.home_url)) return jsonify(data)
def get_custom_domains(): user = g.user custom_domains = CustomDomain.filter_by(user_id=user.id, is_sl_subdomain=False).all() return jsonify( custom_domains=[custom_domain_to_dict(cd) for cd in custom_domains])
def check_custom_domain(): LOG.d("Check verified domain for DNS issues") for custom_domain in CustomDomain.filter_by( verified=True): # type: CustomDomain try: check_single_custom_domain(custom_domain) except ObjectDeletedError: LOG.i("custom domain has been deleted")
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_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: 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")) # 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 = sl_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" }, ) 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) 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 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 custom_domain(): custom_domains = CustomDomain.filter_by(user_id=current_user.id, is_sl_subdomain=False).all() mailboxes = current_user.mailboxes() new_custom_domain_form = NewCustomDomainForm() errors = {} if request.method == "POST": if request.form.get("form-name") == "create": if not current_user.is_premium(): flash("Only premium plan can add custom domain", "warning") return redirect(url_for("dashboard.custom_domain")) if new_custom_domain_form.validate(): new_domain = new_custom_domain_form.domain.data.lower().strip() if new_domain.startswith("http://"): new_domain = new_domain[len("http://"):] if new_domain.startswith("https://"): new_domain = new_domain[len("https://"):] if SLDomain.get_by(domain=new_domain): flash("A custom domain cannot be a built-in domain.", "error") elif CustomDomain.get_by(domain=new_domain): flash(f"{new_domain} already used", "error") elif get_email_domain_part(current_user.email) == new_domain: flash( "You cannot add a domain that you are currently using for your personal email. " "Please change your personal email to your real email", "error", ) elif Mailbox.filter( Mailbox.verified.is_(True), Mailbox.email.endswith(f"@{new_domain}")).first(): flash( f"{new_domain} already used in a SimpleLogin mailbox", "error") else: new_custom_domain = CustomDomain.create( domain=new_domain, user_id=current_user.id) # new domain has ownership verified if its parent has the ownership verified for root_cd in current_user.custom_domains: if (new_domain.endswith("." + root_cd.domain) and root_cd.ownership_verified): LOG.i( "%s ownership verified thanks to %s", new_custom_domain, root_cd, ) new_custom_domain.ownership_verified = True Session.commit() mailbox_ids = request.form.getlist("mailbox_ids") if mailbox_ids: # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if (not mailbox or mailbox.user_id != current_user.id or not mailbox.verified): flash("Something went wrong, please retry", "warning") return redirect( url_for("dashboard.custom_domain")) mailboxes.append(mailbox) for mailbox in mailboxes: DomainMailbox.create( domain_id=new_custom_domain.id, mailbox_id=mailbox.id) Session.commit() flash(f"New domain {new_custom_domain.domain} is created", "success") return redirect( url_for( "dashboard.domain_detail_dns", custom_domain_id=new_custom_domain.id, )) return render_template( "dashboard/custom_domain.html", custom_domains=custom_domains, new_custom_domain_form=new_custom_domain_form, EMAIL_SERVERS_WITH_PRIORITY=EMAIL_SERVERS_WITH_PRIORITY, errors=errors, mailboxes=mailboxes, )
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(): 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") elif not email_domain_can_be_used_as_mailbox(new_email): flash( "You cannot use this email address as your personal inbox.", "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", ) 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: default_domain_id = int(default_domain) # sanity check domain = CustomDomain.get(default_domain_id) if (not domain or domain.user_id != current_user.id or not domain.verified): flash( "Something went wrong, sorry for the inconvenience. Please retry. ", "error", ) return redirect(url_for("dashboard.setting")) current_user.default_random_alias_domain_id = default_domain_id else: current_user.default_random_alias_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 get_alias_suffixes(user: User) -> [AliasSuffix]: """ Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up. """ user_custom_domains = CustomDomain.filter_by( user_id=user.id, ownership_verified=True).all() alias_suffixes: [AliasSuffix] = [] # put custom domain first # for each user domain, generate both the domain and a random suffix version for custom_domain in user_custom_domains: if custom_domain.random_prefix_generation: suffix = "." + user.get_random_alias_suffix( ) + "@" + custom_domain.domain alias_suffix = AliasSuffix( is_custom=True, suffix=suffix, is_premium=False, domain=custom_domain.domain, mx_verified=custom_domain.verified, ) if user.default_alias_custom_domain_id == custom_domain.id: alias_suffixes.insert(0, alias_suffix) else: alias_suffixes.append(alias_suffix) suffix = "@" + custom_domain.domain alias_suffix = AliasSuffix( is_custom=True, suffix=suffix, is_premium=False, domain=custom_domain.domain, mx_verified=custom_domain.verified, ) # put the default domain to top # only if random_prefix_generation isn't enabled if (user.default_alias_custom_domain_id == custom_domain.id and not custom_domain.random_prefix_generation): alias_suffixes.insert(0, alias_suffix) else: alias_suffixes.append(alias_suffix) # then SimpleLogin domain for sl_domain in user.get_sl_domains(): suffix = (("" if DISABLE_ALIAS_SUFFIX else "." + user.get_random_alias_suffix()) + "@" + sl_domain.domain) alias_suffix = AliasSuffix( is_custom=False, suffix=suffix, is_premium=sl_domain.premium_only, domain=sl_domain.domain, mx_verified=True, ) # put the default domain to top if user.default_alias_public_domain_id == sl_domain.id: alias_suffixes.insert(0, alias_suffix) else: alias_suffixes.append(alias_suffix) return alias_suffixes
def subdomain_route(): if not current_user.subdomain_is_available(): flash("Unknown error, redirect to the home page", "error") return redirect(url_for("dashboard.index")) sl_domains = SLDomain.filter_by(can_use_subdomain=True).all() subdomains = CustomDomain.filter_by( user_id=current_user.id, is_sl_subdomain=True ).all() errors = {} if request.method == "POST": if request.form.get("form-name") == "create": if not current_user.is_premium(): flash("Only premium plan can add subdomain", "warning") return redirect(request.url) if current_user.subdomain_quota <= 0: flash( f"You can't create more than {MAX_NB_SUBDOMAIN} subdomains", "error" ) return redirect(request.url) subdomain = request.form.get("subdomain").lower().strip() domain = request.form.get("domain").lower().strip() if len(subdomain) < 3: flash("Subdomain must have at least 3 characters", "error") return redirect(request.url) if re.fullmatch(_SUBDOMAIN_PATTERN, subdomain) is None: flash( "Subdomain can only contain lowercase letters, numbers and dashes (-)", "error", ) return redirect(request.url) if subdomain.endswith("-"): flash("Subdomain can't end with dash (-)", "error") return redirect(request.url) if domain not in [sl_domain.domain for sl_domain in sl_domains]: LOG.e("Domain %s is tampered by %s", domain, current_user) flash("Unknown error, refresh the page", "error") return redirect(request.url) full_domain = f"{subdomain}.{domain}" if CustomDomain.get_by(domain=full_domain): flash(f"{full_domain} already used", "error") elif Mailbox.filter( Mailbox.verified.is_(True), Mailbox.email.endswith(f"@{full_domain}"), ).first(): flash(f"{full_domain} already used in a SimpleLogin mailbox", "error") else: try: new_custom_domain = CustomDomain.create( is_sl_subdomain=True, catch_all=True, # by default catch-all is enabled domain=full_domain, user_id=current_user.id, verified=True, dkim_verified=False, # wildcard DNS does not work for DKIM spf_verified=True, dmarc_verified=False, # wildcard DNS does not work for DMARC ownership_verified=True, commit=True, ) except SubdomainInTrashError: flash( f"{full_domain} has been used before and cannot be reused", "error", ) else: flash( f"New subdomain {new_custom_domain.domain} is created", "success", ) return redirect( url_for( "dashboard.domain_detail", custom_domain_id=new_custom_domain.id, ) ) return render_template( "dashboard/subdomain.html", sl_domains=sl_domains, errors=errors, subdomains=subdomains, )