@bernierllc/email 1.0.0 → 1.1.0

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.
Files changed (64) hide show
  1. package/README.md +76 -217
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +28 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/simple-email-service.d.ts +58 -0
  7. package/dist/simple-email-service.d.ts.map +1 -0
  8. package/dist/simple-email-service.js +416 -0
  9. package/dist/simple-email-service.js.map +1 -0
  10. package/dist/types.d.ts +311 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +33 -0
  13. package/dist/types.js.map +1 -0
  14. package/package.json +53 -22
  15. package/.eslintrc.json +0 -112
  16. package/.flake8 +0 -18
  17. package/.github/workflows/ci.yml +0 -300
  18. package/EXTRACTION_SUMMARY.md +0 -265
  19. package/IMPLEMENTATION_STATUS.md +0 -159
  20. package/OPEN_SOURCE_SETUP.md +0 -420
  21. package/PACKAGE_USAGE.md +0 -471
  22. package/examples/fastapi-example/main.py +0 -257
  23. package/examples/nextjs-example/next-env.d.ts +0 -13
  24. package/examples/nextjs-example/package.json +0 -26
  25. package/examples/nextjs-example/pages/admin/templates.tsx +0 -157
  26. package/examples/nextjs-example/tsconfig.json +0 -28
  27. package/packages/core/package.json +0 -70
  28. package/packages/core/rollup.config.js +0 -37
  29. package/packages/core/specification.md +0 -416
  30. package/packages/core/src/adapters/supabase.ts +0 -291
  31. package/packages/core/src/core/scheduler.ts +0 -356
  32. package/packages/core/src/core/template-manager.ts +0 -388
  33. package/packages/core/src/index.ts +0 -30
  34. package/packages/core/src/providers/base.ts +0 -104
  35. package/packages/core/src/providers/sendgrid.ts +0 -368
  36. package/packages/core/src/types/provider.ts +0 -91
  37. package/packages/core/src/types/scheduled.ts +0 -78
  38. package/packages/core/src/types/template.ts +0 -97
  39. package/packages/core/tsconfig.json +0 -23
  40. package/packages/python/README.md +0 -106
  41. package/packages/python/email_template_manager/__init__.py +0 -66
  42. package/packages/python/email_template_manager/config.py +0 -98
  43. package/packages/python/email_template_manager/core/magic_links.py +0 -245
  44. package/packages/python/email_template_manager/core/manager.py +0 -344
  45. package/packages/python/email_template_manager/core/scheduler.py +0 -473
  46. package/packages/python/email_template_manager/exceptions.py +0 -67
  47. package/packages/python/email_template_manager/models/magic_link.py +0 -59
  48. package/packages/python/email_template_manager/models/scheduled.py +0 -78
  49. package/packages/python/email_template_manager/models/template.py +0 -90
  50. package/packages/python/email_template_manager/providers/aws_ses.py +0 -44
  51. package/packages/python/email_template_manager/providers/base.py +0 -94
  52. package/packages/python/email_template_manager/providers/sendgrid.py +0 -325
  53. package/packages/python/email_template_manager/providers/smtp.py +0 -44
  54. package/packages/python/pyproject.toml +0 -133
  55. package/packages/python/setup.py +0 -93
  56. package/packages/python/specification.md +0 -930
  57. package/packages/react/README.md +0 -13
  58. package/packages/react/package.json +0 -105
  59. package/packages/react/rollup.config.js +0 -37
  60. package/packages/react/specification.md +0 -569
  61. package/packages/react/src/index.ts +0 -20
  62. package/packages/react/tsconfig.json +0 -24
  63. package/src/index.js +0 -1
  64. package/test_package.py +0 -125
