abs-email-core 0.1.1
Core email utilities with pluggable provider support and Jinja2 template rendering
License: MIT
Requires Python: <4.0,>=3.13

# abs-email-core

Core email utilities with pluggable provider support, Jinja2 template rendering, and Quill HTML sanitization.

## Installation

```bash
pip install abs-email-core

# With SendGrid support
pip install "abs-email-core[sendgrid]"
```

## Quick Start

```python
from abs_email_core import SendGridProvider, TemplateService, SendMailRequest

# Initialize provider and template service
provider = SendGridProvider(api_key="your-api-key", from_email="noreply@example.com")
templates = TemplateService(template_dir="/path/to/your/templates")

# Render and send an HTML email
html = templates.render_email_template(title="Welcome", content="<p>Hello!</p>")
request = SendMailRequest(to=["user@example.com"], subject="Welcome", body=html)
provider.send_html_email(request)
```

## Schemas

### `SendMailRequest`

| Field | Type | Required | Description |
|---|---|---|---|
| `to` | `List[EmailStr]` | Yes | Recipient email addresses |
| `subject` | `str` | Yes | Email subject line |
| `body` | `str` | Yes | Email body (plain text or HTML) |
| `from_email` | `EmailStr` | No | Override the provider's default from address |
| `cc` | `List[EmailStr]` | No | CC recipients (default: `[]`) |
| `bcc` | `List[EmailStr]` | No | BCC recipients (default: `[]`) |

### `FileAttachment`

| Field | Type | Required | Description |
|---|---|---|---|
| `file_name` | `str` | Yes | Display name of the attachment |
| `url` | `str` | Yes | URL to the file |
| `file_id` | `str` | No | Optional identifier for the file |

## Providers

### SendGridProvider

Requires the `sendgrid` extra: `pip install "abs-email-core[sendgrid]"`

```python
from abs_email_core import SendGridProvider, SendMailRequest

provider = SendGridProvider(api_key="SG.xxx", from_email="noreply@example.com")

# Plain text email
request = SendMailRequest(to=["user@example.com"], subject="Hello", body="Hi there!")
provider.send_email(request)

# HTML email
request = SendMailRequest(to=["user@example.com"], subject="Hello", body="<h1>Hi!</h1>")
provider.send_html_email(request)

# With file attachments (UploadFile-like objects)
provider.send_email(request, attachments=[upload_file])
```

### Custom Provider

Implement the `EmailProvider` abstract class to add support for other email services:

```python
from abs_email_core.provider.base import EmailProvider

class SMTPProvider(EmailProvider):
    def __init__(self, host: str, port: int, username: str, password: str, from_email: str):
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.from_email = from_email

    def send_email(self, request, attachments=None) -> bool:
        # SMTP implementation
        ...

    def send_html_email(self, request, attachments=None) -> bool:
        # SMTP HTML implementation
        ...
```

## Template Service

`TemplateService` uses Jinja2 to render email templates. The `template_dir` parameter is **required** — point it to the directory containing your Jinja2 template files.

```python
from abs_email_core import TemplateService

templates = TemplateService(template_dir="/path/to/your/templates")

# Render any template by filename
html = templates.render_template("welcome.html", name="John", code="ABC123")

# Shortcut for the bundled email_template.html (must exist in your template_dir)
html = templates.render_email_template(
    title="Welcome",
    content="<p>Your account is ready.</p>",
    footer="GovAssist Inc.",    # optional: footer text
    logo_url="https://...",     # optional: header/footer logo image URL
    attachments=[               # optional: renders download links
        {"file_name": "report.pdf", "url": "https://example.com/report.pdf"},
    ],
)
```

A default `email_template.html` is shipped with the package under `abs_email_core/templates/` — copy it into your service's template directory or use it as a starting point for customization.

## Quill HTML Sanitization

When email content originates from a [Quill](https://quilljs.com/) rich-text editor, the raw HTML often contains structural issues. `TemplateService` provides built-in sanitization that fixes:

- Removes internal `ql-ui` spans injected by Quill
- Normalizes list tags based on `data-list` attribute (`bullet` → `<ul>`, `ordered` → `<ol>`)
- Splits mixed lists (bullet + ordered items in one tag) into separate correct tags
- Merges consecutive same-type lists separated by `<br>` or whitespace
- Cleans `data-list` attributes from `<li>` elements
- Fixes fragmented paragraphs (e.g. `<p>Dear </p>Name<p>,</p>` → `<p>Dear Name,</p>`)

### Usage

```python
# Option 1: Sanitize during template rendering
html = templates.render_email_template(
    title="Welcome",
    content=quill_html,
    sanitize_quill=True,  # enables Quill HTML sanitization before rendering
)

# Option 2: Sanitize standalone (without rendering a template)
clean_html = templates.sanitize_quill_html(quill_html)
```

## Utilities

### Parse Emails

`TemplateService` includes a helper to parse email addresses from strings or lists:

```python
# From comma/newline-separated string
emails = templates._parse_emails("a@example.com, b@example.com\nc@example.com")
# → ["a@example.com", "b@example.com", "c@example.com"]

# From list (passthrough)
emails = templates._parse_emails(["a@example.com", "b@example.com"])
# → ["a@example.com", "b@example.com"]
```

## Dependency Injection (with dependency-injector)

```python
from dependency_injector import containers, providers
from abs_email_core import SendGridProvider, TemplateService

class Container(containers.DeclarativeContainer):
    email_provider = providers.Factory(
        SendGridProvider,
        api_key=configs.SENDGRID_API_KEY,
        from_email=configs.EMAIL_FROM,
    )
    template_service = providers.Factory(
        TemplateService,
        template_dir=configs.EMAIL_TEMPLATE_DIR,
    )
```