From 6a212b5524903c170d420763c9c08a5444745281 Mon Sep 17 00:00:00 2001 From: Konstantin Ryabitsev Date: Tue, 19 Oct 2021 17:34:17 -0400 Subject: Initial implementation of native mail sending I'm felling comfortable that "b4 ty" is sufficiently mature at this point to implement sending thank-yous directly. This is only the initial implementation that covers only the very basic parts of git's sendemail configuration options, but this should actually cover 90% of cases if not more. One important caveat -- I moved the "b4 ty -s" flag to be "b4 ty -t" in order to disambiguate it from the capital -S (that actually does the sending). Since "b4 ty" is still marked as an experimental feature, I feel we can do this without much impact. Signed-off-by: Konstantin Ryabitsev --- b4/__init__.py | 53 ++++++++++++++++++++++++++++++++ b4/command.py | 6 +++- b4/ty.py | 96 ++++++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 125 insertions(+), 30 deletions(-) diff --git a/b4/__init__.py b/b4/__init__.py index f8766d5..adbcf7d 100644 --- a/b4/__init__.py +++ b/b4/__init__.py @@ -130,6 +130,8 @@ DEFAULT_CONFIG = { # git-config for gpg.program, and if that's not set, # we'll use "gpg" and hope for the better 'gpgbin': None, + # When sending mail, use this sendemail identity configuration + 'sendemail-identity': None, } # This is where we store actual config @@ -2480,3 +2482,54 @@ def read_template(tptfile): continue tpt += line return tpt + + +def get_smtp(identity: Optional[str] = None): + import smtplib + if identity: + sconfig = get_config_from_git(rf'sendemail\.{identity}\..*') + sectname = f'sendemail.{identity}' + else: + sconfig = get_config_from_git(rf'sendemail\..*') + sectname = 'sendemail' + if not len(sconfig): + raise smtplib.SMTPException('Unable to find %s settings in any applicable git config' % sectname) + + # Limited support for smtp settings to begin with, but should cover the vast majority of cases + fromaddr = sconfig.get('from') + server = sconfig.get('smtpserver', 'localhost') + port = sconfig.get('smtpserverport', 0) + try: + port = int(port) + except ValueError: + raise smtplib.SMTPException('Invalid smtpport entry in %s' % sectname) + + encryption = sconfig.get('smtpencryption') + # We only authenticate if we have encryption + if encryption: + if encryption in ('tls', 'starttls'): + # We do startssl + smtp = smtplib.SMTP(server, port) + # Introduce ourselves + smtp.ehlo() + # Start encryption + smtp.starttls() + # Introduce ourselves again to get new criteria + smtp.ehlo() + elif encryption in ('ssl', 'smtps'): + # We do TLS from the get-go + smtp = smtplib.SMTP_SSL(server, port) + else: + raise smtplib.SMTPException('Unclear what to do with smtpencryption=%s' % encryption) + + # If we got to this point, we should do authentication. + auser = sconfig.get('smtpuser') + apass = sconfig.get('smtppass') + if auser and apass: + # Let any exceptions bubble up + smtp.login(auser, apass) + else: + # We assume you know what you're doing if you don't need encryption + smtp = smtplib.SMTP(server, port) + + return smtp, fromaddr diff --git a/b4/command.py b/b4/command.py index 199d0c2..9d364dd 100644 --- a/b4/command.py +++ b/b4/command.py @@ -199,7 +199,7 @@ def cmd(): help='Write thanks files into this dir (default=.)') sp_ty.add_argument('-l', '--list', action='store_true', default=False, help='List pull requests and patch series you have retrieved') - sp_ty.add_argument('-s', '--send', default=None, + sp_ty.add_argument('-t', '--thank-for', default=None, help='Generate thankyous for specific entries from -l (e.g.: 1,3-5,7-; or "all")') sp_ty.add_argument('-d', '--discard', default=None, help='Discard specific messages from -l (e.g.: 1,3-5,7-; or "all")') @@ -209,6 +209,10 @@ def cmd(): help='The branch to check against, instead of current') sp_ty.add_argument('--since', default='1.week', help='The --since option to use when auto-matching patches (default=1.week)') + sp_ty.add_argument('-S', '--send-email', action='store_true', dest='sendemail', default=False, + help='Send email instead of writing out .thanks files') + sp_ty.add_argument('--dry-run', action='store_true', dest='dryrun', default=False, + help='Print out emails instead of sending them') sp_ty.set_defaults(func=cmd_ty) # b4 diff diff --git a/b4/ty.py b/b4/ty.py index fbae20c..b790007 100644 --- a/b4/ty.py +++ b/b4/ty.py @@ -374,19 +374,30 @@ def auto_thankanator(cmdargs): sys.exit(0) logger.info('---') - send_messages(applied, cmdargs.gitdir, cmdargs.outdir, wantbranch, since=cmdargs.since) + send_messages(applied, wantbranch, cmdargs) sys.exit(0) -def send_messages(listing, gitdir, outdir, branch, since='1.week'): - # Not really sending, but writing them out to be sent on your own - # We'll probably gain ability to send these once the feature is - # more mature and we're less likely to mess things up - datadir = b4.get_data_dir() +def send_messages(listing, branch, cmdargs): logger.info('Generating %s thank-you letters', len(listing)) - # Check if the outdir exists and if it has any .thanks files in it - if not os.path.exists(outdir): - os.mkdir(outdir) + gitdir = cmdargs.gitdir + datadir = b4.get_data_dir() + fromaddr = None + if cmdargs.sendemail: + # See if we have sendemail-identity set + config = b4.get_main_config() + identity = config.get('sendemail-identity') + try: + smtp, fromaddr = b4.get_smtp(identity) + except Exception as ex: # noqa + logger.critical('Failed to configure the smtp connection:') + logger.critical(ex) + sys.exit(1) + else: + # We write .thanks notes + # Check if the outdir exists and if it has any .thanks files in it + if not os.path.exists(cmdargs.outdir): + os.mkdir(cmdargs.outdir) usercfg = b4.get_user_config() # Do we have a .signature file? @@ -399,10 +410,6 @@ def send_messages(listing, gitdir, outdir, branch, since='1.week'): outgoing = 0 for jsondata in listing: - slug_from = re.sub(r'\W', '_', jsondata['fromemail']) - slug_subj = re.sub(r'\W', '_', jsondata['subject']) - slug = '%s_%s' % (slug_from.lower(), slug_subj.lower()) - slug = re.sub(r'_+', '_', slug) jsondata['myname'] = usercfg['name'] jsondata['myemail'] = usercfg['email'] jsondata['signature'] = signature @@ -411,29 +418,60 @@ def send_messages(listing, gitdir, outdir, branch, since='1.week'): msg = generate_pr_thanks(gitdir, jsondata, branch) else: # This is a patch series - msg = generate_am_thanks(gitdir, jsondata, branch, since) + msg = generate_am_thanks(gitdir, jsondata, branch, cmdargs.since) if msg is None: continue outgoing += 1 - outfile = os.path.join(outdir, '%s.thanks' % slug) - logger.info(' Writing: %s', outfile) msg.set_charset('utf-8') msg.replace_header('Content-Transfer-Encoding', '8bit') - with open(outfile, 'w') as fh: - fh.write(msg.as_string(policy=b4.emlpolicy)) - logger.debug('Cleaning up: %s', jsondata['trackfile']) - fullpath = os.path.join(datadir, jsondata['trackfile']) - os.rename(fullpath, '%s.sent' % fullpath) + if cmdargs.sendemail: + if not fromaddr: + fromaddr = jsondata['myemail'] + if cmdargs.dryrun: + logger.info('--- DRYRUN: message follows ---') + emldata = msg.as_string(policy=b4.emlpolicy) + logger.info('\t' + emldata.replace('\n', '\n\t')) + logger.info('--- DRYRUN: message ends ---') + else: + alldests = email.utils.getaddresses([str(x) for x in msg.get_all('to', [])]) + alldests += email.utils.getaddresses([str(x) for x in msg.get_all('cc', [])]) + sendto = {x[1] for x in alldests} + logger.info('Sending: %s', msg.get('subject')) + mypolicy = email.policy.EmailPolicy(utf8=True, cte_type='8bit') + smtp.sendmail(fromaddr, sendto, msg.as_string(policy=mypolicy)) # noqa + else: + slug_from = re.sub(r'\W', '_', jsondata['fromemail']) + slug_subj = re.sub(r'\W', '_', jsondata['subject']) + slug = '%s_%s' % (slug_from.lower(), slug_subj.lower()) + slug = re.sub(r'_+', '_', slug) + outfile = os.path.join(cmdargs.outdir, '%s.thanks' % slug) + logger.info(' Writing: %s', outfile) + with open(outfile, 'w') as fh: + fh.write(msg.as_string(policy=b4.emlpolicy)) + if cmdargs.dryrun: + logger.info('Dry run, preserving tracked series.') + else: + logger.debug('Cleaning up: %s', jsondata['trackfile']) + fullpath = os.path.join(datadir, jsondata['trackfile']) + os.rename(fullpath, '%s.sent' % fullpath) + logger.info('---') if not outgoing: logger.info('No thanks necessary.') return - logger.debug('Wrote %s thank-you letters', outgoing) - logger.info('You can now run:') - logger.info(' git send-email %s/*.thanks', outdir) + if cmdargs.sendemail: + if cmdargs.dryrun: + logger.info('DRYRUN: generated %s thank-you letters', outgoing) + else: + logger.info('Sent %s thank-you letters', outgoing) + smtp.quit() + else: + logger.debug('Wrote %s thank-you letters', outgoing) + logger.info('You can now run:') + logger.info(' git send-email %s/*.thanks', cmdargs.outdir) def list_tracked(): @@ -465,7 +503,7 @@ def write_tracked(tracked): counter += 1 -def send_selected(cmdargs): +def thank_selected(cmdargs): tracked = list_tracked() if not len(tracked): logger.info('Nothing to do') @@ -494,7 +532,7 @@ def send_selected(cmdargs): sys.exit(0) wantbranch = get_wanted_branch(cmdargs) - send_messages(listing, cmdargs.gitdir, cmdargs.outdir, wantbranch, cmdargs.since) + send_messages(listing, wantbranch, cmdargs) sys.exit(0) @@ -621,9 +659,9 @@ def main(cmdargs): if cmdargs.auto: check_stale_thanks(cmdargs.outdir) auto_thankanator(cmdargs) - elif cmdargs.send: + elif cmdargs.thank: check_stale_thanks(cmdargs.outdir) - send_selected(cmdargs) + thank_selected(cmdargs) elif cmdargs.discard: discard_selected(cmdargs) else: @@ -634,4 +672,4 @@ def main(cmdargs): write_tracked(tracked) logger.info('---') logger.info('You can send them using number ranges, e.g:') - logger.info(' b4 ty -s 1-3,5,7-') + logger.info(' b4 ty -t 1-3,5,7-') -- cgit v1.2.3