Coverage for modules\email\api.py: 23%
275 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 09:25 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 09:25 +0800
1"""
2Email API - REST endpoints for email module
3"""
4from typing import List
5from uuid import UUID
6from ninja import Router, Query
7from ninja.pagination import paginate
8from django.shortcuts import get_object_or_404
9from django.utils import timezone
10from django.http import QueryDict
11from core.auth import JWTAuth
12from core.permissions import require_tenant
13from core.pagination import StandardPagination
15from .models import EmailAccount, EmailFolder, EmailMessage, EmailAttachment, EmailFilter, InternalEmail, InternalEmailRecipient
16from .schemas import (
17 EmailAccountOut, EmailAccountCreateIn, EmailAccountUpdateIn,
18 EmailFolderOut, EmailFolderCreateIn, EmailMessageOut, EmailMessageDetailOut, EmailAttachmentOut,
19 EmailSendIn, EmailDraftIn, EmailMessageUpdateIn,
20 EmailFilterOut, EmailFilterCreateIn, EmailFilterCondition,
21 InternalEmailOut, InternalEmailSendIn, InternalEmailCountOut,
22)
23from .services import EmailAccountService, EmailFolderService, EmailMessageService
25router = Router(tags=['email'])
28# ============= Email Accounts =============
30@router.get('/accounts', response=List[EmailAccountOut], auth=JWTAuth())
31def list_email_accounts(request):
32 """List all connected email accounts."""
33 tenant_id = require_tenant(request)
34 accounts = EmailAccount.objects.filter(
35 tenant_id=tenant_id,
36 user=request.user.id
37 )
39 result = []
40 for account in accounts:
41 # Get unread inbox count using raw SQL to avoid ORM UUID issues
42 from django.db import connection
43 unread = 0
44 with connection.cursor() as cursor:
45 cursor.execute(
46 "SELECT unread_count FROM email_emailfolder WHERE account_id = %s AND folder_type = 'inbox'",
47 [str(account.id)]
48 )
49 row = cursor.fetchone()
50 if row:
51 unread = row[0]
53 result.append({
54 'id': account.id,
55 'provider': account.provider,
56 'connection_type': account.connection_type,
57 'email_address': account.email_address,
58 'display_name': account.display_name,
59 'is_primary': account.is_primary,
60 'sync_enabled': account.sync_enabled,
61 'last_sync_at': account.last_sync_at,
62 'is_verified': account.is_verified,
63 'unread_inbox_count': unread,
64 })
66 return result
69@router.post('/accounts', response=EmailAccountOut, auth=JWTAuth())
70def create_email_account(request, data: EmailAccountCreateIn):
71 """Connect a new email account (IMAP/SMTP)."""
72 import traceback
74 # Write debug to file to ensure we see it
75 with open('d:/fudo_debug.log', 'a') as f:
76 f.write(f"[API] Request received\n")
77 f.write(f"[API] Auth user: {request.user}\n")
78 f.write(f"[API] Authenticated: {request.user.is_authenticated if hasattr(request.user, 'is_authenticated') else 'N/A'}\n")
79 f.write(f"[API] Data: {data.model_dump()}\n")
81 tenant_id = require_tenant(request)
83 with open('d:/fudo_debug.log', 'a') as f:
84 f.write(f"[API] Tenant ID: {tenant_id}\n")
86 try:
87 account, success, message = EmailAccountService.create_imap_account(
88 user=request.user.id,
89 tenant_id=tenant_id,
90 data=data.model_dump()
91 )
92 with open('d:/fudo_debug.log', 'a') as f:
93 f.write(f"[API] Service result: success={success}, message={message}\n")
94 except Exception as e:
95 with open('d:/fudo_debug.log', 'a') as f:
96 f.write(f"[API] Exception: {type(e).__name__}: {str(e)}\n")
97 f.write(traceback.format_exc())
98 raise
100 if not success:
101 from ninja import HttpError
102 raise HttpError(400, message)
104 return EmailAccountOut(
105 id=account.id,
106 provider=account.provider,
107 connection_type=account.connection_type,
108 email_address=account.email_address,
109 display_name=account.display_name or "",
110 is_primary=account.is_primary,
111 sync_enabled=account.sync_enabled,
112 last_sync_at=account.last_sync_at,
113 is_verified=account.is_verified,
114 unread_inbox_count=0,
115 )
118@router.get('/test', auth=JWTAuth())
119def test_endpoint(request):
120 """Test endpoint to check auth and tenant."""
121 tenant_id = require_tenant(request)
122 return {
123 'status': 'ok',
124 'user_id': request.user.id,
125 'tenant_id': str(tenant_id) if tenant_id else None,
126 'email': request.user.email if hasattr(request.user, 'email') else 'no email',
127 }
130@router.patch('/accounts/{account_id}', response=EmailAccountOut, auth=JWTAuth())
131def update_email_account(request, account_id: UUID, data: EmailAccountUpdateIn):
132 """Update email account settings."""
133 tenant_id = require_tenant(request)
134 account = get_object_or_404(
135 EmailAccount,
136 id=account_id,
137 tenant_id=tenant_id
138 )
140 for key, value in data.model_dump(exclude_none=True).items():
141 setattr(account, key, value)
142 account.save()
144 return {
145 'id': account.id,
146 'provider': account.provider,
147 'connection_type': account.connection_type,
148 'email_address': account.email_address,
149 'display_name': account.display_name,
150 'is_primary': account.is_primary,
151 'sync_enabled': account.sync_enabled,
152 'last_sync_at': account.last_sync_at,
153 'is_verified': account.is_verified,
154 'unread_inbox_count': 0,
155 }
158@router.delete('/accounts/{account_id}', auth=JWTAuth())
159def delete_email_account(request, account_id: UUID):
160 """Disconnect an email account."""
161 tenant_id = require_tenant(request)
162 account = get_object_or_404(
163 EmailAccount,
164 id=account_id,
165 tenant_id=tenant_id
166 )
167 account.delete()
168 return {'success': True}
171@router.post('/accounts/{account_id}/sync', auth=JWTAuth())
172def sync_email_account(request, account_id: UUID):
173 """Sync emails for an account."""
174 tenant_id = require_tenant(request)
175 account = get_object_or_404(
176 EmailAccount,
177 id=account_id,
178 tenant_id=tenant_id
179 )
181 success, message = EmailAccountService.sync_account(account)
183 if success:
184 return {'success': True, 'message': message}
185 else:
186 return {'success': False, 'error': message}
189# ============= Gmail OAuth =============
191@router.get('/oauth/authorize', auth=JWTAuth())
192def gmail_oauth_authorize(request):
193 """
194 Get Gmail OAuth authorization URL.
195 Redirects user to Google to authorize access.
196 """
197 try:
198 from .gmail_oauth import GmailOAuthService
199 oauth = GmailOAuthService()
200 auth_url = oauth.get_authorization_url()
201 return {'authorization_url': auth_url}
202 except ImportError as e:
203 return {'error': str(e)}, 500
206@router.get('/oauth/callback', auth=JWTAuth())
207def gmail_oauth_callback(request, code: str = None, error: str = None):
208 """
209 Handle Gmail OAuth callback.
210 Exchanges authorization code for tokens and creates email account.
211 """
212 if error:
213 return {'success': False, 'error': error}
215 if not code:
216 return {'success': False, 'error': 'No authorization code provided'}
218 try:
219 from .gmail_oauth import GmailOAuthService
220 oauth = GmailOAuthService()
222 if oauth.exchange_code_for_tokens(code):
223 # Create email account with OAuth tokens
224 tenant_id = require_tenant(request)
225 account, success, message = EmailAccountService.create_oauth_account(
226 user=request.user.id,
227 tenant_id=tenant_id,
228 credentials={
229 'token': oauth.creds.to_json() if oauth.creds else None,
230 'refresh_token': oauth.creds.refresh_token if oauth.creds else None,
231 }
232 )
234 if success:
235 return {
236 'success': True,
237 'account_id': str(account.id),
238 'message': 'Email account connected successfully'
239 }
240 else:
241 return {'success': False, 'error': message}
242 else:
243 return {'success': False, 'error': 'Failed to exchange code for tokens'}
244 except ImportError as e:
245 return {'success': False, 'error': str(e)}, 500
246 except Exception as e:
247 return {'success': False, 'error': str(e)}, 500
250# ============= Email Folders =============
252@router.get('/folders', response=List[EmailFolderOut], auth=JWTAuth())
253def list_email_folders(request):
254 """List folders for an email account."""
255 import uuid
256 import sqlite3
258 tenant_id = require_tenant(request)
259 account_id = request.GET.get('account_id')
261 if not account_id:
262 return []
264 # Convert account_id string to proper UUID format
265 account_id_hex = account_id.replace('-', '')
266 tenant_id_hex = tenant_id.hex if hasattr(tenant_id, 'hex') else str(tenant_id).replace('-', '')
268 # Use direct sqlite3 to avoid Django's debug SQL issues
269 db_path = 'd:/FUDO/erp/apps/backend/db.sqlite3'
270 with sqlite3.connect(db_path) as conn:
271 cursor = conn.cursor()
272 cursor.execute(
273 """SELECT CAST(id AS TEXT), name, folder_type, unread_count FROM email_emailfolder
274 WHERE tenant_id = ? AND account_id = ?
275 ORDER BY folder_type, name""",
276 [tenant_id_hex, account_id_hex]
277 )
278 rows = cursor.fetchall()
280 return [
281 {
282 'id': str(row[0]), # id is integer, CAST returns text
283 'name': row[1],
284 'folder_type': row[2],
285 'unread_count': row[3],
286 }
287 for row in rows
288 ]
291@router.post('/folders', response=EmailFolderOut, auth=JWTAuth())
292def create_email_folder(request, data: EmailFolderCreateIn, account_id: UUID):
293 """Create a custom email folder."""
294 tenant_id = require_tenant(request)
295 account = get_object_or_404(
296 EmailAccount,
297 id=account_id,
298 tenant_id=tenant_id
299 )
301 # Only allow creating 'custom' type folders via API
302 if data.folder_type != 'custom':
303 from core.exceptions import HttpError
304 raise HttpError(400, "Only custom folders can be created via API")
306 # Check for duplicate name
307 existing = EmailFolder.objects.filter(
308 tenant_id=tenant_id,
309 account=account,
310 name=data.name
311 ).first()
312 if existing:
313 from core.exceptions import HttpError
314 raise HttpError(400, "A folder with this name already exists")
316 folder = EmailFolder.objects.create(
317 tenant_id=tenant_id,
318 account=account,
319 name=data.name,
320 folder_type='custom',
321 unread_count=0
322 )
324 return {
325 'id': folder.id,
326 'name': folder.name,
327 'folder_type': folder.folder_type,
328 'unread_count': folder.unread_count,
329 }
332@router.delete('/folders/{folder_id}', auth=JWTAuth())
333def delete_email_folder(request, account_id: UUID, folder_id: UUID):
334 """Delete a custom email folder."""
335 tenant_id = require_tenant(request)
336 account = get_object_or_404(
337 EmailAccount,
338 id=account_id,
339 tenant_id=tenant_id
340 )
342 folder = get_object_or_404(
343 EmailFolder,
344 id=folder_id,
345 account=account,
346 tenant_id=tenant_id
347 )
349 # Cannot delete system folders
350 if folder.folder_type != 'custom':
351 from core.exceptions import HttpError
352 raise HttpError(400, "Cannot delete system folders")
354 folder.delete()
355 return {'success': True, 'message': 'Folder deleted'}
358# ============= Email Filters/Rules =============
360@router.get('/filters', response=List[EmailFilterOut], auth=JWTAuth())
361def list_email_filters(request, account_id: UUID):
362 """List all filters for an email account."""
363 tenant_id = require_tenant(request)
364 account = get_object_or_404(
365 EmailAccount,
366 id=account_id,
367 tenant_id=tenant_id
368 )
370 filters = EmailFilter.objects.filter(
371 tenant_id=tenant_id,
372 account=account
373 ).order_by('priority', 'name')
375 return [
376 {
377 'id': f.id,
378 'name': f.name,
379 'conditions': f.conditions,
380 'action_type': f.action_type,
381 'action_folder_id': f.action_folder_id,
382 'is_active': f.is_active,
383 'priority': f.priority,
384 }
385 for f in filters
386 ]
389@router.post('/filters', response=EmailFilterOut, auth=JWTAuth())
390def create_email_filter(request, data: EmailFilterCreateIn, account_id: UUID):
391 """Create a new email filter/rule."""
392 tenant_id = require_tenant(request)
393 account = get_object_or_404(
394 EmailAccount,
395 id=account_id,
396 tenant_id=tenant_id
397 )
399 # Validate action_folder if provided
400 action_folder = None
401 if data.action_folder_id:
402 action_folder = get_object_or_404(
403 EmailFolder,
404 id=data.action_folder_id,
405 account=account,
406 tenant_id=tenant_id
407 )
409 email_filter = EmailFilter.objects.create(
410 tenant_id=tenant_id,
411 account=account,
412 name=data.name,
413 conditions={'conditions': [c.model_dump() for c in data.conditions]},
414 action_type=data.action_type,
415 action_folder=action_folder,
416 is_active=data.is_active,
417 priority=data.priority,
418 )
420 return {
421 'id': email_filter.id,
422 'name': email_filter.name,
423 'conditions': email_filter.conditions,
424 'action_type': email_filter.action_type,
425 'action_folder_id': email_filter.action_folder_id,
426 'is_active': email_filter.is_active,
427 'priority': email_filter.priority,
428 }
431@router.put('/filters/{filter_id}', response=EmailFilterOut, auth=JWTAuth())
432def update_email_filter(request, filter_id: UUID, data: EmailFilterCreateIn, account_id: UUID):
433 """Update an email filter."""
434 tenant_id = require_tenant(request)
435 account = get_object_or_404(
436 EmailAccount,
437 id=account_id,
438 tenant_id=tenant_id
439 )
441 email_filter = get_object_or_404(
442 EmailFilter,
443 id=filter_id,
444 account=account,
445 tenant_id=tenant_id
446 )
448 # Validate action_folder if provided
449 action_folder = None
450 if data.action_folder_id:
451 action_folder = get_object_or_404(
452 EmailFolder,
453 id=data.action_folder_id,
454 account=account,
455 tenant_id=tenant_id
456 )
458 email_filter.name = data.name
459 email_filter.conditions = {'conditions': [c.model_dump() for c in data.conditions]}
460 email_filter.action_type = data.action_type
461 email_filter.action_folder = action_folder
462 email_filter.is_active = data.is_active
463 email_filter.priority = data.priority
464 email_filter.save()
466 return {
467 'id': email_filter.id,
468 'name': email_filter.name,
469 'conditions': email_filter.conditions,
470 'action_type': email_filter.action_type,
471 'action_folder_id': email_filter.action_folder_id,
472 'is_active': email_filter.is_active,
473 'priority': email_filter.priority,
474 }
477@router.delete('/filters/{filter_id}', auth=JWTAuth())
478def delete_email_filter(request, filter_id: UUID, account_id: UUID):
479 """Delete an email filter."""
480 tenant_id = require_tenant(request)
481 account = get_object_or_404(
482 EmailAccount,
483 id=account_id,
484 tenant_id=tenant_id
485 )
487 email_filter = get_object_or_404(
488 EmailFilter,
489 id=filter_id,
490 account=account,
491 tenant_id=tenant_id
492 )
494 email_filter.delete()
495 return {'success': True, 'message': 'Filter deleted'}
498# ============= Email Messages =============
500@router.get('/messages', response=List[EmailMessageOut], auth=JWTAuth())
501def list_email_messages(
502 request,
503 account_id: UUID = Query(...),
504 folder: str = Query(default='INBOX'),
505 page: int = Query(default=1),
506 per_page: int = Query(default=50)
507):
508 """List messages in a folder."""
509 tenant_id = require_tenant(request)
510 account = get_object_or_404(
511 EmailAccount,
512 id=account_id,
513 tenant_id=tenant_id
514 )
516 # Fetch from server
517 messages = EmailMessageService.fetch_messages(
518 account=account,
519 folder_name=folder,
520 page=page,
521 per_page=per_page
522 )
524 # Handle both EmailMessage objects and dicts
525 return [
526 {
527 'id': msg['id'] if isinstance(msg, dict) else str(msg.id),
528 'message_id': msg['message_id'] if isinstance(msg, dict) else msg.message_id,
529 'subject': msg['subject'] if isinstance(msg, dict) else msg.subject,
530 'snippet': msg['snippet'] if isinstance(msg, dict) else msg.snippet,
531 'from_address': msg['from_address'] if isinstance(msg, dict) else msg.from_address,
532 'from_name': msg['from_name'] if isinstance(msg, dict) else msg.from_name,
533 'to_addresses': msg['to_addresses'] if isinstance(msg, dict) else (msg.to_addresses or []),
534 'cc_addresses': msg['cc_addresses'] if isinstance(msg, dict) else msg.cc_addresses,
535 'is_read': msg['is_read'] if isinstance(msg, dict) else msg.is_read,
536 'is_starred': msg['is_starred'] if isinstance(msg, dict) else msg.is_starred,
537 'is_draft': msg['is_draft'] if isinstance(msg, dict) else msg.is_draft,
538 'is_sent': msg['is_sent'] if isinstance(msg, dict) else msg.is_sent,
539 'sent_at': msg['sent_at'] if isinstance(msg, dict) else msg.sent_at,
540 'received_at': msg['received_at'] if isinstance(msg, dict) else msg.received_at,
541 'folder_id': msg['folder_id'] if isinstance(msg, dict) else str(msg.folder_id),
542 'account_id': msg['account_id'] if isinstance(msg, dict) else str(msg.account_id),
543 }
544 for msg in messages
545 ]
548@router.get('/messages/{message_id}', response=EmailMessageDetailOut, auth=JWTAuth())
549def get_email_message(request, message_id: UUID, account_id: UUID = Query(...)):
550 """Get message details with body."""
551 tenant_id = require_tenant(request)
552 account = get_object_or_404(
553 EmailAccount,
554 id=account_id,
555 tenant_id=tenant_id
556 )
558 msg = EmailMessageService.get_message_detail(account, message_id)
560 if not msg:
561 return {'error': 'Message not found'}
563 return {
564 'id': msg.id,
565 'message_id': msg.message_id,
566 'subject': msg.subject,
567 'snippet': msg.snippet,
568 'from_address': msg.from_address,
569 'from_name': msg.from_name,
570 'to_addresses': msg.to_addresses or [],
571 'cc_addresses': msg.cc_addresses,
572 'is_read': msg.is_read,
573 'is_starred': msg.is_starred,
574 'is_draft': msg.is_draft,
575 'is_sent': msg.is_sent,
576 'sent_at': msg.sent_at,
577 'received_at': msg.received_at,
578 'folder_id': msg.folder_id,
579 'account_id': msg.account_id,
580 'body_text': msg.body_text,
581 'body_html': msg.body_html,
582 'attachments': [
583 {
584 'id': att.id,
585 'file_name': att.file_name,
586 'content_type': att.content_type,
587 'size_bytes': att.size_bytes,
588 'file_path': att.file_path,
589 }
590 for att in msg.attachments.all()
591 ],
592 }
595@router.patch('/messages/{message_id}', response=EmailMessageOut, auth=JWTAuth())
596def update_email_message(request, message_id: UUID, data: EmailMessageUpdateIn):
597 """Update message (read, starred, flagged)."""
598 tenant_id = require_tenant(request)
599 msg = get_object_or_404(
600 EmailMessage,
601 id=message_id,
602 tenant_id=tenant_id
603 )
605 if data.is_read is not None:
606 if data.is_read:
607 EmailMessageService.mark_as_read(msg)
608 else:
609 EmailMessageService.mark_as_unread(msg)
611 if data.is_starred is not None:
612 msg.is_starred = data.is_starred
613 msg.save()
615 if data.is_flagged is not None:
616 msg.is_flagged = data.is_flagged
617 msg.save()
619 return {
620 'id': msg.id,
621 'message_id': msg.message_id,
622 'subject': msg.subject,
623 'snippet': msg.snippet,
624 'from_address': msg.from_address,
625 'from_name': msg.from_name,
626 'to_addresses': msg.to_addresses or [],
627 'cc_addresses': msg.cc_addresses,
628 'is_read': msg.is_read,
629 'is_starred': msg.is_starred,
630 'is_draft': msg.is_draft,
631 'is_sent': msg.is_sent,
632 'sent_at': msg.sent_at,
633 'received_at': msg.received_at,
634 'folder_id': msg.folder_id,
635 'account_id': msg.account_id,
636 }
639@router.delete('/messages/{message_id}', auth=JWTAuth())
640def delete_email_message(request, message_id: UUID):
641 """Delete a message."""
642 tenant_id = require_tenant(request)
643 msg = get_object_or_404(
644 EmailMessage,
645 id=message_id,
646 tenant_id=tenant_id
647 )
649 EmailMessageService.delete_message(msg)
650 return {'success': True}
653@router.post('/send', auth=JWTAuth())
654def send_email(request, data: EmailSendIn, account_id: UUID):
655 """Send an email."""
656 tenant_id = require_tenant(request)
657 account = get_object_or_404(
658 EmailAccount,
659 id=account_id,
660 tenant_id=tenant_id
661 )
663 to_addresses = [{'email': e} for e in data.to]
664 cc_addresses = [{'email': e} for e in data.cc] if data.cc else None
665 bcc_addresses = [{'email': e} for e in data.bcc] if data.bcc else None
667 success, message = EmailMessageService.send_email(
668 account=account,
669 to_addresses=to_addresses,
670 subject=data.subject,
671 body=data.body,
672 cc=cc_addresses,
673 bcc=bcc_addresses,
674 is_html=data.is_html
675 )
677 if success:
678 return {'success': True, 'message': message}
679 else:
680 return {'success': False, 'error': message}
683@router.post('/drafts', auth=JWTAuth())
684def save_draft(request, data: EmailDraftIn, account_id: UUID):
685 """Save a draft."""
686 tenant_id = require_tenant(request)
687 account = get_object_or_404(
688 EmailAccount,
689 id=account_id,
690 tenant_id=tenant_id
691 )
693 # Find draft folder
694 draft_folder = EmailFolder.objects.filter(
695 tenant_id=tenant_id,
696 account=account,
697 folder_type='draft'
698 ).first()
700 # Create or update draft
701 draft, _ = EmailMessage.objects.update_or_create(
702 tenant_id=tenant_id,
703 account=account,
704 folder=draft_folder,
705 message_id=f"<draft-{request.user.id}-{timezone.now().timestamp()}@local>",
706 defaults={
707 'from_address': account.email_address,
708 'from_name': account.display_name,
709 'to_addresses': [{'email': e} for e in data.to] if data.to else [],
710 'cc_addresses': [{'email': e} for e in data.cc] if data.cc else None,
711 'bcc_addresses': [{'email': e} for e in data.bcc] if data.bcc else None,
712 'subject': data.subject,
713 'body_text': data.body if not data.body.startswith('<') else None,
714 'body_html': data.body if data.body.startswith('<') else None,
715 'is_draft': True,
716 }
717 )
719 return {'id': str(draft.id), 'success': True}
722# ============= Internal Email =============
724@router.get('/internal', response=List[InternalEmailOut], auth=JWTAuth())
725def list_internal_emails(request, page: int = 1, per_page: int = 50):
726 """List internal emails for the current user."""
727 tenant_id = require_tenant(request)
728 user_id = request.user.id
730 # Get emails where user is recipient or sender
731 received = InternalEmailRecipient.objects.filter(
732 tenant_id=tenant_id,
733 recipient_id=user_id
734 ).select_related('email', 'email__sender').order_by('-email__sent_at')
736 start = (page - 1) * per_page
737 end = start + per_page
739 result = []
740 for recipient in received[start:end]:
741 email = recipient.email
742 result.append({
743 'id': email.id,
744 'subject': email.subject,
745 'body': email.body,
746 'sender_name': email.sender.display_name or email.sender.email,
747 'sender_email': email.sender.email,
748 'recipients': [
749 {'email': r.recipient.email, 'name': r.recipient.display_name or None}
750 for r in email.recipients.all()
751 ],
752 'sent_at': email.sent_at,
753 'is_read': recipient.is_read,
754 })
756 return result
759@router.get('/internal/count', response=InternalEmailCountOut, auth=JWTAuth())
760def get_internal_email_count(request):
761 """Get internal email unread count."""
762 tenant_id = require_tenant(request)
763 user_id = request.user.id
765 total = InternalEmailRecipient.objects.filter(
766 tenant_id=tenant_id,
767 recipient_id=user_id
768 ).count()
770 unread = InternalEmailRecipient.objects.filter(
771 tenant_id=tenant_id,
772 recipient_id=user_id,
773 is_read=False
774 ).count()
776 return {'unread_count': unread, 'total_count': total}
779@router.post('/internal', auth=JWTAuth())
780def send_internal_email(request, data: InternalEmailSendIn):
781 """Send an internal email to other ERP users."""
782 tenant_id = require_tenant(request)
783 user_id = request.user.id
785 # Create internal email
786 internal = InternalEmail.objects.create(
787 tenant_id=tenant_id,
788 sender_id=user_id,
789 subject=data.subject,
790 body=data.body,
791 )
793 # Add recipients
794 for recipient_id in data.to:
795 InternalEmailRecipient.objects.create(
796 tenant_id=tenant_id,
797 email=internal,
798 recipient_id=recipient_id,
799 )
801 return {'id': str(internal.id), 'success': True}
804@router.patch('/internal/{email_id}/read', auth=JWTAuth())
805def mark_internal_email_read(request, email_id: UUID):
806 """Mark internal email as read."""
807 tenant_id = require_tenant(request)
808 user_id = request.user.id
810 recipient = InternalEmailRecipient.objects.filter(
811 tenant_id=tenant_id,
812 email_id=email_id,
813 recipient_id=user_id
814 ).first()
816 if recipient:
817 recipient.is_read = True
818 recipient.read_at = timezone.now()
819 recipient.save()
821 return {'success': True}