Merge branch 'wip/djcb/move'

This commit is contained in:
Dirk-Jan C. Binnema 2023-09-24 17:32:22 +03:00
commit 942b822d4a
25 changed files with 772 additions and 145 deletions

View File

@ -138,7 +138,6 @@ test_flags_from_delta_expr()
"-S", Flags::Seen).value() ==
Flags::Unread);
static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() ==
(Flags::Seen|Flags::Passed|Flags::Replied));
/* '-B' is invalid */

View File

@ -144,7 +144,6 @@ constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{
MessageFlagInfo{Flags::HasAttachment,'a', "attach", MessageFlagCategory::Content,
"Has at least one attachment"
},
MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo,
"New or not seen message"
},
@ -289,9 +288,9 @@ flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false)
* @param expr delta expression
* @param flags existing flags
* @param ignore_invalid if @true, ignore invalid flags, otherwise return
* nullopt if an invalid flag is encountered
* Nothing if an invalid flag is encountered
*
* @return new flags, or nullopt in case of error
* @return new flags, or Nothing in case of error
*/
constexpr Option<Flags>
flags_from_delta_expr(std::string_view expr, Flags flags,
@ -317,7 +316,6 @@ flags_from_delta_expr(std::string_view expr, Flags flags,
}
return imply_unread(flags);
}
/**
@ -329,8 +327,7 @@ flags_from_delta_expr(std::string_view expr, Flags flags,
* @return either messages flags or Nothing in case of error.
*/
constexpr Option<Flags>
flags_from_expr(std::string_view expr,
Option<Flags> flags = Nothing)
flags_from_expr(std::string_view expr, Option<Flags> flags = Nothing)
{
if (expr.empty())
return Nothing;
@ -367,7 +364,7 @@ flags_filter(Flags flags, MessageFlagCategory cat)
* @return filtered flags
*/
constexpr Flags
flags_mail_dir_file(Flags flags)
flags_maildir_file(Flags flags)
{
for (auto&& info : AllMessageFlagInfos)
if (info.category != MessageFlagCategory::Maildir &&

View File

@ -421,7 +421,7 @@ Mu::maildir_determine_target(const std::string& old_path,
Flags newflags,
bool new_name)
{
newflags = flags_mail_dir_file(newflags); // filter out irrelevant flags.
newflags = flags_maildir_file(newflags); // filter out irrelevant flags.
/* sanity checks */
if (const auto checked{check_determine_target_params(

View File

@ -64,7 +64,6 @@ Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700,
*/
Result<void> maildir_link(const std::string& src, const std::string& targetpath,
bool unique_names=true);
/**
* Recursively delete all the symbolic links in a directory tree
*
@ -95,7 +94,7 @@ Result<void> maildir_move_message(const std::string& oldpath,
*
* @param old_path an absolute file system path to an existing message in an
* actual maildir
* @param root_maildir_path the absolete file system path under which
* @param root_maildir_path the absolute file system path under which
* all maildirs live.
* @param target_maildir the target maildir; note that this the base-level
* Maildir, ie. /home/user/Maildir/archive, and must _not_ include the
@ -103,18 +102,18 @@ Result<void> maildir_move_message(const std::string& oldpath,
* same filesystem. Can be empty if the message should not be moved to
* a different maildir; note that this may still involve a
* move to another directory (say, from new/ to cur/)
* @param flags to set for the target (influences the filename, path). Any none-Maildir/File
* flags are ignored.
* @param flags to set for the target (influences the filename, path).
* Any non-Maildir/File flags are ignored.
* @param new_name whether to change the basename of the file
*
* @return Full path name of the target file or an Error
*/
Result<std::string>
maildir_determine_target(const std::string& old_path,
const std::string& root_maildir_path,
const std::string& target_maildir,
Flags newflags,
bool new_name);
const std::string& root_maildir_path,
const std::string& target_maildir,
Flags newflags,
bool new_name);
} // namespace Mu

View File

@ -925,9 +925,8 @@ Server::Private::perform_move(Store::Id docid,
/* 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 ids{unwrap(store().move_message(docid, maildir, flags, move_opts))};
for (auto&& id: ids) {
const auto id_paths{unwrap(store().move_message(docid, maildir, flags, move_opts))};
for (auto& [id,path]: id_paths) {
auto idmsg{store().find_message(id)};
if (!idmsg)
mu_warning("failed to find message for id {}", id);
@ -1113,15 +1112,14 @@ Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
}
// move message + dups, present results.
Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
if (rename)
move_opts |= Store::MoveOptions::ChangeName;
auto&& ids = unwrap(store().move_message(docid, {}, nflags, move_opts));
for (auto&& [id, moved_msg]: store().find_messages(ids)) {
const auto ids{Store::id_vec(unwrap(store().move_message(docid, {}, nflags, move_opts)))};
for (auto&& [id, moved_msg]: store().find_messages(ids))
output(mu_format("({} {})", id == docid ? ":view" : ":update",
msg_sexp_str(moved_msg, id, {})));
}
}
void

View File

@ -145,10 +145,12 @@ struct Store::Private {
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);
using PathMessage = std::pair<std::string, Message>;
Result<PathMessage> move_message_unlocked(Message&& msg,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
MoveOptions opts);
XapianDb xapian_db_;
Config config_;
ContactsCache contacts_cache_;
@ -341,8 +343,7 @@ Store::indexer()
Result<Store::Id>
Store::add_message(Message& msg, bool use_transaction, bool is_new)
{
const auto mdir{maildir_from_path(msg.path(),
root_maildir())};
const auto mdir{maildir_from_path(msg.path(), root_maildir())};
if (!mdir)
return Err(mdir.error());
if (auto&& res = msg.set_maildir(mdir.value()); !res)
@ -428,6 +429,23 @@ Store::find_message(Store::Id docid) const
return priv_->find_message_unlocked(docid);
}
Option<Store::Id>
Store::find_message_id(const std::string& path) const
{
constexpr auto path_field{field_from_id(Field::Id::Path)};
std::lock_guard guard{priv_->lock_};
auto enq{xapian_db().enquire()};
enq.set_query(Xapian::Query{path_field.xapian_term(path)});
if (auto mset{enq.get_mset(0, 1)}; mset.empty())
return Nothing; // message not found
else
return Some(*mset.begin());
}
Store::IdMessageVec
Store::find_messages(IdVec ids) const
{
@ -443,7 +461,7 @@ Store::find_messages(IdVec ids) const
}
/**
* Move a message in store and filesystem.
* Move a message in store and filesystem; with DryRun, only calculate the target name.
*
* Lock is assumed taken already
*
@ -454,7 +472,7 @@ Store::find_messages(IdVec ids) const
*
* @return the Message after the moving, or an Error
*/
Result<Message>
Result<Store::Private::PathMessage>
Store::Private::move_message_unlocked(Message&& msg,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
@ -472,21 +490,25 @@ Store::Private::move_message_unlocked(Message&& msg,
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)
return Err(res.error());
// in dry-run mode, we only determine the target-path
if (none_of(opts & MoveOptions::DryRun)) {
/* 3. file move worked, now update the message with the new info.*/
if (auto&& res = msg.update_after_move(
target_path.value(), target_maildir, target_flags); !res)
return Err(res.error());
/* 2. let's move it */
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
return Err(res.error());
/* 4. update message worked; re-store it */
if (auto&& res = update_message_unlocked(msg, old_path); !res)
return Err(res.error());
/* 3. file move worked, now update the message with the new info.*/
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 = update_message_unlocked(msg, old_path); !res)
return Err(res.error());
}
/* 6. Profit! */
return Ok(std::move(msg));
return Ok(PathMessage{std::move(*target_path), std::move(msg)});
}
Store::IdVec
@ -498,8 +520,7 @@ Store::find_duplicates(const std::string& message_id) const
}
Result<Store::IdVec>
Result<Store::IdPathVec>
Store::move_message(Store::Id id,
Option<const std::string&> target_mdir,
Option<Flags> new_flags,
@ -519,16 +540,15 @@ Store::move_message(Store::Id id,
return Err(Error::Code::Store, "cannot find message <{}>", id);
const auto message_id{msg->message_id()};
auto res{priv_->move_message_unlocked(std::move(*msg),
target_mdir, new_flags, opts)};
auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)};
if (!res)
return Err(res.error());
IdVec ids{id};
IdPathVec id_paths{{id, res->first}};
if (none_of(opts & Store::MoveOptions::DupFlags) || message_id.empty() || !new_flags)
return Ok(std::move(ids));
return Ok(std::move(id_paths));
/* handle the dupflags case; i.e. apply (a subset of) the flags to
/* handle the dup-flags case; i.e. apply (a subset of) the flags to
* all messages with the same message-id as well */
auto dups{priv_->find_duplicates_unlocked(*this, message_id)};
for (auto&& dupid: dups) {
@ -542,20 +562,34 @@ Store::move_message(Store::Id id,
/* For now, don't change Draft/Flagged/Trashed */
const auto dup_flags{filter_dup_flags(dup_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) */
/* use the updated new_flags and MoveOptions without DupFlags (so we don't
* recurse) */
opts = opts & ~MoveOptions::DupFlags;
if (auto dup_res = priv_->move_message_unlocked(
std::move(*dup_msg), Nothing,
dup_flags,
Store::MoveOptions::None); !dup_res)
std::move(*dup_msg), Nothing, dup_flags, opts); !dup_res)
mu_warning("failed to move dup: {}", dup_res.error().what());
else
ids.emplace_back(dupid);
id_paths.emplace_back(dupid, dup_res->first);
}
return Ok(std::move(ids));
// sort the dup paths by name;
std::sort(id_paths.begin() + 1, id_paths.end(),
[](const auto& idp1, const auto& idp2) { return idp1.second < idp2.second; });
return Ok(std::move(id_paths));
}
Store::IdVec
Store::id_vec(const IdPathVec& ips)
{
IdVec idv;
for (auto&& ip: ips)
idv.emplace_back(ip.first);
return idv;
}
time_t
Store::dirstamp(const std::string& path) const
{
@ -660,9 +694,11 @@ std::vector<std::string>
Store::maildirs() const
{
std::vector<std::string> mdirs;
const auto prefix_size = root_maildir().size();
const auto prefix_size{root_maildir().size()};
Scanner::Handler handler = [&](const std::string& path, auto&& _1, auto&& _2) {
mdirs.emplace_back(path.substr(prefix_size));
auto md{path.substr(prefix_size)};
mdirs.emplace_back(std::move(md.empty() ? "/" : md));
return true;
};

View File

@ -47,6 +47,8 @@ public:
using Id = Xapian::docid; /**< Id for a message in the store */
static constexpr Id InvalidId = 0; /**< Invalid store id */
using IdVec = std::vector<Id>; /**< Vector of document ids */
using IdPathVec = std::vector<std::pair<Id, std::string>>;
/**< vector of id, path pairs */
/**
* Configuration options.
@ -246,6 +248,15 @@ public:
*/
Option<Message> find_message(Id id) const;
/**
* Find a message's docid based on its path
*
* @param path path to the message
*
* @return the docid or Nothing if not found
*/
Option<Id> find_message_id(const std::string& path) const;
/**
* Find the messages for the given ids
*
@ -282,27 +293,38 @@ public:
enum struct MoveOptions {
None = 0, /**< Defaults */
ChangeName = 1 << 0, /**< Change the name when moving */
DupFlags = 1 << 1, /**< Update flags for duplicate messages too*/
DupFlags = 1 << 1, /**< Update flags for duplicate messages too */
DryRun = 1 << 2, /**< Don't really move, just determine target paths */
};
/**
* 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.
*
* @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
* @param opts move options
*
* @return Result, either an IdVec with ids 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.
*/
Result<IdVec> move_message(Store::Id id,
Option<const std::string&> target_mdir = Nothing,
Option<Flags> new_flags = Nothing,
MoveOptions opts = MoveOptions::None);
Result<IdPathVec> move_message(Store::Id id,
Option<const std::string&> target_mdir = Nothing,
Option<Flags> new_flags = Nothing,
MoveOptions opts = MoveOptions::None);
/**
* Convert IdPathVec -> IdVec
*
* @param ips idpath vector
*
* @return vector of ids
*/
static IdVec id_vec(const IdPathVec& ips);
/**
* Prototype for the ForEachMessageFunc

View File

@ -71,13 +71,7 @@ make_test_store(const std::string& test_path, const TestMap& test_map,
assert_valid_result(store);
/* index the messages */
auto res = store->indexer().start({});
g_assert_true(res);
while(store->indexer().is_running()) {
using namespace std::chrono_literals;
std::this_thread::sleep_for(100ms);
}
g_assert_true(store->indexer().start({},true/*block*/));
if (test_map.size() > 0)
g_assert_false(store->empty());
@ -575,7 +569,7 @@ Boo!
assert_valid_result(moved_msgs);
g_assert_true(moved_msgs->size() == 1);
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0));
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0).first);
g_assert_true(!!moved_msg_opt);
const auto&moved_msg = std::move(*moved_msg_opt);
const auto new_path = moved_msg.path();

