changeset 750:f158c4e8fea2

moved mail related functions to MoinMoin.mail
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Wed, 07 Jun 2006 10:39:30 +0200
parents 3dba26fcfde0
children a098ff6781f4
files MoinMoin/PageEditor.py MoinMoin/PageGraphicalEditor.py MoinMoin/_tests/test_mail_sendmail.py MoinMoin/_tests/test_util_mail.py MoinMoin/mail/__init__.py MoinMoin/mail/mailimport.py MoinMoin/mail/sendmail.py MoinMoin/mailimport.py MoinMoin/user.py MoinMoin/util/mail.py MoinMoin/xmlrpc/ProcessMail.py docs/CHANGES
diffstat 12 files changed, 574 insertions(+), 563 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/PageEditor.py	Wed Jun 07 09:44:08 2006 +0200
+++ b/MoinMoin/PageEditor.py	Wed Jun 07 10:39:30 2006 +0200
@@ -15,7 +15,7 @@
 from MoinMoin.logfile import editlog, eventlog
 from MoinMoin.util import filesys, timefuncs
 import MoinMoin.util.web
-import MoinMoin.util.mail
+from MoinMoin.mail import sendmail
 
 
 #############################################################################
@@ -590,7 +590,7 @@
             else:
                 mailBody = mailBody + _("No differences found!\n", formatted=False)
         
