Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (warning)
85.71%
42 / 49
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProcessBounceWithRegex
85.71% covered (warning)
85.71%
42 / 49
50.00% covered (danger)
50.00%
2 / 4
27.97
0.00% covered (danger)
0.00%
0 / 1
 handleBounce
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 parseMessagePart
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 parseDeliveryStatusMessage
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 extractHeaders
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
14.08
1<?php
2
3namespace MediaWiki\Extension\BounceHandler;
4
5/**
6 * Class ProcessBounceWithRegex
7 *
8 * Extract email headers of a bounce email using various regex functions
9 *
10 * @file
11 * @ingroup Extensions
12 * @author Tony Thomas, Kunal Mehta, Jeff Green
13 * @license GPL-2.0-or-later
14 */
15class ProcessBounceWithRegex extends ProcessBounceEmails {
16    /**
17     * Process email using common regex functions
18     *
19     * @param string $email
20     */
21    public function handleBounce( $email ) {
22        $emailHeaders = $this->extractHeaders( $email );
23        $to = $emailHeaders['to'];
24
25        $processEmail = $this->processEmail( $emailHeaders, $email );
26        if ( !$processEmail ) {
27            $this->handleUnrecognizedBounces( $email, $to );
28        }
29    }
30
31    /**
32     * Parse the single part of delivery status message
33     *
34     * @param string[] $partLines array of strings that contain single lines of the email part
35     * @return string|null String that contains the status code or null if it wasn't found
36     */
37    private function parseMessagePart( $partLines ) {
38        foreach ( $partLines as $partLine ) {
39            if ( preg_match( '/^Content-Type: (.+)/', $partLine, $contentTypeMatch ) ) {
40                if ( $contentTypeMatch[1] != 'message/delivery-status' ) {
41                    break;
42                }
43            }
44            if ( preg_match( '/^Status: (\d\.\d{1,3}\.\d{1,3})/', $partLine, $statusMatch ) ) {
45                return $statusMatch[1];
46            }
47        }
48        return null;
49    }
50
51    /**
52     * Parse the multi-part delivery status message (DSN) according to RFC3464
53     *
54     * @param string[] $emailLines array of strings that contain single lines of the email
55     * @return string|null String that contains the status code or null if it wasn't found
56     */
57    private function parseDeliveryStatusMessage( $emailLines ) {
58        for ( $i = 0; $i < count( $emailLines ) - 1; ++$i ) {
59            $line = $emailLines[$i] . "\n" . $emailLines[$i + 1];
60            if ( preg_match( '/Content-Type: multipart\/report;\s*report-type=delivery-status;' .
61                '\s*boundary="(.+?)"/', $line, $contentTypeMatch ) ) {
62                $partIndices = array_keys( $emailLines, "--$contentTypeMatch[1]" );
63                foreach ( $partIndices as $index ) {
64                    $result = $this->parseMessagePart( array_slice( $emailLines, $index ) );
65                    if ( $result !== null ) {
66                        return $result;
67                    }
68                }
69            }
70        }
71        return null;
72    }
73
74    /**
75     * Extract headers from the received bounce
76     *
77     * @param string $email
78     * @return array $emailHeaders
79     */
80    public function extractHeaders( $email ) {
81        $emailHeaders = [];
82        $emailLines = preg_split( "/(\r?\n|\r)/", $email );
83        foreach ( $emailLines as $emailLine ) {
84            if ( preg_match( "/^To: (.*)/", $emailLine, $toMatch ) ) {
85                $emailHeaders['to'] = $toMatch[1];
86            }
87            if ( preg_match( "/^Subject: (.*)/", $emailLine, $subjectMatch ) ) {
88                $emailHeaders['subject'] = $subjectMatch[1];
89            }
90            if ( preg_match( "/^Date: (.*)/", $emailLine, $dateMatch ) ) {
91                $emailHeaders['date'] = $dateMatch[1];
92            }
93            if ( preg_match( "/^X-Failed-Recipients: (.*)/", $emailLine, $failureMatch ) ) {
94                $emailHeaders['x-failed-recipients'] = $failureMatch[1];
95            }
96            if ( trim( $emailLine ) == "" ) {
97                // Empty line denotes that the header part is finished
98                break;
99            }
100        }
101        $status = $this->parseDeliveryStatusMessage( $emailLines );
102        if ( $status !== null ) {
103            $emailHeaders['status'] = $status;
104        }
105
106        // If the x-failed-recipient header or status code was not found, we should fallback to
107        // a heuristic scan of the message for a SMTP status code
108        if ( !isset( $emailHeaders['status'] ) && !isset( $emailHeaders['x-failed-recipients'] ) ) {
109            foreach ( $emailLines as $emailLine ) {
110                if ( preg_match( '/\s+(?:(?P<smtp>[1-5]\d{2}).)?' .
111                    '(?P<status>[245]\.\d{1,3}\.\d{1,3})?\b/', $emailLine, $statusMatch ) ) {
112                    if ( isset( $statusMatch['smtp'] ) ) {
113                        $emailHeaders['smtp-code'] = $statusMatch['smtp'];
114                        break;
115                    }
116                    if ( isset( $statusMatch['status'] ) ) {
117                        $emailHeaders['status'] = $statusMatch['status'];
118                        break;
119                    }
120                }
121            }
122        }
123        return $emailHeaders;
124    }
125
126}