aboutsummaryrefslogtreecommitdiff
path: root/sjdbmk/sendmail.py
diff options
context:
space:
mode:
Diffstat (limited to 'sjdbmk/sendmail.py')
-rw-r--r--sjdbmk/sendmail.py240
1 files changed, 240 insertions, 0 deletions
diff --git a/sjdbmk/sendmail.py b/sjdbmk/sendmail.py
new file mode 100644
index 0000000..35b1639
--- /dev/null
+++ b/sjdbmk/sendmail.py
@@ -0,0 +1,240 @@
+#!/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=config["sendmail"]["cc_1"].split(" "),
+ bcc=[w.strip() for w in open(config["sendmail"]["bcc_1_file"], "r").readlines() if w.strip()],
+ when=date.replace(
+ hour=int(config["sendmail"]["hour"]),
+ minute=int(config["sendmail"]["minute"]),
+ 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=config["sendmail"]["cc_2"].split(" "),
+ bcc=[w.strip() for w in open(config["sendmail"]["bcc_2_file"], "r").readlines() if w.strip()],
+ when=date.replace(
+ hour=int(config["sendmail"]["hour"]),
+ minute=int(config["sendmail"]["minute"]),
+ 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=config["sendmail"]["cc_1"].split(" "),
+ bcc=[w.strip() for w in open(config["sendmail"]["bcc_1_file"], "r").readlines() if w.strip()],
+ when=date.replace(
+ hour=int(config["sendmail"]["hour"]),
+ minute=int(config["sendmail"]["minute"]),
+ 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=config["sendmail"]["cc_2"].split(" "),
+ bcc=[w.strip() for w in open(config["sendmail"]["bcc_2_file"], "r").readlines() if w.strip()],
+ when=date.replace(
+ hour=int(config["sendmail"]["hour"]),
+ minute=int(config["sendmail"]["minute"]),
+ 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()