Example #1
0
def draft_update_api(public_id):
    data = request.get_json(force=True)
    original_draft = get_draft(public_id, data.get('version'), g.namespace.id,
                               g.db_session)

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), 'bcc')
    from_addr = get_recipients(data.get('from_addr'), 'from_addr')
    reply_to = get_recipients(data.get('reply_to'), 'reply_to')

    if from_addr and len(from_addr) > 1:
        raise InputError("from_addr field can have at most one item")
    if reply_to and len(reply_to) > 1:
        raise InputError("reply_to field can have at most one item")

    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)

    draft = update_draft(g.db_session, g.namespace.account, original_draft,
                         to, subject, body, files, cc, bcc, from_addr,
                         reply_to, tags)
    return g.encoder.jsonify(draft)
Example #2
0
def draft_update_api(public_id):
    data = request.get_json(force=True)
    original_draft = get_draft(public_id, data.get('version'), g.namespace.id,
                               g.db_session)

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), 'bcc')
    from_addr = get_recipients(data.get('from_addr'), 'from_addr')
    reply_to = get_recipients(data.get('reply_to'), 'reply_to')

    if from_addr and len(from_addr) > 1:
        raise InputError("from_addr field can have at most one item")
    if reply_to and len(reply_to) > 1:
        raise InputError("reply_to field can have at most one item")

    subject = data.get('subject')
    body = data.get('body')
    files = get_attachments(data.get('file_ids'), g.namespace.id, g.db_session)

    draft = update_draft(g.db_session, g.namespace.account, original_draft,
                         to, subject, body, files, cc, bcc, from_addr,
                         reply_to)
    return g.encoder.jsonify(draft)
Example #3
0
def draft_create_api():
    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), '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('file_ids'), g.namespace.id,
                                g.db_session)
        replyto_thread = get_thread(data.get('thread_id'),
                                    g.namespace.id, g.db_session)
    except InputError as e:
        return err(404, e.message)

    try:
        draft = sendmail.create_draft(g.db_session, g.namespace.account, to,
                                      subject, body, files, cc, bcc,
                                      tags, replyto_thread)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #4
0
def draft_create_api():
    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), '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('file_ids'), g.namespace.id,
                                g.db_session)
        replyto_thread = get_thread(data.get('thread_id'), g.namespace.id,
                                    g.db_session)
    except InputError as e:
        return err(404, e.message)

    try:
        draft = sendmail.create_draft(g.db_session, g.namespace.account, to,
                                      subject, body, files, cc, bcc, tags,
                                      replyto_thread)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #5
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 #6
0
def draft_update_api(public_id):
    data = request.get_json(force=True)
    original_draft = get_draft(public_id, data.get('version'), g.namespace.id,
                               g.db_session)

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), 'bcc')
    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)

    try:
        draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                      original_draft, to, subject, body,
                                      files, cc, bcc, tags)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #7
0
def draft_update_api(public_id):
    data = request.get_json(force=True)
    original_draft = get_draft(public_id, data.get('version'), g.namespace.id,
                               g.db_session)

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), 'bcc')
    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)

    try:
        draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                      original_draft, to, subject, body, files,
                                      cc, bcc, tags)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #8
0
def draft_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))

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

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

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), '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('file_ids'), g.namespace.id,
                                g.db_session)
    except InputError as e:
        return err(404, e.message)

    try:
        draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                      original_draft, to, subject, body, files,
                                      cc, bcc, tags)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #9
0
def draft_update_api(public_id):
    try:
        valid_public_id(public_id)
    except InputError:
        return err(400, 'Invalid draft id {}'.format(public_id))

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

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

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

    data = request.get_json(force=True)

    to = get_recipients(data.get('to'), 'to')
    cc = get_recipients(data.get('cc'), 'cc')
    bcc = get_recipients(data.get('bcc'), '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('file_ids'), g.namespace.id,
                                g.db_session)
    except InputError as e:
        return err(404, e.message)

    try:
        draft = sendmail.update_draft(g.db_session, g.namespace.account,
                                      original_draft, to, subject, body,
                                      files, cc, bcc, tags)
    except ActionError as e:
        return err(e.error, str(e))

    return g.encoder.jsonify(draft)
