diff --git a/.github/workflows/github-packages.yml b/.github/workflows/github-packages.yml index e5bd5c0..5490fee 100644 --- a/.github/workflows/github-packages.yml +++ b/.github/workflows/github-packages.yml @@ -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 }} diff --git a/src/mailrise/simple_router.py b/src/mailrise/simple_router.py index f854c00..396ab02 100644 --- a/src/mailrise/simple_router.py +++ b/src/mailrise/simple_router.py @@ -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 @@ -141,12 +144,14 @@ 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: @@ -154,10 +159,39 @@ def load_from_yaml(logger: Logger, configs_node: dict[str, typ.Any]) -> SimpleRo 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) diff --git a/tests/test_config.py b/tests/test_config.py index 8481013..f36acbc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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.""" @@ -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 @@ -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 @@ -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: @@ -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: @@ -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: @@ -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: @@ -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.