def try_auto_create_directory(address: str) -> Optional[Alias]: """ Try to create an alias with directory """ # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format if email_belongs_to_alias_domains(address): # if there's no directory separator in the alias, no way to auto-create it if "/" not in address and "+" not in address and "#" not in address: return None # alias contains one of the 3 special directory separator: "/", "+" or "#" if "/" in address: sep = "/" elif "+" in address: sep = "+" else: sep = "#" directory_name = address[:address.find(sep)] LOG.d("directory_name %s", directory_name) directory = Directory.get_by(name=directory_name) if not directory: return None dir_user: User = directory.user if not dir_user.can_create_new_alias(): send_cannot_create_directory_alias(dir_user, address, directory_name) return None try: LOG.d("create alias %s for directory %s", address, directory) mailboxes = directory.mailboxes alias = Alias.create( email=address, user_id=directory.user_id, directory_id=directory.id, mailbox_id=mailboxes[0].id, ) db.session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() return alias except AliasInTrashError: LOG.warning( "Alias %s was deleted before, cannot auto-create using directory %s, user %s", address, directory_name, dir_user, ) return None
def test_get_mailbox_from_mail_from(flask_client): user = User.create( email="[email protected]", password="******", name="Test User", activated=True, commit=True, ) alias = Alias.create( user_id=user.id, email="*****@*****.**", mailbox_id=user.default_mailbox_id, commit=True, ) mb = get_mailbox_from_mail_from("[email protected]", alias) assert mb.email == "[email protected]" mb = get_mailbox_from_mail_from("*****@*****.**", alias) assert mb is None # authorized address AuthorizedAddress.create( user_id=user.id, mailbox_id=user.default_mailbox_id, email="*****@*****.**", commit=True, ) mb = get_mailbox_from_mail_from("*****@*****.**", alias) assert mb.email == "[email protected]"
def import_from_csv(batch_import: BatchImport, user: User, lines): reader = csv.DictReader(lines) for row in reader: try: full_alias = sanitize_email(row["alias"]) note = row["note"] except KeyError: LOG.warning("Cannot parse row %s", row) continue alias_domain = get_email_domain_part(full_alias) custom_domain = CustomDomain.get_by(domain=alias_domain) if (not custom_domain or not custom_domain.verified or custom_domain.user_id != user.id): LOG.debug("domain %s can't be used %s", alias_domain, user) continue if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.d("alias already used %s", full_alias) continue mailboxes = [] if "mailboxes" in row: for mailbox_email in row["mailboxes"].split(): mailbox_email = sanitize_email(mailbox_email) mailbox = Mailbox.get_by(email=mailbox_email) if not mailbox or not mailbox.verified or mailbox.user_id != user.id: LOG.d("mailbox %s can't be used %s", mailbox, user) continue mailboxes.append(mailbox.id) if len(mailboxes) == 0: mailboxes = [user.default_mailbox_id] alias = Alias.create( user_id=user.id, email=full_alias, note=note, mailbox_id=mailboxes[0], custom_domain_id=custom_domain.id, batch_import_id=batch_import.id, ) db.session.commit() db.session.flush() LOG.d("Create %s", alias) for i in range(1, len(mailboxes)): alias_mailbox = AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i], ) db.session.commit() LOG.d("Create %s", alias_mailbox)
def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]: """Try to create an alias with catch-all domain""" # 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 alias_domain = get_email_domain_part(address) custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain) if not custom_domain: return None # custom_domain exists if not custom_domain.catch_all: return None # custom_domain has catch-all enabled domain_user: User = custom_domain.user if not domain_user.can_create_new_alias(): send_cannot_create_domain_alias(domain_user, address, alias_domain) return None try: LOG.d("create alias %s for domain %s", address, custom_domain) mailboxes = custom_domain.mailboxes alias = Alias.create( email=address, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, mailbox_id=mailboxes[0].id, ) db.session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() return alias except AliasInTrashError: LOG.warning( "Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s", address, custom_domain, domain_user, ) return None except IntegrityError: LOG.warning("Alias %s already exists", address) db.session.rollback() alias = Alias.get_by(email=address) return alias except DataError: LOG.warning("Cannot create alias %s", address) db.session.rollback() return None
def test_add_already_existed_alias(flask_client): user = login(flask_client) Session.commit() another_user = User.create( email="[email protected]", password="******", name="Test User", activated=True, commit=True, ) word = random_word() suffix = f".{word}@{EMAIL_DOMAIN}" alias_suffix = AliasSuffix(is_custom=False, suffix=suffix, is_premium=False, domain=EMAIL_DOMAIN) signed_alias_suffix = signer.sign(alias_suffix.serialize()).decode() # alias already exist Alias.create( user_id=another_user.id, email=f"prefix{suffix}", mailbox_id=another_user.default_mailbox_id, commit=True, ) # create the same alias, should return error r = flask_client.post( url_for("dashboard.custom_alias"), data={ "prefix": "prefix", "signed-alias-suffix": signed_alias_suffix, "mailboxes": [user.default_mailbox_id], }, follow_redirects=True, ) assert r.status_code == 200 assert f"prefix{suffix} cannot be used" in r.get_data(True)
def handle_batch_import(batch_import: BatchImport): user = batch_import.user batch_import.processed = True db.session.commit() LOG.debug("Start batch import for %s %s", batch_import, user) file_url = s3.get_url(batch_import.file.path) LOG.d("Download file %s from %s", batch_import.file, file_url) r = requests.get(file_url) lines = [l.decode() for l in r.iter_lines()] reader = csv.DictReader(lines) for row in reader: try: full_alias = row["alias"].lower().strip().replace(" ", "") note = row["note"] except KeyError: LOG.warning("Cannot parse row %s", row) continue alias_domain = get_email_domain_part(full_alias) custom_domain = CustomDomain.get_by(domain=alias_domain) if ( not custom_domain or not custom_domain.verified or custom_domain.user_id != user.id ): LOG.debug("domain %s can't be used %s", alias_domain, user) continue if ( Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias) ): LOG.d("alias already used %s", full_alias) continue alias = Alias.create( user_id=user.id, email=full_alias, note=note, mailbox_id=user.default_mailbox_id, custom_domain_id=custom_domain.id, batch_import_id=batch_import.id, ) db.session.commit() LOG.d("Create %s", alias)
def test_delete_alias(flask_client): user = User.create(email="[email protected]", password="******", name="Test User", activated=True) db.session.commit() alias = Alias.create(user_id=user.id, email="*****@*****.**", mailbox_id=user.default_mailbox_id) db.session.commit() assert Alias.get_by(email="*****@*****.**") delete_alias(alias, user) assert Alias.get_by(email="*****@*****.**") is None assert DeletedAlias.get_by(email=alias.email)
def test_delete_alias_already_in_trash(flask_client): """delete an alias that's already in alias trash""" user = User.create(email="[email protected]", password="******", name="Test User", activated=True) db.session.commit() alias = Alias.create(user_id=user.id, email="*****@*****.**", mailbox_id=user.default_mailbox_id) db.session.commit() # add the alias to global trash db.session.add(DeletedAlias(email=alias.email)) db.session.commit() delete_alias(alias, user) assert Alias.get_by(email="*****@*****.**") is None
def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]: """Try to create an alias with catch-all domain""" # 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 alias_domain = get_email_domain_part(address) custom_domain = CustomDomain.get_by(domain=alias_domain) if not custom_domain: return None # custom_domain exists if not custom_domain.catch_all: return None # custom_domain has catch-all enabled domain_user: User = custom_domain.user if not domain_user.can_create_new_alias(): send_cannot_create_domain_alias(domain_user, address, alias_domain) return None # if alias has been deleted before, do not auto-create it if DeletedAlias.get_by(email=address): LOG.warning( "Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s", address, custom_domain, domain_user, ) return None LOG.d("create alias %s for domain %s", address, custom_domain) alias = Alias.create( email=address, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, mailbox_id=domain_user.default_mailbox_id, ) db.session.commit() return alias
def test_add_alias_in_custom_domain_trash(flask_client): user = login(flask_client) custom_domain = CustomDomain.create(user_id=user.id, domain="ab.cd", ownership_verified=True, commit=True) # delete a custom-domain alias: alias should go the DomainDeletedAlias alias = Alias.create( user_id=user.id, email="*****@*****.**", custom_domain_id=custom_domain.id, mailbox_id=user.default_mailbox_id, commit=True, ) assert DomainDeletedAlias.count() == 0 delete_alias(alias, user) assert DomainDeletedAlias.count() == 1 # create the same alias, should return error suffix = "@ab.cd" alias_suffix = AliasSuffix(is_custom=False, suffix=suffix, is_premium=False, domain=EMAIL_DOMAIN) signed_alias_suffix = signer.sign(alias_suffix.serialize()).decode() r = flask_client.post( url_for("dashboard.custom_alias"), data={ "prefix": "prefix", "signed-alias-suffix": signed_alias_suffix, "mailboxes": [user.default_mailbox_id], }, follow_redirects=True, ) assert r.status_code == 200 assert "You have deleted this alias before. You can restore it on" in r.get_data( True)
def test_add_alias_in_global_trash(flask_client): user = login(flask_client) db.session.commit() another_user = User.create( email="[email protected]", password="******", name="Test User", activated=True, commit=True, ) word = random_word() suffix = f".{word}@{EMAIL_DOMAIN}" signed_suffix = signer.sign(suffix).decode() # delete an alias: alias should go the DeletedAlias alias = Alias.create( user_id=another_user.id, email=f"prefix{suffix}", mailbox_id=another_user.default_mailbox_id, commit=True, ) assert DeletedAlias.query.count() == 0 delete_alias(alias, another_user) assert DeletedAlias.query.count() == 1 # create the same alias, should return error r = flask_client.post( url_for("dashboard.custom_alias"), data={ "prefix": "prefix", "suffix": signed_suffix, "mailboxes": [user.default_mailbox_id], }, follow_redirects=True, ) assert r.status_code == 200 assert f"prefix{suffix} cannot be used" in r.get_data(True)
def test_get_custom_domain_trash(flask_client): user = login(flask_client) cd = CustomDomain.create(user_id=user.id, domain="test1.org", verified=True, commit=True) alias = Alias.create( user_id=user.id, email="*****@*****.**", custom_domain_id=cd.id, mailbox_id=user.default_mailbox_id, commit=True, ) delete_alias(alias, user) r = flask_client.get(f"/api/custom_domains/{cd.id}/trash", ) for deleted_alias in r.json["aliases"]: assert deleted_alias["alias"] assert deleted_alias["creation_timestamp"] > 0
def test_add_alias_in_custom_domain_trash(flask_client): user = login(flask_client) db.session.commit() custom_domain = CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True, commit=True) # delete a custom-domain alias: alias should go the DomainDeletedAlias alias = Alias.create( user_id=user.id, email=f"*****@*****.**", custom_domain_id=custom_domain.id, mailbox_id=user.default_mailbox_id, commit=True, ) assert DomainDeletedAlias.query.count() == 0 delete_alias(alias, user) assert DomainDeletedAlias.query.count() == 1 # create the same alias, should return error suffix = f"@ab.cd" signed_suffix = signer.sign(suffix).decode() r = flask_client.post( url_for("dashboard.custom_alias"), data={ "prefix": "prefix", "suffix": signed_suffix, "mailboxes": [user.default_mailbox_id], }, follow_redirects=True, ) assert r.status_code == 200 assert "You have deleted this alias before. You can restore it on" in r.get_data( True)
def test_export(flask_client): # Create users user1 = login(flask_client) user2 = User.create( email="[email protected]", password="******", name="Wrong user", activated=True ) Session.commit() # Remove onboarding aliases for alias in Alias.filter_by(user_id=user1.id).all(): alias_utils.delete_alias(alias, user1) for alias in Alias.filter_by(user_id=user2.id).all(): alias_utils.delete_alias(alias, user2) Session.commit() # Create domains CustomDomain.create( user_id=user1.id, domain="my-destination-domain.com", verified=True ) CustomDomain.create( user_id=user2.id, domain="bad-destionation-domain.com", verified=True ) Session.commit() # Create mailboxes mailbox1 = Mailbox.create( user_id=user1.id, email="*****@*****.**", verified=True ) mailbox2 = Mailbox.create( user_id=user1.id, email="*****@*****.**", verified=True ) badmailbox1 = Mailbox.create( user_id=user2.id, email="*****@*****.**", verified=True, ) Session.commit() # Create aliases Alias.create( user_id=user1.id, email="*****@*****.**", note="Used on eBay", mailbox_id=mailbox1.id, ) alias2 = Alias.create( user_id=user1.id, email="*****@*****.**", note="Used on Facebook, Instagram.", mailbox_id=mailbox1.id, ) Alias.create( user_id=user2.id, email="*****@*****.**", note="Should not appear", mailbox_id=badmailbox1.id, ) Session.commit() # Add second mailbox to an alias AliasMailbox.create( alias_id=alias2.id, mailbox_id=mailbox2.id, ) Session.commit() # Export r = flask_client.get(url_for("api.export_aliases")) assert r.status_code == 200 assert r.mimetype == "text/csv" assert ( r.data == """alias,note,enabled,mailboxes [email protected],Used on eBay,True,[email protected] [email protected],"Used on Facebook, Instagram.",True,[email protected] [email protected] """.replace( "\n", "\r\n" ).encode() )
def try_auto_create_directory(address: str) -> Optional[Alias]: """ Try to create an alias with directory """ # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format if can_create_directory_for_address(address): # if there's no directory separator in the alias, no way to auto-create it if "/" not in address and "+" not in address and "#" not in address: return None # alias contains one of the 3 special directory separator: "/", "+" or "#" if "/" in address: sep = "/" elif "+" in address: sep = "+" else: sep = "#" directory_name = address[:address.find(sep)] LOG.d("directory_name %s", directory_name) directory = Directory.get_by(name=directory_name) if not directory: return None user: User = directory.user if not user.can_create_new_alias(): send_cannot_create_directory_alias(user, address, directory_name) return None if directory.disabled: send_cannot_create_directory_alias_disabled( user, address, directory_name) return None try: LOG.d("create alias %s for directory %s", address, directory) mailboxes = directory.mailboxes alias = Alias.create( email=address, user_id=directory.user_id, directory_id=directory.id, mailbox_id=mailboxes[0].id, ) if not user.disable_automatic_alias_note: alias.note = f"Created by directory {directory.name}" Session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) Session.commit() return alias except AliasInTrashError: LOG.w( "Alias %s was deleted before, cannot auto-create using directory %s, user %s", address, directory_name, user, ) return None except IntegrityError: LOG.w("Alias %s already exists", address) Session.rollback() alias = Alias.get_by(email=address) return alias
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): LOG.warning("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() ] suffixes = available_suffixes_more_info(current_user) at_least_a_premium_domain = False for suffix in suffixes: if not suffix.is_custom and suffix.is_premium: at_least_a_premium_domain = True break mailboxes = current_user.mailboxes() if request.method == "POST": alias_prefix = request.form.get("prefix").strip().lower().replace( " ", "") signed_suffix = request.form.get("suffix") mailbox_ids = request.form.getlist("mailboxes") alias_note = request.form.get("note") if not check_alias_prefix(alias_prefix): flash( "Only lowercase letters, numbers, dashes (-) and underscores (_) " "are currently supported for alias prefix. Cannot be more than 40 letters", "error", ) return redirect(url_for("dashboard.custom_alias")) # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if (not mailbox or mailbox.user_id != current_user.id or not mailbox.verified): flash("Something went wrong, please retry", "warning") return redirect(url_for("dashboard.custom_alias")) mailboxes.append(mailbox) if not mailboxes: flash("At least one mailbox must be selected", "error") return redirect(url_for("dashboard.custom_alias")) # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.warning("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(url_for("dashboard.custom_alias")) except Exception: LOG.warning("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(url_for("dashboard.custom_alias")) if verify_prefix_suffix(current_user, alias_prefix, alias_suffix): full_alias = alias_prefix + alias_suffix general_error_msg = f"{full_alias} cannot be used" if Alias.get_by(email=full_alias): alias = Alias.get_by(email=full_alias) if alias.user_id == current_user.id: flash(f"You already have this alias {full_alias}", "error") else: flash(general_error_msg, "error") elif DomainDeletedAlias.get_by(email=full_alias): domain_deleted_alias: DomainDeletedAlias = DomainDeletedAlias.get_by( email=full_alias) custom_domain = domain_deleted_alias.domain if domain_deleted_alias.user_id == current_user.id: flash( f"You have deleted this alias before. You can restore it on " f"{custom_domain.domain} 'Deleted Alias' page", "error", ) else: # should never happen as user can only choose their domains LOG.exception( "Deleted Alias %s does not belong to user %s", domain_deleted_alias, ) elif DeletedAlias.get_by(email=full_alias): flash(general_error_msg, "error") else: custom_domain_id = None # get the custom_domain_id if alias is created with a custom domain if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) # check if the alias is currently in the domain trash if domain and DomainDeletedAlias.get_by( domain_id=domain.id, email=full_alias): flash( f"Alias {full_alias} is currently in the {domain.domain} trash. " f"Please remove it from the trash in order to re-create it.", "warning", ) return redirect(url_for("dashboard.custom_alias")) if domain: custom_domain_id = domain.id try: alias = Alias.create( user_id=current_user.id, email=full_alias, note=alias_note, mailbox_id=mailboxes[0].id, custom_domain_id=custom_domain_id, ) db.session.flush() except IntegrityError: LOG.warning("Alias %s already exists", full_alias) db.session.rollback() flash("Unknown error, please retry", "error") return redirect(url_for("dashboard.custom_alias")) for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() flash(f"Alias {full_alias} has been created", "success") return redirect( url_for("dashboard.index", highlight_alias_id=alias.id)) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template( "dashboard/custom_alias.html", user_custom_domains=user_custom_domains, suffixes=suffixes, at_least_a_premium_domain=at_least_a_premium_domain, mailboxes=mailboxes, )
def fake_data(): LOG.d("create fake data") # Remove db if exist if os.path.exists("db.sqlite"): LOG.d("remove existing db file") os.remove("db.sqlite") # Create all tables db.create_all() # Create a user user = User.create( email="*****@*****.**", name="John Wick", password="******", activated=True, is_admin=True, # enable_otp=True, otp_secret="base32secret3232", intro_shown=True, fido_uuid=None, ) user.trial_end = None db.session.commit() # add a profile picture file_path = "profile_pic.svg" s3.upload_from_bytesio( file_path, open(os.path.join(ROOT_DIR, "static", "default-icon.svg"), "rb"), content_type="image/svg", ) file = File.create(user_id=user.id, path=file_path, commit=True) user.profile_picture_id = file.id db.session.commit() # create a bounced email alias = Alias.create_new_random(user) db.session.commit() bounce_email_file_path = "bounce.eml" s3.upload_email_from_bytesio( bounce_email_file_path, open(os.path.join(ROOT_DIR, "local_data", "email_tests", "2.eml"), "rb"), "download.eml", ) refused_email = RefusedEmail.create( path=bounce_email_file_path, full_report_path=bounce_email_file_path, user_id=user.id, commit=True, ) contact = Contact.create( user_id=user.id, alias_id=alias.id, website_email="*****@*****.**", reply_email="*****@*****.**", commit=True, ) EmailLog.create( user_id=user.id, contact_id=contact.id, refused_email_id=refused_email.id, bounced=True, commit=True, ) LifetimeCoupon.create(code="coupon", nb_used=10, commit=True) # Create a subscription for user Subscription.create( user_id=user.id, cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234", update_url="https://checkout.paddle.com/subscription/update?user=1234", subscription_id="123", event_time=arrow.now(), next_bill_date=arrow.now().shift(days=10).date(), plan=PlanEnum.monthly, commit=True, ) CoinbaseSubscription.create( user_id=user.id, end_at=arrow.now().shift(days=10), commit=True ) api_key = ApiKey.create(user_id=user.id, name="Chrome") api_key.code = "code" api_key = ApiKey.create(user_id=user.id, name="Firefox") api_key.code = "codeFF" pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read() m1 = Mailbox.create( user_id=user.id, email="*****@*****.**", verified=True, pgp_public_key=pgp_public_key, ) m1.pgp_finger_print = load_public_key(pgp_public_key) db.session.commit() for i in range(3): if i % 2 == 0: a = Alias.create( email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=m1.id ) else: a = Alias.create( email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=user.default_mailbox_id, ) db.session.commit() if i % 5 == 0: if i % 2 == 0: AliasMailbox.create(alias_id=a.id, mailbox_id=user.default_mailbox_id) else: AliasMailbox.create(alias_id=a.id, mailbox_id=m1.id) db.session.commit() # some aliases don't have any activity # if i % 3 != 0: # contact = Contact.create( # user_id=user.id, # alias_id=a.id, # website_email=f"contact{i}@example.com", # reply_email=f"rep{i}@sl.local", # ) # db.session.commit() # for _ in range(3): # EmailLog.create(user_id=user.id, contact_id=contact.id) # db.session.commit() # have some disabled alias if i % 5 == 0: a.enabled = False db.session.commit() CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) CustomDomain.create( user_id=user.id, domain="very-long-domain.com.net.org", verified=True ) db.session.commit() Directory.create(user_id=user.id, name="abcd") Directory.create(user_id=user.id, name="xyzt") db.session.commit() # Create a client client1 = Client.create_new(name="Demo", user_id=user.id) client1.oauth_client_id = "client-id" client1.oauth_client_secret = "client-secret" client1.published = True db.session.commit() RedirectUri.create(client_id=client1.id, uri="https://ab.com") client2 = Client.create_new(name="Demo 2", user_id=user.id) client2.oauth_client_id = "client-id2" client2.oauth_client_secret = "client-secret2" client2.published = True db.session.commit() ClientUser.create(user_id=user.id, client_id=client1.id, name="Fake Name") referral = Referral.create(user_id=user.id, code="REFCODE", name="First referral") db.session.commit() for i in range(6): Notification.create(user_id=user.id, message=f"""Hey hey <b>{i}</b> """ * 10) db.session.commit() user2 = User.create( email="*****@*****.**", password="******", activated=True, referral_id=referral.id, ) Mailbox.create(user_id=user2.id, email="*****@*****.**", verified=True) db.session.commit() ManualSubscription.create( user_id=user2.id, end_at=arrow.now().shift(years=1, days=1), commit=True )
def new_custom_alias(): """ Currently used by Safari extension. Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ LOG.warning("/alias/custom/new is obsolete") user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") alias_suffix = data.get("alias_suffix", "").strip().lower().replace(" ", "") note = data.get("note") alias_prefix = convert_to_id(alias_prefix) if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 alias = Alias.create(user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note) if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) if domain: LOG.d("set alias %s to domain %s", full_alias, domain) alias.custom_domain_id = domain.id db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return jsonify(alias=full_alias, **serialize_alias_info(get_alias_info(alias))), 201
def new_custom_alias_v3(): """ Create a new custom alias Same as v2 but accept a list of mailboxes as input Input: alias_prefix, for ex "www_groupon_com" signed_suffix, either [email protected] or @my-domain.com mailbox_ids: list of int optional "hostname" in args optional "note" optional "name" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") signed_suffix = data.get("signed_suffix", "").strip() mailbox_ids = data.get("mailbox_ids") note = data.get("note") name = data.get("name") if name: name = name.replace("\n", "") alias_prefix = convert_to_id(alias_prefix) if not check_alias_prefix(alias_prefix): return jsonify(error="alias prefix invalid format or too long"), 400 # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != user.id or not mailbox.verified: return jsonify(error="Errors with Mailbox"), 400 mailboxes.append(mailbox) if not mailboxes: return jsonify(error="At least one mailbox must be selected"), 400 # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.warning("Alias creation time expired for %s", user) return jsonify( error="Alias creation time is expired, please retry"), 412 except Exception: LOG.warning("Alias suffix is tampered, user %s", user) return jsonify(error="Tampered suffix"), 400 if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 custom_domain_id = None if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) if domain: custom_domain_id = domain.id alias = Alias.create( user_id=user.id, email=full_alias, note=note, name=name or None, mailbox_id=mailboxes[0].id, custom_domain_id=custom_domain_id, ) db.session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return ( jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))), 201, )
def new_custom_alias_v2(): """ Create a new custom alias Same as v1 but signed_suffix is actually the suffix with signature, e.g. [email protected] Input: alias_prefix, for ex "www_groupon_com" signed_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error= "You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") signed_suffix = data.get("signed_suffix", "").strip() note = data.get("note") alias_prefix = convert_to_id(alias_prefix) # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.warning("Alias creation time expired for %s", user) return jsonify( error="Alias creation time is expired, please retry"), 412 except Exception: LOG.warning("Alias suffix is tampered, user %s", user) return jsonify(error="Tampered suffix"), 400 if not verify_prefix_suffix(user, alias_prefix, alias_suffix): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 custom_domain_id = None if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) # check if the alias is currently in the domain trash if domain and DomainDeletedAlias.get_by(domain_id=domain.id, email=full_alias): LOG.d( f"Alias {full_alias} is currently in the {domain.domain} trash. " ) return jsonify(error=f"alias {full_alias} in domain trash"), 409 if domain: custom_domain_id = domain.id alias = Alias.create( user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note, custom_domain_id=custom_domain_id, ) db.session.commit() if hostname: AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) db.session.commit() return ( jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))), 201, )
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, time-signed alias-suffix) suffixes = available_suffixes(current_user) 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") signed_suffix = request.form.get("suffix") alias = 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") # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.warning("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(request.url) except Exception: LOG.exception("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(request.url) 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): full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.exception("alias %s already used, very rare!", full_alias) flash(f"Alias {full_alias} already used", "error") return redirect(request.url) else: alias = Alias.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 if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) if domain: alias.custom_domain_id = 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: alias = Alias.get_by(email=chosen_email) if not alias: alias = Alias.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 alias: client_user.alias_id = alias.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 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 # allow any redirect_uri if the app isn't approved hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1" and client.approved: # 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.d("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() # redirect user to the client page redirect_args = construct_redirect_args( client, client_user, nonce, redirect_uri, response_types, scope, state, ) fragment = get_fragment(response_mode, response_types) # construct redirect_uri with redirect_args return redirect( construct_url(redirect_uri, redirect_args, fragment)) 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() ] suffixes = get_available_suffixes(current_user) 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 not current_user.is_authenticated or not current_user.is_active: LOG.i( "Attempt to validate a OAUth allow request by an unauthenticated user" ) return redirect(url_for("auth.login", next=request.url)) if request.form.get("button") == "deny": LOG.d("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.d("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") signed_suffix = request.form.get("suffix") alias = 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") alias_prefix = alias_prefix.strip().lower().replace(" ", "") if not check_alias_prefix(alias_prefix): flash( "Only lowercase letters, numbers, dashes (-), dots (.) and underscores (_) " "are currently supported for alias prefix. Cannot be more than 40 letters", "error", ) return redirect(request.url) # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.w("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(request.url) except Exception: LOG.w("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(request.url) 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): full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.e("alias %s already used, very rare!", full_alias) flash(f"Alias {full_alias} already used", "error") return redirect(request.url) else: alias = Alias.create( user_id=current_user.id, email=full_alias, mailbox_id=current_user.default_mailbox_id, ) 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: alias = Alias.get_by(email=chosen_email) if not alias: alias = Alias.create( email=chosen_email, user_id=current_user.id, mailbox_id=current_user.default_mailbox_id, ) 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 alias: client_user.alias_id = alias.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 Session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = construct_redirect_args(client, client_user, nonce, redirect_uri, response_types, scope, state) fragment = get_fragment(response_mode, response_types) # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): LOG.d("%s can't create new alias", current_user) flash( "You have reached free plan limit, please upgrade to create new aliases", "warning", ) return redirect(url_for("dashboard.index")) user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] alias_suffixes = get_alias_suffixes(current_user) at_least_a_premium_domain = False for alias_suffix in alias_suffixes: if not alias_suffix.is_custom and alias_suffix.is_premium: at_least_a_premium_domain = True break alias_suffixes_with_signature = [ (alias_suffix, signer.sign(alias_suffix.serialize()).decode()) for alias_suffix in alias_suffixes ] mailboxes = current_user.mailboxes() if request.method == "POST": alias_prefix = request.form.get("prefix").strip().lower().replace( " ", "") signed_alias_suffix = request.form.get("signed-alias-suffix") mailbox_ids = request.form.getlist("mailboxes") alias_note = request.form.get("note") if not check_alias_prefix(alias_prefix): flash( "Only lowercase letters, numbers, dashes (-), dots (.) and underscores (_) " "are currently supported for alias prefix. Cannot be more than 40 letters", "error", ) return redirect(request.url) # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if (not mailbox or mailbox.user_id != current_user.id or not mailbox.verified): flash("Something went wrong, please retry", "warning") return redirect(request.url) mailboxes.append(mailbox) if not mailboxes: flash("At least one mailbox must be selected", "error") return redirect(request.url) # hypothesis: user will click on the button in the 600 secs try: signed_alias_suffix_decoded = signer.unsign(signed_alias_suffix, max_age=600).decode() alias_suffix: AliasSuffix = AliasSuffix.deserialize( signed_alias_suffix_decoded) except SignatureExpired: LOG.w("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(request.url) except Exception: LOG.w("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(request.url) if verify_prefix_suffix(current_user, alias_prefix, alias_suffix.suffix): full_alias = alias_prefix + alias_suffix.suffix if ".." in full_alias: flash("Your alias can't contain 2 consecutive dots (..)", "error") return redirect(request.url) try: validate_email(full_alias, check_deliverability=False, allow_smtputf8=False) except EmailNotValidError as e: flash(str(e), "error") return redirect(request.url) general_error_msg = f"{full_alias} cannot be used" if Alias.get_by(email=full_alias): alias = Alias.get_by(email=full_alias) if alias.user_id == current_user.id: flash(f"You already have this alias {full_alias}", "error") else: flash(general_error_msg, "error") elif DomainDeletedAlias.get_by(email=full_alias): domain_deleted_alias: DomainDeletedAlias = DomainDeletedAlias.get_by( email=full_alias) custom_domain = domain_deleted_alias.domain if domain_deleted_alias.user_id == current_user.id: flash( f"You have deleted this alias before. You can restore it on " f"{custom_domain.domain} 'Deleted Alias' page", "error", ) else: # should never happen as user can only choose their domains LOG.e( "Deleted Alias %s does not belong to user %s", domain_deleted_alias, ) elif DeletedAlias.get_by(email=full_alias): flash(general_error_msg, "error") else: try: alias = Alias.create( user_id=current_user.id, email=full_alias, note=alias_note, mailbox_id=mailboxes[0].id, ) Session.flush() except IntegrityError: LOG.w("Alias %s already exists", full_alias) Session.rollback() flash("Unknown error, please retry", "error") return redirect(url_for("dashboard.custom_alias")) for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) Session.commit() flash(f"Alias {full_alias} has been created", "success") return redirect( url_for("dashboard.index", highlight_alias_id=alias.id)) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template( "dashboard/custom_alias.html", user_custom_domains=user_custom_domains, alias_suffixes_with_signature=alias_suffixes_with_signature, at_least_a_premium_domain=at_least_a_premium_domain, mailboxes=mailboxes, )
def try_auto_create_via_domain(address: str) -> Optional[Alias]: """Try to create an alias with catch-all or auto-create rules on custom domain""" # 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 alias_domain = get_email_domain_part(address) custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain) if not custom_domain: return None if not custom_domain.catch_all and len( custom_domain.auto_create_rules) == 0: return None elif not custom_domain.catch_all and len( custom_domain.auto_create_rules) > 0: local = get_email_local_part(address) for rule in custom_domain.auto_create_rules: if regex_match(rule.regex, local): LOG.d( "%s passes %s on %s", address, rule.regex, custom_domain, ) alias_note = f"Created by rule {rule.order} with regex {rule.regex}" mailboxes = rule.mailboxes break else: # no rule passes LOG.d("no rule passed to create %s", local) return else: # catch-all is enabled mailboxes = custom_domain.mailboxes alias_note = "Created by catch-all option" domain_user: User = custom_domain.user if not domain_user.can_create_new_alias(): send_cannot_create_domain_alias(domain_user, address, alias_domain) return None # a rule can have 0 mailboxes. Happened when a mailbox is deleted if not mailboxes: LOG.d("use %s default mailbox for %s %s", domain_user, address, custom_domain) mailboxes = [domain_user.default_mailbox] try: LOG.d("create alias %s for domain %s", address, custom_domain) alias = Alias.create( email=address, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, mailbox_id=mailboxes[0].id, ) if not custom_domain.user.disable_automatic_alias_note: alias.note = alias_note Session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) Session.commit() return alias except AliasInTrashError: LOG.w( "Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s", address, custom_domain, domain_user, ) return None except IntegrityError: LOG.w("Alias %s already exists", address) Session.rollback() alias = Alias.get_by(email=address) return alias except DataError: LOG.w("Cannot create alias %s", address) Session.rollback() return None
def new_random_alias(): """ Create a new random alias Input: (Optional) note Output: 201 if success """ user = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create new random alias", user) return ( jsonify( error= f"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, ) note = None data = request.get_json(silent=True) if data: note = data.get("note") alias = None # custom alias suggestion and suffix hostname = request.args.get("hostname") if hostname and user.include_website_in_one_click_alias: LOG.d("Use %s to create new alias", hostname) # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon ext = tldextract.extract(hostname) prefix_suggestion = ext.domain prefix_suggestion = convert_to_id(prefix_suggestion) suffixes = get_available_suffixes(user) # use the first suffix suggested_alias = prefix_suggestion + suffixes[0].suffix alias = Alias.get_by(email=suggested_alias) # cannot use this alias as it belongs to another user if alias and not alias.user_id == user.id: LOG.d("%s belongs to another user", alias) alias = None elif alias and alias.user_id == user.id: # make sure alias was created for this website if AliasUsedOn.get_by(alias_id=alias.id, hostname=hostname, user_id=alias.user_id): LOG.d("Use existing alias %s", alias) else: LOG.d("%s wasn't created for this website %s", alias, hostname) alias = None elif not alias: LOG.d("create new alias %s", suggested_alias) try: alias = Alias.create( user_id=user.id, email=suggested_alias, note=note, mailbox_id=user.default_mailbox_id, commit=True, ) except AliasInTrashError: LOG.i("Alias %s is in trash", suggested_alias) alias = None if not alias: scheme = user.alias_generator mode = request.args.get("mode") if mode: if mode == "word": scheme = AliasGeneratorEnum.word.value elif mode == "uuid": scheme = AliasGeneratorEnum.uuid.value else: return jsonify( error=f"{mode} must be either word or uuid"), 400 alias = Alias.create_new_random(user=user, scheme=scheme, note=note) Session.commit() if hostname and not AliasUsedOn.get_by(alias_id=alias.id, hostname=hostname): AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) Session.commit() return ( jsonify(alias=alias.email, **serialize_alias_info_v2(get_alias_info_v2(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( "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, time-signed alias-suffix) suffixes = available_suffixes(current_user) mailboxes = current_user.mailboxes() if request.method == "POST": alias_prefix = request.form.get("prefix").strip().lower() signed_suffix = request.form.get("suffix") mailbox_ids = request.form.getlist("mailboxes") alias_note = request.form.get("note") # check if mailbox is not tempered with mailboxes = [] for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) if (not mailbox or mailbox.user_id != current_user.id or not mailbox.verified): flash("Something went wrong, please retry", "warning") return redirect(url_for("dashboard.custom_alias")) mailboxes.append(mailbox) if not mailboxes: flash("At least one mailbox must be selected", "error") return redirect(url_for("dashboard.custom_alias")) # hypothesis: user will click on the button in the 600 secs try: alias_suffix = signer.unsign(signed_suffix, max_age=600).decode() except SignatureExpired: LOG.warning("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(url_for("dashboard.custom_alias")) except Exception: LOG.error("Alias suffix is tampered, user %s", current_user) flash("Unknown error, refresh the page", "error") return redirect(url_for("dashboard.custom_alias")) if verify_prefix_suffix(current_user, alias_prefix, alias_suffix): full_alias = alias_prefix + alias_suffix if (Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias) or DomainDeletedAlias.get_by(email=full_alias)): LOG.d("full alias already used %s", full_alias) flash( f"Alias {full_alias} already exists, please choose another one", "warning", ) else: custom_domain_id = None # get the custom_domain_id if alias is created with a custom domain if alias_suffix.startswith("@"): alias_domain = alias_suffix[1:] domain = CustomDomain.get_by(domain=alias_domain) # check if the alias is currently in the domain trash if domain and DomainDeletedAlias.get_by( domain_id=domain.id, email=full_alias): flash( f"Alias {full_alias} is currently in the {domain.domain} trash. " f"Please remove it from the trash in order to re-create it.", "warning", ) return redirect(url_for("dashboard.custom_alias")) if domain: custom_domain_id = domain.id alias = Alias.create( user_id=current_user.id, email=full_alias, note=alias_note, mailbox_id=mailboxes[0].id, custom_domain_id=custom_domain_id, ) db.session.flush() for i in range(1, len(mailboxes)): AliasMailbox.create( alias_id=alias.id, mailbox_id=mailboxes[i].id, ) db.session.commit() flash(f"Alias {full_alias} has been created", "success") return redirect( url_for("dashboard.index", highlight_alias_id=alias.id)) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template( "dashboard/custom_alias.html", user_custom_domains=user_custom_domains, suffixes=suffixes, mailboxes=mailboxes, )
def fake_data(): LOG.d("create fake data") # Remove db if exist if os.path.exists("db.sqlite"): LOG.d("remove existing db file") os.remove("db.sqlite") # Create all tables db.create_all() # Create a user user = User.create( email="*****@*****.**", name="John Wick", password="******", activated=True, is_admin=True, enable_otp=False, otp_secret="base32secret3232", intro_shown=True, fido_uuid=None, ) user.include_sender_in_reverse_alias = None db.session.commit() user.trial_end = None LifetimeCoupon.create(code="coupon", nb_used=10, commit=True) # Create a subscription for user # Subscription.create( # user_id=user.id, # cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234", # update_url="https://checkout.paddle.com/subscription/update?user=1234", # subscription_id="123", # event_time=arrow.now(), # next_bill_date=arrow.now().shift(days=10).date(), # plan=PlanEnum.monthly, # ) # db.session.commit() CoinbaseSubscription.create(user_id=user.id, end_at=arrow.now().shift(days=10), commit=True) api_key = ApiKey.create(user_id=user.id, name="Chrome") api_key.code = "code" api_key = ApiKey.create(user_id=user.id, name="Firefox") api_key.code = "codeFF" pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read() m1 = Mailbox.create( user_id=user.id, email="*****@*****.**", verified=True, pgp_public_key=pgp_public_key, ) m1.pgp_finger_print = load_public_key(pgp_public_key) db.session.commit() for i in range(3): if i % 2 == 0: a = Alias.create(email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=m1.id) else: a = Alias.create( email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=user.default_mailbox_id, ) db.session.commit() if i % 5 == 0: if i % 2 == 0: AliasMailbox.create(alias_id=a.id, mailbox_id=user.default_mailbox_id) else: AliasMailbox.create(alias_id=a.id, mailbox_id=m1.id) db.session.commit() # some aliases don't have any activity # if i % 3 != 0: # contact = Contact.create( # user_id=user.id, # alias_id=a.id, # website_email=f"contact{i}@example.com", # reply_email=f"rep{i}@sl.local", # ) # db.session.commit() # for _ in range(3): # EmailLog.create(user_id=user.id, contact_id=contact.id) # db.session.commit() # have some disabled alias if i % 5 == 0: a.enabled = False db.session.commit() CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) CustomDomain.create(user_id=user.id, domain="very-long-domain.com.net.org", verified=True) db.session.commit() Directory.create(user_id=user.id, name="abcd") Directory.create(user_id=user.id, name="xyzt") db.session.commit() # Create a client client1 = Client.create_new(name="Demo", user_id=user.id) client1.oauth_client_id = "client-id" client1.oauth_client_secret = "client-secret" client1.published = True db.session.commit() RedirectUri.create(client_id=client1.id, uri="https://ab.com") client2 = Client.create_new(name="Demo 2", user_id=user.id) client2.oauth_client_id = "client-id2" client2.oauth_client_secret = "client-secret2" client2.published = True db.session.commit() ClientUser.create(user_id=user.id, client_id=client1.id, name="Fake Name") referral = Referral.create(user_id=user.id, code="REFCODE", name="First referral") db.session.commit() for i in range(6): Notification.create(user_id=user.id, message=f"""Hey hey <b>{i}</b> """ * 10) db.session.commit() User.create( email="*****@*****.**", password="******", activated=True, referral_id=referral.id, ) db.session.commit()
def fake_data(): LOG.d("create fake data") # Create a user user = User.create( email="*****@*****.**", name="John Wick", password="******", activated=True, is_admin=True, # enable_otp=True, otp_secret="base32secret3232", intro_shown=True, fido_uuid=None, ) user.trial_end = None Session.commit() # add a profile picture file_path = "profile_pic.svg" s3.upload_from_bytesio( file_path, open(os.path.join(ROOT_DIR, "static", "default-icon.svg"), "rb"), content_type="image/svg", ) file = File.create(user_id=user.id, path=file_path, commit=True) user.profile_picture_id = file.id Session.commit() # create a bounced email alias = Alias.create_new_random(user) Session.commit() bounce_email_file_path = "bounce.eml" s3.upload_email_from_bytesio( bounce_email_file_path, open(os.path.join(ROOT_DIR, "local_data", "email_tests", "2.eml"), "rb"), "download.eml", ) refused_email = RefusedEmail.create( path=bounce_email_file_path, full_report_path=bounce_email_file_path, user_id=user.id, commit=True, ) contact = Contact.create( user_id=user.id, alias_id=alias.id, website_email="*****@*****.**", reply_email="*****@*****.**", commit=True, ) EmailLog.create( user_id=user.id, contact_id=contact.id, alias_id=contact.alias_id, refused_email_id=refused_email.id, bounced=True, commit=True, ) LifetimeCoupon.create(code="lifetime-coupon", nb_used=10, commit=True) Coupon.create(code="coupon", commit=True) # Create a subscription for user Subscription.create( user_id=user.id, cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234", update_url="https://checkout.paddle.com/subscription/update?user=1234", subscription_id="123", event_time=arrow.now(), next_bill_date=arrow.now().shift(days=10).date(), plan=PlanEnum.monthly, commit=True, ) CoinbaseSubscription.create(user_id=user.id, end_at=arrow.now().shift(days=10), commit=True) api_key = ApiKey.create(user_id=user.id, name="Chrome") api_key.code = "code" api_key = ApiKey.create(user_id=user.id, name="Firefox") api_key.code = "codeFF" pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read() m1 = Mailbox.create( user_id=user.id, email="*****@*****.**", verified=True, pgp_public_key=pgp_public_key, ) m1.pgp_finger_print = load_public_key(pgp_public_key) Session.commit() # [email protected] is in a LOT of data breaches Alias.create(email="*****@*****.**", user_id=user.id, mailbox_id=m1.id) for i in range(3): if i % 2 == 0: a = Alias.create(email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=m1.id) else: a = Alias.create( email=f"e{i}@{FIRST_ALIAS_DOMAIN}", user_id=user.id, mailbox_id=user.default_mailbox_id, ) Session.commit() if i % 5 == 0: if i % 2 == 0: AliasMailbox.create(alias_id=a.id, mailbox_id=user.default_mailbox_id) else: AliasMailbox.create(alias_id=a.id, mailbox_id=m1.id) Session.commit() # some aliases don't have any activity # if i % 3 != 0: # contact = Contact.create( # user_id=user.id, # alias_id=a.id, # website_email=f"contact{i}@example.com", # reply_email=f"rep{i}@sl.local", # ) # Session.commit() # for _ in range(3): # EmailLog.create(user_id=user.id, contact_id=contact.id, alias_id=contact.alias_id) # Session.commit() # have some disabled alias if i % 5 == 0: a.enabled = False Session.commit() custom_domain1 = CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) Session.commit() Alias.create( user_id=user.id, email="*****@*****.**", mailbox_id=user.default_mailbox_id, custom_domain_id=custom_domain1.id, commit=True, ) Alias.create( user_id=user.id, email="*****@*****.**", mailbox_id=user.default_mailbox_id, custom_domain_id=custom_domain1.id, commit=True, ) Directory.create(user_id=user.id, name="abcd") Directory.create(user_id=user.id, name="xyzt") Session.commit() # Create a client client1 = Client.create_new(name="Demo", user_id=user.id) client1.oauth_client_id = "client-id" client1.oauth_client_secret = "client-secret" Session.commit() RedirectUri.create(client_id=client1.id, uri="https://your-website.com/oauth-callback") client2 = Client.create_new(name="Demo 2", user_id=user.id) client2.oauth_client_id = "client-id2" client2.oauth_client_secret = "client-secret2" Session.commit() ClientUser.create(user_id=user.id, client_id=client1.id, name="Fake Name") referral = Referral.create(user_id=user.id, code="Website", name="First referral") Referral.create(user_id=user.id, code="Podcast", name="First referral") Payout.create(user_id=user.id, amount=1000, number_upgraded_account=100, payment_method="BTC") Payout.create( user_id=user.id, amount=5000, number_upgraded_account=200, payment_method="PayPal", ) Session.commit() for i in range(6): Notification.create(user_id=user.id, message=f"""Hey hey <b>{i}</b> """ * 10) Session.commit() user2 = User.create( email="*****@*****.**", password="******", activated=True, referral_id=referral.id, ) Mailbox.create(user_id=user2.id, email="*****@*****.**", verified=True) Session.commit() ManualSubscription.create( user_id=user2.id, end_at=arrow.now().shift(years=1, days=1), comment="Local manual", commit=True, ) SLDomain.create(domain="premium.com", premium_only=True, commit=True) hibp1 = Hibp.create(name="first breach", description="breach description", commit=True) hibp2 = Hibp.create(name="second breach", description="breach description", commit=True) breached_alias1 = Alias.create(email="*****@*****.**", user_id=user.id, mailbox_id=m1.id, commit=True) breached_alias2 = Alias.create(email="*****@*****.**", user_id=user.id, mailbox_id=m1.id, commit=True) AliasHibp.create(hibp_id=hibp1.id, alias_id=breached_alias1.id) AliasHibp.create(hibp_id=hibp2.id, alias_id=breached_alias2.id) # old domain will have ownership_verified=True CustomDomain.create(user_id=user.id, domain="old.com", verified=True, ownership_verified=True)