Exercise "stg pull" on patches just appending lines.
[stgit] / stgit / commands / mail.py
index d758b9c..470cf65 100644 (file)
@@ -15,18 +15,17 @@ 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, smtplib, email.Utils
+import sys, os, re, time, datetime, smtplib, email.Utils
 from optparse import OptionParser, make_option
 from optparse import OptionParser, make_option
-from time import gmtime, strftime
 
 from stgit.commands.common import *
 from stgit.utils import *
 
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import stack, git, basedir, version
 from stgit.config import config
 
 
 help = 'send a patch or series of patches by e-mail'
 from stgit.config import config
 
 
 help = 'send a patch or series of patches by e-mail'
-usage = """%prog [options] [<patch>]
+usage = """%prog [options] [<patch> [<patch2...]]
 
 Send a patch or a range of patches (defaulting to the applied patches)
 by e-mail using the 'smtpserver' configuration option. The From
 
 Send a patch or a range of patches (defaulting to the applied patches)
 by e-mail using the 'smtpserver' configuration option. The From
@@ -36,10 +35,15 @@ or /usr/share/stgit/templates/patchmail.tmpl). The To/Cc/Bcc addresses
 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 '--first' option. All the
-subsequent e-mails appear as replies to the first e-mail sent (either
-the preamble or the first patch). E-mails can be seen as replies to a
-different e-mail by using the '--refid' option.
+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
+/usr/share/stgit/templates/covermail.tmpl).
+
+All the subsequent e-mails appear as replies to the first e-mail sent
+(either the preamble or the first patch). E-mails can be seen as
+replies to a different e-mail by using the '--refid' option.
 
 SMTP authentication is also possible with '--smtp-user' and
 '--smtp-password' options, also available as configuration settings:
 
 SMTP authentication is also possible with '--smtp-user' and
 '--smtp-password' options, also available as configuration settings:
@@ -58,6 +62,7 @@ the following variables:
   %(diff)s         - unified diff of the patch
   %(diffstat)s     - diff statistics
   %(date)s         - current date/time
   %(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)
   %(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'
   %(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'
@@ -68,8 +73,8 @@ the following variables:
   %(commemail)s    - committer's e-mail
 
 For the preamble e-mail template, only the %(maintainer)s, %(date)s,
   %(commemail)s    - committer's e-mail
 
 For the preamble e-mail template, only the %(maintainer)s, %(date)s,
-%(endofheaders)s, %(patchnr)s, %(totalnr)s and %(number)s variables
-are supported."""
+%(endofheaders)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',
@@ -78,23 +83,36 @@ options = [make_option('-a', '--all',
                        metavar = '[PATCH1][:[PATCH2]]',
                        help = 'e-mail patches between PATCH1 and PATCH2'),
            make_option('--to',
                        metavar = '[PATCH1][:[PATCH2]]',
                        help = 'e-mail patches between PATCH1 and PATCH2'),
            make_option('--to',
-                       help = 'Add TO to the To: list'),
+                       help = 'add TO to the To: list',
+                       action = 'append'),
            make_option('--cc',
            make_option('--cc',
-                       help = 'Add CC to the Cc: list'),
+                       help = 'add CC to the Cc: list',
+                       action = 'append'),
            make_option('--bcc',
            make_option('--bcc',
-                       help = 'Add BCC to the Bcc: list'),
+                       help = 'add BCC to the Bcc: list',
+                       action = 'append'),
+           make_option('-v', '--version', metavar = 'VERSION',
+                       help = 'add VERSION to the [PATCH ...] prefix'),
            make_option('-t', '--template', metavar = 'FILE',
                        help = 'use FILE as the message template'),
            make_option('-t', '--template', metavar = 'FILE',
                        help = 'use FILE as the message template'),
-           make_option('-f', '--first', metavar = 'FILE',
-                       help = 'send FILE as the first message'),
+           make_option('-c', '--cover', metavar = 'FILE',
+                       help = 'send FILE as the cover message'),
+           make_option('-e', '--edit',
+                       help = 'edit the cover message 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',
-                       help = 'Use REFID as the reference id'),
+                       help = 'use REFID as the reference id'),
            make_option('-u', '--smtp-user', metavar = 'USER',
                        help = 'username for SMTP authentication'),
            make_option('-p', '--smtp-password', metavar = 'PASSWORD',
            make_option('-u', '--smtp-user', metavar = 'USER',
                        help = 'username for SMTP authentication'),
            make_option('-p', '--smtp-password', metavar = 'PASSWORD',
-                       help = 'username for SMTP authentication')]
+                       help = 'username for SMTP authentication'),
+           make_option('-b', '--branch',
+                       help = 'use BRANCH instead of the default one'),
+           make_option('-m', '--mbox',
+                       help = 'generate an mbox file instead of sending',
+                       action = 'store_true')]
 
 
 def __get_maintainer():
 
 
 def __get_maintainer():
@@ -108,21 +126,24 @@ def __get_maintainer():
     else:
         return None
 
     else:
         return None
 
-def __parse_addresses(string):
+def __parse_addresses(addresses):
     """Return a two elements tuple: (from, [to])
     """
     """Return a two elements tuple: (from, [to])
     """
-    def __addr_list(string):
-        return re.split('.*?([\w\.]+@[\w\.]+)', string)[1:-1:2]
+    def __addr_list(addrs):
+        m = re.search('[^@\s<,]+@[^>\s,]+', addrs);
+        if (m == None):
+            return []
+        return [ m.group() ] + __addr_list(addrs[m.end():])
 
     from_addr_list = []
     to_addr_list = []
 
     from_addr_list = []
     to_addr_list = []
-    for line in string.split('\n'):
+    for line in addresses.split('\n'):
         if re.match('from:\s+', line, re.I):
             from_addr_list += __addr_list(line)
         elif re.match('(to|cc|bcc):\s+', line, re.I):
             to_addr_list += __addr_list(line)
 
         if re.match('from:\s+', line, re.I):
             from_addr_list += __addr_list(line)
         elif re.match('(to|cc|bcc):\s+', line, re.I):
             to_addr_list += __addr_list(line)
 
-    if len(from_addr_list) != 1:
+    if len(from_addr_list) == 0:
         raise CmdException, 'No "From" address'
     if len(to_addr_list) == 0:
         raise CmdException, 'No "To/Cc/Bcc" addresses'
         raise CmdException, 'No "From" address'
     if len(to_addr_list) == 0:
         raise CmdException, 'No "To/Cc/Bcc" addresses'
@@ -152,21 +173,62 @@ def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
 
     s.quit()
 
 
     s.quit()
 
-def __build_first(tmpl, total_nr, msg_id, options):
-    """Build the first message (series description) to be sent via SMTP
+def __write_mbox(from_addr, msg):
+    """Write an mbox like file to the standard output
     """
     """
-    maintainer = __get_maintainer()
-    if not maintainer:
-        maintainer = ''
+    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(options):
     headers_end = ''
     if options.to:
     headers_end = ''
     if options.to:
-        headers_end += 'To: %s\n' % options.to
+        headers_end += 'To: '
+        for to in options.to:
+            headers_end += '%s, ' % to
+        headers_end = headers_end[:-2] + '\n'
     if options.cc:
     if options.cc:
-        headers_end += 'Cc: %s\n' % options.cc
+        headers_end += 'Cc: '
+        for cc in options.cc:
+            headers_end += '%s, ' % cc
+        headers_end = headers_end[:-2] + '\n'
     if options.bcc:
     if options.bcc:
-        headers_end += 'Bcc: %s\n' % options.bcc
+        headers_end += 'Bcc: '
+        for bcc in options.bcc:
+            headers_end += '%s, ' % bcc
+        headers_end = headers_end[:-2] + '\n'
+    return headers_end
+
+def __build_extra_headers():
+    """Build extra headers like content-type etc.
+    """
+    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
+
+    return headers
+
+def __build_cover(tmpl, total_nr, msg_id, options):
+    """Build the cover message (series description) to be sent via SMTP
+    """
+    maintainer = __get_maintainer()
+    if not maintainer:
+        maintainer = ''
+
+    headers_end = __build_address_headers(options)
     headers_end += 'Message-Id: %s\n' % msg_id
     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:
+        version_str = ''
 
     total_nr_str = str(total_nr)
     patch_nr_str = '0'.zfill(len(total_nr_str))
 
     total_nr_str = str(total_nr)
     patch_nr_str = '0'.zfill(len(total_nr_str))
@@ -178,6 +240,7 @@ def __build_first(tmpl, total_nr, msg_id, options):
     tmpl_dict = {'maintainer':   maintainer,
                  'endofheaders': headers_end,
                  'date':         email.Utils.formatdate(localtime = True),
     tmpl_dict = {'maintainer':   maintainer,
                  'endofheaders': headers_end,
                  'date':         email.Utils.formatdate(localtime = True),
+                 'version':      version_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str}
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str}
@@ -191,7 +254,33 @@ def __build_first(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'
 
-    return msg
+    if options.edit:
+        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)
+
+        # read the message back
+        f = file(fname)
+        msg = f.read()
+        f.close()
+
+    return msg.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
@@ -209,17 +298,17 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     if not maintainer:
         maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail())
 
     if not maintainer:
         maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail())
 
-    headers_end = ''
-    if options.to:
-        headers_end += 'To: %s\n' % options.to
-    if options.cc:
-        headers_end += 'Cc: %s\n' % options.cc
-    if options.bcc:
-        headers_end += 'Bcc: %s\n' % options.bcc
+    headers_end = __build_address_headers(options)
     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 += '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()
+
+    if options.version:
+        version_str = ' %s' % options.version
+    else:
+        version_str = ''
 
     total_nr_str = str(total_nr)
     patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
 
     total_nr_str = str(total_nr)
     patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
@@ -238,6 +327,7 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                  'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),
                                               rev2 = git_id('%s/top' % patch)),
                  'date':         email.Utils.formatdate(localtime = True),
                  'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),
                                               rev2 = git_id('%s/top' % patch)),
                  'date':         email.Utils.formatdate(localtime = True),
+                 'version':      version_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
@@ -259,17 +349,12 @@ 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
+    return msg.strip('\n')
 
 def func(parser, options, args):
     """Send the patches by e-mail using the patchmail.tmpl file as
     a template
     """
 
 def func(parser, options, args):
     """Send the patches by e-mail using the patchmail.tmpl file as
     a template
     """
-    if len(args) > 1:
-        parser.error('incorrect number of arguments')
-
-    if not config.has_option('stgit', 'smtpserver'):
-        raise CmdException, 'smtpserver not defined'
     smtpserver = config.get('stgit', 'smtpserver')
 
     smtpuser = None
     smtpserver = config.get('stgit', 'smtpserver')
 
     smtpuser = None
@@ -280,12 +365,15 @@ def func(parser, options, args):
         smtppassword = config.get('stgit', 'smtppassword')
 
     applied = crt_series.get_applied()
         smtppassword = config.get('stgit', 'smtppassword')
 
     applied = crt_series.get_applied()
-
-    if len(args) == 1:
-        if args[0] in applied:
-            patches = [args[0]]
-        else:
-            raise CmdException, 'Patch "%s" not applied' % args[0]
+    unapplied = crt_series.get_unapplied()
+
+    if len(args) >= 1:
+        for patch in args:
+            if patch in unapplied:
+                raise CmdException, 'Patch "%s" not applied' % patch
+            if not patch in applied:
+                raise CmdException, 'Patch "%s" does not exist' % patch
+        patches = args
     elif options.all:
         patches = applied
     elif options.range:
     elif options.all:
         patches = applied
     elif options.range:
@@ -308,11 +396,17 @@ def func(parser, options, args):
         if start in applied:
             start_idx = applied.index(start)
         else:
         if start in applied:
             start_idx = applied.index(start)
         else:
-            raise CmdException, 'Patch "%s" not applied' % start
+            if start in unapplied:
+                raise CmdException, 'Patch "%s" not applied' % start
+            else:
+                raise CmdException, 'Patch "%s" does not exist' % start
         if stop in applied:
             stop_idx = applied.index(stop) + 1
         else:
         if stop in applied:
             stop_idx = applied.index(stop) + 1
         else:
-            raise CmdException, 'Patch "%s" not applied' % stop
+            if stop in unapplied:
+                raise CmdException, 'Patch "%s" not applied' % stop
+            else:
+                raise CmdException, 'Patch "%s" does not exist' % stop
 
         if start_idx >= stop_idx:
             raise CmdException, 'Incorrect patch range order'
 
         if start_idx >= stop_idx:
             raise CmdException, 'Incorrect patch range order'
@@ -341,44 +435,56 @@ def func(parser, options, args):
     if options.sleep != None:
         sleep = options.sleep
     else:
     if options.sleep != None:
         sleep = options.sleep
     else:
-        sleep = 2
+        sleep = config.getint('stgit', 'smtpdelay')
 
 
-    # send the first message (if any)
-    if options.first:
-        tmpl = file(options.first).read()
+    # send the cover message (if any)
+    if options.cover or options.edit:
+        # find the template file
+        if options.cover:
+            tfile_list = [options.cover]
+        else:
+            tfile_list = [os.path.join(basedir.get(), 'covermail.tmpl'),
+                          os.path.join(sys.prefix,
+                                       'share/stgit/templates/covermail.tmpl')]
+
+        tmpl = None
+        for tfile in tfile_list:
+            if os.path.isfile(tfile):
+                tmpl = file(tfile).read()
+                break
+        if not tmpl:
+            raise CmdException, 'No cover message template file found'
 
         msg_id = email.Utils.make_msgid('stgit')
 
         msg_id = email.Utils.make_msgid('stgit')
-        msg = __build_first(tmpl, total_nr, msg_id, options)
+        msg = __build_cover(tmpl, total_nr, msg_id, options)
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
         ref_id = msg_id
 
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
         ref_id = msg_id
 
-        print 'Sending file "%s"...' % options.first,
-        sys.stdout.flush()
-
-        __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
-                       smtpuser, smtppassword)
-
-        print 'done'
+        if options.mbox:
+            __write_mbox(from_addr, msg)
+        else:
+            print 'Sending the cover message...',
+            sys.stdout.flush()
+            __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
+                           smtpuser, smtppassword)
+            print 'done'
 
     # send the patches
     if options.template:
         tfile_list = [options.template]
     else:
 
     # send the patches
     if options.template:
         tfile_list = [options.template]
     else:
-        tfile_list = []
-
-    tfile_list += [os.path.join(git.base_dir, 'patchmail.tmpl'),
-                   os.path.join(sys.prefix,
-                                'share/stgit/templates/patchmail.tmpl')]
+        tfile_list = [os.path.join(basedir.get(), 'patchmail.tmpl'),
+                      os.path.join(sys.prefix,
+                                   'share/stgit/templates/patchmail.tmpl')]
     tmpl = None
     for tfile in tfile_list:
         if os.path.isfile(tfile):
             tmpl = file(tfile).read()
             break
     if not tmpl:
     tmpl = None
     for tfile in tfile_list:
         if os.path.isfile(tfile):
             tmpl = file(tfile).read()
             break
     if not tmpl:
-        raise CmdException, 'No e-mail template file: %s or %s' \
-              % (tfile_list[-1], tfile_list[-2])
+        raise CmdException, 'No e-mail template file found'
 
     for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)):
         msg_id = email.Utils.make_msgid('stgit')
 
     for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)):
         msg_id = email.Utils.make_msgid('stgit')
@@ -390,10 +496,11 @@ def func(parser, options, args):
         if not ref_id:
             ref_id = msg_id
 
         if not ref_id:
             ref_id = msg_id
 
-        print 'Sending patch "%s"...' % p,
-        sys.stdout.flush()
-
-        __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
-                       smtpuser, smtppassword)
-
-        print 'done'
+        if options.mbox:
+            __write_mbox(from_addr, msg)
+        else:
+            print 'Sending patch "%s"...' % p,
+            sys.stdout.flush()
+            __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
+                           smtpuser, smtppassword)
+            print 'done'