#! /usr/bin/env python3 import json import smtplib import ssl from email import utils from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import pypandoc import argparse import certifi import os import requests import re import datetime import time import logging # only for develop endDate = (datetime.datetime.now() + datetime.timedelta(days=7)).strftime("%Y-%m-%d") participoUrl = "http://cwsvjudo.bplaced.net/participo" class Email: title = None mdText = None def __init__(self, title, mdText): self.title = title self.mdText = mdText def addApiKeyToUrls(self, apiKey, url=participoUrl): self.mdText = re.sub(url+"([^#,\n]*)", url + "\\1" + "?apiKey="+apiKey, self.mdText) @staticmethod def loadFromMdFile(path): from yaml import safe_load # read markdownfile as header and text mdHeader = None with open(argv.mdFilePath) as f: mdHeader = safe_load(get_yaml(f)) title = mdHeader['title'] if checkHeader( mdHeader) else "cwsvJudo newsLetter" mdText = None with open(argv.mdFilePath) as f: mdText = f.read() return Email(title, mdText) def get_yaml(f): '''Extracts the yamlHeader from a Markdown file''' yamlHeader = None pointer = f.tell() if f.readline() != '---\n': f.seek(pointer) return '' readline = iter(f.readline, '') readline = iter(readline.__next__, '---\n') yamlHeader = ''.join(readline) return yamlHeader def checkHeader(header): """check the header for validity useful, if the title was forgotten Args: header (dict): yamlHeader of the mdNewsletter Returns: bool: true if header is alright, false if an error was detected """ retVal = True if not 'title' in header: logging.info("Header has no 'title' attribute!") retVal = False else: if header['title'] is False: logging.info("Empty title!") retVal = False return retVal def getArguments(): argParser = argparse.ArgumentParser( description="Send an Markdown-File as eMail" ) argParser.add_argument( "mdFilePath", help="Path to MarkdownFile to send" ) argParser.add_argument( "-r", "--receiver", help="json file with the receiver") argParser.add_argument( "--dontSend", action="store_true" ) argParser.add_argument( "-c", "--config", nargs="+", type=argparse.FileType("r"), help="yaml formatted config file(s)" ) return argParser.parse_args() def loadFromJson(path): jsonDict = None with open(argv.receiver) as jsonFile: jsonDict = json.load(jsonFile) return jsonDict def loadFromYamlFile(yamlFile): from yaml import safe_load with yamlFile as f: return safe_load(yamlFile) def createApiKey(allowKey, userId, rights, endDate): """call the participo api to create a new api key Args: allowKey (string): api key that allows the creation apiKey-s userId (int): id of the receiver of the new api key rights (string): comma separated list of strings denoting the rights of the api key endDate (string): 'yyyy-mm-dd' formated string denoting the rights of the api key return: new api key or none on failure """ postData = { 'apiKey': allowKey, 'userId': userId, 'rights': rights, 'endDate': endDate } response = requests.post( participoUrl+"/api.apiKeys.add", json=postData) jsonResponse = response.json() return jsonResponse['apiKey'] if 'apiKey' else None def randomSleep(min=60, max=600): from random import randint from time import sleep sleepTime = randint(60, 600) logging.info(f"sleeping for {sleepTime} s") sleep(sleepTime) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) argv = getArguments() config = {} for yamlFile in argv.config: config |= loadFromYamlFile(yamlFile=yamlFile) receivers = loadFromJson(argv.receiver) emailTemplate = Email.loadFromMdFile(argv.mdFilePath) # messages are prepared emails ready to send messageQueue = [] for user in receivers: apiKey = createApiKey( config['apiKey']['createApiKey'], user['id'], "login", endDate) if apiKey is None: logging.info(f"Couldn't get apiKey for apiKey creation! Ok if no apiKey needs to be created.") continue email = Email.loadFromMdFile(argv.mdFilePath) # emailTemplate email.addApiKeyToUrls(apiKey) # Create the plain-text and HTML version of your message # text = pypandoc.convert_text(email.mdText, "plain", format='md', extra_args=[ # "--self-contained", "--resource-path=../aufgaben"]) # plain text did swallow the Url. As workaround we are trying markdown for the plain text text = pypandoc.convert_text(email.mdText, "markdown", format='md', extra_args=[ "--self-contained", "--resource-path=../aufgaben"]) html = pypandoc.convert_text(email.mdText, "html", format='md', extra_args=[ "--self-contained", "--resource-path=../aufgaben"]) # Turn these into plain/html MIMEText objects txtMimeText = MIMEText(text, "plain") htmlMimeText = MIMEText(html, "html") # @todo the message has to be recreated for every email address since it seems to be added by reference to the queue. meaning changing the emailAddress changes it in the previously added also for toAddress in user['eMail']: # create the mail message = MIMEMultipart("alternative") # Setting header data message["Subject"] = email.title message["From"] = config['senderEmailAddress'] message["Reply-To"] = config['senderEmailAddress'] message["Date"] = str(utils.formatdate(localtime=True)) # only set the to-header one time: setting it multiple # times results in a multiple to-entries in the header! # Meaning the mail has to be recreated for each to address. # @todo Find a way to reuse the created mail for every recipent message["To"] = toAddress # Add HTML/plain-text parts to MIMEMultipart message # The email client will try to render the last part first message.attach(htmlMimeText) message.attach(txtMimeText) messageQueue.append(message) # Create a secure SSL context context = ssl.create_default_context() # @todo This is a very bad hack, because the cert checking doesn't work anymore context.check_hostname = False context.verify_mode = ssl.CERT_NONE for message in messageQueue: # randomSleep(min=10, max=20) with smtplib.SMTP_SSL(config['smtp']['serverAddress'], config['smtp']['serverPort'], context=context) as server: server.login(config['smtp']['login'], config['smtp']['password']) if not argv.dontSend: logging.info(f"sending to {message['To']}") server.sendmail( from_addr=config['senderEmailAddress'], to_addrs=message["To"], msg=message.as_string() ) # except smtplib.SMTPSenderRefused as exception: # if exception.smtp_code == 450: # logging.warning(f"Daily Quota exceded. Try again in 1h.") # time.sleep(60 * 60) # server.login(config['smtp']['login'], config['smtp']['password']) # logging.error(f"Unexpected SMTPSenderRefused exception: {exception} - {repr(exception)}")