xref: /haiku/src/kits/mail/HaikuMailFormatFilter.cpp (revision bdd02e0d9d4ff4be6c80aeb799a93692467381dc)
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 
21 struct 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 
29 static 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.
52 void
sanitize_white_space(BString & string)53 sanitize_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 
HaikuMailFormatFilter(BMailProtocol & protocol,const BMailAccountSettings & settings)79 HaikuMailFormatFilter::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 
91 BString
DescriptiveName() const92 HaikuMailFormatFilter::DescriptiveName() const
93 {
94 	// This will not be called by the UI; no need to translate it
95 	return "built-in";
96 }
97 
98 
99 BMailFilterAction
HeaderFetched(entry_ref & ref,BFile & file,BMessage & attributes)100 HaikuMailFormatFilter::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 
209 void
BodyFetched(const entry_ref & ref,BFile & file,BMessage & attributes)210 HaikuMailFormatFilter::BodyFetched(const entry_ref& ref, BFile& file,
211 	BMessage& attributes)
212 {
213 	_SetType(attributes, B_MAIL_TYPE);
214 }
215 
216 
217 void
MessageSent(const entry_ref & ref,BFile & file)218 HaikuMailFormatFilter::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 
236 void
_RemoveExtraWhitespace(BString & name)237 HaikuMailFormatFilter::_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 
260 void
_RemoveLeadingDots(BString & name)261 HaikuMailFormatFilter::_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 
272 BString
_ExtractName(const BString & from)273 HaikuMailFormatFilter::_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 
310 status_t
_SetType(BMessage & attributes,const char * mimeType)311 HaikuMailFormatFilter::_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