Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.71% |
42 / 49 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
ProcessBounceWithRegex | |
85.71% |
42 / 49 |
|
50.00% |
2 / 4 |
27.97 | |
0.00% |
0 / 1 |
handleBounce | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
parseMessagePart | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
parseDeliveryStatusMessage | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
extractHeaders | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
14.08 |
1 | <?php |
2 | |
3 | namespace 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 | */ |
15 | class 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 | } |