33import email
44import email .policy
55import logging
6+ from datetime import datetime , timedelta
67from xmlrpc import client as xmlrpclib
78
89from odoo import _ , api , fields , models
910from odoo .exceptions import UserError , ValidationError
11+ from odoo .tools .mail import decode_message_header , email_split_and_format
1012
1113from .. import match_algorithm
1214
@@ -33,57 +35,67 @@ class FetchmailServerFolder(models.Model):
3335 )
3436 path = fields .Char (
3537 required = True ,
36- help = "The path to your mail folder."
37- " Typically would be something like 'INBOX.myfolder'" ,
38+ string = "Fetch from Folder" ,
39+ help = "Specify the IMAP folder path to retrieve emails from. "
40+ "Typically, this would be something like 'INBOX.myfolder'." ,
3841 )
3942 archive_path = fields .Char (
40- help = "The path where successfully retrieved messages will be stored."
43+ string = "Archive Folder" ,
44+ help = "Specify the folder where successfully retrieved emails will be moved "
45+ "after processing. If left empty, emails will remain in the original folder." ,
4146 )
4247 model_id = fields .Many2one (
4348 comodel_name = "ir.model" ,
4449 required = True ,
4550 ondelete = "cascade" ,
51+ string = "Target Model" ,
4652 help = "The model to attach emails to" ,
4753 )
4854 model_field = fields .Char (
49- "Field (model)" ,
50- help = "The field in your model that contains the field to match against.\n "
55+ help = "Specify the field in the model that will be used for email matching.\n "
5156 "Examples:\n "
52- "'email' if your model is res.partner, or "
53- "'partner_id.email' if you're matching sale orders" ,
57+ "- 'email' if your model is res.partner\n "
58+ "- 'partner_id.email' if matching sale orders. " ,
5459 )
5560 model_order = fields .Char (
56- "Order (model)" ,
57- help = "Field(s) to order by, this mostly useful in conjunction "
58- "with 'Use 1st match'" ,
61+ help = "Specify the field(s) to order by when matching emails. "
62+ "This is mostly useful in conjunction with 'Use 1st Match'." ,
5963 )
6064 match_algorithm = fields .Selection (
6165 selection = [
62- ("odoo_standard" , "Odoo standard " ),
63- ("email_domain" , "Domain of email address " ),
64- ("email_exact" , "Exact mailadress " ),
66+ ("odoo_standard" , "Odoo Standard " ),
67+ ("email_domain" , "Domain of Email Address " ),
68+ ("email_exact" , "Exact Email Address " ),
6569 ],
6670 required = True ,
67- help = "The algorithm used to determine which object an email matches." ,
71+ string = "Matching Algorithm" ,
72+ help = "Select the algorithm used to determine which object an email matches." ,
6873 )
6974 mail_field = fields .Char (
70- "Field (email) " ,
71- help = "The field in the email used for matching."
72- " Typically this is 'to' or 'from' " ,
75+ string = "Email Field " ,
76+ help = "Specify the field in the email used for matching. "
77+ "Typically, this is 'to', 'from' or 'reply_to'. " ,
7378 )
7479 delete_matching = fields .Boolean (
75- "Delete matches" , help = "Delete matched emails from server"
80+ string = "Delete Matched Emails" ,
81+ help = "Enable this option to delete emails after they are successfully "
82+ "processed and matched." ,
7683 )
7784 flag_nonmatching = fields .Boolean (
85+ string = "Flag Non-Matching Emails" ,
7886 default = True ,
79- help = "Flag emails in the server that don't match any object in Odoo" ,
87+ help = "Enable this option to mark emails as important if they do not match "
88+ "any object in Odoo." ,
8089 )
8190 match_first = fields .Boolean (
82- "Use 1st match" ,
83- help = "If there are multiple matches, use the first one. If "
84- "not checked, multiple matches count as no match at all" ,
91+ string = "Use First Match" ,
92+ help = "If there are multiple matches, use the first one. "
93+ "If disabled, multiple matches will be considered as no match." ,
94+ )
95+ domain = fields .Char (
96+ string = "Matching Domain" ,
97+ help = "Define a search filter to narrow down objects for email matching." ,
8598 )
86- domain = fields .Char (help = "Fill in a search filter to narrow down objects to match" )
8799 msg_state = fields .Selection (
88100 selection = [("sent" , "Sent" ), ("received" , "Received" )],
89101 string = "Message state" ,
@@ -93,13 +105,19 @@ class FetchmailServerFolder(models.Model):
93105 active = fields .Boolean (default = True )
94106 action_id = fields .Many2one (
95107 comodel_name = "ir.actions.server" ,
96- name = "Server action " ,
97- help = "Optional custom server action to trigger for each incoming "
98- "mail, on the record that was created or updated by this mail " ,
108+ string = "Server Action " ,
109+ help = "Specify an optional custom server action to trigger for each incoming email. "
110+ "The action will run on the record that was created or updated by this email. " ,
99111 )
100112 fetch_unseen_only = fields .Boolean (
101- help = "By default all undeleted emails are searched. Checking this "
102- "field adds the unread condition." ,
113+ help = "By default, all undeleted emails are retrieved. "
114+ "Enable this option to fetch only unread emails." ,
115+ )
116+ fetch_last_day_only = fields .Boolean (
117+ string = "Fetch Last 24 Hours Only" ,
118+ help = "By default, all emails in the folder are searched. Enable this "
119+ "option to only fetch emails received in the last 24 hours. This helps "
120+ "avoid reprocessing emails if they are not deleted after processing." ,
103121 )
104122
105123 def button_confirm_folder (self ):
@@ -175,13 +193,17 @@ def check_imap_archive_folder(self, connection):
175193 )
176194
177195 def get_criteria (self ):
178- return "UNDELETED" if not self .fetch_unseen_only else "UNSEEN UNDELETED"
196+ criteria = "UNSEEN UNDELETED" if self .fetch_unseen_only else "UNDELETED"
197+ if self .fetch_last_day_only :
198+ yesterday = (datetime .utcnow () - timedelta (days = 1 )).strftime ("%d-%b-%Y" )
199+ criteria = f"SINCE { yesterday } " + criteria
200+ return criteria
179201
180202 def retrieve_imap_folder (self , connection ):
181203 """Retrieve all mails for one IMAP folder."""
182204 self .ensure_one ()
183205 msgids = self .get_msgids (connection , self .get_criteria ())
184- for msgid in msgids [0 ].split ():
206+ for msgid in msgids [0 ].split ()[:: - 1 ] :
185207 # We will accept exceptions for single messages
186208 try :
187209 self .env .cr .execute ("savepoint apply_matching" )
@@ -266,7 +288,7 @@ def run_server_action(self, matched_object_ids):
266288 def fetch_msg (self , connection , msgid ):
267289 """Select a single message from a folder."""
268290 self .ensure_one ()
269- result , msgdata = connection .fetch (msgid , "(RFC822 )" )
291+ result , msgdata = connection .fetch (msgid , "(BODY.PEEK[] )" )
270292 if result != "OK" :
271293 raise UserError (
272294 _ ("Could not fetch %(msgid)s in folder %(folder)s on server %(server)s" )
@@ -309,6 +331,17 @@ def _get_message_dict(self, message):
309331 message_dict = thread_model .message_parse (
310332 message , save_original = self .server_id .original
311333 )
334+ # Populate `reply_to`
335+ message_dict ["reply_to" ] = "," .join (
336+ {
337+ formatted_email
338+ for address in [
339+ decode_message_header (message , "Reply-To" , separator = "," ),
340+ ]
341+ if address
342+ for formatted_email in email_split_and_format (address )
343+ }
344+ )
312345 return message_dict
313346
314347 def _check_message_already_present (self , message_dict ):
@@ -331,10 +364,14 @@ def _find_match(self, message_dict):
331364 matches = matcher .search_matches (self , message_dict )
332365 if not matches :
333366 _logger .info (
334- "No match found for message %(subject)s with msgid %(msgid)s" ,
367+ "No match found for message %(subject)s with msgid %(msgid)s - "
368+ "To: %(to)s - From: %(from)s - Reply-To: %(reply_to)s" ,
335369 {
336370 "subject" : message_dict .get ("subject" , "no subject" ),
337371 "msgid" : message_dict .get ("message_id" , "no msgid" ),
372+ "to" : message_dict .get ("to" , "" ),
373+ "from" : message_dict .get ("from" , "" ),
374+ "reply_to" : message_dict .get ("reply_to" , "" ),
338375 },
339376 )
340377 return None
0 commit comments