• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cypht-org / cypht / 27669028565

17 Jun 2026 05:58AM UTC coverage: 72.974% (+0.2%) from 72.777%
27669028565

push

travis-ci

web-flow
Merge pull request #2008 from IrAlfred/fix-email-css-remote-resource-leak

fix(other): block CSS-based email tracking and restore external resource privacy UI

35 of 41 new or added lines in 1 file covered. (85.37%)

4836 of 6627 relevant lines covered (72.97%)

7.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.47
/modules/core/message_functions.php
1
<?php
2

3
/**
4
 * Core modules
5
 * @package modules
6
 * @subpackage core
7
 */
8

9
/**
10
 * Format a message body that has HMTL markup
11
 * @subpackage core/functions
12
 * @param string $str message HTML
13
 * @param bool $images allow external images
14
 * @return string
15
 */
16
if (!hm_exists('format_msg_html')) {
17
function format_msg_html($str, $images=false) {
18
    $str = mb_eregi_replace('</body>', '', $str);
3✔
19

20
    $config = HTMLPurifier_Config::createDefault();
3✔
21
    $config->set('HTML.DefinitionID', 'hm-message');
3✔
22
    $config->set('HTML.DefinitionRev', 1);
3✔
23
    $config->set('Cache.DefinitionImpl', null);
3✔
24
    $config->set('HTML.TargetBlank', true);
3✔
25
    $config->set('HTML.TargetNoopener', true);
3✔
26
    $config->set('HTML.ForbiddenElements', ['html', 'head']);
3✔
27
    $config->set('CSS.AllowTricky', true);
3✔
28
    // Always block external CSS/resources; <img> loading is gated separately via data-src.
29
    $config->set('URI.DisableExternalResources', true);
3✔
30
    $config->set('URI.AllowedSchemes', array('mailto' => true, 'data' => true, 'http' => true, 'https' => true));
3✔
31
    $config->set('Filter.ExtractStyleBlocks.TidyImpl', true);
3✔
32

33
    if ($def = $config->maybeGetRawHTMLDefinition()) {
3✔
34
        $html_tags = ['img', 'script', 'iframe', 'audio', 'embed', 'source', 'track', 'video', 'a'];
3✔
35
        foreach ($html_tags as $tag) {
3✔
36
            $def->addAttribute($tag, 'data-src', 'Text');
3✔
37
        }
38
        $def->addAttribute('div', 'data-external-resources-blocked', 'Text');
3✔
39
    }
40

41
    try {
42
        $purifier = new HTMLPurifier($config);
3✔
43
        return $purifier->purify($str);
3✔
44
    } catch (Exception $e) {
×
45
        return '';
×
46
    }
47
}}
48

49
/**
50
 * Sanitize HTML for email
51
 * @subpackage core/functions
52
 * @param string $html content to sanitize
53
 * @param int|null $blocked_count running count of stripped remote resources
54
 * @return string
55
 */
56
if (!hm_exists('count_blocked_remote_email_resources')) {
57
function count_blocked_remote_email_resources($html) {
NEW
58
    $count = 0;
×
NEW
59
    $count += preg_match_all('/src="(https?:\/\/[^"]*)"|src=\'(https?:\/\/[^\']*)\'/i', $html, $m);
×
NEW
60
    $count += count_blocked_remote_email_css_resources($html);
×
NEW
61
    return $count;
×
62
}}
63

64
if (!hm_exists('count_blocked_remote_email_css_resources')) {
65
function count_blocked_remote_email_css_resources($html) {
66
    $count = 0;
1✔
67
    $count += preg_match_all(
1✔
68
        '/\s(?:background|dynsrc|lowsrc)\s*=\s*(["\']?)(?:https?:)?\/\/[^"\'>\s]+\1?/i',
1✔
69
        $html,
1✔
70
        $m
1✔
71
    );
1✔
72
    $count += preg_match_all(
1✔
73
        '/url\(\s*["\']?(?:https?:\/\/|\/\/)[^)"\']*["\']?\s*\)/i',
1✔
74
        $html,
1✔
75
        $m
1✔
76
    );
1✔
77
    return $count;
1✔
78
}}
79

