From: Brian Gix <brian.gix@intel.com>
To: linux-bluetooth@vger.kernel.org
Cc: johan.hedberg@gmail.com, inga.stotland@intel.com,
marcel@holtmann.org, brian.gix@intel.com
Subject: [PATCH BlueZ v6 17/26] mesh: Restructure DB to support multiple nodes
Date: Fri, 28 Dec 2018 14:07:36 -0800 [thread overview]
Message-ID: <20181228220745.25147-18-brian.gix@intel.com> (raw)
In-Reply-To: <20181228220745.25147-1-brian.gix@intel.com>
From: Inga Stotland <inga.stotland@intel.com>
Added needed functionality to support the storage of multiple
subnets, subscription lists, and publications per node.
---
mesh/mesh-db.c | 456 +++++++++++++++++++++++++++++++++++++--------------------
mesh/mesh-db.h | 9 +-
2 files changed, 297 insertions(+), 168 deletions(-)
diff --git a/mesh/mesh-db.c b/mesh/mesh-db.c
index 86e17ed9a..5c0b72551 100644
--- a/mesh/mesh-db.c
+++ b/mesh/mesh-db.c
@@ -15,7 +15,6 @@
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
- *
*/
#ifdef HAVE_CONFIG_H
@@ -29,6 +28,7 @@
#include <string.h>
#include <ell/ell.h>
+#include <json-c/json.h>
#include "mesh/mesh-defs.h"
#include "mesh/util.h"
@@ -162,6 +162,7 @@ static json_object *jarray_string_del(json_object *jarray, char *str,
if (str_entry && !strncmp(str, str_entry, len))
continue;
+ json_object_get(jentry);
json_object_array_add(jarray_new, jentry);
}
@@ -193,9 +194,6 @@ static json_object *jarray_key_del(json_object *jarray, int16_t idx)
{
json_object *jarray_new;
int i, sz = json_object_array_length(jarray);
- char idx_str[5];
-
- snprintf(idx_str, 5, "%4.4x", idx);
jarray_new = json_object_new_array();
if (!jarray_new)
@@ -203,16 +201,17 @@ static json_object *jarray_key_del(json_object *jarray, int16_t idx)
for (i = 0; i < sz; ++i) {
json_object *jentry, *jvalue;
- char *str;
jentry = json_object_array_get_idx(jarray, i);
if (json_object_object_get_ex(jentry, "index", &jvalue)) {
- str = (char *)json_object_get_string(jvalue);
- if (str && !strncmp(str, idx_str, 4))
+ int tmp = json_object_get_int(jvalue);
+
+ if (tmp == idx)
continue;
}
+ json_object_get(jentry);
json_object_array_add(jarray_new, jentry);
}
@@ -389,19 +388,10 @@ bool mesh_db_net_key_add(json_object *jobj, uint16_t idx,
char buf[5];
json_object_object_get_ex(jobj, "netKeys", &jarray);
- if (!jarray && (phase != KEY_REFRESH_PHASE_NONE))
- return false;
if (jarray)
jentry = get_key_object(jarray, idx);
- /*
- * The key entry should exist if the key is updated
- * (i.e., Key Refresh is underway)
- */
- if (!jentry && (phase != KEY_REFRESH_PHASE_NONE))
- return false;
-
if (jentry) {
uint8_t buf[16];
json_object *jvalue;
@@ -422,7 +412,7 @@ bool mesh_db_net_key_add(json_object *jobj, uint16_t idx,
return false;
}
- if (phase == KEY_REFRESH_PHASE_NONE) {
+ if (!jentry) {
jentry = json_object_new_object();
if (!jentry)
goto fail;
@@ -442,6 +432,19 @@ bool mesh_db_net_key_add(json_object *jobj, uint16_t idx,
if (!add_key(jentry, "key", key))
goto fail;
+ /* If Key Refresh underway, add placeholder for "Old Key" */
+ if (phase != KEY_REFRESH_PHASE_NONE) {
+ uint8_t buf[16];
+ uint8_t i;
+
+ /* Flip Bits to differentiate */
+ for (i = 0; i < sizeof(buf); i++)
+ buf[i] = key[i] ^ 0xff;
+
+ if (!add_key(jentry, "oldKey", buf))
+ goto fail;
+ }
+
if (!jarray) {
jarray = json_object_new_array();
if (!jarray)
@@ -733,7 +736,7 @@ static bool parse_bindings(json_object *jbindings, struct mesh_db_model *mod)
if (cnt > 0xffff)
return false;
- mod->num_subs = cnt;
+ mod->num_bindings = cnt;
/* Allow empty bindings list */
if (!cnt)
@@ -761,6 +764,130 @@ static bool parse_bindings(json_object *jbindings, struct mesh_db_model *mod)
return true;
}
+static bool get_key_index(json_object *jobj, const char *keyword,
+ uint16_t *index)
+{
+ int idx;
+
+ if (!get_int(jobj, keyword, &idx))
+ return false;
+
+ if (!CHECK_KEY_IDX_RANGE(idx))
+ return false;
+
+ *index = (uint16_t) idx;
+ return true;
+}
+
+static struct mesh_db_pub *parse_model_publication(json_object *jpub)
+{
+ json_object *jvalue;
+ struct mesh_db_pub *pub;
+ int len, value;
+ char *str;
+
+ pub = l_new(struct mesh_db_pub, 1);
+ if (!pub)
+ return NULL;
+
+ json_object_object_get_ex(jpub, "address", &jvalue);
+ str = (char *)json_object_get_string(jvalue);
+ len = strlen(str);
+
+ switch (len) {
+ case 4:
+ if (sscanf(str, "%04hx", &pub->addr) != 1)
+ goto fail;
+ break;
+ case 32:
+ if (!str2hex(str, len, pub->virt_addr, 16))
+ goto fail;
+ pub->virt = true;
+ break;
+ default:
+ goto fail;
+ }
+
+ if (!get_key_index(jpub, "index", &pub->idx))
+ goto fail;
+
+ if (!get_int(jpub, "ttl", &value))
+ goto fail;
+ pub->ttl = (uint8_t) value;
+
+ if (!get_int(jpub, "period", &value))
+ goto fail;
+ pub->period = (uint8_t) value;
+
+ if (!get_int(jpub, "credentials", &value))
+ goto fail;
+ pub->credential = (uint8_t) value;
+
+ if (!get_int(jpub, "retransmit", &value))
+ goto fail;
+
+ pub->retransmit = (uint8_t) value;
+ return pub;
+
+fail:
+ l_free(pub);
+ return NULL;
+}
+
+static bool parse_model_subscriptions(json_object *jsubs,
+ struct mesh_db_model *mod)
+{
+ struct mesh_db_sub *subs;
+ int i, cnt;
+
+ if (json_object_get_type(jsubs) != json_type_array)
+ return NULL;
+
+ cnt = json_object_array_length(jsubs);
+ /* Allow empty array */
+ if (!cnt)
+ return true;
+
+ subs = l_new(struct mesh_db_sub, cnt);
+ if (!subs)
+ return false;
+
+ for (i = 0; i < cnt; ++i) {
+ char *str;
+ int len;
+ json_object *jvalue;
+
+ jvalue = json_object_array_get_idx(jsubs, i);
+ if (!jvalue)
+ return false;
+
+ str = (char *)json_object_get_string(jvalue);
+ len = strlen(str);
+
+ switch (len) {
+ case 4:
+ if (sscanf(str, "%04hx", &subs[i].src.addr) != 1)
+ goto fail;
+ break;
+ case 32:
+ if (!str2hex(str, len, subs[i].src.virt_addr, 16))
+ goto fail;
+ subs[i].virt = true;
+ break;
+ default:
+ goto fail;
+ }
+ }
+
+ mod->num_subs = cnt;
+ mod->subs = subs;
+
+ return true;
+fail:
+ l_free(subs);
+ return false;
+}
+
static bool parse_models(json_object *jmodels, struct mesh_db_element *ele)
{
int i, num_models;
@@ -810,11 +937,22 @@ static bool parse_models(json_object *jmodels, struct mesh_db_element *ele)
json_object_object_get_ex(jmodel, "bind", &jarray);
- if (jarray && (json_object_get_type(jmodels) != json_type_array
+ if (jarray && (json_object_get_type(jarray) != json_type_array
|| !parse_bindings(jarray, mod)))
goto fail;
- /* TODO add pub/sub */
+ json_object_object_get_ex(jmodel, "publish", &jvalue);
+ if (jvalue) {
+ mod->pub = parse_model_publication(jvalue);
+ if (!mod->pub)
+ goto fail;
+ }
+
+ json_object_object_get_ex(jmodel, "subscribe", &jarray);
+
+ if (jarray && !parse_model_subscriptions(jarray, mod))
+ goto fail;
+
l_queue_push_tail(ele->models, mod);
}
@@ -1012,33 +1150,6 @@ static bool parse_composition(json_object *jcomp, struct mesh_db_node *node)
return true;
}
-static uint16_t get_prov_flags(json_object *jarray, uint16_t max_value)
-{
- int i, cnt;
- uint16_t result = 0;
-
- cnt = json_object_array_length(jarray);
- if (!cnt)
- return 0;
-
- for (i = 0; i < cnt; ++i) {
- json_object *jvalue;
- int value;
-
- jvalue = json_object_array_get_idx(jarray, i);
- value = json_object_get_int(jvalue);
- if (value > 16)
- continue;
-
- if ((1 << value) > max_value)
- continue;
-
- result |= (1 << value);
- }
-
- return result;
-}
-
bool mesh_db_read_node(json_object *jnode, mesh_db_node_cb cb, void *user_data)
{
struct mesh_db_node node;
@@ -1091,117 +1202,28 @@ bool mesh_db_read_node(json_object *jnode, mesh_db_node_cb cb, void *user_data)
return cb(&node, user_data);
}
-bool mesh_db_read_unprovisioned_device(json_object *jnode, mesh_db_node_cb cb,
- void *user_data)
-{
- struct mesh_db_node node;
- json_object *jvalue;
- char *str;
-
- if (!cb) {
- l_info("Device read callback is required");
- return false;
- }
-
- memset(&node, 0, sizeof(node));
-
- if (!parse_composition(jnode, &node)) {
- l_info("Failed to parse local device composition");
- return false;
- }
-
- parse_features(jnode, &node);
-
- json_object_object_get_ex(jnode, "elements", &jvalue);
- if (jvalue && json_object_get_type(jvalue) == json_type_array) {
- if (!parse_elements(jvalue, &node))
- return false;
- }
-
- json_object_object_get_ex(jnode, "UUID", &jvalue);
- if (!jvalue)
- return false;
-
- str = (char *)json_object_get_string(jvalue);
- if (!str2hex(str, strlen(str), node.uuid, 16))
- return false;
-
- return cb(&node, user_data);
-}
-
-bool mesh_db_read_prov_info(json_object *jnode, struct mesh_db_prov *prov)
+bool mesh_db_write_uint16_hex(json_object *jobj, const char *desc,
+ uint16_t value)
{
- json_object *jprov, *jarray, *jvalue, *jobj;
- int value;
- char *str;
-
- if (!prov)
- return false;
-
- json_object_object_get_ex(jnode, "provision", &jprov);
- if (!jprov)
- return false;
-
- json_object_object_get_ex(jprov, "algorithms", &jarray);
- if (!jarray || json_object_get_type(jarray) != json_type_array)
- return false;
-
- prov->algorithm = get_prov_flags(jarray, ALG_FIPS_256_ECC);
- if (!prov->algorithm) {
- l_info("At least one algorithm must be indicated");
- return false;
- }
-
- json_object_object_get_ex(jprov, "outputOOB", &jobj);
- json_object_object_get_ex(jobj, "size", &jvalue);
- value = json_object_get_int(jvalue);
- if (value > 8)
- return false;
-
- prov->output_oob.size = (uint8_t) value;
- json_object_object_get_ex(jobj, "actions", &jarray);
- if (!jarray || json_object_get_type(jarray) != json_type_array)
- return false;
-
- prov->output_oob.actions = get_prov_flags(jarray, OOB_OUT_ALPHA);
-
- json_object_object_get_ex(jprov, "inputOOB", &jobj);
- json_object_object_get_ex(jobj, "size", &jvalue);
- value = json_object_get_int(jvalue);
- if (value > 8)
- return false;
-
- prov->input_oob.size = (uint8_t) value;
- json_object_object_get_ex(jobj, "actions", &jarray);
- if (!jarray || json_object_get_type(jarray) != json_type_array)
- return false;
-
- prov->input_oob.actions = get_prov_flags(jarray, OOB_IN_ALPHA);
-
- json_object_object_get_ex(jprov, "publicType", &jvalue);
- prov->pub_type = (json_object_get_boolean(jvalue)) ? 1 : 0;
-
- json_object_object_get_ex(jprov, "staticType", &jvalue);
- prov->static_type = (json_object_get_boolean(jvalue)) ? 1 : 0;
-
- json_object_object_get_ex(jprov, "privateKey", &jvalue);
- if (!jvalue)
- return false;
+ json_object *jstring;
+ char buf[5];
- str = (char *)json_object_get_string(jvalue);
- if (!str2hex(str, strlen(str), prov->priv_key, 32))
+ snprintf(buf, 5, "%4.4x", value);
+ jstring = json_object_new_string(buf);
+ if (!jstring)
return false;
+ json_object_object_add(jobj, desc, jstring);
return true;
}
-bool mesh_db_write_uint16_hex(json_object *jobj, const char *desc,
- uint16_t value)
+bool mesh_db_write_uint32_hex(json_object *jobj, const char *desc,
+ uint32_t value)
{
json_object *jstring;
- char buf[5];
+ char buf[9];
- snprintf(buf, 5, "%4.4x", value);
+ snprintf(buf, 9, "%8.8x", value);
jstring = json_object_new_string(buf);
if (!jstring)
return false;
@@ -1238,23 +1260,23 @@ bool mesh_db_write_bool(json_object *jobj, const char *keyword, bool value)
return true;
}
-bool mesh_db_write_mode(json_object *jobj, const char *keyword, int value)
+static const char *mode_to_string(int mode)
{
- json_object *jstring;
-
- switch (value) {
+ switch (mode) {
case MESH_MODE_DISABLED:
- jstring = json_object_new_string("disabled");
- break;
+ return "disabled";
case MESH_MODE_ENABLED:
- jstring = json_object_new_string("enabled");
- break;
- case MESH_MODE_UNSUPPORTED:
- jstring = json_object_new_string("unsupported");
- break;
+ return "enabled";
default:
- return false;
- };
+ return "unsupported";
+ }
+}
+
+bool mesh_db_write_mode(json_object *jobj, const char *keyword, int value)
+{
+ json_object *jstring;
+
+ jstring = json_object_new_string(mode_to_string(value));
if (!jstring)
return false;
@@ -1272,7 +1294,7 @@ bool mesh_db_write_relay_mode(json_object *jnode, uint8_t mode, uint8_t count,
json_object_object_del(jnode, "relay");
jrelay = json_object_new_object();
- if (jrelay)
+ if (!jrelay)
return false;
if (!mesh_db_write_mode(jrelay, "mode", mode))
@@ -1359,3 +1381,113 @@ void mesh_db_remove_property(json_object *jobj, const char *desc)
{
json_object_object_del(jobj, desc);
}
+
+static void add_model(void *a, void *b)
+{
+ struct mesh_db_model *mod = a;
+ json_object *jmodels = b, *jmodel;
+
+ jmodel = json_object_new_object();
+ if (!jmodel)
+ return;
+
+ if (!mod->vendor)
+ mesh_db_write_uint16_hex(jmodel, "modelId",
+ (uint16_t) mod->id);
+ else
+ mesh_db_write_uint32_hex(jmodel, "modelId", mod->id);
+
+ json_object_array_add(jmodels, jmodel);
+}
+
+/* Add unprovisioned node (local) */
+bool mesh_db_add_node(json_object *jnode, struct mesh_db_node *node) {
+
+ struct mesh_db_modes *modes = &node->modes;
+ const struct l_queue_entry *entry;
+ json_object *jelements;
+
+ /* CID, PID, VID, crpl */
+ if (!mesh_db_write_uint16_hex(jnode, "cid", node->cid))
+ return false;
+
+ if (!mesh_db_write_uint16_hex(jnode, "pid", node->pid))
+ return false;
+
+ if (!mesh_db_write_uint16_hex(jnode, "vid", node->vid))
+ return false;
+
+ if (!mesh_db_write_uint16_hex(jnode, "crpl", node->crpl))
+ return false;
+
+ /* Device UUID */
+ if (!add_key(jnode, "UUID", node->uuid))
+ return false;
+
+ /* Features: relay, LPN, friend, proxy*/
+ if (!mesh_db_write_relay_mode(jnode, modes->relay.state,
+ modes->relay.cnt,
+ modes->relay.interval))
+ return false;
+
+ if (!mesh_db_write_mode(jnode, "lowPower", modes->lpn))
+ return false;
+
+ if (!mesh_db_write_mode(jnode, "friend", modes->friend))
+ return false;
+
+ if (!mesh_db_write_mode(jnode, "proxy", modes->proxy))
+ return false;
+
+ /* Beaconing state */
+ if (!mesh_db_write_mode(jnode, "beacon", modes->beacon))
+ return false;
+
+ /* Sequence number */
+ json_object_object_add(jnode, "sequenceNumber",
+ json_object_new_int(node->seq_number));
+
+ /* Default TTL */
+ json_object_object_add(jnode, "defaultTTL",
+ json_object_new_int(node->ttl));
+
+ /* Elements */
+ jelements = json_object_new_array();
+ if (!jelements)
+ return false;
+
+ entry = l_queue_get_entries(node->elements);
+
+ for (; entry; entry = entry->next) {
+ struct mesh_db_element *ele = entry->data;
+ json_object *jelement, *jmodels;
+
+ jelement = json_object_new_object();
+
+ if (!jelement) {
+ json_object_put(jelements);
+ return false;
+ }
+
+ mesh_db_write_int(jelement, "elementIndex", ele->index);
+ mesh_db_write_uint16_hex(jelement, "location", ele->location);
+ json_object_array_add(jelements, jelement);
+
+ /* Models */
+ if (l_queue_isempty(ele->models))
+ continue;
+
+ jmodels = json_object_new_array();
+ if (!jmodels) {
+ json_object_put(jelements);
+ return false;
+ }
+
+ json_object_object_add(jelement, "models", jmodels);
+ l_queue_foreach(ele->models, add_model, jmodels);
+ }
+
+ json_object_object_add(jnode, "elements", jelements);
+
+ return true;
+}
diff --git a/mesh/mesh-db.h b/mesh/mesh-db.h
index 336302f28..40e60f72d 100644
--- a/mesh/mesh-db.h
+++ b/mesh/mesh-db.h
@@ -15,11 +15,8 @@
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
- *
*/
-#include <json-c/json.h>
-
struct mesh_db_sub {
bool virt;
union {
@@ -103,9 +100,7 @@ typedef bool (*mesh_db_app_key_cb)(uint16_t idx, uint16_t net_idx,
typedef bool (*mesh_db_node_cb)(struct mesh_db_node *node, void *user_data);
bool mesh_db_read_node(json_object *jobj, mesh_db_node_cb cb, void *user_data);
-bool mesh_db_read_unprovisioned_device(json_object *jnode, mesh_db_node_cb cb,
- void *user_data);
-bool mesh_db_read_prov_info(json_object *jnode, struct mesh_db_prov *prov);
+bool mesh_db_add_node(json_object *jnode, struct mesh_db_node *node);
bool mesh_db_read_iv_index(json_object *jobj, uint32_t *idx, bool *update);
bool mesh_db_read_device_key(json_object *jobj, uint8_t key_buf[16]);
bool mesh_db_read_net_transmit(json_object *jobj, uint8_t *cnt,
@@ -124,6 +119,8 @@ bool mesh_db_write_app_key(json_object *jobj, uint16_t net_idx,
bool mesh_db_write_int(json_object *jobj, const char *keyword, int value);
bool mesh_db_write_uint16_hex(json_object *jobj, const char *desc,
uint16_t value);
+bool mesh_db_write_uint32_hex(json_object *jobj, const char *desc,
+ uint32_t value);
bool mesh_db_write_bool(json_object *jobj, const char *keyword, bool value);
bool mesh_db_write_relay_mode(json_object *jnode, uint8_t mode, uint8_t count,
uint16_t interval);
--
2.14.5
next prev parent reply other threads:[~2018-12-28 22:08 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2018-12-28 22:07 [PATCH BlueZ v6 00/26] Major rewrite for Multi-Node and DBus Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 01/26] mesh: Structural changes for mesh Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 02/26] mesh: Utilities for DBus support Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 03/26] mesh: Internal errors Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 04/26] mesh: Rewrite storage for Multiple Nodes Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 05/26] mesh: Rewrite Node handling for multiple nodes Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 06/26] mesh: Rewrite Network layer " Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 07/26] mesh: Direction agnostic PB-ADV implementation Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 08/26] mesh: Acceptor side provisioning implementation Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 09/26] mesh: Initiator " Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 10/26] mesh: Rewrite Controler interface for full init Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 11/26] mesh: Unchanged variables set to const Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 12/26] mesh: Hex-String manipulation, and debug logging Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 13/26] mesh: re-arrange provisioning for DBus API Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 14/26] mesh: Re-architect " Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 15/26] mesh: Multi node Config Server model Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 16/26] mesh: restructure I/O for multiple nodes Brian Gix
2018-12-28 22:07 ` Brian Gix [this message]
2018-12-28 22:07 ` [PATCH BlueZ v6 18/26] mesh: Restructure model services " Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 19/26] mesh: DBUS interface for Provisioning Agent Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 20/26] mesh: restructure App Key storage Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 21/26] mesh: Clean-up Comment style Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 22/26] mesh: Update for DBus API and multi-node support Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 23/26] mesh: Add default location for Mesh Node storage Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 24/26] mesh: Sample Provisioning Agent Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 25/26] mesh: Sample On/Off Client and Server Brian Gix
2018-12-28 22:07 ` [PATCH BlueZ v6 26/26] mesh: Sample Mesh Joiner (provision acceptor) Brian Gix
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20181228220745.25147-18-brian.gix@intel.com \
--to=brian.gix@intel.com \
--cc=inga.stotland@intel.com \
--cc=johan.hedberg@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
--cc=marcel@holtmann.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).