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)
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)
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)
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)
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)
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)
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))
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)
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"'}))
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)
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)
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))
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
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)
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)
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))
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)
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)
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))
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)
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)
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)
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)
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)
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)
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)
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))
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')
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)
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)
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"' }))
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))
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)
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))
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)
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)
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
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)
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"' }))
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
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))
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})
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})
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))
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))
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 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)
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"' }))
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)
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)
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()
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()
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()
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()