diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..672ab43 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2011 Tamas Pozsonyi (pota@mosfet.hu) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fd8c5b --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Li3 mailer plugin + +A plugin for sending email messages from your li3 application. + +## Installation + +Install and register the plugin as described by [the manual](http://lithify.me/docs/manual). + +## Documentation + +The documentation was created with the [li3_docs](https://github.com/UnionOfRAD/li3_docs) plugin in mind, +please see the readme.wiki for details. + +## TODO + + * better documentation + * restructure View/Renderer/etc. (in lithium core) to better support inheritance (and support the `Simple` renderer/loader) + +## Credits + +Thanks to [nateabele](https://github.com/nateabele), [daschl](https://github.com/daschl), [masom](https://github.com/masom) and the lively [li3](http://lithify.me) community. + +## Copyright + +See LICENSE.txt for details. diff --git a/action/Mailer.php b/action/Mailer.php new file mode 100644 index 0000000..4f05df5 --- /dev/null +++ b/action/Mailer.php @@ -0,0 +1,196 @@ + 'li3_mailer\net\mail\Media', + 'delivery' => 'li3_mailer\net\mail\Delivery', + 'message' => 'li3_mailer\net\mail\Message' + ); + + /** + * Create a message object with given options. + * + * @see li3_mailer\net\mail\Message + * @param array $options Options, apart from `Message`'s options if `'class'` + * is presented it will be used for creating the instance, otherwise + * defaults to `'message'` (see `_instance()` and `$_classes`). + * @return li3_mailer\net\mail\Message A message instance. + */ + public static function message(array $options = array()) { + return static::_filter(__FUNCTION__, compact('options'), function($self, $params) { + $options = $params['options']; + $class = isset($options['class']) ? $options['class'] : 'message'; + unset($options['class']); + return $self::invokeMethod('_instance', array($class, $options)); + }); + } + + /** + * Deliver a message. Creates a message with given options, + * renders it with `Media` and delivers it via the transport + * adapter. This method is filterable and the filter receives + * the (cleared) options, data, message (object), transport + * adapter and transport options as parameters. + * + * @see li3_mailer\action\Mailer::message() + * @see li3_mailer\net\mail\Message + * @see li3_mailer\net\mail\Media::render() + * @see li3_mailer\net\mail\Transport::deliver() + * @param string $message_name Name of the message to send. + * @param array $options Options may be: + * - options for creating the message (see `message()`), + * - options for rendering the message (see `Media::render()`), + * sets `'mailer'` (to `Mailer` subclass' short name) and + * `'template'` (to `$message_name`) by default, + * - `'transport'`: transport options (see `Transport::deliver()`), + * - `'data'`: binded data for rendering (see `Media::render()`), + * - `'delivery'`: the delivery configuration and adapter to use + * (configuration will be merged into options for creating the + * the message). + * @return mixed Return value of the transport adapter's deliver method. + */ + public static function deliver($message_name, array $options = array()) { + $options = static::_options($message_name, $options); + + $delivery = static::$_classes['delivery']; + $delivery_name = isset($options['delivery']) ? $options['delivery'] : 'default'; + unset($options['delivery']); + $message_options = $options + $delivery::config($delivery_name); + $message = static::message($message_options); + $transport = $delivery::adapter($delivery_name); + $transport_options = isset($options['transport']) ? (array) $options['transport'] : array(); + unset($options['transport']); + + $data = isset($options['data']) ? $options['data'] : array(); + unset($options['data']); + $class = get_called_class(); + $name = preg_replace('/Mailer$/', '', substr($class, strrpos($class, "\\") + 1)); + $options += array( + 'mailer' => ($name == '' ? null : Inflector::underscore($name)), + 'template' => $message_name + ); + + $media = static::$_classes['media']; + $params = compact('options', 'data', 'message', 'transport', 'transport_options'); + return static::_filter(__FUNCTION__, $params, function($self, $params) use ($media) { + extract($params); + $media::render($message, $data, $options); + return $transport->deliver($message, $transport_options); + }); + } + + /** + * Allows the use of syntactic-sugar like `Mailer::deliverTestWithLocal()` instead of + * `Mailer::deliver('test', array('delivery' => 'local'))`. + * + * @see li3_mailer\action\Mailer::deliver() + * @link http://php.net/manual/en/language.oop5.overloading.php PHP Manual: Overloading + * + * @throws BadMethodCallException On unhandled call, will throw an exception. + * @param string $method Method name caught by `__callStatic()`. + * @param array $params Arguments given to the above `$method` call. + * @return mixed Results of dispatched `Mailer::deliver()` call. + */ + public static function __callStatic($method, $params) { + $found = preg_match('/^deliver(?P\w+)With(?P\w+)$/', $method, $args); + if (!$found) { + preg_match('/^deliver(?P\w+)$/', $method, $args); + } + if ($args) { + $message = Inflector::underscore($args['message']); + if (isset($params[0]) && is_array($params[0])) { + $params = $params[0]; + } + if (isset($args['delivery'])) { + $params['delivery'] = Inflector::underscore($args['delivery']); + } + return static::deliver($message, $params); + } else { + $class = get_called_class(); + $class = substr($class, strrpos($class, "\\") + 1); + throw new BadMethodCallException( + "Method `{$method}` not defined or handled in class `{$class}`." + ); + } + } + + /** + * Get options for a given message. Allows shorter options syntax + * where the first item does not have an associative key (e.g. the + * key is `0`) + * like `('message', array('foo@bar', 'subject' => 'my subject', 'my' => 'data'))` + * which will be translated to + * `('message', array('to' => 'foo@bar', 'subject' => 'my subject', + * 'data' => array('my' => 'data'))`. + * Uses `$_short_options` to detect options that should be extracted + * (unless a value for key `'data'` is already set), + * also merges in the settings from `$_messages`. + * + * @see li3_mailer\action\Mailer::$_messages + * @see li3_mailer\action\Mailer::$_short_options + * @param string $message The message identifier (name). + * @param array $options Options. + * @return array Options. + */ + protected static function _options($message, array $options = array()) { + if (isset($options[0])) { + $to = $options[0]; + unset($options[0]); + if (isset($options['data'])) { + $options += compact('to'); + } else { + $data = $options; + $blank = array_fill_keys(static::$_short_options, null); + $shorts = array_intersect_key($data, $blank); + $data = array_diff_key($data, $shorts); + $options = compact('to', 'data') + $shorts; + } + } + if (array_key_exists($message, static::$_messages)) { + $options = array_merge_recursive((array) static::$_messages[$message], $options); + } + if (isset(static::$_messages[0])) { + $options = array_merge_recursive((array) static::$_messages[0], $options); + } + return $options; + } +} + +?> \ No newline at end of file diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..665524b --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,38 @@ + array_merge(array( + '{:library}\extensions\helper\{:class}\{:name}', + '{:library}\template\helper\{:class}\{:name}' => array('libraries' => 'li3_mailer') +), (array) $existing))); + +/** + * Add paths for delivery transport adapters from this library (plugin). + */ +$existing = Libraries::paths('adapter'); +$key = '{:library}\{:namespace}\{:class}\adapter\{:name}'; +$existing[$key]['libraries'] = array_merge( + (array) $existing[$key]['libraries'], + (array) 'li3_mailer' +); +Libraries::paths(array('adapter' => $existing)); + +/* + * Ensure the mail template resources path exists. + */ +$path = Libraries::get(true, 'resources') . '/tmp/cache/mails'; +if (!is_dir($path)) { + mkdir($path); +} + +/** + * Load the file that configures the delivery system. + */ +require __DIR__ . '/bootstrap/delivery.php'; + +?> \ No newline at end of file diff --git a/config/bootstrap/delivery.php b/config/bootstrap/delivery.php new file mode 100644 index 0000000..3ee3b0f --- /dev/null +++ b/config/bootstrap/delivery.php @@ -0,0 +1,69 @@ + array( +// 'adapter' => 'Simple', +// 'from' => array('My App' => 'my@email.address') +// ))); + +/** + * Sample configuration for `'Swift'` adapter + */ +// Delivery::config(array('default' => array( +// 'adapter' => 'Swift', +// 'transport' => 'smtp', +// 'from' => array('Name' => 'my@address'), +// 'host' => 'example.host', +// 'encryption' => 'ssl' +// ))); + +/** + * ### SwiftMailer support + * + * To use the `'Swift'` adapter with the plugin the SwiftMailer library must be register first. + * To install the library execute + * `git submodule add https://github.com/swiftmailer/swiftmailer.git libraries/swiftmailer` + * and uncomment the following lines. + */ +// use lithium\core\Libraries; +// Libraries::add('swiftmailer', array( +// 'prefix' => 'Swift_', +// 'bootstrap' => 'lib/swift_required.php' +// )); + +?> \ No newline at end of file diff --git a/net/mail/Delivery.php b/net/mail/Delivery.php new file mode 100644 index 0000000..5d85100 --- /dev/null +++ b/net/mail/Delivery.php @@ -0,0 +1,41 @@ + array('adapter' => 'Simple', 'from' => 'you@example.com'), + * 'debug' => array('adapter' => 'Debug'), + * 'default' => array( + * 'adapter' => 'Swift', + * 'from' => 'you@example.com', + * 'bcc' => 'bcc@example.com', + * 'transport' => 'smtp', + * 'host' => 'example.com' + * ) + * ));}}} + * + * @see li3_mailer\net\mail\Message + * @see li3_mailer\net\mail\transport\adapter\Simple + * @see li3_mailer\net\mail\transport\adapter\Debug + * @see li3_mailer\net\mail\transport\adapter\Swift + */ +class Delivery extends \lithium\core\Adaptable { + /** + * A dot-separated path for use by `Libraries::locate()`. Used to look up the correct type of + * adapters for this class. + * + * @var string + */ + protected static $_adapters = 'adapter.net.mail.transport'; +} + +?> \ No newline at end of file diff --git a/net/mail/Grammar.php b/net/mail/Grammar.php new file mode 100644 index 0000000..f270d3a --- /dev/null +++ b/net/mail/Grammar.php @@ -0,0 +1,89 @@ + 'merge'); + + /** + * Adds config values to the public properties when a new object is created. + * + * @param array $config Options. + */ + public function __construct(array $config = array()) { + $grammar = array(); + //Basic building blocks + $grammar['NO-WS-CTL'] = '[\x01-\x08\x0B\x0C\x0E-\x19\x7F]'; + $grammar['text'] = '[\x00-\x08\x0B\x0C\x0E-\x7F]'; + $grammar['quoted-pair'] = "(?:\\\\{$grammar['text']})"; + $grammar['qtext'] = "(?:{$grammar['NO-WS-CTL']}|" . '[\x21\x23-\x5B\x5D-\x7E])'; + $grammar['atext'] = '[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]'; + $grammar['dot-atom-text'] = "(?:{$grammar['atext']}+(\.{$grammar['atext']}+)*)"; + $grammar['no-fold-quote'] = "(?:\"(?:{$grammar['qtext']}|{$grammar['quoted-pair']})*\")"; + $grammar['dtext'] = "(?:{$grammar['NO-WS-CTL']}|" . '[\x21-\x5A\x5E-\x7E])'; + $grammar['no-fold-literal'] = "(?:\[(?:{$grammar['dtext']}|{$grammar['quoted-pair']})*\])"; + //Message IDs + $grammar['id-left'] = "(?:{$grammar['dot-atom-text']}|{$grammar['no-fold-quote']})"; + $grammar['id-right'] = "(?:{$grammar['dot-atom-text']}|{$grammar['no-fold-literal']})"; + $provided = isset($config['grammar']) ? $config['grammar'] : null; + $grammar = array_merge_recursive($grammar, (array) $provided); + parent::__construct(compact('grammar') + $config); + } + + /** + * Set or retrieve grammar definition or definitons. If called + * without arguments (or both arguments are null) returns all + * the defined grammar rules. If only `$key` is provided returns + * the named definition only (if found, `null` otherwise). If + * called with both arguments set it sets the specified grammar + * rule to the given `$value` (and returns `null`). + * + * @see li3_mailer\net\mail\Grammar + * @param string $key Grammar definition key (token name). + * @param string $value Grammar definition (regular expression part). + * @return mixed All the rules (array), a specific rule (string), or `null`. + */ + public function token($key = null, $value = null) { + if ($key) { + if ($value) { + $this->_grammar[$key] = $value; + return; + } + return isset($this->_grammar[$key]) ? $this->_grammar[$key] : null; + } + return $this->_grammar; + } + + /** + * Checks if the id passed comply with RFC 2822. + * + * @param string $id Id. + * @return boolean Result. + */ + public function isValidId($id) { + $preg = '/^' . $this->token('id-left') . '@' . $this->token('id-right') . '$/D'; + return (boolean) preg_match($preg, $id); + } +} + +?> \ No newline at end of file diff --git a/net/mail/Media.php b/net/mail/Media.php new file mode 100644 index 0000000..8da7dac --- /dev/null +++ b/net/mail/Media.php @@ -0,0 +1,355 @@ + 'lithium\action\Request'); + + /** + * Maps a type name to a particular content-type (or multiple types) with a set of options, or + * retrieves information about a type that has been defined. + * + * Alternatively, can be used to retrieve content-type for a registered type (short) name: + * {{{ + * Media::type('html'); // returns 'text/html' + * Media::type('text'); // returns 'text/plain' + * }}} + * + * @see li3_mailer\net\mail\Media::$_types + * @see li3_mailer\net\mail\Media::$_handlers + * @param string $type A file-extension-style type name, i.e. `'text'` or `'html'`. + * @param mixed $content Optional. The content-type string (i.e. `'text/plain'`). May be `false` + * to delete `$type`. + * @param array $options Optional. The handling options for this media type. Possible keys are: + * - `'layout'` _mixed_: Specifies one or more `String::insert()`-style paths to use when + * searching for layout files (either a string or array of strings). + * - `'template'` _mixed_: Specifies one or more `String::insert()`-style paths to use + * when searching for template files (either a string or array of strings). + * - `'view'` _string_: Specifies the view class to use when rendering this content. + * @return mixed If `$content` and `$options` are empty, returns the content-type. Otherwise + * returns `null`. + */ + public static function type($type, $content = null, array $options = array()) { + $defaults = array( + 'view' => false, + 'template' => false, + 'layout' => false + ); + + if ($content === false) { + unset(static::$_types[$type], static::$_handlers[$type]); + } + if (!$content && !$options) { + return static::_types($type); + } + if ($content) { + static::$_types[$type] = $content; + } + static::$_handlers[$type] = $options ? ($options + $defaults) : array(); + } + + /** + * Renders data (usually the result of a mailer delivery action) and generates a string + * representation of it, based on the types of expected output. Also ensures the message's + * fields are valid according to the RFC 2822. + * + * @param object $message A reference to a Message object into which the operation will be + * rendered. The content of the render operation will be assigned to the `$body` + * property of the object. + * @param mixed $data + * @param array $options + * @return void + * @filter + */ + public static function render(&$message, $data = null, array $options = array()) { + $params = array('message' => &$message) + compact('data', 'options'); + $handlers = static::_handlers(); + + static::_filter(__FUNCTION__, $params, function($self, $params) use ($handlers) { + $defaults = array('template' => null, 'layout' => 'default', 'view' => null); + $message =& $params['message']; + $data = $params['data']; + $options = $params['options']; + + foreach ((array) $message->types as $type) { + if (!isset($handlers[$type])) { + throw new MediaException("Unhandled media type `{$type}`."); + } + $handler = $options + $handlers[$type] + $defaults + array('type' => $type); + $filter = function($v) { return $v !== null; }; + $handler = array_filter($handler, $filter) + $handlers['default'] + $defaults; + $handler['paths'] = $self::invokeMethod( + '_finalize_paths', + array($handler['paths'], $options) + ); + + $params = array($handler, $data, $message); + $message->body($type, $self::invokeMethod('_handle', $params)); + } + $message->ensureStandardCompliance(); + }); + } + + /** + * Called by `Media::render()` to render message content. Given a content handler and data, + * calls the content handler and passes in the data, receiving back a rendered content string. + * + * @see lithium\net\mail\Message + * @param array $handler Handler configuration. + * @param array $data Binded data. + * @param object $message A reference to the `Message` object for rendering. + * @return string Rendered content. + * @filter + */ + protected static function _handle($handler, $data, &$message) { + $params = array('message' => &$message) + compact('handler', 'data'); + + return static::_filter(__FUNCTION__, $params, function($self, $params) { + $message = $params['message']; + $handler = $params['handler']; + $data = $params['data']; + $options = $handler; + + switch (true) { + case ($handler['template'] === false) && is_string($data): + return $data; + case $handler['view']: + unset($options['view']); + $instance = $self::view($handler, $data, $message, $options); + return $instance->render('all', (array) $data, $options); + default: + throw new MediaException('Could not interpret type settings for handler.'); + } + }); + } + + /** + * Configures a template object instance, based on a media handler configuration. + * + * @see li3_mailer\net\mail\Media::type() + * @see lithium\template\View::render() + * @see li3_mailer\net\mail\Message + * @param mixed $handler Either a string specifying the name of a media type for which a handler + * is defined, or an array representing a handler configuration. For more on types + * and type handlers, see the `type()` method. + * @param mixed $data The data to be rendered. Usually an array. + * @param object $message The `Message` object. + * @param array $options Any options that will be passed to the `render()` method of the + * templating object. + * @return object Returns an instance of a templating object, usually `lithium\template\View`. + * @filter + */ + public static function view($handler, $data, &$message = null, array $options = array()) { + $params = array('message' => &$message) + compact('handler', 'data', 'options'); + + return static::_filter(__FUNCTION__, $params, function($self, $params) { + $data = $params['data']; + $options = $params['options']; + $handler = $params['handler']; + $message =& $params['message']; + + if (!is_array($handler)) { + $handler = $self::invokeMethod('_handlers', array($handler)); + } + $class = $handler['view']; + unset($handler['view']); + + $config = $handler + array('message' => &$message); + return $self::invokeMethod('_instance', array($class, $config)); + }); + } + + /** + * Calculates the absolute path to a static asset when attaching. By default a + * relative path will be prepended with the given library's path and + * `'/mails/_assets/'` (e.g. the path `'foo/bar.txt'` with the default library will + * be resolved to `'/path/to/li3_install/app/mails/_assets/foo/bar.txt'`). + * + * @see li3_mailer\net\mail\Message::attach() + * @param string $path The path to the asset, relative to the given library's path. If the path + * contains a URI Scheme (eg. `http://`) or is absolute, no path munging will occur. + * @param array $options Contains setting for finding and handling the path, where the keys are + * the following: + * - `'check'`: Check for the existence of the file before returning. Defaults to + * `false`. + * - `'library'`: The name of the library from which to load the asset. Defaults to + * `true`, for the default library. + * @return string Returns the absolute path to the static asset. If checking for the asset's + * existence (`$options['check']`), returns `false` if it does not exist. + * @filter + */ + public static function asset($path, array $options = array()) { + $defaults = array( + 'check' => false, + 'library' => true + ); + $options += $defaults; + $params = compact('path', 'options'); + + return static::_filter(__FUNCTION__, $params, function($self, $params) { + extract($params); + + if (preg_match('/^[a-z0-9-]+:\/\//i', $path)) { + return $path; + } + if ($path[0] !== '/') { + $base = Libraries::get($options['library'], 'path'); + $path = $base . '/mails/_assets/' . $path; + } + if ($options['check'] && !is_file($path)) { + return false; + } + return $path; + }); + } + + /** + * Helper method for listing registered media types. Returns all types, or a single + * content type if a specific type is specified. + * + * @param string $type Type to return. + * @return mixed Array of types, or single type requested. + */ + protected static function _types($type = null) { + $types = static::$_types + array( + 'html' => 'text/html', + 'text' => 'text/plain' + ); + if ($type) { + return isset($types[$type]) ? $types[$type] : null; + } + return $types; + } + + /** + * Helper method for listing registered type handlers. Returns all handlers, or the + * handler for a specific media type, if requested. + * + * @param string $type The type of handler to return. + * @return mixed Array of all handlers, or the handler for a specific type. + */ + protected static function _handlers($type = null) { + $handlers = static::$_handlers + array( + 'default' => array( + 'view' => 'li3_mailer\template\Mail', + 'paths' => array( + 'template' => array( + '{:library}/mails/{:mailer}/{:template}.{:type}.php' => 'mailer', + '{:library}/mails/{:template}.{:type}.php'), + 'layout' => '{:library}/mails/layouts/{:layout}.{:type}.php', + 'element' => '{:library}/mails/elements/{:template}.{:type}.php' + ) + ), + 'html' => array(), + 'text' => array() + ); + + if ($type) { + return isset($handlers[$type]) ? $handlers[$type] : null; + } + return $handlers; + } + + /* + * Finalize paths according to available data. Paths defined as arrays + * may have the `String::insert()` style paths as the indexes and use + * a string or array of strings of keys that should be presented in the + * data to enable that path. This way conditional paths may be defined, + * and is used by the `'default'` handler to enable/disable `'template'` + * paths based on whether the `'mailer'` is available. + * + * @see li3_mailer\net\mail\Media::_handlers() + * @see li3_mailer\net\mail\Media::render() + * @param array $paths The paths configuration that should be finalized. + * @param array $data The data. + * @return array Finalized paths. + */ + protected static function _finalize_paths($paths, array $data) { + $finalized = array(); + foreach ($paths as $type => $path) { + if (!is_array($path)) { + $finalized[$type] = $path; + continue; + } + $subfinalized = array(); + foreach ((array) $path as $string => $needed) { + if (is_int($string) || is_null($needed) || ($needed === array())) { + $subfinalized[] = is_int($string) ? $needed : $string; + continue; + } + foreach ((array) $needed as $var) { + if (!isset($data[$var])) { + continue 2; + } + } + $subfinalized[] = $string; + } + $finalized[$type] = $subfinalized; + } + return $finalized; + } + + /** + * Creates a Request that can be used for rendering (e.g. when constructing + * urls for example). Tries to set the request's `'HTTP_HOST'`, `'HTTPS'` + * and `'base'` variables according to message's `$base_url`. + * + * @see li3_mailer\template\mail\adapter\File::_init() + * @see li3_mailer\net\mail\Message::$base_url + * @param object $message Message. + * @return object Request. + * @filter + */ + protected static function _request($message) { + $params = compact('message'); + return static::_filter(__FUNCTION__, $params, function($self, $params) { + extract($params); + $config = array(); + if ($message && ($base_url = $message->base_url)) { + list($scheme, $url) = explode('://', $base_url); + $parts = explode('/', $url, 2); + $host = array_shift($parts); + $base = array_shift($parts); + $base = $base ? '/' . $base : ''; + $env = array('HTTP_HOST' => $host); + if ($scheme == 'https') { + $env['HTTPS'] = true; + } + $config += compact('env', 'base'); + } + return $self::invokeMethod('_instance', array('request', $config)); + }); + } +} + +?> \ No newline at end of file diff --git a/net/mail/MediaException.php b/net/mail/MediaException.php new file mode 100644 index 0000000..ecac12e --- /dev/null +++ b/net/mail/MediaException.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/net/mail/Message.php b/net/mail/Message.php new file mode 100644 index 0000000..36b93e2 --- /dev/null +++ b/net/mail/Message.php @@ -0,0 +1,634 @@ + 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'avi' => 'video/avi', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bz2', + 'csv' => 'text/csv', + 'dmg' => 'application/x-apple-diskimage', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'eml' => 'message/rfc822', + 'aps' => 'application/postscript', + 'exe' => 'application/x-ms-dos-executable', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gz' => 'application/x-gzip', + 'hqx' => 'application/stuffit', + 'htm' => 'text/html', + 'html' => 'text/html', + 'jar' => 'application/x-java-archive', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'm3u' => 'audio/x-mpegurl', + 'm4a' => 'audio/mp4', + 'mdb' => 'application/x-msaccess', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'odg' => 'vnd.oasis.opendocument.graphics', + 'odp' => 'vnd.oasis.opendocument.presentation', + 'odt' => 'vnd.oasis.opendocument.text', + 'ods' => 'vnd.oasis.opendocument.spreadsheet', + 'ogg' => 'audio/ogg', + 'pdf' => 'application/pdf', + 'png' => 'image/png', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'rar' => 'application/x-rar-compressed', + 'rtf' => 'application/rtf', + 'tar' => 'application/x-tar', + 'sit' => 'application/x-stuffit', + 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'ttf' => 'application/x-font-truetype', + 'txt' => 'text/plain', + 'vcf' => 'text/x-vcard', + 'wav' => 'audio/wav', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'audio/x-ms-wmv', + 'xls' => 'application/excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'zip' => 'application/zip' + ); + + /** + * Message grammar, used for checking generated Cotent-IDs. + * + * @see li3_mailer\net\mail\Message::generateId() + * @see li3_mailer\net\mail\Grammar + * @var object + */ + protected $_grammar; + + /** + * Classes used by `Message`. + * + * @var array + */ + protected $_classes = array( + 'media' => 'li3_mailer\net\mail\Media', 'grammar' => 'li3_mailer\net\mail\Grammar' + ); + + /** + * Auto configuration properties. + * + * @var array + */ + protected $_autoConfig = array('classes' => 'merge', 'mime_types' => 'merge'); + + /** + * Adds config values to the public properties when a new object is created. + * + * @see li3_mailer\net\mail\Message::attach() + * @see li3_mailer\net\mail\Message::_init() + * @see li3_mailer\net\mail\Message::$_mime_types + * @see li3_mailer\net\mail\Message::$_classes + * @param array $config Supported options: + * - the public properties, such as `'date'`, `'to'`, etc., + * - `'attach'` _array_: list of attachment configurations, + * the values may be string paths or configurations indexed + * by path (or integer index if path is not appropiate), see + * `attach()` and `_init()`, + * - `'mime_types'` _array_: list of (additional) mime types for + * autodetecting attachment types, see `$_mime_types` and `attach()`, + * - `'classes'` _array_: class dependencies, see `$_classes`. + */ + public function __construct(array $config = array()) { + foreach (array_filter($config) as $key => $value) { + $this->{$key} = $value; + } + $defaults = array('mime_types' => array()); + parent::__construct($config + $defaults); + } + + /** + * Initialize the message. + * + * Creates the attachments set in configuration (see constructor). + * The `'attach'` array can contain any of: + * + * - integer key with string path value (meaning empty configuration) + * - integer key with configuration array (meanining `null` path) + * - path as key with null (meaning empty configuration) or configuration array as value + * + * For available configuration options see `attach()`. + * + * Furthermore it initializes the grammar (used to validate generated Content-IDs, + * see `randomId()`) and the `$base_url` (see `discoverURL()`). + * + * @see li3_mailer\net\mail\Message::__construct() + * @see li3_mailer\net\mail\Message::attach() + * @see li3_mailer\net\mail\Message::randomId() + * @see li3_mailer\net\mail\Message::discoverURL() + * @return void + */ + protected function _init() { + if (isset($this->_config['attach'])) { + foreach ((array) $this->_config['attach'] as $path => $cfg) { + if (is_int($path)) { + $path = null; + } + if (is_string($cfg)) { + $path = $cfg; + $cfg = null; + } + $this->attach($path, (array) $cfg); + } + } + $grammar = (array) (isset($this->_config['grammar']) ? $this->_config['grammar'] : null); + $this->_grammar = $this->_instance('grammar', compact('grammar')); + $this->base_url = $this->base_url ?: $this->discoverURL(); + if ($this->base_url && strpos($this->base_url, '://') === false) { + $this->base_url = 'http://' . $this->base_url; + } + if ($this->base_url) { + $this->base_url = rtrim($this->base_url, '/'); + } + } + + /** + * Add a header to message. + * + * @param string $key Header name. + * @param string $value Header value (deletes the header if `false`). + * @return void + */ + public function header($key, $value) { + if ($value === false) { + unset($this->headers[$key]); + } else { + $this->headers[$key] = $value; + } + } + + /** + * Add body parts or get body for a given type. + * + * @param mixed $type Content-type. + * @param mixed $data Body parts to add if any. + * @param array $options Options: + * - `'buffer'`: split the body string. + * @return mixed String or array body. + */ + public function body($type, $data = null, $options = array()) { + $default = array('buffer' => null); + $options += $default; + if (!isset($this->body[$type])) { + $this->body[$type] = array(); + } + $this->body[$type] = array_merge((array) $this->body[$type], (array) $data); + $body = join("\n", $this->body[$type]); + return ($options['buffer']) ? str_split($body, $options['buffer']) : $body; + } + + /** + * Get the list of types this as short-name => content-type pairs + * this message should be rendered in. + * + * @return array List of types. + */ + public function types() { + $types = (array) $this->types; + $media = $this->_classes['media']; + return array_combine($types, array_map(function($type) use ($media) { + return $media::type($type); + }, $types)); + } + + /** + * Ensures the message's fields are valid according to the RFC 2822. + * + * @see http://tools.ietf.org/html/rfc2822 + * @see li3_mailer\net\mail\Message::ensureValidDate() + * @see li3_mailer\net\mail\Message::ensureValidFrom() + * @see li3_mailer\net\mail\Message::ensureValidSender() + */ + public function ensureStandardCompliance() { + $this->ensureValidDate(); + $this->ensureValidFrom(); + $this->ensureValidSender(); + } + + /** + * Ensures that the message has a valid `$date` set. Sets the + * current time if not set. + * + * @see http://tools.ietf.org/html/rfc2822#section-3.6 + * @see li3_mailer\net\mail\Message::$date + * @throws RuntimeException Throws an exception if the value + * is not a valid timestamp. + * @return void + */ + public function ensureValidDate() { + if (!$this->date) { + $this->date = time(); + } + if (!is_int($this->date) || $this->date < 0 || $this->date > 2147483647) { + throw new RuntimeException("Invalid date timestamp `{$this->date}` set for `Message`."); + } + } + + /** + * Ensures that the message has a valid `$from` set. + * + * @see http://tools.ietf.org/html/rfc2822#section-3.6 + * @see li3_mailer\net\mail\Message::$from + * @throws RuntimeException Throws an exception if `$from` + * is empty or not an array or string. + * @return void + */ + public function ensureValidFrom() { + if (!$this->from) { + throw new RuntimeException('`Message` should have at least one `$from` address.'); + } else if (!is_string($this->from) && !is_array($this->from)) { + $type = gettype($this->from); + throw new RuntimeException( + "`Message`'s `\$from` field should be a string or an array, `{$type}` given." + ); + } + } + + /** + * Sets `$sender'` if empty and there are multiple `$from` addresses + * (`$sender` will be set to the first) or removes `$sender` if set and + * is identical to the single `$from` address; and ensures `$sender` is a + * single address. According to the RFC 2822 (section 3.6.2.): 'If the + * originator of the message can be indicated by a single mailbox and the + * author and transmitter are identical, the "Sender:" field SHOULD NOT + * be used. Otherwise, both fields SHOULD appear.' + * + * @see http://tools.ietf.org/html/rfc2822#section-3.6.2 + * @see li3_mailer\net\mail\Message::$sender + * @throws RuntimeException Throws an exception if `$sender` + * is set and is not a single address. + * @return void + */ + public function ensureValidSender() { + $from = (array) $this->from; + $sender = (array) $this->sender; + if (!$sender && count($from) > 1) { + $this->sender = array(key($from) => current($from)); + } else if ($sender && count($from) == 1 && $sender == $from) { + $this->sender = null; + } + if ($this->sender && count((array) $this->sender) > 1) { + throw new RuntimeException('`Message` should only have a single `$sender` address.'); + } + } + + /** + * Attach content to the message. + * + * If `$path` is a string it is resolved with `Media::asset()` and its content will be + * attached (with setting the defaults for `'filename'` to file's basename and `'content-type'` + * from `$_mime_types` if file's extension is registered). + * + * If `$path` is `null` (or not a string) then `$options['data']` is used. It must be a string + * and will be used as the content body. If `'filename'` is given and its extension is + * registered in `$_mime_types` then it is used as the default value for `'content-type'`. + * + * Examples: + * {{{ + * // attach a simple file in asset directory + * $message->attach('file.pdf'); + * + * // attach a simple file with absolute path + * $message->attach('/path/to/file.pdf'); + * + * // attach a remote file + * $message->attach('http://example.host/file.pdf'); + * + * // attach a simple file with different filename + * $message->attach('/path/to/file.pdf', array( + * 'filename' => 'cool_file.pdf' + * )); + * + * // attach simple content with filename + * $message->attach(null, array( + * 'data' => 'this is my content', + * 'filename' => 'cool.txt' + * )); + * + * // attach data with content type + * $img_data = create_custom_image(...); + * $message->attach(null, array( + * 'data' => $img_data, + * 'filename' => 'cool.png', + * 'content-type' => 'image/png' + * )); + * }}} + * + * @see li3_mailer\net\mail\Media::asset() + * @see li3_mailer\net\mail\Message::$_mime_types + * @see li3_mailer\net\mail\Message::embed() + * @param string $path Path to file, may be null. + * @param array $options Available options are: + * - `'data'` _string_: content body (if `$path` is a string this is ignored), + * - `'disposition'` _string_: disposition, usually `'attachment'` or `'inline'`, + * defaults to `'attachment'`, + * - `'content-type'` _string_: content-type, defaults to `'application/octet-stream'`, + * - `'filename'` _string_: filename, + * - `'id'` _string_: content-id, useful for embedding, see `embed()`, + * - `'library'` _boolean_ or _string_: name of the library to resolve path with (when + * `$path` is string and is relative), defaults to `true` (meaning the default + * library), see `Media::asset()`, + * - `'check'` _boolean_: check if file exists (if `$path` is string), defaults to + * `true`, see `Media::asset()`. + * @throws RuntimeException Throws an exception if neither `$path` nor `$options['data']` is + * valid, when `$path` is set but does not exists or when `$options['data']` is + * not string (and `$path` is not string). + * @return object Message object this method was called on. + */ + public function attach($path, array $options = array()) { + if (!is_string($path) && !isset($options['data'])) { + throw new RuntimeException('Neither path nor data provided, cannot attach.'); + } + $defaults = array( + 'disposition' => 'attachment', 'content-type' => 'application/octet-stream' + ); + if (is_string($path)) { + $media = $this->_classes['media']; + $asset_defaults = array('check' => true, 'library' => true); + $asset_path = $media::asset($path, $options + $asset_defaults); + if ($asset_path === false) { + throw new RuntimeException( + "File at `{$path}` is not a valid asset, cannot attach." + ); + } + $attach_path = $path; + $path = $asset_path; + unset($options['data']); + $defaults += array('filename' => basename($path)); + $extension = pathinfo($path, PATHINFO_EXTENSION); + if (isset($this->_mime_types[$extension])) { + $defaults['content-type'] = $this->_mime_types[$extension]; + } + $options = compact('path', 'attach_path') + $options + $defaults; + } else { + if (!is_string($options['data'])) { + $type = gettype($options['data']); + throw new RuntimeException( + "Data should be a string, `{$type}` given, cannot attach." + ); + } + if (isset($options['filename'])) { + $extension = pathinfo($options['filename'], PATHINFO_EXTENSION); + if (isset($this->_mime_types[$extension])) { + $defaults['content-type'] = $this->_mime_types[$extension]; + } + } + $options += $defaults; + } + $this->_attachments[] = $options; + return $this; + } + + /** + * Detach content. `$path` should be the same identifier used to `attach()` content, + * e.g. `$path` or `$options['data']`. + * + * @see li3_mailer\net\mail\Message::attach() + * @param string $path Path (or data) used to attach content. + * @return object Message object this method was called on. + */ + public function detach($path) { + $filter = function($cfg) use ($path) { + switch (true) { + case isset($cfg['attach_path']) && $cfg['attach_path'] === $path: + case isset($cfg['data']) && $cfg['data'] === $path: + return false; + } + return true; + }; + $this->_attachments = array_filter($this->_attachments, $filter); + return $this; + } + + /** + * Retrieve all attachments. + * + * @see li3_mailer\net\mail\Message::attach() + * @return array Attachments. + */ + public function attachments() { + return $this->_attachments; + } + + + /** + * Embed content. Sets default options, calls `attach()` with it and returns the Content-ID + * suitable for embedding. + * + * Example: + * {{{ + * //embed a picture + * $cid = $message->embed('picture.png'); + * //use the Content-ID as the src in the body + * $message->body('html', 'my image: my image'); + * }}} + * + * The default options set by this method are: + * + * - `'id'`: generates a random id with `randomId()`, + * - `'disposition'`: defaults to `'inline'`. + * + * @see li3_mailer\net\mail\Message::randomId() + * @see li3_mailer\net\mail\Message::attach() + * @param string $path See `attach()`. + * @param array $options See `attach()`. + * @return string Content-ID. + */ + public function embed($path, array $options = array()) { + $options += array('id' => $this->randomId(), 'disposition' => 'inline'); + $this->attach($path, $options); + return $options['id']; + } + + /** + * Generate a random Content-ID for embedded attachments. Checks its + * validity with `$_grammar`. + * + * @see li3_mailer\net\mail\Message::embed() + * @see li3_mailer\net\mail\Message::$_grammar + * @see li3_mailer\net\mail\Grammar + * @see li3_mailer\net\mail\Grammar::isValidId() + * @return string Content-ID. + */ + protected function randomId() { + $left = time() . '.' . uniqid(); + if (!empty($this->base_url)) { + list($scheme, $url) = explode('://', $this->base_url); + $parts = explode('/', $url, 2); + $right = array_shift($parts); + } else { + $right = 'li3_mailer.generated'; + } + $id = "{$left}@{$right}"; + if (!$this->_grammar->isValidId($id)) { + $id = "{$left}@li3_mailer.generated"; + } + return $id; + } + + /** + * Try to discover base url from `$_SERVER` (with `Request`). + * + * @see li3_mailer\net\mail\Message::_init() + * @see lithium\action\Request + * @return string Base url if found, `null` otherwise. + */ + protected static function discoverURL() { + $request = new Request(); + if ($host = $request->env('HTTP_HOST')) { + $scheme = $request->env('HTTPS') ? 'https://' : 'http://'; + $base = $request->env('base'); + return $scheme . $host . $base; + } + return null; + } +} + +?> \ No newline at end of file diff --git a/net/mail/Transport.php b/net/mail/Transport.php new file mode 100644 index 0000000..c5782b6 --- /dev/null +++ b/net/mail/Transport.php @@ -0,0 +1,40 @@ +"; + }, array_keys($address), $address)); + } else { + return $address; + } + } +} + +?> \ No newline at end of file diff --git a/net/mail/transport/adapter/Debug.php b/net/mail/transport/adapter/Debug.php new file mode 100644 index 0000000..570bf61 --- /dev/null +++ b/net/mail/transport/adapter/Debug.php @@ -0,0 +1,188 @@ + array( + * 'adapter' => 'Debug', + * 'from' => 'my@address', + * 'log' => '/path/to/log', + * 'format' => 'custom', + * 'formats' => array( + * 'custom' => 'Custom log for {:to}, {:subject}' + * ) + * )));}}} + * Apart from message parameters (like `'from'`, `'to'`, etc.) for supported + * options see `deliver()`. + * + * @see li3_mailer\net\mail\Delivery + * @see li3_mailer\net\mail\transport\adapter\Debug::deliver() + */ +class Debug extends \li3_mailer\net\mail\Transport { + /** + * Log a message. + * + * @see li3_mailer\net\mail\transport\adapter\Debug::format() + * @param object $message The message to log. + * @param array $options Options supported: + * - `'log'` _string_ or _resource_: Path to the log file or directory. If points to a + * file entries will be appended to this file, if points to directory every message + * will be logged to a new file in this directory (with a unique name generated with + * `time()` and `uniqid()`). + * Alternatively it may be a resource. Defaults to `'/tmp/logs/mail.log'` relative to + * application's resources. + * - `'format'` _string_: formatter name, defaults to `'normal'`, see `format()`. + * @return boolean Returns `true` if the message was successfully logged, `false` otherwise. + */ + public function deliver($message, array $options = array()) { + $options = $this->_config + $options + array( + 'log' => Libraries::get(true, 'resources') . '/tmp/logs/mail.log', + 'format' => 'normal' + ); + $entry = '[' . date('c') . '] ' . $this->format($message, $options['format']) . PHP_EOL; + $log = $options['log']; + if (!is_resource($log)) { + if (is_dir($log)) { + $log .= DIRECTORY_SEPARATOR . time() . uniqid() . '.mail'; + } + $log = fopen($log , 'a+'); + } + $result = fwrite($log, $entry); + if (!is_resource($options['log'])) { + fclose($log); + } + return $result !== false && $result == strlen($entry); + } + + /** + * Format a message with formatter. + * + * @see li3_mailer\net\mail\transport\adapter\Debug::_formatters() + * @param object $message Message to format. + * @param string $format Formatter name to use. + * @return string Formatted log entry. + */ + protected function format($message, $format) { + $formatters = $this->_formatters(); + $formatter = isset($formatters[$format]) ? $formatters[$format] : null; + switch (true) { + case $formatter instanceof Closure: + return $formatter($message); + case is_string($formatter): + $data = $this->_message_data($message); + return String::insert($formatter, $data); + default: + throw new RuntimeException( + "Formatter for format `{$format}` is neither string nor closure." + ); + } + } + + /** + * Helper method for getting log formatters indexed by name. Values may be + * `String::insert()` style strings (receiving the `Message`'s properties + * as data according to `_message_data()`) or closures (receiving the + * `Message` as the argument and should return a string that will be placed + * in the log). + * Additional formatters may be added with configuration key `'formats'`. + * + * @see li3_mailer\net\mail\transport\adapter\Debug::_message_data() + * @see li3_mailer\net\mail\transport\adapter\Debug::format() + * @see lithium\util\String::insert() + * @return array Available formatters indexed by name. + */ + protected function _formatters() { + $config = $this->_config + array('formats' => array()); + return (array) $config['formats'] + array( + 'short' => 'Sent to {:to} with subject `{:subject}`.', + 'normal' => "Mail sent to {:to} from {:from}" . + " (sender: {:sender}, cc: {:cc}, bcc: {:bcc})\n" . + "with date {:date} and subject `{:subject}` in formats {:types}," . + " text message body:\n{:body_text}\n", + 'full' => "Mail sent to {:to} from {:from}" . + " (sender: {:sender}, cc: {:cc}, bcc: {:bcc})\n" . + "with date {:date} and subject `{:subject}` in formats {:types}," . + " text message body:\n{:body_text}\nhtml message body:\n{:body_html}\n", + 'verbose' => function($message) { + return "Mail sent with properties:\n" . var_export(get_object_vars($message), true); + } + ); + } + + /** + * Helper method to get message property data for `String::insert()` + * style formatters. Additional data may be added with the + * configuration key `'message_data'`, which should be an array of: + * + * - strings: property names (with integer keys) or the special + * `'address'` value with the property name as the key (in which + * case the property will be transformed with `address()`). + * - closures: the keys should be property names, the closure + * receives the message's property as the first argument and + * should return altered data. If the key is `''` the closure will + * receive the message object as the first argument and should + * return an array which will be merged into the data array. + * + * @see li3_mailer\net\mail\transport\adapter\Debug::_formatters() + * @see li3_mailer\net\mail\transport\adapter\Debug::format() + * @see li3_mailer\net\mail\Message + * @param object $message Message. + * @return array Message data. + */ + protected function _message_data($message) { + $config = $this->_config + array('message_data' => array()); + $map = (array) $config['message_data'] + array( + 'subject', 'charset', 'return_path', 'sender' => 'address', 'from' => 'address', + 'reply_to' => 'address', 'to' => 'address', 'cc' => 'address', 'bcc' => 'address', + 'date' => function($time) { return date('Y-m-d H:i:s', $time); }, + 'types' => function($types) { return join(', ', $types); }, + 'headers' => function($headers) { return join(PHP_EOL, $headers); }, + 'body' => function($bodies) { + return join(PHP_EOL, array_map(function($body) { + return join(PHP_EOL, $body); + }, $bodies)); + }, + '' => function($message) { + return array_combine( + array_map(function($type) { + return "body_{$type}"; + }, $message->types), + array_map(function($type) use ($message) { + return $message->body($type); + }, $message->types) + ); + } + ); + $data = array(); + foreach ($map as $prop => $config) { + if ($prop === '') { + continue; + } + if (is_int($prop)) { + $prop = $config; + $config = null; + } + $value = $message->$prop; + if ($config instanceof Closure) { + $value = $config($value); + } else if ($config == 'address') { + $value = $this->address($value); + } + $data[$prop] = $value; + } + if (isset($map['']) && $map[''] instanceof Closure) { + $data = $map['']($message) + $data; + } + return $data; + } +} + +?> \ No newline at end of file diff --git a/net/mail/transport/adapter/Simple.php b/net/mail/transport/adapter/Simple.php new file mode 100644 index 0000000..4e76a21 --- /dev/null +++ b/net/mail/transport/adapter/Simple.php @@ -0,0 +1,117 @@ + array( + * 'adapter' => 'Simple', 'from' => 'my@address' + * )));}}} + * Apart from message parameters (like `'from'`, `'to'`, etc.) no options + * supported. + * + * @see http://php.net/manual/en/function.mail.php + * @see li3_mailer\net\mail\Delivery + * @see li3_mailer\net\mail\transport\adapter\Simple::deliver() + */ +class Simple extends \li3_mailer\net\mail\Transport { + /** + * Message property names for translating a `li3_mailer\net\mail\Message` + * properties to headers (these properties are addresses). + * + * @see li3_mailer\net\mail\transport\adapter\Simple::deliver() + * @see li3_mailer\net\mail\Message + * @var array + */ + protected $_message_addresses = array( + 'return_path' => 'Return-Path', 'sender', 'from', + 'reply_to' => 'Reply-To', 'to', 'cc', 'bcc' + ); + + /** + * Deliver a message with `PHP`'s built-in `mail` function. + * + * @see http://php.net/manual/en/function.mail.php + * @param object $message The message to deliver. + * @param array $options No options supported. + * @return mixed The return value of the `mail` function. + */ + public function deliver($message, array $options = array()) { + $headers = $message->headers; + foreach ($this->_message_addresses as $property => $header) { + if (is_int($property)) { + $property = $header; + $header = ucfirst($property); + } + $headers[$header] = $this->address($message->$property); + } + $headers['Date'] = date('r', $message->date); + $headers['MIME-Version'] = "1.0"; + + $types = $message->types(); + $attachments = $message->attachments(); + $charset = $message->charset; + if (count($types) == 1 && count($attachments) == 0) { + $type = key($types); + $content_type = current($types); + $headers['Content-Type'] = "{$content_type};charset=\"{$charset}\""; + $body = wordwrap($message->body($type), 70); + } else { + $boundary = uniqid('LI3_MAILER_SIMPLE_'); + $headers['Content-Type'] = "multipart/alternative;boundary=\"{$boundary}\""; + $body = "This is a multi-part message in MIME format.\n\n"; + foreach ($types as $type => $content_type) { + $body .= "--{$boundary}\n"; + $body .= "Content-Type: {$content_type};charset=\"{$charset}\"\n\n"; + $body .= wordwrap($message->body($type), 70) . "\n"; + } + foreach ($attachments as $attachment) { + if (isset($attachment['path'])) { + if ($attachment['path'][0] == '/' && !is_readable($attachment['path'])) { + $content = false; + } else { + $content = file_get_contents($attachment['path']); + } + if ($content === false) { + throw new RuntimeException("Can not attach path `{$attachment['path']}`."); + } + } else { + $content = $attachment['data']; + } + $body .= "--{$boundary}\n"; + $filename = isset($attachment['filename']) ? $attachment['filename'] : null; + if (isset($attachment['content-type'])) { + $content_type = $attachment['content-type']; + if ($filename && !preg_match('/;\s+name=/', $content_type)) { + $content_type .= "; name=\"{$filename}\""; + } + $body .= "Content-Type: {$content_type}\n"; + } + if (isset($attachment['disposition'])) { + $disposition = $attachment['disposition']; + if ($filename && !preg_match('/;\s+filename=/', $disposition)) { + $disposition .= "; filename=\"{$filename}\""; + } + $body .= "Content-Disposition: {$disposition}\n"; + } + if (isset($attachment['id'])) { + $body .= "Content-ID: <{$attachment['id']}>\n"; + } + $body .= "\n" . wordwrap($content, 70) . "\n"; + } + $body .= "--{$boundary}--"; + } + + $headers = join("\r\n", array_map(function($name, $value) { + return "{$name}: {$value}"; + }, array_keys($headers), $headers)); + $to = $this->address($message->to); + return mail($to, $message->subject, $body, $headers); + } +} + +?> \ No newline at end of file diff --git a/net/mail/transport/adapter/Swift.php b/net/mail/transport/adapter/Swift.php new file mode 100644 index 0000000..b2e2ca3 --- /dev/null +++ b/net/mail/transport/adapter/Swift.php @@ -0,0 +1,206 @@ + array( + * 'adapter' => 'Swift', + * 'from' => 'my@address', + * 'transport' => 'smtp', + * 'host' => 'example.host', + * 'encryption' => 'ssl' + * )));}}} + * The adapter supports the `Swift_MailTransport`, `Swift_SendmailTransport` and + * `Swift_SmtpTransport` transports (configured with `'transport'` set + * to `'mail'`, `'sendmail'` and `'smtp'` respectively). + * Apart from message parameters (like `'from'`, `'to'`, etc.) for supported + * options see `$_transport` and `deliver()`. + * + * @see http://swiftmailer.org/ + * @see li3_mailer\net\mail\transport\adapter\Swift::$_transport + * @see li3_mailer\net\mail\transport\adapter\Debug::deliver() + * @see li3_mailer\net\mail\Delivery + */ +class Swift extends \li3_mailer\net\mail\Transport { + /** + * Transport option names indexed by transport type. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::transport() + * @var array + */ + protected $_transport = array( + 'mail' => 'extra_params', + 'sendmail' => 'command', + 'smtp' => array( + 'host', 'port', 'timeout', 'encryption', 'sourceip', 'local_domain', + 'auth_mode', 'username', 'password' + ) + ); + + /** + * Message property names for translating a `li3_mailer\net\mail\Message` + * to `Swift_Message`. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::message() + * @see li3_mailer\net\mail\Message + * @see Swift_Message + * @var array + */ + protected $_message_properties = array( + 'subject', 'date', 'return_path', 'sender' => 'address', 'from' => 'address', + 'reply_to' => 'address', 'to' => 'address', 'cc' => 'address', 'bcc' => 'address', 'charset' + ); + + /** + * Message attachment configuration names for translating a `li3_mailer\net\mail\Message`'s + * attachments to `Swift_Attachment`s. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::message() + * @see li3_mailer\net\mail\Message + * @see Swift_Attachment + * @var array + */ + protected $_attachment_properties = array( + 'disposition', 'content-type', 'filename', 'id' + ); + + /** + * Deliver a message with the SwiftMailer library. For available + * options see `transport()`. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::transport() + * @see li3_mailer\net\mail\transport\adapter\Swift::message() + * @param object $message The message to deliver. + * @param array $options Options (see `transport()`). + * @return mixed The return value of the `Swift_Mailer::send()` method. + */ + public function deliver($message, array $options = array()) { + $transport = $this->transport($options); + $message = $this->message($message); + return $transport->send($message); + } + + /** + * Get a transport mailer. Creates a Swift transport with type + * `$_config['transport']` and applies options (see `$_transport`) + * to it from `$_config` and `$options`. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::$_transport + * @see Swift_Mailer + * @see Swift_SmtpTransport + * @see Swift_SendmailTransport + * @see Swift_MailTransport + * @param array $options Options, see `$_transport`. + * @return object A (`Swift_Mailer`) mailer transport for sending (should respond to `send`). + */ + protected function transport(array $options = array()) { + $transport_type = isset($this->_config['transport']) ? $this->_config['transport'] : null; + switch ($transport_type) { + case 'mail': + $transport = Swift_MailTransport::newInstance(); + break; + case 'sendmail': + $transport = Swift_SendmailTransport::newInstance(); + break; + case 'smtp': + $transport = Swift_SmtpTransport::newInstance(); + break; + default: + throw new RuntimeException( + "Unknown transport type `{$transport_type}` for `Swift` adapter." + ); + } + $blank = array_fill_keys((array) $this->_transport[$transport_type], null); + $options = array_intersect_key($options + $this->_config, $blank); + foreach ($options as $prop => $value) { + if (!is_null($value)) { + $method = "set" . Inflector::camelize($prop); + $transport->$method($value); + } + } + return Swift_Mailer::newInstance($transport); + } + + /** + * Create a `Swift_Message` from `li3_mailer\net\mail\Message`. + * + * @see li3_mailer\net\mail\transport\adapter\Swift::$_message_properties + * @see li3_mailer\net\mail\transport\adapter\Swift::$_attachment_properties + * @see li3_mailer\net\mail\Message + * @see Swift_Message + * @param object $message The `Message` object to translate. + * @return object The translated `Swift_Message` object. + */ + protected function message($message) { + $swift_message = Swift_Message::newInstance(); + foreach ($this->_message_properties as $prop => $translated) { + if (is_int($prop)) { + $prop = $translated; + } + if (!is_null($message->$prop)) { + $value = $message->$prop; + if ($translated == 'address') { + $translated = $prop; + if (is_array($value)) { + $newvalue = array(); + foreach ($value as $name => $address) { + if (is_int($name)) { + $newvalue[] = $address; + } else { + $newvalue[$address] = $name; + } + } + $value = $newvalue; + } + } + $method = "set" . Inflector::camelize($translated); + $swift_message->$method($value); + } + } + $first = true; + foreach ($message->types() as $type => $content_type) { + if ($first) { + $first = false; + $swift_message->setBody($message->body($type), $content_type); + } else { + $swift_message->addPart($message->body($type), $content_type); + } + } + $headers = $swift_message->getHeaders(); + foreach ($message->headers as $header => $value) { + $headers->addTextHeader($header, $value); + } + foreach ($message->attachments() as $attachment) { + if (isset($attachment['path'])) { + $swift_attachment = Swift_Attachment::fromPath($attachment['path']); + } else { + $swift_attachment = Swift_Attachment::newInstance($attachment['data']); + } + foreach ($this->_attachment_properties as $prop => $translated) { + if (is_int($prop)) { + $prop = $translated; + } + if (isset($attachment[$prop])) { + $method = "set" . Inflector::camelize($translated); + $swift_attachment->$method($attachment[$prop]); + } + } + $swift_message->attach($swift_attachment); + } + return $swift_message; + } +} + +?> \ No newline at end of file diff --git a/readme.wiki b/readme.wiki new file mode 100644 index 0000000..905969b --- /dev/null +++ b/readme.wiki @@ -0,0 +1,188 @@ +Lithium mailer is a plugin for sending email messages from your li3 application. + +### Mailers + +Delivering an email message involves multiple components. To provide convenient ways to manage emails the plugin +introduces the concept of mailers, whose duty is to create and send messages. For this purpose the plugin implements +a base [ Mailer](li3_mailer/action/Mailer) class which can be subclassed to create mailers with specific options, but also can be used for delivery. + +When subclassing the `Mailer` the `$_messages` property may be used to set configuration for every message, or specific +messages like: + +{{{ +class MyMailer extends \li3_mailer\action\Mailer { + protected static $_messages = array( + array('cc' => array('Me' => 'my@address')), + 'specific' => array('cc' => array('other@address')) + ); +} + +// the message will be cc'd to my@address +MyMailer::deliver('test'); + +// the message will be cc'd both to my@address and other@address +MyMailer::deliver('specific'); + +// the base class also may be used +use li3_mailer\action\Mailer; +Mailer::deliver('message'); +}}} + +Several options may be configured for creating (like `'from'`, `'to'`, etc.), rendering (like `'data'`, `'layout'`, etc.) and +delivering (like `'delivery'`, adapter specific transport options, etc.) the message. + +### Delivery + +Sending an email message is done with the delivery service, which can be configured to handle multiple configurations. +The configuration may hold options for creating a message (like `'from'`, `'to'`, etc.): + +{{{ +use li3_mailer\action\Mailer; +use li3_mailer\net\mail\Deliver; +Delivery::config(array( + 'first' => array('adapter' => 'Simple', 'from' => 'first@address'), + 'second' => array('adapter' => 'Simple', 'from' => 'second@address') +)); + +// send from first@address +Mailer::deliver('test', array('delivery' => 'first')); + +// send from second@address +Mailer::deliver('test', array('delivery' => 'second')); +}}} + +As with other `Adaptable`-based configurations, each delivery configuration is defined by a name, +and an array of options for creating the transport adapter. The [ Delivery](li3_mailer/net/mail/Delivery) +also supports environment-based configuration like: + +{{{ +use li3_mailer\net\mail\Deliver; +Delivery::config(array('default' => array( + 'production' => array('adapter' => 'Simple'), + 'development' => array('adapter' => 'Swift') +))); +}}} + +#### Built-in transport adapters + + - [ **Simple**](li3_mailer/net/mail/transport/adapter/Simple): sends email messages with `PHP`'s built-in function `mail`. + - [ **Swift**](li3_mailer/net/mail/transport/adapter/Swift): depends on the [ SwiftMailer](http://swiftmailer.org/) library for sending email messages. + - [ **Debug**](li3_mailer/net/mail/transport/adapter/Debug): instead of sending email messages it logs them to a given file or directory. + +### Messages + +A [ message](li3_mailer/net/mail/Message) object holds all the related information that is needed for rendering and sending. The `Mailer` can be used to +construct such a message, get a suitable adapter and pass the message to the adapter for delivery. As the examples from earlier have shown there are a +couple places, where the information stored in the message can be customized, in increasing precedence: + + - in delivery config + - in mailer's `$_messages[0]` + - in mailer's `$_messages['message_name']` + - explicit options (e.g. `$options` arguments for `Mailer::deliver()`) + +A message has one special attribute, the `$base_url` property. It is easier to conceive the purpose of this value with understanding +where it is needed and how it is used: + + - When creating embedded attachments (see later) for example the message may need to generate a Content-ID. For generating this + unique id the [ RFC](http://tools.ietf.org/html/rfc2822) recommends the form `timestamp@host`. + - When generated URLs in templates (see later) should be absolute with scheme. + +Furthermore as sending emails from cron scripts is a common use-case the plugin can not depend on having a web environment, where +such a value can be determined. For this purpose the message has this property, which can be configured as seen already (e.g. in the +delivery, the mailer or explicit). To generate correct URLs the `$base_url` should have the format `scheme://host/base` (e.g. +`http://example.com/my/app`). In addition when the message is initialized it will try to autodetect this setting (so it is possible to +not specify this option when sending email messages only from web environment). + +#### Attachments + +A [ message](li3_mailer/net/mail/Message) may have attachments. A simple example with attaching 2 pdf files to the message: +{{{ + $attach = array('file1.pdf', '/path/to/file2.pdf'); + Mailer::deliver('my_message', compact('attach')); +}}} + +The attachment paths may be relative to mail asset path (`app/mails/_assets` by default, determined with `Media`, see later). Apart from +regular files, attaching remote files (if `allow_url_fopen` is enabled) and explicit content are also possible: +{{{ + // attach a remote file + $attach = array('http://example.host/file.pdf'); + Mailer::deliver('my_message', compact('attach')); + + // attach simple content with filename + $attach = array(array( + 'data' => 'this is my content', + 'filename' => 'cool.txt' + )); + Mailer::deliver('my_message', compact('attach')); + + // attach image data with content type + $img_data = create_custom_image(...); + $attach = array(array( + 'data' => $img_data, + 'filename' => 'cool.png', + 'content-type' => 'image/png' + )); + Mailer::deliver('my_message', compact('attach')); +}}} + +### Templates + +Rendering email messsages is similar to rendering responses with a few exceptions. The most important is that instead of having a request +which can be negotiated to infer the most suitable (single) type for response the email message may have multiple types (and does not have a +'corresponding' request). To support this the plugin implements a mail [ Media](li3_mailer/net/mail/Media) class simlar to http's [ Media](li3_mailer/net/http/Media), +which can be used to register new types or configure the built-ins. + +The default base path for email templates is `/app/mails` (instead of `/app/views`) to better separate from view templates, however this can be +configured with the [ Media](li3_mailer/net/mail/Media) class. The exact template will be determined by the message's name and the mailer by default, +so a command like `Mailer::deliver('test')` would use `/app/mails/test.html.php` for rendering html content, while `FooMailer::deliver('test')` would +first look for `/app/mails/foo/test.html.php` and fallback to the former only if the latter can not be found. Similarly the former command +would use the `/app/mails/test.text.php` template for rendering text (plain) content. The default base paths for layouts and elements are +`/app/mails/layouts' and `/app/mails/elements` respectively. + +As with rendering from a controller these defaults may be overriden when sending mail messages: +{{{ + // renders /app/mails/test.{:type}.php with default layout (/app/mails/layouts/default.{:type}.php) + Mailer::deliver('test'); + + // renders /app/mails/other.{:type}.php with default layout + Mailer::deliver('test', array('template' => 'other')); + + // renders /app/mails/test.text.php without layout, only text (plain) type + Mailer::deliver('test', array('types' => 'text', 'layout' => false)); +}}} + +#### Helpers and handlers + +The plugin provides similar helpers for mail templates like `lithium` provides for views: the +[ Html](li3_mailer\template\helper\mail\Html) and [ Form](li3_mailer\template\helper\mail\Form) helpers. +They provide the same functionality as the view helpers. + +The `Html` helper's [ image](li3_mailer\template\helper\mail\Html::image()) method may be used to embed images: +{{{ +

