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.
@@ -0,0 +1,799 @@
1
+ Metadata-Version: 2.4
2
+ Name: amsdal_mail
3
+ Version: 0.1.0
4
+ Summary: Universal email integration plugin for AMSDAL Framework
5
+ Author: AMSDAL Team
6
+ License: MIT
7
+ Keywords: amsdal,email,mail,smtp
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: amsdal
17
+ Requires-Dist: amsdal-utils
18
+ Requires-Dist: pydantic[email]~=2.12
19
+ Provides-Extra: all
20
+ Requires-Dist: aioboto3~=15.4; extra == 'all'
21
+ Requires-Dist: aiosmtplib~=5.1; extra == 'all'
22
+ Requires-Dist: boto3~=1.40; extra == 'all'
23
+ Provides-Extra: ses
24
+ Requires-Dist: aioboto3~=15.4; extra == 'ses'
25
+ Requires-Dist: boto3~=1.40; extra == 'ses'
26
+ Provides-Extra: smtp
27
+ Requires-Dist: aiosmtplib~=5.1; extra == 'smtp'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # AMSDAL Mail
31
+
32
+ Universal email integration plugin for AMSDAL Framework. Provides a unified interface for sending emails through multiple backends (SMTP, AWS SES, etc.).
33
+
34
+ ## Features
35
+
36
+ - **Multiple Backends**: SMTP, AWS SES, Console, Dummy
37
+ - **Unified API**: Single interface for all email services
38
+ - **Async Support**: Native async/await support for all backends
39
+ - **Template Support**: Send templated emails with variable substitution (SES)
40
+ - **Tags & Metadata**: Tag and track emails for analytics and filtering
41
+ - **Click/Open Tracking**: Monitor email opens and link clicks
42
+ - **Inline Images**: Embed images in HTML via CID references
43
+ - **Type Safe**: Full Pydantic validation with type hints
44
+ - **Django-like API**: Familiar interface for Django developers
45
+ - **AMSDAL Integration**: Seamless integration with AMSDAL Framework lifecycle
46
+ - **Extensible**: Easy to add custom backends
47
+
48
+ ## Documentation
49
+
50
+ - **[Quick Start](#quick-start)** - Get started quickly
51
+ - **[Configuration](#configuration)** - Backend and environment setup
52
+ - **[SMTP Usage Guide](docs/SMTP_USAGE.md)** - Detailed SMTP examples (Gmail, etc.)
53
+ - **[Architecture](docs/ARCHITECTURE.md)** - System design and patterns
54
+ - **[Tracking Guide](docs/TRACKING.md)** - Email click/open tracking setup
55
+ - **[Webhooks](#webhooks)** - Receiving tracking events from ESPs
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # Basic installation
61
+ pip install amsdal-mail
62
+
63
+ # With specific backend support
64
+ pip install amsdal-mail[smtp]
65
+ pip install amsdal-mail[ses]
66
+
67
+ # All backends
68
+ pip install amsdal-mail[all]
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ### Basic Usage
74
+
75
+ ```python
76
+ from amsdal_mail import send_mail
77
+
78
+ # Send a simple email
79
+ status = send_mail(
80
+ subject='Hello from AMSDAL Mail',
81
+ message='This is a test email',
82
+ from_email='sender@example.com',
83
+ recipient_list=['recipient@example.com'],
84
+ )
85
+
86
+ print(status.is_success) # True
87
+ ```
88
+
89
+ ### Async Usage
90
+
91
+ ```python
92
+ import asyncio
93
+ from amsdal_mail import asend_mail
94
+
95
+ async def send_async_email():
96
+ status = await asend_mail(
97
+ subject='Async Email',
98
+ message='Sent asynchronously',
99
+ from_email='sender@example.com',
100
+ recipient_list=['recipient@example.com'],
101
+ )
102
+ print(status.is_success)
103
+
104
+ asyncio.run(send_async_email())
105
+ ```
106
+
107
+ ### HTML Emails
108
+
109
+ ```python
110
+ from amsdal_mail import send_mail
111
+
112
+ send_mail(
113
+ subject='HTML Email',
114
+ message='Plain text version',
115
+ html_message='<h1>HTML version</h1>',
116
+ from_email='sender@example.com',
117
+ recipient_list=['recipient@example.com'],
118
+ )
119
+ ```
120
+
121
+ ### Advanced Usage
122
+
123
+ ```python
124
+ from amsdal_mail import get_connection, EmailMessage, Attachment
125
+
126
+ # Get connection to specific backend
127
+ with get_connection('smtp') as conn:
128
+ # Create message with attachments
129
+ message = EmailMessage(
130
+ subject='Email with Attachment',
131
+ body='See attached document',
132
+ from_email='sender@example.com',
133
+ to=['recipient@example.com'],
134
+ cc=['cc@example.com'],
135
+ bcc=['bcc@example.com'],
136
+ reply_to=['reply@example.com'],
137
+ attachments=[
138
+ Attachment(
139
+ filename='document.pdf',
140
+ content=b'PDF content...',
141
+ mimetype='application/pdf',
142
+ )
143
+ ],
144
+ headers={'X-Custom-Header': 'value'},
145
+ )
146
+
147
+ # Send multiple messages in one connection
148
+ status = conn.send_messages([message])
149
+ ```
150
+
151
+ ### Template Support
152
+
153
+ Send emails using ESP templates with variable substitution:
154
+
155
+ ```python
156
+ from amsdal_mail import EmailMessage, get_connection
157
+
158
+ # Basic template with global variables
159
+ message = EmailMessage(
160
+ subject='Welcome!',
161
+ from_email='noreply@example.com',
162
+ to=['user@example.com'],
163
+ template_id='welcome-template',
164
+ merge_global_data={
165
+ 'company': 'Acme Inc',
166
+ 'year': '2024',
167
+ },
168
+ )
169
+
170
+ # Template with per-recipient variables
171
+ message = EmailMessage(
172
+ subject='Order Confirmation',
173
+ from_email='orders@example.com',
174
+ to=['customer@example.com'],
175
+ template_id='order-confirmation',
176
+ merge_data={
177
+ 'customer@example.com': {
178
+ 'name': 'John Doe',
179
+ 'order_id': '12345',
180
+ 'total': '$99.99',
181
+ },
182
+ },
183
+ merge_global_data={
184
+ 'company': 'Acme Inc',
185
+ 'support_email': 'support@example.com',
186
+ },
187
+ )
188
+
189
+ backend = get_connection('ses')
190
+ backend.send_messages([message])
191
+ ```
192
+
193
+ **Note**: Template support is currently available for AWS SES backend. Templates must be created in your ESP dashboard first.
194
+
195
+ ### Tags and Metadata
196
+
197
+ Add tags and metadata for tracking and analytics:
198
+
199
+ ```python
200
+ from amsdal_mail import EmailMessage
201
+
202
+ message = EmailMessage(
203
+ subject='Marketing Newsletter',
204
+ body='Newsletter content...',
205
+ from_email='marketing@example.com',
206
+ to=['subscriber@example.com'],
207
+ tags=['newsletter', 'marketing', 'q4-2024'],
208
+ metadata={
209
+ 'campaign_id': 'fall-sale-2024',
210
+ 'user_id': '12345',
211
+ 'ab_test_variant': 'A',
212
+ },
213
+ )
214
+ ```
215
+
216
+ Tags and metadata are passed to the ESP and can be used for:
217
+ - Filtering and categorizing emails
218
+ - Analytics and reporting
219
+ - A/B testing tracking
220
+ - Custom event data
221
+
222
+ ### Click and Open Tracking
223
+
224
+ Enable tracking to monitor email opens and link clicks:
225
+
226
+ ```python
227
+ from amsdal_mail import EmailMessage, get_connection
228
+
229
+ message = EmailMessage(
230
+ subject='Product Launch',
231
+ body='Check out our new product!',
232
+ html_body='<h1>New Product</h1><a href="https://example.com/product">Learn More</a>',
233
+ from_email='marketing@example.com',
234
+ to=['customer@example.com'],
235
+ track_opens=True, # Track when email is opened
236
+ track_clicks=True, # Track when links are clicked
237
+ )
238
+
239
+ backend = get_connection('ses')
240
+ backend.send_messages([message])
241
+ ```
242
+
243
+ **Important Notes:**
244
+ - **Open tracking** only works for HTML emails (inserts invisible tracking pixel)
245
+ - **Click tracking** rewrites URLs to track clicks before redirecting
246
+ - **AWS SES**: Requires Configuration Set setup (see [docs/TRACKING.md](docs/TRACKING.md))
247
+
248
+ For detailed tracking setup instructions, see [docs/TRACKING.md](docs/TRACKING.md).
249
+
250
+ ### Inline Images
251
+
252
+ Embed images directly in HTML emails using CID references:
253
+
254
+ ```python
255
+ from amsdal_mail import EmailMessage, Attachment, get_connection
256
+
257
+ message = EmailMessage(
258
+ subject='Email with Embedded Image',
259
+ body='Please enable HTML to view this email.',
260
+ html_body='<img src="cid:logo"><h1>Welcome!</h1>',
261
+ from_email='sender@example.com',
262
+ to=['recipient@example.com'],
263
+ attachments=[
264
+ Attachment(
265
+ filename='logo.png',
266
+ content=open('logo.png', 'rb').read(),
267
+ mimetype='image/png',
268
+ content_id='logo', # Referenced as cid:logo in HTML
269
+ ),
270
+ ],
271
+ )
272
+
273
+ connection = get_connection('smtp')
274
+ status = connection.send_messages([message])
275
+ ```
276
+
277
+ For more examples see [docs/SMTP_USAGE.md](docs/SMTP_USAGE.md).
278
+
279
+ ## Configuration
280
+
281
+ ### Environment Variables
282
+
283
+ #### Backend Selection
284
+
285
+ ```bash
286
+ AMSDAL_EMAIL_BACKEND=smtp # console, smtp, dummy, ses
287
+ ```
288
+
289
+ #### SMTP Configuration
290
+
291
+ ```bash
292
+ AMSDAL_EMAIL_HOST=smtp.gmail.com
293
+ AMSDAL_EMAIL_PORT=587
294
+ AMSDAL_EMAIL_USER=your-email@gmail.com
295
+ AMSDAL_EMAIL_PASSWORD=your-password
296
+ AMSDAL_EMAIL_USE_TLS=true
297
+ AMSDAL_EMAIL_USE_SSL=false
298
+ AMSDAL_EMAIL_TIMEOUT=30
299
+ ```
300
+
301
+ #### AWS SES Configuration
302
+
303
+ ```bash
304
+ AMSDAL_EMAIL_BACKEND=ses
305
+ AWS_ACCESS_KEY_ID=your-access-key
306
+ AWS_SECRET_ACCESS_KEY=your-secret-key
307
+ AWS_REGION=us-east-1
308
+ ```
309
+
310
+ ### Programmatic Configuration
311
+
312
+ ```python
313
+ from amsdal_mail import get_connection
314
+
315
+ # Override configuration
316
+ connection = get_connection(
317
+ backend='smtp',
318
+ host='smtp.example.com',
319
+ port=587,
320
+ username='user@example.com',
321
+ password='secret',
322
+ use_tls=True,
323
+ )
324
+
325
+ connection.send_messages([message])
326
+ ```
327
+
328
+ ## Backends
329
+
330
+ ### Console Backend
331
+
332
+ **Purpose**: Development and debugging
333
+
334
+ ```python
335
+ from amsdal_mail import send_mail
336
+
337
+ # Outputs to stdout
338
+ send_mail(
339
+ subject='Test',
340
+ message='This will print to console',
341
+ from_email='sender@example.com',
342
+ recipient_list=['recipient@example.com'],
343
+ )
344
+ ```
345
+
346
+ **Output**:
347
+ ```
348
+ Subject: Test
349
+ From: sender@example.com
350
+ To: recipient@example.com
351
+ Body:
352
+ This will print to console
353
+ ----------------------------------------
354
+ ```
355
+
356
+ ### SMTP Backend
357
+
358
+ **Purpose**: Generic SMTP servers (Gmail, Outlook, etc.)
359
+
360
+ ```bash
361
+ # Configuration
362
+ export AMSDAL_EMAIL_BACKEND=smtp
363
+ export AMSDAL_EMAIL_HOST=smtp.gmail.com
364
+ export AMSDAL_EMAIL_PORT=587
365
+ export AMSDAL_EMAIL_USER=your-email@gmail.com
366
+ export AMSDAL_EMAIL_PASSWORD=your-app-password
367
+ export AMSDAL_EMAIL_USE_TLS=true
368
+ ```
369
+
370
+ **Gmail Example**:
371
+ 1. Enable 2FA in Google Account
372
+ 2. Generate App Password
373
+ 3. Use App Password in `AMSDAL_EMAIL_PASSWORD`
374
+
375
+ For detailed SMTP examples, see [docs/SMTP_USAGE.md](docs/SMTP_USAGE.md).
376
+
377
+ ### AWS SES Backend
378
+
379
+ **Purpose**: Amazon Simple Email Service
380
+
381
+ ```bash
382
+ export AMSDAL_EMAIL_BACKEND=ses
383
+ export AWS_ACCESS_KEY_ID=your-key
384
+ export AWS_SECRET_ACCESS_KEY=your-secret
385
+ export AWS_REGION=us-east-1
386
+ ```
387
+
388
+ ### Dummy Backend
389
+
390
+ **Purpose**: Testing (no actual sending)
391
+
392
+ ```python
393
+ from amsdal_mail import get_connection
394
+
395
+ # Nothing is sent, but returns success status
396
+ connection = get_connection('dummy')
397
+ status = connection.send_messages([message])
398
+ print(status.is_success) # True
399
+ ```
400
+
401
+ ## AMSDAL Framework Integration
402
+
403
+ ### Register Plugin
404
+
405
+ Add to your AMSDAL application configuration:
406
+
407
+ ```python
408
+ # settings.py or .env
409
+ AMSDAL_CONTRIBS = 'amsdal_mail.app.MailAppConfig'
410
+ ```
411
+
412
+ Or multiple plugins:
413
+
414
+ ```bash
415
+ # .env
416
+ AMSDAL_CONTRIBS=amsdal.contrib.auth.app.AuthAppConfig,amsdal_mail.app.MailAppConfig
417
+ ```
418
+
419
+ ### Use in AMSDAL App
420
+
421
+ ```python
422
+ from amsdal_mail import send_mail
423
+
424
+ # Now available throughout your AMSDAL application
425
+ send_mail(
426
+ subject='Welcome',
427
+ message='Thanks for signing up!',
428
+ from_email='noreply@example.com',
429
+ recipient_list=[user.email],
430
+ )
431
+ ```
432
+
433
+ ## API Reference
434
+
435
+ ### send_mail()
436
+
437
+ ```python
438
+ def send_mail(
439
+ subject: str,
440
+ message: str,
441
+ from_email: str,
442
+ recipient_list: list[str] | str,
443
+ fail_silently: bool = False,
444
+ html_message: str | None = None,
445
+ connection = None,
446
+ **kwargs,
447
+ ) -> SendStatus
448
+ ```
449
+
450
+ Send a single email message.
451
+
452
+ **Parameters**:
453
+ - `subject`: Email subject line
454
+ - `message`: Plain text body
455
+ - `from_email`: Sender email address
456
+ - `recipient_list`: List of recipient email addresses (or single string)
457
+ - `fail_silently`: If True, suppress exceptions and return empty SendStatus
458
+ - `html_message`: HTML version of body (optional)
459
+ - `connection`: Reuse existing connection (optional)
460
+ - `**kwargs`: Additional arguments passed to EmailMessage
461
+
462
+ **Returns**: `SendStatus` object with message IDs, statuses, and ESP response details
463
+
464
+ **Example**:
465
+ ```python
466
+ status = send_mail(
467
+ 'Welcome',
468
+ 'Hello!',
469
+ 'noreply@example.com',
470
+ ['user@example.com'],
471
+ )
472
+
473
+ # Access send details
474
+ print(status.message_id) # ESP message ID (e.g., 'msg-123')
475
+ print(status.is_success) # True if all sent successfully
476
+ print(status.status) # {'sent'}
477
+ print(status.recipients) # Per-recipient details
478
+ ```
479
+
480
+ ### asend_mail()
481
+
482
+ ```python
483
+ async def asend_mail(...) -> SendStatus
484
+ ```
485
+
486
+ Async version of `send_mail()`. Returns `SendStatus` object with send details.
487
+
488
+ ### get_connection()
489
+
490
+ ```python
491
+ def get_connection(
492
+ backend: str | None = None,
493
+ fail_silently: bool = False,
494
+ **kwargs,
495
+ )
496
+ ```
497
+
498
+ Get email backend connection.
499
+
500
+ **Parameters**:
501
+ - `backend`: Backend name (`'smtp'`, `'ses'`, `'console'`, `'dummy'`) or full class path
502
+ - `fail_silently`: Suppress errors if True
503
+ - `**kwargs`: Backend-specific configuration
504
+
505
+ **Returns**: Backend instance
506
+
507
+ ### EmailMessage
508
+
509
+ ```python
510
+ from amsdal_mail import EmailMessage, Attachment
511
+
512
+ message = EmailMessage(
513
+ subject: str,
514
+ body: str,
515
+ from_email: EmailStr | str,
516
+ to: list[EmailStr | str],
517
+ cc: list[EmailStr | str] = [],
518
+ bcc: list[EmailStr | str] = [],
519
+ reply_to: list[EmailStr | str] = [],
520
+ attachments: list[Attachment] = [],
521
+ headers: dict[str, str] = {},
522
+ html_body: str | None = None,
523
+ )
524
+ ```
525
+
526
+ Pydantic model representing an email message.
527
+
528
+ ### Attachment
529
+
530
+ ```python
531
+ from amsdal_mail import Attachment
532
+
533
+ attachment = Attachment(
534
+ filename: str, # File name
535
+ content: bytes, # File content
536
+ mimetype: str, # MIME type (e.g., 'application/pdf')
537
+ content_id: str | None, # Content-ID for inline images (e.g., 'logo' for cid:logo)
538
+ )
539
+ ```
540
+
541
+ ### SendStatus
542
+
543
+ ```python
544
+ from amsdal_mail import SendStatus, RecipientStatus
545
+
546
+ # Returned by send_mail() and asend_mail()
547
+ status = send_mail(...)
548
+
549
+ # SendStatus attributes:
550
+ status.message_id # str | set[str] | None - ESP message ID(s)
551
+ status.status # set[SendStatusType] | None - Set of statuses
552
+ status.recipients # dict[str, RecipientStatus] - Per-recipient details
553
+ status.esp_response # Any - Raw ESP API response
554
+
555
+ # Helper methods:
556
+ status.is_success # True if all sent/queued
557
+ status.has_failures # True if any failed/rejected/invalid
558
+ status.get_successful_recipients() # List of successful emails
559
+ status.get_failed_recipients() # List of failed emails
560
+
561
+ # RecipientStatus attributes:
562
+ recipient = status.recipients['user@example.com']
563
+ recipient.message_id # str | None - ESP message ID for this recipient
564
+ recipient.status # 'sent' | 'queued' | 'failed' | 'rejected' | 'invalid' | 'unknown'
565
+ ```
566
+
567
+ **Status Types**:
568
+ - `sent` - ESP has sent the message (queued for delivery)
569
+ - `queued` - ESP will try to send later
570
+ - `invalid` - Recipient email address is not valid
571
+ - `rejected` - Recipient is blacklisted or rejected by ESP
572
+ - `failed` - Send attempt failed for some other reason
573
+ - `unknown` - Status could not be determined
574
+
575
+ ## Testing
576
+
577
+ ### Using Console Backend
578
+
579
+ ```python
580
+ # In your test configuration
581
+ import os
582
+ os.environ['AMSDAL_EMAIL_BACKEND'] = 'console'
583
+
584
+ # Emails will print to stdout
585
+ from amsdal_mail import send_mail
586
+ send_mail(...) # Prints to console
587
+ ```
588
+
589
+ ### Using Dummy Backend
590
+
591
+ ```python
592
+ import os
593
+ os.environ['AMSDAL_EMAIL_BACKEND'] = 'dummy'
594
+
595
+ # Emails are "sent" but do nothing
596
+ from amsdal_mail import send_mail
597
+ status = send_mail(...) # Returns SendStatus, does nothing
598
+ print(status.is_success) # True
599
+ ```
600
+
601
+ ### Mocking in Tests
602
+
603
+ ```python
604
+ import pytest
605
+ from amsdal_mail import send_mail, SendStatus
606
+
607
+ def test_email_sending(mocker):
608
+ # Mock the backend
609
+ mock_backend = mocker.patch('amsdal_mail.backends.get_connection')
610
+ mock_backend.return_value.send_messages.return_value = SendStatus()
611
+
612
+ # Test your code
613
+ result = send_mail(
614
+ subject='Test',
615
+ message='Test message',
616
+ from_email='sender@example.com',
617
+ recipient_list=['recipient@example.com'],
618
+ )
619
+
620
+ mock_backend.return_value.send_messages.assert_called_once()
621
+ ```
622
+
623
+ ## Creating Custom Backends
624
+
625
+ ### Define Backend Class
626
+
627
+ ```python
628
+ from amsdal_mail.backends.base import BaseEmailBackend
629
+ from amsdal_mail import EmailMessage, SendStatus
630
+
631
+ class MyCustomBackend(BaseEmailBackend):
632
+ def __init__(self, api_key: str, **kwargs):
633
+ super().__init__(**kwargs)
634
+ self.api_key = api_key
635
+
636
+ def send_messages(self, email_messages: list[EmailMessage]) -> SendStatus:
637
+ status = SendStatus()
638
+ # Your custom sending logic
639
+ return status
640
+
641
+ async def asend_messages(self, email_messages: list[EmailMessage]) -> SendStatus:
642
+ status = SendStatus()
643
+ # Async implementation
644
+ return status
645
+ ```
646
+
647
+ ### Register Backend
648
+
649
+ ```python
650
+ from amsdal_mail.backends import BACKENDS
651
+ BACKENDS['mycustom'] = 'myapp.backends.MyCustomBackend'
652
+
653
+ # Now you can use it
654
+ from amsdal_mail import get_connection
655
+ connection = get_connection('mycustom', api_key='secret')
656
+ ```
657
+
658
+ Or use full import path:
659
+
660
+ ```python
661
+ connection = get_connection('myapp.backends.MyCustomBackend', api_key='secret')
662
+ ```
663
+
664
+ ## Examples
665
+
666
+ ### Bulk Email Sending
667
+
668
+ ```python
669
+ from amsdal_mail import get_connection, EmailMessage
670
+
671
+ def send_bulk_emails(users: list):
672
+ # Reuse connection for efficiency
673
+ with get_connection('smtp') as conn:
674
+ messages = [
675
+ EmailMessage(
676
+ subject=f'Hello {user.name}',
677
+ body=f'Welcome {user.name}!',
678
+ from_email='noreply@example.com',
679
+ to=[user.email],
680
+ )
681
+ for user in users
682
+ ]
683
+
684
+ # Send all at once
685
+ status = conn.send_messages(messages)
686
+ print(f'Success: {status.is_success}')
687
+ ```
688
+
689
+ ### Transactional Email
690
+
691
+ ```python
692
+ from amsdal_mail import send_mail
693
+
694
+ def send_password_reset(user, reset_link: str):
695
+ send_mail(
696
+ subject='Password Reset Request',
697
+ message=f'''
698
+ Hello {user.name},
699
+
700
+ You requested a password reset. Click the link below:
701
+ {reset_link}
702
+
703
+ If you didn't request this, ignore this email.
704
+ ''',
705
+ html_message=f'''
706
+ <h2>Password Reset Request</h2>
707
+ <p>Hello {user.name},</p>
708
+ <p>You requested a password reset.</p>
709
+ <a href="{reset_link}">Reset Password</a>
710
+ <p>If you didn't request this, ignore this email.</p>
711
+ ''',
712
+ from_email='noreply@example.com',
713
+ recipient_list=[user.email],
714
+ )
715
+ ```
716
+
717
+ ### Async Bulk Sending
718
+
719
+ ```python
720
+ import asyncio
721
+ from amsdal_mail import asend_mail
722
+
723
+ async def send_notifications(users: list):
724
+ tasks = [
725
+ asend_mail(
726
+ subject='New Update',
727
+ message=f'Hi {user.name}, check out our new features!',
728
+ from_email='notifications@example.com',
729
+ recipient_list=[user.email],
730
+ )
731
+ for user in users
732
+ ]
733
+
734
+ # Send concurrently
735
+ results = await asyncio.gather(*tasks)
736
+ print(f'All succeeded: {all(s.is_success for s in results)}')
737
+
738
+ # Run
739
+ asyncio.run(send_notifications(users))
740
+ ```
741
+
742
+ ## Troubleshooting
743
+
744
+ ### Gmail SMTP Issues
745
+
746
+ **Error**: "Username and Password not accepted"
747
+
748
+ **Solution**:
749
+ 1. Enable 2-Factor Authentication
750
+ 2. Generate App Password (Settings > Security > App Passwords)
751
+ 3. Use App Password instead of regular password
752
+
753
+ ### TLS/SSL Issues
754
+
755
+ **Error**: "STARTTLS extension not supported"
756
+
757
+ **Solution**:
758
+ ```bash
759
+ # Try different port and TLS settings
760
+ AMSDAL_EMAIL_PORT=465
761
+ AMSDAL_EMAIL_USE_SSL=true
762
+ AMSDAL_EMAIL_USE_TLS=false
763
+ ```
764
+
765
+ ### Connection Timeout
766
+
767
+ **Error**: "Connection timed out"
768
+
769
+ **Solution**:
770
+ ```bash
771
+ # Increase timeout
772
+ AMSDAL_EMAIL_TIMEOUT=60
773
+ ```
774
+
775
+ ## Development
776
+
777
+ ### Setup
778
+
779
+ ```bash
780
+ git clone <repo-url>
781
+ cd amsdal_mail
782
+
783
+ # Install dependencies
784
+ pip install uv hatch
785
+ hatch env create
786
+ hatch run sync
787
+
788
+ # Run checks
789
+ hatch run all # style + typing
790
+ hatch run test # tests
791
+ hatch run cov # tests with coverage
792
+ ```
793
+
794
+ ### Release Workflow
795
+
796
+ 1. Develop on a feature branch, create PR to `main` — CI runs lint + tests
797
+ 2. When ready to release, create a `release/X.Y.Z` branch, bump version in `amsdal_mail/__about__.py`, update `CHANGELOG.md`
798
+ 3. Merge to `main` — tag `vX.Y.Z` is auto-created from the version in `__about__.py`
799
+ 4. Tag push triggers release — builds, publishes to PyPI, creates GitHub Release with changelog