diff --git a/.gitignore b/.gitignore index 8f1e3b8..b3e1f80 100644 --- a/.gitignore +++ b/.gitignore @@ -190,3 +190,5 @@ Thumbs.db api_keys.txt secrets.json .api_key + +setup-dev-env.ps1 diff --git a/examples/email_example.py b/examples/email_example.py index 969d309..5105dee 100644 --- a/examples/email_example.py +++ b/examples/email_example.py @@ -1,6 +1,6 @@ import os -from devo_global_comms_python import DevoException +from devo_global_comms_python import DevoCommsClient, DevoException def main(): @@ -9,42 +9,72 @@ def main(): print("❌ Please set DEVO_API_KEY environment variable") return + client = DevoCommsClient(api_key=api_key) print("✅ Devo Email Client initialized successfully") print("=" * 60) try: - # Example 1: Send a simple email + # Example: Send an email using the Email API print("📧 EMAIL SEND EXAMPLE") print("-" * 30) print("📤 Sending email...") - print("⚠️ This is a placeholder implementation.") - print(" Update this example when Email API is implemented.") - - # Placeholder email send - update when implementing Email resource - print(" ```python") - print(" email_response = client.email.send(") - print(" to='recipient@example.com',") - print(" subject='Test Email from Devo SDK',") - print(" body='This is a test email.',") - print(" html_body='

Test

This is a test email.

