#!/usr/bin/python3 import http.client import re from urllib.parse import urlparse, urljoin import smtplib from email.mime.text import MIMEText import os import sys import email.utils import argparse import logging import yaml from xdg.BaseDirectory import save_data_path from collections import namedtuple logger = logging.getLogger() # map of default config entries config_defaults = { 'sender': 'noreply@opensuse.org', 'to': 'factory@lists.opensuse.org', 'relay': 'relay.suse.de', 'changesfile': "Changes.{version}.txt", } def _load_config(handle = None): d = config_defaults y = yaml.safe_load(handle) if handle is not None else {} keys = set(d.keys()) | set(y.keys()) for key in keys: y[key] = y.get(key, d.get(key, None)) return y parser = argparse.ArgumentParser(description="Announce new snapshots") parser.add_argument("--dry", action="store_true", help="dry run") parser.add_argument("--debug", action="store_true", help="debug output") parser.add_argument("--verbose", action="store_true", help="verbose") parser.add_argument("--from", dest='sender', metavar="EMAIL", help="sender email address") parser.add_argument("--to", metavar="EMAIL", help="recepient email address") parser.add_argument("--relay", metavar="RELAY", help="SMTP relay server address") parser.add_argument("--version", metavar="VERSION", help="announce specific version") parser.add_argument("--config", metavar="FILE", type=argparse.FileType(), help="YAML config file to override defaults") parser.add_argument("--dump-config", action="store_true", help="dump built in YAML config") parser.add_argument("--state-file", metavar="STATE_FILE", help="Yaml config of previously announced", required=True) options = parser.parse_args() # Set logging configuration logging.basicConfig(level=logging.DEBUG if options.debug else logging.INFO, format='%(asctime)s - %(module)s:%(lineno)d - %(levelname)s - %(message)s') state = {} try: with open(options.state_file, 'r') as file: state = yaml.safe_load(file) if state is None: state = {} except IOError: pass config = _load_config(options.config) if options.sender: config['sender'] = options.sender if options.to: config['to'] = options.to if options.relay: config['relay'] = options.relay if options.dump_config: print(yaml.dump(config, default_flow_style=False)) sys.exit(0) if not config['sender'] or not config['to'] or not config['relay']: logger.error("need to specify --from and --to and --relay") sys.exit(1) if not options.version: u = urlparse(urljoin(config['url'], config['iso'])) conn = http.client.HTTPConnection(u.hostname, 80) conn.request('HEAD', u.path) res = conn.getresponse() if res.status != 302: raise Exception("http fail: %s %s" % (res.status, res.reason)) loc = res.getheader('location') if loc is None: raise Exception("empty location!") m = re.search(r'(?:Snapshot|Build)([\d.]+)-Media', loc) if m is None: raise Exception("failed to parse %s" % loc) version = m.group(1) logger.debug("found version %s", version) else: version = options.version if state.get(config['name'], None) == version: logger.info("version unchanged, exit") sys.exit(0) url = urljoin(config['url'], config['changesfile'].format(version=version)) # take the safer route url = url.replace('download.opensuse.org', 'downloadcontent.opensuse.org') u = urlparse(url) conn = http.client.HTTPConnection(u.hostname, 80) conn.request('HEAD', u.path) res = conn.getresponse() if res.status == 302: loc = res.getheader('location') if loc is None: raise Exception("empty location!") u = urlparse(loc) conn = http.client.HTTPConnection(u.hostname, 80) conn.request('GET', u.path) res = conn.getresponse() if res.status != 200: raise Exception("http %s fail: %s %s" % (u, res.status, res.reason)) txt = res.read().decode('latin1') if '====' not in txt: logger.error("no changes or file corrupt? not sending anything") sys.exit(1) msg = MIMEText(config['bodytemplate'].format(version=version, text=txt)) msg['Subject'] = config['subject'].format(version=version) msg['From'] = config['sender'] msg['To'] = config['to'] msg['Mail-Followup-To'] = config['to'] msg['Date'] = email.utils.formatdate(localtime=1) msg['Message-ID'] = email.utils.make_msgid() if options.dry: print("sending ...") print(msg.as_string()) else: logger.info("announcing version {}".format(version)) s = smtplib.SMTP(config['relay']) s.send_message(msg) s.quit() state[config['name']] = version with open(options.state_file, 'w') as file: yaml.dump(state, file)