Coverage for paperless_mail/models.py: 98%
98 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from django.db import models
2from django.utils import timezone
3from django.utils.translation import gettext_lazy as _
5import documents.models as document_models
8class MailAccount(document_models.ModelWithOwner):
9 class Meta:
10 verbose_name = _("mail account")
11 verbose_name_plural = _("mail accounts")
13 class ImapSecurity(models.IntegerChoices):
14 NONE = 1, _("No encryption")
15 SSL = 2, _("Use SSL")
16 STARTTLS = 3, _("Use STARTTLS")
18 class MailAccountType(models.IntegerChoices):
19 IMAP = 1, _("IMAP")
20 GMAIL_OAUTH = 2, _("Gmail OAuth")
21 OUTLOOK_OAUTH = 3, _("Outlook OAuth")
23 name = models.CharField(_("name"), max_length=256, unique=True)
25 imap_server = models.CharField(_("IMAP server"), max_length=256)
27 imap_port = models.PositiveIntegerField(
28 _("IMAP port"),
29 blank=True,
30 null=True,
31 help_text=_(
32 "This is usually 143 for unencrypted and STARTTLS "
33 "connections, and 993 for SSL connections.",
34 ),
35 )
37 imap_security = models.PositiveSmallIntegerField(
38 _("IMAP security"),
39 choices=ImapSecurity.choices,
40 default=ImapSecurity.SSL,
41 )
43 username = models.CharField(_("username"), max_length=256)
45 password = models.TextField(_("password"))
47 is_token = models.BooleanField(_("Is token authentication"), default=False)
49 character_set = models.CharField(
50 _("character set"),
51 max_length=256,
52 default="UTF-8",
53 help_text=_(
54 "The character set to use when communicating with the "
55 "mail server, such as 'UTF-8' or 'US-ASCII'.",
56 ),
57 )
59 account_type = models.PositiveSmallIntegerField(
60 _("account type"),
61 choices=MailAccountType.choices,
62 default=MailAccountType.IMAP,
63 )
65 refresh_token = models.TextField(
66 _("refresh token"),
67 blank=True,
68 null=True,
69 help_text=_(
70 "The refresh token to use for token authentication e.g. with oauth2.",
71 ),
72 )
74 expiration = models.DateTimeField(
75 _("expiration"),
76 blank=True,
77 null=True,
78 help_text=_(
79 "The expiration date of the refresh token. ",
80 ),
81 )
83 def __str__(self):
84 return self.name
87class MailRule(document_models.ModelWithOwner):
88 class Meta:
89 verbose_name = _("mail rule")
90 verbose_name_plural = _("mail rules")
91 constraints = [
92 models.UniqueConstraint(
93 fields=["name", "owner"],
94 name="%(app_label)s_%(class)s_unique_name_owner",
95 ),
96 models.UniqueConstraint(
97 name="%(app_label)s_%(class)s_name_unique",
98 fields=["name"],
99 condition=models.Q(owner__isnull=True),
100 ),
101 ]
103 class ConsumptionScope(models.IntegerChoices):
104 ATTACHMENTS_ONLY = 1, _("Only process attachments.")
105 EML_ONLY = 2, _("Process full Mail (with embedded attachments in file) as .eml")
106 EVERYTHING = (
107 3,
108 _(
109 "Process full Mail (with embedded attachments in file) as .eml "
110 "+ process attachments as separate documents",
111 ),
112 )
114 class AttachmentProcessing(models.IntegerChoices):
115 ATTACHMENTS_ONLY = 1, _("Only process attachments.")
116 EVERYTHING = 2, _("Process all files, including 'inline' attachments.")
118 class PdfLayout(models.IntegerChoices):
119 DEFAULT = 0, _("System default")
120 TEXT_HTML = 1, _("Text, then HTML")
121 HTML_TEXT = 2, _("HTML, then text")
122 HTML_ONLY = 3, _("HTML only")
123 TEXT_ONLY = 4, _("Text only")
125 class MailAction(models.IntegerChoices):
126 DELETE = 1, _("Delete")
127 MOVE = 2, _("Move to specified folder")
128 MARK_READ = 3, _("Mark as read, don't process read mails")
129 FLAG = 4, _("Flag the mail, don't process flagged mails")
130 TAG = 5, _("Tag the mail with specified tag, don't process tagged mails")
132 class TitleSource(models.IntegerChoices):
133 FROM_SUBJECT = 1, _("Use subject as title")
134 FROM_FILENAME = 2, _("Use attachment filename as title")
135 NONE = 3, _("Do not assign title from rule")
137 class CorrespondentSource(models.IntegerChoices):
138 FROM_NOTHING = 1, _("Do not assign a correspondent")
139 FROM_EMAIL = 2, _("Use mail address")
140 FROM_NAME = 3, _("Use name (or mail address if not available)")
141 FROM_CUSTOM = 4, _("Use correspondent selected below")
143 name = models.CharField(_("name"), max_length=256)
145 order = models.SmallIntegerField(_("order"), default=0)
147 account = models.ForeignKey(
148 MailAccount,
149 related_name="rules",
150 on_delete=models.CASCADE,
151 verbose_name=_("account"),
152 )
154 enabled = models.BooleanField(_("enabled"), default=True)
156 folder = models.CharField(
157 _("folder"),
158 default="INBOX",
159 max_length=256,
160 help_text=_(
161 "Subfolders must be separated by a delimiter, often a dot ('.') or"
162 " slash ('/'), but it varies by mail server.",
163 ),
164 )
166 filter_from = models.CharField(
167 _("filter from"),
168 max_length=256,
169 null=True,
170 blank=True,
171 )
173 filter_to = models.CharField(
174 _("filter to"),
175 max_length=256,
176 null=True,
177 blank=True,
178 )
180 filter_subject = models.CharField(
181 _("filter subject"),
182 max_length=256,
183 null=True,
184 blank=True,
185 )
187 filter_body = models.CharField(
188 _("filter body"),
189 max_length=256,
190 null=True,
191 blank=True,
192 )
194 filter_attachment_filename_include = models.CharField(
195 _("filter attachment filename inclusive"),
196 max_length=256,
197 null=True,
198 blank=True,
199 help_text=_(
200 "Only consume documents which entirely match this "
201 "filename if specified. Wildcards such as *.pdf or "
202 "*invoice* are allowed. Case insensitive.",
203 ),
204 )
206 filter_attachment_filename_exclude = models.CharField(
207 _("filter attachment filename exclusive"),
208 max_length=256,
209 null=True,
210 blank=True,
211 help_text=_(
212 "Do not consume documents which entirely match this "
213 "filename if specified. Wildcards such as *.pdf or "
214 "*invoice* are allowed. Case insensitive.",
215 ),
216 )
218 maximum_age = models.PositiveSmallIntegerField(
219 _("maximum age"),
220 default=30,
221 help_text=_("Specified in days."),
222 )
224 attachment_type = models.PositiveSmallIntegerField(
225 _("attachment type"),
226 choices=AttachmentProcessing.choices,
227 default=AttachmentProcessing.ATTACHMENTS_ONLY,
228 help_text=_(
229 "Inline attachments include embedded images, so it's best "
230 "to combine this option with a filename filter.",
231 ),
232 )
234 consumption_scope = models.PositiveSmallIntegerField(
235 _("consumption scope"),
236 choices=ConsumptionScope.choices,
237 default=ConsumptionScope.ATTACHMENTS_ONLY,
238 )
240 pdf_layout = models.PositiveSmallIntegerField(
241 _("pdf layout"),
242 choices=PdfLayout.choices,
243 default=PdfLayout.DEFAULT,
244 )
246 action = models.PositiveSmallIntegerField(
247 _("action"),
248 choices=MailAction.choices,
249 default=MailAction.MARK_READ,
250 )
252 action_parameter = models.CharField(
253 _("action parameter"),
254 max_length=256,
255 blank=True,
256 null=True,
257 help_text=_(
258 "Additional parameter for the action selected above, "
259 "i.e., "
260 "the target folder of the move to folder action. "
261 "Subfolders must be separated by dots.",
262 ),
263 )
265 assign_title_from = models.PositiveSmallIntegerField(
266 _("assign title from"),
267 choices=TitleSource.choices,
268 default=TitleSource.FROM_SUBJECT,
269 )
271 assign_tags = models.ManyToManyField(
272 document_models.Tag,
273 blank=True,
274 verbose_name=_("assign this tag"),
275 )
277 assign_document_type = models.ForeignKey(
278 document_models.DocumentType,
279 null=True,
280 blank=True,
281 on_delete=models.SET_NULL,
282 verbose_name=_("assign this document type"),
283 )
285 assign_correspondent_from = models.PositiveSmallIntegerField(
286 _("assign correspondent from"),
287 choices=CorrespondentSource.choices,
288 default=CorrespondentSource.FROM_NOTHING,
289 )
291 assign_correspondent = models.ForeignKey(
292 document_models.Correspondent,
293 null=True,
294 blank=True,
295 on_delete=models.SET_NULL,
296 verbose_name=_("assign this correspondent"),
297 )
299 assign_owner_from_rule = models.BooleanField(
300 _("Assign the rule owner to documents"),
301 default=True,
302 )
304 stop_processing = models.BooleanField(
305 _("Stop processing further rules"),
306 default=False,
307 help_text=_(
308 "If True, no further rules will be processed after this one if any document is queued.",
309 ),
310 )
312 def __str__(self):
313 return f"{self.account.name}.{self.name}"
316class ProcessedMail(document_models.ModelWithOwner):
317 rule = models.ForeignKey(
318 MailRule,
319 null=False,
320 blank=False,
321 on_delete=models.CASCADE,
322 editable=False,
323 )
325 folder = models.CharField(
326 _("folder"),
327 null=False,
328 blank=False,
329 max_length=256,
330 editable=False,
331 )
333 uid = models.CharField(
334 _("uid"),
335 null=False,
336 blank=False,
337 max_length=256,
338 editable=False,
339 )
341 uid_validity = models.CharField(
342 _("uid validity"),
343 null=True,
344 blank=True,
345 max_length=64,
346 editable=False,
347 )
349 subject = models.CharField(
350 _("subject"),
351 null=False,
352 blank=False,
353 max_length=256,
354 editable=False,
355 )
357 received = models.DateTimeField(
358 _("received"),
359 null=False,
360 blank=False,
361 editable=False,
362 )
364 processed = models.DateTimeField(
365 _("processed"),
366 default=timezone.now,
367 editable=False,
368 )
370 status = models.CharField(
371 _("status"),
372 null=False,
373 blank=False,
374 max_length=256,
375 editable=False,
376 )
378 error = models.TextField(
379 _("error"),
380 null=True,
381 blank=True,
382 editable=False,
383 )