Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TEmail subclass of TPage for rendering emails through TTemplate. sent via TEmailerModule #760

Open
belisoful opened this issue May 3, 2021 · 7 comments

Comments

@belisoful
Copy link
Member

One of the major functions that modern applications have is send emails. Emails are sent out when users register, forget their username or password, 2FA, news letters, etc.

Emailing is handled in each application separately. There should be a centralized place for sending (and possibly routing) app emails. the default behavior should be to php mail function.

`
/**
* This sends an email within the system. dySendEmail allows
* behaviors to divert this function to a more advanced email system
* @param string $from
* @param string[]|string $to
* @param string $subject
* @param string $contents
* @param array $options
*/
public function sendEmail($from,$to,$subject,$content,$options=null)
{
if (!is_array($options)) {
$options=array();
}

	if (empty($options['FROM']))
		$options['FROM'] = $from;
	if (empty($options['Return-Path']) && ini_get('sendmail_path'))
		$options['Return-Path'] = $from;
	if ($this->dySendEmail(false, $from,$to,$subject,$content,$options))
	{
		foreach ($options as $key => $value)
			$header .= $key . ':' . $value . '\r\n';
		if (is_array($to))
			$to=implode(',',$to);
		@mail($to,$subject,$content,$header);
	}
}

`

This can be the stub to start adding other features like multibyte character support. This function could probably be upgraded significantly. When dySendEmail is implemented by a behavior, to bypass sending email, return true from the dynamic event.

Should this go in TApplication? Should this be its own component? I don't see why or how this would or could become its own component, though I am open to suggestions. Such a simple function doesn't need anything complex.

Should phpMailer be incorporated to send emails? maybe phpMailer could be a separate TModule. I do have a separate Plugin Module for sending emails in a cron job and logging emails to a database with PHPMailer. I'm planning on making this emailer plugin available at some point. but it also would handler the above dynamic method.

@ctrlaltca
Copy link
Member

I usually use composer to include https://swiftmailer.symfony.com/.
I hardly see TApplication as the best place where to add email-related methods.

@belisoful
Copy link
Member Author

thank you for this.

@belisoful
Copy link
Member Author

I'll look into other platforms like drupel and wordpress on how they do email. I know WP has a central email function that relies on phpmailer. my memory isn't great about where that code lives in the architecture of the system.

@belisoful belisoful reopened this May 12, 2021
@belisoful belisoful changed the title Send Email method should be implemented somewhere, TApplication? e-Mailing method should be implemented somewhere like Yii and WP May 12, 2021
@belisoful belisoful changed the title e-Mailing method should be implemented somewhere like Yii and WP e-Mailing method should be implemented somewhere like Yii and WP, like a behavior on TApplication. May 12, 2021
@belisoful
Copy link
Member Author

belisoful commented May 18, 2021

I'm thinking of a TEmailerModule that does the initialization of SwiftMail.
TEmailerModule can use the DbCronModule to ensure that emails are sent. DbCronModule would schedule mailing onEndRequest and failing that (user presses stop on the browser), the next time cron runs.
SwiftMail would be required for PRADO

TEmailerModule could create the email body from TTemplates (or pages), incorporate images/files automagically, etc.

This could have its own directory in Prado\Util like Prado\Util\Email

A behavior can be made to append the emailer module to TApplication to act and have getters like Globalization or Request.

@belisoful belisoful changed the title e-Mailing method should be implemented somewhere like Yii and WP, like a behavior on TApplication. e-Mailing class/interfaces should be implemented somewhere like Yii and WP, like a behavior on TApplication. Jul 23, 2021
@belisoful
Copy link
Member Author

belisoful commented Dec 4, 2022

I have an idea. How about the Emailer function as a Behavior that attaches to Application? Example below.
TComponent needs a "hasMethod" method that checks enabled behaviors for their method. It would be like hasEvent.

A new TEmailTemplate class would capture published files and re-route them to TEmail. TEmail is a subclass of TPage (as the email body) but with To, From, Subject, CC, BCC, etc and without anything regarding the request and response. An Email would work just like a page, as a TTemplateControl (subclass). The Email file would be a "*.email" with the optional implementing "TEmail" php class of the same name.

The below code has not been run or tested but is provided for example and commentary at this point. Basically, it's an application wide email assist function. It does a basic validation on the subject for \n or \r. It accepts text, html text, or combined [$html, $text] array. It automatically checks the $message for or tags to trigger HTML style emails. It automagically sets headers that aren't set: To, From, Reply-To, Content-Type, and MIME-version. If the behavior encodes that all outgoing emails have a CC or BCC, they can be specified in the behavior. Lastly, HTML messages without text versions are stripped of HTML tags and html entities are converted to create a text based version of the HTML automatically.

