]> err.no Git - sope/blobdiff - sope-mime/NGImap4/NGSieveClient.m
fixed an issue with missing Sieve scripts
[sope] / sope-mime / NGImap4 / NGSieveClient.m
index dfa78e1343a2fc1929d171235659f62d4d550e51..e1bc357cf4ab6203a10c8e7b8610b787b1227434 100644 (file)
 
 @interface NGSieveClient(Private)
 
-- (NGHashMap *)processCommand:(NSString *)_command;
-- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;
+- (NGHashMap *)processCommand:(id)_command;
+- (NGHashMap *)processCommand:(id)_command logText:(id)_txt;
 
-- (void)sendCommand:(NSString *)_command;
-- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt;
+- (NSException *)sendCommand:(id)_command;
+- (NSException *)sendCommand:(id)_command logText:(id)_txt;
+- (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c;
 
 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
 - (NSDictionary *)login;
 
+/* parsing */
+
+- (NSString *)readStringToCRLF;
+- (NSString *)readString;
+
 @end
 
-/*"
-**  An implementation of an Imap4 client
-**
-** A folder name always looks like an absolute filename (/inbox/doof) 
-**
-"*/
+/*
+  An implementation of an Imap4 client
+  
+  A folder name always looks like an absolute filename (/inbox/blah)
+  
+  NOTE: Sieve is just the filtering language ...
+  
+  This should be ACAP?
+    http://asg.web.cmu.edu/rfc/rfc2244.html
+
+  ---snip---
+"IMPLEMENTATION" "Cyrus timsieved v2.1.15-IPv6-Debian-2.1.15-0woody.1.0"
+"SASL" "PLAIN"
+"SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex"
+"STARTTLS"
+OK
+  ---snap---
+*/
 
 @implementation NGSieveClient
 
-static int      defaultSievePort = 143;
-static NSNumber *YesNumber = nil;
-static NSNumber *NoNumber  = nil;
+static int      defaultSievePort   = 2000;
+static NSNumber *YesNumber         = nil;
+static NSNumber *NoNumber          = nil;
 static BOOL     ProfileImapEnabled = NO;
 static BOOL     LOG_PASSWORD       = NO;
 static BOOL     debugImap4         = NO;
@@ -73,36 +91,67 @@ static BOOL     debugImap4         = NO;
   NoNumber  = [[NSNumber numberWithBool:NO] retain];
 }
 
++ (id)clientWithURL:(id)_url {
+  return [[[self alloc] initWithURL:_url] autorelease];
+}
+
 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
-  return
-    [[(NGSieveClient *)[self alloc] initWithAddress:_address] autorelease];
+  NGSieveClient *client;
+  
+  client = [self alloc];
+  return [[client initWithAddress:_address] autorelease];
 }
 
 + (id)clientWithHost:(id)_host {
   return [[[self alloc] initWithHost:_host] autorelease];
 }
 
+- (id)initWithNSURL:(NSURL *)_url {
+  NGInternetSocketAddress *a;
+  int port;
+  
+  if ((port = [[_url port] intValue]) == 0)
+    port = defaultSievePort;
+  
+  a = [NGInternetSocketAddress addressWithPort:port 
+                              onHost:[_url host]];
+  if ((self = [self initWithAddress:a])) {
+    self->login    = [[_url user]     copy];
+    self->password = [[_url password] copy];
+  }
+  return self;
+}
+- (id)initWithURL:(id)_url {
+  if (_url == nil) {
+    [self release];
+    return nil;
+  }
+  
+  if (![_url isKindOfClass:[NSURL class]])
+    _url = [NSURL URLWithString:[_url stringValue]];
+  
+  return [self initWithNSURL:_url];
+}
+
 - (id)initWithHost:(id)_host {
   NGInternetSocketAddress *a;
+  
   a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
   return [self initWithAddress:a];
 }
 
