Coverage Report

Created: 2026-09-01 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pigeonhole/src/lib-sieve/plugins/enotify/mailto/uri-mailto.c
Line
Count
Source
1
/* Copyright (c) Pigeonhole authors, see top-level COPYING file */
2
3
/* FIXME: URI syntax conforms to something somewhere in between RFC 2368 and
4
          draft-duerst-mailto-bis-05.txt. Should fully migrate to new
5
          specification when it matures. This requires modifications to the
6
          address parser (no whitespace allowed within the address itself) and
7
          UTF-8 support will be required in the URL.
8
 */
9
10
#include "lib.h"
11
#include "array.h"
12
#include "str.h"
13
#include "str-sanitize.h"
14
15
#include "rfc2822.h"
16
17
#include "sieve-common.h"
18
#include "sieve-error.h"
19
#include "sieve-address.h"
20
#include "sieve-message.h"
21
22
#include "uri-mailto.h"
23
24
/* Parser object */
25
26
struct uri_mailto_parser {
27
  pool_t pool;
28
  const struct uri_mailto_log *log;
29
30
  struct uri_mailto *uri;
31
32
  const char **reserved_headers;
33
  const char **unique_headers;
34
35
  int max_recipients;
36
  int max_headers;
37
};
38
39
/* Error handling */
40
41
#define uri_mailto_error(PARSER, ...) \
42
0
  uri_mailto_log((PARSER), LOG_TYPE_ERROR, \
43
0
           __FILE__, __LINE__, __VA_ARGS__)
44
#define uri_mailto_warning(PARSER, ...) \
45
0
  uri_mailto_log((PARSER), LOG_TYPE_WARNING, \
46
0
           __FILE__, __LINE__, __VA_ARGS__)
47
48
static inline void ATTR_FORMAT(5, 6)
49
uri_mailto_log(struct uri_mailto_parser *parser, enum log_type log_type,
50
         const char *csrc_filename, unsigned int csrc_linenum,
51
         const char *fmt, ...)
52
0
{
53
0
  va_list args;
54
55
0
  if (parser->log == NULL || parser->log->logv == NULL)
56
0
    return;
57
58
0
  va_start(args, fmt);
59
0
  parser->log->logv(parser->log->context, log_type,
60
0
        csrc_filename, csrc_linenum, fmt, args);
61
0
  va_end(args);
62
0
}
63
64
65
/*
66
 * Error handling
67
 */
68
69
/*
70
 * Reserved and unique headers
71
 */
72
73
static inline bool
74
uri_mailto_header_is_reserved(struct uri_mailto_parser *parser,
75
            const char *field_name)
76
0
{
77
0
  const char **hdr = parser->reserved_headers;
78
79
0
  if (hdr == NULL)
80
0
    return FALSE;
81
82
  /* Check whether it is reserved */
83
0
  while (*hdr != NULL) {
84
0
    if (strcasecmp(field_name, *hdr) == 0)
85
0
      return TRUE;
86
0
    hdr++;
87
0
  }
88
0
  return FALSE;
89
0
}
90
91
static inline bool
92
uri_mailto_header_is_unique(struct uri_mailto_parser *parser,
93
          const char *field_name)
94
0
{
95
0
  const char **hdr = parser->unique_headers;
96
97
0
  if (hdr == NULL)
98
0
    return FALSE;
99
100
  /* Check whether it is supposed to be unique */
101
0
  while (*hdr != NULL) {
102
0
    if (strcasecmp(field_name, *hdr) == 0)
103
0
      return TRUE;
104
0
    hdr++;
105
0
  }
106
0
  return FALSE;
107
0
}
108
109
/*
110
 * Low-level URI parsing.
111
 *
112
 * FIXME: much of this implementation will be common to other URI schemes. This
113
 *        should be merged into a common implementation.
114
 */
