From 85c6cca26c68b80f3cc6865173676b90f104033d Mon Sep 17 00:00:00 2001
From: Cryptex-github <64497526+asze17@users.noreply.github.com>
Date: Thu, 26 Mar 2026 15:57:47 -0400
Subject: [PATCH 1/4] Begin implementing digest script
---
backend/hoagiemail/api/mail_view.py | 15 +--
backend/hoagiemail/email/__init__.py | 12 ++
backend/hoagiemail/stuff/digest.py | 158 +++++++++++++++++++++++++++
3 files changed, 171 insertions(+), 14 deletions(-)
create mode 100644 backend/hoagiemail/email/__init__.py
create mode 100644 backend/hoagiemail/stuff/digest.py
diff --git a/backend/hoagiemail/api/mail_view.py b/backend/hoagiemail/api/mail_view.py
index 8eb7478..05c0baa 100644
--- a/backend/hoagiemail/api/mail_view.py
+++ b/backend/hoagiemail/api/mail_view.py
@@ -7,6 +7,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView
+from hoagiemail.email import get_listservs
from hoagiemail.email.limiter import Visitor
from hoagiemail.email.mailjet_client import get_mailjet_client
from hoagiemail.email.sanitize import sanitize_html
@@ -120,20 +121,6 @@ def delete(self, request) -> Response:
return Response({"status": "OK", "message": "Scheduled mail deleted successfully"}, status=status.HTTP_200_OK)
-def get_listservs():
- """Returns list of college listserv recipients"""
- return [
- {"Email": "BUTLERBUZZ@PRINCETON.EDU", "Name": "Butler"},
- {"Email": "WHITMANWIRE@PRINCETON.EDU", "Name": "Whitman"},
- {"Email": "RockyWire@PRINCETON.EDU", "Name": "Rocky"},
- {"Email": "Re-INNformer@PRINCETON.EDU", "Name": "Forbes"},
- {"Email": "westwire@princeton.edu", "Name": "NCW"},
- {"Email": "matheymail@PRINCETON.EDU", "Name": "Mathey"},
- {"Email": "yehyellowpages@princeton.edu", "Name": "Yeh"},
- {"Email": "hoagiemailgradstudents@princeton.edu", "Name": "hoagiemailgradstudents"},
- ]
-
-
def create_message(mail_data, sender_email, to_email):
"""
Creates mailjet message structure.
diff --git a/backend/hoagiemail/email/__init__.py b/backend/hoagiemail/email/__init__.py
new file mode 100644
index 0000000..bd3ac5e
--- /dev/null
+++ b/backend/hoagiemail/email/__init__.py
@@ -0,0 +1,12 @@
+def get_listservs():
+ """Returns list of college listserv recipients"""
+ return [
+ {"Email": "BUTLERBUZZ@PRINCETON.EDU", "Name": "Butler"},
+ {"Email": "WHITMANWIRE@PRINCETON.EDU", "Name": "Whitman"},
+ {"Email": "RockyWire@PRINCETON.EDU", "Name": "Rocky"},
+ {"Email": "Re-INNformer@PRINCETON.EDU", "Name": "Forbes"},
+ {"Email": "westwire@princeton.edu", "Name": "NCW"},
+ {"Email": "matheymail@PRINCETON.EDU", "Name": "Mathey"},
+ {"Email": "yehyellowpages@princeton.edu", "Name": "Yeh"},
+ {"Email": "hoagiemailgradstudents@princeton.edu", "Name": "hoagiemailgradstudents"},
+ ]
diff --git a/backend/hoagiemail/stuff/digest.py b/backend/hoagiemail/stuff/digest.py
new file mode 100644
index 0000000..3d35f43
--- /dev/null
+++ b/backend/hoagiemail/stuff/digest.py
@@ -0,0 +1,158 @@
+from datetime import timedelta
+from typing import Final, TypedDict
+
+from hoagiemail.email import get_listservs
+from hoagiemail.email.mailjet_client import mailjet_client
+
+REQUEST_TIMEOUT: Final[timedelta] = timedelta(seconds=10)
+SUMMER: Final[bool] = False
+SANDWITCH: Final[str] = "
"
+LOGO: Final[str] = "
"
+
+
+class UserInfo:
+ name: str
+ email: str
+
+ def __init__(self, name: str, email: str) -> None:
+ self.name = name
+ self.email = email
+
+
+class Digest:
+ title: str
+ category: str
+ contact: str
+ description: str
+ link: str
+ thumbnail: str
+ name: str
+ email: str
+ tags: list[str]
+ user: UserInfo
+
+ def __init__(
+ self,
+ title: str,
+ category: str,
+ contact: str,
+ description: str,
+ link: str,
+ thumbnail: str,
+ name: str,
+ email: str,
+ tags: list[str],
+ user: UserInfo,
+ ) -> None:
+ self.title = title
+ self.category = category
+ self.contact = contact
+ self.description = description
+ self.link = link
+ self.thumbnail = thumbnail
+ self.name = name
+ self.email = email
+ self.tags = tags
+ self.user = user
+
+
+def link(text: str, link: str) -> str:
+ return f"{text}"
+
+
+def link_mail(text: str) -> str:
+ return link(text, "mailto:" + text)
+
+
+def format_tag(text: str) -> str:
+ return f'{text.title()}'
+
+
+def add_tags(email: str, tags: list[str]) -> str:
+ email += "
"
+
+ for tag in tags:
+ email += format_tag(tag) + " "
+
+ email += "
"
+
+ return email
+
+
+def format_message(message: Digest) -> str:
+ email = ""
+ name = message.name
+
+ match message.category:
+ case "sale":
+ tags = message.tags
+
+ if not tags:
+ tags = message.title.split(", ")
+ # There's a TODO: remove here that I am not sure if I should remove
+
+ email += f"{message.description}
"
+ email += f"Contact: {name} ({link_mail(message.email)})
"
+ add_tags(email, tags)
+ case "lost":
+ if message.thumbnail:
+ email += 'See Picture
'
+
+ email += "" + message.tags[0].upper() + ": " + message.title + "
"
+ email += "" + message.description + "
"
+ email += f"Contact: {name} ({link_mail(message.email)})
"
+ case _:
+ email += "" + message.title + "
"
+ email += "" + message.description + "
"
+ email += f"From: {name} ({link_mail(message.email)})
"
+ add_tags(email, message.tags)
+
+ return email
+
+
+class MailRequest:
+ header: str
+ sender: str
+ body: str
+ email: str
+
+ def __init__(self, header: str, sender: str, body: str, email: str) -> None:
+ self.header = header
+ self.sender = sender
+ self.body = body
+ self.email = email
+
+
+MailJetData = TypedDict(
+ "MailJetData",
+ {
+ "From": dict[str, str],
+ "ReplyTo": dict[str, str],
+ "Cc": list[dict[str, str]],
+ "Subject": str,
+ "Text-part": str,
+ "Html-part": str,
+ "CustomID": str,
+ },
+)
+
+
+def mail_request(request: MailRequest) -> None:
+ data: MailJetData = {
+ "From": {"Email": "hoagie@princeton.edu", "Name": request.sender},
+ "ReplyTo": {"Email": request.email, "Name": request.sender},
+ "Cc": get_listservs(),
+ "Subject": request.header,
+ "Text-part": request.body,
+ "Html-part": request.body,
+ "CustomID": "HoagieStuffDigest",
+ }
+
+ res = mailjet_client.send.create(data={"Messages": [data]})
+ res.raise_for_status()
+
+ if res.status_code != 201:
+ raise RuntimeError("did not receive HTTP 201")
+
+ if res.json()[0]["Status"] != "success":
+ raise RuntimeError("message send not successful")
From 5ad28faf6061b8ee9c888622c0196714acacd6e8 Mon Sep 17 00:00:00 2001
From: Cryptex-github <64497526+asze17@users.noreply.github.com>
Date: Wed, 1 Apr 2026 19:47:55 -0400
Subject: [PATCH 2/4] Finish digest script
---
backend/hoagiemail/stuff/digest.py | 293 +++++++++++++++++------------
1 file changed, 172 insertions(+), 121 deletions(-)
diff --git a/backend/hoagiemail/stuff/digest.py b/backend/hoagiemail/stuff/digest.py
index 3d35f43..4f866d0 100644
--- a/backend/hoagiemail/stuff/digest.py
+++ b/backend/hoagiemail/stuff/digest.py
@@ -1,158 +1,209 @@
+import logging
from datetime import timedelta
-from typing import Final, TypedDict
+from os import getenv
+from typing import Final, Iterable, TypedDict
+
+from django.utils import timezone
from hoagiemail.email import get_listservs
-from hoagiemail.email.mailjet_client import mailjet_client
+from hoagiemail.email.mailjet_client import get_mailjet_client
+from hoagiemail.models import StuffPost
REQUEST_TIMEOUT: Final[timedelta] = timedelta(seconds=10)
SUMMER: Final[bool] = False
SANDWITCH: Final[str] = "
"
LOGO: Final[str] = "
"
+IS_PRODUCTON: Final[bool] = getenv("HOAGIE_MODE") == "production"
-
-class UserInfo:
- name: str
- email: str
-
- def __init__(self, name: str, email: str) -> None:
- self.name = name
- self.email = email
-
-
-class Digest:
- title: str
- category: str
- contact: str
- description: str
- link: str
- thumbnail: str
- name: str
- email: str
- tags: list[str]
- user: UserInfo
-
- def __init__(
- self,
- title: str,
- category: str,
- contact: str,
- description: str,
- link: str,
- thumbnail: str,
- name: str,
- email: str,
- tags: list[str],
- user: UserInfo,
- ) -> None:
- self.title = title
- self.category = category
- self.contact = contact
- self.description = description
- self.link = link
- self.thumbnail = thumbnail
- self.name = name
- self.email = email
- self.tags = tags
- self.user = user
+logger = logging.getLogger(__name__)
def link(text: str, link: str) -> str:
- return f"{text}"
+ return f"{text}"
def link_mail(text: str) -> str:
- return link(text, "mailto:" + text)
+ return link(text, "mailto:" + text)
def format_tag(text: str) -> str:
- return f'{text.title()}'
+ return f'{text.title()}'
-def add_tags(email: str, tags: list[str]) -> str:
- email += ""
+def add_tags(email: str, tags: Iterable[str]) -> str:
+ email += "
"
- for tag in tags:
- email += format_tag(tag) + " "
+ for tag in tags:
+ email += format_tag(tag) + " "
- email += "
"
+ email += "
"
- return email
+ return email
-def format_message(message: Digest) -> str:
- email = ""
- name = message.name
+def format_message(message: StuffPost) -> str:
+ email = ""
+ name = str(message.user)
+ link_email = link_mail(message.user.email)
- match message.category:
- case "sale":
- tags = message.tags
+ match str(message.category):
+ case "sale":
+ tags = [str(t) for t in message.tags.all()]
- if not tags:
- tags = message.title.split(", ")
- # There's a TODO: remove here that I am not sure if I should remove
+ if not tags:
+ tags = message.title.split(", ")
+ # There's a TODO: remove here that I am not sure if I should remove
- email += f"{message.description}
"
- email += f"Contact: {name} ({link_mail(message.email)})
"
- add_tags(email, tags)
- case "lost":
- if message.thumbnail:
- email += 'See Picture
'
+ email += f"{message.description}
"
+ email += f"Contact: {name} ({link_email})
"
+ add_tags(email, tags)
+ case "lost":
+ if message.thumbnail:
+ email += 'See Picture
'
- email += "" + message.tags[0].upper() + ": " + message.title + "
"
- email += "" + message.description + "
"
- email += f"Contact: {name} ({link_mail(message.email)})
"
- case _:
- email += "" + message.title + "
"
- email += "" + message.description + "
"
- email += f"From: {name} ({link_mail(message.email)})
"
- add_tags(email, message.tags)
+ email += "" + message.tags[0].upper() + ": " + message.title + "
"
+ email += "" + message.description + "
"
+ email += f"Contact: {name} ({link_email})
"
+ case _:
+ email += "" + message.title + "
"
+ email += "" + message.description + "
"
+ email += f"From: {name} ({link_email})
"
+ add_tags(email, [str(t) for t in message.tags.all()])
- return email
+ return email
-class MailRequest:
- header: str
- sender: str
- body: str
- email: str
+MailJetData = TypedDict(
+ "MailJetData",
+ {
+ "From": dict[str, str],
+ "ReplyTo": dict[str, str],
+ "Cc": list[dict[str, str]],
+ "Subject": str,
+ "Text-part": str,
+ "Html-part": str,
+ "CustomID": str,
+ },
+)
- def __init__(self, header: str, sender: str, body: str, email: str) -> None:
- self.header = header
- self.sender = sender
- self.body = body
- self.email = email
+def mail_request(header: str, sender: str, body: str, email: str) -> None:
+ data: MailJetData = {
+ "From": {"Email": "hoagie@princeton.edu", "Name": sender},
+ "ReplyTo": {"Email": email, "Name": sender},
+ "Cc": get_listservs(),
+ "Subject": header,
+ "Text-part": body,
+ "Html-part": body,
+ "CustomID": "HoagieStuffDigest",
+ }
+
+ res = get_mailjet_client().send.create(data={"Messages": [data]})
+ res.raise_for_status()
+
+ if res.status_code != 201:
+ raise RuntimeError("did not receive HTTP 201")
+
+ if res.json()[0]["Status"] != "success":
+ raise RuntimeError("message send not successful")
-MailJetData = TypedDict(
- "MailJetData",
- {
- "From": dict[str, str],
- "ReplyTo": dict[str, str],
- "Cc": list[dict[str, str]],
- "Subject": str,
- "Text-part": str,
- "Html-part": str,
- "CustomID": str,
- },
-)
+def run_digest_script() -> None:
+ posts = StuffPost.objects.filter(has_sent=False)
-def mail_request(request: MailRequest) -> None:
- data: MailJetData = {
- "From": {"Email": "hoagie@princeton.edu", "Name": request.sender},
- "ReplyTo": {"Email": request.email, "Name": request.sender},
- "Cc": get_listservs(),
- "Subject": request.header,
- "Text-part": request.body,
- "Html-part": request.body,
- "CustomID": "HoagieStuffDigest",
- }
-
- res = mailjet_client.send.create(data={"Messages": [data]})
- res.raise_for_status()
-
- if res.status_code != 201:
- raise RuntimeError("did not receive HTTP 201")
-
- if res.json()[0]["Status"] != "success":
- raise RuntimeError("message send not successful")
+ if not posts:
+ logging.info("No messages found...Exiting...")
+ return
+
+ digest: dict[str, list[StuffPost]] = {}
+
+ for post in posts:
+ category_posts = digest.get(post.category.name, [])
+ category_posts.append(post)
+ digest[post.category.name] = category_posts
+
+ if len(digest) < 5:
+ is_weekday = timezone.now().weekday() in {1, 3, 5}
+
+ is_digest_day = is_weekday and not SUMMER
+
+ if not is_digest_day:
+ logger.info("not a digest day...Exiting...")
+ return
+
+ logger.info("5 or more digest posts...Running...")
+
+ body = f"""
+
+
{LOGO}
+
+ {
+ '
Here is a digest of posts made to Hoagie Stuff
over past few days. It\'s Summer, so Hoagie is taking things slow.'
+ if SUMMER
+ else '''
Here is a weekly digest of posts made to Hoagie Stuff,
+ from Sales to Lost & Found and more, sent every Tuesday, Thursday, and Saturday.
'''
+ }
+
+
+ Open Hoagie Stuff |
+ Add your message to next digest |
+ Give feedback
+
+
+ """
+
+ if lost_posts := digest["lost"]:
+ body += """
+
🧭 Lost & Found
+
+
+ """
+ for i, post in enumerate(lost_posts):
+ body += format_message(post)
+
+ if i == len(lost_posts) - 1:
+ body += "
"
+
+ body += "
"
+
+ if bulletin_posts := digest["bulletin"]:
+ body += """
+
✉️ Bulletins
+
+ """
+
+ for i, post in enumerate(bulletin_posts):
+ body += format_message(post)
+
+ if i == len(bulletin_posts) - 1:
+ body += "
"
+
+ body += "
"
+
+ if len(digest) != 1:
+ body += f"
That's all! This could have been {len(digest)} emails in your inbox but instead it is just one!
"
+
+ body += f"""
+
You don't need to wait for the next digest to see what's new, check out the Hoagie Stuff
+ to keep up to date with the latest posts before others.
+
+ {SANDWITCH}
+
+ Powered by
HoagieMail
+ In the Hoagie world, hoagies digest you!
+
+
+
+ """
+
+ if IS_PRODUCTON:
+ mail_request(
+ header=f"📬 DIGEST {timezone.now().strftime('m/d')}: Sales, Lost & Found, and more!",
+ sender="Hoagie Mail",
+ body=body,
+ email="hoagie@princeton.edu",
+ )
+
+ logger.info("Sucessfully sent via Hoagie Mail")
+
+ posts.update(has_sent=True)
From ce6c6635fa1b69a063242fe8f5d6cc455c42d39b Mon Sep 17 00:00:00 2001
From: Cryptex-github <64497526+asze17@users.noreply.github.com>
Date: Wed, 1 Apr 2026 20:21:54 -0400
Subject: [PATCH 3/4] Fix time format
---
backend/hoagiemail/stuff/digest.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/hoagiemail/stuff/digest.py b/backend/hoagiemail/stuff/digest.py
index 4f866d0..03a2f22 100644
--- a/backend/hoagiemail/stuff/digest.py
+++ b/backend/hoagiemail/stuff/digest.py
@@ -198,7 +198,7 @@ def run_digest_script() -> None:
if IS_PRODUCTON:
mail_request(
- header=f"📬 DIGEST {timezone.now().strftime('m/d')}: Sales, Lost & Found, and more!",
+ header=f"📬 DIGEST {timezone.now().strftime('%m/%d')}: Sales, Lost & Found, and more!",
sender="Hoagie Mail",
body=body,
email="hoagie@princeton.edu",
From 6108b1cc217d834f48888831d1cf9af63e921a2f Mon Sep 17 00:00:00 2001
From: Alvin Sze <64497526+asze17@users.noreply.github.com>
Date: Wed, 1 Apr 2026 17:39:20 -0700
Subject: [PATCH 4/4] Fix "sandwitch"
---
backend/hoagiemail/stuff/digest.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/hoagiemail/stuff/digest.py b/backend/hoagiemail/stuff/digest.py
index 03a2f22..fa887b1 100644
--- a/backend/hoagiemail/stuff/digest.py
+++ b/backend/hoagiemail/stuff/digest.py
@@ -11,7 +11,7 @@
REQUEST_TIMEOUT: Final[timedelta] = timedelta(seconds=10)
SUMMER: Final[bool] = False
-SANDWITCH: Final[str] = "
"
+HOAGIE_SANDWICH_LOGO: Final[str] = "
"
LOGO: Final[str] = "
"
IS_PRODUCTON: Final[bool] = getenv("HOAGIE_MODE") == "production"
@@ -187,7 +187,7 @@ def run_digest_script() -> None:
You don't need to wait for the next digest to see what's new, check out the Hoagie Stuff
to keep up to date with the latest posts before others.
- {SANDWITCH}
+ {HOAGIE_SANDWICH_LOGO}
Powered by
HoagieMail
In the Hoagie world, hoagies digest you!