Index: django/core/mail.py
===================================================================
--- django/core/mail.py	(revision 7141)
+++ django/core/mail.py	(working copy)
@@ -4,6 +4,7 @@
 
 from django.conf import settings
 from django.utils.encoding import smart_str, force_unicode
+from django.utils.mail import wrap
 from email import Charset, Encoders
 from email.MIMEText import MIMEText
 from email.MIMEMultipart import MIMEMultipart
@@ -86,6 +87,20 @@
     return name, val
 
 class SafeMIMEText(MIMEText):
+    def __init__(self, _text, _subtype='plain', *args, **kwargs):
+        format_flowed = False
+        if 'wrap_text' in kwargs:
+            if kwargs.pop('wrap_text') and _subtype == 'plain':
+                _text = wrap(_text)
+                format_flowed = True
+        if 'format_flowed' in kwargs:
+            if kwargs.pop('format_flowed') and _subtype == 'plain':
+                format_flowed = True
+        MIMEText.__init__(self, _text, _subtype, *args, **kwargs)
+        if format_flowed:
+            self.set_param('format', 'flowed', 'Content-Type')
+            self.set_param('delsp', 'yes', 'Content-Type')
+
     def __setitem__(self, name, val):
         name, val = forbid_multi_line_headers(name, val)
         MIMEText.__setitem__(self, name, val)
@@ -190,7 +205,7 @@
     encoding = None     # None => use settings default
 
     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
-            connection=None, attachments=None, headers=None):
+            connection=None, attachments=None, headers=None, wrap_text=False):
         """
         Initialise a single email message (which can be sent to multiple
         recipients).
@@ -213,6 +228,7 @@
         self.attachments = attachments or []
         self.extra_headers = headers or {}
         self.connection = connection
+        self.wrap_text = wrap_text
 
     def get_connection(self, fail_silently=False):
         if not self.connection:
@@ -221,7 +237,9 @@
 
     def message(self):
         encoding = self.encoding or settings.DEFAULT_CHARSET
-        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET), self.content_subtype, encoding)
+        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
+                           self.content_subtype, encoding,
+                           wrap_text=self.wrap_text)
         if self.attachments:
             body_msg = msg
             msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
@@ -354,4 +372,3 @@
     EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
             settings.SERVER_EMAIL, [a[1] for a in
                 settings.MANAGERS]).send(fail_silently=fail_silently)
-
Index: django/utils/mail.py
===================================================================
--- django/utils/mail.py	(revision 0)
+++ django/utils/mail.py	(revision 0)
@@ -0,0 +1,56 @@
+import re
+from django.utils.encoding import force_unicode
+from django.utils.functional import allow_lazy
+
+def wrap(text, width=78):
+    """
+    An email based word-wrap function that preserves existing line breaks and
+    adds soft line breaks (' \n').
+
+    Long words are not wrapped, so the output text may have lines longer than
+    ``width``.
+
+    Trailing spaces are stripped from each line so they are not confused with
+    soft line breaks. All other white space is preserved.
+
+    Soft-broken lines are parsed for quotes and space-stuffed in accordance
+    with RFC 3676. A break is forced at 998 characters (RFC 2822 2.1.1).
+    """
+    text = force_unicode(text)
+    # Signature lines aren't stripped -- see RFC 3676 4.3
+    text = re.sub(r'(?m)(?<!^--) +$', '', text)
+    def _parse_line(line, quote, new_line):
+        if new_line and line.startswith('>'):
+            quote = re.match('>+', line).group()
+        elif quote:
+            line = '%s %s' % (quote, line)
+        elif (line.startswith('>') or line.startswith('From ')):
+            # Space stuff -- see RFC 3676 4.4
+            line = ' %s' % line
+        max_width = (line.endswith('\n') and width + 1 or width)
+        return line, quote, max_width
+    def _generator():
+        for line in text.splitlines(True):   # True keeps trailing linebreaks
+            quote = ''
+            line, quote, max_width = _parse_line(line, quote, new_line=True)
+            while len(line) > max_width:
+                space = line[:max_width].rfind(' ') + 1
+                if space == 0:
+                    space = line.find(' ') + 1
+                    space = min(space, 998)
+                    if space == 0:
+                        if len(line) > 998:
+                            space = 998
+                        else:
+                            yield line
+                            line = ''
+                            break
+                if space >= max_width:
+                    space -= 1
+                yield '%s \n' % line[:space]
+                line = line[space:]
+                line, quote, max_width = _parse_line(line, quote, False)
+            if line:
+                yield line
+    return u''.join(_generator())
+wrap = allow_lazy(wrap, unicode)
Index: django/utils/text.py
===================================================================
--- django/utils/text.py	(revision 7141)
+++ django/utils/text.py	(working copy)
@@ -10,29 +10,33 @@
 
 def wrap(text, width):
     """
-    A word-wrap function that preserves existing line breaks and most spaces in
-    the text. Expects that existing line breaks are posix newlines.
+    A word-wrap function that preserves existing line breaks. Expects that
+    existing line breaks are posix newlines.
+
+    All white space is preserved except added line breaks consume the space on
+    which they break the line.
+
+    Long words are not wrapped, so the output text may have lines longer than
+    ``width``.
     """
     text = force_unicode(text)
     def _generator():
-        it = iter(text.split(' '))
-        word = it.next()
-        yield word
-        pos = len(word) - word.rfind('\n') - 1
-        for word in it:
-            if "\n" in word:
-                lines = word.split('\n')
-            else:
-                lines = (word,)
-            pos += len(lines[0]) + 1
-            if pos > width:
-                yield '\n'
-                pos = len(lines[-1])
-            else:
-                yield ' '
-                if len(lines) > 1:
-                    pos = len(lines[-1])
-            yield word
+        for line in text.splitlines(True):   # True keeps trailing linebreaks
+            quote = ''
+            max_width = (line.endswith('\n') and width + 1 or width)
+            while len(line) > max_width:
+                space = line[:max_width+1].rfind(' ') + 1
+                if space == 0:
+                    space = line.find(' ') + 1
+                    if space == 0:
+                        yield line
+                        line = ''
+                        break
+                yield '%s\n' % line[:space-1]
+                line = line[space:]
+                max_width = (line.endswith('\n') and width + 1 or width)
+            if line:
+                yield line
     return u''.join(_generator())
 wrap = allow_lazy(wrap, unicode)
 
Index: tests/regressiontests/utils/tests.py
===================================================================
--- tests/regressiontests/utils/tests.py	(revision 7141)
+++ tests/regressiontests/utils/tests.py	(working copy)
@@ -8,11 +8,15 @@
 
 import timesince
 import datastructures
+import text
+import mail
 
 # Extra tests
 __test__ = {
     'timesince': timesince,
     'datastructures': datastructures,
+    'text': text,
+    'mail': mail,
 }
 
 class TestUtilsHtml(TestCase):
Index: tests/regressiontests/utils/mail.py
===================================================================
--- tests/regressiontests/utils/mail.py	(revision 0)
+++ tests/regressiontests/utils/mail.py	(revision 0)
@@ -0,0 +1,39 @@
+r"""
+>>> from django.utils.mail import wrap
+
+>>> wrap('1234 67 9', 100)
+u'1234 67 9'
+>>> wrap('1234 67 9', 9)
+u'1234 67 9'
+>>> wrap('1234 67 9', 8)
+u'1234 67 \n 9'
+
+>>> wrap('short\na long line', 7)
+u'short\na long \n line'
+
+>>> wrap('email wrapping: \nStrips spaces before lines and uses soft spacing for line breaks', 30)
+u'email wrapping:\nStrips spaces before lines  \nand uses soft spacing for  \nline breaks'
+
+>>> long_word = 'l%sng' % ('0123456789'*100)
+
+# Long word broken at 997 (998 - 1 char for space)
+>>> wrap(long_word) == '%s \n%s' % (long_word[:997], long_word[997:])
+True
+>>> out = wrap('a %s word' % long_word, 11)
+>>> '%s...%s' % (out[:20], out[-20:])
+u'a  \nl012345678901234...9012345 \n6789ng word'
+
+>>> wrap('>test me out', 10)
+u'>test me  \n> out'
+>>> wrap('test me >out', 10)
+u'test me  \n >out'
+
+>>> wrap('> one\n>> two which will wrap\n>>> three', 15)
+u'> one\n>> two which  \n>> will wrap\n>>> three'
+
+>>> wrap('preserve  whitespace good  and\n  proper\n', 9)
+u'preserve  \n  \nwhitespace \n good   \nand\n  proper\n'
+
+>>> wrap('do not touch -- \nreal signatures\n-- \nThe boss', 13)
+u'do not touch  \n--\nreal  \nsignatures\n-- \nThe boss'
+"""
Index: tests/regressiontests/utils/text.py
===================================================================
--- tests/regressiontests/utils/text.py	(revision 0)
+++ tests/regressiontests/utils/text.py	(revision 0)
@@ -0,0 +1,23 @@
+r"""
+>>> from django.utils.text import wrap
+
+>>> wrap('1234 67 9', 100)
+u'1234 67 9'
+>>> wrap('1234 67 9', 9)
+u'1234 67 9'
+>>> wrap('1234 67 9', 8)
+u'1234 67\n9'
+
+>>> wrap('short\na long line', 7)
+u'short\na long\nline'
+
+>>> wrap('do-not-break-long-words please? ok', 8)
+u'do-not-break-long-words\nplease?\nok'
+
+>>> long_word = 'l%sng' % ('0123456789'*100)
+>>> wrap(long_word, 50) == long_word
+True
+>>> out = wrap('a %s word' % long_word, 10)
+>>> '%s...%s' % (out[:20], out[-20:])
+u'a\nl01234567890123456...7890123456789ng\nword'
+"""
