Merge branch 'wip/store-move-dups'

This commit is contained in:
Dirk-Jan C. Binnema 2022-12-08 19:34:30 +02:00
commit 8e334bcd8f
9 changed files with 426 additions and 108 deletions

View File

@ -150,6 +150,18 @@ test_flags_filter()
}
[[maybe_unused]] static void
test_flags_keep_unmutable()
{
static_assert(flags_keep_unmutable((Flags::Seen|Flags::Passed),
(Flags::Flagged|Flags::Draft),
Flags::Replied) ==
(Flags::Flagged|Flags::Draft));
}
#ifdef BUILD_TESTS
int
main(int argc, char* argv[])
@ -164,6 +176,8 @@ main(int argc, char* argv[])
test_flags_from_delta_expr);
g_test_add_func("/message/flags/flags-filter",
test_flags_filter);
g_test_add_func("/message/flags/flags-keep-unmutable",
test_flags_keep_unmutable);
return g_test_run();
}

View File

@ -338,6 +338,28 @@ flags_filter(Flags flags, MessageFlagCategory cat)
return flags;
}
/**
* Return flags, where flags = new_flags but with unmutable_flag in the
* result the same as in old_flags
*
* @param old_flags
* @param new_flags
* @param unmutable_flag
*
* @return
*/
constexpr Flags
flags_keep_unmutable(Flags old_flags, Flags new_flags, Flags unmutable_flag)
{
if (any_of(old_flags & unmutable_flag))
return new_flags | unmutable_flag;
else
return new_flags & ~unmutable_flag;
}
/**
* Get a string representation of flags
*

View File

@ -180,7 +180,7 @@ struct MatchDeciderRelated final : public MatchDecider {
auto qm{make_query_match(doc)};
if (should_include(qm)) {
qm.flags = QueryMatch::Flags::Related;
qm.flags |= QueryMatch::Flags::Related;
decider_info_.matches.emplace(doc.get_docid(), std::move(qm));
return true;
} else

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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
@ -661,6 +661,10 @@ assert_thread_paths(const MockQueryResults& qrs, const Expected& expected)
qr.path().value_or("") == exp.first;
});
g_assert_true(it != qrs.end());
g_debug("thread-path (%s@%s): expected: '%s'; got '%s'",
it->message_id().value_or("<none>").c_str(),
it->path().value_or("<none>").c_str(),
exp.second.c_str(), it->query_match().thread_path.c_str());
g_assert_cmpstr(exp.second.c_str(), ==, it->query_match().thread_path.c_str());
}
}
@ -754,13 +758,43 @@ test_dups_dup_first()
calculate_threads(results, false);
assert_thread_paths(results,
{
{"/path2", "0"},
{"/path1", "0:0"},
});
assert_thread_paths(results, {
{"/path2", "0"},
{"/path1", "0:0"},
});
}
static void
test_dups_dup_multi()
{
// now dup becomes the leader; this will _demote_
// r1.
MockQueryResult r1_dup1{"m1", "1", {}};
r1_dup1.query_match().flags |= QueryMatch::Flags::Duplicate;
r1_dup1.path_ = "/path1";
MockQueryResult r1_dup2{"m1", "1", {}};
r1_dup2.query_match().flags |= QueryMatch::Flags::Duplicate;
r1_dup2.path_ = "/path2";
MockQueryResult r1{"m1", "1", {}};
r1.query_match().flags |= QueryMatch::Flags::Leader;
r1.path_ = "/path3";
auto results = MockQueryResults{r1_dup1, r1_dup2, r1};
calculate_threads(results, false);
assert_thread_paths(results, {
{"/path3", "0"},
{"/path1", "0:0"},
{"/path2", "0:1"},
});
}
static void
test_do_not_prune_root_empty_with_children()
{
@ -896,6 +930,7 @@ try {
g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);
g_test_add_func("/threader/dups/dup-first", test_dups_dup_first);
g_test_add_func("/threader/dups/dup-multi", test_dups_dup_multi);
g_test_add_func("/threader/prune/do-not-prune-root-empty-with-children",
test_do_not_prune_root_empty_with_children);

View File

@ -109,17 +109,17 @@ private:
Store::Id docid,
const Option<QueryMatch&> qm) const;
Sexp move_docid(Store::Id docid, Option<std::string> flagstr,
void move_docid(Store::Id docid, Option<std::string> flagstr,
bool new_name, bool no_view);
Sexp perform_move(Store::Id docid,
const Message& msg,
void perform_move(Store::Id docid,
const Message& msg,
const std::string& maildirarg,
Flags flags,
bool new_name,
bool no_view);
bool view_mark_as_read(Store::Id docid, const Message& msg, bool rename);
void view_mark_as_read(Store::Id docid, Message&& msg, bool rename);
Store& store_;
Server::Output output_;
@ -797,7 +797,7 @@ Server::Private::mkdir_handler(const Command& cmd)
":message", format("%s has been created", path.c_str())));
}
Sexp
void
Server::Private::perform_move(Store::Id docid,
const Message& msg,
const std::string& maildirarg,
@ -813,20 +813,26 @@ Server::Private::perform_move(Store::Id docid,
} else /* are we moving to a different mdir, or is it just flags? */
different_mdir = maildir != msg.maildir();
const auto new_msg = store().move_message(docid, maildir, flags, new_name);
if (!new_msg)
throw new_msg.error();
Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
if (new_name)
move_opts |= Store::MoveOptions::ChangeName;
Sexp seq;
seq.put_props(":update", build_message_sexp(new_msg.value(), docid, {}));
/* note, the :move t thing is a hint to the frontend that it
* could remove the particular header */
if (different_mdir)
seq.put_props(":move", Sexp::t());
if (!no_view)
seq.put_props(":maybe-view", Sexp::t());
/* note: we get back _all_ the messages that changed; the first is the
* primary mover; the rest (if present) are any dups affected */
const auto idmsgvec{store().move_message(docid, maildir, flags, move_opts)};
if (!idmsgvec)
throw idmsgvec.error();
return seq;
for (auto&&[id, msg]: *idmsgvec) {
Sexp sexp{":update"_sym, build_message_sexp(idmsgvec->at(0).second, id, {})};
/* note, the :move t thing is a hint to the frontend that it
* could remove the particular header */
if (different_mdir)
sexp.put_props(":move", Sexp::t());
if (!no_view && id == docid)
sexp.put_props(":maybe-view", Sexp::t());
output_sexp(std::move(sexp));
}
}
@ -847,7 +853,7 @@ calculate_message_flags(const Message& msg, Option<std::string> flagopt)
return flags.value();
}
Sexp
void
Server::Private::move_docid(Store::Id docid,
Option<std::string> flagopt,
bool new_name,
@ -861,9 +867,7 @@ Server::Private::move_docid(Store::Id docid,
throw Error{Error::Code::Store, "failed to get message from store"};
const auto flags = calculate_message_flags(msg.value(), flagopt);
auto lst = perform_move(docid, *msg, "", flags, new_name, no_view);
return lst;
perform_move(docid, *msg, "", flags, new_name, no_view);
}
/*
@ -871,9 +875,6 @@ Server::Private::move_docid(Store::Id docid,
* flags. parameters are *either* a 'docid:' or 'msgid:' pointing to
* the message, a 'maildir:' for the target maildir, and a 'flags:'
* parameter for the new flags.
*
* returns an (:update <new-msg-sexp>)
*
*/
void
Server::Private::move_handler(const Command& cmd)
@ -890,8 +891,7 @@ Server::Private::move_handler(const Command& cmd)
"can't move multiple messages at the same time"};
// multi.
for (auto&& docid : docids)
output_sexp(move_docid(docid, flagopt,
rename, no_view));
move_docid(docid, flagopt, rename, no_view);
return;
}
auto docid{docids.at(0)};
@ -907,7 +907,7 @@ Server::Private::move_handler(const Command& cmd)
* we received (ie., flagstr), if any, plus the existing message
* flags. */
const auto flags = calculate_message_flags(msg, flagopt);
output_sexp(perform_move(docid, msg, maildir, flags, rename, no_view));
perform_move(docid, msg, maildir, flags, rename, no_view);
}
void
@ -965,7 +965,7 @@ Server::Private::remove_handler(const Command& cmd)
if (!store().remove_message(path))
g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid);
output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked.
output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked.
}
void
@ -982,43 +982,32 @@ Server::Private::sent_handler(const Command& cmd)
":docid", docid.value()));
}
bool
Server::Private::view_mark_as_read(Store::Id docid, const Message& msg, bool rename)
void
Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
{
/* move some message if the flags changes; and send either a :view (main message
* or :update (the rest))*/
auto maybe_move = [&](Store::Id msg_docid, Flags old_flags,
bool do_rename, bool do_view)->bool {
const auto newflags{flags_from_delta_expr("+S-u-N", old_flags)};
if (!newflags || old_flags == *newflags)
return false;
auto move_res = std::invoke([&]()->Result<Store::IdMessageVec> {
const auto newflags{flags_from_delta_expr("+S-u-N", msg.flags())};
if (!newflags || msg.flags() == *newflags) {
/* case 1: message was already read; do nothing */
Store::IdMessageVec idmvec;
idmvec.emplace_back(docid, std::move(msg));
return idmvec;
} else {
/* case 2: move message (and possibly dups) */
Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
if (rename)
move_opts |= Store::MoveOptions::ChangeName;
return store().move_message(docid, {}, newflags, move_opts);
}
});
auto updated_msg = store().move_message(msg_docid, {}, newflags, do_rename);
if (!updated_msg)
throw updated_msg.error();
if (!move_res)
throw move_res.error();
output_sexp(Sexp().put_props(do_view ? ":view" : ":update",
build_message_sexp(*updated_msg, docid, {})));
return true;
};
/* now get _al_ the message-ids for the given message-id,
* since, we want to apply the read-status to _all_. */
/* first the main message */
bool moved = maybe_move(docid, msg.flags(), rename, true/*:view*/);
/* now any other message with the same message-id */
for (auto&& rel_docid: docids_for_msgid(store_, msg.message_id())) {
/* ignore main one since we already handled it. */
if (rel_docid == docid)
continue;
if (auto msg{store().find_message(docid)}; msg)
maybe_move(rel_docid, msg->flags(), rename, false/*:update*/);
}
return moved;
for (auto&& [id, msg]: move_res.value())
output_sexp(Sexp{id == docid ? ":view"_sym : ":update"_sym,
build_message_sexp(msg, id, {})});
}
void
@ -1038,10 +1027,12 @@ Server::Private::view_handler(const Command& cmd)
.or_else([]{throw Error{Error::Code::Store,
"failed to find message for view"};}).value();
/* if the message is marked-as-read, the response is handled there;
* otherwise, we do so here. */
if (!mark_as_read || !view_mark_as_read(docid, msg, rename))
/* if the message should not be marked-as-read, we're done. */
if (!mark_as_read)
output_sexp(Sexp().put_props(":view", build_message_sexp(msg, docid, {})));
else
view_mark_as_read(docid, std::move(msg), rename);
/* otherwise, mark message and and possible dups as read */
}
Server::Server(Store& store, Server::Output output)

