diff --git a/main.py b/main.py
index 36c35a1f5b2fd068ff1d4c6d8405c43d74d93f4f..3fbe62784c2e5be6e6478c52144e77bb3868a844 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,11 @@
 import argparse
 import os
+from datetime import date
+
 from notify.config import load_config, build_channels
+from notify.timeline.basic import BasicTimelineIterator, FixedDay
+from notify.messages.generator import build_notification
 from notify.calendar.calendar import Calendar
-from notify.messages.generator import generate_notification
 
 
 def parse_args():
@@ -19,27 +22,40 @@ def is_real_run(args):
         or os.environ.get("DO_REAL_RUN", "false") == "true"
 
 
+def get_date(date_str: str) -> date:
+    if date_str == "today":
+        return date.today()
+    else:
+        return date.fromisoformat(date_str)
+
+
 def main():
     args = parse_args()
 
     config = load_config(args.config)
 
     channels = list(build_channels(config))
-    calendar = Calendar(config)
-    info = calendar.get_daily_standup_info(args.day)
-
-    if not info:
-        print("No standup today")
+    timeline = BasicTimelineIterator(
+        workday_calendar=Calendar(config["calendar"]["rules"]),
+        fixed_day=FixedDay(
+            day=config["calendar"]["fixed_point"]["date"],
+            moderator=config["calendar"]["fixed_point"]["moderator"],
+            days_left=config["calendar"]["fixed_point"]["days_left"]
+        ),
+        moderator_days=config["calendar"]["moderator_days"],
+        moderators=config["moderators"]
+    )
+    notification = build_notification(timeline, config, get_date(args.day))
+    if not notification:
+        print("No notification to send")
     else:
-        message = generate_notification(config, info)
-
         if is_real_run(args):
             print("Sending messages on channels...")
             for channel in channels:
-                channel.send_notification(message)
+                channel.send_notification(notification)
         else:
             print("--- This is a dry-run, messages are not sent ---")
-            print(message.message)
+            print(notification.message)
 
 
 if __name__ == "__main__":
diff --git a/notify/calendar/__tests__/test_calendar.py b/notify/calendar/__tests__/test_calendar.py
new file mode 100644
index 0000000000000000000000000000000000000000..20d803c3bf51052305fa986fde0f44b0fde47d3c
--- /dev/null
+++ b/notify/calendar/__tests__/test_calendar.py
@@ -0,0 +1,42 @@
+from pytest import fixture
+from datetime import date
+
+from notify.calendar.calendar import Calendar
+
+
+@fixture
+def calendar():
+    return Calendar([
+        "exclude_all",
+        "include_weekdays",
+        {
+            "name": "exclude_holidays",
+            "options": {
+                "country": "HU"
+            }
+        },
+        {
+            "name": "exclude_dow",
+            "options": {
+                "day": 2
+            }
+        }
+    ])
+
+
+def test_weekday_detection(calendar: Calendar):
+    assert calendar.match_day(date(2022, 7, 22)) is True
+    assert calendar.match_day(date(2022, 7, 23)) is False
+    assert calendar.match_day(date(2022, 7, 25)) is True
+
+
+def test_holiday_detection(calendar: Calendar):
+    # 12.26 is a monday but it is Christmas so it should not be a workday
+    assert calendar.match_day(date(2022, 12, 26)) is False
+    assert calendar.match_day(date(2022, 12, 27)) is True
+
+
+def test_midweek(calendar: Calendar):
+    assert calendar.match_day(date(2022, 9, 13)) is True
+    assert calendar.match_day(date(2022, 9, 14)) is False
+    assert calendar.match_day(date(2022, 9, 15)) is True
diff --git a/notify/calendar/__tests__/test_usual_calendar.py b/notify/calendar/__tests__/test_usual_calendar.py
deleted file mode 100644
index ed55ab7b4c07977170fedf1880c494185f273785..0000000000000000000000000000000000000000
--- a/notify/calendar/__tests__/test_usual_calendar.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from pytest import fixture
-from datetime import datetime
-
-from notify.calendar.calendar import Calendar
-
-
-@fixture
-def calendar():
-    return Calendar({
-        "moderators": ["a", "b", "c"],
-        "calendar": {
-            "rules": [
-                "exclude_all",
-                "include_weekdays",
-                {
-                    "name": "exclude_holidays",
-                    "options": {
-                        "country": "HU"
-                    }
-                }
-            ],
-            "moderator_days": 3,
-            "fixed_point": {
-                "date": datetime(2022, 7, 20),
-                "moderator": "b",
-                "days_left": 2
-            }
-        }
-    })
-
-
-def test_weekday_detection(calendar: Calendar):
-    assert calendar._is_workday(datetime(2022, 7, 22)) is True
-    assert calendar._is_workday(datetime(2022, 7, 23)) is False
-    assert calendar._is_workday(datetime(2022, 7, 25)) is True
-
-
-def test_holiday_detection(calendar: Calendar):
-    # 12.26 is a monday but it is Christmas so it should not be a workday
-    assert calendar._is_workday(datetime(2022, 12, 26)) is False
-    assert calendar._is_workday(datetime(2022, 12, 27)) is True
diff --git a/notify/calendar/calendar.py b/notify/calendar/calendar.py
index 830d2403eda343e113c4cf973d086b47bd7e6246..b352978326d01c06b79414c358d7c7d6a05bd872 100644
--- a/notify/calendar/calendar.py
+++ b/notify/calendar/calendar.py
@@ -1,41 +1,14 @@
-from dataclasses import dataclass
-from typing import Optional
-from datetime import date, timedelta
+from datetime import date
 
 from .rules.rule import CalendarRule
 from .rules.loader import load_rules
 
