Index: /trunk/src/bp-core/bp-core-classes.php
===================================================================
--- /trunk/src/bp-core/bp-core-classes.php	(revision 10469)
+++ /trunk/src/bp-core/bp-core-classes.php	(revision 10470)
@@ -26,2 +26,6 @@
 require dirname( __FILE__ ) . '/classes/class-bp-attachment-avatar.php';
 require dirname( __FILE__ ) . '/classes/class-bp-attachment-cover-image.php';
+require dirname( __FILE__ ) . '/classes/class-bp-email-recipient.php';
+require dirname( __FILE__ ) . '/classes/class-bp-email.php';
+require dirname( __FILE__ ) . '/classes/class-bp-email-delivery.php';
+require dirname( __FILE__ ) . '/classes/class-bp-phpmailer.php';
Index: /trunk/src/bp-core/classes/class-bp-email-delivery.php
===================================================================
--- /trunk/src/bp-core/classes/class-bp-email-delivery.php	(revision 10470)
+++ /trunk/src/bp-core/classes/class-bp-email-delivery.php	(revision 10470)
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Email delivery implementation base class.
+ *
+ * When implementing support for an email delivery service into BuddyPress,
+ * you are required to create a class that implements this interface.
+ *
+ * @since 2.5.0
+ */
+interface BP_Email_Delivery {
+
+	/**
+	 * Send email(s).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool|WP_Error Returns true if email send, else a descriptive WP_Error.
+	 */
+	public function bp_email( BP_Email $email );
+}
Index: /trunk/src/bp-core/classes/class-bp-email-recipient.php
===================================================================
--- /trunk/src/bp-core/classes/class-bp-email-recipient.php	(revision 10470)
+++ /trunk/src/bp-core/classes/class-bp-email-recipient.php	(revision 10470)
@@ -0,0 +1,172 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Represents a recipient that an email will be sent to.
+ *
+ * @since 2.5.0
+ */
+class BP_Email_Recipient {
+
+	/**
+	 * Recipient's email address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $address = '';
+
+	/**
+	 * Recipient's name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $name = '';
+
+	/**
+	 * Optional. A `WP_User` object relating to this recipient.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var WP_User
+	 */
+	protected $user_object = null;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_or_user Either a email address, user ID, WP_User object,
+	 *                                                or an array containg the address and name.
+	 * @param string $name Optional. If $email_or_user is a string, this is the recipient's name.
+	 */
+	public function __construct( $email_or_user, $name = '' ) {
+		$name = sanitize_text_field( $name );
+
+		// User ID, WP_User object.
+		if ( is_int( $email_or_user ) || is_object( $email_or_user ) ) {
+			$this->user_object = is_object( $email_or_user ) ? $email_or_user : get_user_by( 'ID', $email_or_user );
+
+			if ( $this->user_object ) {
+				// This is escaped with esc_html in bp_core_get_user_displayname()
+				$name = wp_specialchars_decode( bp_core_get_user_displayname( $this->user_object->ID ), ENT_QUOTES );
+
+				$this->address = $this->user_object->user_email;
+				$this->name    = sanitize_text_field( $name );
+			}
+
+		// Array, address, and name.
+		} else {
+			if ( ! is_array( $email_or_user ) ) {
+				$email_or_user = array( $email_or_user => $name );
+			}
+
+			// Handle numeric arrays.
+			if ( is_int( key( $email_or_user ) ) ) {
+				$address = current( $email_or_user );
+			} else {
+				$address = key( $email_or_user );
+				$name    = current( $email_or_user );
+			}
+
+			if ( is_email( $address ) ) {
+				$this->address = sanitize_email( $address );
+			}
+
+			$this->name = $name;
+		}
+
+		/**
+		 * Fires inside __construct() method for BP_Email_Recipient class.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string|array|int|WP_User $email_or_user Either a email address, user ID, WP_User object,
+		 *                                                or an array containg the address and name.
+		 * @param string $name If $email_or_user is a string, this is the recipient's name.
+		 * @param BP_Email_Recipient $this Current instance of the email type class.
+		 */
+		do_action( 'bp_email_recipient', $email_or_user, $name, $this );
+	}
+
+	/**
+	 * Get recipient's address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	public function get_address() {
+
+		/**
+		 * Filters the recipient's address before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $address Recipient's address.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_address', $this->address, $this );
+	}
+
+	/**
+	 * Get recipient's name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	public function get_name() {
+
+		/**
+		 * Filters the recipient's name before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $name Recipient's name.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_name', $this->name, $this );
+	}
+
+	/**
+	 * Get WP_User object for this recipient.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'search-email'.
+	 * @return WP_User|null WP_User object, or null if not set.
+	 */
+	public function get_user( $transform = 'raw' ) {
+
+		// If transform "search-email", find the WP_User if not already set.
+		if ( $transform === 'search-email' && ! $this->user_object && $this->address ) {
+			$this->user_object = get_user_by( 'email', $this->address );
+		}
+
+		/**
+		 * Filters the WP_User object for this recipient before it's returned.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param WP_User $name WP_User object for this recipient, or null if not set.
+		 * @param string $transform Optional. How the return value was transformed.
+		 *                          Accepts 'raw' (default) or 'search-email'.
+		 * @param BP_Email $recipient $this Current instance of the email recipient class.
+		 */
+		return apply_filters( 'bp_email_recipient_get_name', $this->user_object, $transform, $this );
+	}
+}
Index: /trunk/src/bp-core/classes/class-bp-email.php
===================================================================
--- /trunk/src/bp-core/classes/class-bp-email.php	(revision 10470)
+++ /trunk/src/bp-core/classes/class-bp-email.php	(revision 10470)
@@ -0,0 +1,964 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Represents an email that will be sent to member(s).
+ *
+ * @since 2.5.0
+ */
+class BP_Email {
+	/**
+	 * Addressee details (BCC).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] BCC recipients.
+	 */
+	protected $bcc = array();
+
+	/**
+	 * Addressee details (CC).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] CC recipients.
+	 */
+	protected $cc = array();
+
+	/**
+	 * Email content (HTML).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_html = '';
+
+	/**
+	 * Email content (plain text).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_plaintext = '';
+
+	/**
+	 * The content type to send the email in ("html" or "plaintext").
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $content_type = 'html';
+
+	/**
+	 * Sender details.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient Sender details.
+	 */
+	protected $from = null;
+
+	/**
+	 * Email headers.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string[] Associative pairing of email header name/value.
+	 */
+	protected $headers = array();
+
+	/**
+	 * The Post object (the source of the email's content and subject).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var WP_Post
+	 */
+	protected $post_object = null;
+
+	/**
+	 * Reply To details.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient "Reply to" details.
+	 */
+	protected $reply_to = null;
+
+	/**
+	 * Email subject.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $subject = '';
+
+	/**
+	 * Email template (the HTML wrapper around the email content).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $template = '{{{content}}}';
+
+	/**
+	 * Addressee details (to).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var BP_Email_Recipient[] Email recipients.
+	 * }
+	 */
+	protected $to = array();
+
+	/**
+	 * Unique identifier for this particular type of email.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $type = '';
+
+	/**
+	 * Token names and replacement values for this email.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string[] Associative pairing of token name (key) and replacement value (value).
+	 */
+	protected $tokens = array();
+
+	/**
+	 * Constructor.
+	 *
+	 * Set the email type and default "from" and "reply to" name and address.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $email_type Unique identifier for a particular type of email.
+	 */
+	public function __construct( $email_type ) {
+		$this->type = $email_type;
+
+		// SERVER_NAME isn't always set (e.g CLI).
+		if ( ! empty( $_SERVER['SERVER_NAME'] ) ) {
+			$domain = strtolower( $_SERVER['SERVER_NAME'] );
+			if ( substr( $domain, 0, 4 ) === 'www.' ) {
+				$domain = substr( $domain, 4 );
+			}
+
+		} elseif ( function_exists( 'gethostname' ) && gethostname() !== false ) {
+			$domain = gethostname();
+
+		} elseif ( php_uname( 'n' ) !== false ) {
+			$domain = php_uname( 'n' );
+
+		} else {
+			$domain = 'localhost.localdomain';
+		}
+
+		// This was escaped with esc_html on the way into the database in sanitize_option().
+		$site_name = wp_specialchars_decode( bp_get_option( 'blogname' ), ENT_QUOTES );
+
+		$this->set_from( "wordpress@$domain", $site_name );
+		$this->set_reply_to( bp_get_option( 'admin_email' ), $site_name );
+
+		/**
+		 * Fires inside __construct() method for BP_Email class.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $email_type Unique identifier for this type of email.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		do_action( 'bp_email', $email_type, $this );
+	}
+
+
+	/*
+	 * Setters/getters.
+	 */
+
+	/**
+	 * Getter function to expose object properties.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $property_name Property to access.
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return mixed Returns null if property does not exist, otherwise the value.
+	 */
+	public function get( $property_name, $transform = 'raw' ) {
+
+		// "content" is replaced by HTML or plain text depending on $content_type.
+		if ( $property_name === 'content' ) {
+			$property_name = 'content_' . $this->get_content_type();
+
+			if ( ! in_array( $property_name, array( 'content_html', 'content_plaintext', ), true ) ) {
+				$property_name = 'content_html';
+			}
+		}
+
+		if ( ! property_exists( $this, $property_name ) ) {
+			return null;
+		}
+
+
+		/**
+		 * Filters the value of the specified email property before transformation.
+		 *
+		 * This is a dynamic filter dependent on the specified key.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param mixed $property_value Property value.
+		 * @param string $property_name
+		 * @param string $transform How to transform the return value.
+		 *                          Accepts 'raw' (default) or 'replace-tokens'.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$retval = apply_filters( "bp_email_get_{$property_name}", $this->$property_name, $property_name, $transform, $this );
+
+		switch ( $transform ) {
+			// Special-case to fill the $template with the email $content.
+			case 'add-content':
+				$retval = str_replace( '{{{content}}}', nl2br( $this->get_content( 'replace-tokens' ) ), $retval );
+				// Fall through.
+
+			case 'replace-tokens':
+				$retval = self::replace_tokens( $retval, $this->get_tokens( 'raw' ) );
+				// Fall through.
+
+			case 'raw':
+			default:
+				// Do nothing.
+		}
+
+		/**
+		 * Filters the value of the specified email $property after transformation.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $retval Property value.
+		 * @param string $property_name
+		 * @param string $transform How to transform the return value.
+		 *                          Accepts 'raw' (default) or 'replace-tokens'.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		return apply_filters( 'bp_email_get_property', $retval, $property_name, $transform, $this );
+	}
+
+	/**
+	 * Get email headers.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string[] Associative pairing of email header name/value.
+	 */
+	public function get_headers( $transform = 'raw' ) {
+		return $this->get( 'headers', $transform );
+	}
+
+	/**
+	 * Get the email's "bcc" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient[] BCC recipients.
+	 */
+	public function get_bcc( $transform = 'raw' ) {
+		return $this->get( 'bcc', $transform );
+	}
+
+	/**
+	 * Get the email's "cc" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient[] CC recipients.
+	 */
+	public function get_cc( $transform = 'raw' ) {
+		return $this->get( 'cc', $transform );
+	}
+
+	/**
+	 * Get the email content.
+	 *
+	 * HTML or plaintext is returned, depending on the email's $content_type.
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string HTML or plaintext, depending on $content_type.
+	 */
+	public function get_content( $transform = 'raw' ) {
+		return $this->get( 'content', $transform );
+	}
+
+	/**
+	 * Get the email content (in HTML).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string HTML email content.
+	 */
+	public function get_content_html( $transform = 'raw' ) {
+		return $this->get( 'content_html', $transform );
+	}
+
+	/**
+	 * Get the email content (in plaintext).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Plain text email content.
+	 */
+	public function get_content_plaintext( $transform = 'raw' ) {
+		return $this->get( 'content_plaintext', $transform );
+	}
+
+	/**
+	 * Get the email content type (HTML or plain text) that the email will be sent in.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email content type ("html" or "plaintext").
+	 */
+	public function get_content_type( $transform = 'raw' ) {
+		return $this->get( 'content_type', $transform );
+	}
+
+	/**
+	 * Get the email's "from" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient "From" recipient.
+	 */
+	public function get_from( $transform = 'raw' ) {
+		return $this->get( 'from', $transform );
+	}
+
+	/**
+	 * Get the Post associated with the email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return WP_Post The post.
+	 */
+	public function get_post_object( $transform = 'raw' ) {
+		return $this->get( 'post_object', $transform );
+	}
+
+	/**
+	 * Get the email's "reply to" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient "Reply to" recipient.
+	 */
+	public function get_reply_to( $transform = 'raw' ) {
+		return $this->get( 'reply_to', $transform );
+	}
+
+	/**
+	 * Get the email subject.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email subject.
+	 */
+	public function get_subject( $transform = 'raw' ) {
+		return $this->get( 'subject', $transform );
+	}
+
+	/**
+	 * Get the email template (the HTML wrapper around the email content).
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string Email template. Assumed to be HTML.
+	 */
+	public function get_template( $transform = 'raw' ) {
+		return $this->get( 'template', $transform );
+	}
+
+	/**
+	 * Get the email's "to" address and name.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return BP_Email_Recipient "To" recipients.
+	 */
+	public function get_to( $transform = 'raw' ) {
+		return $this->get( 'to', $transform );
+	}
+
+	/**
+	 * Get token names and replacement values for this email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $transform Optional. How to transform the return value.
+	 *                          Accepts 'raw' (default) or 'replace-tokens'.
+	 * @return string[] Associative pairing of token name (key) and replacement value (value).
+	 */
+	public function get_tokens( $transform = 'raw' ) {
+		return $this->get( 'tokens', $transform );
+	}
+
+	/**
+	 * Set email headers.
+	 *
+	 * Does NOT let you override to/from, etc. Use the methods provided to set those.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string[] $headers Key/value pairs of header name/values (strings).
+	 * @return BP_Email
+	 */
+	public function set_headers( array $headers ) {
+		$new_headers = array();
+
+		foreach ( $headers as $name => $content ) {
+			$content = str_replace( ':', '', $content );
+			$name    = str_replace( ':', '', $name );
+
+			$new_headers[ sanitize_key( $name ) ] = sanitize_text_field( $content );
+		}
+
+		/**
+		 * Filters the new value of the email's "headers" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string[] $new_headers Key/value pairs of new header name/values (strings).
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->headers = apply_filters( 'bp_email_set_headers', $new_headers, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "bcc" address and name.
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $bcc_address Either a email address, user ID, WP_User object,
+	 *                                              or an array containg the address and name.
+	 * @param string $name Optional. If $bcc_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_bcc( $bcc_address, $name = '' ) {
+		$bcc = array( new BP_Email_Recipient( $bcc_address, $name ) );
+
+		/**
+		 * Filters the new value of the email's "BCC" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] $bcc BCC recipients.
+		 * @param string|array|int|WP_User $bcc_address Either a email address, user ID, WP_User object,
+		 *                                              or an array containg the address and name.
+		 * @param string $name Optional. If $bcc_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->bcc = apply_filters( 'bp_email_set_bcc', $bcc, $bcc_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "cc" address and name.
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $cc_address Either a email address, user ID, WP_User object,
+	 *                                             or an array containg the address and name.
+	 * @param string $name Optional. If $cc_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_cc( $cc_address, $name = '' ) {
+		$cc = array( new BP_Email_Recipient( $cc_address, $name ) );
+
+		/**
+		 * Filters the new value of the email's "CC" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] $cc CC recipients.
+		 * @param string|array|int|WP_User $cc_address Either a email address, user ID, WP_User object,
+		 *                                             or an array containg the address and name.
+		 * @param string $name Optional. If $cc_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->cc = apply_filters( 'bp_email_set_cc', $cc, $cc_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email content (HTML).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content HTML email content.
+	 * @return BP_Email
+	 */
+	public function set_content_html( $content ) {
+
+		/**
+		 * Filters the new value of the email's "content" property (HTML).
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content HTML email content.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_html = apply_filters( 'bp_email_set_content_html', $content, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email content (plain text).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content Plain text email content.
+	 * @return BP_Email
+	 */
+	public function set_content_plaintext( $content ) {
+
+		/**
+		 * Filters the new value of the email's "content" property (plain text).
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content Plain text email content.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_plaintext = apply_filters( 'bp_email_set_content_plaintext', $content, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the content type (HTML or plain text) to send the email in.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $content_type Email content type ("html" or "plaintext").
+	 * @return BP_Email
+	 */
+	public function set_content_type( $content_type ) {
+		if ( ! in_array( $content_type, array( 'html', 'plaintext', ), true ) ) {
+			$class        = get_class_vars( get_class() );
+			$content_type = $class['content_type'];
+		}
+
+		/**
+		 * Filters the new value of the email's "content type" property.
+		 *
+		 * The content type (HTML or plain text) to send the email in.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $content_type Email content type ("html" or "plaintext").
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->content_type = apply_filters( 'bp_email_set_content_type', $content_type, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "from" address and name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+	 *                                                or an array containg the address and name.
+	 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_from( $email_address, $name = '' ) {
+		$from = new BP_Email_Recipient( $email_address, $name );
+
+		/**
+		 * Filters the new value of the email's "from" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient $from Sender details.
+		 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+		 *                                                or an array containg the address and name.
+		 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->from = apply_filters( 'bp_email_set_from', $from, $email_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the Post object containing the email content template.
+	 *
+	 * Also sets the email's subject, content, and template from the Post, for convenience.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param WP_Post $post
+	 * @return BP_Email
+	 */
+	public function set_post_object( WP_Post $post ) {
+
+		/**
+		 * Filters the new value of the email's "post object" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param WP_Post $post A Post.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->post_object = apply_filters( 'bp_email_set_post_object', $post, $this );
+
+		if ( is_a( $this->post_object, 'WP_Post' ) ) {
+			$this->set_subject( $this->post_object->post_title )
+				->set_content_html( $this->post_object->post_content )
+				->set_content_plaintext( $this->post_object->post_excerpt );
+
+			ob_start();
+
+			// Load the template.
+			bp_locate_template( bp_email_get_template( $this->post_object ), true, false );
+ 			$this->template( ob_get_contents() );
+
+			ob_end_clean();
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "reply to" address and name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+	 *                                                or an array containg the address and name.
+	 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_reply_to( $email_address, $name = '' ) {
+		$reply_to = new BP_Email_Recipient( $email_address, $name );
+
+		/**
+		 * Filters the new value of the email's "reply to" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient $reply_to "Reply to" recipient.
+		 * @param string|array|int|WP_User $email_address Either a email address, user ID, WP_User object,
+		 *                                                or an array containg the address and name.
+		 * @param string $name Optional. If $email_address is a string, this is the recipient's name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->reply_to = apply_filters( 'bp_email_set_reply_to', $reply_to, $email_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email subject.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $subject Email subject.
+	 * @return BP_Email
+	 */
+	public function set_subject( $subject ) {
+
+		/**
+		 * Filters the new value of the subject email property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $subject Email subject.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->subject = apply_filters( 'bp_email_set_subject', $subject, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email template (the HTML wrapper around the email content).
+	 *
+	 * This needs to include the string "{{{content}}}" to have the post content added
+	 * when the email template is rendered.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $template Email template. Assumed to be HTML.
+	 * @return BP_Email
+	 */
+	public function set_template( $template ) {
+
+		/**
+		 * Filters the new value of the template email property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $template Email template. Assumed to be HTML.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->template = apply_filters( 'bp_email_set_template', $template, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set the email's "to" address and name.
+	 *
+	 * IMPORTANT NOTE: the assumption with all emails sent by (and belonging to) BuddyPress itself
+	 * is that there will only be a single `$to_address`. This is to simplify token and templating
+	 * logic (for example, if multiple recipients, the "unsubscribe" link in the emails will all
+	 * only link to the first recipient).
+	 *
+	 * To set a single address, the first parameter is the address and the second the name.
+	 * You can also pass a user ID or a WP_User object.
+	 *
+	 * To set multiple addresses, for each array item, the key is the email address and
+	 * the value is the name.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string|array|int|WP_User $to_address Either a email address, user ID, WP_User object,
+	 *                                             or an array containg the address and name.
+	 * @param string $name Optional. If $to_address is a string, this is the recipient's name.
+	 * @return BP_Email
+	 */
+	public function set_to( $to_address, $name = '' ) {
+		$to = array( new BP_Email_Recipient( $to_address, $name ) );
+
+		/**
+		 * Filters the new value of the email's "to" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param BP_Email_Recipient[] "To" recipients.
+		 * @param string $to_address "To" address.
+		 * @param string $name "To" name.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		$this->to = apply_filters( 'bp_email_set_to', $to, $to_address, $name, $this );
+
+		return $this;
+	}
+
+	/**
+	 * Set token names and replacement values for this email.
+	 *
+	 * In templates, tokens are inserted with a Handlebars-like syntax, e.g. `{{token_name}}`.
+	 * { and } are reserved characters. There's no need to specify these brackets in your token names.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string[] $tokens Associative array, contains key/value pairs of token name/value.
+	 *                         Values are a string or a callable function.
+	 * @return BP_Email
+	 */
+	public function set_tokens( array $tokens ) {
+		$formatted_tokens = array();
+
+		foreach ( $tokens as $name => $value ) {
+			$name                      = str_replace( array( '{', '}' ), '', sanitize_text_field( $name ) );
+			$formatted_tokens[ $name ] = $value;
+		}
+
+		/**
+		 * Filters the new value of the email's "tokens" property.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string[] $formatted_tokens Associative pairing of token names (key)
+		 *                                   and replacement values (value).
+		 * @param string[] $tokens           Associative pairing of unformatted token
+		 *                                   names (key) and replacement values (value).
+		 * @param BP_Email $this             Current instance of the email type class.
+		 */
+		$this->tokens = apply_filters( 'bp_email_set_tokens', $formatted_tokens, $tokens, $this );
+
+		return $this;
+	}
+
+
+	/*
+	 * Sanitisation and validation logic.
+	 */
+
+	/**
+	 * Check that we'd be able to send this email.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return bool|WP_Error Returns true if validation succesful, else a descriptive WP_Error.
+	 */
+	public function validate() {
+		$retval = true;
+
+		// BCC, CC, and token properties are optional.
+		if (
+			! $this->get( 'from' ) ||
+			! $this->get( 'to' ) ||
+			! $this->get( 'subject' ) ||
+			! $this->get( 'content' ) ||
+			! $this->get( 'template' )
+		) {
+			$retval = new WP_Error( 'missing_parameter', __CLASS__, $this );
+		}
+
+		/**
+		 * Filters whether the email passes basic validation checks.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param bool|WP_Error $retval Returns true if validation succesful, else a descriptive WP_Error.
+		 * @param BP_Email $this Current instance of the email type class.
+		 */
+		return apply_filters( 'bp_email_validate', $retval, $this );
+	}
+
+
+	/*
+	 * Utility functions.
+	 *
+	 * Unlike other methods in this class, utility functions are not chainable.
+	 */
+
+	/**
+	 * Replace all tokens in the input with appropriate values.
+	 *
+	 * Unlike most other methods in this class, this one is not chainable.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $text
+	 * @param array $tokens Token names and replacement values for the $text.
+	 * @return string
+	 */
+	public static function replace_tokens( $text, $tokens ) {
+		$unescaped = array();
+		$escaped   = array();
+
+		foreach ( $tokens as $token => $value ) {
+			if ( is_callable( $value ) ) {
+				$value = call_user_func( $value );
+			}
+
+			// Some tokens are objects or arrays for backwards compatibilty. See bp_core_deprecated_email_filters().
+			if ( ! is_scalar( $value ) ) {
+				continue;
+			}
+
+			$unescaped[ '{{{' . $token . '}}}' ] = $value;
+			$escaped[ '{{' . $token . '}}' ]     = esc_html( $value );
+		}
+
+		$text = strtr( $text, $unescaped );  // Do first.
+		$text = strtr( $text, $escaped );
+
+		/**
+		 * Filters text that has had tokens replaced.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param string $text
+		 * @param array $tokens Token names and replacement values for the $text.
+		 */
+		return apply_filters( 'bp_email_replace_tokens', $text, $tokens );
+	}
+}
Index: /trunk/src/bp-core/classes/class-bp-phpmailer.php
===================================================================
--- /trunk/src/bp-core/classes/class-bp-phpmailer.php	(revision 10470)
+++ /trunk/src/bp-core/classes/class-bp-phpmailer.php	(revision 10470)
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Email delivery implementation using PHPMailer.
+ *
+ * @since 2.5.0
+ */
+class BP_PHPMailer implements BP_Email_Delivery {
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 2.5.0
+	 */
+	public function __construct() {
+		global $phpmailer;
+
+		// We'll try to use the PHPMailer object that might have been created by WordPress.
+		if ( ! ( $phpmailer instanceof PHPMailer ) ) {
+			require_once ABSPATH . WPINC . '/class-phpmailer.php';
+			require_once ABSPATH . WPINC . '/class-smtp.php';
+			$phpmailer = new PHPMailer( true );
+		}
+	}
+
+	/**
+	 * Send email(s).
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool|WP_Error Returns true if email send, else a descriptive WP_Error.
+	 */
+	public function bp_email( BP_Email $email ) {
+		global $phpmailer;
+
+		/*
+		 * Resets.
+		 */
+
+		$phpmailer->clearAllRecipients();
+		$phpmailer->clearAttachments();
+		$phpmailer->clearCustomHeaders();
+		$phpmailer->clearReplyTos();
+		$phpmailer->Sender = '';
+
+
+		/*
+		 * Set up.
+		 */
+
+		$phpmailer->IsMail();
+		$phpmailer->CharSet  = bp_get_option( 'blog_charset' );
+		$phpmailer->Hostname = self::get_hostname();
+
+
+		/*
+		 * Content.
+		 */
+
+		$phpmailer->Subject = $email->get_subject( 'replace-tokens' );
+		$content_plaintext  = PHPMailer::normalizeBreaks( $email->get_content_plaintext( 'replace-tokens' ) );
+
+		if ( $email->get( 'content_type' ) === 'html' ) {
+			$phpmailer->msgHTML( $email->get_template( 'add-content' ), '', 'wp_strip_all_tags' );
+			$phpmailer->AltBody = $content_plaintext;
+
+		} else {
+			$phpmailer->IsHTML( false );
+			$phpmailer->Body = $content_plaintext;
+		}
+
+		$recipient = $email->get_from();
+		try {
+			$phpmailer->SetFrom( $recipient->get_address(), $recipient->get_name() );
+		} catch ( phpmailerException $e ) {
+		}
+
+		$recipient = $email->get_reply_to();
+		try {
+			$phpmailer->addReplyTo( $recipient->get_address(), $recipient->get_name() );
+		} catch ( phpmailerException $e ) {
+		}
+
+		$recipients = $email->get_to();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddAddress( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$recipients = $email->get_cc();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddCc( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$recipients = $email->get_bcc();
+		foreach ( $recipients as $recipient ) {
+			try {
+				$phpmailer->AddBcc( $recipient->get_address(), $recipient->get_name() );
+			} catch ( phpmailerException $e ) {
+			}
+		}
+
+		$headers = $email->get_headers();
+		foreach ( $headers as $name => $content ) {
+			$phpmailer->AddCustomHeader( $name, $content );
+		}
+
+
+		/**
+		 * Fires after PHPMailer is initialised.
+		 *
+		 * @since 2.5.0
+		 *
+		 * @param PHPMailer $phpmailer The PHPMailer instance.
+		 */
+		do_action( 'bp_phpmailer_init', $phpmailer );
+
+		try {
+			return $phpmailer->Send();
+		} catch ( phpmailerException $e ) {
+			return new WP_Error( $e->getCode(), $e->getMessage(), $email );
+		}
+	}
+
+
+	/*
+	 * Utility/helper functions.
+	 */
+
+	/**
+	 * Get an appropriate hostname for the email. Varies depending on site configuration.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	static public function get_hostname() {
+		if ( is_multisite() ) {
+			return get_current_site()->domain;  // From fix_phpmailer_messageid()
+		}
+
+		return preg_replace( '#^https?://#i', '', bp_get_option( 'home' ) );
+	}
+}
Index: /trunk/tests/phpunit/includes/install.php
===================================================================
--- /trunk/tests/phpunit/includes/install.php	(revision 10469)
+++ /trunk/tests/phpunit/includes/install.php	(revision 10470)
@@ -14,4 +14,5 @@
 require_once $config_file_path;
 require_once $tests_dir_path . '/includes/functions.php';
+require_once $tests_dir_path . '/includes/mock-mailer.php';
 
 function _load_buddypress() {
@@ -49,4 +50,9 @@
 }
 
+function _bp_mock_mailer( $class ) {
+	return 'BP_UnitTest_Mailer';
+}
+tests_add_filter( 'bp_send_email_delivery_class', '_bp_mock_mailer' );
+
 // Install BuddyPress
 bp_version_updater();
Index: /trunk/tests/phpunit/includes/loader.php
===================================================================
--- /trunk/tests/phpunit/includes/loader.php	(revision 10469)
+++ /trunk/tests/phpunit/includes/loader.php	(revision 10470)
@@ -8,2 +8,8 @@
 // Bootstrap BP
 require dirname( __FILE__ ) . '/../../../src/bp-loader.php';
+
+require_once( dirname( __FILE__ ) . '/mock-mailer.php' );
+function _bp_mock_mailer( $class ) {
+	return 'BP_UnitTest_Mailer';
+}
+tests_add_filter( 'bp_send_email_delivery_class', '_bp_mock_mailer' );
Index: /trunk/tests/phpunit/includes/mock-mailer.php
===================================================================
--- /trunk/tests/phpunit/includes/mock-mailer.php	(revision 10470)
+++ /trunk/tests/phpunit/includes/mock-mailer.php	(revision 10470)
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * Mock email delivery implementation.
+ *
+ * @since 2.5.0
+ */
+class BP_UnitTest_Mailer implements BP_Email_Delivery {
+
+	/**
+	 * Send email(s).
+	 *
+	 * @param BP_Email $email Email to send.
+	 * @return bool False if some error occurred.
+	 * @since 2.5.0
+	 */
+	public function bp_email( BP_Email $email ) {
+		return true;
+	}
+}
Index: /trunk/tests/phpunit/testcases/core/class-bp-email-recipient.php
===================================================================
--- /trunk/tests/phpunit/testcases/core/class-bp-email-recipient.php	(revision 10470)
+++ /trunk/tests/phpunit/testcases/core/class-bp-email-recipient.php	(revision 10470)
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @group core
+ * @group BP_Email_Recipient
+ */
+class BP_Email_Recipient_Tests extends BP_UnitTestCase {
+	protected $u1;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->u1 = $this->factory->user->create( array(
+			'display_name' => 'Unit Test',
+			'user_email'   => 'test@example.com',
+		) );
+	}
+
+	public function test_return_with_address_and_name() {
+		$email     = 'test@example.com';
+		$name      = 'Unit Test';
+		$recipient = new BP_Email_Recipient( $email, $name );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_array() {
+		$email     = 'test@example.com';
+		$name      = 'Unit Test';
+		$recipient = new BP_Email_Recipient( array( $email => $name ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertSame( $name, $recipient->get_name() );
+	}
+
+	public function test_return_with_user_id() {
+		$recipient = new BP_Email_Recipient( $this->u1 );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_return_with_wp_user_object() {
+		$recipient = new BP_Email_Recipient( get_user_by( 'id', $this->u1 ) );
+
+		$this->assertSame( 'test@example.com', $recipient->get_address() );
+		$this->assertSame( 'Unit Test', $recipient->get_name() );
+	}
+
+	public function test_return_with_address_and_optional_name() {
+		$email     = 'test@example.com';
+		$recipient = new BP_Email_Recipient( $email );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_return_with_array_and_optional_name() {
+		$email     = 'test@example.com';
+		$recipient = new BP_Email_Recipient( array( $email ) );
+
+		$this->assertSame( $email, $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_should_return_empty_string_if_user_id_id_invalid() {
+		$recipient = new BP_Email_Recipient( time() );
+
+		$this->assertEmpty( $recipient->get_address() );
+		$this->assertEmpty( $recipient->get_name() );
+	}
+
+	public function test_get_wp_user_object_from_email_address() {
+		$recipient = new BP_Email_Recipient( 'test@example.com' );
+		$recipient = $recipient->get_user( 'search-email' );
+
+		$this->assertSame( $this->u1, $recipient->ID );
+		$this->assertSame( 'test@example.com', $recipient->user_email );
+	}
+}
Index: /trunk/tests/phpunit/testcases/core/class-bp-email.php
===================================================================
--- /trunk/tests/phpunit/testcases/core/class-bp-email.php	(revision 10470)
+++ /trunk/tests/phpunit/testcases/core/class-bp-email.php	(revision 10470)
@@ -0,0 +1,158 @@
+<?php
+/**
+ * @group core
+ * @group BP_Email
+ */
+class BP_Tests_Email extends BP_UnitTestCase {
+	public function setUp() {
+		parent::setUp();
+		remove_filter( 'bp_email_get_headers', 'bp_email_set_default_headers', 6, 4 );
+		remove_filter( 'bp_email_get_tokens', 'bp_email_set_default_tokens', 6, 4 );
+	}
+
+	public function tearDown() {
+		add_filter( 'bp_email_get_tokens', 'bp_email_set_default_tokens', 6, 4 );
+		add_filter( 'bp_email_get_headers', 'bp_email_set_default_headers', 6, 4 );
+		parent::tearDown();
+	}
+
+	public function test_valid_subject() {
+		$message = 'test';
+		$email   = new BP_Email( 'fake_type' );
+
+		$email->set_subject( $message );
+		$this->assertSame( $message, $email->get_subject() );
+	}
+
+	public function test_valid_html_content() {
+		$message = '<b>test</b>';
+		$email   = new BP_Email( 'fake_type' );
+
+		$email->set_content_html( $message );
+		$email->set_content_type( 'html' );
+
+		$this->assertSame( $message, $email->get_content() );
+	}
+
+	public function test_valid_plaintext_content() {
+		$message = 'test';
+		$email   = new BP_Email( 'fake_type' );
+
+		$email->set_content_plaintext( $message );
+		$email->set_content_type( 'plaintext' );
+
+		$this->assertSame( $message, $email->get_content() );
+	}
+
+	public function test_valid_template() {
+		$message = 'test';
+		$email   = new BP_Email( 'fake_type' );
+
+		$email->set_template( $message );
+		$this->assertSame( $message, $email->get_template() );
+	}
+
+	public function test_tokens() {
+		$original = array( 'test1' => 'hello', 'test2' => 'world' );
+
+		$email = new BP_Email( 'fake_type' );
+		$email->set_tokens( $original );
+
+		$this->assertSame(
+			array( 'test1', 'test2' ),
+			array_keys( $email->get_tokens() )
+		);
+
+		$this->assertSame(
+			array( 'hello', 'world' ),
+			array_values( $email->get_tokens() )
+		);
+	}
+
+	public function test_headers() {
+		$email = new BP_Email( 'fake_type' );
+
+		$headers = array( 'custom_header' => 'custom_value' );
+		$email->set_headers( $headers );
+		$this->assertSame( $headers, $email->get_headers() );
+	}
+
+	public function test_validation() {
+		$email = new BP_Email( 'fake_type' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );
+		$email->set_content_html( 'testing' );
+
+		$this->assertTrue( $email->validate() );
+	}
+
+	public function test_invalid_characters_are_stripped_from_tokens() {
+		$email = new BP_Email( 'fake_type' );
+		$email->set_tokens( array( 'te{st}1' => 'hello world' ) );
+
+		$this->assertSame(
+			array( 'test1' ),
+			array_keys( $email->get_tokens() )
+		);
+	}
+
+	public function test_token_are_escaped() {
+		$token = '<blink>';
+		$email = new BP_Email( 'fake_type' );
+		$email->set_content_html( '{{test}}' )->set_tokens( array( 'test' => $token ) );
+
+		$this->assertSame(
+			esc_html( $token ),
+			$email->get_content( 'replace-tokens' )
+		);
+	}
+
+	public function test_token_are_not_escaped() {
+		$token = '<blink>';
+		$email = new BP_Email( 'fake_type' );
+		$email->set_content_html( '{{{test}}}' )->set_tokens( array( 'test' => $token ) );
+
+		$this->assertSame(
+			$token,
+			$email->get_content( 'replace-tokens' )
+		);
+	}
+
+	public function test_invalid_headers() {
+		$email = new BP_Email( 'fake_type' );
+
+		$headers = array( 'custom:header' => 'custom:value' );
+		$email->set_headers( $headers );
+		$this->assertNotSame( $headers, $email->get( 'headers' ) );
+		$this->assertSame( array( 'customheader' => 'customvalue' ), $email->get_headers() );
+	}
+
+	public function test_validation_with_missing_required_data() {
+		$email  = new BP_Email( 'fake_type' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );  // Content
+		$result = $email->validate();
+
+		$this->assertTrue( is_wp_error( $result ) );
+		$this->assertSame( 'missing_parameter', $result->get_error_code() );
+	}
+
+	public function test_validation_with_missing_template() {
+		$email  = new BP_Email( 'fake_type' );
+		$email->set_from( 'test1@example.com' )->set_to( 'test2@example.com' )->set_subject( 'testing' );
+		$email->set_content_html( 'testing' )->set_template( '' );
+		$result = $email->validate();
+
+		// Template has a default value, but it can't be blank.
+		$this->assertTrue( is_wp_error( $result ) );
+		$this->assertSame( 'missing_parameter', $result->get_error_code() );
+	}
+
+	public function test_invalid_tags_should_be_removed_from_html_content() {
+		$message = '<b>hello world</b><iframe src="https://example.com"></iframe><b>hello world</b>';
+		$email   = new BP_Email( 'fake_type' );
+
+		$email->set_content_html( $message );
+		$email->set_content_type( 'html' );
+
+		$this->assertSame( '<b>hello world</b><b>hello world</b>', $email->get_content() );
+	}
+}
Index: /trunk/tests/phpunit/testcases/core/functions.php
===================================================================
--- /trunk/tests/phpunit/testcases/core/functions.php	(revision 10469)
+++ /trunk/tests/phpunit/testcases/core/functions.php	(revision 10470)
@@ -708,3 +708,17 @@
 		$this->assertTrue( empty( $not_image ) );
 	}
+
+	public function test_emails_should_have_correct_link_color() {
+		$appearance = bp_email_get_appearance_settings();
+
+		$content    = '<a href="http://example.com">example</a>';
+		$link_color = 'style="color: ' . esc_attr( $appearance['highlight_color'] ) . ';';
+		$result     = bp_email_add_link_color_to_template( $content, 'template', 'add-content' );
+		$this->assertContains( $link_color, $result );
+
+		$content     = '<a href="http://example.com" style="display: block">example</a>';
+		$link_color .= 'display: block';
+		$result      = bp_email_add_link_color_to_template( $content, 'template', 'add-content' );
+		$this->assertContains( $link_color, $result );
+	}
 }