Example #10
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 #11
0
def create_message_from_json(data, namespace, db_session, is_draft):
    """ Construct a Message instance from `data`, a dictionary representing the
    POST body of an API request. All new objects are added to the session, but
    not committed."""

    # Validate the input and get referenced objects (thread, attachments)
    # as necessary.
    to_addr = get_recipients(data.get('to'), 'to')
    cc_addr = get_recipients(data.get('cc'), 'cc')
    bcc_addr = get_recipients(data.get('bcc'), 'bcc')
    from_addr = get_recipients(data.get('from'), 'from')
    reply_to = get_recipients(data.get('reply_to'), 'reply_to')

    if from_addr and len(from_addr) > 1:
        raise InputError("from_addr field can have at most one item")
    if reply_to and len(reply_to) > 1:
        raise InputError("reply_to field can have at most one item")

    subject = data.get('subject')
    if subject is not None and not isinstance(subject, basestring):
        raise InputError('"subject" should be a string')
    body = data.get('body', '')
    if not isinstance(body, basestring):
        raise InputError('"body" should be a string')
    blocks = get_attachments(data.get('file_ids'), namespace.id, db_session)
    reply_to_thread = get_thread(data.get('thread_id'), namespace.id,
                                 db_session)
    reply_to_message = get_message(data.get('reply_to_message_id'),
                                   namespace.id, db_session)
    if reply_to_message is not None and reply_to_thread is not None:
        if reply_to_message not in reply_to_thread.messages:
            raise InputError('Message {} is not in thread {}'.format(
                reply_to_message.public_id, reply_to_thread.public_id))

    with db_session.no_autoflush:
        account = namespace.account
        dt = datetime.utcnow()
        uid = generate_public_id()
        to_addr = to_addr or []
        cc_addr = cc_addr or []
        bcc_addr = bcc_addr or []
        blocks = blocks or []
        if subject is None:
            # If this is a reply with no explicitly specified subject, set the
            # subject from the prior message/thread by default.
            # TODO(emfree): Do we want to allow changing the subject on a reply
            # at all?
            if reply_to_message is not None:
                subject = reply_to_message.subject
            elif reply_to_thread is not None:
                subject = reply_to_thread.subject
        subject = subject or ''

        message = Message()
        message.namespace = namespace
        message.is_created = True
        message.is_draft = is_draft
        message.from_addr = from_addr if from_addr else \
            [(account.name, account.email_address)]
        # TODO(emfree): we should maybe make received_date nullable, so its
        # value doesn't change in the case of a drafted-and-later-reconciled
        # message.
        message.received_date = dt
        message.subject = subject
        message.body = body
        message.to_addr = to_addr
        message.cc_addr = cc_addr
        message.bcc_addr = bcc_addr
        message.reply_to = reply_to
        # TODO(emfree): this is different from the normal 'size' value of a
        # message, which is the size of the entire MIME message.
        message.size = len(body)
        message.is_read = True
        message.is_sent = False
        message.public_id = uid
        message.version = 0
        message.regenerate_inbox_uid()

        # Set the snippet
        message.snippet = message.calculate_html_snippet(body)

        # Associate attachments to the draft message
        for block in blocks:
            # Create a new Part object to associate to the message object.
            # (You can't just set block.message, because if block is an
            # attachment on an existing message, that would dissociate it from
            # the existing message.)
            part = Part(block=block)
            part.namespace_id = namespace.id
            part.content_disposition = 'attachment'
            part.is_inboxapp_attachment = True
            message.parts.append(part)

        update_contacts_from_message(db_session, message, namespace)

        if reply_to_message is not None:
            message.is_reply = True
            _set_reply_headers(message, reply_to_message)
            thread = reply_to_message.thread
            message.reply_to_message = reply_to_message
        elif reply_to_thread is not None:
            message.is_reply = True
            thread = reply_to_thread
            # Construct the in-reply-to and references headers from the last
            # message currently in the thread.
            previous_messages = [m for m in thread.messages if not m.is_draft]
            if previous_messages:
                last_message = previous_messages[-1]
                message.reply_to_message = last_message
                _set_reply_headers(message, last_message)
        else:
            # If this isn't a reply to anything, create a new thread object for
            # the draft.  We specialize the thread class so that we can, for
            # example, add the g_thrid for Gmail later if we reconcile a synced
            # message with this one. This is a huge hack, but works.
            message.is_reply = False
            thread_cls = account.thread_cls
            thread = thread_cls(subject=message.subject,
                                recentdate=message.received_date,
                                namespace=namespace,
                                subjectdate=message.received_date)

        message.thread = thread

    db_session.add(message)
    if is_draft:
        schedule_action('save_draft',
                        message,
                        namespace.id,
                        db_session,
                        version=message.version)
    db_session.flush()
    return message
Example #12
0
def draft_send_api():
    data = request.get_json(force=True)
    if data.get('draft_id') is None:
        if data.get('to') is None:
            return err(400, 'Must specify either draft id + version or '
                       'message recipients.')
    else:
        if data.get('version') is None:
            return err(400, 'Must specify version to send')

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

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

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

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

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

        schedule_action('send_draft', draft, g.namespace.id, g.db_session)
    else:
        to = 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)
        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('file_ids'), g.namespace.id,
                                    g.db_session)
            replyto_thread = get_thread(data.get('thread_id'),
                                        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)
