49
52
attachment_type=None):
50
53
message = MIMEMultipart()
51
54
message.attach(MIMEText(body))
57
for key, value in headers.items():
52
59
if attachment_type is not None:
53
60
attachment = Message()
54
61
attachment.set_payload('attactment data.')
55
62
attachment['Content-Type'] = attachment_type
56
63
attachment['Content-Disposition'] = 'attachment; filename="file.ext"'
57
64
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
65
return make_json_message(message_id, message.as_string())
183
68
class ForkedFakeService:
184
69
"""A Grackle service fake, as a ContextManager."""
186
def __init__(self, port, messages=None, write_logs=False):
71
def __init__(self, port, message_archives=None, write_logs=False):
189
74
: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.
75
:param message_archives: A dict of lists of dicts representing
76
archives of messages. The outer dict represents the archive,
77
the list represents the list of messages for that archive.
193
78
:param write_logs: If true, log messages will be written to stdout.
82
if message_archives is None:
83
self.message_archives = {}
200
self.messages = messages
85
self.message_archives = message_archives
201
86
self.read_end, self.write_end = os.pipe()
202
87
self.write_logs = write_logs
205
def from_client(client, messages=None):
90
def from_client(client, message_archives=None):
206
91
"""Instantiate a ForkedFakeService from the client.
208
93
: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.
94
:param message_archives: A dict of lists of dicts representing
95
archives of messages. The outer dict represents the archive,
96
the list represents the list of messages for that archive.
213
return ForkedFakeService(client.port, messages)
98
return ForkedFakeService(client.port, message_archives)
215
100
def is_ready(self):
216
101
"""Tell the parent process that the server is ready for writes."""
298
181
class TestPutMessage(TestCase):
300
183
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'))
184
client = GrackleClient('localhost', 8420)
185
message_archives = {'arch1': []}
186
with ForkedFakeService.from_client(client, message_archives):
187
client.put_message('arch1', 'id1', StringIO('This is a message'))
188
response = client.get_messages('arch1')
189
self.assertEqual(1, len(response['messages']))
190
message = response['messages'][0]
191
self.assertEqual('id1', message['message_id'])
193
def test_put_message_without_archive(self):
194
client = GrackleClient('localhost', 8421)
195
message_archives = {'arch1': []}
196
with ForkedFakeService.from_client(client, message_archives):
304
197
with ExpectedException(Exception, 'wtf'):
305
client.put_message('arch1', 'asdf',
306
StringIO('This is not a message'))
198
client.put_message('no-archive', 'id1', StringIO('message'))
309
201
class TestGetMessages(TestCase):