Beispiel #1
0
def encrypt_file(data: BytesIO, fingerprint: str) -> str:
    LOG.d("encrypt for %s", fingerprint)
    mem_usage = memory_usage(-1, interval=1, timeout=1)[0]
    LOG.d("mem_usage %s", mem_usage)

    # todo
    if mem_usage > 300:
        LOG.error("Force exit")
        hard_exit()

    r = gpg.encrypt_file(data, fingerprint, always_trust=True)
    if not r.ok:
        # maybe the fingerprint is not loaded on this host, try to load it
        mailbox = Mailbox.get_by(pgp_finger_print=fingerprint)
        if mailbox:
            LOG.d("(re-)load public key for %s", mailbox)
            load_public_key(mailbox.pgp_public_key)

            LOG.d("retry to encrypt")
            data.seek(0)
            r = gpg.encrypt_file(data, fingerprint, always_trust=True)

        if not r.ok:
            raise PGPException(f"Cannot encrypt, status: {r.status}")

    return str(r)
Beispiel #2
0
def delete_alias(alias: Alias, user: User):
    Alias.delete(alias.id)
    db.session.commit()

    # save deleted alias to either global or domain trash
    if alias.custom_domain_id:
        try:
            DomainDeletedAlias.create(user_id=user.id,
                                      email=alias.email,
                                      domain_id=alias.custom_domain_id)
            db.session.commit()
        except IntegrityError:
            LOG.error(
                "alias %s domain %s has been added before to DeletedAlias",
                alias.email,
                alias.custom_domain_id,
            )
            db.session.rollback()
    else:
        try:
            DeletedAlias.create(email=alias.email)
            db.session.commit()
        except IntegrityError:
            LOG.error("alias %s has been added before to DeletedAlias",
                      alias.email)
            db.session.rollback()
Beispiel #3
0
def send_email_with_rate_control(
    user: User,
    alert_type: str,
    to_email: str,
    subject,
    plaintext,
    html=None,
    bounced_email: Optional[Message] = None,
    max_alert_24h=MAX_ALERT_24H,
) -> bool:
    """Same as send_email with rate control over alert_type.
    For now no more than _MAX_ALERT_24h alert can be sent in the last 24h

    Return true if the email is sent, otherwise False
    """
    to_email = to_email.lower().strip()
    one_day_ago = arrow.now().shift(days=-1)
    nb_alert = (SentAlert.query.filter_by(
        alert_type=alert_type,
        to_email=to_email).filter(SentAlert.created_at > one_day_ago).count())

    if nb_alert >= max_alert_24h:
        LOG.error(
            "%s emails were sent to %s in the last 24h, alert type %s",
            nb_alert,
            to_email,
            alert_type,
        )
        return False

    SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email)
    db.session.commit()
    send_email(to_email, subject, plaintext, html, bounced_email)
    return True
