]> err.no Git - sope/blob - sope-mime/NGImap4/NGSieveClient.m
new Xcode projects
[sope] / sope-mime / NGImap4 / NGSieveClient.m
1 /*
2   Copyright (C) 2000-2004 SKYRIX Software AG
3
4   This file is part of OGo
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21 // $Id$
22
23 #include <unistd.h>
24
25 #include "NGSieveClient.h"
26 #include "NGImap4Support.h"
27 #include "NGImap4ResponseParser.h"
28 #include "NSString+Imap4.h"
29 #include "imCommon.h"
30 #include <sys/time.h>
31
32 @interface NGSieveClient(Private)
33
34 - (NGHashMap *)processCommand:(NSString *)_command;
35 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;
36
37 - (void)sendCommand:(NSString *)_command;
38 - (void)sendCommand:(NSString *)_command logText:(NSString *)_txt;
39
40 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
41 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
42 - (NSDictionary *)login;
43
44 @end
45
46 /*"
47 **  An implementation of an Imap4 client
48 **
49 ** A folder name always looks like an absolute filename (/inbox/doof) 
50 **
51 "*/
52
53 @implementation NGSieveClient
54
55 static int      defaultSievePort = 143;
56 static NSNumber *YesNumber = nil;
57 static NSNumber *NoNumber  = nil;
58 static BOOL     ProfileImapEnabled = NO;
59 static BOOL     LOG_PASSWORD       = NO;
60 static BOOL     debugImap4         = NO;
61
62 + (void)initialize {
63   static BOOL didInit = NO;
64   NSUserDefaults *ud;
65   if (didInit) return;
66   didInit = YES;
67   
68   ud = [NSUserDefaults standardUserDefaults];
69   LOG_PASSWORD       = [ud boolForKey:@"SieveLogPassword"];
70   ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"];
71   debugImap4         = [ud boolForKey:@"ImapDebugEnabled"];
72   
73   YesNumber = [[NSNumber numberWithBool:YES] retain];
74   NoNumber  = [[NSNumber numberWithBool:NO] retain];
75 }
76
77 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
78   return
79     [[(NGSieveClient *)[self alloc] initWithAddress:_address] autorelease];
80 }
81
82 + (id)clientWithHost:(id)_host {
83   return [[[self alloc] initWithHost:_host] autorelease];
84 }
85
86 - (id)initWithHost:(id)_host {
87   NGInternetSocketAddress *a;
88   a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
89   return [self initWithAddress:a];
90 }
91
92 /**"
93  ** designated initializer
94 "**/
95
96 - (id)initWithAddress:(id<NGSocketAddress>)_address {
97   if ((self = [super init])) {
98     self->address = [_address retain];
99     self->debug = debugImap4;
100   }
101   return self;
102 }
103
104 - (void)dealloc {
105   [self->text     release];
106   [self->address  release];
107   [self->socket   release];
108   [self->parser   release];
109   [self->login    release];
110   [self->password release];
111   [super dealloc];
112 }
113
114 /* equality */
115
116 - (BOOL)isEqual:(id)_obj {
117   if (_obj == self)
118     return YES;
119   if ([_obj isKindOfClass:[NGSieveClient class]])
120     return [self isEqualToSieveClient:_obj];
121   return NO;
122 }
123
124 - (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj {
125   if (_obj == self) return YES;
126   if (_obj == nil)  return NO;
127   return [[_obj address] isEqual:self->address];
128 }
129
130 /* accessors */
131
132 - (id<NGActiveSocket>)socket {
133   return self->socket;
134 }
135
136 - (id<NGSocketAddress>)address {
137   return self->address;
138 }
139
140 /* connection */
141
142 /*"
143 ** Opens a connection to given Host.
144 "*/
145
146 - (NSDictionary *)openConnection {
147   struct timeval tv;
148   double         ti = 0.0;
149   
150   if (ProfileImapEnabled) {
151     gettimeofday(&tv, NULL);
152     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
153   }
154
155   [self->socket release]; self->socket = nil;
156   [self->parser release]; self->parser = nil;
157   [self->text   release]; self->text   = nil;
158   
159   self->socket =
160     [[NGActiveSocket socketConnectedToAddress:self->address] retain];
161   self->text   = 
162     [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->socket];
163   self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
164
165   /* receive greeting from server without tag-id */
166
167   if (ProfileImapEnabled) {
168     gettimeofday(&tv, NULL);
169     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
170     fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
171            __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
172   }
173   return [self normalizeOpenConnectionResponse:
174                [self->parser parseSieveResponse]];
175 }
176
177 /*"
178 ** Check whether stream is already open (could be closed because server-timeout)
179 "*/
180
181 - (NSNumber *)isConnected {
182   return [NSNumber numberWithBool:
183                      (self->socket == nil)
184                      ? NO : [(NGActiveSocket *)self->socket isAlive]];
185 }
186
187 /*"
188 ** Close a consisting connection.
189 "*/
190
191 - (void)closeConnection {
192   [self->socket close];
193   [self->socket release]; self->socket = nil;
194   [self->text   release]; self->text   = nil; 
195   [self->parser release]; self->parser = nil;
196 }
197
198 /*"
199 ** login with plaintext password authenticating
200 "*/
201
202 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
203   if ((_login == nil) || (_passwd == nil))
204     return nil;
205
206   [self->login    release]; self->login    = nil;
207   [self->password release]; self->password = nil;
208   
209   self->login    = [_login  copy];
210   self->password = [_passwd copy];
211   return [self login];
212 }
213
214 - (void)reconnect {
215   [self closeConnection];  
216   [self openConnection];
217   [self login];
218 }
219
220 - (NSDictionary *)login {
221   NGHashMap *map  = nil;
222   NSData    *auth;
223   char      *buf;
224   int       bufLen, logLen;
225
226
227   logLen = [self->login cStringLength];
228   bufLen = (logLen*2) + [self->password cStringLength] +2;
229
230   buf = calloc(sizeof(char), bufLen);
231
232   sprintf(buf, "%s %s %s", 
233           [self->login cString], [self->login cString],
234           [self->password cString]);
235   
236   buf[logLen] = '\0';
237   buf[logLen*2+1] = '\0';
238   
239   auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
240   auth = [auth dataByEncodingBase64];
241
242   if (LOG_PASSWORD == 1) {
243     map = [self processCommand:[NSString stringWithFormat:
244                                          @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
245                                          [auth length], [auth bytes]]];
246   }
247   else {
248     map = [self processCommand:[NSString stringWithFormat:
249                                          @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
250                                          [auth length], [auth bytes]]
251                 logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
252   }
253   return [self normalizeResponse:map];
254 }
255
256 /*
257 ** logout from the connected host and close the connection
258 */
259
260 - (NSDictionary *)logout {
261   NGHashMap *map = [self processCommand:@"logout"];
262
263   [self closeConnection];
264   return [self normalizeResponse:map];
265 }
266
267 - (NSDictionary *)getScript:(NSString *)_scriptName {
268   [self notImplemented:_cmd];
269   return nil;
270 }
271
272 - (BOOL)isValidScriptName:(NSString *)_name {
273   return ([_name length] == 0) ? NO : YES;
274 }
275
276 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
277   NGHashMap *map;
278   NSString  *s;
279   
280   if (![self isValidScriptName:_name]) {
281     [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
282     return nil;
283   }
284   if ([_script length] == 0) {
285     [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
286     return nil;
287   }
288   
289   s = [NSString stringWithFormat:
290                   @"PUTSCRIPT \"%@\" {%d+}\r\n%@\r\n",
291                   _name, [_script length], _script];
292   map = [self processCommand:s];
293   return [self normalizeResponse:map];
294 }
295
296 - (NSDictionary *)setActiveScript:(NSString *)_name {
297   NGHashMap *map;
298   
299   if (![self isValidScriptName:_name]) {
300     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
301     return nil;
302   }
303   map = [self processCommand:
304               [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
305   return [self normalizeResponse:map];
306 }
307
308 - (NSDictionary *)deleteScript:(NSString *)_name {
309   NGHashMap *map;
310   NSString  *s;
311
312   if (![self isValidScriptName:_name]) {
313     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
314     return nil;
315   }
316
317   s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
318   map = [self processCommand:s];
319   return [self normalizeResponse:map];
320 }
321 - (NSDictionary *)listScript:(NSString *)_script {
322   [self notImplemented:_cmd];
323   return nil;
324 }
325
326
327 /*
328 ** Filter for all responses
329 **       result  : NSNumber (response result)
330 **       exists  : NSNumber (number of exists mails in selectet folder
331 **       recent  : NSNumber (number of recent mails in selectet folder
332 **       expunge : NSArray  (message sequence number of expunged mails
333                              in selectet folder)
334 */
335
336 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
337   id keys[3], values[3];
338   NSParameterAssert(_map != nil);
339   
340   keys[0] = @"RawResponse"; values[0] = _map;
341   keys[1] = @"result";
342   values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
343   return [NSMutableDictionary dictionaryWithObjects:values
344                               forKeys:keys count:2];
345 }
346
347 /*
348 ** filter for open connection
349 */
350
351 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
352   NSMutableDictionary *result;
353   NSString *tmp;
354
355   result = [self normalizeResponse:_map];
356   
357   if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
358     return result;
359
360   if ((tmp = [_map objectForKey:@"implementation"]))
361     [result setObject:tmp forKey:@"server"];
362   if ((tmp = [_map objectForKey:@"sieve"]))
363     [result setObject:tmp forKey:@"capabilities"];
364   return result;
365 }
366
367
368  
369 /*
370 ** filter for list
371 **       list : NSDictionary (folder name as key and flags as value)
372 */
373
374 - (NSString *)description {
375   return [NSString stringWithFormat:@"<Imap4Client[0x%08X]: socket=%@>",
376                      (unsigned)self, [self socket]];
377 }
378
379 /* Private Methods */
380
381 - (BOOL)handleProcessException:(NSException *)_exception
382   repetitionCount:(int)_cnt
383 {
384   if (_cnt > 3) {
385     [_exception raise];
386     return NO;
387   }
388   
389   if ([_exception isKindOfClass:[NGIOException class]]) {
390     [self logWithFormat:
391             @"WARNING: got exception try to restore connection: %@", 
392             _exception];
393     return YES;
394   }
395   if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
396     [self logWithFormat:
397             @"WARNING: Got Parser-Exception try to restore connection: %@",
398             _exception];
399     return YES;
400   }
401   
402   [_exception raise];
403   return NO;
404 }
405
406 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
407   unsigned timeout;
408   
409   timeout = _cnt * 4;
410   [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
411           self->address, timeout];
412   sleep(timeout);
413   [self logWithFormat:@"reconnect ..."];
414 }
415
416 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
417   NGHashMap *map          = nil;
418   BOOL      repeatCommand = NO;
419   int       repeatCnt     = 0;
420
421   struct timeval tv;
422   double         ti = 0.0;
423
424   if (ProfileImapEnabled) {
425     gettimeofday(&tv, NULL);
426     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
427     fprintf(stderr, "{");
428   }
429   do { /* TODO: shouldn't that be a while loop? */
430     if (repeatCommand) {
431       if (repeatCnt > 1)
432         [self waitPriorReconnectWithRepetitionCount:repeatCnt];
433       
434       repeatCnt++;
435       [self reconnect];
436       repeatCommand = NO;
437     }
438     
439     NS_DURING {
440       [self sendCommand:_command logText:_txt];
441       map = [self->parser parseSieveResponse];
442     }
443     NS_HANDLER {
444       repeatCommand = [self handleProcessException:localException
445                             repetitionCount:repeatCnt];
446     }
447     NS_ENDHANDLER;    
448   }
449   while (repeatCommand);
450   
451   if (ProfileImapEnabled) {
452     gettimeofday(&tv, NULL);
453     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
454     fprintf(stderr, "}[%s] <Send Command> : time needed: %4.4fs\n",
455            __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
456   }
457   
458   return map;
459 }
460
461 - (NGHashMap *)processCommand:(NSString *)_command {
462   return [self processCommand:_command logText:_command];
463 }
464
465 - (void)sendCommand:(NSString *)_command logText:(NSString *)_txt {
466   NSString *command       = nil;
467
468   command = _command;
469
470   if (self->debug)
471     fprintf(stderr, "C: %s\n", [_txt cString]);
472   
473   [self->text writeString:command];
474   [self->text writeString:@"\r\n"];
475   [self->text flush];
476 }
477
478 - (void)sendCommand:(NSString *)_command {
479   [self sendCommand:_command logText:_command];
480 }
481
482 @end /* NGSieveClient */