xref: /haiku/src/add-ons/mail_daemon/outbound_protocols/smtp/SMTP.cpp (revision e81a954787e50e56a7f06f72705b7859b6ab06d1)
1 /*
2  * Copyright 2007-2015, Haiku, Inc. All rights reserved.
3  * Copyright 2001-2002 Dr. Zoidberg Enterprises. All rights reserved.
4  * Copyright 2011, Clemens Zeidler <haiku@clemens-zeidler.de>
5  *
6  * Distributed under the terms of the MIT License.
7  */
8 
9 
10 //!	Implementation of the SMTP protocol
11 
12 
13 #include "SMTP.h"
14 
15 #include <map>
16 
17 #include <ctype.h>
18 #include <errno.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <sys/time.h>
22 #include <unistd.h>
23 
24 #include <Alert.h>
25 #include <Catalog.h>
26 #include <DataIO.h>
27 #include <Entry.h>
28 #include <File.h>
29 #include <MenuField.h>
30 #include <Message.h>
31 #include <Path.h>
32 #include <Socket.h>
33 #include <SecureSocket.h>
34 #include <TextControl.h>
35 
36 #include <crypt.h>
37 #include <mail_encoding.h>
38 #include <MailSettings.h>
39 #include <NodeMessage.h>
40 #include <ProtocolConfigView.h>
41 
42 #include "md5.h"
43 
44 
45 #undef B_TRANSLATION_CONTEXT
46 #define B_TRANSLATION_CONTEXT "smtp"
47 
48 
49 #define CRLF "\r\n"
50 #define SMTP_RESPONSE_SIZE 8192
51 
52 //#define DEBUG
53 #ifdef DEBUG
54 #	define D(x) x
55 #	define bug printf
56 #else
57 #	define D(x) ;
58 #endif
59 
60 
61 // Authentication types recognized. Not all methods are implemented.
62 enum AuthType {
63 	LOGIN		= 1,
64 	PLAIN		= 1 << 2,
65 	CRAM_MD5	= 1 << 3,
66 	DIGEST_MD5	= 1 << 4
67 };
68 
69 
70 using namespace std;
71 
72 /*
73 ** Function: md5_hmac
74 ** taken from the file rfc2104.txt
75 ** written by Martin Schaaf <mascha@ma-scha.de>
76 */
77 void
78 MD5Hmac(unsigned char *digest, const unsigned char* text, int text_len,
79 	const unsigned char* key, int key_len)
80 {
81 	MD5_CTX context;
82 	unsigned char k_ipad[64];
83 		// inner padding - key XORd with ipad
84 	unsigned char k_opad[64];
85 		// outer padding - key XORd with opad
86 	int i;
87 
88 	/* start out by storing key in pads */
89 	memset(k_ipad, 0, sizeof k_ipad);
90 	memset(k_opad, 0, sizeof k_opad);
91 	if (key_len > 64) {
92 		/* if key is longer than 64 bytes reset it to key=MD5(key) */
93 		MD5_CTX tctx;
94 
95 		MD5_Init(&tctx);
96 		MD5_Update(&tctx, (unsigned char*)key, key_len);
97 		MD5_Final(k_ipad, &tctx);
98 		MD5_Final(k_opad, &tctx);
99 	} else {
100 		memcpy(k_ipad, key, key_len);
101 		memcpy(k_opad, key, key_len);
102 	}
103 
104 	/*
105 	 * the HMAC_MD5 transform looks like:
106 	 *
107 	 * MD5(K XOR opad, MD5(K XOR ipad, text))
108 	 *
109 	 * where K is an n byte key
110 	 * ipad is the byte 0x36 repeated 64 times
111 	 * opad is the byte 0x5c repeated 64 times
112 	 * and text is the data being protected
113 	 */
114 
115 	/* XOR key with ipad and opad values */
116 	for (i = 0; i < 64; i++) {
117 		k_ipad[i] ^= 0x36;
118 		k_opad[i] ^= 0x5c;
119 	}
120 
121 	/*
122 	 * perform inner MD5
123 	 */
124 	MD5_Init(&context);		      /* init context for 1st
125 					       * pass */
126 	MD5_Update(&context, k_ipad, 64);     /* start with inner pad */
127 	MD5_Update(&context, (unsigned char*)text, text_len); /* then text of datagram */
128 	MD5_Final(digest, &context);	      /* finish up 1st pass */
129 	/*
130 	 * perform outer MD5
131 	 */
132 	MD5_Init(&context);		      /* init context for 2nd
133 					       * pass */
134 	MD5_Update(&context, k_opad, 64);     /* start with outer pad */
135 	MD5_Update(&context, digest, 16);     /* then results of 1st
136 					       * hash */
137 	MD5_Final(digest, &context);	      /* finish up 2nd pass */
138 }
139 
140 
141 void
142 MD5HexHmac(char *hexdigest, const unsigned char* text, int text_len,
143 	const unsigned char* key, int key_len)
144 {
145 	unsigned char digest[16];
146 	int i;
147 	unsigned char c;
148 
149 	MD5Hmac(digest, text, text_len, key, key_len);
150   	for (i = 0;  i < 16;  i++) {
151   		c = digest[i];
152 		*hexdigest++ = (c > 0x9F ? 'a'-10 : '0')+(c>>4);
153 		*hexdigest++ = ((c&0x0F) > 9 ? 'a'-10 : '0')+(c&0x0F);
154   	}
155   	*hexdigest = '\0';
156 }
157 
158 
159 /*
160 ** Function: MD5Sum
161 ** generates an MD5-sum from the given string
162 */
163 void
164 MD5Sum (char* sum, unsigned char *text, int text_len) {
165 	MD5_CTX context;
166 	MD5_Init(&context);
167 	MD5_Update(&context, text, text_len);
168 	MD5_Final((unsigned char*)sum, &context);
169 	sum[16] = '\0';
170 }
171 
172 /*
173 ** Function: MD5Digest
174 ** generates an MD5-digest from the given string
175 */
176 void MD5Digest (char* hexdigest, unsigned char *text, int text_len) {
177 	int i;
178 	unsigned char digest[17];
179 	unsigned char c;
180 
181 	MD5Sum((char*)digest, text, text_len);
182 
183   	for (i = 0;  i < 16;  i++) {
184   		c = digest[i];
185 		*hexdigest++ = (c > 0x9F ? 'a'-10 : '0')+(c>>4);
186 		*hexdigest++ = ((c&0x0F) > 9 ? 'a'-10 : '0')+(c&0x0F);
187   	}
188   	*hexdigest = '\0';
189 }
190 
191 /*
192 ** Function: SplitChallengeIntoMap
193 ** splits a challenge-string into the given map (see RFC-2831)
194 */
195 // :
196 static bool
197 SplitChallengeIntoMap(BString str, map<BString,BString>& m)
198 {
199 	m.clear();
200 	const char* key;
201 	const char* val;
202 	char* s = (char*)str.String();
203 	while(*s != 0) {
204 		while(isspace(*s))
205 			s++;
206 		key = s;
207 		while(isalpha(*s))
208 			s++;
209 		if (*s != '=')
210 			return false;
211 		*s++ = '\0';
212 		while(isspace(*s))
213 			s++;
214 		if (*s=='"') {
215 			val = ++s;
216 			while(*s!='"') {
217 				if (*s == 0)
218 					return false;
219 				s++;
220 			}
221 			*s++ = '\0';
222 		} else {
223 			val = s;
224 			while(*s!=0 && *s!=',' && !isspace(*s))
225 				s++;
226 			if (*s != 0)
227 				*s++ = '\0';
228 		}
229 		m[key] = val;
230 		while(isspace(*s))
231 			s++;
232 		if (*s != ',')
233 			return false;
234 		s++;
235 	}
236 	return true;
237 }
238 
239 
240 // #pragma mark -
241 
242 
243 SMTPProtocol::SMTPProtocol(const BMailAccountSettings& settings)
244 	:
245 	BOutboundMailProtocol("SMTP", settings),
246 	fAuthType(0)
247 {
248 	fSettingsMessage = settings.OutboundSettings();
249 }
250 
251 
252 SMTPProtocol::~SMTPProtocol()
253 {
254 }
255 
256 
257 status_t
258 SMTPProtocol::Connect()
259 {
260 	BString errorMessage;
261 	int32 authMethod = fSettingsMessage.FindInt32("auth_method");
262 
263 	status_t status = B_ERROR;
264 
265 	if (authMethod == 2) {
266 		// POP3 authentication is handled here instead of SMTPProtocol::Login()
267 		// because some servers obviously don't like establishing the connection
268 		// to the SMTP server first...
269 		status_t status = _POP3Authentication();
270 		if (status < B_OK) {
271 			errorMessage << B_TRANSLATE("POP3 authentication failed. The "
272 				"server said:\n") << fLog;
273 			ShowError(errorMessage.String());
274 			return status;
275 		}
276 	}
277 
278 	status = Open(fSettingsMessage.FindString("server"),
279 		fSettingsMessage.FindInt32("port"), authMethod == 1);
280 	if (status < B_OK) {
281 		errorMessage << B_TRANSLATE("Error while opening connection to %serv");
282 		errorMessage.ReplaceFirst("%serv",
283 			fSettingsMessage.FindString("server"));
284 
285 		if (fSettingsMessage.FindInt32("port") > 0)
286 			errorMessage << ":" << fSettingsMessage.FindInt32("port");
287 
288 		if (fLog.Length() > 0)
289 			errorMessage << B_TRANSLATE(". The server says:\n") << fLog;
290 		else {
291 			errorMessage << ". " << strerror(status);
292 		}
293 
294 		ShowError(errorMessage.String());
295 
296 		return status;
297 	}
298 
299 	const char* password = get_passwd(&fSettingsMessage, "cpasswd");
300 	status = Login(fSettingsMessage.FindString("username"), password);
301 	delete[] password;
302 
303 	if (status != B_OK) {
304 		errorMessage << B_TRANSLATE("Error while logging in to %serv")
305 			<< B_TRANSLATE(". The server said:\n") << fLog;
306 		errorMessage.ReplaceFirst("%serv",
307 			fSettingsMessage.FindString("server"));
308 
309 		ShowError(errorMessage.String());
310 	}
311 	return B_OK;
312 }
313 
314 
315 void
316 SMTPProtocol::Disconnect()
317 {
318 	Close();
319 }
320 
321 
322 //! Process EMail to be sent
323 status_t
324 SMTPProtocol::HandleSendMessages(const BMessage& message, off_t totalBytes)
325 {
326 	type_code type;
327 	int32 count;
328 	status_t status = message.GetInfo("ref", &type, &count);
329 	if (status != B_OK)
330 		return status;
331 
332 	// TODO: sort out already sent messages -- the request could
333 	// be issued while we're busy sending them already
334 
335 	SetTotalItems(count);
336 	SetTotalItemsSize(totalBytes);
337 
338 	status = Connect();
339 	if (status != B_OK)
340 		return status;
341 
342 	entry_ref ref;
343 	for (int32 i = 0; message.FindRef("ref", i++, &ref) == B_OK;) {
344 		status = _SendMessage(ref);
345 		if (status != B_OK) {
346 			BString error;
347 			error << "An error occurred while sending the message "
348 				<< ref.name << " (" << strerror(status) << "):\n" << fLog;
349 			ShowError(error.String());
350 
351 			ResetProgress();
352 			break;
353 		}
354 	}
355 
356 	Disconnect();
357 	return B_ERROR;
358 }
359 
360 
361 //! Opens connection to server
362 status_t
363 SMTPProtocol::Open(const char *address, int port, bool esmtp)
364 {
365 	ReportProgress(0, 0, B_TRANSLATE("Connecting to server" B_UTF8_ELLIPSIS));
366 
367 	use_ssl = (fSettingsMessage.FindInt32("flavor") == 1);
368 
369 	if (port <= 0)
370 		port = use_ssl ? 465 : 25;
371 
372 	BNetworkAddress addr(address);
373 	if (addr.InitCheck() != B_OK) {
374 		BString str;
375 		str.SetToFormat("Invalid network address for SMTP server: %s",
376 			strerror(addr.InitCheck()));
377 		ShowError(str.String());
378 		return addr.InitCheck();
379 	}
380 
381 	if (addr.Port() == 0)
382 		addr.SetPort(port);
383 
384 	if (use_ssl)
385 		fSocket = new(std::nothrow) BSecureSocket;
386 	else
387 		fSocket = new(std::nothrow) BSocket;
388 
389 	if (!fSocket)
390 		return B_NO_MEMORY;
391 
392 	if (fSocket->Connect(addr) != B_OK) {
393 		BString error;
394 		error << "Could not connect to SMTP server "
395 			<< fSettingsMessage.FindString("server");
396 		error << ":" << addr.Port();
397 		ShowError(error.String());
398 		delete fSocket;
399 		return B_ERROR;
400 	}
401 
402 	BString line;
403 	ReceiveResponse(line);
404 
405 	char localhost[255];
406 	gethostname(localhost, 255);
407 
408 	if (localhost[0] == 0)
409 		strcpy(localhost, "namethisbebox");
410 
411 	char *cmd = new char[::strlen(localhost)+8];
412 	if (!esmtp)
413 		::sprintf(cmd,"HELO %s" CRLF, localhost);
414 	else
415 		::sprintf(cmd,"EHLO %s" CRLF, localhost);
416 
417 	if (SendCommand(cmd) != B_OK) {
418 		delete[] cmd;
419 		return B_ERROR;
420 	}
421 
422 	delete[] cmd;
423 
424 	// Check auth type
425 	if (esmtp) {
426 		const char *res = fLog.String();
427 		char *p;
428 		if ((p = ::strstr(res, "250-AUTH")) != NULL) {
429 			if(::strstr(p, "LOGIN"))
430 				fAuthType |= LOGIN;
431 			if(::strstr(p, "PLAIN"))
432 				fAuthType |= PLAIN;
433 			if(::strstr(p, "CRAM-MD5"))
434 				fAuthType |= CRAM_MD5;
435 			if(::strstr(p, "DIGEST-MD5")) {
436 				fAuthType |= DIGEST_MD5;
437 				fServerName = address;
438 			}
439 		}
440 	}
441 	return B_OK;
442 }
443 
444 
445 status_t
446 SMTPProtocol::_SendMessage(const entry_ref& ref)
447 {
448 	// open read write to be able to manipulate in MessageReadyToSend hook
449 	BFile file(&ref, B_READ_WRITE);
450 	status_t status = file.InitCheck();
451 	if (status != B_OK)
452 		return status;
453 
454 	BMessage header;
455 	file >> header;
456 
457 	const char *from = header.FindString("MAIL:from");
458 	const char *to = header.FindString("MAIL:recipients");
459 	if (to == NULL)
460 		to = header.FindString("MAIL:to");
461 
462 	if (to == NULL || from == NULL) {
463 		fLog = "Invalid message headers";
464 		return B_ERROR;
465 	}
466 
467 	NotifyMessageReadyToSend(ref, file);
468 	status = Send(to, from, &file);
469 	if (status != B_OK)
470 		return status;
471 	NotifyMessageSent(ref, file);
472 
473 	off_t size = 0;
474 	file.GetSize(&size);
475 	ReportProgress(size, 1);
476 
477 	return B_OK;
478 }
479 
480 
481 status_t
482 SMTPProtocol::_POP3Authentication()
483 {
484 	const entry_ref& entry = fAccountSettings.InboundAddOnRef();
485 	if (strcmp(entry.name, "POP3") != 0)
486 		return B_ERROR;
487 
488 	status_t (*pop3_smtp_auth)(const BMailAccountSettings&);
489 
490 	BPath path(&entry);
491 	image_id image = load_add_on(path.Path());
492 	if (image < 0)
493 		return B_ERROR;
494 	if (get_image_symbol(image, "pop3_smtp_auth",
495 			B_SYMBOL_TYPE_TEXT, (void **)&pop3_smtp_auth) != B_OK) {
496 		unload_add_on(image);
497 		image = -1;
498 		return B_ERROR;
499 	}
500 	status_t status = (*pop3_smtp_auth)(fAccountSettings);
501 	unload_add_on(image);
502 	return status;
503 }
504 
505 
506 status_t
507 SMTPProtocol::Login(const char *_login, const char *password)
508 {
509 	if (fAuthType == 0)
510 		return B_OK;
511 
512 	const char *login = _login;
513 	char hex_digest[33];
514 	BString out;
515 
516 	int32 loginlen = ::strlen(login);
517 	int32 passlen = ::strlen(password);
518 
519 	if (fAuthType & DIGEST_MD5) {
520 		//******* DIGEST-MD5 Authentication ( tested. works fine [with Cyrus SASL] )
521 		// this implements only the subpart of DIGEST-MD5 which is
522 		// required for authentication to SMTP-servers. Integrity-
523 		// and confidentiality-protection are not implemented, as
524 		// they are provided by the use of OpenSSL.
525 		SendCommand("AUTH DIGEST-MD5" CRLF);
526 		const char *res = fLog.String();
527 
528 		if (strncmp(res, "334", 3) != 0)
529 			return B_ERROR;
530 		int32 baselen = ::strlen(&res[4]);
531 		char *base = new char[baselen+1];
532 		baselen = ::decode_base64(base, &res[4], baselen);
533 		base[baselen] = '\0';
534 
535 		D(bug("base: %s\n", base));
536 
537 		map<BString,BString> challengeMap;
538 		SplitChallengeIntoMap(base, challengeMap);
539 
540 		delete[] base;
541 
542 		BString rawResponse = BString("username=") << '"' << login << '"';
543 		rawResponse << ",realm=" << '"' << challengeMap["realm"] << '"';
544 		rawResponse << ",nonce=" << '"' << challengeMap["nonce"] << '"';
545 		rawResponse << ",nc=00000001";
546 		char temp[33];
547 		for( int i=0; i<32; ++i)
548 			temp[i] = 1+(rand()%254);
549 		temp[32] = '\0';
550 		BString rawCnonce(temp);
551 		BString cnonce;
552 		char* cnoncePtr = cnonce.LockBuffer(rawCnonce.Length()*2);
553 		baselen = ::encode_base64(cnoncePtr, rawCnonce.String(), rawCnonce.Length(), true /* headerMode */);
554 		cnoncePtr[baselen] = '\0';
555 		cnonce.UnlockBuffer(baselen);
556 		rawResponse << ",cnonce=" << '"' << cnonce << '"';
557 		rawResponse << ",qop=auth";
558 		BString digestUriValue	= BString("smtp/") << fServerName;
559 		rawResponse << ",digest-uri=" << '"' << digestUriValue << '"';
560 		char sum[17], hex_digest2[33];
561 		BString a1,a2,kd;
562 		BString t1 = BString(login) << ":"
563 				<< challengeMap["realm"] << ":"
564 				<< password;
565 		MD5Sum(sum, (unsigned char*)t1.String(), t1.Length());
566 		a1 << sum << ":" << challengeMap["nonce"] << ":" << cnonce;
567 		MD5Digest(hex_digest, (unsigned char*)a1.String(), a1.Length());
568 		a2 << "AUTHENTICATE:" << digestUriValue;
569 		MD5Digest(hex_digest2, (unsigned char*)a2.String(), a2.Length());
570 		kd << hex_digest << ':' << challengeMap["nonce"]
571 		   << ":" << "00000001" << ':' << cnonce << ':' << "auth"
572 		   << ':' << hex_digest2;
573 		MD5Digest(hex_digest, (unsigned char*)kd.String(), kd.Length());
574 
575 		rawResponse << ",response=" << hex_digest;
576 		BString postResponse;
577 		char *resp = postResponse.LockBuffer(rawResponse.Length() * 2 + 10);
578 		baselen = ::encode_base64(resp, rawResponse.String(), rawResponse.Length(), true /* headerMode */);
579 		resp[baselen] = 0;
580 		postResponse.UnlockBuffer();
581 		postResponse.Append(CRLF);
582 
583 		SendCommand(postResponse.String());
584 
585 		res = fLog.String();
586 		if (atol(res) >= 500)
587 			return B_ERROR;
588 		// actually, we are supposed to check the rspauth sent back
589 		// by the SMTP-server, but that isn't strictly required,
590 		// so we skip that for now.
591 		SendCommand(CRLF);	// finish off authentication
592 		res = fLog.String();
593 		if (atol(res) < 500)
594 			return B_OK;
595 	}
596 	if (fAuthType & CRAM_MD5) {
597 		//******* CRAM-MD5 Authentication ( tested. works fine [with Cyrus SASL] )
598 		SendCommand("AUTH CRAM-MD5" CRLF);
599 		const char *res = fLog.String();
600 
601 		if (strncmp(res, "334", 3) != 0)
602 			return B_ERROR;
603 		int32 baselen = ::strlen(&res[4]);
604 		char *base = new char[baselen+1];
605 		baselen = ::decode_base64(base, &res[4], baselen);
606 		base[baselen] = '\0';
607 
608 		D(bug("base: %s\n", base));
609 
610 		::MD5HexHmac(hex_digest, (const unsigned char *)base, (int)baselen,
611 			(const unsigned char *)password, (int)passlen);
612 
613 		D(bug("%s\n%s\n", base, hex_digest));
614 
615 		delete[] base;
616 
617 		BString preResponse, postResponse;
618 		preResponse = login;
619 		preResponse << " " << hex_digest << CRLF;
620 		char *resp = postResponse.LockBuffer(preResponse.Length() * 2 + 10);
621 		baselen = ::encode_base64(resp, preResponse.String(), preResponse.Length(), true /* headerMode */);
622 		resp[baselen] = 0;
623 		postResponse.UnlockBuffer();
624 		postResponse.Append(CRLF);
625 
626 		SendCommand(postResponse.String());
627 
628 		res = fLog.String();
629 		if (atol(res) < 500)
630 			return B_OK;
631 	}
632 	if (fAuthType & LOGIN) {
633 		//******* LOGIN Authentication ( tested. works fine)
634 		ssize_t encodedsize; // required by our base64 implementation
635 
636 		SendCommand("AUTH LOGIN" CRLF);
637 		const char *res = fLog.String();
638 
639 		if (strncmp(res, "334", 3) != 0)
640 			return B_ERROR;
641 
642 		// Send login name as base64
643 		char *login64 = new char[loginlen*3 + 6];
644 		encodedsize = ::encode_base64(login64, (char *)login, loginlen, true /* headerMode */);
645 		login64[encodedsize] = 0;
646 		strcat (login64, CRLF);
647 		SendCommand(login64);
648 		delete[] login64;
649 
650 		res = fLog.String();
651 		if (strncmp(res,"334",3) != 0)
652 			return B_ERROR;
653 
654 		// Send password as base64
655 		login64 = new char[passlen*3 + 6];
656 		encodedsize = ::encode_base64(login64, (char *)password, passlen, true /* headerMode */);
657 		login64[encodedsize] = 0;
658 		strcat (login64, CRLF);
659 		SendCommand(login64);
660 		delete[] login64;
661 
662 		res = fLog.String();
663 		if (atol(res) < 500)
664 			return B_OK;
665 	}
666 	if (fAuthType & PLAIN) {
667 		//******* PLAIN Authentication ( tested. works fine [with Cyrus SASL] )
668 		// format is:
669 		// 	authenticateID + \0 + username + \0 + password
670 		// 	(where authenticateID is always empty !?!)
671 		BString preResponse, postResponse;
672 		char *stringPntr;
673 		ssize_t encodedLength;
674 		stringPntr = preResponse.LockBuffer(loginlen + passlen + 3);
675 			// +3 to make room for the two \0-chars between the tokens and
676 			// the final delimiter added by sprintf().
677 		sprintf (stringPntr, "%c%s%c%s", 0, login, 0, password);
678 		preResponse.UnlockBuffer(loginlen + passlen + 2);
679 			// +2 in order to leave out the final delimiter (which is not part
680 			// of the string).
681 		stringPntr = postResponse.LockBuffer(preResponse.Length() * 3);
682 		encodedLength = ::encode_base64(stringPntr, preResponse.String(),
683 			preResponse.Length(), true /* headerMode */);
684 		stringPntr[encodedLength] = 0;
685 		postResponse.UnlockBuffer();
686 		postResponse.Prepend("AUTH PLAIN ");
687 		postResponse << CRLF;
688 
689 		SendCommand(postResponse.String());
690 
691 		const char *res = fLog.String();
692 		if (atol(res) < 500)
693 			return B_OK;
694 	}
695 	return B_ERROR;
696 }
697 
698 
699 void
700 SMTPProtocol::Close()
701 {
702 
703 	BString cmd = "QUIT";
704 	cmd += CRLF;
705 
706 	if (SendCommand(cmd.String()) != B_OK) {
707 		// Error
708 	}
709 
710 	delete fSocket;
711 }
712 
713 
714 status_t
715 SMTPProtocol::Send(const char* to, const char* from, BPositionIO *message)
716 {
717 	BString cmd = from;
718 	cmd.Remove(0, cmd.FindFirst("\" <") + 2);
719 	cmd.Prepend("MAIL FROM: ");
720 	cmd += CRLF;
721 	if (SendCommand(cmd.String()) != B_OK)
722 		return B_ERROR;
723 
724 	int32 len = strlen(to);
725 	BString addr("");
726 	for (int32 i = 0;i < len;i++) {
727 		char c = to[i];
728 		if (c != ',')
729 			addr += (char)c;
730 		if (c == ','||i == len-1) {
731 			if(addr.Length() == 0)
732 				continue;
733 			cmd = "RCPT TO: ";
734 			cmd << addr.String() << CRLF;
735 			if (SendCommand(cmd.String()) != B_OK)
736 				return B_ERROR;
737 
738 			addr ="";
739 		}
740 	}
741 
742 	cmd = "DATA";
743 	cmd += CRLF;
744 	if (SendCommand(cmd.String()) != B_OK)
745 		return B_ERROR;
746 
747 	// Send the message data.  Convert lines starting with a period to start
748 	// with two periods and so on.  The actual sequence is CR LF Period.  The
749 	// SMTP server will remove the periods.  Of course, the POP server may then
750 	// add some of its own, but the POP client should take care of them.
751 
752 	ssize_t amountRead;
753 	ssize_t amountToRead;
754 	ssize_t amountUnread;
755 	ssize_t bufferLen = 0;
756 	const int bufferMax = 2000;
757 	bool foundCRLFPeriod;
758 	int i;
759 	bool messageEndedWithCRLF = false;
760 
761 	message->Seek(0, SEEK_END);
762 	amountUnread = message->Position();
763 	message->Seek(0, SEEK_SET);
764 	char *data = new char[bufferMax];
765 
766 	while (true) {
767 		// Fill the buffer if it is getting low, but not every time, to avoid
768 		// small reads.
769 		if (bufferLen < bufferMax / 2) {
770 			amountToRead = bufferMax - bufferLen;
771 			if (amountToRead > amountUnread)
772 				amountToRead = amountUnread;
773 			if (amountToRead > 0) {
774 				amountRead = message->Read (data + bufferLen, amountToRead);
775 				if (amountRead <= 0 || amountRead > amountToRead)
776 					amountUnread = 0; // Just stop reading when an error happens.
777 				else {
778 					amountUnread -= amountRead;
779 					bufferLen += amountRead;
780 				}
781 			}
782 		}
783 
784 		// Look for the next CRLFPeriod triple.
785 		foundCRLFPeriod = false;
786 		for (i = 0; i <= bufferLen - 3; i++) {
787 			if (data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '.') {
788 				foundCRLFPeriod = true;
789 				// Send data up to the CRLF, and include the period too.
790 				if (fSocket->Write(data, i + 3) < 0) {
791 					amountUnread = 0; // Stop when an error happens.
792 					bufferLen = 0;
793 					break;
794 				}
795 				ReportProgress (i + 2 /* Don't include the double period here */,0);
796 				// Move the data over in the buffer, but leave the period there
797 				// so it gets sent a second time.
798 				memmove(data, data + (i + 2), bufferLen - (i + 2));
799 				bufferLen -= i + 2;
800 				break;
801 			}
802 		}
803 
804 		if (!foundCRLFPeriod) {
805 			if (amountUnread <= 0) { // No more data, all we have is in the buffer.
806 				if (bufferLen > 0) {
807 					fSocket->Write(data, bufferLen);
808 					ReportProgress(bufferLen, 0);
809 					if (bufferLen >= 2)
810 						messageEndedWithCRLF = (data[bufferLen-2] == '\r' &&
811 							data[bufferLen-1] == '\n');
812 				}
813 				break; // Finished!
814 			}
815 
816 			// Send most of the buffer, except a few characters to overlap with
817 			// the next read, in case the CRLFPeriod is split between reads.
818 			if (bufferLen > 3) {
819 				if (fSocket->Write(data, bufferLen - 3) < 0)
820 					break; // Stop when an error happens.
821 
822 				ReportProgress(bufferLen - 3, 0);
823 				memmove (data, data + bufferLen - 3, 3);
824 				bufferLen = 3;
825 			}
826 		}
827 	}
828 	delete [] data;
829 
830 	if (messageEndedWithCRLF)
831 		cmd = "." CRLF; // The standard says don't add extra CRLF.
832 	else
833 		cmd = CRLF "." CRLF;
834 
835 	if (SendCommand(cmd.String()) != B_OK)
836 		return B_ERROR;
837 
838 	return B_OK;
839 }
840 
841 
842 //! Receives response from server.
843 int32
844 SMTPProtocol::ReceiveResponse(BString &out)
845 {
846 	out = "";
847 	int32 len = 0,r;
848 	char buf[SMTP_RESPONSE_SIZE];
849 	bigtime_t timeout = 1000000*180; // timeout 180 secs
850 	bool gotCode = false;
851 	int32 errCode;
852 	BString searchStr = "";
853 
854 	if (fSocket->WaitForReadable(timeout) == B_OK) {
855 		while (1) {
856 			r = fSocket->Read(buf, SMTP_RESPONSE_SIZE - 1);
857 			if (r <= 0)
858 				break;
859 
860 			if (!gotCode) {
861 				if (buf[3] == ' ' || buf[3] == '-') {
862 					errCode = atol(buf);
863 					gotCode = true;
864 					searchStr << errCode << ' ';
865 				}
866 			}
867 
868 			len += r;
869 			out.Append(buf, r);
870 
871 			if (strstr(buf, CRLF) && (out.FindFirst(searchStr) != B_ERROR))
872 				break;
873 		}
874 	} else
875 		fLog = "SMTP socket timeout.";
876 
877 	D(bug("S:%s\n", out.String()));
878 	return len;
879 }
880 
881 
882 // Sends SMTP command. Result kept in fLog
883 
884 status_t
885 SMTPProtocol::SendCommand(const char *cmd)
886 {
887 	D(bug("C:%s\n", cmd));
888 
889 	if (fSocket->Write(cmd, ::strlen(cmd)) < 0)
890 		return B_ERROR;
891 	fLog = "";
892 
893 	// Receive
894 	while (1) {
895 		int32 len = ReceiveResponse(fLog);
896 
897 		if (len <= 0) {
898 			D(bug("SMTP: len == %" B_PRId32 "\n", len));
899 			return B_ERROR;
900 		}
901 
902 		if (fLog.Length() > 4 && (fLog[3] == ' ' || fLog[3] == '-'))
903 		{
904 			int32 num = atol(fLog.String());
905 			D(bug("ReplyNumber: %" B_PRId32 "\n", num));
906 			if (num >= 500)
907 				return B_ERROR;
908 
909 			break;
910 		}
911 	}
912 
913 	return B_OK;
914 }
915 
916 
917 // #pragma mark -
918 
919 
920 extern "C" BOutboundMailProtocol*
921 instantiate_outbound_protocol(const BMailAccountSettings& settings)
922 {
923 	return new SMTPProtocol(settings);
924 }
925