diff --git a/bin/checkldap.py b/bin/checkldap.py index a517a38..2b978ef 100644 --- a/bin/checkldap.py +++ b/bin/checkldap.py @@ -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: diff --git a/fields/workflow.py b/fields/workflow.py index 8e0522d..2b86f4b 100644 --- a/fields/workflow.py +++ b/fields/workflow.py @@ -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 diff --git a/gen/__init__.py b/gen/__init__.py index 6f3df74..e8b5d06 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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. diff --git a/gen/installer.py b/gen/installer.py index e43d16f..615f574 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -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 diff --git a/gen/mail.py b/gen/mail.py index d286dae..ab52213 100644 --- a/gen/mail.py +++ b/gen/mail.py @@ -8,32 +8,63 @@ 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.' % \ - (msg, fromAddress, str(to))) - tool.log('subject: %s' % subject) - tool.log('body: %s' % body) - if attachments: - tool.log('%d attachment(s).' % len(attachments)) + if log: + log('mail disabled%s: should send mail from %s to %s.' % \ + (msg, fromAddress, str(to))) + 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).' % \ - (fromAddress, str(to), subject)) + 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') if attachments: @@ -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), - type='warning') + 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): diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 9221d23..ea494c2 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -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: diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 5ace912..d99934e 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -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.''' diff --git a/shared/ldap_connector.py b/shared/ldap.py similarity index 53% rename from shared/ldap_connector.py rename to shared/ldap.py index 896be8c..29f6298 100644 --- a/shared/ldap_connector.py +++ b/shared/ldap.py @@ -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.'''