diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index 071d4cd0618..7c136a5752f 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -77,7 +77,8 @@ - Administrator + -- +Administrator diff --git a/openerp/addons/base/base_demo.xml b/openerp/addons/base/base_demo.xml index cb9c836e0a6..89176d1811b 100644 --- a/openerp/addons/base/base_demo.xml +++ b/openerp/addons/base/base_demo.xml @@ -11,7 +11,8 @@ demo demo - Mr Demo + -- +Mr Demo diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 2d51ad7d6db..0d12a9a8bc3 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -44,8 +44,8 @@ import openerp import openerp.tools as tools from openerp.tools.translate import _ from openerp.tools import float_round, float_repr +from openerp.tools import html_sanitize import simplejson -from openerp.tools.html_sanitize import html_sanitize from openerp import SUPERUSER_ID _logger = logging.getLogger(__name__) diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index 65277218da3..d036ccfc751 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -8,7 +8,7 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not. See the :ref:`test-framework` section in the :ref:`features` list. """ -from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\ +from . import test_expression, test_mail, test_ir_sequence, test_orm,\ test_fields, test_basecase, \ test_view_validation, test_uninstall, test_misc, test_db_cursor from . import test_ir_filters @@ -20,7 +20,7 @@ fast_suite = [ checks = [ test_expression, - test_html_sanitize, + test_mail, test_db_cursor, test_orm, test_fields, diff --git a/openerp/tests/test_html_sanitize.py b/openerp/tests/test_html_sanitize.py deleted file mode 100755 index cb46b325144..00000000000 --- a/openerp/tests/test_html_sanitize.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from openerp.tools.html_sanitize import html_sanitize - -test_case = """ -test1 -
-test2
-test3
-test4
-test5
-test6
  • test7
  • -test8
  1. test9 -
  2. test10
-
-test11
-
-test12

-google -test link -""" - -class TestSanitizer(unittest.TestCase): - - def test_simple(self): - x = "yop" - self.assertEqual(x, html_sanitize(x)) - - def test_trailing_text(self): - x = 'lala

yop

xxx' - self.assertEqual(x, html_sanitize(x)) - - def test_no_exception(self): - html_sanitize(test_case) - - def test_unicode(self): - html_sanitize("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci") - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/openerp/tests/test_mail.py b/openerp/tests/test_mail.py new file mode 100644 index 00000000000..0ad6504a7ae --- /dev/null +++ b/openerp/tests/test_mail.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This test can be run stand-alone with something like: +# > PYTHONPATH=. python2 openerp/tests/test_misc.py +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import unittest2 +from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html + +HTML_SOURCE = """ +test1 +
+test2
+test3
+test4
+test5
+test6
  • test7
  • +test8
  1. test9 +
  2. test10
+
+test11
+
+test12

+google +test link +""" + +TEXT_MAIL1 = """I contact you about our meeting for tomorrow. Here is the schedule I propose: +9 AM: brainstorming about our new amazing business app +9.45 AM: summary +10 AM: meeting with Fabien to present our app +Is everything ok for you ? +-- +Administrator""" + +HTML_MAIL1 = """
+I contact you about our meeting for tomorrow. Here is the schedule I propose: +
+
    +
  • 9 AM: brainstorming about our new amazing business app
  • +
  • 9.45 AM: summary
  • +
  • 10 AM: meeting with Fabien to present our app
  • +
+
Is everything ok for you ?
""" + +GMAIL_REPLY1_SAN = """Hello,

Ok for me. I am replying directly in gmail, without signature.

Kind regards,

Demo.

On Thu, Nov 8, 2012 at 5:29 PM, <dummy@example.com> wrote:
I contact you about our meeting for tomorrow. Here is the schedule I propose:
  • 9 AM: brainstorming about our new amazing business app</span></li>
  • +
  • 9.45 AM: summary
  • 10 AM: meeting with Fabien to present our app
Is everything ok for you ?
+

--
Administrator

+ + +

""" + +THUNDERBIRD_16_REPLY1_SAN = """
On 11/08/2012 05:29 PM, + dummy@example.com wrote:
+
+
I contact you about our meeting for tomorrow. Here is the + schedule I propose:
+
+
  • 9 AM: brainstorming about our new amazing business + app</span></li>
  • +
  • 9.45 AM: summary
  • +
  • 10 AM: meeting with Fabien to present our app
  • +
+
Is everything ok for you ?
+
+

--
+ Administrator

