This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

0
emails/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

92
emails/admin.py Normal file
View File

@@ -0,0 +1,92 @@
from django.contrib import admin
from .models import (
Email, EmailFolder, EmailAttachment, EmailThread, EmailTemplate,
EmailSignature, EmailRule, EmailSearch
)
@admin.register(EmailFolder)
class EmailFolderAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'folder_type', 'is_system', 'created_at')
list_filter = ('folder_type', 'is_system', 'created_at')
search_fields = ('name', 'user__email')
raw_id_fields = ('user', 'parent')
@admin.register(Email)
class EmailAdmin(admin.ModelAdmin):
list_display = ('subject', 'from_email', 'user', 'folder', 'status', 'is_read', 'created_at')
list_filter = ('status', 'is_read', 'is_starred', 'is_important', 'folder', 'created_at')
search_fields = ('subject', 'from_email', 'body_text', 'user__email')
raw_id_fields = ('user', 'folder')
readonly_fields = ('uuid', 'message_id', 'created_at', 'updated_at')
date_hierarchy = 'created_at'
fieldsets = (
('Basic Information', {
'fields': ('user', 'folder', 'subject', 'from_email')
}),
('Recipients', {
'fields': ('to_emails', 'cc_emails', 'bcc_emails', 'reply_to')
}),
('Content', {
'fields': ('body_text', 'body_html')
}),
('Metadata', {
'fields': ('message_id', 'in_reply_to', 'references', 'priority', 'status')
}),
('Flags', {
'fields': ('is_read', 'is_starred', 'is_important', 'is_encrypted')
}),
('Timestamps', {
'fields': ('sent_at', 'received_at', 'created_at', 'updated_at')
}),
)
@admin.register(EmailAttachment)
class EmailAttachmentAdmin(admin.ModelAdmin):
list_display = ('filename', 'email', 'content_type', 'size', 'created_at')
list_filter = ('content_type', 'is_inline', 'created_at')
search_fields = ('filename', 'email__subject')
raw_id_fields = ('email',)
@admin.register(EmailThread)
class EmailThreadAdmin(admin.ModelAdmin):
list_display = ('subject', 'participants', 'last_activity', 'created_at')
list_filter = ('created_at', 'last_activity')
search_fields = ('subject', 'participants')
filter_horizontal = ('emails',)
@admin.register(EmailTemplate)
class EmailTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'is_public', 'created_at')
list_filter = ('is_public', 'created_at')
search_fields = ('name', 'subject', 'user__email')
raw_id_fields = ('user',)
@admin.register(EmailSignature)
class EmailSignatureAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'is_default', 'created_at')
list_filter = ('is_default', 'created_at')
search_fields = ('name', 'user__email')
raw_id_fields = ('user',)
@admin.register(EmailRule)
class EmailRuleAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'is_active', 'condition_field', 'action', 'created_at')
list_filter = ('is_active', 'condition_field', 'action', 'created_at')
search_fields = ('name', 'user__email')
raw_id_fields = ('user',)
@admin.register(EmailSearch)
class EmailSearchAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'created_at')
list_filter = ('created_at',)
search_fields = ('name', 'user__email')
raw_id_fields = ('user',)

6
emails/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class EmailsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'emails'

View File

