def compose(request): if request.method == 'POST': form = MessageForm(request.POST) if not form.is_valid(): messages.add_message( request, messages.ERROR, 'There was a problem sending your message. Please review your inputs and try again' ) context = RequestContext(request, {'form': form}) return render_to_response('inbox/compose.html/', context) else: recipient = form.cleaned_data.get('recipient') subject = form.cleaned_data.get('subject') content = form.cleaned_data.get('content') m = Message(sender=request.user, recipient=recipient, subject=subject, content=content) m.save() messages.add_message(request, messages.SUCCESS, 'Your message was successfully sent.') return HttpResponseRedirect("/inbox/compose/") else: context = RequestContext(request, {'form': MessageForm()}) return render_to_response('inbox/compose.html/', context)
def test_threading_limit(db, folder_sync_engine, monkeypatch): """Test that custom threading doesn't produce arbitrarily long threads, which eventually break things.""" from inbox.models import Message, Thread, Account # Shorten bound to make test faster MAX_THREAD_LENGTH = 10 monkeypatch.setattr( 'inbox.mailsync.backends.imap.generic.MAX_THREAD_LENGTH', MAX_THREAD_LENGTH) namespace_id = folder_sync_engine.namespace_id account = db.session.query(Account).get(folder_sync_engine.account_id) folder = MockFolder('inbox', 'inbox') msg = MockRawMessage(None) for _ in range(3 * MAX_THREAD_LENGTH): m = Message() m.namespace_id = namespace_id m.received_date = datetime.datetime.utcnow() m.references = [] m.size = 0 m.sanitized_body = '' m.snippet = '' m.subject = 'unique subject' uid = MockImapUID(m, account) folder_sync_engine.add_message_attrs(db.session, uid, msg, folder) db.session.add(m) db.session.commit() new_threads = db.session.query(Thread). \ filter(Thread.subject == 'unique subject').all() assert len(new_threads) == 3 assert all( len(thread.messages) == MAX_THREAD_LENGTH for thread in new_threads)
def test_calculate_snippet(): m = Message() # Check that we strip contents of title, script, style tags body = ("<title>EMAIL</title><script>function() {}</script>" "<style>h1 {color:red;}</style>Hello, world") assert m.calculate_html_snippet(body) == "Hello, world" # Check that we replace various incarnations of <br> by spaces body = "Hello,<br>world" assert m.calculate_html_snippet(body) == "Hello, world" body = 'Hello,<br class="">world' assert m.calculate_html_snippet(body) == "Hello, world" body = "Hello,<br />world" assert m.calculate_html_snippet(body) == "Hello, world" body = "Hello,<br><br> world" assert m.calculate_html_snippet(body) == "Hello, world" body = '<div dir="ltr">line1<div>line2</div><div>line3</div><div><br></div></div>' assert m.calculate_html_snippet(body) == "line1 line2 line3" # Check that snippets are properly truncated to 191 characters. body = """Etenim quid est, <strong>Catilina</strong>, quod iam amplius exspectes, si neque nox tenebris obscurare coetus nefarios nec privata domus parietibus continere voces coniurationis tuae potest, si illustrantur, si erumpunt omnia?""" expected_snippet = ("Etenim quid est, Catilina, quod iam amplius " "exspectes, si neque nox tenebris obscurare coetus " "nefarios nec privata domus parietibus continere " "voces coniurationis tuae potest, si illustrantur,") assert len(expected_snippet) == 191 assert m.calculate_html_snippet(body) == expected_snippet
def test_calculate_snippet(): m = Message() # Check that we strip contents of title, script, style tags body = '<title>EMAIL</title><script>function() {}</script>' \ '<style>h1 {color:red;}</style>Hello, world' assert m.calculate_html_snippet(body) == 'Hello, world' # Check that we replace various incarnations of <br> by spaces body = 'Hello,<br>world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br class=\"\">world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br />world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br><br> world' assert m.calculate_html_snippet(body) == 'Hello, world' body = '<div dir="ltr">line1<div>line2</div><div>line3</div><div><br></div></div>' assert m.calculate_html_snippet(body) == 'line1 line2 line3' # Check that snippets are properly truncated to 191 characters. body = '''Etenim quid est, <strong>Catilina</strong>, quod iam amplius exspectes, si neque nox tenebris obscurare coetus nefarios nec privata domus parietibus continere voces coniurationis tuae potest, si illustrantur, si erumpunt omnia?''' expected_snippet = 'Etenim quid est, Catilina, quod iam amplius ' \ 'exspectes, si neque nox tenebris obscurare coetus ' \ 'nefarios nec privata domus parietibus continere ' \ 'voces coniurationis tuae potest, si illustrantur,' assert len(expected_snippet) == 191 assert m.calculate_html_snippet(body) == expected_snippet
def test_threading_limit(db, folder_sync_engine, monkeypatch): """Test that custom threading doesn't produce arbitrarily long threads, which eventually break things.""" from inbox.models import Message, Thread, Account # Shorten bound to make test faster MAX_THREAD_LENGTH = 10 monkeypatch.setattr( 'inbox.mailsync.backends.imap.generic.MAX_THREAD_LENGTH', MAX_THREAD_LENGTH) namespace_id = folder_sync_engine.namespace_id account = db.session.query(Account).get(folder_sync_engine.account_id) account.inbox_folder = Folder(account=account, name='Inbox', canonical_name='inbox') folder = account.inbox_folder msg = MockRawMessage([]) for i in range(3 * MAX_THREAD_LENGTH): m = Message() m.namespace_id = namespace_id m.received_date = datetime.datetime.utcnow() m.references = [] m.size = 0 m.sanitized_body = '' m.snippet = '' m.subject = 'unique subject' uid = ImapUid(message=m, account=account, msg_uid=2222 + i, folder=folder) folder_sync_engine.add_message_attrs(db.session, uid, msg, folder) db.session.add(m) db.session.commit() new_threads = db.session.query(Thread). \ filter(Thread.subject == 'unique subject').all() assert len(new_threads) == 3 assert all(len(thread.messages) == MAX_THREAD_LENGTH for thread in new_threads)
def test_calculate_snippet(): m = Message() # Check that we strip contents of title, script, style tags body = '<title>EMAIL</title><script>function() {}</script>' \ '<style>h1 {color:red;}</style>Hello, world' assert m.calculate_html_snippet(body) == 'Hello, world' # Check that we replace various incarnations of <br> by spaces body = 'Hello,<br>world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br class=\"\">world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br />world' assert m.calculate_html_snippet(body) == 'Hello, world' body = 'Hello,<br><br> world' assert m.calculate_html_snippet(body) == 'Hello, world' # Check that snippets are properly truncated to 191 characters. body = '''Etenim quid est, <strong>Catilina</strong>, quod iam amplius exspectes, si neque nox tenebris obscurare coetus nefarios nec privata domus parietibus continere voces coniurationis tuae potest, si illustrantur, si erumpunt omnia?''' expected_snippet = 'Etenim quid est, Catilina, quod iam amplius ' \ 'exspectes, si neque nox tenebris obscurare coetus ' \ 'nefarios nec privata domus parietibus continere ' \ 'voces coniurationis tuae potest, si illustrantur,' assert len(expected_snippet) == 191 assert m.calculate_html_snippet(body) == expected_snippet
def add_fake_message(namespace_id, thread, to_email, received_date, subject, db_session): """ One-off helper function to add 'fake' messages to the datastore.""" m = Message() m.namespace_id = namespace_id m.from_addr = [('', to_email)] m.received_date = received_date m.subject = subject m.size = 0 m.sanitized_body = '' m.snippet = '' m.thread = thread update_contacts_from_message(db_session, m, thread.namespace) db_session.add(m) db_session.commit()
def test_store_full_body_on_parse_error(default_account, mime_message_with_bad_date): received_date = None m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, mime_message_with_bad_date.to_string()) assert get_from_blockstore(m.data_sha256)
def test_store_full_body_on_parse_error( default_account, mime_message_with_bad_date): received_date = None m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, mime_message_with_bad_date.to_string()) assert m.full_body
def new_message_from_synced(db): received_date = datetime.datetime(2014, 9, 22, 17, 25, 46) new_msg = Message.create_from_synced(default_account(db), 139219, '[Gmail]/All Mail', received_date, raw_message()) assert new_msg.received_date == received_date return new_msg
def test_sanitize_subject(default_account): from inbox.log import configure_logging configure_logging() # Raw message with encoded null bytes in subject header. raw_message_with_wonky_subject = \ '''From: "UPS My Choice" <*****@*****.**> To: [email protected] Subject: =?UTF-8?B?WW91ciBVUFMgUGFja2FnZSB3YXMgZGVsaXZlcmVkAAAA?= Content-Type: text/html; charset=UTF-8 MIME-Version: 1.0 Content-Type: multipart/alternative; boundary=--==_mimepart_553921a23aa2c_3aee3fe2e442b2b815347 --==_mimepart_553921a23aa2c_3aee3fe2e442b2b815347 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable --==_mimepart_553921a23aa2c_3aee3fe2e442b2b815347 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable --==_mimepart_553921a23aa2c_3aee3fe2e442b2b815347 ''' m = Message.create_from_synced(default_account, 22, '[Gmail]/All Mail', datetime.datetime.utcnow(), raw_message_with_wonky_subject) assert m.subject == u'Your UPS Package was delivered'
def create_imap_message(db_session, account, folder, msg): """ IMAP-specific message creation logic. Returns ------- imapuid : inbox.models.backends.imap.ImapUid New db object, which links to new Message and Block objects through relationships. All new objects are uncommitted. """ new_message = Message.create_from_synced( account=account, mid=msg.uid, folder_name=folder.name, received_date=msg.internaldate, body_string=msg.body ) # Check to see if this is a copy of a message that was first created # by the Nylas API. If so, don't create a new object; just use the old one. existing_copy = reconcile_message(new_message, db_session) if existing_copy is not None: new_message = existing_copy imapuid = ImapUid(account=account, folder=folder, msg_uid=msg.uid, message=new_message) imapuid.update_flags(msg.flags) if msg.g_labels is not None: imapuid.update_labels(msg.g_labels) # Update the message's metadata with db_session.no_autoflush: is_draft = imapuid.is_draft and (folder.canonical_name == "drafts" or folder.canonical_name == "all") update_message_metadata(db_session, account, new_message, is_draft) update_contacts_from_message(db_session, new_message, account.namespace) return imapuid
def create_imap_message(db_session, log, account, folder, msg): """ IMAP-specific message creation logic. This is the one function in this file that gets to take an account object instead of an account_id, because we need to relate the account to ImapUids for versioning to work, since it needs to look up the namespace. Returns ------- imapuid : inbox.models.tables.imap.ImapUid New db object, which links to new Message and Block objects through relationships. All new objects are uncommitted. """ new_msg = Message.create_from_synced(account=account, mid=msg.uid, folder_name=folder.name, received_date=msg.internaldate, body_string=msg.body) # Check to see if this is a copy of a message that was first created # by the Inbox API. If so, don't create a new object; just use the old one. existing_copy = reconcile_message(new_msg, db_session) if existing_copy is not None: new_msg = existing_copy imapuid = ImapUid(account=account, folder=folder, msg_uid=msg.uid, message=new_msg) imapuid.update_flags_and_labels(msg.flags, msg.g_labels) new_msg.is_draft = imapuid.is_draft new_msg.is_read = imapuid.is_seen update_contacts_from_message(db_session, new_msg, account.namespace) return imapuid
def gmail_message(): received_date = datetime.datetime.utcfromtimestamp(10**9 + 1) new_msg = Message() new_msg.to_addr = ((u'Somebody', u'*****@*****.**'), (u'Somebody', u'*****@*****.**'),) new_msg.thread_id = 1 new_msg.size = 22 new_msg.is_draft = False new_msg.decode_error = False new_msg.sanitized_body = 'Are you there?' new_msg.snippet = 'Are you there?' new_msg.received_date = received_date return new_msg
def add_fake_message(db_session, thread, from_addr=None, to_addr=None, cc_addr=None, bcc_addr=None): m = Message() m.from_addr = from_addr or [] m.to_addr = to_addr or [] m.cc_addr = cc_addr or [] m.bcc_addr = bcc_addr or [] m.received_date = datetime.utcnow() m.size = 0 m.sanitized_body = '' m.snippet = '' m.thread = thread account_id = thread.namespace.account_id update_contacts_from_message(db_session, m, account_id) db_session.add(m) db_session.commit() return m
def test_store_full_body_on_parse_error( default_account, default_namespace, raw_message_with_bad_date): received_date = None m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, raw_message_with_bad_date) assert m.full_body
def test_parse_body_on_bad_attachment( default_account, raw_message_with_bad_attachment): received_date = None m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, raw_message_with_bad_attachment) assert m.decode_error assert 'dingy blue carpet' in m.body
def test_calculate_snippet(): m = Message() # Check that we strip contents of title, script, style tags body = "<title>EMAIL</title><script>function() {}</script>" "<style>h1 {color:red;}</style>Hello, world" assert m.calculate_html_snippet(body) == "Hello, world" # Check that we replace various incarnations of <br> by spaces body = "Hello,<br>world" assert m.calculate_html_snippet(body) == "Hello, world" body = 'Hello,<br class="">world' assert m.calculate_html_snippet(body) == "Hello, world" body = "Hello,<br />world" assert m.calculate_html_snippet(body) == "Hello, world" body = "Hello,<br><br> world" assert m.calculate_html_snippet(body) == "Hello, world" # Check that snippets are properly truncated to 191 characters. body = """Etenim quid est, <strong>Catilina</strong>, quod iam amplius exspectes, si neque nox tenebris obscurare coetus nefarios nec privata domus parietibus continere voces coniurationis tuae potest, si illustrantur, si erumpunt omnia?""" expected_snippet = ( "Etenim quid est, Catilina, quod iam amplius " "exspectes, si neque nox tenebris obscurare coetus " "nefarios nec privata domus parietibus continere " "voces coniurationis tuae potest, si illustrantur," ) assert len(expected_snippet) == 191 assert m.calculate_html_snippet(body) == expected_snippet
def test_truncate_recipients(db, default_account, default_namespace, thread, raw_message_with_many_recipients): received_date = datetime.datetime(2014, 9, 22, 17, 25, 46) m = Message.create_from_synced( default_account, 139219, "[Gmail]/All Mail", received_date, raw_message_with_many_recipients ) m.thread = thread db.session.add(m) # Check that no database error is raised. db.session.commit()
def create_from_synced(db, account, raw_message): thread = add_fake_thread(db.session, account.namespace.id) received_date = datetime.datetime.utcnow() m = Message.create_from_synced(account, 22, '[Gmail]/All Mail', received_date, raw_message) m.thread = thread db.session.add(m) db.session.commit() return m
def test_sanitize_subject(default_account, mime_message): # Parse a raw message with encoded null bytes in subject header; # check that we strip the null bytes. mime_message.headers['Subject'] = \ '=?UTF-8?B?WW91ciBVUFMgUGFja2FnZSB3YXMgZGVsaXZlcmVkAAAA?=' m = Message.create_from_synced(default_account, 22, '[Gmail]/All Mail', datetime.datetime.utcnow(), mime_message.to_string()) assert m.subject == u'Your UPS Package was delivered'
def compose(request): if request.method == 'POST': form = MessageForm(request.POST) if not form.is_valid(): messages.add_message(request, messages.ERROR, 'There was a problem sending your message. Please review your inputs and try again') context = RequestContext(request, {'form': form}) return render_to_response('inbox/compose.html/', context) else: recipient = form.cleaned_data.get('recipient') subject = form.cleaned_data.get('subject') content = form.cleaned_data.get('content') m = Message(sender=request.user, recipient=recipient, subject=subject, content=content) m.save() messages.add_message(request, messages.SUCCESS, 'Your message was successfully sent.') return HttpResponseRedirect("/inbox/compose/") else: context = RequestContext(request, {'form': MessageForm() }) return render_to_response('inbox/compose.html/', context)
def create_from_synced(db, account, raw_message): thread = add_fake_thread(db.session, account.namespace.id) received_date = datetime.datetime.utcnow() m = Message.create_from_synced(account, 22, "[Gmail]/All Mail", received_date, raw_message) m.thread = thread db.session.add(m) db.session.commit() return m
def test_sanitize_subject(default_account, mime_message): # Parse a raw message with encoded null bytes in subject header; # check that we strip the null bytes. mime_message.headers['Subject'] = \ '=?UTF-8?B?WW91ciBVUFMgUGFja2FnZSB3YXMgZGVsaXZlcmVkAAAA?=' m = Message.create_from_synced( default_account, 22, '[Gmail]/All Mail', datetime.datetime.utcnow(), mime_message.to_string()) assert m.subject == u'Your UPS Package was delivered'
def new_message_from_synced(db, default_account, mime_message): from inbox.models import Message received_date = datetime(2014, 9, 22, 17, 25, 46) new_msg = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, mime_message.to_string()) assert new_msg.received_date == received_date new_msg.is_read = True new_msg.is_starred = False return new_msg
def test_parse_body_on_bad_attachment(default_account, default_namespace, raw_message_with_bad_attachment): received_date = None m = Message.create_from_synced( default_account, 139219, "[Gmail]/All Mail", received_date, raw_message_with_bad_attachment ) assert m.decode_error plain_part, html_part = m.body_parts assert "dingy blue carpet" in plain_part assert "dingy blue carpet" in html_part assert "dingy blue carpet" in m.body
def new_message_from_synced(db, default_account, mime_message): from inbox.models import Message received_date = datetime(2014, 9, 22, 17, 25, 46) new_msg = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, mime_message.to_string()) assert new_msg.received_date == received_date return new_msg
def test_truncate_recipients(db, default_account, default_namespace, thread, raw_message_with_many_recipients): received_date = datetime.datetime(2014, 9, 22, 17, 25, 46) m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, raw_message_with_many_recipients) m.thread = thread db.session.add(m) # Check that no database error is raised. db.session.commit()
def add_fake_msg_with_calendar_part(db_session, account, ics_str): parsed = mime.create.multipart('mixed') parsed.append( mime.create.attachment('text/calendar', ics_str, disposition=None)) msg = Message.create_from_synced(account, 22, '[Gmail]/All Mail', datetime.utcnow(), parsed.to_string()) msg.thread = add_fake_thread(db_session, account.namespace.id) assert msg.has_attached_events return msg
def test_truncate_recipients(db, raw_message_with_many_recipients): account = db.session.query(Account).get(ACCOUNT_ID) assert account.namespace.id == NAMESPACE_ID received_date = datetime.datetime(2014, 9, 22, 17, 25, 46), m = Message.create_from_synced(account, 139219, '[Gmail]/All Mail', received_date, raw_message_with_many_recipients) m.thread_id = 1 db.session.add(m) # Check that no database error is raised. db.session.commit()
def add_fake_msg_with_calendar_part(db_session, account, ics_str): parsed = mime.create.multipart('mixed') parsed.append( mime.create.attachment('text/calendar', ics_str, disposition=None) ) msg = Message.create_from_synced( account, 22, '[Gmail]/All Mail', datetime.utcnow(), parsed.to_string()) msg.thread = add_fake_thread(db_session, account.namespace.id) assert msg.has_attached_events return msg
def test_handle_bad_content_disposition( default_account, default_namespace, raw_message_with_bad_content_disposition): received_date = datetime.datetime(2014, 9, 22, 17, 25, 46) m = Message.create_from_synced(default_account, 139219, '[Gmail]/All Mail', received_date, raw_message_with_bad_content_disposition) assert m.namespace_id == default_namespace.id assert sorted(m.to_addr) == [(u'', u'*****@*****.**'), (u'', u'*****@*****.**'), (u'', u'*****@*****.**')] assert len(m.parts) == 3 assert m.received_date == received_date assert all(part.block.namespace_id == m.namespace_id for part in m.parts)
def send(request): if request.method == 'POST': sender = request.user query_set = User.objects.filter(username=request.POST.get('recipient')) if not query_set.exists(): return index(request, error_messages = ['User "' + str(request.POST.get('recipient')) + '" could not be found.']) recipient = query_set[0] body = request.POST.get('body') encrypt = True if request.POST.get('encrypt') else False key = None if encrypt: key = os.urandom(32) body = AESCipher(key).encrypt(body).decode('utf-8') message = Message(sender=sender, recipient=recipient, body=body, encrypted=encrypt, key=key) message.save() return HttpResponseRedirect('/inbox') else: # This should never happen return index(request, error_messages = ['Invalid request.'])
def test_message_from_synced(db, raw_message): account = db.session.query(Account).get(ACCOUNT_ID) assert account.namespace.id == NAMESPACE_ID received_date = datetime.datetime(2014, 9, 22, 17, 25, 46), m = Message.create_from_synced(account, 139219, '[Gmail]/All Mail', received_date, raw_message) assert m.namespace_id == NAMESPACE_ID assert sorted(m.to_addr) == [(u'', u'*****@*****.**'), (u'', u'*****@*****.**'), (u'', u'*****@*****.**')] assert len(m.parts) == 4 assert 'Attached Message Part' in [part.block.filename for part in m.parts] assert m.received_date == received_date assert all(part.block.namespace_id == m.namespace_id for part in m.parts)
def add_fake_message(account_id, thread, to_email, received_date, db_session): """ One-off helper function to add 'fake' messages to the datastore.""" m = Message() m.from_addr = [('', to_email)] m.received_date = received_date m.size = 0 m.sanitized_body = '' m.snippet = '' m.thread = thread update_contacts_from_message(db_session, m, account_id) db_session.add(m) db_session.commit()
def test_local_storage(db, config): account = db.session.query(Account).get(ACCOUNT_ID) m = Message(account=account, mid='', folder_name='', received_date=datetime.utcnow(), flags='', body_string=message) m.thread_id = THREAD_ID db.session.add(m) db.session.commit() msg = db.session.query(Message).get(m.id) # Ensure .data will access and decrypt the encrypted data from disk assert not hasattr(msg, '_data') for p in msg.parts: assert p.encryption_scheme == \ EncryptionScheme.SECRETBOX_WITH_STATIC_KEY # Accessing .data verifies data integrity data = p.data raw = p._get_from_disk assert data != raw
def add_fake_msg_with_calendar_part(db_session, account, ics_str, thread=None): from inbox.models import Message parsed = mime.create.multipart("mixed") parsed.append(mime.create.attachment("text/calendar", ics_str, disposition=None)) msg = Message.create_from_synced(account, 22, "[Gmail]/All Mail", datetime.utcnow(), parsed.to_string()) msg.from_addr = [("Ben Bitdiddle", "*****@*****.**")] if thread is None: msg.thread = add_fake_thread(db_session, account.namespace.id) else: msg.thread = thread assert msg.has_attached_events return msg
def add_fake_msg_with_calendar_part(db_session, account, ics_str, thread=None): from inbox.models import Message parsed = mime.create.multipart('mixed') parsed.append( mime.create.attachment('text/calendar', ics_str, disposition=None)) msg = Message.create_from_synced(account, 22, '[Gmail]/All Mail', datetime.utcnow(), parsed.to_string()) msg.from_addr = [('Ben Bitdiddle', '*****@*****.**')] if thread is None: msg.thread = add_fake_thread(db_session, account.namespace.id) else: msg.thread = thread assert msg.has_attached_events return msg
def create_imap_message(db_session, account, folder, msg): """ IMAP-specific message creation logic. Returns ------- imapuid : inbox.models.backends.imap.ImapUid New db object, which links to new Message and Block objects through relationships. All new objects are uncommitted. """ log.debug("creating message", account_id=account.id, folder_name=folder.name, mid=msg.uid) new_message = Message.create_from_synced( account=account, mid=msg.uid, folder_name=folder.name, received_date=msg.internaldate, body_string=msg.body, ) # Check to see if this is a copy of a message that was first created # by the Nylas API. If so, don't create a new object; just use the old one. existing_copy = reconcile_message(new_message, db_session) if existing_copy is not None: new_message = existing_copy imapuid = ImapUid(account=account, folder=folder, msg_uid=msg.uid, message=new_message) imapuid.update_flags(msg.flags) if msg.g_labels is not None: imapuid.update_labels(msg.g_labels) # Update the message's metadata with db_session.no_autoflush: is_draft = imapuid.is_draft and (folder.canonical_name == "drafts" or folder.canonical_name == "all") update_message_metadata(db_session, account, new_message, is_draft) update_contacts_from_message(db_session, new_message, account.namespace.id) return imapuid
def test_soft_delete(db, config): from inbox.models import Folder, Message from inbox.models.backends.imap import ImapUid f = Folder(name='DOES NOT EXIST', account_id=ACCOUNT_ID) db.session.add(f) db.session.flush() m = Message() m.namespace_id = NAMESPACE_ID m.thread_id = 1 m.received_date = datetime.datetime.utcnow() m.size = 0 m.sanitized_body = "" m.snippet = "" u = ImapUid(message=m, account_id=ACCOUNT_ID, folder_id=f.id, msg_uid=9999, extra_flags="") db.session.add_all([m, u]) f.mark_deleted() u.mark_deleted() db.session.commit() m_id = m.id # bypass custom query method to confirm creation db.new_session(ignore_soft_deletes=False) f = db.session.query(Folder).filter_by(name='DOES NOT EXIST').one() assert f, "Can't find Folder object" assert f.deleted_at is not None, "Folder not marked as deleted" db.new_session(ignore_soft_deletes=True) with pytest.raises(NoResultFound): db.session.query(Folder).filter(Folder.name == 'DOES NOT EXIST').one() count = db.session.query(Folder).filter( Folder.name == 'DOES NOT EXIST').count() assert count == 0, "Shouldn't find any deleted folders!" m = db.session.query(Message).filter_by(id=m_id).one() assert not m.imapuids, "imapuid was deleted!"
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 format_transactions_after_pointer(namespace, pointer, db_session, result_limit, exclude_types=None, include_types=None, exclude_folders=True, expand=False): """ Return a pair (deltas, new_pointer), where deltas is a list of change events, represented as dictionaries: { "object": <API object type, e.g. "thread">, "event": <"create", "modify", or "delete>, "attributes": <API representation of the object for insert/update events> "cursor": <public_id of the transaction> } and new_pointer is the integer id of the last included transaction Arguments --------- namespace_id: int Id of the namespace for which to get changes. pointer: int Process transactions starting after this id. db_session: new_session database session result_limit: int Maximum number of results to return. (Because we may roll up multiple changes to the same object, fewer results can be returned.) format_transaction_fn: function pointer Function that defines how to format the transactions. exclude_types: list, optional If given, don't include transactions for these types of objects. """ exclude_types = set(exclude_types) if exclude_types else set() # Begin backwards-compatibility shim -- suppress new object types for now, # because clients may not be able to deal with them. exclude_types.add('account') if exclude_folders is True: exclude_types.update(('folder', 'label')) # End backwards-compatibility shim. last_trx = _get_last_trx_id_for_namespace(namespace.id, db_session) if last_trx == pointer: return ([], pointer) while True: # deleted_at condition included to allow this query to be satisfied via # the legacy index on (namespace_id, deleted_at) for performance. # Also need to explicitly specify the index hint because the query # planner is dumb as nails and otherwise would make this super slow for # some values of namespace_id and pointer. # TODO(emfree): Remove this hack and ensure that the right index (on # namespace_id only) exists. transactions = db_session.query(Transaction). \ filter( Transaction.id > pointer, Transaction.namespace_id == namespace.id, Transaction.deleted_at.is_(None)). \ with_hint(Transaction, 'USE INDEX (namespace_id_deleted_at)') if exclude_types is not None: transactions = transactions.filter( ~Transaction.object_type.in_(exclude_types)) if include_types is not None: transactions = transactions.filter( Transaction.object_type.in_(include_types)) transactions = transactions. \ order_by(asc(Transaction.id)).limit(result_limit).all() if not transactions: return ([], pointer) results = [] # Group deltas by object type. trxs_by_obj_type = collections.defaultdict(list) for trx in transactions: trxs_by_obj_type[trx.object_type].append(trx) for obj_type, trxs in trxs_by_obj_type.items(): # Build a dictionary mapping pairs (record_id, command) to # transaction. If successive modifies for a given record id appear # in the list of transactions, this will only keep the latest # one (which is what we want). latest_trxs = {(trx.record_id, trx.command): trx for trx in sorted(trxs, key=lambda t: t.id) }.values() # Load all referenced not-deleted objects. ids_to_query = [ trx.record_id for trx in latest_trxs if trx.command != 'delete' ] object_cls = transaction_objects()[obj_type] query = db_session.query(object_cls).filter( object_cls.id.in_(ids_to_query), object_cls.namespace_id == namespace.id) if object_cls == Thread: query = query.options(*Thread.api_loading_options(expand)) elif object_cls == Message: query = query.options(*Message.api_loading_options(expand)) objects = {obj.id: obj for obj in query} for trx in latest_trxs: delta = { 'object': trx.object_type, 'event': EVENT_NAME_FOR_COMMAND[trx.command], 'id': trx.object_public_id, 'cursor': trx.public_id } if trx.command != 'delete': obj = objects.get(trx.record_id) if obj is None: continue repr_ = encode(obj, namespace_public_id=namespace.public_id, expand=expand) delta['attributes'] = repr_ results.append((trx.id, delta)) if results: # Sort deltas by id of the underlying transactions. results.sort() deltas = [d for _, d in results] return (deltas, results[-1][0]) else: # It's possible that none of the referenced objects exist any more, # meaning the result list is empty. In that case, keep traversing # the log until we get actual results or reach the end. pointer = transactions[-1].id
def create_from_synced(account, raw_message): received_date = datetime.datetime.utcnow() return Message.create_from_synced(account, 22, '[Gmail]/All Mail', received_date, raw_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 format_transactions_after_pointer(namespace, pointer, db_session, result_limit, exclude_types=None, include_types=None, exclude_folders=True, exclude_metadata=True, exclude_account=True, expand=False, is_n1=False): """ Return a pair (deltas, new_pointer), where deltas is a list of change events, represented as dictionaries: { "object": <API object type, e.g. "thread">, "event": <"create", "modify", or "delete>, "attributes": <API representation of the object for insert/update events> "cursor": <public_id of the transaction> } and new_pointer is the integer id of the last included transaction Arguments --------- namespace_id: int Id of the namespace for which to get changes. pointer: int Process transactions starting after this id. db_session: new_session database session result_limit: int Maximum number of results to return. (Because we may roll up multiple changes to the same object, fewer results can be returned.) format_transaction_fn: function pointer Function that defines how to format the transactions. exclude_types: list, optional If given, don't include transactions for these types of objects. """ exclude_types = set(exclude_types) if exclude_types else set() # Begin backwards-compatibility shim -- suppress new object types for now, # because clients may not be able to deal with them. if exclude_folders is True: exclude_types.update(('folder', 'label')) if exclude_account is True: exclude_types.add('account') # End backwards-compatibility shim. # Metadata is excluded by default, and can only be included by setting the # exclude_metadata flag to False. If listed in include_types, remove it. if exclude_metadata is True: exclude_types.add('metadata') if include_types is not None and 'metadata' in include_types: include_types.remove('metadata') last_trx = _get_last_trx_id_for_namespace(namespace.id, db_session) if last_trx == pointer: return ([], pointer) while True: transactions = db_session.query(Transaction). \ filter( Transaction.id > pointer, Transaction.namespace_id == namespace.id) if exclude_types is not None: transactions = transactions.filter( ~Transaction.object_type.in_(exclude_types)) if include_types is not None: transactions = transactions.filter( Transaction.object_type.in_(include_types)) transactions = transactions. \ order_by(asc(Transaction.id)).limit(result_limit).all() if not transactions: return ([], pointer) results = [] # Group deltas by object type. trxs_by_obj_type = collections.defaultdict(list) for trx in transactions: trxs_by_obj_type[trx.object_type].append(trx) for obj_type, trxs in trxs_by_obj_type.items(): # Build a dictionary mapping pairs (record_id, command) to # transaction. If successive modifies for a given record id appear # in the list of transactions, this will only keep the latest # one (which is what we want). latest_trxs = {(trx.record_id, trx.command): trx for trx in sorted(trxs, key=lambda t: t.id) }.values() # Load all referenced not-deleted objects. ids_to_query = [ trx.record_id for trx in latest_trxs if trx.command != 'delete' ] object_cls = transaction_objects()[obj_type] if object_cls == Account: # The base query for Account queries the /Namespace/ table # since the API-returned "`account`" is a `namespace` # under-the-hood. query = db_session.query(Namespace).join(Account).filter( Account.id.in_(ids_to_query), Namespace.id == namespace.id) # Key by /namespace.account_id/ -- # namespace.id may not be equal to account.id # and trx.record_id == account.id for `account` trxs. objects = {obj.account_id: obj for obj in query} else: query = db_session.query(object_cls).filter( object_cls.id.in_(ids_to_query), object_cls.namespace_id == namespace.id) if object_cls == Thread: query = query.options(*Thread.api_loading_options(expand)) elif object_cls == Message: query = query.options(*Message.api_loading_options(expand)) objects = {obj.id: obj for obj in query} for trx in latest_trxs: delta = { 'object': trx.object_type, 'event': EVENT_NAME_FOR_COMMAND[trx.command], 'id': trx.object_public_id, 'cursor': trx.public_id } if trx.command != 'delete': obj = objects.get(trx.record_id) if obj is None: continue repr_ = encode(obj, namespace_public_id=namespace.public_id, expand=expand, is_n1=is_n1) delta['attributes'] = repr_ results.append((trx.id, delta)) if results: # Sort deltas by id of the underlying transactions. results.sort() deltas = [d for _, d in results] return (deltas, results[-1][0]) else: # It's possible that none of the referenced objects exist any more, # meaning the result list is empty. In that case, keep traversing # the log until we get actual results or reach the end. pointer = transactions[-1].id
def add_fake_message(db_session, namespace_id, thread=None, from_addr=None, to_addr=None, cc_addr=None, bcc_addr=None, received_date=None, subject='', body='', snippet='', g_msgid=None, add_sent_category=False): from inbox.models import Message, Category from inbox.contacts.process_mail import update_contacts_from_message m = Message() m.namespace_id = namespace_id m.from_addr = from_addr or [] m.to_addr = to_addr or [] m.cc_addr = cc_addr or [] m.bcc_addr = bcc_addr or [] m.received_date = received_date or datetime.utcnow() m.size = 0 m.is_read = False m.is_starred = False m.body = body m.snippet = snippet m.subject = subject m.g_msgid = g_msgid if thread: thread.messages.append(m) update_contacts_from_message(db_session, m, thread.namespace) db_session.add(m) db_session.commit() if add_sent_category: category = Category.find_or_create(db_session, namespace_id, 'sent', 'sent', type_='folder') if category not in m.categories: m.categories.add(category) db_session.commit() return m
def add_fake_message(db_session, namespace_id, thread=None, from_addr=None, to_addr=None, cc_addr=None, bcc_addr=None, received_date=None, subject='', body='', snippet='', g_msgid=None, add_sent_category=False): from inbox.models import Message, Category from inbox.contacts.process_mail import update_contacts_from_message m = Message() m.namespace_id = namespace_id m.from_addr = from_addr or [] m.to_addr = to_addr or [] m.cc_addr = cc_addr or [] m.bcc_addr = bcc_addr or [] m.received_date = received_date or datetime.utcnow() m.size = 0 m.is_read = False m.is_starred = False m.body = body m.snippet = snippet m.subject = subject m.g_msgid = g_msgid if thread: thread.messages.append(m) update_contacts_from_message(db_session, m, thread.namespace) db_session.add(m) db_session.commit() if add_sent_category: category = Category.find_or_create( db_session, namespace_id, 'sent', 'sent', type_='folder') if category not in m.categories: m.categories.add(category) db_session.commit() return m
def test_threading_limit(db, folder_sync_engine, monkeypatch): """Test that custom threading doesn't produce arbitrarily long threads, which eventually break things.""" from inbox.models import Message, Thread # Shorten bound to make test faster MAX_THREAD_LENGTH = 10 monkeypatch.setattr( 'inbox.mailsync.backends.imap.generic.MAX_THREAD_LENGTH', MAX_THREAD_LENGTH) namespace_id = folder_sync_engine.namespace_id msg = MockRawMessage([]) for i in range(3 * MAX_THREAD_LENGTH): m = Message() m.namespace_id = namespace_id m.received_date = datetime.datetime.utcnow() m.references = [] m.size = 0 m.body = '' m.from_addr = [("Karim Hamidou", "*****@*****.**")] m.to_addr = [("Eben Freeman", "*****@*****.**")] m.snippet = '' m.subject = 'unique subject' db.session.add(m) folder_sync_engine.add_message_to_thread(db.session, m, msg) db.session.commit() new_threads = db.session.query(Thread). \ filter(Thread.subject == 'unique subject').all() assert len(new_threads) == 3 assert all(len(thread.messages) == MAX_THREAD_LENGTH for thread in new_threads)
def add_fake_message(db_session, thread, from_addr=None, to_addr=None, cc_addr=None, bcc_addr=None): m = Message() m.namespace_id = NAMESPACE_ID m.from_addr = from_addr or [] m.to_addr = to_addr or [] m.cc_addr = cc_addr or [] m.bcc_addr = bcc_addr or [] m.received_date = datetime.utcnow() m.size = 0 m.sanitized_body = '' m.snippet = '' m.thread = thread update_contacts_from_message(db_session, m, thread.namespace) db_session.add(m) db_session.commit() return m