1#!/usr/bin/python2 2# Copyright 2015 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7Mail the content of standard input. 8 9Example usage: 10 Use pipe: 11 $ echo "Some content" |./gmail_lib.py -s "subject" abc@bb.com xyz@gmail.com 12 13 Manually input: 14 $ ./gmail_lib.py -s "subject" abc@bb.com xyz@gmail.com 15 > Line 1 16 > Line 2 17 Ctrl-D to end standard input. 18""" 19from __future__ import absolute_import 20from __future__ import division 21from __future__ import print_function 22 23import argparse 24import base64 25import httplib2 26import logging 27import sys 28import os 29import random 30from email.mime.text import MIMEText 31 32import common 33from autotest_lib.client.bin import utils 34from autotest_lib.client.common_lib import global_config 35from autotest_lib.server import site_utils 36 37try: 38 from apiclient.discovery import build as apiclient_build 39 from apiclient import errors as apiclient_errors 40 from oauth2client import file as oauth_client_fileio 41except ImportError as e: 42 apiclient_build = None 43 logging.debug("API client for gmail disabled. %s", e) 44 45# Note: These imports needs to come after the apiclient imports, because 46# of a sys.path war between chromite and autotest crbug.com/622988 47from autotest_lib.server import utils as server_utils 48from chromite.lib import retry_util 49 50try: 51 from chromite.lib import metrics 52except ImportError: 53 metrics = utils.metrics_mock 54 55 56DEFAULT_CREDS_FILE = global_config.global_config.get_config_value( 57 'NOTIFICATIONS', 'gmail_api_credentials', default=None) 58RETRY_DELAY = 5 59RETRY_BACKOFF_FACTOR = 1.5 60MAX_RETRY = 10 61RETRIABLE_MSGS = [ 62 # User-rate limit exceeded 63 r'HttpError 429',] 64 65class GmailApiException(Exception): 66 """Exception raised in accessing Gmail API.""" 67 68 69class Message(): 70 """An email message.""" 71 72 def __init__(self, to, subject, message_text): 73 """Initialize a message. 74 75 @param to: The recievers saperated by comma. 76 e.g. 'abc@gmail.com,xyz@gmail.com' 77 @param subject: String, subject of the message 78 @param message_text: String, content of the message. 79 """ 80 self.to = to 81 self.subject = subject 82 self.message_text = message_text 83 84 85 def get_payload(self): 86 """Get the payload that can be sent to the Gmail API. 87 88 @return: A dictionary representing the message. 89 """ 90 message = MIMEText(self.message_text) 91 message['to'] = self.to 92 message['subject'] = self.subject 93 return {'raw': base64.urlsafe_b64encode(message.as_string())} 94 95 96class GmailApiClient(): 97 """Client that talks to Gmail API.""" 98 99 def __init__(self, oauth_credentials): 100 """Init Gmail API client 101 102 @param oauth_credentials: Path to the oauth credential token. 103 """ 104 if not apiclient_build: 105 raise GmailApiException('Cannot get apiclient library.') 106 107 storage = oauth_client_fileio.Storage(oauth_credentials) 108 credentials = storage.get() 109 if not credentials or credentials.invalid: 110 raise GmailApiException('Invalid credentials for Gmail API, ' 111 'could not send email.') 112 http = credentials.authorize(httplib2.Http()) 113 self._service = apiclient_build('gmail', 'v1', http=http) 114 115 116 def send_message(self, message, ignore_error=True): 117 """Send an email message. 118 119 @param message: Message to be sent. 120 @param ignore_error: If True, will ignore any HttpError. 121 """ 122 try: 123 # 'me' represents the default authorized user. 124 message = self._service.users().messages().send( 125 userId='me', body=message.get_payload()).execute() 126 logging.debug('Email sent: %s' , message['id']) 127 except apiclient_errors.HttpError as error: 128 if ignore_error: 129 logging.error('Failed to send email: %s', error) 130 else: 131 raise 132 133 134def send_email(to, subject, message_text, retry=True, creds_path=None): 135 """Send email. 136 137 @param to: The recipients, separated by comma. 138 @param subject: Subject of the email. 139 @param message_text: Text to send. 140 @param retry: If retry on retriable failures as defined in RETRIABLE_MSGS. 141 @param creds_path: The credential path for gmail account, if None, 142 will use DEFAULT_CREDS_FILE. 143 """ 144 auth_creds = server_utils.get_creds_abspath( 145 creds_path or DEFAULT_CREDS_FILE) 146 if not auth_creds or not os.path.isfile(auth_creds): 147 logging.error('Failed to send email to %s: Credential file does not ' 148 'exist: %s. If this is a prod server, puppet should ' 149 'install it. If you need to be able to send email, ' 150 'find the credential file from chromeos-admin repo and ' 151 'copy it to %s', to, auth_creds, auth_creds) 152 return 153 client = GmailApiClient(oauth_credentials=auth_creds) 154 m = Message(to, subject, message_text) 155 retry_count = MAX_RETRY if retry else 0 156 157 def _run(): 158 """Send the message.""" 159 client.send_message(m, ignore_error=False) 160 161 def handler(exc): 162 """Check if exc is an HttpError and is retriable. 163 164 @param exc: An exception. 165 166 @return: True if is an retriable HttpError. 167 """ 168 if not isinstance(exc, apiclient_errors.HttpError): 169 return False 170 171 error_msg = str(exc) 172 should_retry = any([msg in error_msg for msg in RETRIABLE_MSGS]) 173 if should_retry: 174 logging.warning('Will retry error %s', exc) 175 return should_retry 176 177 success = False 178 try: 179 retry_util.GenericRetry( 180 handler, retry_count, _run, sleep=RETRY_DELAY, 181 backoff_factor=RETRY_BACKOFF_FACTOR) 182 success = True 183 finally: 184 metrics.Counter('chromeos/autotest/send_email/count').increment( 185 fields={'success': success}) 186 187 188if __name__ == '__main__': 189 logging.basicConfig(level=logging.DEBUG) 190 parser = argparse.ArgumentParser( 191 description=__doc__, formatter_class=argparse.RawTextHelpFormatter) 192 parser.add_argument('-s', '--subject', type=str, dest='subject', 193 required=True, help='Subject of the mail') 194 parser.add_argument('-p', type=float, dest='probability', 195 required=False, default=0, 196 help='(optional) per-addressee probability ' 197 'with which to send email. If not specified ' 198 'all addressees will receive message.') 199 parser.add_argument('recipients', nargs='*', 200 help='Email addresses separated by space.') 201 args = parser.parse_args() 202 if not args.recipients or not args.subject: 203 print('Requires both recipients and subject.') 204 sys.exit(1) 205 206 message_text = sys.stdin.read() 207 208 if args.probability: 209 recipients = [] 210 for r in args.recipients: 211 if random.random() < args.probability: 212 recipients.append(r) 213 if recipients: 214 print('Randomly selected recipients %s' % recipients) 215 else: 216 print('Random filtering removed all recipients. Sending nothing.') 217 sys.exit(0) 218 else: 219 recipients = args.recipients 220 221 222 with site_utils.SetupTsMonGlobalState('gmail_lib', short_lived=True): 223 send_email(','.join(recipients), args.subject , message_text) 224