@@ -0,0 +1,201 @@
# Generated by Django 4.2.7 on 2025-09-14 20:10
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Email',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('subject', models.CharField(max_length=500)),
('from_email', models.EmailField(max_length=254)),
('to_emails', models.JSONField(default=list)),
('cc_emails', models.JSONField(default=list)),
('bcc_emails', models.JSONField(default=list)),
('reply_to', models.EmailField(blank=True, max_length=254, null=True)),
('body_text', models.TextField(blank=True)),
('body_html', models.TextField(blank=True)),
('message_id', models.CharField(blank=True, max_length=500, null=True, unique=True)),
('in_reply_to', models.CharField(blank=True, max_length=500, null=True)),
('references', models.TextField(blank=True)),
('priority', models.CharField(choices=[('low', 'Low'), ('normal', 'Normal'), ('high', 'High')], default='normal', max_length=10)),
('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('received', 'Received'), ('failed', 'Failed')], default='draft', max_length=20)),
('is_read', models.BooleanField(default=False)),
('is_starred', models.BooleanField(default=False)),
('is_important', models.BooleanField(default=False)),
('is_encrypted', models.BooleanField(default=False)),
('sent_at', models.DateTimeField(blank=True, null=True)),
('received_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('size', models.BigIntegerField(default=0)),
],
options={
'db_table': 'emails',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='EmailThread',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=500)),
('participants', models.JSONField(default=list)),
('last_activity', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('emails', models.ManyToManyField(related_name='threads', to='emails.email')),
],
options={
'db_table': 'email_threads',
'ordering': ['-last_activity'],
},
),
migrations.CreateModel(
name='EmailSignature',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('content_html', models.TextField()),
('content_text', models.TextField(blank=True)),
('is_default', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_signatures', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'email_signatures',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='EmailRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('is_active', models.BooleanField(default=True)),
('condition_field', models.CharField(max_length=50)),
('condition_operator', models.CharField(max_length=20)),
('condition_value', models.CharField(max_length=500)),
('action', models.CharField(choices=[('move', 'Move to folder'), ('mark_read', 'Mark as read'), ('mark_important', 'Mark as important'), ('forward', 'Forward to'), ('delete', 'Delete'), ('reply', 'Auto-reply')], max_length=20)),
('action_value', models.CharField(blank=True, max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_rules', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'email_rules',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='EmailFolder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('folder_type', models.CharField(choices=[('inbox', 'Inbox'), ('sent', 'Sent'), ('drafts', 'Drafts'), ('trash', 'Trash'), ('spam', 'Spam'), ('archive', 'Archive'), ('custom', 'Custom')], default='custom', max_length=20)),
('color', models.CharField(default='#007bff', max_length=7)),
('is_system', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='emails.emailfolder')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folders', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'email_folders',
'ordering': ['name'],
'unique_together': {('user', 'name')},
},
),
migrations.CreateModel(
name='EmailAttachment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filename', models.CharField(max_length=255)),
('content_type', models.CharField(max_length=100)),
('size', models.BigIntegerField()),
('file', models.FileField(upload_to='email_attachments/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'zip', 'rar'])])),
('is_inline', models.BooleanField(default=False)),
('content_id', models.CharField(blank=True, max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('email', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='emails.email')),
],
options={
'db_table': 'email_attachments',
'ordering': ['filename'],
},
),
migrations.AddField(
model_name='email',
name='folder',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='emails.emailfolder'),
),
migrations.AddField(
model_name='email',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='EmailTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('subject', models.CharField(max_length=500)),
('body_html', models.TextField()),
('body_text', models.TextField(blank=True)),
('is_public', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_templates', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'email_templates',
'ordering': ['name'],
'unique_together': {('user', 'name')},
},
),
migrations.CreateModel(
name='EmailSearch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('query', models.JSONField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_searches', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'email_searches',
'ordering': ['name'],
'unique_together': {('user', 'name')},
},
),
migrations.AddIndex(
model_name='email',
index=models.Index(fields=['user', 'folder', '-created_at'], name='emails_user_id_ceecc1_idx'),
),
migrations.AddIndex(
model_name='email',
index=models.Index(fields=['user', 'is_read'], name='emails_user_id_005dbe_idx'),
),
migrations.AddIndex(
model_name='email',
index=models.Index(fields=['user', 'is_starred'], name='emails_user_id_11204a_idx'),
),
migrations.AddIndex(
model_name='email',
index=models.Index(fields=['message_id'], name='emails_message_7b58ef_idx'),
),
]

View File

Binary file not shown.

265
emails/models.py Normal file
View File

