def valid_public_id(value): try: # raise ValueError on malformed public ids # raise TypeError if an integer is passed in int(value, 36) except (TypeError, ValueError): raise InputError(u'Invalid id: {}'.format(value)) return value
def valid_event(event): if 'when' not in event: raise InputError("Must specify 'when' when creating an event.") valid_when(event['when']) if 'busy' in event and not isinstance(event.get('busy'), bool): raise InputError("'busy' must be true or false") participants = event.get('participants', []) for p in participants: if 'email' not in p: raise InputError("'participants' must must have email") if 'status' in p: if p['status'] not in ('yes', 'no', 'maybe', 'noreply'): raise InputError("'participants' status must be one of: " "yes, no, maybe, noreply")
def valid_delta_object_types(types_arg): types = [item.strip() for item in types_arg.split(',')] allowed_types = ('contact', 'message', 'event', 'file', 'thread', 'calendar', 'draft', 'folder', 'label') for type_ in types: if type_ not in allowed_types: raise InputError('Invalid object type {}'.format(type_)) return types
def comma_separated_email_list(value, key): addresses = value.split(",") # Note that something like "foo,bar"@example.com is technical a valid # email address, but in practice nobody does this (and they shouldn't!) if len(addresses) > 25: # arbitrary limit raise InputError(u"Too many emails. The current limit is 25") good_emails = [] for unvalidated_address in addresses: parsed = address.parse(unvalidated_address, addr_spec_only=True) if not isinstance(parsed, address.EmailAddress): raise InputError( u"Invalid recipient address {}".format(unvalidated_address)) good_emails.append(parsed.address) return good_emails
def parse(request_data, accept_labels): unread = request_data.pop('unread', None) if unread is not None and not isinstance(unread, bool): raise InputError('"unread" must be true or false') starred = request_data.pop('starred', None) if starred is not None and not isinstance(starred, bool): raise InputError('"starred" must be true or false') label_public_ids = None folder_public_id = None if accept_labels: label_public_ids = request_data.pop('labels', None) if (label_public_ids is not None and not isinstance(label_public_ids, list)): raise InputError('"labels" must be a list of strings') if (label_public_ids is not None and not all(isinstance(l, basestring) for l in label_public_ids)): raise InputError('"labels" must be a list of strings') if request_data: raise InputError('Only the "unread", "starred" and "labels" ' 'attributes can be changed') else: folder_public_id = request_data.pop('folder', None) if (folder_public_id is not None and not isinstance(folder_public_id, basestring)): raise InputError('"folder" must be a string') if request_data: raise InputError('Only the "unread", "starred" and "folder" ' 'attributes can be changed') return (unread, starred, label_public_ids, folder_public_id)
def valid_display_name(namespace_id, category_type, display_name, db_session): if display_name is None or not isinstance(display_name, basestring): raise InputError('"display_name" must be a valid string') display_name = display_name.rstrip() if len(display_name) > MAX_INDEXABLE_LENGTH: # Set as MAX_FOLDER_LENGTH, MAX_LABEL_LENGTH raise InputError('"display_name" is too long') if db_session.query(Category).filter( Category.namespace_id == namespace_id, Category.lowercase_name == display_name, Category.type_ == category_type, Category.deleted_at == EPOCH).first() is not None: raise InputError('{} with name "{}" already exists'.format( category_type, display_name)) return display_name
def get_draft(draft_public_id, version, namespace_id, db_session): valid_public_id(draft_public_id) if version is None: raise InputError('Must specify draft version') try: draft = db_session.query(Message).filter( Message.public_id == draft_public_id, Message.namespace_id == namespace_id).one() except NoResultFound: raise NotFoundError("Couldn't find draft {}".format(draft_public_id)) if draft.is_sent or not draft.is_draft: raise InputError('Message {} is not a draft'.format(draft_public_id)) if draft.version != version: raise ConflictError( 'Draft {0}.{1} has already been updated to version {2}'.format( draft_public_id, version, draft.version)) return draft
def contact_create_api(): # TODO(emfree) Detect attempts at duplicate insertions. data = request.get_json(force=True) name = data.get('name') email = data.get('email') if not any((name, email)): raise InputError('Contact name and email cannot both be null.') new_contact = contacts.crud.create(g.namespace, g.db_session, name, email) return g.encoder.jsonify(new_contact)
def event_create_api(): g.parser.add_argument('notify_participants', type=strict_bool, location='args') args = strict_parse_args(g.parser, request.args) notify_participants = args['notify_participants'] data = request.get_json(force=True) calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) if calendar.read_only: raise InputError("Can't create events on read_only calendar.") valid_event(data) title = data.get('title', '') description = data.get('description') location = data.get('location') when = data.get('when') busy = data.get('busy') # client libraries can send explicit key = None automagically if busy is None: busy = True participants = data.get('participants') if participants is None: participants = [] for p in participants: if 'status' not in p: p['status'] = 'noreply' event = Event(calendar=calendar, namespace=g.namespace, uid=uuid.uuid4().hex, provider_name=g.namespace.account.provider, raw_data='', title=title, description=description, location=location, busy=busy, when=when, read_only=False, is_owner=True, participants=participants, sequence_number=0, source='local') g.db_session.add(event) g.db_session.flush() schedule_action('create_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid, notify_participants=notify_participants) return g.encoder.jsonify(event)
def generate_cursor(): data = request.get_json(force=True) if data.keys() != ['start'] or not isinstance(data['start'], int): raise InputError('generate_cursor request body must have the format ' '{"start": <Unix timestamp> (seconds)}') timestamp = int(data['start']) try: datetime.utcfromtimestamp(timestamp) except ValueError: raise InputError('generate_cursor request body must have the format ' '{"start": <Unix timestamp> (seconds)}') cursor = delta_sync.get_transaction_cursor_near_timestamp( g.namespace.id, timestamp, g.db_session) return g.encoder.jsonify({'cursor': cursor})
def calendar_delete_api(public_id): calendar = get_calendar(public_id, g.namespace, g.db_session) if calendar.read_only: raise InputError("Cannot delete a read_only calendar.") result = events.crud.delete_calendar(g.namespace, g.db_session, public_id) return g.encoder.jsonify(result)
def draft_send_api(): data = request.get_json(force=True) draft_public_id = data.get('draft_id') if draft_public_id is not None: draft = get_draft(draft_public_id, data.get('version'), g.namespace.id, g.db_session) if not any((draft.to_addr, draft.cc_addr, draft.bcc_addr)): raise InputError('No recipients specified') validate_draft_recipients(draft) resp = send_draft(g.namespace.account, draft, g.db_session, schedule_remote_delete=True) else: to = get_recipients(data.get('to'), 'to', validate_emails=True) cc = get_recipients(data.get('cc'), 'cc', validate_emails=True) bcc = get_recipients(data.get('bcc'), 'bcc', validate_emails=True) if not any((to, cc, bcc)): raise InputError('No recipients specified') subject = data.get('subject') body = data.get('body') tags = get_tags(data.get('tags'), g.namespace.id, g.db_session) files = get_attachments(data.get('file_ids'), g.namespace.id, g.db_session) replyto_thread = get_thread(data.get('thread_id'), g.namespace.id, g.db_session) draft = sendmail.create_draft(g.db_session, g.namespace.account, to, subject, body, files, cc, bcc, tags, replyto_thread, syncback=False) resp = send_draft(g.namespace.account, draft, g.db_session, schedule_remote_delete=False) return resp
def tag_create_api(): data = request.get_json(force=True) if not ('name' in data.keys() and isinstance(data['name'], basestring)): raise InputError('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: raise InputError('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: raise InputError('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 valid_event_update(event, namespace, db_session): if 'when' in event: valid_when(event['when']) if 'busy' in event and not isinstance(event.get('busy'), bool): raise InputError('\'busy\' must be true or false') calendar = get_calendar(event.get('calendar_id'), namespace, db_session) if calendar and calendar.read_only: raise InputError("Cannot move event to read_only calendar.") participants = event.get('participants', []) for p in participants: if 'email' not in p: raise InputError("'participants' must have email") if 'status' in p: if p['status'] not in ('yes', 'no', 'maybe', 'noreply'): raise InputError("'participants' status must be one of: " "yes, no, maybe, noreply")
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 validate_draft_recipients(draft): """Check that all recipient emails are at least plausible email addresses, before we try to send a draft.""" for field in draft.to_addr, draft.bcc_addr, draft.cc_addr: if field is not None: for _, email_address in field: parsed = address.parse(email_address, addr_spec_only=True) if not isinstance(parsed, address.EmailAddress): raise InputError( u'Invalid recipient address {}'.format(email_address))
def create_draft_from_mime(account, raw_mime, db_session): our_uid = generate_public_id() # base-36 encoded string new_headers = ('X-INBOX-ID: {0}-0\r\n' 'Message-Id: <{0}[email protected]>\r\n' 'User-Agent: NylasMailer/{1}\r\n').format(our_uid, VERSION) new_body = new_headers + raw_mime with db_session.no_autoflush: msg = Message.create_from_synced(account, '', '', datetime.utcnow(), new_body) if msg.from_addr and len(msg.from_addr) > 1: raise InputError("from_addr field can have at most one item") if msg.reply_to and len(msg.reply_to) > 1: raise InputError("reply_to field can have at most one item") if msg.subject is not None and not \ isinstance(msg.subject, basestring): raise InputError('"subject" should be a string') if not isinstance(msg.body, basestring): raise InputError('"body" should be a string') if msg.references or msg.in_reply_to: msg.is_reply = True thread_cls = account.thread_cls msg.thread = thread_cls(subject=msg.subject, recentdate=msg.received_date, namespace=account.namespace, subjectdate=msg.received_date) if msg.attachments: attachment_tag = account.namespace.tags['attachment'] msg.thread.apply_tag(attachment_tag) msg.is_created = True msg.is_sent = True msg.is_draft = False msg.is_read = True db_session.add(msg) db_session.flush() return msg
def update_message_labels(message, db_session, added_categories, removed_categories): special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } added_labels = [] removed_labels = [] for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: removed_labels.append(category.display_name) # Optimistically update message state. for cat in added_categories: message.categories.add(cat) for cat in removed_categories: message.categories.discard(cat) if removed_labels or added_labels: message.categories_changes = True schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)
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 get_tags(tag_public_ids, namespace_id, db_session): tags = set() if tag_public_ids is None: return tags if not isinstance(tag_public_ids, list): raise InputError('{} is not a list of tag ids'.format(tag_public_ids)) for tag_public_id in tag_public_ids: # Validate public id before querying with it valid_public_id(tag_public_id) try: # We're trading a bit of performance for more meaningful error # messages here by looking these up one-by-one. tag = db_session.query(Tag). \ filter(Tag.namespace_id == namespace_id, Tag.public_id == tag_public_id, Tag.user_created).one() tags.add(tag) except NoResultFound: raise InputError('Invalid tag public id {}'.format(tag_public_id)) return tags
def get_thread(thread_public_id, namespace_id, db_session): if thread_public_id is None: return None valid_public_id(thread_public_id) try: return db_session.query(Thread). \ filter(Thread.public_id == thread_public_id, Thread.namespace_id == namespace_id).one() except NoResultFound: raise InputError( 'Invalid thread public id {}'.format(thread_public_id))
def get_message(message_public_id, namespace_id, db_session): if message_public_id is None: return None valid_public_id(message_public_id) try: return db_session.query(Message). \ filter(Message.public_id == message_public_id, Message.namespace_id == namespace_id).one() except NoResultFound: raise InputError( 'Invalid message public id {}'.format(message_public_id))
def get_recipients(recipients, field, validate_emails=False): if recipients is None: return None if not isinstance(recipients, list): raise InputError('Invalid {} field'.format(field)) for r in recipients: if not (isinstance(r, dict) and 'email' in r and isinstance(r['email'], basestring)): raise InputError('Invalid {} field'.format(field)) if 'name' in r and not isinstance(r['name'], basestring): raise InputError('Invalid {} field'.format(field)) if validate_emails: # flanker purports to have a more comprehensive validate_address # function, but it doesn't actually work. So just invoke the # parser. parsed = address.parse(r['email'], addr_spec_only=True) if not isinstance(parsed, address.EmailAddress): raise InputError(u'Invalid recipient address {}'.format( r['email'])) return [(r.get('name', ''), r.get('email', '')) for r in recipients]
def get_attachments(block_public_ids, namespace_id, db_session): attachments = set() if block_public_ids is None: return attachments if not isinstance(block_public_ids, list): raise InputError( '{} is not a list of block ids'.format(block_public_ids)) for block_public_id in block_public_ids: # Validate public ids before querying with them valid_public_id(block_public_id) try: block = db_session.query(Block). \ filter(Block.public_id == block_public_id, Block.namespace_id == namespace_id).one() # In the future we may consider discovering the filetype from the # data by using #magic.from_buffer(data, mime=True)) attachments.add(block) except NoResultFound: raise InputError( 'Invalid block public id {}'.format(block_public_id)) return attachments
def start(): g.log = get_logger() try: watch_state = request.headers[GOOGLE_RESOURCE_STATE_STRING] g.watch_channel_id = request.headers[GOOGLE_CHANNEL_ID_STRING] g.watch_resource_id = request.headers[GOOGLE_RESOURCE_ID_STRING] except KeyError: raise InputError('Malformed headers') if watch_state == 'sync': return resp(204)
def parse_folder(request_data, db_session, namespace_id): folder_public_id = request_data.pop('folder', None) if folder_public_id is None: return 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 sync_deltas(): g.parser.add_argument('cursor', type=valid_public_id, location='args', required=True) g.parser.add_argument('exclude_types', type=valid_delta_object_types, location='args') g.parser.add_argument('wait', type=bool, default=False, location='args') # TODO(emfree): should support `expand` parameter in delta endpoints. args = strict_parse_args(g.parser, request.args) exclude_types = args.get('exclude_types') cursor = args['cursor'] if cursor == '0': start_pointer = 0 else: try: start_pointer, = g.db_session.query(Transaction.id). \ filter(Transaction.public_id == cursor, Transaction.namespace_id == g.namespace.id).one() except NoResultFound: raise InputError('Invalid cursor parameter') # The client wants us to wait until there are changes g.db_session.close() # hack to close the flask session poll_interval = 1 start_time = time.time() while time.time() - start_time < LONG_POLL_REQUEST_TIMEOUT: with session_scope() as db_session: deltas, _ = delta_sync.format_transactions_after_pointer( g.namespace.id, start_pointer, db_session, args['limit'], delta_sync._format_transaction_for_delta_sync, exclude_types) response = { 'cursor_start': cursor, 'deltas': deltas, } if deltas: response['cursor_end'] = deltas[-1]['cursor'] return g.encoder.jsonify(response) # No changes. perhaps wait elif args['wait']: gevent.sleep(poll_interval) else: # Return immediately response['cursor_end'] = cursor return g.encoder.jsonify(response) # If nothing happens until timeout, just return the end of the cursor response['cursor_end'] = cursor return g.encoder.jsonify(response)
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 validate_labels(db_session, added_categories, removed_categories): """ Validate that the labels added and removed obey Gmail's semantics -- Gmail messages MUST belong to exactly ONE of the '[Gmail]All Mail', '[Gmail]Trash', '[Gmail]Spam' folders. """ add = {c.name for c in added_categories if c.name} add_all = "all" in add add_trash = "trash" in add add_spam = "spam" in add if (add_all and (add_trash or add_spam)) or (add_trash and add_spam): raise InputError('Only one of "all", "trash" or "spam" can be added') remove = {c.name for c in removed_categories if c.name} remove_all = "all" in remove remove_trash = "trash" in remove remove_spam = "spam" in remove if remove_all and remove_trash and remove_spam: raise InputError('"all", "trash" and "spam" cannot all be removed')
def calendar_update_api(public_id): calendar = get_calendar(public_id, g.namespace, g.db_session) if calendar.read_only: raise InputError("Cannot update a read_only calendar.") data = request.get_json(force=True) result = events.crud.update_calendar(g.namespace, g.db_session, public_id, data) if result is None: raise NotFoundError("Couldn't find calendar {0}".format(public_id)) return g.encoder.jsonify(result)