1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

mail: Fix charset encoding issue

This PR includes:
- An important fix to charset encoding of from address
- Documentation and examples cleanup
- PEP8 fixes
- Warning on insecure access
- Strict parameter typing
- More modern interface (using lists rather than comma, space or pipe-delimited strings)
- Warn on failure to send mail to some recipients
  ```
   [WARNING]: Failed to send mail to 'foobar': 550 5.1.1 <foobar>:
Recipient address rejected: User unknown in local recipient table
  ```
- Warn on failure to parse some headers
  ```
  [WARNING]: Skipping header 'Foobar', unable to parse
  ```
- Return failed recipients as return value
- Changed default encoding to utf-8
This commit is contained in:
Dag Wieers 2017-09-20 16:19:08 +02:00
parent 5220f30244
commit f5dded59c2
2 changed files with 158 additions and 170 deletions

View file

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012 Dag Wieers <dag@wieers.com> # Copyright: (c) 2012, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
@ -12,10 +12,10 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['stableinterface'], 'status': ['stableinterface'],
'supported_by': 'community'} 'supported_by': 'community'}
DOCUMENTATION = r'''
DOCUMENTATION = '''
--- ---
author: "Dag Wieers (@dagwieers)" author:
- Dag Wieers (@dagwieers)
module: mail module: mail
short_description: Send an email short_description: Send an email
description: description:
@ -31,29 +31,25 @@ description:
- Of course sending out a mail can be equally useful as a way to notify - Of course sending out a mail can be equally useful as a way to notify
one or more people in a team that a specific action has been one or more people in a team that a specific action has been
(successfully) taken. (successfully) taken.
version_added: "0.8" version_added: '0.8'
options: options:
from: from:
description: description:
- The email-address the mail is sent from. May contain address and phrase. - The email-address the mail is sent from. May contain address and phrase.
default: root default: root
required: false
to: to:
description: description:
- The email-address(es) the mail is being sent to. This is - The email-address(es) the mail is being sent to.
a comma-separated list, which may contain address and phrase portions. - This is a list, which may contain address and phrase portions.
default: root default: root
required: false
cc: cc:
description: description:
- The email-address(es) the mail is being copied to. This is - The email-address(es) the mail is being copied to.
a comma-separated list, which may contain address and phrase portions. - This is a list, which may contain address and phrase portions.
required: false
bcc: bcc:
description: description:
- The email-address(es) the mail is being 'blind' copied to. This is - The email-address(es) the mail is being 'blind' copied to.
a comma-separated list, which may contain address and phrase portions. - This is a list, which may contain address and phrase portions.
required: false
subject: subject:
description: description:
- The subject of the email being sent. - The subject of the email being sent.
@ -62,55 +58,47 @@ options:
description: description:
- The body of the email being sent. - The body of the email being sent.
default: $subject default: $subject
required: false
username: username:
description: description:
- If SMTP requires username - If SMTP requires username.
default: null version_added: '1.9'
required: false
version_added: "1.9"
password: password:
description: description:
- If SMTP requires password - If SMTP requires password.
default: null version_added: '1.9'
required: false
version_added: "1.9"
host: host:
description: description:
- The mail server - The mail server.
default: 'localhost' default: localhost
required: false
port: port:
description: description:
- The mail server port. This must be a valid integer between 1 and 65534 - The mail server port.
- This must be a valid integer between 1 and 65534
default: 25 default: 25
required: false version_added: '1.0'
version_added: "1.0"
attach: attach:
description: description:
- A space-separated list of pathnames of files to attach to the message. - A list of pathnames of files to attach to the message.
Attached files will have their content-type set to C(application/octet-stream). - Attached files will have their content-type set to C(application/octet-stream).
default: null default: []
required: false version_added: '1.0'
version_added: "1.0"
headers: headers:
description: description:
- A vertical-bar-separated list of headers which should be added to the message. - A list of headers which should be added to the message.
Each individual header is specified as C(header=value) (see example below). - Each individual header is specified as C(header=value) (see example below).
default: null default: []
required: false version_added: '1.0'
version_added: "1.0"
charset: charset:
description: description:
- The character set of email being sent - The character set of email being sent.
default: 'us-ascii' default: utf-8
required: false
subtype: subtype:
description: description:
- The minor mime type, can be either text or html. The major type is always text. - The minor mime type, can be either C(plain) or C(html).
default: 'plain' - The major type is always C(text).
required: false choices: [ html, plain ]
version_added: "2.0" default: plain
version_added: '2.0'
secure: secure:
description: description:
- If C(always), the connection will only send email if the connection is Encrypted. - If C(always), the connection will only send email if the connection is Encrypted.
@ -119,75 +107,78 @@ options:
- If C(never), the connection will not attempt to setup a secure SSL/TLS session, before sending - If C(never), the connection will not attempt to setup a secure SSL/TLS session, before sending
- If C(starttls), the connection will try to upgrade to a secure SSL/TLS connection, before sending. - If C(starttls), the connection will try to upgrade to a secure SSL/TLS connection, before sending.
If it is unable to do so it will fail. If it is unable to do so it will fail.
choices: [ "always", "never", "try", "starttls"] choices: [ always, never, starttls, try ]
default: 'try' default: try
required: false version_added: '2.3'
version_added: "2.3"
timeout: timeout:
description: description:
- Sets the Timeout in seconds for connection attempts - Sets the timeout in seconds for connection attempts.
default: 20 default: 20
required: false version_added: '2.3'
version_added: "2.3"
''' '''
EXAMPLES = ''' EXAMPLES = r'''
# Example playbook sending mail to root - name: Example playbook sending mail to root
- mail: mail:
subject: 'System {{ ansible_hostname }} has been successfully provisioned.' subject: System {{ ansible_hostname }} has been successfully provisioned.
delegate_to: localhost delegate_to: localhost
# Sending an e-mail using Gmail SMTP servers - name: Sending an e-mail using Gmail SMTP servers
- mail: mail:
host: smtp.gmail.com host: smtp.gmail.com
port: 587 port: 587
username: username@gmail.com username: username@gmail.com
password: mysecret password: mysecret
to: John Smith <john.smith@example.com> to: John Smith <john.smith@example.com>
subject: Ansible-report subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.' body: System {{ ansible_hostname }} has been successfully provisioned.
delegate_to: localhost delegate_to: localhost
# Send e-mail to a bunch of users, attaching files - name: Send e-mail to a bunch of users, attaching files
- mail: mail:
host: 127.0.0.1 host: 127.0.0.1
port: 2025 port: 2025
subject: Ansible-report subject: Ansible-report
body: Hello, this is an e-mail. I hope you like it ;-) body: Hello, this is an e-mail. I hope you like it ;-)
from: jane@example.net (Jane Jolie) from: jane@example.net (Jane Jolie)
to: John Doe <j.d@example.org>, Suzie Something <sue@example.com> to:
- John Doe <j.d@example.org>
- Suzie Something <sue@example.com>
cc: Charlie Root <root@localhost> cc: Charlie Root <root@localhost>
attach: /etc/group /tmp/pavatar2.png attach:
headers: 'Reply-To=john@example.com|X-Special="Something or other"' - /etc/group
charset: utf8 - /tmp/avatar2.png
headers:
- Reply-To=john@example.com
- X-Special="Something or other"
charset: us-ascii
delegate_to: localhost delegate_to: localhost
# Sending an e-mail using the remote machine, not the Ansible controller node - name: Sending an e-mail using the remote machine, not the Ansible controller node
- mail: mail:
host: localhost host: localhost
port: 25 port: 25
to: John Smith <john.smith@example.com> to: John Smith <john.smith@example.com>
subject: Ansible-report subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.' body: System {{ ansible_hostname }} has been successfully provisioned.
# Sending an e-mail using Legacy SSL to the remote machine - name: Sending an e-mail using Legacy SSL to the remote machine
- mail: mail:
host: localhost host: localhost
port: 25 port: 25
to: John Smith <john.smith@example.com> to: John Smith <john.smith@example.com>
subject: Ansible-report subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.' body: System {{ ansible_hostname }} has been successfully provisioned.
secure: always secure: always
# Sending an e-mail using StartTLS to the remote machine - name: Sending an e-mail using StartTLS to the remote machine
- mail: mail:
host: localhost host: localhost
port: 25 port: 25
to: John Smith <john.smith@example.com> to: John Smith <john.smith@example.com>
subject: Ansible-report subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.' body: System {{ ansible_hostname }} has been successfully provisioned.
secure: starttls secure: starttls
''' '''
import os import os
@ -209,23 +200,24 @@ def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
username = dict(default=None), username=dict(type='str'),
password = dict(default=None, no_log=True), password=dict(type='str', no_log=True),
host = dict(default='localhost'), host=dict(type='str', default='localhost'),
port = dict(default=25, type='int'), port=dict(type='int', default=25),
sender = dict(default='root', aliases=['from']), sender=dict(type='str', default='root', aliases=['from']),
to = dict(default='root', aliases=['recipients']), to=dict(type='list', default=['root'], aliases=['recipients']),
cc = dict(default=None), cc=dict(type='list', default=[]),
bcc = dict(default=None), bcc=dict(type='list', default=[]),
subject = dict(required=True, aliases=['msg']), subject=dict(type='str', required=True, aliases=['msg']),
body = dict(default=None), body=dict(type='str'),
attach = dict(default=None), attach=dict(type='list', default=[]),
headers = dict(default=None), headers=dict(type='list', default=[]),
charset = dict(default='us-ascii'), charset=dict(type='str', default='utf-8'),
subtype = dict(default='plain'), subtype=dict(type='str', default='plain', choices=['html', 'plain']),
secure = dict(default='try', choices=['always', 'never', 'try', 'starttls'], type='str'), secure=dict(type='str', default='try', choices=['always', 'never', 'starttls', 'try']),
timeout = dict(default=20, type='int') timeout=dict(type='int', default=20),
) ),
required_together=[['password', 'username']],
) )
username = module.params.get('username') username = module.params.get('username')
@ -244,16 +236,17 @@ def main():
subtype = module.params.get('subtype') subtype = module.params.get('subtype')
secure = module.params.get('secure') secure = module.params.get('secure')
timeout = module.params.get('timeout') timeout = module.params.get('timeout')
sender_phrase, sender_addr = parseaddr(sender)
secure_state = False
code = 0 code = 0
secure_state = False
sender_phrase, sender_addr = parseaddr(sender)
if not body: if not body:
body = subject body = subject
smtp = smtplib.SMTP(timeout=timeout) smtp = smtplib.SMTP(timeout=timeout)
if secure in ('never', 'try', 'starttls'): if secure in ('never', 'starttls', 'try'):
try: try:
code, smtpmessage = smtp.connect(host, port=port) code, smtpmessage = smtp.connect(host, port=port)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
@ -285,7 +278,7 @@ def main():
module.fail_json(rc=1, msg='Helo failed for host %s:%s: %s' % module.fail_json(rc=1, msg='Helo failed for host %s:%s: %s' %
(host, port, to_native(e)), exception=traceback.format_exc()) (host, port, to_native(e)), exception=traceback.format_exc())
if secure in ('try', 'starttls'): if secure in ('starttls', 'try'):
if smtp.has_extn('STARTTLS'): if smtp.has_extn('STARTTLS'):
try: try:
smtp.starttls() smtp.starttls()
@ -310,80 +303,76 @@ def main():
else: else:
module.fail_json(rc=1, msg="No Authentication on the server at %s:%s" % (host, port)) module.fail_json(rc=1, msg="No Authentication on the server at %s:%s" % (host, port))
msg = MIMEMultipart() if not secure_state and (username and password):
msg['Subject'] = Header(subject, charset) module.warn('Username and Password was sent without encryption')
msg['From'] = Header(formataddr((sender_phrase, sender_addr)), charset)
msg.preamble = "Multipart message"
msg.set_charset(charset)
if headers is not None: msg = MIMEMultipart(_charset=charset)
for hdr in [x.strip() for x in headers.split('|')]: msg['From'] = formataddr((sender_phrase, sender_addr))
msg['Subject'] = Header(subject, charset)
msg.preamble = "Multipart message"
for header in headers:
# NOTE: Backward compatible with old syntax using '|' as delimiter
for hdr in [x.strip() for x in header.split('|')]:
try: try:
h_key, h_val = hdr.split('=') h_key, h_val = hdr.split('=')
h_val = to_native(Header(h_val, charset)) h_val = to_native(Header(h_val, charset))
msg.add_header(h_key, h_val) msg.add_header(h_key, h_val)
except: except:
pass module.warn("Skipping header '%s', unable to parse" % hdr)
if 'X-Mailer' not in msg: if 'X-Mailer' not in msg:
msg.add_header('X-Mailer', "Ansible") msg.add_header('X-Mailer', 'Ansible mail module')
addr_list = []
for addr in [x.strip() for x in blindcopies]:
addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase
to_list = [] to_list = []
cc_list = [] for addr in [x.strip() for x in recipients]:
addr_list = []
if recipients is not None:
for addr in [x.strip() for x in recipients.split(',')]:
to_list.append(formataddr(parseaddr(addr))) to_list.append(formataddr(parseaddr(addr)))
addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase
if copies is not None: msg['To'] = ", ".join(to_list)
for addr in [x.strip() for x in copies.split(',')]:
cc_list = []
for addr in [x.strip() for x in copies]:
cc_list.append(formataddr(parseaddr(addr))) cc_list.append(formataddr(parseaddr(addr)))
addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase addr_list.append(parseaddr(addr)[1]) # address only, w/o phrase
if blindcopies is not None:
for addr in [x.strip() for x in blindcopies.split(',')]:
addr_list.append( parseaddr(addr)[1] )
if len(to_list) > 0:
msg['To'] = ", ".join(to_list)
if len(cc_list) > 0:
msg['Cc'] = ", ".join(cc_list) msg['Cc'] = ", ".join(cc_list)
part = MIMEText(body + "\n\n", _subtype=subtype, _charset=charset) part = MIMEText(body + "\n\n", _subtype=subtype, _charset=charset)
msg.attach(part) msg.attach(part)
if attach_files is not None: # NOTE: Backware compatibility with old syntax using space as delimiter is not retained
for file in attach_files.split(): # This breaks files with spaces in it :-(
for filename in attach_files:
try: try:
fp = open(file, 'rb')
part = MIMEBase('application', 'octet-stream') part = MIMEBase('application', 'octet-stream')
with open(filename, 'rb') as fp:
part.set_payload(fp.read()) part.set_payload(fp.read())
fp.close()
encoders.encode_base64(part) encoders.encode_base64(part)
part.add_header('Content-disposition', 'attachment', filename=os.path.basename(filename))
part.add_header('Content-disposition', 'attachment', filename=os.path.basename(file))
msg.attach(part) msg.attach(part)
except Exception as e: except Exception as e:
module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" % module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" %
(file, to_native(e)), exception=traceback.format_exc()) (filename, to_native(e)), exception=traceback.format_exc())
composed = msg.as_string() composed = msg.as_string()
try: try:
smtp.sendmail(sender_addr, set(addr_list), composed) result = smtp.sendmail(sender_addr, set(addr_list), composed)
except Exception as e: except Exception as e:
module.fail_json(rc=1, msg='Failed to send mail to %s: %s' % module.fail_json(rc=1, msg="Failed to send mail to '%s': %s" %
(", ".join(addr_list), to_native(e)), exception=traceback.format_exc()) (", ".join(set(addr_list)), to_native(e)), exception=traceback.format_exc())
smtp.quit() smtp.quit()
if not secure_state and (username and password): if result:
module.exit_json(changed=False, msg='Username and Password was sent without encryption') for key in result:
else: module.warn("Failed to send mail to '%s': %s %s" % (key, result[key][0], result[key][1]))
module.exit_json(changed=False) module.exit_json(msg='Failed to send mail to at least one recipient', result=result)
module.exit_json(msg='Mail sent successfully', result=result)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -344,7 +344,6 @@ lib/ansible/modules/notification/grove.py
lib/ansible/modules/notification/hall.py lib/ansible/modules/notification/hall.py
lib/ansible/modules/notification/irc.py lib/ansible/modules/notification/irc.py
lib/ansible/modules/notification/jabber.py lib/ansible/modules/notification/jabber.py
lib/ansible/modules/notification/mail.py
lib/ansible/modules/notification/mattermost.py lib/ansible/modules/notification/mattermost.py
lib/ansible/modules/notification/mqtt.py lib/ansible/modules/notification/mqtt.py
lib/ansible/modules/notification/osx_say.py lib/ansible/modules/notification/osx_say.py