20
20
# Helper function to extract Message-IDs
21
21
MESSAGE_ID_RE = re .compile (r"<([^<>]+)>" )
22
22
23
-
24
- GMAIL_LABEL_TO_MESSAGE_FLAG = {
23
+ IMAP_LABEL_TO_MESSAGE_FLAG = {
25
24
"Drafts" : "is_draft" ,
26
25
"Brouillons" : "is_draft" ,
26
+ "[Gmail]/Drafts" : "is_draft" ,
27
+ "[Gmail]/Brouillons" : "is_draft" ,
28
+ "DRAFT" : "is_draft" ,
27
29
"Sent" : "is_sender" ,
28
30
"Messages envoyés" : "is_sender" ,
31
+ "[Gmail]/Sent Mail" : "is_sender" ,
32
+ "[Gmail]/Mails envoyés" : "is_sender" ,
33
+ "[Gmail]/Messages envoyés" : "is_sender" ,
34
+ "Sent Mail" : "is_sender" ,
35
+ "Mails envoyés" : "is_sender" ,
29
36
"Archived" : "is_archived" ,
30
37
"Messages archivés" : "is_archived" ,
31
38
"Starred" : "is_starred" ,
39
+ "[Gmail]/Starred" : "is_starred" ,
40
+ "[Gmail]/Suivis" : "is_starred" ,
32
41
"Favoris" : "is_starred" ,
33
42
"Trash" : "is_trashed" ,
43
+ "TRASH" : "is_trashed" ,
44
+ "[Gmail]/Corbeille" : "is_trashed" ,
34
45
"Corbeille" : "is_trashed" ,
46
+ # TODO: '[Gmail]/Important'
47
+ "OUTBOX" : "is_sender" ,
35
48
}
36
49
37
- GMAIL_LABEL_TO_THREAD_FLAG = {
50
+ IMAP_LABEL_TO_THREAD_FLAG = {
38
51
"Spam" : "is_spam" ,
52
+ "QUARANTAINE" : "is_spam" ,
39
53
}
40
54
41
- GMAIL_READ_UNREAD_LABELS = {
55
+ IMAP_READ_UNREAD_LABELS = {
42
56
"Ouvert" : "read" ,
43
57
"Non lus" : "unread" ,
44
58
"Opened" : "read" ,
45
59
"Unread" : "unread" ,
46
60
}
47
61
48
- GMAIL_LABELS_TO_IGNORE = [
62
+ IMAP_LABELS_TO_IGNORE = [
49
63
"Promotions" ,
50
64
"Social" ,
51
65
"Boîte de réception" ,
52
66
"Inbox" ,
67
+ "INBOX" ,
68
+ "[Gmail]/Important" ,
69
+ "[Gmail]/All Mail" ,
70
+ "[Gmail]/Tous les messages" ,
53
71
]
54
72
55
73
59
77
60
78
def compute_labels_and_flags (
61
79
parsed_email : Dict [str , Any ],
80
+ imap_labels : Optional [List [str ]],
81
+ imap_flags : Optional [List [str ]],
62
82
) -> Tuple [List [str ], Dict [str , bool ], Dict [str , bool ]]:
63
83
"""Compute labels and flags for a parsed email."""
64
- labels = parsed_email .get ("gmail_labels" , [])
84
+
85
+ # Combine both imap_labels and gmail_labels from parsed email
86
+ gmail_labels = parsed_email .get ("gmail_labels" , [])
87
+ imap_labels = imap_labels or []
88
+ imap_flags = imap_flags or []
89
+ all_labels = list (imap_labels ) + list (gmail_labels )
90
+
65
91
message_flags = {}
66
92
thread_flags = {}
67
93
labels_to_add = []
68
- for label in labels :
94
+ for label in all_labels :
69
95
# Handle read/unread status
70
- if label in GMAIL_READ_UNREAD_LABELS :
71
- if GMAIL_READ_UNREAD_LABELS [label ] == "read" :
96
+ if label in IMAP_READ_UNREAD_LABELS :
97
+ if IMAP_READ_UNREAD_LABELS [label ] == "read" :
72
98
message_flags ["is_unread" ] = False
73
- elif GMAIL_READ_UNREAD_LABELS [label ] == "unread" :
99
+ elif IMAP_READ_UNREAD_LABELS [label ] == "unread" :
74
100
message_flags ["is_unread" ] = True
75
101
continue # Skip further processing for this label
76
- message_flag = GMAIL_LABEL_TO_MESSAGE_FLAG .get (label )
77
- thread_flag = GMAIL_LABEL_TO_THREAD_FLAG .get (label )
102
+ message_flag = IMAP_LABEL_TO_MESSAGE_FLAG .get (label )
103
+ thread_flag = IMAP_LABEL_TO_THREAD_FLAG .get (label )
78
104
if message_flag :
79
105
message_flags [message_flag ] = True
80
106
elif thread_flag :
81
107
thread_flags [thread_flag ] = True
82
- elif label not in GMAIL_LABELS_TO_IGNORE :
108
+ elif label not in IMAP_LABELS_TO_IGNORE :
83
109
labels_to_add .append (label )
84
110
111
+ # Handle read/unread status via IMAP flags
112
+ if imap_flags :
113
+ # If the \\Seen flag is present, the message is read
114
+ is_seen = "\\ Seen" in imap_flags
115
+ message_flags ["is_unread" ] = not is_seen
116
+
85
117
# Special case: if message is sender or draft, it should not be unread
86
118
if message_flags .get ("is_sender" ) or message_flags .get ("is_draft" ):
87
119
message_flags ["is_unread" ] = False
88
120
121
+ if "is_sender" in imap_flags :
122
+ message_flags ["is_sender" ] = True
123
+
89
124
return labels_to_add , message_flags , thread_flags
90
125
91
126
@@ -230,11 +265,67 @@ def canonicalize_subject(subject):
230
265
return None # potential_parents.first().thread
231
266
232
267
268
+ def _find_thread_by_message_ids (
269
+ in_reply_to : str , references : str , mailbox : models .Mailbox
270
+ ) -> Optional [models .Thread ]:
271
+ """Find thread by message IDs (in_reply_to and references)."""
272
+ # First try to find a thread by message IDs
273
+ if in_reply_to or references :
274
+ thread = models .Thread .objects .filter (
275
+ messages__mime_id__in = [in_reply_to ] if in_reply_to else [],
276
+ accesses__mailbox = mailbox ,
277
+ ).first ()
278
+ if not thread and references :
279
+ # Extract message IDs from references
280
+ ref_ids = MESSAGE_ID_RE .findall (references )
281
+ if ref_ids :
282
+ thread = models .Thread .objects .filter (
283
+ messages__mime_id__in = ref_ids ,
284
+ accesses__mailbox = mailbox ,
285
+ ).first ()
286
+ return thread
287
+ return None
288
+
289
+
290
+ def _handle_duplicate_message (
291
+ existing_message : models .Message ,
292
+ parsed_email : Dict [str , Any ],
293
+ imap_labels : List [str ],
294
+ imap_flags : List [str ],
295
+ mailbox : models .Mailbox ,
296
+ ) -> None :
297
+ """Handle duplicate message by updating labels and flags."""
298
+ # get labels from parsed_email
299
+ labels , message_flags , thread_flags = compute_labels_and_flags (
300
+ parsed_email , imap_labels , imap_flags
301
+ )
302
+ for label in labels :
303
+ try :
304
+ label_obj , _ = models .Label .objects .get_or_create (
305
+ name = label , mailbox = mailbox
306
+ )
307
+ existing_message .thread .labels .add (label_obj )
308
+ for flag , value in message_flags .items ():
309
+ if hasattr (existing_message , flag ):
310
+ setattr (existing_message , flag , value )
311
+ existing_message .save (update_fields = [flag ])
312
+ existing_message .save (update_fields = message_flags .keys ())
313
+ for flag , value in thread_flags .items ():
314
+ if hasattr (existing_message .thread , flag ):
315
+ setattr (existing_message .thread , flag , value )
316
+ existing_message .thread .save (update_fields = thread_flags .keys ())
317
+ except Exception as e :
318
+ logger .exception ("Error creating label %s: %s" , label , e )
319
+ continue
320
+
321
+
233
322
def deliver_inbound_message ( # pylint: disable=too-many-branches, too-many-statements, too-many-locals
234
323
recipient_email : str ,
235
324
parsed_email : Dict [str , Any ],
236
325
raw_data : bytes ,
237
326
is_import : bool = False ,
327
+ imap_labels : Optional [List [str ]] = None ,
328
+ imap_flags : Optional [List [str ]] = None ,
238
329
) -> bool : # Return True on success, False on failure
239
330
"""Deliver a parsed inbound email message to the correct mailbox and thread.
240
331
@@ -267,6 +358,10 @@ def deliver_inbound_message( # pylint: disable=too-many-branches, too-many-stat
267
358
).first ()
268
359
269
360
if existing_message :
361
+ if is_import and imap_labels :
362
+ _handle_duplicate_message (
363
+ existing_message , parsed_email , imap_labels , imap_flags , mailbox
364
+ )
270
365
logger .info (
271
366
"Skipping duplicate message %s (MIME ID: %s) in mailbox %s" ,
272
367
existing_message .id ,
@@ -277,46 +372,34 @@ def deliver_inbound_message( # pylint: disable=too-many-branches, too-many-stat
277
372
278
373
# --- 3. Find or Create Thread --- #
279
374
try :
280
- # thread = None
281
- # if is_import:
282
- # # During import, try to find an existing thread that contains messages
283
- # # with the same subject or referenced message IDs
284
- # subject = parsed_email.get("subject", "")
285
- # in_reply_to = parsed_email.get("in_reply_to")
286
- # references = parsed_email.get("headers", {}).get("references", "")
287
-
288
- # # First try to find a thread by message IDs
289
- # if in_reply_to or references:
290
- # thread = models.Thread.objects.filter(
291
- # messages__mime_id__in=[in_reply_to] if in_reply_to else [],
292
- # accesses__mailbox=mailbox,
293
- # ).first()
294
- # if not thread and references:
295
- # # Extract message IDs from references
296
- # ref_ids = MESSAGE_ID_RE.findall(references)
297
- # if ref_ids:
298
- # thread = models.Thread.objects.filter(
299
- # messages__mime_id__in=ref_ids,
300
- # accesses__mailbox=mailbox,
301
- # ).first()
302
-
303
- # # If no thread found by message IDs, try by subject
304
- # if not thread and subject:
305
- # # Look for threads with similar subjects
306
- # canonical_subject = re.sub(
307
- # r"^((re|fwd|fw|rep|tr|rép)\s*:\s+)+",
308
- # "",
309
- # subject.lower(),
310
- # flags=re.IGNORECASE,
311
- # ).strip()
312
- # thread = models.Thread.objects.filter(
313
- # subject__iregex=rf"^(re|fwd|fw|rep|tr|rép)\s*:\s*{re.escape(canonical_subject)}$",
314
- # accesses__mailbox=mailbox,
315
- # ).first()
316
-
317
- # # If no thread found or not an import, use normal thread finding logic
318
- # if not thread:
319
- thread = find_thread_for_inbound_message (parsed_email , mailbox )
375
+ thread = None
376
+ if is_import :
377
+ # During import, try to find an existing thread that contains messages
378
+ # with the same subject or referenced message IDs
379
+ subject = parsed_email .get ("subject" , "" )
380
+ in_reply_to = parsed_email .get ("in_reply_to" )
381
+ references = parsed_email .get ("headers" , {}).get ("references" , "" )
382
+
383
+ # First try to find a thread by message IDs
384
+ thread = _find_thread_by_message_ids (in_reply_to , references , mailbox )
385
+
386
+ # If no thread found by message IDs, try by subject
387
+ if not thread and subject :
388
+ # Look for threads with similar subjects
389
+ canonical_subject = re .sub (
390
+ r"^((re|fwd|fw|rep|tr|rép)\s*:\s+)+" ,
391
+ "" ,
392
+ subject .lower (),
393
+ flags = re .IGNORECASE ,
394
+ ).strip ()
395
+ thread = models .Thread .objects .filter (
396
+ subject__iregex = rf"^(re|fwd|fw|rep|tr|rép)\s*:\s*{ re .escape (canonical_subject )} $" ,
397
+ accesses__mailbox = mailbox ,
398
+ ).first ()
399
+
400
+ # If no thread found or not an import, use normal thread finding logic
401
+ if not thread :
402
+ thread = find_thread_for_inbound_message (parsed_email , mailbox )
320
403
321
404
if not thread :
322
405
snippet = ""
@@ -355,7 +438,9 @@ def deliver_inbound_message( # pylint: disable=too-many-branches, too-many-stat
355
438
356
439
if is_import :
357
440
# get labels from parsed_email
358
- labels , message_flags , thread_flags = compute_labels_and_flags (parsed_email )
441
+ labels , message_flags , thread_flags = compute_labels_and_flags (
442
+ parsed_email , imap_labels , imap_flags
443
+ )
359
444
for label in labels :
360
445
try :
361
446
label_obj , _ = models .Label .objects .get_or_create (
@@ -367,7 +452,6 @@ def deliver_inbound_message( # pylint: disable=too-many-branches, too-many-stat
367
452
continue
368
453
369
454
# --- 4. Get or Create Sender Contact --- #
370
- logger .warning (parsed_email )
371
455
sender_info = parsed_email .get ("from" , {})
372
456
sender_email = sender_info .get ("email" )
373
457
sender_name = sender_info .get ("name" )
0 commit comments