fix(tickets): Remove invalid source_email_address_id in PlatformEmailReceiver
PlatformEmailReceiver was setting source_email_address_id to a PlatformEmailAddress ID, but the Ticket model expects a TicketEmailAddress foreign key. This caused an integrity error that was rolling back the entire transaction (due to ATOMIC_REQUESTS=True), preventing tickets from being created.
This commit is contained in:
653
PLAN_MULTI_EMAIL_TICKETING.md
Normal file
653
PLAN_MULTI_EMAIL_TICKETING.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# Implementation Plan: Multi-Email Ticketing System
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Add support for multiple email addresses per business in the ticketing system, with color-coded visual indicators and per-email IMAP/SMTP configuration.
|
||||
|
||||
## Current System Analysis
|
||||
|
||||
### Existing Components
|
||||
|
||||
1. **Django Backend (`tickets` app)**
|
||||
- `Ticket` model: Core ticket entity
|
||||
- `TicketComment` model: Ticket responses
|
||||
- `TicketEmailSettings` model: **Singleton** platform-wide email config
|
||||
- `IncomingTicketEmail` model: Email audit log
|
||||
- `TicketEmailReceiver` class: IMAP email fetching
|
||||
- `TicketEmailService` class: SMTP email sending
|
||||
|
||||
2. **Frontend**
|
||||
- `Tickets.tsx`: Main ticket listing page
|
||||
- `TicketModal.tsx`: Ticket detail modal
|
||||
- `useTickets` hook: Fetch tickets
|
||||
- `useTicketEmailSettings` hook: Manage email settings (singleton)
|
||||
- `Settings.tsx`: Business settings page
|
||||
|
||||
3. **Current Email Flow**
|
||||
- Single email account configured platform-wide
|
||||
- Emails matched to tickets by ID in subject/address
|
||||
- Comments created from email replies
|
||||
- New tickets created from unmatched emails
|
||||
|
||||
## Requirements (from user clarification)
|
||||
|
||||
1. **Per-Business Email Addresses**
|
||||
- Each business provides their own email account(s) and credentials
|
||||
- Multiple email addresses per business
|
||||
- Each email has independent IMAP/SMTP settings
|
||||
|
||||
2. **Email Address Properties**
|
||||
- Display name (e.g., "Support", "Billing")
|
||||
- Email address
|
||||
- IMAP settings (host, port, username, password, SSL)
|
||||
- SMTP settings (host, port, username, password, TLS/SSL)
|
||||
- Color for visual identification (hex color code)
|
||||
- Active/inactive status
|
||||
|
||||
3. **Ticket Routing**
|
||||
- Incoming emails matched to business by email address configuration
|
||||
- Reply emails matched to existing tickets
|
||||
- New emails create tickets for that business
|
||||
- System attempts to match sender email to customer/staff in business
|
||||
|
||||
4. **UI Requirements**
|
||||
- Colored left border on ticket rows indicating source email
|
||||
- Business settings page to manage email addresses
|
||||
- Test connection buttons for IMAP/SMTP
|
||||
- Email address selector when creating tickets manually
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Django Backend Models
|
||||
|
||||
#### 1.1 Create `TicketEmailAddress` Model
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/models.py`
|
||||
|
||||
```python
|
||||
class TicketEmailAddress(models.Model):
|
||||
"""
|
||||
Per-business email address configuration for ticket management.
|
||||
Each business can have multiple email addresses with their own settings.
|
||||
"""
|
||||
tenant = models.ForeignKey(
|
||||
Tenant,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='ticket_email_addresses',
|
||||
help_text="Business this email address belongs to"
|
||||
)
|
||||
|
||||
# Display information
|
||||
display_name = models.CharField(
|
||||
max_length=100,
|
||||
help_text="Display name (e.g., 'Support', 'Billing', 'Sales')"
|
||||
)
|
||||
email_address = models.EmailField(
|
||||
help_text="Email address for sending/receiving tickets"
|
||||
)
|
||||
color = models.CharField(
|
||||
max_length=7,
|
||||
default='#3b82f6',
|
||||
help_text="Hex color code for visual identification (e.g., #3b82f6)"
|
||||
)
|
||||
|
||||
# IMAP settings (inbound)
|
||||
imap_host = models.CharField(max_length=255)
|
||||
imap_port = models.IntegerField(default=993)
|
||||
imap_use_ssl = models.BooleanField(default=True)
|
||||
imap_username = models.CharField(max_length=255)
|
||||
imap_password = models.CharField(max_length=255) # Encrypted in production
|
||||
imap_folder = models.CharField(max_length=100, default='INBOX')
|
||||
|
||||
# SMTP settings (outbound)
|
||||
smtp_host = models.CharField(max_length=255)
|
||||
smtp_port = models.IntegerField(default=587)
|
||||
smtp_use_tls = models.BooleanField(default=True)
|
||||
smtp_use_ssl = models.BooleanField(default=False)
|
||||
smtp_username = models.CharField(max_length=255)
|
||||
smtp_password = models.CharField(max_length=255) # Encrypted in production
|
||||
|
||||
# Status and tracking
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether this email address is actively checked"
|
||||
)
|
||||
is_default = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Default email for new tickets in this business"
|
||||
)
|
||||
last_check_at = models.DateTimeField(null=True, blank=True)
|
||||
last_error = models.TextField(blank=True, default='')
|
||||
emails_processed_count = models.IntegerField(default=0)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-is_default', 'display_name']
|
||||
unique_together = [['tenant', 'email_address']]
|
||||
indexes = [
|
||||
models.Index(fields=['tenant', 'is_active']),
|
||||
models.Index(fields=['email_address']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_name} <{self.email_address}> ({self.tenant.name})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Ensure only one default per tenant
|
||||
if self.is_default:
|
||||
TicketEmailAddress.objects.filter(
|
||||
tenant=self.tenant,
|
||||
is_default=True
|
||||
).exclude(pk=self.pk).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
```
|
||||
|
||||
#### 1.2 Update `Ticket` Model
|
||||
|
||||
Add field to track which email address received/sent the ticket:
|
||||
|
||||
```python
|
||||
class Ticket(models.Model):
|
||||
# ... existing fields ...
|
||||
|
||||
source_email_address = models.ForeignKey(
|
||||
'TicketEmailAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='tickets',
|
||||
help_text="Email address this ticket was received from or sent to"
|
||||
)
|
||||
```
|
||||
|
||||
#### 1.3 Update `IncomingTicketEmail` Model
|
||||
|
||||
Add field to track which email address received the email:
|
||||
|
||||
```python
|
||||
class IncomingTicketEmail(models.Model):
|
||||
# ... existing fields ...
|
||||
|
||||
email_address = models.ForeignKey(
|
||||
'TicketEmailAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='incoming_emails',
|
||||
help_text="Email address configuration that received this email"
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 2: Django Backend Logic
|
||||
|
||||
#### 2.1 Update Email Receiver
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/email_receiver.py`
|
||||
|
||||
- Modify `TicketEmailReceiver` to iterate through all active `TicketEmailAddress` objects
|
||||
- Connect to each email address's IMAP server
|
||||
- Process emails for each address
|
||||
- Associate processed tickets with the source email address
|
||||
|
||||
#### 2.2 Update Email Sender
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/email_notifications.py`
|
||||
|
||||
- Modify `TicketEmailService` to use the ticket's `source_email_address` for sending
|
||||
- Fall back to business's default email address if none specified
|
||||
|
||||
### Phase 3: Django Backend API
|
||||
|
||||
#### 3.1 Create Serializers
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/serializers.py`
|
||||
|
||||
```python
|
||||
class TicketEmailAddressSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketEmailAddress
|
||||
fields = [
|
||||
'id', 'tenant', 'display_name', 'email_address', 'color',
|
||||
'imap_host', 'imap_port', 'imap_use_ssl', 'imap_username',
|
||||
'imap_password', 'imap_folder',
|
||||
'smtp_host', 'smtp_port', 'smtp_use_tls', 'smtp_use_ssl',
|
||||
'smtp_username', 'smtp_password',
|
||||
'is_active', 'is_default', 'last_check_at', 'last_error',
|
||||
'emails_processed_count', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['tenant', 'last_check_at', 'last_error',
|
||||
'emails_processed_count', 'created_at', 'updated_at']
|
||||
extra_kwargs = {
|
||||
'imap_password': {'write_only': True},
|
||||
'smtp_password': {'write_only': True},
|
||||
}
|
||||
|
||||
class TicketEmailAddressListSerializer(serializers.ModelSerializer):
|
||||
"""Lightweight serializer without passwords"""
|
||||
class Meta:
|
||||
model = TicketEmailAddress
|
||||
fields = [
|
||||
'id', 'display_name', 'email_address', 'color',
|
||||
'is_active', 'is_default', 'last_check_at',
|
||||
'emails_processed_count'
|
||||
]
|
||||
```
|
||||
|
||||
Update `TicketSerializer` to include email address:
|
||||
|
||||
```python
|
||||
class TicketSerializer(serializers.ModelSerializer):
|
||||
# ... existing fields ...
|
||||
source_email_address = TicketEmailAddressListSerializer(read_only=True)
|
||||
```
|
||||
|
||||
#### 3.2 Create ViewSet
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/views.py`
|
||||
|
||||
```python
|
||||
class TicketEmailAddressViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing ticket email addresses.
|
||||
Only business owners and managers can manage email addresses.
|
||||
"""
|
||||
serializer_class = TicketEmailAddressSerializer
|
||||
permission_classes = [IsTenantUser]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
# Business users see their own email addresses
|
||||
if user.role in ['owner', 'manager', 'staff']:
|
||||
return TicketEmailAddress.objects.filter(
|
||||
tenant=user.tenant
|
||||
)
|
||||
# Platform users see all
|
||||
elif user.role in ['superuser', 'platform_manager']:
|
||||
return TicketEmailAddress.objects.all()
|
||||
return TicketEmailAddress.objects.none()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return TicketEmailAddressListSerializer
|
||||
return TicketEmailAddressSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# Automatically set tenant from current user
|
||||
serializer.save(tenant=self.request.user.tenant)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def test_imap(self, request, pk=None):
|
||||
"""Test IMAP connection for this email address"""
|
||||
email_address = self.get_object()
|
||||
# Test IMAP connection logic
|
||||
return Response({'status': 'success'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def test_smtp(self, request, pk=None):
|
||||
"""Test SMTP connection for this email address"""
|
||||
email_address = self.get_object()
|
||||
# Test SMTP connection logic
|
||||
return Response({'status': 'success'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def fetch_now(self, request, pk=None):
|
||||
"""Manually trigger email fetch for this address"""
|
||||
email_address = self.get_object()
|
||||
# Trigger email fetch
|
||||
return Response({'status': 'fetching'})
|
||||
```
|
||||
|
||||
#### 3.3 Add URL Routes
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/urls.py`
|
||||
|
||||
```python
|
||||
router.register(r'email-addresses', views.TicketEmailAddressViewSet, basename='ticketemailaddress')
|
||||
```
|
||||
|
||||
### Phase 4: Frontend - React Hooks
|
||||
|
||||
#### 4.1 Create API Client Functions
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/api/ticketEmailAddresses.ts` (new file)
|
||||
|
||||
```typescript
|
||||
export interface TicketEmailAddress {
|
||||
id: number;
|
||||
display_name: string;
|
||||
email_address: string;
|
||||
color: string;
|
||||
imap_host: string;
|
||||
imap_port: number;
|
||||
imap_use_ssl: boolean;
|
||||
imap_username: string;
|
||||
imap_password?: string;
|
||||
imap_folder: string;
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
smtp_use_tls: boolean;
|
||||
smtp_use_ssl: boolean;
|
||||
smtp_username: string;
|
||||
smtp_password?: string;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
last_check_at?: string;
|
||||
last_error?: string;
|
||||
emails_processed_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type TicketEmailAddressCreate = Omit<TicketEmailAddress, 'id' | 'last_check_at' | 'last_error' | 'emails_processed_count' | 'created_at' | 'updated_at'>;
|
||||
|
||||
export const getTicketEmailAddresses = async (): Promise<TicketEmailAddress[]> => {
|
||||
const response = await apiClient.get('/tickets/email-addresses/');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createTicketEmailAddress = async (data: TicketEmailAddressCreate): Promise<TicketEmailAddress> => {
|
||||
const response = await apiClient.post('/tickets/email-addresses/', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateTicketEmailAddress = async (id: number, data: Partial<TicketEmailAddressCreate>): Promise<TicketEmailAddress> => {
|
||||
const response = await apiClient.patch(`/tickets/email-addresses/${id}/`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteTicketEmailAddress = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/tickets/email-addresses/${id}/`);
|
||||
};
|
||||
|
||||
export const testImapConnection = async (id: number): Promise<{ status: string; message?: string }> => {
|
||||
const response = await apiClient.post(`/tickets/email-addresses/${id}/test_imap/`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const testSmtpConnection = async (id: number): Promise<{ status: string; message?: string }> => {
|
||||
const response = await apiClient.post(`/tickets/email-addresses/${id}/test_smtp/`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const fetchEmailsNow = async (id: number): Promise<{ status: string }> => {
|
||||
const response = await apiClient.post(`/tickets/email-addresses/${id}/fetch_now/`);
|
||||
return response.data;
|
||||
};
|
||||
```
|
||||
|
||||
#### 4.2 Create React Query Hooks
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/hooks/useTicketEmailAddresses.ts` (new file)
|
||||
|
||||
```typescript
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getTicketEmailAddresses,
|
||||
createTicketEmailAddress,
|
||||
updateTicketEmailAddress,
|
||||
deleteTicketEmailAddress,
|
||||
testImapConnection,
|
||||
testSmtpConnection,
|
||||
fetchEmailsNow,
|
||||
TicketEmailAddress,
|
||||
TicketEmailAddressCreate,
|
||||
} from '../api/ticketEmailAddresses';
|
||||
|
||||
const QUERY_KEY = 'ticketEmailAddresses';
|
||||
|
||||
export const useTicketEmailAddresses = () => {
|
||||
return useQuery<TicketEmailAddress[]>({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: getTicketEmailAddresses,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TicketEmailAddressCreate) => createTicketEmailAddress(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<TicketEmailAddressCreate> }) =>
|
||||
updateTicketEmailAddress(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTicketEmailAddress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestImapConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testImapConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestSmtpConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testSmtpConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useFetchEmailsNow = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => fetchEmailsNow(id),
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 5: Frontend - React Components
|
||||
|
||||
#### 5.1 Email Address Management Component
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/components/TicketEmailAddressManager.tsx` (new file)
|
||||
|
||||
Features:
|
||||
- List all email addresses for the business
|
||||
- Add new email address
|
||||
- Edit existing email address
|
||||
- Delete email address
|
||||
- Test IMAP/SMTP connections
|
||||
- Set default email address
|
||||
- Color picker for visual identification
|
||||
- Enable/disable email addresses
|
||||
|
||||
#### 5.2 Update Ticket List UI
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/pages/Tickets.tsx`
|
||||
|
||||
Modify ticket rows to include colored left border:
|
||||
|
||||
```tsx
|
||||
<div
|
||||
className="ticket-row"
|
||||
style={{
|
||||
borderLeft: ticket.source_email_address
|
||||
? `4px solid ${ticket.source_email_address.color}`
|
||||
: '4px solid transparent'
|
||||
}}
|
||||
>
|
||||
{/* Ticket content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 5.3 Update Types
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/types.ts`
|
||||
|
||||
```typescript
|
||||
export interface TicketEmailAddress {
|
||||
id: number;
|
||||
display_name: string;
|
||||
email_address: string;
|
||||
color: string;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
last_check_at?: string;
|
||||
emails_processed_count: number;
|
||||
}
|
||||
|
||||
export interface Ticket {
|
||||
// ... existing fields ...
|
||||
source_email_address?: TicketEmailAddress;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.4 Add to Business Settings
|
||||
|
||||
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/pages/Settings.tsx`
|
||||
|
||||
Add new tab for "Email Addresses" that renders `TicketEmailAddressManager` component.
|
||||
|
||||
### Phase 6: Database Migration
|
||||
|
||||
#### 6.1 Create Migration
|
||||
|
||||
```bash
|
||||
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations tickets
|
||||
```
|
||||
|
||||
#### 6.2 Run Migration
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml exec django python manage.py migrate tickets
|
||||
```
|
||||
|
||||
#### 6.3 Data Migration (if needed)
|
||||
|
||||
If there's existing `TicketEmailSettings` data, create a data migration to convert it to `TicketEmailAddress` records for each tenant.
|
||||
|
||||
### Phase 7: Testing
|
||||
|
||||
#### 7.1 Backend Tests
|
||||
|
||||
- Test email address CRUD operations
|
||||
- Test email receiver with multiple addresses
|
||||
- Test email sender using correct source address
|
||||
- Test tenant isolation
|
||||
|
||||
#### 7.2 Frontend Tests
|
||||
|
||||
- Test email address list rendering
|
||||
- Test add/edit/delete operations
|
||||
- Test connection testing UI
|
||||
- Test ticket list color borders
|
||||
|
||||
### Phase 8: Documentation
|
||||
|
||||
#### 8.1 User Documentation
|
||||
|
||||
- How to add email addresses
|
||||
- How to configure IMAP/SMTP settings
|
||||
- How to test connections
|
||||
- Color coding explanation
|
||||
|
||||
#### 8.2 Developer Documentation
|
||||
|
||||
- API endpoints documentation
|
||||
- Model relationships
|
||||
- Email processing flow
|
||||
- Celery task schedule (if applicable)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Option 1: Keep Legacy System (Recommended)
|
||||
|
||||
- Keep `TicketEmailSettings` for platform-level configuration
|
||||
- New `TicketEmailAddress` for per-business configuration
|
||||
- Businesses can opt-in to multi-email system
|
||||
- Existing single-email businesses continue working
|
||||
|
||||
### Option 2: Full Migration
|
||||
|
||||
- Deprecate `TicketEmailSettings`
|
||||
- Migrate all existing data to `TicketEmailAddress`
|
||||
- All businesses use new system
|
||||
|
||||
**Recommendation:** Option 1 for backward compatibility
|
||||
|
||||
## Risks & Considerations
|
||||
|
||||
1. **Security**
|
||||
- Email passwords stored in database (consider encryption)
|
||||
- SMTP/IMAP credentials exposure risk
|
||||
- Recommend OAuth2 for Gmail/Outlook in future
|
||||
|
||||
2. **Performance**
|
||||
- Multiple IMAP connections may increase load
|
||||
- Consider Celery task queue for email fetching
|
||||
- Implement rate limiting
|
||||
|
||||
3. **Email Deliverability**
|
||||
- Each business responsible for their own SPF/DKIM records
|
||||
- No centralized email reputation management
|
||||
|
||||
4. **UI/UX**
|
||||
- Color picker needs to be user-friendly
|
||||
- Color accessibility (contrast ratio)
|
||||
- Mobile responsiveness
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **OAuth2 Support**
|
||||
- Google Workspace integration
|
||||
- Microsoft 365 integration
|
||||
|
||||
2. **Email Templates Per Address**
|
||||
- Different signatures per email address
|
||||
- Custom auto-responses
|
||||
|
||||
3. **Analytics**
|
||||
- Email volume by address
|
||||
- Response time by address
|
||||
|
||||
4. **Auto-Assignment**
|
||||
- Route tickets to specific staff based on email address
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
- **Phase 1-2 (Backend Models & Logic):** 2-3 days
|
||||
- **Phase 3 (Backend API):** 1-2 days
|
||||
- **Phase 4-5 (Frontend):** 3-4 days
|
||||
- **Phase 6-7 (Migration & Testing):** 1-2 days
|
||||
- **Phase 8 (Documentation):** 1 day
|
||||
|
||||
**Total Estimated Time:** 8-12 days
|
||||
|
||||
## Approval Required
|
||||
|
||||
Before proceeding with implementation, please confirm:
|
||||
|
||||
1. ✅ Per-business email addresses (not platform-wide)
|
||||
2. ✅ Businesses provide their own IMAP/SMTP credentials
|
||||
3. ✅ Colored left border for visual identification
|
||||
4. ✅ Email address management in business settings (not platform dashboard)
|
||||
5. ⚠️ Security approach for storing email passwords
|
||||
6. ⚠️ Migration strategy (keep legacy vs full migration)
|
||||
|
||||
## Questions for Product Owner
|
||||
|
||||
1. Should we encrypt email passwords in the database?
|
||||
2. Do we need email address approval workflow (platform admin approval)?
|
||||
3. Should there be a limit on number of email addresses per business?
|
||||
4. Do we need email forwarding (forward to another address)?
|
||||
5. Should unmatched emails (not tied to a ticket) create new tickets or be ignored?
|
||||
@@ -901,7 +901,7 @@ class PlatformEmailReceiver:
|
||||
is_sandbox=False,
|
||||
external_email=email_data['from_address'] if not user else None,
|
||||
external_name=email_data['from_name'] if not user else '',
|
||||
source_email_address_id=self.email_address.id,
|
||||
# Note: source_email_address is for TicketEmailAddress, not PlatformEmailAddress
|
||||
)
|
||||
|
||||
TicketComment.objects.create(
|
||||
|
||||
Reference in New Issue
Block a user