Exemple #1
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.exception(
                "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()

        try:
            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()
        except IntegrityError:
            LOG.warning("Contact %s %s already exist", alias, contact_email)
            db.session.rollback()
            contact = Contact.get_by(alias_id=alias.id,
                                     website_email=contact_email)

    return contact
Exemple #2
0
def create_contact_route(alias_id):
    """
    Create contact for an alias
    Input:
        alias_id: in url
        contact: in body
    Output:
        201 if success
        409 if contact already added


    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    user = g.user
    alias: Alias = Alias.get(alias_id)

    if alias.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    contact_addr = data.get("contact")

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

    contact_name, contact_email = parseaddr_unicode(contact_addr)

    # already been added
    if Contact.get_by(alias_id=alias.id, website_email=contact_email):
        return jsonify(error="Contact already added"), 409

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

    LOG.d("create reverse-alias for %s %s", contact_addr, alias)
    db.session.commit()

    return jsonify(**serialize_contact(contact)), 201
Exemple #3
0
def generate_reply_email(contact_email: str) -> str:
    """
    generate a reply_email (aka reverse-alias), make sure it isn't used by any contact
    """
    # shorten email to avoid exceeding the 64 characters
    # from https://tools.ietf.org/html/rfc5321#section-4.5.3
    # "The maximum total length of a user name or other local-part is 64
    #    octets."

    if contact_email:
        # control char: 4 chars (ra+, +)
        # random suffix: max 10 chars
        # maximum: 64

        # make sure contact_email can be ascii-encoded
        contact_email = convert_to_id(contact_email)
        contact_email = contact_email.lower().strip().replace(" ", "")
        contact_email = contact_email[:45]
        contact_email = contact_email.replace("@", ".at.")

    # not use while to avoid infinite loop
    for _ in range(1000):
        if contact_email:
            random_length = random.randint(5, 10)
            reply_email = (
                f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}"
            )
        else:
            random_length = random.randint(10, 50)
            reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}"

        if not Contact.get_by(reply_email=reply_email):
            return reply_email

    raise Exception("Cannot generate reply email")
Exemple #4
0
def is_reverse_alias(address: str) -> bool:
    # to take into account the new reverse-alias that doesn't start with "ra+"
    if Contact.get_by(reply_email=address):
        return True

    return address.endswith(f"@{EMAIL_DOMAIN}") and (
        address.startswith("reply+") or address.startswith("ra+"))
Exemple #5
0
def replace_header_when_reply(msg: Message, alias: Alias, header: str):
    """
    Replace CC or To Reply emails by original emails
    """
    addrs = get_addrs_from_header(msg, header)

    # Nothing to do
    if not addrs:
        return

    new_addrs: [str] = []

    for addr in addrs:
        name, reply_email = parseaddr(addr)

        # no transformation when alias is already in the header
        if reply_email == alias.email:
            continue

        contact = Contact.get_by(reply_email=reply_email)
        if not contact:
            LOG.warning("%s email in reply phase %s must be reply emails",
                        header, reply_email)
            # still keep this email in header
            new_addrs.append(addr)
        else:
            new_addrs.append(formataddr((contact.name, contact.website_email)))

    new_header = ",".join(new_addrs)
    LOG.d("Replace %s header, old: %s, new: %s", header, msg[header],
          new_header)
    add_or_replace_header(msg, header, new_header)
Exemple #6
0
def rate_limited_reply_phase(reply_email: str) -> bool:
    contact = Contact.get_by(reply_email=reply_email)
    if not contact:
        return False

    alias = contact.alias
    return rate_limited_for_alias(alias) or rate_limited_for_mailbox(alias)
Exemple #7
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)

    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
        found = False
        # searching for the key in mailbox
        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)
            found = True

        # searching for the key in contact
        contact = Contact.get_by(pgp_finger_print=fingerprint)
        if contact:
            LOG.d("(re-)load public key for %s", contact)
            load_public_key(contact.pgp_public_key)
            found = True

        if found:
            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)
Exemple #8
0
def greylisting_needed_reply_phase(reply_email: str) -> bool:
    contact = Contact.get_by(reply_email=reply_email)
    if not contact:
        return False

    alias = contact.alias
    return greylisting_needed_for_alias(
        alias) or greylisting_needed_for_mailbox(alias)
Exemple #9
0
def create_contact_route(alias_id):
    """
    Create contact for an alias
    Input:
        alias_id: in url
        contact: in body
    Output:
        201 if success
        409 if contact already added


    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    user = g.user
    alias: Alias = Alias.get(alias_id)

    if alias.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    contact_addr = data.get("contact")

    if not contact_addr:
        return jsonify(error="Contact cannot be empty"), 400

    full_address: EmailAddress = address.parse(contact_addr)
    if not full_address:
        return jsonify(error=f"invalid contact email {contact_addr}"), 400

    contact_name, contact_email = full_address.display_name, full_address.address

    contact_email = sanitize_email(contact_email, not_lower=True)

    # already been added
    contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
    if contact:
        return jsonify(**serialize_contact(contact, existed=True)), 200

    try:
        contact = Contact.create(
            user_id=alias.user_id,
            alias_id=alias.id,
            website_email=contact_email,
            name=contact_name,
            reply_email=generate_reply_email(contact_email, user),
        )
    except CannotCreateContactForReverseAlias:
        return jsonify(
            error="You can't create contact for a reverse alias"), 400

    LOG.d("create reverse-alias for %s %s", contact_addr, alias)
    Session.commit()

    return jsonify(**serialize_contact(contact)), 201
Exemple #10
0
def generate_reply_email():
    # generate a reply_email, make sure it is unique
    # not use while loop to avoid infinite loop
    reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
    for _ in range(1000):
        if not Contact.get_by(reply_email=reply_email):
            # found!
            break
        reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"

    return reply_email
Exemple #11
0
def create_contact_route(alias_id):
    """
    Create contact for an alias
    Input:
        alias_id: in url
        contact: in body
    Output:
        201 if success
        409 if contact already added


    """
    data = request.get_json()
    if not data:
        return jsonify(error="request body cannot be empty"), 400

    user = g.user
    alias: Alias = Alias.get(alias_id)

    if alias.user_id != user.id:
        return jsonify(error="Forbidden"), 403

    contact_addr = data.get("contact")

    if not contact_addr:
        return jsonify(error="Contact cannot be empty"), 400

    contact_name, contact_email = parseaddr_unicode(contact_addr)

    if not is_valid_email(contact_email):
        return jsonify(error=f"invalid contact email {contact_email}"), 400

    contact_email = sanitize_email(contact_email)

    # already been added
    contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
    if contact:
        return jsonify(**serialize_contact(contact, existed=True)), 200

    contact = Contact.create(
        user_id=alias.user_id,
        alias_id=alias.id,
        website_email=contact_email,
        name=contact_name,
        reply_email=generate_reply_email(contact_email, user),
    )

    LOG.d("create reverse-alias for %s %s", contact_addr, alias)
    db.session.commit()

    return jsonify(**serialize_contact(contact)), 201
Exemple #12
0
def generate_reply_email(contact_email: str, user: User) -> str:
    """
    generate a reply_email (aka reverse-alias), make sure it isn't used by any contact
    """
    # shorten email to avoid exceeding the 64 characters
    # from https://tools.ietf.org/html/rfc5321#section-4.5.3
    # "The maximum total length of a user name or other local-part is 64
    #    octets."

    # todo: turns this to False after Dec 20 2020
    include_sender_in_reverse_alias = True

    # user has chosen an option explicitly
    if user.include_sender_in_reverse_alias is not None:
        include_sender_in_reverse_alias = user.include_sender_in_reverse_alias

    if include_sender_in_reverse_alias and contact_email:
        # control char: 4 chars (ra+, +)
        # random suffix: max 10 chars
        # maximum: 64

        # make sure contact_email can be ascii-encoded
        contact_email = convert_to_id(contact_email)
        contact_email = contact_email.lower().strip().replace(" ", "")
        contact_email = contact_email[:45]
        contact_email = contact_email.replace("@", ".at.")
        contact_email = convert_to_alphanumeric(contact_email)

    # not use while to avoid infinite loop
    for _ in range(1000):
        if include_sender_in_reverse_alias and contact_email:
            random_length = random.randint(5, 10)
            reply_email = (
                f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}"
            )
        else:
            random_length = random.randint(20, 50)
            reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}"

        if not Contact.get_by(reply_email=reply_email):
            return reply_email

    raise Exception("Cannot generate reply email")
