/* Copyright (C) 2000-2004 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "NGImap4ResponseParser.h" #include "NGImap4Support.h" #include "imCommon.h" // TODO(hh): code is now prepared for last-exception, but currently it just // raises and may leak the exception object @interface NGImap4ResponseParser(ParsingPrivates) - (BOOL)_parseNumberUntaggedResponse:(NGMutableHashMap *)result_; - (NSDictionary *)_parseBodyContent; @end @implementation NGImap4ResponseParser #define __la(__SELF__, __LACNT) \ ((__SELF__->la == NULL) \ ? [__SELF__->buffer la:__LACNT]\ : __SELF__->la(__SELF__->buffer, @selector(la:), __LACNT)) static __inline__ int _la(NGImap4ResponseParser *self, unsigned _laCnt) { char c = __la(self, _laCnt); if (c == '\r') return _la(self, _laCnt + 1); return c; } static NSDictionary *_parseBody(NGImap4ResponseParser *self); static NSString *_parseBodyString(NGImap4ResponseParser *self, BOOL _convertString); static NSString *_parseBodyDecodeString(NGImap4ResponseParser *self, BOOL _convertString, BOOL _decode); static NSDictionary *_parseBodyParameterList(NGImap4ResponseParser *self); static NSArray *_parseAddressStructure(NGImap4ResponseParser *self); static NSArray *_parseParenthesizedAddressList(NGImap4ResponseParser *self); static NSDictionary *_parseSingleBody(NGImap4ResponseParser *self); static NSDictionary *_parseMultipartBody(NGImap4ResponseParser *self); static int _parseTaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static void _parseUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseByeUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static NSArray *_parseFlagArray(NGImap4ResponseParser *self); static BOOL _parseFlagsUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseOkUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseBadUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseNoUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseThreadResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseStatusResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseListOrLSubResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseCapabilityResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseSearchResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static NSArray *_parseThread(NGImap4ResponseParser *self); static BOOL _parseSortResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static NSNumber *_parseUnsigned(NGImap4ResponseParser *self); static NSString *_parseUntil(NGImap4ResponseParser *self, char _c); static NSString *_parseUntil2(NGImap4ResponseParser *self, char _c1, char _c2); static __inline__ void _match(NGImap4ResponseParser *self, char _match); static __inline__ void _consume(NGImap4ResponseParser *self, unsigned _cnt); static void _parseContinuationResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static NSData *_parseData(NGImap4ResponseParser *self); static void _parseSieveRespone(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseGreetingsSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseDataSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseOkSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseNoSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static NSString *_parseContentSieveResponse(NGImap4ResponseParser *self); static NSString *_parseStringSieveResponse(NGImap4ResponseParser *self); static BOOL _parseQuotaResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static BOOL _parseQuotaRootResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_); static unsigned int LaSize = 4097; static unsigned UseMemoryMappedData = 0; static unsigned Imap4MMDataBoundary = 0; static BOOL debugOn = NO; static BOOL debugDataOn = NO; static NSStringEncoding encoding; static Class StrClass = Nil; static Class NumClass = Nil; static NSStringEncoding defCStringEncoding; static NSNumber *YesNum = nil; static NSNumber *NoNum = nil; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; static BOOL didInit = NO; if (didInit) return; didInit = YES; encoding = [NGMimePartParser defaultHeaderFieldEncoding]; defCStringEncoding = [NSString defaultCStringEncoding]; debugOn = [ud boolForKey:@"ImapDebugEnabled"]; UseMemoryMappedData = [ud boolForKey:@"NoMemoryMappedDataForImapBlobs"]?0:1; Imap4MMDataBoundary = [ud integerForKey:@"Imap4MMDataBoundary"]; if (Imap4MMDataBoundary < 10) /* Note: this should be larger than a usual header size! */ Imap4MMDataBoundary = 2 * LaSize; StrClass = [NSString class]; NumClass = [NSNumber class]; YesNum = [[NumClass numberWithBool:YES] retain]; NoNum = [[NumClass numberWithBool:NO] retain]; } + (id)parserWithStream:(id)_stream { NGImap4ResponseParser *parser; parser = [NGImap4ResponseParser alloc]; /* seperate line to keep gcc happy */ return [[parser initWithStream:_stream] autorelease]; } - (id)initWithStream:(id)_stream { // designated initializer if (_stream == nil) { [self logWithFormat:@"%s: got no stream ...", __PRETTY_FUNCTION__]; [self release]; return nil; } if ((self = [super init])) { id s; s = [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:_stream]; self->buffer = [NGByteBuffer alloc]; self->buffer = [self->buffer initWithSource:s la:LaSize]; [s release]; if ([self->buffer respondsToSelector:@selector(methodForSelector:)]) self->la = (int(*)(id, SEL, unsigned)) [self->buffer methodForSelector:@selector(la:)]; self->debug = debugOn; } return self; } - (id)init { [self release]; [NSException raise:@"InvalidUseOfMethodException" format: @"calling -init on the NGImap4ResponseParser is not allowed"]; return nil; } - (void)dealloc { [self->buffer release]; if (self->debug) [self->serverResponseDebug release]; [super dealloc]; } /* exception handling */ - (void)setLastException:(NSException *)_exc { // TODO: support last exception [_exc raise]; } /* ** Parse Sieve Responses */ - (NGHashMap *)parseSieveResponse { NGMutableHashMap *result; if (self->debug) { if (self->serverResponseDebug != nil) [self->serverResponseDebug release]; self->serverResponseDebug = [[NSMutableString alloc] initWithCapacity:512]; } result = [NGMutableHashMap hashMapWithCapacity:64]; if (_la(self,0) == -1) { [self setLastException:[self->buffer lastException]]; return nil; } _parseSieveRespone(self, result); return result; } - (NGHashMap *)parseResponseForTagId:(int)_tag exception:(NSException **)ex_ { /* parse a response from the server, _tag!=-1 parse until tagged response */ // TODO: is NGHashMap really necessary here? BOOL endOfCommand; NGMutableHashMap *result; if (ex_) *ex_ = nil; if (self->debug) { [self->serverResponseDebug release]; self->serverResponseDebug = nil; self->serverResponseDebug = [[NSMutableString alloc] initWithCapacity:512]; } result = [NGMutableHashMap hashMapWithCapacity:64]; if (_la(self, 0) == -1) { [self logWithFormat:@"%s: catched: %@", __PRETTY_FUNCTION__, [self->buffer lastException]]; if (ex_) { *ex_ = [self->buffer lastException]; return nil; } else { [self setLastException:[self->buffer lastException]]; return nil; } } for (endOfCommand = NO; !endOfCommand; ) { unsigned char l0; l0 = _la(self, 0); if (l0 == '*') { /* those starting with '* ' */ _parseUntaggedResponse(self, result); if ([result objectForKey:@"bye"]) { endOfCommand = YES; } else { if (_tag == -1) { if ([result objectForKey:@"ok"] != nil) endOfCommand = YES; } } } else if (l0 == '+') { /* starting with a '+'? */ _parseContinuationResponse(self, result); endOfCommand = YES; } else if (isdigit(l0)) { /* those starting with a number '24 ', eg '24 OK Completed' */ endOfCommand = (_parseTaggedResponse(self, result) == _tag); } } return result; } - (NGHashMap *)parseResponseForTagId:(int)_tag { // DEPRECATED NSException *e = nil; NGHashMap *hm; hm = [self parseResponseForTagId:_tag exception:&e]; if (e) { [self setLastException:e]; return nil; } return hm; } static void _parseSieveRespone(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (_parseGreetingsSieveResponse(self, result_)) return; if (_parseDataSieveResponse(self, result_)) // la: 1 return; if (_parseOkSieveResponse(self, result_)) // la: 2 return; if (_parseNoSieveResponse(self, result_)) // la: 2 return; } static NSData *_parseData(NGImap4ResponseParser *self) { // TODO: split up method NSData *result; unsigned size; NSNumber *n; if (_la(self, 0) != '{') return nil; if (debugDataOn) [self logWithFormat:@"parse data ..."]; /* got header */ result = nil; _consume(self, 1); if ((n = _parseUnsigned(self)) == nil) { NSException *e; e = [[NGImap4ParserException alloc] initWithFormat:@"expect a number between {}"]; [self setLastException:[e autorelease]]; return nil; } if (debugDataOn) [self logWithFormat:@" parse data: %@", n]; _match(self, '}'); _match(self, '\n'); size = [n intValue]; if (UseMemoryMappedData && (size > Imap4MMDataBoundary)) { static NSProcessInfo *Pi = nil; NGFileStream *stream; unsigned char buf[LaSize + 2]; unsigned char tmpBuf[LaSize + 2]; unsigned wasRead = 0; NSString *path; signed char lastChar; // must be signed if (debugDataOn) [self logWithFormat:@" using memory mapped data ..."]; if (Pi == nil) Pi = [[NSProcessInfo processInfo] retain]; path = [Pi temporaryFileName]; stream = [NGFileStream alloc]; /* extra line to keep gcc happy */ stream = [stream initWithPath:path]; if (![stream openInMode:NGFileWriteOnly]) { NSException *e; e = [[NGImap4ParserException alloc] initWithFormat:@"Could not open temporary file %@", path]; [self setLastException:[e autorelease]]; return nil; } lastChar = -1; while (wasRead < size) { unsigned readCnt, bufCnt, tmpSize, cnt, tmpBufCnt; bufCnt = 0; if (lastChar != -1) { buf[bufCnt++] = lastChar; lastChar = -1; } [self->buffer la:(size - wasRead < LaSize) ? (size - wasRead) : LaSize]; readCnt = [self->buffer readBytes:buf+bufCnt count:size - wasRead]; wasRead+=readCnt; bufCnt +=readCnt; tmpSize = bufCnt - 1; cnt = 0; tmpBufCnt = 0; while (cnt < tmpSize) { if ((buf[cnt] == '\r') && (buf[cnt+1] == '\n')) { cnt++; } tmpBuf[tmpBufCnt++] = buf[cnt++]; } if (cnt < bufCnt) { lastChar = buf[cnt]; } [stream writeBytes:tmpBuf count:tmpBufCnt]; } if (lastChar != -1) [stream writeBytes:&lastChar count:1]; [stream close]; [stream release]; stream = nil; result = [NSData dataWithContentsOfMappedFile:path]; [[NSFileManager defaultManager] removeFileAtPath:path handler:nil]; return result; } if (size == 0) { [self logWithFormat:@"ERROR(%s): got content size '0'!", __PRETTY_FUNCTION__]; return nil; } else { unsigned char *buf = NULL; unsigned wasRead = 0; unsigned char *tmpBuf; unsigned cnt, tmpBufCnt, tmpSize; buf = calloc(size + 10, sizeof(char)); while (wasRead < size) { [self->buffer la:(size - wasRead < LaSize) ? (size - wasRead) : LaSize]; wasRead += [self->buffer readBytes:(buf + wasRead) count:(size - wasRead)]; } /* normalize response \r\n -> \n */ tmpBuf = calloc(size + 10, sizeof(char)); cnt = 0; tmpBufCnt = 0; tmpSize = size == 0 ? 0 : size - 1; while (tmpBufCnt < tmpSize && cnt < size) { if ((buf[cnt] == '\r') && (buf[cnt + 1] == '\n')) cnt++; /* skip \r */ tmpBuf[tmpBufCnt] = buf[cnt]; tmpBufCnt++; cnt++; } if (cnt < size) { tmpBuf[tmpBufCnt] = buf[cnt]; tmpBufCnt++; cnt++; } result = [NSData dataWithBytesNoCopy:tmpBuf length:tmpBufCnt]; if (buf != NULL) free(buf); buf = NULL; return result; } } static int _parseTaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { NSDictionary *d; NSNumber *tag = nil; NSString *res = nil; NSString *desc = nil; NSString *flag = nil; if ((tag = _parseUnsigned(self)) == nil) { NSException *e; if (self->debug) { e = [[NGImap4ParserException alloc] initWithFormat:@"expect a number at begin of tagged response <%@>", self->serverResponseDebug]; } else { e = [[NGImap4ParserException alloc] initWithFormat:@"expect a number at begin of tagged response"]; } e = [e autorelease]; [self setLastException:e]; return -1; } _match(self, ' '); res = [_parseUntil(self, ' ') lowercaseString]; if (_la(self, 0) == '[') { /* Found flag like [READ-ONLY] */ _consume(self, 1); flag = _parseUntil(self, ']'); } desc = _parseUntil(self, '\n'); /* ATTENTION: if no flag was set, flag == nil, in this case all key-value pairs after flag will be ignored */ d = [[NSDictionary alloc] initWithObjectsAndKeys: tag, @"tagId", res, @"result", desc, @"description", flag, @"flag", nil]; [result_ addObject:d forKey:@"ResponseResult"]; [d release]; return [tag intValue]; } static void _parseUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { // TODO: is it really required by IMAP4 that responses are uppercase? // TODO: apparently this code *breaks* with lowercase detection on! unsigned char l0, l1 = 0; _match(self, '*'); _match(self, ' '); l0 = _la(self, 0); switch (l0) { case 'B': l1 = _la(self, 1); if (l1 == 'A' && _parseBadUntaggedResponse(self, result_)) // la: 3 return; if (l1 == 'Y' && _parseByeUntaggedResponse(self, result_)) // la: 3 return; break; case 'C': if (_parseCapabilityResponse(self, result_)) // la: 10 return; break; case 'F': if (_parseFlagsUntaggedResponse(self, result_)) // la: 5 return; break; case 'L': if (_parseListOrLSubResponse(self, result_)) // la: 4 return; break; case 'N': if (_parseNoUntaggedResponse(self, result_)) // la: 2 return; break; case 'O': if (_parseOkUntaggedResponse(self, result_)) // la: 2 /* eg "* OK Completed" */ return; break; case 'R': break; case 'S': switch (_la(self, 1)) { case 'O': // SORT if (_parseSortResponse(self, result_)) // la: 4 return; break; case 'E': // SEARCH if (_parseSearchResponse(self, result_)) // la: 5 return; break; case 'T': // STATUS if (_parseStatusResponse(self, result_)) // la: 6 /* eg "* STATUS INBOX (MESSAGES 0 RECENT 0 UNSEEN 0)" */ return; break; } break; case 'T': if (_parseThreadResponse(self, result_)) // la: 6 return; break; case 'Q': if (_parseQuotaResponse(self, result_)) // la: 6 return; if (_parseQuotaRootResponse(self, result_)) // la: 10 return; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if ([self _parseNumberUntaggedResponse:result_]) // la: 5 /* eg "* 928 FETCH ..." */ return; break; } // TODO: what if none matches? [self logWithFormat:@"%s: no matching tag specifier?", __PRETTY_FUNCTION__]; } static void _parseContinuationResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { _match(self, '+'); _match(self, ' '); [result_ addObject:YesNum forKey:@"ContinuationResponse"]; [result_ addObject:_parseUntil(self, '\n') forKey:@"description"]; } static BOOL _parseListOrLSubResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (((_la(self, 0) == 'L') && (_la(self, 1) == 'I') && (_la(self, 2) == 'S') && (_la(self, 3) == 'T') && (_la(self, 4) == ' ')) || ((_la(self, 0) == 'L') && (_la(self, 1) == 'S') && (_la(self, 2) == 'U') && (_la(self, 3) == 'B') && (_la(self, 4) == ' '))) { NSArray *flags = nil; NSString *delim = nil; NSString *name = nil; NSDictionary *d; _consume(self, 5); flags = _parseFlagArray(self); _match(self, ' '); if (_la(self, 0) == '"') { _match(self, '"'); delim = _parseUntil(self, '"'); _match(self, ' '); } else { _parseUntil(self, ' '); delim = nil; } if (_la(self, 0) == '"') { _consume(self, 1); name = _parseUntil(self, '"'); _parseUntil(self, '\n'); } else { name = _parseUntil(self, '\n'); } d = [[NSDictionary alloc] initWithObjectsAndKeys: name, @"folderName", flags, @"flags", delim, @"delimiter", nil]; [result_ addObject:d forKey:@"list"]; [d release]; return YES; } return NO; } static BOOL _parseCapabilityResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'C') && (_la(self, 1) == 'A') && (_la(self, 2) == 'P') && (_la(self, 3) == 'A') && (_la(self, 4) == 'B') && (_la(self, 5) == 'I') && (_la(self, 6) == 'L') && (_la(self, 7) == 'I') && (_la(self, 8) == 'T') && (_la(self, 9) == 'Y') && (_la(self, 10) == ' ')) { NSString *caps; caps = _parseUntil(self, '\n'); { NSEnumerator *enumerator; id obj; NSMutableArray *array; NSArray *tmp; array = [[NSMutableArray alloc] initWithCapacity:16]; enumerator = [[caps componentsSeparatedByString:@" "] objectEnumerator]; while ((obj = [enumerator nextObject])) { [array addObject:[obj lowercaseString]]; } tmp = [array shallowCopy]; [result_ addObject:tmp forKey:@"capability"]; [array release]; array = nil; [tmp release]; tmp = nil; } return YES; } return NO; } static BOOL _parseSearchResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'S') && (_la(self, 1) == 'E') && (_la(self, 2) == 'A') && (_la(self, 3) == 'R') && (_la(self, 4) == 'C') && (_la(self, 5) == 'H')) { NSMutableArray *msn = nil; _consume(self, 6); msn = [NSMutableArray arrayWithCapacity:128]; while (_la(self, 0) == ' ') { _consume(self, 1); [msn addObject:_parseUnsigned(self)]; } _parseUntil(self, '\n'); [result_ addObject:msn forKey:@"search"]; return YES; } return NO; } static BOOL _parseSortResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'S') && (_la(self, 1) == 'O') && (_la(self, 2) == 'R') && (_la(self, 3) == 'T')) { NSMutableArray *msn = nil; _consume(self, 4); msn = [NSMutableArray arrayWithCapacity:128]; while (_la(self, 0) == ' ') { _consume(self, 1); [msn addObject:_parseUnsigned(self)]; } _parseUntil(self, '\n'); [result_ addObject:msn forKey:@"sort"]; return YES; } return NO; } static BOOL _parseQuotaResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'Q') && (_la(self, 1) == 'U') && (_la(self, 2) == 'O') && (_la(self, 3) == 'T') && (_la(self, 4) == 'A') && (_la(self, 5) == ' ')) { NSString *qRoot; NSMutableDictionary *parse; NSMutableDictionary *quota; _consume(self, 6); quota = [result_ objectForKey:@"quota"]; if (!quota) { quota = [NSMutableDictionary dictionaryWithCapacity:2]; [result_ setObject:quota forKey:@"quota"]; } parse = [NSMutableDictionary dictionaryWithCapacity:3]; qRoot = _parseUntil2(self, ' ', '\n'); if (_la(self, 0) == ' ') { _consume(self, 1); if (_la(self, 0) == '(') { _consume(self,1); if (_la(self, 0) == ')') { /* empty quota response */ _consume(self,1); } else { NSString *key; key = _parseUntil(self, ' '); key = [key lowercaseString]; if ([key isEqualToString:@"storage"]) { NSString *used, *max; used = _parseUntil(self, ' '); max = _parseUntil(self, ')'); [parse setObject:used forKey:@"usedSpace"]; [parse setObject:max forKey:@"maxQuota"]; } else { NSString *v; v = _parseUntil(self, ')'); [parse setObject:v forKey:@"resource"]; } } } [quota setObject:parse forKey:qRoot]; } _parseUntil(self, '\n'); return YES; } return NO; } static BOOL _parseQuotaRootResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'Q') && (_la(self, 1) == 'U') && (_la(self, 2) == 'O') && (_la(self, 3) == 'T') && (_la(self, 4) == 'A') && (_la(self, 5) == 'R') && (_la(self, 6) == 'O') && (_la(self, 7) == 'O') && (_la(self, 8) == 'T') && (_la(self, 9) == ' ')) { NSString *folderName, *folderRoot; NSMutableDictionary *dict; _consume(self, 10); dict = [result_ objectForKey:@"quotaRoot"]; if (!dict) { dict = [NSMutableDictionary dictionaryWithCapacity:2]; [result_ setObject:dict forKey:@"quotaRoot"]; } if (_la(self, 0) == '"') { _consume(self , 1); folderName = _parseUntil(self, '"'); } else { folderName = _parseUntil2(self, '\n', ' '); } if (_la(self, 0) == ' ') { _consume(self, 1); folderRoot = _parseUntil(self, '\n'); } else { _consume(self, 1); folderRoot = nil; } if ([folderName length] && [folderRoot length]) { [dict setObject:folderRoot forKey:folderName]; } return YES; } return NO; } static NSArray *_parseThread(NGImap4ResponseParser *self) { NSMutableArray *array; NSNumber *msg; array = [NSMutableArray arrayWithCapacity:64]; if (_la(self, 0) == '(') { _consume(self, 1); } while (1) { if (_la(self, 0) == '(') { id a; a = _parseThread(self); [array addObject:a]; } else if ((msg = _parseUnsigned(self))) { [array addObject:msg]; } else { return nil; } if (_la(self, 0) == ')') break; else if (_la(self, 0) == ' ') _consume(self, 1); } _match(self, ')'); return array; } static BOOL _parseThreadResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'T') && (_la(self, 1) == 'H') && (_la(self, 2) == 'R') && (_la(self, 3) == 'E') && (_la(self, 4) == 'A') && (_la(self, 5) == 'D')) { NSMutableArray *msn; _consume(self, 6); if (_la(self, 0) == ' ') { _consume(self, 1); } else { [result_ addObject:[NSArray array] forKey:@"thread"]; return YES; } msn = [NSMutableArray arrayWithCapacity:64]; while ((_la(self, 0) == '(')) { NSArray *array; if ((array = _parseThread(self))) [msn addObject:array]; } _parseUntil(self, '\n'); [result_ addObject:msn forKey:@"thread"]; return YES; } return NO; } static BOOL _parseStatusResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'S') && (_la(self, 1) == 'T') && (_la(self, 2) == 'A') && (_la(self, 3) == 'T') && (_la(self, 4) == 'U') && (_la(self, 5) == 'S') && (_la(self, 6) == ' ')) { NSString *name = nil; NSMutableDictionary *flags = nil; NSDictionary *d; _consume(self, 7); if (_la(self, 0) == '"') { _consume(self, 1); name = _parseUntil(self, '"'); _match(self, ' '); } else { name = _parseUntil(self, ' '); } _match(self, '('); flags = [NSMutableDictionary dictionaryWithCapacity:8]; while (_la(self, 0) != ')') { NSString *key = _parseUntil(self, ' '); id value = _parseUntil2(self, ' ', ')'); if (_la(self, 0) == ' ') _consume(self, 1); [flags setObject:[NumClass numberWithInt:[value intValue]] forKey:[key lowercaseString]]; } _match(self, ')'); _parseUntil(self, '\n'); d = [[NSDictionary alloc] initWithObjectsAndKeys: name, @"folderName", flags, @"flags", nil]; [result_ addObject:d forKey:@"status"]; [d release]; return YES; } return NO; } static BOOL _parseByeUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'B') && (_la(self, 1) == 'Y') && (_la(self, 2) == 'E') && (_la(self, 3) == ' ')) { NSString *reason = nil; _consume(self, 4); reason = _parseUntil(self, '\n'); [result_ addObject:reason forKey:@"bye"]; return YES; } return NO; } - (BOOL)_parseNumberUntaggedResponse:(NGMutableHashMap *)result_ { NSNumber *number = nil; NSString *key = nil; if ((number = _parseUnsigned(self)) == nil) return NO; _match(self, ' '); if ((_la(self, 0) == 'F') && (_la(self, 1) == 'E') && (_la(self, 2) == 'T') && (_la(self, 3) == 'C') && (_la(self, 4) == 'H') && (_la(self, 5) == ' ')) { /* got a fetch response (fetch request) */ /* eg: "FETCH (FLAGS (\Seen) UID 5 RFC822.HEADER {2903}" */ NSMutableDictionary *fetch = nil; fetch = [[NSMutableDictionary alloc] initWithCapacity:10]; _consume(self, 6); _match(self, '('); while (_la(self, 0) != ')') { NSString *key = nil; key = [_parseUntil(self, ' ') lowercaseString]; if ([key hasPrefix:@"body["]) { NSDictionary *content; if ((content = [self _parseBodyContent]) != nil) [fetch setObject:content forKey:key]; else [self logWithFormat:@"ERROR: got no body content for key: '%@'",key]; } else if ([key isEqualToString:@"body"]) { [fetch setObject:_parseBody(self) forKey:key]; } else if ([key isEqualToString:@"flags"]) { [fetch setObject:_parseFlagArray(self) forKey:key]; } else if ([key isEqualToString:@"uid"]) { [fetch setObject:_parseUnsigned(self) forKey:key]; } else if ([key isEqualToString:@"rfc822.size"]) { [fetch setObject:_parseUnsigned(self) forKey:key]; } else if ([key hasPrefix:@"rfc822"]) { NSData *data; if (_la(self, 0) == '"') { NSString *str; _consume(self,1); str = _parseUntil(self, '"'); data = [str dataUsingEncoding:defCStringEncoding]; } else data = _parseData(self); if (data != nil) [fetch setObject:data forKey:key]; } else { NSException *e; e = [[NGImap4ParserException alloc] initWithFormat: @"unsupported fetch %@", key]; [self setLastException:[e autorelease]]; return NO; } if (_la(self, 0) == ' ') _consume(self, 1); } if (fetch) { [fetch setObject:number forKey:@"msn"]; [result_ addObject:fetch forKey:@"fetch"]; _consume(self, 1); /* consume ')' */ _match(self, '\n'); } else { /* no correct fetch line */ _parseUntil(self, '\n'); } [fetch release]; fetch = nil; return YES; } { /* got a number request from select like exists or recent */ key = _parseUntil(self, '\n'); [result_ addObject:number forKey:[key lowercaseString]]; } return YES; } static BOOL _parseGreetingsSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { BOOL isOK; while (!(isOK = _parseOkSieveResponse(self, result_))) { NSString *key, *value; if (!(key = _parseStringSieveResponse(self))) { break; } if (_la(self, 0) == ' ') { _consume(self, 1); if (!(value = _parseStringSieveResponse(self))) { break; } } else { value = @""; } _parseUntil(self, '\n'); [result_ addObject:value forKey:[key lowercaseString]]; } return isOK; } static BOOL _parseDataSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { NSString *str; NSData *data; if ((data = _parseData(self)) == nil) return NO; str = [[StrClass alloc] initWithData:data encoding:defCStringEncoding]; [result_ setObject:str forKey:@"data"]; [str release]; str = nil; return YES; } static BOOL _parseOkSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (!((_la(self, 0) == 'O') && (_la(self, 1) == 'K'))) return NO; _consume(self, 2); if (_la(self, 0) == ' ') { NSString *reason; if ((reason = _parseContentSieveResponse(self))) [result_ addObject:reason forKey:@"reason"]; } _parseUntil(self, '\n'); [result_ addObject:YesNum forKey:@"ok"]; return YES; } static BOOL _parseNoSieveResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { NSString *data; if (!((_la(self, 0)=='N') && (_la(self, 1)=='O') && (_la(self, 2)==' '))) return NO; _consume(self, 3); data = _parseContentSieveResponse(self); [result_ addObject:NoNum forKey:@"ok"]; if (data) [result_ addObject:data forKey:@"reason"]; return YES; } static NSString *_parseContentSieveResponse(NGImap4ResponseParser *self) { NSString *str; NSData *data; if ((str = _parseStringSieveResponse(self))) return str; if ((data = _parseData(self)) == nil) return nil; return [[[StrClass alloc] initWithData:data encoding:defCStringEncoding] autorelease]; } static NSString *_parseStringSieveResponse(NGImap4ResponseParser *self) { if (_la(self, 0) != '"') return nil; _consume(self, 1); return _parseUntil(self, '"'); } static NSString *_parseBodyDecodeString(NGImap4ResponseParser *self, BOOL _convertString, BOOL _decode) { NSString *str; if (_la(self, 0) == '"') { _consume(self, 1); str = _parseUntil(self, '"'); } else if (_la(self, 0) == '{') { NSData *data; if (debugDataOn) [self logWithFormat:@"parse body decode string"]; data = _parseData(self); if (_decode) data = [data decodeQuotedPrintableValueOfMIMEHeaderField:nil]; return [[[StrClass alloc] initWithData:data encoding:encoding] autorelease]; } else { str = _parseUntil2(self, ' ', ')'); } if (_convertString) { if ([[str lowercaseString] isEqualToString:@"nil"]) str = @""; } if (_decode) { id d; d = [str dataUsingEncoding:defCStringEncoding]; d = [d decodeQuotedPrintableValueOfMIMEHeaderField:nil]; if ([d isKindOfClass:StrClass]) str = d; else { str = [[[StrClass alloc] initWithData:d encoding:encoding] autorelease]; } } return str; } static NSString *_parseBodyString(NGImap4ResponseParser *self, BOOL _convertString) { return _parseBodyDecodeString(self, _convertString, NO); } static NSDictionary *_parseBodyParameterList(NGImap4ResponseParser *self) { NSMutableDictionary *list; if (_la(self, 0) == '(') { _consume(self, 1); list = [NSMutableDictionary dictionaryWithCapacity:4]; while (_la(self,0) != ')') { NSString *key, *value; if (_la(self, 0) == ' ') _consume(self, 1); key = _parseBodyString(self, YES); _match(self, ' '); value = _parseBodyDecodeString(self, YES, YES); [list setObject:value forKey:[key lowercaseString]]; } _match(self, ')'); } else { NSString *str; str = _parseBodyString(self, YES); if ([str length] > 0) { NSLog(@"%s: got unexpected string %@", __PRETTY_FUNCTION__, str); } list = (id)[NSDictionary dictionary]; } return list; } static NSArray *_parseAddressStructure(NGImap4ResponseParser *self) { NSString *personalName, *sourceRoute, *mailboxName, *hostName; _match(self, '('); personalName = _parseBodyString(self, YES); _match(self, ' '); sourceRoute = _parseBodyString(self, YES); _match(self, ' '); mailboxName = _parseBodyString(self, YES); _match(self, ' '); hostName = _parseBodyString(self, YES); _match(self, ')'); return [NSDictionary dictionaryWithObjectsAndKeys: personalName, @"personalName", sourceRoute, @"sourceRoute", mailboxName, @"mailboxName", hostName, @"hostName", nil]; } static NSArray *_parseParenthesizedAddressList(NGImap4ResponseParser *self) { NSMutableArray *result; result = [NSMutableArray arrayWithCapacity:8]; if (_la(self, 0) == '(') { _consume(self, 1); while (_la(self, 0) != ')') { [result addObject:_parseAddressStructure(self)]; } _consume(self, 1); } else { NSString *str; str = _parseBodyString(self, YES); if ([str length] > 0) { NSLog(@"%s: got unexpected string %@", __PRETTY_FUNCTION__, str); } result = (id)[NSArray array]; } return result; } static NSDictionary *_parseSingleBody(NGImap4ResponseParser *self) { NSString *type, *subtype, *bodyId, *description, *encoding, *bodysize; NSDictionary *parameterList; NSMutableDictionary *dict; type = [_parseBodyString(self, YES) lowercaseString]; _match(self, ' '); subtype = _parseBodyString(self, YES); _match(self, ' '); parameterList = _parseBodyParameterList(self); _match(self, ' '); bodyId = _parseBodyString(self, YES); _match(self, ' '); description = _parseBodyString(self, YES); _match(self, ' '); encoding = _parseBodyString(self, YES); _match(self, ' '); bodysize = _parseBodyString(self, YES); dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: type, @"type", subtype, @"subtype", parameterList, @"parameterList", bodyId, @"bodyId", description, @"description", encoding, @"encoding", bodysize, @"size", nil]; if ([type isEqualToString:@"text"]) { _match(self, ' '); [dict setObject:_parseBodyString(self, YES) forKey:@"lines"]; } else if ([type isEqualToString:@"message"]) { if (_la(self, 0) != ')') { _match(self, ' '); _match(self, '('); [dict setObject:_parseBodyString(self, YES) forKey:@"date"]; _match(self, ' '); [dict setObject:_parseBodyString(self, YES) forKey:@"subject"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"from"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"sender"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"reply-to"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"to"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"cc"]; _match(self, ' '); [dict setObject:_parseParenthesizedAddressList(self) forKey:@"bcc"]; _match(self, ' '); [dict setObject:_parseBodyString(self, YES) forKey:@"in-reply-to"]; _match(self, ' '); [dict setObject:_parseBodyString(self, YES) forKey:@"messageId"]; _match(self, ')'); _match(self, ' '); [dict setObject:_parseBody(self) forKey:@"body"]; _match(self, ' '); [dict setObject:_parseBodyString(self, YES) forKey:@"bodyLines"]; } } return dict; } static NSDictionary *_parseMultipartBody(NGImap4ResponseParser *self) { NSMutableArray *parts; NSString *kind; parts = [NSMutableArray arrayWithCapacity:4]; while (_la(self, 0) == '(') { [parts addObject:_parseBody(self)]; } _match(self, ' '); kind = _parseBodyString(self, YES); return [NSDictionary dictionaryWithObjectsAndKeys: parts, @"parts", @"multipart", @"type", kind , @"subtype", nil]; } static NSDictionary *_parseBody(NGImap4ResponseParser *self) { NSDictionary *result; _match(self, '('); if (_la(self, 0) == '(') { result = _parseMultipartBody(self); } else { result = _parseSingleBody(self); } if (_la(self,0) != ')') { NSString *str; str = _parseUntil(self, ')'); NSLog(@"%s: got noparsed content %@", __PRETTY_FUNCTION__, str); } else _consume(self, 1); return result; } - (NSDictionary *)_parseBodyContent { NSData *data; if (_la(self, 0) == '"') { NSString *str; _consume(self,1); str = _parseUntil(self, '"'); data = [str dataUsingEncoding:defCStringEncoding]; } else data = _parseData(self); if (data == nil) { [self logWithFormat:@"ERROR(%s): got no data.", __PRETTY_FUNCTION__]; return nil; } return [NSDictionary dictionaryWithObject:data forKey:@"data"]; } static NSArray *_parseFlagArray(NGImap4ResponseParser *self) { NSString *flags; _match(self, '('); flags = _parseUntil(self, ')'); if ([flags length] == 0) { static NSArray *emptyArray = nil; if (emptyArray == nil) emptyArray = [[NSArray alloc] init]; return emptyArray; } else return [[flags lowercaseString] componentsSeparatedByString:@" "]; } static BOOL _parseFlagsUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if ((_la(self, 0) == 'F') && (_la(self, 1) == 'L') && (_la(self, 2) == 'A') && (_la(self, 3) == 'G') && (_la(self, 4) == 'S') && (_la(self, 5) == ' ')) { _consume(self, 6); [result_ addObject:_parseFlagArray(self) forKey:@"flags"]; _match(self, '\n'); return YES; } return NO; } static BOOL _parseBadUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (!((_la(self, 0)=='B') && (_la(self, 1)=='A') && (_la(self, 2)=='D') && (_la(self, 3) == ' '))) return NO; _consume(self, 4); [result_ addObject:_parseUntil(self, '\n') forKey:@"bad"]; return YES; } static BOOL _parseNoOrOkArguments(NGImap4ResponseParser *self, NGMutableHashMap *result_, NSString *_key) { NSString *obj; obj = nil; if (_la(self, 0) == '[') { NSString *key; _consume(self, 1); key = _parseUntil2(self, ']', ' '); /* possible kinds of untagged OK responses are either * OK [ALERT] System shutdown in 10 minutes or * OK [UNSEEN 14] or * OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen \*)] */ if (_la(self, 0) == ']') { _consume(self, 1); if (_la(self, 0) == ' ') { id value; _consume(self, 1); value = _parseUntil(self, '\n'); if ([value length] > 0) { obj = [[NSDictionary alloc] initWithObjects:&value forKeys:&key count:1]; } else obj = [key retain]; } else { obj = [key retain]; _parseUntil(self, '\n'); } } else { /* _la(self, 0) should be ' ' */ id value; value = nil; _consume(self, 1); if (_la(self, 0) == '(') { value = _parseFlagArray(self); _consume(self, 1); /* consume ']' */ } else { value = _parseUntil(self, ']'); } { id tmp; tmp = _parseUntil(self, '\n'); obj = [[NSDictionary alloc] initWithObjectsAndKeys: value, key, tmp, @"comment", nil]; } } } else obj = [_parseUntil(self, '\n') retain]; [result_ addObject:obj forKey:_key]; [obj release]; return YES; } static BOOL _parseNoUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (!((_la(self, 0)=='N') && (_la(self, 1)=='O') && (_la(self, 2)==' '))) return NO; _consume(self, 3); return _parseNoOrOkArguments(self, result_, @"no"); } static BOOL _parseOkUntaggedResponse(NGImap4ResponseParser *self, NGMutableHashMap *result_) { if (!((_la(self, 0)=='O') && (_la(self, 1)=='K') && (_la(self, 2)==' '))) return NO; _consume(self, 3); return _parseNoOrOkArguments(self, result_, @"ok"); } static NSNumber *_parseUnsigned(NGImap4ResponseParser *self) { unsigned n; unsigned char c; BOOL isNumber; isNumber = NO; n = 0; c = _la(self, 0); while ((c >= '0') && (c <= '9')) { _consume(self, 1); isNumber = YES; n = 10 * n + (c - 48); c = _la(self, 0); } if (!isNumber) return nil; return [NumClass numberWithUnsignedInt:n]; } static NSString *_parseUntil(NGImap4ResponseParser *self, char _c) { /* _parseUntil(self, char) consume the stop char normalize \r\n constructions */ // TODO: optimize! char buf[1024], c; NSMutableString *str; unsigned cnt; cnt = 0; str = nil; while ((c = _la(self, 0)) != _c) { buf[cnt] = c; _consume(self, 1); cnt++; if (cnt == 1024) { if (str == nil) str = (NSMutableString *) [NSMutableString stringWithCString:buf length:1024]; else { NSString *s; s = [(NSString *)[StrClass alloc] initWithCString:buf length:1024]; [str appendString:s]; [s release]; } cnt = 0; } } _consume(self,1); /* consume known stop char */ if (_c == '\n' && cnt > 0) { if (buf[cnt-1] == '\r') cnt--; } if (str == nil) return [StrClass stringWithCString:buf length:cnt]; else { NSString *s, *s2; s = [(NSString *)[StrClass alloc] initWithCString:buf length:cnt]; s2 = [str stringByAppendingString:s]; [s release]; return s2; } } static NSString *_parseUntil2(NGImap4ResponseParser *self, char _c1, char _c2){ /* _parseUntil2(self, char, char) doesn`t consume the stop-chars */ char buf[1024], c; NSMutableString *str; unsigned cnt; cnt = 0; c = _la(self, 0); str = nil; while ((c != _c1) && (c != _c2)) { buf[cnt] = c; _consume(self, 1); cnt++; if (cnt == 1024) { if (str == nil) str = (NSMutableString *) [NSMutableString stringWithCString:buf length:1024]; else { NSString *s; s = [(NSString *)[StrClass alloc] initWithCString:buf length:1024]; [str appendString:s]; [s release]; } cnt = 0; } c = _la(self, 0); } if (str == nil) return [StrClass stringWithCString:buf length:cnt]; { NSString *s, *s2; s = [(NSString *)[StrClass alloc] initWithCString:buf length:cnt]; s2 = [str stringByAppendingString:s]; [s release]; return s2; } } static __inline__ void _match(NGImap4ResponseParser *self, char _match) { if (_la(self,0) == _match) { _consume(self, 1); return; } { NSException *e; if (self->debug) { e = [[NGImap4ParserException alloc] initWithFormat:@"unexpected char <%c> " @"expected <%c> <%@>", _la(self, 0), _match, self->serverResponseDebug]; } else { e = [[NGImap4ParserException alloc] initWithFormat:@"unexpected char <%c> " @"expected <%c>", _la(self, 0), _match]; } e = [e autorelease]; [self setLastException:e]; } } static __inline__ void _consume(NGImap4ResponseParser *self, unsigned _cnt) { /* Normalize end of line */ if (_cnt == 0) return; _cnt += (__la(self, _cnt - 1) == '\r') ? 1 : 0; if (self->debug) { unsigned cnt; for (cnt = 0; cnt < _cnt; cnt++) { NSString *s; unichar c = _la(self, cnt); if (c == '\r') continue; s = [[StrClass alloc] initWithCharacters:&c length:1]; [self->serverResponseDebug appendString:s]; [s release]; if (c == '\n') { if ([self->serverResponseDebug cStringLength] > 2) { fprintf(stderr, "S[%p]: %s", self, [self->serverResponseDebug cString]); } [self->serverResponseDebug release]; self->serverResponseDebug = [[NSMutableString alloc] initWithCapacity:512]; } } } [self->buffer consume:_cnt]; } @end /* NGImap4ResponseParser */