diff options
Diffstat (limited to 'sendmail2.py')
-rw-r--r-- | sendmail2.py | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/sendmail2.py b/sendmail2.py new file mode 100644 index 0000000..6cfda8c --- /dev/null +++ b/sendmail2.py @@ -0,0 +1,161 @@ +#!/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/>. +# +# **TODO:** Send MIME rather than JSON +# + +from __future__ import annotations +import os +import datetime +import zoneinfo +import argparse +from configparser import ConfigParser +from typing import Optional +import msal # type: ignore +import requests + + +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", +) -> None: + 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} + ] + + response = requests.post( + "https://graph.microsoft.com/v1.0/me/messages", + json=data, + headers={"Authorization": "Bearer " + token}, + timeout=20, + ).json() + response2 = requests.post( + "https://graph.microsoft.com/v1.0/me/messages/%s/send" % response["id"], + headers={"Authorization": "Bearer " + token}, + timeout=20, + ) + if response2.status_code != 202: + print(response2.content) + raise ValueError( + "Graph response to messages/%s/send returned something other than 202 Accepted" + % response["id"], + response2, + ) + # TODO: Handle more errors + + +def main() -> None: + parser = argparse.ArgumentParser(description="Daily Bulletin Sender") + parser.add_argument( + "--date", + default=None, + help="the date of the bulletin to send, in local time, in YYYY-MM-DD; defaults to tomorrow", + ) + 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) + + common = { + "when": date.replace( + hour=int(config["sendmail"]["hour"]), + minute=int(config["sendmail"]["minute"]), + second=0, + microsecond=0, + ), + "content_type": "HTML", + "importance": "Normal", + "subject": config["sendmail"]["subject_format"] + % date.strftime(config["sendmail"]["subject_date_format"]), + "body": html, + } + + sendmail( + token, + to=config["sendmail"]["to_1"].split(" "), + cc=config["sendmail"]["cc_1"].split(" "), + bcc=config["sendmail"]["bcc_1"].split(" "), + **common, # type: ignore + ) + sendmail( + token, + to=config["sendmail"]["to_2"].split(" "), + cc=config["sendmail"]["cc_2"].split(" "), + bcc=config["sendmail"]["bcc_2"].split(" "), + **common, # type: ignore + ) + + +if __name__ == "__main__": + main() |