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 |
| 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 <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
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)*

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
** 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 <string>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#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 <cstdint>
#include <string>
#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;
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;
/**
* 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<std::string, std::string>
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<std::string, size_t> 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() ? "<none>" : 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<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>
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<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;
static constexpr InfoEnum<Format, 8> 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("<number>")

View File

@ -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
*/

View File

@ -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" \
"[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
"[\"\" \"\" 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" \
"[\"\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
"[\"\" \"\" 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();
}