/* * Copyright 2007-2016, Haiku, Inc. All rights reserved. * Copyright 2001-2002 Dr. Zoidberg Enterprises. All rights reserved. * Copyright 2011, Clemens Zeidler * * Distributed under the terms of the MIT License. */ //! POP3Protocol - implementation of the POP3 protocol #include "POP3.h" #include #include #include #include #include #include #include #include #include "md5.h" #include #include #include #include #include #include #include #include #include #include #include #include "crypt.h" #include "MailSettings.h" #include "MessageIO.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "pop3" #define POP3_RETRIEVAL_TIMEOUT 60000000 #define CRLF "\r\n" static void NotHere(BStringList& that, BStringList& otherList, BStringList* results) { for (int32 i = 0; i < otherList.CountStrings(); i++) { if (!that.HasString(otherList.StringAt(i))) results->Add(otherList.StringAt(i)); } } // #pragma mark - POP3Protocol::POP3Protocol(const BMailAccountSettings& settings) : BInboundMailProtocol("POP3", settings), fNumMessages(-1), fMailDropSize(0), fServerConnection(NULL) { printf("POP3Protocol::POP3Protocol(BMailAccountSettings* settings)\n"); fSettings = fAccountSettings.InboundSettings(); fUseSSL = fSettings.FindInt32("flavor") == 1 ? true : false; if (fSettings.FindString("destination", &fDestinationDir) != B_OK) fDestinationDir = "/boot/home/mail/in"; create_directory(fDestinationDir, 0777); fFetchBodyLimit = -1; if (fSettings.HasInt32("partial_download_limit")) fFetchBodyLimit = fSettings.FindInt32("partial_download_limit"); } POP3Protocol::~POP3Protocol() { Disconnect(); } status_t POP3Protocol::Connect() { status_t error = Open(fSettings.FindString("server"), fSettings.FindInt32("port"), fSettings.FindInt32("flavor")); if (error != B_OK) return error; char* password = get_passwd(&fSettings, "cpasswd"); error = Login(fSettings.FindString("username"), password, fSettings.FindInt32("auth_method")); delete[] password; if (error != B_OK) fServerConnection->Disconnect(); return error; } status_t POP3Protocol::Disconnect() { if (fServerConnection == NULL) return B_OK; SendCommand("QUIT" CRLF); fServerConnection->Disconnect(); delete fServerConnection; fServerConnection = NULL; return B_OK; } status_t POP3Protocol::SyncMessages() { bool leaveOnServer; if (fSettings.FindBool("leave_mail_on_server", &leaveOnServer) != B_OK) leaveOnServer = true; // create directory if not exist create_directory(fDestinationDir, 0777); printf("POP3Protocol::SyncMessages()\n"); _ReadManifest(); SetTotalItems(2); ReportProgress(1, 0, B_TRANSLATE("Connect to server" B_UTF8_ELLIPSIS)); status_t error = Connect(); if (error != B_OK) { printf("POP3 could not connect: %s\n", strerror(error)); ResetProgress(); return error; } ReportProgress(1, 0, B_TRANSLATE("Getting UniqueIDs" B_UTF8_ELLIPSIS)); error = _RetrieveUniqueIDs(); if (error < B_OK) { ResetProgress(); Disconnect(); return error; } BStringList toDownload; NotHere(fManifest, fUniqueIDs, &toDownload); int32 numMessages = toDownload.CountStrings(); if (numMessages == 0) { CheckForDeletedMessages(); ResetProgress(); Disconnect(); return B_OK; } ResetProgress(); SetTotalItems(toDownload.CountStrings()); SetTotalItemsSize(fTotalSize); printf("POP3: Messages to download: %i\n", (int)toDownload.CountStrings()); for (int32 i = 0; i < toDownload.CountStrings(); i++) { const char* uid = toDownload.StringAt(i); int32 toRetrieve = fUniqueIDs.IndexOf(uid); if (toRetrieve < 0) { // should not happen! error = B_NAME_NOT_FOUND; printf("POP3: uid %s index %i not found in fUniqueIDs!\n", uid, (int)toRetrieve); continue; } BPath path(fDestinationDir); BString fileName = "Downloading file... uid: "; fileName += uid; fileName.ReplaceAll("/", "_SLASH_"); path.Append(fileName); BEntry entry(path.Path()); BFile file(&entry, B_READ_WRITE | B_CREATE_FILE | B_ERASE_FILE); error = file.InitCheck(); if (error != B_OK) { printf("POP3: Can't create file %s\n ", path.Path()); break; } BMailMessageIO mailIO(this, &file, toRetrieve); BMessage attributes; entry_ref ref; entry.GetRef(&ref); int32 size = MessageSize(toRetrieve); if (fFetchBodyLimit < 0 || size <= fFetchBodyLimit) { error = mailIO.Seek(0, SEEK_END); if (error < 0) { printf("POP3: Failed to download body %s\n ", uid); break; } ProcessMessageFetched(ref, file, attributes); if (!leaveOnServer) Delete(toRetrieve); } else { int32 dummy; error = mailIO.ReadAt(0, &dummy, 1); if (error < 0) { printf("POP3: Failed to download header %s\n ", uid); break; } ProcessHeaderFetched(ref, file, attributes); } ReportProgress(1, 0); const BString uidStr(uid); if (file.WriteAttrString("MAIL:unique_id", &uidStr) < 0) error = B_ERROR; file.WriteAttr("MAIL:size", B_INT32_TYPE, 0, &size, sizeof(int32)); write_read_attr(file, B_UNREAD); // save manifest in case we get disturbed fManifest.Add(uid); _WriteManifest(); } ResetProgress(); CheckForDeletedMessages(); Disconnect(); return error; } status_t POP3Protocol::HandleFetchBody(const entry_ref& ref, const BMessenger& replyTo) { ResetProgress("Fetch body"); SetTotalItems(1); status_t error = Connect(); if (error != B_OK) return error; error = _RetrieveUniqueIDs(); if (error != B_OK) { Disconnect(); return error; } BFile file(&ref, B_READ_WRITE); status_t status = file.InitCheck(); if (status != B_OK) { Disconnect(); return status; } char uidString[256]; BNode node(&ref); if (node.ReadAttr("MAIL:unique_id", B_STRING_TYPE, 0, uidString, 256) < 0) { Disconnect(); return B_ERROR; } int32 toRetrieve = fUniqueIDs.IndexOf(uidString); if (toRetrieve < 0) { Disconnect(); return B_NAME_NOT_FOUND; } bool leaveOnServer; if (fSettings.FindBool("leave_mail_on_server", &leaveOnServer) != B_OK) leaveOnServer = true; // TODO: get rid of this BMailMessageIO! BMailMessageIO io(this, &file, toRetrieve); // read body status = io.Seek(0, SEEK_END); if (status < 0) { Disconnect(); return status; } BMessage attributes; NotifyBodyFetched(ref, file, attributes); ReplyBodyFetched(replyTo, ref, B_OK); if (!leaveOnServer) Delete(toRetrieve); ReportProgress(1, 0); ResetProgress(); Disconnect(); return B_OK; } status_t POP3Protocol::Open(const char* server, int port, int) { ReportProgress(0, 0, B_TRANSLATE("Connecting to POP3 server" B_UTF8_ELLIPSIS)); if (port <= 0) port = fUseSSL ? 995 : 110; fLog = ""; // Prime the error message BString errorMessage(B_TRANSLATE("Error while connecting to server %serv")); errorMessage.ReplaceFirst("%serv", server); if (port != 110) errorMessage << ":" << port; delete fServerConnection; fServerConnection = NULL; BNetworkAddress address(server, port); if (fUseSSL) fServerConnection = new(std::nothrow) BSecureSocket(address); else fServerConnection = new(std::nothrow) BSocket(address); status_t status = B_NO_MEMORY; if (fServerConnection != NULL) status = fServerConnection->InitCheck(); BString line; if (status == B_OK) { ssize_t length = ReceiveLine(line); if (length < 0) status = length; } if (status != B_OK) { fServerConnection->Disconnect(); errorMessage << ": " << strerror(status); ShowError(errorMessage.String()); return status; } if (strncmp(line.String(), "+OK", 3) != 0) { if (line.Length() > 0) { errorMessage << B_TRANSLATE(". The server said:\n") << line.String(); } else errorMessage << B_TRANSLATE(": No reply.\n"); ShowError(errorMessage.String()); fServerConnection->Disconnect(); return B_ERROR; } fLog = line; return B_OK; } status_t POP3Protocol::Login(const char* uid, const char* password, int method) { status_t err; BString errorMessage(B_TRANSLATE("Error while authenticating user %user")); errorMessage.ReplaceFirst("%user", uid); if (method == 1) { //APOP int32 index = fLog.FindFirst("<"); if(index != B_ERROR) { ReportProgress(0, 0, B_TRANSLATE("Sending APOP authentication" B_UTF8_ELLIPSIS)); int32 end = fLog.FindFirst(">", index); BString timestamp(""); fLog.CopyInto(timestamp, index, end - index + 1); timestamp += password; char md5sum[33]; MD5Digest((unsigned char*)timestamp.String(), md5sum); BString cmd = "APOP "; cmd += uid; cmd += " "; cmd += md5sum; cmd += CRLF; err = SendCommand(cmd.String()); if (err != B_OK) { errorMessage << B_TRANSLATE(". The server said:\n") << fLog; ShowError(errorMessage.String()); return err; } return B_OK; } else { errorMessage << B_TRANSLATE(": The server does not support APOP."); ShowError(errorMessage.String()); return B_NOT_ALLOWED; } } ReportProgress(0, 0, B_TRANSLATE("Sending username" B_UTF8_ELLIPSIS)); BString cmd = "USER "; cmd += uid; cmd += CRLF; err = SendCommand(cmd.String()); if (err != B_OK) { errorMessage << B_TRANSLATE(". The server said:\n") << fLog; ShowError(errorMessage.String()); return err; } ReportProgress(0, 0, B_TRANSLATE("Sending password" B_UTF8_ELLIPSIS)); cmd = "PASS "; cmd += password; cmd += CRLF; err = SendCommand(cmd.String()); if (err != B_OK) { errorMessage << B_TRANSLATE(". The server said:\n") << fLog; ShowError(errorMessage.String()); return err; } return B_OK; } status_t POP3Protocol::Stat() { ReportProgress(0, 0, B_TRANSLATE("Getting mailbox size" B_UTF8_ELLIPSIS)); if (SendCommand("STAT" CRLF) < B_OK) return B_ERROR; int32 messages; int32 dropSize; if (sscanf(fLog.String(), "+OK %" B_SCNd32" %" B_SCNd32, &messages, &dropSize) < 2) return B_ERROR; fNumMessages = messages; fMailDropSize = dropSize; return B_OK; } int32 POP3Protocol::Messages() { if (fNumMessages < 0) Stat(); return fNumMessages; } size_t POP3Protocol::MailDropSize() { if (fNumMessages < 0) Stat(); return fMailDropSize; } void POP3Protocol::CheckForDeletedMessages() { { // Delete things from the manifest no longer on the server BStringList list; NotHere(fUniqueIDs, fManifest, &list); fManifest.Remove(list); } if (!fSettings.FindBool("delete_remote_when_local") || fManifest.CountStrings() == 0) return; BStringList toDelete; BStringList queryContents; BVolumeRoster volumes; BVolume volume; while (volumes.GetNextVolume(&volume) == B_OK) { BQuery fido; entry_ref entry; fido.SetVolume(&volume); fido.PushAttr(B_MAIL_ATTR_ACCOUNT_ID); fido.PushInt32(fAccountSettings.AccountID()); fido.PushOp(B_EQ); fido.Fetch(); BString uid; while (fido.GetNextRef(&entry) == B_OK) { BNode(&entry).ReadAttrString("MAIL:unique_id", &uid); queryContents.Add(uid); } } NotHere(queryContents, fManifest, &toDelete); for (int32 i = 0; i < toDelete.CountStrings(); i++) { printf("delete mail on server uid %s\n", toDelete.StringAt(i).String()); Delete(fUniqueIDs.IndexOf(toDelete.StringAt(i))); } // Don't remove ids from fUniqueIDs, the indices have to stay the same when // retrieving new messages. fManifest.Remove(toDelete); // TODO: at some point the purged manifest should be written to disk // otherwise it will grow forever } status_t POP3Protocol::Retrieve(int32 message, BPositionIO* to) { BString cmd; cmd << "RETR " << message + 1 << CRLF; status_t status = RetrieveInternal(cmd.String(), message, to, true); ReportProgress(1, 0); if (status == B_OK) { // Check if the actual message size matches the expected one int32 size = MessageSize(message); to->Seek(0, SEEK_END); if (to->Position() != size) { printf("POP3Protocol::Retrieve Note: message size is %" B_PRIdOFF ", was expecting %" B_PRId32 ", for message #%" B_PRId32 ". Could be a transmission error or a bad POP server " "implementation (does it remove escape codes when it counts " "size?).\n", to->Position(), size, message); } } return status; } status_t POP3Protocol::GetHeader(int32 message, BPositionIO* to) { BString cmd; cmd << "TOP " << message + 1 << " 0" << CRLF; return RetrieveInternal(cmd.String(), message, to, false); } status_t POP3Protocol::RetrieveInternal(const char* command, int32 message, BPositionIO* to, bool postProgress) { const int bufSize = 1024 * 30; // To avoid waiting for the non-arrival of the next data packet, try to // receive only the message size, plus the 3 extra bytes for the ".\r\n" // after the message. Of course, if we get it wrong (or it is a huge // message or has lines starting with escaped periods), it will then switch // back to receiving full buffers until the message is done. int amountToReceive = MessageSize(message) + 3; if (amountToReceive >= bufSize || amountToReceive <= 0) amountToReceive = bufSize - 1; BString bufBString; // Used for auto-dealloc on return feature. char* buf = bufBString.LockBuffer(bufSize); int amountInBuffer = 0; int amountReceived; int testIndex; char* testStr; bool cont = true; bool flushWholeBuffer = false; to->Seek(0, SEEK_SET); if (SendCommand(command) != B_OK) return B_ERROR; while (cont) { status_t result = fServerConnection->WaitForReadable( POP3_RETRIEVAL_TIMEOUT); if (result == B_TIMED_OUT) { // No data available, even after waiting a minute. fLog = "POP3 timeout - no data received after a long wait."; return B_TIMED_OUT; } if (amountToReceive > bufSize - 1 - amountInBuffer) amountToReceive = bufSize - 1 - amountInBuffer; amountReceived = fServerConnection->Read(buf + amountInBuffer, amountToReceive); if (amountReceived < 0) { fLog = strerror(amountReceived); return amountReceived; } if (amountReceived == 0) { fLog = "POP3 data supposedly ready to receive but not received!"; return B_ERROR; } amountToReceive = bufSize - 1; // For next time, read a full buffer. amountInBuffer += amountReceived; buf[amountInBuffer] = 0; // NUL stops tests past the end of buffer. // Look for lines starting with a period. A single period by itself on // a line "\r\n.\r\n" marks the end of the message (thus the need for // at least five characters in the buffer for testing). A period // "\r\n.Stuff" at the start of a line get deleted "\r\nStuff", since // POP adds one as an escape code to let you have message text with // lines starting with a period. For convenience, assume that no // messages start with a period on the very first line, so we can // search for the previous line's "\r\n". for (testIndex = 0; testIndex <= amountInBuffer - 5; testIndex++) { testStr = buf + testIndex; if (testStr[0] == '\r' && testStr[1] == '\n' && testStr[2] == '.') { if (testStr[3] == '\r' && testStr[4] == '\n') { // Found the end of the message marker. // Ignore remaining data. if (amountInBuffer > testIndex + 5) { printf("POP3Protocol::RetrieveInternal Ignoring %d " "bytes of extra data past message end.\n", amountInBuffer - (testIndex + 5)); } amountInBuffer = testIndex + 2; // Don't include ".\r\n". buf[amountInBuffer] = 0; cont = false; } else { // Remove an extra period at the start of a line. // Inefficient, but it doesn't happen often that you have a // dot starting a line of text. Of course, a file with a // lot of double period lines will get processed very // slowly. memmove(buf + testIndex + 2, buf + testIndex + 3, amountInBuffer - (testIndex + 3) + 1); amountInBuffer--; // Watch out for the end of buffer case, when the POP text // is "\r\n..X". Don't want to leave the resulting // "\r\n.X" in the buffer (flush out the whole buffer), // since that will get mistakenly evaluated again in the // next loop and delete a character by mistake. if (testIndex >= amountInBuffer - 4 && testStr[2] == '.') { printf("POP3Protocol::RetrieveInternal: Jackpot! " "You have hit the rare situation with an escaped " "period at the end of the buffer. Aren't you happy" "it decodes it correctly?\n"); flushWholeBuffer = true; } } } } if (cont && !flushWholeBuffer) { // Dump out most of the buffer, but leave the last 4 characters for // comparison continuity, in case the line starting with a period // crosses a buffer boundary. if (amountInBuffer > 4) { to->Write(buf, amountInBuffer - 4); if (postProgress) ReportProgress(0, amountInBuffer - 4); memmove(buf, buf + amountInBuffer - 4, 4); amountInBuffer = 4; } } else { // Dump everything - end of message or flushing the whole buffer. to->Write(buf, amountInBuffer); if (postProgress) ReportProgress(0, amountInBuffer); amountInBuffer = 0; } } return B_OK; } void POP3Protocol::Delete(int32 index) { BString cmd = "DELE "; cmd << (index + 1) << CRLF; if (SendCommand(cmd.String()) != B_OK) { // Error } #if DEBUG puts(fLog.String()); #endif // The mail is just marked as deleted and removed from the server when // sending the QUIT command. Because of that the message number stays // the same and we keep the uid in the uid list. } size_t POP3Protocol::MessageSize(int32 index) { return fSizes[index]; } ssize_t POP3Protocol::ReceiveLine(BString& line) { int32 length = 0; bool flag = false; line = ""; status_t result = fServerConnection->WaitForReadable( POP3_RETRIEVAL_TIMEOUT); if (result == B_TIMED_OUT) return errno; while (true) { // Hope there's an end of line out there else this gets stuck. int32 bytesReceived; uint8 c = 0; bytesReceived = fServerConnection->Read((char*)&c, 1); if (bytesReceived < 0) return bytesReceived; if (c == '\n' || bytesReceived == 0) break; if (c == '\r') { flag = true; } else { if (flag) { length++; line += '\r'; flag = false; } length++; line += (char)c; } } return length; } status_t POP3Protocol::SendCommand(const char* cmd) { // Flush any accumulated garbage data before we send our command, so we // don't misinterrpret responses from previous commands (that got left over // due to bugs) as being from this command. while (fServerConnection->WaitForReadable(1000) == B_OK) { char buffer[4096]; ssize_t amountReceived = fServerConnection->Read(buffer, sizeof(buffer) - 1); if (amountReceived < 0) return amountReceived; buffer[amountReceived] = 0; printf("POP3Protocol::SendCommand Bug! Had to flush %" B_PRIdSSIZE " bytes: %s\n", amountReceived, buffer); } if (fServerConnection->Write(cmd, ::strlen(cmd)) < 0) { fLog = strerror(errno); printf("POP3Protocol::SendCommand Send \"%s\" failed, code %d: %s\n", cmd, errno, fLog.String()); return errno; } fLog = ""; int32 length = ReceiveLine(fLog); if (length <= 0 || fLog.ICompare("+OK", 3) == 0) return B_OK; if (fLog.ICompare("-ERR", 4) == 0) { printf("POP3Protocol::SendCommand \"%s\" got error message " "from server: %s\n", cmd, fLog.String()); return B_ERROR; } printf("POP3Protocol::SendCommand \"%s\" got nonsense message " "from server: %s\n", cmd, fLog.String()); return B_BAD_DATA; // If it's not +OK, and it's not -ERR, then what the heck // is it? Presume an error } void POP3Protocol::MD5Digest(unsigned char* in, char* asciiDigest) { unsigned char digest[16]; MD5_CTX context; MD5Init(&context); MD5Update(&context, in, ::strlen((char*)in)); MD5Final(digest, &context); for (int i = 0; i < 16; i++) { sprintf(asciiDigest + 2 * i, "%02x", digest[i]); } return; } status_t POP3Protocol::_RetrieveUniqueIDs() { fUniqueIDs.MakeEmpty(); fSizes.clear(); fTotalSize = 0; status_t status = SendCommand("UIDL" CRLF); if (status != B_OK) return status; BString result; int32 uidOffset; while (ReceiveLine(result) > 0) { if (result.ByteAt(0) == '.') break; uidOffset = result.FindFirst(' ') + 1; result.Remove(0, uidOffset); fUniqueIDs.Add(result); } if (SendCommand("LIST" CRLF) != B_OK) return B_ERROR; while (ReceiveLine(result) > 0) { if (result.ByteAt(0) == '.') break; int32 index = result.FindLast(" "); int32 size; if (index >= 0) size = atol(&result.String()[index]); else size = 0; fTotalSize += size; fSizes.push_back(size); } return B_OK; } void POP3Protocol::_ReadManifest() { fManifest.MakeEmpty(); BString attribute = "MAIL:"; attribute << fAccountSettings.AccountID() << ":manifest"; // In case someone puts multiple accounts in the same directory BNode node(fDestinationDir); if (node.InitCheck() != B_OK) return; // We already have a directory so we can try to read metadata // from it. Note that it is normal for this directory not to // be found on the first run as it will be later created by // the INBOX system filter. attr_info info; if (node.GetAttrInfo(attribute.String(), &info) != B_OK || info.size == 0) return; void* flatmanifest = malloc(info.size); node.ReadAttr(attribute.String(), fManifest.TypeCode(), 0, flatmanifest, info.size); fManifest.Unflatten(fManifest.TypeCode(), flatmanifest, info.size); free(flatmanifest); } void POP3Protocol::_WriteManifest() { BString attribute = "MAIL:"; attribute << fAccountSettings.AccountID() << ":manifest"; // In case someone puts multiple accounts in the same directory BNode node(fDestinationDir); if (node.InitCheck() != B_OK) { ShowError("Error while saving account manifest: cannot use " "destination directory."); return; } node.RemoveAttr(attribute.String()); ssize_t manifestsize = fManifest.FlattenedSize(); void* flatmanifest = malloc(manifestsize); fManifest.Flatten(flatmanifest, manifestsize); status_t err = node.WriteAttr(attribute.String(), fManifest.TypeCode(), 0, flatmanifest, manifestsize); if (err < 0) { BString error = "Error while saving account manifest: "; error << strerror(err); printf("moep error\n"); ShowError(error.String()); } free(flatmanifest); } // #pragma mark - BInboundMailProtocol* instantiate_inbound_protocol(const BMailAccountSettings& settings) { return new POP3Protocol(settings); } status_t pop3_smtp_auth(const BMailAccountSettings& settings) { POP3Protocol protocol(settings); protocol.Connect(); protocol.Disconnect(); return B_OK; }