Beispiel #4
0
def load_pgp_public_keys():
    """Load PGP public key to keyring"""
    for mailbox in Mailbox.query.filter(Mailbox.pgp_public_key != 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.error("fingerprint %s different for mailbox %s", fingerprint,
                      mailbox)
            mailbox.pgp_finger_print = fingerprint
    db.session.commit()

    for contact in Contact.query.filter(Contact.pgp_public_key != 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.error("fingerprint %s different for contact %s", fingerprint,
                      contact)
            contact.pgp_finger_print = fingerprint

    db.session.commit()

    LOG.d("Finish load_pgp_public_keys")
Beispiel #5
0
    def next_bill_date(self) -> str:
        sub: Subscription = self.get_subscription()
        if sub:
            return sub.next_bill_date.strftime("%Y-%m-%d")

        LOG.error(
            f"next_bill_date() should be called only on user with active subscription. User {self}"
        )
        return ""
Beispiel #6
0
def check_zendesk_response_status(response_code: int) -> bool:
    if response_code != 201:
        if response_code in (401, 422):
            LOG.error("Could not authenticate to Zendesk")
        else:
            LOG.error("Problem with the Zendesk request. Status {}".format(
                response_code))
        return False
    return True
Beispiel #7
0
def get_or_create_contact(contact_from_header: str, mail_from: str,
                          alias: Alias) -> Contact:
    """
    contact_from_header is the RFC 2047 format FROM header
    """
    # contact_from_header can be None, use mail_from in this case instead
    contact_from_header = contact_from_header or mail_from

    # force convert header to string, sometimes contact_from_header is Header object
    contact_from_header = str(contact_from_header)

    contact_name, contact_email = parseaddr_unicode(contact_from_header)
    if not contact_email:
        # From header is wrongly formatted, try with mail_from
        LOG.warning("From header is empty, parse mail_from %s %s", mail_from,
                    alias)
        contact_name, contact_email = parseaddr_unicode(mail_from)
        if not contact_email:
            LOG.error(
                "Cannot parse contact from from_header:%s, mail_from:%s",
                contact_from_header,
                mail_from,
            )

    contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
    if contact:
        if contact.name != contact_name:
            LOG.d(
                "Update contact %s name %s to %s",
                contact,
                contact.name,
                contact_name,
            )
            contact.name = contact_name
            db.session.commit()
    else:
        LOG.debug(
            "create contact for alias %s and contact %s",
            alias,
            contact_from_header,
        )

        reply_email = generate_reply_email()

        contact = Contact.create(
            user_id=alias.user_id,
            alias_id=alias.id,
            website_email=contact_email,
            name=contact_name,
            reply_email=reply_email,
        )
        db.session.commit()

    return contact
Beispiel #8
0
    def delete(cls, obj_id):
        # Put all aliases belonging to this domain to global trash
        try:
            for alias in Alias.query.filter_by(custom_domain_id=obj_id):
                DeletedAlias.create(email=alias.email)
            db.session.commit()
        except IntegrityError:
            LOG.error("Some aliases have been added before to DeletedAlias")
            db.session.rollback()

        cls.query.filter(cls.id == obj_id).delete()
        db.session.commit()
Beispiel #9
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash("ony premium user can choose custom alias", "warning")
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()]
    # List of (is_custom_domain, alias-suffix)
    suffixes = []

    # put custom domain first
    for alias_domain in user_custom_domains:
        suffixes.append((True, "@" + alias_domain))

    # then default domain
    for domain in ALIAS_DOMAINS:
        suffixes.append(
            (
                False,
                ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain,
            )
        )

    if request.method == "POST":
        alias_prefix = request.form.get("prefix")
        alias_suffix = request.form.get("suffix")

        if verify_prefix_suffix(
            current_user, alias_prefix, alias_suffix, user_custom_domains
        ):
            full_alias = alias_prefix + alias_suffix
            if GenEmail.get_by(email=full_alias):
                LOG.d("full alias already used %s", full_alias)
                flash(
                    f"Alias {full_alias} already exists, please choose another one",
                    "warning",
                )
            else:
                gen_email = GenEmail.create(user_id=current_user.id, email=full_alias)
                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id

                return redirect(url_for("dashboard.index"))
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template("dashboard/custom_alias.html", **locals())
Beispiel #10
0
    def delete(cls, obj_id):
        # Put all aliases belonging to this directory to global trash
        try:
            for alias in Alias.query.filter_by(directory_id=obj_id):
                DeletedAlias.create(email=alias.email)
            db.session.commit()
        # this can happen when a previously deleted alias is re-created via catch-all or directory feature
        except IntegrityError:
            LOG.error("Some aliases have been added before to DeletedAlias")
            db.session.rollback()

        cls.query.filter(cls.id == obj_id).delete()
        db.session.commit()
Beispiel #11
0
def delete_alias(alias: Alias, user: User):
    email = alias.email
    Alias.delete(alias.id)
    db.session.commit()

    # try to save deleted alias
    try:
        DeletedAlias.create(email=email)
        db.session.commit()
    # this can happen when a previously deleted alias is re-created via catch-all or directory feature
    except IntegrityError:
        LOG.error("alias %s has been added before to DeletedAlias", email)
        db.session.rollback()
Beispiel #12
0
def verify_prefix_suffix(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()]
    alias_prefix = alias_prefix.strip()
    alias_prefix = convert_to_id(alias_prefix)

    # make sure alias_suffix is either [email protected] or @my-domain.com
    alias_suffix = alias_suffix.strip()
    if alias_suffix.startswith("@"):
        alias_domain = alias_suffix[1:]
        # alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, one of the default ALIAS_DOMAINS
        if DISABLE_ALIAS_SUFFIX:
            if (
                alias_domain not in user_custom_domains
                and alias_domain not in ALIAS_DOMAINS
            ):
                LOG.error("wrong alias suffix %s, user %s", alias_suffix, user)
                return False
        else:
            if alias_domain not in user_custom_domains:
                LOG.error("wrong alias suffix %s, user %s", alias_suffix, user)
                return False
    else:
        if not alias_suffix.startswith("."):
            LOG.error("User %s submits a wrong alias suffix %s", user, alias_suffix)
            return False

        full_alias = alias_prefix + alias_suffix
        if not email_belongs_to_alias_domains(full_alias):
            LOG.error(
                "Alias suffix should end with one of the alias domains %s",
                user,
                alias_suffix,
            )
            return False

        random_word_part = alias_suffix[1 : alias_suffix.find("@")]
        if not word_exist(random_word_part):
            LOG.error(
                "alias suffix %s needs to start with a random word, user %s",
                alias_suffix,
                user,
            )
            return False

    return True
Beispiel #13
0
def load_pgp_public_keys(app):
    """Load PGP public key to keyring"""
    with app.app_context():
        for mailbox in Mailbox.query.filter(
                Mailbox.pgp_public_key != 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.error("fingerprint %s different for mailbox %s",
                          fingerprint, mailbox)
                mailbox.pgp_finger_print = fingerprint

        db.session.commit()
Beispiel #14
0
def cancel_subscription(subscription_id: int) -> 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.error(
            f"cannot cancel subscription {subscription_id}, paddle response: {res}"
        )

    return res["success"]
Beispiel #15
0
def verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains) -> bool:
    """verify if user could create an alias with the given prefix and suffix"""
    alias_prefix = alias_prefix.strip()
    alias_prefix = convert_to_id(alias_prefix)
    if not alias_prefix:  # should be caught on frontend
        return False

    # make sure alias_suffix is either [email protected] or @my-domain.com
    alias_suffix = alias_suffix.strip()
    if alias_suffix.startswith("@"):
        alias_domain = alias_suffix[1:]
        # alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, EMAIL_DOMAIN
        if DISABLE_ALIAS_SUFFIX:
            if alias_domain not in user_custom_domains and alias_domain != EMAIL_DOMAIN:
                LOG.error("wrong alias suffix %s, user %s", alias_suffix, user)
                return False
        else:
            if alias_domain not in user_custom_domains:
                LOG.error("wrong alias suffix %s, user %s", alias_suffix, user)
                return False
    else:
        if not alias_suffix.startswith("."):
            LOG.error("User %s submits a wrong alias suffix %s", user, alias_suffix)
            return False
        if not alias_suffix.endswith(EMAIL_DOMAIN):
            LOG.error(
                "Alias suffix should end with default alias domain %s",
                user,
                alias_suffix,
            )
            return False

        random_word_part = alias_suffix[1 : alias_suffix.find("@")]
        if not word_exist(random_word_part):
            LOG.error(
                "alias suffix %s needs to start with a random word, user %s",
                alias_suffix,
                user,
            )
            return False

    return True
Beispiel #16
0
    def delete(cls, obj_id):
        # Put all aliases belonging to this mailbox to global trash
        try:
            for alias in Alias.query.filter_by(mailbox_id=obj_id):
                # special handling for alias that has several mailboxes and has mailbox_id=obj_id
                if len(alias.mailboxes) > 1:
                    # use the first mailbox found in alias._mailboxes
                    first_mb = alias._mailboxes[0]
                    alias.mailbox_id = first_mb.id
                    alias._mailboxes.remove(first_mb)
                else:
                    # only put aliases that have mailbox as a single mailbox into trash
                    DeletedAlias.create(email=alias.email)
                db.session.commit()
        # this can happen when a previously deleted alias is re-created via catch-all or directory feature
        except IntegrityError:
            LOG.error("Some aliases have been added before to DeletedAlias")
            db.session.rollback()

        cls.query.filter(cls.id == obj_id).delete()
        db.session.commit()
Beispiel #17
0
def sanity_check():
    """
    #TODO: investigate why DNS sometimes not working
    Different sanity checks
    - detect if there's mailbox that's using a invalid domain
    """
    for mailbox in Mailbox.filter_by(verified=True).all():
        # hack to not query DNS too often
        sleep(1)
        if not email_domain_can_be_used_as_mailbox(mailbox.email):
            mailbox.nb_failed_checks += 1
            # alert if too much fail
            if mailbox.nb_failed_checks > 10:
                log_func = LOG.error
            else:
                log_func = LOG.warning

            log_func(
                "issue with mailbox %s domain. #alias %s, nb email log %s",
                mailbox,
                mailbox.nb_alias(),
                mailbox.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 user.email.lower() != user.email:
            LOG.error("%s does not have lowercase email", user)

    for mailbox in Mailbox.filter_by(verified=True).all():
        if mailbox.email.lower() != mailbox.email:
            LOG.error("%s does not have lowercase email", mailbox)

    LOG.d("Finish sanity check")
Beispiel #18
0
    def handle_reply(self, envelope, smtp: SMTP, msg: Message) -> str:
        reply_email = envelope.rcpt_tos[0].lower()

        # reply_email must end with EMAIL_DOMAIN
        if not reply_email.endswith(EMAIL_DOMAIN):
            LOG.error(f"Reply email {reply_email} has wrong domain")
            return "550 wrong reply email"

        forward_email = ForwardEmail.get_by(reply_email=reply_email)
        alias: str = forward_email.gen_email.email
        alias_domain = alias[alias.find("@") + 1:]

        # alias must end with one of the ALIAS_DOMAINS or custom-domain
        if not email_belongs_to_alias_domains(alias):
            if not CustomDomain.get_by(domain=alias_domain):
                return "550 alias unknown by SimpleLogin"

        user_email = forward_email.gen_email.user.email
        if envelope.mail_from.lower() != user_email.lower():
            LOG.error(
                f"Reply email can only be used by user email. Actual mail_from: %s. msg from header: %s, User email %s. reply_email %s",
                envelope.mail_from,
                msg["From"],
                user_email,
                reply_email,
            )

            send_reply_alias_must_use_personal_email(
                forward_email.gen_email.user,
                forward_email.gen_email.email,
                envelope.mail_from,
            )

            send_email(
                envelope.mail_from,
                f"Your email ({envelope.mail_from}) is not allowed to send email to {reply_email}",
                "",
                "",
            )

            return "550 ignored"

        delete_header(msg, "DKIM-Signature")

        # the email comes from alias
        msg.replace_header("From", alias)

        # some email providers like ProtonMail adds automatically the Reply-To field
        # make sure to delete it
        delete_header(msg, "Reply-To")

        msg.replace_header("To", forward_email.website_email)

        # add List-Unsubscribe header
        unsubscribe_link = f"{URL}/dashboard/unsubscribe/{forward_email.gen_email_id}"
        add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
        add_or_replace_header(msg, "List-Unsubscribe-Post",
                              "List-Unsubscribe=One-Click")

        # Received-SPF is injected by postfix-policyd-spf-python can reveal user original email
        delete_header(msg, "Received-SPF")

        LOG.d(
            "send email from %s to %s, mail_options:%s,rcpt_options:%s",
            alias,
            forward_email.website_email,
            envelope.mail_options,
            envelope.rcpt_options,
        )

        if alias_domain in ALIAS_DOMAINS:
            add_dkim_signature(msg, alias_domain)
        # add DKIM-Signature for custom-domain alias
        else:
            custom_domain: CustomDomain = CustomDomain.get_by(
                domain=alias_domain)
            if custom_domain.dkim_verified:
                add_dkim_signature(msg, alias_domain)

        msg_raw = msg.as_string().encode()
        smtp.sendmail(
            alias,
            forward_email.website_email,
            msg_raw,
            envelope.mail_options,
            envelope.rcpt_options,
        )

        ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True)
        db.session.commit()

        return "250 Message accepted for delivery"
Beispiel #19
0
    def handle_forward(self, envelope, smtp: SMTP, msg: Message) -> str:
        """return *status_code message*"""
        alias = envelope.rcpt_tos[0].lower()  # alias@SL

        gen_email = GenEmail.get_by(email=alias)
        if not gen_email:
            LOG.d(
                "alias %s not exist. Try to see if it can be created on the fly",
                alias)

            # try to see if alias could be created on-the-fly
            on_the_fly = False

            # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
            if email_belongs_to_alias_domains(alias):
                if "/" in alias or "+" in alias or "#" in alias:
                    if "/" in alias:
                        sep = "/"
                    elif "+" in alias:
                        sep = "+"
                    else:
                        sep = "#"

                    directory_name = alias[:alias.find(sep)]
                    LOG.d("directory_name %s", directory_name)

                    directory = Directory.get_by(name=directory_name)

                    # Only premium user can use the directory feature
                    if directory:
                        dir_user = directory.user
                        if dir_user.is_premium():
                            LOG.d("create alias %s for directory %s", alias,
                                  directory)
                            on_the_fly = True

                            gen_email = GenEmail.create(
                                email=alias,
                                user_id=directory.user_id,
                                directory_id=directory.id,
                            )
                            db.session.commit()
                        else:
                            LOG.error(
                                "User %s is not premium anymore and cannot create alias with directory",
                                dir_user,
                            )
                            send_cannot_create_directory_alias(
                                dir_user, alias, directory_name)

            # try to create alias on-the-fly with custom-domain catch-all feature
            # check if alias is custom-domain alias and if the custom-domain has catch-all enabled
            if not on_the_fly:
                alias_domain = get_email_domain_part(alias)
                custom_domain = CustomDomain.get_by(domain=alias_domain)

                # Only premium user can continue using the catch-all feature
                if custom_domain and custom_domain.catch_all:
                    domain_user = custom_domain.user
                    if domain_user.is_premium():
                        LOG.d("create alias %s for domain %s", alias,
                              custom_domain)
                        on_the_fly = True

                        gen_email = GenEmail.create(
                            email=alias,
                            user_id=custom_domain.user_id,
                            custom_domain_id=custom_domain.id,
                            automatic_creation=True,
                        )
                        db.session.commit()
                    else:
                        LOG.error(
                            "User %s is not premium anymore and cannot create alias with domain %s",
                            domain_user,
                            alias_domain,
                        )
                        send_cannot_create_domain_alias(
                            domain_user, alias, alias_domain)

            if not on_the_fly:
                LOG.d("alias %s cannot be created on-the-fly, return 510",
                      alias)
                return "510 Email not exist"

        user_email = gen_email.user.email

        website_email = get_email_part(msg["From"])

        forward_email = ForwardEmail.get_by(gen_email_id=gen_email.id,
                                            website_email=website_email)
        if not forward_email:
            LOG.debug(
                "create forward email for alias %s and website email %s",
                alias,
                website_email,
            )

            # generate a reply_email, make sure it is unique
            # not use while to avoid infinite loop
            for _ in range(1000):
                reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
                if not ForwardEmail.get_by(reply_email=reply_email):
                    break

            forward_email = ForwardEmail.create(
                gen_email_id=gen_email.id,
                website_email=website_email,
                website_from=msg["From"],
                reply_email=reply_email,
            )
            db.session.commit()

        forward_log = ForwardEmailLog.create(forward_id=forward_email.id)

        if gen_email.enabled:
            # add custom header
            add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")

            # remove reply-to header if present
            delete_header(msg, "Reply-To")

            # change the from header so the sender comes from @SL
            # so it can pass DMARC check
            # replace the email part in from: header
            from_header = (get_email_name(msg["From"]) + " - " +
                           website_email.replace("@", " at ") +
                           f" <{forward_email.reply_email}>")
            msg.replace_header("From", from_header)
            LOG.d("new from header:%s", from_header)

            # add List-Unsubscribe header
            unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
            add_or_replace_header(msg, "List-Unsubscribe",
                                  f"<{unsubscribe_link}>")
            add_or_replace_header(msg, "List-Unsubscribe-Post",
                                  "List-Unsubscribe=One-Click")

            add_dkim_signature(msg, EMAIL_DOMAIN)

            LOG.d(
                "Forward mail from %s to %s, mail_options %s, rcpt_options %s ",
                website_email,
                user_email,
                envelope.mail_options,
                envelope.rcpt_options,
            )

            # smtp.send_message has UnicodeEncodeErroremail issue
            # encode message raw directly instead
            msg_raw = msg.as_string().encode()
            smtp.sendmail(
                forward_email.reply_email,
                user_email,
                msg_raw,
                envelope.mail_options,
                envelope.rcpt_options,
            )
        else:
            LOG.d("%s is disabled, do not forward", gen_email)
            forward_log.blocked = True

        db.session.commit()
        return "250 Message accepted for delivery"
Beispiel #20
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()]
    # List of (is_custom_domain, alias-suffix)
    suffixes = []

    # put custom domain first
    for alias_domain in user_custom_domains:
        suffixes.append((True, "@" + alias_domain))

    # then default domain
    for domain in ALIAS_DOMAINS:
        suffixes.append(
            (
                False,
                ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain,
            )
        )

    if request.method == "POST":
        alias_prefix = request.form.get("prefix")
        alias_suffix = request.form.get("suffix")
        alias_note = request.form.get("note")

        if verify_prefix_suffix(
            current_user, alias_prefix, alias_suffix, user_custom_domains
        ):
            full_alias = alias_prefix + alias_suffix

            if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
                email=full_alias
            ):
                LOG.d("full alias already used %s", full_alias)
                flash(
                    f"Alias {full_alias} already exists, please choose another one",
                    "warning",
                )
            else:
                gen_email = GenEmail.create(
                    user_id=current_user.id, email=full_alias, note=alias_note
                )

                # get the custom_domain_id if alias is created with a custom domain
                alias_domain = get_email_domain_part(full_alias)
                custom_domain = CustomDomain.get_by(domain=alias_domain)
                if custom_domain:
                    gen_email.custom_domain_id = custom_domain.id

                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_gen_email_id=gen_email.id)
                )
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template("dashboard/custom_alias.html", **locals())
Beispiel #21
0
    def paddle():
        LOG.debug(
            "paddle callback %s %s %s %s %s",
            request.form.get("alert_name"),
            request.form.get("email"),
            request.form.get("customer_name"),
            request.form.get("subscription_id"),
            request.form.get("subscription_plan_id"),
        )

        # make sure the request comes from Paddle
        if not paddle_utils.verify_incoming_request(dict(request.form)):
            LOG.error("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
            user_email = request.form.get("email")
            user = User.get_by(email=user_email)

            if (int(request.form.get("subscription_plan_id")) ==
                    PADDLE_MONTHLY_PRODUCT_ID):
                plan = PlanEnum.monthly
            else:
                plan = PlanEnum.yearly

            sub = Subscription.get_by(user_id=user.id)

            if not sub:
                LOG.d("create a new sub")
                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("update existing sub %s", sub)
                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

            LOG.debug("User %s upgrades!", user)

            db.session.commit()

        elif request.form.get("alert_name") == "subscription_updated":
            subscription_id = request.form.get("subscription_id")
            LOG.debug("Update subscription %s", subscription_id)

            sub: Subscription = Subscription.get_by(
                subscription_id=subscription_id)
            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")
            LOG.error("Cancel subscription %s", subscription_id)

            sub: Subscription = Subscription.get_by(
                subscription_id=subscription_id)
            if sub:
                sub.cancelled = True
                db.session.commit()

        return "OK"
Beispiel #22
0

if __name__ == "__main__":
    while True:
        # run a job 1h earlier or later is not a big deal ...
        min_dt = arrow.now().shift(hours=-1)
        max_dt = arrow.now().shift(hours=1)

        app = new_app()

        with app.app_context():
            for job in Job.query.filter(Job.taken == False,
                                        Job.run_at > min_dt,
                                        Job.run_at <= max_dt).all():
                LOG.d("Take job %s", job)

                # mark the job as taken, whether it will be executed successfully or not
                job.taken = True
                db.session.commit()

                if job.name == JOB_ONBOARDING_1:
                    user_id = job.payload.get("user_id")
                    user = User.get(user_id)

                    LOG.d("run onboarding_1 for user %s", user)
                    onboarding_1(user)
                else:
                    LOG.error("Unknown job name %s", job.name)

        time.sleep(10)
Beispiel #23
0
def spf_pass(
    ip: str,
    envelope,
    mailbox: Mailbox,
    user: User,
    alias: Alias,
    contact_email: str,
    msg: Message,
) -> bool:
    if ip:
        LOG.d("Enforce SPF")
        try:
            r = spf.check2(i=ip, s=envelope.mail_from.lower(), h=None)
        except Exception:
            LOG.error("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.error(
                    "SPF fail for mailbox %s, reason %s, failed IP %s",
                    mailbox.email,
                    r[0],
                    ip,
                )
                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",
                        name=user.name,
                        alias=alias.email,
                        ip=ip,
                        mailbox_url=URL +
                        f"/dashboard/mailbox/{mailbox.id}#spf",
                        to_email=contact_email,
                        subject=msg["Subject"],
                        time=arrow.now(),
                    ),
                    render(
                        "transactional/spf-fail.html",
                        name=user.name,
                        alias=alias.email,
                        ip=ip,
                        mailbox_url=URL +
                        f"/dashboard/mailbox/{mailbox.id}#spf",
                        to_email=contact_email,
                        subject=msg["Subject"],
                        time=arrow.now(),
                    ),
                )
                return False

    else:
        LOG.warning(
            "Could not find %s header %s -> %s",
            _IP_HEADER,
            mailbox.email,
            contact_email,
        )

    return True
