def alias_contact_manager(alias_id): highlight_contact_id = None if request.args.get("highlight_contact_id"): highlight_contact_id = int(request.args.get("highlight_contact_id")) alias = Alias.get(alias_id) page = 0 if request.args.get("page"): page = int(request.args.get("page")) # sanity check if not alias: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if alias.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_addr = new_contact_form.email.data.strip() try: contact_name, contact_email = parseaddr_unicode( contact_addr) except Exception: flash(f"{contact_addr} is invalid", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, )) if not is_valid_email(contact_email): flash(f"{contact_email} is invalid", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, )) contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) # already been added if contact: flash(f"{contact_email} is already added", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=generate_reply_email(contact_email), ) LOG.d("create reverse-alias for %s", contact_addr) db.session.commit() flash(f"Reverse alias for {contact_addr} is created", "success") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) elif request.form.get("form-name") == "delete": contact_id = request.form.get("contact-id") contact = Contact.get(contact_id) if not contact: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) elif contact.alias_id != alias.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) delete_contact_email = contact.website_email Contact.delete(contact_id) db.session.commit() flash(f"Reverse-alias for {delete_contact_email} has been deleted", "success") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) contact_infos = get_contact_infos(alias, page) last_page = len(contact_infos) < PAGE_LIMIT # if highlighted contact isn't included, fetch it # make sure highlighted contact is at array start contact_ids = [contact_info.contact.id for contact_info in contact_infos] if highlight_contact_id and highlight_contact_id not in contact_ids: contact_infos = ( get_contact_infos(alias, contact_id=highlight_contact_id) + contact_infos) return render_template( "dashboard/alias_contact_manager.html", contact_infos=contact_infos, alias=alias, new_contact_form=new_contact_form, highlight_contact_id=highlight_contact_id, page=page, last_page=last_page, )
def paddle(): LOG.debug( f"paddle callback {request.form.get('alert_name')} {request.form}") # make sure the request comes from Paddle if not paddle_utils.verify_incoming_request(dict(request.form)): LOG.exception("request not coming from paddle. Request data:%s", dict(request.form)) return "KO", 400 if (request.form.get("alert_name") == "subscription_created" ): # new user subscribes # the passthrough is json encoded, e.g. # request.form.get("passthrough") = '{"user_id": 88 }' passthrough = json.loads(request.form.get("passthrough")) user_id = passthrough.get("user_id") user = User.get(user_id) subscription_plan_id = int( request.form.get("subscription_plan_id")) if subscription_plan_id in PADDLE_MONTHLY_PRODUCT_IDS: plan = PlanEnum.monthly elif subscription_plan_id in PADDLE_YEARLY_PRODUCT_IDS: plan = PlanEnum.yearly else: LOG.exception( "Unknown subscription_plan_id %s %s", subscription_plan_id, request.form, ) return "No such subscription", 400 sub = Subscription.get_by(user_id=user.id) if not sub: LOG.d(f"create a new Subscription for user {user}") Subscription.create( user_id=user.id, cancel_url=request.form.get("cancel_url"), update_url=request.form.get("update_url"), subscription_id=request.form.get("subscription_id"), event_time=arrow.now(), next_bill_date=arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD").date(), plan=plan, ) else: LOG.d(f"Update an existing Subscription for user {user}") sub.cancel_url = request.form.get("cancel_url") sub.update_url = request.form.get("update_url") sub.subscription_id = request.form.get("subscription_id") sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD").date() sub.plan = plan # make sure to set the new plan as not-cancelled # in case user cancels a plan and subscribes a new plan sub.cancelled = False LOG.debug("User %s upgrades!", user) db.session.commit() elif request.form.get( "alert_name") == "subscription_payment_succeeded": subscription_id = request.form.get("subscription_id") LOG.debug("Update subscription %s", subscription_id) sub: Subscription = Subscription.get_by( subscription_id=subscription_id) # when user subscribes, the "subscription_payment_succeeded" can arrive BEFORE "subscription_created" # at that time, subscription object does not exist yet if sub: sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD").date() db.session.commit() elif request.form.get("alert_name") == "subscription_cancelled": subscription_id = request.form.get("subscription_id") sub: Subscription = Subscription.get_by( subscription_id=subscription_id) if sub: # cancellation_effective_date should be the same as next_bill_date LOG.warning( "Cancel subscription %s %s on %s, next bill date %s", subscription_id, sub.user, request.form.get("cancellation_effective_date"), sub.next_bill_date, ) sub.event_time = arrow.now() sub.cancelled = True db.session.commit() user = sub.user send_email( user.email, "SimpleLogin - what can we do to improve the product?", render( "transactional/subscription-cancel.txt", end_date=request.form.get( "cancellation_effective_date"), ), ) else: return "No such subscription", 400 elif request.form.get("alert_name") == "subscription_updated": subscription_id = request.form.get("subscription_id") sub: Subscription = Subscription.get_by( subscription_id=subscription_id) if sub: LOG.debug( "Update subscription %s %s on %s, next bill date %s", subscription_id, sub.user, request.form.get("cancellation_effective_date"), sub.next_bill_date, ) if (int(request.form.get("subscription_plan_id")) == PADDLE_MONTHLY_PRODUCT_ID): plan = PlanEnum.monthly else: plan = PlanEnum.yearly sub.cancel_url = request.form.get("cancel_url") sub.update_url = request.form.get("update_url") sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD").date() sub.plan = plan # make sure to set the new plan as not-cancelled sub.cancelled = False db.session.commit() else: return "No such subscription", 400 return "OK"
def create_app() -> Flask: app = Flask(__name__) # SimpleLogin is deployed behind NGINX app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1) limiter.init_app(app) app.url_map.strict_slashes = False app.config["SQLALCHEMY_DATABASE_URI"] = DB_URI app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # enable to print all queries generated by sqlalchemy # app.config["SQLALCHEMY_ECHO"] = True app.secret_key = FLASK_SECRET app.config["TEMPLATES_AUTO_RELOAD"] = True # to avoid conflict with other cookie app.config["SESSION_COOKIE_NAME"] = SESSION_COOKIE_NAME if URL.startswith("https"): app.config["SESSION_COOKIE_SECURE"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" setup_error_page(app) init_extensions(app) register_blueprints(app) set_index_page(app) jinja2_filter(app) setup_favicon_route(app) setup_openid_metadata(app) init_admin(app) setup_paddle_callback(app) setup_do_not_track(app) if FLASK_PROFILER_PATH: LOG.d("Enable flask-profiler") app.config["flask_profiler"] = { "enabled": True, "storage": {"engine": "sqlite", "FILE": FLASK_PROFILER_PATH}, "basicAuth": { "enabled": True, "username": "******", "password": FLASK_PROFILER_PASSWORD, }, "ignore": ["^/static/.*", "/git", "/exception"], } flask_profiler.init_app(app) # enable CORS on /api endpoints CORS(app, resources={r"/api/*": {"origins": "*"}}) # set session to permanent so user stays signed in after quitting the browser # the cookie is valid for 7 days @app.before_request def make_session_permanent(): session.permanent = True app.permanent_session_lifetime = timedelta(days=7) return app
def options_v5(): """ Return what options user has when creating new alias. Same as v4 but uses a better format. To be used with /v2/alias/custom/new Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README can_create: bool suffixes: [ { suffix: "suffix", signed_suffix: "signed_suffix" } ] prefix_suggestion: str recommendation: Optional dict alias: str hostname: str """ user = g.user hostname = request.args.get("hostname") ret = { "can_create": user.can_create_new_alias(), "suffixes": [], "prefix_suggestion": "", } # recommendation alias if exist if hostname: # put the latest used alias first q = ( db.session.query(AliasUsedOn, Alias, User) .filter( AliasUsedOn.alias_id == Alias.id, Alias.user_id == user.id, AliasUsedOn.hostname == hostname, ) .order_by(desc(AliasUsedOn.created_at)) ) r = q.first() if r: _, alias, _ = r LOG.d("found alias %s %s %s", alias, hostname, user) ret["recommendation"] = {"alias": alias.email, "hostname": hostname} # custom alias suggestion and suffix if hostname: # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon domain_name = hostname if "." in hostname: parts = hostname.split(".") domain_name = parts[-2] domain_name = convert_to_id(domain_name) ret["prefix_suggestion"] = domain_name # List of (is_custom_domain, alias-suffix, time-signed alias-suffix) suffixes = available_suffixes(user) # custom domain should be put first ret["suffixes"] = [ {"suffix": suffix[1], "signed_suffix": suffix[2]} for suffix in suffixes ] return jsonify(ret)
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, 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) db.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, ) 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, alias_id=contact.alias_id) # db.session.commit() # have some disabled alias if i % 5 == 0: a.enabled = False db.session.commit() custom_domain1 = CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) db.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") 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" db.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" db.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", ) 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), comment="Local manual", commit=True, ) SLDomain.create(domain="premium.com", premium_only=True, commit=True)
async def _hibp_check(api_key, queue): """ Uses a single API key to check the queue as fast as possible. This function to be ran simultaneously (multiple _hibp_check functions with different keys on the same queue) to make maximum use of multiple API keys. """ while True: try: alias_id = queue.get_nowait() except asyncio.QueueEmpty: return alias = Alias.get(alias_id) # an alias can be deleted in the meantime if not alias: return LOG.d("Checking HIBP for %s", alias) request_headers = { "user-agent": "SimpleLogin", "hibp-api-key": api_key, } r = requests.get( f"https://haveibeenpwned.com/api/v3/breachedaccount/{urllib.parse.quote(alias.email)}", headers=request_headers, ) if r.status_code == 200: # Breaches found alias.hibp_breaches = [ Hibp.get_by(name=entry["Name"]) for entry in r.json() ] if len(alias.hibp_breaches) > 0: LOG.w("%s appears in HIBP breaches %s", alias, alias.hibp_breaches) elif r.status_code == 404: # No breaches found alias.hibp_breaches = [] elif r.status_code == 429: # rate limited LOG.w("HIBP rate limited, check alias %s in the next run", alias) await asyncio.sleep(1.6) return elif r.status_code > 500: LOG.w("HIBP server 5** error %s", r.status_code) return else: LOG.error( "An error occured while checking alias %s: %s - %s", alias, r.status_code, r.text, ) return alias.hibp_last_check = arrow.utcnow() db.session.add(alias) db.session.commit() LOG.d("Updated breaches info for %s", alias) await asyncio.sleep(1.6)
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.error("Alias creation time expired for %s", current_user) flash("Alias creation time is expired, please retry", "warning") return redirect(request.url) except Exception: LOG.error("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.error("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) 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 sl_sendmail( from_addr, to_addr, msg: Message, mail_options=(), rcpt_options=(), is_forward: bool = False, retries=2, ignore_smtp_error=False, ): """replace smtp.sendmail""" if NOT_SEND_EMAIL: LOG.d( "send email with subject '%s', from '%s' to '%s'", msg[headers.SUBJECT], msg[headers.FROM], msg[headers.TO], ) return try: start = time.time() if POSTFIX_SUBMISSION_TLS: smtp_port = 587 else: smtp_port = POSTFIX_PORT with SMTP(POSTFIX_SERVER, smtp_port) as smtp: if POSTFIX_SUBMISSION_TLS: smtp.starttls() elapsed = time.time() - start LOG.d("getting a smtp connection takes seconds %s", elapsed) newrelic.agent.record_custom_metric("Custom/smtp_connection_time", elapsed) # smtp.send_message has UnicodeEncodeError # encode message raw directly instead LOG.d( "Sendmail mail_from:%s, rcpt_to:%s, header_from:%s, header_to:%s, header_cc:%s", from_addr, to_addr, msg[headers.FROM], msg[headers.TO], msg[headers.CC], ) smtp.sendmail( from_addr, to_addr, to_bytes(msg), mail_options, rcpt_options, ) except (SMTPServerDisconnected, SMTPRecipientsRefused) as e: if retries > 0: LOG.w( "SMTPServerDisconnected or SMTPRecipientsRefused error %s, retry", e, exc_info=True, ) time.sleep(0.3 * retries) sl_sendmail( from_addr, to_addr, msg, mail_options, rcpt_options, is_forward, retries=retries - 1, ) else: if ignore_smtp_error: LOG.w("Ignore smtp error %s", e) else: raise
def should_ignore_bounce(mail_from: str) -> bool: if IgnoreBounceSender.get_by(mail_from=mail_from): LOG.w("do not send back bounce report to %s", mail_from) return True return False
def should_disable(alias: Alias) -> bool: """Disable an alias if it has too many bounces recently""" # Bypass the bounce rule if alias.cannot_be_disabled: LOG.w("%s cannot be disabled", alias) return False if not ALIAS_AUTOMATIC_DISABLE: return False yesterday = arrow.now().shift(days=-1) nb_bounced_last_24h = (Session.query(EmailLog).filter( EmailLog.bounced.is_(True), EmailLog.is_reply.is_(False), EmailLog.created_at > yesterday, ).filter(EmailLog.alias_id == alias.id).count()) # if more than 12 bounces in 24h -> disable alias if nb_bounced_last_24h > 12: LOG.d("more than 12 bounces in the last 24h, disable alias %s", alias) return True # if more than 5 bounces but has bounces last week -> disable alias elif nb_bounced_last_24h > 5: one_week_ago = arrow.now().shift(days=-8) nb_bounced_7d_1d = (Session.query(EmailLog).filter( EmailLog.bounced.is_(True), EmailLog.is_reply.is_(False), EmailLog.created_at > one_week_ago, EmailLog.created_at < yesterday, ).filter(EmailLog.alias_id == alias.id).count()) if nb_bounced_7d_1d > 1: LOG.d( "more than 5 bounces in the last 24h and more than 1 bounces in the last 7 days, " "disable alias %s", alias, ) return True else: # alias level # if bounces at least 9 days in the last 10 days -> disable alias query = (Session.query( func.date(EmailLog.created_at).label("date"), func.count(EmailLog.id).label("count"), ).filter(EmailLog.alias_id == alias.id).filter( EmailLog.created_at > arrow.now().shift(days=-10), EmailLog.bounced.is_(True), EmailLog.is_reply.is_(False), ).group_by("date")) if query.count() >= 9: LOG.d( "Bounces every day for at least 9 days in the last 10 days, disable alias %s", alias, ) return True # account level query = (Session.query( func.date(EmailLog.created_at).label("date"), func.count(EmailLog.id).label("count"), ).filter(EmailLog.user_id == alias.user_id).filter( EmailLog.created_at > arrow.now().shift(days=-10), EmailLog.bounced.is_(True), EmailLog.is_reply.is_(False), ).group_by("date")) # if an account has more than 10 bounces every day for at least 4 days in the last 10 days, disable alias date_bounces: List[Tuple[arrow.Arrow, int]] = list(query) if len(date_bounces) > 4: if all([v > 10 for _, v in date_bounces]): LOG.d( "+10 bounces for +4 days in the last 10 days on %s, disable alias %s", alias.user, alias, ) return True return False
def spf_pass( envelope, mailbox: Mailbox, user: User, alias: Alias, contact_email: str, msg: Message, ) -> bool: ip = msg[_IP_HEADER] if ip: LOG.d("Enforce SPF on %s %s", ip, envelope.mail_from) try: r = spf.check2(i=ip, s=envelope.mail_from, h=None) except Exception: LOG.e("SPF error, mailbox %s, ip %s", mailbox.email, ip) else: # TODO: Handle temperr case (e.g. dns timeout) # only an absolute pass, or no SPF policy at all is 'valid' if r[0] not in ["pass", "none"]: LOG.w( "SPF fail for mailbox %s, reason %s, failed IP %s", mailbox.email, r[0], ip, ) subject = get_header_unicode(msg[headers.SUBJECT]) send_email_with_rate_control( user, ALERT_SPF, mailbox.email, f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address", render( "transactional/spf-fail.txt", alias=alias.email, ip=ip, mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", to_email=contact_email, subject=subject, time=arrow.now(), ), render( "transactional/spf-fail.html", ip=ip, mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", to_email=contact_email, subject=subject, time=arrow.now(), ), ) return False else: LOG.w( "Could not find %s header %s -> %s", _IP_HEADER, mailbox.email, contact_email, ) return True
def alias_contact_manager(alias_id, forward_email_id=None): gen_email = GenEmail.get(alias_id) # sanity check if not gen_email: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if gen_email.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_email = new_contact_form.email.data # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break website_email = get_email_part(contact_email) # already been added if ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email): flash(f"{website_email} is already added", "error") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=contact_email, reply_email=reply_email, ) LOG.d("create reverse-alias for %s", contact_email) db.session.commit() flash(f"Reverse alias for {contact_email} is created", "success") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, forward_email_id=forward_email.id, )) elif request.form.get("form-name") == "delete": forward_email_id = request.form.get("forward-email-id") forward_email = ForwardEmail.get(forward_email_id) if not forward_email: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) elif forward_email.gen_email_id != gen_email.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) contact_name = forward_email.website_from ForwardEmail.delete(forward_email_id) db.session.commit() flash(f"Reverse-alias for {contact_name} has been deleted", "success") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) # make sure highlighted forward_email is at array start forward_emails = gen_email.forward_emails if forward_email_id: forward_emails = sorted(forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True) return render_template( "dashboard/alias_contact_manager.html", forward_emails=forward_emails, alias=gen_email.email, new_contact_form=new_contact_form, forward_email_id=forward_email_id, )
def domain_detail(custom_domain_id): custom_domain = CustomDomain.get(custom_domain_id) if not custom_domain or custom_domain.user_id != current_user.id: flash("You cannot see this page", "warning") return redirect(url_for("dashboard.index")) if request.method == "POST": if request.form.get("form-name") == "switch-catch-all": custom_domain.catch_all = not custom_domain.catch_all db.session.commit() if custom_domain.catch_all: flash( f"The catch-all has been enabled for {custom_domain.domain}", "success", ) else: flash( f"The catch-all has been disabled for {custom_domain.domain}", "warning", ) return redirect( url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)) elif request.form.get("form-name") == "set-name": custom_domain.name = request.form.get("alias-name").replace( "\n", "") db.session.commit() flash( f"Default alias name for Domain {custom_domain.domain} has been set", "success", ) return redirect( url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)) elif request.form.get( "form-name") == "switch-random-prefix-generation": custom_domain.random_prefix_generation = ( not custom_domain.random_prefix_generation) db.session.commit() if custom_domain.random_prefix_generation: flash( f"Random prefix generation has been enabled for {custom_domain.domain}", "success", ) else: flash( f"Random prefix generation has been disabled for {custom_domain.domain}", "warning", ) return redirect( url_for("dashboard.domain_detail", custom_domain_id=custom_domain.id)) elif request.form.get("form-name") == "delete": name = custom_domain.domain LOG.d("Schedule deleting %s", custom_domain) Thread(target=delete_domain, args=(custom_domain_id, )).start() flash( f"{name} scheduled for deletion." f"You will receive a confirmation email when the deletion is finished", "success", ) return redirect(url_for("dashboard.custom_domain")) nb_alias = Alias.filter_by(custom_domain_id=custom_domain.id).count() return render_template("dashboard/domain_detail/info.html", **locals())
def hard_exit(): pid = os.getpid() LOG.warning("kill pid %s", pid) os.kill(pid, 9)
def stats(): """send admin stats everyday""" if not ADMIN_EMAIL: # nothing to do return # todo: remove metrics1 compute_metrics() stats_today = compute_metric2() stats_yesterday = ( Metric2.query.filter(Metric2.date < stats_today.date) .order_by(Metric2.date.desc()) .first() ) nb_user_increase = increase_percent(stats_yesterday.nb_user, stats_today.nb_user) nb_alias_increase = increase_percent(stats_yesterday.nb_alias, stats_today.nb_alias) nb_forward_increase = increase_percent( stats_yesterday.nb_forward, stats_today.nb_forward ) today = arrow.now().format() html = f""" Stats for {today} <br> nb_user: {stats_today.nb_user} - {increase_percent(stats_yesterday.nb_user, stats_today.nb_user)} <br> nb_premium: {stats_today.nb_premium} - {increase_percent(stats_yesterday.nb_premium, stats_today.nb_premium)} <br> nb_cancelled_premium: {stats_today.nb_cancelled_premium} - {increase_percent(stats_yesterday.nb_cancelled_premium, stats_today.nb_cancelled_premium)} <br> nb_apple_premium: {stats_today.nb_apple_premium} - {increase_percent(stats_yesterday.nb_apple_premium, stats_today.nb_apple_premium)} <br> nb_manual_premium: {stats_today.nb_manual_premium} - {increase_percent(stats_yesterday.nb_manual_premium, stats_today.nb_manual_premium)} <br> nb_coinbase_premium: {stats_today.nb_coinbase_premium} - {increase_percent(stats_yesterday.nb_coinbase_premium, stats_today.nb_coinbase_premium)} <br> nb_alias: {stats_today.nb_alias} - {increase_percent(stats_yesterday.nb_alias, stats_today.nb_alias)} <br> nb_forward: {stats_today.nb_forward} - {increase_percent(stats_yesterday.nb_forward, stats_today.nb_forward)} <br> nb_reply: {stats_today.nb_reply} - {increase_percent(stats_yesterday.nb_reply, stats_today.nb_reply)} <br> nb_block: {stats_today.nb_block} - {increase_percent(stats_yesterday.nb_block, stats_today.nb_block)} <br> nb_bounced: {stats_today.nb_bounced} - {increase_percent(stats_yesterday.nb_bounced, stats_today.nb_bounced)} <br> nb_spam: {stats_today.nb_spam} - {increase_percent(stats_yesterday.nb_spam, stats_today.nb_spam)} <br> nb_custom_domain: {stats_today.nb_verified_custom_domain} - {increase_percent(stats_yesterday.nb_verified_custom_domain, stats_today.nb_verified_custom_domain)} <br> nb_app: {stats_today.nb_app} - {increase_percent(stats_yesterday.nb_app, stats_today.nb_app)} <br> nb_referred_user: {stats_today.nb_referred_user} - {increase_percent(stats_yesterday.nb_referred_user, stats_today.nb_referred_user)} <br> nb_referred_user_upgrade: {stats_today.nb_referred_user_paid} - {increase_percent(stats_yesterday.nb_referred_user_paid, stats_today.nb_referred_user_paid)} <br> """ html += f"""<br> Bounce report: <br> """ for email, bounces in bounce_report(): html += f"{email}: {bounces} <br>" html += f"""<br><br> Alias creation report: <br> """ for email, nb_alias, date in alias_creation_report(): html += f"{email}, {date}: {nb_alias} <br>" LOG.d("report email: %s", html) send_email( ADMIN_EMAIL, subject=f"SimpleLogin Stats for {today}, {nb_user_increase} users, {nb_alias_increase} aliases, {nb_forward_increase} forwards", plaintext="", html=html, )
def send_email( to_email, subject, plaintext, html=None, unsubscribe_link=None, unsubscribe_via_email=False, retries=0, # by default no retry if sending fails ignore_smtp_error=False, ): to_email = sanitize_email(to_email) if NOT_SEND_EMAIL: LOG.d( "send email with subject '%s' to '%s', plaintext: %s, html: %s", subject, to_email, plaintext, html, ) return LOG.d("send email to %s, subject '%s'", to_email, subject) if html: msg = MIMEMultipart("alternative") msg.attach(MIMEText(plaintext)) msg.attach(MIMEText(html, "html")) else: msg = EmailMessage() msg.set_payload(plaintext) msg[headers.CONTENT_TYPE] = "text/plain" msg[headers.SUBJECT] = subject msg[headers.FROM] = f"{NOREPLY} <{NOREPLY}>" msg[headers.TO] = to_email msg_id_header = make_msgid() msg[headers.MESSAGE_ID] = msg_id_header date_header = formatdate() msg[headers.DATE] = date_header if unsubscribe_link: add_or_replace_header(msg, headers.LIST_UNSUBSCRIBE, f"<{unsubscribe_link}>") if not unsubscribe_via_email: add_or_replace_header(msg, headers.LIST_UNSUBSCRIBE_POST, "List-Unsubscribe=One-Click") # add DKIM email_domain = NOREPLY[NOREPLY.find("@") + 1:] add_dkim_signature(msg, email_domain) transaction = TransactionalEmail.create(email=to_email, commit=True) # use a different envelope sender for each transactional email (aka VERP) sl_sendmail( TRANSACTIONAL_BOUNCE_EMAIL.format(transaction.id), to_email, msg, retries=retries, ignore_smtp_error=ignore_smtp_error, )
def sanity_check(): """ #TODO: investigate why DNS sometimes not working Different sanity checks - detect if there's mailbox that's using a invalid domain """ mailbox_ids = ( db.session.query(Mailbox.id) .filter(Mailbox.verified.is_(True), Mailbox.disabled.is_(False)) .all() ) mailbox_ids = [e[0] for e in mailbox_ids] # iterate over id instead of mailbox directly # as a mailbox can be deleted during the sleep time for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) # a mailbox has been deleted if not mailbox: continue # hack to not query DNS too often sleep(1) if not email_can_be_used_as_mailbox(mailbox.email): mailbox.nb_failed_checks += 1 nb_email_log = nb_email_log_for_mailbox(mailbox) # send a warning if mailbox.nb_failed_checks == 5: if mailbox.user.email != mailbox.email: send_email( mailbox.user.email, f"Mailbox {mailbox.email} is disabled", render( "transactional/disable-mailbox-warning.txt", mailbox=mailbox ), render( "transactional/disable-mailbox-warning.html", mailbox=mailbox, ), ) # alert if too much fail and nb_email_log > 100 if mailbox.nb_failed_checks > 10 and nb_email_log > 100: mailbox.disabled = True if mailbox.user.email != mailbox.email: send_email( mailbox.user.email, f"Mailbox {mailbox.email} is disabled", render("transactional/disable-mailbox.txt", mailbox=mailbox), render("transactional/disable-mailbox.html", mailbox=mailbox), ) LOG.warning( "issue with mailbox %s domain. #alias %s, nb email log %s", mailbox, mailbox.nb_alias(), nb_email_log, ) else: # reset nb check mailbox.nb_failed_checks = 0 db.session.commit() for user in User.filter_by(activated=True).all(): if sanitize_email(user.email) != user.email: LOG.exception("%s does not have sanitized email", user) for alias in Alias.query.all(): if sanitize_email(alias.email) != alias.email: LOG.exception("Alias %s email not sanitized", alias) if alias.name and "\n" in alias.name: alias.name = alias.name.replace("\n", "") db.session.commit() LOG.exception("Alias %s name contains linebreak %s", alias, alias.name) contact_email_sanity_date = arrow.get("2021-01-12") for contact in Contact.query.all(): if sanitize_email(contact.reply_email) != contact.reply_email: LOG.exception("Contact %s reply-email not sanitized", contact) if ( sanitize_email(contact.website_email) != contact.website_email and contact.created_at > contact_email_sanity_date ): LOG.exception("Contact %s website-email not sanitized", contact) if not contact.invalid_email and not is_valid_email(contact.website_email): LOG.exception("%s invalid email", contact) contact.invalid_email = True db.session.commit() for mailbox in Mailbox.query.all(): if sanitize_email(mailbox.email) != mailbox.email: LOG.exception("Mailbox %s address not sanitized", mailbox) for contact in Contact.query.all(): if normalize_reply_email(contact.reply_email) != contact.reply_email: LOG.exception( "Contact %s reply email is not normalized %s", contact, contact.reply_email, ) for domain in CustomDomain.query.all(): if domain.name and "\n" in domain.name: LOG.exception("Domain %s name contain linebreak %s", domain, domain.name) migrate_domain_trash() set_custom_domain_for_alias() LOG.d("Finish sanity check")
def email_can_be_used_as_mailbox(email_address: str) -> bool: """Return True if an email can be used as a personal email. Use the email domain as criteria. A domain can be used if it is not: - one of ALIAS_DOMAINS - one of PREMIUM_ALIAS_DOMAINS - one of custom domains - a disposable domain """ try: domain = validate_email(email_address, check_deliverability=False, allow_smtputf8=False).domain except EmailNotValidError: LOG.d("%s is invalid email address", email_address) return False if not domain: LOG.d("no valid domain associated to %s", email_address) return False if SLDomain.get_by(domain=domain): LOG.d("%s is a SL domain", email_address) return False from app.models import CustomDomain if CustomDomain.get_by(domain=domain, verified=True): LOG.d("domain %s is a SimpleLogin custom domain", domain) return False if is_invalid_mailbox_domain(domain): LOG.d("Domain %s is invalid mailbox domain", domain) return False # check if email MX domain is disposable mx_domains = get_mx_domain_list(domain) # if no MX record, email is not valid if not mx_domains: LOG.d("No MX record for domain %s", domain) return False for mx_domain in mx_domains: if is_invalid_mailbox_domain(mx_domain): LOG.d("MX Domain %s %s is invalid mailbox domain", mx_domain, domain) return False return True
async def check_hibp(): """ Check all aliases on the HIBP (Have I Been Pwned) API """ LOG.d("Checking HIBP API for aliases in breaches") if len(HIBP_API_KEYS) == 0: LOG.exception("No HIBP API keys") return LOG.d("Updating list of known breaches") r = requests.get("https://haveibeenpwned.com/api/v3/breaches") for entry in r.json(): Hibp.get_or_create(name=entry["Name"]) db.session.commit() LOG.d("Updated list of known breaches") LOG.d("Preparing list of aliases to check") queue = asyncio.Queue() max_date = arrow.now().shift(days=-HIBP_SCAN_INTERVAL_DAYS) for alias in ( Alias.query.filter( or_(Alias.hibp_last_check.is_(None), Alias.hibp_last_check < max_date) ) .filter(Alias.enabled) .order_by(Alias.hibp_last_check.asc()) .all() ): await queue.put(alias.id) LOG.d("Need to check about %s aliases", queue.qsize()) # Start one checking process per API key # Each checking process will take one alias from the queue, get the info # and then sleep for 1.5 seconds (due to HIBP API request limits) checkers = [] for i in range(len(HIBP_API_KEYS)): checker = asyncio.create_task( _hibp_check( HIBP_API_KEYS[i], queue, ) ) checkers.append(checker) # Wait until all checking processes are done for checker in checkers: await checker LOG.d("Done checking HIBP API for aliases in breaches")
def alias_contact_manager(alias_id): highlight_contact_id = None if request.args.get("highlight_contact_id"): highlight_contact_id = int(request.args.get("highlight_contact_id")) alias = Alias.get(alias_id) page = 0 if request.args.get("page"): page = int(request.args.get("page")) # sanity check if not alias: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if alias.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_addr = new_contact_form.email.data.strip() # generate a reply_email, make sure it is unique # not use while to avoid infinite loop reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): break try: contact_name, contact_email = parseaddr_unicode(contact_addr) except Exception: flash(f"{contact_addr} is invalid", "error") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id,) ) contact_email = contact_email.lower() contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) # already been added if contact: flash(f"{contact_email} is already added", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, ) ) contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=reply_email, ) LOG.d("create reverse-alias for %s", contact_addr) db.session.commit() flash(f"Reverse alias for {contact_addr} is created", "success") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, ) ) elif request.form.get("form-name") == "delete": contact_id = request.form.get("contact-id") contact = Contact.get(contact_id) if not contact: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) elif contact.alias_id != alias.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) delete_contact_email = contact.website_email Contact.delete(contact_id) db.session.commit() flash( f"Reverse-alias for {delete_contact_email} has been deleted", "success" ) return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) # make sure highlighted contact is at array start contacts = alias.get_contacts(page) contact_ids = [contact.id for contact in contacts] last_page = len(contacts) < PAGE_LIMIT if highlight_contact_id not in contact_ids: contact = Contact.get(highlight_contact_id) if contact and contact.alias_id == alias.id: contacts.insert(0, contact) return render_template( "dashboard/alias_contact_manager.html", contacts=contacts, alias=alias, new_contact_form=new_contact_form, highlight_contact_id=highlight_contact_id, page=page, last_page=last_page, )
def github_callback(): # user clicks on cancel if "error" in request.args: flash("Please use another sign in method then", "warning") return redirect("/") github = OAuth2Session( GITHUB_CLIENT_ID, state=session["oauth_state"], scope=["user:email"], redirect_uri=_redirect_uri, ) github.fetch_token( _token_url, client_secret=GITHUB_CLIENT_SECRET, authorization_response=request.url, ) # a dict with "name", "login" github_user_data = github.get("https://api.github.com/user").json() # return list of emails # { # 'email': '*****@*****.**', # 'primary': False, # 'verified': True, # 'visibility': None # } emails = github.get("https://api.github.com/user/emails").json() # only take the primary email email = None for e in emails: if e.get("verified") and e.get("primary"): email = e.get("email") break if not email: LOG.exception( f"cannot get email for github user {github_user_data} {emails}") flash( "Cannot get a valid email from Github, please another way to login/sign up", "error", ) return redirect(url_for("auth.login")) email = sanitize_email(email) user = User.get_by(email=email) if not user: flash( "Sorry you cannot sign up via Github, please use email/password sign-up instead", "error", ) return redirect(url_for("auth.register")) if not SocialAuth.get_by(user_id=user.id, social="github"): SocialAuth.create(user_id=user.id, social="github") db.session.commit() # The activation link contains the original page, for ex authorize page next_url = request.args.get("next") if request.args else None return after_login(user, next_url)
PlanEnum, ApiKey, CustomDomain, LifetimeCoupon, Directory, Mailbox, Referral, AliasMailbox, Notification, PublicDomain, ) from app.monitor.base import monitor_bp from app.oauth.base import oauth_bp if SENTRY_DSN: LOG.d("enable sentry") sentry_sdk.init( dsn=SENTRY_DSN, integrations=[ FlaskIntegration(), SqlalchemyIntegration(), AioHttpIntegration(), ], ) # the app is served behin nginx which uses http and not https os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" def create_light_app() -> Flask: app = Flask(__name__)
def options_v2(): """ Return what options user has when creating new alias. Input: a valid api-key in "Authentication" header and optional "hostname" in args Output: cf README can_create: bool suffixes: [str] prefix_suggestion: str existing: [str] recommendation: Optional dict alias: str hostname: str """ LOG.exception("/v2/alias/options is obsolete") user = g.user hostname = request.args.get("hostname") ret = { "existing": [ ge.email for ge in Alias.query.filter_by(user_id=user.id, enabled=True) ], "can_create": user.can_create_new_alias(), "suffixes": [], "prefix_suggestion": "", } # recommendation alias if exist if hostname: # put the latest used alias first q = ( db.session.query(AliasUsedOn, Alias, User) .filter( AliasUsedOn.alias_id == Alias.id, Alias.user_id == user.id, AliasUsedOn.hostname == hostname, ) .order_by(desc(AliasUsedOn.created_at)) ) r = q.first() if r: _, alias, _ = r LOG.d("found alias %s %s %s", alias, hostname, user) ret["recommendation"] = {"alias": alias.email, "hostname": hostname} # custom alias suggestion and suffix if hostname: # keep only the domain name of hostname, ignore TLD and subdomain # for ex www.groupon.com -> groupon domain_name = hostname if "." in hostname: parts = hostname.split(".") domain_name = parts[-2] domain_name = convert_to_id(domain_name) ret["prefix_suggestion"] = domain_name # maybe better to make sure the suffix is never used before # but this is ok as there's a check when creating a new custom alias for domain in ALIAS_DOMAINS: if DISABLE_ALIAS_SUFFIX: ret["suffixes"].append(f"@{domain}") else: ret["suffixes"].append(f".{random_word()}@{domain}") for custom_domain in user.verified_custom_domains(): ret["suffixes"].append("@" + custom_domain.domain) # custom domain should be put first ret["suffixes"] = list(reversed(ret["suffixes"])) return jsonify(ret)
def forbidden(e): LOG.warning("Client hit rate limit on path %s", request.path) if request.path.startswith("/api/"): return jsonify(error="Rate limit exceeded"), 429 else: return render_template("error/429.html"), 429
def error_handler(e): LOG.exception(e) if request.path.startswith("/api/"): return jsonify(error="Internal error"), 500 else: return render_template("error/500.html"), 500
_hibp_check( HIBP_API_KEYS[i], queue, ) ) checkers.append(checker) # Wait until all checking processes are done for checker in checkers: await checker LOG.d("Done checking HIBP API for aliases in breaches") if __name__ == "__main__": LOG.d("Start running cronjob") parser = argparse.ArgumentParser() parser.add_argument( "-j", "--job", help="Choose a cron job to run", type=str, choices=[ "stats", "notify_trial_end", "notify_manual_subscription_end", "notify_premium_end", "delete_logs", "poll_apple_subscription", "sanity_check", "delete_old_monitoring",
ApiKey, CustomDomain, LifetimeCoupon, Directory, Mailbox, Referral, AliasMailbox, Notification, SLDomain, ) from app.monitor.base import monitor_bp from app.oauth.base import oauth_bp from app.pgp_utils import load_public_key if SENTRY_DSN: LOG.d("enable sentry") sentry_sdk.init( dsn=SENTRY_DSN, release=f"app@{SHA1}", integrations=[ FlaskIntegration(), SqlalchemyIntegration(), ], ) # the app is served behin nginx which uses http and not https os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" def create_light_app() -> Flask: app = Flask(__name__)
def notify_manual_sub_end(): for manual_sub in ManualSubscription.query.all(): need_reminder = False if arrow.now().shift(days=14) > manual_sub.end_at > arrow.now().shift(days=13): need_reminder = True elif arrow.now().shift(days=4) > manual_sub.end_at > arrow.now().shift(days=3): need_reminder = True if need_reminder: user = manual_sub.user LOG.debug("Remind user %s that their manual sub is ending soon", user) send_email( user.email, f"Your subscription will end soon", render( "transactional/manual-subscription-end.txt", user=user, manual_sub=manual_sub, ), render( "transactional/manual-subscription-end.html", user=user, manual_sub=manual_sub, ), ) extend_subscription_url = URL + "/dashboard/coinbase_checkout" for coinbase_subscription in CoinbaseSubscription.query.all(): need_reminder = False if ( arrow.now().shift(days=14) > coinbase_subscription.end_at > arrow.now().shift(days=13) ): need_reminder = True elif ( arrow.now().shift(days=4) > coinbase_subscription.end_at > arrow.now().shift(days=3) ): need_reminder = True if need_reminder: user = coinbase_subscription.user LOG.debug( "Remind user %s that their coinbase subscription is ending soon", user ) send_email( user.email, "Your SimpleLogin subscription will end soon", render( "transactional/coinbase/reminder-subscription.txt", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), render( "transactional/coinbase/reminder-subscription.html", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), )
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, ) db.session.commit() user.trial_end = None LifetimeCoupon.create(code="coupon", nb_used=10) db.session.commit() # 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() 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="*****@*****.**", name="Winston", password="******", activated=True, referral_id=referral.id, ) db.session.commit()
def init_extensions(app: Flask): LOG.debug("init extensions") login_manager.init_app(app) db.init_app(app) migrate.init_app(app)