1/*
2 * Copyright 2011-2013, Haiku, Inc. All rights reserved.
3 * Copyright 2011, Clemens Zeidler <haiku@clemens-zeidler.de>
4 * Copyright 2001-2003 Dr. Zoidberg Enterprises. All rights reserved.
5 *
6 * Distributed under the terms of the MIT License.
7 */
8
9
10#include "HaikuMailFormatFilter.h"
11
12#include <ctype.h>
13
14#include <Directory.h>
15#include <E-mail.h>
16#include <NodeInfo.h>
17
18#include <mail_util.h>
19
20
21struct mail_header_field {
22	const char*	rfc_name;
23	const char*	attr_name;
24	type_code	attr_type;
25	// currently either B_STRING_TYPE and B_TIME_TYPE
26};
27
28
29static const mail_header_field gDefaultFields[] = {
30	{ "To",				B_MAIL_ATTR_TO,			B_STRING_TYPE },
31	{ "From",         	B_MAIL_ATTR_FROM,		B_STRING_TYPE },
32	{ "Cc",				B_MAIL_ATTR_CC,			B_STRING_TYPE },
33	{ "Date",         	B_MAIL_ATTR_WHEN,		B_TIME_TYPE   },
34	{ "Delivery-Date",	B_MAIL_ATTR_WHEN,		B_TIME_TYPE   },
35	{ "Reply-To",     	B_MAIL_ATTR_REPLY,		B_STRING_TYPE },
36	{ "Subject",      	B_MAIL_ATTR_SUBJECT,	B_STRING_TYPE },
37	{ "X-Priority",		B_MAIL_ATTR_PRIORITY,	B_STRING_TYPE },
38		// Priorities with preferred
39	{ "Priority",		B_MAIL_ATTR_PRIORITY,	B_STRING_TYPE },
40		// one first - the numeric
41	{ "X-Msmail-Priority", B_MAIL_ATTR_PRIORITY, B_STRING_TYPE },
42		// one (has more levels).
43	{ "Mime-Version",	B_MAIL_ATTR_MIME,		B_STRING_TYPE },
44	{ "STATUS",       	B_MAIL_ATTR_STATUS,		B_STRING_TYPE },
45	{ "THREAD",       	B_MAIL_ATTR_THREAD,		B_STRING_TYPE },
46	{ "NAME",       	B_MAIL_ATTR_NAME,		B_STRING_TYPE },
47	{ NULL,				NULL,					0 }
48};
49
50
51//!	Replaces tabs and other white space with spaces, compresses spaces.
52void
53sanitize_white_space(BString& string)
54{
55	char* buffer = string.LockBuffer(string.Length() + 1);
56	if (buffer == NULL)
57		return;
58
59	int32 count = string.Length();
60	int32 spaces = 0;
61	for (int32 i = 0; buffer[i] != '\0'; i++, count--) {
62		if (isspace(buffer[i])) {
63			buffer[i] = ' ';
64			spaces++;
65		} else {
66			if (spaces > 1)
67				memmove(buffer + i + 1 - spaces, buffer + i, count + 1);
68			spaces = 0;
69		}
70	}
71
72	string.UnlockBuffer();
73}
74
75
76// #pragma mark -
77
78
79HaikuMailFormatFilter::HaikuMailFormatFilter(BMailProtocol& protocol,
80	const BMailAccountSettings& settings)
81	:
82	BMailFilter(protocol, NULL),
83	fAccountID(settings.AccountID()),
84	fAccountName(settings.Name())
85{
86	const BMessage& outboundSettings = settings.OutboundSettings();
87	outboundSettings.FindString("destination", &fOutboundDirectory);
88}
89
90
91BString
92HaikuMailFormatFilter::DescriptiveName() const
93{
94	// This will not be called by the UI; no need to translate it
95	return "built-in";
96}
97
98
99BMailFilterAction
100HaikuMailFormatFilter::HeaderFetched(entry_ref& ref, BFile& file,
101	BMessage& attributes)
102{
103	file.Seek(0, SEEK_SET);
104
105	// TODO: attributes.AddInt32(B_MAIL_ATTR_CONTENT, length);
106	attributes.AddInt32(B_MAIL_ATTR_ACCOUNT_ID, fAccountID);
107	attributes.AddString(B_MAIL_ATTR_ACCOUNT, fAccountName);
108
109	BString header;
110	off_t size;
111	if (file.GetSize(&size) == B_OK) {
112		char* buffer = header.LockBuffer(size);
113		if (buffer == NULL)
114			return B_NO_MEMORY;
115
116		ssize_t bytesRead = file.Read(buffer, size);
117		if (bytesRead < 0)
118			return bytesRead;
119		if (bytesRead != size)
120			return B_IO_ERROR;
121
122		header.UnlockBuffer(size);
123	}
124
125	for (int i = 0; gDefaultFields[i].rfc_name; ++i) {
126		BString target;
127		status_t status = extract_from_header(header,
128			gDefaultFields[i].rfc_name, target);
129		if (status != B_OK)
130			continue;
131
132		switch (gDefaultFields[i].attr_type){
133			case B_STRING_TYPE:
134				sanitize_white_space(target);
135				attributes.AddString(gDefaultFields[i].attr_name, target);
136				break;
137
138			case B_TIME_TYPE:
139			{
140				time_t when;
141				when = ParseDateWithTimeZone(target);
142				if (when == -1)
143					when = time(NULL); // Use current time if it's undecodable.
144				attributes.AddData(B_MAIL_ATTR_WHEN, B_TIME_TYPE, &when,
145					sizeof(when));
146				break;
147			}
148		}
149	}
150
151	BString senderName = _ExtractName(attributes.FindString(B_MAIL_ATTR_FROM));
152	attributes.AddString(B_MAIL_ATTR_NAME, senderName);
153
154	// Generate a file name for the incoming message.  See also
155	// Message::RenderTo which does a similar thing for outgoing messages.
156	BString name = attributes.FindString(B_MAIL_ATTR_SUBJECT);
157	SubjectToThread(name); // Extract the core subject words.
158	if (name.Length() <= 0)
159		name = "No Subject";
160	attributes.AddString(B_MAIL_ATTR_THREAD, name);
161
162	// Convert the date into a year-month-day fixed digit width format, so that
163	// sorting by file name will give all the messages with the same subject in
164	// order of date.
165	time_t dateAsTime = 0;
166	const time_t* datePntr;
167	ssize_t dateSize;
168	char numericDateString[40];
169	struct tm timeFields;
170
171	if (attributes.FindData(B_MAIL_ATTR_WHEN, B_TIME_TYPE,
172			(const void**)&datePntr, &dateSize) == B_OK)
173		dateAsTime = *datePntr;
174	localtime_r(&dateAsTime, &timeFields);
175	snprintf(numericDateString, sizeof(numericDateString),
176		"%04d%02d%02d%02d%02d%02d",
177		timeFields.tm_year + 1900, timeFields.tm_mon + 1, timeFields.tm_mday,
178		timeFields.tm_hour, timeFields.tm_min, timeFields.tm_sec);
179	name << " " << numericDateString;
180
181	BString workerName = attributes.FindString(B_MAIL_ATTR_FROM);
182	extract_address_name(workerName);
183	name << " " << workerName;
184
185	name.Truncate(222);	// reserve space for the unique number
186
187	// Get rid of annoying characters which are hard to use in the shell.
188	name.ReplaceAll('/', '_');
189	name.ReplaceAll('\'', '_');
190	name.ReplaceAll('"', '_');
191	name.ReplaceAll('!', '_');
192	name.ReplaceAll('<', '_');
193	name.ReplaceAll('>', '_');
194	_RemoveExtraWhitespace(name);
195	_RemoveLeadingDots(name);
196		// Avoid files starting with a dot.
197
198	if (!attributes.HasString(B_MAIL_ATTR_STATUS))
199		attributes.AddString(B_MAIL_ATTR_STATUS, "New");
200
201	_SetType(attributes, B_PARTIAL_MAIL_TYPE);
202
203	ref.set_name(name.String());
204
205	return B_MOVE_MAIL_ACTION;
206}
207
208
209void
210HaikuMailFormatFilter::BodyFetched(const entry_ref& ref, BFile& file,
211	BMessage& attributes)
212{
213	_SetType(attributes, B_MAIL_TYPE);
214}
215
216
217void
218HaikuMailFormatFilter::MessageSent(const entry_ref& ref, BFile& file)
219{
220	mail_flags flags = B_MAIL_SENT;
221	file.WriteAttr(B_MAIL_ATTR_FLAGS, B_INT32_TYPE, 0, &flags, sizeof(int32));
222	file.WriteAttr(B_MAIL_ATTR_STATUS, B_STRING_TYPE, 0, "Sent", 5);
223
224	if (!fOutboundDirectory.IsEmpty()) {
225		create_directory(fOutboundDirectory, 755);
226		BDirectory dir(fOutboundDirectory);
227		// TODO:
228//		fMailProtocol.Looper()->TriggerFileMove(ref, dir);
229		BEntry entry(&ref);
230		entry.MoveTo(&dir);
231		// TODO: report error (via BMailProtocol::MailNotifier())
232	}
233}
234
235
236void
237HaikuMailFormatFilter::_RemoveExtraWhitespace(BString& name)
238{
239	int spaces = 0;
240	for (int i = 0; i <= name.Length();) {
241		if (i < name.Length() && isspace(name.ByteAt(i))) {
242			spaces++;
243			i++;
244		} else if (spaces > 0) {
245			int remove = spaces - 1;
246			// Also remove leading and trailing spaces
247			if (i == remove + 1 || i == name.Length())
248				remove++;
249			else
250				name.SetByteAt(i - spaces, ' ');
251			name.Remove(i - remove, remove);
252			i -= remove;
253			spaces = 0;
254		} else
255			i++;
256	}
257}
258
259
260void
261HaikuMailFormatFilter::_RemoveLeadingDots(BString& name)
262{
263	int dots = 0;
264	while (dots < name.Length() && name.ByteAt(dots) == '.')
265		dots++;
266
267	if (dots > 0)
268		name.Remove(0, dots);
269}
270
271
272BString
273HaikuMailFormatFilter::_ExtractName(const BString& from)
274{
275	// extract name from something like "name" <email@domain.com>
276	// if name is empty return the mail address without "<>"
277
278	BString name;
279	int32 emailStart = from.FindFirst("<");
280	if (emailStart < 0) {
281		name = from;
282		return name.Trim();
283	}
284
285	from.CopyInto(name, 0, emailStart);
286	name.Trim();
287	if (name.Length() >= 2) {
288		if (name[name.Length() - 1] == '\"')
289			name.Truncate(name.Length() - 1, true);
290		if (name[0] == '\"')
291			name.Remove(0, 1);
292		name.Trim();
293	}
294	if (name != "")
295		return name;
296
297	// empty name extract email address
298	name = from;
299	name.Remove(0, emailStart + 1);
300	name.Trim();
301	if (name.Length() < 1)
302		return from;
303	if (name[name.Length() - 1] == '>')
304		name.Truncate(name.Length() - 1, true);
305	name.Trim();
306	return name;
307}
308
309
310status_t
311HaikuMailFormatFilter::_SetType(BMessage& attributes, const char* mimeType)
312{
313	return attributes.SetData("BEOS:TYPE", B_MIME_STRING_TYPE, mimeType,
314		strlen(mimeType) + 1, false);
315}
316