From e1c62707d387dc07ef8333c0bc271313d26facf3 Mon Sep 17 00:00:00 2001 From: changyufei Date: Mon, 2 Apr 2018 16:53:39 +0800 Subject: [PATCH] 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 --- ...ion-by-internal-tool-08910ab2247c3864.yaml | 8 + samples/zaqar/sendmail.py | 228 ++++++++++++++++++ zaqar/conf/notification.py | 38 +++ zaqar/notification/tasks/mailto.py | 21 +- .../tests/unit/notification/test_notifier.py | 1 + 5 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml create mode 100644 samples/zaqar/sendmail.py diff --git a/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml b/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml new file mode 100644 index 000000000..20efe51c4 --- /dev/null +++ b/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml @@ -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. diff --git a/samples/zaqar/sendmail.py b/samples/zaqar/sendmail.py new file mode 100644 index 000000000..ff172189e --- /dev/null +++ b/samples/zaqar/sendmail.py @@ -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""" +
+
+
+
+
+
+
+
+
+
+
+

+ /////////////////// + Respected + {corp_name}user + /////////////////// +

+
+
+
+
+

{confirm_or_alarm}

+
+
+
+
+

{corp_name}— + {home_link}

+
+
+
+""" + +mail_confirm_link = u""" +Your mailbox will be used for receiving system notifications. +If you confirm, click the following link: +Activation link +""" + +mail_alarm_info = u""" + Your alarm information is as follows:{reason}
+ Alarm level:{severity}
+ Alarm name:{alarm_name}
+ 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())) diff --git a/zaqar/conf/notification.py b/zaqar/conf/notification.py index 38653ce1b..e1a7e8f7a 100644 --- a/zaqar/conf/notification.py +++ b/zaqar/conf/notification.py @@ -14,6 +14,39 @@ 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', default='/usr/sbin/sendmail -t -oi', help=( @@ -76,6 +109,11 @@ unsubscribe_confirmation_email_template = cfg.DictOpt( GROUP_NAME = 'notification' ALL_OPTS = [ + smtp_mode, + smtp_host, + smtp_port, + smtp_user_name, + smtp_user_password, smtp_command, max_notifier_workers, require_confirmation, diff --git a/zaqar/notification/tasks/mailto.py b/zaqar/notification/tasks/mailto.py index 1691b6b70..3df4000e0 100644 --- a/zaqar/notification/tasks/mailto.py +++ b/zaqar/notification/tasks/mailto.py @@ -16,6 +16,7 @@ from email.mime import text import json from six.moves import urllib_parse +import smtplib import subprocess from oslo_log import log as logging @@ -67,8 +68,6 @@ class MailtoTask(object): conf_n = kwargs.get('conf').notification try: for message in messages: - p = subprocess.Popen(conf_n.smtp_command.split(' '), - stdin=subprocess.PIPE) # Send confirmation email to subscriber. if (message.get('Message_Type') == MessageType.SubscriptionConfirmation.name): @@ -98,7 +97,23 @@ class MailtoTask(object): msg["from"] = subscription['options'].get('from', '') subject_opt = subscription['options'].get('subject', '') 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()) except OSError as err: LOG.exception('Failed to create process for sendmail, ' diff --git a/zaqar/tests/unit/notification/test_notifier.py b/zaqar/tests/unit/notification/test_notifier.py index 9c161e73f..b2ced9656 100644 --- a/zaqar/tests/unit/notification/test_notifier.py +++ b/zaqar/tests/unit/notification/test_notifier.py @@ -209,6 +209,7 @@ class NotifierTest(testing.TestBase): queue_ctlr.get = mock.Mock(return_value={}) driver = notifier.NotifierDriver(subscription_controller=ctlr, queue_controller=queue_ctlr) + ctlr.driver.conf.notification.smtp_mode = 'third_part' called = set() msg = ('Content-Type: text/plain; charset="us-ascii"\n' 'MIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nto:'