Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/multi-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
- name: Login to GitHub Container Registry
# This step logs in to the GitHub Container Registry (GHCR) using the docker/login-action.
# It uses the GitHub actor's username and the GITHUB_TOKEN secret for authentication.
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand Down Expand Up @@ -221,7 +221,7 @@ jobs:
network=host

- name: Login to GitHub Container Registry
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
# This step logs in to the GitHub Container Registry (GHCR) using the docker/login-action.
# It uses the GitHub actor's username and the GITHUB_TOKEN secret for authentication.
# The login is necessary to push the merged manifest list to GHCR.
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ RUN uv sync \
&& rm -rf /bin/uv uv.lock

# ---- Runtime stage ----
FROM dhi.io/python:3.13-debian13-dev
FROM dhi.io/python:3.13-debian13-dev AS runtime
LABEL org.opencontainers.image.authors=asi@dbca.wa.gov.au
LABEL org.opencontainers.image.source=https://github.com/dbca-wa/resource_tracking

Expand All @@ -50,6 +50,7 @@ COPY --from=builder /app /app
COPY --chown=nonroot:nonroot gunicorn.py manage.py pyproject.toml ./
COPY --chown=nonroot:nonroot resource_tracking ./resource_tracking
COPY --chown=nonroot:nonroot tracking ./tracking

# Compile scripts and collect static files
RUN python -m compileall manage.py resource_tracking tracking \
&& python manage.py collectstatic --noinput
Expand Down
2 changes: 1 addition & 1 deletion kustomize/overlays/prod/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ patches:
- path: service_patch.yaml
images:
- name: ghcr.io/dbca-wa/resource_tracking
newTag: 1.4.49
newTag: 1.4.50
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "resource_tracking"
version = "1.4.49"
version = "1.4.50"
description = "DBCA internal corporate application to download and serve data from remote tracking devices."
authors = [
{ name = "Ashley Felton", email = "ashley.felton@dbca.wa.gov.au" },
Expand All @@ -14,7 +14,7 @@ license = "Apache-2.0"
requires-python = ">=3.13,<4.0"
dependencies = [
"django==5.2.16",
"dbca-utils==3.0.12",
"dbca-utils==3.0.13",
"django-extensions==4.1",
"python-dotenv==1.2.2",
"dj-database-url==3.1.2",
Expand All @@ -24,7 +24,7 @@ dependencies = [
"sentry-sdk[django]==2.66.1",
"django-geojson==4.2.0",
"orjson==3.11.9",
"django-crispy-forms==2.6",
"django-crispy-forms==2.7",
"crispy-bootstrap5==2026.3",
"h11==0.16",
"django-tastypie==0.15.1",
Expand All @@ -36,7 +36,7 @@ dependencies = [

[dependency-groups]
dev = [
"ipython>=9.15.0",
"ipython>=9.16.1",
"ipdb>=0.13.13",
"mixer>=7.2.2",
"pre-commit>=4.6.0",
Expand Down
43 changes: 22 additions & 21 deletions tracking/email_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from email.message import EmailMessage
from email.policy import default
from imaplib import IMAP4_SSL
from ssl import SSLEOFError
from typing import Any, List, Literal, Optional, Tuple

from django.conf import settings
Expand All @@ -17,89 +18,89 @@ def get_imap(mailbox: str = "INBOX") -> IMAP4_SSL | Literal[False]:
imap.login(settings.EMAIL_USER, settings.EMAIL_PASSWORD)
imap.select(mailbox)
return imap
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
except (IMAP4_SSL.abort, IMAP4_SSL.error, SSLEOFError) as err:
LOGGER.warning(f"Unable to log into mailbox: {err}")
return False


def email_get_unread(imap: IMAP4_SSL, from_email_address: str) -> Tuple[str, List] | Literal[False]:
def email_get_unread(imap: IMAP4_SSL, from_email_address: str) -> Tuple[str, List] | Tuple[Literal[False], Literal[False]]:
"""Returns (status, [list of UIDs]) of unread emails from a sending email address."""
search = '(UNSEEN UNFLAGGED FROM "{}")'.format(from_email_address)
search = f'(UNSEEN UNFLAGGED FROM "{from_email_address}")'
try:
status, response = imap.search(None, search)
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to search unread emails: {err}")
return False
return False, False

if status != "OK":
return status, response
# Return status and list of unread email UIDs.
return status, response[0].split()


def email_fetch(imap: IMAP4_SSL, uid: str) -> Tuple[Literal["OK"], EmailMessage] | Literal[False]:
def email_fetch(imap: IMAP4_SSL, uid: str) -> Tuple[str, EmailMessage] | Tuple[Literal[False], Literal[False]]:
"""Fetch a single email and return a tuple of status, EmailMessage"""
msg_data = [None]
try:
status, msg_data = imap.fetch(uid, "(RFC822)")
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to fetch email: {err}")
return False
return False, False

if status != "OK":
return False
return False, False
elif msg_data[0] is None:
return False
return False, False

raw_email = msg_data[0][1]
# Sometimes, we don't have a raw email body (bytes) at this point.
# In this situation, abort and return False.
if not isinstance(raw_email, bytes):
return False
return False, False

email_msg = message_from_bytes(s=raw_email, policy=default)

return status, email_msg


def email_mark_read(imap: IMAP4_SSL, uid: str) -> Tuple[str, list[Any]] | Literal[False]:
def email_mark_read(imap: IMAP4_SSL, uid: str) -> Tuple[str, list[Any]] | Tuple[Literal[False], Literal[False]]:
"""Flag an email as 'Seen' based on passed-in UID."""
try:
status, response = imap.store(uid, "+FLAGS", r"\Seen")
status, response = imap.store(uid, "+FLAGS", r"(\Seen)")
return status, response
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to mark email read: {err}")
return False
return False, False


def email_mark_unread(imap, uid) -> Tuple[Optional[str], Optional[str]] | Literal[False]:
def email_mark_unread(imap, uid) -> Tuple[Optional[str], Optional[str]] | Tuple[Literal[False], Literal[False]]:
"""Remove the 'Seen' flag from an email based on passed-in UID."""
try:
status, response = imap.store(str(uid), "-FLAGS", r"\Seen")
status, response = imap.store(str(uid), "-FLAGS", r"(\Seen)")
return status, response
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to mark email unread: {err}")
return False
return False, False


def email_delete(imap, uid) -> Tuple[Optional[str], Optional[str]] | Literal[False]:
def email_delete(imap, uid) -> Tuple[Optional[str], Optional[str]] | Tuple[Literal[False], Literal[False]]:
"""Flag an email for deletion."""
try:
status, response = imap.store(str(uid), "+FLAGS", r"\Deleted")
status, response = imap.store(str(uid), "+FLAGS", r"(\Deleted)")
return status, response
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to delete email: {err}")
return False
return False, False


def email_flag(imap, uid) -> Tuple[Optional[str], Optional[str]] | Literal[False]:
def email_flag(imap, uid) -> Tuple[Optional[str], Optional[str]] | Tuple[Literal[False], Literal[False]]:
"""Flag an email as unprocessable."""
try:
status, response = imap.store(str(uid), "+FLAGS", r"\Flagged")
status, response = imap.store(str(uid), "+FLAGS", r"(\Flagged)")
return status, response
except (IMAP4_SSL.abort, IMAP4_SSL.error) as err:
LOGGER.warning(f"Unable to flag email: {err}")
return False
return False, False


def email_get_body(msg: EmailMessage) -> Tuple[Optional[str], Optional[str]]:
Expand Down
1 change: 1 addition & 0 deletions tracking/harvest.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def harvest_tracking_email(device_type, purge_email=False):
if purge_email:
flagged += 1
email_utils.email_delete(imap, uid)
LOGGER.info(f"Marking email UID {uid} for deletion")

LOGGER.info(f"Created {created} tracking points, flagged {flagged} emails for deletion")

Expand Down
52 changes: 25 additions & 27 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.