Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/github-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
name: Build and Push
uses: ./.github/actions/docker-build
with:
images: ghcr.io/YoRyan/mailrise
images: ghcr.io/${{ github.repository }}
58 changes: 46 additions & 12 deletions src/mailrise/simple_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,25 @@ class SimpleRouter(Router): # pylint: disable=too-few-public-methods
tuple, where key contains username and domain patterns that can be
matched by fnmatch and sender is the Sender instance itself.
"""
senders: typ.List[typ.Tuple[_Key, _SimpleSender]]
senders: typ.List[typ.Tuple[_Key, _Key, _SimpleSender]]

def __init__(self, senders: typ.List[typ.Tuple[_Key, _SimpleSender]]):
def __init__(self, senders: typ.List[typ.Tuple[_Key, _Key, _SimpleSender]]):
super().__init__()
self.senders = senders

async def email_to_apprise(
self, logger: Logger, email: EmailMessage, auth_data: typ.Any, **kwargs) \
-> typ.AsyncGenerator[AppriseNotification, None]:
for addr in email.to:

sender_addr = email.from_
for addr in [email.to]:
try:
sender_mail = _parsercpt(sender_addr)
rcpt = _parsercpt(addr)
except ValueError:
logger.error('Not a valid Mailrise address: %s', addr)
continue
sender = self.get_sender(rcpt.key)
sender = self.get_sender(sender_mail.key, rcpt.key)
if sender is None:
logger.error('Recipient is not configured: %s', addr)
continue
Expand All @@ -141,23 +144,54 @@ async def email_to_apprise(
attachments=email.attachments
)

def get_sender(self, key: _Key) -> _SimpleSender | None:
def get_sender(self, sender_key: _Key, rect_key: _Key) -> _SimpleSender | None:
"""Find a sender by recipient key."""
return next(
(sender for (pattern_key, sender) in self.senders
if fnmatchcase(key.user, pattern_key.user)
and fnmatchcase(key.domain, pattern_key.domain)), None)
(sender for (sender_pattern_key, rect_pattern_key, sender) in self.senders
if fnmatchcase(sender_key.user, sender_pattern_key.user)
and fnmatchcase(sender_key.domain, sender_pattern_key.domain)
and fnmatchcase(rect_key.user, rect_pattern_key.user)
and fnmatchcase(rect_key.domain, rect_pattern_key.domain)), None)


def load_from_yaml(logger: Logger, configs_node: dict[str, typ.Any]) -> SimpleRouter:
"""Load a simple router from the YAML configs node."""
if not isinstance(configs_node, dict):
logger.critical('The configs node is not a YAML mapping')
raise SystemExit(1)
router = SimpleRouter(
senders=[(_parse_simple_key(logger, key), _load_simple_sender(logger, key, config))
for key, config in configs_node.items()]
)

senders = []
for sender_key, rev_config in configs_node.items():
if not isinstance(rev_config, dict):
logger.critical("YAML config node '%s' is not a mapping", sender_key)
raise SystemExit(1)

# Check if rev_config contains receiver-level nesting or direct config
# A direct config will have 'urls' or 'mailrise' keys at the top level
is_direct_config = 'urls' in rev_config or any(
k for k in rev_config.keys()
if not isinstance(rev_config[k], dict) or k == 'mailrise'
)

if is_direct_config:
# Direct config: sender -> config (no receiver specified)
# Use "*@*" as default receiver
default_recv = "*@*"
senders.append((
_parse_simple_key(logger, sender_key),
_parse_simple_key(logger, default_recv),
_load_simple_sender(logger, sender_key, rev_config)
))
else:
# Nested config: sender -> receiver -> config
for recv_key, config in rev_config.items():
senders.append((
_parse_simple_key(logger, sender_key),
_parse_simple_key(logger, recv_key),
_load_simple_sender(logger, sender_key, config)
))

router = SimpleRouter(senders=senders)
if len(router.senders) < 1:
logger.critical('No Apprise targets are configured')
raise SystemExit(1)
Expand Down
72 changes: 52 additions & 20 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,38 @@ def test_load() -> None:
router = mrise.router
assert isinstance(router, SimpleRouter)
assert len(router.senders) == 1
key = _Key(user='test')
sender_key = _Key(user='test')
rect_key = _Key(user='*@*')
assert mrise.authenticator is None

sender = router.get_sender(key)
sender = router.get_sender(sender_key, rect_key)
assert sender is not None
notifier = _make_notifier(sender.config_yaml)
assert len(notifier) == 1
assert notifier[0].url().startswith('json://localhost/')

def test_sender_receiver_load() -> None:
"""Tests a successful load with :fun:`load_config`."""
file = StringIO("""
configs:
test:
receiver:
urls:
- json://localhost
""")
mrise = load_config(_logger, file)
router = mrise.router
assert isinstance(router, SimpleRouter)
assert len(router.senders) == 1
sender_key = _Key(user='test')
rect_key = _Key(user='receiver')
assert mrise.authenticator is None

sender = router.get_sender(sender_key, rect_key)
assert sender is not None
notifier = _make_notifier(sender.config_yaml)
assert len(notifier) == 1
assert notifier[0].url().startswith('json://localhost/')

def test_multi_load() -> None:
"""Tests a sucessful load with :fun:`load_config` with multiple configs."""
Expand All @@ -76,9 +99,10 @@ def test_multi_load() -> None:
assert len(router.senders) == 2

for user in ('test1', 'test2'):
key = _Key(user=user)
sender_key = _Key(user=user)
rect_key = _Key(user='*@*')

sender = router.get_sender(key)
sender = router.get_sender(sender_key, rect_key)
assert sender is not None
notifier = _make_notifier(sender.config_yaml)
assert len(notifier) == 1
Expand All @@ -101,9 +125,10 @@ def test_mailrise_options() -> None:
router = mrise.router
assert isinstance(router, SimpleRouter)
assert len(router.senders) == 1
key = _Key(user='test')
sender_key = _Key(user='test')
rect_key = _Key(user='*@*')

sender = router.get_sender(key)
sender = router.get_sender(sender_key, rect_key)
assert sender is not None
assert sender.title_template.template == ''
assert sender.body_format == NotifyFormat.TEXT
Expand Down Expand Up @@ -148,8 +173,9 @@ def test_config_keys() -> None:
router = mrise.router
assert isinstance(router, SimpleRouter)
assert len(router.senders) == 1
key = _Key(user='user', domain='example.com')
assert router.get_sender(key) is not None
sender_key = _Key(user='user', domain='example.com')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is not None


def test_fnmatch_config_keys() -> None:
Expand All @@ -165,10 +191,12 @@ def test_fnmatch_config_keys() -> None:
mrise = load_config(_logger, file)
router = mrise.router
assert isinstance(router, SimpleRouter)
key = _Key(user='user', domain='example.com')
assert router.get_sender(key) is None
key = _Key(user='user', domain='mailrise.xyz')
assert router.get_sender(key) is not None
sender_key = _Key(user='user', domain='example.com')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is None
sender_key = _Key(user='user', domain='mailrise.xyz')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is not None

file = StringIO("""
configs:
Expand All @@ -179,8 +207,9 @@ def test_fnmatch_config_keys() -> None:
mrise = load_config(_logger, file)
router = mrise.router
assert isinstance(router, SimpleRouter)
key = _Key(user='user', domain='example.com')
assert router.get_sender(key) is not None
sender_key = _Key(user='user', domain='example.com')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is not None

file = StringIO("""
configs:
Expand All @@ -191,10 +220,12 @@ def test_fnmatch_config_keys() -> None:
mrise = load_config(_logger, file)
router = mrise.router
assert isinstance(router, SimpleRouter)
key = _Key(user='user', domain='example.com')
assert router.get_sender(key) is None
key = _Key(user='thequickbrownfox', domain='example.com')
assert router.get_sender(key) is not None
sender_key = _Key(user='user', domain='example.com')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is None
sender_key = _Key(user='thequickbrownfox', domain='example.com')
rect_key = _Key(user='*@*')
assert router.get_sender(sender_key, rect_key) is not None


def test_authenticator() -> None:
Expand Down Expand Up @@ -242,8 +273,9 @@ def test_env_var() -> None:
router = mrise.router
assert isinstance(router, SimpleRouter)
assert len(router.senders) == 1
key = _Key(user='test')
sender = router.get_sender(key)
sender_key = _Key(user='test')
rect_key = _Key(user='*@*')
sender = router.get_sender(sender_key, rect_key)
assert sender is not None
notifier = _make_notifier(sender.config_yaml)
# Missing type annotation for this property as of Dec 2022.
Expand Down