xref: /plugin/smtp/_test/IntegrationTest.php (revision 8f9f7168582dd10127fb688c7ad7e9116bc5c67b)
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