80
if (!hm_exists('sanitize_email_html')) {
81
function sanitize_email_html($html, &$blocked_count = null) {
82
    $blocked = 0;
1✔
83

84
    $html = preg_replace(
1✔
85
        '/\s(?:background|dynsrc|lowsrc)\s*=\s*(["\']?)(?:https?:)?\/\/[^"\'>\s]+\1?/i',
1✔
86
        '',
1✔
87
        $html,
1✔
88
        -1,
1✔
89
        $attr_count
1✔
90
    );
1✔
91
    $blocked += $attr_count;
1✔
92

93
    $html = preg_replace_callback(
1✔
94
        '/<([^>]+)\s*style\s*=\s*(["\'])(.*?)\2/i',
1✔
95
        function($matches) use (&$blocked) {
1✔
96
            $content = preg_replace_callback(
1✔
97
                '/url\(\s*["\']?(?:https?:\/\/|\/\/)[^)"\']*["\']?\s*\)/i',
1✔
98
                function($url_match) use (&$blocked) {
1✔
99
                    $blocked++;
1✔
100
                    return '';
1✔
101
                },
1✔
102
                $matches[3]
1✔
103
            );
1✔
104
            $content = preg_replace('/\s*;\s*;/', ';', $content);
1✔
105
            $content = trim($content, " ;");
1✔
106
            if ($content === '') {
1✔
NEW
107
                return '<' . $matches[1];
×
108
            }
109
            return '<' . $matches[1] . ' style=' . $matches[2] . $content . $matches[2];
1✔
110
        },
1✔
111
        $html
1✔
112
    );
1✔
113

114
    if ($blocked_count !== null) {
1✔
NEW
115
        $blocked_count = $blocked;
×
116
    }
117

118
    return $html;
1✔
119
}}
120

121
/**
122
 * Convert HTML to plain text
123
 * @param string $html content to convert
124
 * @return string
125
 */
126
if (!hm_exists('convert_html_to_text')) {
127
function convert_html_to_text($html) {
128
    $html = new HTMLToText($html);
2✔
129
    return $html->text;
2✔
130
}}
131

132
/**
133
 * Format image data
134
 * @subpackage core/functions
135
 * @param string $str binary image data
136
 * @param string $mime_type type of image
137
 * return string
138
 */
139
if (!hm_exists('format_msg_image')) {
140
function format_msg_image($str, $mime_type) {
141
    return '<img class="msg_img" alt="" src="data:image/'.$mime_type.';base64,'.chunk_split(base64_encode($str)).'" />';
1✔
142
}}
143

144
/**
145
 * Format a plain text message
146
 * @subpackage core/functions
147
 * @param string $str message text
148
 * @param object $output_mod Hm_Output_Module
149
 */
150
if (!hm_exists('format_msg_text')) {
151
function format_msg_text($str, $output_mod, $links=true) {
152
    $str = str_replace("\t", '    ', $str);
1✔
153
    $str = nl2br(str_replace(' ', '<wbr>', ($output_mod->html_safe($str)))).'<br />';
1✔
154
    $str = preg_replace("/(&(?!amp)[^;]+;)/", " $1", $str);
1✔
155
    if ($links) {
1✔
156
        $link_regex = "/((http|ftp|rtsp)s?:\/\/(%[[:digit:]A-Fa-f][[:digit:]A-Fa-f]|[-_\.!~\*';\/\?#:@&=\+$,%[:alnum:]])+)/m";
1✔
157
        $str = preg_replace($link_regex, "<a href=\"$1\" target=\"_blank\" rel=\"noopener\">$1</a>", $str);
1✔
158
    }
159
    $str = preg_replace("/ (&[^;]+;)/", "$1", $str);
1✔
160
    $str = str_replace('<wbr>', '&#160;<wbr>', $str);
1✔
161
    return preg_replace("/^(&gt;.*<br \/>)/m", "<span class=\"reply_quote\">$1</span>", $str);
1✔
162
}}
163

164
/**
165
 * Format reply text
166
 * @subpackage core/functions
167
 * @param string $txt message text
168
 * @return string
169
 */
170
if (!hm_exists('format_reply_text')) {
171
function format_reply_text($txt) {
172
    $lines = explode("\n", $txt);
5✔
173
    $new_lines = array();
5✔
174
    foreach ($lines as $line) {
5✔
175
        $pre = '> ';
5✔
176
        if (preg_match("/^(>\s*)+/", $line, $matches)) {
5✔
177
            $pre .= $matches[1];
1✔
178
        }
179
        $wrap = 75 + mb_strlen($pre);
5✔
180
        $new_lines[] = preg_replace("/$pre /", "$pre", "> ".wordwrap($line, $wrap, "\n$pre"));
5✔
181
    }
182
    return implode("\n", $new_lines);
5✔
183
}}
184

185
/**
186
 * Get reply to address
187
 * @subpackage core/functions
188
 * @param array $headers message headers
189
 * @param string $type type (forward, reply, reply_all)
190
 * @return string
191
 */
192
if (!hm_exists('reply_to_address')) {
193
function reply_to_address($headers, $type) {
194
    $msg_to = '';
2✔
195
    $msg_cc = '';
2✔
196
    $headers = lc_headers($headers);
2✔
197
    $parsed = array();
2✔
198

199
    if ($type == 'forward') {
2✔
200
        return $msg_to;
1✔
201
    }
202
    foreach (array('reply-to', 'from', 'sender', 'return-path') as $fld) {
2✔
203
        if (array_key_exists($fld, $headers)) {
2✔
204
            list($parsed, $msg_to) = format_reply_address($headers[$fld], $parsed);
2✔
205
            if ($msg_to) {
2✔
206
                break;
2✔
207
            }
208
        }
209
    }
210
    if ($type == 'reply_all') {
2✔
211
        if (array_key_exists('cc', $headers)) {
1✔
212
            list($cc_parsed, $msg_cc) = format_reply_address($headers['cc'], $parsed);
1✔
213
            $parsed += $cc_parsed;
1✔
214
        }
215
        if (array_key_exists('to', $headers)) {
1✔
216
            list($parsed, $recips) = format_reply_address($headers['to'], $parsed);
1✔
217
            if ($recips) {
1✔
218
                if ($msg_cc) {
1✔
219
                    $msg_cc .= ', '.$recips;
1✔
220
                }
221
                else {
222
                    $msg_cc = $recips;
1✔
223
                }
224
            }
225
        }
226
    }
227
    return array($msg_to, $msg_cc);
2✔
228
}}
229

230
/*
231
 * Format a reply address line
232
 * @param string $fld the field values from the E-mail being replied to
233
 * @param array $excluded list of parsed addresses to exclude
234
 * @return string
235
 */
236
if (!hm_exists('format_reply_address')) {
237
function format_reply_address($fld, $excluded) {
238
    $addr = process_address_fld(trim($fld));
2✔
239
    $res = array();
2✔
240
    foreach ($addr as $v) {
2✔
241
        $skip = false;
2✔
242
        foreach ($excluded as $ex) {
2✔
243
            if (mb_strtolower($v['email']) == mb_strtolower($ex['email'])) {
1✔
244
                $skip = true;
1✔
245
                break;
1✔
246
            }
247
        }
248
        if (!$skip) {
2✔
249
            $res[] = $v;
2✔
250
        }
251
    }
252
    if ($res) {
2✔
253
        return array($addr, implode(', ', array_map(function($v) {
2✔
254
            if (trim($v['label'])) {
2✔
255
                return str_replace([',', ';'], '', $v['label']).' '.$v['email'];
1✔
256
            }
257
            else {
258
                return $v['email'];
2✔
259
            }
260
        }, $res)));
2✔
261
    }
262
    return array($addr, '');
1✔
263
}}
264

