From 0869765536cd036221f6cd12921bac18e3e3df46 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Jun 09 2020 11:57:38 +0000 Subject: Add a jinja2 e-mail template for EPN Add options for character set (default utf8) and message subtype (default plain). This will allow for more control for users to do either HTML mail or use ascii for the character set so the attachment is not base64-encoded to make it easier for all mail clients. Collect first and last name as well for each user in order to provide more options for the template engine. Make the From address configurable, defaulting to noreply@ipa_domain Make Subject configurable too. Don't rely on the MTA to set Message-Id: set it using the email module. Fixes: https://pagure.io/freeipa/issue/3687 Signed-off-by: Rob Crittenden Reviewed-By: Michal Polovka Reviewed-By: Christian Heimes Reviewed-By: Michal Polovka Reviewed-By: Christian Heimes --- diff --git a/client/man/epn.conf.5 b/client/man/epn.conf.5 index 753728c..f63fd17 100644 --- a/client/man/epn.conf.5 +++ b/client/man/epn.conf.5 @@ -66,8 +66,19 @@ Specifies the number of seconds to wait for SMTP to respond. Default 60. .B smtp_security Specifies the type of secure connection to make. Options are: none, starttls and ssl. The default is none. .TP +.B mail_from
+Specifies the From: e-mal address value in the e-mails sent. The default is +noreply@ipadefaultemaildomain. This value can be found by running +.I ipa config-show +.TP .B notify_ttls -This is the list of days before a password expiration when ipa-epn shoould notify a user that their password will soon require a reset. If this value is not specified then the default list will be used: 28, 14, 7, 3, 1. +This is the list of days before a password expiration when ipa-epn should notify a user that their password will soon require a reset. If this value is not specified then the default list will be used: 28, 14, 7, 3, 1. +.TP +.B msg_charset +Set the character set of the message. The default is utf8. This will result in he body of the message being base64-encoded. +.TP +.B msg_subtype +Set the message's MIME sub-content type. The default is plain. .SH "FILES" .TP .I /etc/ipa/epn.conf diff --git a/client/man/ipa-epn.1 b/client/man/ipa-epn.1 index 18c8d78..33b9ee9 100644 --- a/client/man/ipa-epn.1 +++ b/client/man/ipa-epn.1 @@ -52,6 +52,18 @@ The \fB\-\-dry\-run\fR CLI option is intented to test ipa\-epn's configuration. For instance, if notify_ttls is set to 21, 14, 3, \fB\-\-dry-run\fR would display the list of users whose passwords would expire in 21, 14, and 3 days in the future. +.SH "TEMPLATE" +The template for the e\-mail message is contained in /etc/ipa/epn/expire_msg.template. The following template variables are available. +.TP +User ID: uid +.TP +Full name: fullname +.TP +First name: first +.TP +Last name: Last +.TP +Password expiration date: expiration .SH "EXAMPLES" .nf diff --git a/freeipa.spec.in b/freeipa.spec.in index c4dcfd0..6009c32 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -1355,6 +1355,7 @@ fi %{_mandir}/man5/epn.conf.5* %attr(644,root,root) %{_unitdir}/ipa-epn.service %attr(644,root,root) %{_unitdir}/ipa-epn.timer +%attr(644,root,root) %{_sysconfdir}/ipa/epn/expire_msg.template %files -n python3-ipaclient %doc README.md Contributors.txt diff --git a/install/share/Makefile.am b/install/share/Makefile.am index 0fc173e..344ffdf 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -106,3 +106,8 @@ dist_app_DATA = \ kdcproxyconfdir = $(IPA_SYSCONF_DIR)/kdcproxy dist_kdcproxyconf_DATA = \ kdcproxy.conf + +epnconfdir = $(IPA_SYSCONF_DIR)/epn +dist_epnconf_DATA = \ + expire_msg.template \ + $(NULL) diff --git a/install/share/expire_msg.template b/install/share/expire_msg.template new file mode 100644 index 0000000..0d0f792 --- /dev/null +++ b/install/share/expire_msg.template @@ -0,0 +1,5 @@ +Hi {{ fullname }}, + +Your password will expire on {{ expiration }}. + +Please change it as soon as possible. diff --git a/ipaclient/install/ipa_epn.py b/ipaclient/install/ipa_epn.py index a7446c7..dfe8d36 100644 --- a/ipaclient/install/ipa_epn.py +++ b/ipaclient/install/ipa_epn.py @@ -36,6 +36,7 @@ from email.utils import formataddr, formatdate from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header +from email.utils import make_msgid from ipaclient.install.client import is_ipa_client_installed from ipaplatform.paths import paths @@ -44,6 +45,8 @@ from ipalib.install import sysrestore from ipapython import admintool, ipaldap from ipapython.dn import DN +from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError + EPN_CONF = "/etc/ipa/epn.conf" EPN_CONFIG = { @@ -54,7 +57,11 @@ EPN_CONFIG = { "smtp_timeout": 60, "smtp_security": "none", "smtp_admin": "root@localhost", + "mail_from": None, "notify_ttls": "28,14,7,3,1", + "msg_charset": "utf8", + "msg_subtype": "plain", + "msg_subject": "Your password will expire soon.", } logger = logging.getLogger(__name__) @@ -116,7 +123,7 @@ class EPNUserList: def add(self, entry): """Parses and appends an LDAP user entry with the uid, cn, - krbpasswordexpiration and mail attributes. + givenname, sn, krbpasswordexpiration and mail attributes. """ try: self._sorted = False @@ -124,6 +131,8 @@ class EPNUserList: dict( uid=str(entry["uid"].pop(0)), cn=str(entry["cn"].pop(0)), + givenname=str(entry.get("givenname", "")), + sn=str(entry["sn"].pop(0)), krbpasswordexpiration=str( entry["krbpasswordexpiration"].pop(0) ), @@ -190,6 +199,8 @@ class EPN(admintool.AdminTool): self._ldap_data = [] self._date_ranges = [] self._mailer = None + self.env = None + self.default_email_domain = None @classmethod def add_options(cls, parser): @@ -241,6 +252,7 @@ class EPN(admintool.AdminTool): self._validate_configuration() self._parse_configuration() self._get_connection() + self._read_ipa_configuration() drop_privileges() if self.options.to_nbdays: self._build_cli_date_ranges() @@ -258,6 +270,8 @@ class EPN(admintool.AdminTool): smtp_username=api.env.smtp_user, smtp_password=api.env.smtp_password, x_mailer=self.command_name, + msg_subtype=api.env.msg_subtype, + msg_charset=api.env.msg_charset, ) self._send_emails() @@ -352,6 +366,17 @@ class EPN(admintool.AdminTool): ) ) + loader = FileSystemLoader(os.path.join(api.env.confdir, 'epn')) + self.env = Environment(loader=loader) + + def _read_ipa_configuration(self): + """Get the IPA configuration""" + api.Backend.rpcclient.connect() + result = api.Command.config_show()['result'] + self.default_email_domain = result.get('ipadefaultemaildomain', + [None])[0] + api.Backend.rpcclient.disconnect() + def _get_connection(self): """Create a connection to LDAP and bind to it. """ @@ -389,7 +414,8 @@ class EPN(admintool.AdminTool): ) search_base = DN(api.env.container_user, api.env.basedn) - attrs_list = ["uid", "krbpasswordexpiration", "mail", "cn"] + attrs_list = ["uid", "krbpasswordexpiration", "mail", "cn", + "gn", "surname"] search_filter = ( "(&(!(nsaccountlock=TRUE)) \ @@ -439,18 +465,29 @@ class EPN(admintool.AdminTool): logger.error("IPA-EPN: mailer was not configured.") return else: + try: + template = self.env.get_template("expire_msg.template") + except TemplateSyntaxError as e: + raise RuntimeError("Parsing template %s failed: %s" % + (e.filename, e)) + if api.env.mail_from: + mail_from = api.env.mail_from + else: + mail_from = "noreply@%s" % self.default_email_domain while self._expiring_password_user_list: entry = self._expiring_password_user_list.pop() + body = template.render( + uid=entry["uid"], + first=entry["givenname"], + last=entry["sn"], + fullname=entry["cn"], + expiration=entry["krbpasswordexpiration"], + ) self._mailer.send_message( - mail_subject="Your password is expiring.", - mail_body=os.linesep.join( - [ - "Hi %s, Your password will expire on %s." - % (entry["cn"], entry["krbpasswordexpiration"]), - "Please change it as soon as possible.", - ] - ), + mail_subject=api.env.msg_subject, + mail_body=body, subscribers=ast.literal_eval(entry["mail"]), + mail_from=mail_from, ) now = datetime.utcnow() expdate = datetime.strptime( @@ -524,6 +561,7 @@ class MTAClient: self._disconnect() def send_message(self, message_str=None, subscribers=None): + result = None try: result = self._conn.sendmail( api.env.smtp_admin, subscribers, message_str, @@ -640,18 +678,17 @@ class MailUserAgent: smtp_username=None, smtp_password=None, x_mailer=None, + msg_subtype="plain", + msg_charset="utf8", ): self._x_mailer = x_mailer self._subject = None self._body = None self._subscribers = None - self._subtype = None - # Let it be plain or html? - self._subtype = "plain" - # UTF-8 only for now - self._charset = "utf-8" + self._subtype = msg_subtype + self._charset = msg_charset self._msg = None self._message_str = None @@ -669,18 +706,20 @@ class MailUserAgent: self._mta_client.cleanup() def send_message( - self, mail_subject=None, mail_body=None, subscribers=None + self, mail_subject=None, mail_body=None, subscribers=None, + mail_from=None ): """Given mail_subject, mail_body, and subscribers, composes the message and sends it. """ - if None in [mail_subject, mail_body, subscribers]: + if None in [mail_subject, mail_body, subscribers, mail_from]: logger.error("IPA-EPN: Tried to send an empty message.") return False self._compose_message( mail_subject=mail_subject, mail_body=mail_body, subscribers=subscribers, + mail_from=mail_from, ) self._mta_client.send_message( message_str=self._message_str, subscribers=subscribers @@ -688,7 +727,7 @@ class MailUserAgent: return True def _compose_message( - self, mail_subject=None, mail_body=None, subscribers=None + self, mail_subject, mail_body, subscribers, mail_from ): """The composer creates a MIME multipart message. """ @@ -698,12 +737,11 @@ class MailUserAgent: self._subscribers = subscribers self._msg = MIMEMultipart(_charset=self._charset) - self._msg["From"] = formataddr( - ("IPA-EPN", "noreply@%s" % socket.getfqdn()) - ) + self._msg["From"] = formataddr(("IPA-EPN", mail_from)) self._msg["To"] = ", ".join(self._subscribers) self._msg["Date"] = formatdate(localtime=True) self._msg["Subject"] = Header(self._subject, self._charset) + self._msg["Message-Id"] = make_msgid() self._msg.preamble = "Multipart message" if "X-Mailer" not in self._msg and self._x_mailer: self._msg.add_header("X-Mailer", self._x_mailer)