feat: Add OAuth email integration and production deployment config

- Add OAuthCredential model for storing Google/Microsoft OAuth tokens
- Add email provider auto-detection endpoint (Gmail, Outlook, Yahoo, etc.)
- Add EmailConfigWizard frontend component with step-by-step setup
- Add OAuth flow endpoints for Google and Microsoft XOAUTH2
- Update production settings to make AWS, Sentry, Mailgun optional
- Update Traefik config for wildcard subdomain routing
- Add logo resize utility script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-29 21:26:17 -05:00
parent cfc1b36ada
commit 7b0cf62019
22 changed files with 3075 additions and 96 deletions

View File

@@ -0,0 +1,133 @@
# Email Configuration Wizard Plan
## Overview
Create a step-by-step wizard for configuring email settings with:
1. Auto-detection of IMAP/SMTP settings from email address
2. OAuth support for Gmail accounts
3. Manual configuration fallback
## Wizard Steps
### Step 1: Email Address Entry
- User enters their support email address (e.g., support@company.com)
- System extracts domain and attempts auto-detection
- Shows detected provider (Gmail, Outlook, Yahoo, custom domain)
### Step 2: Authentication Method Selection
- **For Gmail**: Show "Connect with Google" OAuth button
- **For Outlook/Microsoft 365**: Show "Connect with Microsoft" OAuth button
- **For others**: Show manual configuration option
### Step 3a: OAuth Flow (Gmail/Microsoft)
- Redirect to OAuth provider
- Request mail scopes (IMAP, SMTP access)
- Store OAuth tokens for authentication
- Auto-configure IMAP/SMTP settings
### Step 3b: Manual Configuration
- Pre-fill detected IMAP/SMTP settings
- Allow user to modify if needed
- Password/app-specific password entry
### Step 4: Test & Verify
- Test IMAP connection
- Test SMTP connection
- Show success or troubleshooting steps
### Step 5: Additional Settings
- From name configuration
- Check interval
- Delete after processing toggle
## Email Provider Database
Common providers with auto-detection:
| Domain | Provider | IMAP Host | IMAP Port | SMTP Host | SMTP Port | OAuth |
|--------|----------|-----------|-----------|-----------|-----------|-------|
| gmail.com | Gmail | imap.gmail.com | 993 | smtp.gmail.com | 587 | Yes |
| googlemail.com | Gmail | imap.gmail.com | 993 | smtp.gmail.com | 587 | Yes |
| outlook.com | Microsoft | outlook.office365.com | 993 | smtp.office365.com | 587 | Yes |
| hotmail.com | Microsoft | outlook.office365.com | 993 | smtp.office365.com | 587 | Yes |
| live.com | Microsoft | outlook.office365.com | 993 | smtp.office365.com | 587 | Yes |
| yahoo.com | Yahoo | imap.mail.yahoo.com | 993 | smtp.mail.yahoo.com | 587 | No |
| icloud.com | Apple | imap.mail.me.com | 993 | smtp.mail.me.com | 587 | No |
| aol.com | AOL | imap.aol.com | 993 | smtp.aol.com | 587 | No |
For custom domains: Use MX record lookup to detect if hosted by Gmail/Microsoft
## Backend Changes
### New API Endpoints
1. `POST /api/tickets/email-settings/detect/`
- Input: `{ email: "support@company.com" }`
- Output: Detected provider info and suggested settings
2. `POST /api/tickets/email-settings/oauth/google/`
- Initiate Google OAuth flow for Gmail access
3. `POST /api/tickets/email-settings/oauth/google/callback/`
- Handle OAuth callback, store tokens
4. `POST /api/tickets/email-settings/oauth/microsoft/`
- Initiate Microsoft OAuth flow
5. `POST /api/tickets/email-settings/oauth/microsoft/callback/`
- Handle Microsoft OAuth callback
### Model Changes
Add to TicketEmailSettings:
- `oauth_provider`: CharField (google, microsoft, null)
- `oauth_access_token`: TextField (encrypted)
- `oauth_refresh_token`: TextField (encrypted)
- `oauth_token_expiry`: DateTimeField
- `use_oauth`: BooleanField
### OAuth Scopes Required
**Google Gmail API:**
- `https://mail.google.com/` (full mail access for IMAP/SMTP)
- OR use Gmail API directly instead of IMAP
**Microsoft Graph API:**
- `https://outlook.office.com/IMAP.AccessAsUser.All`
- `https://outlook.office.com/SMTP.Send`
## Frontend Components
### EmailConfigWizard.tsx
Main wizard component with step navigation
### Steps:
1. EmailAddressStep - Email input with domain detection
2. AuthMethodStep - OAuth vs manual selection
3. OAuthConnectStep - OAuth flow handling
4. ManualConfigStep - IMAP/SMTP form fields
5. TestConnectionStep - Connection testing
6. FinalSettingsStep - Additional options
## Implementation Order
1. Backend: Email provider detection endpoint
2. Frontend: Wizard UI with steps
3. Backend: Google OAuth integration
4. Frontend: OAuth flow handling
5. Backend: Microsoft OAuth integration
6. Testing and refinement
## Questions to Resolve
1. Should we use IMAP/SMTP with OAuth tokens, or switch to Gmail/Graph API?
- IMAP/SMTP with XOAUTH2 is simpler, works with existing code
- API approach is more modern but requires rewriting email fetcher
2. Store OAuth tokens in TicketEmailSettings or separate model?
- Same model is simpler
- Separate model allows multiple OAuth connections
3. How to handle token refresh?
- Background task to refresh before expiry
- Refresh on-demand when making email requests

