Commit [a0cd27] Maximize Restore History

[#1723] [#1590] ForgeMail refactoring - move into allura, make mail tasks

Signed-off-by: Rick Copeland <rcopeland@geek.net>

Rick Copeland Rick Copeland 2011-03-17

Dave Brondsema Dave Brondsema 2011-03-23

1 2 3 4 > >> (Page 1 of 4)
added Allura/allura/tasks
removed Allura/allura/ext/admin/nf
removed Allura/allura/ext/admin/nf/admin
removed Allura/allura/ext/admin/nf/admin/js
removed ForgeMail
removed ForgeMail/forgemail
removed ForgeMail/forgemail/README
removed ForgeMail/forgemail/__init__.py
removed ForgeMail/forgemail/app
removed ForgeMail/forgemail/app/__init__.py
removed ForgeMail/forgemail/app/model
removed ForgeMail/forgemail/app/model/__init__.py
removed ForgeMail/forgemail/lib
removed ForgeMail/forgemail/lib/__init__.py
removed ForgeMail/forgemail/reactors
removed ForgeMail/forgemail/reactors/__init__.py
removed ForgeMail/forgemail/sstress.py
removed ForgeMail/forgemail/tests
removed ForgeMail/forgemail/tests/__init__.py
removed ForgeMail/forgemail/tests/test_util.py
removed ForgeMail/forgemail/version.py
removed ForgeMail/run
removed ForgeMail/run/.gitignore
removed ForgeMail/setup.py
removed ForgeMail/test.ini
changed Allura
changed Allura/allura
changed Allura/allura/__init__.py
changed Allura/allura/app.py
changed Allura/allura/command
changed Allura/allura/command/__init__.py
changed Allura/allura/config
changed Allura/allura/config/resources.py
changed Allura/allura/controllers
changed Allura/allura/controllers/repository.py
changed Allura/allura/ext
changed Allura/allura/ext/admin
changed Allura/allura/lib
changed Allura/allura/lib/exceptions.py
changed Allura/allura/lib/repository.py
changed Allura/allura/model
changed Allura/allura/model/auth.py
changed Allura/allura/model/monq_model.py
changed Allura/allura/model/notification.py
changed Allura/allura/tests
changed Allura/setup.py
changed ForgeGit
changed ForgeGit/forgegit
changed ForgeGit/forgegit/git_main.py
changed ForgeHg
changed ForgeHg/forgehg
changed ForgeHg/forgehg/hg_main.py
changed ForgeSVN
changed ForgeSVN/forgesvn
changed ForgeSVN/forgesvn/svn_main.py
changed run_tests
changed scripts
copied Allura/allura/ext/admin/nf/admin/js/admin.js -> Allura/allura/tasks/__init__.py
copied Allura/allura/task.py -> Allura/allura/tasks/repo_tasks.py
copied ForgeMail/LICENSE -> Allura/allura/lib/mail_util.py
copied ForgeMail/forgemail/app/command.py -> Allura/allura/command/smtp_server.py
copied ForgeMail/forgemail/lib/exc.py -> Allura/allura/tasks/event_tasks.py
copied ForgeMail/forgemail/lib/util.py -> Allura/allura/tasks/mail_tasks.py
copied ForgeMail/forgemail/mail_main.py -> scripts/sstress.py
copied ForgeMail/forgemail/open_relay.py -> scripts/open_relay.py
copied ForgeMail/forgemail/reactors/common_react.py -> scripts/setup-scm-server.py
copied ForgeMail/forgemail/tests/test_sendmail.py -> Allura/allura/tests/test_mail_util.py
Allura/allura/tasks
Directory.
Allura/allura/ext/admin/nf
File was removed.
ForgeMail
File was removed.
ForgeMail/forgemail
File was removed.
ForgeMail/forgemail/README
File was removed.
ForgeMail/forgemail/app
File was removed.
ForgeMail/forgemail/app/model
File was removed.
ForgeMail/forgemail/lib
File was removed.
ForgeMail/forgemail/reactors
File was removed.
ForgeMail/forgemail/sstress.py
File was removed.
ForgeMail/forgemail/tests
File was removed.
ForgeMail/forgemail/version.py
File was removed.
ForgeMail/run
File was removed.
ForgeMail/run/.gitignore
File was removed.
ForgeMail/setup.py
File was removed.
ForgeMail/test.ini
File was removed.
Allura
Directory.
Allura/allura
Directory.
Allura/allura/__init__.py Diff Switch to side-by-side view
Loading...
Allura/allura/app.py Diff Switch to side-by-side view
Loading...
Allura/allura/command/__init__.py Diff Switch to side-by-side view
Loading...
Allura/allura/config/resources.py Diff Switch to side-by-side view
Loading...
Allura/allura/controllers/repository.py Diff Switch to side-by-side view
Loading...
Allura/allura/ext
Directory.
Allura/allura/lib
Directory.
Allura/allura/lib/exceptions.py Diff Switch to side-by-side view
Loading...
Allura/allura/lib/repository.py Diff Switch to side-by-side view
Loading...
Allura/allura/model
Directory.
Allura/allura/model/auth.py Diff Switch to side-by-side view
Loading...
Allura/allura/model/monq_model.py Diff Switch to side-by-side view
Loading...
Allura/allura/model/notification.py Diff Switch to side-by-side view
Loading...
Allura/allura/tests
Directory.
Allura/setup.py Diff Switch to side-by-side view
Loading...
ForgeGit
Directory.
ForgeGit/forgegit
Directory.
ForgeGit/forgegit/git_main.py Diff Switch to side-by-side view
Loading...
ForgeHg
Directory.
ForgeHg/forgehg
Directory.
ForgeHg/forgehg/hg_main.py Diff Switch to side-by-side view
Loading...
ForgeSVN
Directory.
ForgeSVN/forgesvn
Directory.
ForgeSVN/forgesvn/svn_main.py Diff Switch to side-by-side view
Loading...
run_tests Diff Switch to side-by-side view
Loading...
scripts
Directory.
Allura/allura/task.py to Allura/allura/tasks/repo_tasks.py
--- a/Allura/allura/task.py
+++ b/Allura/allura/tasks/repo_tasks.py
@@ -3,16 +3,11 @@
 from pylons import c
 
 from allura import model as M
-from allura.lib.utils import task, event_listeners
+from allura.lib.utils import task
 from allura.lib.repository import RepositoryApp
 
 @task
-def event(data):
-    for e in event_listeners(data['event_type']):
-        e()
-
-@task
-def repo_init(data):
+def init(**kwargs):
     c.app.repo.init()
     M.Notification.post_user(
         c.user, c.app.repo, 'created',
@@ -20,22 +15,25 @@
             c.project.shortname, c.app.config.options.mount_point))
 
 @task
