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