| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| |
| # Copyright 2019 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Utilities to send email either through SMTP or SendGMR.""" |
| |
| |
| import base64 |
| import contextlib |
| import datetime |
| 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 getpass |
| import json |
| import os |
| import smtplib |
| import tempfile |
| |
| 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.""" |
| |
| class Attachment(object): |
| """Small class to keep track of attachment info.""" |
| |
| def __init__(self, name, content): |
| 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 ('detective', |
| '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, |
| text_to_send, |
| email_cc=None, |
| email_bcc=None, |
| email_from=None, |
| msg_type="plain", |
| attachments=None, |
| ): |
| """Choose appropriate email method and call it.""" |
| if os.path.exists("/usr/bin/sendgmr"): |
| self.SendGMREmail( |
| email_to, |
| subject, |
| text_to_send, |
| email_cc, |
| email_bcc, |
| email_from, |
| msg_type, |
| attachments, |
| ) |
| else: |
| self.SendSMTPEmail( |
| email_to, |
| subject, |
| text_to_send, |
| email_cc, |
| email_bcc, |
| email_from, |
| msg_type, |
| attachments, |
| ) |
| |
| def SendSMTPEmail( |
| self, |
| email_to, |
| subject, |
| text_to_send, |
| email_cc, |
| email_bcc, |
| email_from, |
| msg_type, |
| attachments, |
| ): |
| """Send email via standard smtp mail.""" |
| # Email summary to the current user. |
| msg = MIMEMultipart() |
| |
| if not email_from: |
| email_from = os.path.basename(__file__) |
| |
| msg["To"] = ",".join(email_to) |
| msg["Subject"] = subject |
| |
| if email_from: |
| msg["From"] = email_from |
| if email_cc: |
| msg["CC"] = ",".join(email_cc) |
| email_to += email_cc |
| if email_bcc: |
| msg["BCC"] = ",".join(email_bcc) |
| email_to += email_bcc |
| |
| msg.attach(MIMEText(text_to_send, msg_type)) |
| if attachments: |
| for attachment in attachments: |
| part = MIMEBase("application", "octet-stream") |
| part.set_payload(attachment.content) |
| Encoders.encode_base64(part) |
| part.add_header( |
| "Content-Disposition", |
| 'attachment; filename="%s"' % attachment.name, |
| ) |
| msg.attach(part) |
| |
| # Send the message via our own SMTP server, but don't include the |
| # envelope header. |
| s = smtplib.SMTP("localhost") |
| s.sendmail(email_from, email_to, msg.as_string()) |
| s.quit() |
| |
| def SendGMREmail( |
| self, |
| email_to, |
| subject, |
| text_to_send, |
| email_cc, |
| email_bcc, |
| email_from, |
| msg_type, |
| attachments, |
| ): |
| """Send email via sendgmr program.""" |
| ce = command_executer.GetCommandExecuter(log_level="none") |
| |
| if not email_from: |
| email_from = getpass.getuser() + "@google.com" |
| |
| to_list = ",".join(email_to) |
| |
| if not text_to_send: |
| text_to_send = "Empty message body." |
| |
| to_be_deleted = [] |
| try: |
| with tempfile.NamedTemporaryFile( |
| "w", encoding="utf-8", delete=False |
| ) as f: |
| f.write(text_to_send) |
| f.flush() |
| to_be_deleted.append(f.name) |
| |
| # Fix single-quotes inside the subject. In bash, to escape a single quote |
| # (e.g 'don't') you need to replace it with '\'' (e.g. 'don'\''t'). To |
| # make Python read the backslash as a backslash rather than an escape |
| # character, you need to double it. So... |
| subject = subject.replace("'", "'\\''") |
| |
| if msg_type == "html": |
| command = ( |
| "sendgmr --to='%s' --from='%s' --subject='%s' " |
| "--html_file='%s' --body_file=/dev/null" |
| % (to_list, email_from, subject, f.name) |
| ) |
| else: |
| command = ( |
| "sendgmr --to='%s' --from='%s' --subject='%s' " |
| "--body_file='%s'" % (to_list, email_from, subject, f.name) |
| ) |
| |
| if email_cc: |
| cc_list = ",".join(email_cc) |
| command += " --cc='%s'" % cc_list |
| if email_bcc: |
| bcc_list = ",".join(email_bcc) |
| command += " --bcc='%s'" % bcc_list |
| |
| if attachments: |
| attachment_files = [] |
| for attachment in attachments: |
| if "<html>" in attachment.content: |
| report_suffix = "_report.html" |
| else: |
| report_suffix = "_report.txt" |
| with tempfile.NamedTemporaryFile( |
| "w", |
| encoding="utf-8", |
| delete=False, |
| suffix=report_suffix, |
| ) as f: |
| f.write(attachment.content) |
| f.flush() |
| attachment_files.append(f.name) |
| files = ",".join(attachment_files) |
| command += " --attachment_files='%s'" % files |
| to_be_deleted += attachment_files |
| |
| # Send the message via our own GMR server. |
| status = ce.RunCommand(command) |
| return status |
| |
| finally: |
| for f in to_be_deleted: |
| os.remove(f) |