-def repo_clone(data):
+def clone(
+    cloned_from_path,
+    cloned_from_name,
+    cloned_from_url):
     c.app.repo.init_as_clone(
-        data['cloned_from_path'],
-        data['cloned_from_name'],
-        data['cloned_from_url'])
+        cloned_from_path,
+        cloned_from_name,
+        cloned_from_url)
     M.Notification.post_user(
         c.user, c.app.repo, 'created',
         text='Repository %s/%s created' % (
             c.project.shortname, c.app.config.options.mount_point))
 
 @task
-def repo_refresh(data):
+def refresh(**kwargs):
     c.app.repo.refresh()
 
 @task
-def repo_uninstall(data):
+def uninstall(**kwargs):
     repo = c.app.repo
     if repo is not None:
         shutil.rmtree(repo.full_fs_path, ignore_errors=True)
ForgeMail/LICENSE to Allura/allura/lib/mail_util.py
--- a/ForgeMail/LICENSE
+++ b/Allura/allura/lib/mail_util.py
@@ -1,202 +1,188 @@
+import re
+import logging
+import smtplib
+import email.feedparser
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email import header
 
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
+import tg
+from paste.deploy.converters import asbool, asint
+from formencode import validators as fev
+from pylons import c
 
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+from allura.lib.helpers import push_config, find_project
+from allura import model as M
+from allura.lib.utils import ConfigProxy
 
-   1. Definitions.
+from . import exc
 
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
+log = logging.getLogger(__name__)
 
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
+RE_MESSAGE_ID = re.compile(r'<(.*)>')
+config = ConfigProxy(
+    common_suffix='forgemail.domain',
+    return_path='forgemail.return_path')
+EMAIL_VALIDATOR=fev.Email(not_empty=True)
 
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
+def Header(text, charset):
+    '''Helper to make sure we don't over-encode headers
 
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
+    (gmail barfs with encoded email addresses.)'''
+    if isinstance(text, header.Header):
+        return text
+    h = header.Header('', charset)
+    for word in text.split(' '):
+        h.append(word)
+    return h
 
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
+def parse_address(addr):
+    userpart, domain = addr.split('@')
+    # remove common domain suffix
+    if not domain.endswith(config.common_suffix):
+        raise exc.AddressException, 'Unknown domain: ' + domain
+    domain = domain[:-len(config.common_suffix)]
+    path = '/'.join(reversed(domain.split('.')))
+    project, mount_point = find_project('/' + path)
+    if project is None:
+        raise exc.AddressException, 'Unknown project: ' + domain
+    if len(mount_point) != 1:
+        raise exc.AddressException, 'Unknown tool: ' + domain
+    with push_config(c, project=project):
+        app = project.app_instance(mount_point[0])
+        if not app:
+            raise exc.AddressException, 'Unknown tool: ' + domain
+    return userpart, project, app
 
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
+def parse_message(data):
+    # Parse the email to its constituent parts
+    parser = email.feedparser.FeedParser()
+    parser.feed(data)
+    msg = parser.close()
+    # Extract relevant data
+    result = {}
+    result['multipart'] = multipart = msg.is_multipart()
+    result['headers'] = dict(msg)
+    result['message_id'] = _parse_message_id(msg.get('Message-ID'))[0]
+    result['in_reply_to'] = _parse_message_id(msg.get('In-Reply-To'))
+    result['references'] = _parse_message_id(msg.get('References'))
+    if multipart:
+        result['parts'] = []
+        for part in msg.walk():
+            dpart = dict(
+                headers=dict(part),
+                message_id=result['message_id'],
+                in_reply_to=result['in_reply_to'],
+                references=result['references'],
+                content_type=part.get_content_type(),
+                filename=part.get_filename(None),
+                payload=part.get_payload(decode=True))
+            charset = part.get_content_charset()
+            if charset:
+                dpart['payload'] = dpart['payload'].decode(charset)
+            result['parts'].append(dpart)
+    else:
+        result['payload'] = msg.get_payload(decode=True)
+        charset = msg.get_content_charset()
+        if charset:
+            result['payload'] = result['payload'].decode(charset)
+    return result
 
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
+def identify_sender(peer, email_address, headers, msg):
+    # Dumb ID -- just look for email address claimed by a particular user
+    addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(email_address))
+    if addr and addr.claimed_by_user_id:
+        return addr.claimed_by_user()
+    addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(headers.get('From')))
+    if addr and addr.claimed_by_user_id:
+        return addr.claimed_by_user()
+    return M.User.anonymous()
 
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
+def encode_email_part(content, content_type):
+    try:
+        return MIMEText(content.encode('iso-8859-1'), content_type, 'iso-8859-1')
+    except:
+        return MIMEText(content.encode('utf-8'), content_type, 'utf-8')
 
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
+def make_multipart_message(*parts):
+    msg = MIMEMultipart('related')
+    msg.preamble = 'This is a multi-part message in MIME format.'
+    alt = MIMEMultipart('alternative')
+    msg.attach(alt)
+    for part in parts:
+        alt.attach(part)
+    return msg
 
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
+def _parse_message_id(msgid):
+    if msgid is None: return []
+    return [ mo.group(1)
+             for mo in RE_MESSAGE_ID.finditer(msgid) ]
 
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
+def _parse_smtp_addr(addr):
+    addr = str(addr)
+    addrs = _parse_message_id(addr)
+    if addrs and addrs[0]: return addrs[0]
+    if '@' in addr: return addr
+    return 'noreply@in.sf.net'
 
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
+def isvalid(addr):
+    '''return True if addr is a (possibly) valid email address, false
+    otherwise'''
+    try:
+        EMAIL_VALIDATOR.to_python(addr, None)
+        return True
+    except fev.Invalid:
+        return False
 
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
+class SMTPClient(object):
 
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
+    def __init__(self):
+        self._client = None
 
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
+    def sendmail(self, addrs, addrfrom, reply_to, subject, message_id, in_reply_to, message):
+        if not addrs: return
+        charset = message.get_charset()
+        if charset is None:
+            charset = 'iso-8859-1'
+        message['To'] = Header(reply_to, charset)
+        message['From'] = Header(addrfrom, charset)
+        message['Reply-To'] = Header(reply_to, charset)
+        message['Subject'] = Header(subject, charset)
+        message['Message-ID'] = Header('<' + message_id + '>', charset)
+        if in_reply_to:
+            if isinstance(in_reply_to, basestring):
+                in_reply_to = [ in_reply_to ]
+            in_reply_to = ','.join(('<' + irt + '>') for irt in in_reply_to)
+            message['In-Reply-To'] = Header(in_reply_to, charset)
+        content = message.as_string()
+        smtp_addrs = map(_parse_smtp_addr, addrs)
+        smtp_addrs = [ a for a in smtp_addrs if isvalid(a) ]
+        if not smtp_addrs:
+            log.warning('No valid addrs in %s, so not sending mail', addrs)
+            return
+        try:
+            self._client.sendmail(
+                config.return_path,
+                smtp_addrs,
+                content)
+        except:
+            self._connect()
+            self._client.sendmail(
+                config.return_path,
+                smtp_addrs,
+                content)
 
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+    def _connect(self):
+        if asbool(tg.config.get('smtp_ssl', False)):
+            smtp_client = smtplib.SMTP_SSL(
+                tg.config.get('smtp_server', 'localhost'),
+                asint(tg.config.get('smtp_port', 25)))
+        else:
+            smtp_client = smtplib.SMTP(
+                tg.config.get('smtp_server', 'localhost'),
+                asint(tg.config.get('smtp_port', 465)))
+        if tg.config.get('smtp_user', None):
+            smtp_client.login(tg.config['smtp_user'], tg.config['smtp_password'])
+        if asbool(tg.config.get('smtp_tls', False)):
+            smtp_client.starttls()
+        self._client = smtp_client
ForgeMail/forgemail/app/command.py to Allura/allura/command/smtp_server.py
--- a/ForgeMail/forgemail/app/command.py
+++ b/Allura/allura/command/smtp_server.py
@@ -1,21 +1,15 @@
 import smtpd
 import asyncore