-accepted_weekdays = [0, 1, 2, 3, 4]
-
-
-@dataclass(frozen=True)
-class CalendarDay:
-    day: date
-    moderator: str
-
-
-@dataclass(frozen=True)
-class DailyStandupInfo:
-    moderator_name: str
-    days_left: int
-    next_moderator_name: str
-
 
 class Calendar:
-    def __init__(self, config):
-        self._moderators = config["moderators"]
-        self._moderator_days = config["calendar"]["moderator_days"]
+    def __init__(self, rules_config: list):
+        self._rules: list[CalendarRule] = load_rules(rules_config)
 
-        self._fixed_date: date = config["calendar"]["fixed_point"]["date"]
-        self._fixed_moderator: str = config["calendar"]["fixed_point"]["moderator"]
-        self._fixed_moderator_days: str = config["calendar"]["fixed_point"]["days_left"]
-
-        if self._fixed_moderator not in self._moderators:
-            raise ValueError("Fixed moderator is not in the list of moderators")
-
-        self._rules: list[CalendarRule] = load_rules(config["calendar"]["rules"])
-
-    def _is_workday(self, qdate: date) -> bool:
+    def match_day(self, qdate: date) -> bool:
         included = False
 
         for rule in self._rules:
@@ -44,65 +17,3 @@ class Calendar:
                 included = evaled.included
 
         return included
