Skip to content

/ Zope / gocept svn checkins / Archive / 2008 / 2008-09 / SVN: r6673 - in gocept.restmail/trunk: . gocept/restmail gocept/restmail/browser

[ << ] [ >> ]

[ SVN: r6671 - in gocept.recipe.updatercd/trunk: . ... ] [ SVN: r6676 - CMFWebmail/branches / Sebastian ... ]

SVN: r6673 - in gocept.restmail/trunk: . gocept/restmail gocept/restmail/browser
Sebastian Wehrmann <sw(at)gocept.com>
2008-09-24 10:55:12 [ FULL ]
Author: sweh
Date: Wed Sep 24 10:55:09 2008
New Revision: 6673

Log:
- add a drafts folder to accounts
- define draft messages
- provide rest api for creating draft messages



Added:
   gocept.restmail/trunk/gocept/restmail/browser/account.py   (contents, props
changed)
   gocept.restmail/trunk/gocept/restmail/draft.py   (contents, props changed)
Modified:
   gocept.restmail/trunk/gocept/restmail/__init__.py
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/imapaccount.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt
   gocept.restmail/trunk/gocept/restmail/zmi.txt
   gocept.restmail/trunk/setup.py

Modified: gocept.restmail/trunk/gocept/restmail/__init__.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/__init__.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/__init__.py	Wed Sep 24 10:55:09 2008
(at)(at) -1,6 +1,7 (at)(at)
 
 def initialize(context):
     import gocept.restmail.imapaccount
+    import gocept.restmail.draft
 
     context.registerClass(
         gocept.restmail.imapaccount.IMAPAccount,
(at)(at) -8,3 +9,9 (at)(at)
         constructors=(gocept.restmail.imapaccount.manage_addAccountForm,
                       gocept.restmail.imapaccount.manage_addAccount),
         icon='www/account.gif')
+
+    context.registerClass(
+        gocept.restmail.draft.DraftMessage,
+        meta_type='Draft Message',
+        constructors=(gocept.restmail.draft.manage_addDraft,),
+        icon='www/account.gif')

Added: gocept.restmail/trunk/gocept/restmail/browser/account.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/browser/account.py	Wed Sep 24
10:55:09 2008
(at)(at) -0,0 +1,18 (at)(at)
+# vim:fileencoding=utf-8
+# Copyright (c) 2008 gocept gmbh & co. kg
+# See also LICENSE.txt
+# $Id$
+"""Account methods."""
+
+import cjson
+import zope.app.zapi
+
+
+class WebAPI(object):
+    """Web API for accounts."""
+
+    def new_draft(self):
+        """Create a new draft message."""
+        draft = self.context.add_draft()
+        data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
+        return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Wed Sep 24
10:55:09 2008
(at)(at) -214,4 +214,12 (at)(at)
     attribute="content"
     />
 
+  <browser:page
+    name="new_draft"
+    for="gocept.restmail.interfaces.IIMAPAccount"
+    permission="zope2.View"
+    class=".account.WebAPI"
+    attribute="new_draft"
+    />
+
 </configure>

Added: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Wed Sep 24 10:55:09 2008
(at)(at) -0,0 +1,48 (at)(at)
+# Copyright (c) 2007 gocept gmbh & co. kg
+# See also LICENSE.txt
+# $Id$
+
+import gocept.restmail.interfaces
+import uuid
+import zope.interface
+
+from OFS.SimpleItem import SimpleItem
+from OFS.PropertyManager import PropertyManager
+
+
+class DraftMessage(SimpleItem, PropertyManager):
+    """A draft for a message."""
+
+    zope.interface.implements(gocept.restmail.interfaces.IDraftMessage)
+
+    meta_type = 'Draft Message'
+
+    manage_options = (PropertyManager.manage_options +
+                      SimpleItem.manage_options)
+
+    _properties = (dict(id='to', type='string', mode='w'),
+                   dict(id='subject', type='string', mode='w'),
+                   dict(id='body', type='text', mode='w'))
+
+    to = u''
+    subject = u''
+    body = u''
+
+
+def add_draft(container):
+    id = str(uuid.uuid1())
+    while id in container.objectIds():
+        id = str(uuid.uuid1())
+
+    draft = DraftMessage(id)
+    container._setObject(id, draft)
+    return container[id]
+
+
+def manage_addDraft(context):
+    """ZMI helper to create a new draft message."""
+    container = context.this()
+    draft = add_draft(container)
+    context.REQUEST.RESPONSE.redirect(
+        draft.absolute_url() + '/manage_workspace')
+

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Wed Sep 24 10:55:09
2008
(at)(at) -7,8 +7,9 (at)(at)
 
 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
 from Products.Five.traversable import Traversable
-from OFS.SimpleItem import SimpleItem
+from OFS.SimpleItem import SimpleItem, Item
 from OFS.PropertyManager import PropertyManager
+from OFS.ObjectManager import ObjectManager
 
 import UserDict
 import gocept.restmail.interfaces
(at)(at) -18,7 +19,7 (at)(at)
 import zope.component
 
 
-class IMAPAccount(SimpleItem, PropertyManager):
+class IMAPAccount(ObjectManager, PropertyManager, Item):
     """A delegate for the gocept.imapapi account object."""
 
     zope.interface.implements(gocept.restmail.interfaces.IIMAPAccount,
(at)(at) -27,8 +28,9 (at)(at)
     meta_type = 'IMAP Account'
 
     manage_options = ((dict(label='Folders', action='manage_listFolders'),) +
-                      PropertyManager.manage_options +
-                      SimpleItem.manage_options)
+                      ObjectManager.manage_options +
+                      PropertyManager.manage_options + 
+                      Item.manage_options)
 
     _properties = (dict(id='host', type='string', mode='w'),
                    dict(id='port', type='int', mode='w'),
(at)(at) -47,6 +49,12 (at)(at)
         self.user = user
         self.password = password
 
+    def manage_afterAdd(self, item, container):
+        self.manage_addProduct['OFSP'].manage_addFolder('drafts')
+
+    def add_draft(self):
+        return gocept.restmail.draft.add_draft(self['drafts'])
+
     def connect(self):
         try:
             account = self._v_account
(at)(at) -251,3 +259,4 (at)(at)
 
     def sortKey(self):
         return id(self.account)
+

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Wed Sep 24 10:55:09
2008
(at)(at) -22,3 +22,14 (at)(at)
 
     def connect():
         """Return a gocept.imapapi.interfaces.IAccount object."""
+
+    def add_draft():
+        """Add a draft message to the account."""
+
+
+class IDraftMessage(zope.interface.Interface):
+    """A draft of a message."""
+
+    to = zope.schema.TextLine(title=u'To')
+    subject = zope.schema.TextLine(title=u'Subject')
+    body = zope.schema.Text(title=u'Message')

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Wed Sep 24 10:55:09 2008
(at)(at) -229,3 +229,17 (at)(at)
 
 >>> cjson.decode(browser.contents)
 {'body': 'Everything is ok!', 'content_type': 'text/plain'}
+
+
+Creating draft messages
+=======================
+
+Accounts can store draft messages. They can be created using the
`(at)(at)new_draft`
+view:
+
+>>> browser.open('http://localhost/account/(at)(at)new_draft')
+>>> print browser.contents
+{"url": "http://localhost/account/drafts/"}
+
+>>> cjson.decode(browser.contents)
+{'url': 'http://localhost/account/drafts/'}

Modified: gocept.restmail/trunk/gocept/restmail/zmi.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/zmi.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/zmi.txt	Wed Sep 24 10:55:09 2008
(at)(at) -180,3 +180,18 (at)(at)
 ^M
 Everything is ok!</pre>
 ...
+
+Draft folder
+------------
+
+Accounts have a draft folder:
+
+>>> browser.getLink('account').click()
+>>> browser.getLink('Contents').click()
+>>> browser.getLink('drafts').click()
+>>> print browser.contents
+<!DOCTYPE...
+    <strong>
+          Folder
+          at <a href="/manage_workspace">&nbsp;/</a><a
href="/account/manage_workspace">account</a>/<a class="strong-link"
href="/account/drafts/manage_workspace">drafts</a>
+</strong>...

Modified: gocept.restmail/trunk/setup.py
==============================================================================
--- gocept.restmail/trunk/setup.py	(original)
+++ gocept.restmail/trunk/setup.py	Wed Sep 24 10:55:09 2008
(at)(at) -17,5 +17,6 (at)(at)
     license='ZPL 2.1',
     namespace_packages=['gocept'],
     install_requires=['python-cjson',
+                      'uuid',
                       ],
 )

SVN: r6674 - in gocept.restmail/trunk/gocept/restmail: . browser
Thomas Lotze <tl(at)gocept.com>
2008-09-24 11:08:55 [ FULL ]
Author: thomas
Date: Wed Sep 24 11:08:53 2008
New Revision: 6674

Log:
add_draft -> new_draft


Modified:
   gocept.restmail/trunk/gocept/restmail/browser/account.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/imapaccount.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/account.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/account.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/account.py	Wed Sep 24
11:08:53 2008
(at)(at) -13,6 +13,6 (at)(at)
 
     def new_draft(self):
         """Create a new draft message."""
-        draft = self.context.add_draft()
+        draft = self.context.new_draft()
         data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
         return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Wed Sep 24 11:08:53 2008
(at)(at) -29,7 +29,7 (at)(at)
     body = u''
 
 
-def add_draft(container):
+def new_draft(container):
     id = str(uuid.uuid1())
     while id in container.objectIds():
         id = str(uuid.uuid1())
(at)(at) -42,7 +42,7 (at)(at)
 def manage_addDraft(context):
     """ZMI helper to create a new draft message."""
     container = context.this()
-    draft = add_draft(container)
+    draft = new_draft(container)
     context.REQUEST.RESPONSE.redirect(
         draft.absolute_url() + '/manage_workspace')
 

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Wed Sep 24 11:08:53
2008
(at)(at) -52,8 +52,8 (at)(at)
     def manage_afterAdd(self, item, container):
         self.manage_addProduct['OFSP'].manage_addFolder('drafts')
 
-    def add_draft(self):
-        return gocept.restmail.draft.add_draft(self['drafts'])
+    def new_draft(self):
+        return gocept.restmail.draft.new_draft(self['drafts'])
 
     def connect(self):
         try:

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Wed Sep 24 11:08:53
2008
(at)(at) -23,7 +23,7 (at)(at)
     def connect():
         """Return a gocept.imapapi.interfaces.IAccount object."""
 
-    def add_draft():
+    def new_draft():
         """Add a draft message to the account."""

SVN: r6675 - in gocept.restmail/trunk/gocept/restmail: . browser
Sebastian Wehrmann <sw(at)gocept.com>
2008-09-24 13:07:17 [ FULL ]
Author: sweh
Date: Wed Sep 24 13:07:16 2008
New Revision: 6675

Log:
added view to retrieve data from a draft message



Added:
   gocept.restmail/trunk/gocept/restmail/browser/draft.py   (contents, props
changed)
Modified:
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/configure.zcml
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/rest.txt

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Wed Sep 24
13:07:16 2008
(at)(at) -222,4 +222,12 (at)(at)
     attribute="new_draft"
     />
 
+  <browser:page
+    name="data"
+    for="gocept.restmail.interfaces.IDraftMessage"
+    permission="zope2.View"
+    class=".draft.WebAPI"
+    attribute="data"
+    />
+
 </configure>

Added: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Wed Sep 24 13:07:16
2008
(at)(at) -0,0 +1,19 (at)(at)
+# vim:fileencoding=utf-8
+# Copyright (c) 2008 gocept gmbh & co. kg
+# See also LICENSE.txt
+# $Id$
+"""Draft methods."""
+
+import cjson
+import zope.app.zapi
+
+
+class WebAPI(object):
+    """Web API for accounts."""
+
+    def data(self):
+        """Return data of the draft message."""
+        data = {'to': self.context.to,
+                'subject': self.context.subject,
+                'body': self.context.body}
+        return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/configure.zcml	Wed Sep 24 13:07:16
2008
(at)(at) -10,6 +10,8 (at)(at)
   <five:traversable class=".imapaccount.Message" />
   <five:traversable class=".imapaccount.BodyPart" />
 
+  <five:traversable class=".draft.DraftMessage" />
+
   <class class=".imapaccount.BodyPart">
     <require interface="gocept.imapapi.interfaces.IBodyPart"
       permission="zope2.View" />

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Wed Sep 24 13:07:16 2008
(at)(at) -28,6 +28,8 (at)(at)
     subject = u''
     body = u''
 
+    def __init__(self, id):
+        self.id = id
 
 def new_draft(container):
     id = str(uuid.uuid1())

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Wed Sep 24 13:07:16 2008
(at)(at) -239,7 +239,7 (at)(at)
 
 >>> browser.open('http://localhost/account/(at)(at)new_draft')
 >>> print browser.contents
-{"url": "http://localhost/account/drafts/"}
+{"url": "http://localhost/account/drafts/...-...-...-...-..."}
 
 >>> cjson.decode(browser.contents)
-{'url': 'http://localhost/account/drafts/'}
+{'url': 'http://localhost/account/drafts/...-...-...-...-...'}

SVN: r6681 - in gocept.restmail/trunk/gocept/restmail: . browser
Thomas Lotze <tl(at)gocept.com>
2008-09-24 14:52:54 [ FULL ]
Author: thomas
Date: Wed Sep 24 14:52:52 2008
New Revision: 6681

Log:
added reply infrastructure, still lacking body quoting and tests


Modified:
   gocept.restmail/trunk/gocept/restmail/browser/account.py
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/browser/message.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/imapaccount.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/account.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/account.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/account.py	Wed Sep 24
14:52:52 2008
(at)(at) -1,7 +1,6 (at)(at)
 # vim:fileencoding=utf-8
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 """Account methods."""
 
 import cjson
(at)(at) -11,8 +10,8 (at)(at)
 class WebAPI(object):
     """Web API for accounts."""
 
-    def new_draft(self):
+    def new_draft(self, message=None):
         """Create a new draft message."""
-        draft = self.context.new_draft()
+        draft = self.context.new_draft(message)
         data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
         return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Wed Sep 24
14:52:52 2008
(at)(at) -204,6 +204,11 (at)(at)
       name="render"
       attribute="render"
       />
+
+    <browser:page
+      name="reply"
+      attribute="reply"
+      />
   </browser:pages>
 
   <browser:page

Modified: gocept.restmail/trunk/gocept/restmail/browser/message.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/message.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/message.py	Wed Sep 24
14:52:52 2008
(at)(at) -1,7 +1,6 (at)(at)
 # vim:fileencoding=utf-8
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 """Mail methods."""
 
 import cjson
(at)(at) -11,6 +10,8 (at)(at)
 import zope.component
 import zope.interface
 
+import gocept.imapapi.interfaces
+
 
 class MessageContainerWebAPI(object):
     """Web API for message containers."""
(at)(at) -56,6 +57,16 (at)(at)
 
         return data
 
+    def reply(self):
+        """Create a new draft message."""
+        container = self.context.parent
+        while gocept.imapapi.interfaces.IAccountContent.providedBy(container):
+            container = container.parent
+        # container is now the account
+        draft = container.new_draft(message)
+        data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
+        return cjson.encode(data)
+
 
 class BodyPartWebAPI(object):
     """Web API for MIME parts of a message body."""

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Wed Sep 24 14:52:52 2008
(at)(at) -1,6 +1,5 (at)(at)
-# Copyright (c) 2007 gocept gmbh & co. kg
+# Copyright (c) 2007-2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 
 import gocept.restmail.interfaces
 import uuid
(at)(at) -31,12 +30,24 (at)(at)
     def __init__(self, id):
         self.id = id
 
-def new_draft(container):
+    def _reply_to(self, message):
+        self.to = message.headers.get('From')
+        subject = message.headers.get('Subject', '')
+        if subject.startswith('Re: '):
+            self.subject = subject
+        else:
+            self.subject = 'Re: ' + subject
+        self.body = get_html_text(message)
+
+
+def new_draft(container, message=None):
     id = str(uuid.uuid1())
     while id in container.objectIds():
         id = str(uuid.uuid1())
 
     draft = DraftMessage(id)
+    if message is not None:
+        draft._reply_to(message)
     container._setObject(id, draft)
     return container[id]
 
(at)(at) -48,3 +59,31 (at)(at)
     context.REQUEST.RESPONSE.redirect(
         draft.absolute_url() + '/manage_workspace')
 
+
+HTML_TEMPLATE = """\
+<HTML>
+<HEAD></HEAD>
+<BODY>
+<PRE>
+%s
+</PRE>
+</BODY>
+</HTML>
+"""
+
+def get_html_text(message):
+    # find text/html body part
+    part = message.body.find_one('text/html')
+    if part:
+        text = part.fetch()
+    else:
+        # fall back to text/plain part
+        part = message.body.find_one('text/plain')
+        text = HTML_TEMPLATE % (part and part.fetch() or '')
+
+    if part:
+        encoding = part['parameters'].get('charset', 'ascii')
+    else:
+        encoding = 'ascii'
+
+    return text.decode(encoding)

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Wed Sep 24 14:52:52
2008
(at)(at) -1,7 +1,6 (at)(at)
 # vim:fileencoding=utf-8
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 """IMAP account object."""
 
 
(at)(at) -52,8 +51,8 (at)(at)
     def manage_afterAdd(self, item, container):
         self.manage_addProduct['OFSP'].manage_addFolder('drafts')
 
-    def new_draft(self):
-        return gocept.restmail.draft.new_draft(self['drafts'])
+    def new_draft(self, message=None):
+        return gocept.restmail.draft.new_draft(self['drafts'], message)
 
     def connect(self):
         try:

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Wed Sep 24 14:52:52
2008
(at)(at) -1,7 +1,6 (at)(at)
 # vim:fileencoding=utf-8
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 """Interface definitions for gocept.restmail"""
 
 import zope.interface
(at)(at) -23,8 +22,13 (at)(at)
     def connect():
         """Return a gocept.imapapi.interfaces.IAccount object."""
 
-    def new_draft():
-        """Add a draft message to the account."""
+    def new_draft(message=None):
+        """Add a draft message to the account.
+
+        If message is not None, the new draft message is prepared as a reply
+        to it.
+
+        """
 
 
 class IDraftMessage(zope.interface.Interface):

SVN: r6682 - in gocept.restmail/trunk/gocept/restmail: . browser
Christian Theune <ct(at)gocept.com>
2008-09-24 15:32:09 [ FULL ]
Author: ctheune
Date: Wed Sep 24 15:32:08 2008
New Revision: 6682

Log:
Fix reply draft message creation.



Modified:
   gocept.restmail/trunk/gocept/restmail/browser/message.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/imapaccount.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/message.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/message.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/message.py	Wed Sep 24
15:32:08 2008
(at)(at) -59,11 +59,15 (at)(at)
 
     def reply(self):
         """Create a new draft message."""
-        container = self.context.parent
-        while gocept.imapapi.interfaces.IAccountContent.providedBy(container):
-            container = container.parent
-        # container is now the account
-        draft = container.new_draft(message)
+        candidate = self.context.aq_inner
+        while True:
+            if gocept.imapapi.interfaces.IAccount.providedBy(candidate):
+                account = candidate
+                break
+            candidate = candidate.getParentNode()
+        else:
+            raise Exception("Message outside of account?!?")
+        draft = account.new_draft(self.context)
         data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
         return cjson.encode(data)
 

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Wed Sep 24 15:32:08 2008
(at)(at) -62,7 +62,6 (at)(at)
 
 HTML_TEMPLATE = """\
 <HTML>
-<HEAD></HEAD>
 <BODY>
 <PRE>
 %s
(at)(at) -73,12 +72,12 (at)(at)
 
 def get_html_text(message):
     # find text/html body part
-    part = message.body.find_one('text/html')
+    part = message.body().find_one('text/html')
     if part:
         text = part.fetch()
     else:
         # fall back to text/plain part
-        part = message.body.find_one('text/plain')
+        part = message.body().find_one('text/plain')
         text = HTML_TEMPLATE % (part and part.fetch() or '')
 
     if part:

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Wed Sep 24 15:32:08
2008
(at)(at) -220,6 +220,9 (at)(at)
     def fetch(self):
         return self._body.fetch()
 
+    def find_one(self, content_type):
+        return self._body.find_one(content_type)
+
 
 class IMAPConnectionDataManager(object):
     """A data manager that closes IMAP connections at transaction

SVN: r6687 - gocept.restmail/trunk/gocept/restmail/browser
Christian Theune <ct(at)gocept.com>
2008-09-24 17:51:11 [ FULL ]
Author: ctheune
Date: Wed Sep 24 17:51:09 2008
New Revision: 6687

Log:
- Implement saving drafts.



Modified:
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/browser/draft.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Wed Sep 24
17:51:09 2008
(at)(at) -227,12 +227,21 (at)(at)
     attribute="new_draft"
     />
 
-  <browser:page
-    name="data"
+  <browser:pages
     for="gocept.restmail.interfaces.IDraftMessage"
     permission="zope2.View"
-    class=".draft.WebAPI"
-    attribute="data"
-    />
+    class=".draft.WebAPI">
+
+    <browser:page
+      name="data"
+      attribute="data"
+      />
+
+    <browser:page
+      name="save"
+      attribute="save"
+      />
+
+  </browser:pages>
 
 </configure>

Modified: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Wed Sep 24 17:51:09
2008
(at)(at) -17,3 +17,9 (at)(at)
                 'subject': self.context.subject,
                 'body': self.context.body}
         return cjson.encode(data)
+
+    def save(self, to, subject, body):
+        """Update the draft."""
+        self.context.to = to
+        self.context.subject = subject
+        self.context.body = body

SVN: r6693 - in gocept.recipe.updatercd/trunk: . src/gocept/recipe/updatercd
Christian Zagrodnick <cz(at)gocept.com>
2008-09-25 08:57:36 [ FULL ]
Author: zagy
Date: Thu Sep 25 08:57:34 2008
New Revision: 6693

Log:
added uninstall and some documentation





Modified:
   gocept.recipe.updatercd/trunk/CHANGES.txt
   gocept.recipe.updatercd/trunk/README.txt
   gocept.recipe.updatercd/trunk/setup.py
   gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/README.txt
   gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/recipe.py

Modified: gocept.recipe.updatercd/trunk/CHANGES.txt
==============================================================================
--- gocept.recipe.updatercd/trunk/CHANGES.txt	(original)
+++ gocept.recipe.updatercd/trunk/CHANGES.txt	Thu Sep 25 08:57:34 2008
(at)(at) -1 +1,7 (at)(at)
-CHANGES
+Changelog
+=========
+
+0.1 (unreleased)
+----------------
+
+- initial release

Modified: gocept.recipe.updatercd/trunk/README.txt
==============================================================================
--- gocept.recipe.updatercd/trunk/README.txt	(original)
+++ gocept.recipe.updatercd/trunk/README.txt	Thu Sep 25 08:57:34 2008
(at)(at) -1 +1,6 (at)(at)
-XXX
+gocept.recipe.updatercd
+=======================
+
+This buildout recipe allows to integrate buildout generated startup scripts to
+be integrated in the system startup on systems which provide the update-rc.d
+command.

Modified: gocept.recipe.updatercd/trunk/setup.py
==============================================================================
--- gocept.recipe.updatercd/trunk/setup.py	(original)
+++ gocept.recipe.updatercd/trunk/setup.py	Thu Sep 25 08:57:34 2008
(at)(at) -45,5 +45,7 (at)(at)
     namespace_packages = ['gocept', 'gocept.recipe'],
     install_requires = ['zc.buildout', 'setuptools'],
     extras_require = {'test': ['zope.testing']},
-    entry_points = {'zc.buildout': ['default = %s.recipe:UpdateRCD' %
name,],},
+    entry_points = {
+        'zc.buildout': ['default = %s.recipe:UpdateRCD' % name],
+        'zc.buildout.uninstall': ['default = %s.recipe:uninstall' % name]},
     )

Modified: gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/README.txt
==============================================================================
---
gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/README.txt	(original)
+++ gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/README.txt	Thu
Sep 25 08:57:34 2008
(at)(at) -1,5 +1,4 (at)(at)
-
-
+A buildout using the recipe might look like this:
 
 >>> write('buildout.cfg', """\
 ... [buildout]
(at)(at) -22,17 +21,40 (at)(at)
 ... parts = zeo instance
 ... command = echo
 ... """)
+
+When the recipe is executed it runs the update-rc.d command. Note that we've
+specified ``echo`` to be called for testing purposes.
+
 >>> print system('bin/buildout'),
 Installing updatercd.
 test-deploy-zeo defaults 80 20
 test-deploy-instance defaults 81 19
 
 
-Must use deployment in target section so we know the name.
+When the section is removed, update-rc.d is called to remove the symlinks:
+
+>>> write('buildout.cfg', """\
+... [buildout]
+... parts =
+...
+... [deploy]
+... name = test-deploy
+...
+... [zeo]
+... deployment = deploy
+... imaginary-zeo-server = blarf
+...
+... [instance]
+... deployment = deploy
+... imaginary-zope-instance = 27
+... """)
+>>> print system('bin/buildout'),
+Uninstalling updatercd.
+-f test-deploy-zeo remove
+-f test-deploy-instance remove
 
-update-rc.d test-deploy-zeo defaults 80 20
-update-rc.d test-deploy-instance defaults 81 19
 
+TODO:
 
 - Fail if referenced part doesn't reference a deployment
 - Fail if counter reaches 100

Modified: gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/recipe.py
==============================================================================
---
gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/recipe.py	(original)
+++ gocept.recipe.updatercd/trunk/src/gocept/recipe/updatercd/recipe.py	Thu Sep
25 08:57:34 2008
(at)(at) -9,7 +9,6 (at)(at)
     def __init__(self, buildout, name, options):
         self.name, self.options = name, options
 
-        options.setdefault('runlevels', '2 3 4 5')
         options.setdefault('start-order', '80')
         options.setdefault('stop-order', str(100-int(options['start-order'])))
         options.setdefault('command', 'update-rc.d')

SVN: r6697 - in gocept.testdb/trunk: . src/gocept/testdb
Wolfgang Schnerring <ws(at)gocept.com>
2008-09-25 09:46:00 [ FULL ]
Author: wosc
Date: Thu Sep 25 09:45:58 2008
New Revision: 6697

Log:
factored in postgresql


Modified:
   gocept.testdb/trunk/setup.py
   gocept.testdb/trunk/src/gocept/testdb/README.txt
   gocept.testdb/trunk/src/gocept/testdb/db.py

Modified: gocept.testdb/trunk/setup.py
==============================================================================
--- gocept.testdb/trunk/setup.py	(original)
+++ gocept.testdb/trunk/setup.py	Thu Sep 25 09:45:58 2008
(at)(at) -17,12 +17,11 (at)(at)
     namespace_packages=['gocept'],
     install_requires=[
         'setuptools',
-        'zope.interface',
-        'zope.component',
         'SQLAlchemy',
         ],
     extras_require=dict(
         test=[
+            'zope.testing',
             'MySQL-python',
             'psycopg2',
             ]),

Modified: gocept.testdb/trunk/src/gocept/testdb/README.txt
==============================================================================
--- gocept.testdb/trunk/src/gocept/testdb/README.txt	(original)
+++ gocept.testdb/trunk/src/gocept/testdb/README.txt	Thu Sep 25 09:45:58 2008
(at)(at) -1,16 +1,30 (at)(at)
 gocept.testdb - temporary database creation
 ----------------------------------------------------
 
->>> import gocept.testdb
 >>> import os.path
->>> db = gocept.testdb.MySQL(schema_path=os.path.join(
-...         os.path.dirname(gocept.testdb.__file__),
-...         'sample.sql'))
+>>> import sqlalchemy
+>>> import gocept.testdb
+
+>>> schema = os.path.join(os.path.dirname(gocept.testdb.__file__),
'sample.sql')
+>>> db = gocept.testdb.MySQL(schema_path=schema)
 >>> db.dsn
 'mysql://localhost/testdb-...'
->>> import sqlalchemy
 >>> engine = sqlalchemy.create_engine(db.dsn)
->>> ignore = engine.connect().execute('SELECT * from tmp_functest')
+>>> conn = engine.connect()
+>>> ignore = conn.execute('SELECT * from tmp_functest')
+>>> ignore = conn.execute('SELECT * from foo')
+>>> db.drop()
+>>> engine.connect().execute('SELECT * from tmp_functest')
+Traceback (most recent call last):
+  ...
+OperationalError:...
+
+>>> db = gocept.testdb.PostgreSQL(schema_path=schema)
+>>> engine = sqlalchemy.create_engine(db.dsn)
+>>> conn = engine.connect()
+>>> ignore = conn.execute('SELECT * from tmp_functest')
+>>> ignore = conn.execute('SELECT * from foo')
+>>> conn.invalidate()
 >>> db.drop()
 >>> engine.connect().execute('SELECT * from tmp_functest')
 Traceback (most recent call last):

Modified: gocept.testdb/trunk/src/gocept/testdb/db.py
==============================================================================
--- gocept.testdb/trunk/src/gocept/testdb/db.py	(original)
+++ gocept.testdb/trunk/src/gocept/testdb/db.py	Thu Sep 25 09:45:58 2008
(at)(at) -4,20 +4,24 (at)(at)
 import os
 import random
 import subprocess
+import time
 
 import sqlalchemy
 
 
-__all__ = ['MySQL']
+__all__ = ['MySQL', 'PostgreSQL']
 
 
-class MySQL(object):
+class Database(object):
+    protocol = None # set by subclass
+
     def __init__(self, schema_path=None, prefix='testdb'):
         self.schema_path = schema_path
         self.db_name = '%s-%i' % (prefix, random.randint(0, 9999))
-        self.db_host = os.environ.get('MYSQL_DATABASE_HOST') or 'localhost'
-        self.db_user = os.environ.get('MYSQL_DATABASE_USER')
-        self.db_pass = os.environ.get('MYSQL_DATABASE_PASS')
+        self.db_host = (os.environ.get('%s_HOST' % self.protocol.upper())
+                       or 'localhost')
+        self.db_user = os.environ.get('%s_USER' % self.protocol.upper())
+        self.db_pass = os.environ.get('%s_PASS' % self.protocol.upper())
 
         login = ''
         if self.db_user:
(at)(at) -25,12 +29,55 (at)(at)
             if self.db_pass:
                 login += ':' + self.db_pass
             login += '(at)'
-        self.dsn = 'mysql://%s%s/%s' % (login, self.db_host, self.db_name)
+        self.dsn = '%s://%s%s/%s' % (self.protocol, login, self.db_host,
+                                     self.db_name)
 
+    def create(self):
         self.create_db()
-        self.create_schema()
+        if self.schema_path:
+            self.create_schema()
         self.mark_testing()
 