Exemple #13
0
def generate_reply_email(contact_email: str, user: User) -> str:
    """
    generate a reply_email (aka reverse-alias), make sure it isn't used by any contact
    """
    # shorten email to avoid exceeding the 64 characters
    # from https://tools.ietf.org/html/rfc5321#section-4.5.3
    # "The maximum total length of a user name or other local-part is 64
    #    octets."

    include_sender_in_reverse_alias = False

    # user has set this option explicitly
    if user.include_sender_in_reverse_alias is not None:
        include_sender_in_reverse_alias = user.include_sender_in_reverse_alias

    if include_sender_in_reverse_alias and contact_email:
        # make sure contact_email can be ascii-encoded
        contact_email = convert_to_id(contact_email)
        contact_email = sanitize_email(contact_email)
        contact_email = contact_email[:45]
        contact_email = contact_email.replace("@", ".at.")
        contact_email = convert_to_alphanumeric(contact_email)

    # not use while to avoid infinite loop
    for _ in range(1000):
        if include_sender_in_reverse_alias and contact_email:
            random_length = random.randint(5, 10)
            reply_email = (
                # do not use the ra+ anymore
                # f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}"
                f"{contact_email}_{random_string(random_length)}@{EMAIL_DOMAIN}"
            )
        else:
            random_length = random.randint(20, 50)
            # do not use the ra+ anymore
            # reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}"
            reply_email = f"{random_string(random_length)}@{EMAIL_DOMAIN}"

        if not Contact.get_by(reply_email=reply_email):
            return reply_email

    raise Exception("Cannot generate reply email")
