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"
     }
 }