[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.'''
import sys
from appy.shared.ldap_connector import LdapConnector
from appy.shared.ldap import LdapConnector
# ------------------------------------------------------------------------------
class LdapTester:

View file

@ -418,7 +418,8 @@ class Transition:
# (Allowed, State) need to be updated here.
if reindex and not obj.isTemporary(): obj.reindex()
# 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)
# Return a message to the user if needed
if not doSay or (name == '_init_'): return

View file

@ -52,74 +52,6 @@ class Tool(Model):
class User(Model):
'''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:
'''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?
groupsForGlobalRoles = False
# 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
# 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
# user interface (CSS, Javascript and image files) is folder "ui" within
# this app.

View file

@ -319,6 +319,20 @@ class ZopeInstaller:
import Products
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):
self.installDependencies()
self.patchZope()
@ -332,6 +346,8 @@ class ZopeInstaller:
self.installCatalog()
self.installTool()
self.installUi()
# Log connections to external servers (ldap, mail...)
self.logConnectedServers()
# Perform migrations if required
Migrator(self).run()
# Update Appy version in the database

View file

@ -8,31 +8,62 @@ from email.Header import Header
from appy.shared.utils import sequenceTypes
# ------------------------------------------------------------------------------
def sendMail(tool, to, subject, body, attachments=None):
'''Sends a mail, via p_tool.mailHost, 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]>".
class MailConfig:
'''Parameters for conneting to a SMTP server.'''
def __init__(self, fromName=None, fromEmail='info@appyframework.org',
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:
1. a tuple (fileName, fileContent): "fileName" is the name of the file
as a string; "fileContent" is the file content, also as a string;
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
fromAddress = tool.mailFrom
if not tool.mailEnabled or not tool.mailHost:
if not tool.mailHost:
fromAddress = config.getFrom()
if not config.enabled or not config.server:
if not config.server:
msg = ' (no mailhost defined)'
else:
msg = ''
tool.log('mail disabled%s: should send mail from %s to %s.' % \
if log:
log('mail disabled%s: should send mail from %s to %s.' % \
(msg, fromAddress, str(to)))
tool.log('subject: %s' % subject)
tool.log('body: %s' % body)
if attachments:
tool.log('%d attachment(s).' % len(attachments))
log('subject: %s' % subject)
log('body: %s' % body)
if attachments and log: log('%d attachment(s).' % len(attachments))
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))
# Create the base MIME message
body = MIMEText(body, 'plain', 'utf-8')
@ -73,26 +104,18 @@ def sendMail(tool, to, subject, body, attachments=None):
msg.attach(part)
# Send the email
try:
smtpInfo = tool.mailHost.split(':', 3)
login = password = None
if len(smtpInfo) == 2:
# 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)
smtpServer = smtplib.SMTP(config.server, port=config.port)
if config.login:
smtpServer.login(config.login, config.password)
res = smtpServer.sendmail(fromAddress, [to], msg.as_string())
smtpServer.quit()
if res:
tool.log('could not send mail to some recipients. %s' % str(res),
if res and log:
log('could not send mail to some recipients. %s' % str(res),
type='warning')
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:
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):

View file

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

View file

@ -656,7 +656,9 @@ class ToolWrapper(AbstractWrapper):
def sendMail(self, to, subject, body, attachments=None):
'''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):
'''Check doc @ToolMixin::formatDate.'''

View file

@ -5,6 +5,78 @@ except ImportError:
# For people that do not care about ldap.
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:
'''This class manages the communication with a LDAP server.'''