Exemple #1
0
def draft_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))
    parent_draft = g.db_session.query(Message). \
        filter(Message.public_id == public_id).first()
    if parent_draft is None or not parent_draft.is_draft or \
            parent_draft.namespace.id != g.namespace.id:
        return err(404, 'No draft with public id {}'.format(public_id))
    if not parent_draft.is_latest:
        return err(409, 'Draft {} has already been updated to {}'.format(
            public_id, g.encoder.cereal(parent_draft.most_recent_revision)))

    # TODO(emfree): what if you try to update a draft on a *thread* that's been
    # deleted?

    data = request.get_json(force=True)

    to = data.get('to')
    cc = data.get('cc')
    bcc = data.get('bcc')
    subject = data.get('subject')
    body = data.get('body')
    try:
        tags = get_tags(data.get('tags'), g.namespace.id, g.db_session)
        files = get_attachments(data.get('files'), g.namespace.id,
                                g.db_session)
    except InputError as e:
        return err(404, e.message)

    draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                  parent_draft, to, subject, body,
                                  files, cc, bcc, tags)
    return g.encoder.jsonify(draft)
Exemple #2
0
def message_read_api(public_id):
    g.parser.add_argument('view', type=view, location='args')
    args = strict_parse_args(g.parser, request.args)
    encoder = APIEncoder(g.namespace.public_id, args['view'] == 'expanded')

    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id,
            Message.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find message {0} ".format(public_id))

    if request.headers.get('Accept', None) == 'message/rfc822':
        if message.full_body is not None:
            return Response(message.full_body.data,
                            mimetype='message/rfc822')
        else:
            g.log.error("Message without full_body attribute: id='{0}'"
                        .format(message.id))
            raise NotFoundError(
                "Couldn't find raw contents for message `{0}` "
                .format(public_id))

    return encoder.jsonify(message)
def message_update_api(public_id):
    data = request.get_json(force=True)
    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id,
            Message.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find message {0} ".format(public_id))
    if data.keys() != ['unread'] or not isinstance(data['unread'], bool):
        raise InputError('Can only change the unread attribute of a '
                         'message')

    # TODO(emfree): Shouldn't allow this on messages that are actually
    # drafts.

    unread_tag = message.namespace.tags['unread']
    unseen_tag = message.namespace.tags['unseen']
    if data['unread']:
        message.is_read = False
        message.thread.apply_tag(unread_tag)
    else:
        message.is_read = True
        message.thread.remove_tag(unseen_tag)
        if all(m.is_read for m in message.thread.messages):
            message.thread.remove_tag(unread_tag)
    return g.encoder.jsonify(message)
Exemple #4
0
def event_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, "Invalid event id {}".format(public_id))
    data = request.get_json(force=True)

    try:
        valid_event_update(data)
    except InputError as e:
        return err(404, e.message)

    # Convert the data into our types where necessary
    # e.g. timestamps, participant_list
    if "start" in data:
        data["start"] = datetime.utcfromtimestamp(int(data.get("start")))
    if "end" in data:
        data["end"] = datetime.utcfromtimestamp(int(data.get("end")))
    if "participants" in data:
        data["participant_list"] = []
        for p in data["participants"]:
            if "status" not in p:
                p["status"] = "noreply"
            data["participant_list"].append(p)
        del data["participants"]

    try:
        result = events.crud.update(g.namespace, g.db_session, public_id, data)
    except InputError as e:
        return err(404, e.message)

    if result is None:
        return err(404, "Couldn't find event with id {0}".format(public_id))
    return g.encoder.jsonify(result)
Exemple #5
0
def event_update_api(public_id):
    valid_public_id(public_id)
    try:
        event = g.db_session.query(Event).filter(
            Event.public_id == public_id,
            Event.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find event {0}".format(public_id))
    if event.read_only:
        raise InputError('Cannot update read_only event.')

    data = request.get_json(force=True)
    valid_event_update(data, g.namespace, g.db_session)

    if 'participants' in data:
        for p in data['participants']:
            if 'status' not in p:
                p['status'] = 'noreply'

    for attr in ['title', 'description', 'location', 'when', 'participants']:
        if attr in data:
            setattr(event, attr, data[attr])

    g.db_session.commit()

    schedule_action('update_event', event, g.namespace.id, g.db_session,
                    calendar_uid=event.calendar.uid)

    return g.encoder.jsonify(event)
Exemple #6
0
def tag_update_api(public_id):
    try:
        valid_public_id(public_id)
        tag = g.db_session.query(Tag).filter(
            Tag.public_id == public_id,
            Tag.namespace_id == g.namespace.id).one()
    except InputError:
        return err(400, '{} is not a valid id'.format(public_id))
    except NoResultFound:
        return err(404, 'No tag found')

    data = request.get_json(force=True)
    if 'name' not in data.keys():
        return err(400, 'Malformed tag update request')
    if 'namespace_id' in data.keys():
        ns_id = data['namespace_id']
        valid_public_id(ns_id)
        if ns_id != g.namespace.id:
            return err(400, 'Cannot change the namespace on a tag.')
    if not tag.user_created:
        return err(403, 'Cannot modify tag {}'.format(public_id))
    new_name = data['name']

    if new_name != tag.name:  # short-circuit rename to same value
        if not Tag.name_available(new_name, g.namespace.id, g.db_session):
            return err(409, 'Tag name already used')
        tag.name = new_name
        g.db_session.commit()
    # TODO(emfree) also support deleting user-created tags.

    return g.encoder.jsonify(tag)
Exemple #7
0
def draft_delete_api(public_id):
    data = request.get_json(force=True)
    if data.get('version') is None:
        return err(400, 'Must specify version to delete')
    version = data.get('version')

    try:
        valid_public_id(public_id)
        draft = g.db_session.query(Message).filter(
            Message.public_id == public_id).one()
    except InputError:
        return err(400, 'Invalid public id {}'.format(public_id))
    except NoResultFound:
        return err(404, 'No draft found with public_id {}'.
                   format(public_id))

    if draft.namespace != g.namespace:
        return err(404, 'No draft found with public_id {}'.
                   format(public_id))

    if not draft.is_draft:
        return err(400, 'Message with public id {} is not a draft'.
                   format(public_id))

    if draft.version != version:
        return err(409, 'Draft {0}.{1} has already been updated to version '
                   '{2}'.format(public_id, version, draft.version))

    result = sendmail.delete_draft(g.db_session, g.namespace.account,
                                   public_id)
    return g.encoder.jsonify(result)
Exemple #8
0
def webhooks_read_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid webhook id {}'.format(public_id))
    if request.method == 'GET':
        try:
            hook = g.db_session.query(Webhook).filter(
                Webhook.public_id == public_id,
                Webhook.namespace_id == g.namespace.id).one()
            return g.encoder.jsonify(hook)
        except NoResultFound:
            return err(404, "Couldn't find webhook with id {}"
                       .format(public_id))

    if request.method == 'PUT':
        data = request.get_json(force=True)
        # We only support updates to the 'active' flag.
        if data.keys() != ['active']:
            return err(400, 'Malformed webhook request')

        try:
            if data['active']:
                get_webhook_client().start_hook(public_id)
            else:
                get_webhook_client().stop_hook(public_id)
            return g.encoder.jsonify({"success": True})
        except zerorpc.RemoteError:
            return err(404, "Couldn't find webhook with id {}"
                       .format(public_id))
Exemple #9
0
def contact_read_api(public_id):
    # Get all data for an existing contact.
    valid_public_id(public_id)
    result = inbox.contacts.crud.read(g.namespace, g.db_session, public_id)
    if result is None:
        raise NotFoundError("Couldn't find contact {0}".format(public_id))
    return g.encoder.jsonify(result)
Exemple #10
0
def auth():
    """ Check for account ID on all non-root URLS """
    if request.path in ('/accounts', '/accounts/', '/') \
                       or request.path.startswith('/w/'):
        return

    if not request.authorization or not request.authorization.username:
        return make_response((
            "Could not verify access credential.", 401,
            {'WWW-Authenticate': 'Basic realm="API '
             'Access Token Required"'}))

    namespace_public_id = request.authorization.username

    with global_session_scope() as db_session:
        try:
            valid_public_id(namespace_public_id)
            namespace = db_session.query(Namespace) \
                .filter(Namespace.public_id == namespace_public_id).one()
            g.namespace_id = namespace.id
            g.account_id = namespace.account.id
        except NoResultFound:
            return make_response((
                "Could not verify access credential.", 401,
                {'WWW-Authenticate': 'Basic realm="API '
                 'Access Token Required"'}))
Exemple #11
0
def event_update_api(public_id):
    valid_public_id(public_id)
    data = request.get_json(force=True)

    valid_event_update(data, g.namespace, g.db_session)

    # Convert the data into our types where necessary
    # e.g. timestamps, participant_list
    if 'start' in data:
        data['start'] = datetime.utcfromtimestamp(int(data.get('start')))
    if 'end' in data:
        data['end'] = datetime.utcfromtimestamp(int(data.get('end')))
    if 'participants' in data:
        data['participant_list'] = []
        for p in data['participants']:
            if 'status' not in p:
                p['status'] = 'noreply'
            data['participant_list'].append(p)
        del data['participants']

    result = events.crud.update(g.namespace, g.db_session, public_id, data)

    if result is None:
        raise NotFoundError("Couldn't find event {0}".format(public_id))

    schedule_action('update_event', result, g.namespace.id, g.db_session)
    return g.encoder.jsonify(result)
Exemple #12
0
def message_api(public_id):
    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id).one()
        assert int(message.namespace.id) == int(g.namespace.id)
    except InputError:
        return err(400, 'Invalid message id {}'.format(public_id))
    except NoResultFound:
        return err(404,
                   "Couldn't find message with id {0} "
                   "on namespace {1}".format(public_id, g.namespace_public_id))
    if request.method == 'GET':
        return g.encoder.jsonify(message)
    elif request.method == 'PUT':
        data = request.get_json(force=True)
        if data.keys() != ['unread'] or not isinstance(data['unread'], bool):
            return err(400,
                       'Can only change the unread attribute of a message')

        # TODO(emfree): Shouldn't allow this on messages that are actually
        # drafts.

        unread_tag = message.namespace.tags['unread']
        unseen_tag = message.namespace.tags['unseen']
        if data['unread']:
            message.is_read = False
            message.thread.apply_tag(unread_tag)
        else:
            message.is_read = True
            message.thread.remove_tag(unseen_tag)
            if all(m.is_read for m in message.thread.messages):
                message.thread.remove_tag(unread_tag)
        return g.encoder.jsonify(message)
