Coverage for modules\email\models.py: 94%
114 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
1from django.db import models
2from core.models import TenantScopedModel, UUIDModel, TimeStampedModel
3from modules.authentication.models import User
6class EmailAccount(TenantScopedModel):
7 """Connected email account (IMAP/SMTP or OAuth)."""
9 PROVIDER_CHOICES = [
10 ('gmail', 'Gmail'),
11 ('outlook', 'Outlook'),
12 ('yahoo', 'Yahoo'),
13 ('other', 'Other'),
14 ]
16 CONNECTION_CHOICES = [
17 ('imap', 'IMAP/SMTP'),
18 ('oauth', 'OAuth2'),
19 ]
21 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_accounts')
22 provider = models.CharField(max_length=32, choices=PROVIDER_CHOICES, default='other')
23 connection_type = models.CharField(max_length=16, choices=CONNECTION_CHOICES, default='imap')
25 # Identity
26 email_address = models.EmailField()
27 display_name = models.CharField(max_length=128, blank=True)
29 # IMAP/SMTP settings
30 imap_host = models.CharField(max_length=256, blank=True, null=True)
31 imap_port = models.IntegerField(default=993)
32 smtp_host = models.CharField(max_length=256, blank=True, null=True)
33 smtp_port = models.IntegerField(default=587)
34 encrypted_password = models.CharField(max_length=512, blank=True, null=True)
36 # OAuth tokens
37 oauth_token = models.JSONField(blank=True, null=True)
38 oauth_refresh_token = models.CharField(max_length=512, blank=True, null=True)
40 # Settings
41 is_primary = models.BooleanField(default=False)
42 sync_enabled = models.BooleanField(default=True)
43 last_sync_at = models.DateTimeField(blank=True, null=True)
44 sync_interval = models.IntegerField(default=5) # minutes
46 # Security
47 use_ssl = models.BooleanField(default=True)
48 is_verified = models.BooleanField(default=False)
50 class Meta:
51 ordering = ['-is_primary', '-created_at']
53 def __str__(self):
54 return f"{self.email_address} ({self.get_provider_display()})"
57class EmailFolder(TenantScopedModel):
58 """Email folder (INBOX, SENT, DRAFTS, etc.)."""
60 FOLDER_TYPE_CHOICES = [
61 ('inbox', 'Inbox'),
62 ('sent', 'Sent'),
63 ('draft', 'Drafts'),
64 ('trash', 'Trash'),
65 ('spam', 'Spam'),
66 ('outbox', 'Outbox'),
67 ('custom', 'Custom'),
68 ]
70 account = models.ForeignKey(EmailAccount, on_delete=models.CASCADE, related_name='folders')
71 name = models.CharField(max_length=128)
72 remote_id = models.CharField(max_length=256, blank=True, null=True)
73 folder_type = models.CharField(max_length=16, choices=FOLDER_TYPE_CHOICES, default='custom')
74 unread_count = models.IntegerField(default=0)
76 class Meta:
77 ordering = ['folder_type', 'name']
78 unique_together = ['account', 'remote_id']
80 def __str__(self):
81 return f"{self.account.email_address}: {self.name}"
84class EmailFilter(UUIDModel, TimeStampedModel):
85 """Email filter/rule for automatic folder sorting."""
87 tenant_id = models.UUIDField(db_index=True)
88 account = models.ForeignKey(
89 EmailAccount,
90 on_delete=models.CASCADE,
91 related_name='filters'
92 )
93 name = models.CharField(max_length=128)
95 # Conditions (JSON field for flexibility)
96 # Example: {'field': 'from', 'operator': 'contains', 'value': 'newsletter'}
97 conditions = models.JSONField(default=dict)
99 # Actions
100 action_type = models.CharField(max_length=32) # 'move_to_folder', 'mark_read', 'star', 'delete'
101 action_folder = models.ForeignKey(
102 EmailFolder,
103 on_delete=models.SET_NULL,
104 null=True,
105 blank=True,
106 related_name='filter_actions'
107 )
109 # Rule settings
110 is_active = models.BooleanField(default=True)
111 priority = models.IntegerField(default=0) # Lower = higher priority
113 class Meta:
114 ordering = ['priority', 'name']
115 verbose_name_plural = 'Email filters'
117 def __str__(self):
118 return f"{self.name} ({self.account.email_address})"
121class EmailMessage(UUIDModel, TimeStampedModel):
122 """Email message."""
124 tenant_id = models.UUIDField(db_index=True)
125 account = models.ForeignKey(EmailAccount, on_delete=models.CASCADE, related_name='messages', null=True, blank=True)
126 folder = models.ForeignKey(EmailFolder, on_delete=models.SET_NULL, null=True, blank=True, related_name='messages')
128 # Identity
129 message_id = models.CharField(max_length=512) # RFC 822 Message-ID
130 uid = models.CharField(max_length=256, blank=True, null=True) # IMAP UID
132 # Addresses
133 from_address = models.CharField(max_length=256)
134 from_name = models.CharField(max_length=128, blank=True, null=True)
135 to_addresses = models.JSONField(default=list) # [{email, name}]
136 cc_addresses = models.JSONField(blank=True, null=True)
137 bcc_addresses = models.JSONField(blank=True, null=True)
139 # Content
140 subject = models.CharField(max_length=512)
141 snippet = models.CharField(max_length=256, blank=True)
142 body_text = models.TextField(blank=True, null=True)
143 body_html = models.TextField(blank=True, null=True)
145 # Metadata
146 is_read = models.BooleanField(default=False)
147 is_starred = models.BooleanField(default=False)
148 is_draft = models.BooleanField(default=False)
149 is_sent = models.BooleanField(default=False)
150 is_internal = models.BooleanField(default=False)
151 is_flagged = models.BooleanField(default=False)
153 # Dates
154 sent_at = models.DateTimeField(blank=True, null=True)
155 received_at = models.DateTimeField(blank=True, null=True)
157 # Size
158 size_bytes = models.IntegerField(default=0)
160 class Meta:
161 ordering = ['-received_at', '-created_at']
162 indexes = [
163 models.Index(fields=['tenant_id', 'folder', '-received_at']),
164 models.Index(fields=['tenant_id', 'account', '-received_at']),
165 ]
167 def __str__(self):
168 return f"{self.subject[:50]} - {self.from_address}"
171class EmailAttachment(UUIDModel, TimeStampedModel):
172 """Email attachment."""
174 tenant_id = models.UUIDField(db_index=True)
175 message = models.ForeignKey(EmailMessage, on_delete=models.CASCADE, related_name='attachments')
176 file_name = models.CharField(max_length=256)
177 content_type = models.CharField(max_length=128)
178 size_bytes = models.IntegerField(default=0)
179 remote_id = models.CharField(max_length=256, blank=True, null=True)
180 file_path = models.CharField(max_length=512, blank=True, null=True) # Local storage path
182 def __str__(self):
183 return self.file_name
186class InternalEmail(UUIDModel, TimeStampedModel):
187 """Internal ERP email between users."""
189 tenant_id = models.UUIDField(db_index=True)
190 sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_internal_emails')
191 recipients = models.ManyToManyField(User, related_name='received_internal_emails', through='InternalEmailRecipient')
192 subject = models.CharField(max_length=512)
193 body = models.TextField()
194 sent_at = models.DateTimeField(auto_now_add=True)
196 class Meta:
197 ordering = ['-sent_at']
199 def __str__(self):
200 return f"Internal: {self.subject[:50]}"
203class InternalEmailRecipient(UUIDModel, TimeStampedModel):
204 """Recipient of internal email with read status."""
206 tenant_id = models.UUIDField(db_index=True)
207 email = models.ForeignKey(InternalEmail, on_delete=models.CASCADE, related_name='email_recipients')
208 recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_recipients')
209 is_read = models.BooleanField(default=False)
210 read_at = models.DateTimeField(blank=True, null=True)
212 class Meta:
213 unique_together = ['email', 'recipient']
215 def __str__(self):
216 return f"To: {self.recipient.email} - {self.email.subject[:30]}"