115
116
static const char _qchar_lookup[256] = {
117
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 00
118
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 10
119
  0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,  // 20
120
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,  // 30
121
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // 40
122
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,  // 50
123
  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // 60
124
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,  // 70
125
126
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 80
127
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 90
128
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // A0
129
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // B0
130
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // C0
131
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // D0
132
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // E0
133
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // F0
134
};
135
136
static inline bool _is_qchar(char c)
137
0
{
138
0
  return ((_qchar_lookup[(unsigned char)c] & 0x01) != 0);
139
0
}
140
141
static inline int _decode_hex_digit(unsigned char digit)
142
0
{
143
0
  switch (digit) {
144
0
  case '0': case '1': case '2': case '3': case '4':
145
0
  case '5': case '6': case '7': case '8': case '9':
146
0
    return (int) digit - '0';
147
0
  case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
148
0
    return (int) digit - 'a' + 0x0a;
149
0
  case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
150
0
    return (int) digit - 'A' + 0x0A;
151
0
  }
152
0
  return -1;
153
0
}
154
155
static bool _parse_hex_value(const char **in, char *out)
156
0
{
157
0
  int value, digit;
158
159
0
  if ((digit = _decode_hex_digit((unsigned char)**in)) < 0)
160
0
    return FALSE;
161
162
0
  value = digit << 4;
163
0
  (*in)++;
164
165
0
  if ((digit = _decode_hex_digit((unsigned char)**in)) < 0)
166
0
    return FALSE;
167
168
0
  value |= digit;
169
0
  (*in)++;
170
171
0
  if (value == 0)
172
0
    return FALSE;
173
174
0
  *out = (char)value;
175
0
  return TRUE;
176
0
}
177
178
/*
179
 * URI recipient parsing
180
 */
181
182
static bool
183
uri_mailto_add_valid_recipient(struct uri_mailto_parser *parser,
184
             string_t *recipient, bool cc)
185
0
{
186
0
  struct uri_mailto *uri = parser->uri;
187
0
  struct uri_mailto_recipient *new_recipient;
188
0
  struct uri_mailto_recipient *rcpts;
189
0
  unsigned int count, i;
190
0
  const char *error;
191
0
  const struct smtp_address *address;
192
193
  /* Verify recipient */
194
0
  if ((address = sieve_address_parse_str(recipient, &error)) == NULL) {
195
0
    uri_mailto_error(parser, "invalid recipient '%s': %s",
196
0
         str_sanitize(str_c(recipient), 80), error);
197
0
    return FALSE;
198
0
  }
199
200
  /* Add recipient to the uri */
201
0
  if (uri != NULL) {
202
    /* Get current recipients */
203
0
    rcpts = array_get_modifiable(&uri->recipients, &count);
204
205
    /* Enforce limits */
206
0
    if (parser->max_recipients > 0 &&
207
0
        (int)count >= parser->max_recipients) {
208
0
      if ((int)count == parser->max_recipients) {
209
0
        uri_mailto_warning(
210
0
          parser,
211
0
          "more than the maximum %u recipients specified; "
212
0
          "rest is discarded",
213
0
          parser->max_recipients);
214
0
      }
215
0
      return TRUE;
216
0
    }
217
218
    /* Check for duplicate first */
219
0
    for (i = 0; i < count; i++) {
220
0
      if (smtp_address_equals(rcpts[i].address, address)) {
221
        /* Upgrade existing Cc: recipient to a To:
222
           recipient if possible */
223
0
        rcpts[i].carbon_copy =
224
0
          (rcpts[i].carbon_copy && cc);
225
226
0
        uri_mailto_warning(
227
0
          parser,
228
0
          "ignored duplicate recipient '%s'",
229
0
          str_sanitize(str_c(recipient), 80));
230
0
        return TRUE;
231
0
      }
232
0
    }
233
234
    /* Add */
235
0
    new_recipient = array_append_space(&uri->recipients);
236
0
    new_recipient->carbon_copy = cc;
237
0
    new_recipient->full = p_strdup(parser->pool, str_c(recipient));
238
0
    new_recipient->address =
239
0
      smtp_address_clone(parser->pool, address);
240
0
  }
241
242
0
  return TRUE;
243
0
}
244
245
static bool
246
uri_mailto_parse_recipients(struct uri_mailto_parser *parser,
247
          const char **uri_p)
