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.
62enum AuthType {
63	LOGIN		= 1,
64	PLAIN		= 1 << 2,
65	CRAM_MD5	= 1 << 3,
66	DIGEST_MD5	= 1 << 4
67};
68
69
70using 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*/
77void
78MD5Hmac(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
141void
142MD5HexHmac(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*/
163void
164MD5Sum (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*/
176void 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// :
196static bool
197SplitChallengeIntoMap(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
243SMTPProtocol::SMTPProtocol(const BMailAccountSettings& settings)
244	:
245	BOutboundMailProtocol("SMTP", settings),
246	fAuthType(0)
247{
248	fSettingsMessage = settings.OutboundSettings();
249}
250
251
252SMTPProtocol::~SMTPProtocol()
253{
254}
255
256
257status_t
258SMTPProtocol::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
315void
316SMTPProtocol::Disconnect()
317{
318	Close();
319}
320
321
322//! Process EMail to be sent
323status_t
324SMTPProtocol::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
362status_t
363SMTPProtocol::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				|| (p = ::strstr(res, "250 AUTH")) != NULL) {
430			if(::strstr(p, "LOGIN"))
431				fAuthType |= LOGIN;
432			if(::strstr(p, "PLAIN"))
433				fAuthType |= PLAIN;
434			if(::strstr(p, "CRAM-MD5"))
435				fAuthType |= CRAM_MD5;
436			if(::strstr(p, "DIGEST-MD5")) {
437				fAuthType |= DIGEST_MD5;
438				fServerName = address;
439			}
440		}
441	}
442	return B_OK;
443}
444
445
446status_t
447SMTPProtocol::_SendMessage(const entry_ref& ref)
448{
449	// open read write to be able to manipulate in MessageReadyToSend hook
450	BFile file(&ref, B_READ_WRITE);
451	status_t status = file.InitCheck();
452	if (status != B_OK)
453		return status;
454
455	BMessage header;
456	file >> header;
457
458	const char *from = header.FindString("MAIL:from");
459	const char *to = header.FindString("MAIL:recipients");
460	if (to == NULL)
461		to = header.FindString("MAIL:to");
462
463	if (to == NULL || from == NULL) {
464		fLog = "Invalid message headers";
465		return B_ERROR;
466	}
467
468	NotifyMessageReadyToSend(ref, file);
469	status = Send(to, from, &file);
470	if (status != B_OK)
471		return status;
472	NotifyMessageSent(ref, file);
473
474	off_t size = 0;
475	file.GetSize(&size);
476	ReportProgress(size, 1);
477
478	return B_OK;
479}
480
481
482status_t
483SMTPProtocol::_POP3Authentication()
484{
485	const entry_ref& entry = fAccountSettings.InboundAddOnRef();
486	if (strcmp(entry.name, "POP3") != 0)
487		return B_ERROR;
488
489	status_t (*pop3_smtp_auth)(const BMailAccountSettings&);
490
491	BPath path(&entry);
492	image_id image = load_add_on(path.Path());
493	if (image < 0)
494		return B_ERROR;
495	if (get_image_symbol(image, "pop3_smtp_auth",
496			B_SYMBOL_TYPE_TEXT, (void **)&pop3_smtp_auth) != B_OK) {
497		unload_add_on(image);
498		image = -1;
499		return B_ERROR;
500	}
501	status_t status = (*pop3_smtp_auth)(fAccountSettings);
502	unload_add_on(image);
503	return status;
504}
505
506
507status_t
508SMTPProtocol::Login(const char *_login, const char *password)
509{
510	if (fAuthType == 0)
511		return B_OK;
512
513	const char *login = _login;
514	char hex_digest[33];
515	BString out;
516
517	int32 loginlen = ::strlen(login);
518	int32 passlen = ::strlen(password);
519
520	if (fAuthType & DIGEST_MD5) {
521		//******* DIGEST-MD5 Authentication ( tested. works fine [with Cyrus SASL] )
522		// this implements only the subpart of DIGEST-MD5 which is
523		// required for authentication to SMTP-servers. Integrity-
524		// and confidentiality-protection are not implemented, as
525		// they are provided by the use of OpenSSL.
526		SendCommand("AUTH DIGEST-MD5" CRLF);
527		const char *res = fLog.String();
528
529		if (strncmp(res, "334", 3) != 0)
530			return B_ERROR;
531		int32 baselen = ::strlen(&res[4]);
532		char *base = new char[baselen+1];
533		baselen = ::decode_base64(base, &res[4], baselen);
534		base[baselen] = '\0';
535
536		D(bug("base: %s\n", base));
537
538		map<BString,BString> challengeMap;
539		SplitChallengeIntoMap(base, challengeMap);
540
541		delete[] base;
542
543		BString rawResponse = BString("username=") << '"' << login << '"';
544		rawResponse << ",realm=" << '"' << challengeMap["realm"] << '"';
545		rawResponse << ",nonce=" << '"' << challengeMap["nonce"] << '"';
546		rawResponse << ",nc=00000001";
547		char temp[33];
548		for( int i=0; i<32; ++i)
549			temp[i] = 1+(rand()%254);
550		temp[32] = '\0';
551		BString rawCnonce(temp);
552		BString cnonce;
553		char* cnoncePtr = cnonce.LockBuffer(rawCnonce.Length()*2);
554		baselen = ::encode_base64(cnoncePtr, rawCnonce.String(), rawCnonce.Length(), true /* headerMode */);
555		cnoncePtr[baselen] = '\0';
556		cnonce.UnlockBuffer(baselen);
557		rawResponse << ",cnonce=" << '"' << cnonce << '"';
558		rawResponse << ",qop=auth";
559		BString digestUriValue	= BString("smtp/") << fServerName;
560		rawResponse << ",digest-uri=" << '"' << digestUriValue << '"';
561		char sum[17], hex_digest2[33];
562		BString a1,a2,kd;
563		BString t1 = BString(login) << ":"
564				<< challengeMap["realm"] << ":"
565				<< password;
566		MD5Sum(sum, (unsigned char*)t1.String(), t1.Length());
567		a1 << sum << ":" << challengeMap["nonce"] << ":" << cnonce;
568		MD5Digest(hex_digest, (unsigned char*)a1.String(), a1.Length());
569		a2 << "AUTHENTICATE:" << digestUriValue;
570		MD5Digest(hex_digest2, (unsigned char*)a2.String(), a2.Length());
571		kd << hex_digest << ':' << challengeMap["nonce"]
572		   << ":" << "00000001" << ':' << cnonce << ':' << "auth"
573		   << ':' << hex_digest2;
574		MD5Digest(hex_digest, (unsigned char*)kd.String(), kd.Length());
575
576		rawResponse << ",response=" << hex_digest;
577		BString postResponse;
578		char *resp = postResponse.LockBuffer(rawResponse.Length() * 2 + 10);
579		baselen = ::encode_base64(resp, rawResponse.String(), rawResponse.Length(), true /* headerMode */);
580		resp[baselen] = 0;
581		postResponse.UnlockBuffer();
582		postResponse.Append(CRLF);
583
584		SendCommand(postResponse.String());
585
586		res = fLog.String();
587		if (atol(res) >= 500)
588			return B_ERROR;
589		// actually, we are supposed to check the rspauth sent back
590		// by the SMTP-server, but that isn't strictly required,
591		// so we skip that for now.
592		SendCommand(CRLF);	// finish off authentication
593		res = fLog.String();
594		if (atol(res) < 500)
595			return B_OK;
596	}
597	if (fAuthType & CRAM_MD5) {
598		//******* CRAM-MD5 Authentication ( tested. works fine [with Cyrus SASL] )
599		SendCommand("AUTH CRAM-MD5" CRLF);
600		const char *res = fLog.String();
601
602		if (strncmp(res, "334", 3) != 0)
603			return B_ERROR;
604		int32 baselen = ::strlen(&res[4]);
605		char *base = new char[baselen+1];
606		baselen = ::decode_base64(base, &res[4], baselen);
607		base[baselen] = '\0';
608
609		D(bug("base: %s\n", base));
610
611		::MD5HexHmac(hex_digest, (const unsigned char *)base, (int)baselen,
612			(const unsigned char *)password, (int)passlen);
613
614		D(bug("%s\n%s\n", base, hex_digest));
615
616		delete[] base;
617
618		BString preResponse, postResponse;
619		preResponse = login;
620		preResponse << " " << hex_digest << CRLF;
621		char *resp = postResponse.LockBuffer(preResponse.Length() * 2 + 10);
622		baselen = ::encode_base64(resp, preResponse.String(), preResponse.Length(), true /* headerMode */);
623		resp[baselen] = 0;
624		postResponse.UnlockBuffer();
625		postResponse.Append(CRLF);
626
627		SendCommand(postResponse.String());
628
629		res = fLog.String();
630		if (atol(res) < 500)
631			return B_OK;
632	}
633	if (fAuthType & LOGIN) {
634		//******* LOGIN Authentication ( tested. works fine)
635		ssize_t encodedsize; // required by our base64 implementation
636
637		SendCommand("AUTH LOGIN" CRLF);
638		const char *res = fLog.String();
639
640		if (strncmp(res, "334", 3) != 0)
641			return B_ERROR;
642
643		// Send login name as base64
644		char *login64 = new char[loginlen*3 + 6];
645		encodedsize = ::encode_base64(login64, (char *)login, loginlen, true /* headerMode */);
646		login64[encodedsize] = 0;
647		strcat (login64, CRLF);
648		SendCommand(login64);
649		delete[] login64;
650
651		res = fLog.String();
652		if (strncmp(res,"334",3) != 0)
653			return B_ERROR;
654
655		// Send password as base64
656		login64 = new char[passlen*3 + 6];
657		encodedsize = ::encode_base64(login64, (char *)password, passlen, true /* headerMode */);
658		login64[encodedsize] = 0;
659		strcat (login64, CRLF);
660		SendCommand(login64);
661		delete[] login64;
662
663		res = fLog.String();
664		if (atol(res) < 500)
665			return B_OK;
666	}
667	if (fAuthType & PLAIN) {
668		//******* PLAIN Authentication ( tested. works fine [with Cyrus SASL] )
669		// format is:
670		// 	authenticateID + \0 + username + \0 + password
671		// 	(where authenticateID is always empty !?!)
672		BString preResponse, postResponse;
673		char *stringPntr;
674		ssize_t encodedLength;
675		stringPntr = preResponse.LockBuffer(loginlen + passlen + 3);
676			// +3 to make room for the two \0-chars between the tokens and
677			// the final delimiter added by sprintf().
678		sprintf (stringPntr, "%c%s%c%s", 0, login, 0, password);
679		preResponse.UnlockBuffer(loginlen + passlen + 2);
680			// +2 in order to leave out the final delimiter (which is not part
681			// of the string).
682		stringPntr = postResponse.LockBuffer(preResponse.Length() * 3);
683		encodedLength = ::encode_base64(stringPntr, preResponse.String(),
684			preResponse.Length(), true /* headerMode */);
685		stringPntr[encodedLength] = 0;
686		postResponse.UnlockBuffer();
687		postResponse.Prepend("AUTH PLAIN ");
688		postResponse << CRLF;
689
690		SendCommand(postResponse.String());
691
692		const char *res = fLog.String();
693		if (atol(res) < 500)
694			return B_OK;
695	}
696	return B_ERROR;
697}
698
699
700void
701SMTPProtocol::Close()
702{
703
704	BString cmd = "QUIT";
705	cmd += CRLF;
706
707	if (SendCommand(cmd.String()) != B_OK) {
708		// Error
709	}
710
711	delete fSocket;
712}
713
714
715status_t
716SMTPProtocol::Send(const char* to, const char* from, BPositionIO *message)
717{
718	BString cmd = from;
719	cmd.Remove(0, cmd.FindFirst("\" <") + 2);
720	cmd.Prepend("MAIL FROM: ");
721	cmd += CRLF;
722	if (SendCommand(cmd.String()) != B_OK)
723		return B_ERROR;
724
725	int32 len = strlen(to);
726	BString addr("");
727	for (int32 i = 0;i < len;i++) {
728		char c = to[i];
729		if (c != ',')
730			addr += (char)c;
731		if (c == ','||i == len-1) {
732			if(addr.Length() == 0)
733				continue;
734			cmd = "RCPT TO: ";
735			cmd << addr.String() << CRLF;
736			if (SendCommand(cmd.String()) != B_OK)
737				return B_ERROR;
738
739			addr ="";
740		}
741	}
742
743	cmd = "DATA";
744	cmd += CRLF;
745	if (SendCommand(cmd.String()) != B_OK)
746		return B_ERROR;
747
748	// Send the message data.  Convert lines starting with a period to start
749	// with two periods and so on.  The actual sequence is CR LF Period.  The
750	// SMTP server will remove the periods.  Of course, the POP server may then
751	// add some of its own, but the POP client should take care of them.
752
753	ssize_t amountRead;
754	ssize_t amountToRead;
755	ssize_t amountUnread;
756	ssize_t bufferLen = 0;
757	const int bufferMax = 2000;
758	bool foundCRLFPeriod;
759	int i;
760	bool messageEndedWithCRLF = false;
761
762	message->Seek(0, SEEK_END);
763	amountUnread = message->Position();
764	message->Seek(0, SEEK_SET);
765	char *data = new char[bufferMax];
766
767	while (true) {
768		// Fill the buffer if it is getting low, but not every time, to avoid
769		// small reads.
770		if (bufferLen < bufferMax / 2) {
771			amountToRead = bufferMax - bufferLen;
772			if (amountToRead > amountUnread)
773				amountToRead = amountUnread;
774			if (amountToRead > 0) {
775				amountRead = message->Read (data + bufferLen, amountToRead);
776				if (amountRead <= 0 || amountRead > amountToRead)
777					amountUnread = 0; // Just stop reading when an error happens.
778				else {
779					amountUnread -= amountRead;
780					bufferLen += amountRead;
781				}
782			}
783		}
784
785		// Look for the next CRLFPeriod triple.
786		foundCRLFPeriod = false;
787		for (i = 0; i <= bufferLen - 3; i++) {
788			if (data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '.') {
789				foundCRLFPeriod = true;
790				// Send data up to the CRLF, and include the period too.
791				if (fSocket->Write(data, i + 3) < 0) {
792					amountUnread = 0; // Stop when an error happens.
793					bufferLen = 0;
794					break;
795				}
796				ReportProgress (i + 2 /* Don't include the double period here */,0);
797				// Move the data over in the buffer, but leave the period there
798				// so it gets sent a second time.
799				memmove(data, data + (i + 2), bufferLen - (i + 2));
800				bufferLen -= i + 2;
801				break;
802			}
803		}
804
805		if (!foundCRLFPeriod) {
806			if (amountUnread <= 0) { // No more data, all we have is in the buffer.
807				if (bufferLen > 0) {
808					fSocket->Write(data, bufferLen);
809					ReportProgress(bufferLen, 0);
810					if (bufferLen >= 2)
811						messageEndedWithCRLF = (data[bufferLen-2] == '\r' &&
812							data[bufferLen-1] == '\n');
813				}
814				break; // Finished!
815			}
816
817			// Send most of the buffer, except a few characters to overlap with
818			// the next read, in case the CRLFPeriod is split between reads.
819			if (bufferLen > 3) {
820				if (fSocket->Write(data, bufferLen - 3) < 0)
821					break; // Stop when an error happens.
822
823				ReportProgress(bufferLen - 3, 0);
824				memmove (data, data + bufferLen - 3, 3);
825				bufferLen = 3;
826			}
827		}
828	}
829	delete [] data;
830
831	if (messageEndedWithCRLF)
832		cmd = "." CRLF; // The standard says don't add extra CRLF.
833	else
834		cmd = CRLF "." CRLF;
835
836	if (SendCommand(cmd.String()) != B_OK)
837		return B_ERROR;
838
839	return B_OK;
840}
841
842
843//! Receives response from server.
844int32
845SMTPProtocol::ReceiveResponse(BString &out)
846{
847	out = "";
848	int32 len = 0,r;
849	char buf[SMTP_RESPONSE_SIZE];
850	bigtime_t timeout = 1000000*180; // timeout 180 secs
851	bool gotCode = false;
852	int32 errCode;
853	BString searchStr = "";
854
855	if (fSocket->WaitForReadable(timeout) == B_OK) {
856		while (1) {
857			r = fSocket->Read(buf, SMTP_RESPONSE_SIZE - 1);
858			if (r <= 0)
859				break;
860
861			if (!gotCode) {
862				if (buf[3] == ' ' || buf[3] == '-') {
863					errCode = atol(buf);
864					gotCode = true;
865					searchStr << errCode << ' ';
866				}
867			}
868
869			len += r;
870			out.Append(buf, r);
871
872			if (strstr(buf, CRLF) && (out.FindFirst(searchStr) != B_ERROR))
873				break;
874		}
875	} else
876		fLog = "SMTP socket timeout.";
877
878	D(bug("S:%s\n", out.String()));
879	return len;
880}
881
882
883// Sends SMTP command. Result kept in fLog
884
885status_t
886SMTPProtocol::SendCommand(const char *cmd)
887{
888	D(bug("C:%s\n", cmd));
889
890	if (fSocket->Write(cmd, ::strlen(cmd)) < 0)
891		return B_ERROR;
892	fLog = "";
893
894	// Receive
895	while (1) {
896		int32 len = ReceiveResponse(fLog);
897
898		if (len <= 0) {
899			D(bug("SMTP: len == %" B_PRId32 "\n", len));
900			return B_ERROR;
901		}
902
903		if (fLog.Length() > 4 && (fLog[3] == ' ' || fLog[3] == '-'))
904		{
905			int32 num = atol(fLog.String());
906			D(bug("ReplyNumber: %" B_PRId32 "\n", num));
907			if (num >= 500)
908				return B_ERROR;
909
910			break;
911		}
912	}
913
914	return B_OK;
915}
916
917
918// #pragma mark -
919
920
921extern "C" BOutboundMailProtocol*
922instantiate_outbound_protocol(const BMailAccountSettings& settings)
923{
924	return new SMTPProtocol(settings);
925}
926