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