View File

@ -244,6 +244,10 @@ struct Store::Private {
Option<Message> find_message_unlocked(Store::Id docid) const;
Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
Result<Message> move_message_unlocked(Message&& msg,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
MoveOptions opts);
/* metadata to write as part of a transaction commit */
std::unordered_map<std::string, std::string> metadata_cache_;
@ -503,44 +507,144 @@ Store::find_message(Store::Id docid) const
return priv_->find_message_unlocked(docid);
}
/**
* Move a message in store and filesystem.
*
* Lock is assumed taken already
*
* @param id message id
* @param target_mdir target_midr (or Nothing for current)
* @param new_flags new flags (or Notthing)
'; * @param opts move_optionss
*
* @return the Message after the moving, or an Error
*/
Result<Message>
Store::move_message(Store::Id id,
Option<const std::string&> target_mdir,
Option<Flags> new_flags, bool change_name)
Store::Private::move_message_unlocked(Message&& msg,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
MoveOptions opts)
{
std::lock_guard guard{priv_->lock_};
auto msg = priv_->find_message_unlocked(id);
if (!msg)
return Err(Error::Code::Store, "cannot find message <%u>", id);
const auto old_path = msg->path();
const auto target_flags = new_flags.value_or(msg->flags());
const auto target_maildir = target_mdir.value_or(msg->maildir());
const auto old_path = msg.path();
const auto target_flags = new_flags.value_or(msg.flags());
const auto target_maildir = target_mdir.value_or(msg.maildir());
/* 1. first determine the file system path of the target */
const auto target_path =
maildir_determine_target(msg->path(), properties().root_maildir,
target_maildir,target_flags, change_name);
maildir_determine_target(msg.path(), properties_.root_maildir,
target_maildir, target_flags,
any_of(opts & MoveOptions::ChangeName));
if (!target_path)
return Err(target_path.error());
/* 2. let's move it */
if (const auto res = maildir_move_message(msg->path(), target_path.value()); !res)
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
return Err(res.error());
/* 3. file move worked, now update the message with the new info.*/
if (auto&& res = msg->update_after_move(
if (auto&& res = msg.update_after_move(
target_path.value(), target_maildir, target_flags); !res)
return Err(res.error());
/* 4. update message worked; re-store it */
if (auto&& res = priv_->update_message_unlocked(*msg, old_path); !res)
if (auto&& res = update_message_unlocked(msg, old_path); !res)
return Err(res.error());
/* 6. Profit! */
return Ok(std::move(msg.value()));
return Ok(std::move(msg));
}
/* get a vec of all messages with the given message id */
static Store::IdMessageVec
messages_with_msgid(const Store& store, const std::string& msgid, size_t max=100)
{
if (msgid.size() > MaxTermLength) {
g_warning("invalid message-id '%s'", msgid.c_str());
return {};
} else if (msgid.empty())
return {};
const auto xprefix{field_from_id(Field::Id::MessageId).shortcut};
/*XXX this is a bit dodgy */
auto tmp{g_ascii_strdown(msgid.c_str(), -1)};
auto expr{g_strdup_printf("%c:%s", xprefix, tmp)};
g_free(tmp);
const auto res{store.run_query(expr, {}, QueryFlags::None, max)};
g_free(expr);
if (!res) {
g_warning("failed to run message-id-query: %s", res.error().what());
return {};
}
if (res->empty()) {
g_warning("could not find message(s) for msgid %s", msgid.c_str());
return {};
}
Store::IdMessageVec imvec;
for (auto&& mi : *res)
imvec.emplace_back(std::make_pair(mi.doc_id(), mi.message().value()));
return imvec;
}
static Flags
filter_dup_flags(Flags old_flags, Flags new_flags)
{
new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Draft);
new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Flagged);
new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Trashed);
return new_flags;
}
Result<Store::IdMessageVec>
Store::move_message(Store::Id id,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
MoveOptions opts)
{
std::lock_guard guard{priv_->lock_};
auto msg{priv_->find_message_unlocked(id)};
if (!msg)
return Err(Error::Code::Store, "cannot find message <%u>", id);
auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)};
if (!res)
return Err(res.error());
IdMessageVec imvec;
imvec.emplace_back(std::make_pair(id, std::move(*res)));
if (none_of(opts & Store::MoveOptions::DupFlags) || !new_flags)
return Ok(std::move(imvec));
/* handle the dupflags case; i.e. apply (a subset of) the flags to
* all messages with the same message-id as well */
for (auto&& [docid, msg]: messages_with_msgid(*this, imvec.at(0).second.message_id())) {
if (docid == id)
continue; // already
/* For now, don't change Draft/Flagged/Trashed */
Flags dup_flags = filter_dup_flags(msg.flags(), *new_flags);
/* use the updated new_flags and default MoveOptions (so we don't recurse, nor do we
* change the base-name of moved messages) */
auto dup_res = priv_->move_message_unlocked(std::move(msg), Nothing,
dup_flags,
Store::MoveOptions::None);
// just log a warning if it fails, but continue.
if (dup_res)
imvec.emplace_back(docid, std::move(*dup_res));
else
g_warning("failed to move dup: %s", dup_res.error().what());
}
return Ok(std::move(imvec));
}
std::string

View File

@ -307,21 +307,37 @@ public:
*/
bool contains_message(const std::string& path) const;
/**
* Move a message both in the filesystem and in the store.
* After a successful move, the message is updated.
* Options for moving
*
*/
enum struct MoveOptions {
None = 0, /**< Defaults */
ChangeName = 1 << 0, /**< Change the name when moving */
DupFlags = 1 << 1, /**< Update flags for duplicate messages too*/
};
/**
* Move a message both in the filesystem and in the store. After a
* successful move, the message is updated.
*
* @param id the id for some message
* @param target_mdir the target maildir (if any)
* @param new_flags new flags (if any)
* @param change_name whether to change the name
*
* @return Result, either the moved message or some error.
* @return Result, either a vec of <doc-id, message> 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.
*/
Result<Message> move_message(Store::Id id,
Option<const std::string&> target_mdir = Nothing,
Option<Flags> new_flags = Nothing,
bool change_name = false);
using IdMessageVec = std::vector<std::pair<Id, Message>>;
Result<IdMessageVec> move_message(Store::Id id,
Option<const std::string&> target_mdir = Nothing,
Option<Flags> new_flags = Nothing,
MoveOptions opts = MoveOptions::None);
/**
* Prototype for the ForEachMessageFunc
@ -466,6 +482,7 @@ private:
};
MU_ENABLE_BITOPS(Store::Options);
MU_ENABLE_BITOPS(Store::MoveOptions);
} // namespace Mu

View File

@ -565,9 +565,12 @@ Boo!
/*
* mark as read, i.e. move to cur/; ensure it really moved.
*/
auto moved_msg = store.move_message(old_docid, Nothing, Flags::Seen, rename);
assert_valid_result(moved_msg);
const auto new_path = moved_msg->path();
auto move_opts{rename ? Store::MoveOptions::ChangeName : Store::MoveOptions::None};
auto moved_msgs = store.move_message(old_docid, Nothing, Flags::Seen, move_opts);
assert_valid_result(moved_msgs);
g_assert_true(moved_msgs->size() == 1);
const auto& moved_msg{moved_msgs->at(0).second};
const auto new_path = moved_msg.path();
if (!rename)
assert_equal(new_path, store.properties().root_maildir + "/inbox/cur/msg:2,S");
g_assert_cmpuint(store.size(), ==, 1);
@ -576,7 +579,7 @@ Boo!
/* also ensure that the cached sexp for the message has been updated;
* that's what mu4e uses */
const auto moved_sexp{moved_msg->sexp()};
const auto moved_sexp{moved_msg.sexp()};
//std::cerr << "@@ " << *moved_msg << '\n';
g_assert_true(moved_sexp.plistp());
g_assert_true(moved_sexp.has_prop(":path"));
@ -666,6 +669,57 @@ Boo!
}
}
static void
test_related_dup_threaded()
{
// test message sent to self, and copy of received msg.
const auto test_msg = R"(From: "Edward Mallory" <ed@leviathan.gb>
To: "Laurence Oliphant <oli@hotmail.com>
Subject: Boo
Date: Wed, 07 Dec 2022 18:38:06 +0200
Message-ID: <875yentbhg.fsf@djcbsoftware.nl>
MIME-Version: 1.0
Content-Type: text/plain
Boo!
)";
const TestMap test_msgs = {
{"sent/cur/msg1", test_msg },
{"inbox/cur/msg1", test_msg },
{"inbox/cur/msg2", test_msg }};
TempDir tdir;
auto store{make_test_store(tdir.path(), test_msgs, {})};
g_assert_cmpuint(store.size(), ==, 3);
// normal query should give 2
{
auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
QueryFlags::None);
assert_valid_result(qr);
g_assert_cmpuint(qr->size(), ==, 2);
}
// a related query should give 3
{
auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
QueryFlags::IncludeRelated);
assert_valid_result(qr);
g_assert_cmpuint(qr->size(), ==, 3);
}
// a related/threading query should give 3.
{
auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
QueryFlags::IncludeRelated | QueryFlags::Threading);
assert_valid_result(qr);
g_assert_cmpuint(qr->size(), ==, 3);
}
}
int
main(int argc, char* argv[])
@ -689,6 +743,8 @@ main(int argc, char* argv[])
test_duplicate_refresh_rename);
g_test_add_func("/store/query/term-split",
test_term_split);
g_test_add_func("/store/query/related-dup-threaded",
test_related_dup_threaded);
return g_test_run();
}

View File

@ -22,6 +22,7 @@
#include <glib.h>
#include <stdlib.h>
#include <thread>
#include <array>
#include <unistd.h>
#include <time.h>
#include <fstream>
@ -372,18 +373,95 @@ Yes, that would be excellent.
// Move the message from new->cur
std::this_thread::sleep_for(1s); /* ctime should change */
const auto msg3 = store->move_message(msg->docid(), {}, Flags::Seen);
assert_valid_result(msg3);
assert_equal(msg3->maildir(), "/a");
assert_equal(msg3->path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S");
g_assert_true(::access(msg3->path().c_str(), R_OK)==0);
const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen);
assert_valid_result(msgs3);
g_assert_true(msgs3->size() == 1);
const auto& msg3{msgs3->at(0).second};
assert_equal(msg3.maildir(), "/a");
assert_equal(msg3.path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S");
g_assert_true(::access(msg3.path().c_str(), R_OK)==0);
g_assert_false(::access(oldpath.c_str(), R_OK)==0);
g_debug("%s", msg3->sexp().to_string().c_str());
g_debug("%s", msg3.sexp().to_string().c_str());
g_assert_cmpuint(store->size(), ==, 1);
}
static void
test_store_move_dups()
{
const std::string msg_text =
R"(From: Valentine Michael Smith <mike@example.com>
To: Raul Endymion <raul@example.com>
Subject: Re: multi-eq hash tables
Date: Tue, 03 May 2022 20:58:02 +0200
Message-ID: <87h766tzzz.fsf@gnus.org>
Yes, that would be excellent.
)";
TempDir tempdir2;
// create a message file + dups
const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a");
assert_valid_result(res1);
const auto res2 = maildir_mkdir(tempdir2.path() + "/Maildir/b");
assert_valid_result(res2);
auto msg1_path = tempdir2.path() + "/Maildir/a/new/msg123";
auto msg2_path = tempdir2.path() + "/Maildir/a/cur/msgabc:2,S";
auto msg3_path = tempdir2.path() + "/Maildir/b/cur/msgdef:2,RS";
TempDir tempdir;
auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir", {}, {})};
assert_valid_result(store);
std::vector<Store::Id> ids;
for (auto&& p: {msg1_path, msg2_path, msg3_path}) {
std::ofstream output{p};
output.write(msg_text.c_str(), msg_text.size());
output.close();
auto res = store->add_message(p);
assert_valid_result(res);
ids.emplace_back(*res);
}
g_assert_cmpuint(store->size(), ==, 3);
// mark main message (+ dups) as seen
auto mres = store->move_message(ids.at(0), {},
Flags::Seen | Flags::Flagged | Flags::Passed,
Store::MoveOptions::DupFlags);
assert_valid_result(mres);
// al three dups should have been updated
g_assert_cmpuint(mres->size(), ==, 3);
// first should be the original
g_assert_cmpuint(mres->at(0).first, ==, ids.at(0));
{ // Message 1
const Message& msg = mres->at(0).second;
assert_equal(msg.path(), tempdir2.path() + "/Maildir/a/cur/msg123:2,FPS");
g_assert_true(msg.flags() == (Flags::Seen|Flags::Flagged|Flags::Passed));
}
// note: Seen and Passed should be added to msg2/3, but Flagged shouldn't
// msg3 should loose its R flag.
auto check_msg2 = [&](const Message& msg) {
assert_equal(msg.path(), tempdir2.path() + "/Maildir/a/cur/msgabc:2,PS");
};
auto check_msg3 = [&](const Message& msg) {
assert_equal(msg.path(), tempdir2.path() + "/Maildir/b/cur/msgdef:2,PS");
};
if (mres->at(1).first == ids.at(1)) {
check_msg2(mres->at(1).second);
check_msg3(mres->at(2).second);
} else {
check_msg2(mres->at(2).second);
check_msg3(mres->at(1).second);
}
}
static void
test_store_fail()
{
@ -412,7 +490,8 @@ main(int argc, char* argv[])
test_message_mailing_list);
g_test_add_func("/store/message/attachments",
test_message_attachments);
g_test_add_func("/store/index/move", test_index_move);
g_test_add_func("/store/index/index-move", test_index_move);
g_test_add_func("/store/index/move-dups", test_store_move_dups);
g_test_add_func("/store/index/fail", test_store_fail);
return g_test_run();