diff --git a/.arcconfig b/.arcconfig new file mode 100644 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,5 @@ +{ + "repository.callsign": "KMAILGUN", + "phabricator.uri": "https://devcentral.nasqueron.org", + "unit.engine": "PhpunitTestEngine" +} diff --git a/.arclint b/.arclint new file mode 100644 --- /dev/null +++ b/.arclint @@ -0,0 +1,41 @@ +{ + "exclude": [ + "(^vendor/)" + ], + "linters": { + "chmod": { + "type": "chmod" + }, + "filename": { + "type": "filename" + }, + "json": { + "type": "json", + "include": [ + "(^\\.arcconfig$)", + "(^\\.arclint$)", + "(\\.json$)" + ] + }, + "merge-conflict": { + "type": "merge-conflict" + }, + "php": { + "type": "php", + "include": "(\\.php$)" + }, + "phpcs": { + "type": "phpcs", + "bin": "vendor/bin/phpcs", + "phpcs.standard": "PSR1", + "include": "(^app/.*\\.php$)" + }, + "spelling": { + "type": "spelling" + }, + "xml": { + "type": "xml", + "include": "(\\.xml$)" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +All notable changes to this project will be documented in this file. +This project adheres to [semantic versioning](http://semver.org/). + +## [0.0.1] - 2016-09-01 +### Added +- Initial version diff --git a/README.md b/README.md new file mode 100644 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# keruald/mailgun + +Mailgun API client + +Allow to retrieve a stored message on Mailgun. diff --git a/composer.json b/composer.json new file mode 100644 --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "keruald/mailgun", + "description": "Mailgun API client to retrieve a mail", + "license": "BSD-2-Clause", + "authors": [ + { + "name": "Sébastien Santoro", + "email": "dereckson@espace-win.org" + }, + { + "name": "Yassine Hadj Messaoud", + "email": "modepadu95@riseup.net" + } + ], + "keywords": [ + "keruald", + "Mailgun" + ], + "support": { + "irc": "irc://irc.freenode.net/wolfplex", + "issues": "https://devcentral.nasqueron.org" + }, + "require": { + "php": ">=5.6.0", + "guzzlehttp/psr7": "~1.3", + "guzzlehttp/guzzle": "~6.0" + }, + "require-dev": { + "phpunit/phpunit": "5.0.*", + "psy/psysh": "dev-master", + "squizlabs/php_codesniffer": "*", + "mockery/mockery": "^0.9.5" + }, + "autoload": { + "psr-4": { + "Keruald\\Mailgun\\": "src/", + "Keruald\\Mailgun\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit tests" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,13 @@ + + + + + ./tests/ + + + + + src/ + + + diff --git a/src/MailgunMessage.php b/src/MailgunMessage.php new file mode 100644 --- /dev/null +++ b/src/MailgunMessage.php @@ -0,0 +1,132 @@ +client = $client; + $this->url = $url; + $this->key = $key; + } + + /** + * Initializes a new instance of the MailgunMessage object from a payload. + * + * @param \GuzzleHttp\Client $client HTTP client + * @param object $payload The payload fired by MailGun routing API + * @param string $key The API key to use to fetch the message. + */ + public static function loadFromEventPayload (ClientInterface $client, $payload, $key) { + $url = self::extractUrlFromEventPayload($payload); + return new self($client, $url, $key); + } + + /// + /// Public API methods + /// + + /** + * Gets a JSON representation of a mail through Mailgun API. + * + * @return stdClass + */ + public function get () { + if ($this->message === null) { + $this->fetch(); + } + + return $this->message; + } + + /// + /// Helper methods to fetch a message from Mailgun. + /// + + /** + * Fetches the message through Mailgun API. + * + * If successful, fills the message property. + * + * @throws \RuntimeException when HTTP status code isn't 200 + */ + private function fetch () { + $response = $this->client->request( + 'GET', + $this->url, + $this->getHttpOptions() + ); + + $result = $response->getBody(); + $this->message = json_decode($result); + } + + /** + * @return array + */ + private function getHttpOptions () { + return [ + 'auth' => [ + 'api', + $this->key + ] + ]; + } + + /// + /// Helper methods to process payload + /// + + /** + * Extracts the MailGun URL to retrieve a stored message. + * + * @return string + * @throw \InvalidArgumentException if payload doesn't contain URL where expected. + */ + private static function extractUrlFromEventPayload ($payload) { + if (!isset($payload->storage->url)) { + throw new InvalidArgumentException("The payload should be an object with a storage.url property."); + } + return $payload->storage->url; + } + +} diff --git a/src/MailgunMessageFactory.php b/src/MailgunMessageFactory.php new file mode 100644 --- /dev/null +++ b/src/MailgunMessageFactory.php @@ -0,0 +1,86 @@ +client = $client; + $this->key = $key; + } + + /// + /// Builder + /// + + /** + * @param string $url The Mailgun URL of the message to fetch. + * @return MailgunMessage + */ + public function getMessage ($url) { + return new MailgunMessage($this->client, $url, $this->key); + } + + /** + * Gets a JSON representation of a mail. + * + * @param string $url The Mailgun URL of the message to fetch. + * @return object + */ + public function fetchMessage ($url) { + return $this->getMessage($url)->get(); + } + + /** + * @param stdClass $payload The payload fired by MailGun routing API + * @return MailgunMessage + */ + public function getMessageFromPayload (stdClass $payload) { + return MailgunMessage::loadFromEventPayload( + $this->client, $payload, $this->key + ); + } + + /** + * Gets a JSON representation of a mail. + * + * @param stdClass $payload The payload fired by MailGun routing API + * @return object + */ + public function fetchMessageFromPayload (stdClass $payload) { + return $this->getMessageFromPayload($payload)->get(); + } + +} diff --git a/tests/MailgunMessageFactoryTest.php b/tests/MailgunMessageFactoryTest.php new file mode 100644 --- /dev/null +++ b/tests/MailgunMessageFactoryTest.php @@ -0,0 +1,55 @@ +factory = new MailgunMessageFactory($client, "0000"); + } + + public function testGetMessage () { + $message = $this->factory->getMessage("http://api/somemessage"); + $this->assertInstanceOf(MailgunMessage::class, $message); + } + + public function testGetMessageFromPayload () { + $payload = self::mockEventPayload(); + $message = $this->factory->getMessageFromPayload($payload); + $this->assertInstanceOf(MailgunMessage::class, $message); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetMessageFromPayloadThrowsExceptionWhenPayloadDoesNotContainUrlInformation () { + $this->factory->getMessageFromPayload(new stdClass); + } + + public function testFetchMessage () { + $message = $this->factory->fetchMessage("http://api/somemessage"); + $this->assertInstanceOf("stdClass", $message); + } + + public function testFetchMessageFromPayload () { + $payload = self::mockEventPayload(); + $message = $this->factory->fetchMessageFromPayload($payload); + $this->assertInstanceOf("stdClass", $message); + } + +} diff --git a/tests/MailgunMessageTest.php b/tests/MailgunMessageTest.php new file mode 100644 --- /dev/null +++ b/tests/MailgunMessageTest.php @@ -0,0 +1,68 @@ +message = new MailgunMessage($client, "https://api/msg", "0000"); + } + + public function testGet () { + $this->assertEquals( + json_decode(self::mockHttpClientResponseBody()), + $this->message->get() + ); + } + + public function testGetWhenMessageIsAlreadyCached () { + $this->message->get(); + $this->assertEquals( + json_decode(self::mockHttpClientResponseBody()), + $this->message->get() + ); + } + + /** + * @expectedException \RuntimeException + */ + public function testFetchThrowsExceptionWhenStatusCodeIsNot200 () { + $client = self::mockHttpClientWithCustomResponse(500, null); + $message = new MailgunMessage($client, "https://api/msg", "0000"); + $message->get(); + } + + public function testLoadFromEventPayload () { + $client = self::mockHttpClient(); + $payload = self::mockEventPayload(); + $message = MailgunMessage::loadFromEventPayload($client, $payload, "0000"); + $this->assertEquals( + json_decode(self::mockHttpClientResponseBody()), + $message->get() + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testLoadFromEventPayloadWithWrongPayload () { + $client = self::mockHttpClient(); + $payload = new stdClass; + MailgunMessage::loadFromEventPayload($client, $payload, "0000"); + } + + +} diff --git a/tests/WithMockHttpClient.php b/tests/WithMockHttpClient.php new file mode 100644 --- /dev/null +++ b/tests/WithMockHttpClient.php @@ -0,0 +1,55 @@ +mockHttpClientResponseBody(); + return self::mockHttpClientWithCustomResponse(200, $body); + } + + /** + * @return \GuzzleHttp\Client + */ + public function mockHttpClientWithCustomResponse ($code, $body) { + $handler = self::getCustomMockHttpClientHandler($code, $body); + return new Client(['handler' => $handler]); + } + + /** + * @return stdClass + */ + public function mockEventPayload () { + return json_decode(file_get_contents(__DIR__ . '/payload.json')); + } + + /// + /// Mock helper methods + /// + + /** + * @return \GuzzleHttp\HandlerStack + */ + protected static function getCustomMockHttpClientHandler ($code, $body, $headers = []) { + return HandlerStack::create(new MockHandler([ + new Response($code, $headers, $body), + ])); + } + + /** + * @return string + */ + protected static function mockHttpClientResponseBody () { + return file_get_contents(__DIR__ . '/response.json'); + } + +} diff --git a/tests/payload.json b/tests/payload.json new file mode 100644 --- /dev/null +++ b/tests/payload.json @@ -0,0 +1,40 @@ +{ + "tags": [], + "timestamp": 1472676730.131279, + "storage": { + "url": "https://so.api.mailgun.net/v3/domains/notifications.domain.tld/messages/somehash", + "key": "somekey" + }, + "envelope": { + "sender": "no-reply@notify.docker.com", + "transport": "smtp", + "targets": "dockerhub@notifications.domain.tld" + }, + "recipient-domain": "notifications.domain.tld", + "method": "smtp", + "campaigns": [], + "user-variables": {}, + "flags": { + "is-routed": null, + "is-authenticated": false, + "is-system-test": false, + "is-test-mode": false + }, + "log-level": "info", + "id": "te1cKBxQTJWxpalPRcnmBw", + "message": { + "headers": { + "to": "dockerhub@notifications.domain.tld", + "message-id": "CAKg6iAHgQ4e8etV=gi4pbBueeA-o0Qsv02u1Cp9ZKTH8-tL9xw@mail.gmail.com", + "from": "no-reply@notify.docker.com", + "subject": "There's been an issue with your automated build" + }, + "attachments": [], + "recipients": [ + "dockerhub@notifications.domain.tld" + ], + "size": 2674 + }, + "recipient": "dockerhub@notifications.domain.tld", + "event": "accepted" +} diff --git a/tests/response.json b/tests/response.json new file mode 100644 --- /dev/null +++ b/tests/response.json @@ -0,0 +1,92 @@ +{ + "Received": "by 10.159.36.111 with HTTP; Wed, 31 Aug 2016 13:52:08 -0700 (PDT)", + "stripped-signature": "", + "From": "Docker Notify ", + "X-Envelope-From": "", + "recipients": "dockerhub@notifications.domain.tld", + "X-Google-Dkim-Signature": "v=1; a=rsa-sha256; c=relaxed\/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=mh8Y6W2HqOMo2y75revb4uvzUOKsWoeabsuU5I2tA6c=; b=XjAi2eyhoKNqT8cY4CSyWDd8YBjtSdvNPCO43SF0kzAUPzH7EI3gc6dqOMmJwXHqgA z5EgS4NaJarYrTIDmV3+w42mpccqv+bqVmFbXWsGSSNEMnW8gaDlVq0ckSwHkbVmogOm Ne447o8wbI5mOSxchuPfRHseD0MN4KWmRiHnpWl7aawA4ADDqB79cSJzn8Z2LCwM3uyw CuoX3UngLt3UURf45xm\/emIsNgdyBkfp6Tdd3BaLHBl3KeeLRJELIu8W+PFYtOogV16T eR6FtOkpXZwT3j9gZ6WbYTzd93uw+j9xamWhvTo4RQY\/uYZVjFsZMcYaJVGnxQPUam7Q Qdyw==", + "To": "dockerhub@notifications.domain.tld", + "message-headers": [ + [ + "X-Mailgun-Incoming", + "Yes" + ], + [ + "X-Envelope-From", + "" + ], + [ + "Received", + "from mail-ua0-f180.google.com (mail-ua0-f180.google.com [209.85.217.180]) by mxa.mailgun.org with ESMTP id 57c7437a.7fa164412130-in3; Wed, 31 Aug 2016 20:52:10 -0000 (UTC)" + ], + [ + "Received", + "by mail-ua0-f180.google.com with SMTP id l94so110731231ual.0 for ; Wed, 31 Aug 2016 13:52:09 -0700 (PDT)" + ], + [ + "Dkim-Signature", + "v=1; a=rsa-sha256; c=relaxed\/relaxed; d=espace-win-org.20150623.gappssmtp.com; s=20150623; h=mime-version:from:date:message-id:subject:to; bh=mh8Y6W2HqOMo2y75revb4uvzUOKsWoeabsuU5I2tA6c=; b=Ojs\/SXj3CLGS8ROxh2xUVkXDsYl2mnm0ICZ3FXvMOeny3d5RhwJNKOP7SN+9uEOabQ Jb3RRfa2lslO7KGI\/Te\/y0+JaC7ZHFv3DwEUY\/12hULQLgX06Kn7Af6W7jWsJinvtfmg CFrAOmCxkS16yURsVsgDsrMDbmm6myAVSUKqnvNK7zWG1\/FKJMlH0FRtJFs1C5AuIOKq MLODcNY8bIP0RY9ipr116MAs60WoeCAM4NbSsJacjC4Rbc8RMdWH0Uc2t9C7wlGTIhcS f2V4TIVA1ijvt1R7ePOfrECVyYf\/xHvmQPgAdJ+\/iwv70isSwfgsUW\/V8VEG4HD1LhEo oHqA==" + ], + [ + "X-Google-Dkim-Signature", + "v=1; a=rsa-sha256; c=relaxed\/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=mh8Y6W2HqOMo2y75revb4uvzUOKsWoeabsuU5I2tA6c=; b=XjAi2eyhoKNqT8cY4CSyWDd8YBjtSdvNPCO43SF0kzAUPzH7EI3gc6dqOMmJwXHqgA z5EgS4NaJarYrTIDmV3+w42mpccqv+bqVmFbXWsGSSNEMnW8gaDlVq0ckSwHkbVmogOm Ne447o8wbI5mOSxchuPfRHseD0MN4KWmRiHnpWl7aawA4ADDqB79cSJzn8Z2LCwM3uyw CuoX3UngLt3UURf45xm\/emIsNgdyBkfp6Tdd3BaLHBl3KeeLRJELIu8W+PFYtOogV16T eR6FtOkpXZwT3j9gZ6WbYTzd93uw+j9xamWhvTo4RQY\/uYZVjFsZMcYaJVGnxQPUam7Q Qdyw==" + ], + [ + "X-Gm-Message-State", + "AE9vXwPy42uZleQ\/IOiMoGMd57W\/+fLn5kLJ7VAIgP3r2dslZxtBKwG6QXocW99ySghDDOL\/RqP7Al9CDS3Xsw==" + ], + [ + "X-Received", + "by 10.31.114.140 with SMTP id n134mr6999753vkc.54.1472676729180; Wed, 31 Aug 2016 13:52:09 -0700 (PDT)" + ], + [ + "Mime-Version", + "1.0" + ], + [ + "Received", + "by 10.159.36.111 with HTTP; Wed, 31 Aug 2016 13:52:08 -0700 (PDT)" + ], + [ + "From", + "Docker Notify " + ], + [ + "Date", + "Wed, 31 Aug 2016 22:52:08 +0200" + ], + [ + "Message-Id", + "" + ], + [ + "Subject", + "There's been an issue with your automated build" + ], + [ + "To", + "dockerhub@notifications.domain.tld" + ], + [ + "Content-Type", + "text\/plain; charset=\"UTF-8\"" + ] + ], + "Dkim-Signature": "v=1; a=rsa-sha256; c=relaxed\/relaxed; d=espace-win-org.20150623.gappssmtp.com; s=20150623; h=mime-version:from:date:message-id:subject:to; bh=mh8Y6W2HqOMo2y75revb4uvzUOKsWoeabsuU5I2tA6c=; b=Ojs\/SXj3CLGS8ROxh2xUVkXDsYl2mnm0ICZ3FXvMOeny3d5RhwJNKOP7SN+9uEOabQ Jb3RRfa2lslO7KGI\/Te\/y0+JaC7ZHFv3DwEUY\/12hULQLgX06Kn7Af6W7jWsJinvtfmg CFrAOmCxkS16yURsVsgDsrMDbmm6myAVSUKqnvNK7zWG1\/FKJMlH0FRtJFs1C5AuIOKq MLODcNY8bIP0RY9ipr116MAs60WoeCAM4NbSsJacjC4Rbc8RMdWH0Uc2t9C7wlGTIhcS f2V4TIVA1ijvt1R7ePOfrECVyYf\/xHvmQPgAdJ+\/iwv70isSwfgsUW\/V8VEG4HD1LhEo oHqA==", + "content-id-map": {}, + "subject": "There's been an issue with your automated build", + "stripped-html": "

Hi Jonh Doe,\r\n\r\nThere seems to have been an issue with your Automated Build\r\n\"acme\/foo\" (VCS repository: acme\/docker-foo)\r\nduring the build step.\r\nYou can find more information on\r\nhttps:\/\/hub.docker.com\/r\/acme\/foo\/builds\/abcdef123456\/<\/p>", + "from": "Docker Notify ", + "sender": "no-reply@notify.docker.com", + "stripped-text": "Hi Jonh Doe,\r\n\r\nThere seems to have been an issue with your Automated Build\r\n\"acme\/foo\" (VCS repository: acme\/docker-foo)\r\nduring the build step.\r\nYou can find more information on\r\nhttps:\/\/hub.docker.com\/r\/acme\/foo\/builds\/abcdef123456\/", + "X-Mailgun-Incoming": "Yes", + "X-Received": "by 10.31.114.140 with SMTP id n134mr6999753vkc.54.1472676729180; Wed, 31 Aug 2016 13:52:09 -0700 (PDT)", + "X-Gm-Message-State": "AE9vXwPy42uZleQ\/IOiMoGMd57W\/+fLn5kLJ7VAIgP3r2dslZxtBKwG6QXocW99ySghDDOL\/RqP7Al9CDS3Xsw==", + "attachments": [], + "Mime-Version": "1.0", + "Date": "Wed, 31 Aug 2016 22:52:08 +0200", + "Message-Id": "", + "Content-Type": "text\/plain; charset=\"UTF-8\"", + "body-plain": "Hi Jonh Doe,\r\n\r\nThere seems to have been an issue with your Automated Build\r\n\"acme\/foo\" (VCS repository: acme\/docker-foo)\r\nduring the build step.\r\nYou can find more information on\r\nhttps:\/\/hub.docker.com\/r\/acme\/foo\/builds\/abcdef123456\/", + "Subject": "There's been an issue with your automated build" +}