2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
24 #include "NGSieveClient.h"
25 #include "NGImap4Support.h"
26 #include "NGImap4ResponseParser.h"
27 #include "NSString+Imap4.h"
31 @interface NGSieveClient(Private)
33 - (NGHashMap *)processCommand:(id)_command;
34 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt;
36 - (NSException *)sendCommand:(id)_command;
37 - (NSException *)sendCommand:(id)_command logText:(id)_txt;
38 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c;
40 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
41 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
42 - (NSDictionary *)login;
46 - (NSString *)readStringToCRLF;
47 - (NSString *)readString;
52 An implementation of an Imap4 client
54 A folder name always looks like an absolute filename (/inbox/blah)
56 NOTE: Sieve is just the filtering language ...
59 http://asg.web.cmu.edu/rfc/rfc2244.html
62 "IMPLEMENTATION" "Cyrus timsieved v2.1.15-IPv6-Debian-2.1.15-0woody.1.0"
64 "SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex"
70 @implementation NGSieveClient
72 static int defaultSievePort = 2000;
73 static NSNumber *YesNumber = nil;
74 static NSNumber *NoNumber = nil;
75 static BOOL ProfileImapEnabled = NO;
76 static BOOL LOG_PASSWORD = NO;
77 static BOOL debugImap4 = NO;
80 static BOOL didInit = NO;
85 ud = [NSUserDefaults standardUserDefaults];
86 LOG_PASSWORD = [ud boolForKey:@"SieveLogPassword"];
87 ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"];
88 debugImap4 = [ud boolForKey:@"ImapDebugEnabled"];
90 YesNumber = [[NSNumber numberWithBool:YES] retain];
91 NoNumber = [[NSNumber numberWithBool:NO] retain];
94 + (id)clientWithURL:(id)_url {
95 return [[[self alloc] initWithURL:_url] autorelease];
98 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
99 NGSieveClient *client;
101 client = [self alloc];
102 return [[client initWithAddress:_address] autorelease];
105 + (id)clientWithHost:(id)_host {
106 return [[[self alloc] initWithHost:_host] autorelease];
109 - (id)initWithNSURL:(NSURL *)_url {
110 NGInternetSocketAddress *a;
113 if ((port = [[_url port] intValue]) == 0)
114 port = defaultSievePort;
116 a = [NGInternetSocketAddress addressWithPort:port
118 if ((self = [self initWithAddress:a])) {
119 self->login = [[_url user] copy];
120 self->password = [[_url password] copy];
124 - (id)initWithURL:(id)_url {
130 if (![_url isKindOfClass:[NSURL class]])
131 _url = [NSURL URLWithString:[_url stringValue]];
133 return [self initWithNSURL:_url];
136 - (id)initWithHost:(id)_host {
137 NGInternetSocketAddress *a;
139 a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
140 return [self initWithAddress:a];
143 - (id)initWithAddress:(id<NGSocketAddress>)_address { // di
144 if ((self = [super init])) {
145 self->address = [_address retain];
146 self->debug = debugImap4;
152 [self->lastException release];
153 [self->address release];
155 [self->socket release];
156 [self->parser release];
157 [self->login release];
158 [self->password release];
164 - (BOOL)isEqual:(id)_obj {
167 if ([_obj isKindOfClass:[NGSieveClient class]])
168 return [self isEqualToSieveClient:_obj];
172 - (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj {
173 if (_obj == self) return YES;
174 if (_obj == nil) return NO;
175 return [[_obj address] isEqual:self->address];
180 - (id<NGActiveSocket>)socket {
184 - (id<NGSocketAddress>)address {
185 return self->address;
190 - (void)setLastException:(NSException *)_ex {
191 ASSIGN(self->lastException, _ex);
193 - (NSException *)lastException {
194 return self->lastException;
196 - (void)resetLastException {
197 [self->lastException release];
198 self->lastException = nil;
203 - (void)resetStreams {
204 [self->socket release]; self->socket = nil;
205 [self->io release]; self->io = nil;
206 [self->parser release]; self->parser = nil;
209 - (NSDictionary *)openConnection {
213 if (ProfileImapEnabled) {
214 gettimeofday(&tv, NULL);
215 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
221 [[NGActiveSocket socketConnectedToAddress:self->address] retain];
222 if (self->socket == nil) {
223 [self logWithFormat:@"ERROR: could not connect: %@", self->address];
227 self->io = [[NGBufferedStream alloc] initWithSource:self->socket];
228 self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
230 /* receive greeting from server without tag-id */
232 if (ProfileImapEnabled) {
233 gettimeofday(&tv, NULL);
234 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
235 fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
236 __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);
238 return [self normalizeOpenConnectionResponse:
239 [self->parser parseSieveResponse]];
242 - (NSNumber *)isConnected {
244 Check whether stream is already open (could be closed due to a server
247 // TODO: why does that return an object?
248 if (self->socket == nil)
249 return [NSNumber numberWithBool:NO];
251 return [NSNumber numberWithBool:[(NGActiveSocket *)self->socket isAlive]];
254 - (void)closeConnection {
255 [self->socket close];
256 [self->socket release]; self->socket = nil;
257 [self->parser release]; self->parser = nil;
260 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
261 /* login with plaintext password authenticating */
263 if ((_login == nil) || (_passwd == nil))
266 [self->login release]; self->login = nil;
267 [self->password release]; self->password = nil;
269 self->login = [_login copy];
270 self->password = [_passwd copy];
275 [self closeConnection];
276 [self openConnection];
280 - (NSDictionary *)login {
281 NGHashMap *map = nil;
286 if (![self->socket isConnected]) {
289 if ((con = [self openConnection]) == nil)
291 if (![[con objectForKey:@"result"] boolValue])
295 logLen = [self->login cStringLength];
296 bufLen = (logLen * 2) + [self->password cStringLength] +2;
298 buf = calloc(bufLen + 2, sizeof(char));
300 sprintf(buf, "%s %s %s",
301 [self->login cString], [self->login cString],
302 [self->password cString]);
305 buf[logLen * 2 + 1] = '\0';
307 auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
308 auth = [auth dataByEncodingBase64];
310 if (LOG_PASSWORD == 1) {
313 s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
314 [auth length], [auth bytes]];
315 map = [self processCommand:s];
320 s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
321 [auth length], [auth bytes]];
322 map = [self processCommand:s
323 logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
327 [self logWithFormat:@"ERROR: got no result from command."];
331 return [self normalizeResponse:map];
334 /* logout from the connected host and close the connection */
336 - (NSDictionary *)logout {
339 map = [self processCommand:@"logout"]; // TODO: check for success!
340 [self closeConnection];
341 return [self normalizeResponse:map];
344 - (NSString *)getScript:(NSString *)_scriptName {
346 NSString *script, *s;
348 s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
349 s = [s stringByAppendingString:@"\""];
350 ex = [self sendCommand:s logText:s attempts:3];
352 [self logWithFormat:@"ERROR: could not get script: %@", ex];
353 [self setLastException:ex];
357 /* read script string */
359 if ((script = [[self readString] autorelease]) == nil)
362 /* read response code */
364 if ((s = [self readStringToCRLF]) == nil) {
365 [self logWithFormat:@"ERROR: could not parse status line."];
368 if ([s length] == 0) { // remainder of previous string
370 if ((s = [self readStringToCRLF]) == nil) {
371 [self logWithFormat:@"ERROR: could not parse status line."];
376 if (![s hasPrefix:@"OK"]) {
377 [self logWithFormat:@"ERROR: status line reports: '%@'", s];
386 - (BOOL)isValidScriptName:(NSString *)_name {
387 return ([_name length] == 0) ? NO : YES;
390 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
391 // TODO: script should be send in UTF-8!
395 if (![self isValidScriptName:_name]) {
396 [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
399 if ([_script length] == 0) {
400 [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
405 s = [s stringByAppendingString:_name];
406 s = [s stringByAppendingString:@"\" {"];
407 s = [s stringByAppendingFormat:@"{%d+}\r\n%@", [_script length], _script];
408 s = [s stringByAppendingString:@"\r\n"];
409 map = [self processCommand:s];
410 return [self normalizeResponse:map];
413 - (NSDictionary *)setActiveScript:(NSString *)_name {
416 if (![self isValidScriptName:_name]) {
417 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
420 map = [self processCommand:
421 [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
422 return [self normalizeResponse:map];
425 - (NSDictionary *)deleteScript:(NSString *)_name {
429 if (![self isValidScriptName:_name]) {
430 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
434 s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
435 map = [self processCommand:s];
436 return [self normalizeResponse:map];
439 - (NSDictionary *)listScripts {
440 NSMutableDictionary *md;
444 ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
446 [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
447 [self setLastException:ex];
453 md = [NSMutableDictionary dictionaryWithCapacity:16];
454 while ((line = [self readStringToCRLF]) != nil) {
455 if ([line hasPrefix:@"OK"])
458 if ([line hasPrefix:@"NO"]) {
463 if ([line hasPrefix:@"{"]) {
464 [self logWithFormat:@"unsupported list response line: '%@'", line];
466 else if ([line hasPrefix:@"\""]) {
471 s = [line substringFromIndex:1];
472 r = [s rangeOfString:@"\""];
475 [self logWithFormat:@"missing closing quote in line: '%@'", line];
476 [line release]; line = nil;
480 s = [s substringToIndex:r.location];
481 isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
483 [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
486 [self logWithFormat:@"unexpected list response line (%d): '%@'",
487 [line length], line];
490 [line release]; line = nil;
493 [line release]; line = nil;
499 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
501 Filter for all responses
502 result : NSNumber (response result)
503 exists : NSNumber (number of exists mails in selectet folder
504 recent : NSNumber (number of recent mails in selectet folder
505 expunge : NSArray (message sequence number of expunged mails
508 id keys[3], values[3];
509 NSParameterAssert(_map != nil);
511 keys[0] = @"RawResponse"; values[0] = _map;
513 values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
514 return [NSMutableDictionary dictionaryWithObjects:values
515 forKeys:keys count:2];
518 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
519 /* filter for open connection */
520 NSMutableDictionary *result;
523 result = [self normalizeResponse:_map];
525 if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
528 if ((tmp = [_map objectForKey:@"implementation"]))
529 [result setObject:tmp forKey:@"server"];
530 if ((tmp = [_map objectForKey:@"sieve"]))
531 [result setObject:tmp forKey:@"capabilities"];
535 /* Private Methods */
537 - (BOOL)handleProcessException:(NSException *)_exception
538 repetitionCount:(int)_cnt
545 if ([_exception isKindOfClass:[NGIOException class]]) {
547 @"WARNING: got exception try to restore connection: %@",
551 if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
553 @"WARNING: Got Parser-Exception try to restore connection: %@",
562 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
566 [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
567 self->address, timeout];
569 [self logWithFormat:@"reconnect ..."];
572 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
573 NGHashMap *map = nil;
574 BOOL repeatCommand = NO;
579 if (ProfileImapEnabled) {
580 gettimeofday(&tv, NULL);
581 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
582 fprintf(stderr, "{");
584 do { /* TODO: shouldn't that be a while loop? */
587 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
597 if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
598 repeatCommand = [self handleProcessException:ex
599 repetitionCount:repeatCnt];
602 map = [self->parser parseSieveResponse];
605 repeatCommand = [self handleProcessException:localException
606 repetitionCount:repeatCnt];
610 while (repeatCommand);
612 if (ProfileImapEnabled) {
613 gettimeofday(&tv, NULL);
614 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
615 fprintf(stderr, "}[%s] <Send Command> : time needed: %4.4fs\n",
616 __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);
622 - (NGHashMap *)processCommand:(id)_command {
623 return [self processCommand:_command logText:_command];
626 - (NSException *)sendCommand:(id)_command logText:(id)_txt {
627 NSString *command = nil;
629 if ((command = _command) == nil) /* missing command */
630 return nil; // TODO: return exception?
635 if ([_txt isKindOfClass:[NSData class]]) {
636 fprintf(stderr, "C: ");
637 fwrite([_txt bytes], [_txt length], 1, stderr);
641 fprintf(stderr, "C: %s\n", [_txt cString]);
646 if (![_command isKindOfClass:[NSData class]])
647 _command = [command dataUsingEncoding:NSUTF8StringEncoding];
649 if (![self->io safeWriteData:_command])
650 return [self->io lastException];
651 if (![self->io writeBytes:"\r\n" count:2])
652 return [self->io lastException];
653 if (![self->io flush])
654 return [self->io lastException];
659 - (NSException *)sendCommand:(id)_command {
660 return [self sendCommand:_command logText:_command];
663 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
668 for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
670 if (repeatCnt > 1) /* one repeat goes without delay */
671 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
677 ex = [self sendCommand:_command logText:_txt];
679 ex = [localException retain];
682 if (ex == nil) /* everything is fine */
685 if (repeatCnt > _c) /* reached max attempts */
688 /* try again for certain exceptions */
689 tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
700 if (![self->io readBytes:&c count:1]) {
701 [self setLastException:[self->io lastException]];
707 - (NSString *)readLiteral {
709 Assumes 1st char is consumed, returns a retained string.
711 Parses: "{" number [ "+" ] "}" CRLF *OCTET
713 unsigned char countBuf[16];
716 unsigned char *octets;
720 for (i = 0; i < 14; i++) {
723 if ((c = [self readByte]) == -1)
731 byteCount = i > 0 ? atoi(countBuf) : 0;
737 if (i == '\r' && i != -1)
748 octets = malloc(byteCount + 4);
749 if (![self->io safeReadBytes:octets count:byteCount]) {
750 [self setLastException:[self->io lastException]];
753 octets[byteCount] = '\0';
755 return [[NSString alloc] initWithUTF8String:octets];
758 - (NSString *)readQuoted {
760 assumes 1st char is consumed, returns a retained string
762 Note: quoted strings are limited to 1KB!
764 unsigned char buf[1032];
773 while ((c != -1) && (c != '"'));
779 return [[NSString alloc] initWithUTF8String:buf];
782 - (NSString *)readStringToCRLF {
783 unsigned char buf[1032];
789 if (c == '\n' || c == '\r')
795 while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
803 if ((c = [self readByte]) != '\n') {
806 [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
807 __PRETTY_FUNCTION__, c];
812 return [[NSString alloc] initWithUTF8String:buf];
815 - (NSString *)readString {
816 /* Note: returns a retained string */
819 if ((c1 = [self readByte]) == -1)
823 return [self readQuoted];
825 return [self readLiteral];
827 return [self readStringToCRLF];
830 - (NSString *)readSieveName {
831 return [self readString];
836 - (NSString *)description {
839 ms = [NSMutableString stringWithCapacity:128];
840 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
842 if (self->socket != nil)
843 [ms appendFormat:@" socket=%@", [self socket]];
845 [ms appendFormat:@" address=%@", self->address];
847 [ms appendString:@">"];
851 @end /* NGSieveClient */