PageRenderTime 219ms CodeModel.GetById 27ms app.highlight 184ms RepoModel.GetById 1ms app.codeStats 1ms

/Source/MBoxItemsController.m

http://google-email-uploader-mac.googlecode.com/
Objective C | 787 lines | 504 code | 171 blank | 112 comment | 111 complexity | 89e0bb85354f1a59017784d669c7b0b8 MD5 | raw file
  1/* Copyright (c) 2009 Google Inc.
  2 *
  3 * Licensed under the Apache License, Version 2.0 (the "License");
  4 * you may not use this file except in compliance with the License.
  5 * You may obtain a copy of the License at
  6 *
  7 *     http://www.apache.org/licenses/LICENSE-2.0
  8 *
  9 * Unless required by applicable law or agreed to in writing, software
 10 * distributed under the License is distributed on an "AS IS" BASIS,
 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 * See the License for the specific language governing permissions and
 13 * limitations under the License.
 14 */
 15
 16#import "MBoxItemsController.h"
 17#import "EmUpUtilities.h"
 18
 19static const char* memsearch(const char* needle, unsigned long long needleLen,
 20                      const char* haystack, unsigned long long haystackLen);
 21
 22@interface MBoxItemsController (PrivateMethods)
 23- (void)loadMailItems;
 24- (NSString *)endOfLineForFileData:(NSData *)fileData;
 25- (NSString *)fromStringForFileData:(NSData *)fileData;
 26- (void)setLastUploadedItem:(OutlineViewItemMBox *)item;
 27
 28- (NSString *)dateStringForMessageFirstLine:(NSString *)firstLine;
 29- (NSArray *)mailItemPropertiesForHeaders:(NSString *)message
 30                                endOfLine:(NSString *)endOfLine;
 31@end
 32
 33
 34@implementation OutlineViewItemMBox
 35
 36- (void)dealloc {
 37  [messageOffsets_ release];
 38  [endOfLine_ release];
 39  [super dealloc];
 40}
 41
 42- (void)setMessageOffsets:(NSArray *)array {
 43  [messageOffsets_ autorelease];
 44  messageOffsets_ = [array retain];
 45}
 46
 47- (NSArray *)messageOffsets {
 48  return messageOffsets_;
 49}
 50
 51- (void)setEndOfLine:(NSString *)str {
 52  [endOfLine_ autorelease];
 53  endOfLine_ = [str copy];
 54}
 55
 56- (NSString *)endOfLine {
 57  return endOfLine_;
 58}
 59@end
 60
 61@implementation MBoxItemsController
 62
 63- (id)initWithMailFolderPath:(NSString *)path
 64                    rootName:(NSString *)rootName
 65{
 66  self = [super init];
 67  if (self) {
 68    mailFolderPath_ = [path copy];
 69    rootName_ = [rootName copy];
 70
 71    [self loadMailItems];
 72  }
 73  return self;
 74}
 75
 76- (void)dealloc {
 77  [mailFolderPath_ release];
 78  [rootName_ release];
 79
 80  [rootItem_ release];
 81  [lastUploadedItem_ release];
 82  [uploadingData_ release];
 83  [super dealloc];
 84}
 85
 86#pragma mark -
 87
 88- (NSString *)defaultMailFolderPath {
 89  [self doesNotRecognizeSelector:_cmd];
 90  return nil;
 91}
 92
 93- (NSString *)defaultRootName {
 94  [self doesNotRecognizeSelector:_cmd];
 95  return nil;
 96}
 97
 98- (const char *)messagePrefix {
 99  [self doesNotRecognizeSelector:_cmd];
100  return nil;
101}
102
103- (OutlineViewItem *)rootItem {
104  return rootItem_;
105}
106
107- (NSString *)mailFolderPath {
108  return mailFolderPath_;
109}
110
111- (NSString *)byteStringReportForAddress:(const unsigned char *)ptr
112                                  length:(unsigned long long)length {
113  NSMutableString *resultStr = [NSMutableString string];
114
115  const int charsPerLine = 16;
116
117  BOOL isDone = NO;
118
119  for (int lineNum = 0; lineNum < 20; lineNum++) {
120
121    int lineOffset = lineNum * charsPerLine;
122
123    NSMutableString *ascii = [NSMutableString string];
124    NSMutableString *hex = [NSMutableString string];
125
126    for (int charNum = 0; charNum < charsPerLine; charNum++) {
127
128      if (lineOffset + charNum >= length) {
129        isDone = YES;
130      }
131
132      if (!isDone) {
133        // not done; create the ascii and hex parts
134        if (*ptr >= 0x20 && *ptr <= 0x7f) {
135          [ascii appendFormat:@"%c", *ptr];
136        } else {
137          [ascii appendString:@"."];
138        }
139
140        [hex appendFormat:@"%02X", *ptr];
141        if (charNum % 2 == 1) [hex appendString:@" "];
142
143        ++ptr;
144      } else {
145        // keep padding the ascii part
146        [ascii appendString:@" "];
147      }
148    }
149
150    [resultStr appendFormat:@"%04X: %@  %@\n", lineOffset, ascii, hex];
151    if (isDone) break;
152  }
153  return resultStr;
154}
155
156- (void)loadMailItems {
157  // build a dictionary for all mailboxes
158  NSFileManager *fileMgr = [NSFileManager defaultManager];
159
160  BOOL isDir = NO;
161  if (![fileMgr fileExistsAtPath:mailFolderPath_ isDirectory:&isDir] || !isDir) {
162    // no MBox mail folder found; leave the root item ivar nil
163    return;
164  }
165
166  NSDirectoryEnumerator *enumerator = [fileMgr enumeratorAtPath:mailFolderPath_];
167
168  rootItem_ = [[OutlineViewItemMBox itemWithName:rootName_
169                level:0] retain];
170
171  NSString *partialPath;
172  OutlineViewItemMBox *lastOutlineItem = rootItem_;
173
174  NSDate *lastDisplayDate = [NSDate date];
175
176  // all messages begin with "\nFrom " or "\rFrom ", except the first
177  // message in the file
178  const char *kFrom = "From ";
179  const int kFromLen = strlen(kFrom);
180
181  // make a map of partial paths to created outline items
182  NSMutableDictionary *itemMap = [NSMutableDictionary dictionaryWithObject:rootItem_
183                                                                    forKey:@""];
184
185  while ((partialPath = [enumerator nextObject]) != nil) {
186
187    NSDictionary *pathAttrs = [enumerator fileAttributes];
188
189    NSString *fullPath = [mailFolderPath_ stringByAppendingPathComponent:partialPath];
190    NSString *partialPathParent = [partialPath stringByDeletingLastPathComponent];
191    NSString *pathExtension = [partialPath pathExtension];
192    NSString *lastPathComponent = [partialPath lastPathComponent];
193
194    // display the path periodically when stepping through mailboxes so the user
195    // knows we're making progress
196    if ([lastDisplayDate timeIntervalSinceNow] < -0.2) {
197      lastDisplayDate = [NSDate date];
198      [[NSNotificationCenter defaultCenter] postNotificationName:kEmUpLoadingMailbox
199                                                          object:fullPath];
200    }
201
202    BOOL isDir = [[pathAttrs objectForKey:NSFileType] isEqual:NSFileTypeDirectory];
203
204    // skip invisible files and dirs
205    if ([lastPathComponent hasPrefix:@"."]) {
206      if (isDir) [enumerator skipDescendents];
207      continue;
208    }
209
210    NSArray *partialPathParts = [partialPath componentsSeparatedByString:@"/"];
211
212    if (isDir) {
213      // add this directory path to the tree
214      OutlineViewItemMBox *dirItem = [itemMap objectForKey:partialPath];
215      if (dirItem == nil) {
216
217        // new directory; create the outline item, store it in the map,
218        // and save it as a child of its parent
219        int level = [partialPathParts count];
220
221        dirItem = [OutlineViewItemMBox itemWithName:lastPathComponent
222                                              level:level];
223        [itemMap setObject:dirItem forKey:partialPath];
224
225        OutlineViewItemMBox *parentItem = [itemMap objectForKey:partialPathParent];
226
227        NSAssert1(parentItem != nil, @"missing parent for %@", partialPath);
228        [parentItem addChild:dirItem];
229      } else {
230       // we've seen this directory before
231      }
232
233    } else if (![pathExtension isEqual:@"toc"] // Eudora metadata
234               && ![pathExtension isEqual:@"msf"]) { // Tbird metadata
235
236      // we'll try to count the messages in files without blowing chunks on
237      // gigabyte-size mbox files
238
239      // make a no-copy NSString from a memory-mapped NSData
240      //
241      // hopefully, this will let us access huge files without killing
242      // the machine
243
244      // we'll explicitly release fileData below to free up the memory-mapped
245      // file
246      NSData *fileData = [[NSData alloc] initWithContentsOfMappedFile:fullPath];
247      unsigned int dataLen = [fileData length];
248      const char *fileDataPtr = [fileData bytes];
249
250      // before attempting to use the file, check its first bytes are "From"
251      if (dataLen > kFromLen
252          && strncmp(fileDataPtr, kFrom, kFromLen) == 0) {
253
254        // figure out the line endings for this mbox file
255        NSString *endOfLine = [self endOfLineForFileData:fileData];
256        if (endOfLine == nil) {
257          NSLog(@"could not determine eol for file: %@", fullPath);
258          endOfLine = @"\n";
259        }
260        unsigned int eolLen = [endOfLine length];
261        
262        // get the "From " string we'll use as a message separator for this file
263        NSString *fromString = [self fromStringForFileData:fileData];
264
265        const char *fromAfterReturn = [[NSString stringWithFormat:@"%@%@",
266                                        endOfLine, fromString] UTF8String];
267        size_t fromAfterReturnLen = strlen(fromAfterReturn);
268
269        // locate the message separators; the first one is at byte zero
270        unsigned long long offset = 0;
271
272        NSNumber *offset0 = [NSNumber numberWithUnsignedLongLong:0];
273        NSMutableArray *offsetsArray = [NSMutableArray arrayWithObject:offset0];
274
275        while (1) {
276          // display the path periodically when stepping through messages of a
277          // mailbox so we look busy
278          if ([lastDisplayDate timeIntervalSinceNow] < -0.2) {
279            lastDisplayDate = [NSDate date];
280            [[NSNotificationCenter defaultCenter] postNotificationName:kEmUpLoadingMailbox
281                                                                object:fullPath];
282          }
283
284          // we use our own memsearch rather than strnstr in case messages
285          // contain nulls
286          const char *foundPtr = memsearch(fromAfterReturn, fromAfterReturnLen,
287                                       fileDataPtr + offset, dataLen - offset);
288          if (!foundPtr) break;
289
290          // skip the initial return or newline char when calculating the
291          // offset of each message
292          offset = foundPtr + eolLen - fileDataPtr;
293
294          // add the offset of this message to our array
295          [offsetsArray addObject:[NSNumber numberWithUnsignedLongLong:offset]];
296
297          // search again from after the last found "\rFrom" prefix
298          offset += fromAfterReturnLen;
299        }
300
301        // add this mailbox to the outline view
302        unsigned int level = [partialPathParts count];
303
304        OutlineViewItemMBox *childItem
305          = [OutlineViewItemMBox itemWithName:lastPathComponent
306                                        level:level];
307
308        [childItem setNumberOfMessages:[offsetsArray count]];
309        [childItem setMessageOffsets:offsetsArray];
310        [childItem setPath:fullPath];
311        [childItem setEndOfLine:endOfLine];
312
313        // add this new item as a child of the most recently-visited folder
314        OutlineViewItemMBox *parentItem = [itemMap objectForKey:partialPathParent];
315
316        NSAssert1(parentItem != nil, @"missing parent for %@", partialPath);
317        [parentItem addChild:childItem];
318
319        // add this to the linked list of outline items to be uploaded
320        [lastOutlineItem setNextOutlineItem:childItem];
321        lastOutlineItem = childItem;
322
323        [itemMap setObject:childItem forKey:partialPath];
324      }
325
326      [fileData release];
327      fileData = nil;
328    }
329  }
330
331  [rootItem_ setState:NSOnState];
332
333  // clear the name of the mailbox being loaded
334  [[NSNotificationCenter defaultCenter] postNotificationName:kEmUpLoadingMailbox
335                                                      object:@""];
336}
337
338- (NSString *)endOfLineForFileData:(NSData *)fileData {
339
340  // since an mbox file should start with a header, scan until we find
341  // a \r, \n, or \r\n, and assume that end-of-line marker applies for the
342  // whole file
343  //
344  // as a sanity check, limit our scan to 1000 chars
345
346  unsigned int numberOfBytesToTest = MIN([fileData length], 1000);
347  char *fileBytes = (char *) [fileData bytes];
348
349  for (unsigned int idx = 0; idx < numberOfBytesToTest; idx++) {
350    char c = fileBytes[idx];
351
352    if (c == '\n') {
353      return @"\n";
354    }
355
356    if (c == '\r') {
357
358      // check if the next character after this return is a newline
359      if (idx + 1 < numberOfBytesToTest
360          && fileBytes[idx+1] == '\n') {
361        return @"\r\n";
362      } else {
363        return @"\r";
364      }
365    }
366  }
367
368  return nil;
369}
370
371- (NSString *)fromStringForFileData:(NSData *)fileData {
372
373  // Eudora mbox files may not reliably change From strings at the beginning
374  // of lines inside message bodies to >From, leading to mistakes in determining
375  // where messages end.  So if the first message in the file is From ???@???
376  // we'll use that as the From string.
377  const char *const kEudoraFromBytes = "From ???@???";
378  int eudoraFromLen = strlen(kEudoraFromBytes);
379
380  if ([fileData length] > eudoraFromLen) {
381    if (memcmp(kEudoraFromBytes, [fileData bytes], eudoraFromLen) == 0) {
382      // this file starts with the Eudora-style From
383      return @"From ???@???";
384    }
385  }
386
387  // for generic mbox, we'll just search for the traditional "From "
388  return @"From ";
389}
390
391- (unsigned int)countSelectedMessages {
392
393  unsigned int count = [rootItem_ recursiveNumberOfCheckedMessages];
394  return count;
395}
396
397- (void)resetUpload {
398  [self setLastUploadedItem:nil];
399}
400
401- (void)failUploadWithMessageIndex:(unsigned int)index
402                              path:(NSString *)path
403                             range:(NSRange)range
404                       errorString:(NSString *)errorStr {
405  
406  // report an upload failure to the user by notifying the main controller
407  NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
408                        path, kEmUpMessagePathKey,
409                        [NSNumber numberWithInt:index], kEmUpMessageIndexKey,
410                        [NSValue valueWithRange:range], kEmUpMessageRangeKey,
411                        errorStr, kEmUpMessageErrorStringKey,
412                        nil];
413  
414  [[NSNotificationCenter defaultCenter] postNotificationName:kEmUpMessageParsingFailed
415                                                      object:dict];
416}
417
418- (GDataEntryMailItem *)nextUploadItem {
419  // find the next view item to upload
420
421  if (lastUploadedItem_ == nil) {
422    // starting from scratch
423    [self setLastUploadedItem:[rootItem_ nextOutlineItem]];
424    lastUploadedIndex_ = -1;
425  }
426
427  while (lastUploadedItem_ != nil) {
428
429    ++lastUploadedIndex_;
430
431    NSArray *messageOffsets = [lastUploadedItem_ messageOffsets];
432
433    unsigned int numberOfMessages = [messageOffsets count];
434
435    NSString *endOfLine = [lastUploadedItem_ endOfLine];
436    const char* utf8EndOfLine = [endOfLine UTF8String];
437
438    BOOL didExhaustFolder = (lastUploadedIndex_ >= numberOfMessages);
439    if (didExhaustFolder) {
440      // move to next upload item (the next mbox)
441      [self setLastUploadedItem:[lastUploadedItem_ nextOutlineItem]];
442      lastUploadedIndex_ = -1;
443
444      // immediately free the memory from the last mbox file
445      [uploadingData_ release];
446      uploadingData_ = nil;
447
448    } else {
449      OutlineViewItemMBox *thisUploadItem = lastUploadedItem_;
450      int thisUploadIndex = lastUploadedIndex_;
451
452      NSCellStateValue state = [thisUploadItem state];
453      if (state != NSOffState) {
454
455        NSString *mboxPath = [thisUploadItem path];
456
457        // if we've not yet opened this mbox file, do it now
458        if (uploadingData_ == nil) {
459
460          // read in the file memory-mapped in case it's huge
461          uploadingData_ = [[NSData alloc] initWithContentsOfMappedFile:mboxPath];
462          if (uploadingData_ == nil) {
463
464            [self failUploadWithMessageIndex:thisUploadIndex
465                                        path:mboxPath
466                                       range:NSMakeRange(0, 0)
467                                 errorString:@"Could not open MBox file"];
468            continue;
469          }
470        }
471
472        unsigned int dataLen = [uploadingData_ length];
473        const char *fileDataPtr = [uploadingData_ bytes];
474
475        NSNumber *thisMessageOffsetNum = [messageOffsets objectAtIndex:thisUploadIndex];
476        unsigned long long thisMessageOffset = [thisMessageOffsetNum unsignedLongLongValue];
477
478        unsigned long long nextMessageOffset;
479
480        BOOL isLastMessage = (thisUploadIndex + 1 == numberOfMessages);
481        if (isLastMessage) {
482          nextMessageOffset = dataLen;
483        } else {
484          NSNumber *nextMessageOffsetNum = [messageOffsets objectAtIndex:(1 + thisUploadIndex)];
485          nextMessageOffset = [nextMessageOffsetNum unsignedLongLongValue];
486        }
487
488        // skip the first line in the message, containing the false From ???@???
489        // and date, and get a "real" pointer to the start of the actual message
490        // text
491        unsigned long long thisMessageLength = nextMessageOffset - thisMessageOffset;
492
493        const char *thisMessagePtr = fileDataPtr + thisMessageOffset;
494        char *realMessagePtr = strnstr(thisMessagePtr, utf8EndOfLine,
495                                       thisMessageLength);
496
497        NSRange messageRange = NSMakeRange(thisMessageOffset, thisMessageLength);
498
499        if (realMessagePtr == NULL) {
500          // something is really wrong if we can't find the expected kind of
501          // end-of-line string
502          NSString *report = [self byteStringReportForAddress:(const unsigned char *)thisMessagePtr
503                                                       length:thisMessageLength];
504          NSLog(@"could not find first return in message (%d-%d) from file: %@\n%@",
505                (int)thisMessageOffset, (int)nextMessageOffset-1, mboxPath,
506                report);
507
508          NSString *template = @"Could not find opening line (bytes %d-%d)";
509          NSString *errMsg = [NSString stringWithFormat:template,
510                              (int)thisMessageOffset, (int)nextMessageOffset-1];
511
512          [self failUploadWithMessageIndex:thisUploadIndex
513                                      path:mboxPath
514                                     range:messageRange
515                               errorString:errMsg];
516          continue;
517        }
518
519        realMessagePtr += [endOfLine length]; // skip return char
520
521        unsigned long long skippedLineLength = realMessagePtr - thisMessagePtr;
522        unsigned long long realMessageLength = thisMessageLength - skippedLineLength;
523
524        // read the message into a string
525        //
526        // Try UTF-8 first, since that's well-defined, unlike other encodings
527        NSStringEncoding encoding = NSUTF8StringEncoding;
528        NSString *messageText = [[[NSString alloc] initWithBytesNoCopy:realMessagePtr
529                                                                length:realMessageLength
530                                                              encoding:encoding
531                                                          freeWhenDone:NO] autorelease];
532        if (messageText == nil) {
533          // try MacRoman encoding
534          encoding = NSMacOSRomanStringEncoding;
535          messageText = [[[NSString alloc] initWithBytesNoCopy:realMessagePtr
536                                                        length:realMessageLength
537                                                      encoding:encoding
538                                                  freeWhenDone:NO] autorelease];
539        }
540
541        if (messageText == nil) {
542          // try WinLatin encoding
543          encoding = NSWindowsCP1252StringEncoding;
544          messageText = [[[NSString alloc] initWithBytesNoCopy:realMessagePtr
545                                                        length:realMessageLength
546                                                      encoding:encoding
547                                                  freeWhenDone:NO] autorelease];
548        }
549
550        if (messageText == nil) {
551          NSLog(@"could not read message (%d-%d) from file: %@",
552                (int)thisMessageOffset, (int)nextMessageOffset-1, mboxPath);
553
554          NSString *template = @"Could not interpret message (bytes %d-%d)";
555          NSString *errMsg = [NSString stringWithFormat:template,
556                              (int)thisMessageOffset, (int)nextMessageOffset-1];
557
558          [self failUploadWithMessageIndex:thisUploadIndex
559                                      path:mboxPath
560                                     range:messageRange
561                               errorString:errMsg];
562          continue;
563        }
564
565        // uploads limited to 31 megs
566        if ([messageText length] < kMaxMesssageSize) {
567
568          // fix up message headers, if necessary
569          messageText = [EmUpUtilities messageTextWithAlteredHeadersForMessageText:messageText
570                                                                         endOfLine:endOfLine];
571
572          NSString *headers = [EmUpUtilities headersForMessageText:messageText
573                                                         endOfLine:endOfLine];
574          if (headers == nil) {
575            NSLog(@"could not find headers in message (bytes %d-%d) from file: %@",
576                  (int)thisMessageOffset, (int)nextMessageOffset-1, mboxPath);
577          }
578
579          // some old eudora mail lacks a Date header; for those, we'll make
580          // one from the initial From line.  The From line represents the
581          // time the message was written, so we'll use the local time
582          // zone
583          NSString *dateStr = [EmUpUtilities stringForHeader:@"Date"
584                                                 fromHeaders:headers
585                                                   endOfLine:endOfLine];
586          if ([dateStr length] == 0) {
587            NSString *firstLine =
588              [[[NSString alloc] initWithBytesNoCopy:(void *)thisMessagePtr
589                                              length:skippedLineLength
590                                            encoding:encoding
591                                        freeWhenDone:NO] autorelease];
592
593            NSString *newDateStr = [self dateStringForMessageFirstLine:firstLine];
594            if ([newDateStr length] > 0) {
595              // insert a date header at the beginning of the message
596              messageText = [NSString stringWithFormat:@"Date: %@%@%@",
597                             newDateStr, endOfLine, messageText];
598            }
599          }
600
601          if (![endOfLine isEqual:@"\n"]) {
602            // The server's header parsing code doesn't respect \r\r as ending a
603            // header block, so it tends to find "invalid headers" in the body
604            // of messages using returns as line separators. So we'll change
605            // returns to newlines before encoding the message.
606            NSMutableString *mutable = [NSMutableString stringWithString:messageText];
607            [mutable replaceOccurrencesOfString:@"\r\n"
608                                     withString:@"\n"
609                                        options:0
610                                          range:NSMakeRange(0, [mutable length])];
611            [mutable replaceOccurrencesOfString:@"\r"
612                                     withString:@"\n"
613                                        options:0
614                                          range:NSMakeRange(0, [mutable length])];
615            endOfLine = @"\n";
616            messageText = mutable;
617          }
618
619          GDataEntryMailItem *newEntry = [GDataEntryMailItem mailItemWithRFC822String:messageText];
620
621          [newEntry setProperty:[thisUploadItem name]
622                         forKey:kEmUpMailboxNameKey];
623
624          [newEntry setProperty:mboxPath
625                         forKey:kEmUpMessagePathKey];
626
627          [newEntry setProperty:[NSNumber numberWithInt:thisUploadIndex]
628                         forKey:kEmUpMessageIndexKey];
629
630          [newEntry setProperty:[NSValue valueWithRange:messageRange]
631                         forKey:kEmUpMessageRangeKey];
632
633
634          NSString *messageID = [EmUpUtilities stringForHeader:@"Message-ID"
635                                                   fromHeaders:headers
636                                                     endOfLine:endOfLine];
637          if (messageID) {
638            [newEntry setProperty:messageID
639                           forKey:kEmUpMessageIDKey];
640          }
641
642          // add message properties
643          NSArray *props = [self mailItemPropertiesForHeaders:headers
644                                                    endOfLine:endOfLine];
645          for (NSString *property in props) {
646            GDataMailItemProperty *prop = [GDataMailItemProperty valueWithString:property];
647            [newEntry addMailItemProperty:prop];
648          }
649
650          return newEntry;
651
652        } else {
653          NSString *template = @"Message too big (bytes %d-%d)";
654          NSString *errMsg = [NSString stringWithFormat:template,
655                              (int)thisMessageOffset, (int)nextMessageOffset-1];
656
657          [self failUploadWithMessageIndex:thisUploadIndex
658                                      path:mboxPath
659                                     range:messageRange
660                               errorString:errMsg];
661        }
662      }
663    }
664  }
665
666  return nil;
667}
668
669- (NSString *)dateStringForMessageFirstLine:(NSString *)firstLine {
670
671  if (firstLine == nil) return nil;
672
673  // look for the characteristic Eudora new-message From line, like
674  //
675  //   From ???@??? Thu Feb 27 20:43:33 2003
676  //
677  // which unfortunately isn't in the date format needed for mail headers
678
679  NSScanner *scanner = [NSScanner scannerWithString:firstLine];
680  NSString *dateStr = nil;
681
682  if ([scanner scanString:@"From ???@???" intoString:nil]
683      && [scanner scanUpToString:@"\r" intoString:&dateStr]) {
684
685    NSCalendarDate *parsedDate = [NSCalendarDate dateWithString:dateStr
686                                                 calendarFormat:@"%a %b %d %H:%M:%S %Y"];
687    if (parsedDate) {
688      // regenerate the date in the proper format, like
689      //   3 Apr 1995 16:38:24 -0700
690      // per http://www.w3.org/Protocols/rfc822/#z28
691
692      NSString *newDateStr = [parsedDate descriptionWithCalendarFormat:@"%d %b %Y %H:%M:%S %z"];
693      return newDateStr;
694    }
695  }
696  return nil;
697}
698
699- (NSArray *)mailItemPropertiesForHeaders:(NSString *)headers
700                                endOfLine:(NSString *)endOfLine {
701
702  NSMutableArray *props = [NSMutableArray array];
703
704  NSString *mozStatus = [EmUpUtilities stringForHeader:@"X-Mozilla-Status"
705                                           fromHeaders:headers
706                                             endOfLine:endOfLine];
707  if (mozStatus) {
708    // convert from hex
709    NSScanner *scanner = [NSScanner scannerWithString:mozStatus];
710
711    unsigned int hexInt = 0;
712    if ([scanner scanHexInt:&hexInt]) {
713
714      BOOL isRead = ((hexInt & 0x0001) != 0);
715      if (!isRead) [props addObject:kGDataMailItemIsUnread];
716
717      BOOL isFlagged = ((hexInt & 0x0004) != 0);
718      if (isFlagged) [props addObject:kGDataMailItemIsStarred];
719
720      BOOL isDeleted = ((hexInt & 0x0008) != 0);
721      if (isDeleted) [props addObject:kGDataMailItemIsTrash];
722
723      // is draft status available in Thunderbird headers?
724    }
725  } else {
726    // eudora status
727    NSString *status = [EmUpUtilities stringForHeader:@"Status"
728                                          fromHeaders:headers
729                                            endOfLine:endOfLine];
730    if (status) {
731      BOOL isUnread = ([status rangeOfString:@"U"].location != NSNotFound);
732      if (isUnread) [props addObject:kGDataMailItemIsUnread];
733    }
734
735    // other mobx status per http://wiki.dovecot.org/MailboxFormat/mbox
736    NSString *xstatus = [EmUpUtilities stringForHeader:@"X-Status"
737                                           fromHeaders:headers
738                                             endOfLine:endOfLine];
739    if (xstatus) {
740      BOOL isFlagged = ([xstatus rangeOfString:@"F"].location != NSNotFound);
741      if (isFlagged) [props addObject:kGDataMailItemIsStarred];
742
743      BOOL isDeleted = ([xstatus rangeOfString:@"D"].location != NSNotFound);
744      if (isDeleted) [props addObject:kGDataMailItemIsTrash];
745
746      BOOL isDraft = ([xstatus rangeOfString:@"T"].location != NSNotFound);
747      if (isDraft) [props addObject:kGDataMailItemIsDraft];
748    }
749  }
750
751  if ([props count] > 0) return props;
752  return nil;
753}
754
755- (void)setLastUploadedItem:(OutlineViewItemMBox *)item {
756  [lastUploadedItem_ autorelease];
757  lastUploadedItem_ = [item retain];
758}
759
760@end
761
762
763// cribbed from GDataMIMEDocument
764const char* memsearch(const char* needle, unsigned long long needleLen,
765                      const char* haystack, unsigned long long haystackLen) {
766
767  // This is a simple approach.  We start off by assuming that both memchr() and
768  // memcmp are implemented efficiently on the given platform.  We search for an
769  // instance of the first char of our needle in the haystack.  If the remaining
770  // size could fit our needle, then we memcmp to see if it occurs at this point
771  // in the haystack.  If not, we move on to search for the first char again,
772  // starting from the next character in the haystack.
773  const char* ptr = haystack;
774  unsigned long long remain = haystackLen;
775  while ((ptr = memchr(ptr, needle[0], remain)) != 0) {
776    remain = haystackLen - (ptr - haystack);
777    if (remain < needleLen) {
778      return NULL;
779    }
780    if (memcmp(ptr, needle, needleLen) == 0) {
781      return ptr;
782    }
783    ptr++;
784    remain--;
785  }
786  return NULL;
787}