aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Burgess IV <gbiv@google.com>2020-01-30 13:56:41 -0800
committerGeorge Burgess <gbiv@chromium.org>2020-02-06 21:18:57 +0000
commit00156eac766dda8e8b2e942c1bf17db7f6060ec0 (patch)
treee72e5776071f383bd62f40fce531faa921beef90
parentd89779ddd3f7224a1706ae4aa1ab71436e4d2b42 (diff)
downloadtoolchain-utils-00156eac766dda8e8b2e942c1bf17db7f6060ec0.tar.gz
cros_utils: add x20 email sending functionality
This CL lets us easily queue up an email for our email sending bits. BUG=b:148609329 TEST=Unittests + ran SendX20Email. It made an email that seemed valid. Change-Id: I2a2b5133fe7b154222ea93cf7121bee420db3dac Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2032224 Tested-by: George Burgess <gbiv@chromium.org> Reviewed-by: Tiancong Wang <tcwang@google.com>
-rwxr-xr-xcros_utils/email_sender.py113
-rwxr-xr-xcros_utils/email_sender_unittest.py120
2 files changed, 229 insertions, 4 deletions
diff --git a/cros_utils/email_sender.py b/cros_utils/email_sender.py
index 0019982e..6b8893ea 100755
--- a/cros_utils/email_sender.py
+++ b/cros_utils/email_sender.py
@@ -9,17 +9,35 @@
from __future__ import print_function
-from email import encoders as Encoders
-from email.mime.base import MIMEBase
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
+import base64
+import contextlib
+import datetime
import getpass
+import json
import os
import smtplib
import tempfile
+from email import encoders as Encoders
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
from cros_utils import command_executer
+X20_PATH = '/google/data/rw/teams/c-compiler-chrome/prod_emails'
+
+
+@contextlib.contextmanager
+def AtomicallyWriteFile(file_path):
+ temp_path = file_path + '.in_progress'
+ try:
+ with open(temp_path, 'w') as f:
+ yield f
+ os.rename(temp_path, file_path)
+ except:
+ os.remove(temp_path)
+ raise
+
class EmailSender(object):
"""Utility class to send email through SMTP or SendGMR."""
@@ -31,6 +49,93 @@ class EmailSender(object):
self.name = name
self.content = content
+ def SendX20Email(self,
+ subject,
+ identifier,
+ well_known_recipients=(),
+ direct_recipients=(),
+ text_body=None,
+ html_body=None):
+ """Enqueues an email in our x20 outbox.
+
+ These emails ultimately get sent by the machinery in
+ //depot/google3/googleclient/chrome/chromeos_toolchain/mailer/mail.go. This
+ kind of sending is intended for accounts that don't have smtp or gmr access
+ (e.g., role accounts), but can be used by anyone with x20 access.
+
+ All emails are sent from `mdb.c-compiler-chrome+${identifier}@google.com`.
+
+ Args:
+ subject: email subject. Must be nonempty.
+ identifier: email identifier, or the text that lands after the `+` in the
+ "From" email address. Must be nonempty.
+ well_known_recipients: a list of well-known recipients for the email.
+ These are translated into addresses by our mailer.
+ Current potential values for this are ('sheriff',
+ 'cwp-team', 'cros-team', 'mage'). Either this or
+ direct_recipients must be a nonempty list.
+ direct_recipients: @google.com emails to send addresses to. Either this
+ or well_known_recipients must be a nonempty list.
+ text_body: a 'text/plain' email body to send. Either this or html_body
+ must be a nonempty string. Both may be specified
+ html_body: a 'text/html' email body to send. Either this or text_body
+ must be a nonempty string. Both may be specified
+ """
+ # `str`s act a lot like tuples/lists. Ensure that we're not accidentally
+ # iterating over one of those (or anything else that's sketchy, for that
+ # matter).
+ if not isinstance(well_known_recipients, (tuple, list)):
+ raise ValueError('`well_known_recipients` is unexpectedly a %s' %
+ type(well_known_recipients))
+
+ if not isinstance(direct_recipients, (tuple, list)):
+ raise ValueError(
+ '`direct_recipients` is unexpectedly a %s' % type(direct_recipients))
+
+ if not subject or not identifier:
+ raise ValueError('both `subject` and `identifier` must be nonempty')
+
+ if not (well_known_recipients or direct_recipients):
+ raise ValueError('either `well_known_recipients` or `direct_recipients` '
+ 'must be specified')
+
+ for recipient in direct_recipients:
+ if not recipient.endswith('@google.com'):
+ raise ValueError('All recipients must end with @google.com')
+
+ if not (text_body or html_body):
+ raise ValueError('either `text_body` or `html_body` must be specified')
+
+ email_json = {
+ 'email_identifier': identifier,
+ 'subject': subject,
+ }
+
+ if well_known_recipients:
+ email_json['well_known_recipients'] = well_known_recipients
+
+ if direct_recipients:
+ email_json['direct_recipients'] = direct_recipients
+
+ if text_body:
+ email_json['body'] = text_body
+
+ if html_body:
+ email_json['html_body'] = html_body
+
+ # The name of this has two parts:
+ # - An easily sortable time, to provide uniqueness and let our emailer
+ # send things in the order they were put into the outbox.
+ # - 64 bits of entropy, so two racing email sends don't clobber the same
+ # file.
+ now = datetime.datetime.utcnow().isoformat('T', 'seconds') + 'Z'
+ entropy = base64.urlsafe_b64encode(os.getrandom(8))
+ entropy_str = entropy.rstrip(b'=').decode('utf-8')
+ result_path = os.path.join(X20_PATH, now + '_' + entropy_str + '.json')
+
+ with AtomicallyWriteFile(result_path) as f:
+ json.dump(email_json, f)
+
def SendEmail(self,
email_to,
subject,
diff --git a/cros_utils/email_sender_unittest.py b/cros_utils/email_sender_unittest.py
new file mode 100755
index 00000000..73492196
--- /dev/null
+++ b/cros_utils/email_sender_unittest.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Tests for email_sender."""
+
+from __future__ import print_function
+
+import contextlib
+import io
+import json
+import unittest
+import unittest.mock as mock
+
+import cros_utils.email_sender as email_sender
+
+
+class Test(unittest.TestCase):
+ """Tests for email_sender."""
+
+ @mock.patch('cros_utils.email_sender.AtomicallyWriteFile')
+ def test_x20_email_sending_rejects_invalid_inputs(self, write_file):
+ test_cases = [
+ {
+ # no subject
+ 'subject': '',
+ 'identifier': 'foo',
+ 'direct_recipients': ['gbiv@google.com'],
+ 'text_body': 'hi',
+ },
+ {
+ 'subject': 'foo',
+ # no identifier
+ 'identifier': '',
+ 'direct_recipients': ['gbiv@google.com'],
+ 'text_body': 'hi',
+ },
+ {
+ 'subject': 'foo',
+ 'identifier': 'foo',
+ # no recipients
+ 'direct_recipients': [],
+ 'text_body': 'hi',
+ },
+ {
+ 'subject': 'foo',
+ 'identifier': 'foo',
+ 'direct_recipients': ['gbiv@google.com'],
+ # no body
+ },
+ {
+ 'subject': 'foo',
+ 'identifier': 'foo',
+ # direct recipients lack @google.
+ 'direct_recipients': ['gbiv'],
+ 'text_body': 'hi',
+ },
+ {
+ 'subject': 'foo',
+ 'identifier': 'foo',
+ # non-list recipients
+ 'direct_recipients': 'gbiv@google.com',
+ 'text_body': 'hi',
+ },
+ {
+ 'subject': 'foo',
+ 'identifier': 'foo',
+ # non-list recipients
+ 'well_known_recipients': 'sheriff',
+ 'text_body': 'hi',
+ },
+ ]
+
+ sender = email_sender.EmailSender()
+ for case in test_cases:
+ with self.assertRaises(ValueError):
+ sender.SendX20Email(**case)
+
+ write_file.assert_not_called()
+
+ @mock.patch('cros_utils.email_sender.AtomicallyWriteFile')
+ def test_x20_email_sending_translates_to_reasonable_json(self, write_file):
+ written_obj = None
+
+ @contextlib.contextmanager
+ def actual_write_file(file_path):
+ nonlocal written_obj
+
+ self.assertTrue(
+ file_path.startswith(email_sender.X20_PATH + '/'), file_path)
+ f = io.StringIO()
+ yield f
+ written_obj = json.loads(f.getvalue())
+
+ write_file.side_effect = actual_write_file
+ email_sender.EmailSender().SendX20Email(
+ subject='hello',
+ identifier='world',
+ well_known_recipients=['sheriff'],
+ direct_recipients=['gbiv@google.com'],
+ text_body='text',
+ html_body='html',
+ )
+
+ self.assertEqual(
+ written_obj, {
+ 'subject': 'hello',
+ 'email_identifier': 'world',
+ 'well_known_recipients': ['sheriff'],
+ 'direct_recipients': ['gbiv@google.com'],
+ 'body': 'text',
+ 'html_body': 'html',
+ })
+
+
+if __name__ == '__main__':
+ unittest.main()