Exemple #13
0
def folder_label_update_api(public_id):
    category_type = g.namespace.account.category_type
    valid_public_id(public_id)
    try:
        category = g.db_session.query(Category).filter(
            Category.namespace_id == g.namespace.id,
            Category.public_id == public_id).one()
    except NoResultFound:
        raise InputError("Couldn't find {} {}".format(
            category_type, public_id))
    if category.name is not None:
        raise InputError("Cannot modify a standard {}".format(category_type))

    data = request.get_json(force=True)
    display_name = data.get('display_name')
    valid_display_name(g.namespace.id, category_type, display_name,
                       g.db_session)

    current_name = category.display_name
    category.display_name = display_name
    g.db_session.flush()

    if category_type == 'folder':
        schedule_action('update_folder', category, g.namespace.id,
                        g.db_session, old_name=current_name)
    else:
        schedule_action('update_label', category, g.namespace.id,
                        g.db_session, old_name=current_name)

    # TODO[k]: Update corresponding folder/ label once syncback is successful,
    # rather than waiting for sync to pick it up?

    return g.encoder.jsonify(category)
def tag_update_api(public_id):
    try:
        valid_public_id(public_id)
        tag = g.db_session.query(Tag).filter(
            Tag.public_id == public_id,
            Tag.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError('No tag found')

    data = request.get_json(force=True)
    if not ('name' in data.keys() and isinstance(data['name'], basestring)):
        raise InputError('Malformed tag update request')
    if 'namespace_id' in data.keys():
        ns_id = data['namespace_id']
        valid_public_id(ns_id)
        if ns_id != g.namespace.public_id:
            raise InputError('Cannot change the namespace on a tag.')
    if not tag.user_created:
        raise InputError('Cannot modify tag {}'.format(public_id))
    # Lowercase tag name, regardless of input casing.
    new_name = data['name'].lower()

    if new_name != tag.name:  # short-circuit rename to same value
        if not Tag.name_available(new_name, g.namespace.id, g.db_session):
            return err(409, 'Tag name already used')
        tag.name = new_name
        g.db_session.commit()

    return g.encoder.jsonify(tag)
def event_update(calendar_public_id):
    try:
        valid_public_id(calendar_public_id)
        with global_session_scope() as db_session:
            calendar = db_session.query(Calendar) \
                .filter(Calendar.public_id == calendar_public_id) \
                .one()
            if calendar.gpush_last_ping is not None:
                time_since_last_ping = (
                    datetime.utcnow() - calendar.gpush_last_ping
                ).total_seconds()

                # Limit write volume, and de-herd, in case we're getting many
                # concurrent updates for the same calendar.
                if time_since_last_ping < 10 + random.randrange(0, 10):
                    return resp(200)

            calendar.handle_gpush_notification()
            db_session.commit()
        return resp(200)
    except ValueError:
        raise InputError('Invalid public ID')
    except NoResultFound:
        g.log.info('Getting push notifications for non-existing calendar',
                   calendar_public_id=calendar_public_id)
        raise NotFoundError("Couldn't find calendar `{0}`"
                            .format(calendar_public_id))
Exemple #16
0
def parse_labels(request_data, db_session, namespace_id):
    # TODO deprecate being able to post "labels" and not "label_ids"
    if 'label_ids' not in request_data and 'labels' not in request_data:
        return

    label_public_ids = request_data.pop('label_ids', []) or \
        request_data.pop('labels', [])

    if not label_public_ids:
        # One of 'label_ids'/ 'labels' was present AND set to [].
        # Not allowed.
        raise InputError('Removing all labels is not allowed.')

    # TODO(emfree): Use a real JSON schema validator for this sort of thing.
    if not isinstance(label_public_ids, list):
        raise InputError('"labels" must be a list')

    for id_ in label_public_ids:
        valid_public_id(id_)

    labels = set()
    for id_ in label_public_ids:
        try:
            category = db_session.query(Category).filter(
                Category.namespace_id == namespace_id,
                Category.public_id == id_).one()
            labels.add(category)
        except NoResultFound:
            raise InputError(u'The label {} does not exist'.format(id_))
    return labels
Exemple #17
0
def calendar_read_api(public_id):
    """Get all data for an existing calendar."""
    valid_public_id(public_id)

    result = events.crud.read_calendar(g.namespace, g.db_session, public_id)
    if result is None:
        raise NotFoundError("Couldn't find calendar {0}".format(public_id))
    return g.encoder.jsonify(result)
Exemple #18
0
def draft_get_api(public_id):
    valid_public_id(public_id)
    draft = g.db_session.query(Message).filter(
        Message.public_id == public_id,
        Message.namespace_id == g.namespace.id).first()
    if draft is None:
        raise NotFoundError("Couldn't find draft {}".format(public_id))
    return g.encoder.jsonify(draft)
Exemple #19
0
def file_read_api(public_id):
    valid_public_id(public_id)
    try:
        f = g.db_session.query(Block).filter(
            Block.public_id == public_id,
            Block.namespace_id == g.namespace.id).one()
        return g.encoder.jsonify(f)
    except NoResultFound:
        raise NotFoundError("Couldn't find file {0} ".format(public_id))
Exemple #20
0
def folders_labels_api_impl(public_id):
    valid_public_id(public_id)
    try:
        category = g.db_session.query(Category). \
            filter(Category.namespace_id == g.namespace.id,
                   Category.public_id == public_id).all()
    except NoResultFound:
        raise NotFoundError("Object not found")
    return g.encoder.jsonify(category)
Exemple #21
0
def draft_get_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))
    draft = sendmail.get_draft(g.db_session, g.namespace.account, public_id)
    if draft is None:
        return err(404, 'No draft found with id {}'.format(public_id))
    return g.encoder.jsonify(draft)
Exemple #22
0
def thread_api(public_id):
    try:
        valid_public_id(public_id)
        thread = g.db_session.query(Thread).filter(
            Thread.public_id == public_id,
            Thread.namespace_id == g.namespace.id).one()
        return g.encoder.jsonify(thread)
    except NoResultFound:
        raise NotFoundError("Couldn't find thread `{0}`".format(public_id))
Exemple #23
0
def draft_send_api():
    data = request.get_json(force=True)
    if data.get("draft_id") is None:
        if data.get("to") is None:
            return err(400, "Must specify either draft id + version or " "message recipients.")
    else:
        if data.get("version") is None:
            return err(400, "Must specify version to send")

    draft_public_id = data.get("draft_id")
    version = data.get("version")
    if draft_public_id is not None:
        try:
            valid_public_id(draft_public_id)
            draft = g.db_session.query(Message).filter(Message.public_id == draft_public_id).one()
        except InputError:
            return err(400, "Invalid public id {}".format(draft_public_id))
        except NoResultFound:
            return err(404, "No draft found with id {}".format(draft_public_id))

        if draft.namespace != g.namespace:
            return err(404, "No draft found with id {}".format(draft_public_id))

        if draft.is_sent or not draft.is_draft:
            return err(400, "Message with id {} is not a draft".format(draft_public_id))

        if not draft.to_addr:
            return err(400, "No 'to:' recipients specified")

        if draft.version != version:
            return err(
                409,
                "Draft {0}.{1} has already been updated to version {2}".format(draft_public_id, version, draft.version),
            )

        schedule_action("send_draft", draft, g.namespace.id, g.db_session)
    else:
        to = data.get("to")
        cc = data.get("cc")
        bcc = data.get("bcc")
        subject = data.get("subject")
        body = data.get("body")
        try:
            tags = get_tags(data.get("tags"), g.namespace.id, g.db_session)
            files = get_attachments(data.get("files"), g.namespace.id, g.db_session)
            replyto_thread = get_thread(data.get("reply_to_thread"), g.namespace.id, g.db_session)
        except InputError as e:
            return err(404, e.message)

        draft = sendmail.create_draft(
            g.db_session, g.namespace.account, to, subject, body, files, cc, bcc, tags, replyto_thread, syncback=False
        )
        schedule_action("send_directly", draft, g.namespace.id, g.db_session)

    draft.state = "sending"
    return g.encoder.jsonify(draft)
Exemple #24
0
def tag_read_api(public_id):
    try:
        valid_public_id(public_id)
        tag = g.db_session.query(Tag).filter(
            Tag.public_id == public_id,
            Tag.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError('No tag found')

    return g.encoder.jsonify(tag)
Exemple #25
0
def event_read_api(public_id):
    """Get all data for an existing event."""
    valid_public_id(public_id)
    try:
        event = g.db_session.query(Event).filter(
            Event.namespace_id == g.namespace.id,
            Event.public_id == public_id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find event id {0}".format(public_id))
    return g.encoder.jsonify(event)
Exemple #26
0
def draft_send_api():
    data = request.get_json(force=True)
    if data.get('draft_id') is None and data.get('to') is None:
        return err(400, 'Must specify either draft id or message recipients.')

    draft_public_id = data.get('draft_id')
    if draft_public_id is not None:
        try:
            valid_public_id(draft_public_id)
            draft = g.db_session.query(Message).filter(
                Message.public_id == draft_public_id).one()
        except InputError:
            return err(400, 'Invalid public id {}'.format(draft_public_id))
        except NoResultFound:
            return err(404, 'No draft found with id {}'.
                       format(draft_public_id))

        if draft.namespace != g.namespace:
            return err(404, 'No draft found with id {}'.
                       format(draft_public_id))

        if draft.is_sent or not draft.is_draft:
            return err(400, 'Message with id {} is not a draft'.
                       format(draft_public_id))

        if not draft.to_addr:
            return err(400, "No 'to:' recipients specified")

        if not draft.is_latest:
            return err(409, 'Draft {} has already been updated to {}'.format(
                draft_public_id, g.encoder.cereal(draft.most_recent_revision)))

        schedule_action('send_draft', draft, g.namespace.id, g.db_session)
    else:
        to = data.get('to')
        cc = data.get('cc')
        bcc = data.get('bcc')
        subject = data.get('subject')
        body = data.get('body')
        try:
            tags = get_tags(data.get('tags'), g.namespace.id, g.db_session)
            files = get_attachments(data.get('files'), g.namespace.id,
                                    g.db_session)
            replyto_thread = get_thread(data.get('reply_to_thread'),
                                        g.namespace.id, g.db_session)
        except InputError as e:
            return err(404, e.message)

        draft = sendmail.create_draft(g.db_session, g.namespace.account, to,
                                      subject, body, files, cc, bcc,
                                      tags, replyto_thread, syncback=False)
        schedule_action('send_directly', draft, g.namespace.id, g.db_session)

    draft.state = 'sending'
    return g.encoder.jsonify(draft)
Exemple #27
0
def contact_read_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, "Invalid contact id {}".format(public_id))
    # TODO auth with account object
    # Get all data for an existing contact.
    result = contacts.crud.read(g.namespace, g.db_session, public_id)
    if result is None:
        return err(404, "Couldn't find contact with id {0}".format(public_id))
    return g.encoder.jsonify(result)
Exemple #28
0
def calendar_read_api(public_id):
    """Get all data for an existing calendar."""
    valid_public_id(public_id)

    try:
        calendar = g.db_session.query(Calendar).filter(
            Calendar.public_id == public_id,
            Calendar.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find calendar {0}".format(public_id))
    return g.encoder.jsonify(calendar)
Exemple #29
0
def thread_api_update(public_id):
    try:
        valid_public_id(public_id)
        thread = g.db_session.query(Thread).filter(
            Thread.public_id == public_id,
            Thread.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find thread `{0}` ".format(public_id))
    data = request.get_json(force=True)
    update_thread(thread, data, g.db_session)
    return g.encoder.jsonify(thread)
Exemple #30
0
def file_read_api(public_id):
    try:
        valid_public_id(public_id)
        f = g.db_session.query(Block).filter(
            Block.public_id == public_id).one()
        return g.encoder.jsonify(f)
    except InputError:
        return err(400, 'Invalid file id {}'.format(public_id))
    except NoResultFound:
        return err(404, "Couldn't find file with id {0} "
                   "on namespace {1}".format(public_id, g.namespace_public_id))
Exemple #31
0
def start():
    g.db_session = InboxSession(engine)

    g.log = get_logger()
    try:
        valid_public_id(g.namespace_public_id)
        g.namespace = g.db_session.query(Namespace) \
            .filter(Namespace.public_id == g.namespace_public_id).one()

        g.encoder = APIEncoder(g.namespace.public_id)
    except (NoResultFound, InputError):
        return err(404, "Couldn't find namespace with id `{0}` ".format(
            g.namespace_public_id))

    g.parser = reqparse.RequestParser(argument_class=ValidatableArgument)
    g.parser.add_argument('limit', default=DEFAULT_LIMIT, type=limit,
                          location='args')
    g.parser.add_argument('offset', default=0, type=int, location='args')
Exemple #32
0
def event_delete_api(public_id):
    g.parser.add_argument('notify_participants',
                          type=strict_bool,
                          location='args')
    args = strict_parse_args(g.parser, request.args)
    notify_participants = args['notify_participants']

    valid_public_id(public_id)
    try:
        event = g.db_session.query(Event).filter_by(
            public_id=public_id, namespace_id=g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find event {0}".format(public_id))
    if event.calendar.read_only:
        raise InputError(
            'Cannot delete event {} from read_only calendar.'.format(
                public_id))

    # Set the local event status to 'cancelled' rather than deleting it,
    # in order to be consistent with how we sync deleted events from the
    # remote, and consequently return them through the events, delta sync APIs
    event.sequence_number += 1
    event.status = 'cancelled'
    g.db_session.commit()

    account = g.namespace.account

    # FIXME @karim: do this in the syncback thread instead.
    if notify_participants and account.provider != 'gmail':
        ical_file = generate_icalendar_invite(event,
                                              invite_type='cancel').to_ical()

        send_invite(ical_file, event, account, invite_type='cancel')

    schedule_action('delete_event',
                    event,
                    g.namespace.id,
                    g.db_session,
                    event_uid=event.uid,
                    calendar_name=event.calendar.name,
                    calendar_uid=event.calendar.uid,
                    notify_participants=notify_participants)

    return g.encoder.jsonify(None)
Exemple #33
0
def event_read_api(public_id):
    """Get all data for an existing event."""
    valid_public_id(public_id)
    g.parser.add_argument('participant_id',
                          type=valid_public_id,
                          location='args')
    g.parser.add_argument('action', type=valid_event_action, location='args')
    g.parser.add_argument('rsvp', type=valid_rsvp, location='args')
    args = strict_parse_args(g.parser, request.args)

    # FIXME karim -- re-enable this after landing the participants refactor (T687)
    #if 'action' in args:
    #    # Participants are able to RSVP to events by clicking on links (e.g.
    #    # that are emailed to them). Therefore, the RSVP action is invoked via
    #    # a GET.
    #    if args['action'] == 'rsvp':
    #        try:
    #            participant_id = args.get('participant_id')
    #            if not participant_id:
    #                return err(404, "Must specify a participant_id with rsvp")

    #            participant = g.db_session.query(Participant).filter_by(
    #                public_id=participant_id).one()

    #            participant.status = args['rsvp']
    #            g.db_session.commit()

    #            result = events.crud.read(g.namespace, g.db_session,
    #                                      public_id)

    #            if result is None:
    #                return err(404, "Couldn't find event with id {0}"
    #                           .format(public_id))

    #            return g.encoder.jsonify(result)
    #        except NoResultFound:
    #            return err(404, "Couldn't find participant with id `{0}` "
    #                       .format(participant_id))

    result = events.crud.read(g.namespace, g.db_session, public_id)
    if result is None:
        raise NotFoundError("Couldn't find event id {0}".format(public_id))
    return g.encoder.jsonify(result)
Exemple #34
0
def auth():
    """ Check for account ID on all non-root URLS """
    if request.path in ('/accounts', '/accounts/', '/', '/n', '/n/') \
            or request.path.startswith('/w/'):
        return

    if request.path.startswith('/n/'):
        ns_parts = filter(None, request.path.split('/'))
        namespace_public_id = ns_parts[1]
        valid_public_id(namespace_public_id)

        with global_session_scope() as db_session:
            try:
                namespace = db_session.query(Namespace) \
                    .filter(Namespace.public_id == namespace_public_id).one()
                g.namespace_id = namespace.id
            except NoResultFound:
                return err(404, "Unknown namespace ID")

    else:
        if not request.authorization or not request.authorization.username:
            return make_response(("Could not verify access credential.", 401, {
                'WWW-Authenticate':
                'Basic realm="API '
                'Access Token Required"'
            }))

        namespace_public_id = request.authorization.username

        with global_session_scope() as db_session:
            try:
                valid_public_id(namespace_public_id)
                namespace = db_session.query(Namespace) \
                    .filter(Namespace.public_id == namespace_public_id).one()
                g.namespace_id = namespace.id
                g.account_id = namespace.account.id
            except NoResultFound:
                return make_response(
                    ("Could not verify access credential.", 401, {
                        'WWW-Authenticate':
                        'Basic realm="API '
                        'Access Token Required"'
                    }))
Exemple #35
0
def parse_folder(request_data, db_session, namespace_id):
    # TODO deprecate being able to post "folder" and not "folder_id"
    if 'folder_id' not in request_data and 'folder' not in request_data:
        return
    folder_public_id = request_data.pop('folder_id', None) or \
        request_data.pop('folder', None)
    if folder_public_id is None:
        # One of 'folder_id'/ 'folder' was present AND set to None.
        # Not allowed.
        raise InputError('Removing all folders is not allowed.')

    valid_public_id(folder_public_id)
    try:
        return db_session.query(Category). \
            filter(Category.namespace_id == namespace_id,
                   Category.public_id == folder_public_id).one()
    except NoResultFound:
        raise InputError(u'The folder {} does not exist'.
                         format(folder_public_id))
def calendar_update(account_public_id):
    g.log.info('Received request to update Google calendar list',
               account_public_id=account_public_id)
    try:
        valid_public_id(account_public_id)
        with global_session_scope() as db_session:
            account = db_session.query(GmailAccount) \
                .filter(GmailAccount.public_id == account_public_id) \
                .one()
            account.handle_gpush_notification()
            db_session.commit()
        return resp(200)
    except ValueError:
        raise InputError('Invalid public ID')
    except NoResultFound:
        g.log.info('Getting push notifications for non-existing account',
                   account_public_id=account_public_id)
        raise NotFoundError(
            "Couldn't find account `{0}`".format(account_public_id))
Exemple #37
0
def event_delete_api(public_id):
    valid_public_id(public_id)
    try:
        event = g.db_session.query(Event).filter_by(
            public_id=public_id,
            namespace_id=g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find event {0}".format(public_id))
    if event.calendar.read_only:
        raise InputError('Cannot delete event {} from read_only '
                         'calendar.'.format(public_id))

    schedule_action('delete_event', event, g.namespace.id, g.db_session,
                    event_uid=event.uid,
                    calendar_name=event.calendar.name,
                    calendar_uid=event.calendar.uid)
    g.db_session.delete(event)
    g.db_session.commit()
    return g.encoder.jsonify(None)
Exemple #38
0
def file_delete_api(public_id):
    valid_public_id(public_id)
    try:
        f = g.db_session.query(Block).filter(
            Block.public_id == public_id,
            Block.namespace_id == g.namespace.id).one()

        if g.db_session.query(Block).join(Part) \
                .filter(Block.public_id == public_id).first() is not None:
            raise InputError("Can't delete file that is attachment.")

        g.db_session.delete(f)
        g.db_session.commit()

        # This is essentially what our other API endpoints do after deleting.
        # Effectively no error == success
        return g.encoder.jsonify(None)
    except NoResultFound:
        raise NotFoundError("Couldn't find file {0} ".format(public_id))
Exemple #39
0
def tag_create_api():
    data = request.get_json(force=True)
    if not ('name' in data.keys() and isinstance(data['name'], basestring)):
        return err(400, 'Malformed tag request')
    if 'namespace_id' in data.keys():
        ns_id = data['namespace_id']
        valid_public_id(ns_id)
        if ns_id != g.namespace.id:
            return err(400, 'Cannot change the namespace on a tag.')
    # Lowercase tag name, regardless of input casing.
    tag_name = data['name'].lower()
    if not Tag.name_available(tag_name, g.namespace.id, g.db_session):
        return err(409, 'Tag name not available')
    if len(tag_name) > MAX_INDEXABLE_LENGTH:
        return err(400, 'Tag name is too long.')

    tag = Tag(name=tag_name, namespace=g.namespace, user_created=True)
    g.db_session.commit()
    return g.encoder.jsonify(tag)
Exemple #40
0
def draft_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))

    data = request.get_json(force=True)
    if data.get('version') is None:
        return err(400, 'Must specify version to update')

    version = data.get('version')
    original_draft = g.db_session.query(Message).filter(
        Message.public_id == public_id).first()
    if original_draft is None or not original_draft.is_draft or \
            original_draft.namespace.id != g.namespace.id:
        return err(404, 'No draft with public id {}'.format(public_id))
    if original_draft.version != version:
        return err(
            409, 'Draft {0}.{1} has already been updated to version '
            '{2}'.format(public_id, version, original_draft.version))

    # TODO(emfree): what if you try to update a draft on a *thread* that's been
    # deleted?

    data = request.get_json(force=True)

    to = data.get('to')
    cc = data.get('cc')
    bcc = data.get('bcc')
    subject = data.get('subject')
    body = data.get('body')
    try:
        tags = get_tags(data.get('tags'), g.namespace.id, g.db_session)
        files = get_attachments(data.get('files'), g.namespace.id,
                                g.db_session)
    except InputError as e:
        return err(404, e.message)

    draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                  original_draft, to, subject, body, files, cc,
                                  bcc, tags)
    return g.encoder.jsonify(draft)
Exemple #41
0
def file_download_api(public_id):
    valid_public_id(public_id)
    try:
        f = g.db_session.query(Block).filter(
            Block.public_id == public_id,
            Block.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find file {0} ".format(public_id))

    # Here we figure out the filename.extension given the
    # properties which were set on the original attachment
    # TODO consider using werkzeug.secure_filename to sanitize?

    if f.content_type:
        ct = f.content_type.lower()
    else:
        # TODO Detect the content-type using the magic library
        # and set ct = the content type, which is used below
        g.log.error("Content type not set! Defaulting to text/plain")
        ct = 'text/plain'

    if f.filename:
        name = f.filename
    else:
        g.log.debug("No filename. Generating...")
        if ct in common_extensions:
            name = 'attachment.{0}'.format(common_extensions[ct])
        else:
            g.log.error("Unknown extension for content-type: {0}"
                        .format(ct))
            # HACK just append the major part of the content type
            name = 'attachment.{0}'.format(ct.split('/')[0])

    # TODO the part.data object should really behave like a stream we can read
    # & write to
    response = make_response(f.data)

    response.headers['Content-Type'] = 'application/octet-stream'  # ct
    response.headers['Content-Disposition'] = \
        u"attachment; filename={0}".format(name)
    g.log.info(response.headers)
    return response
Exemple #42
0
def thread_api_update(public_id):
    try:
        valid_public_id(public_id)
        thread = g.db_session.query(Thread).filter(
            Thread.public_id == public_id,
            Thread.namespace_id == g.namespace.id).one()
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))
    except NoResultFound:
        return err(
            404, "Couldn't find thread with id `{0}` "
            "on namespace {1}".format(public_id, g.namespace_public_id))
    data = request.get_json(force=True)
    if not set(data).issubset({'add_tags', 'remove_tags'}):
        return err(400, 'Can only add or remove tags from thread.')

    removals = data.get('remove_tags', [])

    # TODO(emfree) possibly also support adding/removing tags by tag public id,
    # not just name.

    for tag_name in removals:
        tag = g.db_session.query(Tag).filter(
            Tag.namespace_id == g.namespace.id, Tag.name == tag_name).first()
        if tag is None:
            return err(404, 'No tag found with name {}'.format(tag_name))
        if not tag.user_removable:
            return err(400, 'Cannot remove tag {}'.format(tag_name))
        thread.remove_tag(tag, execute_action=True)

    additions = data.get('add_tags', [])
    for tag_name in additions:
        tag = g.db_session.query(Tag).filter(
            Tag.namespace_id == g.namespace.id, Tag.name == tag_name).first()
        if tag is None:
            return err(404, 'No tag found with name {}'.format(tag_name))
        if not tag.user_addable:
            return err(400, 'Cannot add tag {}'.format(tag_name))
        thread.apply_tag(tag, execute_action=True)

    g.db_session.commit()
    return g.encoder.jsonify(thread)
Exemple #43
0
def auth():
    """ Check for account ID on all non-root URLS """
    if request.path in ('/accounts', '/accounts/', '/') \
            or request.path.startswith('/w/') \
            or request.path.startswith('/c/'):
        return

    if not request.authorization or not request.authorization.username:

        AUTH_ERROR_MSG = ("Could not verify access credential.", 401, {
            'WWW-Authenticate':
            'Basic realm="API '
            'Access Token Required"'
        })

        auth_header = request.headers.get('Authorization', None)

        if not auth_header:
            return make_response(AUTH_ERROR_MSG)

        parts = auth_header.split()

        if (len(parts) != 2 or parts[0].lower() != 'bearer' or not parts[1]):
            return make_response(AUTH_ERROR_MSG)
        namespace_public_id = parts[1]

    else:
        namespace_public_id = request.authorization.username

    with global_session_scope() as db_session:
        try:
            valid_public_id(namespace_public_id)
            namespace = db_session.query(Namespace) \
                .filter(Namespace.public_id == namespace_public_id).one()
            g.namespace_id = namespace.id
            g.account_id = namespace.account.id
        except NoResultFound:
            return make_response(("Could not verify access credential.", 401, {
                'WWW-Authenticate':
                'Basic realm="API '
                'Access Token Required"'
            }))
Exemple #44
0
def parse_labels(request_data, db_session, namespace_id):
    label_public_ids = request_data.pop('labels', None)
    if label_public_ids is None:
        return
    # TODO(emfree): Use a real JSON schema validator for this sort of thing.
    if not isinstance(label_public_ids, list):
        raise InputError('"labels" must be a list')
    for id_ in label_public_ids:
        valid_public_id(id_)

    labels = set()
    for id_ in label_public_ids:
        try:
            cat = db_session.query(Category).filter(
                Category.namespace_id == namespace_id,
                Category.public_id == id_).one()
            labels.add(cat)
        except NoResultFound:
            raise InputError(u'The label {} does not exist'.format(id_))
    return labels
Exemple #45
0
def tag_delete_api(public_id):
    try:
        valid_public_id(public_id)
        t = g.db_session.query(Tag).filter(Tag.public_id == public_id).one()

        if not t.user_created:
            return err(400, "Can't delete non user-created tag.")

        g.db_session.delete(t)
        g.db_session.commit()

        # This is essentially what our other API endpoints do after deleting.
        # Effectively no error == success
        return g.encoder.jsonify(None)
    except InputError:
        return err(400, 'Invalid tag id {}'.format(public_id))
    except NoResultFound:
        return err(
            404, "Couldn't find tag with id {0} "
            "on namespace {1}".format(public_id, g.namespace_public_id))
Exemple #46
0
def raw_message_api(public_id):
    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id,
            Message.namespace_id == g.namespace.id).one()
    except InputError:
        return err(400, 'Invalid message id {}'.format(public_id))
    except NoResultFound:
        return err(404,
                   "Couldn't find raw message with id {0} "
                   "on namespace {1}".format(public_id, g.namespace_public_id))

    if message.full_body is None:
        return err(404,
                   "Couldn't find raw message with id {0} "
                   "on namespace {1}".format(public_id, g.namespace_public_id))

    b64_contents = base64.b64encode(message.full_body.data)
    return g.encoder.jsonify({"rfc2822": b64_contents})
Exemple #47
0
def raw_message_api(public_id):
    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id,
            Message.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find message {0}".format(public_id))

    if message.full_body is None:
        raise NotFoundError("Couldn't find message {0}".format(public_id))

    if message.full_body is not None:
        b64_contents = base64.b64encode(message.full_body.data)
    else:
        g.log.error("Message without full_body attribute: id='{0}'".format(
            message.id))
        raise NotFoundError(
            "Couldn't find raw contents for message `{0}` ".format(public_id))
    return g.encoder.jsonify({"rfc2822": b64_contents})
Exemple #48
0
def event_delete_api(public_id):
    try:
        valid_public_id(public_id)
        event = g.db_session.query(Event).filter_by(
            public_id=public_id). \
            options(subqueryload(Event.calendar)).one()
    except InputError:
        return err(400, 'Invalid event id {}'.format(public_id))
    except NoResultFound:
        return err(404, 'No event found with public_id {}'.
                   format(public_id))
    if event.namespace != g.namespace:
        return err(404, 'No event found with public_id {}'.
                   format(public_id))
    if event.calendar.read_only:
        return err(404, 'Cannot delete event with public_id {} from '
                   ' read_only calendar.'.format(public_id))

    result = events.crud.delete(g.namespace, g.db_session, public_id)
    schedule_action('delete_event', event, g.namespace.id, g.db_session)
    return g.encoder.jsonify(result)
