amsdal_mail 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amsdal_mail/Third-Party Materials - AMSDAL Dependencies - License Notices.md +29 -0
- amsdal_mail/__about__.py +1 -0
- amsdal_mail/__init__.py +164 -0
- amsdal_mail/app.py +36 -0
- amsdal_mail/backends/__init__.py +103 -0
- amsdal_mail/backends/base.py +87 -0
- amsdal_mail/backends/console.py +91 -0
- amsdal_mail/backends/dummy.py +56 -0
- amsdal_mail/backends/ses.py +433 -0
- amsdal_mail/backends/smtp.py +305 -0
- amsdal_mail/events.py +46 -0
- amsdal_mail/exceptions.py +25 -0
- amsdal_mail/message.py +167 -0
- amsdal_mail/py.typed +0 -0
- amsdal_mail/settings.py +21 -0
- amsdal_mail/status.py +189 -0
- amsdal_mail/webhooks/__init__.py +0 -0
- amsdal_mail/webhooks/base.py +32 -0
- amsdal_mail/webhooks/handler.py +43 -0
- amsdal_mail/webhooks/listener.py +41 -0
- amsdal_mail/webhooks/registry.py +21 -0
- amsdal_mail/webhooks/ses.py +201 -0
- amsdal_mail-0.1.0.dist-info/METADATA +799 -0
- amsdal_mail-0.1.0.dist-info/RECORD +25 -0
- amsdal_mail-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Third-Party Materials - AMSDAL Dependencies - License Notices
|
|
2
|
+
|
|
3
|
+
## **pydantic v2.12 or later**
|
|
4
|
+
|
|
5
|
+
### [https://github.com/pydantic/pydantic](https://github.com/pydantic/pydantic)
|
|
6
|
+
|
|
7
|
+
### **MIT License**
|
|
8
|
+
|
|
9
|
+
The MIT License (MIT)
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
amsdal_mail/__about__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
amsdal_mail/__init__.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""AMSDAL Mail - Universal email integration for AMSDAL Framework."""
|
|
2
|
+
|
|
3
|
+
from amsdal_mail.backends import get_connection
|
|
4
|
+
from amsdal_mail.events import EmailTrackingContext
|
|
5
|
+
from amsdal_mail.events import EmailTrackingEvent
|
|
6
|
+
from amsdal_mail.events import TrackingEventType
|
|
7
|
+
from amsdal_mail.exceptions import ConfigurationError
|
|
8
|
+
from amsdal_mail.exceptions import EmailConnectionError
|
|
9
|
+
from amsdal_mail.exceptions import EmailError
|
|
10
|
+
from amsdal_mail.exceptions import SendError
|
|
11
|
+
from amsdal_mail.message import Attachment
|
|
12
|
+
from amsdal_mail.message import EmailMessage
|
|
13
|
+
from amsdal_mail.settings import mail_settings
|
|
14
|
+
from amsdal_mail.status import RecipientStatus
|
|
15
|
+
from amsdal_mail.status import SendStatus
|
|
16
|
+
from amsdal_mail.status import SendStatusType
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'Attachment',
|
|
20
|
+
'ConfigurationError',
|
|
21
|
+
'EmailConnectionError',
|
|
22
|
+
'EmailError',
|
|
23
|
+
'EmailMessage',
|
|
24
|
+
'EmailTrackingContext',
|
|
25
|
+
'EmailTrackingEvent',
|
|
26
|
+
'RecipientStatus',
|
|
27
|
+
'SendError',
|
|
28
|
+
'SendStatus',
|
|
29
|
+
'SendStatusType',
|
|
30
|
+
'TrackingEventType',
|
|
31
|
+
'asend_mail',
|
|
32
|
+
'get_connection',
|
|
33
|
+
'mail_settings',
|
|
34
|
+
'send_mail',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def send_mail(
|
|
39
|
+
subject: str,
|
|
40
|
+
message: str,
|
|
41
|
+
from_email: str,
|
|
42
|
+
recipient_list: list[str] | str,
|
|
43
|
+
*,
|
|
44
|
+
fail_silently: bool = False,
|
|
45
|
+
html_message: str | None = None,
|
|
46
|
+
connection=None,
|
|
47
|
+
**kwargs,
|
|
48
|
+
) -> SendStatus:
|
|
49
|
+
"""
|
|
50
|
+
Send a single email message.
|
|
51
|
+
|
|
52
|
+
This is a convenience function for sending simple emails. For more control,
|
|
53
|
+
use EmailMessage and get_connection() directly.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
subject: Email subject line
|
|
57
|
+
message: Plain text body
|
|
58
|
+
from_email: Sender email address
|
|
59
|
+
recipient_list: List of recipient email addresses (or single string)
|
|
60
|
+
fail_silently: If True, suppress exceptions and return empty SendStatus on failure
|
|
61
|
+
html_message: HTML version of body (optional)
|
|
62
|
+
connection: Reuse existing connection (optional)
|
|
63
|
+
**kwargs: Additional arguments passed to EmailMessage
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
SendStatus object with message IDs, statuses, and ESP response
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
SendError: If sending fails and fail_silently is False
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
>>> status = send_mail(
|
|
73
|
+
... 'Hello',
|
|
74
|
+
... 'This is a test email',
|
|
75
|
+
... 'sender@example.com',
|
|
76
|
+
... ['recipient@example.com'],
|
|
77
|
+
... )
|
|
78
|
+
>>> status.message_id
|
|
79
|
+
'msg-123'
|
|
80
|
+
>>> status.is_success
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
83
|
+
# Get or create connection
|
|
84
|
+
connection = connection or get_connection(fail_silently=fail_silently)
|
|
85
|
+
|
|
86
|
+
# Normalize recipient_list to list
|
|
87
|
+
if isinstance(recipient_list, str):
|
|
88
|
+
recipient_list = [recipient_list]
|
|
89
|
+
|
|
90
|
+
# Create email message
|
|
91
|
+
mail = EmailMessage(
|
|
92
|
+
subject=subject,
|
|
93
|
+
body=message,
|
|
94
|
+
from_email=from_email,
|
|
95
|
+
to=recipient_list,
|
|
96
|
+
html_body=html_message,
|
|
97
|
+
**kwargs,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Send message
|
|
101
|
+
return connection.send_messages([mail])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def asend_mail(
|
|
105
|
+
subject: str,
|
|
106
|
+
message: str,
|
|
107
|
+
from_email: str,
|
|
108
|
+
recipient_list: list[str] | str,
|
|
109
|
+
*,
|
|
110
|
+
fail_silently: bool = False,
|
|
111
|
+
html_message: str | None = None,
|
|
112
|
+
connection=None,
|
|
113
|
+
**kwargs,
|
|
114
|
+
) -> SendStatus:
|
|
115
|
+
"""
|
|
116
|
+
Send a single email message asynchronously.
|
|
117
|
+
|
|
118
|
+
Async version of send_mail(). See send_mail() for parameter documentation.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
subject: Email subject line
|
|
122
|
+
message: Plain text body
|
|
123
|
+
from_email: Sender email address
|
|
124
|
+
recipient_list: List of recipient email addresses (or single string)
|
|
125
|
+
fail_silently: If True, suppress exceptions and return empty SendStatus on failure
|
|
126
|
+
html_message: HTML version of body (optional)
|
|
127
|
+
connection: Reuse existing connection (optional)
|
|
128
|
+
**kwargs: Additional arguments passed to EmailMessage
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
SendStatus object with message IDs, statuses, and ESP response
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
SendError: If sending fails and fail_silently is False
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> status = await asend_mail(
|
|
138
|
+
... 'Hello',
|
|
139
|
+
... 'This is a test email',
|
|
140
|
+
... 'sender@example.com',
|
|
141
|
+
... ['recipient@example.com'],
|
|
142
|
+
... )
|
|
143
|
+
>>> status.message_id
|
|
144
|
+
'msg-123'
|
|
145
|
+
"""
|
|
146
|
+
# Get or create connection
|
|
147
|
+
connection = connection or get_connection(fail_silently=fail_silently)
|
|
148
|
+
|
|
149
|
+
# Normalize recipient_list to list
|
|
150
|
+
if isinstance(recipient_list, str):
|
|
151
|
+
recipient_list = [recipient_list]
|
|
152
|
+
|
|
153
|
+
# Create email message
|
|
154
|
+
mail = EmailMessage(
|
|
155
|
+
subject=subject,
|
|
156
|
+
body=message,
|
|
157
|
+
from_email=from_email,
|
|
158
|
+
to=recipient_list,
|
|
159
|
+
html_body=html_message,
|
|
160
|
+
**kwargs,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Send message asynchronously
|
|
164
|
+
return await connection.asend_messages([mail])
|
amsdal_mail/app.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""AMSDAL integration for amsdal_mail plugin."""
|
|
2
|
+
|
|
3
|
+
from amsdal.contrib.app_config import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MailAppConfig(AppConfig):
|
|
7
|
+
"""
|
|
8
|
+
AMSDAL plugin configuration for amsdal_mail.
|
|
9
|
+
|
|
10
|
+
Register this in your AMSDAL application settings:
|
|
11
|
+
AMSDAL_CONTRIBS = 'amsdal_mail.app.MailAppConfig'
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name = 'amsdal_mail'
|
|
15
|
+
verbose_name = 'AMSDAL Email Integration'
|
|
16
|
+
|
|
17
|
+
def on_setup(self) -> None:
|
|
18
|
+
"""Called when the plugin is loaded by AMSDAL."""
|
|
19
|
+
from amsdal_mail.settings import mail_settings
|
|
20
|
+
|
|
21
|
+
if mail_settings.WEBHOOK_ENABLED:
|
|
22
|
+
self._setup_webhooks()
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _setup_webhooks() -> None:
|
|
26
|
+
from amsdal_server.apps.common.events.server import RouterSetupEvent # type: ignore[import-not-found]
|
|
27
|
+
from amsdal_utils.events import EventBus
|
|
28
|
+
|
|
29
|
+
from amsdal_mail.settings import mail_settings
|
|
30
|
+
from amsdal_mail.webhooks.listener import WebhookRouterListener
|
|
31
|
+
from amsdal_mail.webhooks.registry import WebhookRegistry
|
|
32
|
+
from amsdal_mail.webhooks.ses import SESWebhookParser
|
|
33
|
+
|
|
34
|
+
WebhookRegistry.register('ses', SESWebhookParser(secret=mail_settings.WEBHOOK_SES_SECRET))
|
|
35
|
+
|
|
36
|
+
EventBus.subscribe(RouterSetupEvent, WebhookRouterListener)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Backend registry and connection utilities."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from amsdal_mail.backends.base import BaseEmailBackend
|
|
8
|
+
|
|
9
|
+
# Registry of available backends
|
|
10
|
+
BACKENDS: dict[str, str] = {
|
|
11
|
+
'console': 'amsdal_mail.backends.console.ConsoleBackend',
|
|
12
|
+
'dummy': 'amsdal_mail.backends.dummy.DummyBackend',
|
|
13
|
+
'ses': 'amsdal_mail.backends.ses.SESBackend',
|
|
14
|
+
'smtp': 'amsdal_mail.backends.smtp.SMTPBackend',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def import_string(dotted_path: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Import a class from a dotted module path.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
dotted_path: Full dotted path to class (e.g., 'package.module.ClassName')
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The imported class
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ImportError: If the module or class cannot be imported
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
module_path, class_name = dotted_path.rsplit('.', 1)
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
msg = f"'{dotted_path}' doesn't look like a module path"
|
|
35
|
+
raise ImportError(msg) from e
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
module = importlib.import_module(module_path)
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
msg = f"Module '{module_path}' could not be imported"
|
|
41
|
+
raise ImportError(msg) from e
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return getattr(module, class_name)
|
|
45
|
+
except AttributeError as e:
|
|
46
|
+
msg = f"Module '{module_path}' does not define a '{class_name}' class"
|
|
47
|
+
raise ImportError(msg) from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_connection(
|
|
51
|
+
backend: str | None = None,
|
|
52
|
+
*,
|
|
53
|
+
fail_silently: bool = False,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> BaseEmailBackend:
|
|
56
|
+
"""
|
|
57
|
+
Load an email backend and return an instance.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
backend: Backend name (from BACKENDS registry) or full dotted path.
|
|
61
|
+
If None, uses AMSDAL_EMAIL_BACKEND environment variable.
|
|
62
|
+
Defaults to 'console' if not specified.
|
|
63
|
+
fail_silently: If True, suppress exceptions in send operations
|
|
64
|
+
**kwargs: Backend-specific configuration options
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
An instance of the specified backend
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ImportError: If the backend cannot be imported
|
|
71
|
+
ConfigurationError: If the backend is misconfigured
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> # Use registered backend name
|
|
75
|
+
>>> conn = get_connection('smtp', host='smtp.gmail.com', port=587)
|
|
76
|
+
|
|
77
|
+
>>> # Use full dotted path
|
|
78
|
+
>>> conn = get_connection('myapp.backends.CustomBackend')
|
|
79
|
+
|
|
80
|
+
>>> # Use environment variable
|
|
81
|
+
>>> os.environ['AMSDAL_EMAIL_BACKEND'] = 'smtp'
|
|
82
|
+
>>> conn = get_connection()
|
|
83
|
+
"""
|
|
84
|
+
# Resolve backend name
|
|
85
|
+
if backend is None:
|
|
86
|
+
backend = os.environ.get('AMSDAL_EMAIL_BACKEND', 'console')
|
|
87
|
+
|
|
88
|
+
# Check if it's a registered backend name
|
|
89
|
+
if backend in BACKENDS:
|
|
90
|
+
backend = BACKENDS[backend]
|
|
91
|
+
|
|
92
|
+
# Import and instantiate the backend
|
|
93
|
+
backend_cls = import_string(backend)
|
|
94
|
+
|
|
95
|
+
return backend_cls(fail_silently=fail_silently, **kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
'BACKENDS',
|
|
100
|
+
'BaseEmailBackend',
|
|
101
|
+
'get_connection',
|
|
102
|
+
'import_string',
|
|
103
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Base email backend interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from amsdal_mail.message import EmailMessage
|
|
10
|
+
from amsdal_mail.status import SendStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseEmailBackend(ABC):
|
|
14
|
+
"""Abstract base class for all email backends."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *, fail_silently: bool = False, **kwargs: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the email backend.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
fail_silently: If True, suppress exceptions and return 0 for failed sends
|
|
22
|
+
**kwargs: Backend-specific configuration options
|
|
23
|
+
"""
|
|
24
|
+
self.fail_silently = fail_silently
|
|
25
|
+
self.extra_kwargs = kwargs
|
|
26
|
+
|
|
27
|
+
def open(self) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Open a network connection to the email service
|
|
30
|
+
|
|
31
|
+
This method is called automatically when using the backend as a context manager.
|
|
32
|
+
Override this method if your backend needs to establish a persistent connection.
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def close(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Close the network connection to the email service.
|
|
39
|
+
|
|
40
|
+
This method is called automatically when exiting the context manager.
|
|
41
|
+
Override this method if your backend needs to clean up resources.
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def __enter__(self) -> 'BaseEmailBackend':
|
|
46
|
+
"""Enter context manager - open connection."""
|
|
47
|
+
self.open()
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
51
|
+
"""Exit context manager - close connection."""
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def send_messages(self, email_messages: list['EmailMessage']) -> 'SendStatus':
|
|
56
|
+
"""
|
|
57
|
+
Send one or more email messages.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
email_messages: List of EmailMessage instances to send
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
SendStatus object with message IDs, statuses, and ESP response data
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
SendError: If sending fails and fail_silently is False
|
|
67
|
+
"""
|
|
68
|
+
msg = 'Subclasses must implement send_messages()'
|
|
69
|
+
raise NotImplementedError(msg)
|
|
70
|
+
|
|
71
|
+
async def asend_messages(self, email_messages: list['EmailMessage']) -> 'SendStatus':
|
|
72
|
+
"""
|
|
73
|
+
Send one or more email messages asynchronously.
|
|
74
|
+
|
|
75
|
+
Default implementation falls back to synchronous send_messages().
|
|
76
|
+
Override this method to provide true async implementation.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
email_messages: List of EmailMessage instances to send
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
SendStatus object with message IDs, statuses, and ESP response data
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
SendError: If sending fails and fail_silently is False
|
|
86
|
+
"""
|
|
87
|
+
return self.send_messages(email_messages)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Console email backend - writes emails to stdout."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import IO
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from amsdal_mail.backends.base import BaseEmailBackend
|
|
8
|
+
from amsdal_mail.message import EmailMessage
|
|
9
|
+
from amsdal_mail.status import RecipientStatus
|
|
10
|
+
from amsdal_mail.status import SendStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConsoleBackend(BaseEmailBackend):
|
|
14
|
+
"""
|
|
15
|
+
Email backend that writes messages to a stream (default: stdout).
|
|
16
|
+
|
|
17
|
+
Useful for development and debugging.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, stream: IO[str] | None = None, **kwargs: Any) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the console backend.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
stream: Output stream (defaults to sys.stdout)
|
|
26
|
+
**kwargs: Additional backend options (passed to parent)
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(**kwargs)
|
|
29
|
+
self.stream = stream or sys.stdout
|
|
30
|
+
|
|
31
|
+
def send_messages(self, email_messages: list[EmailMessage]) -> SendStatus:
|
|
32
|
+
"""
|
|
33
|
+
Write email messages to the stream.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
email_messages: List of EmailMessage instances to output
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SendStatus with 'sent' status for all recipients
|
|
40
|
+
"""
|
|
41
|
+
status = SendStatus()
|
|
42
|
+
recipients = {}
|
|
43
|
+
|
|
44
|
+
for message in email_messages:
|
|
45
|
+
self.stream.write('-' * 79 + '\n')
|
|
46
|
+
self.stream.write(f'Subject: {message.subject}\n')
|
|
47
|
+
self.stream.write(f'From: {message.from_email}\n')
|
|
48
|
+
self.stream.write(f'To: {", ".join(str(addr) for addr in message.to)}\n')
|
|
49
|
+
|
|
50
|
+
if message.cc:
|
|
51
|
+
self.stream.write(f'Cc: {", ".join(str(addr) for addr in message.cc)}\n')
|
|
52
|
+
|
|
53
|
+
if message.bcc:
|
|
54
|
+
self.stream.write(f'Bcc: {", ".join(str(addr) for addr in message.bcc)}\n')
|
|
55
|
+
|
|
56
|
+
if message.reply_to:
|
|
57
|
+
self.stream.write(f'Reply-To: {", ".join(str(addr) for addr in message.reply_to)}\n')
|
|
58
|
+
|
|
59
|
+
if message.headers:
|
|
60
|
+
for key, value in message.headers.items():
|
|
61
|
+
self.stream.write(f'{key}: {value}\n')
|
|
62
|
+
|
|
63
|
+
self.stream.write('\n')
|
|
64
|
+
|
|
65
|
+
if message.html_body:
|
|
66
|
+
self.stream.write('Content-Type: multipart/alternative\n')
|
|
67
|
+
self.stream.write('\n--- Plain Text ---\n')
|
|
68
|
+
self.stream.write(message.body)
|
|
69
|
+
self.stream.write('\n\n--- HTML ---\n')
|
|
70
|
+
self.stream.write(message.html_body)
|
|
71
|
+
else:
|
|
72
|
+
self.stream.write(message.body)
|
|
73
|
+
|
|
74
|
+
if message.attachments:
|
|
75
|
+
self.stream.write('\n\n--- Attachments ---\n')
|
|
76
|
+
for attachment in message.attachments:
|
|
77
|
+
size_kb = len(attachment.content) / 1024
|
|
78
|
+
self.stream.write(f'- {attachment.filename} ({attachment.mimetype}, {size_kb:.1f} KB)\n')
|
|
79
|
+
|
|
80
|
+
self.stream.write('\n' + '-' * 79 + '\n\n')
|
|
81
|
+
|
|
82
|
+
# Add recipient status (console backend always succeeds)
|
|
83
|
+
for recipient in message.recipients():
|
|
84
|
+
recipients[recipient] = RecipientStatus(
|
|
85
|
+
message_id=None, # Console backend doesn't generate message IDs
|
|
86
|
+
status='sent',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.stream.flush()
|
|
90
|
+
status.set_recipient_status(recipients)
|
|
91
|
+
return status
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Dummy email backend - does not send emails."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from amsdal_mail.backends.base import BaseEmailBackend
|
|
6
|
+
from amsdal_mail.message import EmailMessage
|
|
7
|
+
from amsdal_mail.status import RecipientStatus
|
|
8
|
+
from amsdal_mail.status import SendStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DummyBackend(BaseEmailBackend):
|
|
12
|
+
"""
|
|
13
|
+
Email backend that does not actually send emails.
|
|
14
|
+
|
|
15
|
+
Useful for testing - pretends to send messages successfully but does nothing.
|
|
16
|
+
Optionally stores messages for inspection during tests.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, *, store_messages: bool = False, **kwargs: Any) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize the dummy backend.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
store_messages: If True, store sent messages in self.messages list
|
|
25
|
+
**kwargs: Additional backend options (passed to parent)
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(**kwargs)
|
|
28
|
+
self.store_messages = store_messages
|
|
29
|
+
self.messages: list[EmailMessage] = []
|
|
30
|
+
|
|
31
|
+
def send_messages(self, email_messages: list[EmailMessage]) -> SendStatus:
|
|
32
|
+
"""
|
|
33
|
+
Pretend to send email messages.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
email_messages: List of EmailMessage instances to "send"
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SendStatus with 'sent' status for all recipients
|
|
40
|
+
"""
|
|
41
|
+
if self.store_messages:
|
|
42
|
+
self.messages.extend(email_messages)
|
|
43
|
+
|
|
44
|
+
status = SendStatus()
|
|
45
|
+
recipients = {}
|
|
46
|
+
|
|
47
|
+
# Mark all recipients as sent (but don't actually send)
|
|
48
|
+
for message in email_messages:
|
|
49
|
+
for recipient in message.recipients():
|
|
50
|
+
recipients[recipient] = RecipientStatus(
|
|
51
|
+
message_id=None, # Dummy backend doesn't generate message IDs
|
|
52
|
+
status='sent',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
status.set_recipient_status(recipients)
|
|
56
|
+
return status
|