1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2012, Dag Wieers (@dagwieers) <dag@wieers.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['stableinterface'], 13 'supported_by': 'community'} 14 15DOCUMENTATION = r''' 16--- 17author: 18- Dag Wieers (@dagwieers) 19module: mail 20short_description: Send an email 21description: 22- This module is useful for sending emails from playbooks. 23- One may wonder why automate sending emails? In complex environments 24 there are from time to time processes that cannot be automated, either 25 because you lack the authority to make it so, or because not everyone 26 agrees to a common approach. 27- If you cannot automate a specific step, but the step is non-blocking, 28 sending out an email to the responsible party to make them perform their 29 part of the bargain is an elegant way to put the responsibility in 30 someone else's lap. 31- Of course sending out a mail can be equally useful as a way to notify 32 one or more people in a team that a specific action has been 33 (successfully) taken. 34version_added: '0.8' 35options: 36 from: 37 description: 38 - The email-address the mail is sent from. May contain address and phrase. 39 type: str 40 default: root 41 to: 42 description: 43 - The email-address(es) the mail is being sent to. 44 - This is a list, which may contain address and phrase portions. 45 type: list 46 default: root 47 aliases: [ recipients ] 48 cc: 49 description: 50 - The email-address(es) the mail is being copied to. 51 - This is a list, which may contain address and phrase portions. 52 type: list 53 bcc: 54 description: 55 - The email-address(es) the mail is being 'blind' copied to. 56 - This is a list, which may contain address and phrase portions. 57 type: list 58 subject: 59 description: 60 - The subject of the email being sent. 61 required: yes 62 type: str 63 body: 64 description: 65 - The body of the email being sent. 66 type: str 67 default: $subject 68 username: 69 description: 70 - If SMTP requires username. 71 type: str 72 version_added: '1.9' 73 password: 74 description: 75 - If SMTP requires password. 76 type: str 77 version_added: '1.9' 78 host: 79 description: 80 - The mail server. 81 type: str 82 default: localhost 83 port: 84 description: 85 - The mail server port. 86 - This must be a valid integer between 1 and 65534 87 type: int 88 default: 25 89 version_added: '1.0' 90 attach: 91 description: 92 - A list of pathnames of files to attach to the message. 93 - Attached files will have their content-type set to C(application/octet-stream). 94 type: list 95 default: [] 96 version_added: '1.0' 97 headers: 98 description: 99 - A list of headers which should be added to the message. 100 - Each individual header is specified as C(header=value) (see example below). 101 type: list 102 default: [] 103 version_added: '1.0' 104 charset: 105 description: 106 - The character set of email being sent. 107 type: str 108 default: utf-8 109 subtype: 110 description: 111 - The minor mime type, can be either C(plain) or C(html). 112 - The major type is always C(text). 113 type: str 114 choices: [ html, plain ] 115 default: plain 116 version_added: '2.0' 117 secure: 118 description: 119 - If C(always), the connection will only send email if the connection is Encrypted. 120 If the server doesn't accept the encrypted connection it will fail. 121 - If C(try), the connection will attempt to setup a secure SSL/TLS session, before trying to send. 122 - If C(never), the connection will not attempt to setup a secure SSL/TLS session, before sending 123 - If C(starttls), the connection will try to upgrade to a secure SSL/TLS connection, before sending. 124 If it is unable to do so it will fail. 125 type: str 126 choices: [ always, never, starttls, try ] 127 default: try 128 version_added: '2.3' 129 timeout: 130 description: 131 - Sets the timeout in seconds for connection attempts. 132 type: int 133 default: 20 134 version_added: '2.3' 135''' 136 137EXAMPLES = r''' 138- name: Example playbook sending mail to root 139 mail: 140 subject: System {{ ansible_hostname }} has been successfully provisioned. 141 delegate_to: localhost 142 143- name: Sending an e-mail using Gmail SMTP servers 144 mail: 145 host: smtp.gmail.com 146 port: 587 147 username: username@gmail.com 148 password: mysecret 149 to: John Smith <john.smith@example.com> 150 subject: Ansible-report 151 body: System {{ ansible_hostname }} has been successfully provisioned. 152 delegate_to: localhost 153 154- name: Send e-mail to a bunch of users, attaching files 155 mail: 156 host: 127.0.0.1 157 port: 2025 158 subject: Ansible-report 159 body: Hello, this is an e-mail. I hope you like it ;-) 160 from: jane@example.net (Jane Jolie) 161 to: 162 - John Doe <j.d@example.org> 163 - Suzie Something <sue@example.com> 164 cc: Charlie Root <root@localhost> 165 attach: 166 - /etc/group 167 - /tmp/avatar2.png 168 headers: 169 - Reply-To=john@example.com 170 - X-Special="Something or other" 171 charset: us-ascii 172 delegate_to: localhost 173 174- name: Sending an e-mail using the remote machine, not the Ansible controller node 175 mail: 176 host: localhost 177 port: 25 178 to: John Smith <john.smith@example.com> 179 subject: Ansible-report 180 body: System {{ ansible_hostname }} has been successfully provisioned. 181 182- name: Sending an e-mail using Legacy SSL to the remote machine 183 mail: 184 host: localhost 185 port: 25 186 to: John Smith <john.smith@example.com> 187 subject: Ansible-report 188 body: System {{ ansible_hostname }} has been successfully provisioned. 189 secure: always 190 191- name: Sending an e-mail using StartTLS to the remote machine 192 mail: 193 host: localhost 194 port: 25 195 to: John Smith <john.smith@example.com> 196 subject: Ansible-report 197 body: System {{ ansible_hostname }} has been successfully provisioned. 198 secure: starttls 199''' 200 201import os 202import smtplib 203import ssl 204import traceback 205from email import encoders 206from email.utils import parseaddr, formataddr, formatdate 207from email.mime.base import MIMEBase 208from email.mime.multipart import MIMEMultipart 209from email.mime.text import MIMEText 210from email.header import Header 211 212from ansible.module_utils.basic import AnsibleModule 213from ansible.module_utils.six import PY3 214from ansible.module_utils._text import to_native 215 216 217def main(): 218 219 module = AnsibleModule( 220 argument_spec=dict( 221 username=dict(type='str'), 222 password=dict(type='str', no_log=True), 223 host=dict(type='str', default='localhost'), 224 port=dict(type='int', default=25), 225 sender=dict(type='str', default='root', aliases=['from']), 226 to=dict(type='list', default=['root'], aliases=['recipients']), 227 cc=dict(type='list', default=[]), 228 bcc=dict(type='list', default=[]), 229 subject=dict(type='str', required=True, aliases=['msg']), 230 body=dict(type='str'), 231 attach=dict(type='list', default=[]), 232 headers=dict(type='list', default=[]), 233 charset=dict(type='str', default='utf-8'), 234 subtype=dict(type='str', default='plain', choices=['html', 'plain']), 235 secure=dict(type='str', default='try', choices=['always', 'never', 'starttls', 'try']), 236 timeout=dict(type='int', default=20), 237 ), 238 required_together=[['password', 'username']], 239 ) 240 241 username = module.params.get('username') 242 password = module.params.get('password') 243 host = module.params.get('host') 244 port = module.params.get('port') 245 sender = module.params.get('sender') 246 recipients = module.params.get('to') 247 copies = module.params.get('cc') 248 blindcopies = module.params.get('bcc') 249 subject = module.params.get('subject') 250 body = module.params.get('body') 251 attach_files = module.params.get('attach') 252 headers = module.params.get('headers') 253 charset = module.params.get('charset') 254 subtype = module.params.get('subtype') 255 secure = module.params.get('secure') 256 timeout = module.params.get('timeout') 257 258 code = 0 259 secure_state = False 260 sender_phrase, sender_addr = parseaddr(sender) 261 262 if not body: 263 body = subject 264 265 try: 266 if secure != 'never': 267 try: 268 if PY3: 269 smtp = smtplib.SMTP_SSL(host=host, port=port, timeout=timeout) 270 else: 271 smtp = smtplib.SMTP_SSL(timeout=timeout) 272 code, smtpmessage = smtp.connect(host, port) 273 secure_state = True 274 except ssl.SSLError as e: 275 if secure == 'always': 276 module.fail_json(rc=1, msg='Unable to start an encrypted session to %s:%s: %s' % 277 (host, port, to_native(e)), exception=traceback.format_exc()) 278 except Exception: 279 pass 280 281 if not secure_state: 282 if PY3: 283 smtp = smtplib.SMTP(host=host, port=port, timeout=timeout) 284 else: 285 smtp = smtplib.SMTP(timeout=timeout) 286 code, smtpmessage = smtp.connect(host, port) 287 288 except smtplib.SMTPException as e: 289 module.fail_json(rc=1, msg='Unable to Connect %s:%s: %s' % (host, port, to_native(e)), exception=traceback.format_exc()) 290 291 try: 292 smtp.ehlo() 293 except smtplib.SMTPException as e: 294 module.fail_json(rc=1, msg='Helo failed for host %s:%s: %s' % (host, port, to_native(e)), exception=traceback.format_exc()) 295 296 if int(code) > 0: 297 if not secure_state and secure in ('starttls', 'try'): 298 if smtp.has_extn('STARTTLS'): 299 try: 300 smtp.starttls() 301 secure_state = True 302 except smtplib.SMTPException as e: 303 module.fail_json(rc=1, msg='Unable to start an encrypted session to %s:%s: %s' % 304 (host, port, to_native(e)), exception=traceback.format_exc()) 305 try: 306 smtp.ehlo() 307 except smtplib.SMTPException as e: 308 module.fail_json(rc=1, msg='Helo failed for host %s:%s: %s' % (host, port, to_native(e)), exception=traceback.format_exc()) 309 else: 310 if secure == 'starttls': 311 module.fail_json(rc=1, msg='StartTLS is not offered on server %s:%s' % (host, port)) 312 313 if username and password: 314 if smtp.has_extn('AUTH'): 315 try: 316 smtp.login(username, password) 317 except smtplib.SMTPAuthenticationError: 318 module.fail_json(rc=1, msg='Authentication to %s:%s failed, please check your username and/or password' % (host, port)) 319 except smtplib.SMTPException: 320 module.fail_json(rc=1, msg='No Suitable authentication method was found on %s:%s' % (host, port)) 321 else: 322 module.fail_json(rc=1, msg="No Authentication on the server at %s:%s" % (host, port)) 323 324 if not secure_state and (username and password): 325 module.warn('Username and Password was sent without encryption') 326 327 msg = MIMEMultipart(_charset=charset) 328 msg['From'] = formataddr((sender_phrase, sender_addr)) 329 msg['Date'] = formatdate(localtime=True) 330 msg['Subject'] = Header(subject, charset) 331 msg.preamble = "Multipart message" 332 333 for header in headers: 334 # NOTE: Backward compatible with old syntax using '|' as delimiter 335 for hdr in [x.strip() for x in header.split('|')]: 336 try: 337 h_key, h_val = hdr.split('=') 338 h_val = to_native(Header(h_val, charset)) 339 msg.add_header(h_key, h_val) 340 except Exception: 341 module.warn("Skipping header '%s', unable to parse" % hdr) 342 343 if 'X-Mailer' not in msg: 344 msg.add_header('X-Mailer', 'Ansible mail module') 345 346 addr_list = [] 347 for addr in [x.strip() for x in blindcopies]: 348 addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase 349 350 to_list = [] 351 for addr in [x.strip() for x in recipients]: 352 to_list.append(formataddr(parseaddr(addr))) 353 addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase 354 msg['To'] = ", ".join(to_list) 355 356 cc_list = [] 357 for addr in [x.strip() for x in copies]: 358 cc_list.append(formataddr(parseaddr(addr))) 359 addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase 360 msg['Cc'] = ", ".join(cc_list) 361 362 part = MIMEText(body + "\n\n", _subtype=subtype, _charset=charset) 363 msg.attach(part) 364 365 # NOTE: Backware compatibility with old syntax using space as delimiter is not retained 366 # This breaks files with spaces in it :-( 367 for filename in attach_files: 368 try: 369 part = MIMEBase('application', 'octet-stream') 370 with open(filename, 'rb') as fp: 371 part.set_payload(fp.read()) 372 encoders.encode_base64(part) 373 part.add_header('Content-disposition', 'attachment', filename=os.path.basename(filename)) 374 msg.attach(part) 375 except Exception as e: 376 module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" % 377 (filename, to_native(e)), exception=traceback.format_exc()) 378 379 composed = msg.as_string() 380 381 try: 382 result = smtp.sendmail(sender_addr, set(addr_list), composed) 383 except Exception as e: 384 module.fail_json(rc=1, msg="Failed to send mail to '%s': %s" % 385 (", ".join(set(addr_list)), to_native(e)), exception=traceback.format_exc()) 386 387 smtp.quit() 388 389 if result: 390 for key in result: 391 module.warn("Failed to send mail to '%s': %s %s" % (key, result[key][0], result[key][1])) 392 module.exit_json(msg='Failed to send mail to at least one recipient', result=result) 393 394 module.exit_json(msg='Mail sent successfully', result=result) 395 396 397if __name__ == '__main__': 398 main() 399