',") - print(" from_email='sender@yourdomain.com'") - print(" )") - print(" print(f'Email sent! ID: {email_response.id}')") - print(" ```") + email_response = client.email.send_email( + subject="Test Email from Devo SDK", + body="This is a test email sent using the Devo Global Communications Python SDK.", + sender="sender@example.com", + recipient="recipient@example.com", + ) + + print("✅ Email sent successfully!") + print(f" 📧 Message ID: {email_response.message_id}") + print(f" 📦 Bulk Email ID: {email_response.bulk_email_id}") + print(f" 📝 Subject: {email_response.subject}") + print(f" 📊 Status: {email_response.status}") + print(f" 💬 Message: {email_response.message}") + print(f" 🕐 Timestamp: {email_response.timestamp}") + print(f" ✅ Success: {email_response.success}") + + # Example with different content + print("\n📧 SENDING EMAIL WITH RICH CONTENT") + print("-" * 40) + + rich_email_response = client.email.send_email( + subject="🎉 Welcome to Devo Communications!", + body=( + "Dear valued customer,\n\n" + "Welcome to our service! We're excited to have you on board.\n\n" + "Best regards,\nThe Devo Team" + ), + sender="welcome@yourcompany.com", + recipient="newcustomer@example.com", + ) + + print("✅ Rich content email sent!") + print(f" 📧 Message ID: {rich_email_response.message_id}") + print(f" 📊 Status: {rich_email_response.status}") + print(f" ✅ Success: {rich_email_response.success}") except DevoException as e: print(f"❌ Email operation failed: {e}") + except Exception as e: + print(f"❌ Unexpected error: {e}") print("\n" + "=" * 60) print("📊 EMAIL EXAMPLE SUMMARY") print("-" * 30) - print("⚠️ This is a placeholder example for Email functionality.") - print("💡 To implement:") - print(" 1. Define Email API endpoints and specifications") - print(" 2. Create Email Pydantic models") - print(" 3. Implement EmailResource class") - print(" 4. Update this example with real functionality") + print("✅ Email API implementation complete!") + print("📤 Successfully demonstrated:") + print(" • Basic email sending") + print(" • Email with rich content and emojis") + print(" • Response parsing and status checking") + print(" • Error handling") + print("\n💡 Features available:") + print(" • Subject and body content") + print(" • Sender and recipient validation") + print(" • Message tracking with unique IDs") + print(" • Status monitoring") + print(" • Timestamp tracking") if __name__ == "__main__": diff --git a/src/devo_global_comms_python/models/email.py b/src/devo_global_comms_python/models/email.py index f23a04f..49eb408 100644 --- a/src/devo_global_comms_python/models/email.py +++ b/src/devo_global_comms_python/models/email.py @@ -4,15 +4,44 @@ from pydantic import BaseModel, Field +# Request Models +class EmailSendRequest(BaseModel): + """ + Request model for email send API. + + Used for POST /user-api/email/send + """ + + subject: str = Field(..., description="Email subject") + body: str = Field(..., description="Email body content") + sender: str = Field(..., description="Sender email address") + recipient: str = Field(..., description="Recipient email address") + + +# Response Models +class EmailSendResponse(BaseModel): + """ + Response model for email send API. + + Returned from POST /user-api/email/send + """ + + success: bool = Field(..., description="Whether the email was sent successfully") + message_id: str = Field(..., description="Unique message identifier") + bulk_email_id: str = Field(..., description="Bulk email identifier") + subject: str = Field(..., description="Email subject") + status: str = Field(..., description="Message status") + message: str = Field(..., description="Status message") + timestamp: datetime = Field(..., description="Timestamp of the response") + + class EmailAttachment(BaseModel): """Email attachment model.""" filename: str = Field(..., description="Attachment filename") content_type: str = Field(..., description="MIME content type") size: int = Field(..., description="File size in bytes") - content_id: Optional[str] = Field( - None, description="Content ID for inline attachments" - ) + content_id: Optional[str] = Field(None, description="Content ID for inline attachments") class EmailMessage(BaseModel): @@ -34,27 +63,17 @@ class EmailMessage(BaseModel): html_body: Optional[str] = Field(None, description="HTML email body") status: str = Field(..., description="Message status") direction: str = Field(..., description="Message direction (inbound/outbound)") - attachments: Optional[List[EmailAttachment]] = Field( - None, description="Email attachments" - ) + attachments: Optional[List[EmailAttachment]] = Field(None, description="Email attachments") error_code: Optional[str] = Field(None, description="Error code if failed") error_message: Optional[str] = Field(None, description="Error message if failed") - date_created: Optional[datetime] = Field( - None, description="Message creation timestamp" - ) + date_created: Optional[datetime] = Field(None, description="Message creation timestamp") date_sent: Optional[datetime] = Field(None, description="Message sent timestamp") - date_delivered: Optional[datetime] = Field( - None, description="Message delivered timestamp" - ) - date_opened: Optional[datetime] = Field( - None, description="Message opened timestamp" - ) + date_delivered: Optional[datetime] = Field(None, description="Message delivered timestamp") + date_opened: Optional[datetime] = Field(None, description="Message opened timestamp") date_clicked: Optional[datetime] = Field(None, description="Link clicked timestamp") - date_updated: Optional[datetime] = Field( - None, description="Message last updated timestamp" - ) + date_updated: Optional[datetime] = Field(None, description="Message last updated timestamp") metadata: Optional[Dict[str, Any]] = Field(None, description="Custom metadata") class Config: - allow_population_by_field_name = True + validate_by_name = True json_encoders = {datetime: lambda v: v.isoformat() if v else None} diff --git a/src/devo_global_comms_python/resources/email.py b/src/devo_global_comms_python/resources/email.py index 227c47b..c959aae 100644 --- a/src/devo_global_comms_python/resources/email.py +++ b/src/devo_global_comms_python/resources/email.py @@ -4,7 +4,7 @@ from .base import BaseResource if TYPE_CHECKING: - from ..models.email import EmailMessage + from ..models.email import EmailMessage, EmailSendResponse class EmailResource(BaseResource): @@ -12,14 +12,55 @@ class EmailResource(BaseResource): Email resource for sending and managing email messages. Example: - >>> message = client.email.send( - ... to="recipient@example.com", + >>> response = client.email.send_email( ... subject="Hello, World!", - ... body="This is a test email." + ... body="This is a test email.", + ... sender="sender@example.com", + ... recipient="recipient@example.com" ... ) - >>> print(message.id) + >>> print(response.message_id) """ + def send_email( + self, + subject: str, + body: str, + sender: str, + recipient: str, + ) -> "EmailSendResponse": + """ + Send an email using the exact API specification. + + Args: + subject: Email subject + body: Email body content + sender: Sender email address + recipient: Recipient email address + + Returns: + EmailSendResponse: The email send response + """ + # Validate inputs + subject = validate_required_string(subject, "subject") + body = validate_required_string(body, "body") + sender = validate_email(sender) + recipient = validate_email(recipient) + + # Prepare request data matching the exact API specification + data = { + "subject": subject, + "body": body, + "sender": sender, + "recipient": recipient, + } + + # Send request to the exact endpoint + response = self.client.post("email/send", json=data) + + from ..models.email import EmailSendResponse + + return EmailSendResponse.model_validate(response.json()) + def send( self, to: str, diff --git a/tests/test_email.py b/tests/test_email.py new file mode 100644 index 0000000..d61fc78 --- /dev/null +++ b/tests/test_email.py @@ -0,0 +1,298 @@ +from datetime import datetime +from unittest.mock import Mock + +import pytest + +from devo_global_comms_python.exceptions import DevoValidationException +from devo_global_comms_python.resources.email import EmailResource + + +class TestEmailResource: + """Test cases for the Email resource with new API implementation.""" + + @pytest.fixture + def email_resource(self, mock_client): + """Create an Email resource instance.""" + return EmailResource(mock_client) + + def test_send_email_success(self, email_resource): + """Test sending an email successfully using the new send API.""" + # Setup mock response matching the API spec + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": "email_123456789", + "bulk_email_id": "bulk_email_123", + "subject": "Hello World!", + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + # Test the send_email method + result = email_resource.send_email( + subject="Hello World!", + body="This is a test email!", + sender="sender@example.com", + recipient="recipient@example.com", + ) + + # Verify the request was made correctly + email_resource.client.post.assert_called_once_with( + "email/send", + json={ + "subject": "Hello World!", + "body": "This is a test email!", + "sender": "sender@example.com", + "recipient": "recipient@example.com", + }, + ) + + # Verify the response is correctly parsed + assert result.success is True + assert result.message_id == "email_123456789" + assert result.bulk_email_id == "bulk_email_123" + assert result.subject == "Hello World!" + assert result.status == "sent" + assert result.message == "Email sent successfully" + assert result.timestamp == datetime.fromisoformat("2019-08-28T06:25:26.715+00:00") + + def test_send_email_missing_subject(self, email_resource): + """Test that missing subject raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + email_resource.send_email( + subject="", + body="This is a test email!", + sender="sender@example.com", + recipient="recipient@example.com", + ) + assert "subject" in str(exc_info.value) + + def test_send_email_missing_body(self, email_resource): + """Test that missing body raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + email_resource.send_email( + subject="Hello World!", + body="", + sender="sender@example.com", + recipient="recipient@example.com", + ) + assert "body" in str(exc_info.value) + + def test_send_email_invalid_sender_email(self, email_resource): + """Test that invalid sender email raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + email_resource.send_email( + subject="Hello World!", + body="This is a test email!", + sender="invalid-email", + recipient="recipient@example.com", + ) + assert "sender" in str(exc_info.value) or "email" in str(exc_info.value) + + def test_send_email_invalid_recipient_email(self, email_resource): + """Test that invalid recipient email raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + email_resource.send_email( + subject="Hello World!", + body="This is a test email!", + sender="sender@example.com", + recipient="invalid-email", + ) + assert "recipient" in str(exc_info.value) or "email" in str(exc_info.value) + + def test_send_email_with_special_characters(self, email_resource): + """Test sending email with special characters in subject and body.""" + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": "email_special_123", + "bulk_email_id": "bulk_email_special_123", + "subject": "🎉 Special Test! @#$%^&*()", + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + special_subject = "🎉 Special Test! @#$%^&*()" + special_body = "This email contains special characters: àáâäæãåā €£¥₹" + + result = email_resource.send_email( + subject=special_subject, + body=special_body, + sender="test@example.com", + recipient="recipient@example.com", + ) + + # Verify the request was made with special characters + email_resource.client.post.assert_called_once_with( + "email/send", + json={ + "subject": special_subject, + "body": special_body, + "sender": "test@example.com", + "recipient": "recipient@example.com", + }, + ) + + assert result.success is True + assert result.subject == special_subject + + def test_send_email_long_content(self, email_resource): + """Test sending email with long subject and body.""" + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": "email_long_123", + "bulk_email_id": "bulk_email_long_123", + "subject": "A" * 100, + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + long_subject = "A" * 100 + long_body = "B" * 1000 + + result = email_resource.send_email( + subject=long_subject, + body=long_body, + sender="sender@example.com", + recipient="recipient@example.com", + ) + + email_resource.client.post.assert_called_once_with( + "email/send", + json={ + "subject": long_subject, + "body": long_body, + "sender": "sender@example.com", + "recipient": "recipient@example.com", + }, + ) + + assert result.success is True + + def test_send_email_api_error_handling(self, email_resource): + """Test that API errors are properly handled.""" + # Mock an API error response + mock_response = Mock() + mock_response.json.return_value = { + "success": False, + "message_id": "", + "bulk_email_id": "", + "subject": "", + "status": "failed", + "message": "Invalid recipient email address", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + result = email_resource.send_email( + subject="Test Subject", + body="Test Body", + sender="sender@example.com", + recipient="recipient@example.com", + ) + + assert result.success is False + assert result.status == "failed" + assert result.message == "Invalid recipient email address" + + def test_send_email_different_email_formats(self, email_resource): + """Test sending emails with different valid email formats.""" + test_cases = [ + ("user@domain.com", "user@domain.com"), + ("user.name@domain.co.uk", "user.name@domain.co.uk"), + ("user+tag@domain.org", "user+tag@domain.org"), + ("user123@sub.domain.com", "user123@sub.domain.com"), + ] + + for sender, recipient in test_cases: + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": f"email_{sender.replace('@', '_').replace('.', '_')}", + "bulk_email_id": "bulk_email_format_test", + "subject": "Format Test", + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + result = email_resource.send_email( + subject="Format Test", + body="Testing different email formats", + sender=sender, + recipient=recipient, + ) + + assert result.success is True + assert result.status == "sent" + + def test_send_email_unicode_content(self, email_resource): + """Test sending email with Unicode content.""" + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": "email_unicode_123", + "bulk_email_id": "bulk_email_unicode_123", + "subject": "Unicode Test: 你好世界", + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + unicode_subject = "Unicode Test: 你好世界" + unicode_body = "Unicode content: Здравствуй мир! مرحبا بالعالم" + + result = email_resource.send_email( + subject=unicode_subject, + body=unicode_body, + sender="sender@example.com", + recipient="recipient@example.com", + ) + + email_resource.client.post.assert_called_once_with( + "email/send", + json={ + "subject": unicode_subject, + "body": unicode_body, + "sender": "sender@example.com", + "recipient": "recipient@example.com", + }, + ) + + assert result.success is True + + def test_send_email_whitespace_handling(self, email_resource): + """Test that whitespace in subject and body is handled correctly.""" + mock_response = Mock() + mock_response.json.return_value = { + "success": True, + "message_id": "email_whitespace_123", + "bulk_email_id": "bulk_email_whitespace_123", + "subject": " Test Subject ", + "status": "sent", + "message": "Email sent successfully", + "timestamp": "2019-08-28T06:25:26.715Z", + } + email_resource.client.post.return_value = mock_response + + # Test that emails with whitespace in subject/body are accepted + # but email addresses must be properly formatted + result = email_resource.send_email( + subject=" Test Subject ", + body=" Test Body ", + sender="sender@example.com", + recipient="recipient@example.com", + ) + + # Should call with the exact values provided + assert email_resource.client.post.called + assert result.success is True