Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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
Beispiel #7
0
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
Beispiel #10
0
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 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'
Beispiel #13
0
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
Beispiel #14
0
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 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
Beispiel #16
0
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
Beispiel #17
0
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_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 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_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'
Beispiel #26
0
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)
Beispiel #27
0
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'
Beispiel #29
0
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
Beispiel #31
0
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
Beispiel #32
0
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
Beispiel #34
0
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()
Beispiel #35
0
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
Beispiel #37
0
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)
Beispiel #38
0
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_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)
Beispiel #40
0
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)
Beispiel #41
0
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)
Beispiel #42
0
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()
Beispiel #43
0
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
Beispiel #44
0
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
Beispiel #45
0
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
Beispiel #46
0
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
Beispiel #47
0
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!"
Beispiel #48
0
def create_draft_from_mime(account, raw_mime, db_session):
    our_uid = generate_public_id()  # base-36 encoded string
    new_headers = ('X-INBOX-ID: {0}-0\r\n'
                   'Message-Id: <{0}[email protected]>\r\n'
                   'User-Agent: NylasMailer/{1}\r\n').format(our_uid, VERSION)
    new_body = new_headers + raw_mime

    with db_session.no_autoflush:
        msg = Message.create_from_synced(account, '', '',
                                         datetime.utcnow(), new_body)

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

        if msg.subject is not None and not \
                                        isinstance(msg.subject, basestring):
            raise InputError('"subject" should be a string')

        if not isinstance(msg.body, basestring):
            raise InputError('"body" should be a string')

        if msg.references or msg.in_reply_to:
            msg.is_reply = True

        thread_cls = account.thread_cls
        msg.thread = thread_cls(
            subject=msg.subject,
            recentdate=msg.received_date,
            namespace=account.namespace,
            subjectdate=msg.received_date)
        if msg.attachments:
                attachment_tag = account.namespace.tags['attachment']
                msg.thread.apply_tag(attachment_tag)

        msg.is_created = True
        msg.is_sent = True
        msg.is_draft = False
        msg.is_read = True
    db_session.add(msg)
    db_session.flush()
    return msg
Beispiel #49
0
def create_draft_from_mime(account, raw_mime, db_session):
    our_uid = generate_public_id()  # base-36 encoded string
    new_headers = ('X-INBOX-ID: {0}-0\r\n'
                   'Message-Id: <{0}[email protected]>\r\n'
                   'User-Agent: NylasMailer/{1}\r\n').format(our_uid, VERSION)
    new_body = new_headers + raw_mime

    with db_session.no_autoflush:
        msg = Message.create_from_synced(account, '', '', datetime.utcnow(),
                                         new_body)

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

        if msg.subject is not None and not \
                                        isinstance(msg.subject, basestring):
            raise InputError('"subject" should be a string')

        if not isinstance(msg.body, basestring):
            raise InputError('"body" should be a string')

        if msg.references or msg.in_reply_to:
            msg.is_reply = True

        thread_cls = account.thread_cls
        msg.thread = thread_cls(subject=msg.subject,
                                recentdate=msg.received_date,
                                namespace=account.namespace,
                                subjectdate=msg.received_date)
        if msg.attachments:
            attachment_tag = account.namespace.tags['attachment']
            msg.thread.apply_tag(attachment_tag)

        msg.is_created = True
        msg.is_sent = True
        msg.is_draft = False
        msg.is_read = True
    db_session.add(msg)
    db_session.flush()
    return msg
Beispiel #50
0
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!"
Beispiel #51
0
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_from_synced(account, raw_message):
    received_date = datetime.datetime.utcnow()
    return Message.create_from_synced(account, 22, '[Gmail]/All Mail',
                                      received_date, raw_message)
Beispiel #54
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
Beispiel #55
0
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
Beispiel #56
0
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
Beispiel #57
0
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
Beispiel #58
0
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)
Beispiel #59
0
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