[PATCH v4] sb: Merge mailer changes from rtems-tools
Chris Johns
chrisj at rtems.org
Thu May 27 00:39:10 UTC 2021
On 12/5/21 3:19 am, Alex White wrote:
> This adds the improved mailer.py script from rtems-tools.
>
> Closes #4388
> ---
> source-builder/sb/mailer.py | 194 ++++++++++++++++++++++++++------
> source-builder/sb/options.py | 26 ++++-
> source-builder/sb/setbuilder.py | 2 +
> 3 files changed, 189 insertions(+), 33 deletions(-)
>
> diff --git a/source-builder/sb/mailer.py b/source-builder/sb/mailer.py
> index ff25df5..aafe6d6 100644
> --- a/source-builder/sb/mailer.py
> +++ b/source-builder/sb/mailer.py
> @@ -1,21 +1,33 @@
> #
> # RTEMS Tools Project (http://www.rtems.org/)
> -# Copyright 2013 Chris Johns (chrisj at rtems.org)
> +# Copyright 2013-2016 Chris Johns (chrisj at rtems.org)
> +# Copyright (C) 2021 On-Line Applications Research Corporation (OAR)
> # All rights reserved.
> #
> # This file is part of the RTEMS Tools package in 'rtems-tools'.
> #
> -# Permission to use, copy, modify, and/or distribute this software for any
> -# purpose with or without fee is hereby granted, provided that the above
> -# copyright notice and this permission notice appear in all copies.
> +# Redistribution and use in source and binary forms, with or without
> +# modification, are permitted provided that the following conditions are met:
> +#
> +# 1. Redistributions of source code must retain the above copyright notice,
> +# this list of conditions and the following disclaimer.
> +#
> +# 2. Redistributions in binary form must reproduce the above copyright notice,
> +# this list of conditions and the following disclaimer in the documentation
> +# and/or other materials provided with the distribution.
> +#
> +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
> +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
> +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
> +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
> +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
> +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
> +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
> +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
> +# POSSIBILITY OF SUCH DAMAGE.
> #
> -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
> -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
>
> #
> # Manage emailing results or reports.
> @@ -28,18 +40,72 @@ import smtplib
> import socket
>
> from . import error
> +from . import execute
> from . import options
> from . import path
>
> +_options = {
> + '--mail' : 'Send email report or results.',
> + '--use-gitconfig': 'Use mail configuration from git config.',
> + '--mail-to' : 'Email address to send the email to.',
> + '--mail-from' : 'Email address the report is from.',
> + '--smtp-host' : 'SMTP host to send via.',
> + '--smtp-port' : 'SMTP port to send via.',
> + '--smtp-user' : 'User for SMTP authentication.',
> + '--smtp-password': 'Password for SMTP authentication.'
> +}
> +
> def append_options(opts):
> - opts['--mail'] = 'Send email report or results.'
> - opts['--smtp-host'] = 'SMTP host to send via.'
> - opts['--mail-to'] = 'Email address to send the email too.'
> - opts['--mail-from'] = 'Email address the report is from.'
> + for o in _options:
> + opts[o] = _options[o]
> +
> +def add_arguments(argsp):
> + argsp.add_argument('--mail', help = _options['--mail'], action = 'store_true')
> + argsp.add_argument('--use-gitconfig', help = _options['--use-gitconfig'], action = 'store_true')
> + no_add = ['--mail', '--use-gitconfig']
> + for o in [opt for opt in list(_options) if opt not in no_add]:
> + argsp.add_argument(o, help = _options[o], type = str)
>
> class mail:
> def __init__(self, opts):
> self.opts = opts
> + self.gitconfig_lines = None
> + if opts.find_arg('--use-gitconfig') is not None:
> + # Read the output of `git config --list` instead of reading the
> + # .gitconfig file directly because Python 2 ConfigParser does not
> + # accept tabs at the beginning of lines.
> + e = execute.capture_execution()
> + exit_code, proc, output = e.open('git config --list', shell=True)
> + if exit_code == 0:
> + self.gitconfig_lines = output.split(os.linesep)
> +
> + def _args_are_macros(self):
> + return isinstance(self.opts, options.command_line)
> +
> + def _get_arg(self, arg):
> + if self._args_are_macros():
> + value = self.opts.find_arg(arg)
> + if value is not None:
> + value = self.opts.find_arg(arg)[1]
> + else:
> + if arg.startswith('--'):
> + arg = arg[2:]
> + arg = arg.replace('-', '_')
> + if arg in vars(self.opts):
> + value = vars(self.opts)[arg]
> + else:
> + value = None
> + return value
> +
> + def _get_from_gitconfig(self, variable_name):
> + if self.gitconfig_lines is None:
> + return None
> +
> + for line in self.gitconfig_lines:
> + if line.startswith(variable_name):
> + ls = line.split('=')
> + if len(ls) >= 2:
> + return ls[1]
>
> def from_address(self):
>
> @@ -52,9 +118,15 @@ class mail:
> l = l[:l.index('\n')]
> return l.strip()
>
> - addr = self.opts.get_arg('--mail-from')
> + addr = self._get_arg('--mail-from')
> if addr is not None:
> - return addr[1]
> + return addr
> + addr = self._get_from_gitconfig('user.email')
> + if addr is not None:
> + name = self._get_from_gitconfig('user.name')
> + if name is not None:
> + addr = '%s <%s>' % (name, addr)
> + return addr
> mailrc = None
> if 'MAILRC' in os.environ:
> mailrc = os.environ['MAILRC']
> @@ -63,9 +135,8 @@ class mail:
> if mailrc is not None and path.exists(mailrc):
> # set from="Joe Blow <joe at blow.org>"
> try:
> - mrc = open(mailrc, 'r')
> - lines = mrc.readlines()
> - mrc.close()
> + with open(mailrc, 'r') as mrc:
> + lines = mrc.readlines()
> except IOError as err:
> raise error.general('error reading: %s' % (mailrc))
> for l in lines:
> @@ -76,40 +147,99 @@ class mail:
> addr = fa[fa.index('=') + 1:].replace('"', ' ').strip()
> if addr is not None:
> return addr
> - addr = self.opts.defaults.get_value('%{_sbgit_mail}')
> + if self._args_are_macros():
> + addr = self.opts.defaults.get_value('%{_sbgit_mail}')
> + else:
> + raise error.general('no valid from address for mail')
> return addr
>
> def smtp_host(self):
> - host = self.opts.get_arg('--smtp-host')
> + host = self._get_arg('--smtp-host')
> if host is not None:
> - return host[1]
> - host = self.opts.defaults.get_value('%{_mail_smtp_host}')
> + return host
> + host = self._get_from_gitconfig('sendemail.smtpserver')
> + if host is not None:
> + return host
> + if self._args_are_macros():
> + host = self.opts.defaults.get_value('%{_mail_smtp_host}')
> if host is not None:
> return host
> return 'localhost'
>
> + def smtp_port(self):
> + port = self._get_arg('--smtp-port')
> + if port is not None:
> + return port
> + port = self._get_from_gitconfig('sendemail.smtpserverport')
> + if port is not None:
> + return port
> + if self._args_are_macros():
> + port = self.opts.defaults.get_value('%{_mail_smtp_port}')
> + return port
> +
> + def smtp_user(self):
> + user = self._get_arg('--smtp-user')
> + if user is not None:
> + return user
> + user = self._get_from_gitconfig('sendemail.smtpuser')
> + return user
> +
> + def smtp_password(self):
> + password = self._get_arg('--smtp-password')
> + if password is not None:
> + return password
> + password = self._get_from_gitconfig('sendemail.smtppass')
> + return password
> +
> def send(self, to_addr, subject, body):
> from_addr = self.from_address()
> msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n" % \
> (from_addr, to_addr, subject) + body
> - if type(to_addr) is str:
> - to_addr = to_addr.split(',')
> - if type(to_addr) is not list:
> - raise error.general('invalid to_addr type')
> + port = self.smtp_port()
> +
> try:
> - s = smtplib.SMTP(self.smtp_host())
> - s.sendmail(from_addr, to_addr, msg)
> + s = smtplib.SMTP(self.smtp_host(), port, timeout=10)
> +
> + password = self.smtp_password()
> + # If a password is provided, assume that authentication is required.
> + if password is not None:
> + user = self.smtp_user()
> + if user is None:
> + user = from_addr
> + s.starttls()
> + s.login(user, password)
> +
> + s.sendmail(from_addr, [to_addr], msg)
> except smtplib.SMTPException as se:
> raise error.general('sending mail: %s' % (str(se)))
> except socket.error as se:
> raise error.general('sending mail: %s' % (str(se)))
>
> + def send_file_as_body(self, to_addr, subject, name, intro = None):
> + try:
> + with open(name, 'r') as f:
> + body = f.readlines()
> + except IOError as err:
> + raise error.general('error reading mail body: %s' % (name))
> + if intro is not None:
> + body = intro + body
> + self.send(to_addr, from_addr, body)
What is this call for?
Chris
> +
> if __name__ == '__main__':
> import sys
> + from . import macros
> optargs = {}
> + rtdir = 'source-builder'
> + defaults = '%s/defaults.mc' % (rtdir)
> append_options(optargs)
> - opts = options.load(sys.argv, optargs = optargs, defaults = 'defaults.mc')
> + opts = options.command_line(base_path = '.',
> + argv = sys.argv,
> + optargs = optargs,
> + defaults = macros.macros(name = defaults, rtdir = rtdir),
> + command_path = '.')
> + options.load(opts)
> m = mail(opts)
> print('From: %s' % (m.from_address()))
> print('SMTP Host: %s' % (m.smtp_host()))
> - m.send(m.from_address(), 'Test mailer.py', 'This is a test')
> + if '--mail' in sys.argv:
> + m.send(m.from_address(), 'Test mailer.py', 'This is a test')
> diff --git a/source-builder/sb/options.py b/source-builder/sb/options.py
> index d6bffd0..a0f196b 100644
> --- a/source-builder/sb/options.py
> +++ b/source-builder/sb/options.py
> @@ -517,6 +517,15 @@ class command_line:
> return None
> return self.parse_args(arg)
>
> + def find_arg(self, arg):
> + if self.optargs is None or arg not in self.optargs:
> + raise error.internal('bad arg: %s' % (arg))
> + for a in self.args:
> + sa = a.split('=')
> + if sa[0].startswith(arg):
> + return sa
> + return None
> +
> def with_arg(self, label, default = 'not-found'):
> # the default if there is no option for without.
> result = default
> @@ -582,7 +591,22 @@ class command_line:
> self.opts['no-install'] = '1'
>
> def info(self):
> - s = ' Command Line: %s%s' % (' '.join(self.argv), os.linesep)
> + # Filter potentially sensitive mail options out.
> + filtered_args = [
> + arg for arg in self.argv
> + if all(
> + smtp_opt not in arg
> + for smtp_opt in [
> + '--smtp-host',
> + '--mail-to',
> + '--mail-from',
> + '--smtp-user',
> + '--smtp-password',
> + '--smtp-port'
> + ]
> + )
> + ]
> + s = ' Command Line: %s%s' % (' '.join(filtered_args), os.linesep)
> s += ' Python: %s' % (sys.version.replace('\n', ''))
> return s
>
> diff --git a/source-builder/sb/setbuilder.py b/source-builder/sb/setbuilder.py
> index b0e2b23..c8c8fee 100644
> --- a/source-builder/sb/setbuilder.py
> +++ b/source-builder/sb/setbuilder.py
> @@ -695,6 +695,8 @@ def run():
> 'log' : '',
> 'reports': [],
> 'failure': None }
> + # Request this now to generate any errors.
> + smtp_host = mail['mail'].smtp_host()
> to_addr = opts.get_arg('--mail-to')
> if to_addr is not None:
> mail['to'] = to_addr[1]
>
More information about the devel
mailing list