View File

@@ -78,6 +78,25 @@ export interface FetchNowResult {
processed: number;
}
export interface EmailProviderDetectResult {
success: boolean;
email: string;
domain: string;
detected: boolean;
detected_via?: 'domain_lookup' | 'mx_record';
provider: 'google' | 'microsoft' | 'yahoo' | 'apple' | 'aol' | 'zoho' | 'protonmail' | 'unknown';
display_name: string;
imap_host?: string;
imap_port?: number;
smtp_host?: string;
smtp_port?: number;
oauth_supported: boolean;
message?: string;
notes?: string;
suggested_imap_port?: number;
suggested_smtp_port?: number;
}
export interface IncomingTicketEmail {
id: number;
message_id: string;
@@ -167,3 +186,77 @@ export const reprocessIncomingEmail = async (id: number): Promise<{
const response = await apiClient.post(`/api/tickets/incoming-emails/${id}/reprocess/`);
return response.data;
};
/**
* Detect email provider from email address
* Auto-detects Gmail, Outlook, Yahoo, iCloud, etc. from domain
* Also checks MX records for custom domains using Google Workspace or Microsoft 365
*/
export const detectEmailProvider = async (email: string): Promise<EmailProviderDetectResult> => {
const response = await apiClient.post('/api/tickets/email-settings/detect/', { email });
return response.data;
};
// OAuth types and functions
export interface OAuthStatusResult {
google: { configured: boolean };
microsoft: { configured: boolean };
}
export interface OAuthInitiateResult {
success: boolean;
authorization_url?: string;
error?: string;
}
export interface OAuthCredential {
id: number;
provider: 'google' | 'microsoft';
email: string;
purpose: string;
is_valid: boolean;
is_expired: boolean;
last_used_at: string | null;
last_error: string;
created_at: string;
}
/**
* Get OAuth configuration status
*/
export const getOAuthStatus = async (): Promise<OAuthStatusResult> => {
const response = await apiClient.get('/api/oauth/status/');
return response.data;
};
/**
* Initiate Google OAuth flow
*/
export const initiateGoogleOAuth = async (purpose: string = 'email'): Promise<OAuthInitiateResult> => {
const response = await apiClient.post('/api/oauth/google/initiate/', { purpose });
return response.data;
};
/**
* Initiate Microsoft OAuth flow
*/
export const initiateMicrosoftOAuth = async (purpose: string = 'email'): Promise<OAuthInitiateResult> => {
const response = await apiClient.post('/api/oauth/microsoft/initiate/', { purpose });
return response.data;
};
/**
* List OAuth credentials
*/
export const getOAuthCredentials = async (): Promise<OAuthCredential[]> => {
const response = await apiClient.get('/api/oauth/credentials/');
return response.data;
};
/**
* Delete OAuth credential
*/
export const deleteOAuthCredential = async (id: number): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.delete(`/api/oauth/credentials/${id}/`);
return response.data;
};

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,19 @@ import {
fetchEmailsNow,
getIncomingEmails,
reprocessIncomingEmail,
detectEmailProvider,
getOAuthStatus,
initiateGoogleOAuth,
initiateMicrosoftOAuth,
getOAuthCredentials,
deleteOAuthCredential,
TicketEmailSettings,
TicketEmailSettingsUpdate,
IncomingTicketEmail,
EmailProviderDetectResult,
OAuthStatusResult,
OAuthInitiateResult,
OAuthCredential,
} from '../api/ticketEmailSettings';
const QUERY_KEY = 'ticketEmailSettings';
@@ -103,4 +113,77 @@ export const useReprocessIncomingEmail = () => {
});
};
export type { TicketEmailSettings, TicketEmailSettingsUpdate, IncomingTicketEmail };
/**
* Hook to detect email provider from email address
*/
export const useDetectEmailProvider = () => {
return useMutation({
mutationFn: (email: string) => detectEmailProvider(email),
});
};
// OAuth Hooks
const OAUTH_STATUS_KEY = 'oauthStatus';
const OAUTH_CREDENTIALS_KEY = 'oauthCredentials';
/**
* Hook to get OAuth configuration status
*/
export const useOAuthStatus = () => {
return useQuery<OAuthStatusResult>({
queryKey: [OAUTH_STATUS_KEY],
queryFn: getOAuthStatus,
});
};
/**
* Hook to initiate Google OAuth flow
*/
export const useInitiateGoogleOAuth = () => {
return useMutation({
mutationFn: (purpose: string = 'email') => initiateGoogleOAuth(purpose),
});
};
/**
* Hook to initiate Microsoft OAuth flow
*/
export const useInitiateMicrosoftOAuth = () => {
return useMutation({
mutationFn: (purpose: string = 'email') => initiateMicrosoftOAuth(purpose),
});
};
/**
* Hook to list OAuth credentials
*/
export const useOAuthCredentials = () => {
return useQuery<OAuthCredential[]>({
queryKey: [OAUTH_CREDENTIALS_KEY],
queryFn: getOAuthCredentials,
});
};
/**
* Hook to delete OAuth credential
*/
export const useDeleteOAuthCredential = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteOAuthCredential(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [OAUTH_CREDENTIALS_KEY] });
},
});
};
export type {
TicketEmailSettings,
TicketEmailSettingsUpdate,
IncomingTicketEmail,
EmailProviderDetectResult,
OAuthStatusResult,
OAuthInitiateResult,
OAuthCredential,
};

