]> err.no Git - sope/blob - sope-mime/NGMail/NGPop3Client.m
minor code cleanups in message generator
[sope] / sope-mime / NGMail / NGPop3Client.m
1 /*
2   Copyright (C) 2000-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
6   SOPE 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   SOPE 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 SOPE; 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 "NGPop3Client.h"
23 #include "NGPop3Support.h"
24 #include "NGMimeMessageParser.h"
25 #include "NGMimeMessage.h"
26 #include "common.h"
27
28 @implementation NGPop3Client
29
30 + (int)version {
31   return 2;
32 }
33
34 + (id)pop3Client {
35   NGActiveSocket *s;
36   
37   s = [NGActiveSocket socketInDomain:[NGInternetSocketDomain domain]];
38   return [[[self alloc] initWithSocket:s] autorelease];
39 }
40
41 - (id)init {
42   NSLog(@"%@: init not supported, use initWithSocket: ..", self);
43   [self release];
44   return nil;
45 }
46
47 - (id)initWithSocket:(id<NGActiveSocket>)_socket {
48   if ((self = [super init])) {
49     self->socket = [_socket retain];
50     NSAssert(self->socket, @"invalid socket parameter");
51     
52     self->connection = 
53       [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:_socket];
54     self->text = 
55       [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->connection];
56     
57     self->state = [self->socket isConnected]
58       ? NGPop3State_AUTHORIZATION
59       : NGPop3State_unconnected;
60   }
61   return self;
62 }
63
64 - (void)dealloc {
65   [self->text         release];
66   [self->connection   release];
67   [self->socket       release];
68   [self->lastResponse release];
69   [super dealloc];
70 }
71
72 /* accessors */
73
74 - (id<NGActiveSocket>)socket {
75   return self->socket;
76 }
77
78 - (NGPop3State)state {
79   return self->state;
80 }
81
82 - (NGPop3Response *)lastResponse {
83   return self->lastResponse;
84 }
85
86 - (void)setDebuggingEnabled:(BOOL)_flag {
87   self->isDebuggingEnabled = _flag;
88 }
89 - (BOOL)isDebuggingEnabled {
90   return self->isDebuggingEnabled;
91 }
92
93 /* connection */
94
95 - (BOOL)connectToAddress:(id<NGSocketAddress>)_address {
96   NSString *greeting = nil;
97
98   [self requireState:NGPop3State_unconnected];
99   
100   [self->socket connectToAddress:_address];
101
102   // receive greeting from server
103   greeting = [self->text readLineAsString];
104   if (self->isDebuggingEnabled)
105     [NGTextErr writeFormat:@"S: %@\n", greeting];
106
107   // is it a welcome ?
108   if (![greeting hasPrefix:@"+OK"])
109     return NO;
110
111   // we are welcome, need to authorize
112   [self gotoState:NGPop3State_AUTHORIZATION];
113
114   return YES;
115 }
116 - (BOOL)connectToHost:(id)_host {
117   return [self connectToAddress:[NGInternetSocketAddress addressWithService:@"pop3"
118                                                          onHost:_host
119                                                          protocol:@"tcp"]];
120 }
121
122 - (void)disconnect {
123   [text   flush];
124   [socket close];
125   [self   gotoState:NGPop3State_unconnected];
126 }
127
128 /* commands */
129
130 - (NGPop3Response *)receiveSimpleReply {
131   NSString *line = [self->text readLineAsString];
132
133   if (line) {
134     NGPop3Response *response = [NGPop3Response responseWithLine:line];
135     ASSIGN(self->lastResponse, response);
136   }
137   else {
138     [self->lastResponse release];
139     self->lastResponse = nil;
140   }
141   return self->lastResponse;
142 }
143
144 - (BOOL)receiveMultilineReply:(NSMutableData *)_data {
145   enum {
146     NGPop3_begin,
147     NGPop3_foundCR,
148     NGPop3_foundCRLF,
149     NGPop3_foundCRLFP,
150     NGPop3_done
151   } pState = NGPop3_begin;
152   void (*addBytes)(id self, SEL _cmd, void *buffer, unsigned int _bufLen);
153   int c;
154
155   addBytes = (void*)[_data methodForSelector:@selector(appendBytes:length:)];
156
157   do {
158     c = [self->connection readByte];
159     if (c == -1) {
160       NSLog(@"ERROR: connection was shut down ..");
161       break;
162     }
163
164     /*
165     if (c >= 32) printf("%i '%c'\n", c, c);
166     else         printf("%i\n", c);
167     */
168     
169     NSAssert((c >= 0) && (c <= 255), @"invalid byte read ..");
170
171     if (pState == NGPop3_foundCRLFP) {
172       if (c == '\r') { // CR LF . CR
173         addBytes(_data, @selector(appendBytes:length:), "\r\n", 2);
174         c = [self->connection readByte];
175         if (c == '\n') {
176           pState = NGPop3_done;
177         }
178         else {
179           char c8 = c;
180           NSLog(@"WARNING: found strange sequence: 'CR LF . CR 0x%x'", c);
181           addBytes(_data, @selector(appendBytes:length:), ".\r", 2);
182           addBytes(_data, @selector(appendBytes:length:), &c8, 1);
183           pState = NGPop3_begin;
184         }
185       }
186       else if (c == '\n') { // CR LF . LF
187         NSLog(@"WARNING: found strange sequence: 'CR LF . LF'");
188         addBytes(_data, @selector(appendBytes:length:), "\r\n.\n", 4);
189         pState = NGPop3_begin;
190       }
191       else { // CR LF . (.|other)
192         char c8 = c;
193         if (c != '.')
194           NSLog(@"WARNING: expected '\\r\\n.\\r' or '\\r\\n..', got '\\r\\n.%c'", c);
195         addBytes(_data, @selector(appendBytes:length:), "\r\n", 2);
196         addBytes(_data, @selector(appendBytes:length:), &c8, 1);
197         pState = NGPop3_begin;
198         continue;
199       }
200     }
201     else if (pState == NGPop3_foundCRLF) {
202       if (c == '.') { // found: CR LF .
203         pState = NGPop3_foundCRLFP;
204         continue;
205       }
206       else if (c == '\r') {
207         addBytes(_data, @selector(appendBytes:length:), "\r\n", 2);
208         pState = NGPop3_foundCR;
209         continue;
210       }
211       else {
212         char c8 = c;
213         addBytes(_data, @selector(appendBytes:length:), "\r\n", 2);
214         addBytes(_data, @selector(appendBytes:length:), &c8, 1);
215         pState = NGPop3_begin;
216       }
217     }
218     else if (pState == NGPop3_foundCR) {
219       if (c == '\n') { // found CR LF
220         pState = NGPop3_foundCRLF;
221         continue;
222       }
223       else {
224         char c8 = c;
225         addBytes(_data, @selector(appendBytes:length:), "\r", 1);
226         addBytes(_data, @selector(appendBytes:length:), &c8, 1);
227         pState = NGPop3_begin;
228       }
229     }
230     else if (c == '\r') {
231       pState = NGPop3_foundCR;
232       continue;
233     }
234     /*
235     else if (c == '\n') {
236       NSLog(@"WARNING: found LF without leading CR ..");
237       pState = NGPop3_foundCRLF;
238       continue;
239       }*/
240     else {
241       char c8 = c;
242       addBytes(_data, @selector(appendBytes:length:), &c8, 1);
243     }
244   }
245   while(pState != NGPop3_done);
246   
247   return (pState == NGPop3_done) ? YES : NO;
248 }
249
250 - (NGPop3Response *)sendCommand:(NSString *)_command {
251   if (self->isDebuggingEnabled) {
252     [NGTextOut writeFormat:@"C: %@\n", _command];
253     [NGTextOut flush];
254   }
255   
256   [text writeString:_command];
257   [text writeString:@"\r\n"];
258   [text flush];
259   return [self receiveSimpleReply];
260 }
261
262 - (NGPop3Response *)sendCommand:(NSString *)_command argument:(NSString *)_argument {
263   if (self->isDebuggingEnabled) {
264     if (![_command isEqualToString:@"PASS"])
265       [NGTextOut writeFormat:@"C: %@ %@\n", _command, _argument];
266     else
267       [NGTextOut writeFormat:@"C: PASS <hidden>\n"];
268   }
269   
270   [text writeString:_command];
271   [text writeFormat:@" %s\r\n", [_argument cString]];
272   [text flush];
273   return [self receiveSimpleReply];
274 }
275 - (NGPop3Response *)sendCommand:(NSString *)_command intArgument:(int)_argument {
276   if (self->isDebuggingEnabled) {
277     if (![_command isEqualToString:@"PASS"])
278       [NGTextOut writeFormat:@"C: %@ %i\n", _command, _argument];
279     else
280       [NGTextOut writeFormat:@"C: PASS <hidden>\n"];
281   }
282   
283   [text writeString:_command];
284   [text writeFormat:@" %i\r\n", _argument];
285   [text flush];
286   return [self receiveSimpleReply];
287 }
288 - (NGPop3Response *)sendCommand:(NSString *)_command
289   intArgument:(int)_arg1 intArgument:(int)_arg2 {
290
291   if (self->isDebuggingEnabled) {
292     if (![_command isEqualToString:@"PASS"])
293       [NGTextOut writeFormat:@"C: %@ %i %i\n", _command, _arg1, _arg2];
294     else
295       [NGTextOut writeFormat:@"C: PASS <hidden>\n"];
296   }
297   
298   [text writeString:_command];
299   [text writeFormat:@" %i %i\r\n", _arg1, _arg2];
300   [text flush];
301   return [self receiveSimpleReply];
302 }
303
304 // state
305
306 - (void)requireState:(NGPop3State)_state {
307   if (_state != [self state]) {
308     [[[NGPop3StateException alloc]
309        initWithClient:self
310        requiredState:_state] raise];
311   }
312 }
313
314 - (void)gotoState:(NGPop3State)_state {
315   self->state = _state;
316 }
317
318 // service commands
319
320 - (BOOL)login:(NSString *)_user password:(NSString *)_passwd {
321   NGPop3Response *reply = nil;
322
323   [self requireState:NGPop3State_AUTHORIZATION];
324
325   reply = [self sendCommand:@"USER" argument:_user];
326   if ([reply isPositive]) {
327     reply = [self sendCommand:@"PASS" argument:_passwd];
328     if ([reply isPositive]) {
329       [self gotoState:NGPop3State_TRANSACTION];
330       return YES;
331     }
332   }
333   NSLog(@"POP3 authorization of user %@ failed ..", _user);
334
335   return NO;
336 }
337
338 - (BOOL)quit {
339   NGPop3Response *reply = nil;
340
341   reply = [self sendCommand:@"QUIT"];
342   if ([reply isPositive]) {
343     unsigned int waitBytes = 0;
344     
345     if (self->state == NGPop3State_TRANSACTION)
346       self->state = NGPop3State_UPDATE;
347
348     if (self->isDebuggingEnabled)
349       [NGTextErr writeFormat:@"S: %@\n", [reply line]];
350
351     // wait for connection close ..
352     while ([self->connection readByte] != -1)
353       waitBytes++;
354
355     self->state = NGPop3State_unconnected;
356   }
357   return [reply isPositive];
358 }
359
360 - (BOOL)statMailDropCount:(int *)_count size:(int *)_size {
361   NGPop3Response *reply = nil;
362   [self requireState:NGPop3State_TRANSACTION];
363   *_count = 0;
364   *_size  = 0;
365
366   reply = [self sendCommand:@"STAT"];
367
368   if ([reply isPositive]) {
369     const char *cstr = [[reply line] cString];
370
371     while ((*cstr != '\0') && (*cstr != ' ')) cstr++;
372     if (*cstr == '\0') return NO;
373     cstr++;
374
375     *_count = atoi(cstr);
376     while ((*cstr != '\0') && (*cstr != ' ')) cstr++;
377     if (*cstr == '\0') return NO;
378     cstr++;
379     
380     *_size = atoi(cstr);
381     return YES;
382   }
383   else
384     return NO;
385 }
386
387 - (NGPop3MessageInfo *)listMessage:(int)_messageNumber {
388   NGPop3Response *reply = nil;
389   [self requireState:NGPop3State_TRANSACTION];
390
391   reply = [self sendCommand:@"LIST" intArgument:_messageNumber];
392   if ([reply isPositive]) {
393     const char *cstr = index([[reply line] cString], ' ');
394
395     if (cstr) {
396       int msgNum;
397       cstr++;
398       msgNum = atoi(cstr);
399       cstr = index(cstr, ' ') + 1;
400       if (cstr > (char *)1) {
401         NGPop3MessageInfo *info   = nil;
402         int               msgSize = atoi(cstr);
403
404         info = [NGPop3MessageInfo infoForMessage:msgNum size:msgSize client:self];
405         return info;
406       }
407     }
408     NSLog(@"ERROR: invalid reply line '%@' ..", [reply line]);
409   }
410   return nil;
411 }
412
413 - (NSEnumerator *)listMessages {
414   NGPop3Response *reply = nil;
415   [self requireState:NGPop3State_TRANSACTION];
416
417   reply = [self sendCommand:@"LIST"];
418   if ([reply isPositive]) {
419     NSMutableArray *array = nil;
420     NSString       *line  = nil;
421     
422     array = [NSMutableArray arrayWithCapacity:128];
423
424     line = [self->text readLineAsString];
425     while ((line != nil) && (![line isEqualToString:@"."])) {
426       NGPop3MessageInfo *info = nil;
427       const char        *cstr = (char *)[line cString];
428       int               msgNum, msgSize;
429
430       msgNum = atoi(cstr);
431       cstr = index(cstr, ' ') + 1;
432       if (cstr > (char *)1)
433         msgSize = atoi(cstr);
434       else {
435         NSLog(@"WARNING(%s): invalid reply line '%@'", __PRETTY_FUNCTION__, line);
436         msgSize = 0;
437       }
438
439       info = [NGPop3MessageInfo infoForMessage:msgNum size:msgSize client:self];
440
441       if (info)
442         [array addObject:info];
443       else
444         NSLog(@"ERROR: could not produce info for line '%@'", line);
445       line = [self->text readLineAsString];
446     }
447
448     return [array objectEnumerator];
449   }
450   else
451     return nil;
452 }
453
454 - (NSData *)retrieveMessage:(int)_msgNumber {
455   NGPop3Response *reply = nil;
456   [self requireState:NGPop3State_TRANSACTION];
457
458   reply = [self sendCommand:@"RETR" intArgument:_msgNumber];
459   if ([reply isPositive]) {
460     NSMutableData *data = nil;
461     const char    *cstr = index([[reply line] cString], ' ');
462     unsigned msgSize = -1;
463
464     if (cstr) {
465       cstr++;
466       msgSize = atoi(cstr);
467       data = [NSMutableData dataWithCapacity:msgSize + 1];
468     }
469     else
470       data = [NSMutableData dataWithCapacity:1024];
471
472     if ([self receiveMultilineReply:data]) {
473       if ((msgSize > 0) && ([data length] > msgSize)) {
474         NSLog(@"data was longer than message size ..");
475         //[data setLength:msgSize];
476       }
477       return data;
478     }
479   }
480   return nil;
481 }
482
483 - (BOOL)deleteMessage:(int)_msgNumber {
484   NGPop3Response *reply = nil;
485   [self requireState:NGPop3State_TRANSACTION];
486
487   reply = [self sendCommand:@"DELE" intArgument:_msgNumber];
488   if ([reply isPositive]) {
489     return YES;
490   }
491   return NO;
492 }
493
494 - (BOOL)noop {
495   [self requireState:NGPop3State_TRANSACTION];
496   return [[self sendCommand:@"NOOP"] isPositive];
497 }
498
499 - (BOOL)reset {
500   [self requireState:NGPop3State_TRANSACTION];
501   return [[self sendCommand:@"RSET"] isPositive];
502 }
503
504 // optional service commands
505
506 - (NSData *)retrieveMessage:(int)_msgNumber bodyLineCount:(int)_numberOfLines {
507   NGPop3Response *reply = nil;
508   [self requireState:NGPop3State_TRANSACTION];
509
510   reply = [self sendCommand:@"TOP"
511                 intArgument:_msgNumber
512                 intArgument:_numberOfLines];
513   if ([reply isPositive]) {
514     NSMutableData *data = nil;
515     const char    *cstr = index([[reply line] cString], ' ');
516     int  msgSize = -1;
517
518     if (cstr) {
519       cstr++;
520       msgSize = atoi(cstr);
521     }
522     data = [NSMutableData dataWithCapacity:1024];
523
524     if ([self receiveMultilineReply:data])
525       return data;
526   }
527   return nil;
528 }
529
530 - (NSDictionary *)uniqueIdMappings {
531   NGPop3Response *reply = nil;
532   [self requireState:NGPop3State_TRANSACTION];
533
534   reply = [self sendCommand:@"UIDL"];
535   if ([reply isPositive]) {
536     NSMutableDictionary *dict = nil;
537     NSString            *line  = nil;
538
539     dict = [NSMutableDictionary dictionaryWithCapacity:256];
540
541     line = [self->text readLineAsString];
542     while ((line != nil) && (![line isEqualToString:@"."])) {
543       const char *cstr = index([line cString], ' ');
544
545       if (cstr) {
546         int msgNum = atoi([line cString]);
547
548         cstr++;
549         [dict setObject:[NSString stringWithCString:cstr]
550               forKey:[NSNumber numberWithInt:msgNum]];
551       }
552       else {
553         NSLog(@"WARNING(%s): invalid reply line '%@'", __PRETTY_FUNCTION__, line);
554       }
555       line = [self->text readLineAsString];
556     }
557
558     return dict;
559   }
560   else
561     return nil;
562 }
563
564 - (NSString *)uniqueIdOfMessage:(int)_messageNumber {
565   NGPop3Response *reply = nil;
566   [self requireState:NGPop3State_TRANSACTION];
567
568   reply = [self sendCommand:@"UIDL" intArgument:_messageNumber];
569   if ([reply isPositive]) {
570     const char *cstr = index([[reply line] cString], ' ');
571
572     if (cstr) { // found message number
573       cstr = index(cstr + 1, ' ');
574       if (cstr) { // found u-id
575         cstr++;
576         return [NSString stringWithCString:cstr];
577       }
578     }
579     NSLog(@"ERROR: invalid reply line '%@' ..", [reply line]);
580   }
581   return nil;
582 }
583
584 /* MIME support */
585
586 - (NSEnumerator *)messageEnumerator {
587   return [[[NGPop3MailDropEnumerator alloc]
588               initWithMessageInfoEnumerator:[self listMessages]] autorelease];
589 }
590 - (NGMimeMessage *)messageWithNumber:(int)_messageNumber {
591   NSData *msgData = [self retrieveMessage:_messageNumber];
592   
593   if (msgData) {
594     NGDataStream        *msgStream;
595     NGMimeMessageParser *parser;
596     NGMimeMessage       *message;
597
598     msgStream = [[NGDataStream alloc] initWithData:msgData];
599     parser    = [[NGMimeMessageParser alloc] init];
600     *(&message) = nil;
601
602     NS_DURING
603       message = (NGMimeMessage *)[parser parsePartFromStream:msgStream];
604     NS_HANDLER
605       message = nil;
606     NS_ENDHANDLER;
607
608     message = [message retain];
609
610     [parser    release]; parser    = nil;
611     [msgStream release]; msgStream = nil;
612     msgData = nil;
613     
614     return [message autorelease];
615   }
616   else
617     return nil;
618 }
619
620 // description
621
622 - (NSString *)description {
623   return [NSString stringWithFormat:@"<POP3Client[0x%08X]: socket=%@>",
624                      (unsigned)self, [self socket]];
625 }
626
627 @end