diff options
author | Question Box Service <qbox@andrewyu.org> | 2023-04-07 10:09:27 +0200 |
---|---|---|
committer | Question Box Service <qbox@andrewyu.org> | 2023-04-07 10:09:27 +0200 |
commit | 75de78518235d3cab6e875e44839b1e81787a897 (patch) | |
tree | b1d72ecb25b7c88f0700fc51707972f99f79ae6a | |
parent | 9477fc1612e8c54e20c647785ffc0ce2c504a53e (diff) | |
download | qbox-75de78518235d3cab6e875e44839b1e81787a897.tar.gz qbox-75de78518235d3cab6e875e44839b1e81787a897.zip |
Add email functionality
-rwxr-xr-x[-rw-r--r--] | app.py | 270 | ||||
-rw-r--r-- | templates/home.html | 3 |
2 files changed, 230 insertions, 43 deletions
@@ -1,18 +1,20 @@ +#!/usr/bin/env python3 +# # qbox - anonymous question board thingy # # Copyright (c) 2022 Ferass EL HAFIDI # Copyright (c) 2022, 2023 Andrew Yu <andrew@andrewyu.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/>. # @@ -25,16 +27,31 @@ from email.mime.text import MIMEText from subprocess import Popen, PIPE - from html import escape import time, os import json +import re +import mailbox + +MAPPING = { + "andrew": ( + "andrew@andrewyu.org", + "andrew.json", + 'https://www.andrewyu.org/', + 'Andrew Yu (Hypfzhqiik)' + ), + "hypfzwmiik": ( + "box2@andrewyu.org", + "hypfzwmiik.json", + 'https://users.andrewyu.org/~hypfzwmiik/', + 'Hypfzwmiik' + ), +} -mapping = {"andrew": ("andrew@andrewyu.org", "andrew.json", "<a href=\"https://www.andrewyu.org/\">Andrew Yu (Hypfzhqiik)</a>"), "hypfzwmiik": ("box2@andrewyu.org", "hypfzwmiik.json", "Hypfzqmiik")} # not gonna spam their mailbox during testing -def ldb(user): +def load_database(user): try: - db_file = open("%s" % mapping[user][1], "r+") + db_file = open("%s" % MAPPING[user][1], "r+") except FileNotFoundError: db = [] else: @@ -43,60 +60,231 @@ def ldb(user): db_file.close() return db + app = Flask(__name__) -def append_question(user, text, ts): - db = ldb(user) - db.append({"q": text, "a": None, "ts": ts}) - with open(mapping[user][1], "w") as db_file: - json.dump(db, db_file, indent=4) -def gpq(db): - gd = "" +def generate_user_list_from_mapping(mapping): + generated_user_list = "" + for username, usertuple in mapping.items(): + generated_user_list += "<li><a href=\"/" + generated_user_list += username + generated_user_list += "\">" + generated_user_list += usertuple[3] + generated_user_list += "</a> [Email: <a href=\"mailto:" + generated_user_list += usertuple[0] + generated_user_list += "\">" + generated_user_list += usertuple[0] + generated_user_list += "</a>, homepage: <a href=\"" + generated_user_list += usertuple[2] + generated_user_list += "\">" + generated_user_list += usertuple[2] + generated_user_list += "</a>]</li>\n" + return generated_user_list + + +def generate_past_questions_from_database(db): + generated_questions_html_listing = "" for qs in reversed(db): - if not qs["a"]: continue - gd += "<hr />" - gd += "<div class=\"single-past-question\">" - gd += "<pre class=\"past-question-question\">" - gd += escape(qs["q"]) # questions are not trusted and must be escaped - gd += "</pre>" - gd += "<span class=\"past-question-answer\">" - gd += qs["a"] # answers are trusted and may include HTML - gd += "</span>" - gd += "</div>" - return gd - -# I think, therefore I am -@app.route('/<user>', methods=['GET', 'POST']) + if not qs["a"]: + continue + generated_questions_html_listing += "<hr />" + generated_questions_html_listing += '<div class="single-past-question">' + generated_questions_html_listing += '<pre class="past-question-question">' + generated_questions_html_listing += escape(qs["q"]) # questions are not trusted and must be escaped + generated_questions_html_listing += "</pre>" + generated_questions_html_listing += '<span class="past-question-answer">' + generated_questions_html_listing += qs["a"] # answers are trusted and may include HTML + generated_questions_html_listing += "</span>" + generated_questions_html_listing += "</div>" + return generated_questions_html_listing + +def dump_database(user, db): + #with open(MAPPING[user][1], "w") as db_file: + # I've replaced this so that if the write fails it doesn't corrupt the JSON file + fn = MAPPING[user][1] + with open(fn + '.tmp', 'w') as db_file: + json.dump(db, db_file, indent=4) + os.replace(fn + '.tmp', fn) + +re_named_address = re.compile("(.*) <(.*)\\@(.*)>") +re_unnamed_address = re.compile("^[<](.*)\\@(.*)[>]$") + + +def parse_address(s): # Returns name, user, host. + attempt_named_address = re_named_address.match(s) + if attempt_named_address: + return ( + attempt_named_address.group(1), + attempt_named_address.group(2), + attempt_named_address.group(3), + ) + + attempt_unnamed_address = re_unnamed_address.match(s) + if attempt_unnamed_address: + return ( + None, + attempt_unnamed_address.group(1), + attempt_unnamed_address.group(2), + ) + + return None # No results, invalid address. + + +@app.route("/<user>", methods=["GET", "POST"]) def qboard(user): - if user not in mapping: + if user not in MAPPING: return render_template("unknown_user.html", faulty_username=user) - elif request.method == 'GET': + elif request.method == "GET": global db - db = ldb(user) - return Response(open("templates/qboard.html", "r").read().replace("{{username}}", mapping[user][2]).replace("{{pq}}", gpq(db)), mimetype='text/html') - elif request.method == 'POST': + db = load_database(user) + + # EMAIL STUFF IS TO BE ADDED HERE + # Why not in a separate email_stuff() function? + # Because lazy + mbox = mailbox.Maildir('/home/qbox/Mail/Inbox') + for msg in mbox: + if msg.get_subdir() != "new": continue + msg.set_subdir("cur") # apparently it's not doing so + mbox.flush() + parsed_address = parse_address(msg["From"]) + from_address = parsed_address[1] + "@" + parsed_address[2] + if from_address != MAPPING[user][0]: continue + if 'In-Reply-To' not in msg.keys() or not msg['In-Reply-To']: + ts = str(time.time()) + newmsg = MIMEText( + f"Hello {MAPPING[user][3]},\n\nI cannot understand this unsolicited message. If you are trying to reply to a message, be sure to use the ``reply'' feature in your email client to indicate that you are reply to that specific notification of mine. If something sounds wrong, contact your server administrator.\n\nQuestion Box System" + ) + newmsg["From"] = "qbox@andrewyu.org" + newmsg["To"] = msg["From"] + if msg["Subject"].startswith("Re: "): + newmsg["Subject"] = msg["Subject"] + else: + newmsg["Subject"] = "Re: " + msg["Subject"] + if "Message-ID" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-ID"] + elif "Message-Id" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-Id"] + newmsg["Message-Id"] = "<qbox-system-%s@andrewyu.org>" % ts + p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) + p.communicate(newmsg.as_bytes()) + return # This return was missing + + print(msg["In-Reply-To"]) + reply_identifier = parse_address(str(msg['In-Reply-To']))[1] # Should be ts + for question in reversed(db): + if reply_identifier == "qbox-" + question["ts"]: + break + else: + ts = str(time.time()) + newmsg = MIMEText( + f"Hello {MAPPING[user][3]},\n\nYou sent me a message, which was supposedly a reply to one of my notifications. However, I do not recognize your In-Reply-To header as one of my Message-IDs, so I don't know how to handle your message. Maybe you could check if you are using your email client's ``reply'' feature correctly, or if the database has been modified to remove the notified post?\n\nThings are getting weird. Contact your server administrator if confused.\n\nQuestion Box System" + ) + newmsg["From"] = "qbox@andrewyu.org" + newmsg["To"] = msg["From"] + if msg["Subject"].startswith("Re: "): + newmsg["Subject"] = msg["Subject"] + else: + newmsg["Subject"] = "Re: " + msg["Subject"] + if "Message-ID" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-ID"] + elif "Message-Id" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-Id"] + newmsg["Message-Id"] = "<qbox-system-%s@andrewyu.org>" % ts + p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) + p.communicate(newmsg.as_bytes()) + return + + for part in msg.walk(): + if part.get_content_type() == "text/plain": + break + else: + ts = str(time.time()) + newmsg = MIMEText( + f"Hello {MAPPING[user][3]},\n\nYour reply was in an incorrect format. Please ensure that it includes at least one subpart of MIME type ``text/plain''.\n\nQuestion Box System" + ) + newmsg["From"] = "qbox@andrewyu.org" + newmsg["To"] = msg["From"] + if msg["Subject"].startswith("Re: "): + newmsg["Subject"] = msg["Subject"] + else: + newmsg["Subject"] = "Re: " + msg["Subject"] + if "Message-ID" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-ID"] + elif "Message-Id" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-Id"] + newmsg["Message-Id"] = "<qbox-system-%s@andrewyu.org>" % ts + p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) + p.communicate(newmsg.as_bytes()) + return + + recieved_message_text_plain = part.get_payload() + + db.remove(question) + question["a"] = recieved_message_text_plain + db.append(question) + dump_database(user, db) + + ts = str(time.time()) + newmsg = MIMEText( + f"Hello {MAPPING[user][3]},\n\nI have received your message and I added it to the question board.\n\nQuestion Box System" + ) + newmsg["From"] = "qbox@andrewyu.org" + newmsg["To"] = msg["From"] + if msg["Subject"].startswith("Re: "): + newmsg["Subject"] = msg["Subject"] + else: + newmsg["Subject"] = "Re: " + msg["Subject"] + if "Message-ID" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-ID"] + elif "Message-Id" in msg.keys(): + newmsg["In-Reply-To"] = msg["Message-Id"] + newmsg["Message-Id"] = "<qbox-system-%s@andrewyu.org>" % ts + p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) + p.communicate(newmsg.as_bytes()) + + return Response( + open("templates/qboard.html", "r") + .read() + .replace("{{username}}", "<a href=\"%s\">%s</a>" % (MAPPING[user][2], MAPPING[user][3])) + .replace("{{pq}}", generate_past_questions_from_database(db)), + mimetype="text/html", + ) + elif request.method == "POST": ts = str(time.time()) if "text" in request.form and request.form["text"].strip(): text = request.form["text"] - append_question(user, text, ts) + db = load_database(user) + db.append({"q": text, "a": None, "ts": ts}) + dump_database(user, db) print(text + "\a") - msg = MIMEText(f"The following message was received in the question box at {ts}. Please reply to this IN PLAIN TEXT EMAIL; you may handwrite HTML in your reply.\n\n{text}") + msg = MIMEText( + f"Hello {MAPPING[user][3]},\n\nThe following message was received in your ({user}'s) question box at {ts}. Please reply to this in plain text email, as in the MIME type must be ``text/plain''; you may handwrite HTML in your reply. Remember to remove any quoted text if your email client adds these automatically. Attachments will be ignored.\n\n{text}\n\nQuestion Box System" + ) msg["From"] = "qbox@andrewyu.org" - msg["To"] = mapping[user][0] + msg["To"] = MAPPING[user][0] msg["Subject"] = "Question Box Message" msg["Message-Id"] = "<qbox-%s@andrewyu.org>" % ts p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) p.communicate(msg.as_bytes()) else: - return Response("Empty submissions are forbidden.", mimetype='text/plain') - return Response("Submission successful.", mimetype='text/plain') + return Response("Empty submissions are forbidden.", mimetype="text/plain") + return Response( + "Submission successful.\n\nPlease press the ``back'' button of your browser or otherwise return to the previous page.", + mimetype="text/plain", + ) return "Invalid request.", 400 -@app.route('/', methods=['GET']) + +@app.route("/", methods=["GET"]) def index(): - return render_template('home.html') + return Response( + open("templates/home.html", "r") + .read() + .replace("{{userlist}}", generate_user_list_from_mapping(MAPPING)), + mimetype="text/html", + ) + if __name__ == "__main__": app.run(port=5728) - diff --git a/templates/home.html b/templates/home.html index 3792bb4..bba7e4e 100644 --- a/templates/home.html +++ b/templates/home.html @@ -12,8 +12,7 @@ <strong style="color: red; font-size: 300%;">Site under maintainance — very unstable!!!</strong> <ul> - <li><a href="/andrew">Andrew/Hypfzhqiik</a></li> - <li><a href="/hypfzwmiik">Hypfzwmiik</a></li> + {{userlist}} </ul> <div id="footer"> |