|
/
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"> /</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"> </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"> </td>
+ </tr>
+ <tr>
+ <td width="48" class="left-gutter-spacing"
style="font-size:1px"> </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"> </td>
+ </tr>
+ <tr>
+ <td class="bottom-spacing" colspan="3" height="15"
style="font-size:1px"> </td>
+ </tr>
+ </tbody></table>
+ </td>
+ <td id="right-back" rowspan="3" width="29"
background="cid:BA068E69-D7FA-4479-A3A4-FCFEFC9C9DE3/rbg.jpg"> </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"> /</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
<ben(at)example.com>"}]
+
+>>> import cjson
+>>> cjson.decode(browser.contents)
+[{'default': True, 'id': 'default-identity', 'title': 'Ben Utzer
<ben(at)example.com>'}]
+
+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
<ben(at)example.com>"},
+ {"default": false, "id": "basti", "title": "Basti
<sw(at)gocept.com>"}]
+
+>>> getRootFolder()['profile'].default_identity = 'basti'
+>>> browser.open('http://localhost/profile/(at)(at)identities')
+>>> print browser.contents
+[{"default": false, "id": "default-identity", "title": "Ben Utzer
<ben(at)example.com>"},
+ {"default": true, "id": "basti", "title": "Basti
<sw(at)gocept.com>"}]
+
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 > and >.
+</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
|
|