From f9755ca36d0ab9112f7d0cb39908931171b61ac6 Mon Sep 17 00:00:00 2001 From: Jan-Piet Mens Date: Tue, 11 Dec 2012 17:33:26 +0100 Subject: [PATCH] mail module: add MIME attachments, port and addresses with phrases Add HTML-escaping to code examples in rST tempate of module-formatter Add support for specifying port, addresses with phrases and attaching files Add support for custom headers and document version_added for new options X-Mailer header added :) protect empty address lists & attachment list, and add bcc --- hacking/templates/rst.j2 | 2 +- library/mail | 158 ++++++++++++++++++++++++++++++++------- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/hacking/templates/rst.j2 b/hacking/templates/rst.j2 index e83b4e9652d..a619f6a298a 100644 --- a/hacking/templates/rst.j2 +++ b/hacking/templates/rst.j2 @@ -48,7 +48,7 @@ {% if example['description'] %}

@{ example['description'] | html_ify }@

{% endif %}

-@{ example['code'] | indent(4, True) }@
+@{ example['code'] | escape | indent(4, True) }@
     

{% endfor %} diff --git a/library/mail b/library/mail index 52280804906..db8bf19d38c 100644 --- a/library/mail +++ b/library/mail @@ -40,24 +40,24 @@ version_added: "0.8" options: from: description: - - The email-address the mail is sent from. + - The email-address the mail is sent from. May contain address and phrase. default: root required: false to: description: - The email-address(es) the mail is being sent to. This is - a comma-separated list. + a comma-separated list, which may contain address and phrase portions. default: root required: false cc: description: - The email-address(es) the mail is being copied to. This is - a comma-separated list. + a comma-separated list, which may contain address and phrase portions. required: false bcc: description: - The email-address(es) the mail is being 'blind' copied to. This is - a comma-separated list. + a comma-separated list, which may contain address and phrase portions. required: false subject: description: @@ -69,61 +69,169 @@ options: - The body of the email being sent. default: $subject required: false + host: + description: + - The mail server + default: 'localhost' + required: false + port: + description: + - The mail server port + default: '25' + required: false + version_added: "1.0" + attach: + description: + - A space-separated list of pathnames of files to attach to the message. + Attached files will have their content-type set to C(application/octet-stream). + default: null + required: false + version_added: "1.0" + headers: + description: + - A vertical-bar-separated list of headers which should be added to the message. + Each individual header is specified as C(header=value) (see example below). + default: null + required: false + version_added: "1.0" examples: - description: "Example playbook sending mail to root" code: "local_action: mail msg='System ${ansible_hostname} has been sucessfully provisioned.'" + - description: Send e-mail to a bunch of users, attaching files + code: | + - local_action: mail + host='127.0.0.1' + port=2025 + subject="Ansible-report" + body="Hello, this is an e-mail. I hope you like it ;-)" + from="jane@example.net (Jane Jolie)" + to="John Doe , Suzie Something " + cc="Charlie Root " + attach="/etc/group /tmp/pavatar2.png" + headers=Reply-To=john@example.com|X-Special="Something or other" """ +import os +import sys import smtplib +try: + from email import encoders + import email.utils + from email.utils import parseaddr, formataddr + from email.mime.base import MIMEBase + from mail.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText +except ImportError: + from email import Encoders as encoders + import email.Utils + from email.Utils import parseaddr, formataddr + from email.MIMEBase import MIMEBase + from email.MIMEMultipart import MIMEMultipart + from email.MIMEText import MIMEText + def main(): module = AnsibleModule( argument_spec = dict( host = dict(default='localhost'), + port = dict(default='25'), sender = dict(default='root', aliases=['from']), to = dict(default='root', aliases=['recipients']), cc = dict(default=None), bcc = dict(default=None), subject = dict(required=True, aliases=['msg']), body = dict(default=None), + attach = dict(default=None), + headers = dict(default=None), ) ) host = module.params.get('host') + port = module.params.get('port') sender = module.params.get('sender') recipients = module.params.get('to') copies = module.params.get('cc') blindcopies = module.params.get('bcc') subject = module.params.get('subject') body = module.params.get('body') + attach_files = module.params.get('attach') + headers = module.params.get('headers') + + sender_phrase, sender_addr = parseaddr(sender) if not body: body = subject try: - smtp = smtplib.SMTP(host) + smtp = smtplib.SMTP(host, port=int(port)) + except Exception, e: + module.fail_json(rc=1, msg='Failed to send mail to server %s on port %s: %s' % (host, port, e)) + + + msg = MIMEMultipart() + msg['Subject'] = subject + msg['From'] = formataddr((sender_phrase, sender_addr)) + msg.preamble = "Multipart message" + + if headers is not None: + for hdr in [x.strip() for x in headers.split('|')]: + try: + h_key, h_val = hdr.split('=') + msg.add_header(h_key, h_val) + except: + pass + + if 'X-Mailer' not in msg: + msg.add_header('X-Mailer', "Ansible") + + to_list = [] + cc_list = [] + addr_list = [] + + if recipients is not None: + for addr in [x.strip() for x in recipients.split(',')]: + to_list.append( formataddr( parseaddr(addr)) ) + addr_list.append( parseaddr(addr)[1] ) # address only, w/o phrase + if copies is not None: + for addr in [x.strip() for x in copies.split(',')]: + cc_list.append( formataddr( parseaddr(addr)) ) + 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) + + part = MIMEText(body + "\n\n") + msg.attach(part) + + if attach_files is not None: + for file in attach_files.split(): + try: + fp = open(file, 'rb') + + part = MIMEBase('application', 'octet-stream') + part.set_payload(fp.read()) + fp.close() + + encoders.encode_base64(part) + + part.add_header('Content-disposition', 'attachment', filename=os.path.basename(file)) + msg.attach(part) + except Exception, e: + module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" % (file, e)) + sys.exit() + + composed = msg.as_string() + + try: + smtp.sendmail(sender_addr, set(addr_list), composed) except Exception, e: - module.fail_json(rc=1, msg='Failed to send mail to server %s: %s' % (host, e)) - - content = 'From: %s\n' % sender - content += 'To: %s\n' % recipients - if copies: - content += 'Cc: %s\n' % copies - content += 'Subject: %s\n\n' % subject - content += body - - addresses = recipients.split(',') - if copies: - addresses += copies.split(',') - if blindcopies: - addresses += blindcopies.split(',') - - for address in addresses: - try: - smtp.sendmail(sender, address, content) - except Exception, e: - module.fail_json(rc=1, msg='Failed to send mail to address %s: %s' % (address, e)) + module.fail_json(rc=1, msg='Failed to send mail to %s: %s' % (", ".join(addr_list), e)) smtp.quit()