Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions fetchmail_attach_from_folder/models/fetchmail_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,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'
Expand Down
114 changes: 83 additions & 31 deletions fetchmail_attach_from_folder/models/fetchmail_server_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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",
Expand All @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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(
_(
Expand All @@ -288,6 +323,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")
Expand Down Expand Up @@ -319,6 +356,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):
Expand All @@ -341,10 +389,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
Expand Down
93 changes: 56 additions & 37 deletions fetchmail_attach_from_folder/views/fetchmail_server.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,50 +61,69 @@
/>
</header>
<group colspan="4" col="2">
<!-- Fetch Options -->
<group>
<separator string="Fetch Options" />
<field name="active" />
<field name="path" placeholder="INBOX.subfolder1" />
<field name="model_id" />
<field name="action_id" />
<field name="match_algorithm" />
<field name="fetch_unseen_only" />
<field name="fetch_last_day_only" />
</group>
<group
name="group_email_match"
attrs="{'invisible':
[('match_algorithm','=','odoo_standard')]}"
>
<field
name="model_field"
placeholder="email"
attrs="{'required':
[('match_algorithm','in',['email_exact','email_domain'])]}"
/>
<field
name="mail_field"
placeholder="to,from"
attrs="{'required':
[('match_algorithm','in',['email_exact','email_domain'])]}"
/>
<field name="match_first" />
<field
name="domain"
placeholder="[('state', '=', 'open')]"
/>
<field
name="model_order"
placeholder="name asc"
attrs="{'readonly':
[('match_first','==',False)],
'required':
[('match_first','==',True)]}"
/>
<field name="flag_nonmatching" />

<!-- Matching Options -->
<group>
<separator string="Matching Options" />
<group colspan="2">
<field
name="model_id"
widget="many2one"
options="{'no_create': True}"
nolabel="0"
/>
<field name="match_algorithm" />
</group>
<group
attrs="{'invisible': [('match_algorithm','=','odoo_standard')]}"
colspan="2"
>
<field
name="model_field"
placeholder="email"
attrs="{'required': [('match_algorithm','in',['email_exact','email_domain'])]}"
/>
<field
name="mail_field"
placeholder="to,from"
attrs="{'required': [('match_algorithm','in',['email_exact','email_domain'])]}"
/>
<field
name="domain"
placeholder="[('state', '=', 'open')]"
/>
<field name="match_first" />
<field
name="model_order"
placeholder="name asc"
attrs="{'readonly': [('match_first','==',False)],
'required': [('match_first','==',True)]}"
/>
</group>
</group>

<!-- Post-Processing Actions -->
<group>
<field name="active" />
<separator string="Model Post-Processing Actions" />
<field name="action_id" />
<field name="msg_state" />
</group>

<!-- Post-Processing Actions -->
<group>
<separator string="Email Post-Processing Actions" />
<field name="seen" />
<field name="archive_path" />
<field name="delete_matching" />
<field name="fetch_unseen_only" />
<field name="msg_state" />
<field name="flag_nonmatching" />
</group>
</group>
</form>
Expand Down