-/**"
- ** designated initializer
-"**/
-
-- (id)initWithAddress:(id<NGSocketAddress>)_address {
+- (id)initWithAddress:(id<NGSocketAddress>)_address { // di
   if ((self = [super init])) {
     self->address = [_address retain];
-    self->debug = debugImap4;
+    self->debug   = debugImap4;
   }
   return self;
 }
 
 - (void)dealloc {
-  [self->text     release];
+  [self->lastException release];
   [self->address  release];
+  [self->io       release];
   [self->socket   release];
   [self->parser   release];
   [self->login    release];
@@ -136,11 +185,26 @@ static BOOL     debugImap4         = NO;
   return self->address;
 }
 
+/* exceptions */
+
+- (void)setLastException:(NSException *)_ex {
+  ASSIGN(self->lastException, _ex);
+}
+- (NSException *)lastException {
+  return self->lastException;
+}
+- (void)resetLastException {
+  [self->lastException release];
+  self->lastException = nil;
+}
+
 /* connection */
 
-/*"
-** Opens a connection to given Host.
-"*/
+- (void)resetStreams {
+  [self->socket release]; self->socket = nil;
+  [self->io     release]; self->io     = nil;
+  [self->parser release]; self->parser = nil;
+}
 
 - (NSDictionary *)openConnection {
   struct timeval tv;
@@ -150,15 +214,17 @@ static BOOL     debugImap4         = NO;
     gettimeofday(&tv, NULL);
     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
   }
-
-  [self->socket release]; self->socket = nil;
-  [self->parser release]; self->parser = nil;
-  [self->text   release]; self->text   = nil;
+  
+  [self resetStreams];
   
   self->socket =
     [[NGActiveSocket socketConnectedToAddress:self->address] retain];
-  self->text   = 
-    [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->socket];
+  if (self->socket == nil) {
+    [self logWithFormat:@"ERROR: could not connect: %@", self->address];
+    return nil;
+  }
+  
+  self->io     = [[NGBufferedStream alloc] initWithSource:self->socket];
   self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
 
   /* receive greeting from server without tag-id */
@@ -173,35 +239,30 @@ static BOOL     debugImap4         = NO;
                [self->parser parseSieveResponse]];
 }
 
-/*"
-** Check whether stream is already open (could be closed because server-timeout)
-"*/
-
 - (NSNumber *)isConnected {
-  return [NSNumber numberWithBool:
-                     (self->socket == nil)
-                     ? NO : [(NGActiveSocket *)self->socket isAlive]];
+  /*
+    Check whether stream is already open (could be closed due to a server
+    timeout)
+  */
+  // TODO: why does that return an object?
+  if (self->socket == nil)
+    return [NSNumber numberWithBool:NO];
+  
+  return [NSNumber numberWithBool:[(NGActiveSocket *)self->socket isAlive]];
 }
 
-/*"
-** Close a consisting connection.
-"*/
-
 - (void)closeConnection {
   [self->socket close];
   [self->socket release]; self->socket = nil;
-  [self->text   release]; self->text   = nil; 
   [self->parser release]; self->parser = nil;
 }
 
-/*"
-** login with plaintext password authenticating
-"*/
-
 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
+  /* login with plaintext password authenticating */
+  
   if ((_login == nil) || (_passwd == nil))
     return nil;
-
+  
   [self->login    release]; self->login    = nil;
   [self->password release]; self->password = nil;
   
@@ -221,13 +282,27 @@ static BOOL     debugImap4         = NO;
   NSData    *auth;
   char      *buf;
   int       bufLen, logLen;
-
-
+  
+  if (![self->socket isConnected]) {
+    id con;
+    
+    if ((con = [self openConnection]) == nil)
+      return nil;
+    if (![[con objectForKey:@"result"] boolValue])
+      return con;
+  }
+  
   logLen = [self->login cStringLength];
-  bufLen = (logLen*2) + [self->password cStringLength] +2;
-
-  buf = calloc(sizeof(char), bufLen);
-
+  bufLen = (logLen * 2) + [self->password cStringLength] +2;
+  
+  buf = calloc(bufLen + 2, sizeof(char));
+  
+  /*
+    Format:
+      authenticate-id
+      authorize-id
+      password
+  */
   sprintf(buf, "%s %s %s", 
           [self->login cString], [self->login cString],
           [self->password cString]);
@@ -238,9 +313,9 @@ static BOOL     debugImap4         = NO;
   auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
   auth = [auth dataByEncodingBase64];
   
-  if (LOG_PASSWORD == 1) {
+  if (LOG_PASSWORD) {
     NSString *s;
-
+    
     s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
                   [auth length], [auth bytes]];
     map = [self processCommand:s];
@@ -253,12 +328,16 @@ static BOOL     debugImap4         = NO;
     map = [self processCommand:s
                 logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
   }
+
+  if (map == nil) {
+    [self logWithFormat:@"ERROR: got no result from command."];
+    return nil;
+  }
+  
   return [self normalizeResponse:map];
 }
 
-/*
-  logout from the connected host and close the connection
-*/
+/* logout from the connected host and close the connection */
 
 - (NSDictionary *)logout {
   NGHashMap *map;
@@ -268,10 +347,56 @@ static BOOL     debugImap4         = NO;
   return [self normalizeResponse:map];
 }
 
-- (NSDictionary *)getScript:(NSString *)_scriptName {
-  // TODO: implement
-  [self notImplemented:_cmd];
-  return nil;
+- (NSString *)getScript:(NSString *)_scriptName {
+  NSException *ex;
+  NSString *script, *s;
+  
+  s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
+  s = [s stringByAppendingString:@"\""];
+  ex = [self sendCommand:s logText:s attempts:3];
+  if (ex != nil) {
+    [self logWithFormat:@"ERROR: could not get script: %@", ex];
+    [self setLastException:ex];
+    return nil;
+  }
+  
+  /* read script string */
+  
+  if ((script = [[self readString] autorelease]) == nil)
+    return nil;
+  
+  if ([script hasPrefix:@"O "] || [script hasPrefix:@"NO "]) {
+    // TODO: not exactly correct, script could begin with this signature
+    // Note: readString read 'NO ...', but the first char is consumed
+    
+    [self logWithFormat:@"ERROR: status line reports: '%@'", script];
+    return nil;
+  }
+  
+  NSLog(@"str: %@", script);
+  
+  /* read response code */
+  
+  if ((s = [self readStringToCRLF]) == nil) {
+    [self logWithFormat:@"ERROR: could not parse status line."];
+    return nil;
+  }
+  if ([s length] == 0) { // remainder of previous string
+    [s release];
+    if ((s = [self readStringToCRLF]) == nil) {
+      [self logWithFormat:@"ERROR: could not parse status line."];
+      return nil;
+    }
+  }
+  
+  if (![s hasPrefix:@"OK"]) {
+    [self logWithFormat:@"ERROR: status line reports: '%@'", s];
+    [s release];
+    return nil;
+  }
+  [s release];
+  
+  return script;
 }
 
 - (BOOL)isValidScriptName:(NSString *)_name {
@@ -321,27 +446,81 @@ static BOOL     debugImap4         = NO;
     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
     return nil;
   }
-
+  
   s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
   map = [self processCommand:s];
   return [self normalizeResponse:map];
 }
-- (NSDictionary *)listScript:(NSString *)_script {
-  [self notImplemented:_cmd];
-  return nil;
-}
 
+- (NSDictionary *)listScripts {
+  NSMutableDictionary *md;
+  NSException *ex;
+  NSString *line;
+  
+  ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
+  if (ex != nil) {
+    [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
+    [self setLastException:ex];
+    return nil;
+  }
+  
+  /* read response */
+  
+  md = [NSMutableDictionary dictionaryWithCapacity:16];
+  while ((line = [self readStringToCRLF]) != nil) {
+    if ([line hasPrefix:@"OK"])
+      break;
+    
+    if ([line hasPrefix:@"NO"]) {
+      md = nil;
+      break;
+    }
+    
+    if ([line hasPrefix:@"{"]) {
+      [self logWithFormat:@"unsupported list response line: '%@'", line];
+    }
+    else if ([line hasPrefix:@"\""]) {
+      NSString *s;
+      NSRange  r;
+      BOOL     isActive;
+      
+      s = [line substringFromIndex:1];
+      r = [s rangeOfString:@"\""];
+      
+      if (r.length == 0) {
+       [self logWithFormat:@"missing closing quote in line: '%@'", line];
+       [line release]; line = nil;
+       continue;
+      }
+      
+      s = [s substringToIndex:r.location];
+      isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
+      
+      [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
+    }
+    else {
+      [self logWithFormat:@"unexpected list response line (%d): '%@'", 
+           [line length], line];
+    }
+    
+    [line release]; line = nil;
+  }
+  
+  [line release]; line = nil;
+  
+  return md;
+}
 
-/*
-** Filter for all responses
-**       result  : NSNumber (response result)
-**       exists  : NSNumber (number of exists mails in selectet folder
-**       recent  : NSNumber (number of recent mails in selectet folder
-**       expunge : NSArray  (message sequence number of expunged mails
-                             in selectet folder)
-*/
 
 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
+  /*
+    Filter for all responses
+       result  : NSNumber (response result)
+       exists  : NSNumber (number of exists mails in selectet folder
+       recent  : NSNumber (number of recent mails in selectet folder
+       expunge : NSArray  (message sequence number of expunged mails
+                          in selectet folder)
+  */
   id keys[3], values[3];
   NSParameterAssert(_map != nil);
   
@@ -352,14 +531,11 @@ static BOOL     debugImap4         = NO;
                               forKeys:keys count:2];
 }
 
