import ast
from builtins import range
import copy
import csv
from functools import cmp_to_key
import json
import logging
import mimetypes
import os
import re
import socket
import sys
import subprocess
import sendemail_common
if sys.version_info >= (3, 0):
from io import (BytesIO, TextIOWrapper)
import urllib.parse as urllib
else:
from cStringIO import StringIO
BytesIO = StringIO
import urllib
import time
import splunk.clilib.cli_common as cli_common
from email import encoders, utils
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import splunk.safe_lxml_etree as et
from mako import template
import mako.filters as filters
import splunk.entity as entity
import splunk.Intersplunk
import splunk.mining.dcutils as dcu
import splunk.pdf.pdfgen_utils as pu
import splunk.search as search
import splunk.secure_smtplib as secure_smtplib
import splunk.ssl_context as ssl_context
from splunk.rest import simpleRequest
from splunk.saved import savedSearchJSONIsAlert
from splunk.util import normalizeBoolean, unicode, format_local_tzoffset
PDF_REPORT_SERVER_TIMEOUT = 600
PDFGEN_SIMPLE_REQUEST_TIMEOUT = 3600
EMAIL_DELIM = re.compile(r'\s*[,;]\s*')
EMAIL_FORMAT = re.compile(r'^[^\s@]+@[^\s@,;]+$')
CHARSET = "UTF-8"
IMPORTANCE_MAP = {
"highest": "1 (Highest)",
"high" : "2 (High)",
"low" : "4 (Low)",
"lowest" : "5 (Lowest)",
"1" : "1 (Highest)",
"2" : "2 (High)",
"4" : "4 (Low)",
"5" : "5 (Lowest)"
}
logger = dcu.getLogger()
class SendEmailException(Exception):
pass
def unquote(val):
if val is not None and len(val) > 1 and val.startswith('"') and val.endswith('"'):
return val[1:-1]
return val
def numsort(x, y):
if y[1] > x[1]:
return -1
elif x[1] > y[1]:
return 1
else:
return 0
def renderTime(results):
for result in results:
if "_time" in result:
try:
result["_time"] = time.ctime(float(result["_time"]))
except:
pass
def getEmailList(email):
tmpList = EMAIL_DELIM.split(email)
emailList = []
for email_item in tmpList:
if re.search(EMAIL_FORMAT, email_item):
emailList.append(email_item)
else:
logger.warn("Invalid email recipient=%s, remove recipient=%s from recipients."
% (email_item, email_item))
return emailList
# SPL-207377 - Normalize email address separators
def normalizeEmail(email, field, recipients):
emailList = getEmailList(email[field])
recipients.extend(emailList)
stripped = ','.join([str(elem) for elem in emailList])
email.replace_header(field, stripped)
def mail(email, argvals, ssContent, sessionKey):
sender = email['From']
use_ssl = normalizeBoolean(ssContent.get('action.email.use_ssl', False))
use_tls = normalizeBoolean(ssContent.get('action.email.use_tls', False))
server = ssContent.get('action.email.mailserver', 'localhost')
username = argvals.get('username', '')
password = argvals.get('password', '')
recipients = []
if email['To']:
normalizeEmail(email, 'To', recipients)
if email['Cc']:
normalizeEmail(email, 'Cc', recipients)
if email['Bcc']:
recipients.extend(getEmailList(email['Bcc']))
del email['Bcc'] # delete bcc from header after adding to recipients
# Clear leading / trailing whitespace from recipients
recipients = [r.strip() for r in recipients]
validRecipients = []
if ssContent.get('action.email.allowedDomainList') != "" and ssContent.get('action.email.allowedDomainList') != None:
domains = []
domains.extend(EMAIL_DELIM.split(ssContent['action.email.allowedDomainList']))
domains = [d.strip() for d in domains]
domains = [d.lower() for d in domains]
recipients = [r.lower() for r in recipients]
for recipient in recipients:
dom = recipient.partition("@")[2]
if not dom in domains:
logger.error("For subject=%s, email recipient=%s is not among the alowedDomainList=%s in alert_actions.conf file. Removing it from the recipients list."
% (ssContent.get('action.email.subject'), recipient, ssContent.get('action.email.allowedDomainList')))
else:
validRecipients.append(recipient)
else:
validRecipients = recipients
mail_log_msg = 'Sending email. subject="%s", encoded_subject="%s", results_link="%s", recipients="%s", server="%s"' % (
ssContent.get('action.email.subject'),
email['Subject'],
ssContent.get('results_link'),
str(validRecipients),
str(server)
)
try:
# make sure the sender is a valid email address
if sender.find("@") == -1:
sender = sender + '@' + socket.gethostname()
if sender.endswith("@"):
sender = sender + 'localhost'
# setup the Open SSL Context
sslHelper = ssl_context.SSLHelper()
serverConfJSON = sslHelper.getServerSettings(sessionKey)
# Pass in settings from alert_actions.conf into context
ctx = sslHelper.createSSLContextFromSettings(
sslConfJSON=ssContent.get('alertActions'),
serverConfJSON=serverConfJSON,
isClientContext=True)
# send the mail
if not use_ssl:
smtp = secure_smtplib.SecureSMTP(host=server)
else:
smtp = secure_smtplib.SecureSMTP_SSL(host=server, sslContext=ctx)
if use_tls:
smtp.starttls(ctx)
if len(username) > 0 and password is not None and len(password) >0:
smtp.login(username, password)
if ssContent.get('action.email.allowedDomainList') != "" and ssContent.get('action.email.allowedDomainList') != None:
if len(validRecipients) == 0:
raise Exception("The email domains of the recipients are not among those on the allowed domain list.")
# Installed SMTP daemon may not support UTF8 as well as
# it may throw other exceptions also.
error = sendemail_common.sendEmailWithUTF8(smtp, sender, validRecipients, email.as_string())
if (error is not None):
logger.debug('send mail with utf8 failed. retrying without utf8 option. Error: %s', str(error))
smtp.sendmail(sender, validRecipients, email.as_string())
smtp.quit()
if ssContent.get('action.email.allowedDomainList') != "" and ssContent.get('action.email.allowedDomainList') != None:
if validRecipients != recipients:
raise Exception("Not all of the recipient email domains are on the allowed domain list. Sending emails only to %s" % str(validRecipients))
logger.info(mail_log_msg)
except Exception as e:
logger.error(mail_log_msg)
raise
def sendEmail(results, settings, keywords, argvals):
for key in argvals:
if key != 'ssname':
argvals[key] = unquote(argvals[key])
namespace = settings['namespace']
owner = settings['owner']
sessionKey = settings['sessionKey']
sid = settings['sid']
ssname = argvals.get('ssname')
isScheduledView = False
is_stream_malert = normalizeBoolean(argvals.get('is_stream_malert'))
# Use this way to GET the correct alert_actions.conf under corresponding app.
entityClass = ['alerts', 'alert_actions', 'email']
uri = entity.buildEndpoint(
entityClass,
namespace=namespace,
owner=owner
)
responseHeaders, responseBody = simpleRequest(uri, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
alertEmail = json.loads(responseBody)
alertContent = alertEmail['entry'][0]['content']
alertContent['allowedDomainList'] = alertContent['allowedDomainList'].strip()
if ssname:
# populate content with savedsearch
if '_ScheduledView__' in ssname or argvals.get('pdfview'):
if '_ScheduledView__' in ssname:
ssname = ssname.replace('_ScheduledView__', '')
else:
ssname = argvals.get('pdfview')
uri = entity.buildEndpoint(
[ 'scheduled', 'views', ssname],
namespace=namespace,
owner=owner
)
isScheduledView = True
else:
if is_stream_malert:
entityClass = ['alerts', 'metric_alerts']
else:
entityClass = ['saved', 'searches']
entityClass.append(ssname)
uri = entity.buildEndpoint(
entityClass,
namespace=namespace,
owner=owner
)
responseHeaders, responseBody = simpleRequest(uri, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
savedSearch = json.loads(responseBody)
ssContent = savedSearch['entry'][0]['content']
# set type of saved search
if isScheduledView:
ssContent['type'] = 'view'
elif is_stream_malert or savedSearchJSONIsAlert(savedSearch):
ssContent['type'] = 'alert'
else:
ssContent['type'] = 'report'
# remap needed attributes that are not already on the content
ssContent['name'] = ssname
ssContent['app'] = savedSearch['entry'][0]['acl'].get('app')
ssContent['owner'] = savedSearch['entry'][0]['acl'].get('owner')
# The footer.text key will always exist for type alert and report.
# It may not exist for scheduled views created before 6.1 therefore the schedule view default footer.text
# should be set if the key does not exist.
# This can be removed once migration has happened to ensure scheduled views always have the footer.text attribute
ssContent['action.email.footer.text'] = ssContent.get('action.email.footer.text', "If you believe you've received this email in error, please see your Splunk administrator.\r\n\r\nsplunk>")
# The message key will always exist for type alert and report.
# It may not exist for scheduled views created before 6.1 therefore the schedule view default message
# should be set if the key does not exist.
# This can be removed once migration has happened to ensure scheduled views always have the message.view attribute
ssContent['action.email.message'] = ssContent.get('action.email.message.' + ssContent.get('type'), 'A PDF was generated for $name$')
if normalizeBoolean(ssContent.get('action.email.useNSSubject', False)):
ssContent['action.email.subject'] = ssContent['action.email.subject.' + ssContent.get('type')]
# prior to 6.1 the results link was sent as the argval sslink, must check both results_link
# and sslink for backwards compatibility
ssContent['results_link'] = argvals.get('results_link', argvals.get('sslink', ''))
if normalizeBoolean(ssContent['results_link']) and normalizeBoolean(ssContent['type']):
split_results_path = urllib.splitquery(ssContent.get('results_link'))[0].split('/')
view_path = '/'.join(split_results_path[:-1]) + '/'
ssType = ssContent.get('type')
useGoLink = False
if '@go' in split_results_path:
useGoLink = True
if ssType == 'alert':
if is_stream_malert:
# SPL-174186
# Note: this will generate link like this: http:///en-US/app/search/analysis_workspace?s=/servicesNS///alerts/metric_alerts/
# but the current MAW working link is this: http:///en-US/app/search/analysis_workspace?s=/servicesNS///saved/searches/
# If MAW UI can create new links for stream alert, this python code will work out of the box. Otherwise, we need to fix it at here.
ssContent['view_link'] = view_path + 'analytics_workspace?' + urllib.urlencode({'s': savedSearch['entry'][0]['links'].get('alternate')})
elif useGoLink:
ssContent['view_link'] = view_path + '@go?' + urllib.urlencode({'s': savedSearch['entry'][0]['links'].get('alternate'), 'dispatch_view': 'alert'})
else:
ssContent['view_link'] = view_path + 'alert?' + urllib.urlencode({'s': savedSearch['entry'][0]['links'].get('alternate')})
elif ssType == 'report':
if useGoLink:
ssContent['view_link'] = view_path + '@go?' + urllib.urlencode({'s': savedSearch['entry'][0]['links'].get('alternate'), 'sid': sid, 'dispatch_view': 'report'})
else:
ssContent['view_link'] = view_path + 'report?' + urllib.urlencode({'s': savedSearch['entry'][0]['links'].get('alternate'), 'sid': sid})
elif ssType == 'view':
ssContent['view_link'] = view_path + ssContent['name']
else:
ssContent['view_link'] = view_path + 'search'
else:
if is_stream_malert:
entityClass = ['alerts', 'metric_alerts']
else:
entityClass = ['saved', 'searches']
entityClass.append('_new')
#assumes that if no ssname then called from searchbar or test email
uri = entity.buildEndpoint(
entityClass,
namespace=namespace,
owner=owner
)
responseHeaders, responseBody = simpleRequest(uri, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
savedSearch = json.loads(responseBody)
ssContent = savedSearch['entry'][0]['content']
searchCommandSSContent = {
'type': 'searchCommand',
'view_link': '',
'action.email.subject': 'Splunk Results'
}
ssContent.update(searchCommandSSContent)
if normalizeBoolean(argvals.get('sendtestemail')):
ssContent['type'] = 'view'
if argvals.get('pdfview'):
ssContent['name'] = argvals.get('pdfview')
isScheduledView = True
if not argvals.get('message'):
ssContent['action.email.message'] = 'Search results attached.'
ssContent['trigger_date'] = None
ssContent['trigger_timeHMS'] = None
ssContent['trigger_time'] = argvals.get('trigger_time')
if normalizeBoolean(ssContent['trigger_time']):
try:
triggerSeconds = time.localtime(float(ssContent['trigger_time']))
ssContent['trigger_date'] = time.strftime("%B %d, %Y", triggerSeconds)
# %Z is deprecated, it does not work consistently between OSes
ssContent['trigger_timeHMS'] = time.strftime("%H:%M:%S ", triggerSeconds) + format_local_tzoffset()
except Exception as e:
logger.error(e)
# layer in arg vals
if argvals.get('to'):
ssContent['action.email.to'] = argvals.get('to')
if argvals.get('bcc'):
ssContent['action.email.bcc'] = argvals.get('bcc')
if argvals.get('cc'):
ssContent['action.email.cc'] = argvals.get('cc')
if argvals.get('format'):
ssContent['action.email.format'] = argvals.get('format')
if argvals.get('from'):
ssContent['action.email.from'] = argvals.get('from')
if argvals.get('inline'):
ssContent['action.email.inline'] = normalizeBoolean(argvals.get('inline'))
if argvals.get('sendresults'):
ssContent['action.email.sendresults'] = normalizeBoolean(argvals.get('sendresults'))
if argvals.get('sendpdf'):
ssContent['action.email.sendpdf'] = normalizeBoolean(argvals.get('sendpdf'))
if argvals.get('sendpng'):
ssContent['action.email.sendpng'] = normalizeBoolean(argvals.get('sendpng'))
if argvals.get('pdfview'):
ssContent['action.email.pdfview'] = argvals.get('pdfview')
if argvals.get('papersize') or ssContent.get('action.email.papersize'):
ssContent['action.email.reportPaperSize'] = argvals.get('papersize') or ssContent.get('action.email.papersize')
if argvals.get('paperorientation') or ssContent.get('action.email.paperorientation'):
ssContent['action.email.reportPaperOrientation'] = argvals.get('paperorientation') or ssContent.get('action.email.paperorientation')
if argvals.get('sendcsv'):
ssContent['action.email.sendcsv'] = normalizeBoolean(argvals.get('sendcsv'))
if argvals.get('allow_empty_attachment'):
ssContent['action.email.allow_empty_attachment'] = normalizeBoolean(argvals.get('allow_empty_attachment'))
if argvals.get('pdf.logo_path'):
ssContent['action.email.pdf.logo_path'] = argvals.get('pdf.logo_path')
if argvals.get('escapeCSVNewline'):
ssContent['action.email.escapeCSVNewline'] = normalizeBoolean(argvals.get('escapeCSVNewline'))
## if 'server' in arg is different than the one set in alert_action.conf, don't use default credentials.
## SPL-135659
setDefaultUserCredentials = 1
if argvals.get('server'):
alert_action_server = ssContent.get('action.email.mailserver', 'localhost')
if alertContent.get('allowedDomainList') == "":
ssContent['action.email.mailserver'] = argvals.get('server')
else:
logger.warn("When 'allowedDomainList' is setup, 'server' argument is not accepted in sendemail command. "
"The 'server' value is obtained from 'mailserver'=%s in alert_actions.conf." % alertContent.get('mailserver'))
assigned_server = ssContent.get('action.email.mailserver', 'localhost')
if str(alert_action_server) != str(assigned_server):
setDefaultUserCredentials = 0
if argvals.get('subject'):
ssContent['action.email.subject'] = argvals.get('subject')
if argvals.get('footer'):
ssContent['action.email.footer.text'] = argvals.get('footer')
if argvals.get('width_sort_columns'):
ssContent['action.email.width_sort_columns'] = normalizeBoolean(argvals.get('width_sort_columns'))
if argvals.get('message'):
ssContent['action.email.message'] = argvals.get('message')
else:
if ssContent['type'] == 'searchCommand':
# set default message for searchCommand emails
if ssContent['action.email.sendresults'] or ssContent['action.email.sendpdf'] or ssContent['action.email.sendcsv']:
if ssContent.get('action.email.inline') and not(ssContent.get('action.email.sendpdf') or ssContent.get('action.email.sendcsv')):
ssContent['action.email.message'] = 'Search results.'
else:
ssContent['action.email.message'] = 'Search results attached.'
else:
ssContent['action.email.message'] = 'Search complete.'
if argvals.get('priority'):
ssContent['action.email.priority'] = argvals.get('priority')
if argvals.get('use_ssl'):
ssContent['action.email.use_ssl'] = normalizeBoolean(argvals.get('use_ssl'))
if argvals.get('use_tls'):
ssContent['action.email.use_tls'] = normalizeBoolean(argvals.get('use_tls'))
if argvals.get('content_type'):
ssContent['action.email.content_type'] = argvals.get('content_type')
# parse the result id for per_result_alert
if argvals.get('results_file'):
dirname, fname = os.path.split(argvals.get('results_file'))
if dirname.endswith('per_result_alert'):
results_ids = re.findall(r'^tmp_(\d+)\.', fname)
if results_ids and len(results_ids) > 0:
ssContent['action.per_alert_result_id'] = results_ids[0]
logger.debug('Parsed action.per_alert_result_id=%s' % (ssContent['action.per_alert_result_id']))
else:
logger.warn('Cannot parse results_id=%s for per result alert' % (fname))
ssContent['graceful'] = normalizeBoolean(argvals.get('graceful', 0))
#if there is no results_link then do not incude it
if is_stream_malert or not normalizeBoolean(ssContent.get('results_link')):
ssContent['action.email.include.results_link'] = False
#need for backwards compatibility
format = ssContent.get('action.email.format')
if format == 'html' or format == 'plain':
ssContent['action.email.format'] = 'table'
ssContent['action.email.content_type'] = format
elif format == 'text':
ssContent['action.email.content_type'] = 'plain'
ssContent['action.email.format'] = 'table'
elif format == 'pdf':
ssContent['action.email.content_type'] = 'html'
ssContent['action.email.format'] = 'table'
#fetch server info
uriToServerInfo = entity.buildEndpoint(['server', 'info'])
serverInfoHeaders, serverInfoBody = simpleRequest(uriToServerInfo, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
serverInfo = json.loads(serverInfoBody)
serverInfoContent = serverInfo['entry'][0]['content']
#fetch job info
jobResponseHeaders = {}
jobResponseBody = {
'entry': [
{
'content': {}
}
]
}
if sid:
uriToJob = entity.buildEndpoint(
[
'search',
'jobs',
sid
],
namespace=namespace,
owner=owner
)
jobResponseHeaders, jobResponseBody = simpleRequest(uriToJob, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
searchJob = json.loads(jobResponseBody)
jobContent = searchJob['entry'][0]['content']
else:
jobContent = jobResponseBody['entry'][0]['content']
#fetch view info
viewResponseHeaders = {}
viewResponseBody = {
'entry': [
{
'content': {}
}
]
}
if isScheduledView:
uriToView = entity.buildEndpoint(
[
'data',
'ui',
'views',
ssContent['name']
],
namespace=namespace,
owner=owner
)
viewResponseHeaders, viewResponseBody = simpleRequest(uriToView, method='GET', getargs={'output_mode':'json'}, sessionKey=sessionKey)
viewResponseBody = json.loads(viewResponseBody)
viewContent = viewResponseBody['entry'][0]['content']
if ssContent.get('action.email.allowedDomainList'):
ssContent['action.email.allowedDomainList'] = ssContent['action.email.allowedDomainList'].strip()
if ssContent.get('action.email.allowedDomainList') != alertContent.get('allowedDomainList'):
ssContent['action.email.allowedDomainList'] = alertContent['allowedDomainList']
logger.warn("For alert=%s, the 'allowedDomainList' value is always obtained from alert_actions.conf."
"The allowedDomainList=%s" % (ssname, alertContent.get('allowedDomainList')))
if alertContent.get('allowedDomainList') != "":
if ssContent.get('action.email.mailserver') != alertContent.get('mailserver'):
ssContent['action.email.mailserver'] = alertContent['mailserver']
logger.warn("For alert=%s, if a 'allowedDomainList' is specified, it uses the 'mailserver'=%s in alert_actions.conf." %
(ssname, ssContent.get('action.email.mailserver')))
valuesForTemplate = buildSafeMergedValues(ssContent, results, serverInfoContent, jobContent, viewContent, argvals.get('results_file'))
realize(valuesForTemplate, ssContent, sessionKey, namespace, owner, argvals)
#Creation of the email object that is to be populated in the build
#prefixed methods.
content_type = ssContent.get('action.email.content_type')
isHtmlBasedContentType = content_type in ['html', 'html_pdf']
inline_studio_png = normalizeBoolean(ssContent.get('action.email.sendpng', False))
if isHtmlBasedContentType or inline_studio_png:
email = MIMEMultipart('mixed')
email.preamble = 'This is a multi-part message in MIME format.'
emailBody = MIMEMultipart('alternative')
email.attach(emailBody)
else:
email = emailBody = MIMEMultipart()
#make time user readable
resultsWithRenderedTime = copy.deepcopy(results)
renderTime(resultsWithRenderedTime)
#potentially mutate argvals if username/password are empty
if setDefaultUserCredentials:
setUserCredentials(argvals, settings)
#build all the different email components
jobCount = getJobCount(jobContent)
#attachments must be built before body so body can inlclude errors cause by attachments but
#must actually be attached after body
toAttach = buildAttachments(settings, ssContent, resultsWithRenderedTime, email, jobCount, argvals)
buildPlainTextBody(ssContent, resultsWithRenderedTime, settings, emailBody, jobCount)
if isHtmlBasedContentType or inline_studio_png:
buildHTMLBody(ssContent, resultsWithRenderedTime, settings, emailBody, email, jobCount, argvals)
buildHeaders(argvals, ssContent, email, sid, serverInfoContent)
#attach attachments
for attachment in toAttach:
email.attach(attachment)
try:
mail(email, argvals, ssContent, sessionKey)
except Exception as e:
errorMessage = str(e) + ' while sending mail to: ' + ssContent.get("action.email.to")
logger.error(errorMessage)
results = dcu.getErrorResults(results, ssContent['graceful'], errorMessage)
return results
def buildSafeMergedValues(ssContent, results, serverInfoContent, jobContent, viewContent, results_file):
mergedObject = {}
#namespace the keys
for key, value in jobContent.items():
mergedObject['token.job.'+key] = value
mergedObject['token.search_id'] = jobContent.get('sid')
for key, value in ssContent.items():
mergedObject['token.'+key] = value
mergedObject['token.name'] = ssContent.get('name')
mergedObject['token.app'] = ssContent.get('app')
mergedObject['token.owner'] = ssContent.get('owner')
for key, value in serverInfoContent.items():
mergedObject['token.server.'+key] = value
if viewContent:
mergedObject['token.dashboard.title'] = mergedObject['token.dashboard.label'] = viewContent.get('label') if viewContent.get('label') else mergedObject['token.name']
mergedObject['token.dashboard.id'] = mergedObject['token.name']
mergedObject['token.dashboard.description'] = getDescriptionFromXml(viewContent.get('eai:data'))
if results:
r = results[0]
for k in r:
if k.startswith('__mv_'):
continue
if isinstance(r[k], list):
mergedObject['token.result.'+k] = ' '.join(map(str,r[k]))
else:
mergedObject['token.result.'+k] = r[k]
mergedObject['token.results.count'] = len(results)
mergedObject['token.results.url'] = ssContent.get('results_link')
mergedObject['token.results.file'] = results_file
return mergedObject
def getDescriptionFromXml(xmlString):
if xmlString:
dashboardNode = et.fromstring(unicode(xmlString).encode('utf-8'))
return dashboardNode.findtext('./description')
else:
return None
def realize(valuesForTemplate, ssContent, sessionKey, namespace, owner, argvals):
stringsForPost = {}
if ssContent.get('action.email.message'):
stringsForPost['action.email.message'] = ssContent['action.email.message']
if ssContent.get('action.email.cc'):
stringsForPost['action.email.cc'] = ssContent['action.email.cc']
if ssContent.get('action.email.bcc'):
stringsForPost['action.email.bcc'] = ssContent['action.email.bcc']
if ssContent.get('action.email.to'):
stringsForPost['action.email.to'] = ssContent['action.email.to']
if ssContent.get('action.email.subject'):
stringsForPost['action.email.subject'] = ssContent['action.email.subject']
if ssContent.get('action.email.footer.text'):
# SPL-144752: allow footer to have no value at all if user choose to not have it.
# This can be done by setting footer = " " (note the space in between)
if len(ssContent['action.email.footer.text'].strip()) > 0:
stringsForPost['action.email.footer.text'] = ssContent['action.email.footer.text']
realizeURI = entity.buildEndpoint([ 'template', 'realize' ])
postargs = valuesForTemplate
postargs['output_mode'] = 'json'
postargs['conf.recurse'] = 0
try:
for key, value in stringsForPost.items():
if len(value.strip()) == 0:
logger.warning('Token substitution may fail due to key:%s contains only whitespaces' % key)
postargs['name'] = value
headers, body = simpleRequest(
realizeURI,
method='POST',
postargs=postargs,
sessionKey=sessionKey
)
body = json.loads(body)
ssContent[key] = body['entry'][0]['content']['eai:data']
except Exception as e:
logger.error(e)
# SPL-96721: email subject didn't get replaced, reset it to ssname
if ssContent.get('action.email.subject') == stringsForPost.get('action.email.subject'):
ssContent['action.email.subject'] = "Splunk Alert:"+argvals['ssname']
ssContent['action.email.message'] = ssContent['action.email.message'] + "\n\nNOTE: The dynamic substitution of the email subject failed because of failed token substitution. A generic subject has been used instead. Please check splunkd.log for additional details."
def setUserCredentials(argvals, settings):
username = argvals.get("username" , "")
password = argvals.get("password" , "")
# fetch credentials from the endpoint if none are supplied or password is encrypted
if (len(username) == 0 and len(password) == 0) or (password.startswith('$1$') or password.startswith('$7$')) :
namespace = settings.get("namespace", None)
sessionKey = settings['sessionKey']
username, password = getCredentials(sessionKey, namespace)
argvals['username'] = username
argvals['password'] = password
def getJobCount(jobContent):
if jobContent.get('statusBuckets') == 0 or (normalizeBoolean(jobContent.get('reportSearch')) and not re.match('sendemail', jobContent.get('reportSearch'))):
return jobContent.get('resultCount')
else:
return jobContent.get('eventCount')
# takes header, html, text
def buildHeaders(argvals, ssContent, email, sid, serverInfoContent):
sender = ssContent.get("action.email.from", "splunk")
to = ssContent.get("action.email.to")
cc = ssContent.get("action.email.cc")
bcc = ssContent.get("action.email.bcc")
subject = ssContent.get("action.email.subject")
priority = ssContent.get("action.email.priority", '')
# use the Header object to specify UTF-8 msg headers, such as subject, to, cc etc
email['Subject'] = Header(subject, CHARSET)
recipients = []
if to:
email['To'] = to
if sender:
email['From'] = sender
if cc:
email['Cc'] = cc
if bcc:
email['Bcc'] = bcc
email['Date'] = utils.formatdate(localtime=True)
if priority:
# look up better name
val = IMPORTANCE_MAP.get(priority.lower(), '')
# unknown value, use value user supplied
if not val:
val = priority
email['X-Priority'] = val
# trace info
if ssContent.get('name'):
email['X-Splunk-Name'] = ssContent.get('name')
if ssContent.get('owner'):
email['X-Splunk-Owner'] = ssContent.get('owner')
if ssContent.get('app'):
email['X-Splunk-App'] = ssContent.get('app')
email['X-Splunk-SID'] = sid
email['X-Splunk-ServerName'] = serverInfoContent.get('serverName')
email['X-Splunk-Version'] = serverInfoContent.get('version')
email['X-Splunk-Build'] = serverInfoContent.get('build')
def buildHTMLBody(ssContent, results, settings, emailbody, email, jobCount, argvals):
messageHTML = re.sub(r'\r\n?|\\r\\n?|\n|\\n', '
\r\n', htmlMessageTemplate().render(msg=ssContent.get('action.email.message')))
body_png = ''
resultsHTML = ''
metaDataHTML = ''
errorHTML = ''
if ssContent['type'] == 'view':
if normalizeBoolean(ssContent.get('action.email.include.view_link')) and ssContent.get('view_link'):
metaDataHTML = htmlMetaDataViewTemplate().render(
view_link=ssContent.get('view_link')
)
if should_inline_png(ssContent, argvals):
try:
name = ssContent.get('action.email.pdfview', '')
name_png = name + '.png'
body_png = inline_image_template().render(cid_name=name_png, name=name)
except ValueError as e:
logger.error('Failed to inline PNG due to {}'.format(e))
else:
if ssContent['type'] != 'searchCommand':
metaDataHTML = htmlMetaDataSSTemplate().render(
jobcount=jobCount,
results_link=ssContent.get('results_link'),
include_results_link=normalizeBoolean(ssContent.get('action.email.include.results_link')),
view_link=ssContent.get('view_link'),
include_view_link=normalizeBoolean(ssContent.get('action.email.include.view_link')),
name=ssContent.get('name'),
include_search=normalizeBoolean(ssContent.get('action.email.include.search')),
ssquery=ssContent.get('search'),
include_smaDefinition=normalizeBoolean(ssContent.get('action.email.include.smaDefinition')),
smaIndexes=ssContent.get('metric_indexes'),
smaFilter=ssContent.get('filter'),
smaGroupby=ssContent.get('groupby'),
smaCondition=ssContent.get('condition'),
smaThreshold=ssContent.get('trigger.threshold'),
smaSuppress=ssContent.get('trigger.suppress'),
smaEvaluationPerGroup=ssContent.get('trigger.evaluation_per_group'),
smaActionPerGroup=ssContent.get('trigger.action_per_group'),
smaTriggerPrepare=ssContent.get('trigger.prepare'),
alert_type=ssContent.get('alert_type'),
include_trigger=normalizeBoolean(ssContent.get('action.email.include.trigger')),
include_inline=normalizeBoolean(ssContent.get('action.email.inline')),
include_trigger_time=normalizeBoolean(ssContent.get('action.email.include.trigger_time')),
trigger_date=ssContent.get('trigger_date'),
trigger_timeHMS=ssContent.get('trigger_timeHMS'),
ssType=ssContent.get('type'),
)
# need to check aciton.email.sendresults for type searchCommand
if normalizeBoolean(ssContent.get('action.email.inline')) and normalizeBoolean(
ssContent.get('action.email.sendresults')):
resultsHTML = htmlResultsTemplate().render(
include_results_link=normalizeBoolean(ssContent.get('action.email.include.results_link')),
results_link=ssContent.get('results_link'),
truncated=normalizeBoolean(settings.get('truncated')),
resultscount=len(results),
jobcount=jobCount,
hasjob=normalizeBoolean(settings.get('sid'))
)
format = ssContent.get('action.email.format')
if format == 'table':
resultsHTML += htmlTableTemplate().render(results=results)
elif format == 'raw':
resultsHTML += htmlRawTemplate().render(results=results)
elif format == 'csv':
resultsHTML += htmlCSVTemplate().render(results=results)
footerHTML = htmlFooterTemplate().render(footer=ssContent.get('action.email.footer.text'), re=re, filters=filters)
errorHTML = htmlErrorTemplate().render(errors=ssContent.get('errorArray'))
wrapperHTML = htmlWrapperTemplate().render(body=messageHTML + body_png + metaDataHTML + errorHTML + resultsHTML + footerHTML)
emailbody.attach(MIMEText(wrapperHTML, 'html', _charset=CHARSET))
def htmlWrapperTemplate():
return template.Template('''
${body}
''')
def htmlMetaDataSSTemplate():
return template.Template('''
% if view_link and name and include_view_link:
${ssType.capitalize()}: | ${name|h} |
% endif
% if ssquery and include_search:
Search String: | ${ssquery|h} |
% endif
% if smaIndexes and include_smaDefinition:
metric_indexes: | ${smaIndexes|h} |
% endif
% if smaFilter and include_smaDefinition:
filter: | ${smaFilter|h} |
% endif
% if smaGroupby and include_smaDefinition:
groupby: | ${smaGroupby|h} |
% endif
% if smaCondition and include_smaDefinition:
condition: | ${smaCondition|h} |
% endif
% if smaThreshold and include_smaDefinition:
trigger.threshold: | ${smaThreshold|h} |
% endif
% if smaSuppress and include_smaDefinition:
trigger.suppress: | ${smaSuppress|h} |
% endif
% if smaEvaluationPerGroup and include_smaDefinition:
trigger.evaluation_per_group: | ${smaEvaluationPerGroup|h} |
% endif
% if smaActionPerGroup and include_smaDefinition:
trigger.action_per_group: | ${smaActionPerGroup|h} |
% endif
% if smaTriggerPrepare and include_smaDefinition:
trigger.prepare: | ${smaTriggerPrepare|h} |
% endif
% if include_trigger and name and alert_type and ssType == "alert":
Trigger: | Saved Search [${name|h}]: ${alert_type}
% if alert_type == "number of events":
(${jobcount})
% endif
|
% endif
% if include_trigger_time and trigger_timeHMS and trigger_date and ssType == "alert":
Trigger Time: | ${trigger_timeHMS} on ${trigger_date}. |
% endif
% if not include_inline:
% if include_results_link:
View results
% endif
% endif
''')
def htmlMetaDataViewTemplate():
return template.Template('''
View dashboard
''')
def htmlErrorTemplate():
return template.Template('''
% if errors:
% for error in errors:
${error|h}
% endfor
% endif
''')
def htmlResultsTemplate():
return template.Template('''
% if truncated:
%if jobcount:
Only the first ${resultscount} of ${jobcount} results are included below.
%else:
Search results in this email have been truncated.
%endif
%if include_results_link:
View all results in Splunk.
%else:
%endif
% elif include_results_link:
% endif
''')
def htmlMessageTemplate():
return template.Template('${msg|h}
')
def inline_image_template():
return template.Template('')
def htmlFooterTemplate():
return template.Template('''
<% footerEscaped = filters.html_entities_escape(footer) %>
<% footerBreaks = re.sub(r'\\r\\n?|\\n', '
', footerEscaped) %>
${footerBreaks}
''')
def htmlTableTemplate():
return template.Template('''
% if len(results) > 0:
<% cols = [] %>
% for key,val in results[0].items():
% if not key.startswith("_") or key == "_raw" or key == "_time":
<% cols.append(key) %>
${key|h} |
% endif
% endfor
% for result in results:
% for col in cols:
% if isinstance(result.get(col), list):
% for val in result.get(col):
${val|h}
% endfor
% else:
${result.get(col)|h}
% endif
|
% endfor
% endfor
% else:
No results found.
% endif
''')
def htmlRawTemplate():
return template.Template('''
% if len(results) > 0:
% if results[0].get("_raw"):
% for result in results:
${result.get("_raw", "")|h}
% endfor
% else:
The results contain no "_raw" field. Please choose another inline format (csv or table).
% endif
% else:
No results found.
% endif
''')
def htmlCSVTemplate():
return template.Template('''
% if len(results) > 0:
<% cols = [] %>
% for key,val in results[0].items():
% if not key.startswith("_") or key == "_raw" or key == "_time":
<% cols.append(key) %>
% endif
% endfor
${','.join(cols)|h}
% for result in results:
<% vals = [] %>
% for col in cols:
% if isinstance(result.get(col), list):
<% vals.append(' '.join(map(str,result.get(col)))) %>
% else:
<% vals.append(result.get(col))%>
% endif
% endfor
${','.join(vals)|h}
% endfor
% else:
No results found.
% endif
''')
def buildPlainTextBody(ssContent, results, settings, email, jobCount):
plainTextMsg = buildPlainTextMessage().render(msg=ssContent.get('action.email.message'))
plainResults = ''
plainTextMeta = ''
plainError = ''
if ssContent['type'] == 'view':
if normalizeBoolean(ssContent.get('action.email.include.view_link')) and ssContent.get('view_link'):
plainTextMeta = buildPlainTextViewMetaData().render(
view_link=ssContent.get('view_link')
)
plainError = buildPlainTextError().render(errors=ssContent.get('errorArray'))
else:
if ssContent['type'] != 'searchCommand':
plainTextMeta = buildPlainTextSSMetaData().render(
jobcount=jobCount,
results_link=ssContent.get('results_link'),
include_results_link=normalizeBoolean(ssContent.get('action.email.include.results_link')),
view_link=ssContent.get('view_link'),
include_view_link=normalizeBoolean(ssContent.get('action.email.include.view_link')),
name=ssContent.get('name'),
include_search=normalizeBoolean(ssContent.get('action.email.include.search')),
ssquery=ssContent.get('search'),
include_smaDefinition=normalizeBoolean(ssContent.get('action.email.include.smaDefinition')),
smaIndexes=ssContent.get('metric_indexes'),
smaFilter=ssContent.get('filter'),
smaGroupby=ssContent.get('groupby'),
smaCondition=ssContent.get('condition'),
smaThreshold=ssContent.get('trigger.threshold'),
smaSuppress=ssContent.get('trigger.suppress'),
smaEvaluationPerGroup=ssContent.get('trigger.evaluation_per_group'),
smaActionPerGroup=ssContent.get('trigger.action_per_group'),
smaTriggerPrepare=ssContent.get('trigger.prepare'),
alert_type=ssContent.get('alert_type'),
include_trigger=normalizeBoolean(ssContent.get('action.email.include.trigger')),
include_inline=normalizeBoolean(ssContent.get('action.email.inline')),
include_trigger_time=normalizeBoolean(ssContent.get('action.email.include.trigger_time')),
trigger_date=ssContent.get('trigger_date'),
trigger_timeHMS=ssContent.get('trigger_timeHMS'),
ssType=ssContent.get('type')
)
plainError = buildPlainTextError().render(errors=ssContent.get('errorArray'))
# need to check aciton.email.sendresults for type searchCommand
if normalizeBoolean(ssContent.get('action.email.inline')) and normalizeBoolean(ssContent.get('action.email.sendresults')):
plainResults = plainResultsTemplate().render(
include_results_link=normalizeBoolean(ssContent.get('action.email.include.results_link')),
results_link=ssContent.get('results_link'),
truncated=normalizeBoolean(settings.get('truncated')),
resultscount=len(results),
jobcount=jobCount,
hasjob=normalizeBoolean(settings.get('sid'))
)
format = ssContent.get('action.email.format')
if format == 'table':
plainResults += plainTableTemplate(results, ssContent)
elif format == 'raw':
plainResults += plainRawTemplate().render(results=results)
elif format == 'csv':
plainResults += plainCSVTemplate(results)
plainFooter = plainFooterTemplate().render(footer=ssContent.get('action.email.footer.text'))
email.attach(MIMEText(plainTextMsg + plainTextMeta + plainError + plainResults + plainFooter, 'plain', _charset=CHARSET))
def buildPlainTextSSMetaData():
return template.Template('''
% if view_link and name and include_view_link:
${ssType.capitalize()} Title: ${name}
${ssType.capitalize()} Location: ${view_link}
% endif
% if ssquery and include_search:
Search String: ${ssquery}
% endif
% if smaIndexes and include_smaDefinition:
metric_indexes: ${smaIndexes}
% endif
% if smaFilter and include_smaDefinition:
filter: ${smaFilter}
% endif
% if smaGroupby and include_smaDefinition:
groupby: ${smaGroupby}
% endif
% if smaCondition and include_smaDefinition:
condition: ${smaCondition}
% endif
% if smaThreshold and include_smaDefinition:
trigger.threshold: ${smaThreshold}
% endif
% if smaSuppress and include_smaDefinition:
trigger.suppress: ${smaSuppress}
% endif
% if smaEvaluationPerGroup and include_smaDefinition:
trigger.evaluation_per_group: ${smaEvaluationPerGroup}
% endif
% if smaActionPerGroup and include_smaDefinition:
trigger.action_per_group: ${smaActionPerGroup}
% endif
% if smaTriggerPrepare and include_smaDefinition:
trigger.prepare: ${smaTriggerPrepare}
% endif
% if include_trigger and name and alert_type and ssType == "alert":
% if alert_type == "number of events":
Trigger: Saved Search [${name}]: ${alert_type} (${jobcount})
% else:
Trigger: Saved Search [${name}]: ${alert_type}
%endif
% endif
% if include_trigger_time and trigger_timeHMS and trigger_date and ssType == "alert":
Trigger Time: ${trigger_timeHMS} on ${trigger_date}.
% endif
% if not include_inline:
% if include_results_link:
View results: ${results_link}
% endif
%endif
''')
def buildPlainTextViewMetaData():
return template.Template('''
View dashboard: ${view_link}
''')
def buildPlainTextMessage():
return template.Template('${msg}')
def plainFooterTemplate():
return template.Template('''
------------------------------------------------------------------------
${footer}
''')
def buildPlainTextError():
return template.Template('''
% if errors:
------------------------------------------------------------------------
% for error in errors:
${error}
% endfor
% endif
''')
def plainResultsTemplate():
return template.Template('''
------------------------------------------------------------------------
% if truncated:
% if jobcount:
Only the first ${resultscount} of ${jobcount} results are included below.
% else:
Search results in this email have been truncated.
% endif
% if include_results_link:
View all results in Splunk: ${results_link}
% endif
% elif include_results_link:
View results in Splunk: ${results_link}
% endif
''')
# sort columns from shortest to largest
def getSortedColumns(results, width_sort_columns):
if len(results) == 0:
return []
columnMaxLens = {}
for result in results:
for k,v in result.items():
# ignore attributes that start with "_"
if k.startswith("_") and k!="_raw" and k!="_time":
continue
if isinstance(v, list):
v = getLongestString(v)
newLen = len(str(v))
oldMax = columnMaxLens.get(k, -1)
#initialize the column width to the length of header (name)
if oldMax == -1:
columnMaxLens[k] = oldMax = len(k)
if newLen > oldMax:
columnMaxLens[k] = newLen
colsAndCounts = []
# sort columns iff asked to
if width_sort_columns:
colsAndCounts = sorted(columnMaxLens.items(), key=cmp_to_key(numsort))
else:
for k,v in results[0].items():
if k in columnMaxLens:
colsAndCounts.append([k, columnMaxLens[k]])
return colsAndCounts
def getLongestString(valuesList):
return max(map(str,valuesList), key=len)
def plainTableTemplate(results, ssContent):
if len(results) > 0:
width_sort_columns = normalizeBoolean(ssContent.get('action.email.width_sort_columns', True))
columnMaxLens = getSortedColumns(results, width_sort_columns)
text = "\n"
space = " "*8
# output column names
for col, maxlen in columnMaxLens:
val = col
padsize = maxlen - len(val)
text += val + ' '*padsize + space
rowBorder = "-"*len(text)
text += "\n" + rowBorder + "\n"
# output each result's values
for result in results:
# maximum number of multivalue fields in any column for a given result
maxFields = -1
for col, maxlen in columnMaxLens:
val = result.get(col, "")
if isinstance(val, list):
maxFields = len(val) if len(val) > maxFields else maxFields
val = val[0]
padsize = maxlen - len(val)
# left justify ALL the columns
text += val + ' '*padsize + space
text += "\n"
# add remaining multivalue items, if any
if maxFields > -1:
for row in range(1, maxFields):
for col, maxlen in columnMaxLens:
val = result.get(col, "")
if isinstance(val, list) and len(val) > row:
val = str(val[row])
else:
# no value in this row if it isn't a list
val = ""
padsize = maxlen - len(val)
text += val + ' '*padsize + space
text += "\n"
text += rowBorder + "\n"
else:
text = "No results found."
return text
def plainCSVTemplate(results):
text = ""
if len(results) > 0:
cols = []
for key,val in results[0].items():
if not key.startswith("_") or key == "_raw" or key == "_time":
cols.append(key)
text = ','.join(cols) +'\n'
for result in results:
vals = []
for col in cols:
val = result.get(col)
if isinstance(val, list):
val = ' '.join(map(str,val))
vals.append(val)
text += ','.join(vals) + '\n'
else:
text = "No results found."
return text
def plainRawTemplate():
return template.Template('''
% if len(results) > 0:
% if results[0].get('_raw'):
% for result in results:
${result.get("_raw", "")}\n
% endfor
% else:
The results contain no "_raw" field. Please choose another inline format (csv or table).
% endif
% else:
No results found.
% endif
''')
def should_inline_png(ssContent, argvals):
sendpng = normalizeBoolean(ssContent.get('action.email.sendpng'), False)
no_errors = len(ssContent.get('errorArray', [])) == 0
inline_specified = normalizeBoolean(ssContent.get('action.email.inline'), False) or normalizeBoolean(argvals.get('inline'), False)
return sendpng and no_errors and inline_specified
def buildAttachments(settings, ssContent, results, email, jobCount, argvals):
toAttach = []
ssContent['errorArray'] = []
sendpdf = normalizeBoolean(ssContent.get('action.email.sendpdf', False))
sendcsv = normalizeBoolean(ssContent.get('action.email.sendcsv', False))
sendpng = normalizeBoolean(ssContent.get('action.email.sendpng', False))
allowEmpty = normalizeBoolean(ssContent.get('action.email.allow_empty_attachment', False))
sendresults = normalizeBoolean(ssContent.get('action.email.sendresults', False))
inline = normalizeBoolean(ssContent.get('action.email.inline', False))
inlineFormat= ssContent.get('action.email.format')
input_type = ssContent['type']
namespace = settings['namespace']
owner = settings['owner']
sessionKey = settings['sessionKey']
searchid = settings.get('sid')
pdfview = ssContent.get('action.email.pdfview', '')
subject = ssContent.get("action.email.subject")
ssName = ssContent.get("name")
server = ssContent.get('action.email.mailserver', 'localhost')
results_link = ssContent.get('results_link')
paperSize = ssContent.get('action.email.reportPaperSize', 'letter')
paperOrientation = ssContent.get('action.email.reportPaperOrientation', 'portrait')
pdfLogoPath = ssContent.get('action.email.pdf.logo_path')
# Despite being generically called a "view", a pdfview is _always_ the ID of
# a splunk dashboard. Also see src/scheduler/SavedSearchAdminHandler.cpp's
# definition of getDefaultScheduledView()
isDashboard = normalizeBoolean(pdfview)
pdf_attachment = None
png_attachment = None
export_type = ''
alertActions = getAlertActions(sessionKey)
if alertActions:
# Take ssContent first, then fallback to alertActions.
ss_filename = ssContent.get('action.email.reportFileName', None)
aa_filename = alertActions.get('reportFileName', None)
fileName = ss_filename if ss_filename else aa_filename
# Ideally we would retrieve the alert actions conf settings in mail()
# which is where these settings are used. But we are already making
# a REST call to get these settings, so we save these settings for later
ssContent['alertActions'] = alertActions
else:
fileName = None
if sendpdf or sendpng:
sendtestemail = normalizeBoolean(argvals.get('sendtestemail', False))
# Dashboard PDF export *always* overrides allowEmpty (action.email.allow_empty_attachment)
if len(results) == 0 and not allowEmpty and not sendtestemail and not isDashboard:
logger.info("Not attaching pdf file due to no matching results and allow_empty_attachment=%s" % str(allowEmpty))
else:
import splunk.pdf.availability as pdf_availability
pdfgen_available = pdf_availability.is_available(session_key=sessionKey)
logger.info("pdfgen_available = %s" % pdfgen_available)
try:
if pdfgen_available:
result_id = None
if ssContent.get('action.per_alert_result_id'):
result_id = ssContent.get('action.per_alert_result_id')
if sendpdf:
export_type = 'pdf'
# will raise an Exception on error
pdf_attachment = generate_attachment(server, subject, searchid, settings, pdfview, ssName, paperSize,
paperOrientation, pdfLogoPath, result_id, export_type, ssContent['errorArray'])
if sendpng:
export_type = 'png'
png_attachment = generate_attachment(server, subject, searchid, settings, pdfview, ssName, paperSize,
paperOrientation, pdfLogoPath, result_id, export_type, ssContent['errorArray'])
except Exception as e:
logger.error("An error occurred while generating a %s: %s" % (export_type.upper(), e))
if len(ssContent['errorArray']) == 0:
# default error message (should never reach here for Studio dashboards)
base_message = "An error occurred while generating the %s." % export_type.upper()
pdf_message = base_message + " " + "See python.log and pdfgen.log for errors."
png_message = base_message + " " + "Only Dashboard Studio supports PNG exports. Migrate to Dashboard Studio to schedule a PNG export."
error_message = png_message if export_type == 'png' else pdf_message
ssContent['errorArray'].append(error_message)
# build up filename to use with attachments
props = {
"owner": owner or 'nobody',
"app": namespace,
"type": "dashboard" if isDashboard else input_type or "report",
"name": pdfview or ssName
}
if pdf_attachment:
pdf_filename = pu.makeReportName(pattern=fileName, type='pdf', reportProps=props)
pdf_attachment.add_header('content-disposition', 'attachment', filename=pdf_filename)
toAttach.append(pdf_attachment)
if png_attachment:
png_filename = pu.makeReportName(pattern=fileName, type='png', reportProps=props)
extension = '.png'
content_id = "<" + pdfview + extension + ">"
png_attachment.add_header('Content-Type', mimetypes.types_map[extension])
png_attachment.add_header("Content-Disposition", "inline; filename=" + png_filename)
if should_inline_png(ssContent, argvals):
# attaches inline
png_attachment.add_header("Content-ID", content_id)
email.attach(png_attachment)
# attaches externally
toAttach.append(png_attachment)
# (input_type == searchCommand and sendresults and not inline) needed for backwards compatibility
# (sendresults and not(sendcsv or sendpdf or inline) and inlineFormat == 'csv')
# needed for backwards compatibility when we did not have sendcsv pre 6.1 SPL-79561
if len(results) == 0 and not allowEmpty:
logger.info("Not attaching csv file due to no matching results and allow_empty_attachment=%s" % str(allowEmpty))
elif (sendcsv or (input_type == 'searchCommand' and sendresults and not inline) or (sendresults and not(sendcsv or sendpdf or inline) and inlineFormat == 'csv')):
csvAttachment = MIMEBase("text", "csv")
# SPL-179427 add choice whether to escape newlines
escapeCSVNewline = normalizeBoolean(ssContent.get('action.email.escapeCSVNewline', True))
csvAttachment.set_payload(generateCSVResults(results, escapeCSVNewline))
encoders.encode_base64(csvAttachment)
props = {
"owner": owner or 'nobody',
"app": namespace,
"type": input_type,
"name": ssName
}
filename = pu.makeReportName(pattern=fileName, type="csv", reportProps=props)
csvAttachment.add_header('Content-Disposition', 'attachment', filename=filename)
toAttach.append(csvAttachment)
if normalizeBoolean(settings.get('truncated')):
if normalizeBoolean(len(results)) and normalizeBoolean(jobCount):
ssContent['errorArray'].append("Only the first %s of %s results are included in the attached csv." %(len(results), jobCount))
else:
ssContent['errorArray'].append("Attached csv results have been truncated.")
return toAttach
def esc(val):
return val.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
def generateCSVResults(results, escapeCSVNewline):
if len(results) == 0:
return 'No matching events found.'
header = []
s = BytesIO()
if sys.version_info >= (3, 0):
# SPL-204962 Scheduled Jobs sendemail csv file Generated Extra Blank Rows
# Per Python 3 csv documentation https://docs.python.org/3/library/csv.html, csv file objects should be opened with newline=''.
#
# Also in the **footnotes** of the documentation
# If newline='' is not specified, newlines embedded inside quoted fields will not be interpreted correctly,
# and on platforms that use \r\n linendings on write an extra \r will be added. It should always be safe to specify
# newline='', since the csv module does its own (universal) newline handling.
t = TextIOWrapper(s, write_through = True, encoding='utf-8', errors='backslashreplace', newline='')
w = csv.writer(t)
else:
w = csv.writer(s)
if "_time" in results[0] : header.append("_time")
if "_raw" in results[0] : header.append("_raw")
# for backwards compatibility remove all internal fields except _raw and _time
for k in results[0].keys():
if k.startswith("_") :
continue
header.append(k)
w.writerow(header)
# output each result's values
for result in results:
row = []
for col in header:
val = result.get(col,"")
if isinstance(val, list):
val = ' '.join(map(str,val))
if (escapeCSVNewline):
row.append(esc(val))
else:
row.append(val)
w.writerow(row)
return s.getvalue()
# call separated for testing purposes
def call_pdfgen_render(sessionKey, parameters):
response, content = simpleRequest("pdfgen/render", sessionKey = sessionKey, postargs = parameters, timeout = PDFGEN_SIMPLE_REQUEST_TIMEOUT)
return response, content
def handle_png_error_messages(content, export_type, error_array):
if export_type != 'png':
return
error_messages = get_error_messages(content)
if error_messages is not None:
error_array.extend(error_messages)
def attempt_json_formatting(decoded_content):
try:
formatted_json_content = json.dumps(ast.literal_eval(decoded_content))
json_data = json.loads(formatted_json_content)
return json_data.get('error_messages', None)
except (ValueError, TypeError, json.JSONDecodeError) as e:
logger.error("Failed to format JSON for error message due to %s" % repr(e))
return None
def get_error_messages(content: bytes):
try:
decoded_content = content.decode('utf-8')
json_data = json.loads(content)
return json_data.get('error_messages', None)
except (UnicodeDecodeError, json.JSONDecodeError):
return attempt_json_formatting(decoded_content)
def generate_attachment(serverURL, subject, sid, settings, pdfViewID, ssName, paperSize, paperOrientation, pdfLogoPath, resultId, export_type, error_array):
"""
Reach out and retrieve a PDF copy of the search results if possible
and return the MIME attachment
"""
sessionKey = settings.get('sessionKey', None)
owner = settings.get('owner', 'nobody')
upper_export_type = export_type.upper()
if not sessionKey:
raise SendEmailException("Can't attach %s - sessionKey unavailable" % upper_export_type)
# build up parameters to the PDF server
parameters = {}
parameters['namespace'] = settings["namespace"]
parameters['owner'] = owner
if pdfViewID:
parameters['input-dashboard'] = pdfViewID
if export_type == 'png':
parameters['type'] = export_type
else:
if ssName:
parameters['input-report'] = ssName
parameters['timezone'] = settings.get('timezone')
elif sid:
# in the event where sendemail is called from search
# and we need to generate pdf re-run the search
job = search.getJob(sid, sessionKey=sessionKey)
jsonJob = job.toJsonable(timeFormat='unix')
searchToRun = jsonJob.get('search').strip()
if searchToRun.lower().startswith('search '):
searchToRun = searchToRun[7:]
sendemailRegex = r'\|\s*sendemail'
if (re.findall(sendemailRegex, searchToRun)):
parameters['input-search'] = re.split(sendemailRegex, searchToRun)[0]
parameters['et'] = jsonJob.get('earliestTime')
parameters['lt'] = jsonJob.get('latestTime')
else:
raise SendEmailException("Can't attach %s - ssName and pdfViewID unavailable" % upper_export_type)
if sid:
if type(sid) is dict:
for sidKey in sid:
parameters[sidKey] = sid[sidKey]
else:
parameters['sid'] = sid
if paperSize and len(paperSize) > 0:
if paperOrientation and paperOrientation != "portrait":
parameters['paper-size'] = "%s-%s" % (paperSize, paperOrientation)
else:
parameters['paper-size'] = paperSize
if pdfLogoPath:
parameters['pdf.logo_path'] = pdfLogoPath
if resultId:
parameters['result_id'] = resultId
# determine if we should set an effective dispatch "now" time for this job
scheduledJobEffectiveTime = getEffectiveTimeOfScheduledJob(settings.get("sid", ""))
logger.info("sendemail:mail effectiveTime=%s" % scheduledJobEffectiveTime)
if scheduledJobEffectiveTime != None:
parameters['now'] = scheduledJobEffectiveTime
try:
response, content = call_pdfgen_render(sessionKey, parameters)
except splunk.SplunkdConnectionException as e:
raise SendEmailException("Failed to fetch %s (SplunkdConnectionException): %s" % (upper_export_type, repr(e)))
except Exception as e:
raise SendEmailException("Failed to fetch %s (Exception type=%s): %s" % (upper_export_type, repr(type(e)), repr(e)))
if response['status']!='200':
raise SendEmailException("Failed to fetch %s (status = %s): %s" % (upper_export_type, repr(response['status']), repr(content)))
if export_type == 'png' and response['content-type'] == 'application/json':
handle_png_error_messages(content, export_type, error_array)
return None
if (export_type == 'pdf' and response['content-type']!='application/pdf') or (export_type == 'png' and response['content-type']!='image/png'):
raise SendEmailException("Failed to fetch %s (content-type = %s): %s" % (upper_export_type, repr(response['content-type']), repr(content)))
mpart = None
if export_type == 'png':
mpart = MIMEImage(content, _subtype=export_type)
else:
mpart = MIMEApplication(content, export_type)
logger.info('Generated %s for email' % upper_export_type)
return mpart
def getEffectiveTimeOfScheduledJob(scheduledJobSid):
""" parse out the effective time from the sid of a scheduled job
if no effective time specified, then return None
scheduledJobSid is of form: scheduler______at__ """
scheduledJobSidParts = scheduledJobSid.split("_")
effectiveTime = None
if "scheduler" in scheduledJobSidParts and len(scheduledJobSidParts) > 4 and scheduledJobSidParts[-3] == "at":
secondsStr = scheduledJobSidParts[-2]
try:
effectiveTime = int(secondsStr)
except:
pass
return effectiveTime
def getCredentials(sessionKey, namespace):
try:
ent = entity.getEntity('admin/alert_actions', 'email', namespace=namespace, owner='nobody', sessionKey=sessionKey)
if 'auth_username' in ent and 'clear_password' in ent:
encrypted_password = ent['clear_password']
splunkhome = os.environ.get('SPLUNK_HOME')
if splunkhome == None:
logger.error('getCredentials - unable to retrieve credentials; SPLUNK_HOME not set')
return None
clear_password = cli_common.decrypt(encrypted_password, setEnv=True)
return ent['auth_username'], clear_password
except Exception as e:
logger.error("Could not get email credentials from splunk, using no credentials. Error: %s" % (str(e)))
return '', ''
def getAlertActions(sessionKey):
settings = None
try:
settings = entity.getEntity('/configs/conf-alert_actions', 'email', sessionKey=sessionKey)
logger.debug("sendemail.getAlertActions conf file settings %s" % settings)
except Exception as e:
logger.error("Could not access or parse email stanza of alert_actions.conf. Error=%s" % str(e))
return settings
def sendHealthAlertEmail(results, settings):
"""
This function will only be called to send health report alerting emails.
Email parameters will be passed through settings/results.
"""
if not results or len(results) < 1:
logger.error("There is no email content specified.")
return
if not settings.get('to') and not settings.get('cc') and not settings.get('bcc'):
logger.error("There are no recipients specified")
return
sessionKey = ""
alertConfig = {}
email = MIMEMultipart()
if settings.get('to'):
email['To'] = settings.get('to')
if settings.get('cc'):
email['Cc'] = settings.get('cc')
if settings.get('bcc'):
email['Bcc'] = settings.get('bcc')
if settings.get('from'):
email['From'] = settings.get('from')
alertConfig['action.email.use_ssl'] = normalizeBoolean(settings.get('use_ssl'))
alertConfig['action.email.use_tls'] = normalizeBoolean(settings.get('use_tls'))
if settings.get('mailserver'):
alertConfig['action.email.mailserver'] = settings.get('mailserver')
if settings.get('auth_username'):
argvals['username'] = settings.get('auth_username')
if settings.get('auth_password'):
argvals['password'] = settings.get('auth_password')
email['Subject'] = Header(results[0].get('subject'), CHARSET)
plainMsg = results[0].get('plain_msg')
email.attach(MIMEText(plainMsg, 'plain', _charset=CHARSET))
try:
mail(email, argvals, alertConfig, sessionKey)
except Exception as e:
errorMessage = 'Error sending Health Report alert. Error="%s".' % e
logger.error(errorMessage)
# For health report email alert, don't need return results.
return None
def createOrganizedResultBuffer(buffer):
# creates the readable buffer for reading search results into the send email script
return TextIOWrapper(buffer, encoding='utf-8', errors='backslashreplace')
if __name__ == '__main__':
if sys.version_info >= (3, 0):
# We need to support data of non utf-8 encodings coming into this script, to do that,
# we allow utf-8 errors
input_buf = createOrganizedResultBuffer(sys.stdin.buffer)
else:
input_buf = sys.stdin
results, dummyresults, settings = splunk.Intersplunk.getOrganizedResults(input_buf)
try:
keywords, argvals = splunk.Intersplunk.getKeywordsAndOptions(CHARSET)
logger.debug('SENDEMAIL keywords: %s, argvals: %s' % (keywords, argvals))
if 'is_health_alert' in argvals:
results = sendHealthAlertEmail(results, settings)
else:
if results or 'ssname' in argvals:
results = sendEmail(results, settings, keywords, argvals)
elif 'sendtestemail' in argvals:
if normalizeBoolean(argvals.get('sendtestemail')):
results = sendEmail(results, settings, keywords, argvals)
else:
logger.warn("search results is empty, no email will be sent")
except Exception as e:
logger.exception(e)
splunk.Intersplunk.outputResults(results)