24
23
UnsupportedDisplayType,
26
from grackle.store import (
29
32
def make_message(message_id, body='body', headers=None, hidden=False):
30
33
if headers is None:
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),
36
'Message-Id': message_id,
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)
48
50
def make_mime_message(message_id, body='body', headers=None, hidden=False,
49
51
attachment_type=None):
50
message = MIMEMultipart()
51
message.attach(MIMEText(body))
52
parts = MIMEMultipart()
53
parts.attach(MIMEText(body))
52
54
if attachment_type is not None:
53
55
attachment = Message()
54
56
attachment.set_payload('attactment data.')
55
57
attachment['Content-Type'] = attachment_type
56
58
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
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('..')
120
if not start_date or not end_date:
121
raise UnparsableDateRange
123
raise UnparsableDateRange
125
for message in messages:
126
if (not parameters['include_hidden'] and message['hidden']):
128
if ('message_ids' in parameters
129
and message['message_id'] not in parameters['message_ids']):
131
if ('date_range' in parameters
132
and (message['date'] < start_date
133
or message['date'] > end_date)):
135
message = dict(message)
136
if 'headers' in parameters:
138
(k, v) for k, v in message['headers'].iteritems()
139
if k in parameters['headers'])
140
message['headers'] = headers
141
if display_type == 'headers-only':
143
elif display_type == 'text-only' and self.is_multipart(message):
145
part.get_payload() for part in message['body']
146
if part.get_content_type() == 'text/plain']
147
message['body'] = '\n\n'.join(text_parts)
148
elif display_type == 'all' and self.is_multipart(message):
149
parts = [str(part.get_payload()) for part in message['body']]
150
message['body'] = '\n\n'.join(parts)
151
max_body = parameters.get('max_body_length')
152
if max_body is not None and display_type != 'headers-only':
153
message['body'] = message['body'][:max_body]
154
new_messages.append(message)
155
messages = new_messages
156
limit = parameters.get('limit', 100)
157
memo = parameters.get('memo')
158
message_id_indices = dict(
159
(m['message_id'], idx) for idx, m in enumerate(messages))
163
start = message_id_indices[memo.encode('rot13')]
165
previous_memo = messages[start - 1]['message_id'].encode('rot13')
168
end = min(start + limit, len(messages))
169
if end < len(messages):
170
next_memo = messages[end]['message_id'].encode('rot13')
173
messages = messages[start:end]
176
'messages': messages,
177
'next_memo': next_memo,
178
'previous_memo': previous_memo
59
parts.attach(attachment)
60
return make_message(message_id, parts.as_string(), headers, hidden)
183
63
class ForkedFakeService:
184
64
"""A Grackle service fake, as a ContextManager."""
186
def __init__(self, port, messages=None, write_logs=False):
66
def __init__(self, port, message_archives=None, write_logs=False):
189
69
:param port: The tcp port to use.
190
:param messages: A dict of lists of dicts representing messages. The
191
outer dict represents the archive, the list represents the list of
192
messages for that archive.
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.
193
73
:param write_logs: If true, log messages will be written to stdout.
77
if message_archives is None:
78
self.message_archives = {}
200
self.messages = messages
80
self.message_archives = message_archives
201
81
self.read_end, self.write_end = os.pipe()
202
82
self.write_logs = write_logs
205
def from_client(client, messages=None):
85
def from_client(client, message_archives=None):
206
86
"""Instantiate a ForkedFakeService from the client.
208
88
:param port: The client to provide service for.
209
:param messages: A dict of lists of dicts representing messages. The
210
outer dict represents the archive, the list represents the list of
211
messages for that archive.
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.
213
return ForkedFakeService(client.port, messages)
93
return ForkedFakeService(client.port, message_archives)
215
95
def is_ready(self):
216
96
"""Tell the parent process that the server is ready for writes."""
261
133
self.logger = logging.getLogger('http')
262
134
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
137
"""Create an archive or message on PUT."""
138
scheme, netloc, path, params, query_string, fragments = (
140
parts = path.split('/')
141
if parts[1] != 'archive':
142
# This is an unknonwn operation?
145
# This expected path is /archive/archive_id/message_id.
147
message = self.rfile.read(int(self.headers['content-length']))
148
self.server.store.put_message(parts[2], parts[3], message)
149
self.send_response(httplib.CREATED)
153
self.send_error(httplib.BAD_REQUEST)
264
155
def do_POST(self):
265
"""Create a message on POST."""
266
message = self.rfile.read(int(self.headers['content-length']))
267
if message == 'This is a message':
268
self.send_response(httplib.CREATED)
272
self.send_error(httplib.BAD_REQUEST)
156
"""Change a message on POST."""
157
scheme, netloc, path, params, query_string, fragments = (
159
parts = path.split('/')
160
if parts[1] != 'archive':
161
# This is an unknonwn operation?
164
# This expected path is /archive/archive_id/message_id.
166
# This expected path is /archive/archive_id/message_id.
167
response = self.server.store.hide_message(
168
parts[2], parts[3], query_string)
169
self.send_response(httplib.OK)
171
self.wfile.write(simplejson.dumps(response))
173
self.send_error(httplib.BAD_REQUEST)
274
175
def do_GET(self):
275
176
"""Retrieve a list of messages on GET."""
298
199
class TestPutMessage(TestCase):
300
201
def test_put_message(self):
301
client = GrackleClient('localhost', 8436)
302
with ForkedFakeService.from_client(client):
303
client.put_message('arch1', 'asdf', StringIO('This is a message'))
202
client = GrackleClient('localhost', 8420)
203
message_archives = {'arch1': []}
204
with ForkedFakeService.from_client(client, message_archives):
205
client.put_message('arch1', 'id1', StringIO('This is a message'))
206
response = client.get_messages('arch1')
207
self.assertEqual(1, len(response['messages']))
208
message = response['messages'][0]
209
self.assertEqual('id1', message['message_id'])
211
def test_put_message_without_archive(self):
212
client = GrackleClient('localhost', 8421)
213
message_archives = {'arch1': []}
214
with ForkedFakeService.from_client(client, message_archives):
304
215
with ExpectedException(Exception, 'wtf'):
305
client.put_message('arch1', 'asdf',
306
StringIO('This is not a message'))
216
client.put_message('no-archive', 'id1', StringIO('message'))
309
219
class TestGetMessages(TestCase):
584
496
with ForkedFakeService.from_client(client, archive):
585
497
with ExpectedException(UnparsableDateRange, ''):
586
498
client.get_messages('baz', date_range='2012-01..12-02..12-03')
501
class TestHideMessages(TestCase):
503
def test_hide_message_true(self):
504
client = GrackleClient('localhost', 8470)
507
make_message('foo', hidden=False),
509
with ForkedFakeService.from_client(client, archive):
510
response = client.hide_message('baz', 'foo', hidden=True)
511
self.assertEqual('foo', response['message_id'])
512
self.assertIs(True, response['hidden'])
514
def test_hide_message_false(self):
515
client = GrackleClient('localhost', 8470)
518
make_message('foo', hidden=True),
520
with ForkedFakeService.from_client(client, archive):
521
response = client.hide_message('baz', 'foo', hidden=False)
522
self.assertEqual('foo', response['message_id'])
523
self.assertIs(False, response['hidden'])