Using smtplib for Zaqar mail delivery

Add the new way which using smtplib for Zaqar email subscription
notification.

An example file for configure mail content and SMTP information
is added as well.

Change-Id: I4a1310b54bec38263981792ba0220ae516bea179
Implements: blueprint zaqar-email-delivery
This commit is contained in:
changyufei 2018-04-02 16:53:39 +08:00 committed by gecong
parent 5830528f97
commit e1c62707d3
5 changed files with 293 additions and 3 deletions

View File

@ -0,0 +1,8 @@
---
features:
- |
Currently the email subscription in Zaqar relay on the third part
tools, such as "sendmail". It means that deployer should install
it out of Zaqar. If he forgets, Zaqar will raise internal error.
This work let Zaqar support email subscription by itself using
the ``smtp`` python library.

228
samples/zaqar/sendmail.py Normal file
View File

@ -0,0 +1,228 @@
# Copyright (c) 2018 Ustack, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
from email.mime.text import MIMEText
from email.parser import Parser
import json
import smtplib
import sys
from keystoneauth1 import loading
from keystoneauth1 import session as ks_session
from oslo_config import cfg
import requests
import retrying
KUNKA_SERVICE_TYPE = 'portal'
"""KUNKA_CORP_INFO_PATH is an API for obtaining information from the database
(such as /api/email/corporation-info), and the returned information
is the value of the field in the mail template. It is connected after
the ip:port which is the database connection information. ip:port
like 127.0.0.1:3306, or other"""
KUNKA_CORP_INFO_PATH = 'Your API address'
# The information is relatively sensitive, suggesting encrypted transmission,
# or stored in the database to return.When "use_ssl" is False, the "port" is
# 25, otherwise, the "port" is 465 in using SSL type.
mail_info = {
"from": "youremail@youremail.com",
"hostname": "yourSMTP_serverAddr",
"username": "yourSMTP_server-username",
"password": "Authorization_code",
"port": 25,
"use_ssl": False
}
# It's a HTML mail template,and can be changed as needed
mail_body = u"""
<div style="font-family: 'Microsoft YaHei', Arial, Helvetica,sans-serif;
position: relative;margin: 0 auto; width: 702px; font-size: 0;">
<div style="display: inline-block; width: 0; height: 0;
border-width: 10px; border-style: solid; border-color:
transparent #343E41 #343E41 transparent;margin-left: 227px;"></div>
<div style="display: inline-block; width: 0; height: 0;
border-width: 10px; border-style: solid; border-color:
transparent transparent #343E41 #343E41;margin-left: 206px;"></div>
<div style="width: 206px; height: 60px; position:absolute;
top: 0px; left: 247px; background-color: #343E41;
background-position: center center; background-size:
contain; background-repeat: no-repeat; z-index: 10;">
<img src="{logo_url}" style=" width: 206px; height: 60px;"></div>
<div style="width: 662px; margin-top: 0; margin-left:
auto; margin-right: auto; background-color: #343E41;
padding: 20px;">
<div style="width: 662px; background-color:
transparent; border: 1.2px solid #01AFC9;
position: relative; padding: 0; text-align: center;">
<div style="width: 20px; height: 20px; position:
absolute; left:0; top: 0; background-color: #01AFC9;
margin: 0;"></div>
<div style="width: 20px; height: 20px; position:
absolute; right:0; top: 0; background-color:
#01AFC9; margin: 0;"></div>
<div style="width: 20px; height: 20px; position:
absolute; left:0; bottom: 0; background-color:
#01AFC9; margin: 0;"></div>
<div style="width: 20px; height: 20px; position:
absolute; right:0; bottom: 0; background-color:
#01AFC9; margin: 0;"></div>
<p style="text-align: center; margin-top:36px;">
<span style="font-size: 6px; color: #ffffff;
vertical-align: middle;">///////////////////</span>
<span style="display: inline-block; margin-left:
40px; margin-right: 40px; color: #ffffff; font-size:
20px; vertical-align: middle;">Respected
{corp_name}user</span>
<span style="font-size: 6px; color: #ffffff;
vertical-align: middle;">///////////////////</span>
</p >
<div>
<div style="margin-top: 12px; color: #ffffff;
font-size: 14px; text-align: left; display:
inline-block; max-width: 60%;">
<div>
<div>
<p>{confirm_or_alarm}</p>
</div>
</div>
</div>
</div>
<p style="margin-top: 16px; margin-bottom: 14px;
font-size: 12px; letter-spacing: 0.67px; text-align:
center; height: 16px; color: #ffffff;">{corp_name}—
<a style="text-decoration: none; color:
#ffffff;" href=" >">{home_link} </a></p >
</div>
</div>
</div>
"""
mail_confirm_link = u"""
Your mailbox will be used for receiving system notifications.
If you confirm, click the following link:
<a href="{confirm_link}" target="_blank">Activation link</a>
"""
mail_alarm_info = u"""
Your alarm information is as follows:{reason}<br>
Alarm level:{severity}<br>
Alarm name:{alarm_name}<br>
Alarm ID {alarm_id}
"""
def prepare_conf():
cfg.CONF(project='zaqar')
loading.register_auth_conf_options(cfg.CONF, 'keystone_authtoken')
def get_admin_session():
auth_plugin = \
loading.load_auth_from_conf_options(cfg.CONF, 'keystone_authtoken')
return ks_session.Session(auth=auth_plugin)
def get_endpoint(session, service_type, interface='internal'):
return session.get_endpoint(service_type=service_type,
interface=interface)
@retrying.retry(stop_max_attempt_number=3)
def get_corp_info(session):
kunka_endpoint = get_endpoint(session, KUNKA_SERVICE_TYPE)
kunka_url = kunka_endpoint + KUNKA_CORP_INFO_PATH
res = None
res = requests.get(kunka_url)
corp_info = res.json()
return {"corp_name": corp_info['corporationName'],
"logo_url": corp_info['emailLogoUrl'],
"home_link": corp_info['homeUrl'],
"from": corp_info['from']}
def generate_msg(subbody, to, from_, subject, **kwargs):
payload = mail_body.format(confirm_or_alarm=subbody, **kwargs)
msg = MIMEText(payload.encode('utf-8'), 'html', 'utf-8')
msg['subject'] = subject
msg['from'] = from_
msg['to'] = to
return msg
def generate_subbody(subbody, **kwargs):
return subbody.format(**kwargs)
def get_confirm_link(str_):
return str_.split('below: ')[-1]
def prepare_msg(msg_str):
headers = Parser().parsestr(msg_str)
payload = headers.get_payload()
msg_subject = headers['subject']
if not headers['subject']:
alarm_info = json.loads(payload)['body']
subject = msg_subject + alarm_info['alarm_name']
template = generate_subbody(mail_alarm_info,
reason=alarm_info['reason'],
severity=alarm_info['severity'],
alarm_name=alarm_info['alarm_name'],
alarm_id=alarm_info['alarm_id'])
else:
subject = msg_subject
template = generate_subbody(mail_confirm_link,
confirm_link=get_confirm_link(payload))
session = get_admin_session()
corp_info = get_corp_info(session)
msg = generate_msg(
template, headers['to'],
corp_info['from'], subject, logo_url=corp_info['logo_url'],
corp_name=corp_info['corp_name'], home_link=corp_info['home_link'])
return msg
@retrying.retry(stop_max_attempt_number=3)
def send_it(msg):
# if "use_ssl" is True, the "port" is 465 in using SSL type,
# or other SSL port.
if mail_info['use_ssl']:
sender = smtplib.SMTP_SSL(mail_info["hostname"], mail_info['port'])
else:
sender = smtplib.SMTP(mail_info["hostname"], mail_info['port'])
sender.set_debuglevel(1)
sender.ehlo(mail_info["hostname"])
try:
sender.login(mail_info["username"], mail_info["password"])
except smtplib.SMTPException:
print("Error: Failed to connect to the SMTP service")
sender.sendmail(msg['from'], msg['to'], msg.as_string())
def send_email(msg_str):
prepare_conf()
send_it(prepare_msg(msg_str))
if __name__ == '__main__':
send_email(''.join(sys.stdin.readlines()))

View File

@ -14,6 +14,39 @@
from oslo_config import cfg from oslo_config import cfg
smtp_mode = cfg.StrOpt(
'smtp_mode', default='third_part',
choices=('third_part', 'self_local'),
help='There are two values can be chosen: third_part or '
'self_local. third_part means Zaqar will use the tools '
'from config option smtp_commnd. self_local means the '
'smtp python library will be used.')
smtp_host = cfg.HostAddressOpt(
'smtp_host',
help='The host IP for the email system. It should be '
'set when smtp_mode is set to self_local.')
smtp_port = cfg.PortOpt(
'smtp_port',
help='The port for the email system. It should be set when '
'smtp_mode is set to self_local.')
smtp_user_name = cfg.StrOpt(
'smtp_user_name',
help='The user name for the email system to login. It should '
'be set when smtp_mode is set to self_local.')
smtp_user_password = cfg.StrOpt(
'smtp_user_password',
help='The user password for the email system to login. It '
'should be set when smtp_mode is set to self_local.')
smtp_command = cfg.StrOpt( smtp_command = cfg.StrOpt(
'smtp_command', default='/usr/sbin/sendmail -t -oi', 'smtp_command', default='/usr/sbin/sendmail -t -oi',
help=( help=(
@ -76,6 +109,11 @@ unsubscribe_confirmation_email_template = cfg.DictOpt(
GROUP_NAME = 'notification' GROUP_NAME = 'notification'
ALL_OPTS = [ ALL_OPTS = [
smtp_mode,
smtp_host,
smtp_port,
smtp_user_name,
smtp_user_password,
smtp_command, smtp_command,
max_notifier_workers, max_notifier_workers,
require_confirmation, require_confirmation,

View File

@ -16,6 +16,7 @@
from email.mime import text from email.mime import text
import json import json
from six.moves import urllib_parse from six.moves import urllib_parse
import smtplib
import subprocess import subprocess
from oslo_log import log as logging from oslo_log import log as logging
@ -67,8 +68,6 @@ class MailtoTask(object):
conf_n = kwargs.get('conf').notification conf_n = kwargs.get('conf').notification
try: try:
for message in messages: for message in messages:
p = subprocess.Popen(conf_n.smtp_command.split(' '),
stdin=subprocess.PIPE)
# Send confirmation email to subscriber. # Send confirmation email to subscriber.
if (message.get('Message_Type') == if (message.get('Message_Type') ==
MessageType.SubscriptionConfirmation.name): MessageType.SubscriptionConfirmation.name):
@ -98,7 +97,23 @@ class MailtoTask(object):
msg["from"] = subscription['options'].get('from', '') msg["from"] = subscription['options'].get('from', '')
subject_opt = subscription['options'].get('subject', '') subject_opt = subscription['options'].get('subject', '')
msg["subject"] = params.get('subject', subject_opt) msg["subject"] = params.get('subject', subject_opt)
p.communicate(msg.as_string()) if conf_n.smtp_mode == 'third_part':
p = subprocess.Popen(conf_n.smtp_command.split(' '),
stdin=subprocess.PIPE)
p.communicate(msg.as_string())
elif conf_n.smtp_mode == 'self_local':
sender = smtplib.SMTP_SSL(conf_n.smtp_host,
conf_n.smtp_port)
sender.set_debuglevel(1)
sender.ehlo(conf_n.smtp_host)
try:
sender.login(conf_n.smtp_user_name,
conf_n.smtp_user_password)
except smtplib.SMTPException:
LOG.error("Failed to connect to the SMTP service")
continue
sender.sendmail(msg['from'], msg['to'], msg.as_string())
LOG.debug("Send mail successfully: %s", msg.as_string()) LOG.debug("Send mail successfully: %s", msg.as_string())
except OSError as err: except OSError as err:
LOG.exception('Failed to create process for sendmail, ' LOG.exception('Failed to create process for sendmail, '

View File

@ -209,6 +209,7 @@ class NotifierTest(testing.TestBase):
queue_ctlr.get = mock.Mock(return_value={}) queue_ctlr.get = mock.Mock(return_value={})
driver = notifier.NotifierDriver(subscription_controller=ctlr, driver = notifier.NotifierDriver(subscription_controller=ctlr,
queue_controller=queue_ctlr) queue_controller=queue_ctlr)
ctlr.driver.conf.notification.smtp_mode = 'third_part'
called = set() called = set()
msg = ('Content-Type: text/plain; charset="us-ascii"\n' msg = ('Content-Type: text/plain; charset="us-ascii"\n'
'MIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nto:' 'MIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nto:'