Only create a 'From: author' line if needed
[stgit] / stgit / commands / mail.py
index 4864660..307a129 100644 (file)
@@ -15,7 +15,8 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os, re, time, datetime, smtplib, email.Utils
+import sys, os, re, time, datetime, smtplib
+import email, email.Utils, email.Header
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
@@ -36,10 +37,10 @@ generated from the template file passed as argument to '--template'
 can either be added to the template file or passed via the
 corresponding command line options.
 
 can either be added to the template file or passed via the
 corresponding command line options.
 
-A preamble e-mail can be sent using the '--cover' and/or '--edit'
-options. The first allows the user to specify a file to be used as a
-template. The latter option will invoke the editor on the specified
-file (defaulting to '.git/covermail.tmpl' or
+A preamble e-mail can be sent using the '--cover' and/or
+'--edit-cover' options. The first allows the user to specify a file to
+be used as a template. The latter option will invoke the editor on the
+specified file (defaulting to '.git/covermail.tmpl' or
 '~/.stgit/templates/covermail.tmpl' or
 '/usr/share/stgit/templates/covermail.tmpl').
 
 '~/.stgit/templates/covermail.tmpl' or
 '/usr/share/stgit/templates/covermail.tmpl').
 
@@ -51,33 +52,29 @@ SMTP authentication is also possible with '--smtp-user' and
 '--smtp-password' options, also available as configuration settings:
 'smtpuser' and 'smtppassword'.
 
 '--smtp-password' options, also available as configuration settings:
 'smtpuser' and 'smtppassword'.
 
-The template e-mail headers and body must be separated by
-'%(endofheaders)s' variable, which is replaced by StGIT with
-additional headers and a blank line. The patch e-mail template accepts
-the following variables:
+The patch e-mail template accepts the following variables:
 
   %(patch)s        - patch name
   %(maintainer)s   - 'authname <authemail>' as read from the config file
   %(shortdescr)s   - the first line of the patch description
   %(longdescr)s    - the rest of the patch description, after the first line
 
   %(patch)s        - patch name
   %(maintainer)s   - 'authname <authemail>' as read from the config file
   %(shortdescr)s   - the first line of the patch description
   %(longdescr)s    - the rest of the patch description, after the first line
-  %(endofheaders)s - delimiter between e-mail headers and body
   %(diff)s         - unified diff of the patch
   %(diffstat)s     - diff statistics
   %(diff)s         - unified diff of the patch
   %(diffstat)s     - diff statistics
-  %(date)s         - current date/time
   %(version)s      - ' version' string passed on the command line (or empty)
   %(prefix)s       - 'prefix ' string passed on the command line
   %(patchnr)s      - patch number
   %(totalnr)s      - total number of patches to be sent
   %(number)s       - empty if only one patch is sent or ' patchnr/totalnr'
   %(version)s      - ' version' string passed on the command line (or empty)
   %(prefix)s       - 'prefix ' string passed on the command line
   %(patchnr)s      - patch number
   %(totalnr)s      - total number of patches to be sent
   %(number)s       - empty if only one patch is sent or ' patchnr/totalnr'
+  %(fromauth)s     - 'From: author\\n\\n' if different from maintainer
   %(authname)s     - author's name
   %(authemail)s    - author's email
   %(authdate)s     - patch creation date
   %(commname)s     - committer's name
   %(commemail)s    - committer's e-mail
 
   %(authname)s     - author's name
   %(authemail)s    - author's email
   %(authdate)s     - patch creation date
   %(commname)s     - committer's name
   %(commemail)s    - committer's e-mail
 
