Repository: privacyidea/privacyidea

Issue:
Mail content sent by email token is rendered as attachment
As [reported by icarroll](https://community.privacyidea.org/t/commas-and-newlines-in-otp-email/634/3?u=fredreichbier), mails sent by the email token functionality are rendered as attachments. This is only an issue on the current master.

The problem probably lies in the email MIME type, which is wrongly set to ``"text/text/html"`` because of the line [here](https://github.com/privacyidea/privacyidea/blob/6b4beecf12523e9da93880bd49b954b48a1d4c2b/privacyidea/lib/tokens/emailtoken.py#L408). This is because ``"text/html"`` is passed to the Python MIMEText class, which prepends ``text/`` to the passed string -- so we end up with ``text/text/html``.

If I change the string in the line referenced above to ``"html"``, the emails are correctly rendered as HTML mails.

However, do we actually want to send HTML-only mails by default? Or should we send a plain/html multipart email as [here](https://docs.python.org/2.7/library/email-examples.html#id4)?


Discussion:
The enhanced email-token-mail-send uses one template file.

I we use multipart, I think we would need to two template files - a plan and an html.

I think if someone uses the template, it is ok to have html?

Hm, no idea :) According to a [2011 Stackoverflow answer](https://stackoverflow.com/questions/5787185/is-it-sensible-to-send-html-only-email-these-days), HTML-only are not a good idea due to spam filters :/

Correct me if I'm mistaken, but to me it seems like we're also sending a text/html mail if the template feature is not used?
Yes. This line you mentioned sends the mail.
https://github.com/privacyidea/privacyidea/blob/6b4beecf12523e9da93880bd49b954b48a1d4c2b/privacyidea/lib/tokens/emailtoken.py#L408

No matter if it is a plain policy or if it is the template.


Resolving patch:
diff --git a/privacyidea/lib/tokens/emailtoken.py b/privacyidea/lib/tokens/emailtoken.py
--- a/privacyidea/lib/tokens/emailtoken.py
+++ b/privacyidea/lib/tokens/emailtoken.py
@@ -234,8 +234,8 @@ def create_challenge(self, transactionid=None, options=None):
             # Gateway error, since checkPIN is successful. A bail
             # out would cancel the checking of the other tokens
             try:
-                message_template = self._get_email_text_or_subject(options)
-                subject_template = self._get_email_text_or_subject(options,
+                message_template, mimetype = self._get_email_text_or_subject(options)
+                subject_template, _ = self._get_email_text_or_subject(options,
                                                                    EMAILACTION.EMAILSUBJECT,
                                                                    "Your OTP")
                 validity = int(get_from_config("email.validtime", 120))
@@ -251,7 +251,8 @@ def create_challenge(self, transactionid=None, options=None):
                 # We send the email after creating the challenge for testing.
                 success, sent_message = self._compose_email(
                     message=message_template,
-                    subject=subject_template)
+                    subject=subject_template,
+                    mimetype=mimetype)
 
             except Exception as e:
                 info = ("The PIN was correct, but the "
@@ -278,13 +279,14 @@ def check_otp(self, anOtpVal, counter=None, window=None, options=None):
         options = options or {}
         ret = HotpTokenClass.check_otp(self, anOtpVal, counter, window, options)
         if ret >= 0 and self._get_auto_email(options):
-            message = self._get_email_text_or_subject(options)
-            subject = self._get_email_text_or_subject(options,
+            message, mimetype = self._get_email_text_or_subject(options)
+            subject, _ = self._get_email_text_or_subject(options,
                                                       action=EMAILACTION.EMAILSUBJECT,
                                                       default="Your OTP")
             self.inc_otp_counter(ret, reset=False)
             success, message = self._compose_email(message=message,
-                                                subject=subject)
+                                                   subject=subject,
+                                                   mimetype=mimetype)
             log.debug("AutoEmail: send new SMS: {0!s}".format(success))
             log.debug("AutoEmail: {0!r}".format(message))
         return ret
@@ -301,10 +303,11 @@ def _get_email_text_or_subject(options,
         :type options: dict
         :param action: The action - either emailtext or emailsubject
         :param default: If no policy can be found, this is the default text
-        :return: Message template
-        :rtype: basestring
+        :return: Message template, MIME type (one of "plain", "html")
+        :rtype: (basestring, basestring)
         """
         message = default
+        mimetype = "plain"
         g = options.get("g")
         username = None
         realm = None
@@ -333,12 +336,13 @@ def _get_email_text_or_subject(options,
             try:
                 with open(message[5:], "r") as f:
                     message = f.read()
+                    mimetype = "html"
             except Exception as e:  # pragma: no cover
                 message = default
                 log.warning(u"Failed to read email template: {0!r}".format(e))
                 log.debug(u"{0!s}".format(traceback.format_exc()))
 
-        return message
+        return message, mimetype
 
     @staticmethod
     def _get_auto_email(options):
@@ -372,13 +376,15 @@ def _get_auto_email(options):
         return autosms
 
     @log_with(log)
-    def _compose_email(self, message="<otp>", subject="Your OTP"):
+    def _compose_email(self, message="<otp>", subject="Your OTP", mimetype="plain"):
         """
         send email
 
         :param message: the email submit message - could contain placeholders
             like <otp> or <serial>
         :type message: string
+        :param mimetype: the message MIME type - one of "plain", "html"
+        :type mimetype: basestring
 
         :return: submitted message
         :rtype: string
@@ -399,13 +405,13 @@ def _compose_email(self, message="<otp>", subject="Your OTP"):
 
         subject = subject.format(otp=otp, serial=serial)
 
-        log.debug("sending Email to {0!r} ".format(recipient))
+        log.debug("sending Email to {0!r}".format(recipient))
 
         identifier = get_from_config("email.identifier")
         if identifier:
             # New way to send email
             ret = send_email_identifier(identifier, recipient, subject, message,
-                                        mimetype="text/html")
+                                        mimetype=mimetype)
         else:
             # old way to send email / DEPRECATED
             mailserver = get_from_config("email.mailserver", "localhost")


Test patch:
diff --git a/tests/test_lib_tokens_email.py b/tests/test_lib_tokens_email.py
--- a/tests/test_lib_tokens_email.py
+++ b/tests/test_lib_tokens_email.py
@@ -357,6 +357,8 @@ def test_19_emailtext(self):
         display_message = c[1]
         self.assertTrue(c[3].get("state"), transactionid)
         self.assertEqual(display_message, "Enter the OTP from the Email:")
+        _, mimetype = token._get_email_text_or_subject(options, EMAILACTION.EMAILTEXT)
+        self.assertEqual(mimetype, "plain")
 
         # Test AUTOEMAIL
         p = set_policy(name="autoemail",
@@ -386,8 +388,9 @@ def test_19_emailtext(self):
         transactionid = "123456098714"
         db_token = Token.query.filter_by(serial=self.serial1).first()
         token = EmailTokenClass(db_token)
-        email_text = token._get_email_text_or_subject(options, EMAILACTION.EMAILTEXT)
+        email_text, mimetype = token._get_email_text_or_subject(options, EMAILACTION.EMAILTEXT)
         self.assertTrue("<p>Hello,</p>" in email_text)
+        self.assertEqual(mimetype, "html")
         c = token.create_challenge(transactionid, options=options)
         self.assertTrue(c[0], c)
         display_message = c[1]