Allow sending mail to external recipients.
authorChristian Herdtweck <christian.herdtweck@intra2net.com>
Thu, 14 Nov 2019 10:44:44 +0000 (11:44 +0100)
committerPlamen Dimitrov <pdimitrov@pevogam.com>
Tue, 19 Nov 2019 09:03:58 +0000 (11:03 +0200)
Email sending functions had always assumed they send to local recipients.
However, with a little modification functions can be used to send to
external recipients, too.

src/mail_validator.py

index aac4a2a..8c93af7 100644 (file)
@@ -156,6 +156,9 @@ class MailValidator():
         """
         Inject emails from `source_path` to `target_path`.
 
+        This uses the script *restore_mail_inject.pl* which injects the mails
+        using IMAP (as opposed to :py:meth:`inject_smtp`).
+
         :param str username: username for the mail injection script
         :param str original_user: original username for the mail injection
                                   script
@@ -175,27 +178,53 @@ class MailValidator():
         result = subprocess.check_output(cmd, shell=True)
         log.debug(result)
 
+    def _prepare_recipients(self, recipients):
+        """
+        Prepare recipient list: ensure list of proper addresses.
+
+        If given a simple string, make a list of strings out of it.
+        If any recipient is just a username, append "@" + localhost to it.
+        Also check that recipients are just email addresses.
+        """
+        hostname = socket.gethostname()
+        if isinstance(recipients, str):
+            recipients = [recipients, ]
+        result = []
+        for recipient in recipients:
+            if '@' in recipient:
+                result.append(recipient)
+            else:
+                result.append(recipient + '@' + hostname)
+            for bad_char in '<>"\'':
+                if bad_char in recipient:
+                    raise ValueError('Recipient must be a "raw" email address,'
+                                     ' not {!r}'.format(recipient))
+        return result
+
     def inject_smtp(self, usernames, emails):
         """
         Inject emails from `source_path` using python's SMTP library.
 
-        :param usernames: usernames of the localhost receivers for each email
-        :type usernames: [str]
-        :param emails: emails to be sent to each user
+        As opposed to :py:meth:`inject_emails`, this actually sends the mail
+        to the local mail server (meaning filtering, archiving, ... will
+        happen).
+
+        :param usernames: username(s) of the localhost receiver(s) for each
+                          email or proper email address(es)
+        :type usernames: str or [str]
+        :param emails: paths to files including full emails (header + body)
+                       to be sent to each user
         :type emails: [str]
         """
-        usernames_string = ",".join(usernames)
-        log.info("Sending emails to %s", usernames_string)
+        recipients = self._prepare_recipients(usernames)
+        log.info("Sending emails to %s", ','.join(recipients))
         with smtplib.SMTP('localhost') as server:
-            hostname = socket.gethostname()
-            users = [username + "@" + hostname for username in usernames]
-
             for email in emails:
                 log.info("Sending email %s", email)
                 with open(os.path.join(self.source_path, email), 'rb') \
                         as file_handle:
                     email_content = file_handle.read()
-                server.sendmail(self.smtp_sender, users, email_content)
+                server.sendmail(self.smtp_sender, recipients, email_content)
 
         # Wait till SMTP queue is processed
         arnied_wrapper.wait_for_email_transfer()
@@ -386,15 +415,17 @@ class MailValidator():
             log.info("Message content '%s' in %s is valid!",
                      content_type, email_path)
 
-    def send_email_with_files(self, username, file_list,
+    def send_email_with_files(self, usernames, file_list,
                               wait_for_transfer=True,
                               autotest_signature=None,
                               subject="my subject"):
         """
-        Send a generated email with attachments.
+        Send a generated email with optional attachments.
 
-        :param str username: username of a localhost receiver of the email
-        :param file_list: files attached to an email
+        :param usernames: username(s) of the localhost receiver(s) or proper
+                          email address(es)
+        :type usernames: str or [str]
+        :param file_list: files attached to an email; can be empty
         :type file_list: [str]
         :param wait_for_transfer: specify whether to wait until arnied_wrapper
                                   confirms email transfer; you can also specify
@@ -407,8 +438,7 @@ class MailValidator():
         """
         text = 'This is an autogenerated email.\n'
 
-        hostname = socket.gethostname()
-        user = username + "@" + hostname
+        recipients = self._prepare_recipients(usernames)
 
         if file_list:   # empty or None or so
             msg = MIMEMultipart()     # pylint: disable=redefined-variable-type
@@ -416,7 +446,7 @@ class MailValidator():
         else:
             msg = MIMEText(text, _charset='utf-8')  # pylint: disable=redefined-variable-type
         msg['From'] = self.smtp_sender
-        msg['To'] = user
+        msg['To'] = ', '.join(recipients)
         msg['Subject'] = subject
         msg['Date'] = formatdate(localtime=True)
         msg.preamble = 'This is a multi-part message in MIME format.\n'
@@ -470,9 +500,10 @@ class MailValidator():
         log.debug("Message successfully created")
         # send via SMTP
 
-        log.debug("Sending message from %s to %s" % (self.smtp_sender, user))
+        log.debug("Sending message from %s to %s"
+                  % (self.smtp_sender, ', '.join(recipients)))
         with smtplib.SMTP('localhost') as server:
-            server.sendmail(self.smtp_sender, user, msg.as_string())
+            server.sendmail(self.smtp_sender, recipients, msg.as_string())
 
         # wait for transfer; complicated by isinstance(False, int) == True
         if wait_for_transfer is False: