def __init__(self, uid=None, public_id=None, **kwargs): if not uid and not public_id: self.public_id = self.uid = generate_public_id() elif not uid: self.uid = generate_public_id() for key, value in kwargs.items(): setattr(self, key, value)
def add_inbox_headers(msg, inbox_uid): """ Set a custom `X-INBOX-ID` header so as to identify messages generated by Inbox. The header is set to a unique id generated randomly per message, and is needed for the correct reconciliation of sent messages on future syncs. Notes ----- We generate the UUID as a base-36 encoded string, and is the same as the public_id of the message object. """ our_uid = inbox_uid if inbox_uid else \ generate_public_id() # base-36 encoded string # Set our own custom header for tracking in `Sent Mail` folder msg.headers['X-INBOX-ID'] = our_uid msg.headers['Message-Id'] = '<{}@mailer.nylas.com>'.format(our_uid) # Potentially also use `X-Mailer` msg.headers['User-Agent'] = 'NylasMailer/{0}'.format(VERSION)
def test_api_update_invalid(db, api_client): acct = db.session.query(Account).filter_by(id=ACCOUNT_ID).one() ns_id = acct.namespace.public_id e_update_data = {'title': 'new title'} e_id = generate_public_id() e_put_resp = api_client.put_data('/events/' + e_id, e_update_data, ns_id) assert e_put_resp.status_code != 200
def test_api_delete_invalid(db, api_client, calendar): e_id = 'asdf' resp = api_client.delete('/events/' + e_id) assert resp.status_code != 200 e_id = generate_public_id() resp = api_client.delete('/events/' + e_id) assert resp.status_code != 200
def default_calendar(self): if not self._default_calendar: public_id = generate_public_id() new_cal = Calendar() new_cal.public_id = public_id new_cal.account = self new_cal.uid = public_id new_cal.read_only = False self._default_calendar = new_cal return self._default_calendar
def upgrade(): from inbox.sqlalchemy_ext.util import generate_public_id from inbox.models.session import session_scope # These all inherit HasPublicID from inbox.models import ( Account, Block, Contact, Message, Namespace, SharedFolder, Thread, User, UserSession, HasPublicID) classes = [ Account, Block, Contact, Message, Namespace, SharedFolder, Thread, User, UserSession] for c in classes: assert issubclass(c, HasPublicID) print '[{0}] adding public_id column... '.format(c.__tablename__), sys.stdout.flush() op.add_column(c.__tablename__, sa.Column( 'public_id', mysql.BINARY(16), nullable=False)) print 'adding index... ', op.create_index( 'ix_{0}_public_id'.format(c.__tablename__), c.__tablename__, ['public_id'], unique=False) print 'Done!' sys.stdout.flush() print 'Finished adding columns. \nNow generating public_ids' with session_scope() as db_session: count = 0 for c in classes: garbage_collect() print '[{0}] Loading rows. '.format(c.__name__), sys.stdout.flush() print 'Generating public_ids', sys.stdout.flush() for r in db_session.query(c).yield_per(chunk_size): count += 1 r.public_id = generate_public_id() if not count % chunk_size: sys.stdout.write('.') sys.stdout.flush() db_session.commit() garbage_collect() sys.stdout.write(' Saving. '.format(c.__name__)), # sys.stdout.flush() sys.stdout.flush() db_session.commit() sys.stdout.write('Done!\n') sys.stdout.flush() print '\nUpdgraded OK!\n'
def test_api_delete_invalid(db, api_client): acct = db.session.query(Account).filter_by(id=ACCOUNT_ID).one() ns_id = acct.namespace.public_id e_id = 'asdf' resp = api_client.delete('/events/' + e_id, ns_id=ns_id) assert resp.status_code != 200 e_id = generate_public_id() resp = api_client.delete('/events/' + e_id, ns_id=ns_id) assert resp.status_code != 200
def test_namespace_id_validation(api_client, db, default_namespace): actual_namespace_id, = db.session.query(Namespace.public_id).first() r = api_client.client.get('/n/{}'.format(actual_namespace_id)) assert r.status_code == 200 fake_namespace_id = generate_public_id() r = api_client.client.get('/n/{}'.format(fake_namespace_id)) assert r.status_code == 404 malformed_namespace_id = 'this string is definitely not base36-decodable' r = api_client.client.get('/n/{}'.format(malformed_namespace_id)) assert r.status_code == 400
def default_calendar(self): if not self._default_calendar: public_id = generate_public_id() new_cal = Calendar() new_cal.public_id = public_id new_cal.namespace = self.namespace new_cal.uid = public_id new_cal.read_only = False new_cal.name = 'default' new_cal.provider_name = 'inbox' self._default_calendar = new_cal return self._default_calendar
def upgrade(): op.add_column("transaction", sa.Column("public_id", mysql.BINARY(16), nullable=True)) op.add_column( "transaction", sa.Column("object_public_id", sa.String(length=191), nullable=True), ) op.create_index("ix_transaction_public_id", "transaction", ["public_id"], unique=False) # TODO(emfree) reflect from inbox.ignition import main_engine from inbox.models.session import session_scope from inbox.sqlalchemy_ext.util import b36_to_bin, generate_public_id engine = main_engine(pool_size=1, max_overflow=0) Base = declarative_base() Base.metadata.reflect(engine) class Transaction(Base): __table__ = Base.metadata.tables["transaction"] with session_scope(versioned=False) as db_session: count = 0 (num_transactions, ) = db_session.query(sa.func.max( Transaction.id)).one() print("Adding public ids to {} transactions".format(num_transactions)) for pointer in range(0, num_transactions + 1, 500): for entry in db_session.query(Transaction).filter( Transaction.id >= pointer, Transaction.id < pointer + 500): entry.public_id = b36_to_bin(generate_public_id()) count += 1 if not count % 500: sys.stdout.write(".") sys.stdout.flush() db_session.commit() garbage_collect() op.alter_column("transaction", "public_id", existing_type=mysql.BINARY(16), nullable=False) op.add_column( "transaction", sa.Column("public_snapshot", sa.Text(length=4194304), nullable=True), ) op.add_column( "transaction", sa.Column("private_snapshot", sa.Text(length=4194304), nullable=True), ) op.drop_column("transaction", u"additional_data")
def upgrade(): op.add_column('transaction', sa.Column('public_id', mysql.BINARY(16), nullable=True)) op.add_column( 'transaction', sa.Column('object_public_id', sa.String(length=191), nullable=True)) op.create_index('ix_transaction_public_id', 'transaction', ['public_id'], unique=False) from inbox.sqlalchemy_ext.util import generate_public_id, b36_to_bin # TODO(emfree) reflect from inbox.models.session import session_scope from inbox.ignition import main_engine engine = main_engine(pool_size=1, max_overflow=0) Base = declarative_base() Base.metadata.reflect(engine) class Transaction(Base): __table__ = Base.metadata.tables['transaction'] with session_scope(versioned=False, ignore_soft_deletes=False) as db_session: count = 0 num_transactions, = db_session.query(sa.func.max(Transaction.id)).one() print 'Adding public ids to {} transactions'.format(num_transactions) for pointer in range(0, num_transactions + 1, 500): for entry in db_session.query(Transaction).filter( Transaction.id >= pointer, Transaction.id < pointer + 500): entry.public_id = b36_to_bin(generate_public_id()) count += 1 if not count % 500: sys.stdout.write('.') sys.stdout.flush() db_session.commit() garbage_collect() op.alter_column('transaction', 'public_id', existing_type=mysql.BINARY(16), nullable=False) op.add_column( 'transaction', sa.Column('public_snapshot', sa.Text(length=4194304), nullable=True)) op.add_column( 'transaction', sa.Column('private_snapshot', sa.Text(length=4194304), nullable=True)) op.drop_column('transaction', u'additional_data')
def upgrade(): op.add_column('transaction', sa.Column('public_id', mysql.BINARY(16), nullable=True)) op.add_column('transaction', sa.Column('object_public_id', sa.String(length=191), nullable=True)) op.create_index('ix_transaction_public_id', 'transaction', ['public_id'], unique=False) from inbox.sqlalchemy_ext.util import generate_public_id, b36_to_bin # TODO(emfree) reflect from inbox.models.session import session_scope from inbox.ignition import main_engine engine = main_engine(pool_size=1, max_overflow=0) Base = declarative_base() Base.metadata.reflect(engine) class Transaction(Base): __table__ = Base.metadata.tables['transaction'] with session_scope(versioned=False) as db_session: count = 0 num_transactions, = db_session.query(sa.func.max(Transaction.id)).one() print 'Adding public ids to {} transactions'.format(num_transactions) for pointer in range(0, num_transactions + 1, 500): for entry in db_session.query(Transaction).filter( Transaction.id >= pointer, Transaction.id < pointer + 500): entry.public_id = b36_to_bin(generate_public_id()) count += 1 if not count % 500: sys.stdout.write('.') sys.stdout.flush() db_session.commit() garbage_collect() op.alter_column('transaction', 'public_id', existing_type=mysql.BINARY(16), nullable=False) op.add_column('transaction', sa.Column('public_snapshot', sa.Text(length=4194304), nullable=True)) op.add_column('transaction', sa.Column('private_snapshot', sa.Text(length=4194304), nullable=True)) op.drop_column('transaction', u'additional_data')
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 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 add_inbox_headers(msg, inbox_uid): """ Set a custom `X-INBOX-ID` header so as to identify messages generated by Inbox. The header is set to a unique id generated randomly per message, and is needed for the correct reconciliation of sent messages on future syncs. Notes ----- We generate the UUID as a base-36 encoded string, and is the same as the public_id of the message object. """ # Set our own custom header for tracking in `Sent Mail` folder msg.headers['X-INBOX-ID'] = inbox_uid if inbox_uid else \ generate_public_id() # base-36 encoded string # Potentially also use `X-Mailer` msg.headers['User-Agent'] = 'Inbox/{0}'.format(VERSION)
def update_draft(db_session, account, original_draft, to_addr=None, subject=None, body=None, blocks=None, cc_addr=None, bcc_addr=None, tags=None): """ Update draft. To maintain our messages are immutable invariant, we create a new draft message object. Returns ------- Message The new draft message object. Notes ----- Messages, including draft messages, are immutable in Inbox. So to update a draft, we create a new draft message object and return its public_id (which is different than the original's). """ def update(attr, value=None): if value is not None: setattr(original_draft, attr, value) if attr == 'sanitized_body': # Update size, snippet too original_draft.size = len(value) original_draft.snippet = original_draft.calculate_html_snippet( value) update('to_addr', to_addr) update('cc_addr', cc_addr) update('bcc_addr', bcc_addr) update('subject', subject if subject else None) update('sanitized_body', body if body else None) update('received_date', datetime.utcnow()) # Remove any attachments that aren't specified new_block_ids = [b.id for b in blocks] for part in filter(lambda x: x.block_id not in new_block_ids, original_draft.parts): original_draft.parts.remove(part) db_session.delete(part) # Parts, tags require special handling for block in blocks: # Don't re-add attachments that are already attached if block.id in [p.block_id for p in original_draft.parts]: continue part = Part(block=block) part.namespace_id = account.namespace.id part.content_disposition = 'attachment' part.is_inboxapp_attachment = True original_draft.parts.append(part) db_session.add(part) thread = original_draft.thread if tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = tags | tags_to_keep # Remove previous message-contact associations, and create new ones. original_draft.contacts = [] update_contacts_from_message(db_session, original_draft, account.namespace) # Delete previous version on remote schedule_action('delete_draft', original_draft, original_draft.namespace.id, db_session, inbox_uid=original_draft.inbox_uid, message_id_header=original_draft.message_id_header) # Update version + inbox_uid (is_created is already set) version = generate_public_id() update('version', version) update('inbox_uid', version) # Sync to remote schedule_action('save_draft', original_draft, original_draft.namespace.id, db_session) db_session.commit() return original_draft
def test_api_update_invalid(db, api_client, calendar): e_update_data = {'title': 'new title'} e_id = generate_public_id() e_put_resp = api_client.put_data('/events/' + e_id, e_update_data) assert e_put_resp.status_code != 200
def create_and_save_draft( db_session, account, to_addr=None, subject=None, body=None, blocks=None, cc_addr=None, bcc_addr=None, new_tags=None, thread=None, is_reply=False, parent_draft=None, ): """ Create a draft object and commit it to the database. """ 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 [] body = body or "" if subject is None and thread is not None: # Set subject from thread by default. subject = thread.subject subject = subject or "" message = SpoolMessage() message.from_addr = [(account.sender_name, account.email_address)] message.created_date = dt # 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.is_sent = False message.state = "draft" if parent_draft is not None: message.parent_draft_id = parent_draft.id message.subject = subject message.sanitized_body = body message.to_addr = to_addr message.cc_addr = cc_addr message.bcc_addr = bcc_addr # 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_draft = True message.is_read = True message.inbox_uid = uid message.public_id = uid # Set the 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() part.namespace_id = account.namespace.id part.content_disposition = "attachment" part.content_type = block.content_type part.is_inboxapp_attachment = True part.data = block.data message.parts.append(part) db_session.add(part) # TODO(emfree) Update contact data here. if is_reply: message.is_reply = True # If we're updating a draft, copy the in-reply-to and references # headers from the parent. Otherwise, construct them from the last # message currently in the thread. if parent_draft is not None: message.in_reply_to = parent_draft.in_reply_to message.references = parent_draft.references else: # Make sure that the headers are constructed from an actual # previous message on the thread, not another draft non_draft_messages = [m for m in thread.messages if not m.is_draft] if non_draft_messages: last_message = non_draft_messages[-1] message.in_reply_to = last_message.message_id_header message.references = last_message.references + "\t" + last_message.message_id_header if thread is None: # Create a new thread object for the draft. thread = Thread( subject=message.subject, recentdate=message.received_date, namespace=account.namespace, subjectdate=message.received_date, ) db_session.add(thread) message.thread = thread # This triggers an autoflush, so we need to execute it after setting # message.thread thread.apply_tag(account.namespace.tags["drafts"]) if new_tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = new_tags | tags_to_keep schedule_action("save_draft", message, message.namespace.id, db_session) db_session.add(message) db_session.commit() return message
def update_draft(db_session, account, original_draft, to=None, subject=None, body=None, blocks=None, cc=None, bcc=None, tags=None): """ Update draft. To maintain our messages are immutable invariant, we create a new draft message object. Returns ------- Message The new draft message object. Notes ----- Messages, including draft messages, are immutable in Inbox. So to update a draft, we create a new draft message object and return its public_id (which is different than the original's). """ def update(attr, value=None): if value is not None: setattr(original_draft, attr, value) if attr == 'sanitized_body': # Update size, snippet too original_draft.size = len(value) original_draft.calculate_html_snippet(value) update('to_addr', _parse_recipients(to) if to else None) update('cc_addr', _parse_recipients(cc) if cc else None) update('bcc_addr', _parse_recipients(bcc) if bcc else None) update('subject', subject if subject else None) update('sanitized_body', body if body else None) update('received_date', datetime.utcnow()) # Parts, tags require special handling for block in blocks: part = Part() part.namespace_id = account.namespace.id part.content_disposition = 'attachment' part.content_type = block.content_type part.is_inboxapp_attachment = True part.data = block.data part.filename = block.filename original_draft.parts.append(part) db_session.add(part) thread = original_draft.thread if tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = tags | tags_to_keep # Delete previous version on remote schedule_action('delete_draft', original_draft, original_draft.namespace.id, db_session, inbox_uid=original_draft.inbox_uid) # Update version + inbox_uid, sync to remote version = generate_public_id() update('version', version) update('inbox_uid', version) schedule_action('save_draft', original_draft, original_draft.namespace.id, db_session) db_session.commit() return original_draft
def create_and_save_draft(db_session, account, to_addr=None, subject=None, body=None, blocks=None, cc_addr=None, bcc_addr=None, new_tags=None, thread=None, is_reply=False, syncback=True): """Create a draft object and commit it to the database.""" with db_session.no_autoflush: dt = datetime.utcnow() uid = generate_public_id() version = generate_public_id() to_addr = to_addr or [] cc_addr = cc_addr or [] bcc_addr = bcc_addr or [] blocks = blocks or [] body = body or '' if subject is None and thread is not None: # Set subject from thread by default. subject = thread.subject subject = subject or '' message = Message() message.namespace = account.namespace message.is_created = True message.is_draft = True message.state = 'draft' message.from_addr = [(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.sanitized_body = body message.to_addr = to_addr message.cc_addr = cc_addr message.bcc_addr = bcc_addr # 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.is_reply = is_reply message.public_id = uid message.version = version message.inbox_uid = version # 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 = account.namespace.id part.content_disposition = 'attachment' part.is_inboxapp_attachment = True message.parts.append(part) db_session.add(part) update_contacts_from_message(db_session, message, account.namespace) if is_reply: message.is_reply = True # Construct the in-reply-to and references headers from the last # message currently in the thread. _set_reply_headers(message, thread) if thread is None: # 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. thread_cls = account.thread_cls thread = thread_cls(subject=message.subject, recentdate=message.received_date, namespace=account.namespace, subjectdate=message.received_date) db_session.add(thread) message.thread = thread thread.apply_tag(account.namespace.tags['drafts']) if new_tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = new_tags | tags_to_keep if syncback: schedule_action('save_draft', message, message.namespace.id, db_session) db_session.add(message) db_session.commit() return message
def create_and_save_draft(db_session, account, to_addr=None, subject=None, body=None, blocks=None, cc_addr=None, bcc_addr=None, new_tags=None, thread=None, is_reply=False, parent_draft=None, syncback=True): """ Create a draft object and commit it to the database. """ 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 [] body = body or '' if subject is None and thread is not None: # Set subject from thread by default. subject = thread.subject subject = subject or '' # Sets is_draft = True, state = 'draft' message = Message.create_draft_message() message.from_addr = [(account.sender_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 if parent_draft is not None: message.parent_draft_id = parent_draft.id message.subject = subject message.sanitized_body = body message.to_addr = to_addr message.cc_addr = cc_addr message.bcc_addr = bcc_addr # 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.is_reply = is_reply message.inbox_uid = uid message.public_id = uid # Set the 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() part.namespace_id = account.namespace.id part.content_disposition = 'attachment' part.content_type = block.content_type part.is_inboxapp_attachment = True part.data = block.data part.filename = block.filename message.parts.append(part) db_session.add(part) # TODO(emfree) Update contact data here. if is_reply: message.is_reply = True # If we're updating a draft, copy the in-reply-to and references # headers from the parent. Otherwise, construct them from the last # message currently in the thread. if parent_draft is not None: message.in_reply_to = parent_draft.in_reply_to message.references = parent_draft.references else: _set_reply_headers(message, thread) if thread is None: # Create a new thread object for the draft. thread = Thread(subject=message.subject, recentdate=message.received_date, namespace=account.namespace, subjectdate=message.received_date) db_session.add(thread) message.thread = thread # This triggers an autoflush, so we need to execute it after setting # message.thread thread.apply_tag(account.namespace.tags['drafts']) if new_tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = new_tags | tags_to_keep if syncback: schedule_action('save_draft', message, message.namespace.id, db_session) db_session.add(message) db_session.commit() return message
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
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
def upgrade(): # These all inherit HasPublicID from inbox.models import ( Account, Block, Contact, HasPublicID, Message, Namespace, SharedFolder, Thread, User, UserSession, ) from inbox.models.session import session_scope from inbox.sqlalchemy_ext.util import generate_public_id classes = [ Account, Block, Contact, Message, Namespace, SharedFolder, Thread, User, UserSession, ] for c in classes: assert issubclass(c, HasPublicID) print "[{0}] adding public_id column... ".format(c.__tablename__), sys.stdout.flush() op.add_column(c.__tablename__, sa.Column("public_id", mysql.BINARY(16), nullable=False)) print "adding index... ", op.create_index( "ix_{0}_public_id".format(c.__tablename__), c.__tablename__, ["public_id"], unique=False, ) print "Done!" sys.stdout.flush() print "Finished adding columns. \nNow generating public_ids" with session_scope() as db_session: count = 0 for c in classes: garbage_collect() print "[{0}] Loading rows. ".format(c.__name__), sys.stdout.flush() print "Generating public_ids", sys.stdout.flush() for r in db_session.query(c).yield_per(chunk_size): count += 1 r.public_id = generate_public_id() if not count % chunk_size: sys.stdout.write(".") sys.stdout.flush() db_session.commit() garbage_collect() sys.stdout.write(" Saving. ".format(c.__name__)), # sys.stdout.flush() sys.stdout.flush() db_session.commit() sys.stdout.write("Done!\n") sys.stdout.flush() print "\nUpdgraded OK!\n"
def create_and_save_draft(db_session, account, to_addr=None, subject=None, body=None, blocks=None, cc_addr=None, bcc_addr=None, new_tags=None, thread=None, is_reply=False, syncback=True): """Create a draft object and commit it to the database.""" dt = datetime.utcnow() uid = generate_public_id() version = generate_public_id() to_addr = to_addr or [] cc_addr = cc_addr or [] bcc_addr = bcc_addr or [] blocks = blocks or [] body = body or '' if subject is None and thread is not None: # Set subject from thread by default. subject = thread.subject subject = subject or '' # Sets is_draft = True, state = 'draft' message = Message.create_draft_message() message.from_addr = [(account.sender_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.sanitized_body = body message.to_addr = to_addr message.cc_addr = cc_addr message.bcc_addr = bcc_addr # 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.is_reply = is_reply message.public_id = uid message.version = version message.inbox_uid = version # Set the 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() part.namespace_id = account.namespace.id part.content_disposition = 'attachment' part.content_type = block.content_type part.is_inboxapp_attachment = True part.data = block.data part.filename = block.filename message.parts.append(part) db_session.add(part) # TODO(emfree) Update contact data here. if is_reply: message.is_reply = True # Construct the in-reply-to and references headers from the last # message currently in the thread. _set_reply_headers(message, thread) if thread is None: # Create a new thread object for the draft. thread = Thread( subject=message.subject, recentdate=message.received_date, namespace=account.namespace, subjectdate=message.received_date) db_session.add(thread) message.thread = thread # This triggers an autoflush, so we need to execute it after setting # message.thread thread.apply_tag(account.namespace.tags['drafts']) if new_tags: tags_to_keep = {tag for tag in thread.tags if not tag.user_created} thread.tags = new_tags | tags_to_keep if syncback: schedule_action('save_draft', message, message.namespace.id, db_session) db_session.add(message) db_session.commit() return message