xref: /php-src/ext/standard/tests/mail/mail_util.inc (revision f62f6a6d)
1<?php
2
3class MailConnecter
4{
5    private $fp = null;
6    private static $instance;
7    private static $init = false;
8
9    private const HOST = 'localhost';
10    private const PORT = 143;
11    private const TIMEOUT = 10;
12
13    private function __construct()
14    {
15        if (($this->fp = @fsockopen(self::HOST, self::PORT)) === false) {
16            die('cannot open imap socket');
17        }
18    }
19
20    public static function getConnection(): self
21    {
22        if (!static::$init) {
23            static::$instance = new self();
24        }
25
26        return static::$instance;
27    }
28
29    public function disconnect(): void
30    {
31        fclose($this->fp);
32    }
33
34    public function fail(string $message): void
35    {
36        $this->disconnect();
37        die($message);
38    }
39
40    public function send(string $tag, string $command): void
41    {
42        fputs($this->fp, "{$tag} {$command}\r\n");
43    }
44
45    public function isSuccess(string $tag): bool
46    {
47        $start = time();
48        while (!feof($this->fp)) {
49            $line = fgets($this->fp);
50            if (!$line) {
51                return false;
52            }
53            if (str_contains($line, $tag)) {
54                if (preg_match('/(NO|BAD|failed|Failure)/i', $line)) {
55                    return false;
56                }
57                return true;
58            }
59            if (time() - $start > self::TIMEOUT) {
60                $this->fail("{$tag} timeout");
61            }
62        }
63        return false;
64    }
65
66    public function getResponse(string $tag, bool $returnArray = false): string|array
67    {
68        $start = time();
69        $output = $returnArray ? [] : '';
70        while (!feof($this->fp)) {
71            $line = fgets($this->fp);
72            if (!$line) {
73                $this->fail("{$tag} failed");
74            }
75            if ($returnArray) {
76                $output[] = $line;
77            } else {
78                $output .= $line;
79            }
80            if (str_contains($line, $tag)) {
81                if (preg_match('/(NO|BAD|failed|Failure)/i', $line)) {
82                    $this->fail("{$tag} failed");
83                }
84                return $output;
85            }
86            if (time() - $start > self::TIMEOUT) {
87                $this->fail("{$tag} timeout");
88            }
89        }
90        $this->fail("{$tag} failed");
91    }
92}
93
94class MailBox
95{
96    private MailConnecter $mailConnecter;
97    private const PASSWORD = 'p4ssw0rd';
98    public const USERS = [
99        'webmaster@example.com',
100        'info@example.com',
101        'admin@example.com',
102        'foo@example.com',
103    ];
104
105    private const LOGIN = 'A00001';
106    private const LOGOUT = 'A00002';
107    private const SELECT_MAILBOX = 'A00003';
108    private const SEARCH = 'A00004';
109    private const FETCH_HEADERS = 'A00005';
110    private const FETCH_BODY = 'A00006';
111    private const DELETE = 'A00007';
112    private const EXPUNGE = 'A00008';
113
114    private function __construct()
115    {
116        $this->mailConnecter = MailConnecter::getConnection();
117    }
118
119    public static function login(string $user): self
120    {
121        $self = new self();
122        $self->mailConnecter->send(self::LOGIN, 'LOGIN '.$user.' '.self::PASSWORD);
123        if (!$self->mailConnecter->isSuccess(self::LOGIN)) {
124            $self->mailConnecter->fail('login failed');
125        }
126        return $self;
127    }
128
129    public function logout(): void
130    {
131        $this->mailConnecter->send(self::LOGOUT, 'LOGOUT');
132        if (!$this->mailConnecter->isSuccess(self::LOGOUT)) {
133            $this->mailConnecter->fail('logout failed');
134        }
135    }
136
137    private function getUidsBySubject(string $subject): array
138    {
139        $this->mailConnecter->send(self::SELECT_MAILBOX, 'SELECT "INBOX"');
140        if (!$this->mailConnecter->isSuccess(self::SELECT_MAILBOX)) {
141            $this->mailConnecter->fail('select mailbox failed');
142        }
143
144        $this->mailConnecter->send(self::SEARCH, "UID SEARCH SUBJECT \"{$subject}\"");
145        $res = $this->mailConnecter->getResponse(self::SEARCH);
146        preg_match('/SEARCH ([0-9 ]+)/is', $res, $matches);
147        return isset($matches[1]) ? explode(' ', trim($matches[1])) : [];
148    }
149
150    public function getMailsBySubject(string $subject): MailCollection
151    {
152        return new MailCollection(array_map(
153            fn($uid) => $this->getHeaders($uid) + ['Body' => $this->getBody($uid)],
154            $this->getUidsBySubject($subject),
155        ));
156    }
157
158    private function getHeaders(int $uid): array
159    {
160        $this->mailConnecter->send(self::FETCH_HEADERS, "UID FETCH {$uid} (BODY[HEADER])");
161        $res = $this->mailConnecter->getResponse(self::FETCH_HEADERS, true);
162
163        $headers = [];
164        foreach ($res as $line) {
165            $line = trim($line);
166            if (!$line) {
167                continue;
168            }
169            $items = explode(':', $line);
170            preg_match('/^(.+?):(.+?)$/', $line, $matches);
171            $key = trim($matches[1] ?? '');
172            $val = trim($matches[2] ?? '');
173            if (!$key || !$val || $val === self::FETCH_HEADERS.' OK UID completed' || $val === ')') {
174                continue;
175            }
176
177            $headers[$key] = $val;
178        }
179        return $headers;
180    }
181
182    private function getBody(int $uid): string
183    {
184        $this->mailConnecter->send(self::FETCH_BODY, "UID FETCH {$uid} (BODY[TEXT])");
185        $body = $this->mailConnecter->getResponse(self::FETCH_BODY, true);
186        $count = count($body);
187        if ($count <= 3) {
188            return '';
189        }
190
191        return implode('', array_slice($body, 1, $count - 3));
192    }
193
194    public function deleteMailsBySubject(string $subject): void
195    {
196        array_map(
197            fn($uid) => $this->mailConnecter->send(self::DELETE, "UID STORE {$uid} +FLAGS (\\Deleted)"),
198            $this->getUidsBySubject($subject),
199        );
200        $this->mailConnecter->send(self::EXPUNGE, 'EXPUNGE');
201    }
202}
203
204class MailCollection
205{
206    public function __construct(private ?array $mailData) {}
207
208    public function isAsExpected(string $from, string $to, string $subject, string $message): bool
209    {
210        $result = true;
211
212        if (!$this->mailData) {
213            $result = false;
214            echo "Email data does not exist.\n";
215        }
216        if (!$this->count() > 1) {
217            $result = false;
218            echo "Multiple email data exist.\n";
219        }
220        if ($this->getHeader('From', true) !== $from) {
221            $result = false;
222            echo "from does not match.\n";
223        }
224        if ($this->getHeader('To', true) !== $to) {
225            $result = false;
226            echo "to does not match.\n";
227        }
228        if ($this->getHeader('Subject', true) !== $subject) {
229            $result = false;
230            echo "subject does not match.\n";
231        }
232        if (trim($this->getBody()) !== trim($message)) {
233            $result = false;
234            echo "body does not match.\n";
235        }
236
237        return $result;
238    }
239
240    public function count(): int
241    {
242        return count($this->mailData);
243    }
244
245    public function getHeader(string $field, bool $ignoreCases = false, int $offset = 0)
246    {
247        if ($ignoreCases) {
248            $mail =  array_change_key_case($this->mailData[$offset] ?? []);
249            $field = strtolower($field);
250        } else {
251            $mail = $this->mailData[$offset] ?? [];
252        }
253
254        return $mail[$field] ?? null;
255    }
256
257    public function getBody(int $offset = 0): ?string
258    {
259        return $this->mailData[$offset]['Body'] ?? null;
260    }
261}
262