diff --git a/.travis.yml b/.travis.yml index a905b36a02e..0218654db8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ before_script: - tests/unit/data/travis/cubrid-setup.sh script: - - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata + - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,swiftmailer after_script: - php vendor/bin/coveralls diff --git a/framework/yii/email/BaseMailer.php b/framework/yii/email/BaseMailer.php new file mode 100644 index 00000000000..7b8897d80a1 --- /dev/null +++ b/framework/yii/email/BaseMailer.php @@ -0,0 +1,81 @@ + + * @since 2.0 + */ +abstract class BaseMailer extends Component +{ + /** + * @var array configuration, which should be applied by default to any new created + * email message instance. + * For example: + * ~~~ + * array( + * 'encoding' => 'UTF-8', + * 'from' => 'noreply@mydomain.com', + * 'bcc' => 'email.test@mydomain.com', + * ) + * ~~~ + */ + private $_defaultMessageConfig = array(); + + /** + * @param array $defaultMessageConfig default message config + */ + public function setDefaultMessageConfig(array $defaultMessageConfig) + { + $this->_defaultMessageConfig = $defaultMessageConfig; + } + + /** + * @return array default message config + */ + public function getDefaultMessageConfig() + { + return $this->_defaultMessageConfig; + } + + /** + * Sends the given email message. + * @param object $message email message instance + * @return boolean whether the message has been sent. + */ + abstract public function send($message); + + /** + * Sends a couple of messages at once. + * Note: some particular mailers may benefit from sending messages as batch, + * saving resources, for example on open/close connection operations, + * they may override this method to create their specific implementation. + * @param array $messages list of email messages, which should be sent. + * @return integer number of successfull sends + */ + public function sendMultiple(array $messages) { + $successCount = 0; + foreach ($messages as $message) { + if ($this->send($message)) { + $successCount++; + } + } + return $successCount; + } +} \ No newline at end of file diff --git a/framework/yii/email/BaseMessage.php b/framework/yii/email/BaseMessage.php new file mode 100644 index 00000000000..8127c9c99c4 --- /dev/null +++ b/framework/yii/email/BaseMessage.php @@ -0,0 +1,125 @@ + + * @since 2.0 + */ +abstract class BaseMessage extends Object +{ + /** + * @return \yii\email\BaseMailer + */ + public function getMailer() + { + return Yii::$app->getComponent('email'); + } + + /** + * Initializes the object. + * This method is invoked at the end of the constructor after the object is initialized with the + * given configuration. + */ + public function init() + { + Yii::configure($this, $this->getMailer()->getDefaultMessageConfig()); + } + + /** + * Sends this email message. + * @return boolean success. + */ + public function send() + { + return $this->getMailer()->send($this); + } + + /** + * Sets message sender. + * @param string|array $from sender email address, if array is given, + * its first element should be sender email address, second - sender name. + */ + abstract public function setFrom($from); + + /** + * Sets message receiver. + * @param string|array $to receiver email address, if array is given, + * its first element should be receiver email address, second - receiver name. + */ + abstract public function setTo($to); + + /** + * Sets message subject. + * @param string $subject message subject + */ + abstract public function setSubject($subject); + + /** + * Sets message plain text content. + * @param string $text message plain text content. + */ + abstract public function setText($text); + + /** + * Sets message HTML content. + * @param string $html message HTML content. + */ + abstract public function setHtml($html); + + /** + * Create file attachment for the email message. + * @param string $content attachment file content. + * @param string $fileName attachment file name. + * @param string $contentType MIME type of the attachment file, by default 'application/octet-stream' will be used. + */ + abstract public function createAttachment($content, $fileName, $contentType = 'application/octet-stream'); + + /** + * Attaches existing file to the email message. + * @param string $fileName full file name + * @param string $contentType MIME type of the attachment file, if empty it will be suggested automatically. + * @param string $attachFileName name, which should be used for attachment, if empty file base name will be used. + * @throws \yii\base\InvalidParamException if given file does not exist. + */ + public function attachFile($fileName, $contentType = null, $attachFileName = null) + { + if (!file_exists($fileName)) { + throw new InvalidParamException('Unable to attach file "' . $fileName . '": file does not exists!'); + } + if (empty($contentType)) { + $contentType = FileHelper::getMimeType($fileName); + } + if (empty($attachFileName)) { + $attachFileName = basename($fileName); + } + $content = file_get_contents($fileName); + $this->createAttachment($content, $attachFileName, $contentType); + } +} \ No newline at end of file diff --git a/framework/yii/email/Message.php b/framework/yii/email/Message.php new file mode 100644 index 00000000000..c7ec5344b63 --- /dev/null +++ b/framework/yii/email/Message.php @@ -0,0 +1,34 @@ +from = 'sender@domain.com'; + * $email->to = 'receiver@domain.com'; + * $email->subject = 'Message Subject'; + * $email->text = 'Message Content'; + * $email->send(); + * ~~~ + * + * This particular class uses 'SwiftMailer' library to perform the message sending. + * Note: you can replace usage of this class by your own one, using [[Yii::$classMap]]: + * ~~~ + * Yii::$classMap['yii\email\Message'] = '/path/to/my/email/Message.php' + * ~~~ + * + * @author Paul Klimov + * @since 2.0 + */ +class Message extends SwiftMessage {} \ No newline at end of file diff --git a/framework/yii/email/VendorMailer.php b/framework/yii/email/VendorMailer.php new file mode 100644 index 00000000000..84a7b2c86f7 --- /dev/null +++ b/framework/yii/email/VendorMailer.php @@ -0,0 +1,104 @@ + + * @since 2.0 + */ +abstract class VendorMailer extends BaseMailer +{ + /** + * @var string|callback vendor autoloader callback or path to autoloader file. + * If the vendor classes autoloading is already managed in some other place, + * for example via Composer, you should leave this field blank. + */ + public $autoload; + /** + * @var array|object vendor mailer instance or its array configuration. + * Note: different implementation of [[VendorMailer]] may process configuration + * in the different way. + */ + private $_vendorMailer = array(); + + /** + * Initializes the object. + * Registers the vendor autoloader, if any. + */ + public function init() + { + $this->setupVendorAutoload(); + } + + /** + * @param array|object $value mailer instance or configuration. + * @throws \yii\base\InvalidConfigException on invalid argument. + */ + public function setVendorMailer($value) + { + if (!is_array($value) && !is_object($value)) { + throw new InvalidConfigException('"' . get_class($this) . '::vendorMailer" should be either object or array, "' . gettype($value) . '" given.'); + } + $this->_vendorMailer = $value; + } + + /** + * @return object vendor mailer instance. + */ + public function getVendorMailer() + { + if (!is_object($this->_vendorMailer)) { + $this->_vendorMailer = $this->createVendorMailer($this->_vendorMailer); + } + return $this->_vendorMailer; + } + + /** + * Sets up the vendor autoloader, if any is specified. + */ + protected function setupVendorAutoload() + { + if (!empty($this->autoload)) { + if (is_string($this->autoload) && file_exists($this->autoload)) { + if (file_exists($this->autoload)) { + require_once($this->autoload); + } elseif (function_exists($this->autoload)) { + spl_autoload_register($this->autoload); + } else { + throw new InvalidConfigException('"' . get_class($this) . '::autoload" value "' . $this->autoload . '" is invalid: no such function or file exists.'); + } + } else { + spl_autoload_register($this->autoload); + } + } + } + + /** + * Creates vendor mailer instance from given configuration. + * @param array $config mailer configuration. + * @return object mailer instance. + */ + abstract protected function createVendorMailer(array $config); + + /** + * Creates the vendor email message instance. + * @return object email message instance. + */ + abstract public function createVendorMessage(); +} \ No newline at end of file diff --git a/framework/yii/email/VendorMessage.php b/framework/yii/email/VendorMessage.php new file mode 100644 index 00000000000..836b5c93832 --- /dev/null +++ b/framework/yii/email/VendorMessage.php @@ -0,0 +1,156 @@ + + * @since 2.0 + */ +abstract class VendorMessage extends BaseMessage +{ + /** + * @var object vendor message instance. + */ + private $_vendorMessage; + + /** + * @inheritdoc + */ + public function __get($name) + { + try { + return parent::__get($name); + } catch (UnknownPropertyException $exception) { + $vendorMessage = $this->getVendorMessage(); + if (property_exists($vendorMessage, $name)) { + return $vendorMessage->$name; + } + $getter = 'get' . $name; + if (method_exists($vendorMessage, $getter)) { + return $vendorMessage->$getter(); + } else { + throw $exception; + } + } + } + + /** + * @inheritdoc + */ + public function __set($name, $value) + { + try { + parent::__set($name, $value); + } catch (UnknownPropertyException $exception) { + $vendorMessage = $this->getVendorMessage(); + if (property_exists($vendorMessage, $name)) { + $vendorMessage->$name = $value; + return; + } + $setter = 'set' . $name; + if (method_exists($vendorMessage, $setter)) { + $vendorMessage->$setter($value); + } else { + throw $exception; + } + } + } + + /** + * @inheritdoc + */ + public function __isset($name) + { + $getter = 'get' . $name; + if (method_exists($this, $getter)) { + return $this->$getter() !== null; + } else { + $vendorMessage = $this->getVendorMessage(); + if (property_exists($vendorMessage, $name)) { + return isset($vendorMessage->$name); + } + if (method_exists($vendorMessage, $getter)) { + return ($vendorMessage->$getter() !== null); + } else { + return false; + } + } + } + + /** + * @inheritdoc + */ + public function __unset($name) + { + $setter = 'set' . $name; + if (method_exists($this, $setter)) { + $this->$setter(null); + } elseif (method_exists($this, 'get' . $name)) { + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); + } else { + $vendorMessage = $this->getVendorMessage(); + if (property_exists($vendorMessage, $name)) { + unset($vendorMessage->$name); + } else { + if (method_exists($vendorMessage, $setter)) { + $vendorMessage->$setter(null); + } elseif (method_exists($vendorMessage, 'get' . $name)) { + throw new InvalidCallException('Unsetting read-only property: ' . get_class($vendorMessage) . '::' . $name); + } + } + } + } + + /** + * @inheritdoc + */ + public function __call($name, $params) + { + $vendorMessage = $this->getVendorMessage(); + if (method_exists($vendorMessage, $name)) { + return call_user_func_array(array($vendorMessage, $name), $params); + } + return parent::__call($name, $params); + } + + /** + * @return object vendor message instance. + */ + public function getVendorMessage() + { + if (!is_object($this->_vendorMessage)) { + $this->_vendorMessage = $this->createVendorMessage(); + } + return $this->_vendorMessage; + } + + /** + * Creates actual vendor message instance. + * @return object vendor message instance. + */ + protected function createVendorMessage() + { + return Yii::$app->getComponent('email')->createVendorMessage(); + } +} \ No newline at end of file diff --git a/framework/yii/email/swift/Mailer.php b/framework/yii/email/swift/Mailer.php new file mode 100644 index 00000000000..31b7e03d27a --- /dev/null +++ b/framework/yii/email/swift/Mailer.php @@ -0,0 +1,107 @@ + array( + * ... + * 'email' => array( + * 'class' => 'yii\email\swift\Mailer', + * 'vendorMailer' => [ + * 'transport' => [ + * 'type' => 'smtp', + * 'host' => 'localhost', + * 'username' => 'username', + * 'password' => 'password', + * 'port' => '587', + * 'encryption' => 'tls', + * ], + * ], + * ), + * ... + * ), + * ~~~ + * + * @see http://swiftmailer.org + * + * @author Paul Klimov + * @since 2.0 + */ +class Mailer extends VendorMailer +{ + /** + * @inheritdoc + */ + public function init() + { + if (!class_exists('Swift', false) && empty($this->autoload)) { + $this->autoload = __DIR__ . '/autoload.php'; + } + parent::init(); + } + + /** + * @inheritdoc + */ + public function send($message) + { + return ($this->getVendorMailer()->send($message->getVendorMessage()) > 0); + } + + /** + * Creates Swift mailer instance from given configuration. + * @param array $config mailer configuration. + * @return \Swift_Mailer mailer instance. + */ + protected function createVendorMailer(array $config) + { + if (array_key_exists('transport', $config)) { + $transportConfig = $config['transport']; + } else { + $transportConfig = array(); + } + return \Swift_Mailer::newInstance($this->createTransport($transportConfig)); + } + + /** + * Creates the Swift email message instance. + * @return \Swift_Message email message instance. + */ + public function createVendorMessage() + { + return new \Swift_Message(); + } + + /** + * Creates email transport instance by its array configuration. + * @param array $config transport configuration. + * @return \Swift_Transport transport instance. + */ + protected function createTransport(array $config) + { + if (array_key_exists('class', $config)) { + $className = $config['class']; + unset($config['class']); + } else { + $className = 'Swift_MailTransport'; + } + $transport = call_user_func(array($className, 'newInstance')); + if (!empty($config)) { + foreach ($config as $name => $value) + $transport->{'set' . $name}($value); // sets option with the setter method + } + return $transport; + } +} \ No newline at end of file diff --git a/framework/yii/email/swift/Message.php b/framework/yii/email/swift/Message.php new file mode 100644 index 00000000000..12b8572f19c --- /dev/null +++ b/framework/yii/email/swift/Message.php @@ -0,0 +1,100 @@ + + * @since 2.0 + */ +class Message extends VendorMessage +{ + /** + * Sets message sender. + * @param string|array $from sender email address, if array is given, + * its first element should be sender email address, second - sender name. + */ + public function setFrom($from) + { + if (is_array($from)) { + list ($address, $name) = $from; + } else { + $address = $from; + $name = null; + } + $this->getVendorMessage()->setFrom($address, $name); + $this->getVendorMessage()->setReplyTo($address, $name); + } + + /** + * Sets message receiver. + * @param string|array $to receiver email address, if array is given, + * its first element should be receiver email address, second - receiver name. + */ + public function setTo($to) + { + if (is_array($to)) { + list ($address, $name) = $to; + } else { + $address = $to; + $name = null; + } + $this->getVendorMessage()->setTo($address, $name); + } + + /** + * Sets message subject. + * @param string $subject message subject + */ + public function setSubject($subject) + { + $this->getVendorMessage()->setSubject($subject); + } + + /** + * Sets message plain text content. + * @param string $text message plain text content. + */ + public function setText($text) + { + $this->getVendorMessage()->setBody($text, 'text/plain'); + } + + /** + * Sets message HTML content. + * @param string $html message HTML content. + */ + public function setHtml($html) + { + $this->getVendorMessage()->setBody($html, 'text/html'); + } + + /** + * Create file attachment for the email message. + * @param string $content - attachment file content. + * @param string $fileName - attachment file name. + * @param string $contentType - MIME type of the attachment file, by default 'application/octet-stream' will be used. + */ + public function createAttachment($content, $fileName, $contentType = 'application/octet-stream') + { + if (empty($contentType)) { + $contentType = 'application/octet-stream'; + } + $attachment = \Swift_Attachment::newInstance($content, $fileName, $contentType); + $this->getVendorMessage()->attach($attachment); + } +} \ No newline at end of file diff --git a/framework/yii/email/swift/autoload.php b/framework/yii/email/swift/autoload.php new file mode 100644 index 00000000000..494150f1f00 --- /dev/null +++ b/framework/yii/email/swift/autoload.php @@ -0,0 +1,9 @@ +mockApplication(); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return TestMailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + return $component; + } + + // Tests : + + public function testDefaultMessageConfig() + { + $defaultMessageConfig = array( + 'id' => 'test-id', + 'encoding' => 'test-encoding', + ); + Yii::$app->getComponent('email')->setDefaultMessageConfig($defaultMessageConfig); + + $message = new Message(); + + foreach ($defaultMessageConfig as $name => $value) { + $this->assertEquals($value, $message->$name); + } + } +} + +/** + * Test Mailer class + */ +class Mailer extends BaseMailer +{ + public $sentMessages = array(); + + public function send($message) + { + $this->sentMessages[] = $message; + } +} + +/** + * Test Message class + */ +class Message extends BaseMessage +{ + public $id; + public $encoding; + + public function setFrom($from) {} + + public function setTo($to) {} + + public function setSubject($subject) {} + + public function setText($text) {} + + public function setHtml($html) {} + + public function createAttachment($content, $fileName, $contentType = 'application/octet-stream') {} +} \ No newline at end of file diff --git a/tests/unit/framework/email/VendorMailerTest.php b/tests/unit/framework/email/VendorMailerTest.php new file mode 100644 index 00000000000..294bb3327d7 --- /dev/null +++ b/tests/unit/framework/email/VendorMailerTest.php @@ -0,0 +1,42 @@ +getMock('yii\email\VendorMailer', array('createVendorMailer', 'createVendorMessage', 'send')); + $mailer->expects($this->any())->method('createVendorMailer')->will($this->returnValue(new \stdClass())); + return $mailer; + } + + // Tests : + + public function testSetupVendorMailer() + { + $mailer = $this->createTestMailer(); + $vendorMailer = new \stdClass(); + $mailer->setVendorMailer($vendorMailer); + $this->assertEquals($vendorMailer, $mailer->getVendorMailer(), 'Unable to setup vendor mailer!'); + } + + /** + * @depends testSetupVendorMailer + */ + public function testGetDefaultVendorMailer() + { + $mailer = $this->createTestMailer(); + $vendorMailer = $mailer->getVendorMailer(); + $this->assertTrue(is_object($vendorMailer), 'Unable to get default vendor mailer!'); + } +} \ No newline at end of file diff --git a/tests/unit/framework/email/VendorMessageTest.php b/tests/unit/framework/email/VendorMessageTest.php new file mode 100644 index 00000000000..72c061a12ce --- /dev/null +++ b/tests/unit/framework/email/VendorMessageTest.php @@ -0,0 +1,135 @@ +mockApplication(); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return TestMailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new TestMailer(); + return $component; + } + + // Tests : + + public function testGetVendorMessage() + { + $message = new TestMessage(); + $vendorMessage = $message->getVendorMessage(); + $this->assertTrue(is_object($vendorMessage), 'Unable to get vendor message!'); + } + + /** + * @depends testGetVendorMessage + */ + public function testVendorMethodCall() + { + $message = new TestMessage(); + $result = $message->composeString(); + $this->assertNotEmpty($result, 'Unable to call method of vendor message!'); + } + + /** + * @depends testGetVendorMessage + */ + public function testVendorPropertyAccess() + { + $message = new TestMessage(); + + $value = 'test public field value'; + $message->publicField = $value; + $this->assertEquals($value, $message->publicField, 'Unable to access public property!'); + $this->assertTrue(isset($message->publicField), 'Unable to check if public property is set!'); + unset($message->publicField); + $this->assertFalse(isset($message->publicField), 'Unable to unset the public property!'); + + $value = 'test private field value'; + $message->privateField = $value; + $this->assertEquals($value, $message->privateField, 'Unable to access virtual property!'); + $this->assertTrue(isset($message->privateField), 'Unable to check if private property is set!'); + unset($message->privateField); + $this->assertFalse(isset($message->privateField), 'Unable to unset the private property!'); + } +} + +/** + * Test Message class + */ +class TestMessage extends AbstractVendorMessage +{ + public function setFrom($from) {} + + public function setTo($to) {} + + public function setSubject($subject) {} + + public function setText($text) {} + + public function setHtml($html) {} + + public function createAttachment($content, $fileName, $contentType = 'application/octet-stream') {} +} + +/** + * Test Vendor Message class + */ +class TestVendorMessage +{ + public $publicField; + private $_privateField; + + public function setPrivateField($value) + { + $this->_privateField = $value; + } + + public function getPrivateField() + { + return $this->_privateField; + } + + public function composeString() + { + return get_class($this); + } +} + +/** + * Test Mailer class + */ +class TestMailer extends VendorMailer +{ + public $sentMessages = array(); + + public function send($message) + { + $this->sentMessages[] = $message; + } + + protected function createVendorMailer(array $config) + { + return new \stdClass(); + } + + public function createVendorMessage() + { + return new TestVendorMessage(); + } +} \ No newline at end of file diff --git a/tests/unit/framework/email/swift/MailerTest.php b/tests/unit/framework/email/swift/MailerTest.php new file mode 100644 index 00000000000..23cd3b07d70 --- /dev/null +++ b/tests/unit/framework/email/swift/MailerTest.php @@ -0,0 +1,47 @@ +mockApplication(array( + 'vendorPath' => Yii::getAlias('@yiiunit/vendor') + )); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + $component->init(); + return $component; + } + + // Tests : + + public function testSend() + { + $emailAddress = 'someuser@somedomain.com'; + $message = new Message(); + $message->setTo($emailAddress); + $message->setFrom($emailAddress); + $message->setSubject('Yii Swift Test'); + $message->setText('Yii Swift Test body'); + $message->send(); + $this->assertTrue(true); + } +} \ No newline at end of file