Ticket #6667: mail_wrapping.diff

File mail_wrapping.diff, 9.8 KB (added by SmileyChris, 7 years ago)
  • django/core/mail.py

     
    44
    55from django.conf import settings
    66from django.utils.encoding import smart_str, force_unicode
     7from django.utils.mail import wrap
    78from email import Charset, Encoders
    89from email.MIMEText import MIMEText
    910from email.MIMEMultipart import MIMEMultipart
     
    8687    return name, val
    8788
    8889class SafeMIMEText(MIMEText):
     90    def __init__(self, _text, _subtype='plain', *args, **kwargs):
     91        format_flowed = False
     92        if 'wrap_text' in kwargs:
     93            if kwargs.pop('wrap_text') and _subtype == 'plain':
     94                _text = wrap(_text)
     95                format_flowed = True
     96        if 'format_flowed' in kwargs:
     97            if kwargs.pop('format_flowed') and _subtype == 'plain':
     98                format_flowed = True
     99        MIMEText.__init__(self, _text, _subtype, *args, **kwargs)
     100        if format_flowed:
     101            self.set_param('format', 'flowed', 'Content-Type')
     102            self.set_param('delsp', 'yes', 'Content-Type')
     103
    89104    def __setitem__(self, name, val):
    90105        name, val = forbid_multi_line_headers(name, val)
    91106        MIMEText.__setitem__(self, name, val)
     
    190205    encoding = None     # None => use settings default
    191206
    192207    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
    193             connection=None, attachments=None, headers=None):
     208            connection=None, attachments=None, headers=None, wrap_text=False):
    194209        """
    195210        Initialise a single email message (which can be sent to multiple
    196211        recipients).
     
    213228        self.attachments = attachments or []
    214229        self.extra_headers = headers or {}
    215230        self.connection = connection
     231        self.wrap_text = wrap_text
    216232
    217233    def get_connection(self, fail_silently=False):
    218234        if not self.connection:
     
    221237
    222238    def message(self):
    223239        encoding = self.encoding or settings.DEFAULT_CHARSET
    224         msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET), self.content_subtype, encoding)
     240        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
     241                           self.content_subtype, encoding,
     242                           wrap_text=self.wrap_text)
    225243        if self.attachments:
    226244            body_msg = msg
    227245            msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
     
    354372    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
    355373            settings.SERVER_EMAIL, [a[1] for a in
    356374                settings.MANAGERS]).send(fail_silently=fail_silently)
    357 
  • django/utils/mail.py

     
     1import re
     2from django.utils.encoding import force_unicode
     3from django.utils.functional import allow_lazy
     4
     5def wrap(text, width=78):
     6    """
     7    An email based word-wrap function that preserves existing line breaks and
     8    adds soft line breaks (' \n').
     9
     10    Long words are not wrapped, so the output text may have lines longer than
     11    ``width``.
     12
     13    Trailing spaces are stripped from each line so they are not confused with
     14    soft line breaks. All other white space is preserved.
     15
     16    Soft-broken lines are parsed for quotes and space-stuffed in accordance
     17    with RFC 3676. A break is forced at 998 characters (RFC 2822 2.1.1).
     18    """
     19    text = force_unicode(text)
     20    # Signature lines aren't stripped -- see RFC 3676 4.3
     21    text = re.sub(r'(?m)(?<!^--) +$', '', text)
     22    def _parse_line(line, quote, new_line):
     23        if new_line and line.startswith('>'):
     24            quote = re.match('>+', line).group()
     25        elif quote:
     26            line = '%s %s' % (quote, line)
     27        elif (line.startswith('>') or line.startswith('From ')):
     28            # Space stuff -- see RFC 3676 4.4
     29            line = ' %s' % line
     30        max_width = (line.endswith('\n') and width + 1 or width)
     31        return line, quote, max_width
     32    def _generator():
     33        for line in text.splitlines(True):   # True keeps trailing linebreaks
     34            quote = ''
     35            line, quote, max_width = _parse_line(line, quote, new_line=True)
     36            while len(line) > max_width:
     37                space = line[:max_width].rfind(' ') + 1
     38                if space == 0:
     39                    space = line.find(' ') + 1
     40                    space = min(space, 998)
     41                    if space == 0:
     42                        if len(line) > 998:
     43                            space = 998
     44                        else:
     45                            yield line
     46                            line = ''
     47                            break
     48                if space >= max_width:
     49                    space -= 1
     50                yield '%s \n' % line[:space]
     51                line = line[space:]
     52                line, quote, max_width = _parse_line(line, quote, False)
     53            if line:
     54                yield line
     55    return u''.join(_generator())
     56wrap = allow_lazy(wrap, unicode)
  • django/utils/text.py

     
    1010
    1111def wrap(text, width):
    1212    """
    13     A word-wrap function that preserves existing line breaks and most spaces in
    14     the text. Expects that existing line breaks are posix newlines.
     13    A word-wrap function that preserves existing line breaks. Expects that
     14    existing line breaks are posix newlines.
     15
     16    All white space is preserved except added line breaks consume the space on
     17    which they break the line.
     18
     19    Long words are not wrapped, so the output text may have lines longer than
     20    ``width``.
    1521    """
    1622    text = force_unicode(text)
    1723    def _generator():
    18         it = iter(text.split(' '))
    19         word = it.next()
    20         yield word
    21         pos = len(word) - word.rfind('\n') - 1
    22         for word in it:
    23             if "\n" in word:
    24                 lines = word.split('\n')
    25             else:
    26                 lines = (word,)
    27             pos += len(lines[0]) + 1
    28             if pos > width:
    29                 yield '\n'
    30                 pos = len(lines[-1])
    31             else:
    32                 yield ' '
    33                 if len(lines) > 1:
    34                     pos = len(lines[-1])
    35             yield word
     24        for line in text.splitlines(True):   # True keeps trailing linebreaks
     25            quote = ''
     26            max_width = (line.endswith('\n') and width + 1 or width)
     27            while len(line) > max_width:
     28                space = line[:max_width+1].rfind(' ') + 1
     29                if space == 0:
     30                    space = line.find(' ') + 1
     31                    if space == 0:
     32                        yield line
     33                        line = ''
     34                        break
     35                yield '%s\n' % line[:space-1]
     36                line = line[space:]
     37                max_width = (line.endswith('\n') and width + 1 or width)
     38            if line:
     39                yield line
    3640    return u''.join(_generator())
    3741wrap = allow_lazy(wrap, unicode)
    3842
  • tests/regressiontests/utils/tests.py

     
    88
    99import timesince
    1010import datastructures
     11import text
     12import mail
    1113
    1214# Extra tests
    1315__test__ = {
    1416    'timesince': timesince,
    1517    'datastructures': datastructures,
     18    'text': text,
     19    'mail': mail,
    1620}
    1721
    1822class TestUtilsHtml(TestCase):
  • tests/regressiontests/utils/mail.py

     
     1r"""
     2>>> from django.utils.mail import wrap
     3
     4>>> wrap('1234 67 9', 100)
     5u'1234 67 9'
     6>>> wrap('1234 67 9', 9)
     7u'1234 67 9'
     8>>> wrap('1234 67 9', 8)
     9u'1234 67 \n 9'
     10
     11>>> wrap('short\na long line', 7)
     12u'short\na long \n line'
     13
     14>>> wrap('email wrapping: \nStrips spaces before lines and uses soft spacing for line breaks', 30)
     15u'email wrapping:\nStrips spaces before lines  \nand uses soft spacing for  \nline breaks'
     16
     17>>> long_word = 'l%sng' % ('0123456789'*100)
     18
     19# Long word broken at 997 (998 - 1 char for space)
     20>>> wrap(long_word) == '%s \n%s' % (long_word[:997], long_word[997:])
     21True
     22>>> out = wrap('a %s word' % long_word, 11)
     23>>> '%s...%s' % (out[:20], out[-20:])
     24u'a  \nl012345678901234...9012345 \n6789ng word'
     25
     26>>> wrap('>test me out', 10)
     27u'>test me  \n> out'
     28>>> wrap('test me >out', 10)
     29u'test me  \n >out'
     30
     31>>> wrap('> one\n>> two which will wrap\n>>> three', 15)
     32u'> one\n>> two which  \n>> will wrap\n>>> three'
     33
     34>>> wrap('preserve  whitespace good  and\n  proper\n', 9)
     35u'preserve  \n  \nwhitespace \n good   \nand\n  proper\n'
     36
     37>>> wrap('do not touch -- \nreal signatures\n-- \nThe boss', 13)
     38u'do not touch  \n--\nreal  \nsignatures\n-- \nThe boss'
     39"""
  • tests/regressiontests/utils/text.py

     
     1r"""
     2>>> from django.utils.text import wrap
     3
     4>>> wrap('1234 67 9', 100)
     5u'1234 67 9'
     6>>> wrap('1234 67 9', 9)
     7u'1234 67 9'
     8>>> wrap('1234 67 9', 8)
     9u'1234 67\n9'
     10
     11>>> wrap('short\na long line', 7)
     12u'short\na long\nline'
     13
     14>>> wrap('do-not-break-long-words please? ok', 8)
     15u'do-not-break-long-words\nplease?\nok'
     16
     17>>> long_word = 'l%sng' % ('0123456789'*100)
     18>>> wrap(long_word, 50) == long_word
     19True
     20>>> out = wrap('a %s word' % long_word, 10)
     21>>> '%s...%s' % (out[:20], out[-20:])
     22u'a\nl01234567890123456...7890123456789ng\nword'
     23"""
Back to Top