aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md9
-rwxr-xr-xsendmail-test.py253
2 files changed, 260 insertions, 2 deletions
diff --git a/README.md b/README.md
index ad1908f..03d3c77 100644
--- a/README.md
+++ b/README.md
@@ -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()