Example #13
0
def create_draft(data, namespace, db_session, syncback):
    """ Construct a draft object (a Message instance) from `data`, a dictionary
    representing the POST body of an API request. All new objects are added to
    the session, but not committed."""

    # Validate the input and get referenced objects (thread, attachments)
    # as necessary.
    to_addr = get_recipients(data.get('to'), 'to')
    cc_addr = get_recipients(data.get('cc'), 'cc')
    bcc_addr = get_recipients(data.get('bcc'), 'bcc')
    from_addr = get_recipients(data.get('from'), 'from')
    reply_to = get_recipients(data.get('reply_to'), 'reply_to')

    if from_addr and len(from_addr) > 1:
        raise InputError("from_addr field can have at most one item")
    if reply_to and len(reply_to) > 1:
        raise InputError("reply_to field can have at most one item")

    subject = data.get('subject')
    if subject is not None and not isinstance(subject, basestring):
        raise InputError('"subject" should be a string')
    body = data.get('body', '')
    if not isinstance(body, basestring):
        raise InputError('"body" should be a string')
    blocks = get_attachments(data.get('file_ids'), namespace.id, db_session)
    reply_to_thread = get_thread(data.get('thread_id'), namespace.id,
                                 db_session)
    reply_to_message = get_message(data.get('reply_to_message_id'),
                                   namespace.id, db_session)
    if reply_to_message is not None and reply_to_thread is not None:
        if reply_to_message not in reply_to_thread.messages:
            raise InputError('Message {} is not in thread {}'.
                             format(reply_to_message.public_id,
                                    reply_to_thread.public_id))

    with db_session.no_autoflush:
        account = namespace.account
        dt = datetime.utcnow()
        uid = generate_public_id()
        to_addr = to_addr or []
        cc_addr = cc_addr or []
        bcc_addr = bcc_addr or []
        blocks = blocks or []
        if subject is None:
            # If this is a reply with no explicitly specified subject, set the
            # subject from the prior message/thread by default.
            # TODO(emfree): Do we want to allow changing the subject on a reply
            # at all?
            if reply_to_message is not None:
                subject = reply_to_message.subject
            elif reply_to_thread is not None:
                subject = reply_to_thread.subject
        subject = subject or ''

        message = Message()
        message.namespace = namespace
        message.is_created = True
        message.is_draft = True
        message.from_addr = from_addr if from_addr else \
            [(account.name, account.email_address)]
        # TODO(emfree): we should maybe make received_date nullable, so its
        # value doesn't change in the case of a drafted-and-later-reconciled
        # message.
        message.received_date = dt
        message.subject = subject
        message.body = body
        message.to_addr = to_addr
        message.cc_addr = cc_addr
        message.bcc_addr = bcc_addr
        message.reply_to = reply_to
        # TODO(emfree): this is different from the normal 'size' value of a
        # message, which is the size of the entire MIME message.
        message.size = len(body)
        message.is_read = True
        message.is_sent = False
        message.public_id = uid
        message.version = 0
        message.regenerate_inbox_uid()

        # Set the snippet
        message.snippet = message.calculate_html_snippet(body)

        # Associate attachments to the draft message
        for block in blocks:
            # Create a new Part object to associate to the message object.
            # (You can't just set block.message, because if block is an
            # attachment on an existing message, that would dissociate it from
            # the existing message.)
            part = Part(block=block)
            part.namespace_id = namespace.id
            part.content_disposition = 'attachment'
            part.is_inboxapp_attachment = True
            message.parts.append(part)

        update_contacts_from_message(db_session, message, namespace)

        if reply_to_message is not None:
            message.is_reply = True
            _set_reply_headers(message, reply_to_message)
            thread = reply_to_message.thread
            message.reply_to_message = reply_to_message
        elif reply_to_thread is not None:
            message.is_reply = True
            thread = reply_to_thread
            # Construct the in-reply-to and references headers from the last
            # message currently in the thread.
            previous_messages = [m for m in thread.messages if not m.is_draft]
            if previous_messages:
                last_message = previous_messages[-1]
                message.reply_to_message = last_message
                _set_reply_headers(message, last_message)
        else:
            # If this isn't a reply to anything, create a new thread object for
            # the draft.  We specialize the thread class so that we can, for
            # example, add the g_thrid for Gmail later if we reconcile a synced
            # message with this one. This is a huge hack, but works.
            message.is_reply = False
            thread_cls = account.thread_cls
            thread = thread_cls(
                subject=message.subject,
                recentdate=message.received_date,
                namespace=namespace,
                subjectdate=message.received_date)

        message.thread = thread

    db_session.add(message)
    if syncback:
        schedule_action('save_draft', message, namespace.id, db_session,
                        version=message.version)
    db_session.flush()
    return message
Example #14
0
def draft_send_api():
    data = request.get_json(force=True)
    if data.get('draft_id') is None:
        if data.get('to') is None:
            return err(
                400, 'Must specify either draft id + version or '
                'message recipients.')
    else:
        if data.get('version') is None:
            return err(400, 'Must specify version to send')

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

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

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

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

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

        validate_draft_recipients(draft)

        try:
            schedule_action('send_draft', draft, g.namespace.id, g.db_session)
        except ActionError as e:
            return err(e.error, str(e))
    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)
        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('file_ids'), g.namespace.id,
                                    g.db_session)
            replyto_thread = get_thread(data.get('thread_id'), g.namespace.id,
                                        g.db_session)
        except InputError as e:
            return err(404, e.message)

        try:
            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)
        except ActionError as e:
            return err(e.error, str(e))

    draft.state = 'sending'
    return g.encoder.jsonify(draft)