23
24
UnsupportedDisplayType,
26
from grackle.store import (
32
29
def make_message(message_id, body='body', headers=None, hidden=False):
33
30
if headers is None:
36
'Message-Id': message_id,
32
headers['Message-Id'] = message_id
34
'message_id': message_id,
36
'thread_id': message_id,
37
'date': headers.get('date', '2005-01-01'),
38
'subject': headers.get('subject', 'subject'),
39
'author': headers.get('author', 'author'),
42
'replies': headers.get('in-reply-to', None),
42
message_headers.update(headers.items())
44
message.set_payload(body)
45
for key, value in message_headers.items():
47
return make_json_message(message_id, message.as_string(), hidden)
50
48
def make_mime_message(message_id, body='body', headers=None, hidden=False,
51
49
attachment_type=None):
52
parts = MIMEMultipart()
53
parts.attach(MIMEText(body))
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
54
52
if attachment_type is not None:
55
53
attachment = Message()
56
54
attachment.set_payload('attactment data.')
57
55
attachment['Content-Type'] = attachment_type
58
56
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
59
parts.attach(attachment)
60
return make_message(message_id, parts.as_string(), headers, hidden)
57
message.attach(attachment)
58
return make_message(message_id, message.get_payload(), headers, hidden)
61
def threaded_messages(messages):
65
for message in messages:
66
if message.get('replies') is None:
67
threads[message['message_id']] = [message]
70
pending.append(message)
71
for message in pending:
72
threads[message['replies']].append(message)
73
return threads.values()
77
"""A memory-backed message store."""
79
def __init__(self, messages):
81
self.messages = messages
84
def is_multipart(message):
85
return isinstance(message['body'], list)
87
def get_messages(self, archive_id, query_string):
88
"""Return matching messages.
90
:param archive_id: The archive to retrieve from.
91
:param query_string: Contains 'parameters', which is a JSON-format
92
string describing parameters.
94
query = parse_qs(query_string)
95
parameters = simplejson.loads(query['parameters'][0])
96
order = parameters.get('order')
97
messages = self.messages[archive_id]
99
if order not in SUPPORTED_ORDERS:
100
raise UnsupportedOrder
101
elif order.startswith('thread_'):
102
threaded = threaded_messages(messages)
104
if order == 'thread_subject':
105
threaded.sort(key=lambda t: t[0]['subject'])
106
if order == 'thread_oldest':
107
threaded.sort(key=lambda t: min(m['date'] for m in t))
108
if order == 'thread_newest':
109
threaded.sort(key=lambda t: max(m['date'] for m in t))
110
for thread in threaded:
111
messages.extend(thread)
113
messages.sort(key=lambda m: m[order])
114
display_type = parameters.get('display_type', 'all')
115
if display_type not in SUPPORTED_DISPLAY_TYPES:
116
raise UnsupportedDisplayType
117
if 'date_range' in parameters:
119
start_date, end_date = parameters['date_range'].split('..')
121
raise UnparsableDateRange
123
for message in messages:
124
if (not parameters['include_hidden'] and message['hidden']):
126
if ('message_ids' in parameters
127
and message['message_id'] not in parameters['message_ids']):
129
if ('date_range' in parameters
130
and (message['date'] < start_date
131
or message['date'] > end_date)):
133
message = dict(message)
134
if 'headers' in parameters:
136
(k, v) for k, v in message['headers'].iteritems()
137
if k in parameters['headers'])
138
message['headers'] = headers
139
if display_type == 'headers-only':
141
elif display_type == 'text-only' and self.is_multipart(message):
143
part.get_payload() for part in message['body']
144
if part.get_content_type() == 'text/plain']
145
message['body'] = '\n\n'.join(text_parts)
146
elif display_type == 'all' and self.is_multipart(message):
147
parts = [str(part.get_payload()) for part in message['body']]
148
message['body'] = '\n\n'.join(parts)
149
max_body = parameters.get('max_body_length')
150
if max_body is not None and display_type != 'headers-only':
151
message['body'] = message['body'][:max_body]
152
new_messages.append(message)
153
messages = new_messages
154
limit = parameters.get('limit', 100)
155
memo = parameters.get('memo')
156
message_id_indices = dict(
157
(m['message_id'], idx) for idx, m in enumerate(messages))
161
start = message_id_indices[memo.encode('rot13')]
163
previous_memo = messages[start - 1]['message_id'].encode('rot13')
166
end = min(start + limit, len(messages))
167
if end < len(messages):
168
next_memo = messages[end]['message_id'].encode('rot13')
171
messages = messages[start:end]
174
'messages': messages,
175
'next_memo': next_memo,
176
'previous_memo': previous_memo
63
181
class ForkedFakeService:
64
182
"""A Grackle service fake, as a ContextManager."""
66
def __init__(self, port, message_archives=None, write_logs=False):
184
def __init__(self, port, messages=None, write_logs=False):
69
187
:param port: The tcp port to use.
70
:param message_archives: A dict of lists of dicts representing
71
archives of messages. The outer dict represents the archive,
72
the list represents the list of messages for that archive.
188
:param messages: A dict of lists of dicts representing messages. The
189
outer dict represents the archive, the list represents the list of
190
messages for that archive.
73
191
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
80
self.message_archives = message_archives
198
self.messages = messages
81
199
self.read_end, self.write_end = os.pipe()
82
200
self.write_logs = write_logs
85
def from_client(client, message_archives=None):
203
def from_client(client, messages=None):
86
204
"""Instantiate a ForkedFakeService from the client.
88
206
:param port: The client to provide service for.
89
:param message_archives: A dict of lists of dicts representing
90
archives of messages. The outer dict represents the archive,
91
the list represents the list of messages for that archive.
207
:param messages: A dict of lists of dicts representing messages. The
208
outer dict represents the archive, the list represents the list of
209
messages for that archive.
93
return ForkedFakeService(client.port, message_archives)
211
return ForkedFakeService(client.port, messages)
95
213
def is_ready(self):
96
214
"""Tell the parent process that the server is ready for writes."""
176
296
class TestPutMessage(TestCase):
178
298
def test_put_message(self):
179
client = GrackleClient('localhost', 8420)
180
message_archives = {'arch1': []}
181
with ForkedFakeService.from_client(client, message_archives):
182
client.put_message('arch1', 'id1', StringIO('This is a message'))
183
response = client.get_messages('arch1')
184
self.assertEqual(1, len(response['messages']))
185
message = response['messages'][0]
186
self.assertEqual('id1', message['message_id'])
188
def test_put_message_without_archive(self):
189
client = GrackleClient('localhost', 8421)
190
message_archives = {'arch1': []}
191
with ForkedFakeService.from_client(client, message_archives):
299
client = GrackleClient('localhost', 8436)
300
with ForkedFakeService.from_client(client):
301
client.put_message('arch1', 'asdf', StringIO('This is a message'))
192
302
with ExpectedException(Exception, 'wtf'):
193
client.put_message('no-archive', 'id1', StringIO('message'))
303
client.put_message('arch1', 'asdf',
304
StringIO('This is not a message'))
196
307
class TestGetMessages(TestCase):
454
563
self.assertEqual(['bar', 'naf', 'qux'], ids)
456
565
def test_date_range_unparsabledaterange(self):
457
client = GrackleClient('localhost', 8449)
566
client = GrackleClient('localhost', 8448)
458
567
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
459
568
with ForkedFakeService.from_client(client, archive):
460
569
with ExpectedException(UnparsableDateRange, ''):
461
570
client.get_messages('baz', date_range='2012-01-01')
463
def test_date_range_unparsabledaterange_missing_part(self):
464
client = GrackleClient('localhost', 8450)
465
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
466
with ForkedFakeService.from_client(client, archive):
467
with ExpectedException(UnparsableDateRange, ''):
468
client.get_messages('baz', date_range='2012-01-01..')
470
def test_date_range_unparsabledaterange_extra_part(self):
471
client = GrackleClient('localhost', 8451)
472
archive = {'baz': [make_message('foo', body=u'abcdefghi')]}
473
with ForkedFakeService.from_client(client, archive):
474
with ExpectedException(UnparsableDateRange, ''):
475
client.get_messages('baz', date_range='2012-01..12-02..12-03')