265
/**
266
 * Get reply to subject
267
 * @subpackage core/functions
268
 * @param array $headers message headers
269
 * @param string $type type (forward, reply, reply_all)
270
 * @return string
271
 */
272
if (!hm_exists('reply_to_subject')) {
273
function reply_to_subject($headers, $type) {
274
    $subject = '';
2✔
275
    if (array_key_exists('Subject', $headers)) {
2✔
276
        if ($type == 'reply' || $type == 'reply_all') {
1✔
277
            if (!preg_match("/^re:/i", trim($headers['Subject']))) {
1✔
278
                $subject = sprintf("Re: %s", $headers['Subject']);
1✔
279
            }
280
        }
281
        elseif ($type == 'forward') {
1✔
282
            if (!preg_match("/^fwd:/i", trim($headers['Subject']))) {
1✔
283
                $subject = sprintf("Fwd: %s", $headers['Subject']);
1✔
284
            }
285
        }
286
        if (!$subject) {
1✔
287
            $subject = $headers['Subject'];
1✔
288
        }
289
    }
290
    return $subject;
2✔
291
}}
292

293
/**
294
 * Get reply message lead in
295
 * @subpackage core/functions
296
 * @param array $headers message headers
297
 * @param string $type type (forward, reply, reply_all)
298
 * @param string $to reply to value
299
 * @param object $output_mod output module object
300
 * @return string
301
 */
302
if (!hm_exists('reply_lead_in')) {
303
function reply_lead_in($headers, $type, $to, $output_mod) {
304
    $lead_in = '';
2✔
305
    if ($type == 'reply' || $type == 'reply_all') {
2✔
306
        if (array_key_exists('Date', $headers)) {
2✔
307
            if ($to) {
1✔
308
                $lead_in = sprintf($output_mod->trans('On %s %s said')."\n\n", $headers['Date'], $to);
1✔
309
            }
310
            else {
311
                $lead_in = sprintf($output_mod->trans('On %s, somebody said')."\n\n", $headers['Date']);
2✔
312
            }
313
        }
314
    }
315
    elseif ($type == 'forward') {
1✔
316
        $flds = array();
1✔
317
        foreach( array('From', 'Date', 'Subject', 'To', 'Cc') as $fld) {
1✔
318
            if (array_key_exists($fld, $headers)) {
1✔
319
                $flds[$fld] = $headers[$fld];
1✔
320
            }
321
        }
322
        $lead_in = "\n\n----- ".$output_mod->trans('begin forwarded message')." -----\n\n";
1✔
323
        foreach ($flds as $fld => $val) {
1✔
324
            $lead_in .= $fld.': '.$val."\n";
1✔
325
        }
326
        $lead_in .= "\n";
1✔
327
    }
328
    return $lead_in;
2✔
329
}}
330

331
/**
332
 * Format reply field details
333
 * @subpackage core/functions
334
 * @param array $headers message headers
335
 * @param string $body message body
336
 * @param string $lead_in body lead in text
337
 * @param string $reply_type type (forward, reply, reply_all)
338
 * @param array $struct message structure details
339
 * @param int $html set to 1 if the output should be HTML
340
 * @return array
341
 */
342
if (!hm_exists('reply_format_body')) {
343
function reply_format_body($headers, $body, $lead_in, $reply_type, $struct, $html) {
344
    $msg = '';
2✔
345
    $type = 'textplain';
2✔
346
    if (array_key_exists('type', $struct) && array_key_exists('subtype', $struct)) {
2✔
347
        $type = mb_strtolower($struct['type']).mb_strtolower($struct['subtype']);
2✔
348
    }
349
    if ($html == 1) {
2✔
350
        $msg = format_reply_as_html($body, $type, $reply_type, $lead_in);
1✔
351
    }
352
    else {
353
        $msg = format_reply_as_text($body, $type, $reply_type, $lead_in);
2✔
354
    }
355
    return $msg;
2✔
356
}}
357

358
/**
359
 * Format reply text as HTML
360
 * @subpackage core/functions
361
 * @param string $body message body
362
 * @param string $type MIME type
363
 * @param string $reply_type type (forward, reply, reply_all)
364
 * @param string $lead_in body lead in text
365
 * @return string
366
 */
367
if (!hm_exists('format_reply_as_html')) {
368
function format_reply_as_html($body, $type, $reply_type, $lead_in) {
369
    if ($type == 'textplain') {
2✔
370
        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
2✔
371
            $msg = nl2br($lead_in.format_reply_text($body));
2✔
372
        }
373
        elseif ($reply_type == 'forward') {
1✔
374
            $msg = nl2br($lead_in.$body);
2✔
375
        }
376
    }
377
    elseif ($type == 'texthtml') {
1✔
378
        $msg = nl2br($lead_in).'<hr /><blockquote>'.format_msg_html($body).'</blockquote>';
1✔
379
    }
380
    return $msg;
2✔
381
}}
382

383
/**
384
 * Format reply text as text
385
 * @subpackage core/functions
386
 * @param string $body message body
387
 * @param string $type MIME type
388
 * @param string $reply_type type (forward, reply, reply_all)
389
 * @param string $lead_in body lead in text
390
 * @return string
391
 */
392
if (!hm_exists('format_reply_as_text')) {
393
function format_reply_as_text($body, $type, $reply_type, $lead_in) {
394
    $msg = '';
3✔
395
    if ($type == 'texthtml') {
3✔
396
        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
1✔
397
            $msg = $lead_in.format_reply_text(convert_html_to_text($body));
1✔
398
        }
399
        elseif ($reply_type == 'forward') {
1✔
400
            $msg = $lead_in.convert_html_to_text($body);
1✔
401
        }
402
    }
403
    elseif ($type == 'textplain') {
3✔
404
        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
3✔
405
            $msg = $lead_in.format_reply_text($body);
3✔
406
        }
407
        else {
408
            $msg = $lead_in.$body;
1✔
409
        }
410
    }
411
    return $msg;
3✔
412
}}
413

414
/**
415
 * Convert header keys to lowercase versions
416
 * @param array $headers message headers
417
 * @return array
418
 */
419
if (!hm_exists('lc_headers')) {
420
function lc_headers($headers) {
421
    return array_change_key_case($headers, CASE_LOWER);
3✔
422
}}
423

424
/**
425
 * Get the in-reply-to message id for replied
426
 * @subpackage core/functions
427
 * @param array $headers message headers
428
 * @param string $type reply type
429
 * @return string
430
 */
431
if (!hm_exists('reply_to_id')) {
432
function reply_to_id($headers, $type) {
433
    $id = '';
2✔
434
    $headers = lc_headers($headers);
2✔
435
    if ($type != 'forward' && array_key_exists('message-id', $headers)) {
2✔
436
        $id = $headers['message-id'];
1✔
437
    }
438
    return $id;
2✔
439
}}
440

441
/**
442
 * Get reply field details
443
 * @subpackage core/functions
444
 * @param string $body message body
445
 * @param array $headers message headers
446
 * @param array $struct message structure details
447
 * @param int $html set to 1 if the output should be HTML
448
 * @param string $type optional type (forward, reply, reply_all)
449
 * @param object $output_mod output module object
450
 * @param string $type the reply type
451
 * @return array
452
 */
453
if (!hm_exists('format_reply_fields')) {
454
function format_reply_fields($body, $headers, $struct, $html, $output_mod, $type='reply') {
455
    $msg_to = '';
1✔
456
    $msg = '';
1✔
457
    $subject = reply_to_subject($headers, $type);
1✔
458
    $msg_id = reply_to_id($headers, $type);
1✔
459
    list($msg_to, $msg_cc) = reply_to_address($headers, $type);
1✔
460
    $lead_in = reply_lead_in($headers, $type, $msg_to, $output_mod);
1✔
461
    $msg = reply_format_body($headers, $body, $lead_in, $type, $struct, $html);
1✔
462
    return array($msg_to, $msg_cc, $subject, $msg, $msg_id);
1✔
463
}}
464

