mam.c

  1/*!
  2 * @file File: mam.c
  3 *
  4 * vim: expandtab:ts=2:sts=2:sw=2
  5 *
  6 * @copyright 
  7 * 
  8 * Copyright (C) 2020 Anoxinon e.V. 
  9 *
 10 * This file is part of xmppc.
 11 *
 12 * xmppc is free software: you can redistribute it and/or modify
 13 * it under the terms of the GNU General Public License as published by
 14 * the Free Software Foundation, either version 3 of the License, or
 15 * (at your option) any later version.
 16 *
 17 * xmppc is distributed in the hope that it will be useful,
 18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 20 * GNU General Public License for more details.
 21 *
 22 * You should have received a copy of the GNU General Public License
 23 * along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 24 *
 25 * German
 26 *
 27 * Diese Datei ist Teil von xmppc.
 28 *
 29 * xmppc ist Freie Software: Sie können es unter den Bedingungen
 30 * der GNU General Public License, wie von der Free Software Foundation,
 31 * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder neueren
 32 * veröffentlichten Version, weiter verteilen und/oder modifizieren.
 33 *
 34 * xmppc wird in der Hoffnung, dass es nützlich sein wird, aber
 35 * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
 36 * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
 37 * Siehe die GNU General Public License für weitere Details.
 38 *
 39 * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
 40 * Programm erhalten haben. Wenn nicht, siehe <https://www.gnu.org/licenses/>.
 41 */
 42
 43#include "mam.h"
 44#include "glib.h"
 45#include "stdbool.h"
 46#include "stdio.h"
 47#include "stdlib.h"
 48#include "string.h"
 49
 50#define MAM_NS "urn:xmpp:mam:2"
 51#define FORWARD_NS "urn:xmpp:forward:0"
 52#define DELAY_NS "urn:xmpp:delay"
 53#define RSM_NS "http://jabber.org/protocol/rsm"
 54
 55#define MAM_DEFAULT_MAX 5
 56#define MAM_MAX_CAP 50
 57#define MAM_DEFAULT_TIMEOUT_SECONDS 300
 58#define MAM_POLL_PERIOD_MS 2000
 59
 60typedef struct {
 61  char *with;
 62  char *start;
 63  char *end;
 64  char *after_id;
 65  char *before_id;
 66  char *ids;
 67  int max;
 68  int timeout;
 69  bool has_narrowing;
 70} mam_options_t;
 71
 72typedef struct {
 73  xmppc_t *xmppc;
 74  mam_options_t options;
 75  bool receive;
 76  int messages_total;
 77  gint64 deadline_us;
 78  char *queryid;
 79  bool done;
 80} mam_run_t;
 81
 82static void _mam_options_init(mam_options_t *options);
 83static void _mam_options_free(mam_options_t *options);
 84static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
 85static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
 86static void _mam_send_request(mam_run_t *run);
 87static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 88static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
 89static int _mam_receive_timeout(xmpp_conn_t *const conn, void *const userdata);
 90static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 91static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 92static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run, bool delete_id_handler);
 93static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
 94static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
 95static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
 96static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
 97static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
 98static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
 99static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