Beispiel #24
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
    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):
                        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))
Beispiel #25
0
def custom_alias():
    # check if user has the right to create custom alias
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash("ony premium user can choose custom alias", "warning")
        return redirect(url_for("dashboard.index"))

    error = ""

    if request.method == "POST":
        if request.form.get("form-name") == "non-custom-domain-name":
            email_prefix = request.form.get("email-prefix")
            email_prefix = convert_to_id(email_prefix)
            email_suffix = request.form.get("email-suffix")

            if not email_prefix:
                error = "alias prefix cannot be empty"
            else:
                full_email = f"{email_prefix}.{email_suffix}@{EMAIL_DOMAIN}"
                # check if email already exists
                if GenEmail.get_by(email=full_email) or DeletedAlias.get_by(
                        email=full_email):
                    error = "email already chosen, please choose another one"
                else:
                    # create the new alias
                    LOG.d("create custom alias %s for user %s", full_email,
                          current_user)
                    gen_email = GenEmail.create(email=full_email,
                                                user_id=current_user.id)
                    db.session.commit()

                    flash(f"Alias {full_email} has been created", "success")
                    session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id

                    return redirect(url_for("dashboard.index"))
        elif request.form.get("form-name") == "custom-domain-name":
            custom_domain_id = request.form.get("custom-domain-id")
            email = request.form.get("email")

            custom_domain = CustomDomain.get(custom_domain_id)

            if not custom_domain:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(url_for("dashboard.custom_alias"))
            elif custom_domain.user_id != current_user.id:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(url_for("dashboard.custom_alias"))
            elif not custom_domain.verified:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(url_for("dashboard.custom_alias"))

            full_email = f"{email}@{custom_domain.domain}"

            if GenEmail.get_by(email=full_email):
                error = f"{full_email} already exist, please choose another one"
            else:
                LOG.d(
                    "create custom alias %s for custom domain %s",
                    full_email,
                    custom_domain.domain,
                )
                gen_email = GenEmail.create(
                    email=full_email,
                    user_id=current_user.id,
                    custom_domain_id=custom_domain.id,
                )
                db.session.commit()
                flash(f"Alias {full_email} has been created", "success")

                session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id
                return redirect(url_for("dashboard.index"))

    email_suffix = random_word()
    return render_template(
        "dashboard/custom_alias.html",
        error=error,
        email_suffix=email_suffix,
        EMAIL_DOMAIN=EMAIL_DOMAIN,
        custom_domains=current_user.verified_custom_domains(),
    )
Beispiel #26
0
def custom_alias():
    # check if user has not exceeded the alias quota
    if not current_user.can_create_new_alias():
        # notify admin
        LOG.error("user %s tries to create custom alias", current_user)
        flash(
            "You have reached free plan limit, please upgrade to create new aliases",
            "warning",
        )
        return redirect(url_for("dashboard.index"))

    user_custom_domains = [
        cd.domain for cd in current_user.verified_custom_domains()
    ]
    # List of (is_custom_domain, alias-suffix, time-signed alias-suffix)
    suffixes = available_suffixes(current_user)

    mailboxes = current_user.mailboxes()

    if request.method == "POST":
        alias_prefix = request.form.get("prefix").strip().lower()
        signed_suffix = request.form.get("suffix")
        mailbox_ids = request.form.getlist("mailboxes")
        alias_note = request.form.get("note")

        # check if mailbox is not tempered with
        mailboxes = []
        for mailbox_id in mailbox_ids:
            mailbox = Mailbox.get(mailbox_id)
            if (not mailbox or mailbox.user_id != current_user.id
                    or not mailbox.verified):
                flash("Something went wrong, please retry", "warning")
                return redirect(url_for("dashboard.custom_alias"))
            mailboxes.append(mailbox)

        if not mailboxes:
            flash("At least one mailbox must be selected", "error")
            return redirect(url_for("dashboard.custom_alias"))

        # hypothesis: user will click on the button in the 600 secs
        try:
            alias_suffix = signer.unsign(signed_suffix, max_age=600).decode()
        except SignatureExpired:
            LOG.warning("Alias creation time expired for %s", current_user)
            flash("Alias creation time is expired, please retry", "warning")
            return redirect(url_for("dashboard.custom_alias"))
        except Exception:
            LOG.error("Alias suffix is tampered, user %s", current_user)
            flash("Unknown error, refresh the page", "error")
            return redirect(url_for("dashboard.custom_alias"))

        if verify_prefix_suffix(current_user, alias_prefix, alias_suffix):
            full_alias = alias_prefix + alias_suffix

            if (Alias.get_by(email=full_alias)
                    or DeletedAlias.get_by(email=full_alias)
                    or DomainDeletedAlias.get_by(email=full_alias)):
                LOG.d("full alias already used %s", full_alias)
                flash(
                    f"Alias {full_alias} already exists, please choose another one",
                    "warning",
                )
            else:
                custom_domain_id = None
                # get the custom_domain_id if alias is created with a custom domain
                if alias_suffix.startswith("@"):
                    alias_domain = alias_suffix[1:]
                    domain = CustomDomain.get_by(domain=alias_domain)

                    # check if the alias is currently in the domain trash
                    if domain and DomainDeletedAlias.get_by(
                            domain_id=domain.id, email=full_alias):
                        flash(
                            f"Alias {full_alias} is currently in the {domain.domain} trash. "
                            f"Please remove it from the trash in order to re-create it.",
                            "warning",
                        )
                        return redirect(url_for("dashboard.custom_alias"))

                    if domain:
                        custom_domain_id = domain.id

                alias = Alias.create(
                    user_id=current_user.id,
                    email=full_alias,
                    note=alias_note,
                    mailbox_id=mailboxes[0].id,
                    custom_domain_id=custom_domain_id,
                )
                db.session.flush()

                for i in range(1, len(mailboxes)):
                    AliasMailbox.create(
                        alias_id=alias.id,
                        mailbox_id=mailboxes[i].id,
                    )

                db.session.commit()
                flash(f"Alias {full_alias} has been created", "success")

                return redirect(
                    url_for("dashboard.index", highlight_alias_id=alias.id))
        # only happen if the request has been "hacked"
        else:
            flash("something went wrong", "warning")

    return render_template(
        "dashboard/custom_alias.html",
        user_custom_domains=user_custom_domains,
        suffixes=suffixes,
        mailboxes=mailboxes,
    )
Beispiel #27
0
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
    address = alias.email
    email_log: EmailLog = EmailLog.create(contact_id=contact.id,
                                          bounced=True,
                                          user_id=contact.user_id)
    db.session.commit()

    nb_bounced = EmailLog.filter_by(contact_id=contact.id,
                                    bounced=True).count()
    disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"

    # <<< Store the bounced email >>>
    # generate a name for the email
    random_name = str(uuid.uuid4())

    full_report_path = f"refused-emails/full-{random_name}.eml"
    s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()),
                                 random_name)

    orig_msg = get_orig_message_from_bounce(msg)
    if not orig_msg:
        LOG.error(
            "Cannot parse original message from bounce message %s %s %s %s",
            alias,
            user,
            contact,
            full_report_path,
        )
        return

    file_path = f"refused-emails/{random_name}.eml"
    s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()),
                                 random_name)
    # <<< END Store the bounced email >>>

    mailbox_id = int(orig_msg[_MAILBOX_ID_HEADER])
    mailbox = Mailbox.get(mailbox_id)
    if not mailbox or mailbox.user_id != user.id:
        LOG.error(
            "Tampered message mailbox_id %s, %s, %s, %s %s",
            mailbox_id,
            user,
            alias,
            contact,
            full_report_path,
        )
        return

    refused_email = RefusedEmail.create(path=file_path,
                                        full_report_path=full_report_path,
                                        user_id=user.id)
    db.session.flush()

    email_log.refused_email_id = refused_email.id
    email_log.bounced_mailbox_id = mailbox.id
    db.session.commit()

    LOG.d("Create refused email %s", refused_email)

    refused_email_url = (URL + f"/dashboard/refused_email?highlight_id=" +
                         str(email_log.id))

    # inform user if this is the first bounced email
    if nb_bounced == 1:
        LOG.d(
            "Inform user %s about bounced email sent by %s to alias %s",
            user,
            contact.website_email,
            address,
        )
        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            # use user mail here as only user is authenticated to see the refused email
            user.email,
            f"Email from {contact.website_email} to {address} cannot be delivered to your inbox",
            render(
                "transactional/bounced-email.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/bounced-email.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            # cannot include bounce email as it can contain spammy text
            # bounced_email=msg,
        )
    # disable the alias the second time email is bounced
    elif nb_bounced >= 2:
        LOG.d(
            "Bounce happens again with alias %s from %s. Disable alias now ",
            address,
            contact.website_email,
        )
        alias.enabled = False
        db.session.commit()

        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            # use user mail here as only user is authenticated to see the refused email
            user.email,
            f"Alias {address} has been disabled due to second undelivered email from {contact.website_email}",
            render(
                "transactional/automatic-disable-alias.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/automatic-disable-alias.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            # cannot include bounce email as it can contain spammy text
            # bounced_email=msg,
        )
