Example #1
0
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
Example #2
0
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")
Example #3
0
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
Example #4
0
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
Example #5
0
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)
Example #6
0
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
Example #7
0
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
Example #8
0
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)
Example #9
0
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)
Example #10
0
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})
Example #11
0
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)
Example #12
0
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
Example #13
0
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)
Example #14
0
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")
Example #15
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))
Example #16
0
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))
Example #17
0
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
Example #18
0
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)
Example #19
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
Example #20
0
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
Example #21
0
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))
Example #22
0
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))
Example #23
0
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]
Example #24
0
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)
Example #26
0
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))
Example #27
0
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)
Example #28
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)
Example #29
0
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')
Example #30
0
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)