def event_update(calendar_public_id):
    request.environ["log_context"]["calendar_public_id"] = calendar_public_id
    try:
        valid_public_id(calendar_public_id)
        allowed, tokens, sleep = limitlion.throttle(
            "gcal:{}".format(calendar_public_id), rps=0.5
        )
        if allowed:
            with global_session_scope() as db_session:
                calendar = (
                    db_session.query(Calendar)
                    .filter(Calendar.public_id == calendar_public_id)
                    .one()
                )
                calendar.handle_gpush_notification()
                db_session.commit()
        return resp(200)
    except ValueError:
        raise InputError("Invalid public ID")
    except NoResultFound:
        raise NotFoundError("Couldn't find calendar `{0}`".format(calendar_public_id))
Exemple #50
0
def file_read_api(public_id):
    try:
        valid_public_id(public_id)
        f = g.db_session.query(Block).filter(
            Block.public_id == public_id).one()
        if hasattr(f, 'message'):
            assert int(f.message.namespace.id) == int(g.namespace.id)
            g.log.info(
                "block's message namespace matches api context namespace")
        else:
            # Block was likely uploaded via file API and not yet sent in a msg
            g.log.debug(
                "This block doesn't have a corresponding message: {}".format(
                    f.public_id))
        return g.encoder.jsonify(f)
    except InputError:
        return err(400, 'Invalid file id {}'.format(public_id))
    except NoResultFound:
        return err(
            404, "Couldn't find file with id {0} "
            "on namespace {1}".format(public_id, g.namespace_public_id))
