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>
6 * Distributed under the terms of the MIT License.
10 //! Implementation of the SMTP protocol
29 #include <MenuField.h>
33 #include <SecureSocket.h>
34 #include <TextControl.h>
37 #include <mail_encoding.h>
38 #include <MailSettings.h>
39 #include <NodeMessage.h>
40 #include <ProtocolConfigView.h>
45 #undef B_TRANSLATION_CONTEXT
46 #define B_TRANSLATION_CONTEXT "smtp"
50 #define SMTP_RESPONSE_SIZE 8192
61 // Authentication types recognized. Not all methods are implemented.
74 ** taken from the file rfc2104.txt
75 ** written by Martin Schaaf <mascha@ma-scha.de>
78 MD5Hmac(unsigned char *digest
, const unsigned char* text
, int text_len
,
79 const unsigned char* key
, int key_len
)
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
88 /* start out by storing key in pads */
89 memset(k_ipad
, 0, sizeof k_ipad
);
90 memset(k_opad
, 0, sizeof k_opad
);
92 /* if key is longer than 64 bytes reset it to key=MD5(key) */
96 MD5_Update(&tctx
, (unsigned char*)key
, key_len
);
97 MD5_Final(k_ipad
, &tctx
);
98 MD5_Final(k_opad
, &tctx
);
100 memcpy(k_ipad
, key
, key_len
);
101 memcpy(k_opad
, key
, key_len
);
105 * the HMAC_MD5 transform looks like:
107 * MD5(K XOR opad, MD5(K XOR ipad, text))
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
115 /* XOR key with ipad and opad values */
116 for (i
= 0; i
< 64; i
++) {
124 MD5_Init(&context
); /* init context for 1st
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 */
132 MD5_Init(&context
); /* init context for 2nd
134 MD5_Update(&context
, k_opad
, 64); /* start with outer pad */
135 MD5_Update(&context
, digest
, 16); /* then results of 1st
137 MD5_Final(digest
, &context
); /* finish up 2nd pass */
142 MD5HexHmac(char *hexdigest
, const unsigned char* text
, int text_len
,
143 const unsigned char* key
, int key_len
)
145 unsigned char digest
[16];
149 MD5Hmac(digest
, text
, text_len
, key
, key_len
);
150 for (i
= 0; i
< 16; i
++) {
152 *hexdigest
++ = (c
> 0x9F ? 'a'-10 : '0')+(c
>>4);
153 *hexdigest
++ = ((c
&0x0F) > 9 ? 'a'-10 : '0')+(c
&0x0F);
161 ** generates an MD5-sum from the given string
164 MD5Sum (char* sum
, unsigned char *text
, int text_len
) {
167 MD5_Update(&context
, text
, text_len
);
168 MD5_Final((unsigned char*)sum
, &context
);
173 ** Function: MD5Digest
174 ** generates an MD5-digest from the given string
176 void MD5Digest (char* hexdigest
, unsigned char *text
, int text_len
) {
178 unsigned char digest
[17];
181 MD5Sum((char*)digest
, text
, text_len
);
183 for (i
= 0; i
< 16; i
++) {
185 *hexdigest
++ = (c
> 0x9F ? 'a'-10 : '0')+(c
>>4);
186 *hexdigest
++ = ((c
&0x0F) > 9 ? 'a'-10 : '0')+(c
&0x0F);
192 ** Function: SplitChallengeIntoMap
193 ** splits a challenge-string into the given map (see RFC-2831)
197 SplitChallengeIntoMap(BString str
, map
<BString
,BString
>& m
)
202 char* s
= (char*)str
.String();
224 while(*s
!=0 && *s
!=',' && !isspace(*s
))
243 SMTPProtocol::SMTPProtocol(const BMailAccountSettings
& settings
)
245 BOutboundMailProtocol("SMTP", settings
),
248 fSettingsMessage
= settings
.OutboundSettings();
252 SMTPProtocol::~SMTPProtocol()
258 SMTPProtocol::Connect()
260 BString errorMessage
;
261 int32 authMethod
= fSettingsMessage
.FindInt32("auth_method");
263 status_t status
= B_ERROR
;
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();
271 errorMessage
<< B_TRANSLATE("POP3 authentication failed. The "
272 "server said:\n") << fLog
;
273 ShowError(errorMessage
.String());
278 status
= Open(fSettingsMessage
.FindString("server"),
279 fSettingsMessage
.FindInt32("port"), authMethod
== 1);
281 errorMessage
<< B_TRANSLATE("Error while opening connection to %serv");
282 errorMessage
.ReplaceFirst("%serv",
283 fSettingsMessage
.FindString("server"));
285 if (fSettingsMessage
.FindInt32("port") > 0)
286 errorMessage
<< ":" << fSettingsMessage
.FindInt32("port");
288 if (fLog
.Length() > 0)
289 errorMessage
<< B_TRANSLATE(". The server says:\n") << fLog
;
291 errorMessage
<< ". " << strerror(status
);
294 ShowError(errorMessage
.String());
299 const char* password
= get_passwd(&fSettingsMessage
, "cpasswd");
300 status
= Login(fSettingsMessage
.FindString("username"), password
);
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"));
309 ShowError(errorMessage
.String());
316 SMTPProtocol::Disconnect()
322 //! Process EMail to be sent
324 SMTPProtocol::HandleSendMessages(const BMessage
& message
, off_t totalBytes
)
328 status_t status
= message
.GetInfo("ref", &type
, &count
);
332 // TODO: sort out already sent messages -- the request could
333 // be issued while we're busy sending them already
335 SetTotalItems(count
);
336 SetTotalItemsSize(totalBytes
);
343 for (int32 i
= 0; message
.FindRef("ref", i
++, &ref
) == B_OK
;) {
344 status
= _SendMessage(ref
);
345 if (status
!= B_OK
) {
347 error
<< "An error occurred while sending the message "
348 << ref
.name
<< " (" << strerror(status
) << "):\n" << fLog
;
349 ShowError(error
.String());
361 //! Opens connection to server
363 SMTPProtocol::Open(const char *address
, int port
, bool esmtp
)
365 ReportProgress(0, 0, B_TRANSLATE("Connecting to server" B_UTF8_ELLIPSIS
));
367 use_ssl
= (fSettingsMessage
.FindInt32("flavor") == 1);
370 port
= use_ssl
? 465 : 25;
372 BNetworkAddress
addr(address
);
373 if (addr
.InitCheck() != B_OK
) {
375 str
.SetToFormat("Invalid network address for SMTP server: %s",
376 strerror(addr
.InitCheck()));
377 ShowError(str
.String());
378 return addr
.InitCheck();
381 if (addr
.Port() == 0)
385 fSocket
= new(std::nothrow
) BSecureSocket
;
387 fSocket
= new(std::nothrow
) BSocket
;
392 if (fSocket
->Connect(addr
) != B_OK
) {
394 error
<< "Could not connect to SMTP server "
395 << fSettingsMessage
.FindString("server");
396 error
<< ":" << addr
.Port();
397 ShowError(error
.String());
403 ReceiveResponse(line
);
406 gethostname(localhost
, 255);
408 if (localhost
[0] == 0)
409 strcpy(localhost
, "namethisbebox");
411 char *cmd
= new char[::strlen(localhost
)+8];
413 ::sprintf(cmd
,"HELO %s" CRLF
, localhost
);
415 ::sprintf(cmd
,"EHLO %s" CRLF
, localhost
);
417 if (SendCommand(cmd
) != B_OK
) {
426 const char *res
= fLog
.String();
428 if ((p
= ::strstr(res
, "250-AUTH")) != NULL
) {
429 if(::strstr(p
, "LOGIN"))
431 if(::strstr(p
, "PLAIN"))
433 if(::strstr(p
, "CRAM-MD5"))
434 fAuthType
|= CRAM_MD5
;
435 if(::strstr(p
, "DIGEST-MD5")) {
436 fAuthType
|= DIGEST_MD5
;
437 fServerName
= address
;
446 SMTPProtocol::_SendMessage(const entry_ref
& ref
)
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();
457 const char *from
= header
.FindString("MAIL:from");
458 const char *to
= header
.FindString("MAIL:recipients");
460 to
= header
.FindString("MAIL:to");
462 if (to
== NULL
|| from
== NULL
) {
463 fLog
= "Invalid message headers";
467 NotifyMessageReadyToSend(ref
, file
);
468 status
= Send(to
, from
, &file
);
471 NotifyMessageSent(ref
, file
);
475 ReportProgress(size
, 1);
482 SMTPProtocol::_POP3Authentication()
484 const entry_ref
& entry
= fAccountSettings
.InboundAddOnRef();
485 if (strcmp(entry
.name
, "POP3") != 0)
488 status_t (*pop3_smtp_auth
)(const BMailAccountSettings
&);
491 image_id image
= load_add_on(path
.Path());
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
);
500 status_t status
= (*pop3_smtp_auth
)(fAccountSettings
);
501 unload_add_on(image
);
507 SMTPProtocol::Login(const char *_login
, const char *password
)
512 const char *login
= _login
;
516 int32 loginlen
= ::strlen(login
);
517 int32 passlen
= ::strlen(password
);
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();
528 if (strncmp(res
, "334", 3) != 0)
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';
535 D(bug("base: %s\n", base
));
537 map
<BString
,BString
> challengeMap
;
538 SplitChallengeIntoMap(base
, challengeMap
);
542 BString rawResponse
= BString("username=") << '"' << login
<< '"';
543 rawResponse
<< ",realm=" << '"' << challengeMap
["realm"] << '"';
544 rawResponse
<< ",nonce=" << '"' << challengeMap
["nonce"] << '"';
545 rawResponse
<< ",nc=00000001";
547 for( int i
=0; i
<32; ++i
)
548 temp
[i
] = 1+(rand()%254);
550 BString
rawCnonce(temp
);
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];
562 BString t1
= BString(login
) << ":"
563 << challengeMap
["realm"] << ":"
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());
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 */);
580 postResponse
.UnlockBuffer();
581 postResponse
.Append(CRLF
);
583 SendCommand(postResponse
.String());
586 if (atol(res
) >= 500)
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
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();
601 if (strncmp(res
, "334", 3) != 0)
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';
608 D(bug("base: %s\n", base
));
610 ::MD5HexHmac(hex_digest
, (const unsigned char *)base
, (int)baselen
,
611 (const unsigned char *)password
, (int)passlen
);
613 D(bug("%s\n%s\n", base
, hex_digest
));
617 BString preResponse
, postResponse
;
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 */);
623 postResponse
.UnlockBuffer();
624 postResponse
.Append(CRLF
);
626 SendCommand(postResponse
.String());
632 if (fAuthType
& LOGIN
) {
633 //******* LOGIN Authentication ( tested. works fine)
634 ssize_t encodedsize
; // required by our base64 implementation
636 SendCommand("AUTH LOGIN" CRLF
);
637 const char *res
= fLog
.String();
639 if (strncmp(res
, "334", 3) != 0)
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
);
651 if (strncmp(res
,"334",3) != 0)
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
);
666 if (fAuthType
& PLAIN
) {
667 //******* PLAIN Authentication ( tested. works fine [with Cyrus SASL] )
669 // authenticateID + \0 + username + \0 + password
670 // (where authenticateID is always empty !?!)
671 BString preResponse
, postResponse
;
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
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
;
689 SendCommand(postResponse
.String());
691 const char *res
= fLog
.String();
700 SMTPProtocol::Close()
703 BString cmd
= "QUIT";
706 if (SendCommand(cmd
.String()) != B_OK
) {
715 SMTPProtocol::Send(const char* to
, const char* from
, BPositionIO
*message
)
718 cmd
.Remove(0, cmd
.FindFirst("\" <") + 2);
719 cmd
.Prepend("MAIL FROM: ");
721 if (SendCommand(cmd
.String()) != B_OK
)
724 int32 len
= strlen(to
);
726 for (int32 i
= 0;i
< len
;i
++) {
730 if (c
== ','||i
== len
-1) {
731 if(addr
.Length() == 0)
734 cmd
<< addr
.String() << CRLF
;
735 if (SendCommand(cmd
.String()) != B_OK
)
744 if (SendCommand(cmd
.String()) != B_OK
)
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.
753 ssize_t amountToRead
;
754 ssize_t amountUnread
;
755 ssize_t bufferLen
= 0;
756 const int bufferMax
= 2000;
757 bool foundCRLFPeriod
;
759 bool messageEndedWithCRLF
= false;
761 message
->Seek(0, SEEK_END
);
762 amountUnread
= message
->Position();
763 message
->Seek(0, SEEK_SET
);
764 char *data
= new char[bufferMax
];
767 // Fill the buffer if it is getting low, but not every time, to avoid
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.
778 amountUnread
-= amountRead
;
779 bufferLen
+= amountRead
;
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.
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));
804 if (!foundCRLFPeriod
) {
805 if (amountUnread
<= 0) { // No more data, all we have is in the buffer.
807 fSocket
->Write(data
, bufferLen
);
808 ReportProgress(bufferLen
, 0);
810 messageEndedWithCRLF
= (data
[bufferLen
-2] == '\r' &&
811 data
[bufferLen
-1] == '\n');
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.
819 if (fSocket
->Write(data
, bufferLen
- 3) < 0)
820 break; // Stop when an error happens.
822 ReportProgress(bufferLen
- 3, 0);
823 memmove (data
, data
+ bufferLen
- 3, 3);
830 if (messageEndedWithCRLF
)
831 cmd
= "." CRLF
; // The standard says don't add extra CRLF.
835 if (SendCommand(cmd
.String()) != B_OK
)
842 //! Receives response from server.
844 SMTPProtocol::ReceiveResponse(BString
&out
)
848 char buf
[SMTP_RESPONSE_SIZE
];
849 bigtime_t timeout
= 1000000*180; // timeout 180 secs
850 bool gotCode
= false;
852 BString searchStr
= "";
854 if (fSocket
->WaitForReadable(timeout
) == B_OK
) {
856 r
= fSocket
->Read(buf
, SMTP_RESPONSE_SIZE
- 1);
861 if (buf
[3] == ' ' || buf
[3] == '-') {
864 searchStr
<< errCode
<< ' ';
871 if (strstr(buf
, CRLF
) && (out
.FindFirst(searchStr
) != B_ERROR
))
875 fLog
= "SMTP socket timeout.";
877 D(bug("S:%s\n", out
.String()));
882 // Sends SMTP command. Result kept in fLog
885 SMTPProtocol::SendCommand(const char *cmd
)
887 D(bug("C:%s\n", cmd
));
889 if (fSocket
->Write(cmd
, ::strlen(cmd
)) < 0)
895 int32 len
= ReceiveResponse(fLog
);
898 D(bug("SMTP: len == %" B_PRId32
"\n", len
));
902 if (fLog
.Length() > 4 && (fLog
[3] == ' ' || fLog
[3] == '-'))
904 int32 num
= atol(fLog
.String());
905 D(bug("ReplyNumber: %" B_PRId32
"\n", num
));
920 extern "C" BOutboundMailProtocol
*
921 instantiate_outbound_protocol(const BMailAccountSettings
& settings
)
923 return new SMTPProtocol(settings
);