@@ -1,106 +0,0 @@
1
- # email-template-manager (Python SDK)
2
-
3
- A comprehensive Python SDK for email template management, providing both low-level components and high-level integrations for FastAPI, Django, and Flask applications.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- # Basic installation
9
- pip install email-template-manager
10
-
11
- # With FastAPI support
12
- pip install email-template-manager[fastapi]
13
-
14
- # With Django support
15
- pip install email-template-manager[django]
16
-
17
- # With all providers
18
- pip install email-template-manager[sendgrid,ses,mailgun]
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ### Basic Usage
24
-
25
- ```python
26
- from email_template_manager import EmailTemplateManager, SendGridProvider, EmailScheduler
27
-
28
- # Initialize components
29
- template_manager = EmailTemplateManager("postgresql://user:pass@localhost/db")
30
- email_provider = SendGridProvider({
31
- "api_key": "your-sendgrid-key",
32
- "from_email": "noreply@yourapp.com"
33
- })
34
- scheduler = EmailScheduler(template_manager, email_provider)
35
-
36
- # Create a template
37
- template = await template_manager.create_template({
38
- "name": "Welcome Email",
39
- "subject": "Welcome {{name}}!",
40
- "html_body": "<h1>Welcome {{name}}</h1><p>Thanks for joining!</p>",
41
- "text_body": "Welcome {{name}}!\n\nThanks for joining!",
42
- "variables": [
43
- {"name": "name", "type": "text", "required": True}
44
- ]
45
- })
46
-
47
- # Schedule an email
48
- scheduled = await scheduler.schedule_email({
49
- "template_id": template.id,
50
- "recipient_email": "user@example.com",
51
- "variables": {"name": "John Doe"},
52
- "trigger_type": "immediate"
53
- })
54
- ```
55
-
56
- ### FastAPI Integration
57
-
58
- ```python
59
- from fastapi import FastAPI
60
- from email_template_manager.integrations.fastapi import create_email_router
61
-
62
- app = FastAPI()
63
-
64
- # Add email management routes
65
- email_router = create_email_router(
66
- database_url="postgresql://...",
67
- email_provider_config={
68
- "provider": "sendgrid",
69
- "api_key": "your-key",
70
- "from_email": "noreply@yourapp.com"
71
- }
72
- )
73
- app.include_router(email_router, prefix="/api")
74
- ```
75
-
76
- ## Core Features
77
-
78
- - **Template Management** - Create, update, and manage email templates with variables
79
- - **Email Scheduling** - Schedule emails based on events or specific times
80
- - **Magic Links** - Generate secure, expiring links for authentication
81
- - **Multiple Providers** - Support for SendGrid, AWS SES, Mailgun, and more
82
- - **Framework Integration** - Ready-to-use integrations for FastAPI, Django, Flask
83
- - **Async Support** - Full async/await support throughout
84
- - **Type Safety** - Complete TypeScript-style type hints
85
-
86
- ## Provider Support
87
-
88
- - SendGrid
89
- - AWS SES
90
- - Mailgun
91
- - Postmark
92
- - Generic SMTP
93
-
94
- ## Framework Integrations
95
-
96
- - FastAPI (async routes and background tasks)
97
- - Django (views and admin integration)
98
- - Flask (blueprints and extensions)
99
-
100
- ## Documentation
101
-
102
- Full documentation available at: https://email-template-manager.readthedocs.io
103
-
104
- ## License
105
-
106
- MIT License - see LICENSE file for details.
@@ -1,66 +0,0 @@
1
- """
2
- Copyright (c) 2025 Bernier LLC
3
-
4
- This file is licensed to the client under a limited-use license.
5
- The client may use and modify this code *only within the scope of the project it was delivered for*.
6
- Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
- """
8
-
9
- """
10
- Email Template Manager - Universal email template management library
11
- """
12
-
13
- __version__ = "1.0.0"
14
-
15
- # Configuration
16
- from .config import EmailManagerConfig
17
- from .core.magic_links import MagicLinkManager
18
-
19
- # Core classes
20
- from .core.manager import EmailTemplateManager
21
- from .core.scheduler import EmailScheduler
22
-
23
- # Exceptions
24
- from .exceptions import (
25
- ConfigurationError,
26
- DeliveryError,
27
- EmailTemplateManagerError,
28
- TemplateNotFoundError,
29
- ValidationError,
30
- )
31
- from .models.magic_link import MagicLink
32
- from .models.scheduled import ScheduledEmail
33
-
34
- # Data models
35
- from .models.template import EmailTemplate, TemplateVariable
36
- from .providers.aws_ses import SESProvider
37
-
38
- # Email providers
39
- from .providers.base import EmailProvider
40
- from .providers.sendgrid import SendGridProvider
41
- from .providers.smtp import SMTPProvider
42
-
43
- __all__ = [
44
- # Core classes
45
- "EmailTemplateManager",
46
- "EmailScheduler",
47
- "MagicLinkManager",
48
- # Models
49
- "EmailTemplate",
50
- "TemplateVariable",
51
- "ScheduledEmail",
52
- "MagicLink",
53
- # Providers
54
- "EmailProvider",
55
- "SendGridProvider",
56
- "SESProvider",
57
- "SMTPProvider",
58
- # Configuration
59
- "EmailManagerConfig",
60
- # Exceptions
61
- "EmailTemplateManagerError",
62
- "TemplateNotFoundError",
63
- "ValidationError",
64
- "DeliveryError",
65
- "ConfigurationError",
66
- ]
@@ -1,98 +0,0 @@
1
- """
2
- Copyright (c) 2025 Bernier LLC
3
-
4
- This file is licensed to the client under a limited-use license.
5
- The client may use and modify this code *only within the scope of the project it was delivered for*.
6
- Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
- """
8
-
9
- """
10
- Configuration models for email template manager
11
- """
12
-
13
- from typing import Any, Dict, List, Literal, Optional
14
-
15
- from pydantic import BaseModel, Field
16
-
17
-
18
- class DatabaseConfig(BaseModel):
19
- """Database configuration"""
20
-
21
- url: str
22
- echo: bool = False
23
- pool_size: int = 5
24
- max_overflow: int = 10
25
- pool_timeout: int = 30
26
-
27
-
28
- class EmailProviderConfig(BaseModel):
29
- """Email provider configuration"""
30
-
31
- provider: Literal["sendgrid", "ses", "mailgun", "postmark", "smtp"]
32
- api_key: Optional[str] = None
33
- region: Optional[str] = None
34
- endpoint: Optional[str] = None
35
- from_email: str
36
- from_name: Optional[str] = None
37
- reply_to: Optional[str] = None
38
- webhook_url: Optional[str] = None
39
- webhook_secret: Optional[str] = None
40
- sandbox: bool = False
41
-
42
-
43
- class TemplateConfig(BaseModel):
44
- """Template configuration"""
45
-
46
- default_from_email: str
47
- default_from_name: Optional[str] = None
48
- liquid_engine: Dict[str, Any] = Field(default_factory=dict)
49
- cache_templates: bool = True
50
- cache_ttl_seconds: int = 3600
51
-
52
-
53
- class SchedulingConfig(BaseModel):
54
- """Scheduling configuration"""
55
-
56
- batch_size: int = 50
57
- default_max_retries: int = 3
58
- retry_delay_minutes: List[int] = Field(default_factory=lambda: [5, 15, 60])
59
- processing_interval_minutes: int = 5
60
- max_concurrent_sends: int = 10
61
-
62
-
63
- class SecurityConfig(BaseModel):
64
- """Security configuration"""
65
-
66
- encryption_key: str
67
- token_length: int = 32
68
- default_expiry_hours: int = 24
69
- hash_algorithm: str = "sha256"
70
-
71
-
72
- class LoggingConfig(BaseModel):
73
- """Logging configuration"""
74
-
75
- level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
76
- format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
77
- include_payload: bool = False
78
- log_to_file: bool = False
79
- log_file_path: Optional[str] = None
80
-
81
-
82
- class EmailManagerConfig(BaseModel):
83
- """Main email manager configuration"""
84
-
85
- database: DatabaseConfig
86
- email_provider: EmailProviderConfig
87
- templates: TemplateConfig
88
- scheduling: SchedulingConfig = Field(default_factory=SchedulingConfig)
89
- security: SecurityConfig
90
- logging: LoggingConfig = Field(default_factory=LoggingConfig)
91
-
92
- # Optional feature flags
93
- enable_webhooks: bool = True
94
- enable_analytics: bool = False
95
- enable_rate_limiting: bool = False
96
-
97
- # Custom metadata
98
- metadata: Dict[str, Any] = Field(default_factory=dict)
@@ -1,245 +0,0 @@
1
- """
2
- Copyright (c) 2025 Bernier LLC
3
-
4
- This file is licensed to the client under a limited-use license.
5
- The client may use and modify this code *only within the scope of the project it was delivered for*.
6
- Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
- """
8
-
9
- """
10
- Magic link manager implementation
11
- """
12
-
13
- import logging
14
- import secrets
15
- import uuid
16
- from datetime import datetime, timedelta
17
- from typing import Any, Dict, Optional
18
-
19
- from cryptography.fernet import Fernet
20
- from sqlalchemy import JSON, Boolean, Column, DateTime, String
21
-
22
- from ..exceptions import EncryptionError
23
- from ..models.magic_link import MagicLink
24
- from .manager import Base, EmailTemplateManager
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- class MagicLinkModel(Base):
30
- """SQLAlchemy model for magic links"""
31
-
32
- __tablename__ = "magic_links"
33
-
34
- id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
35
- token = Column(String, unique=True, nullable=False)
36
- email = Column(String, nullable=False)
37
- type = Column(String, nullable=False)
38
- payload = Column(JSON, default={})
39
- expires_at = Column(DateTime, nullable=False)
40
- used_at = Column(DateTime, nullable=True)
41
- is_used = Column(Boolean, default=False)
42
- link_metadata = Column(JSON, default={})
43
- created_at = Column(DateTime, default=datetime.utcnow)
44
-
45
-
46
- class MagicLinkManager:
47
- """Manages magic links for authentication and secure actions"""
48
-
49
- def __init__(self, template_manager: EmailTemplateManager, encryption_key: str):
50
- """
51
- Initialize magic link manager
52
-
53
- Args:
54
- template_manager: Template manager instance
55
- encryption_key: Key for encrypting/decrypting payloads
56
- """
57
- self.template_manager = template_manager
58
- self.fernet = Fernet(
59
- encryption_key.encode()
60
- if isinstance(encryption_key, str)
61
- else encryption_key
62
- )
63
-
64
- # Ensure tables are created
65
- Base.metadata.create_all(self.template_manager.engine)
66
-
67
- async def generate_magic_link(
68
- self,
69
- email: str,
70
- link_type: str,
71
- payload: Dict[str, Any],
72
- expires_in_hours: int = 24,
73
- ) -> MagicLink:
74
- """Generate a new magic link"""
75
- try:
76
- # Generate secure token
77
- token = secrets.token_urlsafe(32)
78
-
79
- # Encrypt payload
80
- encrypted_payload = self._encrypt_payload(payload)
81
-
82
- # Calculate expiry
83
- expires_at = datetime.utcnow() + timedelta(hours=expires_in_hours)
84
-
85
- with self.template_manager.get_session() as session:
86
- magic_link_model = MagicLinkModel(
87
- id=str(uuid.uuid4()),
88
- token=token,
89
- email=email,
90
- type=link_type,
91
- payload=encrypted_payload,
92
- expires_at=expires_at,
93
- is_used=False,
94
- link_metadata={},
95
- )
96
-
97
- session.add(magic_link_model)
98
- session.commit()
99
- session.refresh(magic_link_model)
100
-
101
- return self._model_to_magic_link(magic_link_model)
102
-
103
- except Exception as e:
104
- logger.error(f"Failed to generate magic link: {str(e)}")
105
- raise EncryptionError(f"Failed to generate magic link: {str(e)}")
106
-
107
- async def validate_magic_link(self, token: str) -> Optional[MagicLink]:
108
- """Validate a magic link token"""
109
- try:
110
- with self.template_manager.get_session() as session:
111
- magic_link_model = (
112
- session.query(MagicLinkModel)
113
- .filter(MagicLinkModel.token == token)
114
- .first()
115
- )
116
-
117
- if not magic_link_model:
118
- return None
119
-
120
- # Check if expired
121
- if magic_link_model.expires_at < datetime.utcnow():
122
- return None
123
-
124
- # Check if already used
125
- if magic_link_model.is_used:
126
- return None
127
-
128
- return self._model_to_magic_link(magic_link_model)
129
-
130
- except Exception as e:
131
- logger.error(f"Failed to validate magic link: {str(e)}")
132
- return None
133
-
134
- async def use_magic_link(self, token: str) -> Optional[MagicLink]:
135
- """Use a magic link (marks it as used)"""
136
- try:
137
- magic_link = await self.validate_magic_link(token)
138
- if not magic_link:
139
- return None
140
-
141
- with self.template_manager.get_session() as session:
142
- magic_link_model = (
143
- session.query(MagicLinkModel)
144
- .filter(MagicLinkModel.token == token)
145
- .first()
146
- )
147
-
148
- if magic_link_model:
149
- magic_link_model.is_used = True # type: ignore
150
- magic_link_model.used_at = datetime.utcnow() # type: ignore
151
- session.commit()
152
-
153
- return self._model_to_magic_link(magic_link_model)
154
-
155
- return None
156
-
157
- except Exception as e:
158
- logger.error(f"Failed to use magic link: {str(e)}")
159
- return None
160
-
161
- async def revoke_magic_link(self, token: str) -> bool:
162
- """Revoke a magic link"""
163
- try:
164
- with self.template_manager.get_session() as session:
165
- magic_link_model = (
166
- session.query(MagicLinkModel)
167
- .filter(MagicLinkModel.token == token)
168
- .first()
169
- )
170
-
171
- if magic_link_model:
172
- magic_link_model.is_used = True # type: ignore
173
- magic_link_model.used_at = datetime.utcnow() # type: ignore
174
- session.commit()
175
- return True
176
-
177
- return False
178
-
179
- except Exception as e:
180
- logger.error(f"Failed to revoke magic link: {str(e)}")
181
- return False
182
-
183
- async def cleanup_expired_links(self) -> int:
184
- """Remove expired magic links"""
185
- try:
186
- with self.template_manager.get_session() as session:
187
- expired_count = (
188
- session.query(MagicLinkModel)
189
- .filter(MagicLinkModel.expires_at < datetime.utcnow())
190
- .count()
191
- )
192
-
193
- session.query(MagicLinkModel).filter(
194
- MagicLinkModel.expires_at < datetime.utcnow()
195
- ).delete()
196
-
197
- session.commit()
198
- return expired_count
199
-
200
- except Exception as e:
201
- logger.error(f"Failed to cleanup expired links: {str(e)}")
202
- return 0
203
-
204
- def _encrypt_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
205
- """Encrypt sensitive payload data"""
206
- try:
207
- import json
208
-
209
- payload_json = json.dumps(payload)
210
- encrypted_data = self.fernet.encrypt(payload_json.encode())
211
- return {"encrypted": encrypted_data.decode()}
212
- except Exception as e:
213
- raise EncryptionError(f"Failed to encrypt payload: {str(e)}")
214
-
215
- def _decrypt_payload(self, encrypted_payload: Dict[str, Any]) -> Dict[str, Any]:
216
- """Decrypt payload data"""
217
- try:
218
- import json
219
-
220
- encrypted_data = encrypted_payload.get("encrypted", "")
221
- decrypted_data = self.fernet.decrypt(encrypted_data.encode())
222
- return json.loads(decrypted_data.decode())
223
- except Exception as e:
224
- raise EncryptionError(f"Failed to decrypt payload: {str(e)}")
225
-
226
- def _model_to_magic_link(self, model: MagicLinkModel) -> MagicLink:
227
- """Convert SQLAlchemy model to Pydantic model"""
228
- # Decrypt payload
229
- try:
230
- decrypted_payload = self._decrypt_payload(model.payload) # type: ignore
231
- except Exception:
232
- decrypted_payload = {}
233
-
234
- return MagicLink(
235
- id=model.id, # type: ignore
236
- token=model.token, # type: ignore
237
- email=model.email, # type: ignore
238
- type=model.type, # type: ignore
239
- payload=decrypted_payload,
240
- expires_at=model.expires_at, # type: ignore
241
- used_at=model.used_at, # type: ignore
242
- is_used=model.is_used, # type: ignore
243
- metadata=model.link_metadata, # type: ignore
244
- created_at=model.created_at, # type: ignore
245
- )