diff --git a/fetchmail_attach_from_folder/models/fetchmail_server.py b/fetchmail_attach_from_folder/models/fetchmail_server.py index a42f8c35644..bb138aebbce 100644 --- a/fetchmail_attach_from_folder/models/fetchmail_server.py +++ b/fetchmail_attach_from_folder/models/fetchmail_server.py @@ -57,9 +57,9 @@ def parse_list_response(line): context={"active_test": False}, ) folders_only = fields.Boolean( - string="Only folders, not inbox", - help="Check this field to leave imap inbox alone" - " and only retrieve mail from configured folders.", + string="Process Only Specified Folders", + help="Enable this option to ignore the default IMAP inbox processing and " + "retrieve emails only from the specified folders.", ) # Below existing fields, that are modified by this module. object_id = fields.Many2one(required=False) # comodel_name='ir.model' @@ -71,10 +71,10 @@ def onchange_server_type(self): self.state = "draft" return result - def fetch_mail(self): + def fetch_mail(self, raise_exception=True): result = True for this in self: if not this.folders_only: - result = result and super(FetchmailServer, this).fetch_mail() + result = result and super(FetchmailServer, this).fetch_mail(raise_exception) this.folder_ids.fetch_mail() return result diff --git a/fetchmail_attach_from_folder/models/fetchmail_server_folder.py b/fetchmail_attach_from_folder/models/fetchmail_server_folder.py index eb173d757f3..4c026d90792 100644 --- a/fetchmail_attach_from_folder/models/fetchmail_server_folder.py +++ b/fetchmail_attach_from_folder/models/fetchmail_server_folder.py @@ -7,6 +7,7 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError +from odoo.tools.mail import decode_message_header, email_split_and_format from .. import match_algorithm @@ -33,57 +34,67 @@ class FetchmailServerFolder(models.Model): ) path = fields.Char( required=True, - help="The path to your mail folder." - " Typically would be something like 'INBOX.myfolder'", + string="Fetch from Folder", + help="Specify the IMAP folder path to retrieve emails from. " + "Typically, this would be something like 'INBOX.myfolder'.", ) archive_path = fields.Char( - help="The path where successfully retrieved messages will be stored." + string="Archive Folder", + help="Specify the folder where successfully retrieved emails will be moved " + "after processing. If left empty, emails will remain in the original folder.", ) model_id = fields.Many2one( comodel_name="ir.model", required=True, ondelete="cascade", + string="Target Model", help="The model to attach emails to", ) model_field = fields.Char( - "Field (model)", - help="The field in your model that contains the field to match against.\n" + help="Specify the field in the model that will be used for email matching.\n" "Examples:\n" - "'email' if your model is res.partner, or " - "'partner_id.email' if you're matching sale orders", + "- 'email' if your model is res.partner\n" + "- 'partner_id.email' if matching sale orders.", ) model_order = fields.Char( - "Order (model)", - help="Field(s) to order by, this mostly useful in conjunction " - "with 'Use 1st match'", + help="Specify the field(s) to order by when matching emails. " + "This is mostly useful in conjunction with 'Use 1st Match'.", ) match_algorithm = fields.Selection( selection=[ - ("odoo_standard", "Odoo standard"), - ("email_domain", "Domain of email address"), - ("email_exact", "Exact mailadress"), + ("odoo_standard", "Odoo Standard"), + ("email_domain", "Domain of Email Address"), + ("email_exact", "Exact Email Address"), ], required=True, - help="The algorithm used to determine which object an email matches.", + string="Matching Algorithm", + help="Select the algorithm used to determine which object an email matches.", ) mail_field = fields.Char( - "Field (email)", - help="The field in the email used for matching." - " Typically this is 'to' or 'from'", + string="Email Field", + help="Specify the field in the email used for matching. " + "Typically, this is 'to', 'from' or 'reply_to'.", ) delete_matching = fields.Boolean( - "Delete matches", help="Delete matched emails from server" + string="Delete Matched Emails", + help="Enable this option to delete emails after they are successfully " + "processed and matched.", ) flag_nonmatching = fields.Boolean( + string="Flag Non-Matching Emails", default=True, - help="Flag emails in the server that don't match any object in Odoo", + help="Enable this option to mark emails as important if they do not match " + "any object in Odoo.", ) match_first = fields.Boolean( - "Use 1st match", - help="If there are multiple matches, use the first one. If " - "not checked, multiple matches count as no match at all", + string="Use First Match", + help="If there are multiple matches, use the first one. " + "If disabled, multiple matches will be considered as no match.", + ) + domain = fields.Char( + string="Matching Domain", + help="Define a search filter to narrow down objects for email matching.", ) - domain = fields.Char(help="Fill in a search filter to narrow down objects to match") msg_state = fields.Selection( selection=[("sent", "Sent"), ("received", "Received")], string="Message state", @@ -93,13 +104,30 @@ class FetchmailServerFolder(models.Model): active = fields.Boolean(default=True) action_id = fields.Many2one( comodel_name="ir.actions.server", - name="Server action", - help="Optional custom server action to trigger for each incoming " - "mail, on the record that was created or updated by this mail", + string="Server Action", + help="Specify an optional custom server action to trigger for each incoming email. " + "The action will run on the record that was created or updated by this email.", ) fetch_unseen_only = fields.Boolean( - help="By default all undeleted emails are searched. Checking this " - "field adds the unread condition.", + help="By default, all undeleted emails are retrieved. " + "Enable this option to fetch only unread emails.", + ) + fetch_last_day_only = fields.Boolean( + string="Fetch Last 24 Hours Only", + help="By default, all emails in the folder are searched. Enable this " + "option to only fetch emails received in the last 24 hours. This helps " + "avoid reprocessing emails if they are not deleted after processing.", + ) + seen = fields.Selection( + selection=[ + ("on_fetch", "Immediately on Fetch"), + ("all", "After Processing"), + ("matching", "When matching"), + ("none", "Never"), + ], + string="Mark email as seen", + required=True, + default="on_fetch", ) def button_confirm_folder(self): @@ -175,7 +203,13 @@ def check_imap_archive_folder(self, connection): ) def get_criteria(self): - return "UNDELETED" if not self.fetch_unseen_only else "UNSEEN UNDELETED" + criteria = "UNSEEN UNDELETED" if self.fetch_unseen_only else "UNDELETED" + if self.fetch_last_day_only: + yesterday = fields.Date.subtract( + fields.Date.context_today(self), days=1 + ).strftime("%d-%b-%Y") + criteria = f"SINCE {yesterday} {criteria}" + return criteria def retrieve_imap_folder(self, connection): """Retrieve all mails for one IMAP folder.""" @@ -271,7 +305,8 @@ def run_server_action(self, matched_object_ids): def fetch_msg(self, connection, message_uid): """Select a single message from a folder.""" self.ensure_one() - result, msgdata = connection.uid("fetch", message_uid, "(RFC822)") + command = "(RFC822)" if self.seen == "on_fetch" else "(BODY.PEEK[])" + result, msgdata = connection.uid("fetch", message_uid, command) if result != "OK": raise UserError( _( @@ -287,6 +322,8 @@ def fetch_msg(self, connection, message_uid): def update_msg(self, connection, message_uid, matched=True, flagged=False): """Update msg in imap folder depending on match and settings.""" + if self.seen == "all" or (matched and self.seen == "matching"): + connection.uid("store", message_uid, "+FLAGS", "\\Seen") if matched: if self.delete_matching: connection.uid("store", message_uid, "+FLAGS", "\\DELETED") @@ -318,6 +355,17 @@ def _get_message_dict(self, message): message_dict = thread_model.message_parse( message, save_original=self.server_id.original ) + # Populate `reply_to` + message_dict["reply_to"] = ",".join( + { + formatted_email + for address in [ + decode_message_header(message, "Reply-To", separator=","), + ] + if address + for formatted_email in email_split_and_format(address) + } + ) return message_dict def _check_message_already_present(self, message_dict): @@ -340,11 +388,14 @@ def _find_match(self, message_dict): matches = matcher.search_matches(self, message_dict) if not matches: _logger.info( - "No match found for message %(subject)s" - " with message_uid %(message_uid)s", + "No match found for message %(subject)s with message_uid %(message_uid)s - " + "To: %(to)s - From: %(from)s - Reply-To: %(reply_to)s", { "subject": message_dict.get("subject", "no subject"), "message_uid": message_dict.get("message_id", "no message_uid"), + "to": message_dict.get("to", ""), + "from": message_dict.get("from", ""), + "reply_to": message_dict.get("reply_to", ""), }, ) return None diff --git a/fetchmail_attach_from_folder/views/fetchmail_server.xml b/fetchmail_attach_from_folder/views/fetchmail_server.xml index f3291768216..819f3113e11 100644 --- a/fetchmail_attach_from_folder/views/fetchmail_server.xml +++ b/fetchmail_attach_from_folder/views/fetchmail_server.xml @@ -60,45 +60,69 @@ /> + + + - - - + + - - - - - - - + + + + + + + + + + + + + + + + + - + + + + + + + + + - - +