From ccd265eb658335fc500cfaceb822b9ab29e8093d Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Sun, 11 Oct 2026 17:33:42 +0200 Subject: [PATCH 1/6] CI: add a native Windows build job (MinGW, OpenSSL, ICU) Build and run the test suite on Windows with MSYS2 (UCRT64), using the Windows platform handler, which was not covered by any CI job. --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6122247b..0cbefbc2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,28 @@ jobs: make install make test + build-windows: + name: build-windows (mingw, openssl, icu) + runs-on: windows-latest + steps: + - name: git-config + run: git config --global core.autocrlf input + - name: install-deps + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + pacboy: cmake:p cppunit:p gcc:p gsasl:p icu:p ninja:p openssl:p pkgconf:p + - name: clone-vmime + uses: actions/checkout@v4 + - name: buildall + shell: msys2 {0} + run: | + set -ex + cmake . -G Ninja -DVMIME_HAVE_MESSAGING_PROTO_SENDMAIL:BOOL=OFF -DVMIME_HAVE_TLS_SUPPORT:BOOL=ON -DVMIME_TLS_SUPPORT_LIB:STRING=openssl -DVMIME_CHARSETCONV_LIB:STRING=icu -DVMIME_BUILD_TESTS=ON -DVMIME_BUILD_SAMPLES=ON + cmake --build . + ctest --output-on-failure + book: name: book (pdf) runs-on: ubuntu-26.04 From baf81fdd5959ac9393f5f7b077376e62f865ba26 Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Mon, 12 Oct 2026 08:35:21 +0200 Subject: [PATCH 2/6] Fix link error with MLang on MinGW --- src/vmime/platforms/windows/windowsHandler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vmime/platforms/windows/windowsHandler.cpp b/src/vmime/platforms/windows/windowsHandler.cpp index f2409402..d6d63b7e 100644 --- a/src/vmime/platforms/windows/windowsHandler.cpp +++ b/src/vmime/platforms/windows/windowsHandler.cpp @@ -43,6 +43,12 @@ #if VMIME_HAVE_MLANG # include + +// Defined here, as IID_IMultiLanguage is missing from MinGW libraries +static const CLSID VMIME_CLSID_CMultiLanguage = + { 0x275c23e2, 0x3747, 0x11d0, { 0x9f, 0xea, 0x00, 0xaa, 0x00, 0x3f, 0x86, 0x46 } }; +static const IID VMIME_IID_IMultiLanguage = + { 0x275c23e1, 0x3747, 0x11d0, { 0x9f, 0xea, 0x00, 0xaa, 0x00, 0x3f, 0x86, 0x46 } }; #endif @@ -132,10 +138,10 @@ const vmime::charset windowsHandler::getLocalCharset() const { IMultiLanguage* pMultiLanguage; CoCreateInstance( - CLSID_CMultiLanguage, + VMIME_CLSID_CMultiLanguage, NULL, CLSCTX_INPROC_SERVER, - IID_IMultiLanguage, + VMIME_IID_IMultiLanguage, (void**) &pMultiLanguage ); From dbb5c1b6bbd85eb88621e73d3760b8d57f931e7d Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Mon, 12 Oct 2026 09:38:21 +0200 Subject: [PATCH 3/6] Tests: use UTF-8 as the ANSI code page on Windows --- CMakeLists.txt | 14 ++++++++++++++ tests/parser/textTest.cpp | 7 ++++++- tests/testUtils.cpp | 3 ++- tests/utf8.manifest | 22 ++++++++++++++++++++++ tests/utf8.rc | 6 ++++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/utf8.manifest create mode 100644 tests/utf8.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index b1c56965..4d42f15c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,6 +268,18 @@ IF(VMIME_BUILD_TESTS) ${CPPUNIT_LIBRARY} ) + # On Windows, embed a manifest in test executables to use UTF-8 as the + # ANSI code page, as some tests expect the local charset to be UTF-8 + SET(VMIME_TESTS_RESOURCE_FILES "") + + IF(MSVC) + # Merged by the linker with the manifest it generates + SET(VMIME_TESTS_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/utf8.manifest) + ELSEIF(MINGW) + # Replaces the default manifest added by the toolchain + SET(VMIME_TESTS_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/utf8.rc) + ENDIF() + # Build one file for each test FOREACH(VMIME_TEST_SRC_FILE ${VMIME_TESTS_SRC_FILES}) @@ -282,6 +294,7 @@ IF(VMIME_BUILD_TESTS) ADD_EXECUTABLE( ${VMIME_TEST_NAME} ${VMIME_TEST_SRC_FILE} + ${VMIME_TESTS_RESOURCE_FILES} ) TARGET_LINK_LIBRARIES( @@ -306,6 +319,7 @@ IF(VMIME_BUILD_TESTS) ADD_EXECUTABLE( "run-tests" ${VMIME_TESTS_SRC_FILES} + ${VMIME_TESTS_RESOURCE_FILES} ) TARGET_LINK_LIBRARIES( diff --git a/tests/parser/textTest.cpp b/tests/parser/textTest.cpp index ce767863..13e498e1 100644 --- a/tests/parser/textTest.cpp +++ b/tests/parser/textTest.cpp @@ -453,7 +453,12 @@ VMIME_TEST_SUITE_BEGIN(textTest) vmime::word("Non-quotable text \xc3\xa9") .generate(ctx, os, 0, NULL, vmime::text::QUOTE_IF_POSSIBLE, NULL); - VASSERT_EQ("3", "=?UTF-8?Q?Non-quotable_text_=C3=A9?=", cleanGeneratedWords(str)); + // Local charset name may be "UTF-8" or "utf-8", depending on the platform + VASSERT_EQ( + "3", + "=?" + vmime::charset::getLocalCharset().getName() + "?Q?Non-quotable_text_=C3=A9?=", + cleanGeneratedWords(str) + ); } void testWordGenerateSpecialCharsets() { diff --git a/tests/testUtils.cpp b/tests/testUtils.cpp index 5ee66b09..054299a8 100644 --- a/tests/testUtils.cpp +++ b/tests/testUtils.cpp @@ -465,7 +465,8 @@ const vmime::string toHex(const vmime::string str) { // Set the global C and C++ locale to a locale which uses UTF-8 encoding, -// as some tests expect the local charset to be UTF-8 +// as some tests expect the local charset to be UTF-8 (on Windows, the local +// charset is the ANSI code page instead, which is set by tests/utf8.manifest) static void setUTF8Locale() { // Try well-known UTF-8 locales, then fall back to the user-configured locale diff --git a/tests/utf8.manifest b/tests/utf8.manifest new file mode 100644 index 00000000..dd5297e2 --- /dev/null +++ b/tests/utf8.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + UTF-8 + + + diff --git a/tests/utf8.rc b/tests/utf8.rc new file mode 100644 index 00000000..b8da505b --- /dev/null +++ b/tests/utf8.rc @@ -0,0 +1,6 @@ +// Application manifest for test executables on Windows (MinGW): use UTF-8 as +// the ANSI code page, which is used as the local charset by windowsHandler, +// as some tests expect the local charset to be UTF-8. +// This replaces the default manifest added by the MinGW toolchain. + +1 24 "utf8.manifest" // CREATEPROCESS_MANIFEST_RESOURCE_ID, RT_MANIFEST From 5c8230956bfc876462a18a142da1b2d95de0f502 Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Mon, 12 Oct 2026 10:50:46 +0200 Subject: [PATCH 4/6] Fixed non-crossplatform path + temp path --- src/vmime/platforms/posix/posixFile.cpp | 14 ++++++++++++++ src/vmime/platforms/posix/posixFile.hpp | 2 ++ src/vmime/platforms/windows/windowsFile.cpp | 14 ++++++++++++++ src/vmime/platforms/windows/windowsFile.hpp | 2 ++ src/vmime/utility/file.hpp | 14 ++++++++++++++ tests/net/maildir/maildirStoreTest.cpp | 10 +++++++--- tests/parser/fileContentHandlerTest.cpp | 9 ++++++--- 7 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/vmime/platforms/posix/posixFile.cpp b/src/vmime/platforms/posix/posixFile.cpp index 9773b3a0..3a064ea9 100644 --- a/src/vmime/platforms/posix/posixFile.cpp +++ b/src/vmime/platforms/posix/posixFile.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include "vmime/exception.hpp" @@ -673,6 +674,19 @@ bool posixFileSystemFactory::isValidPath(const vmime::utility::file::path& path) } +const vmime::utility::file::path posixFileSystemFactory::getTemporaryDirectoryPath() const { + + // Directory specified by the user (POSIX), or default one + const char* dir = ::getenv("TMPDIR"); + + if (!dir || !*dir) { + dir = "/tmp"; + } + + return stringToPathImpl(dir); +} + + void posixFileSystemFactory::reportError(const vmime::utility::path& path, const int err) { vmime::string desc; diff --git a/src/vmime/platforms/posix/posixFile.hpp b/src/vmime/platforms/posix/posixFile.hpp index d1fda9e8..2c89245f 100644 --- a/src/vmime/platforms/posix/posixFile.hpp +++ b/src/vmime/platforms/posix/posixFile.hpp @@ -210,6 +210,8 @@ class posixFileSystemFactory : public vmime::utility::fileSystemFactory { bool isValidPathComponent(const vmime::utility::file::path::component& comp) const; bool isValidPath(const vmime::utility::file::path& path) const; + const vmime::utility::file::path getTemporaryDirectoryPath() const; + static void reportError(const vmime::utility::path& path, const int err); }; diff --git a/src/vmime/platforms/windows/windowsFile.cpp b/src/vmime/platforms/windows/windowsFile.cpp index 774731c7..d4c281b1 100644 --- a/src/vmime/platforms/windows/windowsFile.cpp +++ b/src/vmime/platforms/windows/windowsFile.cpp @@ -208,6 +208,20 @@ bool windowsFileSystemFactory::isValidPath(const vmime::utility::file::path& pat } +const vmime::utility::file::path windowsFileSystemFactory::getTemporaryDirectoryPath() const { + + char buffer[MAX_PATH + 1]; + const DWORD length = GetTempPathA(static_cast (sizeof(buffer)), buffer); + + if (length == 0 || length > sizeof(buffer)) { + reportError(vmime::utility::file::path(), GetLastError()); + } + + // Trailing backslash is ignored + return stringToPathImpl(vmime::string(buffer, length)); +} + + void windowsFileSystemFactory::reportError(const vmime::utility::path& path, const int err) { vmime::string desc; diff --git a/src/vmime/platforms/windows/windowsFile.hpp b/src/vmime/platforms/windows/windowsFile.hpp index dc4b5ed9..a99e3098 100644 --- a/src/vmime/platforms/windows/windowsFile.hpp +++ b/src/vmime/platforms/windows/windowsFile.hpp @@ -59,6 +59,8 @@ class windowsFileSystemFactory : public vmime::utility::fileSystemFactory { bool firstComponent) const; bool isValidPath(const vmime::utility::file::path& path) const; + const vmime::utility::file::path getTemporaryDirectoryPath() const; + static void reportError(const vmime::utility::path& path, const int err); }; diff --git a/src/vmime/utility/file.hpp b/src/vmime/utility/file.hpp index 6e0605e1..197dd27e 100644 --- a/src/vmime/utility/file.hpp +++ b/src/vmime/utility/file.hpp @@ -26,6 +26,7 @@ #include "vmime/config.hpp" +#include "vmime/exception.hpp" #include "vmime/utility/path.hpp" #include "vmime/utility/stream.hpp" @@ -251,6 +252,19 @@ class VMIME_EXPORT fileSystemFactory : public object { * @return true if the path is valid, false otherwise */ virtual bool isValidPath(const file::path& path) const = 0; + + /** Return the path of the directory to use for temporary files. + * + * @return path of the system temporary directory + * @throw exceptions::filesystem_exception if the temporary directory + * cannot be determined, or if not supported on this platform + */ + virtual const file::path getTemporaryDirectoryPath() const { + + throw exceptions::filesystem_exception( + "Temporary directory is not supported on this platform", file::path() + ); + } }; diff --git a/tests/net/maildir/maildirStoreTest.cpp b/tests/net/maildir/maildirStoreTest.cpp index 1f418e81..9109277f 100644 --- a/tests/net/maildir/maildirStoreTest.cpp +++ b/tests/net/maildir/maildirStoreTest.cpp @@ -167,8 +167,11 @@ VMIME_TEST_SUITE_BEGIN(maildirStoreTest) maildirStoreTest() { + vmime::shared_ptr fsf = + vmime::platform::getHandler()->getFileSystemFactory(); + // Temporary directory - m_tempPath = fspath() / fspathc("tmp") // Use /tmp + m_tempPath = fsf->getTemporaryDirectoryPath() / fspathc("vmime" + vmime::utility::stringUtils::toString(std::time(NULL)) + vmime::utility::stringUtils::toString(std::rand())); } @@ -500,8 +503,9 @@ VMIME_TEST_SUITE_BEGIN(maildirStoreTest) vmime::shared_ptr fsf = vmime::platform::getHandler()->getFileSystemFactory(); - vmime::utility::url url(std::string("maildir://localhost") - + fsf->pathToString(m_tempPath)); + // Path is set as is, as it may not start with a separator (eg. on Windows) + vmime::utility::url url("maildir", "localhost"); // maildir://localhost + url.setPath(fsf->pathToString(m_tempPath)); return url; } diff --git a/tests/parser/fileContentHandlerTest.cpp b/tests/parser/fileContentHandlerTest.cpp index e979b52f..2355fc6e 100644 --- a/tests/parser/fileContentHandlerTest.cpp +++ b/tests/parser/fileContentHandlerTest.cpp @@ -47,13 +47,16 @@ VMIME_TEST_SUITE_BEGIN(fileContentHandlerTest) testDataDecoded = "ABCDEFGHIJKLMNOPQRSTUVWXYZ \x12\x34\x56\x78\x90 abcdefghijklmnopqrstuvwxyz0123456789"; testDataEncoded = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogEjRWeJAgYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5"; - std::ostringstream testFilePath; - testFilePath << "/tmp/vmime_test_" << (rand() % 999999999); + std::ostringstream testFileName; + testFileName << "vmime_test_" << (rand() % 999999999); vmime::shared_ptr fsf = vmime::platform::getHandler()->getFileSystemFactory(); - testFile = fsf->create(fsf->stringToPath(testFilePath.str())); + testFile = fsf->create( + fsf->getTemporaryDirectoryPath() + / vmime::utility::file::path::component(testFileName.str()) + ); testFile->createFile(); testFile->getFileWriter()->getOutputStream()->write(testDataEncoded.data(), testDataEncoded.length()); } From a68ca94fdecc5f3ac1e8a60913b2d6317eb52811 Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Mon, 12 Oct 2026 10:57:04 +0200 Subject: [PATCH 5/6] Fix directory listing + removal of directories on Windows --- src/vmime/platforms/windows/windowsFile.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vmime/platforms/windows/windowsFile.cpp b/src/vmime/platforms/windows/windowsFile.cpp index d4c281b1..2d21b17d 100644 --- a/src/vmime/platforms/windows/windowsFile.cpp +++ b/src/vmime/platforms/windows/windowsFile.cpp @@ -422,7 +422,12 @@ void windowsFile::rename(const path& newName) { void windowsFile::remove() { - if (!DeleteFile(m_nativePath.c_str())) { + // DeleteFile() does not remove directories + const BOOL removed = isDirectory() + ? RemoveDirectory(m_nativePath.c_str()) + : DeleteFile(m_nativePath.c_str()); + + if (!removed) { windowsFileSystemFactory::reportError(m_path, GetLastError()); } } @@ -442,7 +447,8 @@ shared_ptr windowsFile::getFileReader() { shared_ptr windowsFile::getFiles() const { - return make_shared (m_path, m_nativePath); + // FindFirstFile() expects a pattern, else it finds the directory itself + return make_shared (m_path, m_nativePath + "\\*"); } From e3ce95d36b07e8004d1e03426d030773b4c954c9 Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Mon, 12 Oct 2026 11:07:59 +0200 Subject: [PATCH 6/6] windows flags delimiter in maildir test --- tests/net/maildir/maildirStoreTest.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/net/maildir/maildirStoreTest.cpp b/tests/net/maildir/maildirStoreTest.cpp index 9109277f..b337492d 100644 --- a/tests/net/maildir/maildirStoreTest.cpp +++ b/tests/net/maildir/maildirStoreTest.cpp @@ -43,6 +43,15 @@ const fpath operator/(const fpath& path, const std::string& c) { } +// Delimiter between message id and flags in file names, +// same as in maildirUtils::buildFilename() +#if VMIME_PLATFORM_IS_WINDOWS +# define TEST_FLAGS_DELIMITER "-" // ':' is not allowed in file names +#else +# define TEST_FLAGS_DELIMITER ":" +#endif + + /** Test messages */ static const vmime::string TEST_MESSAGE_1 = "From: \r\n" @@ -92,7 +101,7 @@ static const vmime::string TEST_MAILDIR_KMAIL[] = { // directories to create }; static const vmime::string TEST_MAILDIRFILES_KMAIL[] = { // files to create and their contents - "/.Folder.directory/.SubFolder.directory/SubSubFolder2/cur/1043236113.351.EmqD:S", TEST_MESSAGE_1, + "/.Folder.directory/.SubFolder.directory/SubSubFolder2/cur/1043236113.351.EmqD" TEST_FLAGS_DELIMITER "S", TEST_MESSAGE_1, "*" // end }; @@ -127,7 +136,7 @@ static const vmime::string TEST_MAILDIRFILES_COURIER[] = { // files to create a "/.Folder.SubFolder/maildirfolder", "", "/.Folder.SubFolder.SubSubFolder1/maildirfolder", "", "/.Folder.SubFolder.SubSubFolder2/maildirfolder", "", - "/.Folder.SubFolder.SubSubFolder2/cur/1043236113.351.EmqD:S", TEST_MESSAGE_1, + "/.Folder.SubFolder.SubSubFolder2/cur/1043236113.351.EmqD" TEST_FLAGS_DELIMITER "S", TEST_MESSAGE_1, "/.Folder2/maildirfolder", "", "*" // end };