Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit ce088e8924c5108750f309542806fd542ca7b2e6 0 parents
@eLod authored
Showing with 4,076 additions and 0 deletions.
  1. +20 −0 LICENSE.txt
  2. +25 −0 README.md
  3. +196 −0 action/Mailer.php
  4. +38 −0 config/bootstrap.php
  5. +69 −0 config/bootstrap/delivery.php
  6. +41 −0 net/mail/Delivery.php
  7. +89 −0 net/mail/Grammar.php
  8. +355 −0 net/mail/Media.php
  9. +14 −0 net/mail/MediaException.php
  10. +634 −0 net/mail/Message.php
  11. +40 −0 net/mail/Transport.php
  12. +188 −0 net/mail/transport/adapter/Debug.php
  13. +117 −0 net/mail/transport/adapter/Simple.php
  14. +206 −0 net/mail/transport/adapter/Swift.php
  15. +188 −0 readme.wiki
  16. +63 −0 template/Mail.php
  17. +14 −0 template/helper/mail/Form.php
  18. +98 −0 template/helper/mail/Html.php
  19. +31 −0 template/mail/Compiler.php
  20. +200 −0 template/mail/adapter/File.php
  21. +107 −0 tests/cases/action/MailerTest.php
  22. +17 −0 tests/cases/net/mail/DeliveryTest.php
  23. +44 −0 tests/cases/net/mail/GrammarTest.php
  24. +148 −0 tests/cases/net/mail/MediaTest.php
  25. +233 −0 tests/cases/net/mail/MessageTest.php
  26. +21 −0 tests/cases/net/mail/TransportTest.php
  27. +144 −0 tests/cases/net/mail/transport/adapter/DebugTest.php
  28. +116 −0 tests/cases/net/mail/transport/adapter/SimpleTest.php
  29. +176 −0 tests/cases/net/mail/transport/adapter/SwiftTest.php
  30. +39 −0 tests/cases/template/MailTest.php
  31. +52 −0 tests/cases/template/helper/mail/HtmlTest.php
  32. +41 −0 tests/cases/template/mail/CompilerTest.php
  33. +109 −0 tests/cases/template/mail/adapter/FileTest.php
  34. +15 −0 tests/mocks/action/Mailer.php
  35. +11 −0 tests/mocks/action/MailerOverload.php
  36. +16 −0 tests/mocks/action/MailerWithOptions.php
  37. +8 −0 tests/mocks/action/TestMailer.php
  38. +15 −0 tests/mocks/net/mail/Delivery.php
  39. +11 −0 tests/mocks/net/mail/DeliveryWithPath.php
  40. +11 −0 tests/mocks/net/mail/Transport.php
  41. +14 −0 tests/mocks/net/mail/transport/adapter/Simple.php
  42. +13 −0 tests/mocks/net/mail/transport/adapter/Swift.php
  43. +14 −0 tests/mocks/net/mail/transport/adapter/SwiftTransport.php
  44. +26 −0 tests/mocks/template/Mail.php
  45. +11 −0 tests/mocks/template/MailWithoutRender.php
  46. +19 −0 tests/mocks/template/mail/Compiler.php
  47. +19 −0 tests/mocks/template/mail/adapter/FileLoader.php
20 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.
25 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.
196 action/Mailer.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace li3_mailer\action;
+
+use lithium\util\Inflector;
+use BadMethodCallException;
+
+/**
+ * The `Mailer` class is the fundamental building block for sending mails within your application.
+ * It may be subclassed to group options (with `$_messages`) and/or templates (view templates can
+ * reside in a folder named after their `Mailer` subclass) together.
+ *
+ * @see li3_mailer\net\mail\Media
+ * @see li3_mailer\net\mail\Delivery
+ * @see li3_mailer\action\Mailer::deliver()
+ */
+class Mailer extends \lithium\core\StaticObject {
+ /**
+ * Holds extra configurations per message (and a default
+ * for every message at key `0` if set). See `Mailer::_options()`.
+ *
+ * @see li3_mailer\action\Mailer::_options()
+ * @var array
+ */
+ protected static $_messages = array();
+
+ /**
+ * The option names that should not be treated as data when parsing
+ * options. See `Mailer::_options()`.
+ *
+ * @see li3_mailer\action\Mailer::_options()
+ * @var array
+ */
+ protected static $_short_options = array('from', 'cc', 'bcc', 'subject', 'delivery');
+
+ /**
+ * Class dependencies.
+ *
+ * @var array
+ */
+ protected static $_classes = array(
+ 'media' => '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<message>\w+)With(?P<delivery>\w+)$/', $method, $args);
+ if (!$found) {
+ preg_match('/^deliver(?P<message>\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;
+ }
+}
+
+?>
38 config/bootstrap.php
@@ -0,0 +1,38 @@
+<?php
+
+use lithium\core\Libraries;
+
+/**
+ * Register paths for mail template helpers.
+ */
+$existing = Libraries::paths('helper');
+Libraries::paths(array('helper' => 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';
+
+?>
69 config/bootstrap/delivery.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * ### Configure email delivery
+ *
+ * The `Delivery` is a configurable service that provides transport adapters for sending emails
+ * from your application. As with other `Adaptable`-based configurations, each delivery
+ * configuration is defined by a name, and an array of information detailing extra configuration
+ * for that delivery adapter. `Delivery` also supports environment-base configurations.
+ *
+ * By default the plugin provides 3 adapters:
+ *
+ * - `'Simple'`: uses `PHP`'s built-in `mail` function to send messages,
+ * - `'Debug'`: does not send mails, but logs them to a file,
+ * - `'Swift'`: uses the `SwiftMailer` library to send messages.
+ *
+ * For configuration options please see the adapters' documentation.
+ *
+ * Apart from the adapter options the configuration may hold settings for the messages that should
+ * be sent with this adapter, like (but not limited to) `'from'`, `'cc'` and other fields. The
+ * `Mailer` will retrieve the configuration when constructing the `Message` for delivery and set
+ * applicable values.
+ *
+ * @see li3_mailer\net\mail\Delivery
+ * @see lithium\core\Adaptable
+ * @see li3_mailer\net\mail\Transport
+ * @see li3_mailer\net\mail\transport\adapter\Simple
+ * @see li3_mailer\net\mail\transport\adapter\Debug
+ * @see li3_mailer\net\mail\transport\adapter\Swift
+ * @see li3_mailer\net\mail\Message
+ * @see li3_mailer\net\mail\Mailer
+ * @see li3_mailer\net\mail\Mailer::deliver()
+ */
+use li3_mailer\net\mail\Delivery;
+
+/**
+ * Sample configuration for `'Simple'` adapter
+ */
+// Delivery::config(array('default' => 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'
+// ));
+
+?>
41 net/mail/Delivery.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+/**
+ * The `Delivery` class provides a consistent interface for configuring
+ * email delivery. Apart from the options for the adapter(s) the configuration
+ * can hold values for constructing the message (e.g. `'to'`, `'bcc'` and other
+ * supported options, see `Message`).
+ *
+ * A simple example configuration (**please note**: you'll need the
+ * SwiftMailer library for the `'Swift'` adapter to work):
+ *
+ * {{{Delivery::config(array(
+ * 'local' => 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';
+}
+
+?>
89 net/mail/Grammar.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+/**
+ * Grammar rules for checking Message validity (particularly Content ID syntax),
+ * implements the RFC 2822 (and friends) ABNF grammar definitions.
+ *
+ * @see http://tools.ietf.org/html/rfc2822
+ * @see li3_mailer\net\mail\Message
+ */
+class Grammar extends \lithium\core\Object {
+ /**
+ * Tokens and matching regular expression( part)s as key value pairs,
+ * defined in RFC 2822 (and some related RFCs).
+ *
+ * @var array
+ */
+ protected $_grammar = array();
+
+ /**
+ * Auto configuration properties.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('grammar' => '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);
+ }
+}
+
+?>
355 net/mail/Media.php
@@ -0,0 +1,355 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+use li3_mailer\net\mail\MediaException;
+use lithium\core\Libraries;
+
+/**
+ * The `Media` class facilitates content-type mapping (mapping between content-types and file
+ * extensions), handling static assets and globally configuring how the framework handles output in
+ * different formats for rendering emails.
+ */
+class Media extends \lithium\core\StaticObject {
+ /**
+ * Maps file extensions to content-types. Used to render content into
+ * message. Can be modified with `Media::type()`.
+ *
+ * @var array
+ * @see li3_mailer\net\mail\Media::type()
+ */
+ protected static $_types = array();
+
+ /**
+ * A map of media handler objects or callbacks, mapped to media types.
+ *
+ * @var array
+ */
+ protected static $_handlers = array();
+
+ /**
+ * Placeholder for class dependencies. This class' dependencies (i.e. templating classes) are
+ * typically specified through other configuration.
+ *
+ * @var array
+ */
+ protected static $_classes = array('request' => '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));
+ });
+ }
+}
+
+?>
14 net/mail/MediaException.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+/**
+ * The `MediaException` is thrown when a message is made to render content in a format not
+ * supported or there is some configuration error for the handler.
+ *
+ * @see li3_mailer\net\mail\Media
+ */
+class MediaException extends \RuntimeException {
+}
+
+?>
634 net/mail/Message.php
@@ -0,0 +1,634 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+use RuntimeException;
+use lithium\action\Request;
+
+/**
+ * A mail message object (RFC 2822).
+ *
+ * @see http://tools.ietf.org/html/rfc2822
+ */
+class Message extends \lithium\core\Object {
+ /**
+ * Subject.
+ *
+ * @var string
+ */
+ public $subject;
+
+ /**
+ * (Origination) date of the message as UNIX timestamp.
+ *
+ * @var integer
+ */
+ public $date;
+
+ /**
+ * Return-path.
+ *
+ * @var string
+ */
+ public $return_path;
+
+ /**
+ * Sender (this should be set to a single address
+ * when multiple from fields are present as per the RFC).
+ *
+ * @see li3_mailer\net\mail\Message::ensureCleanSender()
+ * @var array
+ */
+ public $sender;
+
+ /**
+ * From adress(es).
+ *
+ * @var array
+ */
+ public $from;
+
+ /**
+ * Reply-to adress(es).
+ *
+ * @var array
+ */
+ public $reply_to;
+
+ /**
+ * Recipient(s).
+ *
+ * @var array
+ */
+ public $to;
+
+ /**
+ * Cc address(es).
+ *
+ * @var array
+ */
+ public $cc;
+
+ /**
+ * Bcc address(es).
+ *
+ * @var array
+ */
+ public $bcc;
+
+ /**
+ * Content-Types.
+ *
+ * @var array
+ */
+ public $types = array('html', 'text');
+
+ /**
+ * Character set.
+ *
+ * @var string
+ */
+ public $charset = 'UTF-8';
+
+ /**
+ * Headers.
+ *
+ * @var array
+ */
+ public $headers = array();
+
+ /**
+ * The body of the message, indexed by content type.
+ *
+ * @var array
+ */
+ public $body = array();
+
+ /**
+ * Base URL for the message that should be used to generate
+ * URLs (and the host part is used to construct Content-IDs).
+ *
+ * @see li3_mailer\net\mail\Message::_init()
+ * @see li3_mailer\net\mail\Message::discoverURL()
+ * @see li3_mailer\net\mail\Media::_request()
+ * @see li3_mailer\net\mail\Message::randomId()
+ * @var string
+ */
+ public $base_url = null;
+
+ /**
+ * Attachments for the message.
+ *
+ * @see li3_mailer\net\mail\Message::attach()
+ * @see li3_mailer\net\mail\Message::detach()
+ * @see li3_mailer\net\mail\Message::attachments()
+ * @var array
+ */
+ protected $_attachments = array();
+
+ /**
+ * Mime type auto-detection for attachments.
+ * Content types indexed by extension.
+ *
+ * @see li3_mailer\net\mail\Message::attach()
+ * @var array
+ */
+ protected $_mime_types = array(
+ 'aif' => '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: <img src="cid:' . $cid . '" alt="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;
+ }
+}
+
+?>
40 net/mail/Transport.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace li3_mailer\net\mail;
+
+/**
+ * Base class for delivery (transport) adapters.
+ *
+ * @see li3_mailer\net\mail\Delivery
+ * @see li3_mailer\net\mail\transport\adapter\Simple
+ * @see li3_mailer\net\mail\transport\adapter\Debug
+ * @see li3_mailer\net\mail\transport\adapter\Swift
+ */
+abstract class Transport extends \lithium\core\Object {
+ /**
+ * Deliver a message.
+ *
+ * @see li3_mailer\net\mail\Message
+ * @param object $message The message to deliver.
+ * @param array $options Options.
+ */
+ abstract public function deliver($message, array $options = array());
+
+ /**
+ * Format addresses.
+ *
+ * @param mixed $address Address to format, may be a string or array.
+ * @return string Formatted address list.
+ */
+ protected function address($address) {
+ if (is_array($address)) {
+ return join(", ", array_map(function($name, $address) {
+ return is_int($name) ? $address : "{$name} <{$address}>";
+ }, array_keys($address), $address));
+ } else {
+ return $address;
+ }
+ }
+}
+
+?>
188 net/mail/transport/adapter/Debug.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace li3_mailer\net\mail\transport\adapter;
+
+use lithium\core\Libraries;
+use lithium\util\String;
+use Closure;
+use RuntimeException;
+
+/**
+ * The `Debug` adapter does not send email messages, but logs them to a file.
+ *
+ * An example configuration:
+ * {{{Delivery::config(array('debug' => 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;
+ }
+}
+
+?>
117 net/mail/transport/adapter/Simple.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace li3_mailer\net\mail\transport\adapter;
+
+use RuntimeException;
+
+/**
+ * The `Simple` adapter sends email messages with `PHP`'s built-in function `mail`.
+ *
+ * An example configuration:
+ * {{{Delivery::config(array('simple' => 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);
+ }
+}
+
+?>
206 net/mail/transport/adapter/Swift.php
@@ -0,0 +1,206 @@
+<?php
+
+namespace li3_mailer\net\mail\transport\adapter;
+
+use lithium\util\Inflector;
+use Swift_MailTransport;
+use Swift_SendmailTransport;
+use Swift_SmtpTransport;
+use RuntimeException;
+use Swift_Mailer;
+use Swift_Message;
+use Swift_Attachment;
+
+/**
+ * The `Swift` adapter sends email messages with the SwiftMailer library.
+ *
+ * An example configuration:
+ * {{{Delivery::config(array('swift' => 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;
+ }
+}
+
+?>
188 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',