+
+ +
+ Ok for me. I am replying directly below your mail, using + Thunderbird, with a signature.

+ Did you receive my email about my new laptop, by the way ?

+ Raoul.
-- 
+Raoul Grosbedonnée
+
""" + +TEXT_TPL = """Salut Raoul! +Le 28 oct. 2012 à 00:02, Raoul Grosbedon a écrit : + +> C'est sûr que je suis intéressé (quote)! + +Trouloulou pouet pouet. Je ne vais quand même pas écrire de vrais mails, non mais ho. + +> 2012/10/27 Bert Tartopoils : +>> Diantre, me disè-je en envoyant un message similaire à Martine, mais comment vas-tu (quote)? +>> +>> A la base le contenu était un vrai mail, mais je l'ai quand même réécrit pour ce test, histoire de dire que, quand même, on ne met pas n'importe quoi ici. (quote) +>> +>> Et sinon bon courage pour trouver tes clefs (quote). +>> +>> Bert TARTOPOILS +>> bert.tartopoils@miam.miam +>> +> +> +> -- +> Raoul Grosbedon + +Bert TARTOPOILS +bert.tartopoils@miam.miam +""" + + +class TestSanitizer(unittest2.TestCase): + """ Test the html sanitizer that filters html to remove unwanted attributes """ + + def test_simple(self): + x = "yop" + self.assertEqual(x, html_sanitize(x)) + + def test_trailing_text(self): + x = 'lala

yop

xxx' + self.assertEqual(x, html_sanitize(x)) + + def test_html(self): + sanitized_html = html_sanitize(HTML_SOURCE) + for tag in ['', '"), + ("First

It should be escaped

\nSignature", False, + "

First<p>It should be escaped</p>
Signature

") + ] + for content, container_tag, expected in cases: + html = plaintext2html(content, container_tag) + self.assertEqual(html, expected, 'plaintext2html is broken') + + def test_append_to_html(self): + test_samples = [ + ('some content', '--\nYours truly', True, True, False, + 'some content\n
--\nYours truly
\n'), + ('some content', '--\nYours truly', True, False, False, + 'some content\n

--
Yours truly

\n'), + ('some content', '\n\n

--

\n

Yours truly

\n\n', False, False, False, + 'some content\n\n\n

--

\n

Yours truly

\n\n\n'), + ] + for html, content, plaintext_flag, preserve_flag, container_tag, expected in test_samples: + self.assertEqual(append_content_to_html(html, content, plaintext_flag, preserve_flag, container_tag), expected, 'append_content_to_html is broken') + + +if __name__ == '__main__': + unittest2.main() diff --git a/openerp/tests/test_misc.py b/openerp/tests/test_misc.py index 54ae69899a6..9540c0222f3 100644 --- a/openerp/tests/test_misc.py +++ b/openerp/tests/test_misc.py @@ -4,18 +4,6 @@ import unittest2 from ..tools import misc -class append_content_to_html(unittest2.TestCase): - """ Test some of our generic utility functions """ - - def test_append_to_html(self): - test_samples = [ - ('some content', '--\nYours truly', True, - 'some content\n
--\nYours truly
\n'), - ('some content', '\n\n

--

\n

Yours truly

\n\n', False, - 'some content\n\n\n

--

\n

Yours truly

\n\n\n'), - ] - for html, content, flag, expected in test_samples: - self.assertEqual(misc.append_content_to_html(html,content,flag), expected, 'append_content_to_html is broken') class test_countingstream(unittest2.TestCase): def test_empty_stream(self): diff --git a/openerp/tools/__init__.py b/openerp/tools/__init__.py index a0ca411a9df..af14189bbae 100644 --- a/openerp/tools/__init__.py +++ b/openerp/tools/__init__.py @@ -33,7 +33,7 @@ from pdf_utils import * from yaml_import import * from sql import * from float_utils import * -from html_sanitize import * +from mail import * #.apidoc title: Tools diff --git a/openerp/tools/html_sanitize.py b/openerp/tools/html_sanitize.py deleted file mode 100644 index 6ea7b90e2ba..00000000000 --- a/openerp/tools/html_sanitize.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Business Applications -# Copyright (C) 2012 OpenERP S.A. (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import lxml.html -import operator -import re - -from openerp.loglevels import ustr - -def html_sanitize(src): - if not src: - return src - src = ustr(src, errors='replace') - root = lxml.html.fromstring(u"
%s
" % src) - result = handle_element(root) - res = [] - for element in children(result[0]): - if isinstance(element, basestring): - res.append(element) - else: - element.tail = "" - res.append(lxml.html.tostring(element)) - return ''.join(res) - -# FIXME: shouldn't this be a whitelist rather than a blacklist?! -to_remove = set(["script", "head", "meta", "title", "link", "img"]) -to_unwrap = set(["html", "body"]) - -javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE) - -def handle_a(el, new): - href = el.get("href", "#") - if javascript_regex.search(href): - href = "#" - new.set("href", href) - -special = { - "a": handle_a, -} - -def handle_element(element): - if isinstance(element, basestring): - return [element] - if element.tag in to_remove: - return [] - if element.tag in to_unwrap: - return reduce(operator.add, [handle_element(x) for x in children(element)]) - result = lxml.html.fromstring("<%s />" % element.tag) - for c in children(element): - append_to(handle_element(c), result) - if element.tag in special: - special[element.tag](element, result) - return [result] - -def children(node): - res = [] - if node.text is not None: - res.append(node.text) - for child_node in node.getchildren(): - res.append(child_node) - if child_node.tail is not None: - res.append(child_node.tail) - return res - -def append_to(elements, dest_node): - for element in elements: - if isinstance(element, basestring): - children = dest_node.getchildren() - if len(children) == 0: - dest_node.text = element - else: - children[-1].tail = element - else: - dest_node.append(element) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py new file mode 100644 index 00000000000..39d918da97b --- /dev/null +++ b/openerp/tools/mail.py @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (C) 2012 OpenERP S.A. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from lxml import etree +import cgi +import logging +import lxml.html +import openerp.pooler as pooler +import operator +import random +import re +import socket +import threading +import time + +from openerp.loglevels import ustr + +_logger = logging.getLogger(__name__) + + +#---------------------------------------------------------- +# HTML Sanitizer +#---------------------------------------------------------- + +def html_sanitize(src): + if not src: + return src + src = ustr(src, errors='replace') + root = lxml.html.fromstring(u"
%s
" % src) + result = handle_element(root) + res = [] + for element in children(result[0]): + if isinstance(element, basestring): + res.append(element) + else: + element.tail = "" + res.append(lxml.html.tostring(element)) + return ''.join(res) + +# FIXME: shouldn't this be a whitelist rather than a blacklist?! +to_remove = set(["script", "head", "meta", "title", "link", "img"]) +to_unwrap = set(["html", "body"]) + +javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE) + +def handle_a(el, new): + href = el.get("href", "#") + if javascript_regex.search(href): + href = "#" + new.set("href", href) + +special = { + "a": handle_a, +} + +def handle_element(element): + if isinstance(element, basestring): + return [element] + if element.tag in to_remove: + return [] + if element.tag in to_unwrap: + return reduce(operator.add, [handle_element(x) for x in children(element)]) + result = lxml.html.fromstring("<%s />" % element.tag) + for c in children(element): + append_to(handle_element(c), result) + if element.tag in special: + special[element.tag](element, result) + return [result] + +def children(node): + res = [] + if node.text is not None: + res.append(node.text) + for child_node in node.getchildren(): + res.append(child_node) + if child_node.tail is not None: + res.append(child_node.tail) + return res + +def append_to(elements, dest_node): + for element in elements: + if isinstance(element, basestring): + children = dest_node.getchildren() + if len(children) == 0: + dest_node.text = element + else: + children[-1].tail = element + else: + dest_node.append(element) + + +#---------------------------------------------------------- +# HTML Cleaner +#---------------------------------------------------------- + +def html_email_clean(html): + """ html_email_clean: clean the html to display in the web client. + - strip email quotes (remove blockquote nodes) + - strip signatures (remove --\n{\n)Blahblah), by replacing
by + \n to avoid ignoring signatures converted into html + + :param string html: sanitized html; tags like html or head should not + be present in the html string. This method therefore takes as input + html code coming from a sanitized source, like fields.html. + """ + def _replace_matching_regex(regex, source, replace=''): + dest = '' + idx = 0 + for item in re.finditer(regex, source): + dest += source[idx:item.start()] + replace + idx = item.end() + dest += source[idx:] + return dest + + html = ustr(html) + + # 1. -> \n, because otherwise the tree is obfuscated + br_tags = re.compile(r'([<]\s*[bB][rR]\s*\/?[>])') + html = _replace_matching_regex(br_tags, html, '__BR_TAG__') + + # 2. form a tree, handle (currently ?) pure-text by enclosing them in a pre + root = lxml.html.fromstring(html) + if not len(root) and root.text is None and root.tail is None: + html = '
%s
' % html + root = lxml.html.fromstring(html) + + # 2.5 remove quoted text in nodes + quote_tags = re.compile(r'(\n(>)+[^\n\r]*)') + for node in root.getiterator(): + if not node.text: + continue + node.text = _replace_matching_regex(quote_tags, node.text) + + # 3. remove blockquotes + quotes = [el for el in root.getiterator(tag='blockquote')] + for node in quotes: + # copy the node tail into parent text + if node.tail: + parent = node.getparent() + parent.text = parent.text or '' + node.tail + # remove the node + node.getparent().remove(node) + + # 4. strip signatures + signature = re.compile(r'([-]{2}[\s]?[\r\n]{1,2}[^\z]+)') + for elem in root.getiterator(): + if elem.text: + match = re.search(signature, elem.text) + if match: + elem.text = elem.text[:match.start()] + elem.text[match.end():] + if elem.tail: + match = re.search(signature, elem.tail) + if match: + elem.tail = elem.tail[:match.start()] + elem.tail[match.end():] + + # 5. \n back to
+ html = etree.tostring(root, pretty_print=True) + html = html.replace('__BR_TAG__', '
') + + # 6. Misc cleaning : + # - ClEditor seems to love using

-> replace with
+ br_div_tags = re.compile(r'(
\s*\s*<\/div>)') + html = _replace_matching_regex(br_div_tags, html, '
') + + return html + + +#---------------------------------------------------------- +# HTML/Text management +#---------------------------------------------------------- + +def html2plaintext(html, body_id=None, encoding='utf-8'): + """ From an HTML text, convert the HTML to plain text. + If @param body_id is provided then this is the tag where the + body (not necessarily ) starts. + """ + ## (c) Fry-IT, www.fry-it.com, 2007 + ## + ## download here: http://www.peterbe.com/plog/html2plaintext + + html = ustr(html) + tree = etree.fromstring(html, parser=etree.HTMLParser()) + + if body_id is not None: + source = tree.xpath('//*[@id=%s]' % (body_id,)) + else: + source = tree.xpath('//body') + if len(source): + tree = source[0] + + url_index = [] + i = 0 + for link in tree.findall('.//a'): + url = link.get('href') + if url: + i += 1 + link.tag = 'span' + link.text = '%s [%s]' % (link.text, i) + url_index.append(url) + + html = ustr(etree.tostring(tree, encoding=encoding)) + + html = html.replace('', '*').replace('', '*') + html = html.replace('', '*').replace('', '*') + html = html.replace('

', '*').replace('

', '*') + html = html.replace('

', '**').replace('

', '**') + html = html.replace('

', '**').replace('

', '**') + html = html.replace('', '/').replace('', '/') + html = html.replace('', '\n') + html = html.replace('

', '\n') + html = re.sub('', '\n', html) + html = re.sub('<.*?>', ' ', html) + html = html.replace(' ' * 2, ' ') + + # strip all lines + html = '\n'.join([x.strip() for x in html.splitlines()]) + html = html.replace('\n' * 2, '\n') + + for i, url in enumerate(url_index): + if i == 0: + html += '\n\n' + html += ustr('[%s] %s\n') % (i + 1, url) + + return html + +def plaintext2html(text, container_tag=False): + """ Convert plaintext into html. Content of the text is escaped to manage + html entities, using cgi.escape(). + - all \n,\r are replaced by
+ - enclose content into

+ - 2 or more consecutive
are considered as paragraph breaks + + :param string container_tag: container of the html; by default the + content is embedded into a

+ """ + text = cgi.escape(ustr(text)) + + # 1. replace \n and \r + text = text.replace('\n', '
') + text = text.replace('\r', '
') + + # 2-3: form paragraphs + idx = 0 + final = '

' + br_tags = re.compile(r'(([<]\s*[bB][rR]\s*\/?[>]\s*){2,})') + for item in re.finditer(br_tags, text): + final += text[idx:item.start()] + '

' + idx = item.end() + final += text[idx:] + '

' + + # 4. container + if container_tag: + final = '<%s>%s' % (container_tag, final, container_tag) + return ustr(final) + +def append_content_to_html(html, content, plaintext=True, preserve=False, container_tag=False): + """ Append extra content at the end of an HTML snippet, trying + to locate the end of the HTML document (, , or + EOF), and converting the provided content in html unless ``plaintext`` + is False. + Content conversion can be done in two ways: + - wrapping it into a pre (preserve=True) + - use plaintext2html (preserve=False, using container_tag to wrap the + whole content) + A side-effect of this method is to coerce all HTML tags to + lowercase in ``html``, and strip enclosing or tags in + content if ``plaintext`` is False. + + :param str html: html tagsoup (doesn't have to be XHTML) + :param str content: extra content to append + :param bool plaintext: whether content is plaintext and should + be wrapped in a
 tag.
+        :param bool preserve: if content is plaintext, wrap it into a 
+            instead of converting it into html
+    """
+    html = ustr(html)
+    if plaintext and preserve:
+        content = u'\n
%s
\n' % ustr(content) + elif plaintext: + content = '\n%s\n' % plaintext2html(content, container_tag) + else: + content = re.sub(r'(?i)(||)', '', content) + content = u'\n%s\n' % ustr(content) + # Force all tags to lowercase + html = re.sub(r'(])', + lambda m: '%s%s%s' % (m.group(1), m.group(2).lower(), m.group(3)), html) + insert_location = html.find('') + if insert_location == -1: + insert_location = html.find('') + if insert_location == -1: + return '%s%s' % (html, content) + return '%s%s%s' % (html[:insert_location], content, html[insert_location:]) + +#---------------------------------------------------------- +# Emails +#---------------------------------------------------------- + +email_re = re.compile(r""" + ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part + @ # mandatory @ sign + [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then? + \. + [a-z]{2,3} # TLD + ) + """, re.VERBOSE) +res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE) +command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) + +# Updated in 7.0 to match the model name as well +# Typical form of references is +# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain +reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE) + +def generate_tracking_message_id(res_id): + """Returns a string that can be used in the Message-ID RFC822 header field + + Used to track the replies related to a given object thanks to the "In-Reply-To" + or "References" fields that Mail User Agents will set. + """ + try: + rnd = random.SystemRandom().random() + except NotImplementedError: + rnd = random.random() + rndstr = ("%.15f" % rnd)[2:] + return "<%.15f.%s-openerp-%s@%s>" % (time.time(), rndstr, res_id, socket.gethostname()) + +def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, + attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None, + smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None): + """Low-level function for sending an email (deprecated). + + :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead. + :param email_from: A string used to fill the `From` header, if falsy, + config['email_from'] is used instead. Also used for + the `Reply-To` header if `reply_to` is not provided + :param email_to: a sequence of addresses to send the mail to. + """ + + # If not cr, get cr from current thread database + if not cr: + db_name = getattr(threading.currentThread(), 'dbname', None) + if db_name: + cr = pooler.get_db_only(db_name).cursor() + else: + raise Exception("No database cursor found, please pass one explicitly") + + # Send Email + try: + mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server') + res = False + # Pack Message into MIME Object + email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to, + attachments, message_id, references, openobject_id, subtype, headers=headers) + + res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None, + smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password, + smtp_encryption=('ssl' if ssl else None), smtp_debug=debug) + except Exception: + _logger.exception("tools.email_send failed to deliver email") + return False + finally: + cr.close() + return res + +def email_split(text): + """ Return a list of the email addresses found in ``text`` """ + if not text: + return [] + return re.findall(r'([^ ,<@]+@[^> ,]+)', text) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index caf28e54481..4e3db23bc2d 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -30,8 +30,6 @@ from functools import wraps import subprocess import logging import os -import random -import re import socket import sys import threading @@ -48,8 +46,6 @@ try: except ImportError: html2text = None -import openerp.loglevels as loglevels -import openerp.pooler as pooler from config import config from cache import * @@ -274,168 +270,6 @@ def reverse_enumerate(l): """ return izip(xrange(len(l)-1, -1, -1), reversed(l)) -#---------------------------------------------------------- -# Emails -#---------------------------------------------------------- -email_re = re.compile(r""" - ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part - @ # mandatory @ sign - [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then? - \. - [a-z]{2,3} # TLD - ) - """, re.VERBOSE) -res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE) -command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) - -# Updated in 7.0 to match the model name as well -# Typical form of references is -# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain -reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE) - -def html2plaintext(html, body_id=None, encoding='utf-8'): - """ From an HTML text, convert the HTML to plain text. - If @param body_id is provided then this is the tag where the - body (not necessarily ) starts. - """ - ## (c) Fry-IT, www.fry-it.com, 2007 - ## - ## download here: http://www.peterbe.com/plog/html2plaintext - - html = ustr(html) - - from lxml.etree import tostring, fromstring, HTMLParser - tree = fromstring(html, parser=HTMLParser()) - - if body_id is not None: - source = tree.xpath('//*[@id=%s]'%(body_id,)) - else: - source = tree.xpath('//body') - if len(source): - tree = source[0] - - url_index = [] - i = 0 - for link in tree.findall('.//a'): - url = link.get('href') - if url: - i += 1 - link.tag = 'span' - link.text = '%s [%s]' % (link.text, i) - url_index.append(url) - - html = ustr(tostring(tree, encoding=encoding)) - - html = html.replace('','*').replace('','*') - html = html.replace('','*').replace('','*') - html = html.replace('

','*').replace('

','*') - html = html.replace('

','**').replace('

','**') - html = html.replace('

','**').replace('

','**') - html = html.replace('','/').replace('','/') - html = html.replace('', '\n') - html = html.replace('

', '\n') - html = re.sub('', '\n', html) - html = re.sub('<.*?>', ' ', html) - html = html.replace(' ' * 2, ' ') - - # strip all lines - html = '\n'.join([x.strip() for x in html.splitlines()]) - html = html.replace('\n' * 2, '\n') - - for i, url in enumerate(url_index): - if i == 0: - html += '\n\n' - html += ustr('[%s] %s\n') % (i+1, url) - - return html - -def generate_tracking_message_id(res_id): - """Returns a string that can be used in the Message-ID RFC822 header field - - Used to track the replies related to a given object thanks to the "In-Reply-To" - or "References" fields that Mail User Agents will set. - """ - try: - rnd = random.SystemRandom().random() - except NotImplementedError: - rnd = random.random() - rndstr = ("%.15f" % rnd)[2:] - return "<%.15f.%s-openerp-%s@%s>" % (time.time(), rndstr, res_id, socket.gethostname()) - -def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, - attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None, - smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None): - """Low-level function for sending an email (deprecated). - - :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead. - :param email_from: A string used to fill the `From` header, if falsy, - config['email_from'] is used instead. Also used for - the `Reply-To` header if `reply_to` is not provided - :param email_to: a sequence of addresses to send the mail to. - """ - - # If not cr, get cr from current thread database - if not cr: - db_name = getattr(threading.currentThread(), 'dbname', None) - if db_name: - cr = pooler.get_db_only(db_name).cursor() - else: - raise Exception("No database cursor found, please pass one explicitly") - - # Send Email - try: - mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server') - res = False - # Pack Message into MIME Object - email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to, - attachments, message_id, references, openobject_id, subtype, headers=headers) - - res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None, - smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password, - smtp_encryption=('ssl' if ssl else None), smtp_debug=debug) - except Exception: - _logger.exception("tools.email_send failed to deliver email") - return False - finally: - cr.close() - return res - -def email_split(text): - """ Return a list of the email addresses found in ``text`` """ - if not text: return [] - return re.findall(r'([^ ,<@]+@[^> ,]+)', text) - -def append_content_to_html(html, content, plaintext=True): - """Append extra content at the end of an HTML snippet, trying - to locate the end of the HTML document (, , or - EOF), and wrapping the provided content in a
 block
-       unless ``plaintext`` is False. A side-effect of this
-       method is to coerce all HTML tags to lowercase in ``html``,
-       and strip enclosing  or  tags in content if
-       ``plaintext`` is False.
-       
-       :param str html: html tagsoup (doesn't have to be XHTML)
-       :param str content: extra content to append
-       :param bool plaintext: whether content is plaintext and should
-           be wrapped in a 
 tag.
-    """
-    html = ustr(html)
-    if plaintext:
-        content = u'\n
%s
\n' % ustr(content) - else: - content = re.sub(r'(?i)(||)', '', content) - content = u'\n%s\n'% ustr(content) - # Force all tags to lowercase - html = re.sub(r'(])', - lambda m: '%s%s%s' % (m.group(1),m.group(2).lower(),m.group(3)), html) - insert_location = html.find('') - if insert_location == -1: - insert_location = html.find('') - if insert_location == -1: - return '%s%s' % (html, content) - return '%s%s%s' % (html[:insert_location], content, html[insert_location:]) - - #---------------------------------------------------------- # SMS #----------------------------------------------------------