Skip to content

Commit f4b5a3d

Browse files
committed
Load auto responder from any python script with the defined interface. Improve documentation
1 parent 112bc6c commit f4b5a3d

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ like Thunderbird for more advanced usage.
1313

1414
DO NOT USE FOR PRODUCTION.
1515

16+
### Features
17+
18+
- Generate random mails
19+
- Receive, send mails and reply to mails
20+
- Generate randomized mails
21+
- Manage IMAP flags and automated flagging
22+
- SMTP auto respond feature for automatic testing
23+
1624
### Supported
1725

1826
- SMTP (optionally with STARTTLS)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from email.message import Message
2+
from logging import Logger
3+
4+
from mail_devel.builder import Builder
5+
from mail_devel.smtp import Reply
6+
7+
8+
def reply(message: Message, flags: set[str], _logger: Logger) -> Reply | None:
9+
return Reply(Builder.reply_mail(message), flags - {"Seen"})

src/mail_devel/service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ def parse(cls, args: list[str] | None = None) -> argparse.Namespace:
306306
)
307307
group.add_argument(
308308
"--smtp-responder",
309-
help="Automatically respond to received mails",
309+
help="Automatically respond to received mails. Possible options are "
310+
"pre-defined scripts like reply_once or reply_always or a path to "
311+
"python script with the defined reply() function. See the source of the "
312+
"pre-defined scripts for the interface definition",
310313
)
311314

312315
group = parser.add_argument_group("Options")

src/mail_devel/smtp.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import importlib
2+
import importlib.util
23
import logging
4+
import os
35
from email.message import Message
6+
from types import ModuleType
47
from typing import Callable, Iterable, Type
58

69
from aiosmtpd.handlers import AsyncMessage
@@ -56,18 +59,43 @@ def _convert_flags(
5659
result.append(flag)
5760
return frozenset(result)
5861

62+
def _load_responder_from_file(self, responder: str) -> ModuleType | None:
63+
if not os.path.isfile(responder):
64+
return None
65+
66+
try:
67+
spec = importlib.util.spec_from_file_location("autorespond", responder)
68+
if not spec or not spec.loader:
69+
return None
70+
71+
script = importlib.util.module_from_spec(spec)
72+
if not script:
73+
return None
74+
75+
spec.loader.exec_module(script)
76+
return script
77+
except ImportError:
78+
return None
79+
5980
def load_responder(self, responder: str | None = None) -> None:
6081
if not responder:
6182
return
6283

6384
try:
6485
script = importlib.import_module(f".automation.{responder}", __package__)
86+
_logger.info(f"Loaded auto responder .automation.{responder}")
6587
except ImportError:
66-
return
88+
script = None
89+
90+
if not script:
91+
script = self._load_responder_from_file(responder)
92+
if script:
93+
_logger.info(f"Loaded auto responder {responder}")
6794

68-
reply = getattr(script, "reply", None)
69-
if callable(reply):
70-
self.responder = reply
95+
if script:
96+
reply = getattr(script, "reply", None)
97+
if callable(reply):
98+
self.responder = reply
7199

72100
async def auto_respond(self, message: Message) -> None:
73101
if not callable(self.responder):

0 commit comments

Comments
 (0)