[gen] SMTP and LDAP configuration updated. Module appy.gen.mail can now be used independently of a gen-application.
This commit is contained in:
parent
4947e2956c
commit
ecc3f07a09
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
85
gen/mail.py
85
gen/mail.py
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.'''
|
||||||
|
|
|
@ -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.'''
|
Loading…
Reference in a new issue