@@ -0,0 +1,265 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.core.validators import FileExtensionValidator
import uuid
User = get_user_model()
class EmailFolder(models.Model):
"""Email folders for organizing emails."""
FOLDER_TYPES = [
('inbox', 'Inbox'),
('sent', 'Sent'),
('drafts', 'Drafts'),
('trash', 'Trash'),
('spam', 'Spam'),
('archive', 'Archive'),
('custom', 'Custom'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='folders')
name = models.CharField(max_length=100)
folder_type = models.CharField(max_length=20, choices=FOLDER_TYPES, default='custom')
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
color = models.CharField(max_length=7, default='#007bff') # Hex color
is_system = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'email_folders'
unique_together = ['user', 'name']
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"
class Email(models.Model):
"""Email model for storing email messages."""
PRIORITY_CHOICES = [
('low', 'Low'),
('normal', 'Normal'),
('high', 'High'),
]
STATUS_CHOICES = [
('draft', 'Draft'),
('sent', 'Sent'),
('received', 'Received'),
('failed', 'Failed'),
]
# Basic email information
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='emails')
folder = models.ForeignKey(EmailFolder, on_delete=models.CASCADE, related_name='emails')
# Email headers
subject = models.CharField(max_length=500)
from_email = models.EmailField()
to_emails = models.JSONField(default=list) # List of email addresses
cc_emails = models.JSONField(default=list) # List of email addresses
bcc_emails = models.JSONField(default=list) # List of email addresses
reply_to = models.EmailField(null=True, blank=True)
# Email content
body_text = models.TextField(blank=True)
body_html = models.TextField(blank=True)
# Email metadata
message_id = models.CharField(max_length=500, unique=True, null=True, blank=True)
in_reply_to = models.CharField(max_length=500, null=True, blank=True)
references = models.TextField(blank=True)
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='normal')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
# Flags and status
is_read = models.BooleanField(default=False)
is_starred = models.BooleanField(default=False)
is_important = models.BooleanField(default=False)
is_encrypted = models.BooleanField(default=False)
# Timestamps
sent_at = models.DateTimeField(null=True, blank=True)
received_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Size information
size = models.BigIntegerField(default=0) # Size in bytes
class Meta:
db_table = 'emails'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'folder', '-created_at']),
models.Index(fields=['user', 'is_read']),
models.Index(fields=['user', 'is_starred']),
models.Index(fields=['message_id']),
]
def __str__(self):
return f"{self.subject} - {self.from_email}"
def mark_as_read(self):
"""Mark email as read."""
self.is_read = True
self.save(update_fields=['is_read', 'updated_at'])
def mark_as_unread(self):
"""Mark email as unread."""
self.is_read = False
self.save(update_fields=['is_read', 'updated_at'])
def move_to_folder(self, folder):
"""Move email to another folder."""
self.folder = folder
self.save(update_fields=['folder', 'updated_at'])
def get_recipients(self):
"""Get all recipients (to, cc, bcc)."""
return self.to_emails + self.cc_emails + self.bcc_emails
class EmailAttachment(models.Model):
"""Email attachments."""
email = models.ForeignKey(Email, on_delete=models.CASCADE, related_name='attachments')
filename = models.CharField(max_length=255)
content_type = models.CharField(max_length=100)
size = models.BigIntegerField()
file = models.FileField(
upload_to='email_attachments/',
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'zip', 'rar'])]
)
is_inline = models.BooleanField(default=False)
content_id = models.CharField(max_length=100, blank=True) # For inline attachments
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'email_attachments'
ordering = ['filename']
def __str__(self):
return f"{self.email.subject} - {self.filename}"
class EmailThread(models.Model):
"""Email threads for conversation view."""
emails = models.ManyToManyField(Email, related_name='threads')
subject = models.CharField(max_length=500)
participants = models.JSONField(default=list) # List of email addresses
last_activity = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'email_threads'
ordering = ['-last_activity']
def __str__(self):
return f"Thread: {self.subject}"
class EmailTemplate(models.Model):
"""Email templates for quick composition."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_templates')
name = models.CharField(max_length=100)
subject = models.CharField(max_length=500)
body_html = models.TextField()
body_text = models.TextField(blank=True)
is_public = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'email_templates'
unique_together = ['user', 'name']
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"
class EmailSignature(models.Model):
"""Email signatures."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_signatures')
name = models.CharField(max_length=100)
content_html = models.TextField()
content_text = models.TextField(blank=True)
is_default = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'email_signatures'
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"
def save(self, *args, **kwargs):
if self.is_default:
# Ensure only one default signature per user
EmailSignature.objects.filter(user=self.user, is_default=True).update(is_default=False)
super().save(*args, **kwargs)
class EmailRule(models.Model):
"""Email rules for automatic processing."""
ACTION_CHOICES = [
('move', 'Move to folder'),
('mark_read', 'Mark as read'),
('mark_important', 'Mark as important'),
('forward', 'Forward to'),
('delete', 'Delete'),
('reply', 'Auto-reply'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_rules')
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
# Conditions
condition_field = models.CharField(max_length=50) # from, to, subject, body, etc.
condition_operator = models.CharField(max_length=20) # contains, equals, starts_with, etc.
condition_value = models.CharField(max_length=500)
# Actions
action = models.CharField(max_length=20, choices=ACTION_CHOICES)
action_value = models.CharField(max_length=500, blank=True) # Folder name, email address, etc.
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'email_rules'
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"
class EmailSearch(models.Model):
"""Saved email searches."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_searches')
name = models.CharField(max_length=100)
query = models.JSONField() # Search parameters
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'email_searches'
unique_together = ['user', 'name']
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"

261
emails/serializers.py Normal file
View File

@@ -0,0 +1,261 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import (
Email, EmailFolder, EmailAttachment, EmailThread, EmailTemplate,
EmailSignature, EmailRule, EmailSearch
)
User = get_user_model()
class EmailFolderSerializer(serializers.ModelSerializer):
"""Serializer for email folders."""
email_count = serializers.SerializerMethodField()
unread_count = serializers.SerializerMethodField()
class Meta:
model = EmailFolder
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
def get_email_count(self, obj):
return obj.emails.count()
def get_unread_count(self, obj):
return obj.emails.filter(is_read=False).count()
class EmailAttachmentSerializer(serializers.ModelSerializer):
"""Serializer for email attachments."""
class Meta:
model = EmailAttachment
fields = '__all__'
read_only_fields = ('created_at',)
class EmailSerializer(serializers.ModelSerializer):
"""Serializer for emails."""
attachments = EmailAttachmentSerializer(many=True, read_only=True)
folder_name = serializers.CharField(source='folder.name', read_only=True)
from_name = serializers.SerializerMethodField()
recipients = serializers.SerializerMethodField()
thread_count = serializers.SerializerMethodField()
class Meta:
model = Email
fields = '__all__'
read_only_fields = ('uuid', 'user', 'message_id', 'created_at', 'updated_at')
def get_from_name(self, obj):
# Try to get name from contacts or use email
return obj.from_email
def get_recipients(self, obj):
return {
'to': obj.to_emails,
'cc': obj.cc_emails,
'bcc': obj.bcc_emails,
}
def get_thread_count(self, obj):
return obj.threads.count()
class EmailCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating emails."""
attachments = serializers.ListField(
child=serializers.FileField(),
required=False,
write_only=True
)
class Meta:
model = Email
fields = (
'subject', 'to_emails', 'cc_emails', 'bcc_emails', 'reply_to',
'body_text', 'body_html', 'priority', 'folder', 'attachments'
)
def validate_to_emails(self, value):
if not value:
raise serializers.ValidationError("At least one recipient is required.")
return value
def create(self, validated_data):
attachments_data = validated_data.pop('attachments', [])
user = self.context['request'].user
# Set from_email to user's email
validated_data['from_email'] = user.email
# Create email
email = Email.objects.create(user=user, **validated_data)
# Handle attachments
for attachment_file in attachments_data:
EmailAttachment.objects.create(
email=email,
filename=attachment_file.name,
content_type=attachment_file.content_type,
size=attachment_file.size,
file=attachment_file
)
return email
class EmailReplySerializer(serializers.ModelSerializer):
"""Serializer for replying to emails."""
original_email = serializers.PrimaryKeyRelatedField(queryset=Email.objects.all())
class Meta:
model = Email
fields = ('original_email', 'subject', 'body_text', 'body_html', 'priority')
def create(self, validated_data):
original_email = validated_data.pop('original_email')
user = self.context['request'].user
# Set reply headers
validated_data.update({
'user': user,
'from_email': user.email,
'to_emails': [original_email.from_email],
'in_reply_to': original_email.message_id,
'references': f"{original_email.references} {original_email.message_id}".strip(),
'folder': EmailFolder.objects.get(user=user, folder_type='sent'),
})
# Add "Re: " prefix if not already present
subject = validated_data.get('subject', '')
if not subject.startswith('Re: '):
validated_data['subject'] = f"Re: {subject}"
return Email.objects.create(**validated_data)
class EmailForwardSerializer(serializers.ModelSerializer):
"""Serializer for forwarding emails."""
original_email = serializers.PrimaryKeyRelatedField(queryset=Email.objects.all())
class Meta:
model = Email
fields = ('original_email', 'to_emails', 'cc_emails', 'bcc_emails', 'subject', 'body_text', 'body_html', 'priority')
def create(self, validated_data):
original_email = validated_data.pop('original_email')
user = self.context['request'].user
# Set forward headers
validated_data.update({
'user': user,
'from_email': user.email,
'in_reply_to': original_email.message_id,
'folder': EmailFolder.objects.get(user=user, folder_type='sent'),
})
# Add "Fwd: " prefix if not already present
subject = validated_data.get('subject', '')
if not subject.startswith('Fwd: '):
validated_data['subject'] = f"Fwd: {subject}"
# Add original email content
original_content = f"\n\n--- Forwarded message ---\nFrom: {original_email.from_email}\nDate: {original_email.sent_at}\nSubject: {original_email.subject}\n\n{original_email.body_text}"
validated_data['body_text'] += original_content
return Email.objects.create(**validated_data)
class EmailThreadSerializer(serializers.ModelSerializer):
"""Serializer for email threads."""
emails = EmailSerializer(many=True, read_only=True)
email_count = serializers.SerializerMethodField()
class Meta:
model = EmailThread
fields = '__all__'
def get_email_count(self, obj):
return obj.emails.count()
class EmailTemplateSerializer(serializers.ModelSerializer):
"""Serializer for email templates."""
class Meta:
model = EmailTemplate
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
class EmailSignatureSerializer(serializers.ModelSerializer):
"""Serializer for email signatures."""
class Meta:
model = EmailSignature
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
class EmailRuleSerializer(serializers.ModelSerializer):
"""Serializer for email rules."""
class Meta:
model = EmailRule
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
class EmailSearchSerializer(serializers.ModelSerializer):
"""Serializer for email searches."""
class Meta:
model = EmailSearch
fields = '__all__'
read_only_fields = ('user', 'created_at')
class EmailBulkActionSerializer(serializers.Serializer):
"""Serializer for bulk email actions."""
ACTION_CHOICES = [
('mark_read', 'Mark as read'),
('mark_unread', 'Mark as unread'),
('mark_starred', 'Mark as starred'),
('mark_unstarred', 'Mark as unstarred'),
('mark_important', 'Mark as important'),
('mark_unimportant', 'Mark as unimportant'),
('move_to_folder', 'Move to folder'),
('delete', 'Delete'),
]
email_ids = serializers.ListField(
child=serializers.IntegerField(),
min_length=1
)
action = serializers.ChoiceField(choices=ACTION_CHOICES)
folder_id = serializers.IntegerField(required=False)
def validate_email_ids(self, value):
user = self.context['request'].user
# Verify all emails belong to the user
email_count = Email.objects.filter(id__in=value, user=user).count()
if email_count != len(value):
raise serializers.ValidationError("Some emails don't exist or don't belong to you.")
return value
def validate(self, attrs):
action = attrs.get('action')
folder_id = attrs.get('folder_id')
if action == 'move_to_folder' and not folder_id:
raise serializers.ValidationError("folder_id is required for move_to_folder action.")
return attrs

299
emails/tasks.py Normal file
View File

@@ -0,0 +1,299 @@
from celery import shared_task
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.conf import settings
from django.utils import timezone
import smtplib
import imaplib
import email
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import logging
from .models import Email, EmailFolder, EmailAttachment
from accounts.models import User
logger = logging.getLogger(__name__)
@shared_task
def send_email_task(email_id):
"""Send email asynchronously."""
try:
email_obj = Email.objects.get(id=email_id)
user = email_obj.user
# Create email message
msg = MIMEMultipart('alternative')
msg['From'] = email_obj.from_email
msg['To'] = ', '.join(email_obj.to_emails)
msg['Subject'] = email_obj.subject
if email_obj.cc_emails:
msg['Cc'] = ', '.join(email_obj.cc_emails)
if email_obj.reply_to:
msg['Reply-To'] = email_obj.reply_to
if email_obj.in_reply_to:
msg['In-Reply-To'] = email_obj.in_reply_to
if email_obj.references:
msg['References'] = email_obj.references
# Add body
if email_obj.body_text:
text_part = MIMEText(email_obj.body_text, 'plain')
msg.attach(text_part)
if email_obj.body_html:
html_part = MIMEText(email_obj.body_html, 'html')
msg.attach(html_part)
# Add attachments
for attachment in email_obj.attachments.all():
with open(attachment.file.path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename= {attachment.filename}'
)
msg.attach(part)
# Send email
if user.smtp_host and user.smtp_username:
# Use user's SMTP settings
server = smtplib.SMTP(user.smtp_host, user.smtp_port)
if user.smtp_use_tls:
server.starttls()
server.login(user.smtp_username, user.get_smtp_password())
recipients = email_obj.to_emails + email_obj.cc_emails + email_obj.bcc_emails
server.send_message(msg, to_addrs=recipients)
server.quit()
else:
# Use Django's email backend
django_email = EmailMultiAlternatives(
subject=email_obj.subject,
body=email_obj.body_text,
from_email=email_obj.from_email,
to=email_obj.to_emails,
cc=email_obj.cc_emails,
bcc=email_obj.bcc_emails,
reply_to=[email_obj.reply_to] if email_obj.reply_to else None,
)
if email_obj.body_html:
django_email.attach_alternative(email_obj.body_html, "text/html")
# Add attachments
for attachment in email_obj.attachments.all():
django_email.attach_file(attachment.file.path)
django_email.send()
# Update email status
email_obj.status = 'sent'
email_obj.sent_at = timezone.now()
email_obj.save()
logger.info(f"Email {email_id} sent successfully")
except Exception as e:
logger.error(f"Failed to send email {email_id}: {str(e)}")
# Update email status to failed
try:
email_obj = Email.objects.get(id=email_id)
email_obj.status = 'failed'
email_obj.save()
except Email.DoesNotExist:
pass
@shared_task
def fetch_emails_task(user_id):
"""Fetch emails from IMAP server asynchronously."""
try:
user = User.objects.get(id=user_id)
if not user.imap_host or not user.imap_username:
logger.error(f"IMAP settings not configured for user {user_id}")
return
# Connect to IMAP server
if user.imap_use_ssl:
server = imaplib.IMAP4_SSL(user.imap_host, user.imap_port)
else:
server = imaplib.IMAP4(user.imap_host, user.imap_port)
server.login(user.imap_username, user.get_imap_password())
# Select inbox
server.select('INBOX')
# Search for unseen emails
status, messages = server.search(None, 'UNSEEN')
if status == 'OK':
email_ids = messages[0].split()
# Get or create inbox folder
inbox_folder, created = EmailFolder.objects.get_or_create(
user=user,
folder_type='inbox',
defaults={'name': 'Inbox', 'is_system': True}
)
for email_id in email_ids:
try:
# Fetch email
status, msg_data = server.fetch(email_id, '(RFC822)')
if status == 'OK':
raw_email = msg_data[0][1]
email_message = email.message_from_bytes(raw_email)
# Parse email
subject = email_message.get('Subject', '')
from_email = email_message.get('From', '')
to_emails = email_message.get('To', '').split(',')
cc_emails = email_message.get('Cc', '').split(',') if email_message.get('Cc') else []
message_id = email_message.get('Message-ID', '')
in_reply_to = email_message.get('In-Reply-To', '')
references = email_message.get('References', '')
# Get email body
body_text = ''
body_html = ''
if email_message.is_multipart():
for part in email_message.walk():
content_type = part.get_content_type()
content_disposition = str(part.get('Content-Disposition'))
if content_type == 'text/plain' and 'attachment' not in content_disposition:
body_text = part.get_payload(decode=True).decode()
elif content_type == 'text/html' and 'attachment' not in content_disposition:
body_html = part.get_payload(decode=True).decode()
else:
content_type = email_message.get_content_type()
if content_type == 'text/plain':
body_text = email_message.get_payload(decode=True).decode()
elif content_type == 'text/html':
body_html = email_message.get_payload(decode=True).decode()
# Create email object
email_obj = Email.objects.create(
user=user,
folder=inbox_folder,
subject=subject,
from_email=from_email,
to_emails=[email.strip() for email in to_emails if email.strip()],
cc_emails=[email.strip() for email in cc_emails if email.strip()],
body_text=body_text,
body_html=body_html,
message_id=message_id,
in_reply_to=in_reply_to,
references=references,
status='received',
received_at=timezone.now(),
size=len(raw_email)
)
# Process attachments
if email_message.is_multipart():
for part in email_message.walk():
content_disposition = str(part.get('Content-Disposition'))
if 'attachment' in content_disposition:
filename = part.get_filename()
if filename:
# Save attachment
attachment = EmailAttachment.objects.create(
email=email_obj,
filename=filename,
content_type=part.get_content_type(),
size=len(part.get_payload(decode=True)),
file=part.get_payload(decode=True)
)
logger.info(f"Fetched email {email_obj.id} for user {user_id}")
except Exception as e:
logger.error(f"Error processing email {email_id}: {str(e)}")
continue
server.logout()
logger.info(f"Email fetch completed for user {user_id}")
except Exception as e:
logger.error(f"Failed to fetch emails for user {user_id}: {str(e)}")
@shared_task
def cleanup_old_emails():
"""Clean up old emails from trash folder."""
try:
from django.utils import timezone
from datetime import timedelta
# Delete emails in trash folder older than 30 days
cutoff_date = timezone.now() - timedelta(days=30)
trash_emails = Email.objects.filter(
folder__folder_type='trash',
created_at__lt=cutoff_date
)
count = trash_emails.count()
trash_emails.delete()
logger.info(f"Cleaned up {count} old emails from trash")
except Exception as e:
logger.error(f"Failed to cleanup old emails: {str(e)}")
@shared_task
def process_email_rules():
"""Process email rules for all users."""
try:
from .models import EmailRule
rules = EmailRule.objects.filter(is_active=True)
for rule in rules:
try:
# Apply rule to matching emails
emails = Email.objects.filter(user=rule.user)
# Apply condition
if rule.condition_field == 'from':
emails = emails.filter(from_email__icontains=rule.condition_value)
elif rule.condition_field == 'to':
emails = emails.filter(to_emails__icontains=rule.condition_value)
elif rule.condition_field == 'subject':
emails = emails.filter(subject__icontains=rule.condition_value)
elif rule.condition_field == 'body':
emails = emails.filter(body_text__icontains=rule.condition_value)
# Apply action
if rule.action == 'move':
folder = EmailFolder.objects.get(user=rule.user, name=rule.action_value)
emails.update(folder=folder)
elif rule.action == 'mark_read':
emails.update(is_read=True)
elif rule.action == 'mark_important':
emails.update(is_important=True)
elif rule.action == 'delete':
emails.delete()
logger.info(f"Processed rule {rule.id} for user {rule.user.id}")
except Exception as e:
logger.error(f"Error processing rule {rule.id}: {str(e)}")
continue
except Exception as e:
logger.error(f"Failed to process email rules: {str(e)}")

39
emails/urls.py Normal file
View File

@@ -0,0 +1,39 @@
from django.urls import path
from . import views
urlpatterns = [
# Email folders
path('folders/', views.EmailFolderListCreateView.as_view(), name='email-folder-list'),
path('folders/<int:pk>/', views.EmailFolderDetailView.as_view(), name='email-folder-detail'),
# Emails
path('', views.EmailListCreateView.as_view(), name='email-list'),
path('<int:pk>/', views.EmailDetailView.as_view(), name='email-detail'),
path('reply/', views.EmailReplyView.as_view(), name='email-reply'),
path('forward/', views.EmailForwardView.as_view(), name='email-forward'),
path('bulk-action/', views.EmailBulkActionView.as_view(), name='email-bulk-action'),
path('search/', views.EmailSearchView.as_view(), name='email-search'),
path('threads/<int:pk>/', views.EmailThreadView.as_view(), name='email-thread'),
# Email actions
path('send/', views.send_email, name='send-email'),
path('fetch/', views.fetch_emails, name='fetch-emails'),
path('stats/', views.email_stats, name='email-stats'),
path('test-settings/', views.test_email_settings, name='test-email-settings'),
# Templates
path('templates/', views.EmailTemplateListCreateView.as_view(), name='email-template-list'),
path('templates/<int:pk>/', views.EmailTemplateDetailView.as_view(), name='email-template-detail'),
# Signatures
path('signatures/', views.EmailSignatureListCreateView.as_view(), name='email-signature-list'),
path('signatures/<int:pk>/', views.EmailSignatureDetailView.as_view(), name='email-signature-detail'),
# Rules
path('rules/', views.EmailRuleListCreateView.as_view(), name='email-rule-list'),
path('rules/<int:pk>/', views.EmailRuleDetailView.as_view(), name='email-rule-detail'),
# Saved searches
path('searches/', views.EmailSearchListCreateView.as_view(), name='email-search-list'),
path('searches/<int:pk>/', views.EmailSearchDetailView.as_view(), name='email-search-detail'),
]

405
emails/views.py Normal file
View File

@@ -0,0 +1,405 @@
from rest_framework import generics, status, permissions, filters
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.views import APIView
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q, Count
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django_ratelimit.decorators import ratelimit
import smtplib
import imaplib
import email
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os
from .models import (
Email, EmailFolder, EmailAttachment, EmailThread, EmailTemplate,
EmailSignature, EmailRule, EmailSearch
)
from .serializers import (
EmailSerializer, EmailCreateSerializer, EmailReplySerializer, EmailForwardSerializer,
EmailFolderSerializer, EmailAttachmentSerializer, EmailThreadSerializer,
EmailTemplateSerializer, EmailSignatureSerializer, EmailRuleSerializer,
EmailSearchSerializer, EmailBulkActionSerializer
)
from .tasks import send_email_task, fetch_emails_task
class EmailFolderListCreateView(generics.ListCreateAPIView):
"""List and create email folders."""
serializer_class = EmailFolderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailFolder.objects.filter(user=self.request.user)
class EmailFolderDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete email folder."""
serializer_class = EmailFolderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailFolder.objects.filter(user=self.request.user)
class EmailListCreateView(generics.ListCreateAPIView):
"""List and create emails."""
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['folder', 'is_read', 'is_starred', 'is_important', 'status']
search_fields = ['subject', 'from_email', 'body_text']
ordering_fields = ['created_at', 'sent_at', 'subject']
ordering = ['-created_at']
def get_serializer_class(self):
if self.request.method == 'POST':
return EmailCreateSerializer
return EmailSerializer
def get_queryset(self):
return Email.objects.filter(user=self.request.user).select_related('folder').prefetch_related('attachments')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete email."""
serializer_class = EmailSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Email.objects.filter(user=self.request.user).select_related('folder').prefetch_related('attachments')
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# Mark as read when retrieved
if not instance.is_read:
instance.mark_as_read()
serializer = self.get_serializer(instance)
return Response(serializer.data)
class EmailReplyView(generics.CreateAPIView):
"""Reply to an email."""
serializer_class = EmailReplySerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailForwardView(generics.CreateAPIView):
"""Forward an email."""
serializer_class = EmailForwardSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailBulkActionView(APIView):
"""Perform bulk actions on emails."""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
serializer = EmailBulkActionSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
email_ids = serializer.validated_data['email_ids']
action = serializer.validated_data['action']
folder_id = serializer.validated_data.get('folder_id')
emails = Email.objects.filter(id__in=email_ids, user=request.user)
if action == 'mark_read':
emails.update(is_read=True)
elif action == 'mark_unread':
emails.update(is_read=False)
elif action == 'mark_starred':
emails.update(is_starred=True)
elif action == 'mark_unstarred':
emails.update(is_starred=False)
elif action == 'mark_important':
emails.update(is_important=True)
elif action == 'mark_unimportant':
emails.update(is_important=False)
elif action == 'move_to_folder':
folder = get_object_or_404(EmailFolder, id=folder_id, user=request.user)
emails.update(folder=folder)
elif action == 'delete':
emails.delete()
return Response({'message': f'Bulk action {action} completed successfully'})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class EmailSearchView(generics.ListAPIView):
"""Search emails with advanced filters."""
serializer_class = EmailSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
queryset = Email.objects.filter(user=self.request.user)
# Get search parameters
query = self.request.query_params.get('q', '')
folder = self.request.query_params.get('folder')
is_read = self.request.query_params.get('is_read')
is_starred = self.request.query_params.get('is_starred')
is_important = self.request.query_params.get('is_important')
date_from = self.request.query_params.get('date_from')
date_to = self.request.query_params.get('date_to')
# Apply filters
if query:
queryset = queryset.filter(
Q(subject__icontains=query) |
Q(from_email__icontains=query) |
Q(body_text__icontains=query)
)
if folder:
queryset = queryset.filter(folder_id=folder)
if is_read is not None:
queryset = queryset.filter(is_read=is_read.lower() == 'true')
if is_starred is not None:
queryset = queryset.filter(is_starred=is_starred.lower() == 'true')
if is_important is not None:
queryset = queryset.filter(is_important=is_important.lower() == 'true')
if date_from:
queryset = queryset.filter(created_at__gte=date_from)
if date_to:
queryset = queryset.filter(created_at__lte=date_to)
return queryset.select_related('folder').prefetch_related('attachments')
class EmailThreadView(generics.RetrieveAPIView):
"""Get email thread."""
serializer_class = EmailThreadSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailThread.objects.filter(emails__user=self.request.user).distinct()
class EmailTemplateListCreateView(generics.ListCreateAPIView):
"""List and create email templates."""
serializer_class = EmailTemplateSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailTemplate.objects.filter(
Q(user=self.request.user) | Q(is_public=True)
).order_by('name')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailTemplateDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete email template."""
serializer_class = EmailTemplateSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailTemplate.objects.filter(user=self.request.user)
class EmailSignatureListCreateView(generics.ListCreateAPIView):
"""List and create email signatures."""
serializer_class = EmailSignatureSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailSignature.objects.filter(user=self.request.user).order_by('name')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailSignatureDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete email signature."""
serializer_class = EmailSignatureSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailSignature.objects.filter(user=self.request.user)
class EmailRuleListCreateView(generics.ListCreateAPIView):
"""List and create email rules."""
serializer_class = EmailRuleSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailRule.objects.filter(user=self.request.user).order_by('name')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailRuleDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete email rule."""
serializer_class = EmailRuleSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailRule.objects.filter(user=self.request.user)
class EmailSearchListCreateView(generics.ListCreateAPIView):
"""List and create saved email searches."""
serializer_class = EmailSearchSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailSearch.objects.filter(user=self.request.user).order_by('name')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class EmailSearchDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete saved email search."""
serializer_class = EmailSearchSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return EmailSearch.objects.filter(user=self.request.user)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
@ratelimit(key='user', rate='10/m', method=['POST'])
def send_email(request):
"""Send email endpoint."""
serializer = EmailCreateSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
email = serializer.save()
# Send email asynchronously
send_email_task.delay(email.id)
return Response({
'message': 'Email queued for sending',
'email': EmailSerializer(email).data
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
@ratelimit(key='user', rate='5/m', method=['POST'])
def fetch_emails(request):
"""Fetch emails from IMAP server."""
user = request.user
# Check if user has IMAP settings configured
if not user.imap_host or not user.imap_username:
return Response(
{'error': 'IMAP settings not configured'},
status=status.HTTP_400_BAD_REQUEST
)
# Fetch emails asynchronously
fetch_emails_task.delay(user.id)
return Response({'message': 'Email fetch started'}, status=status.HTTP_200_OK)
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def email_stats(request):
"""Get email statistics."""
user = request.user
stats = {
'total_emails': Email.objects.filter(user=user).count(),
'unread_emails': Email.objects.filter(user=user, is_read=False).count(),
'starred_emails': Email.objects.filter(user=user, is_starred=True).count(),
'important_emails': Email.objects.filter(user=user, is_important=True).count(),
'sent_emails': Email.objects.filter(user=user, status='sent').count(),
'draft_emails': Email.objects.filter(user=user, status='draft').count(),
'folder_stats': EmailFolder.objects.filter(user=user).annotate(
email_count=Count('emails'),
unread_count=Count('emails', filter=Q(emails__is_read=False))
).values('name', 'email_count', 'unread_count'),
}
return Response(stats)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def test_email_settings(request):
"""Test email server settings."""
user = request.user
settings_type = request.data.get('type', 'smtp') # smtp or imap
try:
if settings_type == 'smtp':
# Test SMTP connection
if not user.smtp_host or not user.smtp_username:
return Response(
{'error': 'SMTP settings not configured'},
status=status.HTTP_400_BAD_REQUEST
)
server = smtplib.SMTP(user.smtp_host, user.smtp_port)
if user.smtp_use_tls:
server.starttls()
server.login(user.smtp_username, user.get_smtp_password())
server.quit()
elif settings_type == 'imap':
# Test IMAP connection
if not user.imap_host or not user.imap_username:
return Response(
{'error': 'IMAP settings not configured'},
status=status.HTTP_400_BAD_REQUEST
)
if user.imap_use_ssl:
server = imaplib.IMAP4_SSL(user.imap_host, user.imap_port)
else:
server = imaplib.IMAP4(user.imap_host, user.imap_port)
server.login(user.imap_username, user.get_imap_password())
server.logout()
return Response({'message': f'{settings_type.upper()} connection successful'})
except Exception as e:
return Response(
{'error': f'{settings_type.upper()} connection failed: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
)