-        return util.mail.sendmail(self.request, emails,
+        return sendmail.sendmail(self.request, emails,
             _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s', formatted=False) % {
                 'trivial' : (trivial and _("Trivial ", formatted=False)) or "",
                 'sitename': self.cfg.sitename or "Wiki",
--- a/MoinMoin/PageGraphicalEditor.py	Wed Jun 07 09:44:08 2006 +0200
+++ b/MoinMoin/PageGraphicalEditor.py	Wed Jun 07 10:39:30 2006 +0200
@@ -16,7 +16,6 @@
 from MoinMoin.logfile import editlog, eventlog
 from MoinMoin.util import filesys
 import MoinMoin.util.web
-import MoinMoin.util.mail
 from MoinMoin.parser.text_moin_wiki import Parser
 
 from StringIO import StringIO
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_mail_sendmail.py	Wed Jun 07 10:39:30 2006 +0200
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.mail.sendmail Tests
+
+    @copyright: 2003-2004 by Jürgen Hermann <jh@web.de>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import unittest
+from email.Charset import Charset, QP
+from email.Header import Header
+from MoinMoin.mail import sendmail
+from MoinMoin import config
+
+
+class decodeSpamSafeEmailTestCase(unittest.TestCase):
+    """mail.sendmail: testing mail"""
+    
+    _tests = (
+        ('', ''),
+        ('AT', '@'),
+        ('DOT', '.'),
+        ('DASH', '-'),
+        ('CAPS', ''),
+        ('Mixed', 'Mixed'),
+        ('lower', 'lower'),
+        ('Firstname DOT Lastname AT example DOT net',
+         'Firstname.Lastname@example.net'),
+        ('Firstname . Lastname AT exa mp le DOT n e t',
+         'Firstname.Lastname@example.net'),
+        ('Firstname I DONT WANT SPAM . Lastname@example DOT net',
+         'Firstname.Lastname@example.net'),
+        ('First name I Lastname DONT AT WANT SPAM example DOT n e t',
+         'FirstnameLastname@example.net'),
+        ('first.last@example.com', 'first.last@example.com'),
+        ('first . last @ example . com', 'first.last@example.com'),
+        )
+
+    def testDecodeSpamSafeMail(self):
+        """mail.sendmail: decoding spam safe mail"""
+        for coded, expected in self._tests:
+            result = sendmail.decodeSpamSafeEmail(coded)
+            self.assertEqual(result, expected,
+                             'Expected "%(expected)s" but got "%(result)s"' %
+                             locals())
+
+
+class EncodeAddressTests(unittest.TestCase):
+    """ Address encoding tests
+    
+    See http://www.faqs.org/rfcs/rfc2822.html section 3.4. 
+    Address Specification.
+            
+    mailbox     =   name-addr / addr-spec
+    name-addr   =   [display-name] angle-addr
+    angle-addr  =   [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
+    """    
+    charset = Charset(config.charset)
+    charset.header_encoding = QP
+    charset.body_encoding = QP
+
+    def testSimpleAddress(self):
+        """ mail.sendmail: encode simple address: local@domain """
+        address = u'local@domain'
+        expected = address.encode(config.charset)
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
+
+    def testComposite(self):
+        """ mail.sendmail: encode address: 'Phrase <local@domain>' """
+        address = u'Phrase <local@domain>'
+        phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
+        expected = phrase + '<local@domain>'
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
+                             
+    def testCompositeUnicode(self):
+        """ mail.sendmail: encode Uncode address: 'ויקי <local@domain>' """
+        address = u'ויקי <local@domain>'
+        phrase = str(Header(u'ויקי '.encode('utf-8'), self.charset))
+        expected = phrase + '<local@domain>'
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
+                             
+    def testEmptyPhrase(self):
+        """ mail.sendmail: encode address with empty phrase: '<local@domain>' """
+        address = u'<local@domain>'
+        expected = address.encode(config.charset)
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
+                             
+    def testEmptyAddress(self):
+        """ mail.sendmail: encode address with empty address: 'Phrase <>' 
+        
+        Let the smtp server handle this. We may raise error in such
+        case, but we don't do error checking for mail addresses.
+        """
+        address = u'Phrase <>'
+        phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
+        expected = phrase + '<>'
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
+
+    def testInvalidAddress(self):
+        """ mail.sendmail: encode invalid address 'Phrase <blah' 
+        
+        Assume that this is a simple address. This address will
+        probably cause an error when trying to send mail. Junk in, junk
+        out.
+        """
+        address = u'Phrase <blah'
+        expected = address.encode(config.charset)
+        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
+                             expected)
--- a/MoinMoin/_tests/test_util_mail.py	Wed Jun 07 09:44:08 2006 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    MoinMoin - MoinMoin.util.mail Tests
-
-    @copyright: 2003-2004 by Jürgen Hermann <jh@web.de>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import unittest
-from email.Charset import Charset, QP
-from email.Header import Header
-from MoinMoin.util import mail
-from MoinMoin import config
-
-
-class decodeSpamSafeEmailTestCase(unittest.TestCase):
-    """util.mail: testing mail"""
-    
-    _tests = (
-        ('', ''),
-        ('AT', '@'),
-        ('DOT', '.'),
-        ('DASH', '-'),
-        ('CAPS', ''),
-        ('Mixed', 'Mixed'),
-        ('lower', 'lower'),
-        ('Firstname DOT Lastname AT example DOT net',
-         'Firstname.Lastname@example.net'),
-        ('Firstname . Lastname AT exa mp le DOT n e t',
-         'Firstname.Lastname@example.net'),
-        ('Firstname I DONT WANT SPAM . Lastname@example DOT net',
-         'Firstname.Lastname@example.net'),
-        ('First name I Lastname DONT AT WANT SPAM example DOT n e t',
-         'FirstnameLastname@example.net'),
-        ('first.last@example.com', 'first.last@example.com'),
-        ('first . last @ example . com', 'first.last@example.com'),
-        )
-
-    def testDecodeSpamSafeMail(self):
-        """util.mail: decoding spam safe mail"""
-        for coded, expected in self._tests:
-            result = mail.decodeSpamSafeEmail(coded)
-            self.assertEqual(result, expected,
-                             'Expected "%(expected)s" but got "%(result)s"' %
-                             locals())
-
-
-class EncodeAddressTests(unittest.TestCase):
-    """ Address encoding tests
-    
-    See http://www.faqs.org/rfcs/rfc2822.html section 3.4. 
-    Address Specification.
-            
-    mailbox     =   name-addr / addr-spec
-    name-addr   =   [display-name] angle-addr
-    angle-addr  =   [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
-    """    
-    charset = Charset(config.charset)
-    charset.header_encoding = QP
-    charset.body_encoding = QP
-
-    def testSimpleAddress(self):
-        """ util.mail: encode simple address: local@domain """
-        address = u'local@domain'
-        expected = address.encode(config.charset)
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
-
-    def testComposite(self):
-        """ util.mail: encode address: 'Phrase <local@domain>' """
-        address = u'Phrase <local@domain>'
-        phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
-        expected = phrase + '<local@domain>'
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
-                             
-    def testCompositeUnicode(self):
-        """ util.mail: encode Uncode address: 'ויקי <local@domain>' """
-        address = u'ויקי <local@domain>'
-        phrase = str(Header(u'ויקי '.encode('utf-8'), self.charset))
-        expected = phrase + '<local@domain>'
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
-                             
-    def testEmptyPhrase(self):
-        """ util.mail: encode address with empty phrase: '<local@domain>' """
-        address = u'<local@domain>'
-        expected = address.encode(config.charset)
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
-                             
-    def testEmptyAddress(self):
-        """ util.mail: encode address with empty address: 'Phrase <>' 
-        
-        Let the smtp server handle this. We may raise error in such
-        case, but we don't do error checking for mail addresses.
-        """
-        address = u'Phrase <>'
-        phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
-        expected = phrase + '<>'
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
-
-    def testInvalidAddress(self):
-        """ util.mail: encode invalid address 'Phrase <blah' 
-        
-        Assume that this is a simple address. This address will
-        probably cause an error when trying to send mail. Junk in, junk
-        out.
-        """
-        address = u'Phrase <blah'
-        expected = address.encode(config.charset)
-        self.failUnlessEqual(mail.encodeAddress(address, self.charset),
-                             expected)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/mail/__init__.py	Wed Jun 07 10:39:30 2006 +0200
@@ -0,0 +1,10 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Package Initialization
+
+    Subpackage containing e-mail support code.
+
+    @copyright: 2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/mail/mailimport.py	Wed Jun 07 10:39:30 2006 +0200
@@ -0,0 +1,270 @@
+"""
+    MoinMoin - E-Mail Import into wiki
+    
+    Just call this script with the URL of the wiki as a single argument
+    and feed the mail into stdin.
+
+    @copyright: 2006 by MoinMoin:AlexanderSchremmer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os, sys, re, time
+import email
+from email.Utils import parseaddr, parsedate_tz, mktime_tz
+
+from MoinMoin import user, wikiutil, config
+from MoinMoin.action.AttachFile import add_attachment, AttachmentAlreadyExists
+from MoinMoin.Page import Page
+from MoinMoin.PageEditor import PageEditor
+from MoinMoin.request.CLI import Request as RequestCLI
+# python, at least up to 2.4, ships a broken parser for headers
+from MoinMoin.support.HeaderFixed import decode_header
+
+input = sys.stdin
+
+debug = False
+
+re_subject = re.compile(r"\[([^\]]*)\]")
+re_sigstrip = re.compile("\r?\n-- \r?\n.*$", re.S)
+
+class attachment(object):
+    """ Represents an attachment of a mail. """
+    def __init__(self, filename, mimetype, data):
+        self.filename = filename
+        self.mimetype = mimetype
+        self.data = data
+    
+    def __repr__(self):
+        return "<attachment filename=%r mimetype=%r size=%i bytes>" % (
+            self.filename, self.mimetype, len(self.data))
+
+class ProcessingError(Exception):
+    pass
+
+def log(text):
+    if debug:
+        print >>sys.stderr, text
+
+def decode_2044(header):
+    """ Decodes header field. See RFC 2044. """
+    chunks = decode_header(header)
+    chunks_decoded = []
+    for i in chunks:
+        chunks_decoded.append(i[0].decode(i[1] or 'ascii'))
+    return u''.join(chunks_decoded).strip()
+
+def process_message(message):
+    """ Processes the read message and decodes attachments. """
+    attachments = []
+    html_data = []
+    text_data = []
+   
+    to_addr = parseaddr(decode_2044(message['To']))
+    from_addr = parseaddr(decode_2044(message['From']))
+    cc_addr = parseaddr(decode_2044(message['Cc']))
+    bcc_addr = parseaddr(decode_2044(message['Bcc']))
+    
+    subject = decode_2044(message['Subject'])
+    date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(mktime_tz(parsedate_tz(message['Date']))))
+    
+    log("Processing mail:\n To: %r\n From: %r\n Subject: %r" % (to_addr, from_addr, subject))
+    
+    for part in message.walk():
+        log(" Part " + repr((part.get_charsets(), part.get_content_charset(), part.get_content_type(), part.is_multipart(), )))
+        ct = part.get_content_type()
+        cs = part.get_content_charset() or "latin1"
+        payload = part.get_payload(None, True)
+    
+        fn = part.get_filename()
+        if fn is not None and fn.startswith("=?"): # heuristics ...
+            fn = decode_2044(fn)
+            
+        if fn is None and part["Content-Disposition"] is not None and "attachment" in part["Content-Disposition"]:
+            # this doesn't catch the case where there is no content-disposition but there is a file to offer to the user
+            # i hope that this can be only found in mails that are older than 10 years,
+            # so I won't care about it here
+            fn = part["Content-Description"] or "NoName"
+        if fn:
+            a = attachment(fn, ct, payload)
+            attachments.append(a)
+        else:
+            if ct == 'text/plain':
+                text_data.append(payload.decode(cs))
+                log(repr(payload.decode(cs)))
+            elif ct == 'text/html':
+                html_data.append(payload.decode(cs))
+            elif not part.is_multipart():
+                log("Unknown mail part " + repr((part.get_charsets(), part.get_content_charset(), part.get_content_type(), part.is_multipart(), )))
+
+    return {'text': u"".join(text_data), 'html': u"".join(html_data),
+            'attachments': attachments,
+            'to_addr': to_addr, 'from_addr': from_addr, 'cc_addr': cc_addr, 'bcc_addr': bcc_addr,
+            'subject': subject, 'date': date}
+
+def get_pagename_content(msg, email_subpage_template, wiki_address):
+    """ Generates pagename and content according to the specification
+        that can be found on MoinMoin:FeatureRequests/WikiEmailintegration """
+
+    generate_summary = False
+    choose_html = True
+    
+    pagename_tpl = ""
+    for addr in ('to_addr', 'cc_addr', 'bcc_addr'):
+        if msg[addr][1].strip().lower() == wiki_address:
+            pagename_tpl = msg[addr][0]
+
+    if not pagename_tpl:
+        m = re_subject.match(msg['subject'])
+        if m:
+            pagename_tpl = m.group(1)
+    else:
+        # special fix for outlook users :-)
+        if pagename_tpl[-1] == pagename_tpl[0] == "'":
+            pagename_tpl = pagename_tpl[1:-1]
+    
+    if pagename_tpl.endswith("/"):
+        pagename_tpl += email_subpage_template
+
+    # last resort
+    if not pagename_tpl:
+        pagename_tpl = email_subpage_template
+
+    # rewrite using string.formatter when python 2.4 is mandatory
+    pagename = (pagename_tpl.replace("$from", msg['from_addr'][0]).
+                replace("$date", msg['date']).
+                replace("$subject", msg['subject']))
+
+    if pagename.startswith("+ ") and "/" in pagename:
+        generate_summary = True
+        pagename = pagename[1:].lstrip()
+
+    if choose_html and msg['html']:
+        content = "{{{#!html\n%s\n}}}" % msg['html'].replace("}}}", "} } }")
+    else:
+        # strip signatures ...
+        content = re_sigstrip.sub("", msg['text'])
+
+    return {'pagename': pagename, 'content': content, 'generate_summary': generate_summary}
+
+def import_mail_from_string(request, string):
+    """ Reads an RFC 822 compliant message from a string and imports it
+        to the wiki. """
+    return import_mail_from_message(request, email.message_from_string(string))
+
+def import_mail_from_file(request, input):
+    """ Reads an RFC 822 compliant message from the file `input` and imports it to
+        the wiki. """
+
+    return import_mail_from_message(request, email.message_from_file(input))
+
+def import_mail_from_message(request, message):
+    """ Reads a message generated by the email package and imports it
+        to the wiki. """
+    msg = process_message(message)
+
+    email_subpage_template = request.cfg.mail_import_subpage_template
+    wiki_address = request.cfg.mail_import_wiki_address or request.cfg.mail_from
+
+    request.user = user.get_by_email_address(request, msg['from_addr'][1])
+    
+    if not request.user:
+        raise ProcessingError("No suitable user found for mail address %r" % (msg['from_addr'][1], ))
+
+    d = get_pagename_content(msg, email_subpage_template, wiki_address)
+    pagename = d['pagename']
+    generate_summary = d['generate_summary']
+
+    comment = u"Mail: '%s'" % (msg['subject'], )
+    
+    page = PageEditor(request, pagename, do_editor_backup=0)
+    
+    if not request.user.may.save(page, "", 0):
+        raise ProcessingError("Access denied for page %r" % pagename)
+
+    attachments = []
+    
+    for att in msg['attachments']:
+        i = 0
+        while 1:
+            if i == 0:
+                fname = att.filename
+            else:
+                components = att.filename.split(".")
+                new_suffix = "-" + str(i)
+                # add the counter before the file extension
+                if len(components) > 1:
+                    fname = u"%s%s.%s" % (u".".join(components[:-1]), new_suffix, components[-1])
+                else:
+                    fname = att.filename + new_suffix
+            try:
+                # get the fname again, it might have changed
+                fname = add_attachment(request, pagename, fname, att.data)
+                attachments.append(fname)
+            except AttachmentAlreadyExists:
+                i += 1
+            else:
+                break
+
+    # build an attachment link table for the page with the e-mail
+    escape_link = lambda x: x.replace(" ", "%20")
+    attachment_links = [""] + [u"[attachment:%s attachment:%s]" % tuple([escape_link(u"%s/%s" % (pagename, x))] * 2) for x in attachments]
+
+    # assemble old page content and new mail body together
+    old_content = Page(request, pagename).get_raw_body()
+    if old_content:
+        new_content = u"%s\n-----\n%s" % (old_content, d['content'], )
+    else:
+        new_content = d['content']
+    new_content += "\n" + u"\n * ".join(attachment_links)
+
+    try:
+        page.saveText(new_content, 0, comment=comment)
+    except page.AccessDenied:
+        raise ProcessingError("Access denied for page %r" % pagename)
+    
+    if generate_summary and "/" in pagename:
+        parent_page = u"/".join(pagename.split("/")[:-1])
+        old_content = Page(request, parent_page).get_raw_body().splitlines()
+        
+        found_table = None
+        table_ends = None
+        for lineno, line in enumerate(old_content):
+            if line.startswith("## mail_overview") and old_content[lineno+1].startswith("||"):
+                found_table = lineno
+            elif found_table is not None and line.startswith("||"):
+                table_ends = lineno + 1
+            elif table_ends is not None and not line.startswith("||"):
+                break
+        
+        table_header = (u"\n\n## mail_overview (don't delete this line)\n" +
+                        u"|| '''[[GetText(From)]] ''' || '''[[GetText(To)]] ''' || '''[[GetText(Subject)]] ''' || '''[[GetText(Date)]] ''' || '''[[GetText(Link)]] ''' || '''[[GetText(Attachments)]] ''' ||\n"
+                       )
+        new_line = u'|| %s || %s || %s || [[DateTime(%s)]] || ["%s"] || %s ||' % (
+            msg['from_addr'][0] or msg['from_addr'][1],
+            msg['to_addr'][0] or msg['to_addr'][1],
+            msg['subject'],
+            msg['date'],
+            pagename,
+            " ".join(attachment_links),
+            )
+        if found_table is not None:
+            content = "\n".join(old_content[:table_ends] + [new_line] + old_content[table_ends:])
+        else:
+            content = "\n".join(old_content) + table_header + new_line
+
+        page = PageEditor(request, parent_page, do_editor_backup=0)
+        page.saveText(content, 0, comment=comment)
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1:
+        url = sys.argv[1]
+    else:
+        url = 'localhost/'
+
+    request = RequestCLI(url=url)
+
+    try:
+        import_mail_from_file(request, input)
+    except ProcessingError, e:
+        print >>sys.stderr, "An error occured while processing the message:", e.args
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/mail/sendmail.py	Wed Jun 07 10:39:30 2006 +0200
@@ -0,0 +1,173 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - email helper functions
+
+    @copyright: 2003 by Jrgen Hermann <jh@web.de>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os, re
+from email.Header import Header
+from MoinMoin import config
+
+_transdict = {"AT": "@", "DOT": ".", "DASH": "-"}
+
+
+def encodeAddress(address, charset):
+    """ Encode email address to enable non ascii names 
+    
+    e.g. '"Jrgen Hermann" <jh@web.de>'. According to the RFC, the name
+    part should be encoded, the address should not.
+    
+    @param address: email address, posibly using '"name" <address>' format
+    @type address: unicode
+    @param charset: sepcifying both the charset and the encoding, e.g
+        quoted printble or base64.
+    @type charset: email.Charset.Charset instance
+    @rtype: string
+    @return: encoded address
+    """   
+    composite = re.compile(r'(?P<phrase>.+)(?P<angle_addr>\<.*\>)', 
+                           re.UNICODE)
+    match = composite.match(address)
+    if match:
+        phrase = match.group('phrase').encode(config.charset)
+        phrase = str(Header(phrase, charset))
+        angle_addr = match.group('angle_addr').encode(config.charset)       
+        return phrase + angle_addr
+    else:
+        return address.encode(config.charset)
+
+
+def sendmail(request, to, subject, text, **kw):
+    """ Create and send a text/plain message
+        
+    Return a tuple of success or error indicator and message.
+    
+    @param request: the request object
+    @param to: recipients (list)
+    @param subject: subject of email (unicode)
+    @param text: email body text (unicode)
+    @keyword mail_from: override default mail_from
+    @type mail_from: unicode
+    @rtype: tuple
+    @return: (is_ok, Description of error or OK message)
+    """
+    import smtplib, socket
+    from email.Message import Message
+    from email.Charset import Charset, QP
+    from email.Utils import formatdate, make_msgid
+
+    _ = request.getText
+    cfg = request.cfg    
+    mail_from = kw.get('mail_from', '') or cfg.mail_from
+    subject = subject.encode(config.charset)    
+
+    # Create a text/plain body using CRLF (see RFC2822)
+    text = text.replace(u'\n', u'\r\n')
+    text = text.encode(config.charset)
+
+    # Create a message using config.charset and quoted printable
+    # encoding, which should be supported better by mail clients.
+    # TODO: check if its really works better for major mail clients
+    msg = Message()
+    charset = Charset(config.charset)
+    charset.header_encoding = QP
+    charset.body_encoding = QP
+    msg.set_charset(charset)    
+    msg.set_payload(charset.body_encode(text))
+    
+    # Create message headers
+    # Don't expose emails addreses of the other subscribers, instead we
+    # use the same mail_from, e.g. u"Jrgen Wiki <noreply@mywiki.org>"
+    address = encodeAddress(mail_from, charset) 
+    msg['From'] = address
+    msg['To'] = address
+    msg['Date'] = formatdate()
+    msg['Message-ID'] = make_msgid()
+    msg['Subject'] = Header(subject, charset)
+    
+    if cfg.mail_sendmail:
+        # Set the BCC.  This will be stripped later by sendmail.
+        msg['BCC'] = ','.join(to)
+        # Set Return-Path so that it isn't set (generally incorrectly) for us.
+        msg['Return-Path'] = address
+
+    # Send the message
+    if not cfg.mail_sendmail:
+        try:
+            host, port = (cfg.mail_smarthost + ':25').split(':')[:2]
+            server = smtplib.SMTP(host, int(port))
+            try:
+                #server.set_debuglevel(1)
+                if cfg.mail_login:
+                    user, pwd = cfg.mail_login.split()
+                    try: # try to do tls
+                        server.ehlo()
+                        if server.has_extn('starttls'):
+                            server.starttls()
+                            server.ehlo()
+                    except:
+                        pass
+                    server.login(user, pwd)
+                server.sendmail(mail_from, to, msg.as_string())
+            finally:
+                try:
+                    server.quit()
+                except AttributeError:
+                    # in case the connection failed, SMTP has no "sock" attribute
+                    pass
+        except smtplib.SMTPException, e:
+            return (0, str(e))
+        except (os.error, socket.error), e:
+            return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s") % {
+                'server': cfg.mail_smarthost, 
+                'reason': str(e)
+            })
+    else:
+        try:
+            sendmailp = os.popen(cfg.mail_sendmail, "w") 
+            # msg contains everything we need, so this is a simple write
+            sendmailp.write(msg.as_string())
+            sendmail_status = sendmailp.close()
+            if sendmail_status:
+                return (0, str(sendmail_status))
+        except:
+            return (0, _("Mail not sent"))
+
+    return (1, _("Mail sent OK"))
+
+
+def decodeSpamSafeEmail(address):
+    """ Decode obfuscated email address to standard email address
+
+    Decode a spam-safe email address in `address` by applying the
+    following rules:
+    
+    Known all-uppercase words and their translation:
+        "DOT"   -> "."
+        "AT"    -> "@"
+        "DASH"  -> "-"
+
+    Any unknown all-uppercase words simply get stripped.
+    Use that to make it even harder for spam bots!
+
+    Blanks (spaces) simply get stripped.
+    
+    @param address: obfuscated email address string
+    @rtype: string
+    @return: decoded email address
+    """
+    email = []
+
+    # words are separated by blanks
+    for word in address.split():
+        # is it all-uppercase?
+        if word.isalpha() and word == word.upper():
+            # strip unknown CAPS words
+            word = _transdict.get(word, '')
+        email.append(word)
+
+    # return concatenated parts
+    return ''.join(email)
+
--- a/MoinMoin/mailimport.py	Wed Jun 07 09:44:08 2006 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,270 +0,0 @@
-"""
-    MoinMoin - E-Mail Import
-    
-    Just call this script with the URL of the wiki as a single argument
-    and feed the mail into stdin.
-
-    @copyright: 2006 by MoinMoin:AlexanderSchremmer
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import os, sys, re, time
-import email
-from email.Utils import parseaddr, parsedate_tz, mktime_tz
-
-from MoinMoin import user, wikiutil, config
-from MoinMoin.action.AttachFile import add_attachment, AttachmentAlreadyExists
-from MoinMoin.Page import Page
-from MoinMoin.PageEditor import PageEditor
-from MoinMoin.request.CLI import Request as RequestCLI
-# python, at least up to 2.4, ships a broken parser for headers
-from MoinMoin.support.HeaderFixed import decode_header
-
-input = sys.stdin
-
-debug = False
-
-re_subject = re.compile(r"\[([^\]]*)\]")
-re_sigstrip = re.compile("\r?\n-- \r?\n.*$", re.S)
-
-class attachment(object):
-    """ Represents an attachment of a mail. """
-    def __init__(self, filename, mimetype, data):
-        self.filename = filename
-        self.mimetype = mimetype
-        self.data = data
-    
-    def __repr__(self):
-        return "<attachment filename=%r mimetype=%r size=%i bytes>" % (
-            self.filename, self.mimetype, len(self.data))
-
-class ProcessingError(Exception):
-    pass
-
-def log(text):
-    if debug:
-        print >>sys.stderr, text
-
-def decode_2044(header):
-    """ Decodes header field. See RFC 2044. """
-    chunks = decode_header(header)
-    chunks_decoded = []
-    for i in chunks:
-        chunks_decoded.append(i[0].decode(i[1] or 'ascii'))
-    return u''.join(chunks_decoded).strip()
-
-def process_message(message):
-    """ Processes the read message and decodes attachments. """
-    attachments = []
-    html_data = []
-    text_data = []
-   
-    to_addr = parseaddr(decode_2044(message['To']))
-    from_addr = parseaddr(decode_2044(message['From']))
-    cc_addr = parseaddr(decode_2044(message['Cc']))
-    bcc_addr = parseaddr(decode_2044(message['Bcc']))
-    
-    subject = decode_2044(message['Subject'])
-    date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(mktime_tz(parsedate_tz(message['Date']))))
-    
-    log("Processing mail:\n To: %r\n From: %r\n Subject: %r" % (to_addr, from_addr, subject))
-    
-    for part in message.walk():
-        log(" Part " + repr((part.get_charsets(), part.get_content_charset(), part.get_content_type(), part.is_multipart(), )))
-        ct = part.get_content_type()
-        cs = part.get_content_charset() or "latin1"
-        payload = part.get_payload(None, True)
-    
-        fn = part.get_filename()
-        if fn is not None and fn.startswith("=?"): # heuristics ...
-            fn = decode_2044(fn)
-            
-        if fn is None and part["Content-Disposition"] is not None and "attachment" in part["Content-Disposition"]:
-            # this doesn't catch the case where there is no content-disposition but there is a file to offer to the user
-            # i hope that this can be only found in mails that are older than 10 years,
-            # so I won't care about it here
-            fn = part["Content-Description"] or "NoName"
-        if fn:
-            a = attachment(fn, ct, payload)
-            attachments.append(a)
-        else:
-            if ct == 'text/plain':
-                text_data.append(payload.decode(cs))
-                log(repr(payload.decode(cs)))
-            elif ct == 'text/html':
-                html_data.append(payload.decode(cs))
-            elif not part.is_multipart():
-                log("Unknown mail part " + repr((part.get_charsets(), part.get_content_charset(), part.get_content_type(), part.is_multipart(), )))
-
-    return {'text': u"".join(text_data), 'html': u"".join(html_data),
-            'attachments': attachments,
-            'to_addr': to_addr, 'from_addr': from_addr, 'cc_addr': cc_addr, 'bcc_addr': bcc_addr,
-            'subject': subject, 'date': date}
-
-def get_pagename_content(msg, email_subpage_template, wiki_address):
-    """ Generates pagename and content according to the specification
-        that can be found on MoinMoin:FeatureRequests/WikiEmailintegration """
-
-    generate_summary = False
-    choose_html = True
-    
-    pagename_tpl = ""
-    for addr in ('to_addr', 'cc_addr', 'bcc_addr'):
-        if msg[addr][1].strip().lower() == wiki_address:
-            pagename_tpl = msg[addr][0]
-
-    if not pagename_tpl:
-        m = re_subject.match(msg['subject'])
-        if m:
-            pagename_tpl = m.group(1)
-    else:
-        # special fix for outlook users :-)
-        if pagename_tpl[-1] == pagename_tpl[0] == "'":
-            pagename_tpl = pagename_tpl[1:-1]
-    
-    if pagename_tpl.endswith("/"):
-        pagename_tpl += email_subpage_template
-
-    # last resort
-    if not pagename_tpl:
-        pagename_tpl = email_subpage_template
-
-    # rewrite using string.formatter when python 2.4 is mandatory
-    pagename = (pagename_tpl.replace("$from", msg['from_addr'][0]).
-                replace("$date", msg['date']).
-                replace("$subject", msg['subject']))
-
-    if pagename.startswith("+ ") and "/" in pagename:
-        generate_summary = True
-        pagename = pagename[1:].lstrip()
-
-    if choose_html and msg['html']:
-        content = "{{{#!html\n%s\n}}}" % msg['html'].replace("}}}", "} } }")
-    else:
-        # strip signatures ...
-        content = re_sigstrip.sub("", msg['text'])
-
-    return {'pagename': pagename, 'content': content, 'generate_summary': generate_summary}
-
-def import_mail_from_string(request, string):
-    """ Reads an RFC 822 compliant message from a string and imports it
-        to the wiki. """
-    return import_mail_from_message(request, email.message_from_string(string))
-
-def import_mail_from_file(request, input):
-    """ Reads an RFC 822 compliant message from the file `input` and imports it to
-        the wiki. """
-
-    return import_mail_from_message(request, email.message_from_file(input))
-
-def import_mail_from_message(request, message):
-    """ Reads a message generated by the email package and imports it
-        to the wiki. """
-    msg = process_message(message)
-
-    email_subpage_template = request.cfg.mail_import_subpage_template
-    wiki_address = request.cfg.mail_import_wiki_address or request.cfg.mail_from
-
-    request.user = user.get_by_email_address(request, msg['from_addr'][1])
-    
-    if not request.user:
-        raise ProcessingError("No suitable user found for mail address %r" % (msg['from_addr'][1], ))
-
-    d = get_pagename_content(msg, email_subpage_template, wiki_address)
-    pagename = d['pagename']
-    generate_summary = d['generate_summary']
-
-    comment = u"Mail: '%s'" % (msg['subject'], )
-    
-    page = PageEditor(request, pagename, do_editor_backup=0)
-    
-    if not request.user.may.save(page, "", 0):
-        raise ProcessingError("Access denied for page %r" % pagename)
-
-    attachments = []
-    
-    for att in msg['attachments']:
-        i = 0
-        while 1:
-            if i == 0:
-                fname = att.filename
-            else:
-                components = att.filename.split(".")
-                new_suffix = "-" + str(i)
-                # add the counter before the file extension
-                if len(components) > 1:
-                    fname = u"%s%s.%s" % (u".".join(components[:-1]), new_suffix, components[-1])
-                else:
-                    fname = att.filename + new_suffix
-            try:
-                # get the fname again, it might have changed
-                fname = add_attachment(request, pagename, fname, att.data)
-                attachments.append(fname)
-            except AttachmentAlreadyExists:
-                i += 1
-            else:
-                break
-
-    # build an attachment link table for the page with the e-mail
-    escape_link = lambda x: x.replace(" ", "%20")
-    attachment_links = [""] + [u"[attachment:%s attachment:%s]" % tuple([escape_link(u"%s/%s" % (pagename, x))] * 2) for x in attachments]
-
-    # assemble old page content and new mail body together
-    old_content = Page(request, pagename).get_raw_body()
-    if old_content:
-        new_content = u"%s\n-----\n%s" % (old_content, d['content'], )
-    else:
-        new_content = d['content']
-    new_content += "\n" + u"\n * ".join(attachment_links)
-
-    try:
-        page.saveText(new_content, 0, comment=comment)
-    except page.AccessDenied:
-        raise ProcessingError("Access denied for page %r" % pagename)
-    
-    if generate_summary and "/" in pagename:
-        parent_page = u"/".join(pagename.split("/")[:-1])
-        old_content = Page(request, parent_page).get_raw_body().splitlines()
-        
-        found_table = None
-        table_ends = None
-        for lineno, line in enumerate(old_content):
-            if line.startswith("## mail_overview") and old_content[lineno+1].startswith("||"):
-                found_table = lineno
-            elif found_table is not None and line.startswith("||"):
-                table_ends = lineno + 1
-            elif table_ends is not None and not line.startswith("||"):
-                break
-        
-        table_header = (u"\n\n## mail_overview (don't delete this line)\n" +
-                        u"|| '''[[GetText(From)]] ''' || '''[[GetText(To)]] ''' || '''[[GetText(Subject)]] ''' || '''[[GetText(Date)]] ''' || '''[[GetText(Link)]] ''' || '''[[GetText(Attachments)]] ''' ||\n"
-                       )
-        new_line = u'|| %s || %s || %s || [[DateTime(%s)]] || ["%s"] || %s ||' % (
-            msg['from_addr'][0] or msg['from_addr'][1],
-            msg['to_addr'][0] or msg['to_addr'][1],
-            msg['subject'],
-            msg['date'],
-            pagename,
-            " ".join(attachment_links),
-            )
-        if found_table is not None:
-            content = "\n".join(old_content[:table_ends] + [new_line] + old_content[table_ends:])
-        else:
-            content = "\n".join(old_content) + table_header + new_line
-
-        page = PageEditor(request, parent_page, do_editor_backup=0)
-        page.saveText(content, 0, comment=comment)
-
-if __name__ == "__main__":
-    if len(sys.argv) > 1:
-        url = sys.argv[1]
-    else:
-        url = 'localhost/'
-
-    request = RequestCLI(url=url)
-
-    try:
-        import_mail_from_file(request, input)
-    except ProcessingError, e:
-        print >>sys.stderr, "An error occured while processing the message:", e.args
-
--- a/MoinMoin/user.py	Wed Jun 07 09:44:08 2006 +0200
+++ b/MoinMoin/user.py	Wed Jun 07 10:39:30 2006 +0200
@@ -940,7 +940,7 @@
         return markup
 
     def mailAccountData(self, cleartext_passwd=None):