-For the preamble e-mail template, only the %(maintainer)s, %(date)s,
-%(endofheaders)s, %(version)s, %(patchnr)s, %(totalnr)s and %(number)s
-variables are supported."""
+For the preamble e-mail template, only the %(maintainer)s,
+%(version)s, %(patchnr)s, %(totalnr)s and %(number)s variables are
+supported."""
 
 options = [make_option('-a', '--all',
                        help = 'e-mail all the applied patches',
 
 options = [make_option('-a', '--all',
                        help = 'e-mail all the applied patches',
@@ -105,9 +102,12 @@ options = [make_option('-a', '--all',
                        help = 'use FILE as the message template'),
            make_option('-c', '--cover', metavar = 'FILE',
                        help = 'send FILE as the cover message'),
                        help = 'use FILE as the message template'),
            make_option('-c', '--cover', metavar = 'FILE',
                        help = 'send FILE as the cover message'),
-           make_option('-e', '--edit',
+           make_option('-e', '--edit-cover',
                        help = 'edit the cover message before sending',
                        action = 'store_true'),
                        help = 'edit the cover message before sending',
                        action = 'store_true'),
+           make_option('-E', '--edit-patches',
+                       help = 'edit each patch before sending',
+                       action = 'store_true'),
            make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
                        help = 'sleep for SECONDS between e-mails sending'),
            make_option('--refid',
            make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
                        help = 'sleep for SECONDS between e-mails sending'),
            make_option('--refid',
@@ -181,42 +181,20 @@ def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
 
     s.quit()
 
 
     s.quit()
 
-def __write_mbox(from_addr, msg):
-    """Write an mbox like file to the standard output
-    """
-    r = re.compile('^From ', re.M)
-    msg = r.sub('>\g<0>', msg)
-
-    print 'From %s %s' % (from_addr, datetime.datetime.today().ctime())
-    print msg
-    print
-
-def __build_address_headers(tmpl, options, extra_cc = []):
+def __build_address_headers(msg, options, extra_cc = []):
     """Build the address headers and check existing headers in the
     template.
     """
     """Build the address headers and check existing headers in the
     template.
     """
-    def csv(lst):
-        s = ''
-        for i in lst:
-            if not i:
-                continue
-            if s:
-                s += ', ' + i
-            else:
-                s = i
-        return s
-
-    def replace_header(header, addr, tmpl):
-        r = re.compile('^' + header + ':\s+.+$', re.I | re.M)
-        if r.search(tmpl):
-            tmpl = r.sub('\g<0>, ' + addr, tmpl, 1)
-            h = ''
-        else:
-            h = header + ': ' + addr
+    def __replace_header(header, addr):
+        if addr:
+            crt_addr = msg[header]
+            del msg[header]
 
 
-        return tmpl, h
+            if crt_addr:
+                msg[header] = ', '.join([crt_addr, addr])
+            else:
+                msg[header] = addr
 
 
-    headers = ''
     to_addr = ''
     cc_addr = ''
     bcc_addr = ''
     to_addr = ''
     cc_addr = ''
     bcc_addr = ''
@@ -227,31 +205,19 @@ def __build_address_headers(tmpl, options, extra_cc = []):
         autobcc = ''
 
     if options.to:
         autobcc = ''
 
     if options.to:
-        to_addr = csv(options.to)
+        to_addr = ', '.join(options.to)
     if options.cc:
     if options.cc:
-        cc_addr = csv(options.cc + extra_cc)
+        cc_addr = ', '.join(options.cc + extra_cc)
     elif extra_cc:
     elif extra_cc:
-        cc_addr = csv(extra_cc)
+        cc_addr = ', '.join(extra_cc)
     if options.bcc:
     if options.bcc:
-        bcc_addr = csv(options.bcc + [autobcc])
+        bcc_addr = ', '.join(options.bcc + [autobcc])
     elif autobcc:
         bcc_addr = autobcc
 
     elif autobcc:
         bcc_addr = autobcc
 
-    # replace existing headers
-    if to_addr:
-        tmpl, h = replace_header('To', to_addr, tmpl)
-        if h:
-            headers += h + '\n'
-    if cc_addr:
-        tmpl, h = replace_header('Cc', cc_addr, tmpl)
-        if h:
-            headers += h + '\n'
-    if bcc_addr:
-        tmpl, h = replace_header('Bcc', bcc_addr, tmpl)
-        if h:
-            headers += h + '\n'
-
-    return tmpl, headers
+    __replace_header('To', to_addr)
+    __replace_header('Cc', cc_addr)
+    __replace_header('Bcc', bcc_addr)
 
 def __get_signers_list(msg):
     """Return the address list generated from signed-off-by and
 
 def __get_signers_list(msg):
     """Return the address list generated from signed-off-by and
@@ -267,14 +233,66 @@ def __get_signers_list(msg):
 
     return addr_list
 
 
     return addr_list
 
-def __build_extra_headers():
-    """Build extra headers like content-type etc.
+def __build_extra_headers(msg, msg_id, ref_id = None):
+    """Build extra email headers and encoding
     """
     """
-    headers  = 'Content-Type: text/plain; charset=utf-8; format=fixed\n'
-    headers += 'Content-Transfer-Encoding: 8bit\n'
-    headers += 'User-Agent: StGIT/%s\n' % version.version
+    del msg['Date']
+    msg['Date'] = email.Utils.formatdate(localtime = True)
+    msg['Message-ID'] = msg_id
+    if ref_id:
+        msg['In-Reply-To'] = ref_id
+        msg['References'] = ref_id
+    msg['User-Agent'] = 'StGIT/%s' % version.version
+
+def __encode_message(msg):
+    # 7 or 8 bit encoding
+    charset = email.Charset.Charset('utf-8')
+    charset.body_encoding = None
+
+    # encode headers
+    for header, value in msg.items():
+        words = []
+        for word in value.split(' '):
+            try:
+                uword = unicode(word, 'utf-8')
+            except UnicodeDecodeError:
+                # maybe we should try a different encoding or report
+                # the error. At the moment, we just ignore it
+                pass
+            words.append(email.Header.Header(uword).encode())
+        new_val = ' '.join(words)
+        msg.replace_header(header, new_val)
+
+    # encode the body and set the MIME and encoding headers
+    msg.set_charset(charset)
+
+def __edit_message(msg):
+    fname = '.stgitmail.txt'
+
+    # create the initial file
+    f = file(fname, 'w')
+    f.write(msg)
+    f.close()
+
+    # the editor
+    if config.has_option('stgit', 'editor'):
+        editor = config.get('stgit', 'editor')
+    elif 'EDITOR' in os.environ:
+        editor = os.environ['EDITOR']
+    else:
+        editor = 'vi'
+    editor += ' %s' % fname
+
+    print 'Invoking the editor: "%s"...' % editor,
+    sys.stdout.flush()
+    print 'done (exit code: %d)' % os.system(editor)
 
 
-    return headers
+    # read the message back
+    f = file(fname)
+    msg = f.read()
+    f.close()
+
+    return msg
 
 def __build_cover(tmpl, total_nr, msg_id, options):
     """Build the cover message (series description) to be sent via SMTP
 
 def __build_cover(tmpl, total_nr, msg_id, options):
     """Build the cover message (series description) to be sent via SMTP
@@ -283,13 +301,6 @@ def __build_cover(tmpl, total_nr, msg_id, options):
     if not maintainer:
         maintainer = ''
 
     if not maintainer:
         maintainer = ''
 
-    tmpl, headers_end = __build_address_headers(tmpl, options)
-    headers_end += 'Message-Id: %s\n' % msg_id
-    if options.refid:
-        headers_end += "In-Reply-To: %s\n" % options.refid
-        headers_end += "References: %s\n" % options.refid
-    headers_end += __build_extra_headers()
-
     if options.version:
         version_str = ' %s' % options.version
     else:
     if options.version:
         version_str = ' %s' % options.version
     else:
@@ -308,8 +319,10 @@ def __build_cover(tmpl, total_nr, msg_id, options):
         number_str = ''
 
     tmpl_dict = {'maintainer':   maintainer,
         number_str = ''
 
     tmpl_dict = {'maintainer':   maintainer,
-                 'endofheaders': headers_end,
-                 'date':         email.Utils.formatdate(localtime = True),
+                 # for backward template compatibility
+                 'endofheaders': '',
+                 # for backward template compatibility
+                 'date':         '',
                  'version':      version_str,
                  'prefix':      prefix_str,
                  'patchnr':      patch_nr_str,
                  'version':      version_str,
                  'prefix':      prefix_str,
                  'patchnr':      patch_nr_str,
@@ -317,7 +330,7 @@ def __build_cover(tmpl, total_nr, msg_id, options):
                  'number':       number_str}
 
     try:
                  'number':       number_str}
 
     try:
-        msg = tmpl % tmpl_dict
+        msg_string = tmpl % tmpl_dict
     except KeyError, err:
         raise CmdException, 'Unknown patch template variable: %s' \
               % err
     except KeyError, err:
         raise CmdException, 'Unknown patch template variable: %s' \
               % err
@@ -325,33 +338,22 @@ def __build_cover(tmpl, total_nr, msg_id, options):
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
-    if options.edit:
-        fname = '.stgitmail.txt'
+    if options.edit_cover:
+        msg_string = __edit_message(msg_string)
 
 
-        # create the initial file
-        f = file(fname, 'w+')
-        f.write(msg)
-        f.close()
-
-        # the editor
-        if config.has_option('stgit', 'editor'):
-            editor = config.get('stgit', 'editor')
-        elif 'EDITOR' in os.environ:
-            editor = os.environ['EDITOR']
-        else:
-            editor = 'vi'
-        editor += ' %s' % fname
+    # The Python email message
+    try:
+        msg = email.message_from_string(msg_string)
+    except Exception, ex:
+        raise CmdException, 'template parsing error: %s' % str(ex)
 
 
-        print 'Invoking the editor: "%s"...' % editor,
-        sys.stdout.flush()
-        print 'done (exit code: %d)' % os.system(editor)
+    __build_address_headers(msg, options)
+    __build_extra_headers(msg, msg_id, options.refid)
+    __encode_message(msg)
 
 
-        # read the message back
-        f = file(fname)
-        msg = f.read()
-        f.close()
+    msg_string = msg.as_string(options.mbox)
 
 
-    return msg.strip('\n')
+    return msg_string.strip('\n')
 
 def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     """Build the message to be sent via SMTP
 
 def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     """Build the message to be sent via SMTP
@@ -362,24 +364,22 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     descr_lines = descr.split('\n')
 
     short_descr = descr_lines[0].rstrip()
     descr_lines = descr.split('\n')
 
     short_descr = descr_lines[0].rstrip()
-    long_descr = reduce(lambda x, y: x + '\n' + y,
-                        descr_lines[1:], '').lstrip()
+    long_descr = '\n'.join(descr_lines[1:]).lstrip()
+
+    authname = p.get_authname();
+    authemail = p.get_authemail();
+    commname = p.get_commname();
+    commemail = p.get_commemail();
 
     maintainer = __get_maintainer()
     if not maintainer:
 
     maintainer = __get_maintainer()
     if not maintainer:
-        maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail())
+        maintainer = '%s <%s>' % (commname, commemail)
 
 
-    if options.auto:
-        extra_cc = __get_signers_list(descr)
+    fromauth = '%s <%s>' % (authname, authemail)
+    if fromauth != maintainer:
+        fromauth = 'From: %s\n\n' % fromauth
     else:
     else:
-        extra_cc = []
-
-    tmpl, headers_end = __build_address_headers(tmpl, options, extra_cc)
-    headers_end += 'Message-Id: %s\n' % msg_id
-    if ref_id:
-        headers_end += "In-Reply-To: %s\n" % ref_id
-        headers_end += "References: %s\n" % ref_id
-    headers_end += __build_extra_headers()
+        fromauth = ''
 
     if options.version:
         version_str = ' %s' % options.version
 
     if options.version:
         version_str = ' %s' % options.version
@@ -402,28 +402,32 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                  'maintainer':   maintainer,
                  'shortdescr':   short_descr,
                  'longdescr':    long_descr,
                  'maintainer':   maintainer,
                  'shortdescr':   short_descr,
                  'longdescr':    long_descr,
-                 'endofheaders': headers_end,
+                 # for backward template compatibility
+                 'endofheaders': '',
                  'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),
                                           rev2 = git_id('%s//top' % patch)),
                  'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),
                                               rev2 = git_id('%s//top' % patch)),
                  'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),
                                           rev2 = git_id('%s//top' % patch)),
                  'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),
                                               rev2 = git_id('%s//top' % patch)),
-                 'date':         email.Utils.formatdate(localtime = True),
+                 # for backward template compatibility
+                 'date':         '',
                  'version':      version_str,
                  'prefix':       prefix_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
                  'version':      version_str,
                  'prefix':       prefix_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
-                 'authname':     p.get_authname(),
-                 'authemail':    p.get_authemail(),
+                 'fromauth':     fromauth,
+                 'authname':     authname,
+                 'authemail':    authemail,
                  'authdate':     p.get_authdate(),
                  'authdate':     p.get_authdate(),
-                 'commname':     p.get_commname(),
-                 'commemail':    p.get_commemail()}
+                 'commname':     commname,
+                 'commemail':    commemail}
+    # change None to ''
     for key in tmpl_dict:
         if not tmpl_dict[key]:
             tmpl_dict[key] = ''
 
     try:
     for key in tmpl_dict:
         if not tmpl_dict[key]:
             tmpl_dict[key] = ''
 
     try:
-        msg = tmpl % tmpl_dict
+        msg_string = tmpl % tmpl_dict
     except KeyError, err:
         raise CmdException, 'Unknown patch template variable: %s' \
               % err
     except KeyError, err:
         raise CmdException, 'Unknown patch template variable: %s' \
               % err
@@ -431,7 +435,27 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
-    return msg.strip('\n')
+    if options.edit_patches:
+        msg_string = __edit_message(msg_string)
+
+    # The Python email message
+    try:
+        msg = email.message_from_string(msg_string)
+    except Exception, ex:
+        raise CmdException, 'template parsing error: %s' % str(ex)
+
+    if options.auto:
+        extra_cc = __get_signers_list(descr)
+    else:
+        extra_cc = []
+
+    __build_address_headers(msg, options, extra_cc)
+    __build_extra_headers(msg, msg_id, ref_id)
+    __encode_message(msg)
+
+    msg_string = msg.as_string(options.mbox)
+
+    return msg_string.strip('\n')
 
 def func(parser, options, args):
     """Send the patches by e-mail using the patchmail.tmpl file as
 
 def func(parser, options, args):
     """Send the patches by e-mail using the patchmail.tmpl file as
@@ -481,7 +505,7 @@ def func(parser, options, args):
         sleep = config.getint('stgit', 'smtpdelay')
 
     # send the cover message (if any)
         sleep = config.getint('stgit', 'smtpdelay')
 
     # send the cover message (if any)
-    if options.cover or options.edit:
+    if options.cover or options.edit_cover:
         # find the template file
         if options.cover:
             tmpl = file(options.cover).read()
         # find the template file
         if options.cover:
             tmpl = file(options.cover).read()
@@ -499,7 +523,8 @@ def func(parser, options, args):
             ref_id = msg_id
 
         if options.mbox:
             ref_id = msg_id
 
         if options.mbox:
-            __write_mbox(from_addr, msg)
+            print msg
+            print
         else:
             print 'Sending the cover message...',
             sys.stdout.flush()
         else:
             print 'Sending the cover message...',
             sys.stdout.flush()
@@ -526,7 +551,8 @@ def func(parser, options, args):
             ref_id = msg_id
 
         if options.mbox:
             ref_id = msg_id
 
         if options.mbox:
-            __write_mbox(from_addr, msg)
+            print msg
+            print
         else:
             print 'Sending patch "%s"...' % p,
             sys.stdout.flush()
         else:
             print 'Sending patch "%s"...' % p,
             sys.stdout.flush()