-/*
-** filter for open connection
-*/
-
 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
+  /* filter for open connection */
   NSMutableDictionary *result;
   NSString *tmp;
-
+  
   result = [self normalizeResponse:_map];
   
   if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
@@ -372,18 +548,6 @@ static BOOL     debugImap4         = NO;
   return result;
 }
 
-
-/*
-** filter for list
-**       list : NSDictionary (folder name as key and flags as value)
-*/
-
-- (NSString *)description {
-  return [NSString stringWithFormat:@"<Imap4Client[0x%08X]: socket=%@>",
-                     (unsigned)self, [self socket]];
-}
-
 /* Private Methods */
 
 - (BOOL)handleProcessException:(NSException *)_exception
@@ -421,13 +585,13 @@ static BOOL     debugImap4         = NO;
   [self logWithFormat:@"reconnect ..."];
 }
 
-- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
+- (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
   NGHashMap *map          = nil;
   BOOL      repeatCommand = NO;
   int       repeatCnt     = 0;
   struct timeval tv;
   double         ti = 0.0;
-
+  
   if (ProfileImapEnabled) {
     gettimeofday(&tv, NULL);
     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
@@ -444,8 +608,14 @@ static BOOL     debugImap4         = NO;
     }
     
     NS_DURING {
-      [self sendCommand:_command logText:_txt];
-      map = [self->parser parseSieveResponse];
+      NSException *ex;
+      
+      if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
+       repeatCommand = [self handleProcessException:ex 
+                             repetitionCount:repeatCnt];
+      }
+      else
+       map = [self->parser parseSieveResponse];
     }
     NS_HANDLER {
       repeatCommand = [self handleProcessException:localException
@@ -465,26 +635,234 @@ static BOOL     debugImap4         = NO;
   return map;
 }
 
-- (NGHashMap *)processCommand:(NSString *)_command {
+- (NGHashMap *)processCommand:(id)_command {
   return [self processCommand:_command logText:_command];
 }
 
-- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt {
-  // TODO: should accept 'NSData' commands
+- (NSException *)sendCommand:(id)_command logText:(id)_txt {
   NSString *command = nil;
+  
+  if ((command = _command) == nil) /* missing command */
+    return nil; // TODO: return exception?
+  
+  /* log */
 
-  command = _command;
+  if (self->debug) {
+    if ([_txt isKindOfClass:[NSData class]]) {
+      fprintf(stderr, "C: ");
+      fwrite([_txt bytes], [_txt length], 1, stderr);
+      fputc('\n', stderr);
+    }
+    else
+      fprintf(stderr, "C: %s\n", [_txt cString]);
+  }
 
-  if (self->debug)
-    fprintf(stderr, "C: %s\n", [_txt cString]);
+  /* write */
+  
+  if (![_command isKindOfClass:[NSData class]])
+    _command = [command dataUsingEncoding:NSUTF8StringEncoding];
   
-  [self->text writeString:command];
-  [self->text writeString:@"\r\n"];
-  [self->text flush];
+  if (![self->io safeWriteData:_command])
+    return [self->io lastException];
+  if (![self->io writeBytes:"\r\n" count:2])
+    return [self->io lastException];
+  if (![self->io flush])
+    return [self->io lastException];
+  
+  return nil;
+}
+
+- (NSException *)sendCommand:(id)_command {
+  return [self sendCommand:_command logText:_command];
 }
 
-- (void)sendCommand:(NSString *)_command {
-  [self sendCommand:_command logText:_command];
+- (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
+  NSException *ex;
+  BOOL tryAgain;
+  int  repeatCnt;
+  
+  for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
+    if (repeatCnt > 0) {
+      if (repeatCnt > 1) /* one repeat goes without delay */
+        [self waitPriorReconnectWithRepetitionCount:repeatCnt];
+      [self reconnect];
+      tryAgain = NO;
+    }
+    
+    NS_DURING
+      ex = [self sendCommand:_command logText:_txt];
+    NS_HANDLER
+      ex = [localException retain];
+    NS_ENDHANDLER;
+    
+    if (ex == nil) /* everything is fine */
+      break;
+    
+    if (repeatCnt > _c) /* reached max attempts */
+      break;
+    
+    /* try again for certain exceptions */
+    tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
+  }
+  
+  return ex;
+}
+
+/* low level */
+
+- (int)readByte {
+  unsigned char c;
+  
+  if (![self->io readBytes:&c count:1]) {
+    [self setLastException:[self->io lastException]];
+    return -1;
+  }
+  return c;
+}
+
+- (NSString *)readLiteral {
+  /* 
+     Assumes 1st char is consumed, returns a retained string.
+     
+     Parses: "{" number [ "+" ] "}" CRLF *OCTET
+  */
+  unsigned char countBuf[16];
+  int      i;
+  unsigned byteCount;
+  unsigned char *octets;
+  
+  /* read count */
+  
+  for (i = 0; i < 14; i++) {
+    int c;
+    
+    if ((c = [self readByte]) == -1)
+      return nil;
+    if (c == '}')
+      break;
+    
+    countBuf[i] = c;
+  }
+  countBuf[i] = '\0';
+  byteCount = i > 0 ? atoi(countBuf) : 0;
+  
+  /* read CRLF */
+  
+  i = [self readByte];
+  if (i != '\n') {
+    if (i == '\r' && i != -1)
+      i = [self readByte];
+    if (i == -1)
+      return nil;
+  }
+  
+  /* read octet */
+  
+  if (byteCount == 0)
+    return @"";
+  
+  octets = malloc(byteCount + 4);
+  if (![self->io safeReadBytes:octets count:byteCount]) {
+    [self setLastException:[self->io lastException]];
+    return nil;
+  }
+  octets[byteCount] = '\0';
+  
+  return [[NSString alloc] initWithUTF8String:octets];
+}
+
+- (NSString *)readQuoted {
+  /* 
+     assumes 1st char is consumed, returns a retained string
+
+     Note: quoted strings are limited to 1KB!
+  */
+  unsigned char buf[1032];
+  int i, c;
+  
+  i = 0;
+  do {
+    c      = [self readByte];
+    buf[i] = c;
+    i++;
+  }
+  while ((c != -1) && (c != '"'));
+  buf[i] = '\0';
+  
+  if (c == -1)
+    return nil;
+  
+  return [[NSString alloc] initWithUTF8String:buf];
+}
+
+- (NSString *)readStringToCRLF {
+  unsigned char buf[1032];
+  int i, c;
+  
+  i = 0;
+  do {
+    c = [self readByte];
+    if (c == '\n' || c == '\r')
+      break;
+    
+    buf[i] = c;
+    i++;
+  }
+  while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
+  buf[i] = '\0';
+  
+  if (c == -1)
+    return nil;
+  
+  /* consume CRLF */
+  if (c == '\r') {
+    if ((c = [self readByte]) != '\n') {
+      if (c == -1)
+       return nil;
+      [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
+             __PRETTY_FUNCTION__, c];
+      return nil;
+    }
+  }
+  
+  return [[NSString alloc] initWithUTF8String:buf];
+}
+
+- (NSString *)readString {
+  /* Note: returns a retained string */
+  int c1;
+  
+  if ((c1 = [self readByte]) == -1)
+    return nil;
+  
+  if (c1 == '"')
+    return [self readQuoted];
+  if (c1 == '{')
+    return [self readLiteral];
+
+  // Note: this does not return the first char!
+  return [self readStringToCRLF];
+}
+
+- (NSString *)readSieveName {
+  return [self readString];
+}
+
+/* description */
+
+- (NSString *)description {
+  NSMutableString *ms;
+
+  ms = [NSMutableString stringWithCapacity:128];
+  [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
+  
+  if (self->socket != nil)
+    [ms appendFormat:@" socket=%@", [self socket]];
+  else
+    [ms appendFormat:@" address=%@", self->address];
+
+  [ms appendString:@">"];
+  return ms;
 }
 
 @end /* NGSieveClient */