3
3
# pylint: disable=unused-argument, broad-exception-raised, broad-exception-caught
4
4
import imaplib
5
5
from typing import Any , Dict , List , Tuple
6
+ import quopri
7
+ from email .header import decode_header , make_header
8
+ from urllib .parse import quote , unquote
6
9
7
10
from django .conf import settings
8
11
@@ -393,6 +396,47 @@ def split_mbox_file(content: bytes) -> List[bytes]:
393
396
return messages [::- 1 ]
394
397
395
398
399
+ def decode_imap_folder_name (folder_name : str ) -> str :
400
+ """Decode IMAP folder name from quoted-printable or URL encoding."""
401
+ try :
402
+ # Handle Gmail's modified UTF-7 encoding
403
+ if '&' in folder_name and folder_name .endswith ('-' ):
404
+ # This is Gmail's modified UTF-7 encoding
405
+ # Convert & to + and - to /
406
+ modified = folder_name .replace ('&' , '+' ).replace ('-' , '/' )
407
+ try :
408
+ decoded = modified .encode ('utf-7' ).decode ('utf-8' )
409
+ return decoded
410
+ except Exception :
411
+ pass
412
+
413
+ # Try URL decoding for other cases
414
+ if '%' in folder_name :
415
+ return unquote (folder_name )
416
+
417
+ return folder_name
418
+ except Exception :
419
+ return folder_name
420
+
421
+
422
+ def encode_imap_folder_name (folder_name : str ) -> str :
423
+ """Encode folder name for IMAP commands."""
424
+ try :
425
+ # For Gmail-style folders, use proper IMAP encoding
426
+ if folder_name .startswith ('[Gmail]' ):
427
+ # Gmail folders need to be properly quoted
428
+ return f'"{ folder_name } "'
429
+
430
+ # For folders with spaces or special characters, use proper IMAP encoding
431
+ if ' ' in folder_name or any (char in folder_name for char in ['(' , ')' , '{' , '}' , '"' , '\\ ' ]):
432
+ # Use IMAP's literal string format
433
+ return f'"{ folder_name } "'
434
+
435
+ return folder_name
436
+ except Exception :
437
+ return folder_name
438
+
439
+
396
440
@celery_app .task (bind = True )
397
441
def import_imap_messages_task (
398
442
self ,
@@ -413,8 +457,8 @@ def import_imap_messages_task(
413
457
username: Email address for login
414
458
password: Password for login
415
459
use_ssl: Whether to use SSL
416
- folder: IMAP folder to import from
417
- max_messages: Maximum number of messages to import (0 for all)
460
+ folder: IMAP folder to import from. Use "ALL" or "*" to import from all folders
461
+ max_messages: Maximum number of messages to import per folder (0 for all)
418
462
recipient_id: ID of the recipient mailbox
419
463
420
464
Returns:
@@ -428,78 +472,203 @@ def import_imap_messages_task(
428
472
imap = imaplib .IMAP4 (imap_server , imap_port )
429
473
430
474
# Login
431
-
432
475
imap .login (username , password )
433
476
434
- # Select folder
435
- status , messages = imap .select (folder )
436
- if status != "OK" :
437
- raise Exception (f"Failed to select folder { folder } : { messages } " )
438
-
439
- # Search for all messages
440
- status , message_numbers = imap .search (None , "ALL" )
477
+ # List all folders
478
+ # TODO: check is working for other IMAP servers
479
+ status , folder_list = imap .list ()
441
480
if status != "OK" :
442
- raise Exception (f"Failed to search messages: { message_numbers } " )
443
-
444
- # Get list of message numbers
445
- message_list = message_numbers [0 ].split ()
446
-
447
- # Apply max_messages limit if specified
448
- if max_messages > 0 :
449
- message_list = message_list [- max_messages :] # Get most recent messages
450
-
451
- total_messages = len (message_list )
452
- success_count = 0
453
- failure_count = 0
481
+ raise Exception (f"Failed to list folders: { folder_list } " )
482
+
483
+ # Get selectable folders
484
+ selectable_folders = []
485
+ for folder_info in folder_list :
486
+ folder_info = folder_info .decode ()
487
+ if "\\ Noselect" not in folder_info :
488
+ # Extract folder name - it's the last part in quotes
489
+ folder_name = folder_info .split ('"' )[- 2 ]
490
+ selectable_folders .append (folder_name )
491
+
492
+ logger .info ("Available IMAP folders: %s" , selectable_folders )
493
+
494
+ # Determine which folders to process
495
+ folders_to_process = []
496
+ if folder .upper () in ["ALL" , "*" ]:
497
+ folders_to_process = selectable_folders
498
+ else :
499
+ if folder not in selectable_folders :
500
+ raise Exception (f"Folder '{ folder } ' not found or not selectable" )
501
+ folders_to_process = [folder ]
454
502
455
503
# Get recipient mailbox
456
504
recipient = Mailbox .objects .get (id = recipient_id )
457
505
458
- # Process each message
459
- for i , msg_num in enumerate (message_list , 1 ):
506
+ total_messages = 0
507
+ total_success = 0
508
+ total_failure = 0
509
+ folder_stats = {}
510
+
511
+ # Process each folder
512
+ for current_folder in folders_to_process :
460
513
try :
461
- # Update task state
462
- self .update_state (
463
- state = "PROGRESS" ,
464
- meta = {
465
- "current" : i ,
466
- "total" : total_messages ,
467
- "status" : f"Processing message { i } of { total_messages } " ,
468
- },
514
+ logger .info ("Processing folder: %s" , current_folder )
515
+
516
+ # Create or get label for this folder
517
+ folder_label , _ = Label .objects .get_or_create (
518
+ name = current_folder ,
519
+ mailbox = recipient ,
520
+ defaults = {'color' : '#000000' } # Default color
469
521
)
470
522
471
- # Fetch message
472
- status , msg_data = imap .fetch (msg_num , "(RFC822)" )
523
+ # Encode folder name for IMAP command
524
+ encoded_folder = encode_imap_folder_name (current_folder )
525
+
526
+ # Select folder
527
+ try :
528
+ status , messages = imap .select (encoded_folder )
529
+ except imaplib .IMAP4 .error as e :
530
+ # If first attempt fails, try with the original folder name
531
+ try :
532
+ status , messages = imap .select (current_folder )
533
+ except imaplib .IMAP4 .error :
534
+ # If both attempts fail, log and continue
535
+ logger .error ("Failed to select folder %s: %s" , current_folder , str (e ))
536
+ folder_stats [current_folder ] = {
537
+ "status" : "failed" ,
538
+ "error" : f"Failed to select folder: { str (e )} " ,
539
+ "success_count" : 0 ,
540
+ "failure_count" : 0
541
+ }
542
+ continue
543
+
473
544
if status != "OK" :
474
- logger .error ("Failed to fetch message %s: %s" , msg_num , msg_data )
475
- failure_count += 1
545
+ logger .error ("Failed to select folder %s: %s" , current_folder , messages )
546
+ folder_stats [current_folder ] = {
547
+ "status" : "failed" ,
548
+ "error" : f"Failed to select folder: { messages } " ,
549
+ "success_count" : 0 ,
550
+ "failure_count" : 0
551
+ }
476
552
continue
477
553
478
- # Parse message
479
- raw_email = msg_data [0 ][1 ]
480
- parsed_email = parse_email_message (raw_email )
554
+ # Search for all messages
555
+ status , message_numbers = imap .search (None , "ALL" )
556
+ if status != "OK" :
557
+ logger .error ("Failed to search messages in %s: %s" , current_folder , message_numbers )
558
+ folder_stats [current_folder ] = {
559
+ "status" : "failed" ,
560
+ "error" : f"Failed to search messages: { message_numbers } " ,
561
+ "success_count" : 0 ,
562
+ "failure_count" : 0
563
+ }
564
+ continue
481
565
482
- # Deliver message
483
- if deliver_inbound_message (
484
- str (recipient ), parsed_email , raw_email , is_import = True
485
- ):
486
- success_count += 1
487
- else :
488
- failure_count += 1
566
+ # Get list of message numbers
567
+ message_list = message_numbers [0 ].split ()
568
+
569
+ # Apply max_messages limit if specified
570
+ if max_messages > 0 :
571
+ message_list = message_list [- max_messages :] # Get most recent messages
572
+
573
+ folder_total = len (message_list )
574
+ total_messages += folder_total
575
+ folder_success = 0
576
+ folder_failure = 0
577
+
578
+ # Process each message
579
+ for i , msg_num in enumerate (message_list , 1 ):
580
+ try :
581
+ # Update task state
582
+ self .update_state (
583
+ state = "PROGRESS" ,
584
+ meta = {
585
+ "current_folder" : current_folder ,
586
+ "current" : i ,
587
+ "total" : folder_total ,
588
+ "status" : f"Processing message { i } of { folder_total } in { current_folder } " ,
589
+ },
590
+ )
489
591
490
- except Exception as e :
491
- logger .exception ("Error processing message %s: %s" , msg_num , e )
492
- failure_count += 1
592
+ # Fetch message
593
+ status , msg_data = imap .fetch (msg_num , "(RFC822)" )
594
+ if status != "OK" :
595
+ logger .error ("Failed to fetch message %s: %s" , msg_num , msg_data )
596
+ folder_failure += 1
597
+ continue
598
+
599
+ # Parse message
600
+ raw_email = msg_data [0 ][1 ]
601
+
602
+ # Add debugging for raw email
603
+ logger .debug ("Raw email size: %d bytes" , len (raw_email ))
604
+
605
+ try :
606
+ parsed_email = parse_email_message (raw_email )
607
+ except Exception as parse_error :
608
+ logger .exception ("Error parsing message %s in folder %s: %s" , msg_num , current_folder , parse_error )
609
+ folder_failure += 1
610
+ continue
611
+
612
+ # Add debugging information
613
+ logger .debug ("Processing message in folder %s: Subject=%s, From=%s" ,
614
+ current_folder ,
615
+ parsed_email .get ('headers' , {}).get ('Subject' , 'No Subject' ),
616
+ parsed_email .get ('headers' , {}).get ('From' , 'No From' ))
617
+
618
+ # Deliver message
619
+ try :
620
+ delivery_result = deliver_inbound_message (
621
+ str (recipient ), parsed_email , raw_email , is_import = True , label_name = current_folder
622
+ )
623
+
624
+ if delivery_result :
625
+ folder_success += 1
626
+ logger .debug ("Successfully delivered message in folder %s" , current_folder )
627
+ else :
628
+ folder_failure += 1
629
+ logger .warning ("Failed to deliver message in folder %s" , current_folder )
630
+
631
+ except Exception as delivery_error :
632
+ folder_failure += 1
633
+ logger .exception ("Error delivering message in folder %s: %s" , current_folder , delivery_error )
634
+
635
+ except Exception as e :
636
+ logger .exception ("Error processing message %s in %s: %s" , msg_num , current_folder , e )
637
+ folder_failure += 1
638
+
639
+ # Update folder statistics
640
+ folder_stats [current_folder ] = {
641
+ "status" : "completed" ,
642
+ "total_messages" : folder_total ,
643
+ "success_count" : folder_success ,
644
+ "failure_count" : folder_failure
645
+ }
646
+ total_success += folder_success
647
+ total_failure += folder_failure
493
648
494
- # Logout
495
- imap .close ()
496
- imap .logout ()
649
+ except Exception as e :
650
+ logger .exception ("Error processing folder %s: %s" , current_folder , e )
651
+ folder_stats [current_folder ] = {
652
+ "status" : "failed" ,
653
+ "error" : str (e ),
654
+ "success_count" : 0 ,
655
+ "failure_count" : 0
656
+ }
657
+ total_failure += 1
658
+
659
+ # Logout - only call logout, not close
660
+ try :
661
+ imap .logout ()
662
+ except Exception as e :
663
+ logger .warning ("Error during logout: %s" , e )
497
664
498
665
return {
499
666
"status" : "completed" ,
667
+ "total_folders" : len (folders_to_process ),
500
668
"total_messages" : total_messages ,
501
- "success_count" : success_count ,
502
- "failure_count" : failure_count ,
669
+ "total_success" : total_success ,
670
+ "total_failure" : total_failure ,
671
+ "folder_stats" : folder_stats
503
672
}
504
673
505
674
except Exception as e :
0 commit comments