Completed
Push — master ( da1076...014d21 )
by Alex
02:01
created

Client.php (12 issues)

1
<?php
2
namespace Mezon\Pop3;
3
4
/**
5
 * Class Client
6
 *
7
 * @package Mezon
8
 * @subpackage Pop3Client
9
 * @author Dodonov A.A.
10
 * @version v.1.0 (2019/08/13)
11
 * @copyright Copyright (c) 2019, aeon.org
12
 */
13
14
/**
15
 * POP3 protocol client.
16
 */
17
class Client
18
{
19
20
    /**
21
     * Connection
22
     */
23
    private $connection = false;
24
25
    /**
26
     * Method connects to server
27
     *
28
     * @param string $server
29
     *            Server domain
30
     * @param string $login
31
     *            Login
32
     * @param string $password
33
     *            Password
34
     * @param int $timeOut
35
     *            Timeout
36
     * @param int $port
37
     *            Port number
38
     */
39
    public function connect(string $server, string $login, string $password, int $timeOut = 5, int $port = 110)
40
    {
41
        try {
42
            $errorCode = $errorMessage = '';
43
44
            $context = stream_context_create([
45
                'ssl' => [
46
                    'verify_peer' => false
47
                ]
48
            ]);
49
50
            $this->connection = stream_socket_client(
51
                $server . ":$port",
52
                $errorCode,
0 ignored issues
show
$errorCode of type string is incompatible with the type integer expected by parameter $errno of stream_socket_client(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

52
                /** @scrutinizer ignore-type */ $errorCode,
Loading history...
53
                $errorMessage,
54
                $timeOut,
55
                STREAM_CLIENT_CONNECT,
56
                $context);
57
58
            if ($this->connection === false) {
59
                throw (new \Exception('Connection was not established', - 1));
60
            }
61
62
            $result = fgets($this->connection, 1024);
63
64
            if (substr($result, 0, 3) !== '+OK') {
65
                throw (new \Exception('Connection. ' . $result, 0));
66
            }
67
68
            fputs($this->connection, "USER $login\r\n");
69
70
            $result = fgets($this->connection, 1024);
71
72
            if (substr($result, 0, 3) !== '+OK') {
73
                throw (new \Exception("USER $login " . $result, 0));
74
            }
75
76
            fputs($this->connection, "PASS $password\r\n");
77
78
            $result = fgets($this->connection, 1024);
79
80
            if (substr($result, 0, 3) !== '+OK') {
81
                throw (new \Exception("PASS " . $result . $login, 0));
82
            }
83
        } catch (\Exception $e) {
84
            throw ($e);
85
        }
86
    }
87
88
    /**
89
     * Constructor
90
     *
91
     * @param string $server
92
     *            Server domain
93
     * @param string $login
94
     *            Login
95
     * @param string $password
96
     *            Password
97
     * @param int $timeOut
98
     *            Timeout
99
     * @param int $port
100
     *            Port number
101
     */
102
    public function __construct(string $server, string $login, string $password, int $timeOut = 5, int $port = 110)
103
    {
104
        $this->connect($server, $login, $password, $timeOut, $port);
105
    }
106
107
    /**
108
     * Method returns emails count.
109
     */
110
    public function getCount(): int
111
    {
112
        fputs($this->connection, "STAT\r\n");
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fputs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
        fputs(/** @scrutinizer ignore-type */ $this->connection, "STAT\r\n");
Loading history...
113
114
        $result = fgets($this->connection, 1024);
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fgets(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
        $result = fgets(/** @scrutinizer ignore-type */ $this->connection, 1024);
Loading history...
115
116
        if (substr($result, 0, 3) !== '+OK') {
117
            throw (new \Exception("STAT " . $result, 0));
118
        }
119
120
        $result = explode(' ', $result);
121
122
        return intval($result[1]);
123
    }
124
125
    /**
126
     * Method returns data from connection
127
     *
128
     * @return string Fetched data
129
     */
130
    protected function getData(): string
131
    {
132
        $data = '';
133
134
        while (! feof($this->connection)) {
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of feof(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
        while (! feof(/** @scrutinizer ignore-type */ $this->connection)) {
Loading history...
135
            $buffer = chop(fgets($this->connection, 1024));
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fgets(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

135
            $buffer = chop(fgets(/** @scrutinizer ignore-type */ $this->connection, 1024));
Loading history...
136
137
            if (strpos($buffer, '-ERR') === 0) {
138
                throw (new \Exception(str_replace('-ERR ', '', $buffer), 0));
139
            }
140
141
            $data .= "$buffer\r\n";
142
143
            if (trim($buffer) == '.') {
144
                break;
145
            }
146
        }
147
148
        return $data;
149
    }
150
151
    /**
152
     * Method returns email's headers
153
     *
154
     * @param int $i
155
     *            Number of the message
156
     * @return string Headers
157
     */
158
    public function getMessageHeaders(int $i): string
159
    {
160
        fputs($this->connection, "TOP $i 3\r\n");
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fputs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
        fputs(/** @scrutinizer ignore-type */ $this->connection, "TOP $i 3\r\n");
Loading history...
161
162
        return $this->getData();
163
    }
164
165
    /**
166
     * Method deletes email
167
     *
168
     * @param int $i
169
     *            Number of the message
170
     * @return string Result of the deletion
171
     */
172
    public function deleteMessage($i): string
173
    {
174
        fputs($this->connection, "DELE $i\r\n");
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fputs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        fputs(/** @scrutinizer ignore-type */ $this->connection, "DELE $i\r\n");
Loading history...
175
176
        return fgets($this->connection);
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fgets(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        return fgets(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
177
    }
178
179
    /**
180
     * Method terminates session
181
     */
182
    public function quit()
183
    {
184
        fputs($this->connection, "QUIT\r\n");
0 ignored issues
show
$this->connection of type boolean is incompatible with the type resource expected by parameter $handle of fputs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
        fputs(/** @scrutinizer ignore-type */ $this->connection, "QUIT\r\n");
Loading history...
185
    }
186
187
    /**
188
     * Method parses subject with any prefix
189
     *
190
     * @param string $line
191
     *            Line of the email
192
     * @param int $i
193
     *            Line cursor
194
     * @param array $headers
195
     *            Email headers
196
     * @param string $type
197
     *            Mime type
198
     * @return string Decoded data
199
     */
200
    protected function parseAnyType(string $line, int $i, array $headers, string $type): string
201
    {
202
        $subject = substr($line, 0, strlen($line) - 2);
203
204
        for ($j = $i + 1; $j < count($headers); $j ++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
205
            if (substr($headers[$j], 0, 1) == ' ') {
206
                $subject .= str_replace([
207
                    ' ' . $type,
208
                    '?='
209
                ], [
210
                    '',
211
                    ''
212
                ], $headers[$j]);
213
            } else {
214
                return str_replace('Subject: ', '', iconv_mime_decode($subject . "?=\r\n", 0, "UTF-8"));
215
            }
216
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
217
    }
218
219
    /**
220
     * Method returns message's subject
221
     *
222
     * @param int $i
223
     *            Line number
224
     * @return string Decoded data
225
     */
226
    public function getMessageSubject(int $i): string
227
    {
228
        $headers = $this->getMessageHeaders($i);
229
230
        $headers = explode("\r\n", $headers);
231
232
        foreach ($headers as $i => $line) {
233
            if (strpos($line, 'Subject: ') === 0) {
234
                if (strpos($line, '=?UTF-8?Q?') !== false) {
235
                    return $this->parseAnyType($line, $i, $headers, '=?UTF-8?Q?');
236
                } elseif (strpos($line, '=?UTF-8?B?') !== false) {
237
                    return $this->parseAnyType($line, $i, $headers, '=?UTF-8?B?');
238
                }
239
            }
240
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
241
    }
242
243
    /**
244
     * Method returns true if the mail with the specified subject exists
245
     *
246
     * @param string $subject
247
     *            Searching subject
248
     * @return bool Email exists
249
     */
250
    public function messageWithSubjectExists(string $subject): bool
251
    {
252
        $count = $this->getCount();
253
254
        for ($i = 1; $i <= $count; $i ++) {
255
            $mailSubject = $this->getMessageSubject($i);
256
257
            if ($subject == $mailSubject) {
258
                return true;
259
            }
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * Method removes all the mails with the specified subject
267
     *
268
     * @param string $subject
269
     *            subject of emails to be deleted
270
     */
271
    public function deleteMessagesWithSubject(string $subject)
272
    {
273
        $count = $this->getCount();
274
275
        for ($i = 1; $i <= $count; $i ++) {
276
            $mailSubject = $this->getMessageSubject($i);
277
278
            if ($subject == $mailSubject) {
279
                $this->deleteMessage($i);
280
            }
281
        }
282
    }
283
284
    /**
285
     * Method returns Message-ID
286
     *
287
     * @param string $headers
288
     *            email headers
289
     * @return string Message-ID
290
     */
291
    public static function getMessageId(string $headers): string
292
    {
293
        $matches = [];
294
295
        preg_match('/Message-ID: <([0-9a-zA-Z\.@\-]*)>/mi', $headers, $matches);
296
297
        return $matches[1];
298
    }
299
}
300