1*8f9f7168SAndreas Gohr<?php 2*8f9f7168SAndreas Gohr 3*8f9f7168SAndreas Gohrnamespace dokuwiki\plugin\smtp\test; 4*8f9f7168SAndreas Gohr 5*8f9f7168SAndreas Gohruse DokuWikiTest; 6*8f9f7168SAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 7*8f9f7168SAndreas Gohruse Mailer; 8*8f9f7168SAndreas Gohr 9*8f9f7168SAndreas Gohr/** 10*8f9f7168SAndreas Gohr * Full integration test for the smtp plugin 11*8f9f7168SAndreas Gohr * 12*8f9f7168SAndreas Gohr * This sends a real mail through DokuWiki's Mailer (which the plugin intercepts and 13*8f9f7168SAndreas Gohr * delivers over SMTP) to a running Mailpit server and then verifies through Mailpit's 14*8f9f7168SAndreas Gohr * HTTP API that the message was delivered correctly. 15*8f9f7168SAndreas Gohr * 16*8f9f7168SAndreas Gohr * The Mailpit server is configured through environment variables: 17*8f9f7168SAndreas Gohr * MAILPIT_HOST hostname of the Mailpit server (enables the test) 18*8f9f7168SAndreas Gohr * MAILPIT_SMTP_PORT the SMTP port to deliver to (default: 1025) 19*8f9f7168SAndreas Gohr * MAILPIT_HTTP_PORT the HTTP API/web interface port (default: 8025) 20*8f9f7168SAndreas Gohr * 21*8f9f7168SAndreas Gohr * When MAILPIT_HOST is not set the test is skipped, so it does not break the regular 22*8f9f7168SAndreas Gohr * test suite which runs without the Mailpit service. When it is set the server must be 23*8f9f7168SAndreas Gohr * reachable, otherwise the test fails. 24*8f9f7168SAndreas Gohr * 25*8f9f7168SAndreas Gohr * @group plugin_smtp 26*8f9f7168SAndreas Gohr * @group plugins 27*8f9f7168SAndreas Gohr */ 28*8f9f7168SAndreas Gohrclass IntegrationTest extends DokuWikiTest 29*8f9f7168SAndreas Gohr{ 30*8f9f7168SAndreas Gohr protected $pluginsEnabled = ['smtp']; 31*8f9f7168SAndreas Gohr 32*8f9f7168SAndreas Gohr /** @var string hostname of the Mailpit server */ 33*8f9f7168SAndreas Gohr protected $host; 34*8f9f7168SAndreas Gohr 35*8f9f7168SAndreas Gohr /** @var int SMTP port to deliver to */ 36*8f9f7168SAndreas Gohr protected $smtpPort; 37*8f9f7168SAndreas Gohr 38*8f9f7168SAndreas Gohr /** @var string base URL of the Mailpit HTTP API, without trailing slash */ 39*8f9f7168SAndreas Gohr protected $apiBase; 40*8f9f7168SAndreas Gohr 41*8f9f7168SAndreas Gohr /** @inheritdoc */ 42*8f9f7168SAndreas Gohr public function setUp(): void 43*8f9f7168SAndreas Gohr { 44*8f9f7168SAndreas Gohr parent::setUp(); 45*8f9f7168SAndreas Gohr 46*8f9f7168SAndreas Gohr // the test only runs when a Mailpit server is configured via MAILPIT_HOST, 47*8f9f7168SAndreas Gohr // otherwise it is skipped (eg. regular CI/local runs without Mailpit) 48*8f9f7168SAndreas Gohr $this->host = getenv('MAILPIT_HOST'); 49*8f9f7168SAndreas Gohr if ($this->host === false || $this->host === '') { 50*8f9f7168SAndreas Gohr $this->markTestSkipped('MAILPIT_HOST is not set, skipping the Mailpit integration test'); 51*8f9f7168SAndreas Gohr } 52*8f9f7168SAndreas Gohr 53*8f9f7168SAndreas Gohr $this->smtpPort = (int)(getenv('MAILPIT_SMTP_PORT') ?: 1025); 54*8f9f7168SAndreas Gohr $httpPort = (int)(getenv('MAILPIT_HTTP_PORT') ?: 8025); 55*8f9f7168SAndreas Gohr $this->apiBase = 'http://' . $this->host . ':' . $httpPort; 56*8f9f7168SAndreas Gohr 57*8f9f7168SAndreas Gohr // a Mailpit server was requested, so it must actually be reachable 58*8f9f7168SAndreas Gohr $sock = @fsockopen($this->host, $this->smtpPort, $errno, $errstr, 2); 59*8f9f7168SAndreas Gohr if (!$sock) { 60*8f9f7168SAndreas Gohr $this->fail("No Mailpit SMTP server reachable at $this->host:$this->smtpPort ($errstr)"); 61*8f9f7168SAndreas Gohr } 62*8f9f7168SAndreas Gohr fclose($sock); 63*8f9f7168SAndreas Gohr 64*8f9f7168SAndreas Gohr // point the plugin at our Mailpit server 65*8f9f7168SAndreas Gohr global $conf; 66*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['smtp_host'] = $this->host; 67*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['smtp_port'] = $this->smtpPort; 68*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['smtp_ssl'] = ''; 69*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['auth_user'] = ''; 70*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['auth_pass'] = ''; 71*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['localdomain'] = 'test.localhost'; 72*8f9f7168SAndreas Gohr $conf['plugin']['smtp']['debug'] = 0; 73*8f9f7168SAndreas Gohr 74*8f9f7168SAndreas Gohr // give DokuWiki's Mailer a sane default sender 75*8f9f7168SAndreas Gohr $conf['mailfrom'] = 'wiki@example.com'; 76*8f9f7168SAndreas Gohr } 77*8f9f7168SAndreas Gohr 78*8f9f7168SAndreas Gohr /** 79*8f9f7168SAndreas Gohr * A mail sent through DokuWiki's Mailer must be delivered to Mailpit over SMTP. 80*8f9f7168SAndreas Gohr * 81*8f9f7168SAndreas Gohr * To and Cc come from the message headers, while the Bcc recipient is delivered 82*8f9f7168SAndreas Gohr * through the SMTP envelope even though the plugin strips the Bcc header from the 83*8f9f7168SAndreas Gohr * message body. 84*8f9f7168SAndreas Gohr */ 85*8f9f7168SAndreas Gohr public function testSendMail(): void 86*8f9f7168SAndreas Gohr { 87*8f9f7168SAndreas Gohr $subject = 'SMTP plugin integration test ' . uniqid('', true); 88*8f9f7168SAndreas Gohr $bodyText = 'Hello from the DokuWiki smtp plugin integration test.'; 89*8f9f7168SAndreas Gohr 90*8f9f7168SAndreas Gohr $mailer = new Mailer(); 91*8f9f7168SAndreas Gohr $mailer->from('Wiki Admin <wiki@example.com>'); 92*8f9f7168SAndreas Gohr $mailer->to('Jane Doe <jane@example.com>'); 93*8f9f7168SAndreas Gohr $mailer->cc('carol@example.com'); 94*8f9f7168SAndreas Gohr $mailer->bcc('secret@example.com'); 95*8f9f7168SAndreas Gohr $mailer->subject($subject); 96*8f9f7168SAndreas Gohr $mailer->setBody($bodyText); 97*8f9f7168SAndreas Gohr 98*8f9f7168SAndreas Gohr $ok = $mailer->send(); 99*8f9f7168SAndreas Gohr $this->assertTrue($ok, 'Mailer::send() should report success when talking to Mailpit'); 100*8f9f7168SAndreas Gohr 101*8f9f7168SAndreas Gohr // the message should now be available through the Mailpit API 102*8f9f7168SAndreas Gohr $message = $this->findMessageBySubject($subject); 103*8f9f7168SAndreas Gohr $this->assertNotNull($message, 'The sent message should show up in Mailpit'); 104*8f9f7168SAndreas Gohr 105*8f9f7168SAndreas Gohr // sender and header recipients 106*8f9f7168SAndreas Gohr $this->assertEquals('wiki@example.com', $message['From']['Address']); 107*8f9f7168SAndreas Gohr $this->assertEquals(['jane@example.com'], $this->addresses($message['To'])); 108*8f9f7168SAndreas Gohr $this->assertEquals(['carol@example.com'], $this->addresses($message['Cc'])); 109*8f9f7168SAndreas Gohr $this->assertEquals(['secret@example.com'], $this->addresses($message['Bcc'])); 110*8f9f7168SAndreas Gohr 111*8f9f7168SAndreas Gohr // the body must have arrived intact 112*8f9f7168SAndreas Gohr $full = $this->apiGet('/api/v1/message/' . rawurlencode($message['ID'])); 113*8f9f7168SAndreas Gohr $this->assertStringContainsString($bodyText, $full['Text']); 114*8f9f7168SAndreas Gohr } 115*8f9f7168SAndreas Gohr 116*8f9f7168SAndreas Gohr /** 117*8f9f7168SAndreas Gohr * Reduce a Mailpit address list to a plain list of email addresses 118*8f9f7168SAndreas Gohr * 119*8f9f7168SAndreas Gohr * @param array $list list of {Name, Address} entries as returned by Mailpit 120*8f9f7168SAndreas Gohr * @return string[] 121*8f9f7168SAndreas Gohr */ 122*8f9f7168SAndreas Gohr protected function addresses(array $list): array 123*8f9f7168SAndreas Gohr { 124*8f9f7168SAndreas Gohr return array_map(static fn($addr) => $addr['Address'], $list); 125*8f9f7168SAndreas Gohr } 126*8f9f7168SAndreas Gohr 127*8f9f7168SAndreas Gohr /** 128*8f9f7168SAndreas Gohr * Find a message in Mailpit whose subject contains the given needle 129*8f9f7168SAndreas Gohr * 130*8f9f7168SAndreas Gohr * DokuWiki prefixes the subject with the wiki title (eg. "[My Wiki] ..."), so we 131*8f9f7168SAndreas Gohr * match on a unique substring rather than the full subject. Mailpit stores the 132*8f9f7168SAndreas Gohr * message before acknowledging the SMTP DATA command, so it is available as soon 133*8f9f7168SAndreas Gohr * as Mailer::send() returns - no polling needed. 134*8f9f7168SAndreas Gohr * 135*8f9f7168SAndreas Gohr * @param string $needle a unique substring of the subject 136*8f9f7168SAndreas Gohr * @return array|null the message stub from the listing or null if not found 137*8f9f7168SAndreas Gohr */ 138*8f9f7168SAndreas Gohr protected function findMessageBySubject($needle) 139*8f9f7168SAndreas Gohr { 140*8f9f7168SAndreas Gohr $list = $this->apiGet('/api/v1/messages'); 141*8f9f7168SAndreas Gohr foreach ($list['messages'] as $msg) { 142*8f9f7168SAndreas Gohr if (strpos($msg['Subject'], $needle) !== false) return $msg; 143*8f9f7168SAndreas Gohr } 144*8f9f7168SAndreas Gohr return null; 145*8f9f7168SAndreas Gohr } 146*8f9f7168SAndreas Gohr 147*8f9f7168SAndreas Gohr /** 148*8f9f7168SAndreas Gohr * Perform a GET request against the Mailpit API and return the decoded JSON 149*8f9f7168SAndreas Gohr * 150*8f9f7168SAndreas Gohr * @param string $path API path including the leading slash 151*8f9f7168SAndreas Gohr * @return array 152*8f9f7168SAndreas Gohr */ 153*8f9f7168SAndreas Gohr protected function apiGet($path) 154*8f9f7168SAndreas Gohr { 155*8f9f7168SAndreas Gohr $client = new DokuHTTPClient(); 156*8f9f7168SAndreas Gohr $body = $client->get($this->apiBase . $path); 157*8f9f7168SAndreas Gohr $this->assertNotFalse($body, 'Mailpit API request to ' . $path . ' failed: ' . $client->error); 158*8f9f7168SAndreas Gohr return json_decode($body, true); 159*8f9f7168SAndreas Gohr } 160*8f9f7168SAndreas Gohr} 161