django-codenerix-email 4.0.37__py2.py3-none-any.whl → 4.0.38__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- __version__ = "4.0.37"
1
+ __version__ = "4.0.38"
2
2
 
3
3
  __authors__ = [
4
4
  "Juan Miguel Taboada Godoy <juanmi@juanmitaboada.com>",
@@ -1,4 +1,6 @@
1
1
  import re
2
+ from textwrap import dedent
3
+ from argparse import RawTextHelpFormatter
2
4
 
3
5
  import logging
4
6
 
@@ -31,7 +33,91 @@ from imapclient.exceptions import LoginError # noqa: E402
31
33
  class Command(BaseCommand):
32
34
  help = "Fetches new emails from the configured IMAP account."
33
35
 
36
+ def create_parser(self, prog_name, subcommand, **kwargs):
37
+ """
38
+ Create and return the ArgumentParser instance for this command.
39
+ We are overriding this to use the RawTextHelpFormatter.
40
+ """
41
+ parser = super().create_parser(prog_name, subcommand, **kwargs)
42
+ parser.formatter_class = RawTextHelpFormatter
43
+ return parser
44
+
34
45
  def add_arguments(self, parser):
46
+ parser.epilog = dedent(
47
+ r"""
48
+ This command connects to the configured IMAP server, fetches new
49
+ emails, and saves them as ReceivedEmail objects in the database.
50
+
51
+ By default, it processes only unseen emails. You can use the
52
+ --all option to process all emails in the inbox.
53
+
54
+ You can filter by specific IMAP ID, Message-ID, or Tracking ID
55
+ using the respective options.
56
+
57
+ The --rewrite option allows overwriting existing received emails
58
+ with the same Message-ID.
59
+
60
+ Examples:
61
+ # Fetch unseen emails
62
+ python manage.py emails_recv
63
+
64
+ # Fetch all emails
65
+ python manage.py emails_recv --all
66
+
67
+ # Fetch email by specific IMAP ID
68
+ python manage.py emails_recv --imap-id 123
69
+
70
+ # Fetch email by specific Message-ID
71
+ python manage.py emails_recv --message-id "<...>"
72
+
73
+ # Fetch email by specific Tracking ID
74
+ python manage.py emails_recv --tracking-id "uuid"
75
+
76
+ Make sure to configure the IMAP settings in your Django settings:
77
+ IMAP_EMAIL_HOST = "imap.example.com"
78
+ IMAP_EMAIL_PORT = 993
79
+ IMAP_EMAIL_USER = "your_username"
80
+ IMAP_EMAIL_PASSWORD = "your_password"
81
+ IMAP_EMAIL_SSL = True # (default: True)
82
+ IMAP_EMAIL_INBOX_FOLDER = "INBOX" # (default: "INBOX")
83
+ IMAP_EMAIL_FILTERS = {
84
+ "SUBJECT": [r".*"],
85
+ "FROM": [r".*"],
86
+ "MESSAGE-ID": [r".*"],
87
+ "TO": [
88
+ r"^bounce@becas\.com",
89
+ r"^bounces@becas\.com",
90
+ r"^no-reply@becas\.com",
91
+ r"^hola@becas\.com",
92
+ r"^tuasesor@becas\.com",
93
+ r"^[a-zA-Z0-9._%+-]+@becas\.com", # *@becas.com
94
+ ],
95
+ "BODY_PLAIN": [r".*"],
96
+ "BODY_HTML": [r".*"],
97
+ "HEADER": [("X-Custom-Header", r".*")],
98
+ "BOUNCE_TYPE": ["hard", "soft"],
99
+ "BOUNCE_REASON": [r".*"],
100
+ "TRACKING_ID": True,
101
+ }
102
+
103
+ Filters are applied using AND logic across different fields and
104
+ OR logic within the same field.
105
+
106
+ The filters works as follows:
107
+ - SUBJECT, FROM, MESSAGE-ID, TO, BODY_PLAIN, BODY_HTML:
108
+ regex match (case-insensitive) against the respective field.
109
+ - HEADER: tuple of (header name, regex) to match specific headers.
110
+ - BOUNCE_TYPE: "hard" or "soft" to filter by bounce type.
111
+ - BOUNCE_REASON: regex to match the bounce reason.
112
+ - TRACKING_ID: if True, only process emails with a tracking ID.
113
+
114
+ If IMAP_EMAIL_FILTERS is not set, all emails are processed.
115
+
116
+ Note: This command marks processed emails as read (Seen) to avoid
117
+ reprocessing them in future runs.
118
+ """ # noqa: E501
119
+ )
120
+
35
121
  # Named (optional) arguments
36
122
  parser.add_argument(
37
123
  "--silent",
@@ -244,6 +330,8 @@ class Command(BaseCommand):
244
330
  efrom = msg.get("From")
245
331
  eto = msg.get("To")
246
332
  eid = msg.get("Message-ID")
333
+ ecc = msg.get("Cc")
334
+ ebc = msg.get("Bcc")
247
335
 
248
336
  # If we can't get a Message-ID, use the IMAP ID as fallback
249
337
  # to avoid duplicates
@@ -305,7 +393,8 @@ class Command(BaseCommand):
305
393
  self.stdout.write(
306
394
  self.style.WARNING(
307
395
  f"Tracking ID {tracking_id} found "
308
- "but no matching sent email."
396
+ f"for IMAP ID {imap_id} but no "
397
+ "matching sent email."
309
398
  )
310
399
  )
311
400
 
@@ -328,6 +417,33 @@ class Command(BaseCommand):
328
417
  )
329
418
  headers[header] = decoded_value
330
419
 
420
+ # Let emails pass based on filtering system
421
+ (filter_passed, filter_reason) = self.filter_pass(
422
+ subject,
423
+ efrom,
424
+ eto,
425
+ ecc,
426
+ ebc,
427
+ eid,
428
+ body_plain,
429
+ body_html,
430
+ headers,
431
+ bounce_type,
432
+ bounce_reason,
433
+ tracking_id,
434
+ )
435
+ if not filter_passed:
436
+ if self.verbose:
437
+ self.stdout.write(
438
+ self.style.NOTICE(
439
+ f"Skipping email with IMAP ID: {imap_id} "
440
+ f"(FILTER: {filter_reason})"
441
+ )
442
+ )
443
+ # Mark the message as read to avoid reprocessing
444
+ server.add_flags(imap_id, [b"\\Seen"])
445
+ continue
446
+
331
447
  # Create EmailReceived object if doesn't exist
332
448
  if not email_received:
333
449
  overwriting = False
@@ -360,7 +476,12 @@ class Command(BaseCommand):
360
476
  created_count += 1
361
477
  verb = "Created"
362
478
 
479
+ # Show info about the processed email
363
480
  if self.verbose:
481
+ if overwriting:
482
+ style = self.style.MIGRATE_HEADING
483
+ else:
484
+ style = self.style.WARNING
364
485
  msg = (
365
486
  f"{verb} email with IMAP ID: "
366
487
  f"{imap_id} (link={tracking_id})"
@@ -371,19 +492,23 @@ class Command(BaseCommand):
371
492
  )
372
493
  bounce_reason_str = bounce_reason or "Unknown"
373
494
  self.stdout.write(
374
- self.style.WARNING(
495
+ style(
375
496
  f"{msg} "
376
497
  f"[{bounce_type_str} bounce, "
377
498
  f"reason={bounce_reason_str}]"
378
499
  )
379
500
  )
380
501
  else:
381
- self.stdout.write(self.style.SUCCESS(msg))
502
+ if overwriting:
503
+ style = self.style.MIGRATE_HEADING
504
+ else:
505
+ style = self.style.SUCCESS
506
+ self.stdout.write(style(msg))
382
507
 
383
508
  else:
384
509
  if self.verbose:
385
510
  self.stdout.write(
386
- self.style.WARNING(
511
+ self.style.HTTP_INFO(
387
512
  f"Skipping email with IMAP ID: {imap_id} (DUP)"
388
513
  )
389
514
  )
@@ -599,3 +724,112 @@ class Command(BaseCommand):
599
724
  bounce_reason = "Unknown (Subject Keyword)"
600
725
 
601
726
  return (bounce_type, bounce_reason)
727
+
728
+ def filter_pass(
729
+ self,
730
+ subject: str,
731
+ efrom: str,
732
+ eto: str,
733
+ ecc: str,
734
+ ebc: str,
735
+ eid: str,
736
+ body_plain: str,
737
+ body_html: str,
738
+ headers: dict,
739
+ bounce_type: Optional[str],
740
+ bounce_reason: Optional[str],
741
+ tracking_id: Optional[str],
742
+ ) -> tuple[bool, str]:
743
+ """
744
+ Applies filtering rules to determine if an email should be processed.
745
+
746
+ Returns:
747
+ True if the email should be processed, False otherwise.
748
+ """
749
+
750
+ # Get filters from settings
751
+ filters = getattr(settings, "IMAP_EMAIL_FILTERS", None)
752
+ if not filters:
753
+ # No filters defined, allow processing
754
+ return (True, "No filters defined, processing all.")
755
+
756
+ # Helper function to apply regex list to a value
757
+ def match_any(value: str, patterns: list) -> bool:
758
+ for pattern in patterns:
759
+ if re.search(pattern, value, re.IGNORECASE):
760
+ return True # Match found
761
+ return False # No matches
762
+
763
+ # Apply filters
764
+ for key, patterns in filters.items():
765
+ if key == "SUBJECT" and patterns:
766
+ if not match_any(subject or "", patterns):
767
+ return (False, f"SUBJECT failed: {subject}")
768
+
769
+ elif key == "FROM" and patterns:
770
+ if not match_any(efrom or "", patterns):
771
+ return (False, f"FROM failed: {efrom}")
772
+
773
+ elif key == "TO" and patterns:
774
+ if not any(
775
+ match_any(targets or "", patterns)
776
+ for targets in (
777
+ eto or "",
778
+ ecc or "",
779
+ ebc or "",
780
+ )
781
+ ):
782
+ # If no direct match, try more robust email extraction
783
+ # Analize only the email addresses
784
+ emails = re.split(r"[;,]\s*", eto or "")
785
+ emails += re.split(r"[;,]\s*", ecc or "")
786
+ emails += re.split(r"[;,]\s*", ebc or "")
787
+
788
+ # Stay only with the email and strip < and > if present
789
+ email_only = [
790
+ e.split()[-1].strip("<>") for e in emails if e
791
+ ]
792
+
793
+ # Check if any of the emails match
794
+ if not any(match_any(e, patterns) for e in email_only):
795
+ return (False, f"TO failed: {eto}")
796
+
797
+ elif key == "MESSAGE-ID" and patterns:
798
+ if not match_any(eid or "", patterns):
799
+ return (False, f"MESSAGE-ID failed: {eid}")
800
+
801
+ elif key == "BODY_PLAIN" and patterns:
802
+ if not match_any(body_plain or "", patterns):
803
+ return (False, f"BODY_PLAIN failed: {body_plain}")
804
+
805
+ elif key == "BODY_HTML" and patterns:
806
+ if not match_any(body_html or "", patterns):
807
+ return (False, f"BODY_HTML failed: {body_html}")
808
+
809
+ elif key == "HEADER" and patterns:
810
+ header_matched = False
811
+ for header_name, header_patterns in patterns:
812
+ header_value = headers.get(header_name, "")
813
+ if match_any(header_value, header_patterns):
814
+ header_matched = True
815
+ break
816
+ if not header_matched:
817
+ return (False, f"HEADER failed: {headers}")
818
+
819
+ elif key == "BOUNCE_TYPE" and patterns:
820
+ if bounce_type not in patterns:
821
+ return (False, f"BOUNCE_TYPE failed: {bounce_type}")
822
+
823
+ elif key == "BOUNCE_REASON" and patterns:
824
+ if not bounce_reason or not match_any(bounce_reason, patterns):
825
+ return (
826
+ False,
827
+ f"BOUNCE_REASON failed: {bounce_reason}",
828
+ )
829
+
830
+ elif key == "TRACKING_ID" and patterns:
831
+ if patterns and not tracking_id:
832
+ return (False, "TRACKING_ID failed: No tracking ID")
833
+
834
+ # If all filters passed, allow processing
835
+ return (True, "All filters passed.")
@@ -12,7 +12,9 @@
12
12
  class="text-nowrap">{{row.eto|codenerix}}</td>
13
13
  <td
14
14
  ng-class="{'info': (!row.email__pk) && (!row.bounce_type), 'warning': row.bounce_type=='S', 'danger': row.bounce_type=='H', 'text-warning': row.bounce_type=='S', 'text-danger': row.bounce_type=='H'}"
15
- class="text-nowrap">{{row.subject|codenerix}}</td>
15
+ class="text-nowrap">
16
+ <span codenerix-html-compile='row.subject|codenerix:"shorttext:50"'></span>
17
+ </td>
16
18
  <td
17
19
  ng-class="{'info': (!row.email__pk) && (!row.bounce_type), 'warning': row.bounce_type=='S', 'danger': row.bounce_type=='H', 'text-warning': row.bounce_type=='S', 'text-danger': row.bounce_type=='H'}"
18
20
  ng-click="$event.stopPropagation();"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-codenerix-email
3
- Version: 4.0.37
3
+ Version: 4.0.38
4
4
  Summary: Codenerix Email is a module that enables CODENERIX to set send emails in a general manner.
5
5
  Home-page: https://github.com/codenerix/django-codenerix-email
6
6
  Author: Juan Miguel Taboada Godoy <juanmi@juanmitaboada.com>, Juan Soler Ruiz <soleronline@gmail.com>
@@ -1,4 +1,4 @@
1
- codenerix_email/__init__.py,sha256=nmegdQqWfQuMKIEdzqRtGCeg5es1m1w4_DZMJfzSRcg,149
1
+ codenerix_email/__init__.py,sha256=d3ZDJ3aRhP_spoILto0bxpnTTieGJNC9i6wvMu6LSDo,149
2
2
  codenerix_email/admin.py,sha256=w259UKFk_opGEl6PJjYHXWAHQ_8emgqmiixKT5Rid4A,1180
3
3
  codenerix_email/apps.py,sha256=WXqu1XQibDDyCvvQYt2JbTK4GIpW8BNv5DCbRJS2mmk,149
4
4
  codenerix_email/forms.py,sha256=38byLGxg1MOLAY1kAYChxYZj64tSgyfRvDcSbIOdV0I,5521
@@ -827,8 +827,8 @@ codenerix_email/.mypy_cache/3.10/zoneinfo/_common.data.json,sha256=e4xbNKL_yQ5h5
827
827
  codenerix_email/.mypy_cache/3.10/zoneinfo/_common.meta.json,sha256=5K19XWobpSjKwiv7bZFZRcxsEfoh7b-FhYcmqB-2Iic,1737
828
828
  codenerix_email/.mypy_cache/3.10/zoneinfo/_tzpath.data.json,sha256=CFx7Q1XfUhhuNX69prkxyirG8rfvEDCNgEHWQigKC_A,5632
829
829
  codenerix_email/.mypy_cache/3.10/zoneinfo/_tzpath.meta.json,sha256=d1HJ_xFBI1orlZSVhH0gHWLI-dJG3zY-ZOlctOl62yU,1765
830
- codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=BVGH5_FfAI_IwJgqrXXzP-rsKn-MC6qCbtu3xJnll0g,313
831
- codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=ifkZHL6PJ-TFRdhWEpJ9r1KPdQOFN5_n_-lFhu8UVzE,337
830
+ codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=X9LGnGfm6qTbAvEiyenE8Z40jbxj4g1kfeoRKITsWik,313
831
+ codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=JEmf6oZE-e3TUb2xe1Zbe_XjVsq1vfS5qIuNUfdakRc,337
832
832
  codenerix_email/__pycache__/__init__.cpython-35.pyc,sha256=dl9lYAgrokJptUj3JAhiqTlX7d_CbncOxZeTc1USc88,308
833
833
  codenerix_email/__pycache__/__init__.cpython-37.pyc,sha256=5d1CeFU5DrfnwrRpvSw1bHvLN9hoHXjUA3ln3rXCDo8,306
834
834
  codenerix_email/__pycache__/__init__.cpython-39.pyc,sha256=0c6KWU_eOTlF5l9fNWv8l41b0LcfVQNUsqDvJTv2YyU,300
@@ -868,7 +868,7 @@ codenerix_email/management/__pycache__/__init__.cpython-35.pyc,sha256=sBoEWs6zdI
868
868
  codenerix_email/management/__pycache__/__init__.cpython-39.pyc,sha256=uPXklfliVd3b8pLOJQT9ZeKcqmJMrGychvt68BsPulY,168
869
869
  codenerix_email/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
870
870
  codenerix_email/management/commands/email_test.py,sha256=SPsiq0s48sEX_G3piNHq-c1gDOH800H2zTvM19ei2LY,5605
871
- codenerix_email/management/commands/emails_recv.py,sha256=RsUfv5s0XMReQ1Y0LjfLp-LJ4F7nFX2z_T1-QAAWcYY,24897
871
+ codenerix_email/management/commands/emails_recv.py,sha256=fDFGJZqQBGAK4jZGFITzIXZCFdMvB35nwuXmJ40XMA4,34448
872
872
  codenerix_email/management/commands/emails_send.py,sha256=scCFklro4WVMYm-1ataSjUMsPT-Ie5u_DdA55CQcTCQ,7944
873
873
  codenerix_email/management/commands/recv_emails.py,sha256=aXmhdXlamiNxRpMIDSKBXUBhkOcwi5l_Pme7jSQUCME,273
874
874
  codenerix_email/management/commands/send_emails.py,sha256=a1MnpvZKAEFdXNfmI5oFUkVxy4PZ1AjaJS6GH90zeD0,273
@@ -1613,7 +1613,7 @@ codenerix_email/management/commands/.mypy_cache/3.10/zoneinfo/_tzpath.meta.json,
1613
1613
  codenerix_email/management/commands/__pycache__/__init__.cpython-310.pyc,sha256=3-VfdLuaiBIg4KTIy7GETYTt2-AbfCU0vlH6fFZx7_M,189
1614
1614
  codenerix_email/management/commands/__pycache__/__init__.cpython-311.pyc,sha256=yiqmtIhQMYXWx7g7XT-mvQNgGu_X2ymasWvVxIqPsBE,211
1615
1615
  codenerix_email/management/commands/__pycache__/email_test.cpython-311.pyc,sha256=ql-fe6qem8jGQCu-7Wm3ZCEvzOiVMsCdy0IxOyGnJbo,5946
1616
- codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc,sha256=yhLzihKfS8KIBdf_WKNYaRuCpP5Lhv57Ui5eue0_hnk,20952
1616
+ codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc,sha256=2xdfhIh76dxEnxx7wxaDsCeycJM4wRCb14C0CJax6to,30330
1617
1617
  codenerix_email/management/commands/__pycache__/emails_send.cpython-311.pyc,sha256=XSOhv92hH9G4Z9juLmXFTOLCS-oggZBA2TFyKIsxmd8,6868
1618
1618
  codenerix_email/management/commands/__pycache__/recv_emails.cpython-310.pyc,sha256=qzj8puxw6pRAz_ptltybySs2mybOwdoZ8LB7Fw8YMGc,9897
1619
1619
  codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc,sha256=YJoyWZ8Ax_ej7u9_YKjtru8BN8bKlMuvqNiTsLoWYUw,643
@@ -1686,9 +1686,9 @@ codenerix_email/migrations/__pycache__/__init__.cpython-311.pyc,sha256=RbbUUEhcJ
1686
1686
  codenerix_email/migrations/__pycache__/__init__.cpython-35.pyc,sha256=2g70xiMW6oJNkIpRM-0Dr5h7AUac-3xyCXPONxp9BBw,147
1687
1687
  codenerix_email/migrations/__pycache__/__init__.cpython-39.pyc,sha256=qNj2NH0YvoWPnCKxkVZPsEFsbM05y7t1njMskNISdVQ,168
1688
1688
  codenerix_email/static/codenerix_email/emailmessages_rows.html,sha256=NyZpKPSHAAIywJX2ncS0H2bkqOVtMUwAdbYmkC4dKmk,2202
1689
- codenerix_email/static/codenerix_email/emailreceiveds_rows.html,sha256=k7-Qe7Y9mrKRJXl69aVe5hHUvGdyrLbMoh9gkpyCG2U,1976
1690
- django_codenerix_email-4.0.37.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
1691
- django_codenerix_email-4.0.37.dist-info/METADATA,sha256=Y84bu_edEiA6UoycJZCYyUVES1Z9JrJWzOrHVUhrsPI,2676
1692
- django_codenerix_email-4.0.37.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
1693
- django_codenerix_email-4.0.37.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
1694
- django_codenerix_email-4.0.37.dist-info/RECORD,,
1689
+ codenerix_email/static/codenerix_email/emailreceiveds_rows.html,sha256=u9DXVdzKhx9WlJgcK_cT2DfGp9bGIKTBn3LDQplB4g4,2032
1690
+ django_codenerix_email-4.0.38.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
1691
+ django_codenerix_email-4.0.38.dist-info/METADATA,sha256=a7ZWYCckl3ouPLkC7uN1-LuUTN4Qxm0tlKiQHPSTRZY,2676
1692
+ django_codenerix_email-4.0.38.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
1693
+ django_codenerix_email-4.0.38.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
1694
+ django_codenerix_email-4.0.38.dist-info/RECORD,,