-import email.feedparser
-from pprint import pformat
 
 import tg
-import pylons
 from paste.script import command
 
-import allura.command
-from allura.lib.helpers import find_project
+import allura.tasks
 from allura.command import base
 
 from paste.deploy.converters import asint
 
-M = None
-
-class SMTPServerCommand(allura.command.Command):
+class SMTPServerCommand(base.Command):
     min_args=1
     max_args=1
     usage = '<ini file>'
@@ -26,13 +20,10 @@
                             ' and/or tool'))
 
     def command(self):
-        global M
         self.basic_setup()
-        from allura import model
-        M = model
-        server = MailServer((tg.config.get('forgemail.host', '0.0.0.0'),
-                             asint(tg.config.get('forgemail.port', 8825))),
-                            None)
+        MailServer((tg.config.get('forgemail.host', '0.0.0.0'),
+                    asint(tg.config.get('forgemail.port', 8825))),
+                   None)
         asyncore.loop()
 
 class MailServer(smtpd.SMTPServer):
@@ -40,8 +31,6 @@
     def process_message(self, peer, mailfrom, rcpttos, data):
         base.log.info('Msg Received from %s for %s', mailfrom, rcpttos)
         base.log.info(' (%d bytes)', len(data))
-        pylons.g.publish('audit', 'forgemail.received_email',
-                         dict(peer=peer, mailfrom=mailfrom,
-                              rcpttos=rcpttos, data=data),
-                         serializer='pickle')
+        allura.tasks.mail_tasks.route_email(
+            peer=peer, mailfrom=mailfrom, rcpttos=rcpttos, data=data)
         base.log.info('Msg passed along')
ForgeMail/forgemail/lib/exc.py to Allura/allura/tasks/event_tasks.py
--- a/ForgeMail/forgemail/lib/exc.py
+++ b/Allura/allura/tasks/event_tasks.py
@@ -1,3 +1,6 @@
-class ForgeMailException(Exception): pass
-class AddressException(ForgeMailException): pass
-    
+from allura.lib.utils import task, event_listeners
+
+@task
+def event(event_type, **kwargs):
+    for e in event_listeners(event_type):
+        e(**kwargs)
ForgeMail/forgemail/lib/util.py to Allura/allura/tasks/mail_tasks.py
--- a/ForgeMail/forgemail/lib/util.py
+++ b/Allura/allura/tasks/mail_tasks.py
@@ -1,190 +1,128 @@
-import re
 import logging
-import smtplib
-import email.feedparser
-from email.MIMEMultipart import MIMEMultipart
-from email.MIMEText import MIMEText
-from email import header
 
-import tg
-from paste.deploy.converters import asbool, asint
-from formencode import validators as fev
-from pylons import c
+from pylons import c, g
+from bson import ObjectId
 
-from allura.lib.helpers import push_config, find_project
 from allura import model as M
-from allura.lib.utils import ConfigProxy
-
-from . import exc
+from allura.lib import helpers as h
+from allura.lib.utils import task
+from allura.lib import mail_util
+from allura.lib import exceptions as exc
 
 log = logging.getLogger(__name__)
 
-RE_MESSAGE_ID = re.compile(r'<(.*)>')
-config = ConfigProxy(
-    common_suffix='forgemail.domain',
-    return_path='forgemail.return_path')
-EMAIL_VALIDATOR=fev.Email(not_empty=True)
+smtp_client = mail_util.SMTPClient()
 