248
0
{
249
0
  string_t *to = t_str_new(128);
250
0
  const char *p = *uri_p;
251
252
0
  if (*p == '\0' || *p == '?')
253
0
    return TRUE;
254
255
0
  while (*p != '\0' && *p != '?') {
256
0
    if (*p == '%') {
257
      /* % encoded character */
258
0
      char ch;
259
260
0
      p++;
261
262
      /* Parse 2-digit hex value */
263
0
      if (!_parse_hex_value(&p, &ch)) {
264
0
        uri_mailto_error(parser, "invalid %% encoding");
265
0
        return FALSE;
266
0
      }
267
268
      /* Check for delimiter */
269
0
      if (ch == ',') {
270
        /* Verify and add recipient */
271
0
        if (!uri_mailto_add_valid_recipient(
272
0
          parser, to, FALSE))
273
0
          return FALSE;
274
275
        /* Reset for next recipient */
276
0
        str_truncate(to, 0);
277
0
      } else {
278
        /* Content character */
279
0
        str_append_c(to, ch);
280
0
      }
281
0
    } else {
282
0
      if (*p == ':' || *p == ';' || *p == ',' ||
283
0
          !_is_qchar(*p)) {
284
0
        uri_mailto_error(
285
0
          parser,
286
0
          "invalid character '%c' in 'to' part", *p);
287
0
        return FALSE;
288
0
      }
289
290
      /* Content character */
291
0
      str_append_c(to, *p);
292
0
      p++;
293
0
    }
294
0
  }
295
296
0
  i_assert(*p == '\0' || *p == '?');
297
298
  /* Verify and add recipient */
299
0
  if (!uri_mailto_add_valid_recipient(parser, to, FALSE))
300
0
    return FALSE;
301
302
0
  *uri_p = p;
303
0
  return TRUE;
304
0
}
305
306
static bool
307
uri_mailto_parse_header_recipients(struct uri_mailto_parser *parser,
308
           string_t *rcpt_header, bool cc)
309
0
{
310
0
  string_t *to = t_str_new(128);
311
0
  const char *p = (const char *)str_data(rcpt_header);
312
0
  const char *pend = p + str_len(rcpt_header);
313
314
0
  while (p < pend) {
315
0
    if (*p == ',') {
316
      /* Verify and add recipient */
317
0
      if (!uri_mailto_add_valid_recipient(parser, to, cc))
318
0
        return FALSE;
319
320
      /* Reset for next recipient */
321
0
      str_truncate(to, 0);
322
0
    } else {
323
      /* Content character */
324
0
      str_append_c(to, *p);
325
0
    }
326
0
    p++;
327
0
  }
328
329
  /* Verify and add recipient */
330
0
  if (!uri_mailto_add_valid_recipient(parser, to, cc))
331
0
    return FALSE;
332
333
0
  return TRUE;
334
0
}
335
336
/* URI header parsing */
337
338
static bool
339
uri_mailto_header_is_duplicate(struct uri_mailto_parser *parser,
340
             const char *field_name)
341
0
{
342
0
  struct uri_mailto *uri = parser->uri;
343
344
0
  if (uri == NULL)
345
0
    return FALSE;
346
347
0
  if (uri_mailto_header_is_unique(parser, field_name)) {
348
0
    const struct uri_mailto_header_field *hdrs;
349
0
    unsigned int count, i;
350
351
0
    hdrs = array_get(&uri->headers, &count);
352
0
    for (i = 0; i < count; i++) {
353
0
      if (strcasecmp(hdrs[i].name, field_name) == 0)
354
0
        return TRUE;
355
0
    }
356
0
  }
357
0
  return FALSE;
358
0
}
359
360
static bool
361
uri_mailto_parse_headers(struct uri_mailto_parser *parser, const char **uri_p)
362
0
{
363
0
  struct uri_mailto *uri = parser->uri;
364
0
  unsigned int header_count = 0;
365
0
  string_t *field = t_str_new(128);
366
0
  const char *p = *uri_p;
367
368
0
  while (*p != '\0') {
369
0
    enum {
370
0
      _HNAME_IGNORED,
371
0
      _HNAME_GENERIC,
372
0
      _HNAME_TO,
373
0
      _HNAME_CC,
374
0
      _HNAME_SUBJECT,
375
0
      _HNAME_BODY,
376
0
    } hname_type = _HNAME_GENERIC;
377
0
    struct uri_mailto_header_field *hdrf = NULL;
378
0
    const char *field_name;
379
380
    /* Parse field name */
381
0
    while (*p != '\0' && *p != '=') {
382
0
      char ch = *p;
383
0
      p++;
384
385
0
      if (ch == '%') {
386
        /* Encoded, parse 2-digit hex value */
387
0
        if (!_parse_hex_value(&p, &ch)) {
388
0
          uri_mailto_error(parser,
389
0
               "invalid %% encoding");
390
0
          return FALSE;
391
0
        }
392
0
      } else if (ch != '=' && !_is_qchar(ch)) {
393
0
        uri_mailto_error(
394
0
          parser,
395
0
          "invalid character '%c' in header field name part",
396
0
          ch);
397
0
        return FALSE;
398
0
      }
399
400
0
      str_append_c(field, ch);
401
0
    }
402
0
    if (*p != '\0')
403
0
      p++;
404
405
    /* Verify field name */
406
0
    if (!rfc2822_header_field_name_verify(str_c(field),
407
0
                  str_len(field))) {
408
0
      uri_mailto_error(parser, "invalid header field name");
409
0
      return FALSE;
410
0
    }
411
412
0
    if (parser->max_headers > -1 &&
413
0
        (int)header_count >= parser->max_headers) {
414
      /* Refuse to accept more headers than allowed by policy
415
       */
416
0
      if ((int)header_count == parser->max_headers) {
417
0
        uri_mailto_warning(
418
0
          parser,
419
0
          "more than the maximum %u headers specified; "
420
0
          "rest is discarded",
421
0
          parser->max_headers);
422
0
      }
423
424
0
      hname_type = _HNAME_IGNORED;
425
0
    } else {
426
      /* Add new header field to array and assign its name */
427
428
0
      field_name = str_c(field);
429
0
      if (strcasecmp(field_name, "to") == 0)
430
0
        hname_type = _HNAME_TO;
431
0
      else if (strcasecmp(field_name, "cc") == 0)
432
0
        hname_type = _HNAME_CC;
433
0
      else if (strcasecmp(field_name, "subject") == 0)
434
0
        hname_type = _HNAME_SUBJECT;
435
0
      else if (strcasecmp(field_name, "body") == 0)
436
0
        hname_type = _HNAME_BODY;
437
0
      else if (!uri_mailto_header_is_reserved(parser, field_name)) {
438
0
        if (uri != NULL) {
439
0
          if (!uri_mailto_header_is_duplicate(parser, field_name)) {
440
0
            hdrf = array_append_space(&uri->headers);
441
0
            hdrf->name = p_strdup(parser->pool, field_name);
442
0
          } else {
443
0
            uri_mailto_warning(
444
0
              parser,
445
0
              "ignored duplicate for unique header field '%s'",
446
0
              str_sanitize(field_name, 32));
447
0
            hname_type = _HNAME_IGNORED;
448
0
          }
449
0
        } else {
450
0
          hname_type = _HNAME_IGNORED;
451
0
        }
452
0
      } else {
453
0
        uri_mailto_warning(
454
0
          parser,
455
0
          "ignored reserved header field '%s'",
456
0
          str_sanitize(field_name, 32));
457
0
        hname_type = _HNAME_IGNORED;
458
0
      }
459
0
    }
460
461
0
    header_count++;
462
463
    /* Reset for field body */
464
0
    str_truncate(field, 0);
465
466
    /* Parse field body */
467
0
    while (*p != '\0' && *p != '&') {
468
0
      char ch = *p;
469
0
      p++;
470
471
0
      if (ch == '%') {
472
        /* Encoded, parse 2-digit hex value */
473
0
        if (!_parse_hex_value(&p, &ch)) {
474
0
          uri_mailto_error(parser,
475
0
               "invalid %% encoding");
476
0
          return FALSE;
477
0
        }
478
0
      } else if (ch != '=' && !_is_qchar(ch)) {
479
0
        uri_mailto_error(
480
0
          parser,
481
0
          "invalid character '%c' in header field value part",
482
0
          ch);
483
0
        return FALSE;
484
0
      }
485
0
      str_append_c(field, ch);
486
0
    }
487
0
    if (*p != '\0')
488
0
      p++;
489
490
    /* Verify field body */
491
0
    if (hname_type == _HNAME_BODY) {
492
      // FIXME: verify body ...
493
0
    } else {
494
0
      if (!rfc2822_header_field_body_verify(
495
0
        str_c(field), str_len(field), FALSE, FALSE)) {
496
0
        uri_mailto_error(parser,
497
0
             "invalid header field body");
498
0
        return FALSE;
499
0
      }
500
0
    }
501
502
    /* Assign field body */
503
504
0
    switch (hname_type) {
505
0
    case _HNAME_IGNORED:
506
0
      break;
507
0
    case _HNAME_TO:
508
      /* Gracefully allow duplicate To fields */
509
0
      if (!uri_mailto_parse_header_recipients(
510
0
        parser, field, FALSE))
511
0
        return FALSE;
512
0
      break;
513
0
    case _HNAME_CC:
514
      /* Gracefully allow duplicate Cc fields */
515
0
      if (!uri_mailto_parse_header_recipients(
516
0
        parser, field, TRUE))
517
0
        return FALSE;
518
0
      break;
519
0
    case _HNAME_SUBJECT:
520
      /* Ignore duplicate subject field */
521
0
      if (uri != NULL) {
522
0
        if (uri->subject == NULL) {
523
0
          uri->subject =
524
0
            p_strdup(parser->pool, str_c(field));
525
0
        } else {
526
0
          uri_mailto_warning(
527
0
            parser,
528
0
            "ignored duplicate subject field");
529
0
        }
530
0
      }
531
0
      break;
532
0
    case _HNAME_BODY:
533
      /* Ignore duplicate body field */
534
0
      if (uri != NULL) {
535
0
        if (uri->body == NULL) {
536
0
          uri->body = p_strdup(
537
0
            parser->pool, str_c(field));
538
0
        } else {
539
0
          uri_mailto_warning(
540
0
            parser, "ignored duplicate body field");
541
0
        }
542
0
      }
543
0
      break;
544
0
    case _HNAME_GENERIC:
545
0
      if (uri != NULL && hdrf != NULL)
546
0
        hdrf->body = p_strdup(parser->pool, str_c(field));
547
0
      break;
548
0
    }
549
550
    /* Reset for next name */
551
0
    str_truncate(field, 0);
552
0
  }
553
554
  /* Skip '&' */
555
0
  if (*p != '\0')
556
0
    p++;
557
558
0
  *uri_p = p;
559
0
  return TRUE;
560
0
}
561
562
static bool
563
uri_mailto_parse_uri(struct uri_mailto_parser *parser, const char *uri_body)
564
0
{
565
0
  const char *p = uri_body;
566
567
  /*
568
   * mailtoURI   = "mailto:" [ to ] [ hfields ]
569
   * to          = [ addr-spec *("%2C" addr-spec ) ]
570
   * hfields     = "?" hfield *( "&" hfield )
571
   * hfield      = hfname "=" hfvalue
572
   * hfname      = *qchar
573
   * hfvalue     = *qchar
574
   * addr-spec   = local-part "@" domain
575
   * local-part  = dot-atom / quoted-string
576
   * qchar       = unreserved / pct-encoded / some-delims
577
   * some-delims = "!" / "$" / "'" / "(" / ")" / "*"
578
   *               / "+" / "," / ";" / ":" / "@"
579
   *
580
   * to         ~= *tqchar
581
   * tqchar     ~= <qchar> without ";" and ":"
582
   *
583
   * Scheme 'mailto:' already parsed, starting parse after colon
584
   */
585
586
  /* First extract to-part by searching for '?' and decoding % items
587
   */
588
589
0
  if (!uri_mailto_parse_recipients(parser, &p))
590
0
    return FALSE;
591
592
0
  if (*p == '\0')
593
0
    return TRUE;
594
0
  i_assert(*p == '?');
595
0
  p++;
596
597
  /* Extract hfield items */
598
599
0
  while (*p != '\0') {
600
    /* Extract hfield item by searching for '&' and decoding '%'
601
       items */
602
0
    if (!uri_mailto_parse_headers(parser, &p))
603
0
      return FALSE;
604
0
  }
605
0
  return TRUE;
606
0
}
607
608
/*
609
 * Validation
610
 */
611
612
bool uri_mailto_validate(const char *uri_body,
613
       const char **reserved_headers,
614
       const char **unique_headers, int max_recipients,
615
       int max_headers, const struct uri_mailto_log *log)
616
0
{
617
0
  struct uri_mailto_parser parser;
618
619
0
  i_zero(&parser);
620
0
  parser.log = log;
621
0
  parser.max_recipients = max_recipients;
622
0
  parser.max_headers = max_headers;
623
0
  parser.reserved_headers = reserved_headers;
624
0
  parser.unique_headers = unique_headers;
625
626
  /* If no errors are reported, we don't need to record any data */
627
0
  if (log != NULL) {
628
0
    parser.pool = pool_datastack_create();
629
630
0
    parser.uri = p_new(parser.pool, struct uri_mailto, 1);
631
0
    p_array_init(&parser.uri->recipients, parser.pool, max_recipients);
632
0
    p_array_init(&parser.uri->headers, parser.pool, max_headers);
633
0
  }
634
635
0
  if (!uri_mailto_parse_uri(&parser, uri_body))
636
0
    return FALSE;
637
638
0
  if (log != NULL) {
639
0
    if (array_count(&parser.uri->recipients) == 0) {
640
0
      uri_mailto_warning(
641
0
        &parser,
642
0
        "notification URI specifies no recipients");
643
0
    }
644
0
  }
645
0
  return TRUE;
646
0
}
647
648
/*
649
 * Parsing
650
 */
651
652
struct uri_mailto *
653
uri_mailto_parse(const char *uri_body, pool_t pool,
654
     const char **reserved_headers, const char **unique_headers,
655
     int max_recipients, int max_headers,
656
     const struct uri_mailto_log *log)
657
0
{
658
0
  struct uri_mailto_parser parser;
659
660
0
  parser.pool = pool;
661
0
  parser.log = log;
662
0
  parser.max_recipients = max_recipients;
663
0
  parser.max_headers = max_headers;
664
0
  parser.reserved_headers = reserved_headers;
665
0
  parser.unique_headers = unique_headers;
666
667
0
  parser.uri = p_new(pool, struct uri_mailto, 1);
668
0
  p_array_init(&parser.uri->recipients, pool, max_recipients);
669
0
  p_array_init(&parser.uri->headers, pool, max_headers);
670
671
0
  if (!uri_mailto_parse_uri(&parser, uri_body))
672
0
    return NULL;
673
674
0
  if (log != NULL) {
675
0
    if (array_count(&parser.uri->recipients) == 0) {
676
0
      uri_mailto_warning(
677
0
        &parser,
678
0
        "notification URI specifies no recipients");
679
0
    }
680
0
  }
681
0
  return parser.uri;
682
0
}