master
ブランチ master
LittleBeast / src / Std / Test / LibMailer.php
8.4 KB、190行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Test;
39
40require_once __DIR__.'/../../../autoload.php';
41
42use Std\Lib\Tester;
43use Std\Lib\Mailer;
44use Std\Test\Helper\MockServer;
45use Std\Test\Helper\TestCert;
46
47$test = new Tester([
48 'colorOutput' => true,
49 'verboseOutput' => true
50]);
51
52$test->describe('メーラー(偽物のSMTPサーバー)', function($test): void {
53 $log = tempnam(sys_get_temp_dir(), 'lbsmtp_');
54 $srv = MockServer::start('smtp-server.php', [$log]);
55 if ($srv === null) {
56 $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)');
57 }
58
59 $info = fn() => ['host' => '127.0.0.1', 'port' => $srv->port, 'user' => 'me@076.moe', 'pass' => 'ひみつ', 'from' => 'me@076.moe'];
60 $send = function(callable $fn) use ($info, $log): string {
61 file_put_contents($log, '');
62 $mail = new Mailer($info());
63 $mail->connect();
64 try {
65 $mail->authenticate();
66 $fn($mail);
67 } finally {
68 $mail->disconnect();
69 }
70 return (string)file_get_contents($log);
71 };
72
73 $test->it('MAILER_ENABLEDが無効なら設定なしで作れないはず', function($test): void {
74 if (MAILER_ENABLED) {
75 $test->assertNotNull(new Mailer());
76 return;
77 }
78 $test->assertThrows(fn() => new Mailer(), \Exception::class);
79 });
80
81 $test->it('認証してメールを送れるはず', function($test) use ($send): void {
82 $out = $send(fn($m) => $m->send('you@example.com', 'テスト件名', "こんにちは\n2行目", 'あなた'));
83
84 $test->assertStringContains("C: AUTH LOGIN\r\n", $out);
85 $test->assertStringContains('C: '.base64_encode('me@076.moe')."\r\n", $out);
86 $test->assertStringContains('C: '.base64_encode('ひみつ')."\r\n", $out);
87 $test->assertStringContains("C: MAIL FROM: <me@076.moe>\r\n", $out);
88 $test->assertStringContains("C: RCPT TO: <you@example.com>\r\n", $out);
89 $test->assertStringContains("C: QUIT\r\n", $out);
90
91 $test->assertStringContains('Subject: '.mb_encode_mimeheader('テスト件名', 'UTF-8', 'Q'), $out);
92 $test->assertStringContains('To: '.mb_encode_mimeheader('あなた', 'UTF-8', 'Q').' <you@example.com>', $out);
93 $test->assertTrue((bool)preg_match('/^Message-ID: <[0-9a-f]{32}@076\.moe>\r$/m', $out), 'Message-IDが無い');
94 $test->assertStringContains('Content-Transfer-Encoding: quoted-printable', $out);
95 });
96
97 $test->it('EHLOでは自分のホスト名を名乗るはず', function($test) use ($send): void {
98 $out = $send(fn($m) => $m->send('you@example.com', 'x', 'x'));
99 $test->assertStringContains('C: EHLO '.(gethostname() ?: 'localhost')."\r\n", $out);
100 $test->assertFalse(str_contains($out, 'C: EHLO 127.0.0.1'));
101 });
102
103 $test->it('本文をquoted-printableでエンコードし、改行をCRLFに揃えるはず', function($test) use ($send): void {
104 $out = $send(fn($m) => $m->send('you@example.com', 'x', "日本語\n二行目"));
105 preg_match("/\r\n\r\n(.*)END DATA/s", $out, $m);
106 $body = quoted_printable_decode($m[1] ?? '');
107 $test->assertEquals("日本語\r\n二行目\r\n", $body);
108 });
109
110 $test->it('「.」で始まる行をドットスタッフィングするはず', function($test) use ($send): void {
111 // 以前は「.」だけの行で本文が途中で終わった
112 $out = $send(fn($m) => $m->send('you@example.com', 'x', "前\n.\n.後ろ\n終わり"));
113 $test->assertStringContains("\r\n..\r\n..=E5=BE=8C", $out);
114 $test->assertStringContains('終わり', quoted_printable_decode($out));
115 });
116
117 $test->it('ヘッダー注入を拒否するはず', function($test) use ($info): void {
118 $mail = new Mailer($info());
119 $mail->connect();
120 try {
121 $test->assertThrows(fn() => $mail->send("you@example.com>\r\nRCPT TO: <evil@example.com", 'x', 'x'), \InvalidArgumentException::class);
122 $test->assertThrows(fn() => $mail->send('you@example.com', "件名\r\nBcc: evil@example.com", 'x'), \InvalidArgumentException::class);
123 $test->assertThrows(fn() => $mail->send('you@example.com', 'x', 'x', "名前\nBcc: x"), \InvalidArgumentException::class);
124 $test->assertThrows(fn() => $mail->send('not-an-address', 'x', 'x'), \InvalidArgumentException::class);
125 } finally {
126 $mail->disconnect();
127 }
128 });
129
130 $test->it('拒否された宛先は例外になるはず', function($test) use ($send): void {
131 $test->assertThrows(fn() => $send(fn($m) => $m->send('reject@example.com', 'x', 'x')), \Exception::class);
132 });
133
134 $test->it('接続出来ない場合は例外になるはず', function($test): void {
135 $probe = stream_socket_server('tcp://127.0.0.1:0');
136 $port = (int)substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
137 fclose($probe);
138 $mail = new Mailer(['host' => '127.0.0.1', 'port' => $port, 'from' => 'me@076.moe']);
139 $test->assertThrows(fn() => $mail->connect(), \Exception::class);
140 });
141
142 $srv?->stop();
143 @unlink($log);
144});
145
146$test->describe('メーラー(STARTTLS、PHP純正のTLS 1.3)', function($test): void {
147 $cert = TestCert::get('ec');
148 $log = tempnam(sys_get_temp_dir(), 'lbsmtp_');
149 $srv = MockServer::start('smtp-server.php', [$log, $cert['cert'], $cert['key']]);
150 if ($srv === null) {
151 $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)');
152 return;
153 }
154 $info = fn(array $tls) => ['host' => 'localhost', 'port' => $srv->port, 'user' => 'me@076.moe', 'pass' => 'ひみつ', 'from' => 'me@076.moe', 'tls' => $tls];
155
156 $test->it('STARTTLSの後に認証して送れるはず', function($test) use ($info, $log, $cert): void {
157 file_put_contents($log, '');
158 $mail = new Mailer($info(['cafile' => $cert['ca']]));
159 $mail->connect();
160 try {
161 $mail->authenticate();
162 $mail->send('you@example.com', '暗号化', '本文');
163 } finally {
164 $mail->disconnect();
165 }
166 $out = (string)file_get_contents($log);
167 $test->assertStringContains("C: STARTTLS\r\n", $out);
168 $test->assertStringContains('TLS: TLS_', $out);
169 // パスワードはTLSの後に送る
170 $test->assertTrue(strpos($out, 'TLS: TLS_') < strpos($out, 'C: '.base64_encode('ひみつ')));
171 $test->assertStringContains("C: QUIT\r\n", $out);
172 });
173
174 $test->it('信頼出来ない証明書なら認証せずに例外になるはず', function($test) use ($info, $log): void {
175 file_put_contents($log, '');
176 $mail = new Mailer($info(['cafile' => TestCert::get('self')['cert']]));
177 $mail->connect();
178 try {
179 $test->assertThrows(fn() => $mail->authenticate(), \Exception::class);
180 } finally {
181 $mail->disconnect();
182 }
183 $test->assertFalse(str_contains((string)file_get_contents($log), base64_encode('ひみつ')));
184 });
185
186 $srv->stop();
187 @unlink($log);
188});
189
190$test->printSummary();