-def Header(text, charset):
-    '''Helper to make sure we don't over-encode headers
+@task
+def handle_message(topic, message):
+    c.app.handle_message(topic, message)
 
-    (gmail barfs with encoded email addresses.)'''
-    if isinstance(text, header.Header):
-        return text
-    h = header.Header('', charset)
-    for word in text.split(' '):
-        h.append(word)
-    return h
+@task
+def route_email(
+    peer, mailfrom, rcpttos, data):
+    '''Route messages according to their destination:
 
-def parse_address(addr):
-    userpart, domain = addr.split('@')
-    # remove common domain suffix
-    if not domain.endswith(config.common_suffix):
-        raise exc.AddressException, 'Unknown domain: ' + domain
-    domain = domain[:-len(config.common_suffix)]
-    path = '/'.join(reversed(domain.split('.')))
+    <topic>@<mount_point>.<subproj2>.<subproj1>.<project>.projects.sourceforge.net
+    goes to the audit with routing ID
+    <tool name>.mail.<topic>
+    '''
+    try:
+        msg = mail_util.parse_message(data)
+    except:
+        log.exception('Parse Error: (%r,%r,%r)', peer, mailfrom, rcpttos)
+        return
+    c.user = mail_util.identify_sender(data['peer'], data['mailfrom'], msg['headers'], msg)
+    log.info('Received email from %s', c.user.username)
+    # For each of the addrs, determine the project/app and route appropriately
+    for addr in data['rcpttos']:
+        try:
+            userpart, project, app = mail_util.parse_address(addr)
+            with h.push_config(c, project=project, app=app):
+                if not app.has_access(c.user, userpart):
+                    log.info('Access denied for %s to mailbox %s', c.user, userpart)
+                else:
+                    if msg['multipart']:
+                        msg_hdrs = msg['headers']
+                        for part in msg['parts']:
+                            if part.get('content_type', '').startswith('multipart/'): continue
+                            msg = dict(
+                                headers=dict(msg_hdrs, **part['headers']),
+                                message_id=part['message_id'],
+                                in_reply_to=part['in_reply_to'],
+                                references=part['references'],
+                                filename=part['filename'],
+                                content_type=part['content_type'],
+                                payload=part['payload'])
+                            handle_message.post(
+                                topic=userpart,
+                                message=msg)
+                    else:
+                        handle_message.post(
+                            topic=userpart,
+                            message=msg)
+        except exc.MailError, e:
+            log.error('Error routing email to %s: %s', addr, e)
+        except:
+            log.exception('Error routing mail to %s', addr)
 
-    project, mount_point = find_project('/' + path)
-    if project is None:
-        raise exc.AddressException, 'Unknown project: ' + domain
-    if len(mount_point) != 1:
-        raise exc.AddressException, 'Unknown tool: ' + domain
-    with push_config(c, project=project):
-        app = project.app_instance(mount_point[0])
-        if not app:
-            raise exc.AddressException, 'Unknown tool: ' + domain
-        topic = '%s.msg.%s' % (app.config.tool_name, userpart)
-    return topic, project, app
+@task
+def sendmail(
+    fromaddr,
+    destinations,
+    text,
+    reply_to,
+    subject,
+    message_id,
+    in_reply_to=None):
 
-def parse_message(data):
-    # Parse the email to its constituent parts
-    parser = email.feedparser.FeedParser()
-    parser.feed(data)
-    msg = parser.close()
-    # Extract relevant data
-    result = {}
-    result['multipart'] = multipart = msg.is_multipart()
-    result['headers'] = dict(msg)
-    result['message_id'] = _parse_message_id(msg.get('Message-ID'))[0]
-    result['in_reply_to'] = _parse_message_id(msg.get('In-Reply-To'))
-    result['references'] = _parse_message_id(msg.get('References'))
-    if multipart:
-        result['parts'] = []
-        for part in msg.walk():
-            dpart = dict(
-                headers=dict(part),
-                message_id=result['message_id'],
-                in_reply_to=result['in_reply_to'],
-                references=result['references'],
-                content_type=part.get_content_type(),
-                filename=part.get_filename(None),
-                payload=part.get_payload(decode=True))
-            charset = part.get_content_charset()
-            if charset:
-                dpart['payload'] = dpart['payload'].decode(charset)
-            result['parts'].append(dpart)
-    else:
-        result['payload'] = msg.get_payload(decode=True)
-        charset = msg.get_content_charset()
-        if charset:
-            result['payload'] = result['payload'].decode(charset)
-    return result
-
-def identify_sender(peer, email_address, headers, msg):
-    # Dumb ID -- just look for email address claimed by a particular user
-    addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(email_address))
-    if addr and addr.claimed_by_user_id:
-        return addr.claimed_by_user()
-    addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(headers.get('From')))
-    if addr and addr.claimed_by_user_id:
-        return addr.claimed_by_user()
-    return M.User.anonymous()
-
-def encode_email_part(content, content_type):
-    try:
-        return MIMEText(content.encode('iso-8859-1'), content_type, 'iso-8859-1')
-    except:
-        return MIMEText(content.encode('utf-8'), content_type, 'utf-8')
-
-def make_multipart_message(*parts):
-    msg = MIMEMultipart('related')
-    msg.preamble = 'This is a multi-part message in MIME format.'
-    alt = MIMEMultipart('alternative')
-    msg.attach(alt)
-    for part in parts:
-        alt.attach(part)
-    return msg
-
-def _parse_message_id(msgid):
-    if msgid is None: return []
-    return [ mo.group(1)
-             for mo in RE_MESSAGE_ID.finditer(msgid) ]
-
-def _parse_smtp_addr(addr):
-    addr = str(addr)
-    addrs = _parse_message_id(addr)
-    if addrs and addrs[0]: return addrs[0]
-    if '@' in addr: return addr
-    return 'noreply@in.sf.net'
-
-def isvalid(addr):
-    '''return True if addr is a (possibly) valid email address, false
-    otherwise'''
-    try:
-        EMAIL_VALIDATOR.to_python(addr, None)
-        return True
-    except fev.Invalid:
-        return False
-
-class SMTPClient(object):
-
-    def __init__(self):
-        self._client = None
-
-    def sendmail(self, addrs, addrfrom, reply_to, subject, message_id, in_reply_to, message):
-        if not addrs: return
-        charset = message.get_charset()
-        if charset is None:
-            charset = 'iso-8859-1'
-        message['To'] = Header(reply_to, charset)
-        message['From'] = Header(addrfrom, charset)
-        message['Reply-To'] = Header(reply_to, charset)
-        message['Subject'] = Header(subject, charset)
-        message['Message-ID'] = Header('<' + message_id + '>', charset)
-        if in_reply_to:
-            if isinstance(in_reply_to, basestring):
-                in_reply_to = [ in_reply_to ]
-            in_reply_to = ','.join(('<' + irt + '>') for irt in in_reply_to)
-            message['In-Reply-To'] = Header(in_reply_to, charset)
-        content = message.as_string()
-        smtp_addrs = map(_parse_smtp_addr, addrs)
-        smtp_addrs = [ a for a in smtp_addrs if isvalid(a) ]
-        if not smtp_addrs:
-            log.warning('No valid addrs in %s, so not sending mail', addrs)
-            return
-        try:
-            self._client.sendmail(
-                config.return_path,
-                smtp_addrs,
-                content)
-        except:
-            self._connect()
-            self._client.sendmail(
-                config.return_path,
-                smtp_addrs,
-                content)
-
-    def _connect(self):
-        if asbool(tg.config.get('smtp_ssl', False)):
-            smtp_client = smtplib.SMTP_SSL(
-                tg.config.get('smtp_server', 'localhost'),
-                asint(tg.config.get('smtp_port', 25)))
+    addrs_plain = []
+    addrs_html = []
+    addrs_multi = []
+    if '@' not in fromaddr:
+        user = M.User.query.get(_id=ObjectId(fromaddr))
+        if not user:
+            log.warning('Cannot find user with ID %s', fromaddr)
+            fromaddr = 'noreply@in.sf.net'
         else:
-            smtp_client = smtplib.SMTP(
-                tg.config.get('smtp_server', 'localhost'),
-                asint(tg.config.get('smtp_port', 465)))
-        if tg.config.get('smtp_user', None):
-            smtp_client.login(tg.config['smtp_user'], tg.config['smtp_password'])
-        if asbool(tg.config.get('smtp_tls', False)):
-            smtp_client.starttls()
-        self._client = smtp_client
+            fromaddr = user.email_address_header()
+    # Divide addresses based on preferred email formats
+    for addr in destinations:
+        if mail_util.isvalid(addr):
+            addrs_plain.append(addr)
+        else:
+            try:
+                user = M.User.query.get(_id=ObjectId(addr))
+                if not user:
+                    log.warning('Cannot find user with ID %s', addr)
+                    continue
+            except:
+                log.exception('Error looking up user with ID %r')
+                continue
+            addr = user.email_address_header()
+            if not addr and user.email_addresses:
+                addr = user.email_addresses[0]
+                log.warning('User %s has not set primary email address, using %s',
+                            user._id, addr)
+            if not addr:
+                log.error("User %s (%s) has not set any email address, can't deliver",
+                          user._id, user.username)
+                continue
+            if user.get_pref('email_format') == 'plain':
+                addrs_plain.append(addr)
+            elif user.get_pref('email_format') == 'html':
+                addrs_html.append(addr)
+            else:
+                addrs_multi.append(addr)
+    plain_msg = mail_util.encode_email_part(text, 'plain')
+    html_text = g.forge_markdown(email=True).convert(text)
+    html_msg = mail_util.encode_email_part(html_text, 'html')
+    multi_msg = mail_util.make_multipart_message(plain_msg, html_msg)
+    smtp_client.sendmail(
+        addrs_multi, fromaddr, reply_to, subject, message_id,
+        in_reply_to, multi_msg)
+    smtp_client.sendmail(
+        addrs_plain, fromaddr, reply_to, subject, message_id,
+        in_reply_to, plain_msg)
+    smtp_client.sendmail(
+        addrs_html, fromaddr, reply_to, subject, message_id,
+        in_reply_to, html_msg)
ForgeMail/forgemail/mail_main.py to scripts/sstress.py
--- a/ForgeMail/forgemail/mail_main.py
+++ b/scripts/sstress.py
@@ -1,34 +1,36 @@
- #-*- python -*-
-import logging
+#!/usr/bin/env python
+'''
+sstress - an SMTP stress testing tool
+'''
 
-# Pyforge-specific imports
-from allura.app import Application
-from allura.lib.helpers import mixin_reactors
+import smtplib
+import threading
+import time
 
-# Local imports
-from . import version
-from .reactors import common_react
+C = 5
+N = 1000
+TOADDR = 'nobody@localhost'
+SERVER = 'localhost'
+PORT = 8825
+SIZE = 10 * (2**10)
+EMAIL_TEXT = 'X' * SIZE
 
-log = logging.getLogger(__name__)
+def main():
+    threads = [ threading.Thread(target=stress) for x in xrange(C) ]
+    begin = time.time()
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+    end = time.time()
+    elapsed = end - begin
+    print '%d requests completed in %f seconds' % (N, elapsed)
+    print '%f requests/second' % (N/elapsed)
 
-class ForgeMailApp(Application):
-    '''Reactor-only app'''
-    __version__ = version.__version__
-    wsgi=None
-    installable=False
-    sitemap = []
-    sidebar_menu = []
-    tool_label='Mail'
-    default_mount_label='Mail'
-    default_mount_point='mail'
-    ordinal=0
+def stress():
+    server = smtplib.SMTP(SERVER, PORT)
+    for x in xrange(N/C):
+        server.sendmail('sstress@localhost', TOADDR, EMAIL_TEXT)
 
-    def install(self, project):
-        raise NotImplemented, 'install'
-
-    def uninstall(self, project):
-        raise NotImplemented, 'install'
-
-mixin_reactors(ForgeMailApp, common_react)
-
-
+if __name__ == '__main__':
+    main()
ForgeMail/forgemail/open_relay.py to scripts/open_relay.py
--- a/ForgeMail/forgemail/open_relay.py
+++ b/scripts/open_relay.py
@@ -5,7 +5,6 @@
 import smtplib
 import asyncore
 from ConfigParser import ConfigParser
-
 
 log = logging.getLogger(__name__)
 
@@ -20,7 +19,7 @@
     password = cp.get('open_relay', 'password')
     smtp_client = MailClient(host,
                              port,
-                             ssl, tls, 
+                             ssl, tls,
                              username, password)
     MailServer(('0.0.0.0', 8826), None,
                smtp_client=smtp_client)
ForgeMail/forgemail/reactors/common_react.py to scripts/setup-scm-server.py
--- a/ForgeMail/forgemail/reactors/common_react.py
+++ b/scripts/setup-scm-server.py
@@ -1,153 +1,106 @@
-import logging
+import os
+import string
+from tempfile import mkstemp
+from ConfigParser import ConfigParser, NoOptionError
 
-from pylons import c, g
-from bson import ObjectId
+config = ConfigParser()
 
-from allura.lib.decorators import audit, react
-from allura.lib.helpers import push_config
-from allura import model as M
+def main():
+    config.read('.setup-scm-cache')
+    if not config.has_section('scm'):
+        config.add_section('scm')
+    domain = get_value('domain', 'dc=example,dc=com')
+    if config.get('start slapd', 'y') == 'y':
+        run('service slapd start')
+    if config.get('add base ldap schemas', 'y') == 'y':
+        run('ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/cosine.ldif')
+        run('ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/nis.ldif')
+        run('ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/inetorgperson.ldif')
+    secret = config.get('admin password', 'secret')
+    if config.get('add backend ldif', 'y') == 'y':
+        add_ldif(backend_ldif, domain=domain, secret=secret)
+    if config.get('add frontend ldif', 'y') == 'y':
+        add_ldif(frontend_ldif, domain=domain, secret=secret)
 
-from forgemail.lib import util, exc
 
-log = logging.getLogger(__name__)
+def get_value(key, default):
+    try:
+        value = config.get('scm', key)
+    except NoOptionError:
+        value = raw_input('%s? [%s]' % key, default)
+        if not value: value = default
+        config.set('scm', key, value)
+    return value
 
-smtp_client = util.SMTPClient()
+def run(command):
+    rc = os.system(command)
+    assert rc == 0
+    return rc
 
-@audit('search.check_commit')
-@react('forgemail.fire')
-def fire_ready_emails(routing_key, data):
-    M.Mailbox.fire_ready()
+def add_ldif(template, **values):
+    fd, name = mkstemp()
+    os.write(fd, template.substitute(values))
+    os.close(fd)
+    run('ldapadd -Y EXTERNAL -H ldapi:/// -f %s' % name)
+    os.remove(name)
 
-@react('forgemail.notify')
-def received_notification(routing_key, data):
-    g.set_app(data['mount_point'])
-    M.Mailbox.deliver(
-        data['notification_id'],
-        data['artifact_index_id'],
-        data['topic'])
-    g.publish('react', 'forgemail.fire')
+backend_ldif=string.Template('''
+# Load dynamic backend modules
+dn: cn=module,cn=config
+objectClass: olcModuleList
+cn: module
+olcModulepath: /usr/lib/ldap
+olcModuleload: back_hdb
 
-@audit('forgemail.received_email')
-def received_email(routing_key, data):
-    '''Route messages according to their destination:
+# Database settings
+dn: olcDatabase=hdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcHdbConfig
+olcDatabase: {1}hdb
+olcSuffix: $domain
+olcDbDirectory: /var/lib/ldap
+olcRootDN: cn=admin,$domain
+olcRootPW: $secret
+olcDbConfig: set_cachesize 0 2097152 0
+olcDbConfig: set_lk_max_objects 1500
+olcDbConfig: set_lk_max_locks 1500
+olcDbConfig: set_lk_max_lockers 1500
+olcDbIndex: objectClass eq
+olcLastMod: TRUE
+olcDbCheckpoint: 512 30
+olcAccess: to attrs=userPassword by dn="cn=admin,$domain" write by anonymous auth by self write by * none
+olcAccess: to attrs=shadowLastChange by self write by * read
+olcAccess: to dn.base="" by * read
+olcAccess: to * by dn="cn=admin,$domain" write by * read
 
-    <topic>@<mount_point>.<subproj2>.<subproj1>.<project>.projects.sourceforge.net
-    goes to the audit with routing ID
-    <tool name>.mail.<topic>
-    '''
-    try:
-        msg = util.parse_message(data['data'])
-    except:
-        log.exception('Error parsing email: %r', data)
-        return
-    user = util.identify_sender(data['peer'], data['mailfrom'], msg['headers'], msg)
-    log.info('Received email from %s', user.username)
-    # For each of the addrs, determine the project/app and route appropriately
-    for addr in data['rcpttos']:
-        try:
-            topic, project, app = util.parse_address(addr)
-            routing_key = topic
-            with push_config(c, project=project, app=app):
-                if not app.has_access(user, topic):
-                    log.info('Access denied for %s to mailbox %s',
-                             user, topic)
-                else:
-                    log.info('Sending message to audit queue %s', topic)
-                    if msg['multipart']:
-                        msg_hdrs = msg['headers']
-                        for part in msg['parts']:
-                            if part.get('content_type', '').startswith('multipart/'): continue
-                            msg = dict(
-                                headers=dict(msg_hdrs, **part['headers']),
-                                message_id=part['message_id'],
-                                in_reply_to=part['in_reply_to'],
-                                references=part['references'],
-                                filename=part['filename'],
-                                content_type=part['content_type'],
-                                payload=part['payload'],
-                                user_id=user._id and str(user._id))
-                            g.publish('audit', routing_key, msg,
-                                      serializer='yaml')
-                    else:
-                        g.publish('audit', routing_key,
-                                  dict(msg, user_id=user._id and str(user._id)),
-                                  serializer='pickle')
-        except exc.ForgeMailException, e:
-            log.error('Error routing email to %s: %s', addr, e)
-        except:
-            log.exception('Error routing mail to %s', addr)
+''')
 
-@audit('forgemail.send_email')
-def send_email(routing_key, data):
-    addrs_plain = []
-    addrs_html = []
-    addrs_multi = []
-    fromaddr = data['from']
-    if '@' not in fromaddr:
-        user = M.User.query.get(_id=ObjectId(fromaddr))
-        if not user:
-            log.warning('Cannot find user with ID %s', fromaddr)
-            fromaddr = 'noreply@in.sf.net'
-        else:
-            fromaddr = user.email_address_header()
-    # Divide addresses based on preferred email formats
-    for addr in data['destinations']:
-        if util.isvalid(addr):
-            addrs_plain.append(addr)
-        else:
-            try:
-                user = M.User.query.get(_id=ObjectId(addr))
-                if not user:
-                    log.warning('Cannot find user with ID %s', addr)
-                    continue
-            except:
-                log.exception('Error looking up user with ID %r')
-                continue
-            addr = user.email_address_header()
-            if not addr and user.email_addresses:
-                addr = user.email_addresses[0]
-                log.warning('User %s has not set primary email address, using %s',
-                            user._id, addr)
-            if not addr:
-                log.error("User %s (%s) has not set any email address, can't deliver",
-                          user._id, user.username)
-                continue
-            if user.get_pref('email_format') == 'plain':
-                addrs_plain.append(addr)
-            elif user.get_pref('email_format') == 'html':
-                addrs_html.append(addr)
-            else:
-                addrs_multi.append(addr)
-    plain_msg = util.encode_email_part(data['text'], 'plain')
-    html_text = g.forge_markdown(email=True).convert(data['text'])
-    html_msg = util.encode_email_part(html_text, 'html')
-    multi_msg = util.make_multipart_message(plain_msg, html_msg)
-    smtp_client.sendmail(
-        addrs_multi,
-        fromaddr,
-        data['reply_to'],
-        data['subject'],
-        data['message_id'],
-        data.get('in_reply_to', None),
-        multi_msg)
-    smtp_client.sendmail(
-        addrs_plain,
-        fromaddr,
-        data['reply_to'],
-        data['subject'],
-        data['message_id'],
-        data.get('in_reply_to', None),
-        plain_msg)
-    smtp_client.sendmail(
-        addrs_html,
-        fromaddr,
-        data['reply_to'],
-        data['subject'],
-        data['message_id'],
-        data.get('in_reply_to', None),
-        html_msg)
+frontend_ldif=string.Template('''
+# Create top-level object in domain
+dn: $domain
+objectClass: top
+objectClass: dcObject
+objectclass: organization
+o: SCM Host Organization
+dc: SCM
+description: SCM Host Server
 
-        
-            
-    
+# Admin user.
+dn: cn=admin,$domain
+objectClass: simpleSecurityObject
+objectClass: organizationalRole
+cn: admin
+description: LDAP administrator
+userPassword: $secret
 
+dn: ou=people,$domain
+objectClass: organizationalUnit
+ou: people
+
+dn: ou=groups,$domain
+objectClass: organizationalUnit
+ou: groups
+''')
+
+if __name__ == '__main__':
+    main()
ForgeMail/forgemail/tests/test_sendmail.py to Allura/allura/tests/test_mail_util.py
--- a/ForgeMail/forgemail/tests/test_sendmail.py
+++ b/Allura/allura/tests/test_mail_util.py
@@ -1,88 +1,24 @@
 # -*- coding: utf-8 -*-
-import os
 import unittest
-from datetime import timedelta
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email import header
+from email.parser import Parser
 
-
-from mock import Mock
-from pylons import g, c
-
-import ming
-from ming.orm import session, ThreadLocalORMSession
+import tg
+from nose.tools import raises, assert_equal
+from ming.orm import ThreadLocalORMSession
 
 from alluratest.controller import setup_basic_test, setup_global_objects
-from allura import model as M
-from allura.lib import helpers as h
-from forgemail.lib.util import SMTPClient, encode_email_part
-from forgemail.reactors import common_react
+from allura.lib.utils import ConfigProxy
 
-class TestSendmail(unittest.TestCase):
+from forgemail.lib.util import parse_address, parse_message
+from forgemail.lib.exc import AddressException
+from forgemail.reactors.common_react import received_email
 
-    def setUp(self):
-        setup_basic_test()
-        setup_global_objects()
-        ThreadLocalORMSession.flush_all()
-        ThreadLocalORMSession.close_all()
-        self.mail = SMTPClient()
-        self.mail._client = Mock(spec=[
-                'sendmail'])
-        self.mail._client.sendmail = Mock()
-
-    def test_addr(self):
-        self.mail.sendmail(
-            ['test@example.com'],
-            'test@example.com',
-            'test@example.com',
-            'Subject',
-            'message_id@example.com',
-            None,
-            encode_email_part('Test message', 'text/plain'))
-        assert self.mail._client.sendmail.called
-
-    def test_bad_addr(self):
-        self.mail.sendmail(
-            ['@example.com'],
-            'test@example.com',
-            'test@example.com',
-            'Subject',
-            'message_id@example.com',
-            None,
-            encode_email_part('Test message', 'text/plain'))
-        assert not self.mail._client.sendmail.called
-
-    def test_user(self):
-        u = M.User.by_username('test-admin')
-        u.set_pref('display_name', u'Rick Copeland')
-        u.set_pref('email_address', 'test@example.com')
-        self.mail.sendmail(
-            ['test@example.com'],
-            u.email_address_header(),
-            u.email_address_header(),
-            'Subject',
-            'message_id@example.com',
-            None,
-            encode_email_part('Test message', 'text/plain'))
-        assert self.mail._client.sendmail.called
-        args, kwargs =  self.mail._client.sendmail.call_args
-        assert args[0] == 'noreply@sourceforge.net'
-        assert '"Rick Copeland" <test@example.com>' in args[2]
-
-    def test_user_unicode(self):
-        u = M.User.by_username('test-admin')
-        u.set_pref('display_name', u'Rick Cop��land')
-        u.set_pref('email_address', 'test@example.com')
-        self.mail.sendmail(
-            ['test@example.com'],
-            u.email_address_header(),
-            u.email_address_header(),
-            'Subject',
-            'message_id@example.com',
-            None,
-            encode_email_part('Test message', 'text/plain'))
-        assert self.mail._client.sendmail.called
-        args, kwargs =  self.mail._client.sendmail.call_args
-        assert args[0] == 'noreply@sourceforge.net'
-        assert '=?utf-8?q?=22Rick_Cop=C3=A9land=22?= <test@example.com>' in args[2]
+config = ConfigProxy(
+    common_suffix='forgemail.domain',
+    return_path='forgemail.return_path')
 
 class TestReactor(unittest.TestCase):
 