-        from MoinMoin.util import mail
+        from MoinMoin.mail import sendmail
         from MoinMoin.wikiutil import getSysPage
         _ = self._request.getText
 
@@ -980,7 +980,7 @@
 
         subject = _('[%(sitename)s] Your wiki account data',
                     formatted=False) % {'sitename': self._cfg.sitename or "Wiki"}
-        mailok, msg = mail.sendmail(self._request, [self.email], subject,
+        mailok, msg = sendmail.sendmail(self._request, [self.email], subject,
                                     text, mail_from=self._cfg.mail_from)
         return msg
 
--- a/MoinMoin/util/mail.py	Wed Jun 07 09:44:08 2006 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - email helper functions
-
-    @copyright: 2003 by Jrgen Hermann <jh@web.de>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import os, re
-from email.Header import Header
-from MoinMoin import config
-
-_transdict = {"AT": "@", "DOT": ".", "DASH": "-"}
-
-
-def encodeAddress(address, charset):
-    """ Encode email address to enable non ascii names 
-    
-    e.g. '"Jrgen Hermann" <jh@web.de>'. According to the RFC, the name
-    part should be encoded, the address should not.
-    
-    @param address: email address, posibly using '"name" <address>' format
-    @type address: unicode
-    @param charset: sepcifying both the charset and the encoding, e.g
-        quoted printble or base64.
-    @type charset: email.Charset.Charset instance
-    @rtype: string
-    @return: encoded address
-    """   
-    composite = re.compile(r'(?P<phrase>.+)(?P<angle_addr>\<.*\>)', 
-                           re.UNICODE)
-    match = composite.match(address)
-    if match:
-        phrase = match.group('phrase').encode(config.charset)
-        phrase = str(Header(phrase, charset))
-        angle_addr = match.group('angle_addr').encode(config.charset)       
-        return phrase + angle_addr
-    else:
-        return address.encode(config.charset)
-
-
-def sendmail(request, to, subject, text, **kw):
-    """ Create and send a text/plain message
-        
-    Return a tuple of success or error indicator and message.
-    
-    @param request: the request object
-    @param to: recipients (list)
-    @param subject: subject of email (unicode)
-    @param text: email body text (unicode)
-    @keyword mail_from: override default mail_from
-    @type mail_from: unicode
-    @rtype: tuple
-    @return: (is_ok, Description of error or OK message)
-    """
-    import smtplib, socket
-    from email.Message import Message
-    from email.Charset import Charset, QP
-    from email.Utils import formatdate, make_msgid
-
-    _ = request.getText
-    cfg = request.cfg    
-    mail_from = kw.get('mail_from', '') or cfg.mail_from
-    subject = subject.encode(config.charset)    
-
-    # Create a text/plain body using CRLF (see RFC2822)
-    text = text.replace(u'\n', u'\r\n')
-    text = text.encode(config.charset)
-
-    # Create a message using config.charset and quoted printable
-    # encoding, which should be supported better by mail clients.
-    # TODO: check if its really works better for major mail clients
-    msg = Message()
-    charset = Charset(config.charset)
-    charset.header_encoding = QP
-    charset.body_encoding = QP
-    msg.set_charset(charset)    
-    msg.set_payload(charset.body_encode(text))
-    
-    # Create message headers
-    # Don't expose emails addreses of the other subscribers, instead we
-    # use the same mail_from, e.g. u"Jrgen Wiki <noreply@mywiki.org>"
-    address = encodeAddress(mail_from, charset) 
-    msg['From'] = address
-    msg['To'] = address
-    msg['Date'] = formatdate()
-    msg['Message-ID'] = make_msgid()
-    msg['Subject'] = Header(subject, charset)
-    
-    if cfg.mail_sendmail:
-        # Set the BCC.  This will be stripped later by sendmail.
-        msg['BCC'] = ','.join(to)
-        # Set Return-Path so that it isn't set (generally incorrectly) for us.
-        msg['Return-Path'] = address
-
-    # Send the message
-    if not cfg.mail_sendmail:
-        try:
-            host, port = (cfg.mail_smarthost + ':25').split(':')[:2]
-            server = smtplib.SMTP(host, int(port))
-            try:
-                #server.set_debuglevel(1)
-                if cfg.mail_login:
-                    user, pwd = cfg.mail_login.split()
-                    try: # try to do tls
-                        server.ehlo()
-                        if server.has_extn('starttls'):
-                            server.starttls()
-                            server.ehlo()
-                    except:
-                        pass
-                    server.login(user, pwd)
-                server.sendmail(mail_from, to, msg.as_string())
-            finally:
-                try:
-                    server.quit()
-                except AttributeError:
-                    # in case the connection failed, SMTP has no "sock" attribute
-                    pass
-        except smtplib.SMTPException, e:
-            return (0, str(e))
-        except (os.error, socket.error), e:
-            return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s") % {
-                'server': cfg.mail_smarthost, 
-                'reason': str(e)
-            })
-    else:
-        try:
-            sendmailp = os.popen(cfg.mail_sendmail, "w") 
-            # msg contains everything we need, so this is a simple write
-            sendmailp.write(msg.as_string())
-            sendmail_status = sendmailp.close()
-            if sendmail_status:
-                return (0, str(sendmail_status))
-        except:
-            return (0, _("Mail not sent"))
-
-    return (1, _("Mail sent OK"))
-
-
-def decodeSpamSafeEmail(address):
-    """ Decode obfuscated email address to standard email address
-
-    Decode a spam-safe email address in `address` by applying the
-    following rules:
-    
-    Known all-uppercase words and their translation:
-        "DOT"   -> "."
-        "AT"    -> "@"
-        "DASH"  -> "-"
-
-    Any unknown all-uppercase words simply get stripped.
-    Use that to make it even harder for spam bots!
-
-    Blanks (spaces) simply get stripped.
-    
-    @param address: obfuscated email address string
-    @rtype: string
-    @return: decoded email address
-    """
-    email = []
-
-    # words are separated by blanks
-    for word in address.split():
-        # is it all-uppercase?
-        if word.isalpha() and word == word.upper():
-            # strip unknown CAPS words
-            word = _transdict.get(word, '')
-        email.append(word)
-
-    # return concatenated parts
-    return ''.join(email)
-
--- a/MoinMoin/xmlrpc/ProcessMail.py	Wed Jun 07 09:44:08 2006 +0200
+++ b/MoinMoin/xmlrpc/ProcessMail.py	Wed Jun 07 10:39:30 2006 +0200
@@ -6,7 +6,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin import mailimport
+from MoinMoin.mail import mailimport
 
 def execute(xmlrpcobj, secret, mail):
     request = xmlrpcobj.request
--- a/docs/CHANGES	Wed Jun 07 09:44:08 2006 +0200
+++ b/docs/CHANGES	Wed Jun 07 10:39:30 2006 +0200
@@ -62,6 +62,8 @@
       moved security.py to security/__init__.py,
       moved wikiacl.py to security/__init__.py.
     * moved logfile/logfile.py to logfile/__init__.py
+    * moved mailimport.py to mail/mailimport.py,
+      moved util/mail.py to mail/sendmail.py
     * added wikiutil.MimeType class (works internally with sanitized mime
       types because the official ones suck)
     * renamed parsers to module names representing sane mimetypes, e.g.: