[gen] SMTP and LDAP configuration updated. Module appy.gen.mail can now be used independently of a gen-application.

This commit is contained in:
Gaetan Delannay 2014-09-18 11:08:29 +02:00
parent 4947e2956c
commit ecc3f07a09
8 changed files with 155 additions and 107 deletions

View file

@ -1,6 +1,6 @@
'''This script allows to check a LDAP connection.''' '''This script allows to check a LDAP connection.'''
import sys import sys
from appy.shared.ldap_connector import LdapConnector from appy.shared.ldap import LdapConnector
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class LdapTester: class LdapTester:

View file

@ -418,7 +418,8 @@ class Transition:
# (Allowed, State) need to be updated here. # (Allowed, State) need to be updated here.
if reindex and not obj.isTemporary(): obj.reindex() if reindex and not obj.isTemporary(): obj.reindex()
# Send notifications if needed # Send notifications if needed
if doNotify and self.notify and obj.getTool(True).mailEnabled: mail = obj.getTool().getProductConfig(True).mail
if doNotify and self.notify and mail and mail.enabled:
sendNotification(obj.appy(), self, name, wf) sendNotification(obj.appy(), self, name, wf)
# Return a message to the user if needed # Return a message to the user if needed
if not doSay or (name == '_init_'): return if not doSay or (name == '_init_'): return

View file

@ -52,74 +52,6 @@ class Tool(Model):
class User(Model): class User(Model):
'''Subclass me to extend or modify the User class.''' '''Subclass me to extend or modify the User class.'''
# ------------------------------------------------------------------------------
class LdapConfig:
'''Parameters for authenticating users to an LDAP server.'''
ldapAttributes = { 'loginAttribute':None, 'emailAttribute':'email',
'fullNameAttribute':'title',
'firstNameAttribute':'firstName',
'lastNameAttribute':'name' }
def __init__(self):
self.server = '' # Name of the LDAP server
self.port = None # Port for this server.
# Login and password of the technical power user that the Appy
# application will use to connect to the LDAP.
self.adminLogin = ''
self.adminPassword = ''
# LDAP attribute to use as login for authenticating users.
self.loginAttribute = 'dn' # Can also be "mail", "sAMAccountName", "cn"
# LDAP attributes for storing email
self.emailAttribute = None
# LDAP attribute for storing full name (first + last name)
self.fullNameAttribute = None
# Alternately, LDAP attributes for storing 1st & last names separately.
self.firstNameAttribute = None
self.lastNameAttribute = None
# LDAP classes defining the users stored in the LDAP.
self.userClasses = ('top', 'person')
self.baseDn = '' # Base DN where to find users in the LDAP.
self.scope = 'SUBTREE' # Scope of the search within self.baseDn
def getServerUri(self):
'''Returns the complete URI for accessing the LDAP, ie
"ldap://some.ldap.server:389".'''
port = self.port or 389
return 'ldap://%s:%d' % (self.server, port)
def getUserFilterValues(self, login):
'''Gets the filter values required to perform a query for finding user
corresponding to p_login in the LDAP.'''
res = [(self.loginAttribute, login)]
for userClass in self.userClasses:
res.append( ('objectClass', userClass) )
return res
def getUserAttributes(self):
'''Gets the attributes we want to get from the LDAP for characterizing
a user.'''
res = []
for name in self.ldapAttributes.iterkeys():
if getattr(self, name):
res.append(getattr(self, name))
return res
def getUserParams(self, ldapData):
'''Formats the user-related p_ldapData retrieved from the ldap, as a
dict of params usable for creating or updating the corresponding
Appy user.'''
res = {}
for name, appyName in self.ldapAttributes.iteritems():
if not appyName: continue
# Get the name of the attribute as known in the LDAP.
ldapName = getattr(self, name)
if not ldapName: continue
if ldapData.has_key(ldapName) and ldapData[ldapName]:
value = ldapData[ldapName]
if isinstance(value, list): value = value[0]
res[appyName] = value
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Config: class Config:
'''If you want to specify some configuration parameters for appy.gen and '''If you want to specify some configuration parameters for appy.gen and
@ -169,8 +101,11 @@ class Config:
# Create a group for every global role? # Create a group for every global role?
groupsForGlobalRoles = False groupsForGlobalRoles = False
# When using a LDAP for authenticating users, place an instance of class # When using a LDAP for authenticating users, place an instance of class
# LdapConfig above in the field below. # appy.shared.ldap.LdapConfig in the field below.
ldap = None ldap = None
# When using a SMTP mail server for sending emails from your app, place an
# instance of class appy.gen.mail.MailConfig in the field below.
mail = None
# For an app, the default folder where to look for static content for the # For an app, the default folder where to look for static content for the
# user interface (CSS, Javascript and image files) is folder "ui" within # user interface (CSS, Javascript and image files) is folder "ui" within
# this app. # this app.

