diff --git a/.gitignore b/.gitignore index 7d847cc..df65869 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ .env .vs/ .venv/ +notifications.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8bccc44..f57355d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ COPY . /app # Optionally mount /data to store the data -# VOLUME /data +VOLUME /data ENTRYPOINT ["python3"] CMD ["main.py"] diff --git a/requirements.txt b/requirements.txt index 8cf7fcb..45869f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ flask gunicorn requests -python-dotenv \ No newline at end of file +python-dotenv +cachetools +markdownify \ No newline at end of file diff --git a/server.py b/server.py index 240a1a8..5bfdfe2 100644 --- a/server.py +++ b/server.py @@ -15,11 +15,17 @@ import json import requests from datetime import datetime import dotenv +from functools import cache +from markdownify import markdownify as md dotenv.load_dotenv() app = Flask(__name__) +notification_file = "/data/notifications.json" +if not os.path.exists("/data"): + notification_file = "./notifications.json" + def find(name, path): for root, dirs, files in os.walk(path): @@ -27,6 +33,8 @@ def find(name, path): return os.path.join(root, name) # Assets routes + + @app.route("/assets/") def send_assets(path): if path.endswith(".json"): @@ -74,7 +82,335 @@ def wellknown(path): # region Main routes @app.route("/") def index(): - return render_template("index.html") + # Check if user is logged in + token = request.cookies.get("token") + if token: + return redirect("/inbox") + + return render_template("login.html", login=f"https://login.hns.au/auth?return={request.url}login") + + +@app.route("/login") +def login(): + # Get username and token from args + token = request.args.get("token") + # Set token cookie + response = make_response(redirect("/inbox")) + response.set_cookie("token", token) + return response + + +@app.route("/logout") +def logout(): + # Delete token cookie + response = make_response(redirect("/")) + response.set_cookie("token", "") + return response + + +@app.route("/inbox") +def inbox(): + # Check if user is logged in + token = request.cookies.get("token") + if not token: + return redirect("/") + + # Get user info + + user = get_user(token) + if not user: + return redirect("/logout") + + email = user["hemail"] + return render_template("inbox.html", emails=get_email(email)) + + +@app.route("/delete/") +def delete(path): + # Check if user is logged in + token = request.cookies.get("token") + if not token: + return redirect("/") + + # Get user info + user = get_user(token) + if not user: + return redirect("/logout") + + email = user["hemail"] + delete_email(path, email) + return redirect("/inbox") + + +@app.route("/notifications") +def notifications(): + # Check if user is logged in + token = request.cookies.get("token") + if not token: + return redirect("/") + + # Get user info + user = get_user(token) + if not user: + return redirect("/logout") + + email = user["hemail"] + notification = get_notification_settings(email) + if not notification: + return render_template("notifications.html", email=email) + return render_template("notifications.html", email=email, webhook=notification["webhook"], email_alert=notification["email"]) + + +@app.route("/notifications", methods=["POST"]) +def notification_set(): + # Check if user is logged in + token = request.cookies.get("token") + if not token: + return redirect("/") + + # Get user info + user = get_user(token) + if not user: + return redirect("/logout") + + email = user["hemail"] + webhook = request.form.get("webhook") + email_alert = request.form.get("email") + + save_notification_settings(email, webhook, email_alert) + + return redirect("/notifications") + + +def save_notification_settings(email, webhook, email_alert): + if not os.path.exists(notification_file): + with open(notification_file, "w") as f: + json.dump({ + email: { + "webhook": webhook, + "email": email_alert + } + }, f, indent=4) + + with open(notification_file, "r") as f: + data = json.load(f) + data[email] = { + "webhook": webhook, + "email": email_alert + } + with open(notification_file, "w") as f: + json.dump(data, f, indent=4) + + +def get_notification_settings(email): + if not os.path.exists(notification_file): + return False + + with open(notification_file, "r") as f: + data = json.load(f) + if email not in data: + return {"webhook": "", "email": ""} + return data[email] + + +def delete_email(conversation_id, email): + # Get email by id + conversation = get_conversation(conversation_id) + if not conversation: + return + print(conversation) + + if len(conversation) < 1: + return + + headers = { + "X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"), + } + for thread in conversation: + for message in thread["messages"]: + if email in message["to"]: + requests.delete( + f"https://ticket.woodburn.au/api/conversations/{thread['id']}", headers=headers) + return + + +@app.route("/api/email", methods=["POST"]) +def newemail(): + # Get the json + json = request.get_json() + + # Get header x-freescout-event + event = request.headers.get("x-freescout-event") + + if event == "convo.created": + send_notification(json) + elif event == "convo.customer.reply.created": + send_notification(json) + + return jsonify(json) + + +@app.route("/api/email/") +def getemail(path): + email = f'{path}@login.hns.au' + return jsonify(get_email(email)) + + +def send_notification(data): + emails = data["cc"] + for email in emails: + notifications = get_notification_settings(email) + if not notifications: + continue + if notifications["email"] != "": + send_email(notifications["email"], data) + if notifications["webhook"] != "": + send_webhook(notifications["webhook"], data) + + +def send_email(email, data): + print(f"Sending email to {email}") + headers = { + "X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"), + } + sender = f"{data['createdBy']['firstName']} { + data['createdBy']['lastName']} ({data['createdBy']['email']})" + message = data["_embedded"]["threads"][0]["body"] + + body = { + "type": "email", + "mailboxId": os.getenv("FREESCOUT_MAILBOX"), + "subject": f"New email from {sender}", + "customer": { + "email": email + }, + "threads": [ + { + "text": f"You have a new email from {sender}\n\n{message}", + "type": "message", + "user": 1 + } + ], + "imported": False, + "status": "closed", + } + req = requests.post( + "https://ticket.woodburn.au/api/conversations", json=body, headers=headers) + if req.status_code != 201: + print(f"Failed to send email") + print(req.text) + + +def send_webhook(webhook, data): + print(f"Sending webhook to {webhook}") + sender = f"{data['createdBy']['firstName']} { + data['createdBy']['lastName']} ({data['createdBy']['email']})" + message = data["_embedded"]["threads"][0]["body"] + + message = md(message) + + trimmed = len(message) > 3999 + # Only keep 4000 chars + message = message[:4000] + + subject = data["subject"] + body = { + "content": "You have a new email", + "embeds": [ + { + "title": subject, + "description": message, + "url": "https://email.hns.au", + "color": 5814783, + "author": { + "name": sender + } + } + ], + "username": "HNS Login Email", + "avatar_url": "https://hns.au/favicon.png", + "attachments": [] + } + if trimmed: + body["embeds"][0]["footer"] = { + "text": "Email has been trimmed" + } + + req = requests.post( + webhook, json=body) + + +@cache +def get_user(token): + req = requests.get(f"https://login.hns.au/auth/user?token={token}") + if req.status_code != 200: + return False + return req.json() + + +@cache +def get_email(email): + params = { + "embed": "threads", + "mailboxId": os.getenv("FREESCOUT_MAILBOX"), + "status": "active", + "type": "email", + "sortField": "updatedAt", + "sortOrder": "desc", + "page": "1", + "pageSize": "100" + } + headers = { + "X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"), + } + + conversations = requests.get( + f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers) + + if conversations.status_code != 200: + return jsonify({"email": email, "error": "Could not get conversations"}, status=400) + + threads = [] + conversations = conversations.json() + for conversation in conversations["_embedded"]["conversations"]: + addresses = conversation["cc"] + for address in addresses: + if address == email: + threads.append(parse_email(conversation)) + + return {"email": email, "conversations": threads} + + +@cache +def get_conversation(id): + params = { + "number": id, + "embed": "threads", + "mailboxId": os.getenv("FREESCOUT_MAILBOX"), + "status": "active", + "type": "email", + "sortField": "updatedAt", + "sortOrder": "desc", + "page": "1", + "pageSize": "100", + + } + headers = { + "X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"), + } + + conversations = requests.get( + f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers) + + if conversations.status_code != 200: + return {"conversations": []} + + threads = [] + conversations = conversations.json() + for conversation in conversations["_embedded"]["conversations"]: + threads.append(parse_email(conversation)) + + return threads @app.route("/") @@ -100,10 +436,24 @@ def catch_all(path: str): # endregion +def parse_email(conversation): + messages = [] + threads = conversation["_embedded"]["threads"] + for thread in threads: + messages.append({ + "from": thread["createdBy"]["email"], + "name": f'{thread["createdBy"]["firstName"]} {thread["createdBy"]["lastName"]}', + "body": thread["body"], + "date": thread["createdAt"], + "to": thread["to"] + }) + return {"messages": messages, "id": conversation["id"]} # region Error Catching # 404 catch all + + @app.errorhandler(404) def not_found(e): return render_template("404.html"), 404 diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 9635f9c..5ad6c9c 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -2,19 +2,103 @@ body { background-color: #000000; color: #ffffff; } + h1 { font-size: 50px; margin: 0; padding: 0; } + .centre { margin-top: 10%; text-align: center; } + a { color: #ffffff; text-decoration: none; } + a:hover { text-decoration: underline; +} + +header h1 { + display: inline; +} + +.button { + background-color: #ffffff; + border: 1px solid #ffffff; + border-radius: 5px; + color: #000000; + padding: 10px; + text-align: center; + text-decoration: none; + display: inline-block; +} + +.button:hover { + background-color: #000000; + border: 1px solid #ffffff; + border-radius: 5px; + color: #ffffff; + text-decoration: none; +} + +.conversation { + border: 5px solid #ffffff; + border-radius: 5px; + margin-bottom: 25px; + padding: 10px; +} + +.email { + border: 1px solid #ffffff; + border-radius: 5px; + + margin: auto; + margin-top: 10px; + margin-bottom: 10px; + padding: 10px; + width: fit-content; +} + +.from { + font-weight: bold; + display: inline; +} + +.date { + display: inline; + font-size: 12px; + color: #ffffff; + font-weight: bold; + margin-top: 10px; +} + +.body { + margin-top: 10px; + font-size: 12px; + color: #000000; + background-color: #ffffff; + border-radius: 10px; + padding: 10px; +} + +.body a { + color: #000000; +} + +input { + background-color: #ffffff; + border: 1px solid #ffffff; + border-radius: 5px; + color: #000000; + padding: 10px; + text-decoration: none; + display: inline-block; + margin-top: 10px; + margin-bottom: 10px; + width: min(500px, 90%); } \ No newline at end of file diff --git a/templates/inbox.html b/templates/inbox.html new file mode 100644 index 0000000..5717b9a --- /dev/null +++ b/templates/inbox.html @@ -0,0 +1,37 @@ + + + + + + + Inbox | HNS Login Email + + + + + +
+

HNS Login Email

{{emails.email}} | Notifications | Logout +
+ +
+ {% for conversation in emails.conversations %} +
+ Conversation: {{conversation.id}} + {% for message in conversation.messages %} + + {% endfor %} + + Delete Conversation +
+ + + {% endfor %} +
+ + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..5754002 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,19 @@ + + + + + + + HNS Login Email + + + + + + + + + + \ No newline at end of file diff --git a/templates/notifications.html b/templates/notifications.html new file mode 100644 index 0000000..3b9f99e --- /dev/null +++ b/templates/notifications.html @@ -0,0 +1,33 @@ + + + + + + + Notifications | HNS Login Email + + + + + +
+

HNS Login Email

{{email}} | Inbox | Logout +
+ +
+
+ + + + +
+ + +
+ +
+
+ + + \ No newline at end of file