Beispiel #28
0
    def handle_reply(self, envelope, smtp: SMTP, msg: EmailMessage) -> str:
        reply_email = envelope.rcpt_tos[0]

        # reply_email must end with EMAIL_DOMAIN
        if not reply_email.endswith(EMAIL_DOMAIN):
            LOG.error(f"Reply email {reply_email} has wrong domain")
            return "550 wrong reply email"

        forward_email = ForwardEmail.get_by(reply_email=reply_email)
        alias: str = forward_email.gen_email.email

        # alias must end with EMAIL_DOMAIN or custom-domain
        alias_domain = alias[alias.find("@") + 1 :]
        if alias_domain != EMAIL_DOMAIN:
            if not CustomDomain.get_by(domain=alias_domain):
                return "550 alias unknown by SimpleLogin"

        user_email = forward_email.gen_email.user.email
        if envelope.mail_from != user_email:
            LOG.error(
                f"Reply email can only be used by user email. Actual mail_from: %s. User email %s",
                envelope.mail_from,
                user_email,
            )

            send_email(
                envelope.mail_from,
                f"Your email ({envelope.mail_from}) is not allowed to send email to {reply_email}",
                "",
                "",
            )

            return "250 ignored"

        # remove DKIM-Signature
        if msg["DKIM-Signature"]:
            LOG.d("Remove DKIM-Signature %s", msg["DKIM-Signature"])
            del msg["DKIM-Signature"]

        # email seems to come from alias
        msg.replace_header("From", alias)
        msg.replace_header("To", forward_email.website_email)

        # add List-Unsubscribe header
        unsubscribe_link = f"{URL}/dashboard/unsubscribe/{forward_email.gen_email_id}"
        add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
        add_or_replace_header(
            msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
        )

        LOG.d(
            "send email from %s to %s, mail_options:%s,rcpt_options:%s",
            alias,
            forward_email.website_email,
            envelope.mail_options,
            envelope.rcpt_options,
        )

        if alias_domain == EMAIL_DOMAIN:
            add_dkim_signature(msg, EMAIL_DOMAIN)
        # add DKIM-Signature for non-custom-domain alias
        else:
            custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
            if custom_domain.dkim_verified:
                add_dkim_signature(msg, alias_domain)

        msg_raw = msg.as_string().encode()
        smtp.sendmail(
            alias,
            forward_email.website_email,
            msg_raw,
            envelope.mail_options,
            envelope.rcpt_options,
        )

        ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True)
        db.session.commit()

        return "250 Message accepted for delivery"
Beispiel #29
0
def verify_receipt(receipt_data, user,
                   password) -> Optional[AppleSubscription]:
    """Call verifyReceipt endpoint and create/update AppleSubscription table
    Call the production URL for verifyReceipt first,
    and proceed to verify with the 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")
    r = requests.post(_PROD_URL,
                      json={
                          "receipt-data": receipt_data,
                          "password": password
                      })

    if r.json() == {"status": 21007}:
        # try sandbox_url
        LOG.warning("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.warning(
            "verifyReceipt status !=0, probably invalid receipt. User %s",
            user,
        )
        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.warning("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"]
            == _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.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.error("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,
        )

    db.session.commit()

    return apple_sub
Beispiel #30
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.error(
            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 = email.strip().lower()
    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)