Exemple #51
0
def message_read_api(public_id):
    g.parser.add_argument('view', type=view, location='args')
    args = strict_parse_args(g.parser, request.args)
    encoder = APIEncoder(g.namespace.public_id, args['view'] == 'expanded')
    try:
        valid_public_id(public_id)
        message = g.db_session.query(Message).filter(
            Message.public_id == public_id,
            Message.namespace_id == g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find message {0} ".format(public_id))
    if request.headers.get('Accept', None) == 'message/rfc822':
        if message.full_body is not None:
            return Response(message.full_body.data,
                            mimetype='message/rfc822')
        else:
            g.log.error("Message without full_body attribute: id='{0}'"
                        .format(message.id))
            raise NotFoundError(
                "Couldn't find raw contents for message `{0}` "
                .format(public_id))
    return encoder.jsonify(message)
Exemple #52
0
def folder_label_update_api(public_id):
    category_type = g.namespace.account.category_type
    valid_public_id(public_id)
    try:
        category = g.db_session.query(Category).filter(
            Category.namespace_id == g.namespace.id,
            Category.public_id == public_id).one()
    except NoResultFound:
        raise InputError("Couldn't find {} {}".format(category_type,
                                                      public_id))
    if category.name is not None:
        raise InputError("Cannot modify a standard {}".format(category_type))

    data = request.get_json(force=True)
    display_name = data.get('display_name')
    valid_display_name(g.namespace.id, category_type, display_name,
                       g.db_session)

    current_name = category.display_name
    category.display_name = display_name
    g.db_session.flush()

    if category_type == 'folder':
        schedule_action('update_folder',
                        category,
                        g.namespace.id,
                        g.db_session,
                        old_name=current_name)
    else:
        schedule_action('update_label',
                        category,
                        g.namespace.id,
                        g.db_session,
                        old_name=current_name)

    # TODO[k]: Update corresponding folder/ label once syncback is successful,
    # rather than waiting for sync to pick it up?

    return g.encoder.jsonify(category)
Exemple #53
0
def event_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid event id {}'.format(public_id))
    data = request.get_json(force=True)

    try:
        valid_event_update(data, g.namespace, g.db_session)
    except InputError as e:
        return err(404, str(e))

    # Convert the data into our types where necessary
    # e.g. timestamps, participant_list
    if 'start' in data:
        data['start'] = datetime.utcfromtimestamp(int(data.get('start')))
    if 'end' in data:
        data['end'] = datetime.utcfromtimestamp(int(data.get('end')))
    if 'participants' in data:
        data['participant_list'] = []
        for p in data['participants']:
            if 'status' not in p:
                p['status'] = 'noreply'
            data['participant_list'].append(p)
        del data['participants']

    try:
        result = events.crud.update(g.namespace, g.db_session,
                                    public_id, data)
    except InputError as e:
        return err(400, str(e))

    if result is None:
        return err(404, "Couldn't find event with id {0}".
                   format(public_id))

    schedule_action('update_event', result, g.namespace.id, g.db_session)
    return g.encoder.jsonify(result)
Exemple #54
0
def auth_user(request):
    """
        Authentication for user-specific routes, for example
        getting messages for one user
    """
    if not request.authorization or not request.authorization.username:
        AUTH_ERROR_MSG = ("Could not verify access credential.", 401, {
            'WWW-Authenticate':
            'Basic realm="API '
            'Access Token Required"'
        })

        auth_header = request.headers.get('Authorization', None)

        if not auth_header:
            return make_response(AUTH_ERROR_MSG)

        parts = auth_header.split()

        if len(parts) != 2 or parts[0].lower() != 'bearer' or not parts[1]:
            return make_response(AUTH_ERROR_MSG)
        namespace_public_id = parts[1]
    else:
        namespace_public_id = request.authorization.username

    with global_session_scope() as db_session:
        try:
            valid_public_id(namespace_public_id)
            namespace = db_session.query(Namespace) \
                .filter(Namespace.public_id == namespace_public_id).one()
            g.namespace_id = namespace.id
            g.account_id = namespace.account.id
        except NoResultFound:
            return make_response(("Could not verify access credential.", 401, {
                'WWW-Authenticate':
                'Basic realm="API '
                'Access Token Required"'
            }))
Exemple #55
0
def event_delete_api(public_id):
    valid_public_id(public_id)
    try:
        event = g.db_session.query(Event).filter_by(
            public_id=public_id,
            namespace_id=g.namespace.id).one()
    except NoResultFound:
        raise NotFoundError("Couldn't find event {0}".format(public_id))
    if event.calendar.read_only:
        raise InputError('Cannot delete event {} from read_only calendar.'.
                         format(public_id))

    # Set the local event status to 'cancelled' rather than deleting it,
    # in order to be consistent with how we sync deleted events from the
    # remote, and consequently return them through the events, delta sync APIs
    event.status = 'cancelled'
    g.db_session.commit()

    schedule_action('delete_event', event, g.namespace.id, g.db_session,
                    event_uid=event.uid, calendar_name=event.calendar.name,
                    calendar_uid=event.calendar.uid)

    return g.encoder.jsonify(None)
Exemple #56
0
def tag_read_update_api(public_id):
    try:
        valid_public_id(public_id)
        tag = g.db_session.query(Tag).filter(
            Tag.public_id == public_id,
            Tag.namespace_id == g.namespace.id).one()
    except ValueError:
        return err(400, '{} is not a valid id'.format(public_id))
    except NoResultFound:
        return err(404, 'No tag found')
    if request.method == 'GET':
        return g.encoder.jsonify(tag)
    elif request.method == 'PUT':
        data = request.get_json(force=True)
        if data.keys() != ['name']:
            return err(400, 'Malformed tag update request')
        if not tag.user_created:
            return err(403, 'Cannot modify tag {}'.format(public_id))
        new_name = data['name']
        if not Tag.name_available(new_name, g.namespace.id, g.db_session):
            return err(409, 'Tag name already used')
        tag.name = new_name
        g.db_session.commit()
        return g.encoder.jsonify(tag)
Exemple #57
0
def messages_or_drafts(namespace_id, drafts, subject, from_addr, to_addr,
                       cc_addr, bcc_addr, any_email, thread_public_id,
                       started_before, started_after, last_message_before,
                       last_message_after, received_before, received_after,
                       filename, in_, unread, starred, limit, offset, view,
                       db_session):
    # Warning: complexities ahead. This function sets up the query that gets
    # results for the /messages API. It loads from several tables, supports a
    # variety of views and filters, and is performance-critical for the API. As
    # such, it is not super simple.
    #
    # We bake the generated query to avoid paying query compilation overhead on
    # every request. This requires some attention: every parameter that can
    # vary between calls *must* be inserted via bindparam(), or else the first
    # value passed will be baked into the query and reused on each request.
    # Subqueries (on contact tables) can't be properly baked, so we have to
    # call query.spoil() on those code paths.

    param_dict = {
        'namespace_id': namespace_id,
        'drafts': drafts,
        'subject': subject,
        'from_addr': from_addr,
        'to_addr': to_addr,
        'cc_addr': cc_addr,
        'bcc_addr': bcc_addr,
        'any_email': any_email,
        'thread_public_id': thread_public_id,
        'received_before': received_before,
        'received_after': received_after,
        'started_before': started_before,
        'started_after': started_after,
        'last_message_before': last_message_before,
        'last_message_after': last_message_after,
        'filename': filename,
        'in_': in_,
        'unread': unread,
        'starred': starred,
        'limit': limit,
        'offset': offset
    }

    if view == 'count':
        query = bakery(lambda s: s.query(func.count(Message.id)))
    elif view == 'ids':
        query = bakery(lambda s: s.query(Message.public_id))
    else:
        query = bakery(lambda s: s.query(Message))
    query += lambda q: q.join(Thread)
    query += lambda q: q.filter(
        Message.namespace_id == bindparam('namespace_id'), Message.is_draft ==
        bindparam('drafts'))

    if subject is not None:
        query += lambda q: q.filter(Message.subject == bindparam('subject'))

    if unread is not None:
        query += lambda q: q.filter(Message.is_read != bindparam('unread'))

    if starred is not None:
        query += lambda q: q.filter(Message.is_starred == bindparam('starred'))

    if thread_public_id is not None:
        query += lambda q: q.filter(Thread.public_id == bindparam(
            'thread_public_id'))

    # TODO: deprecate thread-oriented date filters on message endpoints.
    if started_before is not None:
        query += lambda q: q.filter(
            Thread.subjectdate < bindparam('started_before'), Thread.
            namespace_id == bindparam('namespace_id'))

    if started_after is not None:
        query += lambda q: q.filter(
            Thread.subjectdate > bindparam('started_after'), Thread.
            namespace_id == bindparam('namespace_id'))

    if last_message_before is not None:
        query += lambda q: q.filter(
            Thread.recentdate < bindparam('last_message_before'), Thread.
            namespace_id == bindparam('namespace_id'))

    if last_message_after is not None:
        query += lambda q: q.filter(
            Thread.recentdate > bindparam('last_message_after'), Thread.
            namespace_id == bindparam('namespace_id'))

    if received_before is not None:
        query += lambda q: q.filter(Message.received_date <= bindparam(
            'received_before'))

    if received_after is not None:
        query += lambda q: q.filter(Message.received_date > bindparam(
            'received_after'))

    if to_addr is not None:
        query.spoil()
        to_query = db_session.query(MessageContactAssociation.message_id). \
            join(Contact).filter(
                MessageContactAssociation.field == 'to_addr',
                Contact.email_address == to_addr,
                Contact.namespace_id == bindparam('namespace_id')).subquery()
        query += lambda q: q.filter(Message.id.in_(to_query))

    if from_addr is not None:
        query.spoil()
        from_query = db_session.query(MessageContactAssociation.message_id). \
            join(Contact).filter(
                MessageContactAssociation.field == 'from_addr',
                Contact.email_address == from_addr,
                Contact.namespace_id == bindparam('namespace_id')).subquery()
        query += lambda q: q.filter(Message.id.in_(from_query))

    if cc_addr is not None:
        query.spoil()
        cc_query = db_session.query(MessageContactAssociation.message_id). \
            join(Contact).filter(
                MessageContactAssociation.field == 'cc_addr',
                Contact.email_address == cc_addr,
                Contact.namespace_id == bindparam('namespace_id')).subquery()
        query += lambda q: q.filter(Message.id.in_(cc_query))

    if bcc_addr is not None:
        query.spoil()
        bcc_query = db_session.query(MessageContactAssociation.message_id). \
            join(Contact).filter(
                MessageContactAssociation.field == 'bcc_addr',
                Contact.email_address == bcc_addr,
                Contact.namespace_id == bindparam('namespace_id')).subquery()
        query += lambda q: q.filter(Message.id.in_(bcc_query))

    if any_email is not None:
        query.spoil()
        any_email_query = db_session.query(
            MessageContactAssociation.message_id).join(Contact). \
            filter(Contact.email_address.in_(any_email),
                   Contact.namespace_id == bindparam('namespace_id')). \
            subquery()
        query += lambda q: q.filter(Message.id.in_(any_email_query))

    if filename is not None:
        query += lambda q: q.join(Part).join(Block). \
            filter(Block.filename == bindparam('filename'),
                   Block.namespace_id == bindparam('namespace_id'))

    if in_ is not None:
        query.spoil()
        category_filters = [
            Category.name == bindparam('in_'),
            Category.display_name == bindparam('in_')
        ]
        try:
            valid_public_id(in_)
            category_filters.append(Category.public_id == bindparam('in_id'))
            # Type conversion and bindparams interact poorly -- you can't do
            # e.g.
            # query.filter(or_(Category.name == bindparam('in_'),
            #                  Category.public_id == bindparam('in_')))
            # because the binary conversion defined by Category.public_id will
            # be applied to the bound value prior to its insertion in the
            # query. So we define another bindparam for the public_id:
            param_dict['in_id'] = in_
        except InputError:
            pass
        query += lambda q: q.join(MessageCategory).join(Category). \
            filter(Category.namespace_id == namespace_id,
                   or_(*category_filters))

    if view == 'count':
        res = query(db_session).params(**param_dict).one()[0]
        return {"count": res}

    query += lambda q: q.order_by(desc(Message.received_date))
    query += lambda q: q.limit(bindparam('limit'))
    if offset:
        query += lambda q: q.offset(bindparam('offset'))

    if view == 'ids':
        res = query(db_session).params(**param_dict).all()
        return [x[0] for x in res]

    # Eager-load related attributes to make constructing API representations
    # faster. Note that we don't use the options defined by
    # Message.api_loading_options() here because we already have a join to the
    # thread table. We should eventually try to simplify this.
    query += lambda q: q.options(
        contains_eager(Message.thread),
        subqueryload(Message.messagecategories).joinedload('category'),
        subqueryload(Message.parts).joinedload(Part.block),
        subqueryload(Message.events))

    prepared = query(db_session).params(**param_dict)
    return prepared.all()
Exemple #58
0
def threads(namespace_id, subject, from_addr, to_addr, cc_addr, bcc_addr,
            any_email, thread_public_id, started_before, started_after,
            last_message_before, last_message_after, filename, in_, unread,
            starred, limit, offset, view, db_session):

    if view == 'count':
        query = db_session.query(func.count(Thread.id))
    elif view == 'ids':
        query = db_session.query(Thread.public_id)
    else:
        query = db_session.query(Thread)

    filters = [Thread.namespace_id == namespace_id]
    if thread_public_id is not None:
        filters.append(Thread.public_id == thread_public_id)

    if started_before is not None:
        filters.append(Thread.subjectdate < started_before)

    if started_after is not None:
        filters.append(Thread.subjectdate > started_after)

    if last_message_before is not None:
        filters.append(Thread.recentdate < last_message_before)

    if last_message_after is not None:
        filters.append(Thread.recentdate > last_message_after)

    if subject is not None:
        filters.append(Thread.subject == subject)

    query = query.filter(*filters)

    if from_addr is not None:
        from_query = db_session.query(Message.thread_id). \
            join(MessageContactAssociation).join(Contact).filter(
                Contact.email_address == from_addr,
                Contact.namespace_id == namespace_id,
                MessageContactAssociation.field == 'from_addr').subquery()
        query = query.filter(Thread.id.in_(from_query))

    if to_addr is not None:
        to_query = db_session.query(Message.thread_id). \
            join(MessageContactAssociation).join(Contact).filter(
                Contact.email_address == to_addr,
                Contact.namespace_id == namespace_id,
                MessageContactAssociation.field == 'to_addr').subquery()
        query = query.filter(Thread.id.in_(to_query))

    if cc_addr is not None:
        cc_query = db_session.query(Message.thread_id). \
            join(MessageContactAssociation).join(Contact).filter(
                Contact.email_address == cc_addr,
                Contact.namespace_id == namespace_id,
                MessageContactAssociation.field == 'cc_addr').subquery()
        query = query.filter(Thread.id.in_(cc_query))

    if bcc_addr is not None:
        bcc_query = db_session.query(Message.thread_id). \
            join(MessageContactAssociation).join(Contact).filter(
                Contact.email_address == bcc_addr,
                Contact.namespace_id == namespace_id,
                MessageContactAssociation.field == 'bcc_addr').subquery()
        query = query.filter(Thread.id.in_(bcc_query))

    if any_email is not None:
        any_contact_query = db_session.query(Message.thread_id). \
            join(MessageContactAssociation).join(Contact). \
            filter(Contact.email_address.in_(any_email),
                   Contact.namespace_id == namespace_id).subquery()
        query = query.filter(Thread.id.in_(any_contact_query))

    if filename is not None:
        files_query = db_session.query(Message.thread_id). \
            join(Part).join(Block). \
            filter(Block.filename == filename,
                   Block.namespace_id == namespace_id). \
            subquery()
        query = query.filter(Thread.id.in_(files_query))

    if in_ is not None:
        category_filters = [Category.name == in_, Category.display_name == in_]
        try:
            valid_public_id(in_)
            category_filters.append(Category.public_id == in_)
        except InputError:
            pass
        category_query = db_session.query(Message.thread_id). \
            join(MessageCategory).join(Category). \
            filter(Category.namespace_id == namespace_id,
                   or_(*category_filters)).subquery()
        query = query.filter(Thread.id.in_(category_query))

    if unread is not None:
        read = not unread
        unread_query = db_session.query(Message.thread_id).filter(
            Message.namespace_id == namespace_id,
            Message.is_read == read).subquery()
        query = query.filter(Thread.id.in_(unread_query))

    if starred is not None:
        starred_query = db_session.query(Message.thread_id).filter(
            Message.namespace_id == namespace_id,
            Message.is_starred == starred).subquery()
        query = query.filter(Thread.id.in_(starred_query))

    if view == 'count':
        return {"count": query.one()[0]}

    # Eager-load some objects in order to make constructing API
    # representations faster.
    if view != 'ids':
        expand = (view == 'expanded')
        query = query.options(*Thread.api_loading_options(expand))

    query = query.order_by(desc(Thread.recentdate)).limit(limit)

    if offset:
        query = query.offset(offset)

    if view == 'ids':
        return [x[0] for x in query.all()]

    return query.all()
Exemple #59
0
def messages_or_drafts(
    namespace_id,
    drafts,
    subject,
    from_addr,
    to_addr,
    cc_addr,
    bcc_addr,
    any_email,
    thread_public_id,
    started_before,
    started_after,
    last_message_before,
    last_message_after,
    received_before,
    received_after,
    filename,
    in_,
    unread,
    starred,
    limit,
    offset,
    view,
    db_session,
):
    # Warning: complexities ahead. This function sets up the query that gets
    # results for the /messages API. It loads from several tables, supports a
    # variety of views and filters, and is performance-critical for the API. As
    # such, it is not super simple.
    #
    # We bake the generated query to avoid paying query compilation overhead on
    # every request. This requires some attention: every parameter that can
    # vary between calls *must* be inserted via bindparam(), or else the first
    # value passed will be baked into the query and reused on each request.
    # Subqueries (on contact tables) can't be properly baked, so we have to
    # call query.spoil() on those code paths.

    param_dict = {
        "namespace_id": namespace_id,
        "drafts": drafts,
        "subject": subject,
        "from_addr": from_addr,
        "to_addr": to_addr,
        "cc_addr": cc_addr,
        "bcc_addr": bcc_addr,
        "any_email": any_email,
        "thread_public_id": thread_public_id,
        "received_before": received_before,
        "received_after": received_after,
        "started_before": started_before,
        "started_after": started_after,
        "last_message_before": last_message_before,
        "last_message_after": last_message_after,
        "filename": filename,
        "in_": in_,
        "unread": unread,
        "starred": starred,
        "limit": limit,
        "offset": offset,
    }

    if view == "count":
        query = bakery(lambda s: s.query(func.count(Message.id)))
    elif view == "ids":
        query = bakery(lambda s: s.query(Message.public_id))
    else:
        query = bakery(lambda s: s.query(Message))

        # Sometimes MySQL doesn't pick the right index. In the case of a
        # regular /messages query, ix_message_ns_id_is_draft_received_date
        # is the best index because we always filter on
        # the namespace_id, is_draft and then order by received_date.
        # For other "exotic" queries, we let the MySQL query planner
        # pick the right index.
        if all(v is None for v in [
                subject,
                from_addr,
                to_addr,
                cc_addr,
                bcc_addr,
                any_email,
                thread_public_id,
                filename,
                in_,
                started_before,
                started_after,
                last_message_before,
                last_message_after,
        ]):
            query += lambda q: q.with_hint(
                Message,
                "FORCE INDEX (ix_message_ns_id_is_draft_received_date)",
                "mysql",
            )

    query += lambda q: q.join(Thread, Message.thread_id == Thread.id)
    query += lambda q: q.filter(
        Message.namespace_id == bindparam("namespace_id"),
        Message.is_draft == bindparam("drafts"),
        Thread.deleted_at == None,
    )

    if subject is not None:
        query += lambda q: q.filter(Message.subject == bindparam("subject"))

    if unread is not None:
        query += lambda q: q.filter(Message.is_read != bindparam("unread"))

    if starred is not None:
        query += lambda q: q.filter(Message.is_starred == bindparam("starred"))

    if thread_public_id is not None:
        query += lambda q: q.filter(Thread.public_id == bindparam(
            "thread_public_id"))

    # TODO: deprecate thread-oriented date filters on message endpoints.
    if started_before is not None:
        query += lambda q: q.filter(
            Thread.subjectdate < bindparam("started_before"),
            Thread.namespace_id == bindparam("namespace_id"),
        )

    if started_after is not None:
        query += lambda q: q.filter(
            Thread.subjectdate > bindparam("started_after"),
            Thread.namespace_id == bindparam("namespace_id"),
        )

    if last_message_before is not None:
        query += lambda q: q.filter(
            Thread.recentdate < bindparam("last_message_before"),
            Thread.namespace_id == bindparam("namespace_id"),
        )

    if last_message_after is not None:
        query += lambda q: q.filter(
            Thread.recentdate > bindparam("last_message_after"),
            Thread.namespace_id == bindparam("namespace_id"),
        )

    if received_before is not None:
        query += lambda q: q.filter(Message.received_date <= bindparam(
            "received_before"))

    if received_after is not None:
        query += lambda q: q.filter(Message.received_date > bindparam(
            "received_after"))

    if to_addr is not None:
        query.spoil()
        to_query = (db_session.query(
            MessageContactAssociation.message_id).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    MessageContactAssociation.field == "to_addr",
                    Contact.email_address == to_addr,
                    Contact.namespace_id == bindparam("namespace_id"),
                ).subquery())
        query += lambda q: q.filter(Message.id.in_(to_query))

    if from_addr is not None:
        query.spoil()
        from_query = (db_session.query(
            MessageContactAssociation.message_id).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    MessageContactAssociation.field == "from_addr",
                    Contact.email_address == from_addr,
                    Contact.namespace_id == bindparam("namespace_id"),
                ).subquery())
        query += lambda q: q.filter(Message.id.in_(from_query))

    if cc_addr is not None:
        query.spoil()
        cc_query = (db_session.query(
            MessageContactAssociation.message_id).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    MessageContactAssociation.field == "cc_addr",
                    Contact.email_address == cc_addr,
                    Contact.namespace_id == bindparam("namespace_id"),
                ).subquery())
        query += lambda q: q.filter(Message.id.in_(cc_query))

    if bcc_addr is not None:
        query.spoil()
        bcc_query = (db_session.query(
            MessageContactAssociation.message_id).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    MessageContactAssociation.field == "bcc_addr",
                    Contact.email_address == bcc_addr,
                    Contact.namespace_id == bindparam("namespace_id"),
                ).subquery())
        query += lambda q: q.filter(Message.id.in_(bcc_query))

    if any_email is not None:
        query.spoil()
        any_email_query = (db_session.query(
            MessageContactAssociation.message_id).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    Contact.email_address.in_(any_email),
                    Contact.namespace_id == bindparam("namespace_id"),
                ).subquery())
        query += lambda q: q.filter(Message.id.in_(any_email_query))

    if filename is not None:
        query += (lambda q: q.join(Part).join(Block).filter(
            Block.filename == bindparam("filename"),
            Block.namespace_id == bindparam("namespace_id"),
        ))

    if in_ is not None:
        query.spoil()
        category_filters = [
            Category.name == bindparam("in_"),
            Category.display_name == bindparam("in_"),
        ]
        try:
            valid_public_id(in_)
            category_filters.append(Category.public_id == bindparam("in_id"))
            # Type conversion and bindparams interact poorly -- you can't do
            # e.g.
            # query.filter(or_(Category.name == bindparam('in_'),
            #                  Category.public_id == bindparam('in_')))
            # because the binary conversion defined by Category.public_id will
            # be applied to the bound value prior to its insertion in the
            # query. So we define another bindparam for the public_id:
            param_dict["in_id"] = in_
        except InputError:
            pass
        query += (lambda q: q.prefix_with("STRAIGHT_JOIN").join(
            Message.messagecategories).join(MessageCategory.category).filter(
                Category.namespace_id == namespace_id, or_(*category_filters)))

    if view == "count":
        res = query(db_session).params(**param_dict).one()[0]
        return {"count": res}

    query += lambda q: q.order_by(desc(Message.received_date))
    query += lambda q: q.limit(bindparam("limit"))
    if offset:
        query += lambda q: q.offset(bindparam("offset"))

    if view == "ids":
        res = query(db_session).params(**param_dict).all()
        return [x[0] for x in res]

    # Eager-load related attributes to make constructing API representations
    # faster. Note that we don't use the options defined by
    # Message.api_loading_options() here because we already have a join to the
    # thread table. We should eventually try to simplify this.
    query += lambda q: q.options(
        contains_eager(Message.thread),
        subqueryload(Message.messagecategories).joinedload(
            "category", "created_at"),
        subqueryload(Message.parts).joinedload(Part.block),
        subqueryload(Message.events),
    )

    prepared = query(db_session).params(**param_dict)
    return prepared.all()