View File

@@ -55,7 +55,8 @@ import {
useTestSmtpConnection,
useFetchEmailsNow,
} from '../../hooks/useTicketEmailSettings';
import { Send } from 'lucide-react';
import { Send, Wand2 } from 'lucide-react';
import EmailConfigWizard from '../../components/EmailConfigWizard';
type TabType = 'general' | 'stripe' | 'tiers' | 'oauth';
@@ -119,12 +120,14 @@ const PlatformSettings: React.FC = () => {
const GeneralSettingsTab: React.FC = () => {
const { t } = useTranslation();
const { data: emailSettings, isLoading, error } = useTicketEmailSettings();
const { data: emailSettings, isLoading, error, refetch } = useTicketEmailSettings();
const updateMutation = useUpdateTicketEmailSettings();
const testImapMutation = useTestImapConnection();
const testSmtpMutation = useTestSmtpConnection();
const fetchNowMutation = useFetchEmailsNow();
const [showWizard, setShowWizard] = useState(false);
const [formData, setFormData] = useState({
// IMAP settings
imap_host: '',
@@ -228,14 +231,39 @@ const GeneralSettingsTab: React.FC = () => {
);
}
// Show wizard if requested
if (showWizard) {
return (
<div className="space-y-6">
<EmailConfigWizard
onComplete={() => {
setShowWizard(false);
refetch();
}}
onCancel={() => setShowWizard(false)}
initialEmail={emailSettings?.imap_username || ''}
/>
</div>
);
}
return (
<div className="space-y-6">
{/* Email Processing Status */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Mail className="w-5 h-5" />
{t('platform.settings.emailProcessing', 'Support Email Processing')}
</h2>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Mail className="w-5 h-5" />
{t('platform.settings.emailProcessing', 'Support Email Processing')}
</h2>
<button
onClick={() => setShowWizard(true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
>
<Wand2 className="w-4 h-4" />
{t('platform.settings.setupWizard', 'Setup Wizard')}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">