~didrocks/unity/altf10

42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
1
__all__ = [
2
    'MemoryStore',
3
    ]
4
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
5
import email
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
6
import simplejson
7
from urlparse import parse_qs
8
44 by Curtis Hovey
Move errors to their own module.
9
from grackle.error import (
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
10
    ArchiveIdNotFound,
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
11
    MessageIdNotFound,
12
    UnparsableDateRange,
13
    UnsupportedDisplayType,
14
    UnsupportedOrder,
15
    )
16
17
18
SUPPORTED_DISPLAY_TYPES = set(['all', 'text-only', 'headers-only'])
19
20
21
SUPPORTED_ORDERS = set(
22
    ['date', 'author', 'subject', 'thread_newest', 'thread_oldest',
23
     'thread_subject'])
24
25
26
def threaded_messages(messages):
27
    threads = {}
28
    count = 0
29
    pending = []
30
    for message in messages:
31
        if message.get('replies') is None:
32
            threads[message['message_id']] = [message]
33
            count += 1
34
        else:
35
            pending.append(message)
36
    for message in pending:
37
        threads[message['replies']].append(message)
38
    return threads.values()
39
40
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
41
def make_json_message(message_id, raw_message):
42
    message = email.message_from_string(raw_message)
43
    headers = dict(message.items())
46 by Curtis Hovey
Implemented a partial put into the MemoryStore.
44
    message = {
45
        'message_id': message_id,
46
        'headers': headers,
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
47
        # This is broken because the in-reply-to must be encoded.
48
        # X-Message-ID-Hash is calculated from the Base 32.
49
        'thread_id': headers.get('in-reply-to', message_id),
50
        'date': headers.get('date'),
51
        'subject': headers.get('subject'),
52
        'author': headers.get('from'),
53
        'hidden': False,
46 by Curtis Hovey
Implemented a partial put into the MemoryStore.
54
        'attachments': [],
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
55
        'replies': [],
56
        'body': raw_message,
46 by Curtis Hovey
Implemented a partial put into the MemoryStore.
57
        }
58
    return message
59
60
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
61
class MemoryStore:
62
    """A memory-backed message store."""
63
45 by Curtis Hovey
Rename variable for clarity.
64
    def __init__(self, message_archives):
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
65
        """Constructor."""
45 by Curtis Hovey
Rename variable for clarity.
66
        self.message_archives = message_archives
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
67
68
    @staticmethod
69
    def is_multipart(message):
70
        return isinstance(message['body'], list)
71
47 by Curtis Hovey
Raise ArchiveIdNotFound if the client puts a message into an unknown archive.
72
    def put_message(self, archive_id, message_id, raw_message):
73
        # XXX sinzui 2012-02-29: this needs to raise an error
74
        # if the th archive_id is invalid, message_id is not base32
75
        # or the raw message is not an email.
76
        if archive_id not in self.message_archives:
77
            raise ArchiveIdNotFound()
78
        if not raw_message:
79
            raise ValueError('raw_message is not a message.')
80
        json_message = make_json_message(message_id, raw_message)
46 by Curtis Hovey
Implemented a partial put into the MemoryStore.
81
        messages = self.message_archives[archive_id]
82
        messages.append(json_message)
83
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
84
    def get_messages(self, archive_id, query_string):
85
        """Return matching messages.
86
87
        :param archive_id: The archive to retrieve from.
88
        :param query_string: Contains 'parameters', which is a JSON-format
89
            string describing parameters.
90
        """
91
        query = parse_qs(query_string)
92
        parameters = simplejson.loads(query['parameters'][0])
93
        order = parameters.get('order')
45 by Curtis Hovey
Rename variable for clarity.
94
        messages = self.message_archives[archive_id]
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
95
        if order is not None:
96
            if order not in SUPPORTED_ORDERS:
97
                raise UnsupportedOrder
98
            elif order.startswith('thread_'):
99
                threaded = threaded_messages(messages)
100
                messages = []