The Most Important piece, after all that email automation, is the event to reroute the email to a different component in the system via the event OnSendEmail. The handlers can cancel sending email by returning something other than null. If the email is handled by a handler and cancels the default emailer, return false if it did not work and true if it did.

Lastly, the email html and attachments are post processed into an email body and sent via PHP mail function if not handled by the OnSendMail. This is a basic email processor. OnSendMail could reroute emails to a SwiftMail or phpMailer PRADO Module.

// Behavior attaches to TApplication.
class TEmailerBehavior extends \Prado\Util\TBehavior
{
	/** @var null|string Emails are sent from this address */
	protected $_defaultFromAddress;
	
	/** @var null|string Emails have this reply to email address when  */
	protected $_defaultReplyToAddress;
	
	/** @var null|string Emails all have this added to the Cc field  */
	protected $_defaultCcAddress;
	
	/** @var null|string Emails all have this added to the Bcc field  */
	protected $_defaultBccAddress;
	
	/**
	 * This method places no-cache meta in the head.
	 * @param array<string>|string The email To Address(es).
	 * @param string $subject Subject of the Email, no returns
	 * @param array<string>|string $message The string message or an array,
	 *  where [0] is the html string message and [1] is text message without html.
	 * @param array $headers Email Headers, arary keys are Header names and array values are
	 *  the Header Values.
	 * @param array $attachments File paths to the attachment, the data itself, or
	 * array('path'=>'/path/to/file/file.jpg','mime'=>'image/jpeg','data'=>$filedata, 'name'=>'filename.jpg').
	 * Keys are the new "cid" reference email [file] names or can be specified in the array.
	 * 'name' is required. 'data' and 'mime' is optional. either 'path' or 'data' must be provided.
	 * @param string $parameters parameters for the mail program/function.
	 * @param bool is this one of many emails, default false.
	 * @param mixed $to
	 * @param mixed $many
	 */
	public function sendMail($to, $subject, $message, $headers = [], $attachments = [], $parameters = "", $many = false)
	{
		if (strpos($subject, "\n") !== false || strpos($subject, "\r") !== false) {
			return false;
		}
		$isHTML = false;
		$html = null;
		if (is_array($message)) {
			if ($isHTML = count($message) > 1) {
				$html = $message[0];
				$message = $message[1];
			} else {
				$message = $message[0];
			}
		}
		if (!$isHTML && preg_match('/(<html\W*?>|<body\W*?>)/', $message)) {
			$isHTML = true;
			$html = $message;
			$message = null;
		}
		if ($isHTML && $message === null) {
			$message = $this->htmlToText($html);
		}
		$hasFrom = $hasReplyTo = $hasCc = $hasBcc = $hasDate = $hasMIMEVersion = $hasContentType = false;
		$ccHeader = 'Cc';
		$bccHeader = 'Bcc';
		$contentTypeHeader = 'Content-Type';
		foreach ($headers ?? [] as $header => $value) {
			$lowerHeader = strtolower($header);
			if ($lowerHeader == 'from') {
				$hasFrom = true;
			} elseif ($lowerHeader == 'reply-to') {
				$hasReplyTo = true;
			} elseif ($lowerHeader == 'cc') {
				$ccHeader = $header;
				$hasCc = true;
			} elseif ($lowerHeader == 'bcc') {
				$bccHeader = $header;
				$hasBcc = true;
			} elseif ($lowerHeader == 'date') {
				$hasDate = true;
			} elseif ($lowerHeader == 'mime-version') {
				$hasMIMEVersion = true;
			} elseif ($lowerHeader == 'content-type') {
				$contentTypeHeader = $header;
				$hasContentType = true;
			}
		}
		if (is_array($to)) {
			$to = implode(', ', $to);
		}
		//fancy up the headers if they aren't
		$from = $this->getDefaultFromAddress();
		if (!$hasFrom && $from) {
			$headers['From'] = $from;
		}
		if (!$hasReplyTo && (($replyTo = $this->getDefaultReplyToAddress()) || $from)) {
			$headers['Reply-To'] = $replyTo ?? $from;
		}
		if (!$hasContentType && $isHTML) {
			$headers[$contentTypeHeader] = 'text/html; charset=utf-8';
			$hasContentType = true;
		}
		if (!$hasDate) {
			$headers['Date'] = date('D, j M Y G:i:s O');
		}
		if ($hasContentType && !$hasMIMEVersion) {
			$headers['MIME-Version'] = '1.0';
		}
		if ($bcc = $this->getDefaultBccAddress()) {
			$headers[$bccHeader] = strlen($headers[$bccHeader]) ? $headers[$bccHeader] . ', ' . $bcc : $bcc;
		}
		if ($cc = $this->getDefaultCcAddress()) {
			$headers[$ccHeader] = strlen($headers[$ccHeader]) ? $headers[$ccHeader] . ', ' . $cc : $cc;
		}

		// TEventResults::EVENT_RESULT_FEED_FORWARD stops further handling of email by other handlers in case of more than one.
		//Filters, capture handlers should prioritize to less than default priority and return null or a replacement email array.
		$result = $this->onSendMail($to, $subject, $isHTML ? [$html, $message] : $message, $headers, $attachments, $parameters, $many);

		if (count($result = array_filter($result, function ($a) {
			return $a !== null && !is_array($a);
		}))) {
			return array_reduce($result, function ($a, $b) {
				return $a | $b;
			}, 0);
		}


		$headers['X-Mailer'] = 'PHP/' . phpversion() . ' PRADO/' . Prado::getVersion();

		$hasAttachments = count($attachments ?? []);
		$htmlAndText = $html && $message;
		$isMultipart = $hasAttachments || $htmlAndText;
		$crln = "\r\n";

		$maxColumns = 78;
		if ($isMultipart) {
			$boundary = md5(time());
			$boundaryPre = '--' . $boundary;
			$contentType = $headers[$contentTypeHeader];

			$headers[$contentTypeHeader] = 'multipart/mixed; boundary="' . $boundary . '"';
			$content = $boundaryPre . $crln . $crln;

			$contentBoundary = $boundary;
			$contentBoundaryPre = '--' . $contentBoundary;
			if ($hasAttachments && $htmlAndText) {
				$contentBoundary = 'Message-' . $boundary;
				$contentBoundaryPre = '--' . $contentBoundary;
				$content .= "Content-Type: multipart/alternative; boundary=\"{$contentBoundary}\"" . $crln . $crln;
				$content .= $contentBoundaryPre . $crln;
			}
			$debug = false;
			if ($message) {
				$content .= $contentTypeHeader . ": text/plain; charset=UTF-8" . $crln;
				$content .= $debug ? '' : 'Content-Transfer-Encoding: quoted-printable' . $crln;
				$content .= $crln;
				
				$content .= ($debug ? wordwrap($message, $maxColumns, "\n") : quoted_printable_encode($message)) . $crln . $crln;
			}
			if ($html) {
				$encodeHtml = false; //false to debug, normally true.
				if ($message) {
					$content .= $contentBoundaryPre . $crln . $crln;
				}
				$content .= $contentTypeHeader . ': ' . $contentType . $crln;
				$content .= 'MIME-Version: 1.0' . $crln;
				if (!$debug) {
					$content .= 'Content-Transfer-Encoding: base64' . $crln;
				}
				$content .= $crln;
				if (!$debug) {
					$content .= chunk_split(base64_encode($html));
				} else {
					$content .= wordwrap($html, $maxColumns, "\n");
				}
				$content .= $crln . $crln;
			}
			if ($contentBoundary != $boundary) {
				$content .= $contentBoundaryPre . '--' . $crln . $crln;
			}

			foreach ($attachments ?? [] as $attchmentName => $data) {
				$content .= $boundaryPre . $crln . $crln;
				$mime = null;
				if (is_array($data)) {
					$path = $data['path'] ?? null;
					$mime = $data['mime'] ?? null;
					$data = $data['data'] ?? null;
					$load = !$data;
					$attchmentName = $data['name'] ?? $attchmentName;
				} else {
					$path = $data;
					$load = true;
				}
				if ($load && file_exists($path)) {
					$mime = $mime ?? mime_content_type($path);
					$data = file_get_contents($path);
				}
				if (!$mime) {
					$mime = 'application/octet-stream';
				}

				$dataSize = strlen($data);

				$content .= "Content-Type: {$mime} name=\"{$attchmentName}\"" . $crln;
				$content .= "Content-Transfer-Encoding: base64" . $crln;
				$content .= 'MIME-Version: 1.0' . $crln;
				$content .= "Content-Disposition: attachment; filename=\"{$attchmentName}\"; size={$dataSize}" . $crln . $crln;
				$content .= chunk_split(base64_encode($data)) . $crln . $crln;
			}
			$content .= $boundaryPre . "--" . $crln . $crln;
		} else {
			$content = wordwrap(($message != null ? $message : null) ?? $html, $maxColumns, "\n");
		}
		
		return mail($to, $subject, $content, $headers, $parameters);
	}
	/** TODO: All the getters, setters, and events */
}