100static char *_mam_normalize_datetime(const char *value);
101static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
102static void _mam_print_compact_message(xmpp_stanza_t *result);
103static void _mam_print_escaped_attr(const char *text);
104static void _mam_print_escaped_text(const char *text);
105static void _mam_print_escaped(const char *text, bool attribute);
106static void _mam_usage(xmppc_t *xmppc);
107
108void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
109  mam_options_t options;
110
111  if(argc >= 1) {
112    if (strcmp("list", argv[0]) == 0) {
113      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
114        _mam_start_request(xmppc, &options, false, false);
115      } else {
116        xmpp_disconnect(xmppc->conn);
117      }
118      return;
119    } else if (strcmp("pretty", argv[0]) == 0) {
120      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
121        _mam_start_request(xmppc, &options, true, false);
122      } else {
123        xmpp_disconnect(xmppc->conn);
124      }
125      return;
126    } else if (strcmp("receive", argv[0]) == 0) {
127      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
128        _mam_start_request(xmppc, &options, false, true);
129      } else {
130        xmpp_disconnect(xmppc->conn);
131      }
132      return;
133    }
134  }
135
136  _mam_usage(xmppc);
137  xmpp_disconnect(xmppc->conn);
138}
139
140static void _mam_options_init(mam_options_t *options) {
141  memset(options, 0, sizeof(*options));
142  options->max = MAM_DEFAULT_MAX;
143  options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
144}
145
146static void _mam_options_free(mam_options_t *options) {
147  g_free(options->with);
148  g_free(options->start);
149  g_free(options->end);
150  g_free(options->after_id);
151  g_free(options->before_id);
152  g_free(options->ids);
153}
154
155static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
156  _mam_options_init(options);
157
158  for (int i = 0; i < argc; i++) {
159    char *term = argv[i];
160    char *equals = strchr(term, '=');
161
162    if (!equals) {
163      if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
164        _mam_options_free(options);
165        return false;
166      }
167      options->has_narrowing = true;
168      continue;
169    }
170
171    char *key = g_strndup(term, equals - term);
172    const char *value = equals + 1;
173
174    if (strcmp(key, "with") == 0) {
175      if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
176        g_free(key);
177        _mam_options_free(options);
178        return false;
179      }
180      options->has_narrowing = true;
181    } else if (strcmp(key, "start") == 0) {
182      char *normalized = _mam_normalize_datetime(value);
183      if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
184        g_free(normalized);
185        g_free(key);
186        _mam_options_free(options);
187        return false;
188      }
189      g_free(normalized);
190      options->has_narrowing = true;
191    } else if (strcmp(key, "end") == 0) {
192      char *normalized = _mam_normalize_datetime(value);
193      if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
194        g_free(normalized);
195        g_free(key);
196        _mam_options_free(options);
197        return false;
198      }
199      g_free(normalized);
200      options->has_narrowing = true;
201    } else if (strcmp(key, "after-id") == 0) {
202      if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
203        g_free(key);
204        _mam_options_free(options);
205        return false;
206      }
207      options->has_narrowing = true;
208    } else if (strcmp(key, "before-id") == 0) {
209      if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
210        g_free(key);
211        _mam_options_free(options);
212        return false;
213      }
214      options->has_narrowing = true;
215    } else if (strcmp(key, "ids") == 0) {
216      if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
217        g_free(key);
218        _mam_options_free(options);
219        return false;
220      }
221      options->has_narrowing = true;
222    } else if (strcmp(key, "max") == 0) {
223      if (!_mam_parse_max(xmppc, options, value)) {
224        g_free(key);
225        _mam_options_free(options);
226        return false;
227      }
228    } else if (receive && strcmp(key, "timeout") == 0) {
229      if (!_mam_parse_timeout(xmppc, options, value)) {
230        g_free(key);
231        _mam_options_free(options);
232        return false;
233      }
234    } else {
235      logError(xmppc, "Unknown MAM field: %s\n", key);
236      g_free(key);
237      _mam_options_free(options);
238      return false;
239    }
240
241    g_free(key);
242  }
243
244  if (receive) {
245    if (!options->with) {
246      logError(xmppc, "mam receive requires a JID or with=<jid>\n");
247      _mam_options_free(options);
248      return false;
249    }
250    if (!options->after_id) {
251      logError(xmppc, "mam receive requires after-id=<mam-id>\n");
252      _mam_options_free(options);
253      return false;
254    }
255  } else if (!options->has_narrowing) {
256    logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
257    _mam_options_free(options);
258    return false;
259  }
260
261  if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
262    char *start = _mam_normalize_datetime("-5m");
263    options->start = start;
264  }
265
266  return true;
267}
268
269static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
270  mam_run_t *run = g_new0(mam_run_t, 1);
271  run->xmppc = xmppc;
272  run->options = *options;
273  run->receive = receive;
274  if (receive) {
275    unsigned long timeout_ms = (unsigned long)run->options.timeout * 1000;
276    run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
277    xmpp_timed_handler_add(xmppc->conn, _mam_receive_timeout, timeout_ms, run);
278  }
279
280  if (pretty) {
281    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
282  } else {
283    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
284  }
285
286  _mam_send_request(run);
287}
288
289static void _mam_send_request(mam_run_t *run) {
290  xmppc_t *xmppc = run->xmppc;
291  xmpp_ctx_t *ctx = xmppc->ctx;
292  char max[16];
293
294  g_snprintf(max, sizeof(max), "%d", run->options.max);
295
296  char* id = xmpp_uuid_gen(ctx);
297  g_free(run->queryid);
298  run->queryid = g_strdup(id);
299
300  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
301  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
302  xmpp_stanza_set_name(query, "query");
303  xmpp_stanza_set_ns(query, MAM_NS);
304  xmpp_stanza_set_attribute(query, "queryid", id);
305
306  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
307  xmpp_stanza_set_name(x,"x");
308  xmpp_stanza_set_ns(x, "jabber:x:data");
309  xmpp_stanza_set_type(x,"submit");
310  xmpp_stanza_add_child(query,x);
311
312  _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
313
314  if (run->options.with) {
315    _mam_add_field(ctx, x, "with", run->options.with, NULL);
316  }
317  if (run->options.start) {
318    _mam_add_field(ctx, x, "start", run->options.start, NULL);
319  }
320  if (run->options.end) {
321    _mam_add_field(ctx, x, "end", run->options.end, NULL);
322  }
323  if (run->options.ids) {
324    _mam_add_ids_field(ctx, x, run->options.ids);
325  }
326
327  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
328  xmpp_stanza_set_name(set, "set");
329  xmpp_stanza_set_ns(set, RSM_NS);
330  _mam_add_text_child(ctx, set, "max", max);
331  if (run->options.after_id) {
332    _mam_add_text_child(ctx, set, "after", run->options.after_id);
333  }
334  if (run->options.before_id) {
335    _mam_add_text_child(ctx, set, "before", run->options.before_id);
336  }
337  xmpp_stanza_add_child(query, set);
338
339  xmpp_stanza_add_child(iq,query);
340  xmpp_id_handler_add(xmppc->conn, _mam_finish, id, run);
341  xmpp_send(xmppc->conn,iq);
342
343  xmpp_stanza_release(iq);
344  xmpp_stanza_release(query);
345  xmpp_stanza_release(x);
346  xmpp_stanza_release(set);
347  free(id);
348}
349
350static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
351  mam_run_t *run = (mam_run_t *)userdata;
352  const char *type = xmpp_stanza_get_type(stanza);
353
354  if (run->done) {
355    return 0;
356  }
357
358  if (type && strcmp(type, "error") == 0) {
359    logError(run->xmppc, "MAM request failed\n");
360    _mam_disconnect_and_free(conn, run, false);
361    return 0;
362  }
363
364  xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
365  if(fin) {
366    if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
367      xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
368    } else {
369      _mam_disconnect_and_free(conn, run, false);
370    }
371  }
372  return 0;
373}
374
375static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
376  mam_run_t *run = (mam_run_t *)userdata;
377
378  if (run->done) {
379    return 0;
380  }
381
382  if (g_get_monotonic_time() >= run->deadline_us) {
383    _mam_disconnect_and_free(conn, run, false);
384    return 0;
385  }
386
387  _mam_send_request(run);
388  return 0;
389}
390
391static int _mam_receive_timeout(xmpp_conn_t *const conn, void *const userdata) {
392  mam_run_t *run = (mam_run_t *)userdata;
393
394  if (!run->done) {
395    _mam_disconnect_and_free(conn, run, true);
396  }
397
398  return 0;
399}
400
401static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run, bool delete_id_handler) {
402  if (run->done) {
403    return;
404  }
405
406  run->done = true;
407  xmpp_handler_delete(conn, _mam_display_message);
408  xmpp_handler_delete(conn, _mam_display_pretty_message);
409  xmpp_timed_handler_delete(conn, _mam_receive_poll);
410  xmpp_timed_handler_delete(conn, _mam_receive_timeout);
411  if (delete_id_handler && run->queryid) {
412    xmpp_id_handler_delete(conn, _mam_finish, run->queryid);
413  }
414  _mam_options_free(&run->options);
415  g_free(run->queryid);
416  g_free(run);
417  xmpp_disconnect(conn);
418}
419
420static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
421  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
422  xmpp_stanza_set_name(field,"field");
423  xmpp_stanza_set_attribute(field, "var", var);
424  if (type) {
425    xmpp_stanza_set_type(field, type);
426  }
427
428  _mam_add_text_child(ctx, field, "value", value);
429  xmpp_stanza_add_child(x, field);
430  xmpp_stanza_release(field);
431}
432
433static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
434  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
435  xmpp_stanza_set_name(field,"field");
436  xmpp_stanza_set_attribute(field, "var", "ids");
437
438  char **parts = g_strsplit(ids, ",", -1);
439  for (int i = 0; parts[i] != NULL; i++) {
440    if (parts[i][0] != '\0') {
441      _mam_add_text_child(ctx, field, "value", parts[i]);
442    }
443  }
444  g_strfreev(parts);
445
446  xmpp_stanza_add_child(x, field);
447  xmpp_stanza_release(field);
448}
449
450static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
451  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
452  xmpp_stanza_set_name(child, name);
453
454  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
455  xmpp_stanza_set_text(body, text ? text : "");
456  xmpp_stanza_add_child(child, body);
457  xmpp_stanza_release(body);
458
459  xmpp_stanza_add_child(parent, child);
460  xmpp_stanza_release(child);
461}
462
463static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
464  if (*target) {
465    logError(xmppc, "Duplicate MAM field: %s\n", key);
466    return false;
467  }
468  if (!allow_empty && (!value || value[0] == '\0')) {
469    logError(xmppc, "MAM field requires a value: %s\n", key);
470    return false;
471  }
472  *target = g_strdup(value ? value : "");
473  return true;
474}
475
476static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
477  char *end = NULL;
478  gint64 parsed = g_ascii_strtoll(value, &end, 10);
479
480  if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
481    logError(xmppc, "max must be a positive integer\n");
482    return false;
483  }
484
485  if (parsed > MAM_MAX_CAP) {
486    parsed = MAM_MAX_CAP;
487  }
488
489  options->max = (int)parsed;
490  return true;
491}
492
493static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
494  gint64 seconds = 0;
495
496  if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT || (guint64)seconds > ((guint64)G_MAXULONG / 1000)) {
497    logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
498    return false;
499  }
500
501  options->timeout = (int)seconds;
502  return true;
503}
504
505static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
506  char *end = NULL;
507  gint64 parsed = 0;
508  gint64 multiplier = 1;
509
510  if (!value || value[0] == '\0') {
511    return false;
512  }
513
514  parsed = g_ascii_strtoll(value, &end, 10);
515  if (end == value || parsed < 0) {
516    return false;
517  }
518
519  if (*end == '\0') {
520    multiplier = 1;
521  } else if (end[1] == '\0') {
522    if (*end == 's') {
523      multiplier = 1;
524    } else if (*end == 'm') {
525      multiplier = 60;
526    } else if (*end == 'h') {
527      multiplier = 60 * 60;
528    } else if (*end == 'd') {
529      multiplier = 60 * 60 * 24;
530    } else {
531      return false;
532    }
533  } else {
534    return false;
535  }
536
537  if (parsed > G_MAXINT64 / multiplier) {
538    return false;
539  }
540
541  *seconds = parsed * multiplier;
542  return true;
543}
544
545static char *_mam_normalize_datetime(const char *value) {
546  if (value && value[0] == '-' && value[1] != '\0') {
547    gint64 seconds = 0;
548    if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
549      GDateTime *now = g_date_time_new_now_utc();
550      GDateTime *then = g_date_time_add_seconds(now, -seconds);
551      char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
552      g_date_time_unref(then);
553      g_date_time_unref(now);
554      return formatted;
555    }
556  }
557
558  return g_strdup(value);
559}
560
561static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
562  const char *ns = xmpp_stanza_get_ns(result);
563  if (!ns || strcmp(MAM_NS, ns) != 0) {
564    return false;
565  }
566
567  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
568  return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
569}
570
571static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
572  mam_run_t *run = (mam_run_t *)userdata;
573  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
574
575  if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
576    _mam_print_compact_message(result);
577    run->messages_total++;
578  }
579  return 1;
580}
581
582static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
583{
584  mam_run_t *run = (mam_run_t *)userdata;
585  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
586
587  if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {
588
589    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
590    if (forwarded) {
591
592      xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
593      if (message) {
594        const char* const from = xmpp_stanza_get_from(message);
595        xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
596
597        if (body) {
598          char *text = xmpp_stanza_get_text(body);
599          printf("%s: %s\n", from ? from : "", text ? text : "");
600          free(text);
601          run->messages_total++;
602        }
603      }
604    }
605  }
606
607  return 1;
608}
609
610static void _mam_print_compact_message(xmpp_stanza_t *result) {
611  const char *mam_id = xmpp_stanza_get_attribute(result, "id");
612  const char *message_id = "";
613  const char *stamp = "";
614  const char *from = "";
615  const char *to = "";
616  char *body_text = NULL;
617
618  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
619  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
620
621  if (forwarded) {
622    xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
623    if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
624      stamp = xmpp_stanza_get_attribute(delay, "stamp");
625    }
626  }
627
628  if (message) {
629    xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
630    if (xmpp_stanza_get_id(message)) {
631      message_id = xmpp_stanza_get_id(message);
632    }
633    if (xmpp_stanza_get_from(message)) {
634      from = xmpp_stanza_get_from(message);
635    }
636    if (xmpp_stanza_get_to(message)) {
637      to = xmpp_stanza_get_to(message);
638    }
639    if (body) {
640      body_text = xmpp_stanza_get_text(body);
641    }
642  }
643
644  printf("<message id=\"");
645  _mam_print_escaped_attr(mam_id ? mam_id : "");
646  printf("\" message-id=\"");
647  _mam_print_escaped_attr(message_id);
648  printf("\" stamp=\"");
649  _mam_print_escaped_attr(stamp);
650  printf("\" from=\"");
651  _mam_print_escaped_attr(from);
652  printf("\" to=\"");
653  _mam_print_escaped_attr(to);
654  printf("\"><body>");
655  _mam_print_escaped_text(body_text ? body_text : "");
656  printf("</body></message>\n");
657  fflush(stdout);
658
659  free(body_text);
660}
661
662static void _mam_print_escaped_attr(const char *text) {
663  _mam_print_escaped(text, true);
664}
665
666static void _mam_print_escaped_text(const char *text) {
667  _mam_print_escaped(text, false);
668}
669
670static void _mam_print_escaped(const char *text, bool attribute) {
671  const unsigned char *p = (const unsigned char *)(text ? text : "");
672
673  while (*p) {
674    switch (*p) {
675      case '&':
676        printf("&amp;");
677        break;
678      case '<':
679        printf("&lt;");
680        break;
681      case '>':
682        printf("&gt;");
683        break;
684      case '"':
685        if (attribute) {
686          printf("&quot;");
687        } else {
688          putchar('"');
689        }
690        break;
691      case '\n':
692        printf("&#10;");
693        break;
694      case '\r':
695        printf("&#13;");
696        break;
697      case '\t':
698        if (attribute) {
699          printf("&#9;");
700        } else {
701          putchar('\t');
702        }
703        break;
704      default:
705        if (*p < 0x20) {
706          printf("&#x%02X;", *p);
707        } else {
708          putchar(*p);
709        }
710        break;
711    }
712    p++;
713  }
714}
715
716static void _mam_usage(xmppc_t *xmppc) {
717  logError(xmppc, "Usage:\n");
718  logError(xmppc, "  mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
719  logError(xmppc, "  mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
720}