101
                if order == 'thread_subject':
102
                    threaded.sort(key=lambda t: t[0]['subject'])
103
                if order == 'thread_oldest':
104
                    threaded.sort(key=lambda t: min(m['date'] for m in t))
105
                if order == 'thread_newest':
106
                    threaded.sort(key=lambda t: max(m['date'] for m in t))
107
                for thread in threaded:
108
                    messages.extend(thread)
109
            else:
110
                messages.sort(key=lambda m: m[order])
111
        display_type = parameters.get('display_type', 'all')
112
        if display_type not in SUPPORTED_DISPLAY_TYPES:
113
            raise UnsupportedDisplayType
114
        if 'date_range' in parameters:
115
            try:
116
                start_date, end_date = parameters['date_range'].split('..')
117
                if not start_date or not end_date:
118
                    raise UnparsableDateRange
119
            except ValueError:
120
                raise UnparsableDateRange
121
        new_messages = []
122
        for message in messages:
123
            if (not parameters['include_hidden'] and message['hidden']):
124
                continue
125
            if ('message_ids' in parameters
126
                and message['message_id'] not in parameters['message_ids']):
127
                continue
128
            if ('date_range' in parameters
129
                and (message['date'] < start_date
130
                     or message['date'] > end_date)):
131
                continue
132
            message = dict(message)
133
            if 'headers' in parameters:
134
                headers = dict(
135
                    (k, v) for k, v in message['headers'].iteritems()
136
                    if k in parameters['headers'])
137
                message['headers'] = headers
138
            if display_type == 'headers-only':
139
                del message['body']
140
            elif display_type == 'text-only' and self.is_multipart(message):
141
                text_parts = [
142
                    part.get_payload() for part in message['body']
143
                    if part.get_content_type() == 'text/plain']
144
                message['body'] = '\n\n'.join(text_parts)
145
            elif display_type == 'all' and self.is_multipart(message):
146
                parts = [str(part.get_payload()) for part in message['body']]
147
                message['body'] = '\n\n'.join(parts)
148
            max_body = parameters.get('max_body_length')
149
            if max_body is not None and display_type != 'headers-only':
150
                message['body'] = message['body'][:max_body]
151
            new_messages.append(message)
152
        messages = new_messages
153
        limit = parameters.get('limit', 100)
154
        memo = parameters.get('memo')
155
        message_id_indices = dict(
156
            (m['message_id'], idx) for idx, m in enumerate(messages))
157
        if memo is None:
158
            start = 0
159
        else:
160
            start = message_id_indices[memo.encode('rot13')]
161
        if start > 0:
162
            previous_memo = messages[start - 1]['message_id'].encode('rot13')
163
        else:
164
            previous_memo = None
165
        end = min(start + limit, len(messages))
166
        if end < len(messages):
167
            next_memo = messages[end]['message_id'].encode('rot13')
168
        else:
169
            next_memo = None
170
        messages = messages[start:end]
171
172
        response = {
173
            'messages': messages,
174
            'next_memo': next_memo,
175
            'previous_memo': previous_memo
176
            }
177
        return response
178
179
    def hide_message(self, archive_id, query_string):
180
        """Return matching messages.
181
182
        :param archive_id: The archive to retrieve from.
183
        :param query_string: Contains 'parameters', which is a JSON-format
184
            string describing parameters.
185
        """
186
        query = parse_qs(query_string)
187
        parameters = simplejson.loads(query['parameters'][0])
188
        message_id = parameters['message_id']
189
        hidden = parameters['hidden']
45 by Curtis Hovey
Rename variable for clarity.
190
        messages = self.message_archives[archive_id]
42 by Curtis Hovey
Separate gracle MemoryStore to its own module.
191
        for message in messages:
192
            if message['message_id'] == message_id:
193
                message['hidden'] = hidden
194
            response = {
195
                'message_id': message_id,
196
                'hidden': hidden,
197
                }
198
            return response
199
        raise MessageIdNotFound