diff --git a/lib/message/mu-flags.hh b/lib/message/mu-flags.hh index 8e424dd0..ee01702c 100644 --- a/lib/message/mu-flags.hh +++ b/lib/message/mu-flags.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2023 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 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 @@ -86,7 +86,7 @@ enum struct MessageFlagCategory { }; /** - * Info about invidual message flags + * Info about individual message flags * */ struct MessageFlagInfo { @@ -373,9 +373,6 @@ flags_maildir_file(Flags flags) return flags; } - - - /** * Return flags, where flags = new_flags but with unmutable_flag in the * result the same as in old_flags diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 3d4d2df2..339abd14 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -881,6 +881,14 @@ Message::update_after_move(const std::string& new_path, priv_->doc.add(Field::Id::Path, new_path); priv_->doc.add(Field::Id::Changed, priv_->ctime); + // note: content-flags are retained from the existing; the unread flag + // is implied. only file-flags are allowed for new_flags; anything else + // is filtered-out + + new_flags = flags_maildir_file(new_flags); + new_flags |= flags_filter(flags(), MessageFlagCategory::Content); + new_flags = imply_unread(new_flags); + set_flags(new_flags); if (const auto res = set_maildir(sanitize_maildir(new_maildir)); !res) diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh index 9be90960..c6b69173 100644 --- a/lib/message/mu-message.hh +++ b/lib/message/mu-message.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2024 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 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 @@ -292,11 +292,10 @@ public: */ Flags flags() const { return document().flags_value(); } - /** - * Update the flags for this message. This is useful for flags - * that can only be determined after the message has been created already, - * such as the 'personal' flag. + * Update the flags for this message. This is useful for flags that can + * only be determined after the message has been created already, such + * as the 'personal' flag. * * @param flags new flags. */ @@ -366,11 +365,14 @@ public: */ const Sexp& sexp() const; - /* - * And some non-const message, for updating an existing - * message after a file-system move. + /** + * Update the message after a move * - * @return Ok or an error. + * @param new_path the new file-system path; non-file flags are ignored + * @param new_maildir the new maildir + * @param new_flags the new flags + * + * @return Ok() or some error. */ Result update_after_move(const std::string& new_path, const std::string& new_maildir, diff --git a/lib/mu-store.hh b/lib/mu-store.hh index 24130de0..cc7946a5 100644 --- a/lib/mu-store.hh +++ b/lib/mu-store.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2024 Dirk-Jan C. Binnema +** Copyright (C) 2017-2025 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 @@ -306,20 +306,24 @@ public: }; /** - * Move a message both in the filesystem and in the store. After a successful move, the - * message is updated. + * Move a message both in the filesystem and in the store. + * + * After a successful move, the message is updated. A moved message gets + * a new doc-id, since the message-path is a unique-id for the message * * @param id the id for some message * @param target_mdir the target maildir (if any) * @param new_flags new flags (if any) * @param opts move options * - * @return Result, either an IdPathVec with ids and paths for the moved message(s) or some - * error. Note that in case of success at least one message is returned, and only with - * MoveOptions::DupFlags can it be more than one. + * @return Result, either an IdPathVec with ids and paths for the moved + * message(s) or some error. Note that in case of success at least one + * message is returned, and only with MoveOptions::DupFlags can it be + * more than one. * - * The first element of the IdPathVec, is the main message that got move; any subsequent - * (if any) are the duplicate paths, sorted by path-name. + * The first element of the IdPathVec, is the main message that got + * move; any subsequent (if any) are the duplicate paths, sorted by + * path-name. */ Result move_message(Store::Id id, Option target_mdir = Nothing, diff --git a/mu/mu-cmd-move.cc b/mu/mu-cmd-move.cc index 7ad7a483..bdb39838 100644 --- a/mu/mu-cmd-move.cc +++ b/mu/mu-cmd-move.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2025 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 @@ -28,7 +28,6 @@ using namespace Mu; - Result Mu::mu_cmd_move(Mu::Store& store, const Options& opts) { @@ -50,7 +49,6 @@ Mu::mu_cmd_move(Mu::Store& store, const Options& opts) "Must have at least one of destination and flags"); else if (!dest.empty()) { const auto mdirs{store.maildirs()}; - if (!seq_some(mdirs, [&](auto &&d){ return d == dest;})) return Err(Error{Error::Code::InvalidArgument, "No maildir '{}' in store", dest} @@ -259,6 +257,65 @@ test_move_real() } } + +static void +test_move_retain_non_file_flags() +{ + g_test_bug("2831"); + + allow_warnings(); + + TempDir tdir; + const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())}; + + auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()}); + assert_valid_command(res); + + const auto testpath{join_paths(tdir.path(), "testdir")}; + const auto src{join_paths(testpath, "cur", "multimime!2,FS")}; + + Store::Id id{}; + { + auto store = Store::make_new(dbpath, testpath, {}); + assert_valid_result(store); + const auto idopt{store->add_message(src, true)}; + g_assert_true(!!idopt); + id = *idopt; + const auto msg{store->find_message(id)}; + g_assert_true(!!msg); + g_assert_true(msg->flags() == (Flags::Seen | Flags::Flagged | Flags::HasAttachment)); + } + + // make a message 'New' + const auto dst{join_paths(testpath, "new", "multimime")}; + { + auto res = run_command0({MU_PROGRAM, "--debug", "move", "--muhome", + tdir.path(), src, "--flags", "N"}); + assert_valid_command(res); + assert_equal(res->standard_out, dst + '\n'); + // double-check it really moved. + g_assert_true(::access(dst.c_str(), F_OK) == 0); + g_assert_true(::access(src.c_str(), F_OK) != 0); + } + + // re-open db + { + const auto store = Store::make(dbpath); + assert_valid_result(store); + + const auto newid{store->find_message_id(dst)}; + g_assert_true(!!newid); + + const auto msg{store->find_message(*newid)}; + g_assert_true(!!msg); + + // check the old flags + const auto flags = msg->flags(); + assert_equal(to_string(flags), to_string(Flags::New | Flags::Unread | + Flags::HasAttachment)); + } +} + int main(int argc, char* argv[]) { @@ -266,6 +323,8 @@ main(int argc, char* argv[]) g_test_add_func("/cmd/move/dry-run", test_move_dry_run); g_test_add_func("/cmd/move/real", test_move_real); + g_test_add_func("/cmd/move/retain-non-file-flags", + test_move_retain_non_file_flags); return g_test_run();