@belisoful
Copy link
Member Author

The vision I have is to use a TTemplate to generate an email body. What would come from this?

Here is my first pass:

I new "Email" directory would act similarly to the "Pages" directory. Emails are accessed via paths like a Page is accessed. eg "Registration.Welcome" in folder "Registration" and template "Welcome."

TEmail would have a ".email" extension and would act/subclass as a TPage, as in: having an implementation php class. TEmail should have all the standard Email properties, To, From, CC, BCC, Subject.
The ".email" template directives could set the email Subject. And emails can be localized like a TPage.

TEmailerBehavior would attach to the TApplication and act like a TService and provide Default From address, CC and BCC addresses for all emails. It provides SendEmail and CreateEmail behavior functions to TApplication. This behavior should have embedded configurations for setting a pages' [an emails'] default MasterClass, and such, like TPageService has for xml tags "pages" and "page", etc.

The TEMailerBehavior accepts an email similar to php mail. $message can be text, html, or an array of [$html, $text]. If only HTML is provided, the text version is derived from the html. Sending mail raises the "onSendMail" event to handle, log, filter, or do something with the email. If not handled, it renders the email and sends it by php mail function.

A simple module can be written for rerouting emails through OnSendMail handlers to swiftmail or phpmailer and cancel the default mail handler. all elements of the email (like attachments) are sent to the handler as well.

