diff options
Diffstat (limited to '')
-rw-r--r-- | README.md | 9 | ||||
-rwxr-xr-x | sendmail-test.py | 253 |
2 files changed, 260 insertions, 2 deletions
@@ -2,6 +2,12 @@ [Daily Bulletin Home Page](https://sj.ykps.net/sjdb/) +**Please note that any code presented herein, or any information present on the +Daily Buletin, does not represent the school in its official capacity. +Information provided herein are provided on a best-effort basis by students who +are otherwise unaffiliated with the school administration (and also have a very +busy academic life).** + Daily Bulletins are bulletin boards for students and staff at the Songjiang campus of [YK Pao School](https://ykpaoschool.cn), which are delivered by email every school day. They contain information such as @@ -9,8 +15,7 @@ itineraries notices, Daily Inspirations, exam schedules (if there are exam sessions ongoing), the daily menu, etc. This repository contains the source code of the modern Daily Bulletin's -build system. **It is a work in progress and isn't in production use -yet**. +build system. It does not contain the actual Daily Bulletins. ## Installation diff --git a/sendmail-test.py b/sendmail-test.py new file mode 100755 index 0000000..7d366fb --- /dev/null +++ b/sendmail-test.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# +# Send the Daily Bulletin the next morning +# Copyright (C) 2024 Runxi Yu <https://runxiyu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +from __future__ import annotations +from configparser import ConfigParser +from typing import Optional +from pprint import pprint +import datetime +import zoneinfo +import argparse +import os + +import requests +import msal # type: ignore + + +def acquire_token(app: msal.PublicClientApplication, config: ConfigParser) -> str: + result = app.acquire_token_by_username_password( + config["credentials"]["username"], + config["credentials"]["password"], + scopes=config["credentials"]["scope"].split(" "), + ) + + if "access_token" in result: + assert isinstance(result["access_token"], str) + return result["access_token"] + raise ValueError("Authentication error in password login") + + +def sendmail( + token: str, + subject: str, + body: str, + to: list[str], + bcc: list[str], + cc: list[str], + when: Optional[datetime.datetime] = None, + content_type: str = "HTML", + importance: str = "Normal", + reply_to: Optional[str] = None, +) -> str: + data = { + "subject": subject, + "importance": importance, + "body": {"contentType": content_type, "content": body}, + "toRecipients": [{"emailAddress": {"address": a}} for a in to], + "ccRecipients": [{"emailAddress": {"address": a}} for a in cc], + "bccRecipients": [{"emailAddress": {"address": a}} for a in bcc], + } + + if when is not None: + if when.tzinfo is None: + raise TypeError("Naive datetimes are no longer supported") + utcwhen = when.astimezone(datetime.timezone.utc) + isoval = utcwhen.isoformat(timespec="seconds").replace("+00:00", "Z") + data["singleValueExtendedProperties"] = [ + {"id": "SystemTime 0x3FEF", "value": isoval} + ] + + if not reply_to: + response = requests.post( + "https://graph.microsoft.com/v1.0/me/messages", + json=data, + headers={ + "Authorization": "Bearer %s" % token, + "Prefer": 'IdType="ImmutableId"', + }, + timeout=20, + ).json() + else: + response = requests.post( + "https://graph.microsoft.com/v1.0/me/messages/%s/createReply" % reply_to, + json=data, + headers={ + "Authorization": "Bearer %s" % token, + "Prefer": 'IdType="ImmutableId"', + }, + timeout=20, + ).json() + + try: + msgid = response["id"] + except KeyError: + pprint(response) + raise ValueError("Unable to add email to drafts") + + assert isinstance(msgid, str) + + response2 = requests.post( + "https://graph.microsoft.com/v1.0/me/messages/%s/send" % msgid, + headers={"Authorization": "Bearer " + token}, + timeout=20, + ) + + if response2.status_code != 202: + pprint(response2.content.decode("utf-8", "replace")) + raise ValueError( + "Graph response to messages/%s/send returned something other than 202 Accepted" + % response["id"], + ) + + return msgid + + +def main() -> None: + parser = argparse.ArgumentParser(description="Daily Bulletin Sender") + parser.add_argument( + "-d", + "--date", + default=None, + help="the date of the bulletin to send, in local time, in YYYY-MM-DD; defaults to tomorrow", + ) + parser.add_argument( + "-r", + "--reply", + action="store_true", + help="Reply to the previous bulletin when sending (BROKEN)", + ) + parser.add_argument( + "--config", default="config.ini", help="path to the configuration file" + ) + args = parser.parse_args() + config = ConfigParser() + config.read(args.config) + if args.date: + date = datetime.datetime.strptime(args.date, "%Y-%m-%d").replace( + tzinfo=zoneinfo.ZoneInfo(config["general"]["timezone"]) + ) + else: + date = datetime.datetime.now( + zoneinfo.ZoneInfo(config["general"]["timezone"]) + ) + datetime.timedelta(days=1) + + os.chdir(config["general"]["build_path"]) + + html_filename = "sjdb-%s.html" % date.strftime("%Y%m%d") + with open(html_filename, "r", encoding="utf-8") as html_fd: + html = html_fd.read() + + app = msal.PublicClientApplication( + config["credentials"]["client_id"], + authority=config["credentials"]["authority"], + ) + token = acquire_token(app, config) + + if not args.reply: + a = sendmail( + token, + subject=config["sendmail"]["subject_format"] + % date.strftime(config["sendmail"]["subject_date_format"]), + body=html, + to=config["sendmail"]["to_1"].split(" "), + cc=[], + bcc=["s22537@ykpaoschool.cn"], + when=date.replace( + hour=17, + minute=20, + second=0, + microsecond=0, + ), + content_type="HTML", + importance="Normal", + ) + assert a + with open("last-a.txt", "w") as fd: + fd.write(a) + b = sendmail( + token, + subject=config["sendmail"]["subject_format"] + % date.strftime(config["sendmail"]["subject_date_format"]), + body=html, + to=config["sendmail"]["to_2"].split(" "), + cc=[], + bcc=["s22537@ykpaoschool.cn"], + when=date.replace( + hour=17, + minute=20, + second=0, + microsecond=0, + ), + content_type="HTML", + importance="Normal", + ) + assert b + with open("last-b.txt", "w") as fd: + fd.write(b) + else: + with open("last-a.txt", "r") as fd: + last_a = fd.read().strip() + a = sendmail( + token, + subject=config["sendmail"]["subject_format"] + % date.strftime(config["sendmail"]["subject_date_format"]), + body=html, + to=config["sendmail"]["to_1"].split(" "), + cc=[], + bcc=["s22537@ykpaoschool.cn"], + when=date.replace( + hour=17, + minute=20, + second=0, + microsecond=0, + ), + content_type="HTML", + importance="Normal", + reply_to=last_a, + ) + assert a + with open("last-a.txt", "w") as fd: + fd.write(a) + with open("last-b.txt", "r") as fd: + last_b = fd.read().strip() + b = sendmail( + token, + subject=config["sendmail"]["subject_format"] + % date.strftime(config["sendmail"]["subject_date_format"]), + body=html, + to=config["sendmail"]["to_2"].split(" "), + cc=[], + bcc=["s22537@ykpaoschool.cn"], + when=date.replace( + hour=17, + minute=20, + second=0, + microsecond=0, + ), + content_type="HTML", + importance="Normal", + reply_to=last_b, + ) + assert b + with open("last-b.txt", "w") as fd: + fd.write(b) + + +if __name__ == "__main__": + main() |