267 lines
8.2 KiB
Python
Executable File
267 lines
8.2 KiB
Python
Executable File
#! /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")
|
|
|
|
PARTICIPO_URL = "https://cwsvjudo.de/participo"
|
|
|
|
|
|
class Email:
|
|
title = None
|
|
mdText = None
|
|
|
|
def __init__(self, title, mdText):
|
|
self.title = title
|
|
self.mdText = mdText
|
|
|
|
def addApiKeyToUrls(self, apiKey, url=PARTICIPO_URL):
|
|
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)",
|
|
)
|
|
argParser.add_argument(
|
|
"-p",
|
|
"--participoUrl",
|
|
help="url for the participo app (e.g. indicator for appending the personal apiKey to the event url)",
|
|
default=PARTICIPO_URL,
|
|
)
|
|
|
|
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(argv.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.get("userId", user.get("id")),
|
|
"login",
|
|
endDate,
|
|
)
|
|
if apiKey is None:
|
|
logging.error(
|
|
f"Failed to create apiKey for user {user['id']} ({user['loginName']})"
|
|
)
|
|
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)}")
|