465
/**
466
 * decode mail fields to human readable text
467
 * @param string $string field to decode
468
 * @return string decoded field
469
 */
470
if (!hm_exists('decode_fld')) {
471
function decode_fld($string) {
472
    if (mb_strpos($string, '=?') === false) {
1✔
473
        return $string;
1✔
474
    }
475
    $string = preg_replace("/\?=\s+=\?/", '?==?', $string);
1✔
476
    if (preg_match_all("/(=\?[^\?]+\?(q|b)\?[^\?]+\?=)/i", $string, $matches)) {
1✔
477
        foreach ($matches[1] as $v) {
1✔
478
            $fld = mb_substr($v, 2, -2);
1✔
479
            $charset = mb_strtolower(mb_substr($fld, 0, mb_strpos($fld, '?')));
1✔
480
            $fld = mb_substr($fld, (mb_strlen($charset) + 1));
1✔
481
            $encoding = $fld[0];
1✔
482
            $fld = mb_substr($fld, (mb_strpos($fld, '?') + 1));
1✔
483
            if (mb_strtoupper($encoding) == 'B') {
1✔
484
                $fld = convert_to_utf8(base64_decode($fld), $charset);
1✔
485
            }
486
            elseif (mb_strtoupper($encoding) == 'Q') {
1✔
487
                $fld = convert_to_utf8(quoted_printable_decode(str_replace('_', ' ', $fld)), $charset);
1✔
488
            }
489
            $string = str_replace($v, $fld, $string);
1✔
490
        }
491
    }
492
    return trim($string);
1✔
493
}}
494

495
if (!hm_exists('convert_to_utf8')) {
496
function convert_to_utf8($data, $from_encoding) {
497
    try {
498
        $data = mb_convert_encoding($data, 'UTF-8', $from_encoding);
1✔
499
    } catch (ValueError $e) {
×
500
        $data = iconv($from_encoding, 'UTF-8', $data);
×
501
    }
502
    return $data;
1✔
503
}}
504

505
/**
506
 * @subpackage core/class
507
 */
508
class HTMLToText {
509

510
    public $text = '';
511
    private $current = false;
512
    private $blocks = array('table', 'li', 'div', 'h1', 'h2', 'br', 'h3', 'h4', 'h5', 'p', 'tr');
513
    private $skips = array('head', 'script', 'style');
514

515
    function __construct($html) {
2✔
516
        $doc = new DOMDocument();
2✔
517
        libxml_use_internal_errors(true);
2✔
518

519
        // Check if already valid UTF-8, if not convert it
520
        if (!mb_check_encoding($html, 'UTF-8')) {
2✔
521
            // Try to detect and convert the encoding
522
            $html = mb_convert_encoding($html, 'UTF-8', mb_detect_encoding($html, mb_list_encodings(), true));
×
523
        }
524
        $doc->loadHTML('<?xml encoding="UTF-8">' . $html);
2✔
525
        libxml_clear_errors();
2✔
526

527
        if (trim($html) && $doc->hasChildNodes()) {
2✔
528
            $this->parse_nodes($doc->childNodes);
2✔
529
        }
530
        $this->text = trim(strip_tags(html_entity_decode(preg_replace("/\n{2,}/m", "\n\n", $this->text), ENT_QUOTES, "UTF-8")));
2✔
531
    }
532

533
    function block($tag) {
2✔
534
        in_array($tag, $this->blocks) && $this->current != $tag ? $this->text .= "\n" : false;
2✔
535
        $this->current = $tag;
2✔
536
    }
537

538
    function parse_nodes($nodes) {
2✔
539
        $trims = " \t\n\r\0\x0B";
2✔
540
        foreach ($nodes as $node) {
2✔
541
            if (!in_array($node->nodeName, $this->skips)) {
2✔
542
                $this->block($node->nodeName);
2✔
543
                if ($node->nodeName == '#text' && trim($node->textContent, $trims)) {
2✔
544
                    $this->text .= trim($node->textContent, $trims)." ";
2✔
545
                }
546
                $node->hasChildNodes() ? $this->parse_nodes($node->childNodes) : false;
2✔
547
            }
548
        }
549
    }
550
}
551

552
/**
553
 * trim a potential E-mail value
554
 * @param $val string E-mail value
555
 * @return string trimmed value
556
 */
557
if (!hm_exists('addr_split')) {
558
function trim_email($val) {
559
    $seps = array(',', ';');
3✔
560
    $misc = array('"', "'", '>', '<');
3✔
561
    return trim($val, implode(array_merge($misc, $seps)));
3✔
562
}}
563

564
/**
565
 * Split an address field
566
 * @param $str string field value
567
 * @param $seps array break chars
568
 * @return array results
569
 */
570
if (!hm_exists('addr_split')) {
571
function addr_split($str, $seps = array(',', ';')) {
572
    $str = preg_replace('/(\s){2,}/', ' ', $str);
4✔
573
    $max = mb_strlen($str);
4✔
574
    $word = '';
4✔
575
    $words = array();
4✔
576
    $capture = false;
4✔
577
    $capture_chars = array('"' => '"', '(' => ')', '<' => '>');
4✔
578
    for ($i=0;$i<$max;$i++) {
4✔
579
        $char = mb_substr($str, $i, 1);
4✔
580
        if ($capture && $capture_chars[$capture] == $char) {
4✔
581
            $capture = false;
2✔
582
        }
583
        elseif (!$capture && in_array($char, array_keys($capture_chars))) {
4✔
584
            $capture = $char;
2✔
585
        }
586

587
        if (!$capture && in_array($char, $seps)) {
4✔
588
            $words[] = trim($word);
2✔
589
            $word = '';
2✔
590
        }
591
        else {
592
            $word .= $char;
4✔
593
        }
594
    }
595
    $words[] = trim($word);
4✔
596
    return $words;
4✔
597
}}
598

599
/**
600
 * Parse an address field
601
 * @param $str string field value
602
 * @return array results
603
 */
604
if (!hm_exists('addr_parse')) {
605
function addr_parse($str) {
606
    $label = array();
3✔
607
    $email = '';
3✔
608
    $comment = array();
3✔
609
    foreach (addr_split($str, array(' ')) as $token) {
3✔
610
        if (is_email_address(trim_email($token))) {
3✔
611
            $email = trim_email($token);
3✔
612
        }
613
        else {
614
            $label[] = $token;
2✔
615
        }
616
    }
617
    $label = implode(' ', $label);
3✔
618
    if (preg_match('/\([^)]+\)/', $label, $matches)) {
3✔
619
        foreach ($matches as $match) {
1✔
620
            $comment[] = $match;
1✔
621
            $label = str_replace($match, '', $label);
1✔
622
        }
623
        $comment = implode(',', $comment);
1✔
624
    }
625
    else {
626
        $comment = '';
3✔
627
    }
628
    return array('email' => $email, 'label' => preg_replace('/[\pZ\pC]+/u', ' ', trim($label, ' \'"')), 'comment' => $comment);
3✔
629
}}
630

631
/**
632
 * Parse an address field
633
 * @param $fld string field value
634
 * @return array results
635
 */
636
if (!hm_exists('process_address_fld')) {
637
function process_address_fld($fld) {
638
    $res = array();
3✔
639
    $count = 0;
3✔
640
    $pre = false;
3✔
641
    foreach (addr_split($fld) as $str) {
3✔
642
        $addr = addr_parse($str);
3✔
643
        if ($addr['email']) {
3✔
644
            if ($pre) {
3✔
645
                $addr['label'] = $pre.' '.$addr['label'];
×
646
                $pre = false;
×
647
            }
648
            $res[$count] = $addr;
3✔
649
        }
650
        elseif ($addr['label']) {
1✔
651
            $pre = $addr['label'];
1✔
652
        }
653
        $count++;
3✔
654
    }
655
    return $res;
3✔
656
}}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc