Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
-import sys, os, re, time, datetime, smtplib
+import sys, os, re, time, datetime, socket, smtplib, getpass
import email, email.Utils, email.Header
from optparse import OptionParser, make_option
from stgit.commands.common import *
from stgit.utils import *
+from stgit.out import *
from stgit import stack, git, version, templates
from stgit.config import config
help = 'send a patch or series of patches by e-mail'
-usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+usage = r"""%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
-Send a patch or a range of patches by e-mail using the 'smtpserver'
-configuration option. The From address and the e-mail format are
-generated from the template file passed as argument to '--template'
-(defaulting to '.git/patchmail.tmpl' or
+Send a patch or a range of patches by e-mail using the SMTP server
+specified by the 'stgit.smtpserver' configuration option, or the
+'--smtp-server' command line option. The From address and the e-mail
+format are generated from the template file passed as argument to
+'--template' (defaulting to '.git/patchmail.tmpl' or
'~/.stgit/templates/patchmail.tmpl' or
-'/usr/share/stgit/templates/patchmail.tmpl').
+'/usr/share/stgit/templates/patchmail.tmpl'). A patch can be sent as
+attachment using the --attach option in which case the 'mailattch.tmpl'
+template will be used instead of 'patchmail.tmpl'.
The To/Cc/Bcc addresses can either be added to the template file or
passed via the corresponding command line options. They can be e-mail
SMTP authentication is also possible with '--smtp-user' and
'--smtp-password' options, also available as configuration settings:
-'smtpuser' and 'smtppassword'.
+'smtpuser' and 'smtppassword'. TLS encryption can be enabled by
+'--smtp-tls' option and 'smtptls' setting.
-The patch e-mail template accepts the following variables:
+The following variables are accepted by both the preamble and the
+patch e-mail templates:
- %(patch)s - patch name
- %(sender)s - 'sender' or 'authname <authemail>' as per the config file
- %(shortdescr)s - the first line of the patch description
- %(longdescr)s - the rest of the patch description, after the first line
- %(diff)s - unified diff of the patch
%(diffstat)s - diff statistics
- %(version)s - ' version' string passed on the command line (or empty)
- %(prefix)s - 'prefix ' string passed on the command line
+ %(number)s - empty if only one patch is sent or ' patchnr/totalnr'
%(patchnr)s - patch number
+ %(sender)s - 'sender' or 'authname <authemail>' as per the config file
%(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 sender
- %(authname)s - author's name
- %(authemail)s - author's email
+ %(version)s - ' version' string passed on the command line (or empty)
+
+In addition to the common variables, the preamble e-mail template
+accepts the following:
+
+ %(shortlog)s - first line of each patch description, listed by author
+
+In addition to the common variables, the patch e-mail template accepts
+the following:
+
%(authdate)s - patch creation date
- %(commname)s - committer's name
+ %(authemail)s - author's email
+ %(authname)s - author's name
%(commemail)s - committer's e-mail
+ %(commname)s - committer's name
+ %(diff)s - unified diff of the patch
+ %(fromauth)s - 'From: author\n\n' if different from sender
+ %(longdescr)s - the rest of the patch description, after the first line
+ %(patch)s - patch name
+ %(prefix)s - 'prefix ' string passed on the command line
+ %(shortdescr)s - the first line of the patch description"""
-For the preamble e-mail template, only the %(sender)s, %(version)s,
-%(patchnr)s, %(totalnr)s and %(number)s variables are supported."""
-
+directory = DirectoryHasRepository()
options = [make_option('-a', '--all',
help = 'e-mail all the applied patches',
action = 'store_true'),
make_option('--noreply',
help = 'do not send subsequent messages as replies',
action = 'store_true'),
+ make_option('--unrelated',
+ help = 'send patches without sequence numbering',
+ action = 'store_true'),
+ make_option('--attach',
+ help = 'send a patch as attachment',
+ action = 'store_true'),
make_option('-v', '--version', metavar = 'VERSION',
help = 'add VERSION to the [PATCH ...] prefix'),
make_option('--prefix', metavar = 'PREFIX',
help = 'sleep for SECONDS between e-mails sending'),
make_option('--refid',
help = 'use REFID as the reference id'),
+ make_option('--smtp-server', metavar = 'HOST[:PORT]',
+ help = 'SMTP server to use for sending mail'),
make_option('-u', '--smtp-user', metavar = 'USER',
help = 'username for SMTP authentication'),
make_option('-p', '--smtp-password', metavar = 'PASSWORD',
help = 'username for SMTP authentication'),
+ make_option('-T', '--smtp-tls',
+ help = 'use SMTP with TLS encryption',
+ action = 'store_true'),
make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
- make_option('--binary',
- help = 'output a diff even for binary files',
- action = 'store_true'),
make_option('-m', '--mbox',
help = 'generate an mbox file instead of sending',
- action = 'store_true')]
+ action = 'store_true')
+ ] + make_diff_opts_option()
def __get_sender():
return (from_addr_list[0], to_addr_list)
def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep,
- smtpuser, smtppassword):
+ smtpuser, smtppassword, use_tls):
"""Send the message using the given SMTP server
"""
try:
try:
if smtpuser and smtppassword:
s.ehlo()
+ if use_tls:
+ if not hasattr(socket, 'ssl'):
+ raise CmdException, "cannot use TLS - no SSL support in Python"
+ s.starttls()
+ s.ehlo()
s.login(smtpuser, smtppassword)
- s.sendmail(from_addr, to_addr_list, msg)
+ result = s.sendmail(from_addr, to_addr_list, msg)
+ if len(result):
+ print "mail server refused delivery for the following recipients: %s" % result
# give recipients a chance of receiving patches in the correct order
time.sleep(sleep)
except Exception, err:
to_addr = ', '.join(options.to)
if options.cc:
cc_addr = ', '.join(options.cc + extra_cc)
+ cc_addr = ', '.join(options.cc + extra_cc)
elif extra_cc:
cc_addr = ', '.join(extra_cc)
if options.bcc:
"""
addr_list = []
- r = re.compile('^(signed-off-by|acked-by):\s+(.+)$', re.I)
+ r = re.compile('^(signed-off-by|acked-by|cc):\s+(.+)$', re.I)
for line in msg.split('\n'):
m = r.match(line)
if m:
msg['Date'] = email.Utils.formatdate(localtime = True)
msg['Message-ID'] = msg_id
if ref_id:
+ # make sure the ref id has the angle brackets
+ ref_id = '<%s>' % ref_id.strip(' \t\n<>')
msg['In-Reply-To'] = ref_id
msg['References'] = ref_id
msg['User-Agent'] = 'StGIT/%s' % version.version
msg.replace_header(header, new_val)
# encode the body and set the MIME and encoding headers
- msg.set_charset(charset)
+ if msg.is_multipart():
+ for p in msg.get_payload():
+ p.set_charset(charset)
+ else:
+ msg.set_charset(charset)
def __edit_message(msg):
fname = '.stgitmail.txt'
return msg
-def __build_cover(tmpl, total_nr, msg_id, options):
+def __build_cover(tmpl, patches, msg_id, options):
"""Build the cover message (series description) to be sent via SMTP
"""
sender = __get_sender()
if options.prefix:
prefix_str = options.prefix + ' '
else:
- prefix_str = ''
+ confprefix = config.get('stgit.mail.prefix')
+ if confprefix:
+ prefix_str = confprefix + ' '
+ else:
+ prefix_str = ''
- total_nr_str = str(total_nr)
+ total_nr_str = str(len(patches))
patch_nr_str = '0'.zfill(len(total_nr_str))
- if total_nr > 1:
+ if len(patches) > 1:
number_str = ' %s/%s' % (patch_nr_str, total_nr_str)
else:
number_str = ''
'prefix': prefix_str,
'patchnr': patch_nr_str,
'totalnr': total_nr_str,
- 'number': number_str}
+ 'number': number_str,
+ 'shortlog': stack.shortlog(crt_series.get_patch(p)
+ for p in patches),
+ 'diffstat': git.diffstat(git.diff(
+ rev1 = git_id(crt_series, '%s//bottom' % patches[0]),
+ rev2 = git_id(crt_series, '%s//top' % patches[-1])))}
try:
msg_string = tmpl % tmpl_dict
"""
p = crt_series.get_patch(patch)
- descr = p.get_description().strip()
- descr_lines = descr.split('\n')
+ if p.get_description():
+ descr = p.get_description().strip()
+ else:
+ # provide a place holder and force the edit message option on
+ descr = '<empty message>'
+ options.edit_patches = True
+ descr_lines = descr.split('\n')
short_descr = descr_lines[0].rstrip()
long_descr = '\n'.join(descr_lines[1:]).lstrip()
if options.prefix:
prefix_str = options.prefix + ' '
else:
- prefix_str = ''
-
+ confprefix = config.get('stgit.mail.prefix')
+ if confprefix:
+ prefix_str = confprefix + ' '
+ else:
+ prefix_str = ''
+
total_nr_str = str(total_nr)
patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
- if total_nr > 1:
+ if not options.unrelated and total_nr > 1:
number_str = ' %s/%s' % (patch_nr_str, total_nr_str)
else:
number_str = ''
+ diff = git.diff(rev1 = git_id(crt_series, '%s//bottom' % patch),
+ rev2 = git_id(crt_series, '%s//top' % patch),
+ diff_flags = options.diff_flags)
tmpl_dict = {'patch': patch,
'sender': sender,
# for backward template compatibility
'longdescr': long_descr,
# for backward template compatibility
'endofheaders': '',
- 'diff': git.diff(rev1 = git_id('%s//bottom' % patch),
- rev2 = git_id('%s//top' % patch),
- binary = options.binary),
- 'diffstat': git.diffstat(rev1 = git_id('%s//bottom'%patch),
- rev2 = git_id('%s//top' % patch)),
+ 'diff': diff,
+ 'diffstat': git.diffstat(diff),
# for backward template compatibility
'date': '',
'version': version_str,
"""Send the patches by e-mail using the patchmail.tmpl file as
a template
"""
- smtpserver = config.get('stgit.smtpserver')
+ smtpserver = options.smtp_server or config.get('stgit.smtpserver')
applied = crt_series.get_applied()
else:
raise CmdException, 'Incorrect options. Unknown patches to send'
+ out.start('Checking the validity of the patches')
+ for p in patches:
+ if crt_series.empty_patch(p):
+ raise CmdException, 'Cannot send empty patch "%s"' % p
+ out.done()
+
smtppassword = options.smtp_password or config.get('stgit.smtppassword')
smtpuser = options.smtp_user or config.get('stgit.smtpuser')
+ smtpusetls = options.smtp_tls or config.get('stgit.smtptls') == 'yes'
if (smtppassword and not smtpuser):
raise CmdException, 'SMTP password supplied, username needed'
+ if (smtpusetls and not smtpuser):
+ raise CmdException, 'SMTP over TLS requested, username needed'
if (smtpuser and not smtppassword):
- raise CmdException, 'SMTP username supplied, password needed'
+ smtppassword = getpass.getpass("Please enter SMTP password: ")
total_nr = len(patches)
if total_nr == 0:
raise CmdException, 'No patches to send'
- if options.noreply:
- ref_id = None
- else:
+ if options.refid:
+ if options.noreply or options.unrelated:
+ raise CmdException, \
+ '--refid option not allowed with --noreply or --unrelated'
ref_id = options.refid
+ else:
+ ref_id = None
sleep = options.sleep or config.getint('stgit.smtpdelay')
# send the cover message (if any)
if options.cover or options.edit_cover:
+ if options.unrelated:
+ raise CmdException, 'cover sending not allowed with --unrelated'
+
# find the template file
if options.cover:
tmpl = file(options.cover).read()
raise CmdException, 'No cover message template file found'
msg_id = email.Utils.make_msgid('stgit')
- msg = __build_cover(tmpl, total_nr, msg_id, options)
+ msg = __build_cover(tmpl, patches, msg_id, options)
from_addr, to_addr_list = __parse_addresses(msg)
msg_string = msg.as_string(options.mbox)
ref_id = msg_id
if options.mbox:
- print msg_string
+ out.stdout_raw(msg_string + '\n')
else:
- print 'Sending the cover message...',
- sys.stdout.flush()
+ out.start('Sending the cover message')
__send_message(smtpserver, from_addr, to_addr_list, msg_string,
- sleep, smtpuser, smtppassword)
- print 'done'
+ sleep, smtpuser, smtppassword, smtpusetls)
+ out.done()
# send the patches
if options.template:
tmpl = file(options.template).read()
else:
- tmpl = templates.get_template('patchmail.tmpl')
+ if options.attach:
+ tmpl = templates.get_template('mailattch.tmpl')
+ else:
+ tmpl = templates.get_template('patchmail.tmpl')
if not tmpl:
raise CmdException, 'No e-mail template file found'
msg_string = msg.as_string(options.mbox)
# subsequent e-mails are seen as replies to the first one
- if not options.noreply and not ref_id:
+ if not options.noreply and not options.unrelated and not ref_id:
ref_id = msg_id
if options.mbox:
- print msg_string
+ out.stdout_raw(msg_string + '\n')
else:
- print 'Sending patch "%s"...' % p,
- sys.stdout.flush()
+ out.start('Sending patch "%s"' % p)
__send_message(smtpserver, from_addr, to_addr_list, msg_string,
- sleep, smtpuser, smtppassword)
- print 'done'
+ sleep, smtpuser, smtppassword, smtpusetls)
+ out.done()