feat: implement email service backend with Hostinger Mail API integration
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 46s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 46s
This commit is contained in:
parent
35e1e09401
commit
2a6a201e0b
9 changed files with 150 additions and 83 deletions
|
|
@ -1,28 +0,0 @@
|
|||
import boto3
|
||||
import json
|
||||
import logging
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_secret(secret_name, region_name="ap-south-2"):
|
||||
session = boto3.session.Session()
|
||||
client = session.client(
|
||||
service_name='secretsmanager',
|
||||
region_name=region_name
|
||||
)
|
||||
|
||||
try:
|
||||
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
||||
except ClientError as e:
|
||||
logger.error(f"Error fetching secret {secret_name}: {e}")
|
||||
return {}
|
||||
|
||||
if 'SecretString' in get_secret_value_response:
|
||||
secret = get_secret_value_response['SecretString']
|
||||
try:
|
||||
return json.loads(secret)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Secret {secret_name} is not valid JSON")
|
||||
return {}
|
||||
return {}
|
||||
|
|
@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/6.1/ref/settings/
|
|||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from core.aws_secrets import get_secret
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
|
@ -93,15 +92,14 @@ if DEBUG:
|
|||
}
|
||||
}
|
||||
else:
|
||||
db_secret = get_secret("arn:aws:secretsmanager:ap-south-2:764709663363:secret:emailservicebetadb-ZXQg5w")
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': db_secret.get('database', 'emailservice'),
|
||||
'USER': db_secret.get('username', 'vignesh'),
|
||||
'PASSWORD': db_secret.get('password', ''),
|
||||
'HOST': os.environ.get('DB_HOST', db_secret.get('host', '16.113.106.152')),
|
||||
'PORT': db_secret.get('port', '5432'),
|
||||
'NAME': os.environ.get('DB_NAME', 'emailservice'),
|
||||
'USER': os.environ.get('DB_USER', 'vignesh'),
|
||||
'PASSWORD': os.environ.get('DB_PASSWORD', 'vtechnosoft@123A'),
|
||||
'HOST': os.environ.get('DB_HOST', '16.113.106.152'),
|
||||
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +149,16 @@ MEDIA_ROOT = BASE_DIR / 'media'
|
|||
# Email
|
||||
# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration
|
||||
|
||||
email_secret = get_secret("arn:aws:secretsmanager:ap-south-2:764709663363:secret:emailcredentials-DOIQtc")
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = email_secret.get('smtp')
|
||||
EMAIL_PORT = int(email_secret.get('smtp_port', 465) or 465)
|
||||
EMAIL_HOST = 'smtp.hostinger.com'
|
||||
EMAIL_PORT = 465
|
||||
EMAIL_USE_SSL = True
|
||||
EMAIL_HOST_USER = email_secret.get('username')
|
||||
EMAIL_HOST_PASSWORD = email_secret.get('password')
|
||||
EMAIL_HOST_USER = 'info@tradhox.com'
|
||||
EMAIL_HOST_PASSWORD = ''
|
||||
|
||||
# Hostinger Mail API Python SDK Configuration
|
||||
HOSTINGER_BEARER_TOKEN = '5bee28a2a982c7713f9254014fead914ab6ef18f4cc0685ef1c9919410e9fc82'
|
||||
HOSTINGER_MAILBOX_RESOURCE_ID = 'ACd8456f8f9be7a07b7f1bccb636d6'
|
||||
|
||||
# Logging configuration
|
||||
import os
|
||||
|
|
|
|||
18
logs/app.log
Normal file
18
logs/app.log
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
INFO 2026-10-01 18:31:42,978 autoreload 14240 23976 Watching for file changes with StatReloader
|
||||
INFO 2026-10-01 18:31:53,221 log 14240 10268 "GET / HTTP/1.1" 200 2417
|
||||
WARNING 2026-10-01 18:31:53,480 log 14240 10268 Not Found: /favicon.ico
|
||||
WARNING 2026-10-01 18:31:53,481 log 14240 10268 "GET /favicon.ico HTTP/1.1" 404 3318
|
||||
INFO 2026-10-01 18:31:55,792 log 14240 10268 "GET /test-email/ HTTP/1.1" 200 2688
|
||||
INFO 2026-10-01 18:32:07,309 services 14240 10268 Hostinger SDK: Attempting to send email to vichunice@gmail.com via mailbox ACd8456f8f9be7a07b7f1bccb636d6
|
||||
INFO 2026-10-01 18:32:15,970 services 14240 10268 Hostinger SDK: Successfully sent email to vichunice@gmail.com
|
||||
INFO 2026-10-01 18:32:15,971 views 14240 10268 Successfully sent 'verification' email to vichunice@gmail.com
|
||||
INFO 2026-10-01 18:32:15,979 log 14240 10268 "POST /test-email/ HTTP/1.1" 302 0
|
||||
INFO 2026-10-01 18:32:15,982 log 14240 10268 "GET / HTTP/1.1" 200 2417
|
||||
INFO 2026-10-01 18:32:20,907 log 14240 10268 "GET /logs/ HTTP/1.1" 200 2518
|
||||
INFO 2026-10-01 18:35:59,965 log 14240 10268 "GET / HTTP/1.1" 200 2417
|
||||
INFO 2026-10-01 18:36:00,975 log 14240 10268 "GET /test-email/ HTTP/1.1" 200 2906
|
||||
INFO 2026-10-01 18:36:08,813 services 14240 10268 Hostinger SDK: Attempting to send email to vichunice@gmail.com via mailbox ACd8456f8f9be7a07b7f1bccb636d6
|
||||
INFO 2026-10-01 18:36:10,222 services 14240 10268 Hostinger SDK: Successfully sent email to vichunice@gmail.com
|
||||
INFO 2026-10-01 18:36:10,223 views 14240 10268 Successfully sent 'verification' email to vichunice@gmail.com
|
||||
INFO 2026-10-01 18:36:10,232 log 14240 10268 "POST /test-email/ HTTP/1.1" 302 0
|
||||
INFO 2026-10-01 18:36:10,235 log 14240 10268 "GET / HTTP/1.1" 200 2417
|
||||
0
logs/error.log
Normal file
0
logs/error.log
Normal file
|
|
@ -1,11 +1,11 @@
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from .models import EmailLog
|
||||
from .services import send_email_message
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -41,16 +41,12 @@ class SendEmailAPIView(APIView):
|
|||
html_content = render_to_string(template_path, context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject,
|
||||
text_content,
|
||||
settings.EMAIL_HOST_USER,
|
||||
[recipient]
|
||||
send_email_message(
|
||||
recipient=recipient,
|
||||
subject=subject,
|
||||
text_content=text_content,
|
||||
html_content=html_content
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
|
||||
logger.info(f"API: Attempting to send '{template_name}' email to {recipient} via {getattr(settings, 'EMAIL_HOST', 'unknown')}")
|
||||
msg.send(fail_silently=False)
|
||||
logger.info(f"API: Successfully sent '{template_name}' email to {recipient}")
|
||||
|
||||
EmailLog.objects.create(
|
||||
|
|
|
|||
103
mailer/services.py
Normal file
103
mailer/services.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import logging
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
import hostinger_mail_api
|
||||
from hostinger_mail_api import Configuration, ApiClient, SendApi, AccountApi, V1SendRequest, ApiException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def send_email_via_hostinger(
|
||||
recipient: str,
|
||||
subject: str,
|
||||
text_content: str,
|
||||
html_content: str = None,
|
||||
bearer_token: str = None,
|
||||
mailbox_resource_id: str = None,
|
||||
display_name: str = None
|
||||
):
|
||||
"""
|
||||
Sends an email using the Hostinger Mail API Python SDK.
|
||||
"""
|
||||
token = bearer_token or getattr(settings, 'HOSTINGER_BEARER_TOKEN', '5bee28a2a982c7713f9254014fead914ab6ef18f4cc0685ef1c9919410e9fc82')
|
||||
|
||||
if not token:
|
||||
raise ValueError("Hostinger bearer token is missing. Please configure HOSTINGER_BEARER_TOKEN.")
|
||||
|
||||
config = Configuration(access_token=token)
|
||||
|
||||
with ApiClient(config) as api_client:
|
||||
target_mailbox_id = mailbox_resource_id or getattr(settings, 'HOSTINGER_MAILBOX_RESOURCE_ID', 'ACd8456f8f9be7a07b7f1bccb636d6')
|
||||
|
||||
# Auto-detect mailbox resource ID if not explicitly provided
|
||||
if not target_mailbox_id:
|
||||
try:
|
||||
account_api = AccountApi(api_client)
|
||||
account_info = account_api.get_current_account()
|
||||
if account_info and hasattr(account_info, 'data') and account_info.data:
|
||||
mailboxes = getattr(account_info.data, 'mailboxes', []) or []
|
||||
for mb in mailboxes:
|
||||
sender_address = getattr(settings, 'EMAIL_HOST_USER', '')
|
||||
if sender_address and getattr(mb, 'address', '') == sender_address:
|
||||
target_mailbox_id = getattr(mb, 'resource_id', None)
|
||||
break
|
||||
if not target_mailbox_id and mailboxes:
|
||||
target_mailbox_id = getattr(mailboxes[0], 'resource_id', None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Hostinger SDK: Could not auto-detect mailbox resource ID: {e}")
|
||||
|
||||
if not target_mailbox_id:
|
||||
raise ValueError("Hostinger Mailbox Resource ID is required.")
|
||||
|
||||
send_api = SendApi(api_client)
|
||||
send_request = V1SendRequest(
|
||||
to=[recipient],
|
||||
subject=subject,
|
||||
text=text_content,
|
||||
html=html_content or None,
|
||||
display_name=display_name or getattr(settings, 'EMAIL_DISPLAY_NAME', None)
|
||||
)
|
||||
|
||||
logger.info(f"Hostinger SDK: Attempting to send email to {recipient} via mailbox {target_mailbox_id}")
|
||||
response = send_api.send_email(
|
||||
mailbox_resource_id=target_mailbox_id,
|
||||
v1_send_request=send_request
|
||||
)
|
||||
logger.info(f"Hostinger SDK: Successfully sent email to {recipient}")
|
||||
return response
|
||||
|
||||
def send_email_message(
|
||||
recipient: str,
|
||||
subject: str,
|
||||
text_content: str,
|
||||
html_content: str = None,
|
||||
display_name: str = None
|
||||
):
|
||||
"""
|
||||
Unified email dispatcher. Uses Hostinger Mail API SDK if BEARER_TOKEN is available,
|
||||
otherwise falls back to Django SMTP backend.
|
||||
"""
|
||||
token = getattr(settings, 'HOSTINGER_BEARER_TOKEN', '5bee28a2a982c7713f9254014fead914ab6ef18f4cc0685ef1c9919410e9fc82')
|
||||
|
||||
if token:
|
||||
return send_email_via_hostinger(
|
||||
recipient=recipient,
|
||||
subject=subject,
|
||||
text_content=text_content,
|
||||
html_content=html_content,
|
||||
display_name=display_name
|
||||
)
|
||||
|
||||
# Fallback to standard SMTP via Django core mail
|
||||
sender = getattr(settings, 'EMAIL_HOST_USER', 'noreply@tradhox.com')
|
||||
msg = EmailMultiAlternatives(
|
||||
subject,
|
||||
text_content,
|
||||
sender,
|
||||
[recipient]
|
||||
)
|
||||
if html_content:
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
|
||||
logger.info(f"SMTP Fallback: Sending email to {recipient} via {getattr(settings, 'EMAIL_HOST', 'default SMTP')}")
|
||||
return msg.send(fail_silently=False)
|
||||
|
|
@ -29,28 +29,12 @@
|
|||
<!-- Verification Action -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 0 32px 32px 32px;">
|
||||
<div style="background-color: #fbf9f8; border: 1px solid #d9c5b2; border-radius: 4px; padding: 24px; margin-bottom: 32px;">
|
||||
<div style="background-color: #fbf9f8; border: 1px solid #d9c5b2; border-radius: 4px; padding: 24px;">
|
||||
<p style="margin: 0 0 16px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: #636260; font-weight: 600;">Your Verification Code</p>
|
||||
<div style="font-family: 'Libre Caslon Text', serif; font-size: 40px; letter-spacing: 8px; font-weight: 700; color: #6b1a2c; margin-bottom: 24px;">
|
||||
<div style="font-family: 'Libre Caslon Text', serif; font-size: 40px; letter-spacing: 8px; font-weight: 700; color: #6b1a2c; margin: 0;">
|
||||
{{ otp }}
|
||||
</div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td width="40%" style="border-top: 1px solid #d9c5b2;"></td>
|
||||
<td width="20%" align="center" style="font-size: 12px; color: #636260; text-transform: uppercase; letter-spacing: 0.1em;">OR</td>
|
||||
<td width="40%" style="border-top: 1px solid #d9c5b2;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 4px;" bgcolor="#6b1a2c">
|
||||
<a href="{{ verification_link }}" target="_blank" style="font-size: 14px; font-weight: 600; font-family: 'Work Sans', sans-serif; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 4px; display: inline-block;">
|
||||
Verify Email Address →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Security Notice -->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from .models import EmailLog
|
||||
from .services import send_email_message
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -45,16 +45,12 @@ def send_test_email(request):
|
|||
html_content = render_to_string(f'emails/sellercentral/common/{template_name}.html', context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject,
|
||||
text_content,
|
||||
settings.EMAIL_HOST_USER,
|
||||
[recipient]
|
||||
send_email_message(
|
||||
recipient=recipient,
|
||||
subject=subject,
|
||||
text_content=text_content,
|
||||
html_content=html_content
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
|
||||
logger.info(f"Attempting to send '{template_name}' test email to {recipient} via {settings.EMAIL_HOST}")
|
||||
msg.send(fail_silently=False)
|
||||
logger.info(f"Successfully sent '{template_name}' email to {recipient}")
|
||||
|
||||
# Log success
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
asgiref==3.12.1
|
||||
boto3==1.43.78
|
||||
botocore==1.43.78
|
||||
Django==6.1
|
||||
djangorestframework==3.18.0
|
||||
djangorestframework-api-key==3.1.0
|
||||
jmespath==1.1.0
|
||||
packaging==26.3
|
||||
psycopg2-binary==2.9.12
|
||||
python-dateutil==2.9.0.post0
|
||||
s3transfer==0.19.2
|
||||
six==1.17.0
|
||||
sqlparse==0.6.0
|
||||
urllib3==2.7.0
|
||||
gunicorn==22.0.0
|
||||
whitenoise==6.7.0
|
||||
hostinger_mail_api==1.18.0
|
||||
pydantic>=2.0.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue