Ejemplo n.º 1
0
def get_spam_score(message: Message,
                   email_log: EmailLog,
                   can_retry=True) -> (float, dict):
    """
    Return the spam score and spam report
    """
    LOG.d("get spam score for %s", email_log)
    sa_input = to_bytes(message)

    # Spamassassin requires to have an ending linebreak
    if not sa_input.endswith(b"\n"):
        LOG.d("add linebreak to spamassassin input")
        sa_input += b"\n"

    try:
        # wait for at max 300s which is the default spamd timeout-child
        sa = SpamAssassin(sa_input, host=SPAMASSASSIN_HOST, timeout=300)
        return sa.get_score(), sa.get_report_json()
    except Exception:
        if can_retry:
            LOG.w("SpamAssassin exception, retry")
            time.sleep(3)
            return get_spam_score(message, email_log, can_retry=False)
        else:
            # return a negative score so the message is always considered as ham
            LOG.e("SpamAssassin exception, ignore spam check")
            return -999, None
Ejemplo n.º 2
0
def try_auto_create(address: str) -> Optional[Alias]:
    """Try to auto-create the alias using directory or catch-all domain"""
    # VERP for reply phase is {BOUNCE_PREFIX_FOR_REPLY_PHASE}+{email_log.id}+@{alias_domain}
    if address.startswith(
            f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+") and "+@" in address:
        LOG.e("alias %s can't start with %s", address,
              BOUNCE_PREFIX_FOR_REPLY_PHASE)
        return None

    # VERP for forward phase is BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX
    if address.startswith(BOUNCE_PREFIX) and address.endswith(BOUNCE_SUFFIX):
        LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX)
        return None

    try:
        # NOT allow unicode for now
        validate_email(address,
                       check_deliverability=False,
                       allow_smtputf8=False)
    except EmailNotValidError:
        return None

    alias = try_auto_create_via_domain(address)
    if not alias:
        alias = try_auto_create_directory(address)

    return alias
Ejemplo n.º 3
0
    def paddle_coupon():
        LOG.d(f"paddle coupon callback %s", request.form)

        if not paddle_utils.verify_incoming_request(dict(request.form)):
            LOG.e("request not coming from paddle. Request data:%s",
                  dict(request.form))
            return "KO", 400

        product_id = request.form.get("p_product_id")
        if product_id != PADDLE_COUPON_ID:
            LOG.e("product_id %s not match with %s", product_id,
                  PADDLE_COUPON_ID)
            return "KO", 400

        email = request.form.get("email")
        LOG.d("Paddle coupon request for %s", email)

        coupon = Coupon.create(
            code=random_string(30),
            comment="For 1-year coupon",
            expires_date=arrow.now().shift(years=1, days=-1),
            commit=True,
        )

        return (
            f"Your 1-year coupon is <b>{coupon.code}</b> <br> "
            f"It's valid until <b>{coupon.expires_date.date().isoformat()}</b>"
        )
Ejemplo n.º 4
0
def change_plan(user: User, subscription_id: str, plan_id) -> (bool, str):
    """return whether the operation is successful and an optional error message"""
    r = requests.post(
        "https://vendors.paddle.com/api/2.0/subscription/users/update",
        data={
            "vendor_id": PADDLE_VENDOR_ID,
            "vendor_auth_code": PADDLE_AUTH_CODE,
            "subscription_id": subscription_id,
            "plan_id": plan_id,
        },
    )
    res = r.json()
    if not res["success"]:
        try:
            # "unable to complete the resubscription because we could not charge the customer for the resubscription"
            if res["error"]["code"] == 147:
                LOG.w(
                    "could not charge the customer for the resubscription error %s,%s",
                    subscription_id,
                    user,
                )
                return False, "Your card cannot be charged"
        except KeyError:
            LOG.e(
                f"cannot change subscription {subscription_id} to {plan_id}, paddle response: {res}"
            )
            return False, ""

        return False, ""

    return res["success"], ""
Ejemplo n.º 5
0
def verify_id_token(id_token) -> bool:
    try:
        jwt.JWT(key=_key, jwt=id_token)
    except Exception:
        LOG.e("id token not verified")
        return False
    else:
        return True
Ejemplo n.º 6
0
def handle_coinbase_event(event) -> bool:
    user_id = int(event["data"]["metadata"]["user_id"])
    code = event["data"]["code"]
    user = User.get(user_id)
    if not user:
        LOG.e("User not found %s", user_id)
        return False

    coinbase_subscription: CoinbaseSubscription = CoinbaseSubscription.get_by(
        user_id=user_id)

    if not coinbase_subscription:
        LOG.d("Create a coinbase subscription for %s", user)
        coinbase_subscription = CoinbaseSubscription.create(
            user_id=user_id,
            end_at=arrow.now().shift(years=1),
            code=code,
            commit=True)
        send_email(
            user.email,
            "Your SimpleLogin account has been upgraded",
            render(
                "transactional/coinbase/new-subscription.txt",
                coinbase_subscription=coinbase_subscription,
            ),
            render(
                "transactional/coinbase/new-subscription.html",
                coinbase_subscription=coinbase_subscription,
            ),
        )
    else:
        if coinbase_subscription.code != code:
            LOG.d("Update code from %s to %s", coinbase_subscription.code,
                  code)
            coinbase_subscription.code = code

        if coinbase_subscription.is_active():
            coinbase_subscription.end_at = coinbase_subscription.end_at.shift(
                years=1)
        else:  # already expired subscription
            coinbase_subscription.end_at = arrow.now().shift(years=1)

        Session.commit()

        send_email(
            user.email,
            "Your SimpleLogin account has been extended",
            render(
                "transactional/coinbase/extend-subscription.txt",
                coinbase_subscription=coinbase_subscription,
            ),
            render(
                "transactional/coinbase/extend-subscription.html",
                coinbase_subscription=coinbase_subscription,
            ),
        )

    return True
Ejemplo n.º 7
0
def cancel_subscription(subscription_id: str) -> bool:
    r = requests.post(
        "https://vendors.paddle.com/api/2.0/subscription/users_cancel",
        data={
            "vendor_id": PADDLE_VENDOR_ID,
            "vendor_auth_code": PADDLE_AUTH_CODE,
            "subscription_id": subscription_id,
        },
    )
    res = r.json()
    if not res["success"]:
        LOG.e(f"cannot cancel subscription {subscription_id}, paddle response: {res}")

    return res["success"]
Ejemplo n.º 8
0
def sanitize_alias_address_name():
    count = 0
    # using Alias.all() will take all the memory
    for alias in Alias.yield_per_query():
        if count % 1000 == 0:
            LOG.d("process %s", count)

        count += 1
        if sanitize_email(alias.email) != alias.email:
            LOG.e("Alias %s email not sanitized", alias)

        if alias.name and "\n" in alias.name:
            alias.name = alias.name.replace("\n", "")
            Session.commit()
            LOG.e("Alias %s name contains linebreak %s", alias, alias.name)
Ejemplo n.º 9
0
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.e("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_entry = Hibp.get_or_create(name=entry["Name"])
        hibp_entry.date = arrow.get(entry["BreachDate"])
        hibp_entry.description = entry["Description"]

    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.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")
Ejemplo n.º 10
0
def provider2_sms():
    encoded = request.headers.get(PHONE_PROVIDER_2_HEADER)
    try:
        jwt.decode(encoded, key=PHONE_PROVIDER_2_SECRET, algorithms="HS256")
    except (InvalidSignatureError, DecodeError):
        LOG.e(
            "Unauthenticated callback %s %s %s %s",
            request.headers,
            request.method,
            request.args,
            request.json,
        )
        return "not ok", 400

    # request.json should be a dict where
    # msisdn is the sender
    # receiver is the receiver
    # For ex:
    # {'id': 2042489247, 'msisdn': 33612345678, 'country_code': 'FR', 'country_prefix': 33, 'receiver': 33687654321,
    # 'message': 'Test 1', 'senttime': 1641401781, 'webhook_label': 'Hagekar', 'sender': None,
    # 'mcc': None, 'mnc': None, 'validity_period': None, 'encoding': 'UTF8', 'udh': None, 'payload': None}

    to_number: str = str(request.json.get("receiver"))
    if not to_number.startswith("+"):
        to_number = "+" + to_number

    from_number = str(request.json.get("msisdn"))
    if not from_number.startswith("+"):
        from_number = "+" + from_number

    body = request.json.get("message")

    LOG.d("%s->%s:%s", from_number, to_number, body)

    phone_number = PhoneNumber.get_by(number=to_number)
    if phone_number:
        PhoneMessage.create(
            number_id=phone_number.id,
            from_number=from_number,
            body=body,
            commit=True,
        )
    else:
        LOG.e("Unknown phone number %s %s", to_number, request.json)
        return "not ok", 200
    return "ok", 200
Ejemplo n.º 11
0
def set_custom_domain_for_alias():
    """Go through all aliases and make sure custom_domain is correctly set"""
    sl_domains = [sl_domain.domain for sl_domain in SLDomain.all()]
    for alias in Alias.yield_per_query().filter(
            Alias.custom_domain_id.is_(None)):
        if (not any(
                alias.email.endswith(f"@{sl_domain}")
                for sl_domain in sl_domains) and not alias.custom_domain_id):
            alias_domain = get_email_domain_part(alias.email)
            custom_domain = CustomDomain.get_by(domain=alias_domain)
            if custom_domain:
                LOG.e("set %s for %s", custom_domain, alias)
                alias.custom_domain_id = custom_domain.id
            else:  # phantom domain
                LOG.d("phantom domain %s %s %s", alias.user, alias,
                      alias.enabled)

    Session.commit()
Ejemplo n.º 12
0
def twilio_sms():
    LOG.d("%s %s %s", request.args, request.form, request.data)
    resp = MessagingResponse()

    to_number = request.form.get("To")
    from_number = request.form.get("From")
    body = request.form.get("Body")

    LOG.d("%s->%s:%s", from_number, to_number, body)

    phone_number = PhoneNumber.get_by(number=to_number)
    if phone_number:
        PhoneMessage.create(
            number_id=phone_number.id,
            from_number=from_number,
            body=body,
            commit=True,
        )
    else:
        LOG.e("Unknown phone number %s %s", to_number, request.form)
    return str(resp)
Ejemplo n.º 13
0
async def get_spam_score_async(message: Message) -> float:
    sa_input = to_bytes(message)

    # Spamassassin requires to have an ending linebreak
    if not sa_input.endswith(b"\n"):
        LOG.d("add linebreak to spamassassin input")
        sa_input += b"\n"

    try:
        # wait for at max 300s which is the default spamd timeout-child
        response = await asyncio.wait_for(aiospamc.check(
            sa_input, host=SPAMASSASSIN_HOST),
                                          timeout=300)
        return response.headers["Spam"].score
    except asyncio.TimeoutError:
        LOG.e("SpamAssassin timeout")
        # return a negative score so the message is always considered as ham
        return -999
    except Exception:
        LOG.e("SpamAssassin exception")
        return -999
Ejemplo n.º 14
0
    def coinbase_webhook():
        # event payload
        request_data = request.data.decode("utf-8")
        # webhook signature
        request_sig = request.headers.get("X-CC-Webhook-Signature", None)

        try:
            # signature verification and event object construction
            event = Webhook.construct_event(request_data, request_sig,
                                            COINBASE_WEBHOOK_SECRET)
        except (WebhookInvalidPayload, SignatureVerificationError) as e:
            LOG.e("Invalid Coinbase webhook")
            return str(e), 400

        LOG.d("Coinbase event %s", event)

        if event["type"] == "charge:confirmed":
            if handle_coinbase_event(event):
                return "success", 200
            else:
                return "error", 400

        return "success", 200
Ejemplo n.º 15
0
def load_pgp_public_keys():
    """Load PGP public key to keyring"""
    for mailbox in Mailbox.filter(Mailbox.pgp_public_key.isnot(None)).all():
        LOG.d("Load PGP key for mailbox %s", mailbox)
        fingerprint = load_public_key(mailbox.pgp_public_key)

        # sanity check
        if fingerprint != mailbox.pgp_finger_print:
            LOG.e("fingerprint %s different for mailbox %s", fingerprint, mailbox)
            mailbox.pgp_finger_print = fingerprint
    Session.commit()

    for contact in Contact.filter(Contact.pgp_public_key.isnot(None)).all():
        LOG.d("Load PGP key for %s", contact)
        fingerprint = load_public_key(contact.pgp_public_key)

        # sanity check
        if fingerprint != contact.pgp_finger_print:
            LOG.e("fingerprint %s different for contact %s", fingerprint, contact)
            contact.pgp_finger_print = fingerprint

    Session.commit()

    LOG.d("Finish load_pgp_public_keys")
Ejemplo n.º 16
0
def get_encoding(msg: Message) -> EmailEncoding:
    """
    Return the message encoding, possible values:
    - quoted-printable
    - base64
    - 7bit: default if unknown or empty
    """
    cte = str(msg.get(headers.CONTENT_TRANSFER_ENCODING, "")).lower().strip()
    if cte in ("", "7bit", "8bit", "binary", "8bit;", "utf-8"):
        return EmailEncoding.NO

    if cte == "base64":
        return EmailEncoding.BASE64

    if cte == "quoted-printable":
        return EmailEncoding.QUOTED

    # some email services use unknown encoding
    if cte in ("amazonses.com", ):
        return EmailEncoding.NO

    LOG.e("Unknown encoding %s", cte)

    return EmailEncoding.NO
Ejemplo n.º 17
0
def provider1_sms():
    if request.headers.get(PHONE_PROVIDER_1_HEADER) != PHONE_PROVIDER_1_SECRET:
        LOG.e(
            "Unauthenticated callback %s %s %s %s",
            request.headers,
            request.method,
            request.args,
            request.data,
        )
        return "not ok", 200

    # request.form should be a dict that contains message_id, number, text, sim_card_number.
    # "number" is the contact number and "sim_card_number" the virtual number
    # The "reception_date" is in local time and shouldn't be used
    # For ex:
    # ImmutableMultiDict([('message_id', 'sms_0000000000000000000000'), ('number', '+33600112233'),
    # ('text', 'Lorem Ipsum is simply dummy text ...'), ('sim_card_number', '12345'),
    # ('reception_date', '2022-01-04 14:42:51')])
    to_number = request.form.get("sim_card_number")
    from_number = request.form.get("number")
    body = request.form.get("text")

    LOG.d("%s->%s:%s", from_number, to_number, body)

    phone_number = PhoneNumber.get_by(number=to_number)
    if phone_number:
        PhoneMessage.create(
            number_id=phone_number.id,
            from_number=from_number,
            body=body,
            commit=True,
        )
    else:
        LOG.e("Unknown phone number %s %s", to_number, request.form)
        return "not ok", 200
    return "ok", 200
Ejemplo n.º 18
0
def verify_prefix_suffix(user: User, alias_prefix, alias_suffix) -> bool:
    """verify if user could create an alias with the given prefix and suffix"""
    if not alias_prefix or not alias_suffix:  # should be caught on frontend
        return False

    user_custom_domains = [cd.domain for cd in user.verified_custom_domains()]

    # make sure alias_suffix is either [email protected] or @my-domain.com
    alias_suffix = alias_suffix.strip()
    # alias_domain_prefix is either a .random_word or ""
    alias_domain_prefix, alias_domain = alias_suffix.split("@", 1)

    # alias_domain must be either one of user custom domains or built-in domains
    if alias_domain not in user.available_alias_domains():
        LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
        return False

    # SimpleLogin domain case:
    # 1) alias_suffix must start with "." and
    # 2) alias_domain_prefix must come from the word list
    if (alias_domain in user.available_sl_domains()
            and alias_domain not in user_custom_domains
            # when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty
            and not DISABLE_ALIAS_SUFFIX):

        if not alias_domain_prefix.startswith("."):
            LOG.e("User %s submits a wrong alias suffix %s", user,
                  alias_suffix)
            return False

    else:
        if alias_domain not in user_custom_domains:
            if not DISABLE_ALIAS_SUFFIX:
                LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
                return False

            if alias_domain not in user.available_sl_domains():
                LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
                return False

    return True
Ejemplo n.º 19
0
def subdomain_route():
    if not current_user.subdomain_is_available():
        flash("Unknown error, redirect to the home page", "error")
        return redirect(url_for("dashboard.index"))

    sl_domains = SLDomain.filter_by(can_use_subdomain=True).all()
    subdomains = CustomDomain.filter_by(
        user_id=current_user.id, is_sl_subdomain=True
    ).all()

    errors = {}

    if request.method == "POST":
        if request.form.get("form-name") == "create":
            if not current_user.is_premium():
                flash("Only premium plan can add subdomain", "warning")
                return redirect(request.url)

            if current_user.subdomain_quota <= 0:
                flash(
                    f"You can't create more than {MAX_NB_SUBDOMAIN} subdomains", "error"
                )
                return redirect(request.url)

            subdomain = request.form.get("subdomain").lower().strip()
            domain = request.form.get("domain").lower().strip()

            if len(subdomain) < 3:
                flash("Subdomain must have at least 3 characters", "error")
                return redirect(request.url)

            if re.fullmatch(_SUBDOMAIN_PATTERN, subdomain) is None:
                flash(
                    "Subdomain can only contain lowercase letters, numbers and dashes (-)",
                    "error",
                )
                return redirect(request.url)

            if subdomain.endswith("-"):
                flash("Subdomain can't end with dash (-)", "error")
                return redirect(request.url)

            if domain not in [sl_domain.domain for sl_domain in sl_domains]:
                LOG.e("Domain %s is tampered by %s", domain, current_user)
                flash("Unknown error, refresh the page", "error")
                return redirect(request.url)

            full_domain = f"{subdomain}.{domain}"

            if CustomDomain.get_by(domain=full_domain):
                flash(f"{full_domain} already used", "error")
            elif Mailbox.filter(
                Mailbox.verified.is_(True),
                Mailbox.email.endswith(f"@{full_domain}"),
            ).first():
                flash(f"{full_domain} already used in a SimpleLogin mailbox", "error")
            else:
                try:
                    new_custom_domain = CustomDomain.create(
                        is_sl_subdomain=True,
                        catch_all=True,  # by default catch-all is enabled
                        domain=full_domain,
                        user_id=current_user.id,
                        verified=True,
                        dkim_verified=False,  # wildcard DNS does not work for DKIM
                        spf_verified=True,
                        dmarc_verified=False,  # wildcard DNS does not work for DMARC
                        ownership_verified=True,
                        commit=True,
                    )
                except SubdomainInTrashError:
                    flash(
                        f"{full_domain} has been used before and cannot be reused",
                        "error",
                    )
                else:
                    flash(
                        f"New subdomain {new_custom_domain.domain} is created",
                        "success",
                    )

                    return redirect(
                        url_for(
                            "dashboard.domain_detail",
                            custom_domain_id=new_custom_domain.id,
                        )
                    )

    return render_template(
        "dashboard/subdomain.html",
        sl_domains=sl_domains,
        errors=errors,
        subdomains=subdomains,
    )
Ejemplo n.º 20
0
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.e(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")
        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)
Ejemplo n.º 21
0
 def error_handler(e):
     LOG.e(e)
     if request.path.startswith("/api/"):
         return jsonify(error="Internal error"), 500
     else:
         return render_template("error/500.html"), 500
Ejemplo n.º 22
0
def verify_receipt(receipt_data, user,
                   password) -> Optional[AppleSubscription]:
    """
    Call https://buy.itunes.apple.com/verifyReceipt and create/update AppleSubscription table
    Call the production URL for verifyReceipt first,
        use sandbox URL if receive a 21007 status code.

    Return AppleSubscription object if success

    https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
    """
    LOG.d("start verify_receipt")
    try:
        r = requests.post(_PROD_URL,
                          json={
                              "receipt-data": receipt_data,
                              "password": password
                          })
    except RequestException:
        LOG.w("cannot call Apple server %s", _PROD_URL)
        return None

    if r.status_code >= 500:
        LOG.w("Apple server error, response:%s %s", r, r.content)
        return None

    if r.json() == {"status": 21007}:
        # try sandbox_url
        LOG.w("Use the sandbox url instead")
        r = requests.post(
            _SANDBOX_URL,
            json={
                "receipt-data": receipt_data,
                "password": password
            },
        )

    data = r.json()
    # data has the following format
    # {
    #     "status": 0,
    #     "environment": "Sandbox",
    #     "receipt": {
    #         "receipt_type": "ProductionSandbox",
    #         "adam_id": 0,
    #         "app_item_id": 0,
    #         "bundle_id": "io.simplelogin.ios-app",
    #         "application_version": "2",
    #         "download_id": 0,
    #         "version_external_identifier": 0,
    #         "receipt_creation_date": "2020-04-18 16:36:34 Etc/GMT",
    #         "receipt_creation_date_ms": "1587227794000",
    #         "receipt_creation_date_pst": "2020-04-18 09:36:34 America/Los_Angeles",
    #         "request_date": "2020-04-18 16:46:36 Etc/GMT",
    #         "request_date_ms": "1587228396496",
    #         "request_date_pst": "2020-04-18 09:46:36 America/Los_Angeles",
    #         "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
    #         "original_purchase_date_ms": "1375340400000",
    #         "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
    #         "original_application_version": "1.0",
    #         "in_app": [
    #             {
    #                 "quantity": "1",
    #                 "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #                 "transaction_id": "1000000653584474",
    #                 "original_transaction_id": "1000000653584474",
    #                 "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
    #                 "purchase_date_ms": "1587227262000",
    #                 "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
    #                 "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #                 "original_purchase_date_ms": "1587227264000",
    #                 "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #                 "expires_date": "2020-04-18 16:32:42 Etc/GMT",
    #                 "expires_date_ms": "1587227562000",
    #                 "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
    #                 "web_order_line_item_id": "1000000051847459",
    #                 "is_trial_period": "false",
    #                 "is_in_intro_offer_period": "false",
    #             },
    #             {
    #                 "quantity": "1",
    #                 "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #                 "transaction_id": "1000000653584861",
    #                 "original_transaction_id": "1000000653584474",
    #                 "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
    #                 "purchase_date_ms": "1587227562000",
    #                 "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
    #                 "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #                 "original_purchase_date_ms": "1587227264000",
    #                 "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #                 "expires_date": "2020-04-18 16:37:42 Etc/GMT",
    #                 "expires_date_ms": "1587227862000",
    #                 "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
    #                 "web_order_line_item_id": "1000000051847461",
    #                 "is_trial_period": "false",
    #                 "is_in_intro_offer_period": "false",
    #             },
    #         ],
    #     },
    #     "latest_receipt_info": [
    #         {
    #             "quantity": "1",
    #             "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "transaction_id": "1000000653584474",
    #             "original_transaction_id": "1000000653584474",
    #             "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
    #             "purchase_date_ms": "1587227262000",
    #             "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
    #             "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #             "original_purchase_date_ms": "1587227264000",
    #             "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #             "expires_date": "2020-04-18 16:32:42 Etc/GMT",
    #             "expires_date_ms": "1587227562000",
    #             "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
    #             "web_order_line_item_id": "1000000051847459",
    #             "is_trial_period": "false",
    #             "is_in_intro_offer_period": "false",
    #             "subscription_group_identifier": "20624274",
    #         },
    #         {
    #             "quantity": "1",
    #             "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "transaction_id": "1000000653584861",
    #             "original_transaction_id": "1000000653584474",
    #             "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
    #             "purchase_date_ms": "1587227562000",
    #             "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
    #             "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #             "original_purchase_date_ms": "1587227264000",
    #             "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #             "expires_date": "2020-04-18 16:37:42 Etc/GMT",
    #             "expires_date_ms": "1587227862000",
    #             "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
    #             "web_order_line_item_id": "1000000051847461",
    #             "is_trial_period": "false",
    #             "is_in_intro_offer_period": "false",
    #             "subscription_group_identifier": "20624274",
    #         },
    #         {
    #             "quantity": "1",
    #             "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "transaction_id": "1000000653585235",
    #             "original_transaction_id": "1000000653584474",
    #             "purchase_date": "2020-04-18 16:38:16 Etc/GMT",
    #             "purchase_date_ms": "1587227896000",
    #             "purchase_date_pst": "2020-04-18 09:38:16 America/Los_Angeles",
    #             "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #             "original_purchase_date_ms": "1587227264000",
    #             "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #             "expires_date": "2020-04-18 16:43:16 Etc/GMT",
    #             "expires_date_ms": "1587228196000",
    #             "expires_date_pst": "2020-04-18 09:43:16 America/Los_Angeles",
    #             "web_order_line_item_id": "1000000051847500",
    #             "is_trial_period": "false",
    #             "is_in_intro_offer_period": "false",
    #             "subscription_group_identifier": "20624274",
    #         },
    #         {
    #             "quantity": "1",
    #             "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "transaction_id": "1000000653585760",
    #             "original_transaction_id": "1000000653584474",
    #             "purchase_date": "2020-04-18 16:44:25 Etc/GMT",
    #             "purchase_date_ms": "1587228265000",
    #             "purchase_date_pst": "2020-04-18 09:44:25 America/Los_Angeles",
    #             "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #             "original_purchase_date_ms": "1587227264000",
    #             "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #             "expires_date": "2020-04-18 16:49:25 Etc/GMT",
    #             "expires_date_ms": "1587228565000",
    #             "expires_date_pst": "2020-04-18 09:49:25 America/Los_Angeles",
    #             "web_order_line_item_id": "1000000051847566",
    #             "is_trial_period": "false",
    #             "is_in_intro_offer_period": "false",
    #             "subscription_group_identifier": "20624274",
    #         },
    #     ],
    #     "latest_receipt": "very long string",
    #     "pending_renewal_info": [
    #         {
    #             "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "original_transaction_id": "1000000653584474",
    #             "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #             "auto_renew_status": "1",
    #         }
    #     ],
    # }

    if data["status"] != 0:
        LOG.w(
            "verifyReceipt status !=0, probably invalid receipt. User %s, data %s",
            user,
            data,
        )
        return None

    # each item in data["receipt"]["in_app"] has the following format
    # {
    #     "quantity": "1",
    #     "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
    #     "transaction_id": "1000000653584474",
    #     "original_transaction_id": "1000000653584474",
    #     "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
    #     "purchase_date_ms": "1587227262000",
    #     "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
    #     "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
    #     "original_purchase_date_ms": "1587227264000",
    #     "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
    #     "expires_date": "2020-04-18 16:32:42 Etc/GMT",
    #     "expires_date_ms": "1587227562000",
    #     "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
    #     "web_order_line_item_id": "1000000051847459",
    #     "is_trial_period": "false",
    #     "is_in_intro_offer_period": "false",
    # }
    transactions = data["receipt"]["in_app"]
    if not transactions:
        LOG.w("Empty transactions in data %s", data)
        return None

    latest_transaction = max(transactions,
                             key=lambda t: int(t["expires_date_ms"]))
    original_transaction_id = latest_transaction["original_transaction_id"]
    expires_date = arrow.get(int(latest_transaction["expires_date_ms"]) / 1000)
    plan = (PlanEnum.monthly if latest_transaction["product_id"]
            in (_MONTHLY_PRODUCT_ID,
                _MACAPP_MONTHLY_PRODUCT_ID) else PlanEnum.yearly)

    apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=user.id)

    if apple_sub:
        LOG.d(
            "Update AppleSubscription for user %s, expired at %s, plan %s",
            user,
            expires_date,
            plan,
        )
        apple_sub.receipt_data = receipt_data
        apple_sub.expires_date = expires_date
        apple_sub.original_transaction_id = original_transaction_id
        apple_sub.product_id = latest_transaction["product_id"]
        apple_sub.plan = plan
    else:
        # the same original_transaction_id has been used on another account
        if AppleSubscription.get_by(
                original_transaction_id=original_transaction_id):
            LOG.e("Same Apple Sub has been used before, current user %s", user)
            return None

        LOG.d(
            "Create new AppleSubscription for user %s, expired at %s, plan %s",
            user,
            expires_date,
            plan,
        )
        apple_sub = AppleSubscription.create(
            user_id=user.id,
            receipt_data=receipt_data,
            expires_date=expires_date,
            original_transaction_id=original_transaction_id,
            plan=plan,
            product_id=latest_transaction["product_id"],
        )

    Session.commit()

    return apple_sub
Ejemplo n.º 23
0
    def paddle():
        LOG.d(
            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.e("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.e(
                    "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.d("User %s upgrades!", user)

            Session.commit()

        elif request.form.get(
                "alert_name") == "subscription_payment_succeeded":
            subscription_id = request.form.get("subscription_id")
            LOG.d("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()

                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.w(
                    "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
                Session.commit()

                user = sub.user

                send_email(
                    user.email,
                    "SimpleLogin - your subscription is canceled",
                    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.d(
                    "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

                Session.commit()
            else:
                return "No such subscription", 400
        elif request.form.get("alert_name") == "payment_refunded":
            subscription_id = request.form.get("subscription_id")
            LOG.d("Refund request for subscription %s", subscription_id)

            sub: Subscription = Subscription.get_by(
                subscription_id=subscription_id)

            if sub:
                user = sub.user
                Subscription.delete(sub.id)
                Session.commit()
                LOG.e("%s requests a refund", user)

        return "OK"
Ejemplo n.º 24
0
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
Ejemplo n.º 25
0
                elif job.name == JOB_DELETE_DOMAIN:
                    custom_domain_id = job.payload.get("custom_domain_id")
                    custom_domain = CustomDomain.get(custom_domain_id)
                    if not custom_domain:
                        continue

                    domain_name = custom_domain.domain
                    user = custom_domain.user

                    CustomDomain.delete(custom_domain.id)
                    Session.commit()

                    LOG.d("Domain %s deleted", domain_name)

                    send_email(
                        user.email,
                        f"Your domain {domain_name} has been deleted",
                        f"""Domain {domain_name} along with its aliases are deleted successfully.

Regards,
SimpleLogin team.
""",
                        retries=3,
                    )

                else:
                    LOG.e("Unknown job name %s", job.name)

            time.sleep(10)
Ejemplo n.º 26
0
def sanity_check():
    LOG.d("sanitize user email")
    for user in User.filter_by(activated=True).all():
        if sanitize_email(user.email) != user.email:
            LOG.e("%s does not have sanitized email", user)

    LOG.d("sanitize alias address & name")
    sanitize_alias_address_name()

    LOG.d("sanity contact address")
    contact_email_sanity_date = arrow.get("2021-01-12")
    for contact in Contact.yield_per_query():
        if sanitize_email(contact.reply_email) != contact.reply_email:
            LOG.e("Contact %s reply-email not sanitized", contact)

        if (sanitize_email(contact.website_email,
                           not_lower=True) != contact.website_email
                and contact.created_at > contact_email_sanity_date):
            LOG.e("Contact %s website-email not sanitized", contact)

        if not contact.invalid_email and not is_valid_email(
                contact.website_email):
            LOG.e("%s invalid email", contact)
            contact.invalid_email = True
    Session.commit()

    LOG.d("sanitize mailbox address")
    for mailbox in Mailbox.yield_per_query():
        if sanitize_email(mailbox.email) != mailbox.email:
            LOG.e("Mailbox %s address not sanitized", mailbox)

    LOG.d("normalize reverse alias")
    for contact in Contact.yield_per_query():
        if normalize_reply_email(contact.reply_email) != contact.reply_email:
            LOG.e(
                "Contact %s reply email is not normalized %s",
                contact,
                contact.reply_email,
            )

    LOG.d("clean domain name")
    for domain in CustomDomain.yield_per_query():
        if domain.name and "\n" in domain.name:
            LOG.e("Domain %s name contain linebreak %s", domain, domain.name)

    LOG.d("migrate domain trash if needed")
    migrate_domain_trash()

    LOG.d("fix custom domain for alias")
    set_custom_domain_for_alias()

    LOG.d("check mailbox valid domain")
    check_mailbox_valid_domain()

    LOG.d(
        """check if there's an email that starts with "\u200f" (right-to-left mark (RLM))"""
    )
    for contact in (Contact.yield_per_query().filter(
            Contact.website_email.startswith("\u200f")).all()):
        contact.website_email = contact.website_email.replace("\u200f", "")
        LOG.e("remove right-to-left mark (RLM) from %s", contact)
    Session.commit()

    LOG.d("Finish sanity check")
Ejemplo n.º 27
0
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))
Ejemplo n.º 28
0
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,
    )