cfind: rework, add support for json output

Update the old cfind code, and add json output support while doing so.
This commit is contained in:
Dirk-Jan C. Binnema 2022-12-29 19:05:22 +02:00
parent 2229e2e77e
commit 5300b7ce82
5 changed files with 351 additions and 374 deletions

View File

@ -58,6 +58,7 @@ sets the output format to the given value. The following are available:
| org-contact | org-mode org-contact format | | org-contact | org-mode org-contact format |
| bbdb | BBDB format | | bbdb | BBDB format |
| csv | comma-separated values [1] | | csv | comma-separated values [1] |
| json | JSON format |
[1] *CSV is not fully standardized, but *mu cfind* follows some common practices: [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 #+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 <syb@example.com>",
"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\" <ed@example.com>",
"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 * INTEGRATION WITH MUTT
You can use *mu cfind* as an external address book server for *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 #+include: "copyright.inc" :minlevel 1
* SEE ALSO * SEE ALSO
*mu(1)*, *mu-index(1)*, *mu-find(1)*, *pcre(3)* *mu(1)*, *mu-index(1)*, *mu-find(1)*, *pcre(3)*, *jq(1)*

View File

@ -1,410 +1,323 @@
/* /*
** Copyright (C) 2011-2022 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 ** This program is free software; you can redistribute it and/or modify it
** the terms of the GNU General Public License as published by the Free Software ** under the terms of the GNU General Public License as published by the
** Foundation; either version 3, or (at your option) any later version. ** 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 ** This program is distributed in the hope that it will be useful,
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS ** but WITHOUT ANY WARRANTY; without even the implied warranty of
** FOR A PARTICULAR PURPOSE. See the GNU General Public License for more ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** details. ** GNU General Public License for more details.
** **
** You should have received a copy of the GNU General Public License along with ** You should have received a copy of the GNU General Public License
** this program; if not, write to the Free Software Foundation, Inc., 51 ** along with this program; if not, write to the Free Software Foundation,
** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
** **
*/ */
#include "config.h" #include "config.h"
#include <string>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "mu-cmd.hh" #include "mu-cmd.hh"
#include "mu-contacts-cache.hh"
#include "utils/mu-util.h" #include <cstdint>
#include "utils/mu-utils.hh" #include <string>
#include "utils/mu-error.hh" #include <functional>
#include <unordered_map>
#include <utils/mu-utils.hh>
#include <utils/mu-regex.hh>
#include <utils/mu-util.h>
#include <utils/mu-option.hh>
using namespace Mu; using namespace Mu;
enum struct ItemType { Header, Footer, Normal };
using OutputFunc = std::function<void(ItemType itype, Option<const Contact&>, const Options&)>;
using OptContact = Option<const Contact&>;
using Format = Options::Cfind::Format;
/** // simplistic guess of first & last names, for setting
* macro to check whether the string is empty, ie. if it's NULL or // some initial value.
* it's length is 0 static std::pair<std::string, std::string>
* guess_first_last_name(const std::string& name)
* @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)
{ {
const gchar* lastsp; if (name.empty())
return {};
if (!name) const auto lastspc = name.find_last_of(' ');
return g_strdup(""); if (lastspc == name.npos)
return { name, "" }; // no last name
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);
else 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 // candidate nick and a _count_ for that given nick, to uniquify them.
* first name, last name, the nick will be first name + the first char static std::unordered_map<std::string, size_t> nicks;
* of the last name. otherwise, it's just the first name. clearly, static std::string
* this is just a rough guess for setting an initial value for nicks. guess_nick(const Contact& contact)
*
* @param name a name
*
* @return the guessed nick, as a newly allocated string (free with g_free)
*/
static gchar*
cleanup_str(const char* str)
{ {
gchar* s; auto cleanup = [](const std::string& str) {
const gchar* cur; std::string clean;
unsigned i; for (auto& c: str) // XXX: support non-ascii
if (!::ispunct(c) && !::isspace(c))
clean += c;
return clean;
};
if (mu_str_is_empty(str)) auto nick = cleanup(std::invoke([&]()->std::string {
return g_strdup("");
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) { const auto names{guess_first_last_name(contact.name)};
if (ispunct(*cur) || isspace(*cur)) /* if there's no last name, use first name as the nick */
continue; if (names.second.empty())
else return names.first;
s[i++] = *cur;
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; return nick;
} }
using Format = Options::Cfind::Format;
static void static void
print_header(Format format) output_plain(ItemType itype, OptContact contact, const Options& opts)
{ {
switch (format) { if (!contact)
case Format::Bbdb: return;
g_print(";; -*-coding: utf-8-emacs;-*-\n"
";;; file-version: 6\n"); const auto col1{opts.nocolor ? "" : MU_COLOR_MAGENTA};
break; const auto col2{opts.nocolor ? "" : MU_COLOR_GREEN};
case Format::MuttAddressBook: const auto coldef{opts.nocolor ? "" : MU_COLOR_DEFAULT};
g_print("Matching addresses in the mu database:\n");
break; mu_util_print_encoded("%s%s%s%s%s%s%s\n",
default: col1,
break; contact->name.c_str(),
} coldef,
contact->name.empty() ? "" : " ",
col2,
contact->email.c_str(),
coldef);
} }
static void 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()); const auto nick{guess_nick(*contact)};
lname = guess_last_name(name.c_str()); 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))}; static void
const auto timestamp{time_to_string("%Y-%m-%d", tstamp)}; 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\") " g_print("[\"%s\" \"%s\" nil nil nil nil (\"%s\") "
"((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n", "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n",
fname, names.first.c_str(),
lname, names.second.c_str(),
email.c_str(), contact->email.c_str(),
now.c_str(), now.c_str(),
timestamp.c_str()); timestamp.c_str());
g_free(fname);
g_free(lname);
} }
static void static void
each_contact_mutt_alias(const std::string& email, output_csv(ItemType itype, OptContact contact, const Options& opts)
const std::string& name,
GHashTable* nicks)
{ {
if (name.empty()) if (!contact)
return; return;
char* nick = guess_nick(name.c_str(), nicks); mu_util_print_encoded("%s,%s\n",
mu_util_print_encoded("alias %s %s <%s>\n", nick, name.c_str(), email.c_str()); contact->name.empty() ? "" : Mu::quote(contact->name).c_str(),
Mu::quote(contact->email).c_str());
g_free(nick);
} }
static void static void
each_contact_wl(const std::string& email, output_json(ItemType itype, OptContact contact, const Options& opts)
const std::string& name,
GHashTable* nicks)
{ {
if (name.empty()) if (itype == ItemType::Header)
return; g_print("[\n");
if (contact) {
g_print("%s", itype == ItemType::Header ? "" : ",\n");
g_print (" {\n");
char* nick = guess_nick(name.c_str(), nicks); const std::string name = contact->name.empty() ? "null" : Mu::quote(contact->name);
mu_util_print_encoded("%s \"%s\" \"%s\"\n", email.c_str(), nick, name.c_str()); mu_util_print_encoded(
g_free(nick); " \"email\" : \"%s\",\n"
} " \"name\" : %s,\n"
" \"display\" : %s,\n"
static void " \"last-seen\" : %" PRId64 ",\n"
print_plain(const std::string& email, const std::string& name, bool color) " \"last-seen-iso\" : \"%s\",\n"
{ " \"personal\" : %s,\n"
if (!name.empty()) { " \"frequency\" : %" PRId64 "\n",
if (color) contact->email.c_str(),
::fputs(MU_COLOR_MAGENTA, stdout); name.c_str(),
mu_util_fputs_encoded(name.c_str(), stdout); Mu::quote(contact->display_name(true)).c_str(),
::fputs(" ", stdout); contact->message_date,
time_to_string("%FT%TZ", contact->message_date).c_str(),
contact->personal ? "true" : "false",
contact->frequency);
g_print (" }");
} }
if (color) if (itype == ItemType::Footer)
::fputs(MU_COLOR_GREEN, stdout); g_print("\n]\n");
mu_util_fputs_encoded(email.c_str(), stdout);
if (color)
fputs(MU_COLOR_DEFAULT, stdout);
fputs("\n", stdout);
} }
struct ECData { static OutputFunc
Format format; find_output_func(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)
{ {
if (ecdata.personal && !ci.personal) #pragma GCC diagnostic push
return; #pragma GCC diagnostic error "-Wswitch"
switch(format) {
if (ci.message_date < ecdata.after) case Format::Plain:
return; return output_plain;
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) {
case Format::MuttAlias: case Format::MuttAlias:
each_contact_mutt_alias(ci.email, ci.name, ecdata.nicks); return output_mutt_alias;
break;
case Format::MuttAddressBook: case Format::MuttAddressBook:
mu_util_print_encoded("%s\t%s\t\n", ci.email.c_str(), ci.name.c_str()); return output_mutt_address_book;
break;
case Format::Wanderlust: case Format::Wanderlust:
each_contact_wl(ci.email, ci.name, ecdata.nicks); return output_wanderlust;
break;
case Format::OrgContact: case Format::OrgContact:
if (!ci.name.empty()) return output_org_contact;
mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n",
ci.name.c_str(),
ci.email.c_str());
break;
case Format::Bbdb: case Format::Bbdb:
each_contact_bbdb(ci.email, ci.name, ci.message_date); return output_bbdb;
break;
case Format::Csv: case Format::Csv:
mu_util_print_encoded("%s,%s\n", return output_csv;
ci.name.empty() ? "" : Mu::quote(ci.name).c_str(), case Format::Json:
Mu::quote(ci.email).c_str()); return output_json;
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() ? "<none>" : ci.name.c_str(),
ci.display_name(true).c_str(),
ci.personal ? "yes" : "no",
ci.frequency,
datebuf);
}
break;
default: default:
print_plain(ci.email, ci.name, ecdata.color); g_warning("unsupported format");
return {};
} }
#pragma GCC diagnostic pop
} }
static Result<void>
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<void> Result<void>
Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts) Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts)
{ {
return run_cmd_cfind(store, size_t num{};
opts.cfind.rx_pattern, OutputFunc output = find_output_func(opts.cfind.format);
opts.cfind.personal, if (!output)
opts.cfind.after.value_or(0), return Err(Error::Code::Internal,
opts.cfind.maxnum.value_or(0), "missing output function");
opts.cfind.format,
!opts.nocolor); // 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<GRegexCompileFlags>
(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();
} }

View File

@ -167,30 +167,14 @@ sub_cfind(CLI::App& sub, Options& opts)
{ {
using Format = Options::Cfind::Format; using Format = Options::Cfind::Format;
static constexpr InfoEnum<Format, 8> FormatInfos = {{ static constexpr InfoEnum<Format, 8> FormatInfos = {{
{ Format::Plain, { Format::Plain, {"plain", "Plain output"} },
{"plain", "Plain output"} { Format::MuttAlias, {"mutt-alias", "Mutt alias"} },
}, { Format::MuttAddressBook, {"mutt-ab", "Mutt address book"}},
{ Format::MuttAlias, { Format::Wanderlust, {"wl", "Wanderlust"}},
{"mutt-alias", "Mutt alias"} { Format::OrgContact, {"org-contact", "org-contact"}},
}, { Format::Bbdb, {"bbdb", "Emacs BBDB"}},
{ Format::MuttAddressBook, { Format::Csv, {"csv", "comma-separated values"}},
{"mutt-ab", "Mutt address book"} { Format::Json, {"json", "format as json array"}},
},
{ 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"}
}
}}; }};
const auto fhelp = options_help(FormatInfos, Format::Plain); 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, sub.add_option("pattern", opts.cfind.rx_pattern,
"Regular expression pattern to match"); "Regular expression pattern to match");
sub.add_flag("--personal,-p", opts.cfind.personal, sub.add_flag("--personal,-p", opts.cfind.personal,
"Only show 'personal' contacts"); "Only show 'personal' contacts");
sub.add_option("--maxnum,-n", opts.cfind.maxnum, sub.add_option("--maxnum,-n", opts.cfind.maxnum,
"Maximum number of results") "Maximum number of results")
->type_name("<number>") ->type_name("<number>")

View File

@ -53,7 +53,7 @@ struct Options {
std::string muhome; /**< alternative mu dir */ std::string muhome; /**< alternative mu dir */
enum struct SubCommand { 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*/ Remove, Script, Server, Verify, View/*must be last*/
}; };
static constexpr std::size_t SubCommandNum = static constexpr std::size_t SubCommandNum =
@ -91,7 +91,7 @@ struct Options {
*/ */
struct Cfind { struct Cfind {
enum struct Format { Plain, MuttAlias, MuttAddressBook, enum struct Format { Plain, MuttAlias, MuttAddressBook,
Wanderlust, OrgContact, Bbdb, Csv, Debug }; Wanderlust, OrgContact, Bbdb, Csv, Json };
Format format; /**< Output format */ Format format; /**< Output format */
bool personal; /**< only show personal contacts */ bool personal; /**< only show personal contacts */
OptTStamp after; /**< only last seen after tstamp */ OptTStamp after; /**< only last seen after tstamp */
@ -112,11 +112,11 @@ struct Options {
std::string message; /**< path to message file */ std::string message; /**< path to message file */
bool save_all; /**< extract all parts */ bool save_all; /**< extract all parts */
bool save_attachments; /**< extract all attachment 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 */ std::string targetdir{}; /**< where to save attachments */
bool overwrite; /**< overwrite same-named files */ bool overwrite; /**< overwrite same-named files */
bool play; /**< try to 'play' attachment */ bool play; /**< try to 'play' attachment */
std::string filename_rx; /**< Filename rx to save */ std::string filename_rx; /**< Filename rx to save */
} extract; } extract;
/* /*
@ -183,7 +183,7 @@ struct Options {
*/ */
struct Mkdir { struct Mkdir {
StringVec dirs; /**< Dir(s) to create */ StringVec dirs; /**< Dir(s) to create */
mode_t mode; /**< Mode for the maildir */ mode_t mode; /**< Mode for the maildir */
} mkdir; } mkdir;
/* /*
@ -200,7 +200,7 @@ struct Options {
std::string name; /**< name of script */ std::string name; /**< name of script */
StringVec params; /**< script params */ StringVec params; /**< script params */
} script; } script;
/* /*
* Server * Server
*/ */

View File

@ -114,20 +114,20 @@ test_mu_cfind_bbdb(void)
#define frm1 \ #define frm1 \
";; -*-coding: utf-8-emacs;-*-\n" \ ";; -*-coding: utf-8-emacs;-*-\n" \
";;; file-version: 6\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\") " \ "((creation-date . \"%s\") " \
"(time-stamp . \"1970-01-01\")) nil]\n" \ "(time-stamp . \"1970-01-01\")) nil]\n" \
"[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ "[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
"((creation-date . \"%s\") " \ "((creation-date . \"%s\") " \
"(time-stamp . \"1970-01-01\")) nil]\n" "(time-stamp . \"1970-01-01\")) nil]\n"
#define frm2 \ #define frm2 \
";; -*-coding: utf-8-emacs;-*-\n" \ ";; -*-coding: utf-8-emacs;-*-\n" \
";;; file-version: 6\n" \ ";;; file-version: 6\n" \
"[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \ "[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
"((creation-date . \"%s\") " \ "((creation-date . \"%s\") " \
"(time-stamp . \"1970-01-01\")) nil]\n" \ "(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\") " \ "((creation-date . \"%s\") " \
"(time-stamp . \"1970-01-01\")) nil]\n" "(time-stamp . \"1970-01-01\")) nil]\n"
@ -322,6 +322,41 @@ test_mu_cfind_csv(void)
g_free(erroutput); 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 int
main(int argc, char* argv[]) 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-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-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-csv", test_mu_cfind_csv);
g_test_add_func("/mu-cmd-cfind/test-mu-cfind-json", test_mu_cfind_json);
return g_test_run(); return g_test_run();
} }