My image: image('my/image.png'); ?>

+}}} +This will place an element into the message with its source attribute set to the `Content-ID` that belongs to the attachment. + +Creating or replacing a mail template helper is done exactly like regular view helpers with the exception that +mail helpers are namespaced as `helper\mail\Name` (e.g. should be placed in `app/extensions/helper/mail/Name.php`). + +Handlers are very similar to helpers and there is two commonly used handler that behaves a bit different +in mail templates compared to view templates: + + - The `url` handler will generate absolute URLs by default. + - The `path` handler will generate absolute URLs by default and `'cid:ID'` style values for embeds. + +#### Referencing the message + +In the template context `$this` refers to the `Renderer` object, which exposes the message, so it is +possible to add or change the subject, add header(s) or attach files: +{{{ + message()->subject('My subject); ?> + message()->header('Custom', 'header'); ?> + message()->attach('file.pdf'); ?> +}}} + +Please note that when the message has multiple types, these methods should be placed in only one template, +as calling them multiple times will lead to unexpected behavior: for example while the last value will override +the formers when calling `subject` (rendering order is determined by the message's type order), attaching the +same file from multiple templates will result in multiple attachments of the same file. \ No newline at end of file diff --git a/template/Mail.php b/template/Mail.php new file mode 100644 index 0000000..83cff89 --- /dev/null +++ b/template/Mail.php @@ -0,0 +1,63 @@ + 'merge', 'steps' => 'merge' + ); + + /** + * Perform initialization. + * + * @return void + */ + protected function _init() { + Object::_init(); + + $encoding = 'UTF-8'; + + if ($this->_message) { + $encoding =& $this->_message->charset; + } + $h = function($data) use (&$encoding) { + return htmlspecialchars((string) $data, ENT_QUOTES, $encoding); + }; + $this->outputFilters += compact('h') + $this->_config['outputFilters']; + + foreach (array('loader', 'renderer') as $key) { + if (is_object($this->_config[$key])) { + $this->{'_' . $key} = $this->_config[$key]; + continue; + } + $class = $this->_config[$key]; + $config = array('view' => $this) + $this->_config; + $this->{'_' . $key} = Libraries::instance('adapter.template.mail', $class, $config); + } + } +} + +?> \ No newline at end of file diff --git a/template/helper/mail/Form.php b/template/helper/mail/Form.php new file mode 100644 index 0000000..2c60b32 --- /dev/null +++ b/template/helper/mail/Form.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/template/helper/mail/Html.php b/template/helper/mail/Html.php new file mode 100644 index 0000000..c948935 --- /dev/null +++ b/template/helper/mail/Html.php @@ -0,0 +1,98 @@ +_context->message()->charset; + return parent::charset($encoding); + } + + /** + * Creates a formatted element. Unless `$path` is an URL + * it embeds the image into the message and the `src` attribute of the + * element will get set to the Content-ID of the attachment. + * This behaviour can be overriden with setting the special option + * `'embed'` to `false` (which defaults to `true`). If `'embed'` + * is `false` the URL is calculated with `http\Media`, so it will + * return the web accessible path (see the context's `'path'` handler). + * Optionally `$path` may be an array and then it is resolved to an + * URL with context's `'url'` handler. + * + * Examples: + * {{{ + * // will embed an image + * $this->image('/path/to/my/image.png'); + * + * // will embed an image relative to mail asset path + * $this->image('my/image.png'); + * + * // will use an URL + * $this->image('http://my.url/image.png'); + * + * // will use an URL computed with http\Media (path is relative to `app/webroot/img`) + * $this->image('my/image.png', array('embed' => false)); + * + * // will use an URL computed with http\Media + * $this->image('/path/to/my/image.png', array('embed' => false)); + * }}} + * + * It is possible to pass extra options for path resolving and attaching, like: + * {{{ + * // embed the image from a particular library's mail asset path + * $this->image('my/image.png', array('library' => 'foo')); + * + * // embed the image with a different filename + * $this->image('my/image.png', array('filename' => 'cool.png')); + * + * // embed data + * $this->image(null, array( + * 'data' => $img_data', + * 'content-type' => 'image/png', + * 'filename' => 'cool.png' + * )); + * }}} + * + * @see li3_mailer\template\mail\adapter\File::_init() + * @param string $path Path to the image file. + * @param array $options Array of HTML attributes and other options. + * @return string Formatted element. + * @filter This method can be filtered. + */ + public function image($path, array $options = array()) { + $embed = !(is_string($path) && preg_match('/^[a-z0-9-]+:\/\//i', $path)); + $defaults = compact('embed'); + $options += array('alt' => ''); + list($scope, $options) = $this->_options($defaults, $options); + $clear = array('library', 'check', 'data', 'content-type', 'filename'); + $options = array_diff_key($options, array_fill_keys($clear, null)); + $path = is_array($path) ? $this->_context->url($path) : $path; + $params = compact('path', 'options', 'scope'); + $method = __METHOD__; + + return $this->_filter($method, $params, function($self, $params, $chain) use ($method) { + extract($params); + $args = array($method, 'image', compact('path', 'options'), $scope); + return $self->invokeMethod('_render', $args); + }); + } +} + +?> \ No newline at end of file diff --git a/template/mail/Compiler.php b/template/mail/Compiler.php new file mode 100644 index 0000000..2fba4e0 --- /dev/null +++ b/template/mail/Compiler.php @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/template/mail/adapter/File.php b/template/mail/adapter/File.php new file mode 100644 index 0000000..5ecd905 --- /dev/null +++ b/template/mail/adapter/File.php @@ -0,0 +1,200 @@ + 'merge', 'message', 'context', + 'strings', 'handlers', 'view', 'compile', 'paths' + ); + + /** + * Context values that exist across all templates rendered in this context. These values + * are usually rendered in the layout template after all other values have rendered. + * + * @var array + */ + protected $_context = array( + 'content' => '', 'scripts' => array(), 'styles' => array(), 'head' => array() + ); + + /** + * `File`'s dependencies. These classes are used by the output handlers to generate URLs + * for dynamic resources and static assets, as well as compiling the templates. + * + * @see Renderer::$_handlers + * @var array + */ + protected $_classes = array( + 'compiler' => 'li3_mailer\template\mail\Compiler', + 'router' => 'lithium\net\http\Router', + 'media' => 'li3_mailer\net\mail\Media', + 'web_media' => 'lithium\net\http\Media' + ); + + /** + * The `Message` object instance, if applicable. + * + * @var object The message object. + */ + protected $_message = null; + + /** + * Renderer constructor. + * + * Accepts these following configuration parameters: + * - `view`: The `View` object associated with this renderer. + * - `strings`: String templates used by helpers. + * - `handlers`: An array of output handlers for string template inputs. + * - `message`: The `Message` object associated with this renderer. + * - `context`: An array of the current rendering context data, like `content`. + * + * @param array $config + */ + public function __construct(array $config = array()) { + $defaults = array( + 'message' => null, + 'context' => array( + 'content' => '', 'scripts' => array(), + 'styles' => array(), 'head' => array() + ) + ); + parent::__construct((array) $config + $defaults); + } + + /** + * Sets the default output handlers for string template inputs. + * + * @return void + */ + protected function _init() { + Object::_init(); //do not set handlers from View's Renderer + + $classes =& $this->_classes; + $message =& $this->_message; + if (!$this->_request && $message) { + $this->_request = $classes['media']::invokeMethod('_request', array($message)); + } + $request =& $this->_request; + $context =& $this->_context; + $h = $this->_view ? $this->_view->outputFilters['h'] : null; + + $this->_handlers += array( + 'url' => function($url, $ref, array $options = array()) use (&$classes, &$request, $h) { + $defaults = array('absolute' => true); + $url = $classes['router']::match($url ?: '', $request, $options + $defaults); + return $h ? str_replace('&', '&', $h($url)) : $url; + }, + 'path' => function + ($path, $ref, array $options = array()) use (&$classes, &$request, &$message) { + $embed = isset($options['embed']) && $options['embed']; + unset($options['embed']); + if ($embed) { + return 'cid:' . $message->embed($path, $options); + } else { + $defaults = array('base' => $request ? $request->env('base') : ''); + $type = 'generic'; + + if (is_array($ref) && $ref[0] && $ref[1]) { + list($helper, $methodRef) = $ref; + list($class, $method) = explode('::', $methodRef); + $type = $helper->contentMap[$method]; + } + $path = $classes['web_media']::asset($path, $type, $options + $defaults); + if ($path[0] == '/') { + $host = ''; + if ($request) { + $host .= $request->env('HTTPS') ? 'https://' : 'http://'; + $host .= $request->env('HTTP_HOST'); + } + $path = $host . $path; + } + return $path; + } + }, + 'options' => '_attributes', + 'title' => 'escape', + 'scripts' => function($scripts) use (&$context) { + return "\n\t" . join("\n\t", $context['scripts']) . "\n"; + }, + 'styles' => function($styles) use (&$context) { + return "\n\t" . join("\n\t", $context['styles']) . "\n"; + }, + 'head' => function($head) use (&$context) { + return "\n\t" . join("\n\t", $context['head']) . "\n"; + } + ); + + unset($this->_config['view']); + } + + /** + * Returns the `Message` object associated with this rendering context. + * + * @return object Returns an instance of `li3_mailer\net\mail\Message`, which provides the i.e. + * the encoding for the document being the result of templates rendered by this context. + */ + public function message() { + return $this->_message; + } + + /** + * Brokers access to helpers attached to this rendering context, and loads helpers on-demand if + * they are not available. + * + * @param string $name Helper name + * @param array $config + * @return object + */ + public function helper($name, array $config = array()) { + if (isset($this->_helpers[$name])) { + return $this->_helpers[$name]; + } + try { + $config += array('context' => $this); + $helper = Libraries::instance('helper.mail', ucfirst($name), $config); + return $this->_helpers[$name] = $helper; + } catch (ClassNotFoundException $e) { + throw new RuntimeException("Mail helper `{$name}` not found."); + } + } + + /** + * Shortcut method used to render elements and other nested templates from inside the templating + * layer. + * + * @param string $type The type of template to render, usually either `'element'` or + * `'template'`. Indicates the process used to render the content. See + * `lithium\template\View::$_processes` for more info. + * @param string $template The template file name. For example, if `'header'` is passed, and + * `$type` is set to `'element'`, then the template rendered will be + * `views/elements/header.html.php` (assuming the default configuration). + * @param array $data An array of any other local variables that should be injected into the + * template. By default, only the values used to render the current template will + * be sent. If `$data` is non-empty, both sets of variables will be merged. + * @param array $options Any options accepted by `template\View::render()`. + * @return string Returns a the rendered template content as a string. + */ + protected function _render($type, $template, array $data = array(), array $options = array()) { + return $this->_view->render($type, $data + $this->_data, compact('template') + $options); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/action/MailerTest.php b/tests/cases/action/MailerTest.php new file mode 100644 index 0000000..1787e0d --- /dev/null +++ b/tests/cases/action/MailerTest.php @@ -0,0 +1,107 @@ +next($self, $params, $chain); + return $params + compact('result'); +}; +Mailer::applyFilter('deliver', $filter); +TestMailer::applyFilter('deliver', $filter); + +class MailerTest extends \lithium\test\Unit { + public function testCreateMessage() { + $to = 'foo@bar'; + $message = Mailer::message(compact('to')); + $this->assertTrue($message instanceof Message); + $this->assertEqual($to, $message->to); + } + + public function testDeliver() { + $params = Mailer::deliver('message_name', array( + 'to' => 'foo@bar', + 'data' => array('my' => 'data'), + 'transport' => array('extra' => 'data'), + 'view' => 'li3_mailer\tests\mocks\template\Mail' + )); + extract($params); + $this->assertTrue(array_key_exists('mailer', $options)); + $this->assertNull($options['mailer']); + $this->assertTrue(isset($options['template'])); + $this->assertEqual('message_name', $options['template']); + $this->assertEqual(array('my' => 'data'), $data); + $this->assertEqual('foo@bar', $message->to); + $this->assertEqual('adapter@config', $message->from); + $this->assertEqual('fake rendered message', $message->body('html')); + $this->assertEqual('fake rendered message', $message->body('text')); + $this->assertFalse(is_null($transport)); + $this->assertEqual(array('extra' => 'data'), $transport_options); + } + + public function testSetsMailer() { + $params = Mailer::deliver('message_name', array('template' => false, 'data' => 'string')); + extract($params); + $this->assertEqual(null, $options['mailer']); + $params = TestMailer::deliver( + 'message_name', + array('template' => false, 'data' => 'string') + ); + extract($params); + $this->assertEqual('test', $options['mailer']); + } + + public function testOverloading() { + list($message, $options) = MailerOverload::deliverTest(); + $this->assertEqual('test', $message); + $this->assertEqual(array(), $options); + list($message, $options) = MailerOverload::deliverTestWithLocal(); + $this->assertEqual('test', $message); + $this->assertEqual(array('delivery' => 'local'), $options); + list($message, $options) = MailerOverload::deliverTestWithLocal(array('foo' => 'bar')); + $this->assertEqual(array('foo' => 'bar', 'delivery' => 'local'), $options); + list($message, $options) = MailerOverload::deliverTestWithLocal('foo@bar'); + $this->assertEqual(array('foo@bar', 'delivery' => 'local'), $options); + list($message, $options) = MailerOverload::deliverTestWithLocal(array( + 'foo@bar', 'foo' => 'bar' + )); + $this->assertEqual(array('foo@bar', 'foo' => 'bar', 'delivery' => 'local'), $options); + } + + public function testOverloadException() { + $this->expectException('Method `foobar` not defined or handled in class `Mailer`.'); + Mailer::foobar(); + } + + public function testOptions() { + $options = array('foo@bar', 'subject' => 'my subject', 'my' => 'data'); + $expected = array( + 'to' => 'foo@bar', + 'subject' => 'my subject', + 'data' => array('my' => 'data', 'additional' => 'data') + ); + $result = MailerWithOptions::options('without_extra_options', $options); + $this->assertEqual($expected, $result); + $expected = array( + 'to' => 'foo@bar', + 'subject' => 'my subject', + 'data' => array('my' => 'data', 'additional' => 'data', 'extra' => 'data') + ); + $result = MailerWithOptions::options('with_extra_options', $options); + $this->assertEqual($expected, $result); + $options = array('foo@bar', 'data' => array('my' => 'data')); + $expected = array( + 'to' => 'foo@bar', + 'data' => array('my' => 'data', 'additional' => 'data') + ); + $result = MailerWithOptions::options('without_extra_options', $options); + $this->assertEqual($expected, $result); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/DeliveryTest.php b/tests/cases/net/mail/DeliveryTest.php new file mode 100644 index 0000000..1d70b11 --- /dev/null +++ b/tests/cases/net/mail/DeliveryTest.php @@ -0,0 +1,17 @@ + 'Simple'), DeliveryWithPath::_adaptersPath()); + $class = DeliveryWithPath::invokeMethod('_class', $params); + $adapter = new $class(); + $this->assertTrue($adapter instanceof Transport); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/GrammarTest.php b/tests/cases/net/mail/GrammarTest.php new file mode 100644 index 0000000..997e400 --- /dev/null +++ b/tests/cases/net/mail/GrammarTest.php @@ -0,0 +1,44 @@ + 'bar', 'baz' => 'qux'); + $g = new Grammar(compact('grammar')); + $this->assertFalse(is_null($g->token('NO-WS-CTL'))); + $this->assertNull($g->token('nonexistent')); + foreach ($grammar as $key => $expected) { + $this->assertEqual($expected, $g->token($key)); + } + $tokens = $g->token(); + foreach ($grammar as $key => $expected) { + $this->assertTrue(array_key_exists($key, $tokens)); + $this->assertEqual($expected, $tokens[$key]); + } + $g->token('extra', 'value'); + $this->assertEqual('value', $g->token('extra')); + $tokens = $g->token(); + $this->assertTrue(array_key_exists('extra', $tokens)); + $this->assertEqual('value', $tokens['extra']); + } + + public function testIsValidId() { + $valid = array( + 'a@b', 'example@host', '1234567890@li3_mailer.generated', + '123{}$#!+|^%@li3_mailer.generated' + ); + $invalid = array('a@b@c', 'a@@b', 'a<@b', 'a>@b', 'a"@b'); + $grammar = new Grammar(); + foreach ($valid as $id) { + $this->assertTrue($grammar->isValidId($id)); + } + foreach ($invalid as $id) { + $this->assertFalse($grammar->isValidId($id)); + } + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/MediaTest.php b/tests/cases/net/mail/MediaTest.php new file mode 100644 index 0000000..280f0ca --- /dev/null +++ b/tests/cases/net/mail/MediaTest.php @@ -0,0 +1,148 @@ +assertEqual('text/plain', Media::type('text')); + $this->assertEqual('text/html', Media::type('html')); + $this->assertEqual(null, Media::type('foo')); + Media::type('foo', 'bar'); + $this->assertEqual('bar', Media::type('foo')); + $this->assertEqual(array( + 'text' => 'text/plain', 'html' => 'text/html', 'foo' => 'bar' + ), Media::invokeMethod('_types')); + Media::type('foo', false); + $this->assertEqual(null, Media::type('foo')); + } + + public function testRender() { + $message = new Message(array('from' => 'valid@address')); + Media::render($message, 'body', array('template' => false)); + $this->assertEqual('body', $message->body('text')); + $this->assertEqual('body', $message->body('html')); + } + + public function testRenderWithView() { + Media::type('foo', 'bar', array('view' => 'li3_mailer\tests\mocks\template\Mail')); + $message = new Message(array('from' => 'valid@address', 'types' => 'foo')); + Media::render($message); + $this->assertEqual('fake rendered message', $message->body('foo')); + Media::type('foo', false); + } + + public function testBadHandler() { + Media::type('foo', 'bar', array('view' => false, 'template' => false)); + $message = new Message(array('from' => 'valid@address', 'types' => 'foo')); + $this->expectException('Could not interpret type settings for handler.'); + Media::render($message); + Media::type('foo', false); + } + + public function testView() { + $message = new Message(); + Media::type('foo', 'bar', array('view' => 'li3_mailer\tests\mocks\template\Mail')); + $view = Media::view('foo', array(), $message); + $this->assertTrue($view instanceof Mail); + $this->assertEqual($message, $view->message()); + Media::type('foo', false); + } + + public function testTemplatePaths() { + $message = new Message(); + Media::type('foo', 'bar', array( + 'view' => 'li3_mailer\tests\mocks\template\Mail', + 'template' => null + )); + $base_handler = Media::invokeMethod('_handlers', array('foo')); + $base_handler += Media::invokeMethod('_handlers', array('default')); + $base_handler += array('compile' => false); + + $handler = $base_handler; + $options = array( + 'library' => true, 'mailer' => null, 'template' => 'foo', 'type' => 'text' + ); + $handler['paths'] = Media::invokeMethod( + '_finalize_paths', + array($handler['paths'], $options) + ); + $view = Media::view($handler, array(), $message); + $this->assertTrue($view instanceof Mail); + $loader = $view->loader(); + $template = $loader->template('template', $options); + $this->assertEqual(array(LITHIUM_APP_PATH . '/mails/foo.text.php'), $template); + + $handler = $base_handler; + $options = array( + 'library' => true, 'mailer' => 'bar', 'template' => 'foo', 'type' => 'text' + ); + $handler['paths'] = Media::invokeMethod( + '_finalize_paths', + array($handler['paths'], $options) + ); + $view = Media::view($handler, array(), $message); + $this->assertTrue($view instanceof Mail); + $loader = $view->loader(); + $template = $loader->template('template', $options); + $this->assertEqual(array( + LITHIUM_APP_PATH . '/mails/bar/foo.text.php', + LITHIUM_APP_PATH . '/mails/foo.text.php' + ), $template); + + Media::type('foo', false); + } + + public function testBadType() { + $message = new Message(array('types' => 'foobar')); + $this->expectException('Unhandled media type `foobar`.'); + Media::render($message); + } + + public function testAsset() { + $result = Media::asset('foo/bar'); + $this->assertEqual(LITHIUM_APP_PATH . '/mails/_assets/foo/bar', $result); + Libraries::add('foo', array('path' => '/a/path')); + $result = Media::asset('foo/bar', array('library' => 'foo')); + $this->assertEqual('/a/path/mails/_assets/foo/bar', $result); + Libraries::remove('foo'); + $result = Media::asset('/foo/bar'); + $this->assertEqual('/foo/bar', $result); + $result = Media::asset('http://example.com/foo/bar'); + $this->assertEqual('http://example.com/foo/bar', $result); + $result = Media::asset('foo/bar', array('check' => true)); + $this->assertFalse($result); + } + + public function testRequest() { + $request = Media::invokeMethod('_request', array(null)); + $this->assertEqual(null, $request->env('HTTP_HOST')); + + $tests = array( + 'foo.local' => array('HTTP_HOST' => 'foo.local'), + 'http://foo.local' => array('HTTP_HOST' => 'foo.local'), + 'https://foo.local' => array('HTTP_HOST' => 'foo.local', 'HTTPS' => true), + 'http://foo.local/base' => array('HTTP_HOST' => 'foo.local', 'base' => '/base') + ); + foreach ($tests as $base_url => $expect) { + $message = new Message(compact('base_url')); + $request = Media::invokeMethod('_request', array($message)); + $expect += array('HTTPS' => false, 'base' => ''); + foreach ($expect as $key => $expected) { + $result = $request->env($key); + $this->assertEqual( + $expected, + $result, + "`{$key}` failed for {$base_url} ({$message->base_url})," + . " expected: `{$expected}`, result: `{$result}`" + ); + } + } + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/MessageTest.php b/tests/cases/net/mail/MessageTest.php new file mode 100644 index 0000000..f2b2e06 --- /dev/null +++ b/tests/cases/net/mail/MessageTest.php @@ -0,0 +1,233 @@ + $expected) { + $this->assertEqual($expected, $message->$prop); + } + } + + public function testHeaders() { + $message = new Message(); + $this->assertEqual(array(), $message->headers); + $message->header('Foo', 'bar'); + $this->assertEqual(array('Foo' => 'bar'), $message->headers); + $message->header('Foo', false); + $this->assertEqual(array(), $message->headers); + } + + public function testBody() { + $message = new Message(); + $this->assertEqual(array(), $message->body); + $message->body('foo', 'bar'); + $this->assertEqual(array('foo' => array('bar')), $message->body); + $this->assertEqual('bar', $message->body('foo')); + $this->assertEqual('', $message->body('bar')); + } + + public function testTypes() { + $message = new Message(); + $this->assertEqual(array('html', 'text'), $message->types); + $this->assertEqual(array('html' => 'text/html', 'text' => 'text/plain'), $message->types()); + } + + public function testEnsureValidDate() { + $message = new Message(); + $this->assertEqual(null, $message->date); + $message->invokeMethod('ensureValidDate'); + $this->assertTrue(is_int($message->date)); + $message = new Message(array('date' => 'invalid')); + $this->assertEqual('invalid', $message->date); + $this->expectException('/Invalid date timestamp `invalid`/'); + $message->invokeMethod('ensureValidDate'); + } + + public function testEnsureValidFromWithInteger() { + $message = new Message(array('from' => 42)); + $this->expectException('/`\$from` field should be a string or an array/'); + $message->invokeMethod('ensureValidFrom'); + } + + public function testEnsureValidFromWithEmpty() { + foreach (array(null, false, 0) as $from) { + $message = new Message(compact('from')); + $this->expectException('`Message` should have at least one `$from` address.'); + $message->invokeMethod('ensureValidFrom'); + } + } + + public function testEnsureValidFromWithValid() { + $message = new Message(array('from' => 'valid@address')); + $message->invokeMethod('ensureValidFrom'); + } + + public function testEnsureValidSender() { + $message = new Message(array('from' => 'foo@bar')); + $message->invokeMethod('ensureValidSender'); + $this->assertEqual(null, $message->sender); + + $message = new Message(array('from' => array('foo@bar', 'bar@foo'))); + $message->invokeMethod('ensureValidSender'); + $this->assertEqual(array('foo@bar'), $message->sender); + + $message = new Message(array('from' => array('foo' => 'foo@bar', 'bar' => 'bar@foo'))); + $message->invokeMethod('ensureValidSender'); + $this->assertEqual(array('foo' => 'foo@bar'), $message->sender); + + $message = new Message(array('from' => 'foo@bar', 'sender' => 'foo@bar')); + $message->invokeMethod('ensureValidSender'); + $this->assertEqual(null, $message->sender); + + $message = new Message(array('from' => 'foo@bar', 'sender' => 'bar@foo')); + $message->invokeMethod('ensureValidSender'); + $this->assertEqual('bar@foo', $message->sender); + + $message = new Message(array('sender' => array('foo@bar', 'bar@foo'))); + $this->expectException('`Message` should only have a single `$sender` address.'); + $message->invokeMethod('ensureValidSender'); + } + + public function testEnsureStandardCompliance() { + $from = array('foo@bar', 'bar@foo'); + $message = new Message(compact('from')); + $message->ensureStandardCompliance(); + $this->assertTrue(is_int($message->date)); + $this->assertEqual($from, $message->from); + $this->assertEqual(array('foo@bar'), $message->sender); + } + + public function testBaseUrl() { + $message = new Message(array('base_url' => 'foo.local')); + $this->assertEqual('http://foo.local', $message->base_url); + $message = new Message(array('base_url' => 'http://foo.bar')); + $this->assertEqual('http://foo.bar', $message->base_url); + $message = new Message(array('base_url' => 'http://foo.bar/')); + $this->assertEqual('http://foo.bar', $message->base_url); + $oldserver = $_SERVER; + $_SERVER = array( + 'HTTP_HOST' => 'foo.bar', 'HTTPS' => true, 'PHP_SELF' => '/foo/bar/index.php' + ) + $_SERVER; + $message = new Message(); + $this->assertEqual('https://foo.bar/foo/bar', $message->base_url); + $_SERVER = $oldserver; + } + + public function testRandomId() { + $message = new Message(array('base_url' => 'foo.local')); + $this->assertPattern('/^[^@]+@foo.local$/', $message->invokeMethod('randomId')); + $message = new Message(); + $this->assertPattern('/^[^@]+@li3_mailer.generated$/', $message->invokeMethod('randomId')); + $message = new Message(array('base_url' => 'foo@local')); + $this->assertPattern('/^[^@]+@li3_mailer.generated$/', $message->invokeMethod('randomId')); + } + + public function testAttacAndDetach() { + $message = new Message(); + $this->assertEqual(array(), $message->attachments()); + $message->attach(null, array('data' => 'my data')); + $attachments = $message->attachments(); + $this->assertEqual(1, count($attachments)); + $this->assertEqual('my data', $attachments[0]['data']); + $message->detach('my data'); + $this->assertEqual(array(), $message->attachments()); + } + + public function testAttachErrorNothingToAttach() { + $message = new Message(); + $this->expectException('/^Neither path nor data provided, cannot attach\.$/'); + $message->attach(null); + } + + public function testAttachErrorFileDoesNotExist() { + $message = new Message(); + $this->expectException('/^File at `foo\/bar` is not a valid asset, cannot attach\.$/'); + $message->attach('foo/bar'); + } + + public function testAttachErrorDataIsInvalid() { + $message = new Message(); + $this->expectException('/^Data should be a string, `integer` given, cannot attach\.$/'); + $message->attach(null, array('data' => 42)); + } + + public function testAttachRelativePath() { + $message = new Message(); + $message->attach('foo/bar.png', array('check' => false)); + $attachments = $message->attachments(); + $this->assertEqual(1, count($attachments)); + $this->assertEqual( + LITHIUM_APP_PATH . '/mails/_assets/foo/bar.png', + $attachments[0]['path'] + ); + $this->assertEqual('image/png', $attachments[0]['content-type']); + $this->assertEqual('bar.png', $attachments[0]['filename']); + $message->detach('foo/bar.png'); + $this->assertEqual(array(), $message->attachments()); + } + + public function testAttachAbsolutePath() { + $message = new Message(); + $message->attach('/foo/bar.png', array('check' => false)); + $attachments = $message->attachments(); + $this->assertEqual(1, count($attachments)); + $this->assertEqual('/foo/bar.png', $attachments[0]['path']); + $this->assertEqual('image/png', $attachments[0]['content-type']); + $this->assertEqual('bar.png', $attachments[0]['filename']); + $message->detach('/foo/bar.png'); + $this->assertEqual(array(), $message->attachments()); + } + + public function testAttachData() { + $message = new Message(); + $message->attach(null, array('data' => 'test content', 'filename' => 'test.txt')); + $attachments = $message->attachments(); + $this->assertEqual(1, count($attachments)); + $this->assertEqual('test content', $attachments[0]['data']); + $this->assertEqual('text/plain', $attachments[0]['content-type']); + $message->detach('test content'); + $this->assertEqual(array(), $message->attachments()); + } + + public function testAttachFromConstructor() { + $message = new Message(array('attach' => array( + array('data' => 'test content'), + 'foo/bar' => array('check' => false), + __FILE__ + ))); + $attachments = $message->attachments(); + $this->assertEqual(3, count($attachments)); + $this->assertEqual('test content', $attachments[0]['data']); + $this->assertEqual('foo/bar', $attachments[1]['attach_path']); + $this->assertEqual(__FILE__, $attachments[2]['attach_path']); + $message->detach(__FILE__); + $this->assertEqual(2, count($message->attachments())); + $message->detach(__FILE__); + $this->assertEqual(2, count($message->attachments())); + $message->detach('test content'); + $this->assertEqual(1, count($message->attachments())); + $message->detach('foo/bar'); + $this->assertEqual(0, count($message->attachments())); + } + + public function testEmbed() { + $message = new Message(); + $result = $message->embed('foo/bar', array('check' => false)); + $this->assertPattern('/^[^@]+@li3_mailer.generated$/', $result); + $attachments = $message->attachments(); + $this->assertEqual(1, count($attachments)); + $this->assertEqual('foo/bar', $attachments[0]['attach_path']); + $this->assertEqual('inline', $attachments[0]['disposition']); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/TransportTest.php b/tests/cases/net/mail/TransportTest.php new file mode 100644 index 0000000..6ba6eac --- /dev/null +++ b/tests/cases/net/mail/TransportTest.php @@ -0,0 +1,21 @@ +assertEqual('foo@bar', $transport->invokeMethod('address', array('foo@bar'))); + $this->assertEqual('foo@bar', $transport->invokeMethod('address', array(array('foo@bar')))); + $result = $transport->invokeMethod('address', array(array('Foo' => 'foo@bar'))); + $this->assertEqual('Foo ', $result); + $result = $transport->invokeMethod('address', array(array( + 'Foo' => 'foo@bar', 'Bar' => 'bar@foo' + ))); + $this->assertEqual('Foo , Bar ', $result); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/transport/adapter/DebugTest.php b/tests/cases/net/mail/transport/adapter/DebugTest.php new file mode 100644 index 0000000..466d8c2 --- /dev/null +++ b/tests/cases/net/mail/transport/adapter/DebugTest.php @@ -0,0 +1,144 @@ + 'foo@bar', 'subject' => 'test subject')); + $debug = new Debug(); + $log = fopen('php://memory', 'r+'); + $format = 'short'; + $delivered = $debug->deliver($message, compact('log', 'format')); + $this->assertTrue($delivered); + rewind($log); + $result = stream_get_contents($log); + fclose($log); + $this->assertPattern( + '/^\[[\d:\+\-T]+\] Sent to foo@bar with subject `test subject`.\n$/', + $result + ); + } + + public function testDeliverLogToFile() { + $path = realpath(Libraries::get(true, 'resources') . '/tmp/tests'); + $this->skipIf(!is_writable($path), "Path `{$path}` is not writable."); + $log = $path . DIRECTORY_SEPARATOR . 'mail.log'; + file_put_contents($log, "initial content\n"); + $message = new Message(array('to' => 'foo@bar', 'subject' => 'test subject')); + $debug = new Debug(); + $format = 'short'; + $delivered = $debug->deliver($message, compact('log', 'format')); + $this->assertTrue($delivered); + $result = file_get_contents($log); + $this->assertPattern( + '/^initial content\n\[[\d:\+\-T]+\] Sent to foo@bar with subject `test subject`.\n$/', + $result + ); + unlink($log); + } + + public function testDeliverLogToDir() { + $path = realpath(Libraries::get(true, 'resources') . '/tmp/tests'); + $this->skipIf(!is_writable($path), "Path `{$path}` is not writable."); + $log = $path . DIRECTORY_SEPARATOR . 'mails'; + if (!is_dir($log)) { + mkdir($log); + } + $glob = $log . DIRECTORY_SEPARATOR . '*.mail'; + $oldresults = glob($glob); + $message = new Message(array('to' => 'foo@bar', 'subject' => 'test subject')); + $debug = new Debug(); + $format = 'short'; + $delivered = $debug->deliver($message, compact('log', 'format')); + $this->assertTrue($delivered); + $results = array_diff(glob($glob), $oldresults); + $this->assertEqual(1, count($results)); + $result_file = current($results); + $result = file_get_contents($result_file); + $this->assertPattern( + '/^\[[\d:\+\-T]+\] Sent to foo@bar with subject `test subject`.\n$/', + $result + ); + unlink($result_file); + } + + public function testFormatShort() { + $message = new Message(array('to' => 'foo@bar', 'subject' => 'test subject')); + $debug = new Debug(); + $result = $debug->invokeMethod('format', array($message, 'short')); + $this->assertEqual('Sent to foo@bar with subject `test subject`.', $result); + } + + public function testFormatNormal() { + $time = time(); + $date = date('Y-m-d H:i:s'); + $message = new Message(array( + 'to' => 'to', 'from' => 'from', 'sender' => 'sender', 'cc' => 'cc', + 'bcc' => 'bcc', 'date' => $time, 'subject' => 'subject' + )); + $message->body('text', 'text body'); + $debug = new Debug(); + $result = $debug->invokeMethod('format', array($message, 'normal')); + $expected = "Mail sent to to from from (sender: sender, cc: cc, bcc: bcc)\n" . + "with date {$date} and subject `subject` in formats html, text, text message body:\n" . + "text body\n"; + $this->assertEqual($expected, $result); + } + + public function testFormatFull() { + $time = time(); + $date = date('Y-m-d H:i:s'); + $message = new Message(array( + 'to' => 'to', 'from' => 'from', 'sender' => 'sender', 'cc' => 'cc', + 'bcc' => 'bcc', 'date' => $time, 'subject' => 'subject' + )); + $message->body('text', 'text body'); + $message->body('html', 'html body'); + $debug = new Debug(); + $result = $debug->invokeMethod('format', array($message, 'full')); + $expected = "Mail sent to to from from (sender: sender, cc: cc, bcc: bcc)\n" . + "with date {$date} and subject `subject` in formats html, text, text message body:\n" . + "text body\nhtml message body:\nhtml body\n"; + $this->assertEqual($expected, $result); + } + + public function testFormatVerbose() { + $message = new Message(); + $debug = new Debug(); + $result = $debug->invokeMethod('format', array($message, 'verbose')); + $formatter = function ($message) { //need to scope $message to hide protected vars + return "Mail sent with properties:\n" . var_export(get_object_vars($message), true); + }; + $expected = $formatter($message); + $this->assertEqual($expected, $result); + } + + public function testFormatNoData() { + $message = new Message(); + $debug = new Debug(); + $result = $debug->invokeMethod('format', array($message, 'short')); + $this->assertEqual('Sent to with subject ``.', $result); + } + + public function testFormatExtraFormatter() { + $message = new Message(); + $debug = new Debug(array('formats' => array('foo' => function($message) { + return 'foo'; + }))); + $result = $debug->invokeMethod('format', array($message, 'foo')); + $this->assertEqual('foo', $result); + } + + public function testFormatBadFormatter() { + $message = new Message(); + $debug = new Debug(); + $this->expectException('Formatter for format `foo` is neither string nor closure.'); + $debug->invokeMethod('format', array($message, 'foo')); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/transport/adapter/SimpleTest.php b/tests/cases/net/mail/transport/adapter/SimpleTest.php new file mode 100644 index 0000000..42baf73 --- /dev/null +++ b/tests/cases/net/mail/transport/adapter/SimpleTest.php @@ -0,0 +1,116 @@ + 'foo@bar', 'from' => 'valid@address', + 'subject' => 'test subject', 'types' => 'text', + 'headers' => array('Custom' => 'foo') + )); + $message->body('text', 'test body'); + $params = $simple->deliver($message); + extract($params); + $this->assertEqual('foo@bar', $to); + $this->assertEqual('test subject', $subject); + $this->assertEqual('test body', $body); + $this->assertPattern('/(^|\r\n)From: valid@address(\r\n|$)/', $headers); + $this->assertPattern('/(^|\r\n)MIME-Version: 1.0(\r\n|$)/', $headers); + $this->assertPattern( + '/(^|\r\n)Content-Type: text\/plain;charset="' . $message->charset . '"(\r\n|$)/', + $headers + ); + $this->assertPattern('/(^|\r\n)Custom: foo(\r\n|$)/', $headers); + } + + public function testHtmlMessage() { + $simple = new Simple(); + $message = new Message(array( + 'to' => 'foo@bar', 'from' => 'valid@address', + 'subject' => 'test subject', 'types' => 'html' + )); + $message->body('html', 'test body'); + $params = $simple->deliver($message); + extract($params); + $this->assertEqual('foo@bar', $to); + $this->assertEqual('test subject', $subject); + $this->assertEqual('test body', $body); + $this->assertPattern('/(^|\r\n)From: valid@address(\r\n|$)/', $headers); + $this->assertPattern('/(^|\r\n)MIME-Version: 1.0(\r\n|$)/', $headers); + $this->assertPattern( + '/(^|\r\n)Content-Type: text\/html;charset="' . $message->charset . '"(\r\n|$)/', + $headers + ); + } + + public function testMultipartMessage() { + $simple = new Simple(); + $message = new Message(array( + 'to' => 'foo@bar', 'from' => 'valid@address', 'subject' => 'test subject' + )); + $message->body('text', 'test text body'); + $message->body('html', 'test html body'); + $params = $simple->deliver($message); + extract($params); + $this->assertEqual('foo@bar', $to); + $this->assertEqual('test subject', $subject); + $this->assertPattern('/^This is a multi-part message in MIME format.\n\n/', $body); + $charset = $message->charset; + $this->assertPattern( + '/\nContent-Type: text\/plain;charset="' . $charset . '"\n\ntest text body\n/', + $body + ); + $this->assertPattern( + '/\nContent-Type: text\/html;charset="' . $charset . '"\n\ntest html body<\/b>\n/', + $body + ); + $this->assertPattern('/(^|\r\n)From: valid@address(\r\n|$)/', $headers); + $this->assertPattern('/(^|\r\n)MIME-Version: 1.0(\r\n|$)/', $headers); + $this->assertPattern( + '/(^|\r\n)Content-Type: multipart\/alternative;boundary="[^"]+"(\r\n|$)/', + $headers + ); + } + + public function testAttachments() { + $simple = new Simple(); + $path = tempnam('/tmp', 'li3_mailer_test'); + file_put_contents($path, 'file data'); + $message = new Message(array('attach' => array( + array('data' => 'my data', 'filename' => 'cool.txt'), + $path => array( + 'filename' => 'file.txt', 'content-type' => 'text/plain', 'id' => 'foo@bar' + ) + ))); + $message->body('text', 'text body'); + $message->body('html', 'html body'); + $params = $simple->deliver($message); + extract($params); + $c_type = 'Content-Type: text\/plain; name="cool.txt"'; + $c_disposition = 'Content-Disposition: attachment; filename="cool.txt"'; + $preg = '/\n' . $c_type . '\n' . $c_disposition . '\n\nmy data\n/'; + $this->assertPattern($preg, $body); + $c_type = 'Content-Type: text\/plain; name="file.txt"'; + $c_disposition = 'Content-Disposition: attachment; filename="file.txt"'; + $c_id = 'Content-ID: \'; + $preg = '/\n' . $c_type . '\n' . $c_disposition . '\n' . $c_id . '\n\nfile data\n/'; + $this->assertPattern($preg, $body); + unlink($path); + } + + public function testAttachmentErrorNoFile() { + $simple = new Simple(); + $message = new Message(array('attach' => array( + '/foo/bar' => array('filename' => 'file.txt', 'check' => false) + ))); + $this->expectException('/^Can not attach path `\/foo\/bar`\.$/'); + $simple->deliver($message); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/net/mail/transport/adapter/SwiftTest.php b/tests/cases/net/mail/transport/adapter/SwiftTest.php new file mode 100644 index 0000000..e8ef0c9 --- /dev/null +++ b/tests/cases/net/mail/transport/adapter/SwiftTest.php @@ -0,0 +1,176 @@ +skipIf(!$swift_available, 'SwiftMailer library not available.'); + } + + public function testDeliver() { + $swift = new MockSwift(); + $message = new Message(array('subject' => 'test subject')); + $transport = $swift->deliver($message); + $this->assertEqual(1, count($transport->delivered)); + $swift_message = $transport->delivered[0]; + $this->assertEqual('test subject', $swift_message->getSubject()); + } + + public function testBadTransport() { + $swift = new Swift(); + $this->expectException('Unknown transport type `` for `Swift` adapter.'); + $transport = $swift->invokeMethod('transport'); + + $swift = new Swift(array('transport' => 'foo')); + $this->expectException('Unknown transport type `foo` for `Swift` adapter.'); + $transport = $swift->invokeMethod('transport'); + } + + public function testMailTransport() { + $swift = new Swift(array('transport' => 'mail')); + $mailer = $swift->invokeMethod('transport'); + $this->assertTrue($mailer instanceof Swift_Mailer); + $transport = $mailer->getTransport(); + $this->assertTrue($transport instanceof Swift_MailTransport); + + $swift = new Swift(array('transport' => 'mail', 'extra_params' => 'foo')); + $mailer = $swift->invokeMethod('transport'); + $this->assertEqual('foo', $mailer->getTransport()->getExtraParams()); + + $swift = new Swift(array('transport' => 'mail')); + $mailer = $swift->invokeMethod('transport', array(array('extra_params' => 'foo'))); + $this->assertEqual('foo', $mailer->getTransport()->getExtraParams()); + } + + public function testSendmailTransport() { + $swift = new Swift(array('transport' => 'sendmail')); + $mailer = $swift->invokeMethod('transport'); + $this->assertTrue($mailer instanceof Swift_Mailer); + $transport = $mailer->getTransport(); + $this->assertTrue($transport instanceof Swift_SendmailTransport); + + $swift = new Swift(array('transport' => 'sendmail', 'command' => 'foo')); + $mailer = $swift->invokeMethod('transport'); + $this->assertEqual('foo', $mailer->getTransport()->getCommand()); + + $swift = new Swift(array('transport' => 'sendmail')); + $mailer = $swift->invokeMethod('transport', array(array('command' => 'foo'))); + $this->assertEqual('foo', $mailer->getTransport()->getCommand()); + } + + public function testSmtpTransport() { + $swift = new Swift(array('transport' => 'smtp')); + $mailer = $swift->invokeMethod('transport'); + $this->assertTrue($mailer instanceof Swift_Mailer); + $transport = $mailer->getTransport(); + $this->assertTrue($transport instanceof Swift_SmtpTransport); + + $options = array( + 'host' => 'test host', 'port' => 'test port', 'timeout' => 'test timeout', + 'encryption' => 'test encryption', 'sourceip' => 'test sourceip', + 'local_domain' => 'test local_domain', 'auth_mode' => 'test auth_mode', + 'username' => 'test username', 'password' => 'test password' + ); + $swift = new Swift(array('transport' => 'smtp') + $options); + $mailer = $swift->invokeMethod('transport'); + $transport = $mailer->getTransport(); + $this->assertEqual('test host', $transport->getHost()); + $this->assertEqual('test port', $transport->getPort()); + $this->assertEqual('test timeout', $transport->getTimeout()); + $this->assertEqual('test encryption', $transport->getEncryption()); + $this->assertEqual('test sourceip', $transport->getSourceip()); + $this->assertEqual('test local_domain', $transport->getLocalDomain()); + $this->assertEqual('test auth_mode', $transport->getAuthMode()); + $this->assertEqual('test username', $transport->getUsername()); + $this->assertEqual('test password', $transport->getPassword()); + + $swift = new Swift(array('transport' => 'smtp')); + $mailer = $swift->invokeMethod('transport', array($options)); + $transport = $mailer->getTransport(); + $this->assertEqual('test host', $transport->getHost()); + $this->assertEqual('test port', $transport->getPort()); + $this->assertEqual('test timeout', $transport->getTimeout()); + $this->assertEqual('test encryption', $transport->getEncryption()); + $this->assertEqual('test sourceip', $transport->getSourceip()); + $this->assertEqual('test local_domain', $transport->getLocalDomain()); + $this->assertEqual('test auth_mode', $transport->getAuthMode()); + $this->assertEqual('test username', $transport->getUsername()); + $this->assertEqual('test password', $transport->getPassword()); + } + + public function testMessage() { + $message = new Message(array( + 'to' => array('Foo' => 'foo@bar'), 'from' => 'valid@address', + 'subject' => 'test subject', 'types' => 'text', 'charset' => 'ISO-8859-1', + 'cc' => array('Bar' => 'bar@foo', 'baz@qux'), 'headers' => array('Custom' => 'foo') + )); + $message->body('text', 'test body'); + $swift = new Swift(); + $swift_message = $swift->invokeMethod('message', array($message)); + $this->assertEqual(array('foo@bar' => 'Foo'), $swift_message->getTo()); + $this->assertEqual(array('valid@address' => null), $swift_message->getFrom()); + $this->assertEqual(array('bar@foo' => 'Bar', 'baz@qux' => null), $swift_message->getCc()); + $this->assertEqual('test subject', $swift_message->getSubject()); + $this->assertEqual('test body', $swift_message->getBody()); + $this->assertEqual('ISO-8859-1', $swift_message->getCharset()); + $header = $swift_message->getHeaders()->get('Custom'); + $this->assertEqual('foo', $header->getValue()); + } + + public function testMultipartMessage() { + $message = new Message(); + $message->body('text', 'test text body'); + $message->body('html', 'test html body'); + $swift = new Swift(); + $swift_message = $swift->invokeMethod('message', array($message)); + $this->assertEqual('test html body', $swift_message->getBody()); + $children = $swift_message->getChildren(); + $this->assertEqual(1, count($children)); + $this->assertEqual('test text body', $children[0]->getBody()); + } + + public function testAttachments() { + $path = tempnam('/tmp', 'li3_mailer_test'); + file_put_contents($path, 'file data'); + $message = new Message(array('attach' => array( + array('data' => 'my data', 'filename' => 'cool.txt'), + $path => array( + 'filename' => 'file.txt', 'content-type' => 'text/plain', 'id' => 'foo@bar' + ) + ))); + $message->body('text', 'text body'); + $message->body('html', 'html body'); + $swift = new Swift(); + $swift_message = $swift->invokeMethod('message', array($message)); + $children = $swift_message->getChildren(); + $attachments = array_filter($children, function($child) { + return in_array($child->getBody(), array('file data', 'my data')); + }); + $this->assertEqual(2, count($attachments)); + list($file, $data) = array_values($attachments); + if ($data->getBody() != 'my data') { + list($file, $data) = array($data, $file); + } + //swift wipes out body for files, maybe a bug? + //$this->assertEqual('file data', $file->getBody()); + $this->assertEqual('file.txt', $file->getFilename()); + $this->assertEqual('text/plain', $file->getContentType()); + $this->assertEqual('foo@bar', $file->getId()); + + $this->assertEqual('my data', $data->getBody()); + $this->assertEqual('cool.txt', $data->getFilename()); + $this->assertEqual('text/plain', $data->getContentType()); + unlink($path); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/template/MailTest.php b/tests/cases/template/MailTest.php new file mode 100644 index 0000000..647bb51 --- /dev/null +++ b/tests/cases/template/MailTest.php @@ -0,0 +1,39 @@ +outputFilters['h']; + $this->assertTrue(mb_check_encoding($handler($string), 'UTF-8')); + } + + public function testSetsEncoding() { + $string = "Joël"; + $encoding = 'ISO-8859-1'; + $message = new Message(compact('encoding')); + $mail = new Mail(compact('message')); + $handler = $mail->outputFilters['h']; + $this->assertTrue(mb_check_encoding($handler($string), $encoding)); + } + + public function testLoadsMailAdapter() { + $mail = new MockMail(); + $this->assertTrue($mail->renderer() instanceof File); + } + + public function testSetsRenderer() { + $renderer = new File(); + $mail = new MockMail(compact('renderer')); + $this->assertEqual($renderer, $mail->renderer()); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/template/helper/mail/HtmlTest.php b/tests/cases/template/helper/mail/HtmlTest.php new file mode 100644 index 0000000..4a728dd --- /dev/null +++ b/tests/cases/template/helper/mail/HtmlTest.php @@ -0,0 +1,52 @@ +charset(); + $this->assertTags($result, array('meta' => array( + 'charset' => $message->charset + ))); + } + + public function testImage() { + $message = new Message(array('base_url' => 'foo.local')); + $context = new File(compact('message')); + $html = new Html(compact('context')); + + $result = $html->image('test.gif', array('check' => false)); + $this->assertTags($result, array('img' => array( + 'src' => 'regex:/cid:[^@]+@foo.local/', 'alt' => '' + ))); + + $result = $html->image('/foo/bar.gif', array('check' => false)); + $this->assertTags($result, array('img' => array( + 'src' => 'regex:/cid:[^@]+@foo.local/', 'alt' => '' + ))); + + $result = $html->image('test.gif', array('embed' => false)); + $this->assertTags($result, array('img' => array( + 'src' => 'http://foo.local/img/test.gif', 'alt' => '' + ))); + + $result = $html->image('http://example.com/logo.gif'); + $this->assertTags($result, array('img' => array( + 'src' => 'http://example.com/logo.gif', 'alt' => '' + ))); + + $result = $html->image('/foo/bar.gif', array('embed' => false)); + $this->assertTags($result, array('img' => array( + 'src' => 'http://foo.local/foo/bar.gif', 'alt' => '' + ))); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/template/mail/CompilerTest.php b/tests/cases/template/mail/CompilerTest.php new file mode 100644 index 0000000..1f25884 --- /dev/null +++ b/tests/cases/template/mail/CompilerTest.php @@ -0,0 +1,41 @@ +skipIf(!is_writable($write_dir), "Path `{$write_dir}` is not writable."); + $path = realpath(Libraries::get(true, 'resources') . '/tmp/tests'); + $this->skipIf(!is_writable($path), "Path `{$path}` is not writable."); + $file = $path . DIRECTORY_SEPARATOR . 'mail_template.html.php'; + file_put_contents($file, 'test mail template'); + $compiler = new Compiler(); + $template = $compiler->template($file, $options); + $this->assertEqual(0, strpos($template, $write_dir)); + $this->assertTrue(is_file($template)); + $result = file_get_contents($template); + $this->assertEqual('test mail template', $result); + unlink($template); + } + + public function testSetsPath() { + $write_dir = realpath(Libraries::get(true, 'resources') . '/tmp/cache/mails'); + $this->checkWritesTo($write_dir); + } + + public function testDoesNotOverridePath() { + $base_dir = realpath(Libraries::get(true, 'resources') . '/tmp/cache/mails'); + $this->skipIf(!is_writable($base_dir), "Path `{$base_dir}` is not writable."); + $write_dir = $base_dir . DIRECTORY_SEPARATOR . 'test_override_path'; + if (!is_dir($write_dir)) { + mkdir($write_dir); + } + $this->checkWritesTo($write_dir, array('path' => $write_dir)); + rmdir($write_dir); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/template/mail/adapter/FileTest.php b/tests/cases/template/mail/adapter/FileTest.php new file mode 100644 index 0000000..27797b0 --- /dev/null +++ b/tests/cases/template/mail/adapter/FileTest.php @@ -0,0 +1,109 @@ +_routes = Router::get(); + Router::reset(); + Router::connect('/{:controller}/{:action}'); + } + + public function resetRoutes() { + Router::reset(); + + foreach ($this->_routes as $route) { + Router::connect($route); + } + } + + public function testInitialization() { + $file = new File(); + + $expected = array('url', 'path', 'options', 'title', 'scripts', 'styles', 'head'); + $result = array_keys($file->handlers()); + $this->assertEqual($expected, $result); + + $expected = array( + 'content' => '', 'scripts' => array(), + 'styles' => array(), 'head' => array() + ); + $this->assertEqual($expected, $file->context()); + } + + public function testCoreHandlers() { + $message = new Message(array('base_url' => 'foo.local')); + $file = new File(compact('message')); + + $this->setDefaultRoute(); + $url = $file->applyHandler(null, null, 'url', array( + 'controller' => 'foo', 'action' => 'bar' + )); + $this->assertEqual('http://foo.local/foo/bar', $url); + $this->resetRoutes(); + + $helper = new Html(); + $class = get_class($helper); + $path = $file->applyHandler($helper, "{$class}::script", 'path', 'foo/file'); + $this->assertEqual('http://foo.local/js/foo/file.js', $path); + $this->assertEqual('http://foo.local/some/generic/path', $file->path('some/generic/path')); + $this->assertPattern( + '/^cid:[^@]+@foo.local$/', + $file->path('image.png', array('embed' => true, 'check' => false)) + ); + } + + public function testHelperNamespace() { + $file = new File(); + $helper = $file->helper('html'); + $this->assertTrue($helper instanceof Html); + //test cache + $helper2 = $file->helper('html'); + $this->assertEqual($helper2, $helper); + } + + public function testBadHelper() { + $file = new File(); + $this->expectException('Mail helper `foo` not found.'); + $helper = $file->helper('foo'); + } + + public function testMessage() { + $message = new Message(); + $file = new File(compact('message')); + $this->assertEqual($message, $file->message()); + } + + public function testRenderDoesNotSetLibrary() { + $view = new MailWithoutRender(); + $file = new File(compact('view')); + $params = $file->invokeMethod('_render', array('element', 'foo')); + extract($params); + $this->assertFalse(isset($options['library'])); + } + + public function testHandlers() { + $file = new File(); + + $this->assertFalse(trim($file->scripts())); + $this->assertEqual('foobar', trim($file->scripts('foobar'))); + $this->assertEqual('foobar', trim($file->scripts())); + + $this->assertFalse(trim($file->styles())); + $this->assertEqual('foobar', trim($file->styles('foobar'))); + $this->assertEqual('foobar', trim($file->styles())); + + $this->assertFalse(trim($file->head())); + $this->assertEqual('foo', trim($file->head('foo'))); + $this->assertEqual("foo\n\tbar", trim($file->head('bar'))); + $this->assertEqual("foo\n\tbar", trim($file->head())); + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/action/Mailer.php b/tests/mocks/action/Mailer.php new file mode 100644 index 0000000..8826bc6 --- /dev/null +++ b/tests/mocks/action/Mailer.php @@ -0,0 +1,15 @@ + 'li3_mailer\net\mail\Media', + 'message' => 'li3_mailer\net\mail\Message', + 'delivery' => 'li3_mailer\tests\mocks\net\mail\Delivery' + ); + + protected static $_messages = array(array('delivery' => 'test')); +} + +?> \ No newline at end of file diff --git a/tests/mocks/action/MailerOverload.php b/tests/mocks/action/MailerOverload.php new file mode 100644 index 0000000..bec5f78 --- /dev/null +++ b/tests/mocks/action/MailerOverload.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/tests/mocks/action/MailerWithOptions.php b/tests/mocks/action/MailerWithOptions.php new file mode 100644 index 0000000..e26ca74 --- /dev/null +++ b/tests/mocks/action/MailerWithOptions.php @@ -0,0 +1,16 @@ + array('additional' => 'data')), + 'with_extra_options' => array('data' => array('extra' => 'data')) + ); + + public static function options($message, array $options = array()) { + return static::_options($message, $options); + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/action/TestMailer.php b/tests/mocks/action/TestMailer.php new file mode 100644 index 0000000..854198d --- /dev/null +++ b/tests/mocks/action/TestMailer.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/tests/mocks/net/mail/Delivery.php b/tests/mocks/net/mail/Delivery.php new file mode 100644 index 0000000..4dddffb --- /dev/null +++ b/tests/mocks/net/mail/Delivery.php @@ -0,0 +1,15 @@ + array('from' => 'adapter@config')); + + public static function adapter($name = null) { + return $name == 'test' ? new Transport() : null; + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/net/mail/DeliveryWithPath.php b/tests/mocks/net/mail/DeliveryWithPath.php new file mode 100644 index 0000000..8b34b09 --- /dev/null +++ b/tests/mocks/net/mail/DeliveryWithPath.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/tests/mocks/net/mail/Transport.php b/tests/mocks/net/mail/Transport.php new file mode 100644 index 0000000..949ffd4 --- /dev/null +++ b/tests/mocks/net/mail/Transport.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/tests/mocks/net/mail/transport/adapter/Simple.php b/tests/mocks/net/mail/transport/adapter/Simple.php new file mode 100644 index 0000000..e8e2b3f --- /dev/null +++ b/tests/mocks/net/mail/transport/adapter/Simple.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/tests/mocks/net/mail/transport/adapter/Swift.php b/tests/mocks/net/mail/transport/adapter/Swift.php new file mode 100644 index 0000000..40e914b --- /dev/null +++ b/tests/mocks/net/mail/transport/adapter/Swift.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/tests/mocks/net/mail/transport/adapter/SwiftTransport.php b/tests/mocks/net/mail/transport/adapter/SwiftTransport.php new file mode 100644 index 0000000..7f07bce --- /dev/null +++ b/tests/mocks/net/mail/transport/adapter/SwiftTransport.php @@ -0,0 +1,14 @@ +delivered[] = $message; + return $this; + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/template/Mail.php b/tests/mocks/template/Mail.php new file mode 100644 index 0000000..c21744d --- /dev/null +++ b/tests/mocks/template/Mail.php @@ -0,0 +1,26 @@ +_renderer; + } + + public function loader() { + $config = array('view' => $this) + $this->_config; + return new FileLoader($config); + } + + public function message() { + return $this->_message; + } + + public function render($process, array $data = array(), array $options = array()) { + return 'fake rendered message'; + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/template/MailWithoutRender.php b/tests/mocks/template/MailWithoutRender.php new file mode 100644 index 0000000..546d8a6 --- /dev/null +++ b/tests/mocks/template/MailWithoutRender.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/tests/mocks/template/mail/Compiler.php b/tests/mocks/template/mail/Compiler.php new file mode 100644 index 0000000..13a6b79 --- /dev/null +++ b/tests/mocks/template/mail/Compiler.php @@ -0,0 +1,19 @@ +_renderer; + } + + public function message() { + return $this->_message; + } + + public function render($process, array $data = array(), array $options = array()) { + return 'fake rendered message'; + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/template/mail/adapter/FileLoader.php b/tests/mocks/template/mail/adapter/FileLoader.php new file mode 100644 index 0000000..9ac9a32 --- /dev/null +++ b/tests/mocks/template/mail/adapter/FileLoader.php @@ -0,0 +1,19 @@ +_paths[$type])) { + throw new TemplateException("Invalid template type '{$type}'."); + } + return array_map(function($path) use ($params) { + return String::insert($path, $params); + }, (array) $this->_paths[$type]); + } +} + +?> \ No newline at end of file