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