Exemple #14
0
def handle_reply(envelope, smtp: SMTP, msg: Message,
                 rcpt_to: str) -> (bool, str):
    """
    return whether an email has been delivered and
    the smtp status ("250 Message accepted", "550 Non-existent email address", etc)
    """
    reply_email = rcpt_to.lower().strip()

    # reply_email must end with EMAIL_DOMAIN
    if not reply_email.endswith(EMAIL_DOMAIN):
        LOG.warning(f"Reply email {reply_email} has wrong domain")
        return False, "550 SL E2"

    contact = Contact.get_by(reply_email=reply_email)
    if not contact:
        LOG.warning(f"No such forward-email with {reply_email} as reply-email")
        return False, "550 SL E4"

    alias = contact.alias
    address: str = contact.alias.email
    alias_domain = address[address.find("@") + 1:]

    # alias must end with one of the ALIAS_DOMAINS or custom-domain
    if not email_belongs_to_alias_domains(alias.email):
        if not CustomDomain.get_by(domain=alias_domain):
            return False, "550 SL E5"

    user = alias.user
    mail_from = envelope.mail_from.lower().strip()

    # bounce email initiated by Postfix
    # can happen in case emails cannot be delivered to user-email
    # in this case Postfix will try to send a bounce report to original sender, which is
    # the "reply email"
    if mail_from == "<>":
        LOG.warning(
            "Bounce when sending to alias %s from %s, user %s",
            alias,
            contact,
            user,
        )

        handle_bounce(contact, alias, msg, user)
        return False, "550 SL E6"

    mailbox = Mailbox.get_by(email=mail_from, user_id=user.id)
    if not mailbox or mailbox not in alias.mailboxes:
        # only mailbox can send email to the reply-email
        handle_unknown_mailbox(envelope, msg, reply_email, user, alias)
        return False, "550 SL E7"

    if ENFORCE_SPF and mailbox.force_spf:
        ip = msg[_IP_HEADER]
        if not spf_pass(ip, envelope, mailbox, user, alias,
                        contact.website_email, msg):
            # cannot use 4** here as sender will retry. 5** because that generates bounce report
            return True, "250 SL E11"

    delete_header(msg, _IP_HEADER)

    delete_header(msg, "DKIM-Signature")
    delete_header(msg, "Received")

    # make the email comes from alias
    from_header = alias.email
    # add alias name from alias
    if alias.name:
        LOG.d("Put alias name in from header")
        from_header = formataddr((alias.name, alias.email))
    elif alias.custom_domain:
        LOG.d("Put domain default alias name in from header")

        # add alias name from domain
        if alias.custom_domain.name:
            from_header = formataddr((alias.custom_domain.name, alias.email))

    add_or_replace_header(msg, "From", from_header)

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

    # remove sender header if present as this could reveal user real email
    delete_header(msg, "Sender")
    delete_header(msg, "X-Sender")

    replace_header_when_reply(msg, alias, "To")
    replace_header_when_reply(msg, alias, "Cc")

    # 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.email,
        contact.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)

    smtp.sendmail(
        alias.email,
        contact.website_email,
        msg.as_bytes(),
        envelope.mail_options,
        envelope.rcpt_options,
    )

    EmailLog.create(contact_id=contact.id,
                    is_reply=True,
                    user_id=contact.user_id)
    db.session.commit()

    return True, "250 Message accepted for delivery"
def handle_reply(envelope, smtp: SMTP, msg: Message,
                 rcpt_to: str) -> (bool, str):
    """
    return whether an email has been delivered and
    the smtp status ("250 Message accepted", "550 Non-existent email address", etc)
    """
    reply_email = rcpt_to.lower().strip()

    # reply_email must end with EMAIL_DOMAIN
    if not reply_email.endswith(EMAIL_DOMAIN):
        LOG.warning(f"Reply email {reply_email} has wrong domain")
        return False, "550 SL E2"

    contact = Contact.get_by(reply_email=reply_email)
    if not contact:
        LOG.warning(f"No such forward-email with {reply_email} as reply-email")
        return False, "550 SL E4 Email not exist"

    alias = contact.alias
    address: str = contact.alias.email
    alias_domain = address[address.find("@") + 1:]

    # alias must end with one of the ALIAS_DOMAINS or custom-domain
    if not email_belongs_to_alias_domains(alias.email):
        if not CustomDomain.get_by(domain=alias_domain):
            return False, "550 SL E5"

    user = alias.user
    mail_from = envelope.mail_from.lower().strip()

    # bounce email initiated by Postfix
    # can happen in case emails cannot be delivered to user-email
    # in this case Postfix will try to send a bounce report to original sender, which is
    # the "reply email"
    if mail_from == "<>":
        LOG.warning(
            "Bounce when sending to alias %s from %s, user %s",
            alias,
            contact,
            user,
        )

        handle_bounce(contact, alias, msg, user)
        return False, "550 SL E6"

    mailbox = Mailbox.get_by(email=mail_from, user_id=user.id)
    if not mailbox or mailbox not in alias.mailboxes:
        # only mailbox can send email to the reply-email
        handle_unknown_mailbox(envelope, msg, reply_email, user, alias)
        return False, "550 SL E7"

    if ENFORCE_SPF and mailbox.force_spf:
        ip = msg[_IP_HEADER]
        if not spf_pass(ip, envelope, mailbox, user, alias,
                        contact.website_email, msg):
            # cannot use 4** here as sender will retry. 5** because that generates bounce report
            return True, "250 SL E11"

    delete_header(msg, _IP_HEADER)

    delete_header(msg, "DKIM-Signature")
    delete_header(msg, "Received")

    # make the email comes from alias
    from_header = alias.email
    # add alias name from alias
    if alias.name:
        LOG.d("Put alias name in from header")
        from_header = formataddr((alias.name, alias.email))
    elif alias.custom_domain:
        LOG.d("Put domain default alias name in from header")

        # add alias name from domain
        if alias.custom_domain.name:
            from_header = formataddr((alias.custom_domain.name, alias.email))

    add_or_replace_header(msg, "From", from_header)

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

    # remove sender header if present as this could reveal user real email
    delete_header(msg, "Sender")
    delete_header(msg, "X-Sender")

    replace_header_when_reply(msg, alias, "To")
    replace_header_when_reply(msg, alias, "Cc")

    # 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.email,
        contact.website_email,
        envelope.mail_options,
        envelope.rcpt_options,
    )

    # replace "*****@*****.**" by the contact email in the email body
    # as this is usually included when replying
    if user.replace_reverse_alias:
        if msg.is_multipart():
            for part in msg.walk():
                if part.get_content_maintype() != "text":
                    continue
                part = replace_str_in_msg(part, reply_email,
                                          contact.website_email)

        else:
            msg = replace_str_in_msg(msg, reply_email, contact.website_email)

    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)

    # create PGP email if needed
    if contact.pgp_finger_print and user.is_premium():
        LOG.d("Encrypt message for contact %s", contact)
        try:
            msg = prepare_pgp_message(msg, contact.pgp_finger_print)
        except PGPException:
            LOG.exception("Cannot encrypt message %s -> %s. %s %s", alias,
                          contact, mailbox, user)
            # so the client can retry later
            return False, "421 SL E13 Retry later"

    try:
        smtp.sendmail(
            alias.email,
            contact.website_email,
            msg.as_bytes(),
            envelope.mail_options,
            envelope.rcpt_options,
        )
    except Exception:
        LOG.exception("Cannot send email from %s to %s", alias, contact)
        send_email(
            mailbox.email,
            f"Email cannot be sent to {contact.email} from {alias.email}",
            render(
                "transactional/reply-error.txt",
                user=user,
                alias=alias,
                contact=contact,
                contact_domain=get_email_domain_part(contact.email),
            ),
            render(
                "transactional/reply-error.html",
                user=user,
                alias=alias,
                contact=contact,
                contact_domain=get_email_domain_part(contact.email),
            ),
        )
    else:
        EmailLog.create(contact_id=contact.id,
                        is_reply=True,
                        user_id=contact.user_id)

    db.session.commit()
    return True, "250 Message accepted for delivery"
Exemple #16
0
def replace_header_when_forward(msg: Message, alias: Alias, header: str):
    """
    Replace CC or To header by Reply emails in forward phase
    """
    addrs = get_addrs_from_header(msg, header)

    # Nothing to do
    if not addrs:
        return

    new_addrs: [str] = []
    need_replace = False

    for addr in addrs:
        contact_name, contact_email = parseaddr_unicode(addr)

        # no transformation when alias is already in the header
        if contact_email == alias.email:
            new_addrs.append(addr)
            continue

        contact = Contact.get_by(alias_id=alias.id,
                                 website_email=contact_email)
        if contact:
            # update the contact name if needed
            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 email %s, header %s",
                alias,
                contact_email,
                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,
                is_cc=header.lower() == "cc",
            )
            db.session.commit()

        new_addrs.append(contact.new_addr())
        need_replace = True

    if need_replace:
        new_header = ",".join(new_addrs)
        LOG.d("Replace %s header, old: %s, new: %s", header, msg[header],
              new_header)
        add_or_replace_header(msg, header, new_header)
    else:
        LOG.d("No need to replace %s header", header)
Exemple #17
0
def alias_contact_manager(alias_id):
    highlight_contact_id = None
    if request.args.get("highlight_contact_id"):
        highlight_contact_id = int(request.args.get("highlight_contact_id"))

    alias = Alias.get(alias_id)

    # sanity check
    if not alias:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    if alias.user_id != current_user.id:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    new_contact_form = NewContactForm()

    if request.method == "POST":
        if request.form.get("form-name") == "create":
            if new_contact_form.validate():
                contact_addr = new_contact_form.email.data.strip()

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

                try:
                    contact_name, contact_email = parseaddr_unicode(
                        contact_addr)
                except Exception:
                    flash(f"{contact_addr} is invalid", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                        ))
                contact_email = contact_email.lower()

                contact = Contact.get_by(alias_id=alias.id,
                                         website_email=contact_email)
                # already been added
                if contact:
                    flash(f"{contact_email} is already added", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                            highlight_contact_id=contact.id,
                        ))

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

                LOG.d("create reverse-alias for %s", contact_addr)
                db.session.commit()
                flash(f"Reverse alias for {contact_addr} is created",
                      "success")

                return redirect(
                    url_for(
                        "dashboard.alias_contact_manager",
                        alias_id=alias_id,
                        highlight_contact_id=contact.id,
                    ))
        elif request.form.get("form-name") == "delete":
            contact_id = request.form.get("contact-id")
            contact = Contact.get(contact_id)

            if not contact:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(
                    url_for("dashboard.alias_contact_manager",
                            alias_id=alias_id))
            elif contact.alias_id != alias.id:
                flash("You cannot delete reverse-alias", "warning")
                return redirect(
                    url_for("dashboard.alias_contact_manager",
                            alias_id=alias_id))

            delete_contact_email = contact.website_email
            Contact.delete(contact_id)
            db.session.commit()

            flash(f"Reverse-alias for {delete_contact_email} has been deleted",
                  "success")

            return redirect(
                url_for("dashboard.alias_contact_manager", alias_id=alias_id))

    # make sure highlighted contact is at array start
    contacts = alias.contacts

    if highlight_contact_id:
        contacts = sorted(contacts,
                          key=lambda fe: fe.id == highlight_contact_id,
                          reverse=True)

    return render_template(
        "dashboard/alias_contact_manager.html",
        contacts=contacts,
        alias=alias,
        new_contact_form=new_contact_form,
        highlight_contact_id=highlight_contact_id,
    )
