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