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