You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
10 KiB
315 lines
10 KiB
#!/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)
|