Exemple #60
0
def threads(
    namespace_id,
    subject,
    from_addr,
    to_addr,
    cc_addr,
    bcc_addr,
    any_email,
    message_id_header,
    thread_public_id,
    started_before,
    started_after,
    last_message_before,
    last_message_after,
    filename,
    in_,
    unread,
    starred,
    limit,
    offset,
    view,
    db_session,
):

    if view == "count":
        query = db_session.query(func.count(Thread.id))
    elif view == "ids":
        query = db_session.query(Thread.public_id)
    else:
        query = db_session.query(Thread)

    filters = [Thread.namespace_id == namespace_id, Thread.deleted_at == None]
    if thread_public_id is not None:
        filters.append(Thread.public_id == thread_public_id)

    if started_before is not None:
        filters.append(Thread.subjectdate < started_before)

    if started_after is not None:
        filters.append(Thread.subjectdate > started_after)

    if last_message_before is not None:
        filters.append(Thread.recentdate < last_message_before)

    if last_message_after is not None:
        filters.append(Thread.recentdate > last_message_after)

    if subject is not None:
        filters.append(Thread.subject == subject)

    query = query.filter(*filters)

    if from_addr is not None:
        from_query = contact_subquery(db_session, namespace_id, from_addr,
                                      "from_addr")
        query = query.filter(Thread.id.in_(from_query))

    if to_addr is not None:
        to_query = contact_subquery(db_session, namespace_id, to_addr,
                                    "to_addr")
        query = query.filter(Thread.id.in_(to_query))

    if cc_addr is not None:
        cc_query = contact_subquery(db_session, namespace_id, cc_addr,
                                    "cc_addr")
        query = query.filter(Thread.id.in_(cc_query))

    if bcc_addr is not None:
        bcc_query = contact_subquery(db_session, namespace_id, bcc_addr,
                                     "bcc_addr")
        query = query.filter(Thread.id.in_(bcc_query))

    if any_email is not None:
        any_contact_query = (db_session.query(
            Message.thread_id).join(MessageContactAssociation).join(
                Contact,
                MessageContactAssociation.contact_id == Contact.id).filter(
                    Contact.email_address.in_(any_email),
                    Contact.namespace_id == namespace_id,
                ).subquery())
        query = query.filter(Thread.id.in_(any_contact_query))

    if message_id_header is not None:
        message_id_query = db_session.query(Message.thread_id).filter(
            Message.message_id_header == message_id_header)
        query = query.filter(Thread.id.in_(message_id_query))

    if filename is not None:
        files_query = (db_session.query(
            Message.thread_id).join(Part).join(Block).filter(
                Block.filename == filename,
                Block.namespace_id == namespace_id).subquery())
        query = query.filter(Thread.id.in_(files_query))

    if in_ is not None:
        category_filters = [Category.name == in_, Category.display_name == in_]
        try:
            valid_public_id(in_)
            category_filters.append(Category.public_id == in_)
        except InputError:
            pass
        category_query = (db_session.query(
            Message.thread_id).prefix_with("STRAIGHT_JOIN").join(
                Message.messagecategories).join(
                    MessageCategory.category).filter(
                        Category.namespace_id == namespace_id,
                        or_(*category_filters)).subquery())
        query = query.filter(Thread.id.in_(category_query))

    if unread is not None:
        read = not unread
        unread_query = (db_session.query(Message.thread_id).filter(
            Message.namespace_id == namespace_id,
            Message.is_read == read).subquery())
        query = query.filter(Thread.id.in_(unread_query))

    if starred is not None:
        starred_query = (db_session.query(Message.thread_id).filter(
            Message.namespace_id == namespace_id,
            Message.is_starred == starred).subquery())
        query = query.filter(Thread.id.in_(starred_query))

    if view == "count":
        return {"count": query.one()[0]}

    # Eager-load some objects in order to make constructing API
    # representations faster.
    if view != "ids":
        expand = view == "expanded"
        query = query.options(*Thread.api_loading_options(expand))

    query = query.order_by(desc(Thread.recentdate)).limit(limit)

    if offset:
        query = query.offset(offset)

    if view == "ids":
        return [x[0] for x in query.all()]

    return query.all()