View file

@ -319,6 +319,20 @@ class ZopeInstaller:
import Products import Products
install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {}) install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {})
def logConnectedServers(self):
'''Simply log the names of servers (LDAP, mail...) this app wants to
connnect to.'''
cfg = self.config.appConfig
servers = []
# Are we connected to a LDAP server for authenticating our users?
for sv in ('ldap', 'mail'):
if not getattr(cfg, sv): continue
svConfig = getattr(cfg, sv)
enabled = svConfig.enabled and 'enabled' or 'disabled'
servers.append('%s (%s, %s)' % (svConfig.server, sv, enabled))
if servers:
self.logger.info('server(s) %s configured.' % ', '.join(servers))
def install(self): def install(self):
self.installDependencies() self.installDependencies()
self.patchZope() self.patchZope()
@ -332,6 +346,8 @@ class ZopeInstaller:
self.installCatalog() self.installCatalog()
self.installTool() self.installTool()
self.installUi() self.installUi()
# Log connections to external servers (ldap, mail...)
self.logConnectedServers()
# Perform migrations if required # Perform migrations if required
Migrator(self).run() Migrator(self).run()
# Update Appy version in the database # Update Appy version in the database

View file

@ -8,32 +8,63 @@ from email.Header import Header
from appy.shared.utils import sequenceTypes from appy.shared.utils import sequenceTypes
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def sendMail(tool, to, subject, body, attachments=None): class MailConfig:
'''Sends a mail, via p_tool.mailHost, to p_to (a single email recipient or '''Parameters for conneting to a SMTP server.'''
a list of recipients). Every (string) recipient can be an email address def __init__(self, fromName=None, fromEmail='info@appyframework.org',
or a string of the form "[name] <[email]>". server='localhost', port=25, login=None, password=None,
enabled=True):
# The name that will appear in the "from" part of the messages
self.fromName = fromName
# The email that will appear in the "from" part of the messages
self.fromEmail = fromEmail
# The SMTP server address
self.server = server
# The SMTP server port
self.port = port
# Optional credentials to the SMTP server.
self.login = login
self.password = password
# Is this server connection enabled ?
self.enabled = enabled
def getFrom(self):
'''Gets the "from" part of the messages to send.'''
if self.fromName: return '%s <%s>' % (self.fromName, self.fromEmail)
return self.fromEmail
# ------------------------------------------------------------------------------
def sendMail(config, to, subject, body, attachments=None, log=None):
'''Sends a mail, via the smtp server defined in the p_config (an instance of
appy.gen.mail.MailConfig above), to p_to (a single email recipient or a
list of recipients). Every (string) recipient can be an email address or
a string of the form "[name] <[email]>".
p_attachment must be a list or tuple whose elements can have 2 forms: p_attachment must be a list or tuple whose elements can have 2 forms:
1. a tuple (fileName, fileContent): "fileName" is the name of the file 1. a tuple (fileName, fileContent): "fileName" is the name of the file
as a string; "fileContent" is the file content, also as a string; as a string; "fileContent" is the file content, also as a string;
2. a appy.fields.file.FileInfo instance. 2. a appy.fields.file.FileInfo instance.
p_log can be a function/method accepting a single string arg.
''' '''
if not config:
if log: log('Must send mail but no smtp server configured.')
return
# Just log things if mail is disabled # Just log things if mail is disabled
fromAddress = tool.mailFrom fromAddress = config.getFrom()
if not tool.mailEnabled or not tool.mailHost: if not config.enabled or not config.server:
if not tool.mailHost: if not config.server:
msg = ' (no mailhost defined)' msg = ' (no mailhost defined)'
else: else:
msg = '' msg = ''
tool.log('mail disabled%s: should send mail from %s to %s.' % \ if log:
(msg, fromAddress, str(to))) log('mail disabled%s: should send mail from %s to %s.' % \
tool.log('subject: %s' % subject) (msg, fromAddress, str(to)))
tool.log('body: %s' % body) log('subject: %s' % subject)
if attachments: log('body: %s' % body)
tool.log('%d attachment(s).' % len(attachments)) if attachments and log: log('%d attachment(s).' % len(attachments))
return return
tool.log('sending mail from %s to %s (subject: %s).' % \ if log: log('sending mail from %s to %s (subject: %s).' % \
(fromAddress, str(to), subject)) (fromAddress, str(to), subject))
# Create the base MIME message # Create the base MIME message
body = MIMEText(body, 'plain', 'utf-8') body = MIMEText(body, 'plain', 'utf-8')
if attachments: if attachments:
@ -73,26 +104,18 @@ def sendMail(tool, to, subject, body, attachments=None):
msg.attach(part) msg.attach(part)
# Send the email # Send the email
try: try:
smtpInfo = tool.mailHost.split(':', 3) smtpServer = smtplib.SMTP(config.server, port=config.port)
login = password = None if config.login:
if len(smtpInfo) == 2: smtpServer.login(config.login, config.password)
# We simply have server and port
server, port = smtpInfo
else:
# We also have login and password
server, port, login, password = smtpInfo
smtpServer = smtplib.SMTP(server, port=int(port))
if login:
smtpServer.login(login, password)
res = smtpServer.sendmail(fromAddress, [to], msg.as_string()) res = smtpServer.sendmail(fromAddress, [to], msg.as_string())
smtpServer.quit() smtpServer.quit()
if res: if res and log:
tool.log('could not send mail to some recipients. %s' % str(res), log('could not send mail to some recipients. %s' % str(res),
type='warning') type='warning')
except smtplib.SMTPException, e: except smtplib.SMTPException, e:
tool.log('mail sending failed: %s' % str(e), type='error') if log: log('mail sending failed: %s' % str(e), type='error')
except socket.error, se: except socket.error, se:
tool.log('mail sending failed: %s' % str(se), type='error') if log: log('mail sending failed: %s' % str(se), type='error')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def sendNotification(obj, transition, transitionName, workflow): def sendNotification(obj, transition, transitionName, workflow):

View file

@ -8,11 +8,10 @@ from appy.gen import utils as gutils
from appy.gen.mixins import BaseMixin from appy.gen.mixins import BaseMixin
from appy.gen.wrappers import AbstractWrapper from appy.gen.wrappers import AbstractWrapper
from appy.gen.descriptors import ClassDescriptor from appy.gen.descriptors import ClassDescriptor
from appy.gen.mail import sendMail
from appy.shared import mimeTypes from appy.shared import mimeTypes
from appy.shared import utils as sutils from appy.shared import utils as sutils
from appy.shared.data import languages from appy.shared.data import languages
from appy.shared.ldap_connector import LdapConnector from appy.shared.ldap import LdapConnector
try: try:
from AccessControl.ZopeSecurityPolicy import _noroles from AccessControl.ZopeSecurityPolicy import _noroles
except ImportError: except ImportError:
@ -1334,7 +1333,7 @@ class ToolMixin(BaseMixin):
subject = self.translate('reinit_password') subject = self.translate('reinit_password')
map = {'url':initUrl, 'siteUrl':self.getSiteUrl()} map = {'url':initUrl, 'siteUrl':self.getSiteUrl()}
body= self.translate('reinit_password_body', mapping=map, format='text') body= self.translate('reinit_password_body', mapping=map, format='text')
sendMail(appyTool, email, subject, body) appyTool.sendMail(email, subject, body)
return self.goto(backUrl, msg) return self.goto(backUrl, msg)
def doPasswordReinit(self): def doPasswordReinit(self):
@ -1363,7 +1362,7 @@ class ToolMixin(BaseMixin):
map = {'password': newPassword, 'siteUrl': siteUrl} map = {'password': newPassword, 'siteUrl': siteUrl}
body = self.translate('new_password_body', mapping=map, body = self.translate('new_password_body', mapping=map,
format='text') format='text')
sendMail(appyTool, email, subject, body) appyTool.sendMail(email, subject, body)
os.remove(tokenFile) os.remove(tokenFile)
res = self.goto(siteUrl, self.translate('new_password_sent')) res = self.goto(siteUrl, self.translate('new_password_sent'))
if not res: if not res:

View file

@ -656,7 +656,9 @@ class ToolWrapper(AbstractWrapper):
def sendMail(self, to, subject, body, attachments=None): def sendMail(self, to, subject, body, attachments=None):
'''Sends a mail. See doc for appy.gen.mail.sendMail.''' '''Sends a mail. See doc for appy.gen.mail.sendMail.'''
sendMail(self, to, subject, body, attachments=attachments) mailConfig = self.o.getProductConfig(True).mail
sendMail(mailConfig, to, subject, body, attachments=attachments,
log=self.log)
def formatDate(self, date, format=None, withHour=True, language=None): def formatDate(self, date, format=None, withHour=True, language=None):
'''Check doc @ToolMixin::formatDate.''' '''Check doc @ToolMixin::formatDate.'''

View file

@ -5,6 +5,78 @@ except ImportError:
# For people that do not care about ldap. # For people that do not care about ldap.
ldap = None ldap = None
# ------------------------------------------------------------------------------
class LdapConfig:
'''Parameters for authenticating users to an LDAP server. This class is
used by gen-applications. For a pure, appy-independent LDAP connector,
see the class LdapConnector below.'''
ldapAttributes = { 'loginAttribute':None, 'emailAttribute':'email',
'fullNameAttribute':'title',
'firstNameAttribute':'firstName',
'lastNameAttribute':'name' }
def __init__(self):
self.server = '' # Name of the LDAP server
self.port = None # Port for this server.
# Login and password of the technical power user that the Appy
# application will use to connect to the LDAP.
self.adminLogin = ''
self.adminPassword = ''
# LDAP attribute to use as login for authenticating users.
self.loginAttribute = 'dn' # Can also be "mail", "sAMAccountName", "cn"
# LDAP attributes for storing email
self.emailAttribute = None
# LDAP attribute for storing full name (first + last name)
self.fullNameAttribute = None
# Alternately, LDAP attributes for storing 1st & last names separately.
self.firstNameAttribute = None
self.lastNameAttribute = None
# LDAP classes defining the users stored in the LDAP.
self.userClasses = ('top', 'person')
self.baseDn = '' # Base DN where to find users in the LDAP.
self.scope = 'SUBTREE' # Scope of the search within self.baseDn
# Is this server connection enabled ?
self.enabled = True
def getServerUri(self):
'''Returns the complete URI for accessing the LDAP, ie
"ldap://some.ldap.server:389".'''
port = self.port or 389
return 'ldap://%s:%d' % (self.server, port)
def getUserFilterValues(self, login):
'''Gets the filter values required to perform a query for finding user
corresponding to p_login in the LDAP.'''
res = [(self.loginAttribute, login)]
for userClass in self.userClasses:
res.append( ('objectClass', userClass) )
return res
def getUserAttributes(self):
'''Gets the attributes we want to get from the LDAP for characterizing
a user.'''
res = []
for name in self.ldapAttributes.iterkeys():
if getattr(self, name):
res.append(getattr(self, name))
return res
def getUserParams(self, ldapData):
'''Formats the user-related p_ldapData retrieved from the ldap, as a
dict of params usable for creating or updating the corresponding
Appy user.'''
res = {}
for name, appyName in self.ldapAttributes.iteritems():
if not appyName: continue
# Get the name of the attribute as known in the LDAP.
ldapName = getattr(self, name)
if not ldapName: continue
if ldapData.has_key(ldapName) and ldapData[ldapName]:
value = ldapData[ldapName]
if isinstance(value, list): value = value[0]
res[appyName] = value
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class LdapConnector: class LdapConnector:
'''This class manages the communication with a LDAP server.''' '''This class manages the communication with a LDAP server.'''