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} mam_run_t;
 80
 81static void _mam_options_init(mam_options_t *options);
 82static void _mam_options_free(mam_options_t *options);
 83static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
 84static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
 85static void _mam_send_request(mam_run_t *run);
 86static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 87static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
 88static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 89static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 90static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run);
 91static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
 92static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
 93static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
 94static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
 95static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
 96static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
 97static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
 98static char *_mam_normalize_datetime(const char *value);
 99static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
100static void _mam_print_compact_message(xmpp_stanza_t *result);
101static void _mam_print_escaped_attr(const char *text);
102static void _mam_print_escaped_text(const char *text);
103static void _mam_print_escaped(const char *text, bool attribute);
104static void _mam_usage(xmppc_t *xmppc);
105
106void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
107  mam_options_t options;
108
109  if(argc >= 1) {
110    if (strcmp("list", argv[0]) == 0) {
111      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
112        _mam_start_request(xmppc, &options, false, false);
113      } else {
114        xmpp_disconnect(xmppc->conn);
115      }
116      return;
117    } else if (strcmp("pretty", argv[0]) == 0) {
118      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
119        _mam_start_request(xmppc, &options, true, false);
120      } else {
121        xmpp_disconnect(xmppc->conn);
122      }
123      return;
124    } else if (strcmp("receive", argv[0]) == 0) {
125      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
126        _mam_start_request(xmppc, &options, false, true);
127      } else {
128        xmpp_disconnect(xmppc->conn);
129      }
130      return;
131    }
132  }
133
134  _mam_usage(xmppc);
135  xmpp_disconnect(xmppc->conn);
136}
137
138static void _mam_options_init(mam_options_t *options) {
139  memset(options, 0, sizeof(*options));
140  options->max = MAM_DEFAULT_MAX;
141  options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
142}
143
144static void _mam_options_free(mam_options_t *options) {
145  g_free(options->with);
146  g_free(options->start);
147  g_free(options->end);
148  g_free(options->after_id);
149  g_free(options->before_id);
150  g_free(options->ids);
151}
152
153static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
154  _mam_options_init(options);
155
156  for (int i = 0; i < argc; i++) {
157    char *term = argv[i];
158    char *equals = strchr(term, '=');
159
160    if (!equals) {
161      if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
162        _mam_options_free(options);
163        return false;
164      }
165      options->has_narrowing = true;
166      continue;
167    }
168
169    char *key = g_strndup(term, equals - term);
170    const char *value = equals + 1;
171
172    if (strcmp(key, "with") == 0) {
173      if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
174        g_free(key);
175        _mam_options_free(options);
176        return false;
177      }
178      options->has_narrowing = true;
179    } else if (strcmp(key, "start") == 0) {
180      char *normalized = _mam_normalize_datetime(value);
181      if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
182        g_free(normalized);
183        g_free(key);
184        _mam_options_free(options);
185        return false;
186      }
187      g_free(normalized);
188      options->has_narrowing = true;
189    } else if (strcmp(key, "end") == 0) {
190      char *normalized = _mam_normalize_datetime(value);
191      if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
192        g_free(normalized);
193        g_free(key);
194        _mam_options_free(options);
195        return false;
196      }
197      g_free(normalized);
198      options->has_narrowing = true;
199    } else if (strcmp(key, "after-id") == 0) {
200      if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
201        g_free(key);
202        _mam_options_free(options);
203        return false;
204      }
205      options->has_narrowing = true;
206    } else if (strcmp(key, "before-id") == 0) {
207      if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
208        g_free(key);
209        _mam_options_free(options);
210        return false;
211      }
212      options->has_narrowing = true;
213    } else if (strcmp(key, "ids") == 0) {
214      if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
215        g_free(key);
216        _mam_options_free(options);
217        return false;
218      }
219      options->has_narrowing = true;
220    } else if (strcmp(key, "max") == 0) {
221      if (!_mam_parse_max(xmppc, options, value)) {
222        g_free(key);
223        _mam_options_free(options);
224        return false;
225      }
226    } else if (receive && strcmp(key, "timeout") == 0) {
227      if (!_mam_parse_timeout(xmppc, options, value)) {
228        g_free(key);
229        _mam_options_free(options);
230        return false;
231      }
232    } else {
233      logError(xmppc, "Unknown MAM field: %s\n", key);
234      g_free(key);
235      _mam_options_free(options);
236      return false;
237    }
238
239    g_free(key);
240  }
241
242  if (receive) {
243    if (!options->with) {
244      logError(xmppc, "mam receive requires a JID or with=<jid>\n");
245      _mam_options_free(options);
246      return false;
247    }
248    if (!options->after_id) {
249      logError(xmppc, "mam receive requires after-id=<mam-id>\n");
250      _mam_options_free(options);
251      return false;
252    }
253  } else if (!options->has_narrowing) {
254    logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
255    _mam_options_free(options);
256    return false;
257  }
258
259  if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
260    char *start = _mam_normalize_datetime("-5m");
261    options->start = start;
262  }
263
264  return true;
265}
266
267static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
268  mam_run_t *run = g_new0(mam_run_t, 1);
269  run->xmppc = xmppc;
270  run->options = *options;
271  run->receive = receive;
272  if (receive) {
273    run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
274  }
275
276  if (pretty) {
277    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
278  } else {
279    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
280  }
281
282  _mam_send_request(run);
283}
284
285static void _mam_send_request(mam_run_t *run) {
286  xmppc_t *xmppc = run->xmppc;
287  xmpp_ctx_t *ctx = xmppc->ctx;
288  char max[16];
289
290  g_snprintf(max, sizeof(max), "%d", run->options.max);
291
292  char* id = xmpp_uuid_gen(ctx);
293  g_free(run->queryid);
294  run->queryid = g_strdup(id);
295
296  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
297  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
298  xmpp_stanza_set_name(query, "query");
299  xmpp_stanza_set_ns(query, MAM_NS);
300  xmpp_stanza_set_attribute(query, "queryid", id);
301
302  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
303  xmpp_stanza_set_name(x,"x");
304  xmpp_stanza_set_ns(x, "jabber:x:data");
305  xmpp_stanza_set_type(x,"submit");
306  xmpp_stanza_add_child(query,x);
307
308  _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
309
310  if (run->options.with) {
311    _mam_add_field(ctx, x, "with", run->options.with, NULL);
312  }
313  if (run->options.start) {
314    _mam_add_field(ctx, x, "start", run->options.start, NULL);
315  }
316  if (run->options.end) {
317    _mam_add_field(ctx, x, "end", run->options.end, NULL);
318  }
319  if (run->options.ids) {
320    _mam_add_ids_field(ctx, x, run->options.ids);
321  }
322
323  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
324  xmpp_stanza_set_name(set, "set");
325  xmpp_stanza_set_ns(set, RSM_NS);
326  _mam_add_text_child(ctx, set, "max", max);
327  if (run->options.after_id) {
328    _mam_add_text_child(ctx, set, "after", run->options.after_id);
329  }
330  if (run->options.before_id) {
331    _mam_add_text_child(ctx, set, "before", run->options.before_id);
332  }
333  xmpp_stanza_add_child(query, set);
334
335  xmpp_stanza_add_child(iq,query);
336  xmpp_id_handler_add(xmppc->conn, _mam_finish, id, run);
337  xmpp_send(xmppc->conn,iq);
338
339  xmpp_stanza_release(iq);
340  xmpp_stanza_release(query);
341  xmpp_stanza_release(x);
342  xmpp_stanza_release(set);
343  free(id);
344}
345
346static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
347  mam_run_t *run = (mam_run_t *)userdata;
348  const char *type = xmpp_stanza_get_type(stanza);
349
350  if (type && strcmp(type, "error") == 0) {
351    logError(run->xmppc, "MAM request failed\n");
352    _mam_disconnect_and_free(conn, run);
353    return 0;
354  }
355
356  xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
357  if(fin) {
358    if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
359      xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
360    } else {
361      _mam_disconnect_and_free(conn, run);
362    }
363  }
364  return 0;
365}
366
367static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
368  mam_run_t *run = (mam_run_t *)userdata;
369
370  if (g_get_monotonic_time() >= run->deadline_us) {
371    _mam_disconnect_and_free(conn, run);
372    return 0;
373  }
374
375  _mam_send_request(run);
376  return 0;
377}
378
379static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run) {
380  xmpp_handler_delete(conn, _mam_display_message);
381  xmpp_handler_delete(conn, _mam_display_pretty_message);
382  _mam_options_free(&run->options);
383  g_free(run->queryid);
384  g_free(run);
385  xmpp_disconnect(conn);
386}
387
388static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
389  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
390  xmpp_stanza_set_name(field,"field");
391  xmpp_stanza_set_attribute(field, "var", var);
392  if (type) {
393    xmpp_stanza_set_type(field, type);
394  }
395
396  _mam_add_text_child(ctx, field, "value", value);
397  xmpp_stanza_add_child(x, field);
398  xmpp_stanza_release(field);
399}
400
401static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
402  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
403  xmpp_stanza_set_name(field,"field");
404  xmpp_stanza_set_attribute(field, "var", "ids");
405
406  char **parts = g_strsplit(ids, ",", -1);
407  for (int i = 0; parts[i] != NULL; i++) {
408    if (parts[i][0] != '\0') {
409      _mam_add_text_child(ctx, field, "value", parts[i]);
410    }
411  }
412  g_strfreev(parts);
413
414  xmpp_stanza_add_child(x, field);
415  xmpp_stanza_release(field);
416}
417
418static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
419  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
420  xmpp_stanza_set_name(child, name);
421
422  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
423  xmpp_stanza_set_text(body, text ? text : "");
424  xmpp_stanza_add_child(child, body);
425  xmpp_stanza_release(body);
426
427  xmpp_stanza_add_child(parent, child);
428  xmpp_stanza_release(child);
429}
430
431static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
432  if (*target) {
433    logError(xmppc, "Duplicate MAM field: %s\n", key);
434    return false;
435  }
436  if (!allow_empty && (!value || value[0] == '\0')) {
437    logError(xmppc, "MAM field requires a value: %s\n", key);
438    return false;
439  }
440  *target = g_strdup(value ? value : "");
441  return true;
442}
443
444static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
445  char *end = NULL;
446  gint64 parsed = g_ascii_strtoll(value, &end, 10);
447
448  if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
449    logError(xmppc, "max must be a positive integer\n");
450    return false;
451  }
452
453  if (parsed > MAM_MAX_CAP) {
454    parsed = MAM_MAX_CAP;
455  }
456
457  options->max = (int)parsed;
458  return true;
459}
460
461static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
462  gint64 seconds = 0;
463
464  if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT) {
465    logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
466    return false;
467  }
468
469  options->timeout = (int)seconds;
470  return true;
471}
472
473static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
474  char *end = NULL;
475  gint64 parsed = 0;
476  gint64 multiplier = 1;
477
478  if (!value || value[0] == '\0') {
479    return false;
480  }
481
482  parsed = g_ascii_strtoll(value, &end, 10);
483  if (end == value || parsed < 0) {
484    return false;
485  }
486
487  if (*end == '\0') {
488    multiplier = 1;
489  } else if (end[1] == '\0') {
490    if (*end == 's') {
491      multiplier = 1;
492    } else if (*end == 'm') {
493      multiplier = 60;
494    } else if (*end == 'h') {
495      multiplier = 60 * 60;
496    } else if (*end == 'd') {
497      multiplier = 60 * 60 * 24;
498    } else {
499      return false;
500    }
501  } else {
502    return false;
503  }
504
505  *seconds = parsed * multiplier;
506  return true;
507}
508
509static char *_mam_normalize_datetime(const char *value) {
510  if (value && value[0] == '-' && value[1] != '\0') {
511    gint64 seconds = 0;
512    if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
513      GDateTime *now = g_date_time_new_now_utc();
514      GDateTime *then = g_date_time_add_seconds(now, -seconds);
515      char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
516      g_date_time_unref(then);
517      g_date_time_unref(now);
518      return formatted;
519    }
520  }
521
522  return g_strdup(value);
523}
524
525static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
526  const char *ns = xmpp_stanza_get_ns(result);
527  if (!ns || strcmp(MAM_NS, ns) != 0) {
528    return false;
529  }
530
531  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
532  return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
533}
534
535static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
536  mam_run_t *run = (mam_run_t *)userdata;
537  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
538
539  if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
540    _mam_print_compact_message(result);
541    run->messages_total++;
542  }
543  return 1;
544}
545
546static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
547{
548  mam_run_t *run = (mam_run_t *)userdata;
549  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
550
551  if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {
552
553    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
554    if (forwarded) {
555
556      xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
557      if (message) {
558        const char* const from = xmpp_stanza_get_from(message);
559        xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
560
561        if (body) {
562          char *text = xmpp_stanza_get_text(body);
563          printf("%s: %s\n", from ? from : "", text ? text : "");
564          free(text);
565          run->messages_total++;
566        }
567      }
568    }
569  }
570
571  return 1;
572}
573
574static void _mam_print_compact_message(xmpp_stanza_t *result) {
575  const char *mam_id = xmpp_stanza_get_attribute(result, "id");
576  const char *message_id = "";
577  const char *stamp = "";
578  const char *from = "";
579  const char *to = "";
580  char *body_text = NULL;
581
582  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
583  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
584
585  if (forwarded) {
586    xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
587    if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
588      stamp = xmpp_stanza_get_attribute(delay, "stamp");
589    }
590  }
591
592  if (message) {
593    xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
594    if (xmpp_stanza_get_id(message)) {
595      message_id = xmpp_stanza_get_id(message);
596    }
597    if (xmpp_stanza_get_from(message)) {
598      from = xmpp_stanza_get_from(message);
599    }
600    if (xmpp_stanza_get_to(message)) {
601      to = xmpp_stanza_get_to(message);
602    }
603    if (body) {
604      body_text = xmpp_stanza_get_text(body);
605    }
606  }
607
608  printf("<message id=\"");
609  _mam_print_escaped_attr(mam_id ? mam_id : "");
610  printf("\" message-id=\"");
611  _mam_print_escaped_attr(message_id);
612  printf("\" stamp=\"");
613  _mam_print_escaped_attr(stamp);
614  printf("\" from=\"");
615  _mam_print_escaped_attr(from);
616  printf("\" to=\"");
617  _mam_print_escaped_attr(to);
618  printf("\"><body>");
619  _mam_print_escaped_text(body_text ? body_text : "");
620  printf("</body></message>\n");
621  fflush(stdout);
622
623  free(body_text);
624}
625
626static void _mam_print_escaped_attr(const char *text) {
627  _mam_print_escaped(text, true);
628}
629
630static void _mam_print_escaped_text(const char *text) {
631  _mam_print_escaped(text, false);
632}
633
634static void _mam_print_escaped(const char *text, bool attribute) {
635  const unsigned char *p = (const unsigned char *)(text ? text : "");
636
637  while (*p) {
638    switch (*p) {
639      case '&':
640        printf("&amp;");
641        break;
642      case '<':
643        printf("&lt;");
644        break;
645      case '>':
646        printf("&gt;");
647        break;
648      case '"':
649        if (attribute) {
650          printf("&quot;");
651        } else {
652          putchar('"');
653        }
654        break;
655      case '\n':
656        printf("&#10;");
657        break;
658      case '\r':
659        printf("&#13;");
660        break;
661      case '\t':
662        if (attribute) {
663          printf("&#9;");
664        } else {
665          putchar('\t');
666        }
667        break;
668      default:
669        if (*p < 0x20) {
670          printf("&#x%02X;", *p);
671        } else {
672          putchar(*p);
673        }
674        break;
675    }
676    p++;
677  }
678}
679
680static void _mam_usage(xmppc_t *xmppc) {
681  logError(xmppc, "Usage:\n");
682  logError(xmppc, "  mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
683  logError(xmppc, "  mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
684}