mirror of https://github.com/djcb/mu.git
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:
parent
2229e2e77e
commit
5300b7ce82
|
@ -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)*
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>")
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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" \
|
||||||
"[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
|
"[\"Mü\" \"\" 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" \
|
||||||
"[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
|
"[\"Mü\" \"\" 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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue