SVN: CMF/trunk/C - added 'email_charset' property and 'makeEmail' function

Yvo Schubbe <[email protected]>
Newsgroups gmane.comp.web.zope.cmf.cvs
Message-ID <[email protected]>
Log message for revision 70654:
  - added 'email_charset' property and 'makeEmail' function
  - fixed encoding issues in registered_email and password_email

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFDefault/profiles/default/properties.xml
  U   CMF/trunk/CMFDefault/skins/zpt_control/portal_config_control.py
  U   CMF/trunk/CMFDefault/skins/zpt_generic/password_email.py
  U   CMF/trunk/CMFDefault/skins/zpt_generic/password_email_template.pt
  U   CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_form.py
  U   CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_template.pt
  U   CMF/trunk/CMFDefault/skins/zpt_generic/registered_email.py
  U   CMF/trunk/CMFDefault/skins/zpt_generic/registered_email_template.pt
  U   CMF/trunk/CMFDefault/tests/RegistrationTool.txt
  U   CMF/trunk/CMFDefault/tests/test_RegistrationTool.py
  U   CMF/trunk/CMFDefault/utils.py

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CHANGES.txt	2006-10-15 14:49:21 UTC (rev 70654)
@@ -2,12 +2,22 @@
 
   New Features
 
+    - Portal: Added 'email_charset' property.
+
+    - CMFDefault utils: Added 'makeEmail' function.
+
     - CMFDefault.Image and CMFDefault.File: Overridden index_html methods
       add Cache Policy Manager-awareness and thus bring these implementations
       in line with CMFCore.FSFile and CMFCore.FSImage
       (http://www.zope.org/Collectors/CMF/454)
 
+  Bug Fixes
 
+    - CMFDefault skins: Fixed encoding issues in welcome and reminder emails.
+      'password_email' and 'registered_email' now encode their return value
+      correctly, using 'email_charset' and the new 'makeEmail' function.
+
+
 CMF 2.1.0-alpha (2006/10/09)
 
   New Features

Modified: CMF/trunk/CMFDefault/profiles/default/properties.xml
===================================================================
--- CMF/trunk/CMFDefault/profiles/default/properties.xml	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/profiles/default/properties.xml	2006-10-15 14:49:21 UTC (rev 70654)
@@ -8,5 +8,6 @@
     type="string">Portal Administrator</property>
  <property name="validate_email" type="boolean">False</property>
  <property name="default_charset" type="string">utf-8</property>
+ <property name="email_charset" type="string">iso-8859-1</property>
  <property name="enable_permalink" type="boolean">False</property>
 </site>

Modified: CMF/trunk/CMFDefault/skins/zpt_control/portal_config_control.py
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_control/portal_config_control.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_control/portal_config_control.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -7,6 +7,8 @@
 
 if not ptool.hasProperty('default_charset'):
     ptool.manage_addProperty('default_charset', '', 'string')
+if not ptool.hasProperty('email_charset'):
+    ptool.manage_addProperty('email_charset', '', 'string')
 ptool.editProperties(kw)
 
 return context.setStatus(True, _(u'CMF Settings changed.'))

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/password_email.py
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/password_email.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/password_email.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -1,29 +1,25 @@
-##parameters=member=None, password='baz'
+##parameters=member=None, password='secret'
 ##
 from Products.CMFCore.utils import getToolByName
 from Products.CMFDefault.utils import decode
+from Products.CMFDefault.utils import makeEmail
+from Products.CMFDefault.utils import Message as _
 
 atool = getToolByName(script, 'portal_actions')
 ptool = getToolByName(script, 'portal_properties')
 utool = getToolByName(script, 'portal_url')
-default_charset = ptool.getProperty('default_charset')
 portal_url = utool()
 
 
 options = {}
-
-email_from_name = ptool.getProperty('email_from_name')
-email_from_address = ptool.getProperty('email_from_address')
-options['portal_address'] = '%s <%s>' % (email_from_name, email_from_address)
-member_address = member and member.email or '[email protected]'
-options['member_address'] = '<%s>' % member_address
-options['content_type'] = 'text/plain; charset=%s' % default_charset
-
-options['portal_title'] = ptool.title()
 options['password'] = password
 
-rendered = context.password_email_template(**decode(options, script))
-if isinstance(rendered, unicode):
-    return rendered.encode(default_charset)
-else:
-    return rendered
+headers = {}
+headers['Subject'] = _(u'${portal_title}: Membership reminder',
+                      mapping={'portal_title': decode(ptool.title(), script)})
+headers['From'] = '%s <%s>' % (ptool.getProperty('email_from_name'),
+                               ptool.getProperty('email_from_address'))
+headers['To'] = '<%s>' % (member and member.email or '[email protected]')
+
+mtext = context.password_email_template(**decode(options, script))
+return makeEmail(mtext, script, headers)

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/password_email_template.pt
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/password_email_template.pt	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/password_email_template.pt	2006-10-15 14:49:21 UTC (rev 70654)
@@ -1,11 +1,5 @@
 <tal:page i18n:domain="cmf_default"
->Subject: <tal:span i18n:translate=""><tal:span i18n:name="portal_title"
-    tal:content="options/portal_title" />: Membership reminder</tal:span>
-From: <tal:span tal:replace="structure options/portal_address" />
-To: <tal:span tal:replace="structure options/member_address" />
-Content-Type: <tal:span tal:replace="structure options/content_type" />
-
-<tal:span i18n:translate="">Your password: <tal:span i18n:name="password"
+><tal:span i18n:translate="">Your password: <tal:span i18n:name="password"
     tal:content="options/password | default">baz</tal:span></tal:span>
 
 <tal:span i18n:translate="">Request made by IP <tal:span i18n:name="ip"

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_form.py
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_form.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_form.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -30,6 +30,7 @@
                     'validate_email': ptool.getProperty('validate_email'),
                     'default_charset':
                                     ptool.getProperty('default_charset', ''),
+                    'email_charset': ptool.getProperty('email_charset', ''),
                     'listButtonInfos': tuple(buttons) }
 
 return context.reconfig_template(**decode(options, script))

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_template.pt
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_template.pt	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/reconfig_template.pt	2006-10-15 14:49:21 UTC (rev 70654)
@@ -94,6 +94,17 @@
   </td>
  </tr>
  <tr>
+  <th i18n:translate="">Portal email encoding</th>
+  <td>
+   <input name="email_charset" value=""
+      tal:attributes="value form/email_charset" />
+   <dl class="FieldHelp">
+    <dd i18n:translate="">Charset used to encode emails send by the portal.
+     If empty, 'utf-8' is used if necessary.</dd>
+   </dl>
+  </td>
+ </tr>
+ <tr>
   <td>&nbsp;</td>
   <td>
    <metal:macro metal:use-macro="context/form_widgets/macros/buttons" />

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/registered_email.py
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/registered_email.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/registered_email.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -1,23 +1,18 @@
-##parameters=member=None, password='baz', email='[email protected]'
+##parameters=member=None, password='secret', email='[email protected]'
 ##
 from Products.CMFCore.utils import getToolByName
 from Products.CMFDefault.utils import decode
+from Products.CMFDefault.utils import makeEmail
+from Products.CMFDefault.utils import Message as _
 
 atool = getToolByName(script, 'portal_actions')
 ptool = getToolByName(script, 'portal_properties')
 utool = getToolByName(script, 'portal_url')
-default_charset = ptool.getProperty('default_charset')
 portal_url = utool()
 
 
 options = {}
 
-email_from_name = ptool.getProperty('email_from_name')
-email_from_address = ptool.getProperty('email_from_address')
-options['portal_address'] = '%s <%s>' % (email_from_name, email_from_address)
-options['member_address'] = '<%s>' % email
-options['content_type'] = 'text/plain; charset=%s' % default_charset
-
 options['portal_title'] = ptool.title()
 options['portal_description'] = ptool.getProperty('description')
 options['portal_url'] = portal_url
@@ -28,10 +23,16 @@
 
 target = atool.getActionInfo('user/login')['url']
 options['login_url'] = '%s' % target
+
+email_from_name = ptool.getProperty('email_from_name')
 options['signature'] = email_from_name
 
-rendered = context.registered_email_template(**decode(options, script))
-if isinstance(rendered, unicode):
-    return rendered.encode(default_charset)
-else:
-    return rendered
+headers = {}
+headers['Subject'] = _(u'${portal_title}: Your Membership Information',
+                      mapping={'portal_title': decode(ptool.title(), script)})
+headers['From'] = '%s <%s>' % (email_from_name,
+                               ptool.getProperty('email_from_address'))
+headers['To'] = '<%s>' % email
+
+mtext = context.registered_email_template(**decode(options, script))
+return makeEmail(mtext, script, headers)

Modified: CMF/trunk/CMFDefault/skins/zpt_generic/registered_email_template.pt
===================================================================
--- CMF/trunk/CMFDefault/skins/zpt_generic/registered_email_template.pt	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/skins/zpt_generic/registered_email_template.pt	2006-10-15 14:49:21 UTC (rev 70654)
@@ -1,12 +1,5 @@
 <tal:page i18n:domain="cmf_default"
->Subject: <tal:span i18n:translate=""><tal:span i18n:name="portal_title"
-    tal:content="options/portal_title"
-    />: Your Membership Information</tal:span>
-From: <tal:span tal:replace="structure options/portal_address" />
-To: <tal:span tal:replace="structure options/member_address" />
-Content-Type: <tal:span tal:replace="structure options/content_type" />
-
-<tal:span i18n:translate=""
+><tal:span i18n:translate=""
 >You have been registered as a member of "<tal:span i18n:name="portal_title"
    tal:content="options/portal_title" />", which
 allows you to personalize your view of the website and participate in the

Modified: CMF/trunk/CMFDefault/tests/RegistrationTool.txt
===================================================================
--- CMF/trunk/CMFDefault/tests/RegistrationTool.txt	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/tests/RegistrationTool.txt	2006-10-15 14:49:21 UTC (rev 70654)
@@ -1,4 +1,5 @@
 RegistrationTool
+----------------
 
   First we need some dummy code::
 
@@ -46,3 +47,164 @@
 
     >>> rtool.MailHost.lastMessage
     'Welcome: foo, secret, [email protected]'
+
+password_email and registered_email
+-----------------------------------
+
+  First we need some dummy code::
+
+    >>> from os.path import join
+    >>> from OFS.Folder import Folder
+    >>> from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
+    >>> from Products.PythonScripts.PythonScript import PythonScript
+    >>> class DummySite(Folder):
+    ...     def getPhysicalRoot(self): return self
+    ...     def getPhysicalPath(self): return ('root',)
+    ...     def addResource(self, dir, f_name):
+    ...         r_name, f_type = f_name.split('.')
+    ...         if f_type == 'pt': r = ZopePageTemplate(r_name)
+    ...         if f_type == 'py': r = PythonScript(r_name)
+    ...         f = file(join(dir, f_name), 'r')
+    ...         r.write(f.read())
+    ...         f.close()
+    ...         setattr(self, r_name, r)
+
+    >>> class DummyTool(Folder):
+    ...     def getActionInfo(self, action_chain): return self.login_action
+    ...     def getProperty(self, id, d=None): return getattr(self, id, d)
+    ...     def title(self): return self.Title
+    ...     def __call__(self): return self.url
+
+    >>> from zope import interface
+    >>> from zope.i18n.interfaces import INegotiator
+    >>> class Negotiator:
+    ...     interface.implements(INegotiator)
+    ...     def getLanguage(*ignored): return 'test'
+
+    >>> from zope.i18n.testmessagecatalog import TestMessageFallbackDomain
+    >>> class DummyFallbackTranslationService:
+    ...     def translate(self, domain, msgid, mapping, context,
+    ...                   target_language, default):
+    ...         util = TestMessageFallbackDomain(domain)
+    ...         return util.translate(msgid, mapping, context,
+    ...                               target_language, default)
+
+  And have to set up security and dummy translations::
+
+    >>> from AccessControl.SecurityManagement import newSecurityManager
+    >>> from AccessControl.User import UnrestrictedUser
+    >>> newSecurityManager(None, UnrestrictedUser('god', '', ['Manager'], ''))
+
+    >>> from zope import component
+    >>> component.provideUtility(Negotiator())
+    >>> from Products.Five import i18n
+    >>> old_fallback_translation_service = i18n._fallback_translation_service
+    >>> i18n._fallback_translation_service = DummyFallbackTranslationService()
+
+  Now we can set up password_email and registered_email with dummy context::
+
+    >>> from Testing.makerequest import makerequest
+    >>> s = makerequest(DummySite())
+    >>> s.REQUEST.environ['HTTP_X_FORWARDED_FOR'] = 'NNN.NNN.NNN.NNN'
+    >>> s.portal_actions = s.portal_properties = s.portal_url = DummyTool()
+    >>> s.ZopeTime = 'NNNN/NN/NN'
+    >>> s.description = 'THE SITE DESCRIPTION.'
+    >>> s.default_charset = 'utf-8'
+    >>> s.email_from_name = u'WEBMASTER \xc4\xd6\xdc'.encode('utf-8')
+    >>> s.email_from_address = '[email protected]'
+    >>> s.Title = 'WWW.EXAMPLE.ORG'
+    >>> s.url = 'PORTAL_URL'
+    >>> s.login_action = {'url': 'LOGIN_URL'}
+
+    >>> from os.path import dirname
+    >>> from Products import CMFDefault
+    >>> dir = join(dirname(CMFDefault.__file__), 'skins', 'zpt_generic')
+    >>> s.addResource(dir, 'password_email.py')
+    >>> s.addResource(dir, 'password_email_template.pt')
+    >>> s.addResource(dir, 'registered_email.py')
+    >>> s.addResource(dir, 'registered_email_template.pt')
+
+  password_email creates a complete reminder email::
+
+    >>> s.email_charset = 'iso-8859-1'
+    >>> print s.password_email()
+    Content-Type: text/plain; charset="us-ascii"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: 7bit
+    To: <[email protected]>
+    From: WEBMASTER =?iso-8859-1?q?=C4=D6=DC?= <[email protected]>
+    Subject: [[cmf_default][WWW.EXAMPLE.ORG: Membership reminder]]
+    <BLANKLINE>
+    [[cmf_default][Your password: secret]]
+    <BLANKLINE>
+    [[cmf_default][Request made by IP NNN.NNN.NNN.NNN at NNNN/NN/NN]]
+    <BLANKLINE>
+    <BLANKLINE>
+
+    >>> s.email_charset = ''
+    >>> print s.password_email()
+    Content-Type: text/plain; charset="us-ascii"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: 7bit
+    To: <[email protected]>
+    From: WEBMASTER =?utf-8?b?w4TDlsOc?= <[email protected]>
+    Subject: [[cmf_default][WWW.EXAMPLE.ORG: Membership reminder]]
+    <BLANKLINE>
+    [[cmf_default][Your password: secret]]
+    <BLANKLINE>
+    [[cmf_default][Request made by IP NNN.NNN.NNN.NNN at NNNN/NN/NN]]
+    <BLANKLINE>
+    <BLANKLINE>
+
+  registered_email creates a complete welcome email::
+
+    >>> s.email_charset = 'iso-8859-1'
+    >>> print s.registered_email()
+    Content-Type: text/plain; charset="iso-8859-1"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    To: <[email protected]>
+    From: WEBMASTER =?iso-8859-1?q?=C4=D6=DC?= <[email protected]>
+    Subject: [[cmf_default][WWW.EXAMPLE.ORG: Your Membership Information]]
+    <BLANKLINE>
+    [[cmf_default][You have been ... (You have been ...)]]
+    <BLANKLINE>
+    [[cmf_default][This describes the purpose of the website:]]
+    <BLANKLINE>
+    THE SITE DESCRIPTION.
+    <BLANKLINE>
+    [[cmf_default][Visit us at PORTAL_URL]]
+    <BLANKLINE>
+    [[cmf_default][Here is your login data (mind upper and lower case):]]
+    <BLANKLINE>
+    [[cmf_default][Member ID]]: foo
+    [[cmf_default][Password]]: secret
+    <BLANKLINE>
+    [[cmf_default][You can use this URL to log in:]]
+    <BLANKLINE>
+    LOGIN_URL
+    <BLANKLINE>
+    <BLANKLINE>
+    WEBMASTER =C4=D6=DC
+    <BLANKLINE>
+    <BLANKLINE>
+
+    >>> s.email_charset = ''
+    >>> print s.registered_email()
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: base64
+    To: <[email protected]>
+    From: WEBMASTER =?utf-8?b?w4TDlsOc?= <[email protected]>
+    Subject: [[cmf_default][WWW.EXAMPLE.ORG: Your Membership Information]]
+    <BLANKLINE>
+    W1tj...
+    <BLANKLINE>
+
+  Finally we have to clean up::
+
+    >>> i18n._fallback_translation_service = old_fallback_translation_service
+    >>> from AccessControl.SecurityManagement import noSecurityManager
+    >>> noSecurityManager()
+    >>> from zope.testing.cleanup import cleanUp
+    >>> cleanUp()

Modified: CMF/trunk/CMFDefault/tests/test_RegistrationTool.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_RegistrationTool.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/tests/test_RegistrationTool.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -140,7 +140,7 @@
     return unittest.TestSuite((
         unittest.makeSuite(RegistrationToolTests),
         doctest.DocFileSuite('RegistrationTool.txt',
-                             optionflags=doctest.NORMALIZE_WHITESPACE),
+               optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)),
         ))
 
 if __name__ == '__main__':

Modified: CMF/trunk/CMFDefault/utils.py
===================================================================
--- CMF/trunk/CMFDefault/utils.py	2006-10-15 14:43:52 UTC (rev 70653)
+++ CMF/trunk/CMFDefault/utils.py	2006-10-15 14:49:21 UTC (rev 70654)
@@ -19,6 +19,8 @@
 import re
 import StringIO
 import rfc822
+from email.Header import make_header
+from email.MIMEText import MIMEText
 from sgmllib import SGMLParser
 
 from AccessControl import ModuleSecurityInfo
@@ -26,6 +28,7 @@
 from Products.PageTemplates.GlobalTranslationService \
         import getGlobalTranslationService
 from ZTUtils.Zope import complex_marshal
+from zope import i18n
 from zope.i18n.interfaces import IUserPreferredCharsets
 from zope.i18nmessageid import MessageFactory
 
@@ -466,5 +469,24 @@
     charsets = envadapter.getPreferredCharsets() or ['utf-8']
     return charsets[0]
 
+security.declarePublic('makeEmail')
+def makeEmail(mtext, context, headers={}):
+    """ Make email message.
+    """
+    ptool = getToolByName(context, 'portal_properties')
+    email_charset = ptool.getProperty('email_charset', None) or 'utf-8'
+    try:
+        msg = MIMEText(mtext.encode(), 'plain')
+    except UnicodeEncodeError:
+        msg = MIMEText(mtext.encode(email_charset), 'plain', email_charset)
+    for k, val in headers.items():
+        if isinstance(val, str):
+            val = decode(val, context)
+        if isinstance(val, i18n.Message):
+            val = translate(val, context)
+        header = make_header([ (w, email_charset) for w in val.split(' ') ])
+        msg[k] = str(header)
+    return msg.as_string()
+
 security.declarePublic('Message')
 Message = _ = MessageFactory('cmf_default')
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.