+    def create_db(self):
+        db_result = subprocess.call(self.cmd_create)
+        if db_result != 0:
+            raise SystemExit("Could not create database %r" % self.db_name)
+
+    def mark_testing(self):
+        engine = sqlalchemy.create_engine(self.dsn)
+        meta = sqlalchemy.MetaData()
+        meta.bind = engine
+        table = sqlalchemy.Table('tmp_functest', meta,
+                                 sqlalchemy.Column('dummy',
sqlalchemy.Integer))
+        table.create()
+        engine.dispose()
+
+    def drop(self):
+        def _drop():
+            return subprocess.call(self.cmd_drop)
+        db_result = _drop()
+        if db_result != 0:
+            # give the database some time to shut down
+            time.sleep(1)
+            db_result = _drop()
+            if db_result != 0:
+                raise RuntimeError("Could not drop database %r" %
self.db_name)
+
+    def create_schema(self):
+        pass
+
+
+class MySQL(Database):
+    protocol = 'mysql'
+
+    def __init__(self, *args, **kw):
+        super(MySQL, self).__init__(*args, **kw)
+        self.cmd_create = self.login_args(
+                'mysqladmin', ['create', self.db_name])
+        self.cmd_drop = self.login_args(
+                'mysqladmin', ['--force', 'drop', self.db_name])
+        self.create()
+
     def login_args(self, command, extra_args=()):
         args = [
             command,
(at)(at) -40,16 +87,7 (at)(at)
         args.extend(extra_args)
         return args
 
-    def create_db(self):
-        db_result = subprocess.call(self.login_args(
-                'mysqladmin', ['create', self.db_name]))
-        if db_result != 0:
-            raise SystemExit("Could not create database %r" %
-                             self.db_name)
-
     def create_schema(self):
-        if not self.schema_path:
-            return
         db_input = subprocess.Popen(self.login_args(
                 'mysql', [self.db_name]),
                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
(at)(at) -57,26 +95,33 (at)(at)
         stdout, stderr = db_input.communicate(open(self.schema_path).read())
         if db_input.returncode != 0:
             print stderr
-            raise SystemExit("Could not initialize schema in database %r." %
-                             self.db_name)
+            raise RuntimeError("Could not initialize schema in database %r." %
+                               self.db_name)
 
-    def mark_testing(self):
-        engine = sqlalchemy.create_engine(self.dsn)
-        meta = sqlalchemy.MetaData()
-        meta.bind = engine
-        table = sqlalchemy.Table('tmp_functest', meta,
-                                 sqlalchemy.Column('dummy',
sqlalchemy.Integer))
-        table.create()
 
-    def drop(self):
-        def _drop():
-            return subprocess.call(self.login_args(
-                'mysqladmin', ['--force', 'drop', self.db_name]))
-        db_result = _drop()
+class PostgreSQL(Database):
+    protocol = 'postgres'
+
+    def __init__(self, *args, **kw):
+        super(PostgreSQL, self).__init__(*args, **kw)
+        self.cmd_create = self.login_args('createdb', [self.db_name])
+        self.cmd_drop = self.login_args('dropdb', [self.db_name])
+        self.create()
+
+    def login_args(self, command, extra_args=()):
+        args = [
+            command,
+            '--quiet', '-h', self.db_host]
+        if self.db_user:
+            args.extend(['-U', self.db_user])
+        args.extend(extra_args)
+        return args
+
+    def create_schema(self):
+        db_result = subprocess.call(self.login_args(
+                'psql', ['-f', self.schema_path,
+                         '-v', 'ON_ERROR_STOP=true',
+                         self.db_name]))
         if db_result != 0:
-            # Give the database some time to shut down.
-            time.sleep(1)
-            db_result = drop()
-            if db_result != 0:
-                raise SystemExit("Could not drop database %r" %
-                                 self.db_name)
+            raise RuntimeError("Could not initialize schema in database %r." %
+                               self.db_name)

SVN: r6698 - in gocept.testdb/trunk: . src/gocept/testdb
Wolfgang Schnerring <ws(at)gocept.com>
2008-09-25 10:00:51 [ FULL ]
Author: wosc
Date: Thu Sep 25 10:00:50 2008
New Revision: 6698

Log:
a little explanatory text


Modified:
   gocept.testdb/trunk/setup.py
   gocept.testdb/trunk/src/gocept/testdb/README.txt

Modified: gocept.testdb/trunk/setup.py
==============================================================================
--- gocept.testdb/trunk/setup.py	(original)
+++ gocept.testdb/trunk/setup.py	Thu Sep 25 10:00:50 2008
(at)(at) -9,6 +9,9 (at)(at)
     author='gocept',
     author_email='mail(at)gocept.com',
     description='Creates and drops temporary databases for testing purposes.',
+    long_description = (
+        open(os.path.join('src', 'gocept', 'testdb', 'README.txt')).read()
+    ),
     packages=find_packages('src'),
     package_dir = {'': 'src'},
     include_package_data=True,

Modified: gocept.testdb/trunk/src/gocept/testdb/README.txt
==============================================================================
--- gocept.testdb/trunk/src/gocept/testdb/README.txt	(original)
+++ gocept.testdb/trunk/src/gocept/testdb/README.txt	Thu Sep 25 10:00:50 2008
(at)(at) -1,24 +1,51 (at)(at)
 gocept.testdb - temporary database creation
 ----------------------------------------------------
 
+gocept.testdb provides small helper classes that create and drop temporary
+databases.
+
 >>> import os.path
 >>> import sqlalchemy
 >>> import gocept.testdb
-
 >>> schema = os.path.join(os.path.dirname(gocept.testdb.__file__),
'sample.sql')
+
+First, create a test database object
+
 >>> db = gocept.testdb.MySQL(schema_path=schema)
+
+This will use the appropriate command-line tools to create a database with a
+random name (you can specify a prefix if desired).
+Login information can be specified via environment variables
+(MYSQL_HOST default localhost, MYSQL_USER default None, MYSQL_PASS default
None)
+
+The dbapi DSN can then be used to connect to the database:
+
 >>> db.dsn
 'mysql://localhost/testdb-...'
 >>> engine = sqlalchemy.create_engine(db.dsn)
+
+The database is marked as a testing database by creating a table called
+'tmp_functest' in it:
+
 >>> conn = engine.connect()
 >>> ignore = conn.execute('SELECT * from tmp_functest')
+
+If you passed a schema_path to the constructor, the SQL code in this file
+is executed, e. g. to set up tables:
+
 >>> ignore = conn.execute('SELECT * from foo')
+
+When done, simply drop the database:
+
 >>> db.drop()
 >>> engine.connect().execute('SELECT * from tmp_functest')
 Traceback (most recent call last):
   ...
 OperationalError:...
 
+
+The same procedure also works for PostgreSQL:
+
 >>> db = gocept.testdb.PostgreSQL(schema_path=schema)
 >>> engine = sqlalchemy.create_engine(db.dsn)
 >>> conn = engine.connect()

SVN: r6700 - in gocept.restmail/trunk: . gocept/restmail gocept/restmail/browser
Thomas Lotze <tl(at)gocept.com>
2008-09-25 10:16:23 [ FULL ]
Author: thomas
Date: Thu Sep 25 10:16:21 2008
New Revision: 6700

Log:
actually quote the body of the message


Modified:
   gocept.restmail/trunk/gocept/restmail/browser/draft.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/setup.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Thu Sep 25 10:16:21
2008
(at)(at) -15,7 +15,7 (at)(at)
         """Return data of the draft message."""
         data = {'to': self.context.to,
                 'subject': self.context.subject,
-                'body': self.context.body}
+                'body': self.context.body.replace('%', '%p').replace('<',
'%l')}
         return cjson.encode(data)
 
     def save(self, to, subject, body):

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 10:16:21 2008
(at)(at) -1,7 +1,10 (at)(at)
 # Copyright (c) 2007-2008 gocept gmbh & co. kg
 # See also LICENSE.txt
 
+import StringIO
+
 import gocept.restmail.interfaces
+import lxml.etree
 import uuid
 import zope.interface
 
(at)(at) -62,10 +65,11 (at)(at)
 
 HTML_TEMPLATE = """\
 <HTML>
+<HEAD></HEAD>
 <BODY>
-<PRE>
+<BLOCKQUOTE>
 %s
-</PRE>
+</BLOCKQUOTE>
 </BODY>
 </HTML>
 """
(at)(at) -75,14 +79,19 (at)(at)
     part = message.body().find_one('text/html')
     if part:
         text = part.fetch()
+        tree = lxml.etree.parse(StringIO.StringIO(text),
+                                parser=lxml.etree.HTMLParser())
+        body = tree.xpath('body')[0]
+        body.tag = 'DIV'
+        text = lxml.etree.tostring(body)
     else:
         # fall back to text/plain part
         part = message.body().find_one('text/plain')
-        text = HTML_TEMPLATE % (part and part.fetch() or '')
+        text = '<PRE>\n%s\n</PRE>' % (part and part.fetch() or '')
 
     if part:
         encoding = part['parameters'].get('charset', 'ascii')
     else:
         encoding = 'ascii'
 
-    return text.decode(encoding)
+    return HTML_TEMPLATE % text.decode(encoding)

Modified: gocept.restmail/trunk/setup.py
==============================================================================
--- gocept.restmail/trunk/setup.py	(original)
+++ gocept.restmail/trunk/setup.py	Thu Sep 25 10:16:21 2008
(at)(at) -1,6 +1,5 (at)(at)
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 
 from setuptools import setup, find_packages
 
(at)(at) -16,7 +15,8 (at)(at)
     zip_safe=False,
     license='ZPL 2.1',
     namespace_packages=['gocept'],
-    install_requires=['python-cjson',
+    install_requires=['lxml',
+                      'python-cjson',
                       'uuid',
                       ],
 )

SVN: r6702 - in gocept.restmail/trunk/gocept/restmail: . browser
Christian Theune <ct(at)gocept.com>
2008-09-25 10:48:33 [ FULL ]
Author: ctheune
Date: Thu Sep 25 10:48:32 2008
New Revision: 6702

Log:
Implement sending messages. Also added test coverage for saving drafts.



Modified:
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/browser/draft.py
   gocept.restmail/trunk/gocept/restmail/configure.zcml
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt
   gocept.restmail/trunk/gocept/restmail/tests.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Thu Sep 25
10:48:32 2008
(at)(at) -242,6 +242,11 (at)(at)
       attribute="save"
       />
 
+    <browser:page
+      name="send"
+      attribute="send"
+      />
+
   </browser:pages>
 
 </configure>

Modified: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Thu Sep 25 10:48:32
2008
(at)(at) -23,3 +23,8 (at)(at)
         self.context.to = to
         self.context.subject = subject
         self.context.body = body
+
+    def send(self, to, subject, body):
+        """Update draft and send this message."""
+        self.save(to, subject, body)
+        self.context.send()

Modified: gocept.restmail/trunk/gocept/restmail/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/configure.zcml	Thu Sep 25 10:48:32
2008
(at)(at) -12,6 +12,8 (at)(at)
 
   <five:traversable class=".draft.DraftMessage" />
 
+  <five:deprecatedManageAddDelete class=".imapaccount.IMAPAccount" />
+
   <class class=".imapaccount.BodyPart">
     <require interface="gocept.imapapi.interfaces.IBodyPart"
       permission="zope2.View" />

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 10:48:32 2008
(at)(at) -1,6 +1,7 (at)(at)
 # Copyright (c) 2007-2008 gocept gmbh & co. kg
 # See also LICENSE.txt
 
+import email.MIMEText
 import StringIO
 
 import gocept.restmail.interfaces
(at)(at) -42,6 +43,23 (at)(at)
             self.subject = 'Re: ' + subject
         self.body = get_html_text(message)
 
+    def send(self):
+        # 1. Convert to RFC 822 message
+        message = email.MIMEText.MIMEText(self.body, 'html')
+        message['From'] = 'ct(at)gocept.com'
+        message['To'] = self.to
+        message['Subject'] = self.subject
+
+        # 2. Send via MailHost
+        mh = self.MailHost
+        mh.send(message.as_string())     # XXX RAM usage
+
+        # 3. Store in `Sent` Folder
+        # XXX
+
+        # 4. Delete draft
+        self.aq_inner.getParentNode().manage_delObjects([self.getId()])
+
 
 def new_draft(container, message=None):
     id = str(uuid.uuid1())

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Thu Sep 25 10:48:32
2008
(at)(at) -37,3 +37,11 (at)(at)
     to = zope.schema.TextLine(title=u'To')
     subject = zope.schema.TextLine(title=u'Subject')
     body = zope.schema.Text(title=u'Message')
+
+    def send():
+        """Send the message.
+        
+        Will delete the draft, but store a copy of the generated RfC 822
+        message in the account's `Sent` folder.
+
+        """

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Thu Sep 25 10:48:32 2008
(at)(at) -231,8 +231,11 (at)(at)
 {'body': 'Everything is ok!', 'content_type': 'text/plain'}
 
 
+Draft messages
+==============
+
 Creating draft messages
-=======================
+-----------------------
 
 Accounts can store draft messages. They can be created using the
`(at)(at)new_draft`
 view:
(at)(at) -241,5 +244,56 (at)(at)
 >>> print browser.contents
 {"url": "http://localhost/account/drafts/...-...-...-...-..."}
 
->>> cjson.decode(browser.contents)
+>>> draft = cjson.decode(browser.contents)
+>>> draft
 {'url': 'http://localhost/account/drafts/...-...-...-...-...'}
+
+
+Retrieving data of draft messages
+---------------------------------
+
+After a message was created, its data is empty:
+
+>>> browser.open(draft['url']+'/(at)(at)data')
+>>> print browser.contents
+{"body": "", "to": "", "subject": ""}
+>>> cjson.decode(browser.contents)
+{'body': '', 'to': '', 'subject': ''}
+
+Updating draft messages
+-----------------------
+
+The draft's data can be updated:
+
+# XXX New testbrowser support `post`ing directly. The `(at)(at)save` view is a
+# mutating view and shouldn't be called via GET
+
+>>> browser.open(draft['url']+'/(at)(at)save',
+...     'to=ct(at)gocept.com&subject=asdf&body=HelloHello')
+>>> browser.open(draft['url']+'/(at)(at)data')
+>>> print browser.contents
+{"body": "HelloHello", "to": "ct(at)gocept.com", "subject": "asdf"}
+
+Sending draft messages
+----------------------
+
+A draft message can be send. Sending also requires to submit the current data
+again.
+
+>>> browser.open(draft['url']+'/(at)(at)send',
+...     'to=ct(at)gocept.com&subject=asdf&body=HelloHello')
+Content-Type: text/html; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+From: ct(at)gocept.com
+To: ct(at)gocept.com
+Subject: asdf
+<BLANKLINE>
+HelloHello
+
+After successfully sending a message, its draft
+will be deleted.
+
+>>> browser.open(draft['url']+'/(at)(at)data')
+Traceback (most recent call last):
+NotFound: ...

Modified: gocept.restmail/trunk/gocept/restmail/tests.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/tests.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/tests.py	Thu Sep 25 10:48:32 2008
(at)(at) -58,6 +58,7 (at)(at)
 
         self.base_url = zope_root_url
 
+
     def testSetUp(self):
         gocept.imapapi.tests.setUp(None)
 
(at)(at) -87,9 +88,16 (at)(at)
 
 def setUp(test):
     test.app['acl_users']._doAddUser('manager', 'asdf', ('Manager',), [])
+    test.app.manage_addProduct['MailHost'].manage_addMailHost('MailHost')
 
 
 def test_suite():
+    # Monkey patch the mail host for printing.
+    import Products.MailHost.MailHost
+    def new_send(self, message, *args, **kw):
+        print message
+    Products.MailHost.MailHost.MailHost.send = new_send
+
     suite = unittest.TestSuite()
     suite.addTest(FunctionalDocFileSuite('zmi.txt', 'rest.txt',
                                          'connection.txt', setUp=setUp))

SVN: r6705 - gocept.restmail/trunk/gocept/restmail
Christian Theune <ct(at)gocept.com>
2008-09-25 11:19:09 [ FULL ]
Author: ctheune
Date: Thu Sep 25 11:19:07 2008
New Revision: 6705

Log:
Add API to append messages to folders.
Store the sent message in the `Sent` folder.



Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/imapaccount.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 11:19:07 2008
(at)(at) -49,13 +49,16 (at)(at)
         message['From'] = 'ct(at)gocept.com'
         message['To'] = self.to
         message['Subject'] = self.subject
+        message = message.as_string() # XXX Memory usage
 
         # 2. Send via MailHost
         mh = self.MailHost
-        mh.send(message.as_string())     # XXX RAM usage
+        mh.send(message)
 
         # 3. Store in `Sent` Folder
-        # XXX
+        account = self.aq_inner.getParentNode().getParentNode()
+        sent = account.getSentFolder()
+        sent.append(message)
 
         # 4. Delete draft
         self.aq_inner.getParentNode().manage_delObjects([self.getId()])

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Thu Sep 25 11:19:07
2008
(at)(at) -78,6 +78,10 (at)(at)
             folders = [f.__of__(self.aq_inner) for f in folders]
         return folders
 
+    def getSentFolder(self):
+        # XXX Allow this to be differentiated
+        return self.folders('INBOX')[0]
+
     # ZMI support
 
     isPrincipiaFolderish = True
(at)(at) -136,6 +140,9 (at)(at)
             messages = self.folder.messages.values()
         return [Message(message).__of__(self.aq_inner) for message in
messages]
 
+    def append(self, message):
+        self.folder.append(message)
+
     # ZMI support
 
     meta_type = 'IMAP Folder'

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Thu Sep 25 11:19:07
2008
(at)(at) -30,6 +30,9 (at)(at)
 
         """
 
+    def getSentFolder():
+        """Return the folder that is used for storing outgoing messages."""
+
 
 class IDraftMessage(zope.interface.Interface):
     """A draft of a message."""

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Thu Sep 25 11:19:07 2008
(at)(at) -291,9 +291,11 (at)(at)
 <BLANKLINE>
 HelloHello
 
-After successfully sending a message, its draft
-will be deleted.
+After successfully sending a message, its draft will be deleted, but the sent
+message will be in the account's `Sent` folder:
 
 >>> browser.open(draft['url']+'/(at)(at)data')
 Traceback (most recent call last):
 NotFound: ...
+>>> getRootFolder()['account'].getSentFolder().messages()[-1].raw
+'Content-Type: text/html; charset="us-ascii"\r\nMIME-Version:
1.0\r\nContent-Transfer-Encoding: 7bit\r\nFrom: ct(at)gocept.com\r\nTo:
ct(at)gocept.com\r\nSubject: asdf\r\n\r\nHelloHello'

SVN: r6708 - in gocept.restmail/trunk/gocept/restmail: . testmessages
Thomas Lotze <tl(at)gocept.com>
2008-09-25 14:11:36 [ FULL ]
Author: thomas
Date: Thu Sep 25 14:11:34 2008
New Revision: 6708

Log:
refactored extraction of text to quote, added encoding tests


Added:
   gocept.restmail/trunk/gocept/restmail/quoting.txt   (contents, props
changed)
   gocept.restmail/trunk/gocept/restmail/testmessages/
   gocept.restmail/trunk/gocept/restmail/testmessages/00-ascii-no-encoding
   gocept.restmail/trunk/gocept/restmail/testmessages/01-utf8-correct
   gocept.restmail/trunk/gocept/restmail/testmessages/02-utf8-not-declared
   gocept.restmail/trunk/gocept/restmail/testmessages/03-utf8-broken
Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/tests.py

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 14:11:34 2008
(at)(at) -96,23 +96,38 (at)(at)
 """
 
 def get_html_text(message):
-    # find text/html body part
-    part = message.body().find_one('text/html')
+    part = get_text_part(message)
     if part:
-        text = part.fetch()
+        content_type = part['content_type']
+        text = get_unicode_body(part)
+    else:
+        content_type = 'text/plain'
+        text = u''
+
+    if content_type == 'text/html':
         tree = lxml.etree.parse(StringIO.StringIO(text),
                                 parser=lxml.etree.HTMLParser())
         body = tree.xpath('body')[0]
         body.tag = 'DIV'
         text = lxml.etree.tostring(body)
     else:
-        # fall back to text/plain part
-        part = message.body().find_one('text/plain')
-        text = '<PRE>\n%s\n</PRE>' % (part and part.fetch() or '')
+        text = '<PRE>\n%s\n</PRE>' % text
+
+    return HTML_TEMPLATE % text
+
+
+def get_text_part(message):
+    for content_type in ('text/html', 'text/plain'):
+        part = message.body().find_one(content_type)
+        if part:
+            return part
 
-    if part:
-        encoding = part['parameters'].get('charset', 'ascii')
-    else:
-        encoding = 'ascii'
 
-    return HTML_TEMPLATE % text.decode(encoding)
+def get_unicode_body(part):
+    content_type = part['content_type']
+    encoding = part['parameters'].get('charset', 'iso-8859-15')
+    text = part.fetch()
+    try:
+        return text.decode(encoding)
+    except UnicodeDecodeError:
+        return text.decode('iso-8859-15')

Added: gocept.restmail/trunk/gocept/restmail/quoting.txt
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/quoting.txt	Thu Sep 25 14:11:34 2008
(at)(at) -0,0 +1,57 (at)(at)
+=======
+Quoting
+=======
+
+Finding the right part
+======================
+
+Encoding
+========
+
+We have a number of messages in a mail folder named
+testquoting[#loadmessages]_[#getmessages]_[#printunicodebody]_:
+
+>>> from gocept.restmail.draft import get_text_part, get_unicode_body
+>>> p(get_unicode_body(get_text_part(messages[0])))
+I'm a message with no funny characters.
+
+Correctly specified encoding works:
+
+>>> p(get_unicode_body(get_text_part(messages[1])))
+I'm a message with some funny characters:
+- umlauts \xe4\xf6\xfc
+- some japanese spam: \u30e1\u30fc\u30eb\u306e\u3084\u308a\u53d6
+- a little cyrillic: \u0420\u0443\u0441\u0441\u043a\u0438\u0439
+
+But unspecified encoding defaults to ISO-8859-15, in this case mistakenly
+interpreting UTF-8 escape characters as ISO-8859-15 printables:
+
+>>> p(get_unicode_body(get_text_part(messages[2])))
+I'm a message with some funny characters:
+- umlauts \xc3\u20ac\xc3\xb6\xc3\u0152
+- some japanese spam:
\xe3\x83\xa1\xe3\x83\u0152\xe3\x83\xab\xe3\x81\xae\xe3\x82\x84\xe3\x82\x8a\xe5\x8f\x96
+- a little cyrillic:
\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\u017e\xd0\xb9
+
+Broken encoding will result in ISO-8859-15 to be applied as well:
+
+>>> p(get_unicode_body(get_text_part(messages[3])))
+I'm a message with some funny characters:
+- umlauts \xc3\u20ac\xc3\xb6\xc3
+- some japanese spam:
\xe3\x83\xa1\xe3\x83\u0152\xe3\x83\xab\xe3\x81\xae\xe3\x82\x84\xe3\x82\x8a\xe5\x8f\x96
+- a little cyrillic:
\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\u017e\xd0\xb9
+
+
+.. [#loadmessages]
+    >>> import gocept.restmail.tests
+    >>> gocept.restmail.tests.load_messages('testmessages',
'testquoting')
+
+.. [#getmessages]
+    >>> from gocept.restmail.imapaccount import IMAPAccount
+    >>> account = IMAPAccount('account', 'localhost', 10143, 'test',
'bsdf')
+    >>> account = account.__of__(app)
+    >>> folder = account.folders('testquoting')[0]
+    >>> messages = folder.messages()
+
+.. [#printunicodebody]
+    >>> def p(text):
+    ...     print repr(text)[2:-1].replace('\\r\\n', '\n')

Added: gocept.restmail/trunk/gocept/restmail/testmessages/00-ascii-no-encoding
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/testmessages/00-ascii-no-encoding	Thu
Sep 25 14:11:34 2008
(at)(at) -0,0 +1,5 (at)(at)
+From: me(at)example.org
+To: you(at)example.org
+Subject: something simple
+
+I'm a message with no funny characters.

Added: gocept.restmail/trunk/gocept/restmail/testmessages/01-utf8-correct
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/testmessages/01-utf8-correct	Thu Sep
25 14:11:34 2008
(at)(at) -0,0 +1,9 (at)(at)
+From: me(at)example.org
+To: you(at)example.org
+Subject: something simple
+Content-type: text/plain; charset=utf-8
+
+I'm a message with some funny characters:
+- umlauts äöü
+- some japanese spam: メールのやり取
+- a little cyrillic: Русский

Added: gocept.restmail/trunk/gocept/restmail/testmessages/02-utf8-not-declared
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/testmessages/02-utf8-not-declared	Thu
Sep 25 14:11:34 2008
(at)(at) -0,0 +1,9 (at)(at)
+From: me(at)example.org
+To: you(at)example.org
+Subject: something simple
+Content-type: text/plain
+
+I'm a message with some funny characters:
+- umlauts äöü
+- some japanese spam: メールのやり取
+- a little cyrillic: Русский

Added: gocept.restmail/trunk/gocept/restmail/testmessages/03-utf8-broken
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/testmessages/03-utf8-broken	Thu Sep
25 14:11:34 2008
(at)(at) -0,0 +1,9 (at)(at)
+From: me(at)example.org
+To: you(at)example.org
+Subject: something simple
+Content-type: text/plain; charset=utf-8
+
+I'm a message with some funny characters:
+- umlauts ÀöÃ
+- some japanese spam: メヌルのやり取
+- a little cyrillic: РусскОй

Modified: gocept.restmail/trunk/gocept/restmail/tests.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/tests.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/tests.py	Thu Sep 25 14:11:34 2008
(at)(at) -1,9 +1,14 (at)(at)
 # vim:fileencoding=utf-8
 # Copyright (c) 2008 gocept gmbh & co. kg
 # See also LICENSE.txt
-# $Id$
 """Test harness for gocept.restmail."""
 
+import imaplib
+import os
+import os.path
+import time
+import unittest
+
 from zope.testing import doctest
 
 import App.Product
(at)(at) -11,9 +16,9 (at)(at)
 import Testing.ZopeTestCase
 import Testing.ZopeTestCase.ZopeLite
 import Testing.ZopeTestCase.utils
-import gocept.imapapi.tests
 import transaction
-import unittest
+
+import gocept.imapapi.tests
 
 
 class Zope2FunctionalLayer(object):
(at)(at) -91,6 +96,35 (at)(at)
     test.app.manage_addProduct['MailHost'].manage_addMailHost('MailHost')
 
 
+def load_messages(path, folder_name):
+    server = imaplib.IMAP4('localhost', 10143)
+    server.login('test', 'bsdf')
+    # Clean up the test folder from previous runs. We do not delete at the
+    # end of a run to preserve data for debugging purposes.
+    server.delete(folder_name)
+
+    # Re-create the test folder.
+    gocept.imapapi.tests.callIMAP(server, 'create', folder_name)
+
+    # Create messages in the test folder.
+    path = os.path.join(os.path.dirname(__file__), path)
+    for filename in sorted(os.listdir(path)):
+        if filename.startswith('.') or filename.endswith('~'):
+            continue
+        filepath = os.path.join(path, filename)
+        timestamp = os.path.getmtime(filepath)
+        localtime = time.localtime(timestamp)
+        date = time.strftime('"%d-%b-%Y %H:%M:%S +0200"', localtime)
+        message = open(filepath).read()
+        gocept.imapapi.tests.callIMAP(
+            server, 'append', folder_name, '', date, message)
+
+    # Done.
+    status, data = server.logout()
+    assert status == 'BYE'
+
+
+
 def test_suite():
     # Monkey patch the mail host for printing.
     import Products.MailHost.MailHost
(at)(at) -99,6 +133,11 (at)(at)
     Products.MailHost.MailHost.MailHost.send = new_send
 
     suite = unittest.TestSuite()
-    suite.addTest(FunctionalDocFileSuite('zmi.txt', 'rest.txt',
-                                         'connection.txt', setUp=setUp))
+    suite.addTest(FunctionalDocFileSuite(
+        'connection.txt',
+        'quoting.txt',
+        'rest.txt',
+        'zmi.txt',
+        setUp=setUp,
+        optionflags=doctest.INTERPRET_FOOTNOTES))
     return suite

SVN: r6709 - in gocept.restmail/trunk/gocept/restmail: . testmessages
Christian Zagrodnick <cz(at)gocept.com>
2008-09-25 14:55:30 [ FULL ]
Author: zagy
Date: Thu Sep 25 14:55:28 2008
New Revision: 6709

Log:
test that get_text_part works correctly


Added:
   gocept.restmail/trunk/gocept/restmail/testmessages/04-html
Modified:
   gocept.restmail/trunk/gocept/restmail/quoting.txt

Modified: gocept.restmail/trunk/gocept/restmail/quoting.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/quoting.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/quoting.txt	Thu Sep 25 14:55:28 2008
(at)(at) -5,13 +5,29 (at)(at)
 Finding the right part
 ======================
 
+When there is only a text part it will be
+choosen[#loadmessages]_[#getmessages]_[#printunicodebody]_:
+
+>>> from gocept.restmail.draft import get_text_part, get_unicode_body
+>>> part = get_text_part(messages[0])
+>>> part
+<gocept.imapapi.message.BodyPart object at 0x...>
+>>> part.fetch()
+"I'm a message with no funny characters.\r\n"
+
+When a mail contains HTML, the HTML part will be used:
+
+>>> print get_text_part(messages[4]).fetch()
+<html><head>
+	<title>Air Mail</title>
+    ...
+
+
 Encoding
 ========
 
-We have a number of messages in a mail folder named
-testquoting[#loadmessages]_[#getmessages]_[#printunicodebody]_:
+We have a number of messages in a mail folder named testquoting:
 
->>> from gocept.restmail.draft import get_text_part, get_unicode_body
 >>> p(get_unicode_body(get_text_part(messages[0])))
 I'm a message with no funny characters.
 

Added: gocept.restmail/trunk/gocept/restmail/testmessages/04-html
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/testmessages/04-html	Thu Sep 25
14:55:28 2008
(at)(at) -0,0 +1,131 (at)(at)
+To: Thomas Lotze <tl(at)gocept.com>
+Message-Id: <18EE7CC0-44FB-4802-BA8E-58F7BE1F7274(at)gocept.com>
+Content-Type: multipart/signed; boundary=Apple-Mail-138--776877415;
micalg=sha1; protocol="application/pkcs7-signature"
+Subject: HA Tee Em El
+Mime-Version: 1.0 (Apple Message framework v929.2)
+Date: Thu, 25 Sep 2008 13:50:58 +0200
+X-Mailer: Apple Mail (2.929.2)
+
+
+--Apple-Mail-138--776877415
+Content-Type: multipart/alternative;
+	boundary=Apple-Mail-136--776877495
+
+
+--Apple-Mail-136--776877495
+Content-Type: text/plain;
+	charset=US-ASCII;
+	format=flowed;
+	delsp=yes
+Content-Transfer-Encoding: 7bit
+
+
+
+
+  	
+
+  	
+Fun in the sun.
+
+Duis non sequ ismodol oreetuer irilet dolore facidunt, vulluptat se  
+volore consecte dolesed dolor se velit et ver adion se magnisc illandi  
+eti gniaet, vendre feumert aeniamc onullutpatin hent ipiteti wisi esse  
+dolesting ero dunt utpatin aut ipit, quation ullum ea autpatie eui  
+nulla facilit la consectet odipsum magnis henim nit augiatie doluptat.  
+Ut landiat te dolobortis nulput autpat luptate ndigniat vel doluptat  
+niam dolorti inullaore.
+
+Click here to see my photos: http://www.apple.com
+
+Catch up with you soon,
+Tracey
+
+
+
+
+
+--Apple-Mail-136--776877495
+Content-Type: multipart/related;
+	boundary=Apple-Mail-137--776877494;
+	type="text/html"
+
+
+--Apple-Mail-137--776877494
+Content-Type: text/html;
+	charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><head>
+	<title>Air Mail</title>
+</head><body style="background-repeat: initial;
background-attachment: initial; -webkit-background-clip: initial;
-webkit-background-origin: initial; background-color: initial; margin-top: 0px;
margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px;
padding-right: 0px; padding-bottom: 0px; padding-left: 0px; background-image:
url(cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/bg_pattern.jpg);
background-position: 50% 0px; "> 
+<div style="background-repeat: initial; background-attachment: initial;
-webkit-background-clip: initial; -webkit-background-origin: initial;
background-color: initial; background-image:
url(cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/bg_pattern.jpg);
background-position: 50% 0px; ">
+	<table id="main-table" width="677" cellpadding="0" cellspacing="0"
align="center">
+		<tbody><tr>
+			<td id="tbg" colspan="3"><img width="677" height="28" alt=""
style="display:block"
src="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/tbg.jpg"></td>
+		</tr>
+		<tr>
+			<td id="head" colspan="3"><img width="677" height="220"
style="display:block"
src="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/2/Photos"></td>
+		</tr>
+		<tr>
+			<td id="left-back" rowspan="3" width="27"
background="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/lbg.jpg">&nbsp;</td>
+			<td id="body-center" width="621" style="line-height:2;"
background="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/letter_bg.jpg"><table
cellspacing="0" cellpadding="0" class="spacing-table-for-ie5-and-hotmail">
+					<tbody><tr>
+						<td class="top-spacing" colspan="3" height="13"
style="font-size:1px">&nbsp;</td>
+					</tr>
+					<tr>
+						<td width="48" class="left-gutter-spacing"
style="font-size:1px">&nbsp;</td>
+						<td class="content-holder AppleTilingElement" valign="top"
height="362"><div class="AppleResizingDiv">
+							<div class="body-title" id="body-title"><font face="Times,
serif" color="#e53b19" style="font-size:31px"><em>
+								<span apple-content-name="title" style="display:block;width:525px"
applecontenteditable="true">Fun in the sun.</span>
+							</em></font></div>
+							<br>
+							<font face="Times, serif" color="#000000"
style="font-size:15.5px"><div class="body-content" id="body-content"
style="line-height:1.75;">
+								<span apple-content-name="body" style="display:block;width:525px"
applecontenteditable="true">
+									<div>Duis non sequ ismodol oreetuer irilet dolore facidunt,
vulluptat se volore consecte dolesed dolor se velit et ver adion se magnisc
illandi eti gniaet, vendre feumert aeniamc onullutpatin hent ipiteti wisi esse
dolesting ero dunt utpatin aut ipit, quation ullum ea autpatie eui nulla
facilit la consectet odipsum magnis henim nit augiatie doluptat. Ut landiat te
dolobortis nulput autpat luptate ndigniat vel doluptat niam dolorti
inullaore.<div><br></div></div>
+									<div>Click here to see my photos: <a href="http://www.apple.com">http://www.apple.com</a><div><br></div></div>
+									<div>Catch up with you soon,<br>Tracey</div>
+								</span>
+							</div></font>
+						</div></td>
+						<td width="48" class="right-gutter-spacing"
style="font-size:1px">&nbsp;</td>
+					</tr>
+					<tr>
+						<td class="bottom-spacing" colspan="3" height="15"
style="font-size:1px">&nbsp;</td>
+					</tr>
+				</tbody></table>
+			</td>
+			<td id="right-back" rowspan="3" width="29"
background="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/rbg.jpg">&nbsp;</td>
+		</tr>
+		<tr>
+			<td id="foot" class="foot"><img width="622" height="27" alt=""
style="display:block"
src="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/bottom.jpg"></td>
+		</tr>
+		<tr>
+			<td id="bg" class="bbg"><img width="622" height="25" alt=""
style="display:block"
src="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/bbg.jpg"></td>
+		</tr>
+	</tbody></table>
+</div>
+
+</body></html>
+--Apple-Mail-137--776877494
+Content-Disposition: inline;
+	filename=tbg.jpg
+Content-Transfer-Encoding: base64
+Content-Type: image/jpeg;
+	x-unix-mode=0664;
+	x-apple-mail-type=stationery;
+	name="tbg.jpg"
+Content-Id: <BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/tbg.jpg>
+
+/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAASAAA/+IFOElDQ19QUk9GSUxFAAEB
+AAAFKGFwcGwCIAAAbW50clJHQiBYWVogB9IABQANAAwAAAAAYWNzcEFQUEwAAAAAYXBwbAAAAAAA
+AAAAAAAAAAAAAAEAAPbWAAEAAAAA0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAANclhZWgAAASAAAAAUZ1hZWgAAATQAAAAUYlhZWgAAAUgAAAAUd3Rw
+dAAAAVwAAAAUY2hhZAAAAXAAAAAsclRSQwAAAZwAAAAOZ1RSQwAAAZwAAAAOYlRSQwAAAZwAAAAO
+...
+ZGVzYwAAAawAAAA/Y3BydAAAAlQAAABIdmNndAAAAewAAAAwbmRpbgAAAhwAAAA4ZHNjbQAAApwA
+SwRnW3HT0wlclqSRREiS/GubA6vRnFPvj6hg0XJJLfxmspnL82mixjCk9kcyRJYn301C0uPBDt9e
+mjskPS91D1rrs5l99lH5ERiUQTx6Ivb1/aICMEWL43yB59Dnk5ttT5XAcBV9jUx8Qyku9jdRYku8
+43GCZEeAoW9EU2C/YBy/NFpCnuh4Nl6Iz6GLYbq0lvv4OBVN2wAAAAAAAA==
+
+--Apple-Mail-138--776877415--
+

SVN: r6711 - in gocept.restmail/trunk/gocept/restmail: . browser www
Christian Theune <ct(at)gocept.com>
2008-09-25 16:04:28 [ FULL ]
Author: ctheune
Date: Thu Sep 25 16:04:24 2008
New Revision: 6711

Log:
Add concept of `profiles` which group together accounts, store draft mails and
manage SMTP `mail host` objects and identities.

Clean up documentation.



Added:
   gocept.restmail/trunk/gocept/restmail/browser/profile.py   (contents, props
changed)
   gocept.restmail/trunk/gocept/restmail/profile.py   (contents, props changed)
   gocept.restmail/trunk/gocept/restmail/www/add_profile.pt   (contents, props
changed)
Modified:
   gocept.restmail/trunk/gocept/restmail/__init__.py
   gocept.restmail/trunk/gocept/restmail/browser/account.py
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/configure.zcml
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/imapaccount.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt
   gocept.restmail/trunk/gocept/restmail/zmi.txt

Modified: gocept.restmail/trunk/gocept/restmail/__init__.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/__init__.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/__init__.py	Thu Sep 25 16:04:24 2008
(at)(at) -2,6 +2,14 (at)(at)
 def initialize(context):
     import gocept.restmail.imapaccount
     import gocept.restmail.draft
+    import gocept.restmail.profile
+
+    context.registerClass(
+        gocept.restmail.profile.Profile,
+        meta_type='Webmail Profile',
+        constructors=(gocept.restmail.profile.manage_addProfileForm,
+                      gocept.restmail.profile.manage_addProfile),
+        icon='www/account.gif')
 
     context.registerClass(
         gocept.restmail.imapaccount.IMAPAccount,

Modified: gocept.restmail/trunk/gocept/restmail/browser/account.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/account.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/account.py	Thu Sep 25
16:04:24 2008
(at)(at) -10,8 +10,4 (at)(at)
 class WebAPI(object):
     """Web API for accounts."""
 
-    def new_draft(self, message=None):
-        """Create a new draft message."""
-        draft = self.context.new_draft(message)
-        data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
-        return cjson.encode(data)
+    pass

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Thu Sep 25
16:04:24 2008
(at)(at) -219,14 +219,6 (at)(at)
     attribute="content"
     />
 
-  <browser:page
-    name="new_draft"
-    for="gocept.restmail.interfaces.IIMAPAccount"
-    permission="zope2.View"
-    class=".account.WebAPI"
-    attribute="new_draft"
-    />
-
   <browser:pages
     for="gocept.restmail.interfaces.IDraftMessage"
     permission="zope2.View"
(at)(at) -249,4 +241,26 (at)(at)
 
   </browser:pages>
 
+  <browser:pages
+    for="gocept.restmail.interfaces.IProfile"
+    permission="zope2.View"
+    class=".profile.WebAPI">
+
+    <browser:page
+      name="accounts"
+      attribute="accounts"
+      />
+
+    <browser:page
+      name="new_draft"
+      attribute="new_draft"
+      />
+
+    <browser:page
+      name="default_identity"
+      attribute="default_identity"
+      />
+
+  </browser:pages>
+
 </configure>

Added: gocept.restmail/trunk/gocept/restmail/browser/profile.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/browser/profile.py	Thu Sep 25
16:04:24 2008
(at)(at) -0,0 +1,30 (at)(at)
+# vim:fileencoding=utf-8
+# Copyright (c) 2008 gocept gmbh & co. kg
+# See also LICENSE.txt
+"""Profile methods."""
+
+import cjson
+import zope.app.zapi
+
+
+class WebAPI(object):
+    """Web API for profiles."""
+
+    def accounts(self):
+        """List the accounts in this profile."""
+        data = [{'url': zope.app.zapi.absoluteURL(account, self.request),
+                 'children': len(account.folders()),
+                 'name': account.title_or_id()}
+                for account in self.context.objectValues('IMAP Account')]
+        return cjson.encode(data)
+
+    def new_draft(self, message=None):
+        """Create a new draft message."""
+        draft = self.context.new_draft(message)
+        data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
+        return cjson.encode(data)
+
+    def default_identity(self):
+        data = {'name': self.context.name,
+                'address': self.context.address}
+        return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/configure.zcml	Thu Sep 25 16:04:24
2008
(at)(at) -12,7 +12,9 (at)(at)
 
   <five:traversable class=".draft.DraftMessage" />
 
-  <five:deprecatedManageAddDelete class=".imapaccount.IMAPAccount" />
+  <five:traversable class=".profile.Profile" />
+
+  <five:deprecatedManageAddDelete class=".profile.Profile" />
 
   <class class=".imapaccount.BodyPart">
     <require interface="gocept.imapapi.interfaces.IBodyPart"
(at)(at) -34,5 +36,11 (at)(at)
       permission="zope2.View" />
   </class>
 
+  <class class=".profile.Profile">
+    <require interface="gocept.restmail.interfaces.IProfile"
+      permission="zope2.View" />
+  </class>
+
+  <adapter factory=".profile.lookup_profile" />
 
 </configure>

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 16:04:24 2008
(at)(at) -44,20 +44,21 (at)(at)
         self.body = get_html_text(message)
 
     def send(self):
+        profile = gocept.restmail.interfaces.IProfile(self)
+
         # 1. Convert to RFC 822 message
         message = email.MIMEText.MIMEText(self.body, 'html')
-        message['From'] = 'ct(at)gocept.com'
+        message['From'] = '%s <%s>' % (profile.name, profile.address)
         message['To'] = self.to
         message['Subject'] = self.subject
         message = message.as_string() # XXX Memory usage
 
         # 2. Send via MailHost
-        mh = self.MailHost
+        mh = profile[profile.smtp_server]
         mh.send(message)
 
         # 3. Store in `Sent` Folder
-        account = self.aq_inner.getParentNode().getParentNode()
-        sent = account.getSentFolder()
+        sent = profile.getSentFolder()
         sent.append(message)
 
         # 4. Delete draft

Modified: gocept.restmail/trunk/gocept/restmail/imapaccount.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/imapaccount.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/imapaccount.py	Thu Sep 25 16:04:24
2008
(at)(at) -3,7 +3,6 (at)(at)
 # See also LICENSE.txt
 """IMAP account object."""
 
-
 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
 from Products.Five.traversable import Traversable
 from OFS.SimpleItem import SimpleItem, Item
(at)(at) -48,12 +47,6 (at)(at)
         self.user = user
         self.password = password
 
-    def manage_afterAdd(self, item, container):
-        self.manage_addProduct['OFSP'].manage_addFolder('drafts')
-
-    def new_draft(self, message=None):
-        return gocept.restmail.draft.new_draft(self['drafts'], message)
-
     def connect(self):
         try:
             account = self._v_account
(at)(at) -78,10 +71,6 (at)(at)
             folders = [f.__of__(self.aq_inner) for f in folders]
         return folders
 
-    def getSentFolder(self):
-        # XXX Allow this to be differentiated
-        return self.folders('INBOX')[0]
-
     # ZMI support
 
     isPrincipiaFolderish = True

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Thu Sep 25 16:04:24
2008
(at)(at) -7,6 +7,25 (at)(at)
 import zope.schema
 
 
+class IProfile(zope.interface.Interface):
+    """A multi-account profile for one person."""
+
+    name = zope.schema.TextLine(title=u'Real name')
+    address = zope.schema.TextLine(title=u'Mail address')
+    smtp_server = zope.schema.TextLine(title=u'Outgoing mail server')
+
+    def new_draft(message=None):
+        """Add a draft message to the account.
+
+        If message is not None, the new draft message is prepared as a reply
+        to it.
+
+        """
+
+    def getSentFolder():
+        """Return the folder that is used for storing outgoing messages."""
+
+
 class IIMAPAccount(zope.interface.Interface):
     """An account on an IMAP service.
 
(at)(at) -22,17 +41,6 (at)(at)
     def connect():
         """Return a gocept.imapapi.interfaces.IAccount object."""
 
-    def new_draft(message=None):
-        """Add a draft message to the account.
-
-        If message is not None, the new draft message is prepared as a reply
-        to it.
-
-        """
-
-    def getSentFolder():
-        """Return the folder that is used for storing outgoing messages."""
-
 
 class IDraftMessage(zope.interface.Interface):
     """A draft of a message."""

Added: gocept.restmail/trunk/gocept/restmail/profile.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/profile.py	Thu Sep 25 16:04:24 2008
(at)(at) -0,0 +1,65 (at)(at)
+# Copyright (c) 2007-2008 gocept gmbh & co. kg
+# See also LICENSE.txt
+
+from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+import gocept.restmail.interfaces
+import zope.interface
+
+from OFS.SimpleItem import Item
+from OFS.ObjectManager import ObjectManager
+from OFS.PropertyManager import PropertyManager
+
+
+class Profile(ObjectManager, PropertyManager, Item):
+    """A multi-account profile for one person."""
+
+    zope.interface.implements(gocept.restmail.interfaces.IProfile)
+
+    meta_type = 'Webmail Profile'
+
+    manage_options = (ObjectManager.manage_options + 
+                      PropertyManager.manage_options +
+                      Item.manage_options)
+
+    _properties = (dict(id='name', type='string', mode='w'),
+                   dict(id='address', type='string', mode='w'),
+                   dict(id='smtp_server', type='string', mode='w'))
+
+    name = u''
+    address = u''
+    smtp_server = u''
+
+    def __init__(self, id):
+        self.id = id
+
+    def manage_afterAdd(self, item, container):
+        self.manage_addProduct['OFSP'].manage_addFolder('drafts')
+
+    def new_draft(self, message=None):
+        return gocept.restmail.draft.new_draft(self['drafts'], message)
+
+    def getSentFolder(self):
+        # XXX Allow this to be differentiated
+        return self.objectValues('IMAP Account')[0].folders('INBOX')[0]
+
+
+manage_addProfileForm = PageTemplateFile('www/add_profile.pt', globals())
+
+
+def manage_addProfile(context, id):
+    """ZMI helper to create a new webmail profile."""
+    profile = Profile(id)
+    container = context.this()
+    container._setObject(id, profile)
+    profile = container[id]
+    context.REQUEST.RESPONSE.redirect(
+        profile.absolute_url() + '/manage_workspace')
+
+
+(at)zope.component.adapter(zope.interface.Interface)
+(at)zope.interface.implementer(gocept.restmail.interfaces.IProfile)
+def lookup_profile(context):
+    while context:
+        if gocept.restmail.interfaces.IProfile.providedBy(context):
+            return context
+        context = context.aq_inner.getParentNode()

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Thu Sep 25 16:04:24 2008
(at)(at) -2,6 +2,11 (at)(at)
 The RESTful API
 ===============
 
+.. contents::
+
+Introduction
+============
+
 The RESTful API is the heart of gocept.restmail. It provides access to mail
 accounts, folders and messages by making them reachable as web resources
 (using URLs) and applying views and operations to them.
(at)(at) -15,12 +20,20 (at)(at)
 might include a lot of data that may take a while to download, slowing down
 the client potentially.
 
-Lets create an account first that can be tested:
+Lets create a profile and an account that can be tested:
 
 >>> from Products.Five.testbrowser import Browser
 >>> browser = Browser()
 >>> browser.addHeader('Authorization', 'Basic manager:asdf')
 >>> browser.open('http://localhost/manage_main')
+>>> browser.getControl(name=':action').displayValue = ['Webmail
Profile']
+>>> browser.getControl('Add').click()
+>>> browser.getControl('Id').value = 'profile'
+>>> browser.getControl('Add').click()
+>>> getRootFolder()['profile'].name = 'Ben Utzer'
+>>> getRootFolder()['profile'].address = 'ben(at)example.com'
+>>> getRootFolder()['profile'].smtp_server = 'default'
+>>>
getRootFolder()['profile'].manage_addProduct['MailHost'].manage_addMailHost('default')
 >>> browser.getControl(name=':action').displayValue = ['IMAP
Account']
 >>> browser.getControl('Add').click()
 >>> browser.getControl('Id').value = 'account'
(at)(at) -31,11 +44,15 (at)(at)
 >>> browser.getControl('Add').click()
 >>> browser.handleErrors = False
 
-Resource URLs
-=============
+URL schemes
+===========
+
+Resources
+---------
 
 The REST API exposes various kinds of resources, including:
 
+- Profiles
 - Accounts
 - Folders
 - Messages
(at)(at) -44,48 +61,77 (at)(at)
 All of those are accessible by a URL. To allow access to sub-folders and
 messages on folders we adhere to the following URL path scheme:
 
-Account names
+Profile and account names
   have no special rules
 
 Folder names
-  are prefixed with a '+', like
-  http://localhost/account/+INBOX
+  are prefixed with a `+`, like:
+  `http://localhost/profile/account/+INBOX`
 
 Message names
-  are prefixed with an '*', like:
-  http://localhost/account/+INBOX/*1-123
+  are prefixed with an `*`, like:
+  `http://localhost/profile/account/+INBOX/*1-123`
 
 Message parts
   have no special rules.
 
-View URLs
-=========
+Views
+-----
 
 View URLs are explicitly defined only for the namespaced-variant using the
-`(at)(at)` or `++view++` prefixes. The tend to work without the prefix at some
+`(at)(at)` or `++view++` prefixes. They tend to work without the prefix at
some
 points too, but don't rely on it. Always ask for a view like this:
 
-  http://localhost/account/(at)(at)folders
+  http://localhost/profile/account/(at)(at)folders
+
+Profile API
+===========
+
+A profile is a container for accounts. It also manages some general
+user preferences, like the default identity.
+
+Default identity
+----------------
+
+The default identity should be used when composing a new draft without
+a reference to an existing account or message. It identifies the public
+name, return address and the SMTP server that will be used:
+
+>>> browser.open('http://localhost/profile/(at)(at)default_identity')
+>>> print browser.contents
+{"name": "Ben Utzer", "address": "ben(at)example.com"}
+
+Account listing
+---------------
+
+Each profile can list the accounts that are registered with it:
+
+>>> browser.open('http://localhost/profile/(at)(at)accounts')
+>>> print browser.contents
+[{"url": "http://localhost/profile/account",
"children": 2, "name": "account"}]
 
 
-Browsing the account's folders
-===============================
+Account API
+===========
 
-Our account has the URL `http://localhost/account`. The view
`(at)(at)folders`
-returns a JSON representation of the account's folder list:
+Browsing folders
+----------------
 
->>> browser.open('http://localhost/account/(at)(at)folders')
+Our account has the URL `http://localhost/profile/account`.
The view
+`(at)(at)folders` returns a JSON representation of the account's folder list:
+
+>>> browser.open('http://localhost/profile/account/(at)(at)folders')
 >>> print browser.contents
-[{"url": "http://localhost/account/+Bar",
"name": "Bar", "children": 0},
- {"url": "http://localhost/account/+INBOX",
"name": "INBOX", "children": 1}]
+[{"url": "http://localhost/profile/account/+Bar",
"name": "Bar", "children": 0},
+ {"url": "http://localhost/profile/account/+INBOX",
"name": "INBOX", "children": 1}]
 
 This is valid JSON and we can parse it:
 
 >>> import cjson
 >>> folders = cjson.decode(browser.contents)
 >>> folders
-[{'url': 'http://localhost/account/+Bar',
'name': 'Bar', 'children': 0},
- {'url': 'http://localhost/account/+INBOX',
'name': 'INBOX', 'children': 1}]
+[{'url': 'http://localhost/profile/account/+Bar',
'name': 'Bar', 'children': 0},
+ {'url': 'http://localhost/profile/account/+INBOX',
'name': 'INBOX', 'children': 1}]
 >>> len(folders)
 2
 
(at)(at) -93,7 +139,7 (at)(at)
 folder, it's user readable name and the number of sub-folders:
 
 >>> folders[0]['url']
-'http://localhost/account/+Bar'
+'http://localhost/profile/account/+Bar'
 >>> folders[0]['name']
 'Bar'
 >>> folders[0]['children']
(at)(at) -101,34 +147,33 (at)(at)
 
 The `(at)(at)folders` view can be used both on the account and on folders:
 
->>> browser.open('http://localhost/account/+INBOX/(at)(at)folders')
+>>> browser.open('http://localhost/profile/account/+INBOX/(at)(at)folders')
 >>> print browser.contents
-[{"url": "http://localhost/account/+INBOX/+Baz",
"name": "Baz",
+[{"url": "http://localhost/profile/account/+INBOX/+Baz",
"name": "Baz",
   "children": 0}]
 
+Creating new folders
+--------------------
 
-Creating new mail folders
-=========================
-
->>> browser.open('http://localhost/account/+INBOX/(at)(at)create_folder')
+>>> browser.open('http://localhost/profile/account/+INBOX/(at)(at)create_folder')
 Traceback (most recent call last):
 HTTPError: HTTP Error 400: Bad Request
 
 # XXX How to post made-up form data or JSON easiest?
 
 
-Retrieve a list of mails
-========================
+Retrieve a list of messages
+---------------------------
 
-Every folder provides the view `(at)(at)messages` which lists all messages of
a
-folder:
+Every folder provides the view `(at)(at)messages` which lists all messages of
+a folder:
 
->>> browser.open('http://localhost/account/+INBOX/(at)(at)messages')
+>>> browser.open('http://localhost/profile/account/+INBOX/(at)(at)messages')
 >>> print browser.contents
-[{"url": "http://localhost/account/+INBOX/*...",
+[{"url": "http://localhost/profile/account/+INBOX/*...",
   "Date": "02-Jul-2008 03:05:00 +0200", "From": "test(at)localhost",
   "Subject": "Mail 1"},
- {"url": "http://localhost/account/+INBOX/*...",
+ {"url": "http://localhost/profile/account/+INBOX/*...",
   "Date": "02-Jul-2008 03:06:00 +0200", "From": "test(at)localhost",
   "Subject": "Mail 2"}]
 
(at)(at) -136,10 +181,10 (at)(at)
 
 >>> messages = cjson.decode(browser.contents)
 >>> messages
-[{'url': 'http://localhost/account/+INBOX/*...',
+[{'url': 'http://localhost/profile/account/+INBOX/*...',
   'Date': '02-Jul-2008 03:05:00 +0200', 'From': 'test(at)localhost',
   'Subject': 'Mail 1'},
- {'url': 'http://localhost/account/+INBOX/*...',
+ {'url': 'http://localhost/profile/account/+INBOX/*...',
   'Date': '02-Jul-2008 03:06:00 +0200', 'From': 'test(at)localhost',
   'Subject': 'Mail 2'}]
 >>> len(messages)
(at)(at) -149,7 +194,7 (at)(at)
 `From` and `Subject`.
 
 >>> messages[0]['url']
-'http://localhost/account/+INBOX/*...'
+'http://localhost/profile/account/+INBOX/*...'
 >>> messages[0]['Date']
 '02-Jul-2008 03:05:00 +0200'
 >>> messages[0]['Subject']
(at)(at) -157,22 +202,21 (at)(at)
 >>> messages[0]['From']
 'test(at)localhost'
 
-Please note that only `url` is promised to have a reasonable value. Messages
-might not provide the date, subject or from header in which case the field
-will be `null`.
-
-XXX Also note that the date format is currently according to RFC 822 but this
-will likely change in the future.
+Please note that only `url` is promised to have a reasonable value.
+Messages might not provide the date, subject or from header in which
+case the field will be `null`.
 
+XXX Also note that the date format is currently according to RFC 822
+but this will likely change in the future.
 
-Retrieving data of an individual mail
-=====================================
+Message API
+===========
 
 XXX Expand by demonstrating more complex message structures.
 
-The metadata for each mail (including the headers) can be retrieved using the
-`(at)(at)metadata` view on the resource. This currently lists all headers of
the
-mail as well as its hierarchical structure:
+The metadata for each message (including the headers) can be retrieved using
+the `(at)(at)metadata` view on the resource. This currently lists all headers
of the
+message as well as its hierarchical structure:
 
 >>> browser.open(messages[0]['url']+'/(at)(at)metadata')
 >>> print browser.contents
(at)(at) -180,27 +224,29 (at)(at)
   {"X-No-Encoding-Header": "Text \ufffd or not",
    "X-Correct-Encoding-Header": "Text \u00fc",
    "X-Wrong-Encoding-Header": "Text \ufffd\ufffd",
+   "X-Unknown-Encoding-Header": "Text \ufffd\ufffd",
    "Date": "02-Jul-2008 03:05:00 +0200",
    "From": "test(at)localhost",
    "X-IMAPAPI-Test": "1",
    "Subject": "Mail 1"},
  "structure":
-  {"url": "http://localhost/account/+INBOX/*.../*body",
+  {"url": "http://localhost/profile/account/+INBOX/*.../*body",
    "content_type": "text/plain"}}
 
 Again, this is valid JSON:
 
 >>> cjson.decode(browser.contents)
 {'headers':
-  {'From': 'test(at)localhost',
+  {'X-Unknown-Encoding-Header': u'Text \ufffd\ufffd',
    'X-Correct-Encoding-Header': u'Text \xfc',
+   'X-No-Encoding-Header': u'Text \ufffd or not',
    'X-Wrong-Encoding-Header': u'Text \ufffd\ufffd',
    'Date': '02-Jul-2008 03:05:00 +0200',
-   'X-No-Encoding-Header': u'Text \ufffd or not',
+   'From': 'test(at)localhost',
    'X-IMAPAPI-Test': '1',
    'Subject': 'Mail 1'},
  'structure':
-  {'url': 'http://localhost/account/+INBOX/*.../*body',
+  {'url': 'http://localhost/profile/account/+INBOX/*.../*body',
    'content_type': 'text/plain'}}
 
 Each body part is published as a resource and can be downloaded by accessing
(at)(at) -214,8 +260,8 (at)(at)
 Everything is ok!
 
 
-Accessing MIME parts of a message's body
-========================================
+Accessing MIME parts
+--------------------
 
 Every message consists of at least one part which has a body of data and some
 properties, among them at least the part's MIME type. We can access these data
(at)(at) -231,22 +277,22 (at)(at)
 {'body': 'Everything is ok!', 'content_type': 'text/plain'}
 
 
-Draft messages
-==============
+Sending messages
+================
 
 Creating draft messages
 -----------------------
 
-Accounts can store draft messages. They can be created using the
`(at)(at)new_draft`
+Profiles can store draft messages. They can be created using the
`(at)(at)new_draft`
 view:
 
->>> browser.open('http://localhost/account/(at)(at)new_draft')
+>>> browser.open('http://localhost/profile/(at)(at)new_draft')
 >>> print browser.contents
-{"url": "http://localhost/account/drafts/...-...-...-...-..."}
+{"url": "http://localhost/profile/drafts/...-...-...-...-..."}
 
 >>> draft = cjson.decode(browser.contents)
 >>> draft
-{'url': 'http://localhost/account/drafts/...-...-...-...-...'}
+{'url': 'http://localhost/profile/drafts/...-...-...-...-...'}
 
 
 Retrieving data of draft messages
(at)(at) -285,7 +331,7 (at)(at)
 Content-Type: text/html; charset="us-ascii"
 MIME-Version: 1.0
 Content-Transfer-Encoding: 7bit
-From: ct(at)gocept.com
+From: Ben Utzer <ben(at)example.com>
 To: ct(at)gocept.com
 Subject: asdf
 <BLANKLINE>
(at)(at) -297,5 +343,5 (at)(at)
 >>> browser.open(draft['url']+'/(at)(at)data')
 Traceback (most recent call last):
 NotFound: ...
->>> getRootFolder()['account'].getSentFolder().messages()[-1].raw
-'Content-Type: text/html; charset="us-ascii"\r\nMIME-Version:
1.0\r\nContent-Transfer-Encoding: 7bit\r\nFrom: ct(at)gocept.com\r\nTo:
ct(at)gocept.com\r\nSubject: asdf\r\n\r\nHelloHello'
+>>> getRootFolder()['profile'].getSentFolder().messages()[-1].raw
+'Content-Type: text/html; charset="us-ascii"\r\nMIME-Version:
1.0\r\nContent-Transfer-Encoding: 7bit\r\nFrom: Ben Utzer
<ben(at)example.com>\r\nTo: ct(at)gocept.com\r\nSubject:
asdf\r\n\r\nHelloHello'

Added: gocept.restmail/trunk/gocept/restmail/www/add_profile.pt
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/www/add_profile.pt	Thu Sep 25
16:04:24 2008
(at)(at) -0,0 +1,31 (at)(at)
+<h1 tal:replace="structure here/manage_page_header">Header</h1>
+
+<h2>Add webmail profile</h2>
+
+<form action="manage_addProfile" method="post"
+      enctype="multipart/form-data">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+      <label for="id">Id</label>
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" id="id" name="id" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit"
+     value=" Add " />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<h1 tal:replace="structure here/manage_page_footer">Footer</h1>

Modified: gocept.restmail/trunk/gocept/restmail/zmi.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/zmi.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/zmi.txt	Thu Sep 25 16:04:24 2008
(at)(at) -10,7 +10,21 (at)(at)
 >>> browser.addHeader('Authorization', 'Basic manager:asdf')
 >>> browser.open('http://localhost/manage_main')
 
-Lets start by creating an IMAP account:
+Lets start by creating an webmail profile:
+
+>>> browser.getControl(name=':action').displayValue = ['Webmail
Profile']
+>>> browser.getControl('Add').click()
+>>> browser.getControl('Id').value = 'profile'
+>>> browser.getControl('Add').click()
+
+Within the profile, there is an (empty) drafts folder:
+
+>>> getRootFolder()['profile']['drafts']
+<Folder at /profile/drafts>
+>>> getRootFolder()['profile']['drafts'].objectValues()
+[]
+
+Profiles are used to manage multiple accounts together. Let's create an IMAP
account:
 
 >>> browser.getControl(name=':action').displayValue = ['IMAP
Account']
 >>> browser.getControl('Add').click()
(at)(at) -32,14 +46,14 (at)(at)
         <tr class="row-normal">
           <td>
             <div class="list-item">
-              <a href="http://localhost/account/+Bar/manage_workspace">Bar</a>
+              <a href="http://localhost/profile/account/+Bar/manage_workspace">Bar</a>
             </div>
           </td>
         </tr>
         <tr class="row-hilite">
           <td>
             <div class="list-item">
-              <a href="http://localhost/account/+INBOX/manage_workspace">INBOX</a>
+              <a href="http://localhost/profile/account/+INBOX/manage_workspace">INBOX</a>
             </div>
           </td>
         </tr>
(at)(at) -58,7 +72,7 (at)(at)
         <tr class="row-normal">
           <td>
             <div class="list-item">
-              <a href="http://localhost/account/+INBOX/+Baz/manage_workspace">Baz</a>
+              <a href="http://localhost/profile/account/+INBOX/+Baz/manage_workspace">Baz</a>
             </div>
           </td>
         </tr>
(at)(at) -80,7 +94,7 (at)(at)
     <td><div
class="list-item">test(at)localhost</div></td>
     <td>
       <div class="list-item">
-        <a href="http://localhost/account/+INBOX/*.../manage_workspace">Mail
1</a>
+        <a href="http://localhost/profile/account/+INBOX/*.../manage_workspace">Mail
1</a>
       </div>
     </td>
     <td><div class="list-item">02-Jul-2008 03:05:00
+0200</div></td>
(at)(at) -89,7 +103,7 (at)(at)
     <td><div
class="list-item">test(at)localhost</div></td>
     <td>
       <div class="list-item">
-        <a href="http://localhost/account/+INBOX/*.../manage_workspace">Mail
2</a>
+        <a href="http://localhost/profile/account/+INBOX/*.../manage_workspace">Mail
2</a>
       </div>
     </td>
     <td><div class="list-item">02-Jul-2008 03:06:00
+0200</div></td>
(at)(at) -159,6 +173,10 (at)(at)
     <dd class="std-text">Text � or not</dd>
 <BLANKLINE>
 <BLANKLINE>
+    <dt class="std-text"
style="font-weight:bold;">X-Unknown-Encoding-Header</dt>
+    <dd class="std-text">Text ��</dd>
+<BLANKLINE>
+<BLANKLINE>
     <dt class="std-text"
style="font-weight:bold;">X-Wrong-Encoding-Header</dt>
     <dd class="std-text">Text ��</dd>
 <BLANKLINE>
(at)(at) -174,24 +192,10 (at)(at)
 X-IMAPAPI-Test: 1^M
 X-No-Encoding-Header: Text ... or not^M
 X-Wrong-Encoding-Header: =?ascii?q?Text_=C3=BC?=^M
+X-Unknown-Encoding-Header: =?foobarschnappeldiwutz?q?Text_=C3=BC?=^M
 X-Correct-Encoding-Header: =?utf-8?q?Text_=C3=BC?=^M
 Date: 02-Jul-2008 03:05:00 +0200^M
 Subject: Mail 1^M
 ^M
 Everything is ok!</pre>
 ...
-
-Draft folder
-------------
-
-Accounts have a draft folder:
-
->>> browser.getLink('account').click()
->>> browser.getLink('Contents').click()
->>> browser.getLink('drafts').click()
->>> print browser.contents
-<!DOCTYPE...
-    <strong>
-          Folder
-          at <a href="/manage_workspace">&nbsp;/</a><a
href="/account/manage_workspace">account</a>/<a class="strong-link"
href="/account/drafts/manage_workspace">drafts</a>
-</strong>...

SVN: r6713 - in gocept.restmail/trunk/gocept/restmail: . browser www
Christian Theune <ct(at)gocept.com>
2008-09-25 17:00:35 [ FULL ]
Author: ctheune
Date: Thu Sep 25 17:00:32 2008
New Revision: 6713

Log:
Factor out the identity from the profile to prepare managing multiple
identities.



Added:
   gocept.restmail/trunk/gocept/restmail/identity.py   (contents, props
changed)
   gocept.restmail/trunk/gocept/restmail/www/add_identity.pt   (contents, props
changed)
Modified:
   gocept.restmail/trunk/gocept/restmail/__init__.py
   gocept.restmail/trunk/gocept/restmail/browser/profile.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/profile.py
   gocept.restmail/trunk/gocept/restmail/rest.txt

Modified: gocept.restmail/trunk/gocept/restmail/__init__.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/__init__.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/__init__.py	Thu Sep 25 17:00:32 2008
(at)(at) -3,6 +3,7 (at)(at)
     import gocept.restmail.imapaccount
     import gocept.restmail.draft
     import gocept.restmail.profile
+    import gocept.restmail.identity
 
     context.registerClass(
         gocept.restmail.profile.Profile,
(at)(at) -12,6 +13,13 (at)(at)
         icon='www/account.gif')
 
     context.registerClass(
+        gocept.restmail.identity.Identity,
+        meta_type='Webmail Identity',
+        constructors=(gocept.restmail.identity.manage_addIdentityForm,
+                      gocept.restmail.identity.manage_addIdentity),
+        icon='www/account.gif')
+
+    context.registerClass(
         gocept.restmail.imapaccount.IMAPAccount,
         meta_type='IMAP Account',
         constructors=(gocept.restmail.imapaccount.manage_addAccountForm,

Modified: gocept.restmail/trunk/gocept/restmail/browser/profile.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/profile.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/profile.py	Thu Sep 25
17:00:32 2008
(at)(at) -25,6 +25,7 (at)(at)
         return cjson.encode(data)
 
     def default_identity(self):
-        data = {'name': self.context.name,
-                'address': self.context.address}
+        identity = self.context[self.context.default_identity]
+        data = {'name': identity.name,
+                'address': identity.address}
         return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Thu Sep 25 17:00:32 2008
(at)(at) -45,20 +45,21 (at)(at)
 
     def send(self):
         profile = gocept.restmail.interfaces.IProfile(self)
+        identity = profile[profile.default_identity]
 
         # 1. Convert to RFC 822 message
         message = email.MIMEText.MIMEText(self.body, 'html')
-        message['From'] = '%s <%s>' % (profile.name, profile.address)
+        message['From'] = '%s <%s>' % (identity.name, identity.address)
         message['To'] = self.to
         message['Subject'] = self.subject
         message = message.as_string() # XXX Memory usage
 
         # 2. Send via MailHost
-        mh = profile[profile.smtp_server]
+        mh = profile[identity.smtp_server]
         mh.send(message)
 
         # 3. Store in `Sent` Folder
-        sent = profile.getSentFolder()
+        sent = profile.restrictedTraverse(identity.sent_folder)
         sent.append(message)
 
         # 4. Delete draft

Added: gocept.restmail/trunk/gocept/restmail/identity.py
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/identity.py	Thu Sep 25 17:00:32 2008
(at)(at) -0,0 +1,49 (at)(at)
+# Copyright (c) 2007-2008 gocept gmbh & co. kg
+# See also LICENSE.txt
+
+from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+import gocept.restmail.interfaces
+import zope.interface
+
+from OFS.SimpleItem import SimpleItem
+from OFS.PropertyManager import PropertyManager
+
+
+class Identity(PropertyManager, SimpleItem):
+    """An identity associates a public name, an email address and an outgoing
+    mail server.
+
+    """
+
+    zope.interface.implements(gocept.restmail.interfaces.IIdentity)
+
+    meta_type = 'Webmail Identity'
+
+    manage_options = (PropertyManager.manage_options +
+                      SimpleItem.manage_options)
+
+    _properties = (dict(id='name', type='string', mode='w'),
+                   dict(id='address', type='string', mode='w'),
+                   dict(id='sent_folder', type='string', mode='w'),
+                   dict(id='smtp_server', type='string', mode='w'))
+
+    name = u''
+    address = u''
+    sent_folder = ''
+    smtp_server = u''
+
+    def __init__(self, id):
+        self.id = id
+
+
+manage_addIdentityForm = PageTemplateFile('www/add_identity.pt', globals())
+
+
+def manage_addIdentity(context, id):
+    """ZMI helper to create a new identity."""
+    identity = Identity(id)
+    container = context.this()
+    container._setObject(id, identity)
+    identity = container[id]
+    context.REQUEST.RESPONSE.redirect(
+        identity.absolute_url() + '/manage_workspace')

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Thu Sep 25 17:00:32
2008
(at)(at) -10,9 +10,7 (at)(at)
 class IProfile(zope.interface.Interface):
     """A multi-account profile for one person."""
 
-    name = zope.schema.TextLine(title=u'Real name')
-    address = zope.schema.TextLine(title=u'Mail address')
-    smtp_server = zope.schema.TextLine(title=u'Outgoing mail server')
+    default_identity = zope.schema.TextLine(title=u'ID of the default
identity')
 
     def new_draft(message=None):
         """Add a draft message to the account.
(at)(at) -22,8 +20,17 (at)(at)
 
         """
 
-    def getSentFolder():
-        """Return the folder that is used for storing outgoing messages."""
+
+class IIdentity(zope.interface.Interface):
+    """An identity associates a public name, an email address and an outgoing
+    mail server.
+
+    """
+
+    name = zope.schema.TextLine(title=u'Real name')
+    address = zope.schema.TextLine(title=u'Mail address')
+    smtp_server = zope.schema.TextLine(title=u'Outgoing mail server')
+    sent_folder = zope.schema.TextLine(title=u'Path to the sent folder within
the profile')
 
 
 class IIMAPAccount(zope.interface.Interface):

Modified: gocept.restmail/trunk/gocept/restmail/profile.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/profile.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/profile.py	Thu Sep 25 17:00:32 2008
(at)(at) -21,26 +21,23 (at)(at)
                       PropertyManager.manage_options +
                       Item.manage_options)
 
-    _properties = (dict(id='name', type='string', mode='w'),
-                   dict(id='address', type='string', mode='w'),
-                   dict(id='smtp_server', type='string', mode='w'))
-
-    name = u''
-    address = u''
-    smtp_server = u''
+    _properties = (dict(id='default_identity', type='selection',
+                        mode='w', select_variable='manage_identities'),)
+
+    default_identity = u'default-identity'
 
     def __init__(self, id):
         self.id = id
 
     def manage_afterAdd(self, item, container):
         self.manage_addProduct['OFSP'].manage_addFolder('drafts')
+       
self.manage_addProduct['gocept.restmail'].manage_addIdentity('default-identity')
 
     def new_draft(self, message=None):
         return gocept.restmail.draft.new_draft(self['drafts'], message)
 
-    def getSentFolder(self):
-        # XXX Allow this to be differentiated
-        return self.objectValues('IMAP Account')[0].folders('INBOX')[0]
+    def manage_identities(self):
+        return self.objectIds('Webmail Identity')
 
 
 manage_addProfileForm = PageTemplateFile('www/add_profile.pt', globals())

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Thu Sep 25 17:00:32 2008
(at)(at) -30,10 +30,11 (at)(at)
 >>> browser.getControl('Add').click()
 >>> browser.getControl('Id').value = 'profile'
 >>> browser.getControl('Add').click()
->>> getRootFolder()['profile'].name = 'Ben Utzer'
->>> getRootFolder()['profile'].address = 'ben(at)example.com'
->>> getRootFolder()['profile'].smtp_server = 'default'
->>>
getRootFolder()['profile'].manage_addProduct['MailHost'].manage_addMailHost('default')
+>>> getRootFolder()['profile']['default-identity'].name = 'Ben Utzer'
+>>> getRootFolder()['profile']['default-identity'].address =
'ben(at)example.com'
+>>> getRootFolder()['profile']['default-identity'].smtp_server =
'default-smtp'
+>>> getRootFolder()['profile']['default-identity'].sent_folder =
'account/+INBOX'
+>>>
getRootFolder()['profile'].manage_addProduct['MailHost'].manage_addMailHost('default-smtp')
 >>> browser.getControl(name=':action').displayValue = ['IMAP
Account']
 >>> browser.getControl('Add').click()
 >>> browser.getControl('Id').value = 'account'
(at)(at) -343,5 +344,8 (at)(at)
 >>> browser.open(draft['url']+'/(at)(at)data')
 Traceback (most recent call last):
 NotFound: ...
->>> getRootFolder()['profile'].getSentFolder().messages()[-1].raw
+>>> profile = getRootFolder()['profile']
+>>> sent_folder = profile.restrictedTraverse(
+...     profile['default-identity'].sent_folder)
+>>> sent_folder.messages()[-1].raw
 'Content-Type: text/html; charset="us-ascii"\r\nMIME-Version:
1.0\r\nContent-Transfer-Encoding: 7bit\r\nFrom: Ben Utzer
<ben(at)example.com>\r\nTo: ct(at)gocept.com\r\nSubject:
asdf\r\n\r\nHelloHello'

Added: gocept.restmail/trunk/gocept/restmail/www/add_identity.pt
==============================================================================
--- (empty file)
+++ gocept.restmail/trunk/gocept/restmail/www/add_identity.pt	Thu Sep 25
17:00:32 2008
(at)(at) -0,0 +1,31 (at)(at)
+<h1 tal:replace="structure here/manage_page_header">Header</h1>
+
+<h2>Add webmail identity</h2>
+
+<form action="manage_addIdentity" method="post"
+      enctype="multipart/form-data">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+      <label for="id">Id</label>
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" id="id" name="id" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit"
+     value=" Add " />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<h1 tal:replace="structure here/manage_page_footer">Footer</h1>

SVN: r6722 - in gocept.restmail/trunk/gocept/restmail: . browser
Sebastian Wehrmann <sw(at)gocept.com>
2008-09-26 13:08:47 [ FULL ]
Author: sweh
Date: Fri Sep 26 13:08:45 2008
New Revision: 6722

Log:
listing of existing drafts
selection of identities for drafts



Modified:
   gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
   gocept.restmail/trunk/gocept/restmail/browser/draft.py
   gocept.restmail/trunk/gocept/restmail/browser/profile.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/identity.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt

Modified: gocept.restmail/trunk/gocept/restmail/browser/configure.zcml
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/configure.zcml	Fri Sep 26
13:08:45 2008
(at)(at) -257,8 +257,13 (at)(at)
       />
 
     <browser:page
-      name="default_identity"
-      attribute="default_identity"
+      name="drafts"
+      attribute="drafts"
+      />
+
+    <browser:page
+      name="identities"
+      attribute="identities"
       />
 
   </browser:pages>

Modified: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Fri Sep 26 13:08:45
2008
(at)(at) -13,18 +13,20 (at)(at)
 
     def data(self):
         """Return data of the draft message."""
-        data = {'to': self.context.to,
+        data = {'identity': self.context.identity,
+                'to': self.context.to,
                 'subject': self.context.subject,
                 'body': self.context.body.replace('%', '%p').replace('<',
'%l')}
         return cjson.encode(data)
 
-    def save(self, to, subject, body):
+    def save(self, identity, to, subject, body):
         """Update the draft."""
+        self.context.identity = identity
         self.context.to = to
         self.context.subject = subject
         self.context.body = body
 
-    def send(self, to, subject, body):
+    def send(self, identity, to, subject, body):
         """Update draft and send this message."""
-        self.save(to, subject, body)
+        self.save(identity, to, subject, body)
         self.context.send()

Modified: gocept.restmail/trunk/gocept/restmail/browser/profile.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/profile.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/profile.py	Fri Sep 26
13:08:45 2008
(at)(at) -4,6 +4,7 (at)(at)
 """Profile methods."""
 
 import cjson
+import xml.sax.saxutils
 import zope.app.zapi
 
 
(at)(at) -24,8 +25,18 (at)(at)
         data = {'url': zope.app.zapi.absoluteURL(draft, self.request)}
         return cjson.encode(data)
 
-    def default_identity(self):
-        identity = self.context[self.context.default_identity]
-        data = {'name': identity.name,
-                'address': identity.address}
+    def drafts(self):
+        """List all drafts."""
+        data = [{'url': zope.app.zapi.absoluteURL(draft, self.request),
+                 'to': draft.to,
+                 'subject': draft.subject}
+                for draft in self.context.drafts.objectValues()]
+        return cjson.encode(data)
+
+    def identities(self):
+        profile = self.context
+        data = [{'id': identity.getId(),
+                 'title': xml.sax.saxutils.escape(identity.From()),
+                 'default': identity.getId() == profile.default_identity}
+                 for identity in profile.objectValues('Webmail Identity')]
         return cjson.encode(data)

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Fri Sep 26 13:08:45 2008
(at)(at) -23,10 +23,12 (at)(at)
     manage_options = (PropertyManager.manage_options +
                       SimpleItem.manage_options)
 
-    _properties = (dict(id='to', type='string', mode='w'),
+    _properties = (dict(id='identity', type='string', mode='w'),
+                   dict(id='to', type='string', mode='w'),
                    dict(id='subject', type='string', mode='w'),
                    dict(id='body', type='text', mode='w'))
 
+    identity = u''
     to = u''
     subject = u''
     body = u''
(at)(at) -45,11 +47,11 (at)(at)
 
     def send(self):
         profile = gocept.restmail.interfaces.IProfile(self)
-        identity = profile[profile.default_identity]
+        identity = profile[self.identity]
 
         # 1. Convert to RFC 822 message
         message = email.MIMEText.MIMEText(self.body, 'html')
-        message['From'] = '%s <%s>' % (identity.name, identity.address)
+        message['From'] = identity.From()
         message['To'] = self.to
         message['Subject'] = self.subject
         message = message.as_string() # XXX Memory usage

Modified: gocept.restmail/trunk/gocept/restmail/identity.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/identity.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/identity.py	Fri Sep 26 13:08:45 2008
(at)(at) -35,6 +35,9 (at)(at)
     def __init__(self, id):
         self.id = id
 
+    def From(self):
+        """An RfC 822 conformant `From` representation of this identity."""
+        return '%s <%s>' % (self.name, self.address)
 
 manage_addIdentityForm = PageTemplateFile('www/add_identity.pt', globals())
 
(at)(at) -45,5 +48,6 (at)(at)
     container = context.this()
     container._setObject(id, identity)
     identity = container[id]
-    context.REQUEST.RESPONSE.redirect(
-        identity.absolute_url() + '/manage_workspace')
+    if hasattr(context, 'REQUEST'):
+        context.REQUEST.RESPONSE.redirect(
+            identity.absolute_url() + '/manage_workspace')

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Fri Sep 26 13:08:45
2008
(at)(at) -52,13 +52,14 (at)(at)
 class IDraftMessage(zope.interface.Interface):
     """A draft of a message."""
 
+    identity = zope.schema.TextLine(title=u'Identity')
     to = zope.schema.TextLine(title=u'To')
     subject = zope.schema.TextLine(title=u'Subject')
     body = zope.schema.Text(title=u'Message')
 
     def send():
         """Send the message.
-        
+
         Will delete the draft, but store a copy of the generated RfC 822
         message in the account's `Sent` folder.
 

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Fri Sep 26 13:08:45 2008
(at)(at) -91,16 +91,38 (at)(at)
 A profile is a container for accounts. It also manages some general
 user preferences, like the default identity.
 
-Default identity
+Identity listing
 ----------------
 
-The default identity should be used when composing a new draft without
-a reference to an existing account or message. It identifies the public
-name, return address and the SMTP server that will be used:
+Each profile can list the identities that are registered with it. The listing
+contains a user-readable title, the identity's `id` and a flag marking the
+default identity:
 
->>> browser.open('http://localhost/profile/(at)(at)default_identity')
+>>> browser.open('http://localhost/profile/(at)(at)identities')
 >>> print browser.contents
-{"name": "Ben Utzer", "address": "ben(at)example.com"}
+[{"default": true, "id": "default-identity", "title": "Ben Utzer
&lt;ben(at)example.com&gt;"}]
+
+>>> import cjson
+>>> cjson.decode(browser.contents)
+[{'default': True, 'id': 'default-identity', 'title': 'Ben Utzer
&lt;ben(at)example.com&gt;'}]
+
+With multiple identities only the default identity is marked:
+
+>>> getRootFolder()['profile'].manage_addProduct[
+...     'gocept.restmail'].manage_addIdentity('basti')
+>>> getRootFolder()['profile']['basti'].name = 'Basti'
+>>> getRootFolder()['profile']['basti'].address = 'sw(at)gocept.com'
+>>> browser.open('http://localhost/profile/(at)(at)identities')
+>>> print browser.contents
+[{"default": true, "id": "default-identity", "title": "Ben Utzer
&lt;ben(at)example.com&gt;"},
+ {"default": false, "id": "basti", "title": "Basti
&lt;sw(at)gocept.com&gt;"}]
+
+>>> getRootFolder()['profile'].default_identity = 'basti'
+>>> browser.open('http://localhost/profile/(at)(at)identities')
+>>> print browser.contents
+[{"default": false, "id": "default-identity", "title": "Ben Utzer
&lt;ben(at)example.com&gt;"},
+ {"default": true, "id": "basti", "title": "Basti
&lt;sw(at)gocept.com&gt;"}]
+
 
 Account listing
 ---------------
(at)(at) -128,7 +150,6 (at)(at)
 
 This is valid JSON and we can parse it:
 
->>> import cjson
 >>> folders = cjson.decode(browser.contents)
 >>> folders
 [{'url': 'http://localhost/profile/account/+Bar',
'name': 'Bar', 'children': 0},
(at)(at) -303,9 +324,18 (at)(at)
 
 >>> browser.open(draft['url']+'/(at)(at)data')
 >>> print browser.contents
-{"body": "", "to": "", "subject": ""}
+{"body": "", "to": "", "identity": "", "subject": ""}
 >>> cjson.decode(browser.contents)
-{'body': '', 'to': '', 'subject': ''}
+{'body': '', 'to': '', 'identity': '', 'subject': ''}
+
+
+Getting a list of existing draft messages
+-----------------------------------------
+
+>>> browser.open('http://localhost/profile/(at)(at)drafts')
+>>> print browser.contents
+[{"url": "http://localhost/profile/drafts/...-...-...-...-...",
"to": "", "subject": ""}]
+
 
 Updating draft messages
 -----------------------
(at)(at) -316,10 +346,10 (at)(at)
 # mutating view and shouldn't be called via GET
 
 >>> browser.open(draft['url']+'/(at)(at)save',
-...     'to=ct(at)gocept.com&subject=asdf&body=HelloHello')
+...    
'to=ct(at)gocept.com&identity=basti&subject=asdf&body=HelloHello')
 >>> browser.open(draft['url']+'/(at)(at)data')
 >>> print browser.contents
-{"body": "HelloHello", "to": "ct(at)gocept.com", "subject": "asdf"}
+{"body": "HelloHello", "to": "ct(at)gocept.com", "identity": "basti",
"subject": "asdf"}
 
 Sending draft messages
 ----------------------
(at)(at) -328,7 +358,7 (at)(at)
 again.
 
 >>> browser.open(draft['url']+'/(at)(at)send',
-...     'to=ct(at)gocept.com&subject=asdf&body=HelloHello')
+...    
'to=ct(at)gocept.com&identity=default-identity&subject=asdf&body=HelloHello')
 Content-Type: text/html; charset="us-ascii"
 MIME-Version: 1.0
 Content-Transfer-Encoding: 7bit

SVN: r6726 - gocept.restmail/trunk/gocept/restmail
Thomas Lotze <tl(at)gocept.com>
2008-09-29 13:35:52 [ FULL ]
Author: thomas
Date: Mon Sep 29 13:35:50 2008
New Revision: 6726

Log:
factored out HTML quoting, added test, renamed quoting.txt -> draft.txt


Added:
   gocept.restmail/trunk/gocept/restmail/draft.txt
      - copied, changed from r6725,
gocept.restmail/trunk/gocept/restmail/quoting.txt
Removed:
   gocept.restmail/trunk/gocept/restmail/quoting.txt
Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/tests.py

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Mon Sep 29 13:35:50 2008
(at)(at) -2,7 +2,6 (at)(at)
 # See also LICENSE.txt
 
 import email.MIMEText
-import StringIO
 
 import gocept.restmail.interfaces
 import lxml.etree
(at)(at) -89,14 +88,14 (at)(at)
 
 
 HTML_TEMPLATE = """\
-<HTML>
-<HEAD></HEAD>
-<BODY>
-<BLOCKQUOTE>
+<html>
+<head></head>
+<body>
+<blockquote>
 %s
-</BLOCKQUOTE>
-</BODY>
-</HTML>
+</blockquote>
+</body>
+</html>
 """
 
 def get_html_text(message):
(at)(at) -109,13 +108,9 (at)(at)
         text = u''
 
     if content_type == 'text/html':
-        tree = lxml.etree.parse(StringIO.StringIO(text),
-                                parser=lxml.etree.HTMLParser())
-        body = tree.xpath('body')[0]
-        body.tag = 'DIV'
-        text = lxml.etree.tostring(body)
+        text = quote_html(text)
     else:
-        text = '<PRE>\n%s\n</PRE>' % text
+        text = '<pre>\n%s\n</pre>' % text
 
     return HTML_TEMPLATE % text
 
(at)(at) -135,3 +130,10 (at)(at)
         return text.decode(encoding)
     except UnicodeDecodeError:
         return text.decode('iso-8859-15')
+
+
+def quote_html(original):
+    tree = lxml.etree.fromstring(original, parser=lxml.etree.HTMLParser())
+    body = tree.xpath('body')[0] # XXX breaks if no body is present
+    body.tag = 'div'
+    return HTML_TEMPLATE % lxml.etree.tostring(body)

Copied: gocept.restmail/trunk/gocept/restmail/draft.txt (from r6725,
gocept.restmail/trunk/gocept/restmail/quoting.txt)
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/quoting.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.txt	Mon Sep 29 13:35:50 2008
(at)(at) -8,7 +8,7 (at)(at)
 When there is only a text part it will be
 choosen[#loadmessages]_[#getmessages]_[#printunicodebody]_:
 
->>> from gocept.restmail.draft import get_text_part, get_unicode_body
+>>> from gocept.restmail.draft import get_text_part
 >>> part = get_text_part(messages[0])
 >>> part
 <gocept.imapapi.message.BodyPart object at 0x...>
(at)(at) -28,6 +28,7 (at)(at)
 
 We have a number of messages in a mail folder named testquoting:
 
+>>> from gocept.restmail.draft import get_unicode_body
 >>> p(get_unicode_body(get_text_part(messages[0])))
 I'm a message with no funny characters.
 
(at)(at) -57,6 +58,34 (at)(at)
 - a little cyrillic:
\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\u017e\xd0\xb9
 
 
+Quoting an HTML message
+=======================
+
+HTML quoting uses the blockquote element:
+
+>>> from gocept.restmail.draft import quote_html
+>>> print quote_html(u"""\
+... <html><head></head><body>
+... <p>This is a simple HTML message.</p>
+... </body></html>
+... """)
+<html>
+<head></head>
+<body>
+<blockquote>
+<div>
+<p>This is a simple HTML message.</p>
+</div>
+</blockquote>
+</body>
+</html>
+
+
+Quoting a plain text message
+============================
+
+
+
 .. [#loadmessages]
     >>> import gocept.restmail.tests
     >>> gocept.restmail.tests.load_messages('testmessages',
'testquoting')

Modified: gocept.restmail/trunk/gocept/restmail/tests.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/tests.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/tests.py	Mon Sep 29 13:35:50 2008
(at)(at) -135,7 +135,7 (at)(at)
     suite = unittest.TestSuite()
     suite.addTest(FunctionalDocFileSuite(
         'connection.txt',
-        'quoting.txt',
+        'draft.txt',
         'rest.txt',
         'zmi.txt',
         setUp=setUp,

SVN: r6727 - in gocept.restmail/trunk/gocept/restmail: . browser
Sebastian Wehrmann <sw(at)gocept.com>
2008-09-29 13:38:43 [ FULL ]
Author: sweh
Date: Mon Sep 29 13:38:42 2008
New Revision: 6727

Log:
added date to message drafts
added renormalizing checkers for date and message id



Modified:
   gocept.restmail/trunk/gocept/restmail/browser/draft.py
   gocept.restmail/trunk/gocept/restmail/browser/profile.py
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/interfaces.py
   gocept.restmail/trunk/gocept/restmail/rest.txt
   gocept.restmail/trunk/gocept/restmail/tests.py

Modified: gocept.restmail/trunk/gocept/restmail/browser/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/draft.py	Mon Sep 29 13:38:42
2008
(at)(at) -14,6 +14,7 (at)(at)
     def data(self):
         """Return data of the draft message."""
         data = {'identity': self.context.identity,
+                'date': self.context.date.isoformat(),
                 'to': self.context.to,
                 'subject': self.context.subject,
                 'body': self.context.body.replace('%', '%p').replace('<',
'%l')}

Modified: gocept.restmail/trunk/gocept/restmail/browser/profile.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/browser/profile.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/browser/profile.py	Mon Sep 29
13:38:42 2008
(at)(at) -29,7 +29,8 (at)(at)
         """List all drafts."""
         data = [{'url': zope.app.zapi.absoluteURL(draft, self.request),
                  'to': draft.to,
-                 'subject': draft.subject}
+                 'subject': draft.subject,
+                 'date': draft.date.isoformat()}
                 for draft in self.context.drafts.objectValues()]
         return cjson.encode(data)
 

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Mon Sep 29 13:38:42 2008
(at)(at) -1,6 +1,7 (at)(at)
 # Copyright (c) 2007-2008 gocept gmbh & co. kg
 # See also LICENSE.txt
 
+import datetime
 import email.MIMEText
 
 import gocept.restmail.interfaces
(at)(at) -28,12 +29,14 (at)(at)
                    dict(id='body', type='text', mode='w'))
 
     identity = u''
+    date = None
     to = u''
     subject = u''
     body = u''
 
     def __init__(self, id):
         self.id = id
+        self.date = datetime.datetime.now()
 
     def _reply_to(self, message):
         self.to = message.headers.get('From')

Modified: gocept.restmail/trunk/gocept/restmail/interfaces.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/interfaces.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/interfaces.py	Mon Sep 29 13:38:42
2008
(at)(at) -53,6 +53,7 (at)(at)
     """A draft of a message."""
 
     identity = zope.schema.TextLine(title=u'Identity')
+    date = zope.schema.Datetime(title=u'Creation date')
     to = zope.schema.TextLine(title=u'To')
     subject = zope.schema.TextLine(title=u'Subject')
     body = zope.schema.Text(title=u'Message')

Modified: gocept.restmail/trunk/gocept/restmail/rest.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/rest.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/rest.txt	Mon Sep 29 13:38:42 2008
(at)(at) -310,11 +310,11 (at)(at)
 
 >>> browser.open('http://localhost/profile/(at)(at)new_draft')
 >>> print browser.contents
-{"url": "http://localhost/profile/drafts/...-...-...-...-..."}
+{"url": "http://localhost/profile/drafts/<GUID>"}
 
 >>> draft = cjson.decode(browser.contents)
 >>> draft
-{'url': 'http://localhost/profile/drafts/...-...-...-...-...'}
+{'url': 'http://localhost/profile/drafts/<GUID>'}
 
 
 Retrieving data of draft messages
(at)(at) -324,9 +324,9 (at)(at)
 
 >>> browser.open(draft['url']+'/(at)(at)data')
 >>> print browser.contents
-{"body": "", "to": "", "identity": "", "subject": ""}
+{"date": "<ISO DATE>", "to": "", "body": "", "identity": "", "subject":
""}
 >>> cjson.decode(browser.contents)
-{'body': '', 'to': '', 'identity': '', 'subject': ''}
+{'date': '<ISO DATE>', 'to': '', 'body': '', 'identity': '', 'subject':
''}
 
 
 Getting a list of existing draft messages
(at)(at) -334,7 +334,7 (at)(at)
 
 >>> browser.open('http://localhost/profile/(at)(at)drafts')
 >>> print browser.contents
-[{"url": "http://localhost/profile/drafts/...-...-...-...-...",
"to": "", "subject": ""}]
+[{"url": "http://localhost/profile/drafts/<GUID>",
"to": "", "date": "<ISO DATE>", "subject": ""}]
 
 
 Updating draft messages
(at)(at) -349,7 +349,7 (at)(at)
 ...    
'to=ct(at)gocept.com&identity=basti&subject=asdf&body=HelloHello')
 >>> browser.open(draft['url']+'/(at)(at)data')
 >>> print browser.contents
-{"body": "HelloHello", "to": "ct(at)gocept.com", "identity": "basti",
"subject": "asdf"}
+{"date": "<ISO DATE>", "to": "ct(at)gocept.com", "body": "HelloHello",
"identity": "basti", "subject": "asdf"}
 
 Sending draft messages
 ----------------------

Modified: gocept.restmail/trunk/gocept/restmail/tests.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/tests.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/tests.py	Mon Sep 29 13:38:42 2008
(at)(at) -6,10 +6,12 (at)(at)
 import imaplib
 import os
 import os.path
+import re
 import time
 import unittest
 
 from zope.testing import doctest
+import zope.testing.renormalizing
 
 import App.Product
 import App.ProductContext
(at)(at) -124,6 +126,12 (at)(at)
     assert status == 'BYE'
 
 
+checker = zope.testing.renormalizing.RENormalizing([
+   
(re.compile('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+'),
+     "<ISO DATE>"),
+   
(re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'),
+     "<GUID>")])
+
 
 def test_suite():
     # Monkey patch the mail host for printing.
(at)(at) -139,5 +147,6 (at)(at)
         'rest.txt',
         'zmi.txt',
         setUp=setUp,
-        optionflags=doctest.INTERPRET_FOOTNOTES))
+        optionflags=doctest.INTERPRET_FOOTNOTES,
+        checker=checker))
     return suite

SVN: r6729 - gocept.restmail/trunk/gocept/restmail
Thomas Lotze <tl(at)gocept.com>
2008-09-29 13:43:56 [ FULL ]
Author: thomas
Date: Mon Sep 29 13:43:55 2008
New Revision: 6729

Log:
tested some egde cases of HTML quoting


Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/draft.txt

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Mon Sep 29 13:43:55 2008
(at)(at) -137,6 +137,6 (at)(at)
 
 def quote_html(original):
     tree = lxml.etree.fromstring(original, parser=lxml.etree.HTMLParser())
-    body = tree.xpath('body')[0] # XXX breaks if no body is present
+    body = tree.xpath('body')[0]
     body.tag = 'div'
     return HTML_TEMPLATE % lxml.etree.tostring(body)

Modified: gocept.restmail/trunk/gocept/restmail/draft.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.txt	Mon Sep 29 13:43:55 2008
(at)(at) -80,6 +80,45 (at)(at)
 </body>
 </html>
 
+In quoting, we drop anything that was contained in the HTML head. We think
+this is the right thing to do since normal mail clients should - in our
+opinion - write nothing semantically meaningful into the head, anyway.
+
+>>> from gocept.restmail.draft import quote_html
+>>> print quote_html(u"""\
+... <html><head>
+... <style>.spam { text-style: blink; }</style>
+... </head><body>
+... <p>This is a simple HTML message.</p>
+... </body></html>
+... """)
+<html>
+<head></head>
+<body>
+<blockquote>
+<div>
+<p>This is a simple HTML message.</p>
+</div>
+</blockquote>
+</body>
+</html>
+
+Quoting does not break on a malformed message that doesn't have a body or html
+element in the first place:
+
+>>> from gocept.restmail.draft import quote_html
+>>> print quote_html(u"""\
+... <p>This is a simple HTML message.</p>
+... """)
+<html>
+<head></head>
+<body>
+<blockquote>
+<div><p>This is a simple HTML message.</p></div>
+</blockquote>
+</body>
+</html>
+
 
 Quoting a plain text message
 ============================

SVN: r6732 - gocept.restmail/trunk/gocept/restmail
Thomas Lotze <tl(at)gocept.com>
2008-09-29 14:59:56 [ FULL ]
Author: thomas
Date: Mon Sep 29 14:59:54 2008
New Revision: 6732

Log:
added plain text quoting


Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/draft.txt

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Mon Sep 29 14:59:54 2008
(at)(at) -94,9 +94,7 (at)(at)
 <html>
 <head></head>
 <body>
-<blockquote>
 %s
-</blockquote>
 </body>
 </html>
 """
(at)(at) -113,7 +111,7 (at)(at)
     if content_type == 'text/html':
         text = quote_html(text)
     else:
-        text = '<pre>\n%s\n</pre>' % text
+        text = quote_text(text)
 
     return HTML_TEMPLATE % text
 
(at)(at) -138,5 +136,45 (at)(at)
 def quote_html(original):
     tree = lxml.etree.fromstring(original, parser=lxml.etree.HTMLParser())
     body = tree.xpath('body')[0]
-    body.tag = 'div'
+    body.tag = 'blockquote'
     return HTML_TEMPLATE % lxml.etree.tostring(body)
+
+
+def quote_text(original):
+    root = blockquote = lxml.etree.Element('root')
+    last_level = -1
+    for line in original.splitlines(True):
+        level, line = unquote(line)
+        if level > last_level:
+            for i in xrange(level - last_level):
+                old = blockquote
+                blockquote = lxml.etree.Element('blockquote')
+                old.append(blockquote)
+        elif level < last_level:
+            for i in xrange(last_level - level):
+                blockquote = blockquote.getparent()
+        if level != last_level:
+            pre = lxml.etree.Element('pre')
+            blockquote.append(pre)
+            pre.text = ''
+        pre.text += line
+        last_level = level
+    return HTML_TEMPLATE % lxml.etree.tostring(root.getchildren()[0])
+
+
+def unquote(line):
+    # XXX Whether a space is removed after the quote characters should depend
+    # on whether a space is there in all lines of the quote level in question.
+    level = 0
+    for index, c in enumerate(line):
+        if c == '>':
+            level += 1
+            last_quote = index
+        elif c != ' ':
+            break
+    if level:
+        if line[last_quote+1] == ' ':
+            line = line[last_quote+2:]
+        else:
+            line = line[last_quote+1:]
+    return level, line

Modified: gocept.restmail/trunk/gocept/restmail/draft.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.txt	Mon Sep 29 14:59:54 2008
(at)(at) -73,9 +73,7 (at)(at)
 <head></head>
 <body>
 <blockquote>
-<div>
 <p>This is a simple HTML message.</p>
-</div>
 </blockquote>
 </body>
 </html>
(at)(at) -84,7 +82,6 (at)(at)
 this is the right thing to do since normal mail clients should - in our
 opinion - write nothing semantically meaningful into the head, anyway.
 
->>> from gocept.restmail.draft import quote_html
 >>> print quote_html(u"""\
 ... <html><head>
 ... <style>.spam { text-style: blink; }</style>
(at)(at) -96,9 +93,7 (at)(at)
 <head></head>
 <body>
 <blockquote>
-<div>
 <p>This is a simple HTML message.</p>
-</div>
 </blockquote>
 </body>
 </html>
(at)(at) -106,16 +101,13 (at)(at)
 Quoting does not break on a malformed message that doesn't have a body or html
 element in the first place:
 
->>> from gocept.restmail.draft import quote_html
 >>> print quote_html(u"""\
 ... <p>This is a simple HTML message.</p>
 ... """)
 <html>
 <head></head>
 <body>
-<blockquote>
-<div><p>This is a simple HTML message.</p></div>
-</blockquote>
+<blockquote><p>This is a simple HTML
message.</p></blockquote>
 </body>
 </html>
 
(at)(at) -123,6 +115,85 (at)(at)
 Quoting a plain text message
 ============================
 
+Plain text quoting creates HTML for the editor to modify, wrapping the
+original text in a pre element. It also uses the blockquote element for
+quoting:
+
+>>> from gocept.restmail.draft import quote_text
+>>> print quote_text(u"""\
+... This is a simple HTML message.
+... """)
+<html>
+<head></head>
+<body>
+<blockquote><pre>This is a simple HTML message.
+</pre></blockquote>
+</body>
+</html>
+
+Plain-text quoting is interpreted:
+
+>>> print quote_text(u"""\
+... Somebody wrote:
+... > This is a simple HTML message.
+...
+... Right.
+... """)
+<html>
+<head></head>
+<body>
+<blockquote><pre>Somebody wrote:
+</pre><blockquote><pre>This is a simple HTML message.
+</pre></blockquote><pre>
+Right.
+</pre></blockquote>
+</body>
+</html>
+
+This also works with several levels of quoting and a quoted first line:
+
+>>> print quote_text(u"""\
+... >Somebody wrote:
+... >> This is a simple HTML message.
+...
+... > Right.
+... > > Oh, and there can be whitespace
+...  >> between the > and >.
+... > >> Or several levels quoted differently.
+... """)
+<html>
+<head></head>
+<body>
+<blockquote><blockquote><pre>Somebody wrote:
+</pre><blockquote><pre>This is a simple HTML message.
+</pre></blockquote></blockquote><pre>
+</pre><blockquote><pre>Right.
+</pre><blockquote><pre>Oh, and there can be whitespace
+between the &gt; and &gt;.
+</pre><blockquote><pre>Or several levels quoted differently.
+</pre></blockquote></blockquote></blockquote></blockquote>
+</body>
+</html>
+
+While whitespace on unquoted lines is exactly preserved, one space immediately
+after the last quote character is removed if present:
+
+>>> print quote_text(u"""\
+...     four leading spaces
+... >no leading space after quote
+... > one leading space after quote
+... >  two leading spaces after quote
+... """)
+<html>
+<head></head>
+<body>
+<blockquote><pre>    four leading spaces
+</pre><blockquote><pre>no leading space after quote
+one leading space after quote
+ two leading spaces after quote
+</pre></blockquote></blockquote>
+</body>
+</html>
 
 
 .. [#loadmessages]

SVN: r6733 - gocept.restmail/trunk/gocept/restmail
Thomas Lotze <tl(at)gocept.com>
2008-09-29 15:22:17 [ FULL ]
Author: thomas
Date: Mon Sep 29 15:22:16 2008
New Revision: 6733

Log:
fixed line-ending handling


Modified:
   gocept.restmail/trunk/gocept/restmail/draft.py
   gocept.restmail/trunk/gocept/restmail/draft.txt

Modified: gocept.restmail/trunk/gocept/restmail/draft.py
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.py	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.py	Mon Sep 29 15:22:16 2008
(at)(at) -143,7 +143,7 (at)(at)
 def quote_text(original):
     root = blockquote = lxml.etree.Element('root')
     last_level = -1
-    for line in original.splitlines(True):
+    for line in original.splitlines():
         level, line = unquote(line)
         if level > last_level:
             for i in xrange(level - last_level):
(at)(at) -157,7 +157,7 (at)(at)
             pre = lxml.etree.Element('pre')
             blockquote.append(pre)
             pre.text = ''
-        pre.text += line
+        pre.text += line + '\n'
         last_level = level
     return HTML_TEMPLATE % lxml.etree.tostring(root.getchildren()[0])
 

Modified: gocept.restmail/trunk/gocept/restmail/draft.txt
==============================================================================
--- gocept.restmail/trunk/gocept/restmail/draft.txt	(original)
+++ gocept.restmail/trunk/gocept/restmail/draft.txt	Mon Sep 29 15:22:16 2008
(at)(at) -121,12 +121,28 (at)(at)
 
 >>> from gocept.restmail.draft import quote_text
 >>> print quote_text(u"""\
-... This is a simple HTML message.
+... This is a plain-text message.
 ... """)
 <html>
 <head></head>
 <body>
-<blockquote><pre>This is a simple HTML message.
+<blockquote><pre>This is a plain-text message.
+</pre></blockquote>
+</body>
+</html>
+
+DOS-style line endings are handled correctly:
+
+>>> from gocept.restmail.draft import quote_text
+>>> print quote_text(u"""\
+... This is a plain-text message.\r\n\
+... It has two lines.
+... """)
+<html>
+<head></head>
+<body>
+<blockquote><pre>This is a plain-text message.
+It has two lines.
 </pre></blockquote>
 </body>
 </html>
(at)(at) -135,7 +151,7 (at)(at)
 
 >>> print quote_text(u"""\
 ... Somebody wrote:
-... > This is a simple HTML message.
+... > This is a plain-text message.
 ...
 ... Right.
 ... """)
(at)(at) -143,7 +159,7 (at)(at)
 <head></head>
 <body>
 <blockquote><pre>Somebody wrote:
-</pre><blockquote><pre>This is a simple HTML message.
+</pre><blockquote><pre>This is a plain-text message.
 </pre></blockquote><pre>
 Right.
 </pre></blockquote>
(at)(at) -154,7 +170,7 (at)(at)
 
 >>> print quote_text(u"""\
 ... >Somebody wrote:
-... >> This is a simple HTML message.
+... >> This is a plain-text message.
 ...
 ... > Right.
 ... > > Oh, and there can be whitespace
(at)(at) -165,7 +181,7 (at)(at)
 <head></head>
 <body>
 <blockquote><blockquote><pre>Somebody wrote:
-</pre><blockquote><pre>This is a simple HTML message.
+</pre><blockquote><pre>This is a plain-text message.
 </pre></blockquote></blockquote><pre>
 </pre><blockquote><pre>Right.
 </pre><blockquote><pre>Oh, and there can be whitespace

MailBoxer