Mercurial > moin > 1.9
changeset 714:3128414a1380
Merge with main.
author | Alexander Schremmer <alex AT alexanderweb DOT de> |
---|---|
date | Tue, 23 May 2006 10:33:19 +0200 |
parents | fd679c4f4665 (current diff) db1cc2a726b9 (diff) |
children | 1cecac2e6cfb |
files | |
diffstat | 11 files changed, 500 insertions(+), 48 deletions(-) [+] |
line wrap: on
line diff
--- a/MoinMoin/action/AttachFile.py Tue May 23 10:32:40 2006 +0200 +++ b/MoinMoin/action/AttachFile.py Tue May 23 10:33:19 2006 +0200 @@ -41,6 +41,9 @@ ### External interface - these are called from the core code ############################################################################# +class AttachmentAlreadyExists(Exception): + pass + def getBasePath(request): """ Get base path where page dirs for attachments are stored. """ @@ -155,7 +158,42 @@ } return "\n<p>\n%s\n</p>\n" % attach_info +def add_attachment(request, pagename, target, filecontent): + # replace illegal chars + target = wikiutil.taintfilename(target) + # set mimetype from extension, or from given mimetype + #type, encoding = mimetypes.guess_type(target) + #if not type: + # ext = None + # if request.form.has_key('mime'): + # ext = mimetypes.guess_extension(request.form['mime'][0]) + # if not ext: + # type, encoding = mimetypes.guess_type(filename) + # if type: + # ext = mimetypes.guess_extension(type) + # else: + # ext = '' + # target = target + ext + + # get directory, and possibly create it + attach_dir = getAttachDir(request, pagename, create=1) + # save file + fpath = os.path.join(attach_dir, target).encode(config.charset) + if os.path.exists(fpath): + raise AttachmentAlreadyExists + else: + stream = open(fpath, 'wb') + try: + stream.write(filecontent) + finally: + stream.close() + os.chmod(fpath, 0666 & config.umask) + + _addLogEntry(request, 'ATTNEW', pagename, target) + + return target + ############################################################################# ### Internal helpers ############################################################################# @@ -536,49 +574,23 @@ filecontent = request.form['file'][0] # preprocess the filename - # 1. strip leading drive and path (IE misbehaviour) + # strip leading drive and path (IE misbehaviour) if len(target) > 1 and (target[1] == ':' or target[0] == '\\'): # C:.... or \path... or \\server\... bsindex = target.rfind('\\') if bsindex >= 0: target = target[bsindex+1:] - - # 2. replace illegal chars - target = wikiutil.taintfilename(target) - - # set mimetype from extension, or from given mimetype - #type, encoding = mimetypes.guess_type(target) - #if not type: - # ext = None - # if request.form.has_key('mime'): - # ext = mimetypes.guess_extension(request.form['mime'][0]) - # if not ext: - # type, encoding = mimetypes.guess_type(filename) - # if type: - # ext = mimetypes.guess_extension(type) - # else: - # ext = '' - # target = target + ext - - # get directory, and possibly create it - attach_dir = getAttachDir(request, pagename, create=1) - # save file - fpath = os.path.join(attach_dir, target).encode(config.charset) - if os.path.exists(fpath): - msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % { - 'target': target, 'filename': filename} - else: - stream = open(fpath, 'wb') - try: - stream.write(filecontent) - finally: - stream.close() - os.chmod(fpath, 0666 & config.umask) + + # add the attachment + try: + add_attachment(request, pagename, target, filecontent) bytes = len(filecontent) msg = _("Attachment '%(target)s' (remote name '%(filename)s')" " with %(bytes)d bytes saved.") % { 'target': target, 'filename': filename, 'bytes': bytes} - _addLogEntry(request, 'ATTNEW', pagename, target) + except AttachmentAlreadyExists: + msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % { + 'target': target, 'filename': filename} # return attachment list upload_form(pagename, request, msg)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/mailimport.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,270 @@ +""" + 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-%d %H:%M", 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("$subj", 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.email_subpage_template + wiki_address = request.cfg.email_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"Imported mail from '%s' re '%s'" % (msg['from_addr'][0], 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 || %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: + 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/multiconfig.py Tue May 23 10:32:40 2006 +0200 +++ b/MoinMoin/multiconfig.py Tue May 23 10:33:19 2006 +0200 @@ -2,7 +2,7 @@ """ MoinMoin - Multiple configuration handler and Configuration defaults class - @copyright: 2000-2004 by Jürgen Hermann <jh@web.de> + @copyright: 2000-2004 by Jrgen Hermann <jh@web.de> @license: GNU GPL, see COPYING for details. """ @@ -239,6 +239,9 @@ } edit_locking = 'warn 10' # None, 'warn <timeout mins>', 'lock <timeout mins>' edit_rows = 20 + email_subpage_template = u"$from-$date-$subj" # used for mail import + email_wiki_address = None # the e-mail address for e-mails that should go into the wiki + email_secret = "" hacks = {} # { 'feature1': value1, ... } # Configuration for features still in development. @@ -267,7 +270,7 @@ mail_login = None # or "user pwd" if you need to use SMTP AUTH mail_sendmail = None # "/usr/sbin/sendmail -t -i" to not use SMTP, but sendmail mail_smarthost = None - mail_from = None # u'Jürgen Wiki <noreply@jhwiki.org>' + mail_from = None # u'Jrgen Wiki <noreply@jhwiki.org>' navi_bar = [u'RecentChanges', u'FindPage', u'HelpContents', ] nonexist_qm = 0
--- a/MoinMoin/script/__init__.py Tue May 23 10:32:40 2006 +0200 +++ b/MoinMoin/script/__init__.py Tue May 23 10:33:19 2006 +0200 @@ -24,7 +24,7 @@ def fatal(msgtext, **kw): """ Print error msg to stderr and exit. """ - sys.stderr.write("FATAL ERROR: " + msgtext + "\n") + sys.stderr.write("\n\nFATAL ERROR: " + msgtext + "\n") if kw.get('usage', 0): maindict = vars(sys.modules[script_module]) if maindict.has_key('usage'): @@ -58,12 +58,13 @@ import optparse from MoinMoin import version + # what does this code do? at least it does not work. cmd = self.script_module.__name__.split('.')[-1].replace('_', '-') rev = "%s %s [%s]" % (version.project, version.release, version.revision) - sys.argv[0] = cmd + #sys.argv[0] = cmd self.parser = optparse.OptionParser( - usage="%(cmd)s %(usage)s\n\n" % {'cmd': cmd, 'usage': usage, }, + usage="%(cmd)s [command] %(usage)s" % {'cmd': os.path.basename(sys.argv[0]), 'usage': usage, }, version=rev) self.parser.allow_interspersed_args = False if def_values: @@ -139,11 +140,14 @@ args = self.args if len(args) < 2: - self.parser.error("you must specify a command module and name.") - sys.exit(1) + self.parser.print_help() + fatal("You must specify a command module and name.") cmd_module, cmd_name = args[:2] from MoinMoin import wikiutil - plugin_class = wikiutil.importBuiltinPlugin('script.%s' % cmd_module, cmd_name, 'PluginScript') + try: + plugin_class = wikiutil.importBuiltinPlugin('script.%s' % cmd_module, cmd_name, 'PluginScript') + except wikiutil.PluginMissingError: + fatal("Command plugin %r, command %r was not found." % (cmd_module, cmd_name)) plugin_class(args[2:], self.options).run() # all starts again there
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/script/xmlrpc/__init__.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,12 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - XMLRPC CLI Scripts + + @copyright: 2006 by MoinMoin:AlexanderSchremmer + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.util import pysupport + +# create a list of extension scripts from the subpackage directory +modules = pysupport.getPackageModules(__file__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/script/xmlrpc/mailimport.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,38 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - MailImport script + + Imports a mail into the wiki. + + @copyright: 2006 by MoinMoin:AlexanderSchremmer + @license: GNU GPL, see COPYING for details. +""" + +import sys +import xmlrpclib + +from MoinMoin.script import MoinScript, fatal + +input = sys.stdin + +class PluginScript(MoinScript): + """ Mail Importer """ + + def __init__(self, argv, def_values): + MoinScript.__init__(self, argv, def_values) + + def mainloop(self): + try: + import mailimportconf + except ImportError: + fatal("Could not find the file mailimportconf.py. Maybe you want to use the config param?") + + secret = mailimportconf.mailimport_secret + url = mailimportconf.mailimport_url + + s = xmlrpclib.ServerProxy(url) + + result = s.ProcessMail(secret, input.read()) + + if result != "OK": + print >>sys.stderr, result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/HeaderFixed.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,75 @@ +# copied from email.Header because the original is broken + +# Copyright (C) 2002-2004 Python Software Foundation +# Author: Ben Gertzfield, Barry Warsaw +# Contact: email-sig@python.org + +import sys + +from email.Header import ecre + +import email.quopriMIME +import email.base64MIME +from email.Errors import HeaderParseError +from email.Charset import Charset + +if sys.version_info[:3] < (2, 9, 0): # insert the version number + # of a fixed python here + + def decode_header(header): + """Decode a message header value without converting charset. + + Returns a list of (decoded_string, charset) pairs containing each of the + decoded parts of the header. Charset is None for non-encoded parts of the + header, otherwise a lower-case string containing the name of the character + set specified in the encoded string. + + An email.Errors.HeaderParseError may be raised when certain decoding error + occurs (e.g. a base64 decoding exception). + """ + # If no encoding, just return the header + header = str(header) + if not ecre.search(header): + return [(header, None)] + decoded = [] + dec = '' + for line in header.splitlines(): + # This line might not have an encoding in it + if not ecre.search(line): + decoded.append((line, None)) + continue + parts = ecre.split(line) + while parts: + unenc = parts.pop(0).rstrip() + if unenc: + # Should we continue a long line? + if decoded and decoded[-1][1] is None: + decoded[-1] = (decoded[-1][0] + SPACE + unenc, None) + else: + decoded.append((unenc, None)) + if parts: + charset, encoding = [s.lower() for s in parts[0:2]] + encoded = parts[2] + dec = None + if encoding == 'q': + dec = email.quopriMIME.header_decode(encoded) + elif encoding == 'b': + try: + dec = email.base64MIME.decode(encoded) + except binascii.Error: + # Turn this into a higher level exception. BAW: Right + # now we throw the lower level exception away but + # when/if we get exception chaining, we'll preserve it. + raise HeaderParseError + if dec is None: + dec = encoded + + if decoded and decoded[-1][1] == charset: + decoded[-1] = (decoded[-1][0] + dec, decoded[-1][1]) + else: + decoded.append((dec, charset)) + del parts[0:3] + return decoded + +else: + from email.Header import decode_header
--- a/MoinMoin/user.py Tue May 23 10:32:40 2006 +0200 +++ b/MoinMoin/user.py Tue May 23 10:33:19 2006 +0200 @@ -33,6 +33,13 @@ userlist = [f for f in files if user_re.match(f)] return userlist +def get_by_email_address(request, email_address): + """ Searches for a user with a particular e-mail address and + returns it.""" + for uid in getUserList(request): + theuser = User(request, uid) + if theuser.valid and theuser.email.lower() == email_address.lower(): + return theuser def getUserId(request, searchName): """
--- a/MoinMoin/userform.py Tue May 23 10:32:40 2006 +0200 +++ b/MoinMoin/userform.py Tue May 23 10:33:19 2006 +0200 @@ -76,12 +76,10 @@ except KeyError: return _("Please provide a valid email address!") - users = user.getUserList(self.request) - for uid in users: - theuser = user.User(self.request, uid) - if theuser.valid and theuser.email.lower() == email: - msg = theuser.mailAccountData() - return wikiutil.escape(msg) + u = user.get_by_email_address(self.request, email) + if u: + msg = u.mailAccountData() + return wikiutil.escape(msg) return _("Found no account matching the given email address '%(email)s'!") % {'email': wikiutil.escape(email)}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/xmlrpc/ProcessMail.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,26 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - This plugin is used for multi-tier mail processing + + @copyright: 2006 by MoinMoin:AlexanderSchremmer + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin import mailimport + +def execute(xmlrpcobj, secret, mail): + request = xmlrpcobj.request + secret = xmlrpcobj._instr(secret) + # hmm, repeated re-encoding looks suboptimal in terms of speed + mail = xmlrpcobj._instr(mail).encode("utf-8") + + if request.cfg.email_secret != secret: + return u"Invalid password" + + try: + mailimport.import_mail_from_string(request, mail) + except mailimport.ProcessingError, e: + err = u"An error occured while processing the message: " + str(e.args) + request.log(err) + return xmlrpcobj._outstr(err) + return xmlrpcobj._outstr(u"OK")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wiki/config/mailimportconf.py Tue May 23 10:33:19 2006 +0200 @@ -0,0 +1,7 @@ +# This is the configuration file for the mail import client + +# This secret has to be known by the wiki server +mailimport_secret = u"foo" + +# Only needed for wiki farms +mailimport_url = u"http://localhost:81/?action=xmlrpc2"