]> err.no Git - sope/blob - sope-mime/NGMail/NGSmtpClient.m
fixed copyrights for 2005
[sope] / sope-mime / NGMail / NGSmtpClient.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 "NGSmtpClient.h"
23 #include "NGSmtpSupport.h"
24 #include "NGSmtpReplyCodes.h"
25 #include "common.h"
26
27 @interface NGSmtpClient(PrivateMethods)
28 - (void)_fetchExtensionInfo;
29 @end
30
31 @implementation NGSmtpClient
32
33 + (int)version {
34   return 2;
35 }
36
37 + (id)smtpClient {
38   NGActiveSocket *s;
39   s = [NGActiveSocket socketInDomain:[NGInternetSocketDomain domain]];
40   return [[[self alloc] initWithSocket:s] autorelease];
41 }
42
43 - (id)init {
44   NSLog(@"%@: init not supported, use initWithSocket: ..", self);
45   [self release];
46   return nil;
47 }
48
49 - (id)initWithSocket:(id<NGActiveSocket>)_socket { // designated initializer
50   if ((self = [super init])) {
51     self->socket = [_socket retain];
52     NSAssert(self->socket, @"invalid socket parameter");
53
54     [self setDebuggingEnabled:YES];
55
56     self->connection = 
57       [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:_socket];
58     self->text = 
59       [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->connection];
60
61     self->state = [self->socket isConnected]
62       ? NGSmtpState_connected
63       : NGSmtpState_unconnected;
64   }
65   return self;
66 }
67
68 - (void)dealloc {
69   [self->text       release];
70   [self->connection release];
71   [self->socket     release];
72   [super dealloc];
73 }
74
75 // accessors
76
77 - (id<NGActiveSocket>)socket {
78   return self->socket;
79 }
80
81 - (NGSmtpState)state {
82   return self->state;
83 }
84
85 - (void)setDebuggingEnabled:(BOOL)_flag {
86   self->isDebuggingEnabled = _flag;
87 }
88 - (BOOL)isDebuggingEnabled {
89   return self->isDebuggingEnabled;
90 }
91
92 // connection
93
94 - (BOOL)connectToHost:(id)_host {
95   return [self connectToAddress:
96                  [NGInternetSocketAddress addressWithService:@"smtp"
97                                           onHost:_host protocol:@"tcp"]];
98 }
99
100 - (BOOL)connectToAddress:(id<NGSocketAddress>)_address {
101   NGSmtpResponse *greeting = nil;
102
103   [self requireState:NGSmtpState_unconnected];
104   
105   if (self->isDebuggingEnabled)
106     [NGTextErr writeFormat:@"C: connect to %@\n", _address];
107   
108   [self->socket connectToAddress:_address];
109
110   // receive greetings from server
111   greeting = [self receiveReply];
112   if (self->isDebuggingEnabled)
113     [NGTextErr writeFormat:@"S: %@\n", greeting];
114
115   if ([greeting isPositive]) {
116     [self gotoState:NGSmtpState_connected];
117     [self _fetchExtensionInfo];
118
119     if (self->isDebuggingEnabled) {
120       if (self->extensions.hasPipelining)
121         [NGTextErr writeFormat:@"S: pipelining extension supported.\n"];
122       if (self->extensions.hasSize)
123         [NGTextErr writeFormat:@"S: size extension supported.\n"];
124       if (self->extensions.hasHelp)
125         [NGTextErr writeFormat:@"S: help extension supported.\n"];
126       if (self->extensions.hasExpand)
127         [NGTextErr writeFormat:@"S: expand extension supported.\n"];
128     }
129     return YES;
130   }
131   else {
132     [self disconnect];
133     return NO;
134   }
135 }
136
137 - (void)disconnect {
138   [text   flush];
139   [socket close];
140   [self gotoState:NGSmtpState_unconnected];
141 }
142
143 // state
144
145 - (void)requireState:(NGSmtpState)_state {
146   if (_state != [self state]) {
147     [NSException raise:@"SMTPException"
148                  format:@"require state %i, now in %i", _state, [self state]];
149   }
150 }
151 - (void)denyState:(NGSmtpState)_state {
152   if ([self state] == _state) {
153     [NSException raise:@"SMTPException"
154                  format:@"not allowed in state %i", [self state]];
155   }
156 }
157
158 - (void)gotoState:(NGSmtpState)_state {
159   self->state = _state;
160 }
161
162 - (BOOL)isTransactionInProgress {
163   return (self->state == NGSmtpState_TRANSACTION);
164 }
165 - (void)abortTransaction {
166   [self gotoState:NGSmtpState_connected];
167 }
168
169 // replies
170
171 - (NGSmtpResponse *)receiveReply {
172   NSMutableString *desc = nil;
173   NSString        *line = nil;
174   NGSmtpReplyCode code  = -1;
175
176   line = [self->text readLineAsString];
177   if ([line length] < 4) {
178     NSLog(@"SMTP: reply has invalid format (%@)", line);
179     return nil;
180   }
181   code = [[line substringToIndex:3] intValue];
182   //if (self->isDebuggingEnabled)
183   //  [NGTextErr writeFormat:@"S: reply with code %i follows ..\n", code];
184
185   desc = [NSMutableString stringWithCapacity:[line length]];
186   while ([line characterAtIndex:3] == '-') {
187     if ([line length] < 4) {
188       NSLog(@"SMTP: reply has invalid format (text=%@, line=%@)", desc, line);
189       break;
190     }
191     [desc appendString:[line substringFromIndex:4]];
192     [desc appendString:@"\n"];
193     line = [self->text readLineAsString];
194   }
195   if ([line length] >= 4)
196     [desc appendString:[line substringFromIndex:4]];
197
198   return [NGSmtpResponse responseWithCode:code text:desc];
199 }
200
201 // commands
202
203 - (NGSmtpResponse *)sendCommand:(NSString *)_command {
204   if (self->isDebuggingEnabled) {
205     [NGTextOut writeFormat:@"C: %@\n", _command];
206     [NGTextOut flush];
207   }
208   
209   [text writeString:_command];
210   [text writeString:@"\r\n"];
211   [text flush];
212   return [self receiveReply];
213 }
214 - (NGSmtpResponse *)sendCommand:(NSString *)_command argument:(NSString *)_argument {
215   if (self->isDebuggingEnabled) {
216     [NGTextOut writeFormat:@"C: %@ %@\n", _command, _argument];
217     [NGTextOut flush];
218   }
219   
220   [text writeString:_command];
221   [text writeString:@" "];
222   [text writeString:_argument];
223   [text writeString:@"\r\n"];
224   [text flush];
225   return [self receiveReply];
226 }
227
228 // service commands
229
230 - (void)_fetchExtensionInfo {
231   NGSmtpResponse *reply = nil;
232   NSString       *hostName = nil;
233   
234   hostName = [(NGInternetSocketAddress *)[self->socket localAddress] hostName];
235
236   reply = [self sendCommand:@"EHLO" argument:hostName];
237   if ([reply code] == NGSmtpActionCompleted) {
238     NSEnumerator *lines = [[[reply text] componentsSeparatedByString:@"\n"]
239                                    objectEnumerator];
240     NSString     *line = nil;
241
242     if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
243
244     while ((line = [lines nextObject])) {
245       if ([line hasPrefix:@"EXPN"])
246         self->extensions.hasExpand = YES;
247       else if ([line hasPrefix:@"SIZE"])
248         self->extensions.hasSize = YES;
249       else if ([line hasPrefix:@"PIPELINING"])
250         self->extensions.hasPipelining = YES;
251       else if ([line hasPrefix:@"HELP"])
252         self->extensions.hasHelp = YES;
253     }
254     lines = nil;
255   }
256   else {
257     if (self->isDebuggingEnabled) {
258       [NGTextErr writeFormat:@"S: %@\n", reply];
259       [NGTextErr writeFormat:@" .. could not get extension info.\n"];
260     }
261   }
262 }
263
264 - (BOOL)_simpleServiceCommand:(NSString *)_command expectCode:(NGSmtpReplyCode)_code {
265   NGSmtpResponse *reply = nil;
266
267   [self denyState:NGSmtpState_unconnected];
268
269   reply = [self sendCommand:_command];
270   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
271   if ([reply isPositive]) {
272     if ([reply code] != _code)
273       NSLog(@"SMTP(%@): expected reply code %i, got code %i ..",
274             _command, _code, [reply code]);
275     return YES;
276   }
277   return NO;
278 }
279
280 - (BOOL)quit {
281   NGSmtpResponse *reply = nil;
282
283   [self requireState:NGSmtpState_connected];
284   
285   reply = [self sendCommand:@"QUIT"];
286   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
287   if ([reply isPositive]) {
288     unsigned int waitBytes = 0;
289     
290     if ([reply code] == NGSmtpServiceClosingChannel) {
291       // wait for connection close ..
292       while ([self->connection readByte] != -1)
293         waitBytes++;
294     }
295     else
296       NSLog(@"SMTP(QUIT): unexpected reply code (%i), disconnecting ..", [reply code]);
297     return YES;
298   }
299   return NO;
300 }
301
302 - (BOOL)helloWithHostname:(NSString *)_host {
303   NGSmtpResponse *reply = nil;
304
305   [self denyState:NGSmtpState_unconnected];
306   
307   reply = [self sendCommand:@"HELO" argument:_host];
308   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
309   if ([reply isPositive]) {
310     if ([reply code] != NGSmtpActionCompleted) {
311       NSLog(@"SMTP(HELO): expected reply code %i, got code %i ..",
312             NGSmtpActionCompleted, [reply code]);
313     }
314     return YES;
315   }
316   return NO;
317 }
318 - (BOOL)hello {
319   NSString *hostName = nil;
320   hostName = [(NGInternetSocketAddress *)[self->socket localAddress] hostName];
321   return [self helloWithHostname:hostName];
322 }
323
324 - (BOOL)noop {
325   return [self _simpleServiceCommand:@"NOOP" expectCode:NGSmtpActionCompleted];
326 }
327
328 - (BOOL)reset {
329   if ([self _simpleServiceCommand:@"RSET" expectCode:NGSmtpActionCompleted]) {
330     if ([self isTransactionInProgress])
331       [self abortTransaction];
332     return YES;
333   }
334   else
335     return NO;
336 }
337
338 - (NSString *)help {
339   NGSmtpResponse *reply = nil;
340
341   [self denyState:NGSmtpState_unconnected];
342   
343   reply = [self sendCommand:@"HELP"];
344   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
345   if ([reply isPositive]) {
346     if ([reply code] != NGSmtpHelpMessage) {
347       NSLog(@"SMTP(HELP): expected reply code %i, got code %i ..",
348             NGSmtpHelpMessage, [reply code]);
349     }
350     return [reply text];
351   }
352   return nil;
353 }
354 - (NSString *)helpForTopic:(NSString *)_topic {
355   NGSmtpResponse *reply = nil;
356   [self denyState:NGSmtpState_unconnected];
357   
358   reply = [self sendCommand:@"HELP" argument:_topic];
359   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
360   if ([reply isPositive]) {
361     if ([reply code] != NGSmtpHelpMessage) {
362       NSLog(@"SMTP(HELP): expected reply code %i, got code %i ..",
363             NGSmtpHelpMessage, [reply code]);
364     }
365     return [reply text];
366   }
367   return nil;
368 }
369
370 - (BOOL)verifyAddress:(id)_address {
371   NGSmtpResponse *reply = nil;
372   [self denyState:NGSmtpState_unconnected];
373
374   reply = [self sendCommand:@"VRFY" argument:[_address stringValue]];
375   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
376   if ([reply isPositive]) {
377     if ([reply code] != NGSmtpActionCompleted) {
378       NSLog(@"SMTP(VRFY): expected reply code %i, got code %i ..",
379             NGSmtpActionCompleted, [reply code]);
380     }
381     return YES;
382   }
383   else if ([reply code] == NGSmtpMailboxNotFound) {
384     return NO;
385   }
386   else {
387     NSLog(@"SMTP(VRFY): expected positive or 550 reply code, got code %i ..", [reply code]);
388     return NO;
389   }
390 }
391
392 // transaction commands
393
394 - (BOOL)mailFrom:(id)_sender {
395   NGSmtpResponse *reply  = nil;
396   NSString       *sender = nil;
397   [self requireState:NGSmtpState_connected];
398
399   sender = [@"FROM:" stringByAppendingString:[_sender stringValue]];
400   reply  = [self sendCommand:@"MAIL" argument:sender];
401   if ([reply isPositive]) {
402     if ([reply code] != NGSmtpActionCompleted) {
403       NSLog(@"SMTP(MAIL FROM): expected reply code %i, got code %i ..",
404             NGSmtpActionCompleted, [reply code]);
405     }
406     return YES;
407   }
408   return NO;
409 }
410
411 - (BOOL)recipientTo:(id)_receiver {
412   NGSmtpResponse *reply = nil;
413   NSString       *rcpt  = nil;
414   
415   [self requireState:NGSmtpState_TRANSACTION];
416   
417   rcpt  = [@"TO:" stringByAppendingString:[_receiver stringValue]];
418   reply = [self sendCommand:@"RCPT" argument:rcpt];
419   if ([reply isPositive]) {
420     if ([reply code] != NGSmtpActionCompleted) {
421       NSLog(@"SMTP(RCPT TO): expected reply code %i, got code %i ..",
422             NGSmtpActionCompleted, [reply code]);
423     }
424     return YES;
425   }
426   return NO;
427 }
428
429 - (BOOL)sendData:(NSData *)_data {
430   NGSmtpResponse *reply = nil;
431   
432   [self requireState:NGSmtpState_TRANSACTION];
433
434   reply = [self sendCommand:@"DATA"];
435   if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
436   if (([reply code] >= 300) && ([reply code] < 400)) {
437     if ([reply code] != NGSmtpStartMailInput) {
438       NSLog(@"SMTP(DATA): expected reply code %i, got code %i ..",
439             NGSmtpStartMailInput, [reply code]);
440     }
441     [self->text flush];
442
443     if (self->isDebuggingEnabled)
444       [NGTextErr writeFormat:@"C: data(%i bytes) ..\n", [_data bytes]];
445     
446     [self->connection safeWriteBytes:[_data bytes] count:[_data length]];
447     [self->connection safeWriteBytes:".\r\n" count:3];
448     [self->connection flush];
449
450     reply = [self receiveReply];
451     if (self->isDebuggingEnabled) [NGTextErr writeFormat:@"S: %@\n", reply];
452     if ([reply isPositive]) {
453       return YES;
454     }
455     else {
456       NSLog(@"SMTP(DATA): mail input failed, got code %i ..", [reply code]);
457     }
458   }
459   return NO;
460 }
461
462 // description
463
464 - (NSString *)description {
465   return [NSString stringWithFormat:@"<SMTP-Client[0x%08X]: socket=%@>",
466                      (unsigned)self, [self socket]];
467 }
468
469 @end