diff -x __pycache__ -ru thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.cpp thunderbird/thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.cpp --- thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.cpp 2025-05-13 15:22:25.000000000 +0200 +++ thunderbird/thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.cpp 2025-08-21 03:49:50.217872643 +0200 @@ -41,6 +41,15 @@ #include "mozilla/Unused.h" #include "nsIUUIDGenerator.h" #include "nsIArray.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsIProcess.h" +#include "mozilla/RandomNum.h" +#include "nsIServerSocket.h" +#include "nsIAsyncInputStream.h" +#include "nsISocketTransport.h" +#include "nsIBinaryInputStream.h" +#include "nsIObjectInputStream.h" +#include "nsDirectoryServiceDefs.h" #define PORT_NOT_SET -1 @@ -707,12 +716,241 @@ return NS_OK; } +nsresult +nsMsgIncomingServer::GetPasswordCommand(nsTArray &passwordCommand) { + nsString serializedCommand; + + nsresult rv = GetUnicharValue("passwordCommand", serializedCommand); + NS_ENSURE_SUCCESS(rv, rv); + + if (serializedCommand.Length() == 0) + return NS_OK; + + // Serialization is achieved by joining the arguments with a comma. + // Commas are themselves allowed to be backslash-espaced. + + nsString currentArgument; + bool nextShouldBeEscaped = false; + + for (unsigned int i = 0; i < serializedCommand.Length(); i++) { + char c = serializedCommand[i]; + + switch (c) { + case ',': + if (nextShouldBeEscaped) { + currentArgument.Append(','); + nextShouldBeEscaped = false; + } else { + passwordCommand.AppendElement(currentArgument); + currentArgument.Truncate(); + } + break; + + case '\\': + if (nextShouldBeEscaped) + currentArgument.Append(','); + + nextShouldBeEscaped = !nextShouldBeEscaped; + break; + + default: + currentArgument.Append(c); + break; + } + } + + if (nextShouldBeEscaped) + currentArgument.Append('\\'); + passwordCommand.AppendElement(currentArgument); + + return NS_OK; +} + +class PasswordCommandObserver final + : public nsIObserver, + public nsIServerSocketListener, + public nsIInputStreamCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSISERVERSOCKETLISTENER + NS_DECL_NSIINPUTSTREAMCALLBACK + + private: + bool isCommandDone; + bool passwordReceived; + nsAString& password; + nsTArray received; + + public: + PasswordCommandObserver(nsAString& password); + bool IsDone(); + private: + ~PasswordCommandObserver(); +}; + +NS_IMPL_ISUPPORTS(PasswordCommandObserver, nsIObserver, nsIServerSocketListener, nsIInputStreamCallback) + +PasswordCommandObserver::PasswordCommandObserver(nsAString& password) : password(password) { + isCommandDone = false; + passwordReceived = false; +} +PasswordCommandObserver::~PasswordCommandObserver() {} + +bool +PasswordCommandObserver::IsDone() { + return isCommandDone && passwordReceived; +} + +NS_IMETHODIMP +PasswordCommandObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + isCommandDone = true; + + return strcmp(aTopic, "process-failed") == 0 ? NS_ERROR_FAILURE : NS_OK; +} + +NS_IMETHODIMP +PasswordCommandObserver::OnSocketAccepted(nsIServerSocket *aServ, nsISocketTransport *aTransport) { + nsresult rv; + + // The socket can be closed. This does not close the existing connection. + rv = aServ->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stream; + rv = aTransport->OpenInputStream(0, 0, 0, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr astream; + astream = do_QueryInterface(stream); + rv = astream->AsyncWait(this, 0, 0, NS_GetCurrentThread()); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +PasswordCommandObserver::OnStopListening(nsIServerSocket *aServ, nsresult aStatus) { + return NS_OK; +} + +NS_IMETHODIMP +PasswordCommandObserver::OnInputStreamReady(nsIAsyncInputStream *aStream) { + nsresult rv; + + int64_t len; + rv = aStream->Available((uint64_t*)&len); + if (NS_FAILED(rv)) + // note: aStream->Available() can also return -1. + len = -1; + + if (len == -1) { + password.Assign(NS_ConvertUTF8toUTF16((char*)received.Elements(), received.Length())); + passwordReceived = true; + } else { + if (len > 0) { + nsCOMPtr bin = NS_NewObjectInputStream(aStream); + nsTArray data; + rv = bin->ReadByteArray(len, data); + NS_ENSURE_SUCCESS(rv, rv); + received.AppendElements(data); + } + + rv = aStream->AsyncWait(this, 0, 0, NS_GetCurrentThread()); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsMsgIncomingServer::RunPasswordCommand(nsTArray &passwordCommand, nsAString& password) { + nsresult rv; + + nsCOMPtr socketFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(socketFile)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString tempSocketName; + uint64_t randomSocketName = mozilla::RandomUint64().valueOrFrom([] { + return 0; + }); + const char* alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + tempSocketName.AppendPrintf( + "tmp-%c%c%c.sock", + alphabet[randomSocketName % 36], + alphabet[(randomSocketName / 36) % 36], + alphabet[(randomSocketName / (36*36)) % 36] + ); + socketFile->AppendRelativePath(tempSocketName); + + nsCOMPtr socket = + do_CreateInstance(NS_SERVERSOCKET_CONTRACTID); + socket->InitWithFilename(socketFile, 0600, -1); + + auto executable = passwordCommand[0]; + nsCOMPtr executableFile = + do_CreateInstance("@mozilla.org/file/local;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + executableFile->InitWithPath(executable); + + nsCOMPtr process = + do_CreateInstance("@mozilla.org/process/util;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = process->Init(executableFile); + NS_ENSURE_SUCCESS(rv, rv); + + auto observer = new PasswordCommandObserver(password); + + socket->AsyncListen(observer); + + auto argsCount = passwordCommand.Length(); + const char** args = new const char*[argsCount]; + for (unsigned int i = 0; i < argsCount - 1; i++) { + NS_ConvertUTF16toUTF8 argUtf8(passwordCommand[i + 1]); + args[i] = strdup(argUtf8.get()); + } + nsString socketPath; + rv = socketFile->GetPath(socketPath); + NS_ENSURE_SUCCESS(rv, rv); + NS_ConvertUTF16toUTF8 socketPathUtf8(socketPath); + args[argsCount - 1] = strdup(socketPathUtf8.get()); + + rv = process->RunAsync(args, argsCount, observer, false); + if (NS_FAILED(rv)) + return rv; + + mozilla::SpinEventLoopUntil("nsMsgIncomingServer::RunPasswordCommand"_ns, [&]() { + return observer->IsDone(); + }); + + for (unsigned int i = 0; i < argsCount; i++) + free((void*)args[i]); + delete[] args; + + return NS_OK; +} + NS_IMETHODIMP nsMsgIncomingServer::GetPasswordWithUI(const nsAString& aPromptMessage, const nsAString& aPromptTitle, nsAString& aPassword) { nsresult rv = NS_OK; + nsTArray passwordCommand; + rv = GetPasswordCommand(passwordCommand); + NS_ENSURE_SUCCESS(rv, rv); + + if (passwordCommand.Length() > 0) { + rv = RunPasswordCommand(passwordCommand, aPassword); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetPassword(aPassword); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + if (m_password.IsEmpty()) { // let's see if we have the password in the password manager and // can avoid this prompting thing. This makes it easier to get embedders diff -x __pycache__ -ru thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.h thunderbird/thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.h --- thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.h 2025-05-13 15:22:25.000000000 +0200 +++ thunderbird/thunderbird-128.10.1/comm/mailnews/base/src/nsMsgIncomingServer.h 2025-08-19 12:39:19.236805257 +0200 @@ -48,6 +48,9 @@ nsCString m_serverKey; bool m_hasShutDown; + nsresult GetPasswordCommand(nsTArray &passwordCommand); + nsresult RunPasswordCommand(nsTArray &passwordCommand, nsAString& password); + // Sets m_password, if password found. Can return NS_ERROR_ABORT if the // user cancels the master password dialog. nsresult GetPasswordWithoutUI();