Exemple #18
0
def alias_contact_manager(alias_id):
    highlight_contact_id = None
    if request.args.get("highlight_contact_id"):
        highlight_contact_id = int(request.args.get("highlight_contact_id"))

    alias = Alias.get(alias_id)

    page = 0
    if request.args.get("page"):
        page = int(request.args.get("page"))

    query = request.args.get("query") or ""

    # sanity check
    if not alias:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    if alias.user_id != current_user.id:
        flash("You do not have access to this page", "warning")
        return redirect(url_for("dashboard.index"))

    new_contact_form = NewContactForm()

    if request.method == "POST":
        if request.form.get("form-name") == "create":
            if new_contact_form.validate():
                contact_addr = new_contact_form.email.data.strip()

                try:
                    contact_name, contact_email = parseaddr_unicode(
                        contact_addr)
                    contact_email = sanitize_email(contact_email)
                except Exception:
                    flash(f"{contact_addr} is invalid", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                        ))

                if not is_valid_email(contact_email):
                    flash(f"{contact_email} is invalid", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                        ))

                contact = Contact.get_by(alias_id=alias.id,
                                         website_email=contact_email)
                # already been added
                if contact:
                    flash(f"{contact_email} is already added", "error")
                    return redirect(
                        url_for(
                            "dashboard.alias_contact_manager",
                            alias_id=alias_id,
                            highlight_contact_id=contact.id,
                        ))

                contact = Contact.create(
                    user_id=alias.user_id,
                    alias_id=alias.id,
                    website_email=contact_email,
                    name=contact_name,
                    reply_email=generate_reply_email(contact_email,
                                                     current_user),
                )

                LOG.d("create reverse-alias for %s", contact_addr)
                db.session.commit()
                flash(f"Reverse alias for {contact_addr} is created",
                      "success")

                return redirect(
                    url_for(
                        "dashboard.alias_contact_manager",
                        alias_id=alias_id,
                        highlight_contact_id=contact.id,
                    ))
        elif request.form.get("form-name") == "delete":
            contact_id = request.form.get("contact-id")
            contact = Contact.get(contact_id)

            if not contact:
                flash("Unknown error. Refresh the page", "warning")
                return redirect(
                    url_for("dashboard.alias_contact_manager",
                            alias_id=alias_id))
            elif contact.alias_id != alias.id:
                flash("You cannot delete reverse-alias", "warning")
                return redirect(
                    url_for("dashboard.alias_contact_manager",
                            alias_id=alias_id))

            delete_contact_email = contact.website_email
            Contact.delete(contact_id)
            db.session.commit()

            flash(f"Reverse-alias for {delete_contact_email} has been deleted",
                  "success")

            return redirect(
                url_for("dashboard.alias_contact_manager", alias_id=alias_id))

        elif request.form.get("form-name") == "search":
            query = request.form.get("query")
            return redirect(
                url_for(
                    "dashboard.alias_contact_manager",
                    alias_id=alias_id,
                    query=query,
                    highlight_contact_id=highlight_contact_id,
                ))

    contact_infos = get_contact_infos(alias, page, query=query)
    last_page = len(contact_infos) < PAGE_LIMIT

    # if highlighted contact isn't included, fetch it
    # make sure highlighted contact is at array start
    contact_ids = [contact_info.contact.id for contact_info in contact_infos]
    if highlight_contact_id and highlight_contact_id not in contact_ids:
        contact_infos = (get_contact_infos(
            alias, contact_id=highlight_contact_id, query=query) +
                         contact_infos)

    return render_template(
        "dashboard/alias_contact_manager.html",
        contact_infos=contact_infos,
        alias=alias,
        new_contact_form=new_contact_form,
        highlight_contact_id=highlight_contact_id,
        page=page,
        last_page=last_page,
        query=query,
    )