View File

@ -381,7 +381,7 @@ Yes, that would be excellent.
const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen);
assert_valid_result(msgs3);
g_assert_true(msgs3->size() == 1);
auto&& msg3_opt{store->find_message(msgs3->at(0))};
auto&& msg3_opt{store->find_message(msgs3->at(0).first/*id*/)};
g_assert_true(!!msg3_opt);
auto&& msg3{std::move(*msg3_opt)};
@ -442,11 +442,11 @@ Yes, that would be excellent.
assert_valid_result(mres);
mu_info("found {} matches", mres->size());
for (auto&& m: *mres)
mu_info("id: {}", m);
mu_info("id: {}: {}", m.first, m.second);
// al three dups should have been updated
g_assert_cmpuint(mres->size(), ==, 3);
auto&& id_msgs{store->find_messages(*mres)};
auto&& id_msgs{store->find_messages(Store::id_vec(*mres))};
// first should be the original
g_assert_cmpuint(id_msgs.at(0).first, ==, ids.at(0));

View File

@ -105,5 +105,11 @@ test('test-html-to-text',
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_utils_dep]))
test('test-error',
executable('test-error', 'mu-error.cc',
install: false,
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_utils_dep]))
subdir('tests')

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2020-2023 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
@ -148,20 +148,16 @@ using ArgMap = CommandHandler::ArgMap;
using ArgInfo = CommandHandler::ArgInfo;
using CommandInfo = CommandHandler::CommandInfo;
static bool
static Result<void>
call(const CommandInfoMap& cmap, const std::string& str) try {
const auto cmd{Command::make_parse(str)};
if (!cmd)
throw Error(Error::Code::Internal, "invalid sexp str");
const auto res{CommandHandler(cmap).invoke(*cmd)};
return !!res;
if (const auto cmd{Command::make_parse(str)}; !cmd)
return Err(Error::Code::Internal, "invalid s-expression '{}'", str);
else
return CommandHandler(cmap).invoke(*cmd);
} catch (const Error& err) {
mu_warning("{}", err.what());
return false;
return Err(Error{err});
}
static void
@ -169,18 +165,49 @@ test_command()
{
allow_warnings();
CommandInfoMap cmap;
cmap.emplace(
CommandInfoMap ci_map;
ci_map.emplace(
"my-command",
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
"My command,",
{}});
ci_map.emplace(
"another-command",
CommandInfo{
ArgMap{
{":queries", ArgInfo{Sexp::Type::List, false,
"queries for which to get read/unread numbers"}},
{":symbol", ArgInfo{Sexp::Type::Symbol, true,
"some boring symbol"}},
{":bool", ArgInfo{Sexp::Type::Symbol, true,
"some even more boring boolean symbol"}},
{":symbol2", ArgInfo{Sexp::Type::Symbol, false,
"some even more boring symbol"}},
{":bool2", ArgInfo{Sexp::Type::Symbol, false,
"some boring boolean symbol"}},
},
"get unread/totals information for a list of queries",
[&](const auto& params) {
const auto queries{params.string_vec_arg(":queries")
.value_or(std::vector<std::string>{})};
g_assert_cmpuint(queries.size(),==,3);
g_assert_true(params.bool_arg(":bool").value_or(false) == true);
assert_equal(params.symbol_arg(":symbol").value_or("boo"), "sym");
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
g_assert_false(!!params.bool_arg(":bool2"));
g_assert_false(!!params.bool_arg(":symbol2"));
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
}});
CommandHandler handler(std::move(ci_map));
const auto cmap{handler.info_map()};
assert_valid_result(call(cmap, "(my-command :param1 \"hello\")"));
assert_valid_result(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
g_assert_false(!!call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
assert_valid_result(call(cmap, "(another-command :queries (\"foo\" \"bar\" \"cuux\") "
":symbol sym :bool true)"));
}
static void

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2020-2023 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
@ -218,7 +218,7 @@ struct CommandHandler {
* first, then alphabetical.
*
* @return vec with the sorted names.
*/
*/ /* LCOV_EXCL_START */
std::vector<std::string> sorted_argnames() const {
// sort args -- by required, then alphabetical.
std::vector<std::string> names;
@ -234,6 +234,7 @@ struct CommandHandler {
});
return names;
}
/* LCOV_EXCL_STOP */
};
@ -263,6 +264,7 @@ private:
const CommandInfoMap cmap_;
};
/* LCOV_EXCL_START */
static inline std::ostream&
operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
{
@ -270,6 +272,7 @@ operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
return os;
}
/* LCOV_EXCL_STOP */
static inline std::ostream&
operator<<(std::ostream& os, const CommandHandler::CommandInfo& info)

64
lib/utils/mu-error.cc Normal file
View File

@ -0,0 +1,64 @@
/*
** Copyright (C) 2023 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
** 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.
**
*/
#if BUILD_TESTS
#include "mu-error.hh"
#include "mu-test-utils.hh"
using namespace Mu;
static void
test_fill_error()
{
const Error err{Error::Code::Internal, "boo!"};
GError *gerr{};
err.fill_g_error(&gerr);
assert_equal(gerr->message, "boo!");
g_assert_cmpint(gerr->code, ==, static_cast<int>(err.code()));
g_clear_error(&gerr);
}
static void
test_add_hint()
{
Error err(Error::Code::Internal, "baa!");
err.add_hint("hello");
assert_equal(err.hint(), "hello");
}
int
main(int argc, char* argv[])
{
mu_test_init(&argc, &argv);
g_test_add_func("/error/fill-error", test_fill_error);
g_test_add_func("/error/add-hint", test_add_hint);
return g_test_run();
}
#endif /*BUILD_TESTS*/

View File

@ -44,8 +44,8 @@ constexpr uint32_t err_enum(uint8_t code, uint8_t rv, uint8_t cat) {
struct Error final : public std::exception {
// 16 lower bits are for the error code the next 8 bits is for the return code
// upper byte is for flags
// 16 lower bits are for the error code;the next 8 bits are for the return code; the upper
// byte is for flags
static constexpr uint8_t SoftError = 1;
enum struct Code: uint32_t {

View File

@ -36,11 +36,13 @@
using namespace Mu;
/* LCOV_EXCL_START*/
bool
Mu::mu_test_mu_hacker()
{
return !!g_getenv("MU_HACKER");
}
/* LCOV_EXCL_STOP*/
const char*
@ -65,8 +67,10 @@ Mu::set_en_us_utf8_locale()
setlocale(LC_ALL, "en_US.UTF-8");
if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
/* LCOV_EXCL_START*/
mu_println("Note: Unit tests require the en_US.utf8 locale. "
"Ignoring test cases.");
/* LCOV_EXCL_STOP*/
return false;
}
@ -130,13 +134,10 @@ Mu::TempDir::~TempDir()
return;
}
/* ugly */
GError *err{};
const auto cmd{mu_format("/bin/rm -rf '{}'", path_)};
if (!g_spawn_command_line_sync(cmd.c_str(), NULL,
NULL, NULL, &err)) {
mu_warning("error: {}", err ? err->message : "?");
g_clear_error(&err);
if (auto&& res{run_command0({RM_PROGRAM, "-fr", path_})}; !res) {
/* LCOV_EXCL_START*/
mu_warning("error removing {}: {}", path_, format_as(res.error()));
/* LCOV_EXCL_STOP*/
} else
mu_debug("removed '{}'", path_);
}

View File

@ -158,10 +158,13 @@ Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome)
return mu_config;
case Mu::RuntimePath::Scripts:
return join_paths(mu_config, "scripts");
/*LCOV_EXCL_START*/
default:
throw std::logic_error("unknown path");
/*LCOV_EXCL_STOP*/
}
}
/* LCOV_EXCL_START*/
static gpointer
cancel_wait(gpointer data)
@ -212,6 +215,7 @@ Mu::g_cancellable_new_with_timeout(guint timeout)
}
/* LCOV_EXCL_STOP*/
/* LCOV_EXCL_START*/
Result<std::string>
Mu::read_from_stdin()
{
@ -234,13 +238,14 @@ Mu::read_from_stdin()
G_MEMORY_OUTPUT_STREAM(outmem))),
g_memory_output_stream_get_size(G_MEMORY_OUTPUT_STREAM(outmem))});
}
/* LCOV_EXCL_STOP*/
/*
* Set the child to a group leader to avoid being killed when the
* parent group is killed.
*/
/*LCOV_EXCL_START*/
static void
maybe_setsid (G_GNUC_UNUSED gpointer user_data)
{
@ -248,6 +253,7 @@ maybe_setsid (G_GNUC_UNUSED gpointer user_data)
setsid();
#endif /*HAVE_SETSID*/
}
/*LCOV_EXCL_STOP*/
Result<Mu::CommandOutput>
Mu::run_command(std::initializer_list<std::string> args, bool try_setsid)
@ -451,7 +457,20 @@ test_join_paths()
assert_equal(join_paths("/a/b///c/d//", "e"), "/a/b/c/d/e");
}
static void
test_runtime_paths()
{
TempDir tdir;
assert_equal(runtime_path(RuntimePath::Cache, tdir.path()), tdir.path());
assert_equal(runtime_path(RuntimePath::XapianDb, tdir.path()),
join_paths(tdir.path(), "xapian"));
assert_equal(runtime_path(RuntimePath::Bookmarks, tdir.path()),
join_paths(tdir.path(), "bookmarks"));
assert_equal(runtime_path(RuntimePath::Config, tdir.path()), tdir.path());
assert_equal(runtime_path(RuntimePath::Scripts, tdir.path()),
join_paths(tdir.path(), "scripts"));
}
int
main(int argc, char* argv[])
@ -473,6 +492,8 @@ main(int argc, char* argv[])
test_program_in_path);
g_test_add_func("/utils/join-paths",
test_join_paths);
g_test_add_func("/utils/runtime-paths",
test_runtime_paths);
return g_test_run();
}

View File

@ -47,7 +47,6 @@
namespace Mu {
/*
* Separator characters used in various places; importantly,
* they are not used in UTF-8
@ -55,7 +54,6 @@ namespace Mu {
constexpr const auto SepaChar1 = '\xfe';
constexpr const auto SepaChar2 = '\xff';
/*
* Logging/printing/formatting functions connect libfmt with the Glib logging
* system. We wrap so perhaps at some point (C++23?) we can use std:: instead.
@ -88,6 +86,7 @@ void mu_warning(fmt::format_string<T...> frm, T&&... args) noexcept {
g_log("mu", G_LOG_LEVEL_WARNING, "%s",
fmt::format(frm, std::forward<T>(args)...).c_str());
}
/* LCOV_EXCL_START*/
template<typename...T>
void mu_critical(fmt::format_string<T...> frm, T&&... args) noexcept {
g_log("mu", G_LOG_LEVEL_CRITICAL, "%s",
@ -98,6 +97,7 @@ void mu_error(fmt::format_string<T...> frm, T&&... args) noexcept {
g_log("mu", G_LOG_LEVEL_ERROR, "%s",
fmt::format(frm, std::forward<T>(args)...).c_str());
}
/* LCOV_EXCL_STOP*/
/*
* Printing; add our wrapper functions, one day we might be able to use std::
@ -355,8 +355,10 @@ struct StopWatch {
StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {}
~StopWatch() {
const auto us{static_cast<double>(to_us(Clock::now() - start_))};
/* LCOV_EXCL_START*/
if (us > 2000000)
mu_debug("sw: {}: finished after {:.1f} s", name_, us / 1000000);
/* LCOV_EXCL_STOP*/
else if (us > 2000)
mu_debug("sw: {}: finished after {:.1f} ms", name_, us / 1000);
else

115
man/mu-move.1.org Normal file
View File

@ -0,0 +1,115 @@
#+TITLE: MU MOVE
#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
* NAME
*mu move* - move a message file or change its flags
* SYNOPSIS
*mu [common-options] move [options] <src> [--flags=<flags>] [<target>]*
* DESCRIPTION
*mu move* is the command for moving messages in a Maildir or changing their flags.
For any change, both the message file in the file system as well as its
representation in the database are updated accordingly.
The source message file and target-maildir must reside under the root-maildir
for mu's database (see *mu info store*).
* MOVE OPTIONS
** --flags=<flags>
specify the new message flags. See *FLAGS* for details.
** --change-name
change the basename of the message file when moving; this can be useful when
using some external tools such as *mbsync(1)* which otherwise get confused
** --update-dups
update the flags of duplicate messages too, where "duplicate messages" are
defined as all message that share the same message-id. Note that the
Draft/Flagged/Trashed flags are deliberately _not_ changed if you change those on
the source message.
** --dry-run,-n
print the target filename(s), but don't change anything.
Note that with the ~--change-name~, the target name is not constant, so you cannot
use a dry-run to predict the exact name when doing a 'real' run.
#+include: "common-options.inc" :minlevel 1
* FLAGS
(Note: if you are not familiar with Maildirs, please refer to the *maildir(5)*
man-page, or see http://cr.yp.to/proto/maildir.html)
The message flags specify the Maildir-metadata for a message and are represented
by uppercase letters at the end of the message file name for all 'non-new'
messages, i.e. messages that live in the ~cur/~ sub-directory of a Maildir.
| Flag | Meaning |
|------+------------------------------------|
| D | Draft message |
| F | Flagged message |
| P | Passed message (i.e., 'forwarded') |
| R | Replied message |
| S | Seen message |
| T | Trashed; to be deleted later |
New messages (in the ~new/~ sub-directory) do not have flags encoded in their
file-name; but we *mu* uses 'N' in the ~--flags~ to represent that:
| Flag | Meaning |
|------+---------|
| N | New |
Thus, changing flags means changing the letters at the end of the message
file-name, except when setting or removing the 'N' (new) flag. Setting or
un-setting the New flag causes the message is to be moved from ~cur/~ to ~new/~ or
vice-versa, respectively. When marking a message as New, it looses the other
flags.
* ABSOLUTE AND RELATIVE FLAGS
You can specify the flags with the ~--flags~ parameter, and do either with either
*absolute* or *relative* flags.
Absolute flags just specify the new flags by their letters; e.g. to specify a
/Trashed/, /Seen/, /Replied/ message, you'd use ~--flags STR~.
#+end_example
Relative flags are relative to the current flags for some message, and each of
the flags is prefixed with either ~+~ ("add this flag") or ~-~ ("remove this flag").
So to add the /Seen/ flag and remove the /Draft/ flag from whatever the message
already has, ~--flags +S-D~.
You cannot combine relative and relative flags.
* EXAMPLES
** change some flags
#+begin_example
$ mu move /home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,S --flags +F
/home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,FS
#+end_example
** move to a different maildir
#+begin_example
$ mu move /home/user/Maildir/project1/cur/1695559560.a73985881f4611ac2.hostname!2,S /project2
/home/user/Maildir/project2/cur/1695559560.a73985881f4611ac2.hostname!2,S
#+end_example
#+include: "prefooter.inc" :minlevel 1
* SEE ALSO
*maildir(5)*

View File

@ -26,6 +26,7 @@ mu = executable(
'mu-cmd-init.cc',
'mu-cmd-index.cc',
'mu-cmd-mkdir.cc',
'mu-cmd-move.cc',
'mu-cmd-remove.cc',
'mu-cmd-script.cc',
'mu-cmd-server.cc',
@ -76,6 +77,13 @@ test('test-cmd-mkdir',
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_dep]))
test('test-cmd-move',
executable('test-cmd-move',
'mu-cmd-move.cc',
install: false,
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_dep]))
test('test-cmd-remove',
executable('test-cmd-remove',
'mu-cmd-remove.cc',

View File

@ -113,20 +113,13 @@ test_add_fail()
int
main(int argc, char* argv[]) try {
main(int argc, char* argv[])
{
mu_test_init(&argc, &argv);
g_test_add_func("/cmd/add/ok", test_add_ok);
g_test_add_func("/cmd/add/fail", test_add_fail);
return g_test_run();
} catch (const Error& e) {
mu_printerrln("{}", e.what());
return 1;
} catch (...) {
mu_printerrln("caught exception");
return 1;
}
#endif /*BUILD_TESTS*/

276
mu/mu-cmd-move.cc Normal file
View File

@ -0,0 +1,276 @@
/*
** Copyright (C) 2023 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
** 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 "config.h"
#include "mu-cmd.hh"
#include "mu-store.hh"
#include "mu-maildir.hh"
#include "message/mu-message-file.hh"
#include <unistd.h>
using namespace Mu;
Result<void>
Mu::mu_cmd_move(Mu::Store& store, const Options& opts)
{
const auto& src{opts.move.src};
if (::access(src.c_str(), R_OK) != 0 || determine_dtype(src) != DT_REG)
return Err(Error::Code::InvalidArgument,
"Source is not a readable file");
auto id{store.find_message_id(src)};
if (!id)
return Err(Error{Error::Code::InvalidArgument,
"Source file is not present in database"}
.add_hint("Perhaps run mu index?"));
std::string dest{opts.move.dest};
Option<const std::string&> dest_path;
if (dest.empty() && opts.move.flags.empty())
return Err(Error::Code::InvalidArgument,
"Must have at least one of destination and flags");
else if (!dest.empty()) {
const auto mdirs{store.maildirs()};
mu_printerrln("XXXX");
for (auto&& m:mdirs)
mu_printerrln("m:'{}'", m);
if (!seq_some(mdirs, [&](auto &&d){ return d == dest;}))
return Err(Error{Error::Code::InvalidArgument,
"No maildir '{}' in store", dest}
.add_hint("Try 'mu mkdir'"));
else
dest_path = dest;
}
auto old_flags{flags_from_path(src)};
if (!old_flags)
return Err(Error::Code::InvalidArgument, "failed to determine old flags");
Flags new_flags;
if (!opts.move.flags.empty()) {
if (auto&& nflags{flags_from_expr(to_string_view(opts.move.flags),
*old_flags)}; !nflags)
return Err(Error::Code::InvalidArgument, "Invalid flags");
else
new_flags = flags_maildir_file(*nflags);
if (any_of(new_flags & Flags::New) && new_flags != Flags::New)
return Err(Error{Error::Code::File,
"the New flag cannot be combined with others"}
.add_hint("See the mu-move manpage"));
}
Store::MoveOptions move_opts{};
if (opts.move.change_name)
move_opts |= Store::MoveOptions::ChangeName;
if (opts.move.update_dups)
move_opts |= Store::MoveOptions::DupFlags;
if (opts.move.dry_run)
move_opts |= Store::MoveOptions::DryRun;
auto id_paths = store.move_message(*id, dest_path, new_flags, move_opts);
if (!id_paths)
return Err(std::move(id_paths.error()));
for (const auto&[_id, path]: *id_paths)
mu_println("{}", path);
return Ok();
}
#ifdef BUILD_TESTS
/*
* Tests.
*
*/
#include "utils/mu-test-utils.hh"
static void
test_move_dry_run()
{
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", "1220863042.12663_1.mindcrime!2,S")};
{
auto store = Store::make_new(dbpath, testpath, {});
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
// make a message 'New'
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "N", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
assert_equal(res->standard_out, dst + '\n');
g_assert_true(::access(dst.c_str(), F_OK) != 0);
g_assert_true(::access(src.c_str(), F_OK) == 0);
}
// change some flags
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "FP", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FP")};
assert_equal(res->standard_out, dst + '\n');
}
// change some relative flag
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "+F", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FS")};
assert_equal(res->standard_out, dst + '\n');
}
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "-S+P+T", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,PT")};
assert_equal(res->standard_out, dst + '\n');
}
// change maildir
for (auto& o : {"o1", "o2"})
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"/o1", "--flags", "-S+F", "--dry-run"});
assert_valid_command(res);
assert_equal(res->standard_out,
join_paths(testpath,
"o1/cur", "1220863042.12663_1.mindcrime!2,F") + "\n");
}
// change-dups; first create some dups and index them.
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o1/cur")}));
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o2/cur")}));
{
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
// change some flags + update dups
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "-S+S+T+R", "--update-dups", "--dry-run"});
assert_valid_command(res);
auto p{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,RST")};
auto p1{join_paths(testpath, "o1", "cur", "1220863042.12663_1.mindcrime!2,RS")};
auto p2{join_paths(testpath, "o2", "cur", "1220863042.12663_1.mindcrime!2,RS")};
assert_equal(res->standard_out, mu_format("{}\n{}\n{}\n", p, p1, p2));
}
}
static void
test_move_real()
{
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", "1220863042.12663_1.mindcrime!2,S")};
{
auto store = Store::make_new(dbpath, testpath, {});
assert_valid_result(res);
g_assert_true(store->indexer().start({}, true/*block*/));
}
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "N"});
assert_valid_command(res);
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
g_assert_true(::access(dst.c_str(), F_OK) == 0);
g_assert_true(::access(src.c_str(), F_OK) != 0);
}
// change flags, maildir, update-dups
// change-dups; first create some dups and index them.
const auto src2{join_paths(testpath, "cur", "1305664394.2171_402.cthulhu!2,")};
for (auto& o : {"o1", "o2", "o3"})
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o1/cur")}));
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o2/new")}));
{
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
auto res2 = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src2, "/o3",
"--flags", "-S+S+T+R", "--update-dups", "--change-name"});
assert_valid_command(res2);
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
for (auto&& f: split(res2->standard_out, "\n")) {
//mu_println(">> {}", f);
if (f.length() > 2)
g_assert_true(::access(f.c_str(), F_OK) == 0);
}
}
int
main(int argc, char* argv[])
{
mu_test_init(&argc, &argv);
g_test_add_func("/cmd/move/dry-run", test_move_dry_run);
g_test_add_func("/cmd/move/real", test_move_real);
return g_test_run();
}
#endif /*BUILD_TESTS*/

View File

@ -140,6 +140,8 @@ Mu::mu_cmd_execute(const Options& opts) try {
return with_writable_store(mu_cmd_add, opts);
case Options::SubCommand::Remove:
return with_writable_store(mu_cmd_remove, opts);
case Options::SubCommand::Move:
return with_writable_store(mu_cmd_move, opts);
case Options::SubCommand::Index:
return with_writable_store(mu_cmd_index, opts);

View File

@ -127,6 +127,15 @@ Result<void> mu_cmd_init(const Options& opts);
*/
Result<void> mu_cmd_mkdir(const Options& opts);
/**
* execute the 'move' command
*
* @param opts configuration options
*
* @return Ok() or some error
*/
Result<void> mu_cmd_move(Store& store, const Options& opts);
/**
* execute the 'remove' command
*

View File

@ -197,6 +197,12 @@ static const std::function ExpandPath = [](std::string filepath)->std::string {
return filepath = std::move(res.value());
};
// Canonicalize path
static const std::function CanonicalizePath = [](std::string filepath)->std::string {
return filepath = canonicalize_filename(filepath);
};
/*
* common
*/
@ -481,6 +487,31 @@ sub_mkdir(CLI::App& sub, Options& opts)
->required();
}
static void
sub_move(CLI::App& sub, Options& opts)
{
sub.add_flag("--change-name", opts.move.change_name,
"Change name of target file");
sub.add_flag("--update-dups", opts.move.update_dups,
"Update duplicate messages too");
sub.add_flag("--dry-run,-n", opts.move.dry_run,
"Print target name, but do not change anything");
sub.add_option("--flags", opts.move.flags, "Target flags")
->type_name("<flags>");
sub.add_option("source", opts.move.src, "Message file to move")
->type_name("<message-path>")
->transform(ExpandPath, "expand path")
->transform(CanonicalizePath, "canonicalize path")
->required();
sub.add_option("destination", opts.move.dest,
"Destination maildir")
->type_name("<maildir>");
}
static void
sub_remove(CLI::App& sub, Options& opts)
{
@ -602,7 +633,7 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
},
{ SubCommand::Info,
{Category::NeedsReadOnlyStore,
"info", "Show information about the message store database", sub_info }
"info", "Show information", sub_info }
},
{ SubCommand::Init,
{Category::NeedsWritableStore,
@ -612,6 +643,10 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
{Category::None,
"mkdir", "Create a new Maildir", sub_mkdir }
},
{ SubCommand::Move,
{Category::NeedsWritableStore,
"move", "Move a message or change flags", sub_move }
},
{ SubCommand::Remove,
{Category::NeedsWritableStore,
"remove", "Remove message from file-system and database", sub_remove }
@ -718,7 +753,8 @@ add_global_options(CLI::App& cli, Options& opts)
cli.add_flag("-q,--quiet", opts.quiet, "Hide non-essential output");
cli.add_flag("-v,--verbose", opts.verbose, "Show verbose output");
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr");
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr")
->group(""/*always hide*/);
cli.add_flag("--nocolor", opts.nocolor, "Don't show ANSI colors")
->default_val(Options::default_no_color())
->default_str(Options::default_no_color() ? "<true>" : "<false>");
@ -780,7 +816,7 @@ There is NO WARRANTY, to the extent permitted by law.
->transform(ExpandPath, "expand path");
}
/* add scripts (if supported) as semi-subscommands as well */
/* add scripts (if supported) as semi-subcommands as well */
const auto scripts = add_scripts(app, opts);
try {
@ -842,6 +878,11 @@ Options::category(Options::SubCommand sub)
static constexpr bool
validate_subcommand_ids()
{
size_t val{};
for (auto& cmd: Options::SubCommands)
if (static_cast<size_t>(cmd) != val++)
return false;
for (auto u = 0U; u != SubCommandInfos.size(); ++u)
if (static_cast<size_t>(SubCommandInfos.at(u).first) != u)
return false;

View File

@ -8,7 +8,7 @@
**
** 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
** 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
@ -36,10 +36,10 @@
/* command-line options for Mu */
namespace Mu {
struct Options {
using OptSize = Option<std::size_t>;
using SizeVec = std::vector<std::size_t>;
using OptTStamp = Option<std::time_t>;
using OptFieldId = Option<Field::Id>;
using OptSize = Option<std::size_t>;
using SizeVec = std::vector<std::size_t>;
using OptTStamp = Option<std::time_t>;
using OptFieldId = Option<Field::Id>;
using StringVec = std::vector<std::string>;
/*
@ -62,10 +62,11 @@ struct Options {
enum struct SubCommand {
Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir,
Remove, Script, Server, Verify, View/*must be last*/
Move, Remove, Script, Server, Verify, View,
// <private>
__count__
};
static constexpr std::size_t SubCommandNum =
1 + static_cast<std::size_t>(SubCommand::View);
static constexpr auto SubCommandNum = static_cast<size_t>(SubCommand::__count__);
static constexpr std::array<SubCommand, SubCommandNum> SubCommands = {{
SubCommand::Add,
SubCommand::Cfind,
@ -77,6 +78,7 @@ struct Options {
SubCommand::Info,
SubCommand::Init,
SubCommand::Mkdir,
SubCommand::Move,
SubCommand::Remove,
SubCommand::Script,
SubCommand::Server,
@ -84,7 +86,6 @@ struct Options {
SubCommand::View
}};
Option<SubCommand> sub_command; /**< The chosen sub-command, if any. */
/*
@ -117,16 +118,16 @@ struct Options {
* Extract
*/
struct Extract: public Crypto {
std::string message; /**< path to message file */
std::string message; /**< path to message file */
bool save_all; /**< extract all parts */
bool save_attachments; /**< extract all attachment parts */
SizeVec parts; /**< parts to save / open */
SizeVec parts; /**< parts to save / open */
std::string targetdir{}; /**< where to save attachments */
bool overwrite; /**< overwrite same-named files */
bool play; /**< try to 'play' attachment */
std::string filename_rx; /**< Filename rx to save */
bool uncooked{}; /**< Whether to avoid massaging
* output filename */
std::string filename_rx; /**< Filename rx to save */
bool uncooked{}; /**< Whether to avoid massaging
* the output filename */
} extract;
/*
@ -138,7 +139,7 @@ struct Options {
*/
struct Find {
std::string fields; /**< fields to show in output */
Field::Id sortfield; /**< field to sort by */
Field::Id sortfield; /**< field to sort by */
OptSize maxnum; /**< max # of entries to print */
bool reverse; /**< sort in revers order (z->a) */
bool threads; /**< show message threads */
@ -146,7 +147,7 @@ struct Options {
std::string linksdir; /**< directory for links */
OptSize summary_len; /**< max # of lines for summary */
std::string bookmark; /**< use bookmark */
bool analyze; /**< analyze query */
bool analyze; /**< analyze query */
enum struct Format { Plain, Links, Xml, Json, Sexp, Exec };
Format format; /**< Output format */
@ -158,7 +159,7 @@ struct Options {
bool auto_retrieve; /**< assume we're online */
bool decrypt; /**< try to decrypt the body */
StringVec query; /**< search query */
StringVec query; /**< search query */
} find;
struct Help {
@ -189,10 +190,10 @@ struct Options {
StringVec my_addresses; /**< personal e-mail addresses */
StringVec ignored_addresses; /**< addresses to be ignored for
* the contacts-cache */
OptSize max_msg_size; /**< max size for message files */
OptSize max_msg_size; /**< max size for message files */
OptSize batch_size; /**< db transaction batch size */
bool reinit; /**< re-initialize */
bool support_ngrams; /**< support CJK etc. ngrams */
bool support_ngrams; /**< support CJK etc. ngrams */
} init;
@ -204,6 +205,19 @@ struct Options {
mode_t mode; /**< Mode for the maildir */
} mkdir;
/*
* Move
*/
struct Move {
std::string src; /**< Source file */
std::string dest; /**< Destination dir */
std::string flags; /**< Flags for destination */
bool change_name; /**< Change basename for destination */
bool update_dups; /**< Update duplicate messages too */
bool dry_run; /**< Just print the result path,
but do not change anything */
} move;
/*
* Remove
*/
@ -215,7 +229,7 @@ struct Options {
* Scripts (i.e., finding scriot)
*/
struct Script {
std::string name; /**< name of script */
std::string name; /**< name of script */
StringVec params; /**< script params */
} script;
@ -225,7 +239,7 @@ struct Options {
struct Server {
bool commands; /**< dump docs for commands */
std::string eval; /**< command to evaluate */
bool allow_temp_file; /**< temp-file optimization allowed? */
bool allow_temp_file; /**< temp-file optimization allowed? */
} server;
/*