├── .gitignore ├── Mime ├── Message.php ├── Part.php ├── HeaderBag.php └── Parser.php ├── Tests ├── BehatCustomContext.php ├── BehatTraitContext.php ├── MessageParserTest.php ├── PersonTest.php ├── AbstractTest.php ├── ClientTest.php ├── BehatExtensionTest.php └── MessagePartTest.php ├── Behat ├── MailCatcherAwareInterface.php ├── MailCatcherExtension │ ├── services │ │ └── core.xml │ ├── ContextInitializer.php │ └── Extension.php ├── MailCatcherTrait.php └── MailCatcherContext.php ├── .travis.yml ├── CONTRIBUTORS.md ├── composer.json ├── LICENSE ├── phpunit.xml.dist ├── Person.php ├── CHANGELOG.md ├── i18n └── es.xliff ├── Attachment.php ├── README.md ├── Client.php └── Message.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /Mime/Message.php: -------------------------------------------------------------------------------- 1 | findMail(Message::SUBJECT_CRITERIA, 'Welcome!'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexandresalome/mailcatcher", 3 | "license": "MIT", 4 | "type": "library", 5 | "description": "A library to access MailCatcher", 6 | "autoload": { 7 | "psr-0": { "Alex\\MailCatcher": "" } 8 | }, 9 | "target-dir": "Alex/MailCatcher", 10 | "require": { 11 | "php": ">=5.3.3", 12 | "ext-json": "*", 13 | "symfony/dom-crawler": "~2.3 || ~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0" 14 | }, 15 | 16 | "require-dev": { 17 | "swiftmailer/swiftmailer": "~5.0", 18 | "behat/behat": "~3.0", 19 | "phpunit/phpunit": "~4.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexandre Salomé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Tests/MessageParserTest.php: -------------------------------------------------------------------------------- 1 | parsePart($message); 19 | 20 | $this->assertEquals('Hello world', $content, 'content is "Hello world"'); 21 | $this->assertEquals('bar', $headers->get('Foo')); 22 | $this->assertEquals('baz', $headers->get('Bar')); 23 | } 24 | 25 | public function testHeaderMultiline() 26 | { 27 | $message = <<parsePart($message); 39 | 40 | $this->assertEquals('Hello world', $content, 'content is "Hello world"'); 41 | $this->assertEquals('barbaz', $headers->get('Foo')); 42 | $this->assertEquals('foobarbaz', $headers->get('Bar')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/PersonTest.php: -------------------------------------------------------------------------------- 1 | ', 'foo', 'foo@example.org'), 21 | array('', null, 'foo@example.org'), 22 | ); 23 | } 24 | 25 | /** 26 | * @dataProvider provideIncorrect 27 | * @expectedException \InvalidArgumentException 28 | */ 29 | public function testIncorrect($string) 30 | { 31 | Person::createFromString($string); 32 | } 33 | 34 | /** 35 | * @dataProvider provideCorrect 36 | */ 37 | public function testCorrect($string, $name, $email) 38 | { 39 | $person = Person::createFromString($string); 40 | 41 | $this->assertEquals($email, $person->getEmail(), "Email is correct"); 42 | $this->assertEquals($name, $person->getName(), "Name is correct"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | Tests 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | . 30 | 31 | Tests 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Behat/MailCatcherExtension/services/core.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Alex\MailCatcher\Client 8 | Alex\MailCatcher\Behat\MailCatcherExtension\ContextInitializer 9 | 10 | 11 | 12 | 13 | 14 | %behat.mailcatcher.client.url% 15 | 16 | 17 | 18 | 19 | %behat.mailcatcher.purge_before_scenario% 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Behat/MailCatcherTrait.php: -------------------------------------------------------------------------------- 1 | mailCatcherClient = $client; 22 | } 23 | 24 | /** 25 | * Returns the mailcatcher client. 26 | * 27 | * @return Client 28 | * 29 | * @throws \RuntimeException client if missing from context 30 | */ 31 | public function getMailCatcherClient() 32 | { 33 | if (null === $this->mailCatcherClient) { 34 | throw new \RuntimeException(sprintf('No MailCatcher client injected.')); 35 | } 36 | 37 | return $this->mailCatcherClient; 38 | } 39 | 40 | /** 41 | * @return Message 42 | */ 43 | protected function findMail($type, $value) 44 | { 45 | $criterias = array($type => $value); 46 | 47 | $message = $this->getMailCatcherClient()->searchOne($criterias); 48 | 49 | if (null === $message) { 50 | throw new \InvalidArgumentException(sprintf('Unable to find a message with criterias "%s".', json_encode($criterias))); 51 | } 52 | 53 | return $message; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Behat/MailCatcherExtension/ContextInitializer.php: -------------------------------------------------------------------------------- 1 | client = $client; 23 | $this->purgeBeforeScenario = $purgeBeforeScenario; 24 | } 25 | 26 | /** 27 | * @param Context $context 28 | * 29 | * @return bool 30 | */ 31 | public function supports(Context $context) 32 | { 33 | return $context instanceof MailCatcherAwareInterface || $context instanceof MailCatcherContext; 34 | } 35 | 36 | /** 37 | * @param Context $context 38 | */ 39 | public function initializeContext(Context $context) 40 | { 41 | if ($context instanceof MailCatcherAwareInterface) { 42 | $context->setMailCatcherClient($this->client); 43 | } 44 | 45 | if ($context instanceof MailCatcherContext) { 46 | $context->setMailCatcherConfiguration($this->purgeBeforeScenario); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Person.php: -------------------------------------------------------------------------------- 1 | name = null === $name ? null : (string) $name; 17 | $this->email = null === $email ? null : (string) $email; 18 | } 19 | 20 | /** 21 | * @param $text 22 | * 23 | * @return bool 24 | */ 25 | public function match($text) 26 | { 27 | return false !== strpos((string) $this->name, $text) || false !== strpos((string) $this->email, $text); 28 | } 29 | 30 | /** 31 | * @return null|string 32 | */ 33 | public function getName() 34 | { 35 | return $this->name; 36 | } 37 | 38 | /** 39 | * @return null|string 40 | */ 41 | public function getEmail() 42 | { 43 | return $this->email; 44 | } 45 | 46 | /** 47 | * @param Person $person 48 | * 49 | * @return bool 50 | */ 51 | public function equals(Person $person) 52 | { 53 | return $person->getName() === $this->name && $person->getEmail() === $this->email; 54 | } 55 | 56 | /** 57 | * @param $string 58 | * 59 | * @return Person 60 | */ 61 | public static function createFromString($string) 62 | { 63 | if (preg_match('/^(?:(.+) )?<(.+)>$/', $string, $vars)) { 64 | $name = $vars[1] === '' ? null : $vars[1]; 65 | $email = $vars[2] === '' ? null : $vars[2]; 66 | return new Person($name, $email); 67 | } 68 | 69 | throw new \InvalidArgumentException(sprintf('Unable to parse Person "%s".', $string)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGLOG 2 | 3 | ## 1.3.0 (04/08/2020) 4 | 5 | - [bug] #28 - seeInMail($text): display content in the exception (Alexis Lefebvre) 6 | - [feature] #36 - Support for Symfony5 (Vincent Moulene) 7 | - [bug] #27 - Support for multipart with parameters (Nirbhay Patil) 8 | - [bug] Support for mailcatcher 1.7 (Alexandre Salomé) 9 | 10 | ## 1.2.0 (08/07/2016) 11 | 12 | * [feature] Adds a Trait to ease the construction of custom Behat classes (Alexandre Salomé) 13 | * [feature] Allow connection error when purging before scenario (Alexandre Salomé) 14 | * [feature] Test on Travis against multiple PHP versions (Alexandre Salomé) 15 | 16 | ## 1.1.1 (17/03/2016) 17 | 18 | * [feature] #23 - support for Symfony3 (OwlyCode) 19 | 20 | ## 1.1.0 (18/01/2016) 21 | 22 | * [feature] #21 - Multi-language support for Behat steps (Sergio Gómez) 23 | * [feature] #19 - Add support for multipart/related to parser (Nicolas Hart) 24 | * [feature] #18 - Added extra assertions to validate mail content without opening it (Joeri Timmermans) 25 | 26 | ## 1.0.2 (13/10/2015) 27 | 28 | * [bug] #17 - URL cant be changed because the service is not used right (Joeri Timmermans) 29 | 30 | ## 1.0.0 - (06/07/2015) 31 | 32 | * [feature] Support from Symfony 2.3 (Francois PAULIN) 33 | * [feature] Support for PHP 5.3 (πR) 34 | * [feature] #9 - Support for Behat 3 (Igor, Ilya Troy) 35 | 36 | ## 0.3.0 (19/01/2015) 37 | 38 | * [feature] #8 - Ability to delete single message (Garanzha Dmitriy) 39 | * [feature] #3 - Large parsing support for mail (Matt Parker) 40 | 41 | ## 0.2.0 (08/10/2013) 42 | 43 | * [feature] Proper error handling of the server (Alexandre Salomé) 44 | 45 | ## 0.1.0 (09/07/2013) 46 | 47 | * [feature] Initial version of the MailCatcher library (Alexandre Salomé) 48 | * [feature] Behat extension (Alexandre Salomé) 49 | -------------------------------------------------------------------------------- /Tests/AbstractTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('mailcatcher HTTP missing'); 13 | } 14 | 15 | return new Client($_SERVER['MAILCATCHER_HTTP']); 16 | } 17 | 18 | public function sendMessage(\Swift_Mime_MimePart $message) 19 | { 20 | if (!isset($_SERVER['MAILCATCHER_SMTP'])) { 21 | $this->markTestSkipped('mailcatcher SMTP missing'); 22 | } 23 | 24 | if (!preg_match('#^smtp://(?P[^:]+):(?P\d+)$#', $_SERVER['MAILCATCHER_SMTP'], $vars)) { 25 | throw new \InvalidArgumentException(sprintf('SMTP URL malformatted. Expected smtp://host:port, got "%s".', $_SERVER['MAILCATCHER_SMTP'])); 26 | } 27 | 28 | $host = $vars['host']; 29 | $port = $vars['port']; 30 | 31 | static $mailer; 32 | if (null === $mailer) { 33 | $transport = \Swift_SmtpTransport::newInstance($host, $port); 34 | $mailer = \Swift_Mailer::newInstance($transport); 35 | } 36 | 37 | if (!$mailer->send($message)) { 38 | throw new \RuntimeException('Unable to send message'); 39 | } 40 | } 41 | 42 | public function createFixtures() 43 | { 44 | $client = $this->getClient(); 45 | $client->purge(); 46 | 47 | for ($i = 1; $i <= 7; $i++) { 48 | 49 | // 7 = 2 x 3 + 1 50 | $detail = floor($i/3).' x 3 + '.($i - floor($i/3)*3); 51 | 52 | $message = \Swift_Message::newInstance() 53 | ->setSubject($i.' = '.$detail) 54 | ->setFrom(array('foo'.$i.'@example.org' => 'Foo '.$detail)) 55 | ->setTo(array('bar@example.org' => 'Bar')) 56 | ->setBody('Bazinga! '.$detail) 57 | ; 58 | 59 | $this->sendMessage($message); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /i18n/es.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | \d+) mails? should be sent$/]]> 47 | \d+) correos? deberían haber sido enviados/]]> 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Attachment.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Attachment 11 | { 12 | /** 13 | * @var Client 14 | */ 15 | protected $client; 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $filename; 21 | 22 | /** 23 | * @var int 24 | */ 25 | protected $size; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $type; 31 | 32 | /** 33 | * Attachment CID 34 | * 35 | * @var string 36 | */ 37 | protected $cid; 38 | 39 | /** 40 | * Href to download attachment. 41 | * 42 | * @var string 43 | */ 44 | protected $href; 45 | 46 | /** 47 | * Content of attachment. 48 | * 49 | * @var string 50 | */ 51 | protected $content; 52 | 53 | /** 54 | * Constructor. 55 | * 56 | * @param Client $client 57 | * @param array $data 58 | */ 59 | public function __construct(Client $client, array $data = array()) 60 | { 61 | $this->client = $client; 62 | $this->loadFromArray($data); 63 | } 64 | 65 | /** 66 | * Loads data into the Attachment from an array. 67 | * 68 | * @param array $array 69 | * 70 | * @return Attachment 71 | */ 72 | public function loadFromArray(array $array) 73 | { 74 | if (isset($array['filename'])) { 75 | $this->filename = $array['filename']; 76 | } 77 | 78 | if (isset($array['size'])) { 79 | $this->size = $array['size']; 80 | } 81 | 82 | if (isset($array['type'])) { 83 | $this->type = $array['type']; 84 | } 85 | 86 | if (isset($array['cid'])) { 87 | $this->cid = $array['cid']; 88 | } 89 | 90 | if (isset($array['href'])) { 91 | $this->href = $array['href']; 92 | } 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Returns filename. 99 | * 100 | * @return string 101 | */ 102 | public function getFilename() 103 | { 104 | return $this->filename; 105 | } 106 | 107 | /** 108 | * Returns size. 109 | * 110 | * @return int 111 | */ 112 | public function getSize() 113 | { 114 | return $this->size; 115 | } 116 | 117 | /** 118 | * Returns type. 119 | * 120 | * @return string 121 | */ 122 | public function getType() 123 | { 124 | return $this->type; 125 | } 126 | 127 | /** 128 | * Returns CID. 129 | * 130 | * @return string 131 | */ 132 | public function getCid() 133 | { 134 | return $this->cid; 135 | } 136 | 137 | /** 138 | * Returns HREF. 139 | * 140 | * @return string 141 | */ 142 | public function getHref() 143 | { 144 | return $this->href; 145 | } 146 | 147 | /** 148 | * Returns content, raw format. 149 | * 150 | * @return string 151 | */ 152 | public function getContent() 153 | { 154 | if (null === $this->content) { 155 | $this->content = $this->client->requestRaw('GET', $this->href); 156 | } 157 | 158 | return $this->content; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Behat/MailCatcherExtension/Extension.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Extension implements ExtensionInterface 22 | { 23 | const MAILCATCHER_ID = 'mailcatcher'; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function load(ContainerBuilder $container, array $config) 29 | { 30 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/services')); 31 | $loader->load('core.xml'); 32 | 33 | $this->loadContextInitializer($container); 34 | 35 | $container->setParameter('behat.mailcatcher.client.url', $config['url']); 36 | $container->setParameter('behat.mailcatcher.purge_before_scenario', $config['purge_before_scenario']); 37 | } 38 | 39 | /** 40 | * @param ContainerBuilder $container 41 | */ 42 | private function loadContextInitializer(ContainerBuilder $container) 43 | { 44 | $definition = new Definition('Alex\MailCatcher\Behat\MailCatcherExtension\ContextInitializer', array( 45 | new Reference(self::MAILCATCHER_ID), 46 | '%behat.mailcatcher.purge_before_scenario%' 47 | )); 48 | $definition->addTag(ContextExtension::INITIALIZER_TAG, array('priority' => 0)); 49 | $container->setDefinition('mailcatcher.context_initializer', $definition); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function configure(ArrayNodeDefinition $builder) 56 | { 57 | $builder 58 | ->children() 59 | ->booleanNode('purge_before_scenario')->defaultTrue()->end() 60 | ->scalarNode('url')->defaultValue('http://localhost:1080')->end() 61 | ->end() 62 | ; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function getCompilerPasses() 69 | { 70 | return array(); 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | protected function loadEnvironmentConfiguration() 77 | { 78 | $config = array(); 79 | 80 | if ($url = getenv('MAILCATCHER_URL')) { 81 | $config['url'] = $url; 82 | } 83 | 84 | return $config; 85 | } 86 | 87 | /** 88 | * {@inheritDoc} 89 | */ 90 | public function getConfigKey() 91 | { 92 | return 'mailcatcher'; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function initialize(ExtensionManager $extensionManager) 99 | { 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | public function process(ContainerBuilder $container) 106 | { 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Mime/Part.php: -------------------------------------------------------------------------------- 1 | headers, $this->content) = $parser->parsePart($source); 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getContent() 35 | { 36 | return $this->content; 37 | } 38 | 39 | /** 40 | * @return HeaderBag 41 | */ 42 | public function getHeaders() 43 | { 44 | return $this->headers; 45 | } 46 | 47 | /** 48 | * @return boolean 49 | */ 50 | public function isMultipart() 51 | { 52 | if (null === $this->parts) { 53 | $this->loadParts(); 54 | } 55 | 56 | return false !== $this->parts; 57 | } 58 | 59 | /** 60 | * @return null 61 | */ 62 | public function getParts() 63 | { 64 | if (null === $this->parts) { 65 | $this->loadParts(); 66 | } 67 | 68 | if (false === $this->parts) { 69 | throw new \RuntimeException('Can\'t get parts: message is not multipart'); 70 | } 71 | 72 | return $this->parts; 73 | } 74 | 75 | /** 76 | * @param $type 77 | * 78 | * @return bool 79 | */ 80 | public function hasPart($type) 81 | { 82 | try { 83 | $this->getPart($type); 84 | 85 | return true; 86 | } catch (\InvalidArgumentException $e) { 87 | return false; 88 | } 89 | } 90 | 91 | /** 92 | * @param $type 93 | * 94 | * @return mixed 95 | */ 96 | public function getPart($type) 97 | { 98 | $parts = $this->getParts(); 99 | 100 | foreach ($parts as $part) { 101 | if (0 === strpos($part->getHeaders()->get('Content-Type'), $type)) { 102 | return $part; 103 | } 104 | } 105 | 106 | throw new \InvalidArgumentException(sprintf('Unable to find part with Content-Type "%s" in parts. Got: %s', $type, implode("", array_map(function ($part) { 107 | return "\n- ".$part->getHeaders()->get('Content-Type'); 108 | }, $parts)))); 109 | } 110 | 111 | /** 112 | * 113 | */ 114 | private function loadParts() 115 | { 116 | $content = $this->getContent(); 117 | $headers = $this->getHeaders(); 118 | 119 | if (null === $content || null === $headers) { 120 | throw new \RuntimeException('Unable to load part: no content or headers set in message.'); 121 | } 122 | 123 | $contentType = $headers->get('Content-Type'); 124 | if (0 !== strpos($contentType, 'multipart')) { 125 | $this->parts = false; 126 | 127 | return; 128 | } 129 | 130 | if (!preg_match('#^multipart/(alternative|mixed|related);.*boundary="?([^"]*)"?$#', $contentType, $vars)) { 131 | throw new \InvalidArgumentException(sprintf('Unable to parse multipart header: "%s".', $contentType)); 132 | } 133 | 134 | $parser = new Parser(); 135 | $this->parts = $parser->parseBoundary($content, $vars[2]); 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | getClient(); 10 | $client->purge(); 11 | $this->assertEquals(0, $client->getMessageCount()); 12 | 13 | $message = \Swift_Message::newInstance() 14 | ->setSubject('Hello') 15 | ->setFrom(array('foo@example.org' => 'Foo')) 16 | ->setTo(array('bar@example.org' => 'Bar')) 17 | ->setBody('Baz') 18 | ; 19 | 20 | $this->sendMessage($message); 21 | $this->assertEquals(1, $client->getMessageCount()); 22 | $this->sendMessage($message); 23 | $this->sendMessage($message); 24 | $this->assertEquals(3, $client->getMessageCount()); 25 | 26 | $client->purge(); 27 | $this->assertEquals(0, $client->getMessageCount()); 28 | } 29 | 30 | public function testSearch() 31 | { 32 | $client = $this->getClient(); 33 | $this->createFixtures(); 34 | 35 | // searchOne 36 | $message = $client->searchOne(array('subject' => '3 =')); 37 | $this->assertInstanceOf('Alex\MailCatcher\Message', $message); 38 | $this->assertEquals('3 = 1 x 3 + 0', $message->getSubject()); 39 | 40 | // search 41 | $messages = $client->search(); 42 | $this->assertCount(7, $messages); 43 | 44 | // search: contains 45 | $messages = $client->search(array('contains' => '+ 1')); 46 | $this->assertCount(3, $messages); 47 | } 48 | 49 | public function testAttachment() 50 | { 51 | $client = $this->getClient(); 52 | $client->purge(); 53 | 54 | $message = \Swift_Message::newInstance() 55 | ->setSubject('Hello') 56 | ->setFrom(array('foo@example.org' => 'Foo')) 57 | ->setTo(array('bar@example.org' => 'Bar')) 58 | ->setBody('Baz') 59 | ->attach(new \Swift_Attachment('foobar', 'foo.txt', 'text/plain')) 60 | ; 61 | 62 | $this->sendMessage($message); 63 | 64 | $message = $client->searchOne(); 65 | 66 | $this->assertInstanceOf('Alex\MailCatcher\Message', $message, "message exists"); 67 | $this->assertTrue($message->hasAttachments(), "message has attachments"); 68 | 69 | $attachments = $message->getAttachments(); 70 | $attachment = $attachments[0]; 71 | 72 | $this->assertEquals('foo.txt', $attachment->getFilename(), "attachment filename is correct"); 73 | $this->assertEquals(6, $attachment->getSize(), "attachment size is correct"); 74 | $this->assertEquals('text/plain', $attachment->getType(), "attachment type is correct"); 75 | $this->assertEquals('foobar', $attachment->getContent(), 'attachment content is correct'); 76 | } 77 | 78 | public function testMultipart() 79 | { 80 | $client = $this->getClient(); 81 | $client->purge(); 82 | 83 | $message = \Swift_Message::newInstance() 84 | ->setSubject('Multipart') 85 | ->setFrom(array('foo@example.org' => 'Foo')) 86 | ->setTo(array('bar@example.org' => 'Bar')) 87 | ->addPart($html = str_repeat('

foo

', 30), 'text/html') 88 | ->addPart($text = str_repeat('foo ', 50), 'text/plain') 89 | ; 90 | 91 | $this->sendMessage($message); 92 | 93 | $message = $client->searchOne(); 94 | 95 | $this->assertTrue($message->isMultipart()); 96 | $this->assertEquals($html, $message->getPart('text/html')->getContent()); 97 | $this->assertEquals($text, $message->getPart('text/plain')->getContent()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/BehatExtensionTest.php: -------------------------------------------------------------------------------- 1 | runBehat(array( 14 | "When I do something", 15 | ), true, true); 16 | } 17 | 18 | public function testTrait() 19 | { 20 | if (version_compare(PHP_VERSION, "5.4") < 0) { 21 | $this->markTestSkipped("PHP version not supported"); 22 | } 23 | $this->getClient()->purge(); 24 | 25 | $this->sendMessage(\Swift_Message::newInstance() 26 | ->setSubject('Welcome!') 27 | ->setFrom('world@example.org') 28 | ->setTo('mailcatcher@example.org') 29 | ->setBody('This is a message from world to mailcatcher') 30 | ); 31 | 32 | $this->runBehat(array( 33 | "Then a welcome mail should be sent", 34 | )); 35 | } 36 | 37 | public function testCriterias() 38 | { 39 | $this->getClient()->purge(); 40 | 41 | $this->sendMessage(\Swift_Message::newInstance() 42 | ->setSubject('hello mailcatcher') 43 | ->setFrom('world@example.org') 44 | ->setTo('mailcatcher@example.org') 45 | ->setBody('This is a message from world to mailcatcher') 46 | ); 47 | 48 | $this->sendMessage(\Swift_Message::newInstance() 49 | ->setSubject('hello php') 50 | ->setFrom('world@example.org') 51 | ->setTo('php@example.org') 52 | ->setBody('This is a message from world to php') 53 | ); 54 | 55 | $this->runBehat(array( 56 | 57 | "Then 2 mails should be sent", 58 | "Then 2 mail should be sent", 59 | 60 | // Criteria "from" 61 | 'When I open mail from "world@example.org"', 62 | 'Then I should see "from world" in mail', 63 | 64 | // Criteria "to" 65 | 'When I open mail to "mailcatcher@example.org"', 66 | 'Then I should see "from world to mailcatcher" in mail', 67 | 68 | // Criteria "containing" 69 | 'When I open mail containing "to mailcatcher"', 70 | 'Then I should see "from world to mailcatcher" in mail', 71 | 72 | // Criteria "with subject" 73 | 'When I open mail with subject "hello mailcatcher"', 74 | 'Then I should see "from world to mailcatcher" in mail', 75 | )); 76 | 77 | $this->runBehat(array( 78 | "Then 0 mails should be sent" 79 | ), true); 80 | 81 | } 82 | 83 | private function runBehat($steps, $purge_before_scenario = false, $failServer = false) 84 | { 85 | $client = $this->getClient(); 86 | 87 | $file = tempnam(sys_get_temp_dir(), 'mailcatcher_'); 88 | unlink($file); 89 | $configFile = $file.'.yml'; 90 | $outputFile = $file.'.output'; 91 | $file = $file.'.feature'; 92 | $content = "Feature: Test\n\n Scenario: Test\n ".implode("\n ", $steps)."\n"; 93 | 94 | $contexts = array( 95 | 'Alex\MailCatcher\Behat\MailCatcherContext', 96 | 'Alex\MailCatcher\Tests\BehatCustomContext', 97 | ); 98 | 99 | if (version_compare(PHP_VERSION, "5.4") > 0) { 100 | $contexts[] = 'Alex\MailCatcher\Tests\BehatTraitContext'; 101 | } 102 | 103 | $config = json_encode(array( 104 | 'default' => array( 105 | 'suites' => array( 106 | 'default' => array( 107 | 'paths' => array(sys_get_temp_dir()), 108 | 'contexts' => $contexts, 109 | ), 110 | ), 111 | 'extensions' => array( 112 | 'Alex\MailCatcher\Behat\MailCatcherExtension\Extension' => array( 113 | 'url' => $failServer ? 'http://localhost:1337' : $client->getUrl(), 114 | 'purge_before_scenario' => $purge_before_scenario 115 | ), 116 | ) 117 | ) 118 | )); 119 | 120 | try { 121 | $application = new ApplicationFactory(); 122 | $behat = $application->createApplication(); 123 | $behat->setAutoExit(false); 124 | 125 | chdir(sys_get_temp_dir()); 126 | $input = new ArgvInput(array('behat', '--format', 'progress', '--config', $configFile, '--out', $outputFile, $file)); 127 | $output = new BufferedOutput(); 128 | 129 | file_put_contents($file, $content); 130 | file_put_contents($configFile, $config); 131 | $result = $behat->run($input, $output); 132 | unlink($file); 133 | unlink($configFile); 134 | 135 | } catch (\Exception $exception) { 136 | unlink($file); 137 | unlink($file.'.config'); 138 | $this->fail($exception->getMessage()); 139 | $result = null; 140 | } 141 | 142 | if ($result !== 0) { 143 | $fileOutput = file_exists($outputFile) ? file_get_contents($outputFile) : '*no file*'; 144 | $this->fail(sprintf("Behat execution finished with status %d (expected 0).\nConsole output:\n===============\n%s\n\nOutput file:\n============\n%s", $result, $output->fetch(), $fileOutput)); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Mime/HeaderBag.php: -------------------------------------------------------------------------------- 1 | headers = array(); 20 | foreach ($headers as $key => $values) { 21 | $this->set($key, $values); 22 | } 23 | } 24 | 25 | /** 26 | * Returns the headers. 27 | * 28 | * @return array An array of headers 29 | */ 30 | public function all() 31 | { 32 | return $this->headers; 33 | } 34 | 35 | /** 36 | * Returns the parameter keys. 37 | * 38 | * @return array An array of parameter keys 39 | */ 40 | public function keys() 41 | { 42 | return array_keys($this->headers); 43 | } 44 | 45 | /** 46 | * Replaces the current HTTP headers by a new set. 47 | * 48 | * @param array $headers An array of HTTP headers 49 | */ 50 | public function replace(array $headers = array()) 51 | { 52 | $this->headers = array(); 53 | $this->add($headers); 54 | } 55 | 56 | /** 57 | * Adds new headers the current HTTP headers set. 58 | * 59 | * @param array $headers An array of HTTP headers 60 | */ 61 | public function add(array $headers) 62 | { 63 | foreach ($headers as $key => $values) { 64 | $this->set($key, $values); 65 | } 66 | } 67 | 68 | /** 69 | * Returns a header value by name. 70 | * 71 | * @param string $key The header name 72 | * @param mixed $default The default value 73 | * @param Boolean $first Whether to return the first value or all header values 74 | * 75 | * @return string|array The first header value if $first is true, an array of values otherwise 76 | */ 77 | public function get($key, $default = null, $first = true) 78 | { 79 | $key = strtr(strtolower($key), '_', '-'); 80 | 81 | if (!array_key_exists($key, $this->headers)) { 82 | if (null === $default) { 83 | return $first ? null : array(); 84 | } 85 | 86 | return $first ? $default : array($default); 87 | } 88 | 89 | if ($first) { 90 | return count($this->headers[$key]) ? $this->headers[$key][0] : $default; 91 | } 92 | 93 | return $this->headers[$key]; 94 | } 95 | 96 | /** 97 | * Sets a header by name. 98 | * 99 | * @param string $key The key 100 | * @param string|array $values The value or an array of values 101 | * @param Boolean $replace Whether to replace the actual value or not (true by default) 102 | */ 103 | public function set($key, $values, $replace = true) 104 | { 105 | $key = strtr(strtolower($key), '_', '-'); 106 | 107 | $values = array_values((array) $values); 108 | 109 | if (true === $replace || !isset($this->headers[$key])) { 110 | $this->headers[$key] = $values; 111 | } else { 112 | $this->headers[$key] = array_merge($this->headers[$key], $values); 113 | } 114 | 115 | if ('cache-control' === $key) { 116 | $this->cacheControl = $this->parseCacheControl($values[0]); 117 | } 118 | } 119 | 120 | /** 121 | * Returns true if the HTTP header is defined. 122 | * 123 | * @param string $key The HTTP header 124 | * 125 | * @return Boolean true if the parameter exists, false otherwise 126 | */ 127 | public function has($key) 128 | { 129 | return array_key_exists(strtr(strtolower($key), '_', '-'), $this->headers); 130 | } 131 | 132 | /** 133 | * Returns true if the given HTTP header contains the given value. 134 | * 135 | * @param string $key The HTTP header name 136 | * @param string $value The HTTP value 137 | * 138 | * @return Boolean true if the value is contained in the header, false otherwise 139 | */ 140 | public function contains($key, $value) 141 | { 142 | return in_array($value, $this->get($key, null, false)); 143 | } 144 | 145 | /** 146 | * Removes a header. 147 | * 148 | * @param string $key The HTTP header name 149 | */ 150 | public function remove($key) 151 | { 152 | $key = strtr(strtolower($key), '_', '-'); 153 | 154 | unset($this->headers[$key]); 155 | 156 | if ('cache-control' === $key) { 157 | $this->cacheControl = array(); 158 | } 159 | } 160 | 161 | /** 162 | * Returns the HTTP header value converted to a date. 163 | * 164 | * @param string $key The parameter key 165 | * @param \DateTime $default The default value 166 | * 167 | * @return null|\DateTime The parsed DateTime or the default value if the header does not exist 168 | * 169 | * @throws \RuntimeException When the HTTP header is not parseable 170 | */ 171 | public function getDate($key, \DateTime $default = null) 172 | { 173 | if (null === $value = $this->get($key)) { 174 | return $default; 175 | } 176 | 177 | if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) { 178 | throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value)); 179 | } 180 | 181 | return $date; 182 | } 183 | 184 | /** 185 | * Returns an iterator for headers. 186 | * 187 | * @return \ArrayIterator An \ArrayIterator instance 188 | */ 189 | #[\ReturnTypeWillChange] 190 | public function getIterator() 191 | { 192 | return new \ArrayIterator($this->headers); 193 | } 194 | 195 | /** 196 | * Returns the number of headers. 197 | * 198 | * @return int The number of headers 199 | */ 200 | #[\ReturnTypeWillChange] 201 | public function count() 202 | { 203 | return count($this->headers); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MailCatcher for PHP 2 | 3 | ![Build status](https://travis-ci.org/alexandresalome/mailcatcher.png?branch=master) [![Latest Stable Version](https://poser.pugx.org/alexandresalome/mailcatcher/v/stable)](https://packagist.org/packages/alexandresalome/mailcatcher) [![Total Downloads](https://poser.pugx.org/alexandresalome/mailcatcher/downloads)](https://packagist.org/packages/alexandresalome/mailcatcher) [![License](https://poser.pugx.org/alexandresalome/mailcatcher/license)](https://packagist.org/packages/alexandresalome/mailcatcher) [![Monthly Downloads](https://poser.pugx.org/alexandresalome/mailcatcher/d/monthly)](https://packagist.org/packages/alexandresalome/mailcatcher) [![Daily Downloads](https://poser.pugx.org/alexandresalome/mailcatcher/d/daily)](https://packagist.org/packages/alexandresalome/mailcatcher) 4 | 5 | Integrates [MailCatcher](http://mailcatcher.me) in your PHP application. 6 | 7 | * [View CHANGELOG](CHANGELOG.md) 8 | * [View CONTRIBUTORS](CONTRIBUTORS.md) 9 | 10 | MailCatcher is a simple SMTP server with an HTTP API, and this library aims to 11 | integrate it to make it easy to use it with PHP. 12 | 13 | ## Behat extension 14 | 15 | This library provides a Behat extension to help you test mails in your application. 16 | 17 | To use it, you first need to be sure [MailCatcher](http://mailcatcher.me) is 18 | properly installed and running. You can use docker to execute it: 19 | 20 | ```bash 21 | docker run -d -p 1080:1080 -p 1025:1025 --name mailcatcher schickling/mailcatcher 22 | ``` 23 | 24 | First, configure in your ``behat.yml``: 25 | 26 | ```yaml 27 | default: 28 | extensions: 29 | Alex\MailCatcher\Behat\MailCatcherExtension\Extension: 30 | url: http://localhost:1080 31 | purge_before_scenario: true 32 | ``` 33 | 34 | Then, add the **MailCatcherContext** context in your **FeatureContext** file: 35 | 36 | ```php 37 | use Alex\MailCatcher\Behat\MailCatcherContext; 38 | use Behat\Behat\Context\BehatContext; 39 | 40 | class FeatureContext extends BehatContext 41 | { 42 | public function __construct(array $parameters) 43 | { 44 | $this->useContext('mailcatcher', new MailCatcherContext()); 45 | } 46 | } 47 | ``` 48 | 49 | ### Available steps 50 | 51 | This extension provides you mail context in your tests. To use assertions, you 52 | must first **open a mail** using criterias. 53 | 54 | Once it's opened, you can make **assertions** on it and **click** in it. 55 | 56 | **Server manipulation** 57 | 58 | Deletes all messages on the server 59 | 60 | * When I purge mails 61 | 62 | **Mail opening** 63 | 64 | * When I open mail from "**foo@example.org**" 65 | * When I open mail containing "**a message**" 66 | * When I open mail to "**me@example.org**" 67 | * When I open mail with subject "**Welcome, mister Bond!**" 68 | 69 | **Assertion** 70 | 71 | Verify number of messages sent to the server: 72 | 73 | * Then **1** mail should be sent 74 | * Then **13** mails should be sent 75 | 76 | Verify text presence in message: 77 | 78 | * Then I should see "**something**" in mail 79 | * Then I should see "**something else**" in mail 80 | 81 | Verify text presence in mail without opening: 82 | 83 | * Then I should see mail from "**foo@example.org**" 84 | * Then I should see mail containing "**a message**" 85 | * Then I should see mail to "**me@example.org**" 86 | * Then I should see mail with subject "**Welcome, mister Bond!**" 87 | 88 | ### Custom mailcatcher context 89 | 90 | **Only available from PHP 5.4** 91 | 92 | If you want to create a context class that relates to MailCatcher, you can use the **MailCatcherTrait** to get the mailcatcher client injected inside your class: 93 | 94 | ```php 95 | use Alex\MailCatcher\Behat\MailCatcherAwareInterface; 96 | use Alex\MailCatcher\Behat\MailCatcherTrait; 97 | use Alex\MailCatcher\Message; 98 | use Behat\Behat\Context\Context; 99 | 100 | class WelcomeContext implements Context, MailCatcherAwareInterface 101 | { 102 | use MailCatcherTrait; 103 | 104 | /** 105 | * @Then /^a welcome mail should be sent$/ 106 | */ 107 | public function testTrait() 108 | { 109 | $this->findMail(Message::SUBJECT_CRITERIA, 'Welcome!'); 110 | } 111 | } 112 | ``` 113 | 114 | This trait offers the following methods: 115 | 116 | * **getMailCatcherClient()**: returns the mailcatcher **Client** instance. 117 | * **findMail($criteria, $value)**: facility to search for a given message, or throws an exception if not found 118 | 119 | **Don't forget** to implement the **MailCatcherAwareInterface** to get the mailcatcher client injected inside your context class. 120 | 121 | ## Client API 122 | 123 | Browse easily your API with the integrated SDK: 124 | 125 | ```php 126 | $client = new Alex\MailCatcher\Client('http://localhost:1080'); 127 | 128 | // Returns all messages 129 | $messages = $client->search(); 130 | 131 | // Count messages 132 | $client->getMessageCount(); 133 | 134 | // Filter messages 135 | $messages = $client->search(array( 136 | 'from' => 'bob@example.org', 137 | 'to' => 'alice@example.org', 138 | 'subject' => 'Bla', 139 | 'contains' => 'Hello', 140 | 'attachments' => true, 141 | 'format' => 'html', 142 | ), $limit = 3); 143 | 144 | // Search one message 145 | $message = $client->searchOne(array('subject' => 'Welcome')); 146 | ``` 147 | 148 | **Message API** 149 | 150 | ```php 151 | // Message API, get the content of a message 152 | $subject = $message->getSubject(); 153 | $plainTextBody = $message->getPart('text/plain')->getContent(); 154 | $htmlBody = $message->getPart('text/html')->getContent(); 155 | 156 | // Message API, return a Person object or an array of Person object 157 | $person = $message->getFrom(); 158 | $persons = $message->getRecipients(); 159 | 160 | // Person API 161 | $person = $message->getFrom(); 162 | 163 | $name = $person->getName(); // null means not provided 164 | $mail = $person->getMail(); 165 | 166 | // Attachments 167 | $message->hasAttachments(); 168 | $message->getAttachments(); 169 | 170 | // Delete 171 | $message->delete(); 172 | ``` 173 | 174 | **Attachment API** 175 | 176 | ```php 177 | // Attachment API 178 | $attachment->getFilename(); 179 | $attachment->getSize(); 180 | $attachment->getType(); 181 | $attachment->getContent(); 182 | ``` 183 | -------------------------------------------------------------------------------- /Client.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Client 11 | { 12 | /** 13 | * @var string 14 | */ 15 | protected $url; 16 | 17 | /** 18 | * @var array 19 | */ 20 | protected $messages = array(); 21 | 22 | /** 23 | * Creates a new client. 24 | * 25 | * @param string $url url of the server 26 | */ 27 | public function __construct($url = 'http://localhost:1080') 28 | { 29 | $this->url = $url; 30 | } 31 | 32 | /** 33 | * Deletes all messages on server. 34 | * 35 | * @return Client 36 | */ 37 | public function purge() 38 | { 39 | $this->request('DELETE'); 40 | $this->messages = array(); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return string URL of server used by the client 47 | */ 48 | public function getUrl() 49 | { 50 | return $this->url; 51 | } 52 | 53 | /** 54 | * Returns the number of messages on the server. 55 | * 56 | * @return int 57 | */ 58 | public function getMessageCount() 59 | { 60 | return count($this->request('GET')); 61 | } 62 | 63 | /** 64 | * Searches for one messages on the server. 65 | * 66 | * See method `Message::match` method for more informations on criterias. 67 | * 68 | * @param array $criterias 69 | * 70 | * @return Message|null 71 | */ 72 | public function searchOne(array $criterias = array()) 73 | { 74 | $results = $this->search($criterias, 1); 75 | 76 | if (count($results) !== 1) { 77 | return null; 78 | } 79 | 80 | return $results[0]; 81 | } 82 | 83 | /** 84 | * Searches for messages on the server. 85 | * 86 | * See method `Method::match` for more informations on criterias. 87 | * 88 | * @param array $criterias an array of criterias 89 | * @param int $limit maximum number of elements to fetch. Null means no limit 90 | * 91 | * @return array a list of messages 92 | */ 93 | public function search(array $criterias = array(), $limit = null) 94 | { 95 | $messages = array(); 96 | 97 | foreach ($this->request('GET') as $message) { 98 | if (isset($this->messages[$message['id']])) { 99 | $messages[] = $this->messages[$message['id']]; 100 | } else { 101 | // From mailcatcher v1.7, the request on / does not return the source 102 | if (!isset($message['source'])) { 103 | $source = $this->request('GET', $message['id'] . '.source', array(), false); 104 | $message['source'] = $source; 105 | } 106 | $messages[] = $this->messages[$message['id']] = new Message($this, $message); 107 | } 108 | } 109 | 110 | $result = array(); 111 | foreach ($messages as $message) { 112 | if (null !== $limit && count($result) >= $limit) { 113 | break; 114 | } 115 | if ($message->match($criterias)) { 116 | $result[] = $message; 117 | } 118 | } 119 | 120 | return $result; 121 | } 122 | 123 | /** 124 | * Request the API of MailCatcher. 125 | * 126 | * @param string $method HTTP method to use (POST, PUT, GET, DELETE) 127 | * @param string $path relative path from '/messages' (ex: null, '132.json') 128 | * @param array $parameters parameters to POST 129 | * 130 | * @return array response body 131 | */ 132 | public function request($method, $path = null, $parameters = array(), $jsonDecode = true) 133 | { 134 | if (null === $path) { 135 | $url = '/messages'; 136 | } else { 137 | $url = '/messages/'.$path; 138 | } 139 | 140 | $raw = $this->requestRaw($method, $url, $parameters); 141 | 142 | return $jsonDecode ? json_decode($raw, true) : $raw; 143 | } 144 | 145 | /** 146 | * Raw method to request the API of MailCatcher. 147 | * 148 | * @param string $method HTTP method 149 | * @param string $url absolute URL on server (`/messages/132.json`) 150 | * @param array $parameters parameters to POST 151 | * 152 | * @return string response body 153 | * @throws \RuntimeException 154 | */ 155 | public function requestRaw($method, $url, $parameters = array()) 156 | { 157 | $url = $this->url.$url; 158 | 159 | if (false === $curl = curl_init()) { 160 | throw new \RuntimeException('Unable to create a new cURL handle'); 161 | } 162 | 163 | $options = array( 164 | CURLOPT_RETURNTRANSFER => true, 165 | CURLOPT_HEADER => false, 166 | CURLOPT_CUSTOMREQUEST => $method, 167 | CURLOPT_URL => $url, 168 | CURLOPT_TIMEOUT_MS => 3000, 169 | CURLOPT_TIMEOUT => 3, 170 | CURLOPT_FOLLOWLOCATION => 1, 171 | CURLOPT_MAXREDIRS => 5, 172 | CURLOPT_FAILONERROR => true, 173 | CURLOPT_SSL_VERIFYPEER => false, 174 | ); 175 | 176 | switch ($method) { 177 | case 'HEAD': 178 | $options[CURLOPT_NOBODY] = true; 179 | break; 180 | 181 | case 'GET': 182 | $options[CURLOPT_HTTPGET] = true; 183 | break; 184 | 185 | case 'POST': 186 | case 'PUT': 187 | case 'DELETE': 188 | case 'PATCH': 189 | $options[CURLOPT_POSTFIELDS] = http_build_query($parameters); 190 | 191 | break; 192 | } 193 | 194 | curl_setopt_array($curl, $options); 195 | 196 | $result = curl_exec($curl); 197 | 198 | $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); 199 | 200 | if ($statusCode === 200 || $statusCode === 204 || ($statusCode >= 300 && $statusCode <= 303)) { 201 | return $result; 202 | } 203 | 204 | if (0 === $statusCode) { 205 | throw new \RuntimeException(sprintf('Unable to connect to "%s".', $this->url)); 206 | } 207 | 208 | throw new \RuntimeException(sprintf('Unexpected status code for url "%s". Expected valid code, got %s.', $url, $statusCode)); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Mime/Parser.php: -------------------------------------------------------------------------------- 1 | content = $content; 23 | $this->cursor = 0; 24 | 25 | try { 26 | return $this->doParseBoundary($boundary); 27 | } catch (\Exception $e) { 28 | $text = substr($this->content, $this->cursor, 10); 29 | 30 | throw new \InvalidArgumentException(sprintf('Error while parsing "%s" (cursor: %d, text: "%s").'."\n%s", $e->getMessage(), $this->cursor, $text, $e->getTraceAsString())); 31 | } 32 | } 33 | 34 | /** 35 | * @param $text 36 | * 37 | * @return array 38 | */ 39 | public function parsePart($text) 40 | { 41 | $text = str_replace("\r", '', $text); // acceptable 42 | 43 | $this->content = $text; 44 | $this->cursor = 0; 45 | 46 | try { 47 | return $this->doParsePart(); 48 | } catch (\Exception $e) { 49 | $text = substr($this->content, $this->cursor, 10); 50 | 51 | throw new \InvalidArgumentException(sprintf('Error while parsing "%s" (cursor: %d, text: "%s").'."\n%s", $e->getMessage(), $this->cursor, $text, $e->getTraceAsString())); 52 | } 53 | } 54 | 55 | /** 56 | * @param $boundary 57 | * 58 | * @return array|null 59 | */ 60 | private function doParseBoundary($boundary) 61 | { 62 | $result = array(); 63 | $prefix = "--".$boundary; 64 | 65 | $this->consumeRegexp("/\n*/"); 66 | $this->consumeTo($prefix); 67 | $this->consume($prefix); 68 | 69 | while ($this->expects("\n")) { 70 | $content = $this->consumeTo("\n".$prefix); 71 | 72 | $part = new Part(); 73 | $part->loadSource($content); 74 | if ($part->isMultipart()) { 75 | $result = array_merge($result, $part->getParts()); 76 | } else { 77 | $result[] = $part; 78 | } 79 | 80 | $this->consume("\n".$prefix); 81 | } 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | private function doParsePart() 90 | { 91 | $headerBag = $this->parseHeaderBag(); 92 | 93 | $this->consume("\n"); 94 | 95 | $content = $this->consumeAll(); 96 | 97 | if ($headerBag->get('Content-Transfer-Encoding') == 'quoted-printable') { 98 | $content = quoted_printable_decode(rtrim($content, "\n")); 99 | } 100 | 101 | return array($headerBag, $content); 102 | } 103 | 104 | /** 105 | * @return HeaderBag 106 | */ 107 | private function parseHeaderBag() 108 | { 109 | $headerBag = new HeaderBag(); 110 | 111 | while ($this->parseHeader($headerBag)) { 112 | continue; 113 | } 114 | 115 | return $headerBag; 116 | } 117 | 118 | /** 119 | * @param HeaderBag $headerBag 120 | * 121 | * @return bool 122 | */ 123 | private function parseHeader(HeaderBag $headerBag) 124 | { 125 | try { 126 | $vars = $this->consumeRegexp('/('.self::TOKEN_HEADER_NAME.'): ?/'); 127 | $headerName = $vars[1]; 128 | $value = $this->consumeTo("\n"); 129 | $this->consume("\n"); 130 | while ($this->expects(" ") || $this->expects("\t")) { 131 | $value .= $this->consumeTo("\n"); 132 | $this->consume("\n"); 133 | } 134 | 135 | $headerBag->set($headerName, $value); 136 | } catch (\InvalidArgumentException $e) { 137 | return false; 138 | } 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * @return bool 145 | */ 146 | protected function isFinished() 147 | { 148 | return $this->cursor === $this->length; 149 | } 150 | 151 | /** 152 | * @return string 153 | */ 154 | protected function consumeAll() 155 | { 156 | $rest = substr($this->content, $this->cursor); 157 | $this->cursor += strlen($rest); 158 | 159 | return $rest; 160 | } 161 | 162 | /** 163 | * @param $expected 164 | * 165 | * @return bool 166 | */ 167 | protected function expects($expected) 168 | { 169 | $length = strlen($expected); 170 | $actual = substr($this->content, $this->cursor, $length); 171 | if ($actual !== $expected) { 172 | return false; 173 | } 174 | 175 | $this->cursor += $length; 176 | 177 | return true; 178 | } 179 | 180 | /** 181 | * @param $regexp 182 | * 183 | * @return mixed 184 | */ 185 | protected function consumeRegexp($regexp) 186 | { 187 | if (!preg_match($regexp.'A', $this->content, $vars, 0, $this->cursor)) { 188 | throw new \InvalidArgumentException('No match for regexp '.$regexp.' Upcoming: '.substr($this->content, $this->cursor, 30)); 189 | } 190 | 191 | $this->cursor += strlen($vars[0]); 192 | 193 | return $vars; 194 | } 195 | 196 | /** 197 | * @param $text 198 | * 199 | * @return string 200 | */ 201 | protected function consumeTo($text) 202 | { 203 | $pos = strpos($this->content, $text, $this->cursor); 204 | 205 | if (false === $pos) { 206 | throw new \InvalidArgumentException(sprintf('Unable to find "%s"', $text)); 207 | } 208 | 209 | $result = substr($this->content, $this->cursor, $pos - $this->cursor); 210 | $this->cursor = $pos; 211 | 212 | return $result; 213 | } 214 | 215 | /** 216 | * @param $expected 217 | * 218 | * @return mixed 219 | */ 220 | protected function consume($expected) 221 | { 222 | $length = strlen($expected); 223 | $actual = substr($this->content, $this->cursor, $length); 224 | if ($actual !== $expected) { 225 | throw new \InvalidArgumentException(sprintf('Expected "%s", but got "%s" (%s)', $expected, $actual, substr($this->content, $this->cursor, 10))); 226 | } 227 | $this->cursor += $length; 228 | 229 | return $expected; 230 | } 231 | 232 | /** 233 | * @return mixed 234 | */ 235 | protected function consumeNewLine() 236 | { 237 | return $this->consume("\n"); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Behat/MailCatcherContext.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class MailCatcherContext implements Context, TranslatableContext, MailCatcherAwareInterface 17 | { 18 | /** 19 | * This property is duplicated from MailCatcherTrait for support in PHP 5.3 20 | * 21 | * @var Client|null 22 | */ 23 | protected $mailCatcherClient; 24 | 25 | /** 26 | * @var boolean 27 | */ 28 | protected $purgeBeforeScenario; 29 | 30 | /** 31 | * @var Message|null 32 | */ 33 | protected $currentMessage; 34 | 35 | /** 36 | * Sets mailcatcher configuration. 37 | * 38 | * @param boolean $purgeBeforeScenario set false if you don't want context to purge before scenario 39 | */ 40 | public function setMailCatcherConfiguration($purgeBeforeScenario = true) 41 | { 42 | $this->purgeBeforeScenario = $purgeBeforeScenario; 43 | } 44 | 45 | /** 46 | * @BeforeScenario 47 | */ 48 | public function beforeScenario() 49 | { 50 | if (!$this->purgeBeforeScenario) { 51 | return; 52 | } 53 | 54 | $this->currentMessage = null; 55 | try { 56 | $this->getMailCatcherClient()->purge(); 57 | } catch (\Exception $e) { 58 | @trigger_error("Unable to purge mailcatcher: ".$e->getMessage()); 59 | } 60 | } 61 | 62 | 63 | /** 64 | * @When /^I purge mails$/ 65 | */ 66 | public function purge() 67 | { 68 | $this->getMailCatcherClient()->purge(); 69 | } 70 | 71 | /** 72 | * @When /^I open mail from "([^"]+)"$/ 73 | */ 74 | public function openMailFrom($value) 75 | { 76 | $message = $this->findMail(Message::FROM_CRITERIA, $value); 77 | 78 | $this->currentMessage = $message; 79 | } 80 | 81 | /** 82 | * @When /^I open mail with subject "([^"]+)"$/ 83 | */ 84 | public function openMailSubject($value) 85 | { 86 | $message = $this->findMail(Message::SUBJECT_CRITERIA, $value); 87 | 88 | $this->currentMessage = $message; 89 | } 90 | 91 | /** 92 | * @When /^I open mail to "([^"]+)"$/ 93 | */ 94 | public function openMailTo($value) 95 | { 96 | $message = $this->findMail(Message::TO_CRITERIA, $value); 97 | 98 | $this->currentMessage = $message; 99 | } 100 | 101 | /** 102 | * @When /^I open mail containing "([^"]+)"$/ 103 | */ 104 | public function openMailContaining($value) 105 | { 106 | $message = $this->findMail(Message::CONTAINS_CRITERIA, $value); 107 | 108 | $this->currentMessage = $message; 109 | } 110 | 111 | /** 112 | * @Then /^I should see mail from "([^"]+)"$/ 113 | */ 114 | public function seeMailFrom($value) 115 | { 116 | $message = $this->findMail(Message::FROM_CRITERIA, $value); 117 | } 118 | 119 | /** 120 | * @Then /^I should see mail with subject "([^"]+)"$/ 121 | */ 122 | public function seeMailSubject($value) 123 | { 124 | $message = $this->findMail(Message::SUBJECT_CRITERIA, $value); 125 | } 126 | 127 | /** 128 | * @Then /^I should see mail to "([^"]+)"$/ 129 | */ 130 | public function seeMailTo($value) 131 | { 132 | $message = $this->findMail(Message::TO_CRITERIA, $value); 133 | } 134 | 135 | /** 136 | * @Then /^I should see mail containing "([^"]+)"$/ 137 | */ 138 | public function seeMailContaining($value) 139 | { 140 | $message = $this->findMail(Message::CONTAINS_CRITERIA, $value); 141 | } 142 | 143 | 144 | /** 145 | * @Then /^I should see "([^"]+)" in mail$/ 146 | */ 147 | public function seeInMail($text) 148 | { 149 | $message = $this->getCurrentMessage(); 150 | 151 | if (!$message->isMultipart()) { 152 | $content = $message->getContent(); 153 | } elseif ($message->hasPart('text/html')) { 154 | $content = $this->getCrawler($message)->text(); 155 | } elseif ($message->hasPart('text/plain')) { 156 | $content = $message->getPart('text/plain')->getContent(); 157 | } else { 158 | throw new \RuntimeException(sprintf('Unable to read mail')); 159 | } 160 | 161 | if (false === strpos($content, $text)) { 162 | throw new \InvalidArgumentException(sprintf("Unable to find text \"%s\" in current message:\n%s", $text, $content)); 163 | } 164 | } 165 | 166 | /** 167 | * @Then /^(?P\d+) mails? should be sent$/ 168 | */ 169 | public function verifyMailsSent($count) 170 | { 171 | $count = (int) $count; 172 | $actual = $this->getMailCatcherClient()->getMessageCount(); 173 | 174 | if ($count !== $actual) { 175 | throw new \InvalidArgumentException(sprintf('Expected %d mails to be sent, got %d.', $count, $actual)); 176 | } 177 | } 178 | 179 | /** 180 | * Returns list of definition translation resources paths. 181 | * 182 | * @return array 183 | */ 184 | public static function getTranslationResources() 185 | { 186 | return glob(__DIR__.'/../i18n/*.xliff'); 187 | } 188 | 189 | /** 190 | * @return Message|null 191 | */ 192 | private function getCurrentMessage() 193 | { 194 | if (null === $this->currentMessage) { 195 | throw new \RuntimeException('No message selected'); 196 | } 197 | 198 | return $this->currentMessage; 199 | } 200 | 201 | /** 202 | * @param Message $message 203 | * 204 | * @return Crawler 205 | */ 206 | private function getCrawler(Message $message) 207 | { 208 | if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { 209 | throw new \RuntimeException('Can\'t crawl HTML: Symfony DomCrawler component is missing from autoloading.'); 210 | } 211 | 212 | return new Crawler($message->getPart('text/html')->getContent()); 213 | } 214 | 215 | /** 216 | * This method is duplicated from MailCatcherTrait, for support in PHP 5.3 217 | * 218 | * Sets the mailcatcher client. 219 | * 220 | * @param Client $client a mailcatcher client 221 | */ 222 | public function setMailCatcherClient(Client $client) 223 | { 224 | $this->mailCatcherClient = $client; 225 | } 226 | 227 | /** 228 | * This method is duplicated from MailCatcherTrait, for support in PHP 5.3 229 | * 230 | * Returns the mailcatcher client. 231 | * 232 | * @return Client 233 | * 234 | * @throws \RuntimeException client if missing from context 235 | */ 236 | public function getMailCatcherClient() 237 | { 238 | if (null === $this->mailCatcherClient) { 239 | throw new \RuntimeException(sprintf('No MailCatcher client injected.')); 240 | } 241 | 242 | return $this->mailCatcherClient; 243 | } 244 | 245 | /** 246 | * This method is duplicated from MailCatcherTrait, for support in PHP 5.3 247 | * 248 | * @return Message 249 | */ 250 | protected function findMail($type, $value) 251 | { 252 | $criterias = array($type => $value); 253 | 254 | $message = $this->getMailCatcherClient()->searchOne($criterias); 255 | 256 | if (null === $message) { 257 | throw new \InvalidArgumentException(sprintf('Unable to find a message with criterias "%s".', json_encode($criterias))); 258 | } 259 | 260 | return $message; 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /Message.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Message extends BaseMessage 14 | { 15 | const ATTACHMENTS_CRITERIA = 'attachments'; 16 | const CONTAINS_CRITERIA = 'contains'; 17 | const FORMAT_CRITERIA = 'format'; 18 | const FROM_CRITERIA = 'from'; 19 | const TO_CRITERIA = 'to'; 20 | const SUBJECT_CRITERIA = 'subject'; 21 | 22 | /** 23 | * @var Client 24 | */ 25 | protected $client; 26 | 27 | /** 28 | * @var int 29 | */ 30 | protected $id; 31 | 32 | /** 33 | * @var int 34 | */ 35 | protected $size; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $subject; 41 | 42 | /** 43 | * @var string 44 | */ 45 | protected $type; 46 | 47 | /** 48 | * @var Person 49 | */ 50 | protected $sender; 51 | 52 | /** 53 | * @var array array of Person 54 | */ 55 | protected $recipients; 56 | 57 | /** 58 | * @var array array of Attachment 59 | */ 60 | protected $attachments; 61 | 62 | /** 63 | * @var DateTime 64 | */ 65 | protected $createdAt; 66 | 67 | /** 68 | * @var array 69 | */ 70 | protected $formats; 71 | 72 | /** 73 | * Constructor 74 | * 75 | * @param Client $client 76 | * @param array $data 77 | */ 78 | public function __construct(Client $client, array $data = array()) 79 | { 80 | $this->client = $client; 81 | $this->loadFromArray($data); 82 | } 83 | 84 | /** 85 | * @return Message 86 | */ 87 | public function loadFromArray(array $array) 88 | { 89 | if (isset($array['id'])) { 90 | $this->id = $array['id']; 91 | } 92 | 93 | if (isset($array['created_at'])) { 94 | $this->createdAt = new \DateTime($array['created_at']); 95 | } 96 | 97 | if (isset($array['size'])) { 98 | $this->size = $array['size']; 99 | } 100 | 101 | if (isset($array['subject'])) { 102 | $this->subject = $array['subject']; 103 | } 104 | 105 | if (isset($array['sender'])) { 106 | $this->sender = Person::createFromString($array['sender']); 107 | } 108 | 109 | if (isset($array['recipients'])) { 110 | $this->recipients = array_map(function ($string) { 111 | return Person::createFromString($string); 112 | }, $array['recipients']); 113 | } 114 | 115 | if (isset($array['formats'])) { 116 | $this->formats = $array['formats']; 117 | } 118 | 119 | if (isset($array['type'])) { 120 | $this->type = $array['type']; 121 | } 122 | 123 | if (isset($array['attachments'])) { 124 | $client = $this->client; 125 | $this->attachments = array_map(function ($array) use ($client) { 126 | return new Attachment($client, $array); 127 | }, $array['attachments']); 128 | } 129 | 130 | if (isset($array['source'])) { 131 | $this->loadSource($array['source']); 132 | } 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * @param array $criterias 139 | * 140 | * @return bool 141 | */ 142 | public function match(array $criterias) 143 | { 144 | foreach ($criterias as $type => $value) { 145 | switch ($type) { 146 | case self::FROM_CRITERIA: 147 | if (!$this->getSender()->match($value)) { 148 | return false; 149 | } 150 | 151 | break; 152 | 153 | case self::SUBJECT_CRITERIA: 154 | if (false === strpos($this->getSubject(), $value)) { 155 | return false; 156 | } 157 | 158 | break; 159 | 160 | case self::TO_CRITERIA: 161 | $foundTo = false; 162 | foreach ($this->getRecipients() as $recipient) { 163 | if ($recipient->match($value)) { 164 | $foundTo = true; 165 | break; 166 | } 167 | } 168 | 169 | if (!$foundTo) { 170 | return false; 171 | } 172 | 173 | break; 174 | 175 | case self::CONTAINS_CRITERIA: 176 | if (false === strpos($this->getContent(), $value)) { 177 | return false; 178 | } 179 | 180 | break; 181 | 182 | case self::FORMAT_CRITERIA: 183 | if (!$this->hasFormat($value)) { 184 | return false; 185 | } 186 | 187 | break; 188 | 189 | case self::ATTACHMENTS_CRITERIA: 190 | if (!is_bool($value)) { 191 | throw new \InvalidArgumentException(sprintf('Expected a boolean, got a "%s".', gettype($value))); 192 | } 193 | 194 | if ($value != $this->hasAttachments()) { 195 | return false; 196 | } 197 | 198 | break; 199 | 200 | default: 201 | throw new \InvalidArgumentException(sprintf('Unexpected type of criteria: "%s".', $type)); 202 | } 203 | } 204 | 205 | return true; 206 | } 207 | 208 | /** 209 | * @param $format 210 | * 211 | * @return bool 212 | */ 213 | public function hasFormat($format) 214 | { 215 | return in_array($format, $this->getFormats()); 216 | } 217 | 218 | /** 219 | * @return array 220 | */ 221 | public function getFormats() 222 | { 223 | if (null === $this->formats) { 224 | $this->hydrate(); 225 | } 226 | 227 | return $this->formats; 228 | } 229 | 230 | /** 231 | * @return integer 232 | */ 233 | public function getId() 234 | { 235 | return $this->id; 236 | } 237 | 238 | /** 239 | * @return integer 240 | */ 241 | public function getSize() 242 | { 243 | if (null === $this->size) { 244 | $this->hydrate(); 245 | } 246 | 247 | return $this->size; 248 | } 249 | 250 | /** 251 | * @return string 252 | */ 253 | public function getSubject() 254 | { 255 | if (null === $this->subject) { 256 | $this->hydrate(); 257 | } 258 | 259 | return $this->subject; 260 | } 261 | 262 | /** 263 | * @return boolean 264 | */ 265 | public function isPlain() 266 | { 267 | return $this->getType() === 'text/plain'; 268 | } 269 | 270 | /** 271 | * @return string 272 | */ 273 | public function getType() 274 | { 275 | if (null === $this->type) { 276 | $this->hydrate(); 277 | } 278 | 279 | return $this->type; 280 | } 281 | 282 | /** 283 | * @return array array of Attachment 284 | */ 285 | public function getAttachments() 286 | { 287 | if (null === $this->attachments) { 288 | $this->hydrate(); 289 | } 290 | 291 | return $this->attachments; 292 | } 293 | 294 | /** 295 | * @return boolean 296 | */ 297 | public function hasAttachments() 298 | { 299 | return count($this->getAttachments()) > 0; 300 | } 301 | 302 | /** 303 | * @return Person 304 | */ 305 | public function getSender() 306 | { 307 | if (null === $this->sender) { 308 | $this->hydrate(); 309 | } 310 | 311 | return $this->sender; 312 | } 313 | 314 | /** 315 | * @return array 316 | */ 317 | public function getRecipients() 318 | { 319 | if (null === $this->recipients) { 320 | $this->hydrate(); 321 | } 322 | 323 | return $this->recipients; 324 | } 325 | 326 | /** 327 | * @return HeaderBag 328 | */ 329 | public function getHeaders() 330 | { 331 | if (null === $this->headers) { 332 | $this->hydrate(); 333 | } 334 | 335 | return $this->headers; 336 | } 337 | 338 | /** 339 | * @return string 340 | */ 341 | public function getContent() 342 | { 343 | if (null === $this->content) { 344 | $this->hydrate(); 345 | } 346 | 347 | return $this->content; 348 | } 349 | 350 | /** 351 | * @return \DateTime 352 | */ 353 | public function getCreatedAt() 354 | { 355 | if (null === $this->createdAt) { 356 | $this->hydrate(); 357 | } 358 | 359 | return $this->createdAt; 360 | } 361 | 362 | private function hydrate() 363 | { 364 | $this->loadFromArray($this->client->request('GET', $this->id.'.json')); 365 | } 366 | 367 | /** 368 | * @return string 369 | */ 370 | public function delete() 371 | { 372 | return $this->client->request('DELETE', $this->id); 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /Tests/MessagePartTest.php: -------------------------------------------------------------------------------- 1 | 18 | To: Matt Parker 19 | Content-Type: multipart/mixed; boundary=f46d0447861ff9929f04f143ce67 20 | 21 | --f46d0447861ff9929f04f143ce67 22 | Content-Type: multipart/alternative; boundary=f46d0447861ff9929a04f143ce65 23 | 24 | --f46d0447861ff9929a04f143ce65 25 | Content-Type: text/plain; charset=ISO-8859-1 26 | 27 | this is *some bold text* 28 | and some normal. 29 | 30 | --f46d0447861ff9929a04f143ce65 31 | Content-Type: text/html; charset=ISO-8859-1 32 | Content-Transfer-Encoding: quoted-printable 33 | 34 |
this is some bold text=A0
35 |
36 |
and some normal.

37 | 38 | --f46d0447861ff9929a04f143ce65-- 39 | --f46d0447861ff9929f04f143ce67 40 | Content-Type: text/plain; charset=US-ASCII; name="test file.txt" 41 | Content-Disposition: attachment; filename="test file.txt" 42 | Content-Transfer-Encoding: base64 43 | X-Attachment-Id: f_hr3gqtxm0 44 | 45 | dGVzdCBmaWxlCg== 46 | --f46d0447861ff9929f04f143ce67-- 47 | EOF; 48 | 49 | $part = new Part; 50 | 51 | $part->loadSource($message); 52 | 53 | $this->assertContains( 54 | 'this is *some bold text*', 55 | $part->getPart('text/plain')->getContent(), 56 | 'Can we extract the plain text section?' 57 | ); 58 | $this->assertContains( 59 | 'this is some bold text', 60 | $part->getPart('text/html')->getContent(), 61 | 'Can we get the html content?' 62 | ); 63 | } 64 | 65 | public function testMultiPartWithQuotedBoundary () 66 | { 67 | $message = << 73 | To: Matt Parker 74 | Content-Type: multipart/mixed; boundary="=_f46d0447861ff9929f04f143ce67" 75 | 76 | --=_f46d0447861ff9929f04f143ce67 77 | Content-Type: multipart/alternative; boundary=f46d0447861ff9929a04f143ce65 78 | 79 | --f46d0447861ff9929a04f143ce65 80 | Content-Type: text/plain; charset=ISO-8859-1 81 | 82 | this is *some bold text* 83 | and some normal. 84 | 85 | --f46d0447861ff9929a04f143ce65 86 | Content-Type: text/html; charset=ISO-8859-1 87 | Content-Transfer-Encoding: quoted-printable 88 | 89 |
this is some bold text=A0
90 |
91 |
and some normal.

92 | 93 | --f46d0447861ff9929a04f143ce65-- 94 | --=_f46d0447861ff9929f04f143ce67 95 | Content-Type: text/plain; charset=US-ASCII; name="test file.txt" 96 | Content-Disposition: attachment; filename="test file.txt" 97 | Content-Transfer-Encoding: base64 98 | X-Attachment-Id: f_hr3gqtxm0 99 | 100 | dGVzdCBmaWxlCg== 101 | --=_f46d0447861ff9929f04f143ce67-- 102 | EOF; 103 | 104 | $part = new Part; 105 | 106 | $part->loadSource($message); 107 | 108 | $this->assertContains( 109 | 'this is *some bold text*', 110 | $part->getPart('text/plain')->getContent(), 111 | 'Can we extract the plain text section?' 112 | ); 113 | $this->assertContains( 114 | 'this is some bold text', 115 | $part->getPart('text/html')->getContent(), 116 | 'Can we get the html content?' 117 | ); 118 | } 119 | 120 | 121 | 122 | public function testMultiPartWithAMimeFormatWarningMessage () 123 | { 124 | $message = <<
We sent this message using Lamplight - click here to unsubscribe. 153 | 154 | --=_6d8e9e6de65fe1320ba67bf1757d7c85-- 155 | 156 | --=_dba29f3ae3414895d994f90b8b013498 157 | Content-Type: text/plain 158 | Content-Transfer-Encoding: base64 159 | Content-Disposition: attachment; filename="testupload.txt" 160 | 161 | aGVsbG8sIHRoaXMgaXMgYSBuaWNlIHRleHQgZmlsZSB0byB1cGxvYWQK 162 | --=_dba29f3ae3414895d994f90b8b013498-- 163 | EOF; 164 | $part = new Part; 165 | 166 | $part->loadSource($message); 167 | 168 | $this->assertContains( 169 | 'nice file attached', 170 | $part->getPart('text/plain')->getContent(), 171 | 'Can we extract the plain text section?' 172 | ); 173 | 174 | 175 | } 176 | 177 | public function testMultiPartWithEmbeddedImage() 178 | { 179 | $message = << 185 | To: Matt Parker 186 | Content-Type: multipart/alternative; boundary=f46d0447861ff9929f04f143ce67 187 | 188 | 189 | --f46d0447861ff9929f04f143ce67 190 | Content-Type: text/plain; charset=ISO-8859-1 191 | 192 | this is *some bold text* 193 | and some normal. 194 | 195 | 196 | --f46d0447861ff9929f04f143ce67 197 | Content-Type: multipart/related; boundary=f46d0447861ff9929a04f143ce65 198 | 199 | 200 | --f46d0447861ff9929a04f143ce65 201 | Content-Type: text/html; charset=ISO-8859-1 202 | Content-Transfer-Encoding: quoted-printable 203 | 204 |
this is some bold text=A0
205 |
206 | 3D"Placeholder" 207 |
and some normal.

208 | 209 | 210 | --f46d0447861ff9929a04f143ce65 211 | Content-Type: image/png; name=1x1.png 212 | Content-Transfer-Encoding: base64 213 | Content-Disposition: inline; filename=1x1.png 214 | Content-ID: <7cc6d387fc99f96a7ef17d905df333f6> 215 | 216 | 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 264 | 265 | --f46d0447861ff9929a04f143ce65-- 266 | 267 | 268 | --f46d0447861ff9929f04f143ce67-- 269 | EOF; 270 | 271 | $part = new Part; 272 | 273 | $part->loadSource($message); 274 | 275 | $this->assertContains( 276 | 'this is *some bold text*', 277 | $part->getPart('text/plain')->getContent(), 278 | 'Can we extract the plain text section?' 279 | ); 280 | 281 | $this->assertContains( 282 | 'this is some bold text', 283 | $part->getPart('text/html')->getContent(), 284 | 'Can we get the html content?' 285 | ); 286 | } 287 | } 288 | --------------------------------------------------------------------------------