Email folder should have configuration files in the Email Folder as well, like Pages. But emails do not need authorization rules. So TPageConfiguration can be updated to parameterize the name of the "pages" and "page" and doing authorization rules. TEmailConfiguration can turn off authorization rules and change "pages" to "emails" and "page" to "email."

TEmailTemplate is a subclass of TTemplate that acts as a rerouter for publishing files to be either URLs, attached to the email, or embedded when specified. Default to URLs. so an TEmail acts like a TPage.

@belisoful belisoful changed the title e-Mailing class/interfaces should be implemented somewhere like Yii and WP, like a behavior on TApplication. TEmail subclass of TPage for rendering emails via TEmailTemplate. sent via TEmailerBehavior for TApplication Dec 9, 2022
@belisoful belisoful changed the title TEmail subclass of TPage for rendering emails via TEmailTemplate. sent via TEmailerBehavior for TApplication TEmail subclass of TPage for rendering emails through TEmailTemplate. sent via TEmailerBehavior for TApplication Dec 9, 2022
@belisoful
Copy link
Member Author

It's morphed from a complex behavior into a TEmailerModule that acts like a TPageService but without dependency upon Service, Request, and Response. Defaults to sending by php mail(). It cleans up the headers. The TEmailerBehavior for TApplication is 3 methods (createEmail, sendMail, and sendEmail) and a property (EmailerModule) and acts as a pass-through. Emails are configured like pages. For example, default "From" and "MasterClass" is set in configuration with a <emails /> element like <pages /> element. TEmail has a built in send method. It has custom replacement data as well.

I think a TPhpMailerBehavior for TEmailerModule should be included in the framework for a SMTP email solution. obviously, the user would have to require the email composer package to use the behavior. email behaviors would open up respective mail objects given configuration parameters for/upon sending the email. There is a parameter as to whether or not there are many emails and to retain the object and mail connection.

So far here are the new classes: TEmailConfiguration, TEmailerBehavior, TEmailerModule,, TEmailEventParameter, TEmail,, TEmailTemplate, TEmailPublishStyle.

Here are the edits to existing classes: TEmailLogRoute checks the Application for method "sendMail" and uses that before defaulting to php mail(). TPageConfiguration parameterizes processing authorization rules, "pages" and "page" tags. It also moves some functionality into methods for broader use. TTemplateManager needs to a $tplClass to be able to instance a sub-class of TTemplate and a [non-request based] $culture parameter.

@belisoful belisoful changed the title TEmail subclass of TPage for rendering emails through TEmailTemplate. sent via TEmailerBehavior for TApplication TEmail subclass of TPage for rendering emails through TTemplate. sent via TEmailerModule Dec 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants