From 5300b7ce82c6f394a3a1d1a0d2bcffca9ed9cee1 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Thu, 29 Dec 2022 19:05:22 +0200 Subject: [PATCH] cfind: rework, add support for json output Update the old cfind code, and add json output support while doing so. --- man/mu-cfind.1.org | 48 ++- mu/mu-cmd-cfind.cc | 587 +++++++++++++++------------------- mu/mu-options.cc | 34 +- mu/mu-options.hh | 12 +- mu/tests/test-mu-cmd-cfind.cc | 44 ++- 5 files changed, 351 insertions(+), 374 deletions(-) diff --git a/man/mu-cfind.1.org b/man/mu-cfind.1.org index 840e1b67..1bbcecaa 100644 --- a/man/mu-cfind.1.org +++ b/man/mu-cfind.1.org @@ -58,6 +58,7 @@ sets the output format to the given value. The following are available: | org-contact | org-mode org-contact format | | bbdb | BBDB format | | csv | comma-separated values [1] | +| json | JSON format | [1] *CSV is not fully standardized, but *mu cfind* follows some common practices: @@ -83,6 +84,51 @@ only seen in mailing-list messages. See the ~--my-address~ parameter to *mu init #+include: "common-options.inc" :minlevel 1 +* JSON FORMAT + +With ~--format=json~, the matching contacts come out as a JSON array, e.g., +#+begin_example +[ + { + "email" : "syb@example.com", + "name" : "Sybil Gerard", + "display" : "Sybil Gerard ", + "last-seen" : 1075982687, + "last-seen-iso" : "2004-02-05T14:04:47Z", + "personal" : false, + "frequency" : 14 + }, + { + "email" : "ed@example.com", + "name" : "Mallory, Edward", + "display" : "\"Mallory, Edward\" ", + "last-seen" : 1425991805, + "last-seen-iso" : "2015-03-10T14:50:05Z", + "personal" : true, + "frequency" : 2 + } +] +#+end_example + +Each contact has the following fields: + +| property | description | +|---------------+--------------------------------------------------------------------------| +| ~email~ | the email-address | +| ~name~ | the name (or ~none~) | +| ~display~ | the combination name and e-mail address for display purposes | +| ~last-seen~ | date of most recent message with this contact (Unix time) | +| ~last-seen-iso~ | ~last-seen~ represented as an ISO-8601 timestamp | +| ~personal~ | whether the email was seen in a message together with a personal address | +| ~frequency~ | approximation of the number of times this contact was seen in messages | + +The JSON format is useful for further processing, e.g. using the *jq(1)* tool: + +List display names, sorted by their last-seen date: +#+begin_example +$ mu cfind --format=json --personal | jq -r '.[] | ."last-seen-iso" + " " + .display' | sort +#+end_example + * INTEGRATION WITH MUTT You can use *mu cfind* as an external address book server for *mutt*. @@ -113,4 +159,4 @@ otherwise: 1 for a generals error and 2 for 'no matches'. #+include: "copyright.inc" :minlevel 1 * SEE ALSO -*mu(1)*, *mu-index(1)*, *mu-find(1)*, *pcre(3)* +*mu(1)*, *mu-index(1)*, *mu-find(1)*, *pcre(3)*, *jq(1)* diff --git a/mu/mu-cmd-cfind.cc b/mu/mu-cmd-cfind.cc index bd6fb2d9..c26c7436 100644 --- a/mu/mu-cmd-cfind.cc +++ b/mu/mu-cmd-cfind.cc @@ -1,410 +1,323 @@ /* -** Copyright (C) 2011-2022 Dirk-Jan C. Binnema +** 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 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. +** 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. +** 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 - -#include -#include -#include -#include - #include "mu-cmd.hh" -#include "mu-contacts-cache.hh" -#include "utils/mu-util.h" -#include "utils/mu-utils.hh" -#include "utils/mu-error.hh" +#include +#include +#include +#include + +#include +#include +#include +#include using namespace Mu; +enum struct ItemType { Header, Footer, Normal }; +using OutputFunc = std::function, const Options&)>; +using OptContact = Option; +using Format = Options::Cfind::Format; -/** - * macro to check whether the string is empty, ie. if it's NULL or - * it's length is 0 - * - * @param S a string - * - * @return TRUE if the string is empty, FALSE otherwise - */ -#define mu_str_is_empty(S) ((!(S)||!(*S))?TRUE:FALSE) - - -/** - * guess the last name for the given name; clearly, - * this is just a rough guess for setting an initial value. - * - * @param name a name - * - * @return the last name, as a newly allocated string (free with - * g_free) - */ -static gchar* -guess_last_name(const char* name) +// simplistic guess of first & last names, for setting +// some initial value. +static std::pair +guess_first_last_name(const std::string& name) { - const gchar* lastsp; + if (name.empty()) + return {}; - if (!name) - return g_strdup(""); - - lastsp = g_strrstr(name, " "); - - return g_strdup(lastsp ? lastsp + 1 : ""); -} - -/** - * guess the first name for the given name; clearly, - * this is just a rough guess for setting an initial value. - * - * @param name a name - * - * @return the first name, as a newly allocated string (free with - * g_free) - */ -static gchar* -guess_first_name(const char* name) -{ - const gchar* lastsp; - - if (!name) - return g_strdup(""); - - lastsp = g_strrstr(name, " "); - - if (lastsp) - return g_strndup(name, lastsp - name); + const auto lastspc = name.find_last_of(' '); + if (lastspc == name.npos) + return { name, "" }; // no last name else - return g_strdup(name); + return { name.substr(0, lastspc), name.substr(lastspc + 1)}; } -/** - * guess some nick name for the given name; if we can determine an - * first name, last name, the nick will be first name + the first char - * of the last name. otherwise, it's just the first name. clearly, - * this is just a rough guess for setting an initial value for nicks. - * - * @param name a name - * - * @return the guessed nick, as a newly allocated string (free with g_free) - */ -static gchar* -cleanup_str(const char* str) + +// candidate nick and a _count_ for that given nick, to uniquify them. +static std::unordered_map nicks; +static std::string +guess_nick(const Contact& contact) { - gchar* s; - const gchar* cur; - unsigned i; + auto cleanup = [](const std::string& str) { + std::string clean; + for (auto& c: str) // XXX: support non-ascii + if (!::ispunct(c) && !::isspace(c)) + clean += c; + return clean; + }; - if (mu_str_is_empty(str)) - return g_strdup(""); + auto nick = cleanup(std::invoke([&]()->std::string { - s = g_new0(char, strlen(str) + 1); + // no name? use the user part from the addr + if (contact.name.empty()) { + const auto pos{contact.email.find('@')}; + if (pos == std::string::npos) + return contact.email; // no '@' + else + return contact.email.substr(0, pos); + } - for (cur = str, i = 0; *cur; ++cur) { - if (ispunct(*cur) || isspace(*cur)) - continue; - else - s[i++] = *cur; + const auto names{guess_first_last_name(contact.name)}; + /* if there's no last name, use first name as the nick */ + if (names.second.empty()) + return names.first; + + char initial[7] = {}; + if (g_unichar_to_utf8(g_utf8_get_char(names.second.c_str()), initial) == 0) { + /* couldn't we get an initial for the last name? + * just use the first name*/ + return names.first; + } else // prepend the initial + return names.first + initial; + })); + + // uniquify. + if (auto it = nicks.find(nick); it == nicks.cend()) + nicks.emplace(nick, 0); + else { + ++it->second; + nick = format("%s%zu", nick.c_str(), ++it->second); } - return s; -} - -static char* -uniquify_nick(const char* nick, GHashTable* nicks) -{ - guint u; - - for (u = 2; u != 1000; ++u) { - char* cand; - cand = g_strdup_printf("%s%u", nick, u); - if (!g_hash_table_contains(nicks, cand)) - return cand; - } - - return g_strdup(nick); /* if all else fails */ -} - -static gchar* -guess_nick(const char* name, GHashTable* nicks) -{ - gchar *fname, *lname, *nick; - gchar initial[7]; - - fname = guess_first_name(name); - lname = guess_last_name(name); - - /* if there's no last name, use first name as the nick */ - if (mu_str_is_empty(fname) || mu_str_is_empty(lname)) { - g_free(lname); - nick = fname; - goto leave; - } - - memset(initial, 0, sizeof(initial)); - /* couldn't we get an initial for the last name? */ - if (g_unichar_to_utf8(g_utf8_get_char(lname), initial) == 0) { - g_free(lname); - nick = fname; - goto leave; - } - - nick = g_strdup_printf("%s%s", fname, initial); - g_free(fname); - g_free(lname); - -leave : { - gchar* tmp; - tmp = cleanup_str(nick); - g_free(nick); - nick = tmp; -} - - if (g_hash_table_contains(nicks, nick)) { - char* tmp; - tmp = uniquify_nick(nick, nicks); - g_free(nick); - nick = tmp; - } - - g_hash_table_add(nicks, g_strdup(nick)); - return nick; } -using Format = Options::Cfind::Format; static void -print_header(Format format) +output_plain(ItemType itype, OptContact contact, const Options& opts) { - switch (format) { - case Format::Bbdb: - g_print(";; -*-coding: utf-8-emacs;-*-\n" - ";;; file-version: 6\n"); - break; - case Format::MuttAddressBook: - g_print("Matching addresses in the mu database:\n"); - break; - default: - break; - } + if (!contact) + return; + + const auto col1{opts.nocolor ? "" : MU_COLOR_MAGENTA}; + const auto col2{opts.nocolor ? "" : MU_COLOR_GREEN}; + const auto coldef{opts.nocolor ? "" : MU_COLOR_DEFAULT}; + + mu_util_print_encoded("%s%s%s%s%s%s%s\n", + col1, + contact->name.c_str(), + coldef, + contact->name.empty() ? "" : " ", + col2, + contact->email.c_str(), + coldef); } static void -each_contact_bbdb(const std::string& email, const std::string& name, time_t tstamp) +output_mutt_alias(ItemType itype, OptContact contact, const Options& opts) { - char *fname, *lname; + if (!contact) + return; - fname = guess_first_name(name.c_str()); - lname = guess_last_name(name.c_str()); + const auto nick{guess_nick(*contact)}; + mu_util_print_encoded("alias %s %s <%s>\n", nick.c_str(), + contact->name.c_str(), contact->email.c_str()); +} - const auto now{time_to_string("%Y-%m-%d", time(NULL))}; - const auto timestamp{time_to_string("%Y-%m-%d", tstamp)}; +static void +output_mutt_address_book(ItemType itype, OptContact contact, const Options& opts) +{ + if (itype == ItemType::Header) + g_print ("Matching addresses in the mu database:\n"); + + if (!contact) + return; + + mu_util_print_encoded("%s\t%s\t\n", + contact->email.c_str(), + contact->name.c_str()); +} + +static void +output_wanderlust(ItemType itype, OptContact contact, const Options& opts) +{ + if (!contact || contact->name.empty()) + return; + + auto nick=guess_nick(*contact); + + mu_util_print_encoded("%s \"%s\" \"%s\"\n", + contact->email.c_str(), + nick.c_str(), + contact->name.c_str()); +} + +static void +output_org_contact(ItemType itype, OptContact contact, const Options& opts) +{ + if (!contact || contact->name.empty()) + return; + + mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n", + contact->name.c_str(), + contact->email.c_str()); +} + +static void +output_bbdb(ItemType itype, OptContact contact, const Options& opts) +{ + if (itype == ItemType::Header) + g_print (";; -*-coding: utf-8-emacs;-*-\n" + ";;; file-version: 6\n"); + if (!contact) + return; + + const auto names{guess_first_last_name(contact->name)}; + const auto now{time_to_string("%Y-%m-%d", ::time(NULL))}; + const auto timestamp{time_to_string("%Y-%m-%d", contact->message_date)}; g_print("[\"%s\" \"%s\" nil nil nil nil (\"%s\") " "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n", - fname, - lname, - email.c_str(), + names.first.c_str(), + names.second.c_str(), + contact->email.c_str(), now.c_str(), timestamp.c_str()); - - g_free(fname); - g_free(lname); } static void -each_contact_mutt_alias(const std::string& email, - const std::string& name, - GHashTable* nicks) +output_csv(ItemType itype, OptContact contact, const Options& opts) { - if (name.empty()) + if (!contact) return; - char* nick = guess_nick(name.c_str(), nicks); - mu_util_print_encoded("alias %s %s <%s>\n", nick, name.c_str(), email.c_str()); - - g_free(nick); + mu_util_print_encoded("%s,%s\n", + contact->name.empty() ? "" : Mu::quote(contact->name).c_str(), + Mu::quote(contact->email).c_str()); } static void -each_contact_wl(const std::string& email, - const std::string& name, - GHashTable* nicks) +output_json(ItemType itype, OptContact contact, const Options& opts) { - if (name.empty()) - return; + if (itype == ItemType::Header) + g_print("[\n"); + if (contact) { + g_print("%s", itype == ItemType::Header ? "" : ",\n"); + g_print (" {\n"); - char* nick = guess_nick(name.c_str(), nicks); - mu_util_print_encoded("%s \"%s\" \"%s\"\n", email.c_str(), nick, name.c_str()); - g_free(nick); -} - -static void -print_plain(const std::string& email, const std::string& name, bool color) -{ - if (!name.empty()) { - if (color) - ::fputs(MU_COLOR_MAGENTA, stdout); - mu_util_fputs_encoded(name.c_str(), stdout); - ::fputs(" ", stdout); + const std::string name = contact->name.empty() ? "null" : Mu::quote(contact->name); + mu_util_print_encoded( + " \"email\" : \"%s\",\n" + " \"name\" : %s,\n" + " \"display\" : %s,\n" + " \"last-seen\" : %" PRId64 ",\n" + " \"last-seen-iso\" : \"%s\",\n" + " \"personal\" : %s,\n" + " \"frequency\" : %" PRId64 "\n", + contact->email.c_str(), + name.c_str(), + Mu::quote(contact->display_name(true)).c_str(), + contact->message_date, + time_to_string("%FT%TZ", contact->message_date).c_str(), + contact->personal ? "true" : "false", + contact->frequency); + g_print (" }"); } - if (color) - ::fputs(MU_COLOR_GREEN, stdout); - - mu_util_fputs_encoded(email.c_str(), stdout); - - if (color) - fputs(MU_COLOR_DEFAULT, stdout); - - fputs("\n", stdout); + if (itype == ItemType::Footer) + g_print("\n]\n"); } -struct ECData { - Format format; - gboolean color, personal; - time_t after; - GRegex* rx; - GHashTable* nicks; - size_t maxnum; - size_t n; -}; - -static void -each_contact(const Mu::Contact& ci, ECData& ecdata) +static OutputFunc +find_output_func(Format format) { - if (ecdata.personal && !ci.personal) - return; - - if (ci.message_date < ecdata.after) - return; - - if (ecdata.rx && - !g_regex_match(ecdata.rx, ci.email.c_str(), (GRegexMatchFlags)0, NULL) && - !g_regex_match(ecdata.rx, - ci.name.empty() ? "" : ci.name.c_str(), - (GRegexMatchFlags)0, - NULL)) - return; - - ++ecdata.n; - - switch (ecdata.format) { +#pragma GCC diagnostic push +#pragma GCC diagnostic error "-Wswitch" + switch(format) { + case Format::Plain: + return output_plain; case Format::MuttAlias: - each_contact_mutt_alias(ci.email, ci.name, ecdata.nicks); - break; + return output_mutt_alias; case Format::MuttAddressBook: - mu_util_print_encoded("%s\t%s\t\n", ci.email.c_str(), ci.name.c_str()); - break; + return output_mutt_address_book; case Format::Wanderlust: - each_contact_wl(ci.email, ci.name, ecdata.nicks); - break; + return output_wanderlust; case Format::OrgContact: - if (!ci.name.empty()) - mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n", - ci.name.c_str(), - ci.email.c_str()); - break; + return output_org_contact; case Format::Bbdb: - each_contact_bbdb(ci.email, ci.name, ci.message_date); - break; + return output_bbdb; case Format::Csv: - mu_util_print_encoded("%s,%s\n", - ci.name.empty() ? "" : Mu::quote(ci.name).c_str(), - Mu::quote(ci.email).c_str()); - break; - case Format::Debug: { - char datebuf[32]; - const auto mdate(static_cast<::time_t>(ci.message_date)); - ::strftime(datebuf, sizeof(datebuf), "%F %T", ::gmtime(&mdate)); - g_print("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n" - "\tlast-seen: %s\n", - ci.email.c_str(), - ci.name.empty() ? "" : ci.name.c_str(), - ci.display_name(true).c_str(), - ci.personal ? "yes" : "no", - ci.frequency, - datebuf); - } - break; + return output_csv; + case Format::Json: + return output_json; default: - print_plain(ci.email, ci.name, ecdata.color); + g_warning("unsupported format"); + return {}; } +#pragma GCC diagnostic pop } -static Result -run_cmd_cfind(const Mu::Store& store, - const std::string& pattern, - bool personal, - time_t after, - size_t maxnum, - Format format, - bool color) -{ - ECData ecdata{}; - GError *err{}; - - memset(&ecdata, 0, sizeof(ecdata)); - - if (!pattern.empty()) { - ecdata.rx = g_regex_new( - pattern.c_str(), - (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE), - (GRegexMatchFlags)0, &err); - - if (!ecdata.rx) - return Err(Error::Code::Internal, &err, "invalid cfind regexp"); - } - - ecdata.personal = personal; - ecdata.n = 0; - ecdata.after = after; - ecdata.maxnum = maxnum; - ecdata.format = format; - ecdata.color = color; - ecdata.nicks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - - print_header(format); - - store.contacts_cache().for_each([&](const auto& ci) { - each_contact(ci, ecdata); - return ecdata.maxnum == 0 || ecdata.n < ecdata.maxnum; - }); - g_hash_table_unref(ecdata.nicks); - - if (ecdata.rx) - g_regex_unref(ecdata.rx); - - if (ecdata.n == 0) - return Err(Error::Code::ContactNotFound, "no matching contacts found"); - else - return Ok(); -} Result Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts) { - return run_cmd_cfind(store, - opts.cfind.rx_pattern, - opts.cfind.personal, - opts.cfind.after.value_or(0), - opts.cfind.maxnum.value_or(0), - opts.cfind.format, - !opts.nocolor); + size_t num{}; + OutputFunc output = find_output_func(opts.cfind.format); + if (!output) + return Err(Error::Code::Internal, + "missing output function"); + + // get the pattern regex, if any. + Regex rx{}; + if (!opts.cfind.rx_pattern.empty()) { + if (auto&& res = Regex::make(opts.cfind.rx_pattern, + static_cast + (G_REGEX_OPTIMIZE|G_REGEX_CASELESS)); !res) + return Err(std::move(res.error())); + else + rx = res.value(); + } + + nicks.clear(); + store.contacts_cache().for_each([&](const Contact& contact)->bool { + + if (opts.cfind.maxnum && num > *opts.cfind.maxnum) + return false; /* stop the loop */ + + if (!contact.has_valid_email()) + return true; /* next */ + + // filter for maxnum, personal & "after" + if ((opts.cfind.personal && !contact.personal) || + (opts.cfind.after.value_or(0) > contact.message_date)) + return true; /* next */ + + // filter for regex, if any. + if (rx) { + if (!rx.matches(contact.name) && !rx.matches(contact.email)) + return true; /* next */ + } + + /* seems we have a match! display it. */ + const auto itype{num == 0 ? ItemType::Header : ItemType::Normal}; + output(itype, contact, opts); + ++num; + return true; + }); + + if (num == 0) + return Err(Error::Code::ContactNotFound, "no matching contacts found"); + + output(ItemType::Footer, Nothing, opts); + return Ok(); } diff --git a/mu/mu-options.cc b/mu/mu-options.cc index 438cac79..30b4e67f 100644 --- a/mu/mu-options.cc +++ b/mu/mu-options.cc @@ -167,30 +167,14 @@ sub_cfind(CLI::App& sub, Options& opts) { using Format = Options::Cfind::Format; static constexpr InfoEnum FormatInfos = {{ - { Format::Plain, - {"plain", "Plain output"} - }, - { Format::MuttAlias, - {"mutt-alias", "Mutt alias"} - }, - { Format::MuttAddressBook, - {"mutt-ab", "Mutt address book"} - }, - { Format::Wanderlust, - {"wl", "Wanderlust"} - }, - { Format::OrgContact, - {"org-contact", "org-contact"} - }, - { Format::Bbdb, - {"bbdb", "BBDB"} - }, - { Format::Csv, - {"csv", "comma-separated values"} - }, - { Format::Debug, - {"debug", "debug output"} - } + { Format::Plain, {"plain", "Plain output"} }, + { Format::MuttAlias, {"mutt-alias", "Mutt alias"} }, + { Format::MuttAddressBook, {"mutt-ab", "Mutt address book"}}, + { Format::Wanderlust, {"wl", "Wanderlust"}}, + { Format::OrgContact, {"org-contact", "org-contact"}}, + { Format::Bbdb, {"bbdb", "Emacs BBDB"}}, + { Format::Csv, {"csv", "comma-separated values"}}, + { Format::Json, {"json", "format as json array"}}, }}; const auto fhelp = options_help(FormatInfos, Format::Plain); @@ -205,10 +189,8 @@ sub_cfind(CLI::App& sub, Options& opts) sub.add_option("pattern", opts.cfind.rx_pattern, "Regular expression pattern to match"); - sub.add_flag("--personal,-p", opts.cfind.personal, "Only show 'personal' contacts"); - sub.add_option("--maxnum,-n", opts.cfind.maxnum, "Maximum number of results") ->type_name("") diff --git a/mu/mu-options.hh b/mu/mu-options.hh index ac059dce..fc0eb69c 100644 --- a/mu/mu-options.hh +++ b/mu/mu-options.hh @@ -53,7 +53,7 @@ struct Options { std::string muhome; /**< alternative mu dir */ enum struct SubCommand { - Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir, + Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir, Remove, Script, Server, Verify, View/*must be last*/ }; static constexpr std::size_t SubCommandNum = @@ -91,7 +91,7 @@ struct Options { */ struct Cfind { enum struct Format { Plain, MuttAlias, MuttAddressBook, - Wanderlust, OrgContact, Bbdb, Csv, Debug }; + Wanderlust, OrgContact, Bbdb, Csv, Json }; Format format; /**< Output format */ bool personal; /**< only show personal contacts */ OptTStamp after; /**< only last seen after tstamp */ @@ -112,11 +112,11 @@ struct Options { 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 */ + std::string filename_rx; /**< Filename rx to save */ } extract; /* @@ -183,7 +183,7 @@ struct Options { */ struct Mkdir { StringVec dirs; /**< Dir(s) to create */ - mode_t mode; /**< Mode for the maildir */ + mode_t mode; /**< Mode for the maildir */ } mkdir; /* @@ -200,7 +200,7 @@ struct Options { std::string name; /**< name of script */ StringVec params; /**< script params */ } script; - + /* * Server */ diff --git a/mu/tests/test-mu-cmd-cfind.cc b/mu/tests/test-mu-cmd-cfind.cc index 1a0dbeba..76b0bf2a 100644 --- a/mu/tests/test-mu-cmd-cfind.cc +++ b/mu/tests/test-mu-cmd-cfind.cc @@ -114,20 +114,20 @@ test_mu_cfind_bbdb(void) #define frm1 \ ";; -*-coding: utf-8-emacs;-*-\n" \ ";;; file-version: 6\n" \ - "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \ + "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \ "((creation-date . \"%s\") " \ "(time-stamp . \"1970-01-01\")) nil]\n" \ - "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ + "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ "((creation-date . \"%s\") " \ "(time-stamp . \"1970-01-01\")) nil]\n" #define frm2 \ ";; -*-coding: utf-8-emacs;-*-\n" \ ";;; file-version: 6\n" \ - "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ + "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ "((creation-date . \"%s\") " \ "(time-stamp . \"1970-01-01\")) nil]\n" \ - "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \ + "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \ "((creation-date . \"%s\") " \ "(time-stamp . \"1970-01-01\")) nil]\n" @@ -322,6 +322,41 @@ test_mu_cfind_csv(void) g_free(erroutput); } + +static void +test_mu_cfind_json() +{ + gchar *cmdline, *output, *erroutput; + + cmdline = g_strdup_printf("%s --nocolor cfind --muhome=%s --format=json ^a@example\\.com", + MU_PROGRAM, + CONTACTS_CACHE.c_str()); + + if (g_test_verbose()) + g_print("%s\n", cmdline); + + output = erroutput = NULL; + g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL)); + g_assert(output); + + const auto expected = R"([ + { + "email" : "a@example.com", + "name" : null, + "display" : "a@example.com", + "last-seen" : 1463331445, + "last-seen-iso" : "2016-05-15T19:57:25Z", + "personal" : false, + "frequency" : 1 + } +] +)"; + g_assert_cmpstr(output, ==, expected); + g_free(cmdline); + g_free(output); + g_free(erroutput); +} + int main(int argc, char* argv[]) { @@ -340,6 +375,7 @@ main(int argc, char* argv[]) g_test_add_func("/mu-cmd-cfind/test-mu-cfind-mutt-ab", test_mu_cfind_mutt_ab); g_test_add_func("/mu-cmd-cfind/test-mu-cfind-org-contact", test_mu_cfind_org_contact); g_test_add_func("/mu-cmd-cfind/test-mu-cfind-csv", test_mu_cfind_csv); + g_test_add_func("/mu-cmd-cfind/test-mu-cfind-json", test_mu_cfind_json); return g_test_run(); }