diff --git a/lib/message/meson.build b/lib/message/meson.build index ca3025bf..3997fe28 100644 --- a/lib/message/meson.build +++ b/lib/message/meson.build @@ -19,6 +19,7 @@ lib_mu_message=static_library( 'mu-message', [ 'mu-message.cc', + 'mu-message-file.cc', 'mu-message-part.cc', 'mu-contact.cc', 'mu-document.cc', @@ -85,3 +86,10 @@ test('test-priority', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-message-file', + executable('test-message-file', + 'mu-message-file.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_message_dep])) diff --git a/lib/message/mu-flags.cc b/lib/message/mu-flags.cc index d45a2424..b4b96037 100644 --- a/lib/message/mu-flags.cc +++ b/lib/message/mu-flags.cc @@ -38,88 +38,6 @@ Mu::to_string(Flags flags) return str; } -/* - * The file-components, ie. - * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS - * => { - * "1631819685.fb7b279bbb0a7b66.evergrey", - * ':', - * "2,", - * "RS" - * } - */ -struct FileParts { - std::string base; - char separator; - std::string flags_suffix; -}; - -static FileParts -message_file_parts(const std::string& file) -{ - const auto pos{file.find_last_of(":!;")}; - - /* no suffix at all? */ - if (pos == std::string::npos || - pos >= file.length() - 3 || - file[pos + 1] != '2' || - file[pos + 2] != ',') - return FileParts{ file, ':', {}}; - - return FileParts { - file.substr(0, pos), - file[pos], - file.substr(pos + 3) - }; -} - - -struct DirFile { - std::string dir; - std::string file; - bool is_new; -}; - -static Option -base_message_dir_file(const std::string& path) -{ - constexpr auto newdir{ G_DIR_SEPARATOR_S "new"}; - - if (path.empty()) - return Nothing; - - char *dirname{g_path_get_dirname(path.c_str())}; - bool is_new{!!g_str_has_suffix(dirname, newdir)}; - - std::string mdir{dirname, ::strlen(dirname) - 4}; - g_free(dirname); - - char *basename{g_path_get_basename(path.c_str())}; - std::string bname{basename}; - g_free(basename); - - return DirFile{std::move(mdir), std::move(bname), is_new}; -} - -Mu::Option -Mu::flags_from_path(const std::string& path) -{ /* - * this gets us the source maildir filesystem path, the directory - * in which new/ & cur/ lives, and the source file - */ - auto dirfile{base_message_dir_file(path)}; - if (!dirfile) - return Nothing; - - /* a message under new/ is just.. New. Filename is not considered */ - if (dirfile->is_new) - return Flags::New; - - /* it's cur/ message, so parse the file name */ - const auto parts{message_file_parts(dirfile->file)}; - return flags_from_absolute_expr(parts.flags_suffix, true/*ignore invalid*/); -} - /* * flags & flag-info diff --git a/lib/message/mu-flags.hh b/lib/message/mu-flags.hh index 9bb7b177..20363335 100644 --- a/lib/message/mu-flags.hh +++ b/lib/message/mu-flags.hh @@ -317,19 +317,6 @@ flags_from_expr(std::string_view expr, return flags_from_absolute_expr(expr, true); } -/** - * Get the Maildir flags from the full path of a mailfile. The flags are as - * specified in http://cr.yp.to/proto/maildir.html, plus MU_MSG_FLAG_NEW for new - * messages, ie the ones that live in new/. The flags are logically OR'ed. Note - * that the file does not have to exist; the flags are based on the path only. - * - * @param pathname of a mailfile; it does not have to refer to an existing - * file/message - * - * @return the message flags or Nothing - */ -Option flags_from_path(const std::string& path); - /** * Filter out flags which are not in the given category * diff --git a/lib/message/mu-message-file.cc b/lib/message/mu-message-file.cc new file mode 100644 index 00000000..41861608 --- /dev/null +++ b/lib/message/mu-message-file.cc @@ -0,0 +1,203 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-message-file.hh" + +using namespace Mu; + +Result +Mu::maildir_from_path(const std::string& path, const std::string& root) +{ + const auto pos = path.find(root); + if (pos != 0 || path[root.length()] != '/') + return Err(Error{Error::Code::InvalidArgument, + "root '%s' is not a root for path '%s'", + root.c_str(), path.c_str()}); + + auto mdir{path.substr(root.length())}; + auto slash{mdir.rfind('/')}; + + if (G_UNLIKELY(slash == std::string::npos) || slash < 4) + return Err(Error{Error::Code::InvalidArgument, + "invalid path: %s", path.c_str()}); + mdir.erase(slash); + auto subdir = mdir.data() + slash - 4; + if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && strncmp(subdir, "/new", 4))) + return Err(Error::Code::InvalidArgument, + "cannot find '/new' or '/cur' - invalid path: %s", + path.c_str()); + + if (mdir.length() == 4) + return "/"; + + mdir.erase(mdir.length() - 4); + + return Ok(std::move(mdir)); +} + +Mu::FileParts +Mu::message_file_parts(const std::string& file) +{ + const auto pos{file.find_last_of(":!;")}; + + /* no suffix at all? */ + if (pos == std::string::npos || + pos > file.length() - 3 || + file[pos + 1] != '2' || + file[pos + 2] != ',') + return FileParts{ file, ':', {}}; + + return FileParts { + file.substr(0, pos), + file[pos], + file.substr(pos + 3) + }; +} + +Mu::Result +Mu::base_message_dir_file(const std::string& path) +{ + constexpr auto newdir{ G_DIR_SEPARATOR_S "new"}; + + char *dirname{g_path_get_dirname(path.c_str())}; + bool is_new{!!g_str_has_suffix(dirname, newdir)}; + + std::string mdir{dirname, ::strlen(dirname) - 4}; + g_free(dirname); + + char *basename{g_path_get_basename(path.c_str())}; + std::string bname{basename}; + g_free(basename); + + return Ok(DirFile{std::move(mdir), std::move(bname), is_new}); +} + +Mu::Result +Mu::flags_from_path(const std::string& path) +{ /* + * this gets us the source maildir filesystem path, the directory + * in which new/ & cur/ lives, and the source file + */ + auto dirfile{base_message_dir_file(path)}; + if (!dirfile) + return Err(std::move(dirfile.error())); + + /* a message under new/ is just.. New. Filename is not considered */ + if (dirfile->is_new) + return Ok(Flags::New); + + /* it's cur/ message, so parse the file name */ + const auto parts{message_file_parts(dirfile->file)}; + auto flags{flags_from_absolute_expr(parts.flags_suffix, + true/*ignore invalid*/)}; + if (!flags) { + /* LCOV_EXCL_START*/ + return Err(Error{Error::Code::InvalidArgument, + "invalid flags ('%s')", parts.flags_suffix.c_str()}); + /* LCOV_EXCL_STOP*/ + } else + return Ok(std::move(flags.value())); +} + + + + +#ifdef BUILD_TESTS + +static void +test_maildir_from_path() +{ + std::array, 1> test_cases = {{ + { "/home/foo/Maildir/hello/cur/msg123", "/home/foo/Maildir", "/hello" } + }}; + + for(auto&& tcase: test_cases) { + const auto res{maildir_from_path(std::get<0>(tcase), std::get<1>(tcase))}; + assert_valid_result(res); + assert_equal(*res, std::get<2>(tcase)); + } + + g_assert_false(!!maildir_from_path("/home/foo/Maildir/cur/test1", "/home/bar")); + g_assert_false(!!maildir_from_path("/x", "/x/y")); + g_assert_false(!!maildir_from_path("/home/a/Maildir/b/xxx/test", "/home/a/Maildir")); +} + +static void +test_base_message_dir_file() +{ + struct TestCase { + const std::string path; + DirFile expected; + }; + std::array test_cases = {{ + { "/home/djcb/Maildir/foo/cur/msg:2,S", + { "/home/djcb/Maildir/foo", "msg:2,S", false } } + }}; + for(auto&& tcase: test_cases) { + const auto res{base_message_dir_file(tcase.path)}; + assert_valid_result(res); + assert_equal(res->dir, tcase.expected.dir); + assert_equal(res->file, tcase.expected.file); + g_assert_cmpuint(res->is_new, ==, tcase.expected.is_new); + } +} + +static void +test_flags_from_path() +{ + std::array, 5> test_cases = {{ + {"/home/foo/Maildir/test/cur/123456:2,FSR", + (Flags::Replied | Flags::Seen | Flags::Flagged)}, + {"/home/foo/Maildir/test/new/123456", Flags::New}, + {/* NOTE: when in new/, the :2,.. stuff is ignored */ + "/home/foo/Maildir/test/new/123456:2,FR", + Flags::New}, + {"/home/foo/Maildir/test/cur/123456:2,DTP", + (Flags::Draft | Flags::Trashed | Flags::Passed)}, + {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen} + }}; + + for (auto&& tcase: test_cases) { + auto res{flags_from_path(tcase.first)}; + assert_valid_result(res); + /* LCOV_EXCL_START*/ + if (g_test_verbose()) { + g_print("%s -> <%s>\n", tcase.first.c_str(), + to_string(res.value()).c_str()); + g_assert_true(res.value() == tcase.second); + } + /*LCOV_EXCL_STOP*/ + } +} + + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/file/maildir-from-path", + test_maildir_from_path); + g_test_add_func("/message/file/base-message-dir-file", + test_base_message_dir_file); + g_test_add_func("/message/file/flags-from-path", test_flags_from_path); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-message-file.hh b/lib/message/mu-message-file.hh new file mode 100644 index 00000000..09a9ed3f --- /dev/null +++ b/lib/message/mu-message-file.hh @@ -0,0 +1,98 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MESSAGE_FILE_HH__ +#define MU_MESSAGE_FILE_HH__ + +#include "mu-flags.hh" +#include + +namespace Mu { + +/* + * The file-components, ie. + * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS + * => { + * "1631819685.fb7b279bbb0a7b66.evergrey", + * ':', + * "2,", + * "RS" + * } + */ +struct FileParts { + std::string base; /**< basename */ + char separator; /**< separator */ + std::string flags_suffix; /**< suffix (with flags) */ +}; + +/** + * Get the file-parts for some message-file + * + * @param file path to some message file (does not have to exist) + * + * @return FileParts for the message file + */ +FileParts message_file_parts(const std::string& file); + + +struct DirFile { + std::string dir; + std::string file; + bool is_new; +}; + +/** + * Get information about the message file componemts + * + * @param path message path + * + * @return the components for the message file or an error. + */ +Result base_message_dir_file(const std::string& path); + + + +/** + * Get the Maildir flags from the full path of a mailfile. The flags are as + * specified in http://cr.yp.to/proto/maildir.html, plus Flag::New for new + * messages, ie the ones that live in new/. The flags are logically OR'ed. Note + * that the file does not have to exist; the flags are based on the path only. + * + * @param pathname of a mailfile; it does not have to refer to an + * actual message + * + * @return the message flags or an error + */ +Result flags_from_path(const std::string& pathname); + +/** + * get the maildir for a certain message path, ie, the path *before* + * cur/ or new/ and *after* the root. + * + * @param path path for some message + * @param root filesystem root for the maildir + * + * @return the maildir or an Error + */ +Result maildir_from_path(const std::string& path, + const std::string& root); +} // Mu + + +#endif /* MU_MESSAGE_FILE_HH__ */ diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh index e5f8afec..2e30222a 100644 --- a/lib/message/mu-message.hh +++ b/lib/message/mu-message.hh @@ -29,6 +29,8 @@ #include "mu-fields.hh" #include "mu-document.hh" #include "mu-message-part.hh" +#include "mu-message-file.hh" + #include #include "utils/mu-utils.hh" diff --git a/lib/mu-maildir.cc b/lib/mu-maildir.cc index 7a563bb3..1b191768 100644 --- a/lib/mu-maildir.cc +++ b/lib/mu-maildir.cc @@ -247,123 +247,6 @@ Mu::maildir_clear_links(const std::string& path) return Ok(); } -Result -Mu::maildir_from_path(const std::string& path, const std::string& root) -{ - const auto pos = path.find(root); - if (pos != 0 || path[root.length()] != '/') - return Err(Error{Error::Code::InvalidArgument, - "root '%s' is not a root for path '%s'", - root.c_str(), path.c_str()}); - - auto mdir{path.substr(root.length())}; - auto slash{mdir.rfind('/')}; - - if (G_UNLIKELY(slash == std::string::npos) || slash < 4) - return Err(Error{Error::Code::InvalidArgument, - "invalid path: %s", path.c_str()}); - mdir.erase(slash); - auto subdir = mdir.data() + slash - 4; - if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && strncmp(subdir, "/new", 4))) - return Err(Error::Code::InvalidArgument, - "cannot find '/new' or '/cur' - invalid path: %s", - path.c_str()); - - if (mdir.length() == 4) - return "/"; - - mdir.erase(mdir.length() - 4); - - return Ok(std::move(mdir)); -} - - -/* - * The file-components, ie. - * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS - * => { - * "1631819685.fb7b279bbb0a7b66.evergrey", - * ':', - * "2,", - * "RS" - * } - */ -struct FileParts { - std::string base; - char separator; - std::string flags_suffix; -}; - -static FileParts -message_file_parts(const std::string& file) -{ - const auto pos{file.find_last_of(":!;")}; - - /* no suffix at all? */ - if (pos == std::string::npos || - pos > file.length() - 3 || - file[pos + 1] != '2' || - file[pos + 2] != ',') - return FileParts{ file, ':', {}}; - - return FileParts { - file.substr(0, pos), - file[pos], - file.substr(pos + 3) - }; -} - -struct DirFile { - std::string dir; - std::string file; - bool is_new; -}; - -static Result -base_message_dir_file(const std::string& path) -{ - constexpr auto newdir{ G_DIR_SEPARATOR_S "new"}; - - char *dirname{g_path_get_dirname(path.c_str())}; - bool is_new{!!g_str_has_suffix(dirname, newdir)}; - - std::string mdir{dirname, ::strlen(dirname) - 4}; - g_free(dirname); - - char *basename{g_path_get_basename(path.c_str())}; - std::string bname{basename}; - g_free(basename); - - return Ok(DirFile{std::move(mdir), std::move(bname), is_new}); -} - -// refactor: we have the same code in mu-flags.cc - -Mu::Result -Mu::maildir_flags_from_path(const std::string& path) -{ /* - * this gets us the source maildir filesystem path, the directory - * in which new/ & cur/ lives, and the source file - */ - auto dirfile{base_message_dir_file(path)}; - if (!dirfile) - return Err(std::move(dirfile.error())); - - /* a message under new/ is just.. New. Filename is not considered */ - if (dirfile->is_new) - return Ok(Flags::New); - - /* it's cur/ message, so parse the file name */ - const auto parts{message_file_parts(dirfile->file)}; - auto flags{flags_from_absolute_expr(parts.flags_suffix, - true/*ignore invalid*/)}; - if (!flags) - return Err(Error{Error::Code::InvalidArgument, - "invalid flags ('%s')", parts.flags_suffix.c_str()}); - else - return Ok(std::move(flags.value())); -} - static size_t get_file_size(const std::string& path) diff --git a/lib/mu-maildir.hh b/lib/mu-maildir.hh index bb48c241..d2c4d568 100644 --- a/lib/mu-maildir.hh +++ b/lib/mu-maildir.hh @@ -71,19 +71,6 @@ Result maildir_link(const std::string& src, const std::string& targetpath) */ Result maildir_clear_links(const std::string& dir); -/** - * Get the Maildir flags from the full path of a mailfile. The flags are as - * specified in http://cr.yp.to/proto/maildir.html, plus Flag::New for new - * messages, ie the ones that live in new/. The flags are logically OR'ed. Note - * that the file does not have to exist; the flags are based on the path only. - * - * @param pathname of a mailfile; it does not have to refer to an - * actual message - * - * @return the message flags or an error - */ -Result maildir_flags_from_path(const std::string& pathname); - /** * get the maildir for a certain message path, ie, the path *before* * cur/ or new/ and *after* the root. diff --git a/lib/tests/test-mu-maildir.cc b/lib/tests/test-mu-maildir.cc index 08bccb97..364e9cdd 100644 --- a/lib/tests/test-mu-maildir.cc +++ b/lib/tests/test-mu-maildir.cc @@ -168,34 +168,6 @@ test_maildir_mkdir_05(void) g_assert_false(!!maildir_mkdir({}, 0755, true)); } -static void -test_maildir_flags_from_path(void) -{ - int i; - struct { - const char* path; - Flags flags; - } paths[] = { - {"/home/foo/Maildir/test/cur/123456:2,FSR", - (Flags::Replied | Flags::Seen | Flags::Flagged)}, - {"/home/foo/Maildir/test/new/123456", Flags::New}, - {/* NOTE: when in new/, the :2,.. stuff is ignored */ - "/home/foo/Maildir/test/new/123456:2,FR", - Flags::New}, - {"/home/foo/Maildir/test/cur/123456:2,DTP", - (Flags::Draft | Flags::Trashed | Flags::Passed)}, - {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen}}; - - for (i = 0; i != G_N_ELEMENTS(paths); ++i) { - auto res{maildir_flags_from_path(paths[i].path)}; - g_assert_true(!!res); - if (g_test_verbose()) - g_print("%s -> <%s>\n", paths[i].path, - to_string(res.value()).c_str()); - g_assert_true(res.value() == paths[i].flags); - } -} - [[maybe_unused]] static void assert_matches_regexp(const char* str, const char* rx) { @@ -494,9 +466,6 @@ main(int argc, char* argv[]) g_test_add_func("/mu-maildir/mu-maildir-mkdir-04", test_maildir_mkdir_04); g_test_add_func("/mu-maildir/mu-maildir-mkdir-05", test_maildir_mkdir_05); - g_test_add_func("/mu-maildir/mu-maildir-flags-from-path", - test_maildir_flags_from_path); - g_test_add_func("/mu-maildir/mu-maildir-determine-target-ok", test_determine_target_ok); g_test_add_func("/mu-maildir/mu-maildir-determine-target-fail",