diff --git a/.gitignore b/.gitignore deleted file mode 100644 index dcb5ce1ec603d783cc0a96825a4e5073cec2f63f..0000000000000000000000000000000000000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -vendor -composer.lock -nbproject -logs -config.php -test diff --git a/classes/MailBodyBuilder.php b/classes/MailBodyBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..6c8baba044c186ba8d93ae8bd1f8dd4402411ff8 --- /dev/null +++ b/classes/MailBodyBuilder.php @@ -0,0 +1,160 @@ +<?php + +namespace RAP; + +/** + * This class is used to build an e-mail message body both in HTML and in its + * equivalent plaintext. + */ +class MailBodyBuilder { + + private $htmlBody; + private $textBody; + private $openedHTMLTags; + private $editable; + + function __construct() { + $this->htmlBody = ""; + $this->textBody = ""; + $this->openedHTMLTags = []; + $this->editable = true; + } + + private function checkEditable() { + if (!$this->editable) { + throw new \Exception("You cannot edit the body after it has been generated"); + } + } + + public function addText($text) { + $this->checkEditable(); + $this->htmlBody .= $text; + $this->textBody .= $text; + return $this; + } + + public function addLineBreak() { + $this->checkEditable(); + $this->htmlBody .= "<br>\n"; + $this->textBody .= "\n"; + return $this; + } + + public function addHr() { + $this->checkEditable(); + $this->htmlBody .= "<hr/>\n"; + $this->textBody .= "\n---------------------\n"; + return $this; + } + + public function addLinkWithDescription($url, $text) { + $this->checkEditable(); + $this->htmlBody .= '<a href="' . $url . '" target="blank_">' . $text . "</a>\n"; + $this->textBody .= $text . " ( " . $url . " ) "; + return $this; + } + + public function addLink($url) { + $this->checkEditable(); + $this->htmlBody .= '<a href="' . $url . '" target="blank_">' . $url . "</a>\n"; + $this->textBody .= $url . " "; + return $this; + } + + public function addEmailAddress($email, $description) { + $this->checkEditable(); + $this->htmlBody .= '<a href="mailto:' . $email . '">' . $description . '</a>'; + $this->textBody .= $description . " (" . $email . ")"; + return $this; + } + + private function openHTMLTag($tag, $equivalentPlainText) { + $this->checkEditable(); + if (in_array($tag, $this->openedHTMLTags)) { + throw new \Exception("You are already inside a " . $tag . " tag!"); + } + $this->openedHTMLTags[] = $tag; + $this->htmlBody .= "<" . $tag . ">"; + $this->textBody .= $equivalentPlainText; + return $this; + } + + private function closeHTMLTag($tag, $equivalentPlainText) { + $this->checkEditable(); + if ($this->openedHTMLTags[count($this->openedHTMLTags) - 1] !== $tag) { + throw new \Exception("You are not inside a " . $tag . " tag!"); + } + array_pop($this->openedHTMLTags); + $this->htmlBody .= "</" . $tag . ">"; + $this->textBody .= $equivalentPlainText; + return $this; + } + + public function startBold() { + return $this->openHTMLTag("strong", "*"); + } + + public function endBold() { + return $this->closeHTMLTag("strong", "*"); + } + + public function startParagraph() { + return $this->openHTMLTag("p", ""); + } + + public function endParagraph() { + return $this->closeHTMLTag("p", "\n"); + } + + public function startList() { + return $this->openHTMLTag("ul", "\n"); + } + + public function startListItem() { + return $this->openHTMLTag("li", " * "); + } + + public function endListItem() { + return $this->closeHTMLTag("li", "\n"); + } + + public function endList() { + return $this->closeHTMLTag("ul", "\n"); + } + + private function checkEnd() { + if (count($this->openedHTMLTags) > 0) { + $unclosedTags = ""; + foreach ($this->openedHTMLTags as $tag) { + $unclosedTags .= $tag . " "; + } + throw new \Exception("You must close all tags before generating email body! Unclosed tags: " . $unclosedTags); + } + } + + private function setCompatibleLineBreaks($value) { + return str_replace("\n", "\n\r", $value); + } + + private function finalizeBodyIfNecessary() { + if ($this->editable) { + $this->checkEnd(); + + $this->htmlBody = $this->setCompatibleLineBreaks($this->htmlBody); + $this->textBody = $this->setCompatibleLineBreaks($this->textBody); + + $this->editable = false; + } + } + + public function getTextPlainBody() { + $this->finalizeBodyIfNecessary(); + return $this->textBody; + } + + public function getHTMLBody() { + $this->finalizeBodyIfNecessary(); + return $this->htmlBody; + } + +} diff --git a/classes/MailSender.php b/classes/MailSender.php index deca8f7bf27fe4efef888a98cf1fe278a0fdc6ed..5e8c7d434eb48ab9d44bdbbd37a284054d2d2c37 100644 --- a/classes/MailSender.php +++ b/classes/MailSender.php @@ -24,6 +24,8 @@ namespace RAP; +use \PHPMailer\PHPMailer\PHPMailer; + /** * Manage mail sending. * Currently used only for join email messages. @@ -32,10 +34,20 @@ class MailSender { private $serverName; private $basePath; + private $mbb; public function __construct($serverName, $basePath) { $this->serverName = $serverName; $this->basePath = $basePath; + $this->mbb = new MailBodyBuilder(); + } + + private function addDescriptionItem($key, $value) { + $this->mbb->startBold() + ->addText($key) + ->endBold() + ->addText(": " . $value) + ->addLineBreak(); } /** @@ -47,53 +59,86 @@ class MailSender { */ public function sendJoinEmail(User $recipientUser, User $applicantUser, $token) { - $subject = "IA2 RAP: Join request"; - - $header = "From: noreply@" . $this->serverName . "\r\n"; - $header .= "Content-Type: text/html; charset=UTF-8"; + global $auditLog; $confirmJoinURL = $this->basePath . '/confirm-join?token=' . $token; - $body = "Dear IA2 user,<br/><br/>"; - $body .= "the following user requested to join your accounts on the " - . "<a href=\"https://sso.ia2.inaf.it/rap-ia2/\" target=\"blank_\">RAP facility</a>:<br/><br/>"; + $this->mbb->startParagraph() + ->addText("Dear IA2 user,") + ->addLineBreak() + ->addText("the following user requested to join your accounts on the ") + ->addLinkWithDescription("https://sso.ia2.inaf.it/rap-ia2/", "RAP facility") + ->addText(":") + ->endParagraph(); foreach ($applicantUser->identities as $identity) { - $body .= "<b>Type</b>: " . $identity->type . "<br/>"; - + $this->addDescriptionItem("Type", $identity->type); if ($identity->name !== null) { - $body .= "<b>Name</b>: " . $identity->name . "<br/>"; + $this->addDescriptionItem("Name", $identity->name); } - if ($identity->surname !== null) { - $body .= "<b>Surname</b>: " . $identity->surname . "<br/>"; + $this->addDescriptionItem("Surname", $identity->surname); } - - $body .= "<b>E-mail</b>: " . $identity->email . "<br/>"; - + $this->addDescriptionItem("E-mail", $identity->email); if ($identity->eppn !== null) { - $body .= "<b>Eppn</b>: " . $identity->eppn . "<br/>"; + $this->addDescriptionItem("Eppn", $identity->eppn); } - if ($identity->institution !== null) { - $body .= "<b>Institution</b>: " . $identity->institution . "<br/>"; + $this->addDescriptionItem("Institution", $identity->institution); } - $body .= "<br/>"; + $this->mbb->addLineBreak(); } - $body .= "<br/>If you and this user are <b>the same person</b> click on the following link for joining your accounts:<br/>"; - $body .= "<a href=\"$confirmJoinURL\" target=\"blank_\">$confirmJoinURL</a>"; - $body .= "<br/><br/>Otherwise you can ignore this email.<br/>"; - - $body .= '<p><b>Please don\'t use this functionality for sharing resources between your coworkers</b>, use <a href="https://sso.ia2.inaf.it/grouper">Grouper</a> for that.</p>'; - $body .= '<br/>'; - - $body .= "<b>*** This is an automatically generated email, please do not reply to this message ***</b><br/>"; - $body .= "If you need information please contact <a href=\"mailto:ia2@oats.inaf.it\">IA2 Staff</a>"; - - mail($recipientUser->getPrimaryEmail(), $subject, $body, $header); + $this->mbb->startParagraph() + ->addText("If you and this user are ") + ->startBold() + ->addText("the same person") + ->endBold() + ->addText(" click on the following link for joining your accounts: ") + ->addLink($confirmJoinURL) + ->addLineBreak() + ->addText("Otherwise you can ignore this email.") + ->endParagraph() + // + ->startParagraph() + ->startBold() + ->addText("Please don't use this functionality for sharing resources between your coworkers") + ->endBold() + ->addText(", use ") + ->addLinkWithDescription("https://sso.ia2.inaf.it/grouper", "Grouper") + ->addText(" for that.") + ->endParagraph() + // + ->addLineBreak() + ->startBold() + ->addText("*** This is an automatically generated email, please do not reply to this message ***") + ->endBold() + ->addLineBreak() + ->addText("If you need information please contact ") + ->addEmailAddress("ia2@oats.inaf.it", "IA2 Staff"); + + $mail = new PHPMailer(true); // Passing `true` enables exceptions + try { + + $toAddress = $recipientUser->getPrimaryEmail(); + + $mail->isSMTP(); + $mail->Port = 25; + $mail->setFrom("noreply@" . $this->serverName, 'IA2 SSO'); + $mail->addAddress($toAddress); + $mail->CharSet = 'utf-8'; + $mail->Subject = "IA2 RAP: Join request"; + $mail->Body = $this->mbb->getHTMLBody(); + $mail->AltBody = $this->mbb->getTextPlainBody(); + + $auditLog->info("JOIN email. Sending to " . $toAddress); + $mail->send(); + } catch (\Exception $ex) { + error_log($ex->getMessage()); + throw $ex; + } } } diff --git a/composer.json b/composer.json index 173d5a2f52e13c6602ca36ea1cc4285cf320cea3..a49af190f691b89e0b02da0048ed489b96ba7a15 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "mikecao/flight": "1.3.2", "google/apiclient": "2.1.3", "facebook/graph-sdk": "^5.5", - "monolog/monolog": "^1.22" + "monolog/monolog": "^1.22", + "phpmailer/phpmailer": "^6.0" } }