-
-    def _get_moderator_offset(self):
-        return \
-            self._moderators.index(self._fixed_moderator) * self._moderator_days + \
-            self._moderator_days - self._fixed_moderator_days - 1
-
-    def _count_days(self, from_date: date, target_date: date) -> int:
-        from_date = from_date
-        iterator = from_date
-        date_counter = 0
-
-        while iterator < target_date:
-            iterator += timedelta(days=1)
-            if self._is_workday(iterator):
-                date_counter += 1
-
-        return date_counter
-
-    def _build_calendar(self, from_day: date) -> list[CalendarDay]:
-        days: list[CalendarDay] = []
-        last_cached_day = self._fixed_date
-        last_cached_day_count = 0
-        moderator_offset = self._get_moderator_offset()
-
-        check_day = from_day
-        while len(days) < (self._moderator_days + 2):
-            if self._is_workday(check_day):
-                count = self._count_days(last_cached_day, check_day) + last_cached_day_count
-                last_cached_day = check_day
-                last_cached_day_count = count
-
-                moderator_id = (count + moderator_offset) // self._moderator_days
-                days.append(CalendarDay(
-                    day=check_day,
-                    moderator=self._moderators[moderator_id % len(self._moderators)]
-                ))
-            check_day += timedelta(days=1)
-
-        return days
-
-    def get_daily_standup_info(self, day: str) -> Optional[DailyStandupInfo]:
-        if day == "today":
-            today = date.today()
-        else:
-            today = date.fromisoformat(day)
-
-        if not self._is_workday(today):
-            return None
-        days = self._build_calendar(today)
-
-        current_moderator = days[0].moderator
-        days_left = 0
-        i = 0
-        while current_moderator == days[i+1].moderator:
-            days_left += 1
-            i += 1
-
-        return DailyStandupInfo(
-            moderator_name=current_moderator,
-            days_left=days_left,
-            next_moderator_name=days[i+1].moderator
-        )
diff --git a/notify/calendar/rules/__tests__/test_exclude_dow.py b/notify/calendar/rules/__tests__/test_exclude_dow.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc05d5811ef3a18b02f20a09295163277ddbae1
--- /dev/null
+++ b/notify/calendar/rules/__tests__/test_exclude_dow.py
@@ -0,0 +1,14 @@
+from pytest import fixture
+from datetime import date
+from notify.calendar.rules.exclude_dow import ExcludeDayOfWeek, RuleResult
+
+
+@fixture
+def rule():
+    return ExcludeDayOfWeek(2)
+
+
+def test_days(rule: ExcludeDayOfWeek):
+    assert rule.apply(date(2022, 9, 13)).applicable is False
+    assert rule.apply(date(2022, 9, 14)) == RuleResult(applicable=True, included=False)
+    assert rule.apply(date(2022, 9, 15)).applicable is False
diff --git a/notify/calendar/rules/exclude_dow.py b/notify/calendar/rules/exclude_dow.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecb576c6495dd005d6926050ba05d372f162ebcf
--- /dev/null
+++ b/notify/calendar/rules/exclude_dow.py
@@ -0,0 +1,15 @@
+from datetime import date
+from .rule import CalendarRule, RuleResult
+
+
+class ExcludeDayOfWeek(CalendarRule):
+    def __init__(self, day: int):
+        super().__init__()
+
+        self._exclude_day = day
+
+    def apply(self, day: date) -> RuleResult:
+        if day.weekday() == self._exclude_day:
+            return RuleResult(applicable=True, included=False)
+        else:
+            return RuleResult(applicable=False, included=False)
diff --git a/notify/calendar/rules/loader.py b/notify/calendar/rules/loader.py
index 4cbe919cfc6937603f3fd05150f48fe2aaa5a591..7014caeeae5573274ea378717f52ae4d635cab17 100644
--- a/notify/calendar/rules/loader.py
+++ b/notify/calendar/rules/loader.py
@@ -29,5 +29,8 @@ def load_rule(rule_config: dict) -> CalendarRule:
     elif rule_config["name"] == "include_days":
         from .include_days import IncludeDays
         return IncludeDays(**options)
+    elif rule_config["name"] == "exclude_dow":
+        from .exclude_dow import ExcludeDayOfWeek
+        return ExcludeDayOfWeek(**options)
     else:
         raise ValueError("Unknown rule: " + rule_config["name"])
diff --git a/notify/channels/vonage_sms.py b/notify/channels/vonage_sms.py
index 834d32f9b6bf2cbfb44b7ceb6fedafc815443b5a..b970e3a3cb4bdbf0d3f64174d821a5f95ccf7736 100644
--- a/notify/channels/vonage_sms.py
+++ b/notify/channels/vonage_sms.py
@@ -18,7 +18,8 @@ class VonageSmsChannel(NotificationChannel):
                 self._client.send_message({
                     'from': self._sender,
                     'to': number,
-                    'text': message.message
+                    'text': message.short_message
+                    if message.short_message else message.message
                 })
             except Exception as e:
                 print(f"Failed to send SMS: {e}")
diff --git a/notify/messages/__tests__/test_generator.py b/notify/messages/__tests__/test_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f20db2ea112bee0740ecd78a5f63677511132da
--- /dev/null
+++ b/notify/messages/__tests__/test_generator.py
@@ -0,0 +1,74 @@
+from pytest import fixture
+from datetime import date
+
+from notify.timeline.iterator import TimelineIterator
+from notify.timeline.basic import BasicTimelineIterator, FixedDay
+from notify.calendar.calendar import Calendar
+from notify.messages.generator import build_notification
+
+
+@fixture
+def basic_timeline_iterator():
+    return BasicTimelineIterator(
+        workday_calendar=Calendar([
+            "exclude_all",
+            "include_weekdays",
+            {
+                "name": "exclude_holidays",
+                "options": {
+                    "country": "HU"
+                }
+            },
+        ]),
+        fixed_day=FixedDay(
+            day=date(2022, 9, 21),
+            moderator="John",
+            days_left=2
+        ),
+        moderator_days=3,
+        moderators=["John", "Jane", "Jack"]
+    )
+
+
+@fixture
+def template_config():
+    return {
+        "message": {
+            "title": "Daily standup",
+            "normal_template": "daily.j2",
+            "short_template": "daily-short.j2"
+        }
+    }
+
+
+def test_weekend(basic_timeline_iterator: TimelineIterator, template_config):
+    msg = build_notification(basic_timeline_iterator, template_config, date(2022, 9, 25))
+    assert msg is None
+
+
+def test_message_generation(basic_timeline_iterator: TimelineIterator, template_config):
+    msg = build_notification(basic_timeline_iterator, template_config, date(2022, 9, 21))
+    assert msg.title == "Daily standup"
+    assert msg.message == """Dear Team Tirith!
+
+Today John will hold the daily meeting!
+
+The upcoming schedule for the next 10 workdays is as follows:
+    2022-09-22 (Thursday): John
+    2022-09-23 (Friday): John
+    2022-09-26 (Monday): Jane
+    2022-09-27 (Tuesday): Jane
+    2022-09-28 (Wednesday): Jane
+    2022-09-29 (Thursday): Jack
+    2022-09-30 (Friday): Jack
+    2022-10-03 (Monday): Jack
+    2022-10-04 (Tuesday): John
+    2022-10-05 (Wednesday): John
+
+Have a nice day!"""
+
+    assert msg.short_message == """Dear Team Tirith!
+
+Today John will hold the daily meeting!
+
+Have a nice day!"""
diff --git a/notify/messages/generator.py b/notify/messages/generator.py
index 72da79ed2a14bf966c40c4c4af6af2c2c429943b..eae6588c4cf809a2fab5b3eded4722bd1b3752d3 100644
--- a/notify/messages/generator.py
+++ b/notify/messages/generator.py
@@ -1,10 +1,48 @@
-from ..calendar.calendar import DailyStandupInfo
+import jinja2
+from datetime import date
+
+from ..timeline.iterator import TimelineIterator, CalendarDay
 from .notification import Notification
 
 
-def generate_notification(config, info: DailyStandupInfo) -> Notification:
-    template = config["message"]["message_template"]
+def render(tempalte_file, data) -> str:
+    templateLoader = jinja2.FileSystemLoader(searchpath="./templates")
+    templateEnv = jinja2.Environment(loader=templateLoader)
+    template = templateEnv.get_template(tempalte_file)
+    return template.render(data)
+
+
+def day_to_data(day: CalendarDay) -> dict:
+    return {
+        **day.__dict__,
+        "dow": day.day.strftime('%A')
+    }
+
+
+def build_notification(iter: TimelineIterator, config, starting_day: date) -> Notification:
+    iter.set_day(starting_day)
+
+    days: list[CalendarDay] = []
+    for day in iter:
+        days.append(day)
+        if len(days) >= 11:
+            break
+
+    if len(days) == 0 or days[0].day != starting_day:
+        return None
+
+    template_data = {
+        "today": day_to_data(days[0]) if len(days) >= 1 else None,
+        "days": [day_to_data(day) for day in days[1:]]
+    }
+
+    title = config["message"]["title"]
+    normal_msg_template_file = config["message"]["normal_template"]
+    short_msg_template_file = config["message"]["short_template"]
+
     return Notification(
-        title=config["message"]["title"],
-        message=template.format(**info.__dict__)
+        title=title,
+        message=render(normal_msg_template_file, template_data),
+        short_message=render(short_msg_template_file, template_data)
+        if short_msg_template_file else None
     )
diff --git a/notify/messages/notification.py b/notify/messages/notification.py
index 3ece9f8927f06371c128be737837e62b70f8d495..802578b8c2ced37ad5b5b02608775b42af09c0b0 100644
--- a/notify/messages/notification.py
+++ b/notify/messages/notification.py
@@ -1,7 +1,9 @@
 from dataclasses import dataclass
+from typing import Optional
 
 
 @dataclass(frozen=True)
 class Notification:
     title: str
     message: str
+    short_message: Optional[str]
diff --git a/notify/timeline/__tests__/test_basic.py b/notify/timeline/__tests__/test_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..6299dc6aafc54f11d87d348b254fdefbad6b79af
--- /dev/null
+++ b/notify/timeline/__tests__/test_basic.py
@@ -0,0 +1,72 @@
+from pytest import fixture
+from datetime import date
+
+from notify.timeline.basic import BasicTimelineIterator, FixedDay, CalendarDay
+from notify.calendar.calendar import Calendar
+
+
+@fixture
+def basic_timeline():
+    return BasicTimelineIterator(
+        workday_calendar=Calendar([
+            "exclude_all",
+            "include_weekdays",
+            {
+                "name": "exclude_holidays",
+                "options": {
+                    "country": "HU"
+                }
+            },
+        ]),
+        fixed_day=FixedDay(
+            day=date(2022, 9, 21),
+            moderator="a",
+            days_left=2
+        ),
+        moderator_days=3,
+        moderators=["a", "b", "c"]
+    )
+
+
+def test_fixed_day(basic_timeline: BasicTimelineIterator):
+    basic_timeline.set_day(date(2022, 9, 21))
+    assert basic_timeline.__next__() == CalendarDay(day=date(2022, 9, 21), moderator="a")
+
+
+def test_iteration(basic_timeline: BasicTimelineIterator):
+    expected_items = [
+        CalendarDay(day=date(2022, 9, 21), moderator="a"),
+        CalendarDay(day=date(2022, 9, 22), moderator="a"),
+        CalendarDay(day=date(2022, 9, 23), moderator="a"),
+        CalendarDay(day=date(2022, 9, 26), moderator="b"),
+        CalendarDay(day=date(2022, 9, 27), moderator="b"),
+        CalendarDay(day=date(2022, 9, 28), moderator="b"),
+        CalendarDay(day=date(2022, 9, 29), moderator="c"),
+    ]
+
+    basic_timeline.set_day(date(2022, 9, 21))
+
+    for day in basic_timeline:
+        expected = expected_items.pop(0)
+        assert day == expected
+
+        if len(expected_items) == 0:
+            break
+
+
+def test_far_in_future(basic_timeline: BasicTimelineIterator):
+    expected_items = [
+        CalendarDay(day=date(2046, 9, 21), moderator="c"),
+        CalendarDay(day=date(2046, 9, 24), moderator="c"),
+        CalendarDay(day=date(2046, 9, 25), moderator="c"),
+        CalendarDay(day=date(2046, 9, 26), moderator="a"),
+    ]
+
+    basic_timeline.set_day(date(2046, 9, 21))
+
+    for day in basic_timeline:
+        expected = expected_items.pop(0)
+        assert day == expected
+
+        if len(expected_items) == 0:
+            break
diff --git a/notify/timeline/basic.py b/notify/timeline/basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..164b5c650334aa59d3d1c908fc29e8539076186f
--- /dev/null
+++ b/notify/timeline/basic.py
@@ -0,0 +1,61 @@
+from dataclasses import dataclass
+from datetime import date, timedelta
+
+from .iterator import TimelineIterator, CalendarDay
+from ..calendar.calendar import Calendar
+
+
+@dataclass
+class FixedDay():
+    day: date
+    moderator: str
+    days_left: int
+
+
+class BasicTimelineIterator(TimelineIterator):
+    def __init__(self,
+                 workday_calendar: Calendar, fixed_day: FixedDay,
+                 moderators: list[str], moderator_days: int) -> None:
+        self._workday_calendar = workday_calendar
+        self._fixed_day = fixed_day
+        self._moderators = moderators
+        self._moderator_days = moderator_days
+
+        self._offset_days = self._get_initial_offset()
+
+    def _get_initial_offset(self):
+        return \
+            self._moderators.index(self._fixed_day.moderator) * self._moderator_days + \
+            self._moderator_days - self._fixed_day.days_left - 1
+
+    def _count_days(self, from_date: date, target_date: date) -> int:
+        from_date = from_date
+        iterator = from_date
+        date_counter = 0
+
+        while iterator < target_date:
+            iterator += timedelta(days=1)
+            if self._workday_calendar.match_day(iterator):
+                date_counter += 1
+
+        return date_counter
+
+    def set_day(self, day: date) -> None:
+        self._check_day = day - timedelta(days=1)
+        self._quit_day = day + timedelta(days=365)
+
+    def __next__(self) -> CalendarDay:
+        while True:
+            self._check_day += timedelta(days=1)
+
+            if self._check_day > self._quit_day:
+                raise StopIteration
+
+            if self._workday_calendar.match_day(self._check_day):
+                count = self._count_days(self._fixed_day.day, self._check_day)
+                moderator_id = (count + self._offset_days) // self._moderator_days
+
+                return CalendarDay(
+                    day=self._check_day,
+                    moderator=self._moderators[moderator_id % len(self._moderators)]
+                )
diff --git a/notify/timeline/iterator.py b/notify/timeline/iterator.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cdb9f009c308afedf90b04e0c321f25a09fe1b6
--- /dev/null
+++ b/notify/timeline/iterator.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+from datetime import date
+
+
+@dataclass(frozen=True)
+class CalendarDay:
+    day: date
+    moderator: str
+
+
+class TimelineIterator():
+    def __iter__(self) -> 'TimelineIterator':
+        return self
+
+    def set_day(self, day: date) -> None:
+        raise NotImplementedError
+
+    def __next__(self) -> 'CalendarDay':
+        raise NotImplementedError
diff --git a/requirements.txt b/requirements.txt
index f10e50914f0a07d8018b10ce9d4dd28d0731968a..eeed271b15cddb5a6ea8a6f61e22f5cd3dddef7b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@ nexmo
 redmail
 holidays
 deepmerge
+jinja2
 
 # test
 pytest
diff --git a/templates/daily-short.j2 b/templates/daily-short.j2
new file mode 100644
index 0000000000000000000000000000000000000000..4ea61d68b1f700289ac9fdc4bbc6000105e0262d
--- /dev/null
+++ b/templates/daily-short.j2
@@ -0,0 +1,5 @@
+Dear Team Tirith!
+
+Today {{ today.moderator }} will hold the daily meeting!
+
+Have a nice day!
diff --git a/templates/daily.j2 b/templates/daily.j2
new file mode 100644
index 0000000000000000000000000000000000000000..e47e0ca23f1786089c9ff611ad81ffc97c784179
--- /dev/null
+++ b/templates/daily.j2
@@ -0,0 +1,10 @@
+Dear Team Tirith!
+
+Today {{ today.moderator }} will hold the daily meeting!
+
+The upcoming schedule for the next {{ days | length }} workdays is as follows:
+{%- for item in days %}
+    {{ item.day }} ({{ item.dow }}): {{ item.moderator }}
+{%- endfor %}
+
+Have a nice day!