Fix some SSL errors in mail.py causing SMTP Syntax Errors (Rebase of https://github.com/ansible/ansible-modules-extras/pull/708 ) (#19253)

* Rebase of https://github.com/ansible/ansible-modules-extras/pull/708

708 was full of extraneous merge commits interwoven with commits to
implement the feature. In the end the only way I could clean this up
in reasonable time was to just take a regular diff between the PR and
the base.  This lost the history of intermediate commits but I've
preserved attribution to @dayton967 via git's --author field.

Although I preserved the logic of the PR, there were a few additional
things that I cleaned up:

* Fixed import of email.mime.multipart
* Used the argspec to set port and timeout to integers instead of having
  ad hoc code inside of the module.
* Used argspec's choices for secure instead of ad hoc code inside of the
  module.
* Removed some unused variables
* Made secure_state a python boolean instead of using 0 and 1
* Used secure with string comparisons instead of turning it into an
  integer code.  This is much more readable.
* Fixed catching of SMTPExceptions (SMTPException wasn't imported
  directly so it needed to use the smtplib namespace.)
This commit is contained in:
Toshio Kuratomi 2016-12-13 04:28:24 -08:00 committed by GitHub
parent bdc4fa6b99
commit a8af6c6baf

View file

@ -22,7 +22,7 @@ ANSIBLE_METADATA = {'status': ['stableinterface'],
'supported_by': 'committer', 'supported_by': 'committer',
'version': '1.0'} 'version': '1.0'}
DOCUMENTATION = """ DOCUMENTATION = '''
--- ---
author: "Dag Wieers (@dagwieers)" author: "Dag Wieers (@dagwieers)"
module: mail module: mail
@ -91,8 +91,8 @@ options:
required: false required: false
port: port:
description: description:
- The mail server port - The mail server port. This must be a valid integer between 1 and 65534
default: '25' default: 25
required: false required: false
version_added: "1.0" version_added: "1.0"
attach: attach:
@ -120,7 +120,25 @@ options:
default: 'plain' default: 'plain'
required: false required: false
version_added: "2.0" version_added: "2.0"
""" secure:
description:
- If C(always), the connection will only send email if the connection is Encrypted.
If the server doesn't accept the encrypted connection it will fail.
- If C(try), the connection will attempt to setup a secure SSL/TLS session, before trying to send.
- 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 it is unable to do so it will fail.
choices: [ "always", "never", "try", "starttls"]
default: 'try'
required: false
version_added: "2.3"
timeout:
description:
- Sets the Timeout in seconds for connection attempts
default: 20
required: false
version_added: "2.3"
'''
EXAMPLES = ''' EXAMPLES = '''
# Example playbook sending mail to root # Example playbook sending mail to root
@ -160,28 +178,50 @@ EXAMPLES = '''
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
- mail:
host: localhost
port: 25
to: John Smith <john.smith@example.com>
subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.'
secure: always
# Sending an e-mail using StartTLS to the remote machine
- mail:
host: localhost
port: 25
to: John Smith <john.smith@example.com>
subject: Ansible-report
body: 'System {{ ansible_hostname }} has been successfully provisioned.'
secure: starttls
''' '''
import os import os
import sys
import smtplib import smtplib
import ssl import ssl
try: try:
# Python 2.6+
from email import encoders from email import encoders
import email.utils
from email.utils import parseaddr, formataddr from email.utils import parseaddr, formataddr
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from mail.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
except ImportError: except ImportError:
# Python 2.4 & 2.5
from email import Encoders as encoders from email import Encoders as encoders
import email.Utils
from email.Utils import parseaddr, formataddr from email.Utils import parseaddr, formataddr
from email.MIMEBase import MIMEBase from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText from email.MIMEText import MIMEText
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.pycompat24 import get_exception
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
@ -189,7 +229,7 @@ def main():
username = dict(default=None), username = dict(default=None),
password = dict(default=None, no_log=True), password = dict(default=None, no_log=True),
host = dict(default='localhost'), host = dict(default='localhost'),
port = dict(default='25'), port = dict(default=25, type='int'),
sender = dict(default='root', aliases=['from']), sender = dict(default='root', aliases=['from']),
to = dict(default='root', aliases=['recipients']), to = dict(default='root', aliases=['recipients']),
cc = dict(default=None), cc = dict(default=None),
@ -199,7 +239,9 @@ def main():
attach = dict(default=None), attach = dict(default=None),
headers = dict(default=None), headers = dict(default=None),
charset = dict(default='us-ascii'), charset = dict(default='us-ascii'),
subtype = dict(default='plain') subtype = dict(default='plain'),
secure = dict(default='try', choices=['always', 'never', 'try', 'starttls'], type='str'),
timeout = dict(default=20, type='int')
) )
) )
@ -217,28 +259,76 @@ def main():
headers = module.params.get('headers') headers = module.params.get('headers')
charset = module.params.get('charset') charset = module.params.get('charset')
subtype = module.params.get('subtype') subtype = module.params.get('subtype')
secure = module.params.get('secure')
timeout = module.params.get('timeout')
sender_phrase, sender_addr = parseaddr(sender) sender_phrase, sender_addr = parseaddr(sender)
secure_state = False
code = 0
auth_flag = ""
if not body: if not body:
body = subject body = subject
try: smtp = smtplib.SMTP(timeout=timeout)
try:
smtp = smtplib.SMTP_SSL(host, port=int(port))
except (smtplib.SMTPException, ssl.SSLError):
smtp = smtplib.SMTP(host, port=int(port))
except Exception:
e = get_exception()
module.fail_json(rc=1, msg='Failed to send mail to server %s on port %s: %s' % (host, port, e))
smtp.ehlo() if secure in ('never', 'try', 'starttls'):
if username and password:
if smtp.has_extn('STARTTLS'):
smtp.starttls()
try: try:
smtp.login(username, password) code, smtpmessage = smtp.connect(host, port=port)
except smtplib.SMTPAuthenticationError: except smtplib.SMTPException:
module.fail_json(msg="Authentication to %s:%s failed, please check your username and/or password" % (host, port)) e = get_exception()
if secure == 'try':
try:
smtp = smtplib.SMTP_SSL(timeout=timeout)
code, smtpmessage = smtp.connect(host, port=port)
secure_state = True
except ssl.SSLError:
e = get_exception()
module.fail_json(rc=1, msg='Unable to start an encrypted session to %s:%s: %s' % (host, port, e))
else:
module.fail_json(rc=1, msg='Unable to Connect to %s:%s: %s' % (host, port, e))
if (secure == 'always'):
try:
smtp = smtplib.SMTP_SSL(timeout=timeout)
code, smtpmessage = smtp.connect(host, port=port)
secure_state = True
except ssl.SSLError:
e = get_exception()
module.fail_json(rc=1, msg='Unable to start an encrypted session to %s:%s: %s' % (host, port, e))
if int(code) > 0:
try:
smtp.ehlo()
except smtplib.SMTPException:
e = get_exception()
module.fail_json(rc=1, msg='Helo failed for host %s:%s: %s' % (host, port, e))
auth_flag = smtp.has_extn('AUTH')
if secure in ('try', 'starttls'):
if smtp.has_extn('STARTTLS'):
try:
smtp.starttls()
smtp.ehlo()
secure_state = True
except smtplib.SMTPException:
e = get_exception()
module.fail_json(rc=1, msg='Unable to start an encrypted session to %s:%s: %s' % (host, port, e))
else:
if secure == 'starttls':
module.fail_json(rc=1, msg='StartTLS is not offered on server %s:%s' % (host, port))
if username and password:
if auth_flag:
try:
smtp.login(username, password)
except smtplib.SMTPAuthenticationError:
module.fail_json(rc=1, msg='Authentication to %s:%s failed, please check your username and/or password' % (host, port))
except smtplib.SMTPException:
module.fail_json(rc=1, msg='No Suitable authentication method was found on %s:%s' % (host, port))
else:
module.fail_json(rc=1, msg="No Authentication on the server at %s:%s" % (host, port))
msg = MIMEMultipart() msg = MIMEMultipart()
msg['Subject'] = subject msg['Subject'] = subject
@ -307,11 +397,11 @@ def main():
smtp.quit() smtp.quit()
module.exit_json(changed=False) if not secure_state and (username and password):
module.exit_json(changed=False, msg='Username and Password was sent without encryption')
else:
module.exit_json(changed=False)
# import module snippets
from ansible.module_utils.basic import *
from ansible.module_utils.pycompat24 import get_exception
if __name__ == '__main__': if __name__ == '__main__':
main() main()