@@ -91,81 +27,69 @@
         setup_global_objects()
         ThreadLocalORMSession.flush_all()
         ThreadLocalORMSession.close_all()
-        common_react.smtp_client._client = Mock(spec=[
-                'sendmail'])
-        common_react.smtp_client._client.sendmail = self.sendmail = Mock()
 
-    def test_send_mail(self):
-        addr = 'test@example.com'
-        common_react.send_email(None, {
-                'from':addr,
-                'reply_to':addr,
-                'destinations':[addr],
-                'message_id':'test@example.com',
-                'subject':'Test message',
-                'text':'Test message'})
-        assert self.sendmail.called
-        args, kwargs =  self.sendmail.call_args
-        (from_addr, ids, msg) = args
-        assert 'Content-Type: text/plain; charset="iso-8859-1"\n' in msg, msg
-        assert 'Subject: =?iso-8859-1?q?Test?= =?iso-8859-1?q?message?=\n' in msg, msg
+    @raises(AddressException)
+    def test_parse_address_bad_domain(self):
+        parse_address('foo@bar.com')
 
-    def test_send_mail_unicode(self):
-        addr = 'test@example.com'
-        common_react.send_email(None, {
-                'from':addr,
-                'reply_to':addr,
-                'destinations':[addr],
-                'message_id':'test@example.com',
-                'subject': u'Test ��� message',
-                'text': u'Test ��� message'})
-        assert self.sendmail.called
-        args, kwargs =  self.sendmail.call_args
-        (from_addr, ids, msg) = args
-        assert 'Content-Type: text/plain; charset="utf-8"\n' in msg, msg
-        assert 'Subject: Test =?utf-8?b?4peO?= message\n' in msg, msg
+    @raises(AddressException)
+    def test_parse_address_bad_project(self):
+        parse_address('foo@wiki.unicorns.p' + config.common_suffix)
 
-    def test_user(self):
-        u = M.User.by_username('test-admin')
-        u.set_pref('display_name', u'Rick Copeland')
-        u.set_pref('email_address', 'test@example.com')
-        addr = str(u._id)
-        common_react.send_email(None, {
-                'from':addr,
-                'reply_to':addr,
-                'destinations':[addr],
-                'message_id':'test@example.com',
-                'subject':'Test message',
-                'text':'Test message'})
-        assert self.sendmail.called
-        args, kwargs =  self.sendmail.call_args
-        assert args[0] == 'noreply@sourceforge.net'
-        assert '"Rick Copeland" <test@example.com>' in args[2]
+    @raises(AddressException)
+    def test_parse_address_missing_tool(self):
+        parse_address('foo@test.p' + config.common_suffix)
 
-    def test_bad_user(self):
-        addr = 'test@example.com'
-        common_react.send_email(None, {
-                'from':addr,
-                'reply_to':addr,
-                'destinations':[None],
-                'message_id':'test@example.com',
-                'subject':'Test message',
-                'text':'Test message'})
-        assert not self.sendmail.called
+    @raises(AddressException)
+    def test_parse_address_bad_tool(self):
+        parse_address('foo@hammer.test.p' + config.common_suffix)
 
-    def test_user_unicode(self):
-        u = M.User.by_username('test-admin')
-        u.set_pref('display_name', u'Rick Cop��land')
-        u.set_pref('email_address', 'test@example.com')
-        addr = str(u._id)
-        common_react.send_email(None, {
-                'from':addr,
-                'reply_to':addr,
-                'destinations':[addr],
-                'message_id':'test@example.com',
-                'subject':'Test message',
-                'text':'Test message'})
-        assert self.sendmail.called
-        args, kwargs =  self.sendmail.call_args
-        assert args[0] == 'noreply@sourceforge.net'
-        assert '=?utf-8?q?=22Rick_Cop=C3=A9land=22?= <test@example.com>' in args[2]
+    def test_parse_address_good(self):
+        topic, project, app = parse_address('foo@wiki.test.p' + config.common_suffix)
+        assert_equal(topic, 'Wiki.msg.foo')
+        assert_equal(project.name, 'test')
+        assert_equal(app.__class__.__name__, 'ForgeWikiApp')
+
+    def test_unicode_simple_message(self):
+        charset = 'utf-8'
+        msg1 = MIMEText(u'''���� �������������������� ��������������
+�������������� ���������������� ����������������
+�������������� �� ����������; ��������������
+������������ ���� �������� ������������ ����������
+�� �������������� ������������������ ������������������;'''.encode(charset),
+                        'plain',
+                        charset)
+        msg1['Message-ID'] = '<foo@bar.com>'
+        s_msg = msg1.as_string()
+        msg2 = parse_message(s_msg)
+        assert isinstance(msg2['payload'], unicode)
+
+    def test_unicode_complex_message(self):
+        charset = 'utf-8'
+        p1 = MIMEText(u'''���� �������������������� ��������������
+�������������� ���������������� ����������������
+�������������� �� ����������; ��������������
+������������ ���� �������� ������������ ����������
+�� �������������� ������������������ ������������������;'''.encode(charset),
+                        'plain',
+                        charset)
+        p2 = MIMEText(u'''<p>���� �������������������� ��������������
+�������������� ���������������� ����������������
+�������������� �� ����������; ��������������
+������������ ���� �������� ������������ ����������
+�� �������������� ������������������ ������������������;</p>'''.encode(charset),
+                        'plain',
+                        charset)
+        msg1 = MIMEMultipart()
+        msg1['Message-ID'] = '<foo@bar.com>'
+        msg1.attach(p1)
+        msg1.attach(p2)
+        s_msg = msg1.as_string()
+        msg2 = parse_message(s_msg)
+        for part in msg2['parts']:
+            if part['payload'] is None: continue
+            assert isinstance(part['payload'], unicode)
+
+    def test_malformed_email_no_exception(self):
+        msg = MIMEText('Bad email, no Message-ID')
+        received_email('', msg.as_string())
1 2 3 4 > >> (Page 1 of 4)