diff --git a/src/api/main.py b/src/api/main.py
index 4e861c1..0d96293 100644
--- a/src/api/main.py
+++ b/src/api/main.py
@@ -24,6 +24,7 @@ from api.auth import get_current_user, resolve_account_for_user
from api.schemas import ContactListResponse, ContactOut, SyncRunOut
from config import Config
from mailer import build_message, fetch_birthdays_for_date, send_message
+from utils import fmt_birthday_age, fmt_birthday_short
logging.basicConfig(level=Config.LOG_LEVEL, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("api")
@@ -33,6 +34,8 @@ app.add_middleware(SessionMiddleware, secret_key=secrets.token_hex(32), session_
app.mount("/static", StaticFiles(directory="api/static"), name="static")
templates = Jinja2Templates(directory="api/templates")
templates.env.filters["urlquote"] = lambda s: quote_plus(s or "")
+templates.env.filters["fmt_birthday"] = fmt_birthday_short
+templates.env.filters["fmt_age"] = fmt_birthday_age
def _fmt_ts(dt) -> str | None:
diff --git a/src/api/templates/contact.html b/src/api/templates/contact.html
index 578df63..da6646f 100644
--- a/src/api/templates/contact.html
+++ b/src/api/templates/contact.html
@@ -332,7 +332,7 @@
{% if contact.birthday %}
- 馃巶 {{ contact.birthday }}
+ 馃巶 {{ contact.birthday|fmt_birthday }}
{% endif %}
diff --git a/src/api/templates/dashboard.html b/src/api/templates/dashboard.html
index 9240f13..c1061a3 100644
--- a/src/api/templates/dashboard.html
+++ b/src/api/templates/dashboard.html
@@ -367,7 +367,7 @@
{{ b.organization }}
{% endif %}
- {{ b.birthday.strftime('%d.%m.') }}{% if b.birthday.year and b.birthday.year < current_year %} 路 {{ current_year - b.birthday.year }} Jahre{% endif %}
+ {{ b.birthday|fmt_birthday }}{% if b.birthday|fmt_age %} 路 {{ b.birthday|fmt_age(current_year) }} Jahre{% endif %}
{% if b.photo_url %}
diff --git a/src/api/templates/index.html b/src/api/templates/index.html
index 2e2ad1c..4c89376 100644
--- a/src/api/templates/index.html
+++ b/src/api/templates/index.html
@@ -198,7 +198,7 @@
{{ c.organization }}
{% endif %}
{% if c.birthday %}
- {{ c.birthday }}
+ {{ c.birthday|fmt_birthday }}
{% endif %}
diff --git a/src/mailer.py b/src/mailer.py
index b79181f..11044b8 100644
--- a/src/mailer.py
+++ b/src/mailer.py
@@ -13,6 +13,7 @@ from email.message import EmailMessage
import db
from config import Config
+from utils import fmt_birthday_age, fmt_birthday_short
logging.basicConfig(level=Config.LOG_LEVEL, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("mailer")
@@ -68,9 +69,10 @@ def build_message(birthdays: list[dict], target_date: date | None = None) -> Ema
plain_lines = [f"Heutige Geburtstage ({today.isoformat()}):", ""]
for b in birthdays:
- age = today.year - b["birthday"].year
- date_str = b["birthday"].strftime("%d.%m.")
- line = f"- {b['full_name']} 路 {date_str} 路 {age} Jahre"
+ date_str = fmt_birthday_short(b["birthday"])
+ age = fmt_birthday_age(b["birthday"], today)
+ age_str = f" 路 {age} Jahre" if age is not None else ""
+ line = f"- {b['full_name']} 路 {date_str}{age_str}"
if b.get("organization"):
line += f" ({b['organization']})"
if Config.WEB_URL:
@@ -82,8 +84,9 @@ def build_message(birthdays: list[dict], target_date: date | None = None) -> Ema
cards_html = ""
for b in birthdays:
- age = today.year - b["birthday"].year
- date_str = b["birthday"].strftime("%d.%m.")
+ date_str = fmt_birthday_short(b["birthday"])
+ age = fmt_birthday_age(b["birthday"], today)
+ age_str = f" 路 {age} Jahre" if age is not None else ""
contact_link = f"{Config.WEB_URL.rstrip('/')}/contacts/{b['id']}" if Config.WEB_URL else ""
name_html = f'
{b["full_name"]}' if contact_link else b["full_name"]
is_today = b["birthday"].month == today.month and b["birthday"].day == today.day
@@ -99,7 +102,7 @@ def build_message(birthdays: list[dict], target_date: date | None = None) -> Ema
{name_html}
{org_html}
-
馃巶 {date_str} 路 {age} Jahre
+
馃巶 {date_str}{age_str}
{photo_html}
diff --git a/src/utils.py b/src/utils.py
new file mode 100644
index 0000000..0a6c2db
--- /dev/null
+++ b/src/utils.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+# Licensed under the MIT License. See LICENSE file in project root for details.
+from datetime import date
+
+UNKNOWN_YEARS = frozenset({0, 1604, 1900})
+
+
+def is_unknown_year(birthday: date) -> bool:
+ return birthday.year in UNKNOWN_YEARS
+
+
+def fmt_birthday_short(birthday: date) -> str:
+ if is_unknown_year(birthday):
+ return birthday.strftime("%d.%m.")
+ return birthday.strftime("%d.%m.%Y")
+
+
+def fmt_birthday_age(birthday: date, reference) -> int | None:
+ if is_unknown_year(birthday):
+ return None
+ ref_year = reference if isinstance(reference, int) else reference.year
+ return ref_year - birthday.year