Skip to content

Commit bd2ad4a

Browse files
committed
Update fetchmail_server_folder
- Update fields description for clarity - Support fetch_last_day_only to fetch only 24 hours of emails - Process emails in reverse order - Use BODY.PEEK to avoid marking email as seen - Support email field 'reply_to' to matching - Improve logging - Relayout view to group related fields
1 parent 2817d0f commit bd2ad4a

File tree

3 files changed

+126
-72
lines changed

3 files changed

+126
-72
lines changed

fetchmail_attach_from_folder/models/fetchmail_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ def parse_list_response(line):
5656
context={"active_test": False},
5757
)
5858
folders_only = fields.Boolean(
59-
string="Only folders, not inbox",
60-
help="Check this field to leave imap inbox alone"
61-
" and only retrieve mail from configured folders.",
59+
string="Process Only Specified Folders",
60+
help="Enable this option to ignore the default IMAP inbox processing and "
61+
"retrieve emails only from the specified folders.",
6262
)
6363
# Below existing fields, that are modified by this module.
6464
object_id = fields.Many2one(required=False) # comodel_name='ir.model'

fetchmail_attach_from_folder/models/fetchmail_server_folder.py

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import email
44
import email.policy
55
import logging
6+
from datetime import datetime, timedelta
67
from xmlrpc import client as xmlrpclib
78

89
from odoo import _, api, fields, models
910
from odoo.exceptions import UserError, ValidationError
11+
from odoo.tools.mail import decode_message_header, email_split_and_format
1012

1113
from .. 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

fetchmail_attach_from_folder/views/fetchmail_server.xml

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -61,50 +61,67 @@
6161
/>
6262
</header>
6363
<group colspan="4" col="2">
64+
<!-- Fetch Options -->
6465
<group>
66+
<separator string="Fetch Options" />
67+
<field name="active" />
6568
<field name="path" placeholder="INBOX.subfolder1" />
66-
<field name="model_id" />
67-
<field name="action_id" />
68-
<field name="match_algorithm" />
69+
<field name="fetch_unseen_only" />
70+
<field name="fetch_last_day_only" />
6971
</group>
70-
<group
71-
name="group_email_match"
72-
attrs="{'invisible':
73-
[('match_algorithm','=','odoo_standard')]}"
74-
>
75-
<field
76-
name="model_field"
77-
placeholder="email"
78-
attrs="{'required':
79-
[('match_algorithm','in',['email_exact','email_domain'])]}"
80-
/>
81-
<field
82-
name="mail_field"
83-
placeholder="to,from"
84-
attrs="{'required':
85-
[('match_algorithm','in',['email_exact','email_domain'])]}"
86-
/>
87-
<field name="match_first" />
88-
<field
89-
name="domain"
90-
placeholder="[('state', '=', 'open')]"
91-
/>
92-
<field
93-
name="model_order"
94-
placeholder="name asc"
95-
attrs="{'readonly':
96-
[('match_first','==',False)],
97-
'required':
98-
[('match_first','==',True)]}"
99-
/>
100-
<field name="flag_nonmatching" />
72+
73+
<!-- Matching Options -->
74+
<group>
75+
<separator string="Matching Options" />
76+
<group colspan="2">
77+
<field
78+
name="model_id"
79+
widget="many2one"
80+
options="{'no_create': True}"
81+
nolabel="0"
82+
/>
83+
<field name="match_algorithm" />
84+
</group>
85+
<group
86+
attrs="{'invisible': [('match_algorithm','=','odoo_standard')]}"
87+
colspan="2"
88+
>
89+
<field
90+
name="model_field"
91+
placeholder="email"
92+
attrs="{'required': [('match_algorithm','in',['email_exact','email_domain'])]}"
93+
/>
94+
<field
95+
name="mail_field"
96+
placeholder="to,from"
97+
attrs="{'required': [('match_algorithm','in',['email_exact','email_domain'])]}"
98+
/>
99+
<field
100+
name="domain"
101+
placeholder="[('state', '=', 'open')]"
102+
/>
103+
<field name="match_first" />
104+
<field
105+
name="model_order"
106+
placeholder="name asc"
107+
attrs="{'readonly': [('match_first','==',False)], 'required': [('match_first','==',True)]}"
108+
/>
109+
</group>
101110
</group>
111+
112+
<!-- Post-Processing Actions -->
102113
<group>
103-
<field name="active" />
114+
<separator string="Model Post-Processing Actions" />
115+
<field name="action_id" />
116+
<field name="msg_state" />
117+
</group>
118+
119+
<!-- Post-Processing Actions -->
120+
<group>
121+
<separator string="Email Post-Processing Actions" />
104122
<field name="archive_path" />
105123
<field name="delete_matching" />
106-
<field name="fetch_unseen_only" />
107-
<field name="msg_state" />
124+
<field name="flag_nonmatching" />
108125
</group>
109126
</group>
110127
</form>

0 commit comments

Comments
 (0)